| 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'])