Overall Statistics |
Total Orders 1409 Average Win 0.55% Average Loss -0.66% Compounding Annual Return 18.670% Drawdown 33.900% Expectancy 0.408 Start Equity 100000 End Equity 635643.45 Net Profit 535.643% Sharpe Ratio 0.728 Sortino Ratio 0.77 Probabilistic Sharpe Ratio 21.079% Loss Rate 24% Win Rate 76% Profit-Loss Ratio 0.84 Alpha 0.042 Beta 0.974 Annual Standard Deviation 0.167 Annual Variance 0.028 Information Ratio 0.442 Tracking Error 0.091 Treynor Ratio 0.124 Total Fees $1956.30 Estimated Strategy Capacity $67000000.00 Lowest Capacity Asset DIS R735QTJ8XC9X Portfolio Turnover 2.60% |
# region imports from AlgorithmImports import * # endregion # Andreas Clenow Momentum (Static Assets), Framework from datetime import timedelta from collections import deque from scipy import stats import numpy as np # ============================== # STRATEGY INPUTS # ============================== # All relevant strategy variables are declared here for easy customization # List of tradable tickers (symbols) to be used in the strategy TRADABLE_SYMBOLS = ['GS', 'JPM', 'HD', 'COST', 'DIS'] # Period length for the custom momentum indicator (e.g., 50-day lookback) MOMENTUM_PERIOD = 50 # Number of top securities to be selected based on momentum (e.g., top 3) TOP_N_ASSETS = 3 # Starting capital for the strategy STARTING_CAPITAL = 100000 # Start and (optional) end date for the backtest START_DATE = (2014, 1, 1) # END_DATE = (2024, 10, 1) # Uncomment to specify an end date # ============================== # END OF STRATEGY INPUTS # ============================== class ClenowMomentum(AlphaModel): def __init__(self): self.PERIOD = MOMENTUM_PERIOD # Period for calculating momentum (from strategy inputs) self.N = TOP_N_ASSETS # Number of top assets to generate insights for (from strategy inputs) self.indi = {} # Dictionary to store indicators self.indi_Update = {} # Updated indicators self.securities = [] # List of securities def OnSecuritiesChanged(self, algorithm, changes): # Handle when the universe of securities changes for security in changes.AddedSecurities: if security.Symbol.Value == 'SPY': continue # Skip SPY as it's used for benchmarking, not trading self.securities.append(security) symbol = security.Symbol # Create and register custom momentum indicator for each security self.indi[symbol] = My_Custom('My_Custom', symbol, self.PERIOD) algorithm.RegisterIndicator(symbol, self.indi[symbol], Resolution.Daily) # Warm up the indicator with historical data history = algorithm.History(symbol, self.PERIOD, Resolution.Daily) self.indi[symbol].Warmup(history) def Update(self, algorithm, data): # Generate trading insights based on updated momentum indicator values insights = [] # Check which indicators are ready for generating signals ready = [indicator for symbol, indicator in self.indi.items() if indicator.IsReady] # Sort the indicators by momentum value and select top N assets ordered = sorted(ready, key=lambda x: x.Value, reverse=False)[:self.N] # Generate an insight for each of the top N securities for x in ordered: insights.append(Insight.Price(x.symbol, timedelta(1), InsightDirection.Up)) # Plot momentum indicator values for all tradable symbols for idx, symbol in enumerate(self.indi.keys()): algorithm.Plot('Custom_Slope', f'Value {symbol.Value}', list(self.indi.values())[idx].Value) return insights class FrameworkAlgorithm(QCAlgorithm): def Initialize(self): # Set the start date and initial capital (from strategy inputs) self.SetStartDate(*START_DATE) self.SetCash(STARTING_CAPITAL) # Create the tradable symbols based on the defined variable symbols = [Symbol.Create(t, SecurityType.Equity, Market.USA) for t in TRADABLE_SYMBOLS] # Use manual universe selection based on the symbols self.SetUniverseSelection(ManualUniverseSelectionModel(symbols)) self.UniverseSettings.Resolution = Resolution.Daily # Add the custom Alpha model self.AddAlpha(ClenowMomentum()) # Set the portfolio construction and execution models self.Settings.RebalancePortfolioOnInsightChanges = False self.Settings.RebalancePortfolioOnSecurityChanges = True self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel(self.DateRules.Every(DayOfWeek.Monday))) self.SetExecution(ImmediateExecutionModel()) # Add SPY as the market benchmark for tracking performance self.MKT = self.AddEquity('SPY', Resolution.Daily).Symbol self.mkt = [] # Set up a daily trade bar consolidator for SPY self.consolidator = TradeBarConsolidator(timedelta(days=1)) self.consolidator.DataConsolidated += self.consolidation_handler self.SubscriptionManager.AddConsolidator(self.MKT, self.consolidator) # Fetch historical data for SPY self.history = self.History(self.MKT, 2, Resolution.Daily) self.history = self.history['close'].unstack(level=0).dropna() def consolidation_handler(self, sender, consolidated): # Update historical SPY data self.history.loc[consolidated.EndTime, consolidated.Symbol] = consolidated.Close self.history = self.history.iloc[-2:] def OnEndOfDay(self): # Track and plot SPY performance mkt_price = self.history[[self.MKT]].iloc[-1] self.mkt.append(mkt_price) mkt_perf = self.mkt[-1] / self.mkt[0] * STARTING_CAPITAL # Use starting capital from inputs self.Plot('Strategy Equity', 'SPY', mkt_perf) # Plot portfolio leverage account_leverage = self.Portfolio.TotalHoldingsValue / self.Portfolio.TotalPortfolioValue self.Plot('Holdings', 'leverage', round(account_leverage, 2)) class My_Custom: def __init__(self, name, symbol, period): # Custom momentum indicator initialization self.symbol = symbol self.Name = name self.Time = datetime.min self.Value = 0 self.Slope = 0 self.Corr = 0 self.queue = deque(maxlen=period) # Queue to store rolling prices self.IsReady = False # Flag to check readiness of the indicator def Update(self, input): # Update the indicator with the latest price return self.Update2(input.Time, input.Close) def Update2(self, time, value): # Append the new price and calculate the indicator if enough data is available self.queue.appendleft(value) count = len(self.queue) self.Time = time self.IsReady = count == self.queue.maxlen # Perform linear regression to calculate momentum if the indicator is ready if self.IsReady: y = np.log(self.queue) # Log-transformed price data x = [range(len(y))] reg = stats.linregress(x, y) slope, corr = reg[0], reg[2] self.Slope = slope self.Corr = corr self.annualized_slope = float(np.power(np.exp(self.Slope), 252) - 1) * 2.00 self.Value = (self.annualized_slope) * float(corr**2) return self.IsReady def Warmup(self,history): # Warm up the indicator with historical price data for index, row in history.loc[self.symbol].iterrows(): self.Update2(index, row['close'])