| Overall Statistics |
|
Total Orders 127 Average Win 26.63% Average Loss -4.40% Compounding Annual Return 87.517% Drawdown 60.600% Expectancy 3.028 Start Equity 100000 End Equity 6752409.44 Net Profit 6652.409% Sharpe Ratio 1.467 Sortino Ratio 1.751 Probabilistic Sharpe Ratio 65.720% Loss Rate 43% Win Rate 57% Profit-Loss Ratio 6.05 Alpha 0.611 Beta 1.47 Annual Standard Deviation 0.491 Annual Variance 0.241 Information Ratio 1.445 Tracking Error 0.447 Treynor Ratio 0.49 Total Fees $14955.94 Estimated Strategy Capacity $0 Lowest Capacity Asset AMD R735QTJ8XC9X Portfolio Turnover 5.16% Drawdown Recovery 484 |
#region imports
from AlgorithmImports import *
#endregion
class DualMomentumTechStocksEnhanced(QCAlgorithm):
"""
Enhanced Dual Momentum Strategy for Tech Stocks
Strategy:
- Universe: Configurable tech stocks (default: AMD, TSLA, AMZN, AAPL, SPXL)
- Momentum Score: Weighted sum of 1-month, 3-month, and 6-month returns
- Rebalance: Configurable (default: weekly)
- Position: 100% in single stock with highest momentum score
Features:
- Configurable lookback periods
- Optional momentum weighting
- Detailed performance tracking
- Risk management options
Based on: https://github.com/johnhou13579/Dual-Momentum-Trading-Bot
"""
def Initialize(self):
# === BASIC SETTINGS ===
self.SetStartDate(2014, 1, 1)
self.SetEndDate(2020, 9, 11)
self.SetCash(100000)
# === STRATEGY PARAMETERS ===
# Momentum lookback periods (in trading days)
self.lookback_1m = 21 # ~1 month
self.lookback_3m = 63 # ~3 months
self.lookback_6m = 126 # ~6 months
# Momentum weighting (set all to 1.0 for equal weight, or adjust)
# Higher weight = more importance in final score
self.weight_1m = 1.0
self.weight_3m = 1.0
self.weight_6m = 1.0
# Rebalance frequency
# Options: "weekly", "monthly", "bimonthly", "quarterly"
self.rebalance_frequency = "weekly"
# === UNIVERSE ===
self.tickers = ["AMD", "TSLA", "AMZN", "AAPL", "SPXL"]
self.symbols = {}
for ticker in self.tickers:
self.symbols[ticker] = self.AddEquity(ticker, Resolution.Daily).Symbol
# === STATE TRACKING ===
self.current_holding = None
self.momentum_history = [] # Track momentum scores over time
# === SCHEDULING ===
self.SetRebalanceSchedule()
# Warm up period
warmup_days = max(self.lookback_1m, self.lookback_3m, self.lookback_6m) + 10
self.SetWarmUp(timedelta(days=warmup_days))
# === CHARTS ===
self.InitializeCharts()
def SetRebalanceSchedule(self):
"""Set up rebalancing schedule based on frequency parameter"""
if self.rebalance_frequency == "monthly":
self.rebalance_months = list(range(1, 13)) # All months
elif self.rebalance_frequency == "bimonthly":
self.rebalance_months = [1, 3, 5, 7, 9, 11] # Every other month
elif self.rebalance_frequency == "quarterly":
self.rebalance_months = [1, 4, 7, 10] # Quarterly
elif self.rebalance_frequency == "weekly":
self.rebalance_months = list(range(1, 13)) # All months (handled by weekly schedule)
else:
raise ValueError(f"Invalid rebalance frequency: {self.rebalance_frequency}")
# Schedule rebalancing
if self.rebalance_frequency == "weekly":
# Rebalance every Monday
self.Schedule.On(
self.DateRules.Every(DayOfWeek.Monday),
self.TimeRules.AfterMarketOpen(self.tickers[0], 30),
self.Rebalance
)
else:
# Monthly, bimonthly, or quarterly
self.Schedule.On(
self.DateRules.MonthStart(self.tickers[0]),
self.TimeRules.AfterMarketOpen(self.tickers[0], 30),
self.Rebalance
)
def InitializeCharts(self):
"""Initialize custom charts for tracking"""
# Momentum score chart
momentum_chart = Chart("Momentum Scores")
for ticker in self.tickers:
momentum_chart.AddSeries(Series(ticker, SeriesType.Line, ""))
self.AddChart(momentum_chart)
# Holdings chart
holdings_chart = Chart("Current Holding")
holdings_chart.AddSeries(Series("Holding Value", SeriesType.Line, "$"))
self.AddChart(holdings_chart)
def CalculateMomentumScore(self, symbol, name):
"""
Calculate momentum score for a given symbol
Returns:
dict with momentum score and component returns, or None if calculation fails
"""
try:
# Get historical prices (get extra to be safe)
history_days = self.lookback_6m + 20
history = self.History(symbol, history_days, Resolution.Daily)
if history.empty or len(history) < self.lookback_6m:
return None
# Get closing prices
closes = history['close']
current_price = closes.iloc[-1]
# Calculate returns for each period
returns = {}
# 1-month return
if len(closes) >= self.lookback_1m:
past_price = closes.iloc[-self.lookback_1m]
returns['1m'] = (current_price - past_price) / past_price
else:
returns['1m'] = 0
# 3-month return
if len(closes) >= self.lookback_3m:
past_price = closes.iloc[-self.lookback_3m]
returns['3m'] = (current_price - past_price) / past_price
else:
returns['3m'] = 0
# 6-month return
if len(closes) >= self.lookback_6m:
past_price = closes.iloc[-self.lookback_6m]
returns['6m'] = (current_price - past_price) / past_price
else:
returns['6m'] = 0
# Calculate weighted momentum score
momentum_score = (
returns['1m'] * self.weight_1m +
returns['3m'] * self.weight_3m +
returns['6m'] * self.weight_6m
)
return {
'symbol': symbol,
'score': momentum_score,
'returns': returns,
'current_price': current_price
}
except Exception as e:
self.Debug(f"Error calculating momentum for {name}: {str(e)}")
return None
def Rebalance(self):
"""Calculate momentum scores and rebalance portfolio"""
# Skip if warming up
if self.IsWarmingUp:
return
# Only check months if NOT weekly
if self.rebalance_frequency != "weekly":
if self.Time.month not in self.rebalance_months:
return
self.Debug(f"\n{'='*60}")
self.Debug(f"REBALANCING - {self.Time.strftime('%Y-%m-%d')}")
self.Debug(f"{'='*60}")
# Calculate momentum scores for all symbols
momentum_scores = {}
for name, symbol in self.symbols.items():
result = self.CalculateMomentumScore(symbol, name)
if result is not None:
momentum_scores[name] = result
# Log details
returns = result['returns']
self.Debug(
f"{name:6s}: Score={result['score']:7.4f} | "
f"1m={returns['1m']:7.2%} | "
f"3m={returns['3m']:7.2%} | "
f"6m={returns['6m']:7.2%}"
)
# Plot momentum score
self.Plot("Momentum Scores", name, result['score'])
# Find the symbol with highest momentum score
if not momentum_scores:
self.Debug("No momentum scores calculated, skipping rebalance")
return
best_name = max(momentum_scores.items(), key=lambda x: x[1]['score'])[0]
best_data = momentum_scores[best_name]
best_symbol = best_data['symbol']
best_score = best_data['score']
self.Debug(f"\n{'*'*60}")
self.Debug(f"WINNER: {best_name} with momentum score: {best_score:.4f}")
self.Debug(f"{'*'*60}\n")
# Store momentum history for analysis
self.momentum_history.append({
'date': self.Time,
'winner': best_name,
'score': best_score,
'all_scores': {k: v['score'] for k, v in momentum_scores.items()}
})
# Execute trade if needed
if self.current_holding != best_symbol:
# Liquidate current position
if self.current_holding is not None:
old_name = [n for n, s in self.symbols.items() if s == self.current_holding][0]
self.Liquidate(self.current_holding)
self.Debug(f"→ Liquidated {old_name}")
# Enter new position (100% of portfolio)
self.SetHoldings(best_symbol, 1.0)
self.current_holding = best_symbol
self.Debug(f"→ Entered {best_name} at 100%")
else:
self.Debug(f"→ No change, continuing to hold {best_name}")
# Plot current holding value
if self.current_holding:
holding_value = self.Portfolio[self.current_holding].HoldingsValue
self.Plot("Current Holding", "Holding Value", holding_value)
def OnData(self, data):
"""OnData event - all logic in scheduled rebalance"""
pass
def OnEndOfAlgorithm(self):
"""Called at the end of the algorithm"""
self.Debug(f"\n{'='*60}")
self.Debug("ALGORITHM SUMMARY")
self.Debug(f"{'='*60}")
self.Debug(f"Start Date: {self.StartDate.strftime('%Y-%m-%d')}")
self.Debug(f"End Date: {self.EndDate.strftime('%Y-%m-%d')}")
self.Debug(f"Initial Capital: ${100000:,.2f}")
self.Debug(f"Final Portfolio Value: ${self.Portfolio.TotalPortfolioValue:,.2f}")
total_return = (self.Portfolio.TotalPortfolioValue - 100000) / 100000
self.Debug(f"Total Return: {total_return:.2%}")
if self.current_holding:
final_stock = [n for n, s in self.symbols.items() if s == self.current_holding][0]
self.Debug(f"Final Holding: {final_stock}")
# Analyze momentum history
if self.momentum_history:
self.Debug(f"\nTotal Rebalances: {len(self.momentum_history)}")
# Count holdings
holdings_count = {}
for entry in self.momentum_history:
winner = entry['winner']
holdings_count[winner] = holdings_count.get(winner, 0) + 1
self.Debug("\nHolding Frequency:")
for ticker, count in sorted(holdings_count.items(), key=lambda x: x[1], reverse=True):
pct = count / len(self.momentum_history) * 100
self.Debug(f" {ticker}: {count} times ({pct:.1f}%)")
self.Debug(f"{'='*60}\n")