| Overall Statistics |
|
Total Orders 130 Average Win 2.42% Average Loss -2.71% Compounding Annual Return 11.461% Drawdown 16.500% Expectancy 0.202 Start Equity 10000 End Equity 12647.13 Net Profit 26.471% Sharpe Ratio 0.232 Sortino Ratio 0.265 Probabilistic Sharpe Ratio 18.744% Loss Rate 37% Win Rate 63% Profit-Loss Ratio 0.89 Alpha -0.031 Beta 0.629 Annual Standard Deviation 0.177 Annual Variance 0.031 Information Ratio -0.434 Tracking Error 0.169 Treynor Ratio 0.065 Total Fees $167.24 Estimated Strategy Capacity $8300000.00 Lowest Capacity Asset MRNA X05QXHPHSF39 Portfolio Turnover 4.35% |
from AlgorithmImports import *
class Nasdaq100MeanReversion(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2023, 1, 1) # Set Start Date
self.SetEndDate(2025, 3, 1) # Set End Date
self.SetCash(10000) # Set Strategy Cash
# Universe selection - using Nasdaq 100 ETF to get constituents
self.nasdaq100 = self.AddEquity("QQQ", Resolution.Daily).Symbol
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.Universe.ETF(self.nasdaq100, self.UniverseSettings, self.EtfConstituentsFilter))
# Strategy parameters
self.spread_percentage = 0.0007 # 0.07% trading cost
self.lookback = 14 # ROC lookback period
self.investment_per_trade = 3000 # Amount to invest per trade
self.max_positions = 10 # Maximum number of concurrent positions
self.max_days_held = 20 # Maximum holding period
# Data dictionaries for tracking
self.assets = {} # Dictionary to store securities data
self.trades = {} # Dictionary to store active trades
self.roc_data = {} # Store ROC calculations
self.atr_data = {} # Store ATR calculations
self.holdings = {} # Store position holdings
# Schedule the daily update function
self.schedule.on(self.date_rules.every_day(),
self.time_rules.before_market_close("QQQ", 10),
self.DailyUpdate)
# Portfolio tracking metrics
self.net_profit = 0
self.realized_trades = []
self.Log(f"Strategy initialized with {self.investment_per_trade} investment per trade")
def EtfConstituentsFilter(self, constituents: List[ETFConstituentUniverse]) -> List[Symbol]:
# Filter only valid constituents with weight > 0
selected = [c for c in constituents if c.Weight > 0 and c.Symbol is not None]
if not selected: # If no valid symbols, return an empty list safely
self.Debug("No valid ETF constituents found!")
return []
# Sort by weight and take top 100
selected = sorted(selected, key=lambda c: c.Weight, reverse=True)[:100]
# Debugging: Show selected symbols
# extracted_symbols = [c.Symbol.Value for c in selected]
# self.Debug(f"Selected Constituents: {extracted_symbols}")
return [c.Symbol for c in selected]
def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
symbol = security.Symbol.Value
if symbol not in self.assets:
# Initialize data for the security
self.assets[symbol] = security
self.roc_data[symbol] = RateOfChangePercent(self.lookback)
self.atr_data[symbol] = AverageTrueRange(self.lookback)
self.holdings[symbol] = 0
self.add_security(security.Type, symbol, Resolution.Daily) # Register the security
# Add the required data
self.register_indicator(symbol, self.roc_data[symbol], Resolution.Daily)
self.register_indicator(symbol, self.atr_data[symbol], Resolution.Daily)
self.Debug(f"Added {symbol} to universe")
for security in changes.RemovedSecurities:
symbol = security.Symbol
if symbol in self.assets:
if self.Portfolio[symbol].Invested:
self.Liquidate(symbol)
self.Debug(f"Liquidated {symbol} due to removal from universe")
self.assets.pop(symbol, None)
self.roc_data.pop(symbol, None)
self.atr_data.pop(symbol, None)
self.holdings.pop(symbol, None)
if symbol in self.trades:
self.trades.pop(symbol, None)
self.Debug(f"Removed {symbol} from universe")
self.Debug(f"Total securities in assets: {len(self.assets)}")
self.Log(f"Total securities in assets: {len(self.assets)}")
def DailyUpdate(self):
# Check for exit conditions first
self.CheckExitSignals()
# Check for entry conditions if we have capacity
if len(self.trades) < self.max_positions:
self.CheckEntrySignals()
def CheckEntrySignals(self):
# Calculate how many new positions we can open
available_slots = self.max_positions - len(self.trades)
available_capital = self.Portfolio.Cash
max_new_positions = min(int(available_capital / self.investment_per_trade), available_slots)
if max_new_positions <= 0:
return
# Find potential entry signals
potential_entries = []
for symbol, security in self.assets.items():
# Skip if already in a trade
if symbol in self.trades:
continue
# Skip if data not ready
if not self.roc_data[symbol].IsReady or not self.atr_data[symbol].IsReady:
continue
# Get ROC value
roc = self.roc_data[symbol].Current.Value
# if roc < -20:
# self.debug(f"Current ROC for {symbol}: {roc}")
prev_roc = self.roc_data[symbol].Previous.Value
# if prev_roc < -20:
# self.debug(f"Prev ROC for {symbol}: {roc}")
# Entry condition: ROC < -20% and ROC increasing
if roc is not None and prev_roc is not None:
if roc < -20 and roc > prev_roc:
# Calculate momentum for ranking
momentum = roc - prev_roc
potential_entries.append((symbol, momentum))
# Sort by momentum (highest first) and take top entries
if potential_entries:
sorted_entries = sorted(potential_entries, key=lambda x: x[1], reverse=True)
entries_to_take = sorted_entries[:max_new_positions]
for symbol, _ in entries_to_take:
self.EnterTrade(symbol)
def EnterTrade(self, symbol):
if symbol in self.trades or symbol not in self.assets:
self.debug(f"Already in trade, or no symbol for {symbol}")
return
security = self.assets[symbol]
current_price = security.Price
# Adjust entry price with spread
entry_price = current_price * (1 + self.spread_percentage)
# Calculate position size
position_size = self.investment_per_trade / entry_price
# Calculate stop loss and take profit based on ATR
atr = self.atr_data[symbol].Current.Value
stop_loss = entry_price - (2.5 * atr)
take_profit = entry_price + (1 * atr)
# Store trade info
self.trades[symbol] = {
'entry_date': self.Time,
'entry_price': entry_price,
'position_size': position_size,
'stop_loss': stop_loss,
'take_profit': take_profit,
'days_held': 1
}
# Execute the trade
self.MarketOrder(symbol, position_size)
self.debug(f"Entered trade for {symbol} at {entry_price}, Stop: {stop_loss}, TP: {take_profit}")
def CheckExitSignals(self):
symbols_to_exit = []
for symbol, trade in self.trades.items():
security = self.assets[symbol]
current_price = security.Price
# Adjust exit price with spread
exit_price = current_price * (1 - self.spread_percentage)
# Check exit conditions
exit_signal = None
# 1. Take profit
if exit_price >= trade['take_profit']:
exit_signal = "Take Profit"
# 2. Stop loss
elif exit_price <= trade['stop_loss']:
exit_signal = "Stop Loss"
# 3. Max days held
elif trade['days_held'] >= self.max_days_held:
exit_signal = "Max Days Held"
# If we have an exit signal, add to exit list
if exit_signal:
symbols_to_exit.append((symbol, exit_signal))
else:
# Increment days held
trade['days_held'] += 1
# Process exits
for symbol, exit_signal in symbols_to_exit:
self.ExitTrade(symbol, exit_signal)
def ExitTrade(self, symbol, exit_signal):
if symbol not in self.trades:
return
trade = self.trades[symbol]
# Calculate profit
security = self.assets[symbol]
current_price = security.Price * (1 - self.spread_percentage) # Adjust for spread
profit_percentage = (current_price / trade['entry_price'] - 1) * 100
profit_amount = trade['position_size'] * (current_price - trade['entry_price'])
# Close the position
self.Liquidate(symbol)
# Record trade
self.realized_trades.append({
'symbol': symbol,
'entry_date': trade['entry_date'],
'exit_date': self.Time,
'days_held': trade['days_held'],
'entry_price': trade['entry_price'],
'exit_price': current_price,
'exit_signal': exit_signal,
'profit_percentage': profit_percentage,
'profit_amount': profit_amount
})
# Update net profit
self.net_profit += profit_amount
# Remove from active trades
self.trades.pop(symbol)
self.Debug(f"Exited {symbol} with {exit_signal}. Profit: {profit_amount:.2f} ({profit_percentage:.2f}%)")
def OnData(self, data):
# Main strategy logic runs in scheduled events (DailyUpdate)
pass
def on_end_of_algorithm(self) -> None:
# Final statistics
self.Debug(f"Strategy completed with net profit: ${self.net_profit:.2f}")
self.Debug(f"Total trades: {len(self.realized_trades)}")
if len(self.realized_trades) > 0:
# Calculate win rate
winning_trades = [t for t in self.realized_trades if t['profit_amount'] > 0]
win_rate = len(winning_trades) / len(self.realized_trades) * 100
# Calculate average profit
avg_profit = sum(t['profit_amount'] for t in self.realized_trades) / len(self.realized_trades)
# Calculate average holding period
avg_days = sum(t['days_held'] for t in self.realized_trades) / len(self.realized_trades)
self.Debug(f"Win rate: {win_rate:.2f}%")
self.Debug(f"Average profit per trade: ${avg_profit:.2f}")
self.Debug(f"Average holding period: {avg_days:.2f} days")