| Overall Statistics |
|
Total Trades 6436 Average Win 0.24% Average Loss -0.33% Compounding Annual Return 0.272% Drawdown 65.400% Expectancy -0.003 Net Profit 1.206% Sharpe Ratio 0.221 Probabilistic Sharpe Ratio 2.317% Loss Rate 42% Win Rate 58% Profit-Loss Ratio 0.73 Alpha 0 Beta 0 Annual Standard Deviation 0.413 Annual Variance 0.17 Information Ratio 0.221 Tracking Error 0.413 Treynor Ratio 0 Total Fees $9554.99 Estimated Strategy Capacity $730000.00 Lowest Capacity Asset ICVX XQIXVMC7JPT1 Portfolio Turnover 6.28% |
#region imports
from AlgorithmImports import *
#endregion
class MomentumQuantilesAlphaModel(AlphaModel):
symbol_data_by_symbol = {}
day = -1
def __init__(self, quantiles, lookback_months):
self.quantiles = quantiles
self.lookback_months = lookback_months
def Update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
# Reset indicators when corporate actions occur
for symbol in set(data.Splits.keys() + data.Dividends.keys()):
if symbol in self.symbol_data_by_symbol:
self.symbol_data_by_symbol[symbol].reset()
# Only emit insights when there is quote data, not when a corporate action occurs (at midnight)
if data.QuoteBars.Count == 0:
return []
# Only emit insights once per day
if self.day == algorithm.Time.day:
return []
self.day = algorithm.Time.day
# Get the momentum of each asset in the universe
momentum_by_symbol = {}
for symbol, symbol_data in self.symbol_data_by_symbol.items():
if symbol in data.QuoteBars and symbol_data.IsReady:
momentum_by_symbol[symbol] = symbol_data.indicator.Current.Value
# Determine how many assets to hold in the portfolio
quantile_size = int(len(momentum_by_symbol)/self.quantiles)
if quantile_size == 0:
return []
# Create insights to long the assets in the universe with the greatest momentum
weight = 1 / quantile_size
expiry = list(self.symbol_data_by_symbol.values())[0].hours.GetNextMarketOpen(algorithm.Time, False) - timedelta(seconds=1)
insights = []
for symbol, _ in sorted(momentum_by_symbol.items(), key=lambda x: x[1], reverse=True)[:quantile_size]:
insights.append(Insight.Price(symbol, expiry, InsightDirection.Up, weight=weight))
return insights
def OnSecuritiesChanged(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
# Create SymbolData objects for each stock in the universe
added_symbols = []
for security in changes.AddedSecurities:
symbol = security.Symbol
self.symbol_data_by_symbol[symbol] = SymbolData(algorithm, security, self.lookback_months)
added_symbols.append(symbol)
# Warm up the indicators of newly-added stocks
if added_symbols:
history = algorithm.History[TradeBar](added_symbols, (self.lookback_months+1) * 30, Resolution.Daily, dataNormalizationMode=DataNormalizationMode.ScaledRaw)
for trade_bars in history:
for bar in trade_bars.Values:
self.symbol_data_by_symbol[bar.Symbol].update(bar)
# Remove the SymbolData object when the stock is removed from the universe
for security in changes.RemovedSecurities:
symbol = security.Symbol
if symbol in self.symbol_data_by_symbol:
symbol_data = self.symbol_data_by_symbol.pop(symbol, None)
if symbol_data:
symbol_data.dispose()
class SymbolData:
def __init__(self, algorithm, security, lookback_months):
self.algorithm = algorithm
self.symbol = security.Symbol
self.hours = security.Exchange.Hours
# Create an indicator that automatically updates each month
self.indicator = MomentumPercent(lookback_months)
self.register_indicator()
@property
def IsReady(self):
return self.indicator.IsReady
def register_indicator(self):
# Update the indicator with monthly bars
self.consolidator = TradeBarConsolidator(Calendar.Monthly)
self.algorithm.SubscriptionManager.AddConsolidator(self.symbol, self.consolidator)
self.algorithm.RegisterIndicator(self.symbol, self.indicator, self.consolidator)
def update(self, bar):
self.consolidator.Update(bar)
def reset(self):
self.indicator.Reset()
self.dispose()
self.register_indicator()
history = self.algorithm.History[TradeBar](self.symbol, (self.indicator.WarmUpPeriod+1) * 30, Resolution.Daily, dataNormalizationMode=DataNormalizationMode.ScaledRaw)
for bar in history:
self.consolidator.Update(bar)
def dispose(self):
# Stop updating consolidator when the security is removed from the universe
self.algorithm.SubscriptionManager.RemoveConsolidator(self.symbol, self.consolidator)
# region imports
from AlgorithmImports import *
from universe import SPYAndQQQConstituentsUniverseSelectionModel
from alpha import MomentumQuantilesAlphaModel
# endregion
class TacticalMomentumRankAlgorithm(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2019, 1, 1)
self.SetEndDate(2023, 6, 1)
self.SetCash(100000)
self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)
self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw
self.AddUniverseSelection(SPYAndQQQConstituentsUniverseSelectionModel(self.UniverseSettings))
self.AddAlpha(MomentumQuantilesAlphaModel(
int(self.GetParameter("quantiles")),
int(self.GetParameter("lookback_months"))
))
self.Settings.RebalancePortfolioOnSecurityChanges = False
self.Settings.RebalancePortfolioOnInsightChanges = False
self.day = -1
self.SetPortfolioConstruction(InsightWeightingPortfolioConstructionModel(self.rebalance_func))
self.AddRiskManagement(NullRiskManagementModel())
self.SetExecution(ImmediateExecutionModel())
self.SetWarmUp(timedelta(7))
def rebalance_func(self, time):
if self.day != self.Time.day and not self.IsWarmingUp and self.CurrentSlice.QuoteBars.Count > 0:
self.day = self.Time.day
return time
return None
def OnWarmupFinished(self):
# Exit positions that aren't backed by existing insights.
# If you don't want this behavior, delete this method definition.
for security_holding in self.Portfolio.Values:
if not security_holding.Invested:
continue
symbol = security_holding.Symbol
if not self.Insights.HasActiveInsights(symbol, self.UtcTime):
self.Insights.Add(Insight.Price(symbol, timedelta(seconds=1), InsightDirection.Flat, weight=1))
#region imports from AlgorithmImports import * #endregion # 05/19/2023: -Added a warm-up period to restore the algorithm state between deployments. # -Added OnWarmupFinished to liquidate existing holdings that aren't backed by active insights. # -Removed flat insights because https://github.com/QuantConnect/Lean/pull/7251 made them unnecessary. # https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_a34c371a3b4818e5157cd76b876ecae0.html
# region imports
from AlgorithmImports import *
#endregion
class SPYAndQQQConstituentsUniverseSelectionModel(UniverseSelectionModel):
def __init__(self, universe_settings: UniverseSettings = None) -> None:
self.spy_symbol = Symbol.Create("SPY", SecurityType.Equity, Market.USA)
self.qqq_symbol = Symbol.Create("QQQ", SecurityType.Equity, Market.USA)
self.iwm_symbol = Symbol.Create("IWM", SecurityType.Equity, Market.USA)
self.universe_settings = universe_settings or UniverseSettings()
def CreateUniverses(self, algorithm: QCAlgorithm) -> List[Universe]:
spy_universe = ETFConstituentsUniverse(self.spy_symbol, self.universe_settings, lambda constituents: [c.Symbol for c in constituents])
qqq_universe = ETFConstituentsUniverse(self.qqq_symbol, self.universe_settings, lambda constituents: [c.Symbol for c in constituents])
iwm_universe = ETFConstituentsUniverse(self.iwm_symbol, self.universe_settings, lambda constituents: [c.Symbol for c in constituents])
return [spy_universe, qqq_universe, iwm_universe]