| Overall Statistics |
|
Total Orders 2108 Average Win 0.25% Average Loss -0.24% Compounding Annual Return 42.367% Drawdown 20.600% Expectancy 0.286 Start Equity 100000 End Equity 202815.42 Net Profit 102.815% Sharpe Ratio 1.194 Sortino Ratio 1.358 Probabilistic Sharpe Ratio 67.300% Loss Rate 36% Win Rate 64% Profit-Loss Ratio 1.02 Alpha 0.074 Beta 1.446 Annual Standard Deviation 0.211 Annual Variance 0.044 Information Ratio 0.848 Tracking Error 0.152 Treynor Ratio 0.174 Total Fees $2331.60 Estimated Strategy Capacity $0 Lowest Capacity Asset ADBE R735QTJ8XC9X Portfolio Turnover 16.11% Drawdown Recovery 129 |
from AlgorithmImports import *
import numpy as np
class EnhancedMomentumAlgorithm(QCAlgorithm):
def Initialize(self):
self.SetCash(100000)
self.SetStartDate(2023, 1, 1)
self.SetEndDate(2024, 12, 31)
# Enhanced parameters
self.portfolio_size = 15 # More concentrated for better alpha
self.rebalance_weeks = 1 # Bi-weekly for faster trend capture
# Risk management
self.max_position_size = 0.10 # 10% max per stock
self.momentum_threshold = 0.10 # Only buy stocks with >5% momentum
self.volatility_lookback = 20
# Enhanced universe - focus on growth and momentum sectors
self.stock_symbols = [
# Tech leaders
"AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA", "TSLA", "NFLX", "AMD", "ADBE",
# Growth stocks
"CRM", "SHOP", "SQ", "PYPL", "ROKU", "ZOOM", "SNOW", "PLTR", "RBLX", "U",
# Momentum favorites
"COIN", "ARKK", "QQQ", "TQQQ", "SPYG", "VUG", "IWF", "VGT", "XLK", "FTEC"
]
# Add symbols with error handling
self.symbols = []
for ticker in self.stock_symbols:
try:
symbol = self.AddEquity(ticker, Resolution.Daily).Symbol
self.symbols.append(symbol)
except:
continue
# Advanced data storage
self.price_history = {}
self.volume_history = {}
self.returns_history = {}
# Performance tracking
self.rebalance_count = 0
self.winning_trades = 0
self.total_trades = 0
self.SetWarmUp(30, Resolution.Daily) # More data for better calculations
# Bi-weekly rebalancing
self.Schedule.On(self.DateRules.Every([DayOfWeek.Monday, DayOfWeek.Thursday]),
self.TimeRules.AfterMarketOpen("AAPL", 30),
self.Rebalance)
def OnData(self, data):
if self.IsWarmingUp:
return
# Store enhanced data
for symbol in self.symbols:
if symbol in data and data[symbol] is not None:
bar = data[symbol]
# Initialize data structures
if symbol not in self.price_history:
self.price_history[symbol] = RollingWindow[float](120)
self.volume_history[symbol] = RollingWindow[float](60)
self.returns_history[symbol] = RollingWindow[float](60)
# Store price and volume
if hasattr(bar, 'Close') and bar.Close > 0:
current_price = float(bar.Close)
self.price_history[symbol].Add(current_price)
# Calculate daily returns
if self.price_history[symbol].Count > 1:
prev_price = self.price_history[symbol][1]
daily_return = (current_price / prev_price) - 1
self.returns_history[symbol].Add(daily_return)
if hasattr(bar, 'Volume') and bar.Volume > 0:
self.volume_history[symbol].Add(float(bar.Volume))
def Rebalance(self):
if self.IsWarmingUp:
return
# Calculate enhanced momentum scores
momentum_data = {}
for symbol in self.symbols:
if (symbol in self.price_history and
self.price_history[symbol].Count >= 60 and
symbol in self.returns_history and
self.returns_history[symbol].Count >= 20):
try:
score_data = self.CalculateEnhancedMomentum(symbol)
if score_data['composite_score'] > self.momentum_threshold:
momentum_data[symbol] = score_data
except Exception as e:
continue
if len(momentum_data) < 5:
self.Log("Insufficient momentum data for rebalancing")
return
# Rank by composite momentum score
ranked_stocks = sorted(momentum_data.items(),
key=lambda x: x[1]['composite_score'],
reverse=True)
# Select top performers with risk adjustment
selected_stocks = []
for symbol, data in ranked_stocks[:self.portfolio_size]:
# Additional quality filters
if (data['volatility'] < 0.5 and # Not too volatile
data['volume_trend'] > 0.8): # Good liquidity
selected_stocks.append((symbol, data))
# Ensure minimum selection
if len(selected_stocks) < 8:
selected_stocks = [(s, d) for s, d in ranked_stocks[:max(8, len(ranked_stocks)//2)]]
# Dynamic position sizing based on momentum strength
self.ExecuteTrades(selected_stocks)
# Logging
self.rebalance_count += 1
avg_momentum = np.mean([data['composite_score'] for _, data in selected_stocks])
selected_tickers = [str(s).split()[0] for s, _ in selected_stocks]
self.Log(f"Rebalance #{self.rebalance_count}: {len(selected_stocks)} stocks")
self.Log(f"Avg momentum: {avg_momentum:.2%}")
self.Log(f"Top picks: {', '.join(selected_tickers[:5])}")
def CalculateEnhancedMomentum(self, symbol):
"""Calculate multi-factor momentum score"""
prices = [self.price_history[symbol][i] for i in range(60)][::-1] # Reverse for chronological order
returns = [self.returns_history[symbol][i] for i in range(20)]
# Multiple momentum timeframes
mom_1w = (prices[-1] / prices[-5] - 1) if len(prices) >= 5 else 0
mom_1m = (prices[-1] / prices[-20] - 1) if len(prices) >= 20 else 0
mom_3m = (prices[-1] / prices[-60] - 1) if len(prices) >= 60 else 0
# Risk-adjusted momentum
volatility = np.std(returns) if len(returns) > 5 else 0.5
sharpe_momentum = mom_1m / (volatility + 0.01) if volatility > 0 else 0
# Acceleration (momentum of momentum)
recent_mom = (prices[-1] / prices[-10] - 1) if len(prices) >= 10 else 0
older_mom = (prices[-10] / prices[-20] - 1) if len(prices) >= 20 else 0
acceleration = recent_mom - older_mom
# Volume trend
volumes = [self.volume_history[symbol][i] for i in range(min(10, self.volume_history[symbol].Count))]
volume_trend = 1.0
if len(volumes) >= 10:
recent_vol = np.mean(volumes[:5])
older_vol = np.mean(volumes[5:10])
volume_trend = recent_vol / (older_vol + 1)
# Composite score with weights optimized for 2023
composite_score = (
0.40 * mom_1m + # Primary momentum
0.25 * sharpe_momentum + # Risk-adjusted
0.20 * mom_1w + # Short-term trend
0.10 * acceleration + # Momentum acceleration
0.05 * (mom_3m * 0.5) # Long-term context (reduced weight)
)
return {
'composite_score': composite_score,
'momentum_1m': mom_1m,
'volatility': volatility,
'acceleration': acceleration,
'volume_trend': volume_trend
}
def ExecuteTrades(self, selected_stocks):
"""Execute trades with dynamic position sizing"""
# Calculate position sizes based on momentum strength
total_momentum = sum([data['composite_score'] for _, data in selected_stocks])
target_positions = {}
for symbol, data in selected_stocks:
# Base equal weight
base_weight = 1.0 / len(selected_stocks)
# Momentum tilt (up to 50% adjustment)
if total_momentum > 0:
momentum_weight = (data['composite_score'] / total_momentum) * 0.5
else:
momentum_weight = 0
# Final weight with maximum position limit
final_weight = min(base_weight + momentum_weight, self.max_position_size)
target_positions[symbol] = final_weight
# Normalize weights to sum to ~1.0
total_weight = sum(target_positions.values())
if total_weight > 0:
target_positions = {k: v/total_weight for k, v in target_positions.items()}
# Execute liquidations
for symbol in list(self.Portfolio.Keys):
if symbol not in target_positions and self.Portfolio[symbol].Invested:
holding = self.Portfolio[symbol]
if holding.UnrealizedProfitPercent > 0:
self.winning_trades += 1
self.total_trades += 1
self.Liquidate(symbol)
# Execute new positions
for symbol, weight in target_positions.items():
if weight > 0.01: # Minimum position size
self.SetHoldings(symbol, weight)
def OnEndOfAlgorithm(self):
"""Performance summary"""
final_value = self.Portfolio.TotalPortfolioValue
total_return = (final_value / 100000 - 1) * 100
win_rate = (self.winning_trades / self.total_trades * 100) if self.total_trades > 0 else 0
self.Log("=" * 50)
self.Log("ENHANCED MOMENTUM STRATEGY RESULTS")
self.Log("=" * 50)
self.Log(f"Initial Capital: $100,000")
self.Log(f"Final Portfolio Value: ${final_value:,.0f}")
self.Log(f"Total Return: {total_return:.1f}%")
self.Log(f"Win Rate: {win_rate:.1f}%")
self.Log(f"Total Rebalances: {self.rebalance_count}")
self.Log(f"Total Trades: {self.total_trades}")
# Performance vs SPY (approximate)
spy_2023_return = 24.2 # Actual SPY return for 2023
alpha = total_return - spy_2023_return
self.Log(f"Alpha vs SPY: {alpha:.1f}%")
self.Log("=" * 50)