| Overall Statistics |
|
Total Orders 224 Average Win 2.52% Average Loss -2.00% Compounding Annual Return 25.816% Drawdown 24.000% Expectancy 0.508 Start Equity 100000 End Equity 347048.95 Net Profit 247.049% Sharpe Ratio 0.89 Sortino Ratio 0.96 Probabilistic Sharpe Ratio 42.127% Loss Rate 33% Win Rate 67% Profit-Loss Ratio 1.26 Alpha 0.122 Beta 0.457 Annual Standard Deviation 0.19 Annual Variance 0.036 Information Ratio 0.332 Tracking Error 0.196 Treynor Ratio 0.37 Total Fees $823.61 Estimated Strategy Capacity $0 Lowest Capacity Asset XON R735QTJ8XC9X Portfolio Turnover 2.85% Drawdown Recovery 489 |
# region imports
from AlgorithmImports import *
import numpy as np
# endregion
class AlphaTrendComposite(QCAlgorithm):
def initialize(self):
# 1. Setup: 5 Year backtest to capture 2020 (Crash) and 2022 (Inflation)
self.set_start_date(2019, 1, 1)
self.set_end_date(2024, 6, 1)
self.set_cash(100000)
self.settings.rebalance_portfolio_on_insight_changes = False
self.settings.rebalance_portfolio_on_security_changes = False
# 2. Universe: The Dow 30 (Blue Chips) + Big Tech
# We manually list them to ensure no data errors with delisted stocks
self.tickers = [
'AAPL', 'AXP', 'BA', 'CAT', 'CSCO', 'CVX', 'DIS', 'GS', 'HD', 'IBM',
'INTC', 'JPM', 'KO', 'MCD', 'MMM', 'MRK', 'MSFT', 'NKE', 'PG', 'TRV',
'UNH', 'V', 'VZ', 'WMT', 'XOM', 'AMZN', 'GOOG', 'META', 'NVDA', 'AMD'
]
self.symbols = [self.add_equity(t, Resolution.DAILY).symbol for t in self.tickers]
# 3. The Benchmark (SPY) for Alpha calculation and Trend Filtering
self.spy = self.add_equity('SPY', Resolution.DAILY).symbol
# 4. The Trend Filter (200 Day Moving Average)
self.spy_ma = self.sma(self.spy, 200, Resolution.DAILY)
# 5. Parameters
self.lookback = 62 # ~3 months for regression stability
self.top_n = 3 # Concentrate on the top 3 winners
# 6. Schedule: Rebalance at the start of every month
self.schedule.on(self.date_rules.month_start(self.spy),
self.time_rules.after_market_open(self.spy, 30),
self.rebalance)
self.set_warm_up(200)
def rebalance(self):
if self.is_warming_up: return
# --- SAFETY CHECK ---
# If SPY is below its 200-day average, the market is in a downtrend.
# We go to CASH. This saves you from -50% drawdowns.
if self.securities[self.spy].price < self.spy_ma.current.value:
self.liquidate()
self.plot("State", "Cash", 1)
return
# If we are here, the market is safe. Let's find the best stocks.
self.plot("State", "Invested", 1)
# --- ALPHA CALCULATION ---
history = self.history(self.symbols + [self.spy], self.lookback, Resolution.DAILY).close.unstack(level=0)
if history.empty: return
alphas = {}
# Get Benchmark (SPY) returns
if self.spy not in history: return
spy_returns = history[self.spy].pct_change().dropna()
for symbol in self.symbols:
if symbol not in history: continue
# Get Stock returns
stock_returns = history[symbol].pct_change().dropna()
# Align data lengths
min_len = min(len(spy_returns), len(stock_returns))
if min_len < 30: continue # Not enough data
y = stock_returns.tail(min_len).values
x = spy_returns.tail(min_len).values
# Linear Regression: Stock = Alpha + Beta * SPY
# We stack [SPY, 1] to get the intercept (Alpha)
A = np.vstack([x, np.ones(len(x))]).T
try:
# result[0] is [slope, intercept]. Intercept is Alpha.
result = np.linalg.lstsq(A, y, rcond=None)
alpha = result[0][1] # The intercept
alphas[symbol] = alpha
except:
continue
# --- SELECTION & EXECUTION ---
# Sort by Alpha (Highest first)
sorted_by_alpha = sorted(alphas.items(), key=lambda x: x[1], reverse=True)
# Select Top N
selected_symbols = [x[0] for x in sorted_by_alpha[:self.top_n]]
if not selected_symbols: return
# Allocate 100% equity divided equally among the winners
# We liquidate others first to free up cash
self.set_holdings([PortfolioTarget(s, 1.0/len(selected_symbols)) for s in selected_symbols], True)