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]