| Overall Statistics |
|
Total Orders 793 Average Win 0.15% Average Loss -0.09% Compounding Annual Return 9.329% Drawdown 6.900% Expectancy 0.204 Start Equity 25000 End Equity 29991.42 Net Profit 19.966% Sharpe Ratio 0.212 Sortino Ratio 0.274 Probabilistic Sharpe Ratio 73.993% Loss Rate 53% Win Rate 47% Profit-Loss Ratio 1.57 Alpha -0.003 Beta 0.132 Annual Standard Deviation 0.046 Annual Variance 0.002 Information Ratio -0.722 Tracking Error 0.124 Treynor Ratio 0.074 Total Fees $793.00 Estimated Strategy Capacity $22000000.00 Lowest Capacity Asset CME SKAIZ0LWKM05 Portfolio Turnover 3.23% Drawdown Recovery 49 |
from AlgorithmImports import *
import numpy as np
import pandas as pd
from datetime import timedelta
class LargeCapDriftDispersionAlgorithm(QCAlgorithm):
def Initialize(self):
# 1. Backtest Settings
self.SetStartDate(2024, 1, 1)
self.SetCash(25000)
self.SetTimeZone(TimeZones.Chicago)
self.resolution = Resolution.Minute
self.spy = self.AddEquity("SPY", self.resolution).Symbol
self.SetBenchmark("SPY")
# 2. Brokerage Model (Margin is required for shorting)
self.SetBrokerageModel(
BrokerageName.InteractiveBrokersBrokerage,
AccountType.Margin
)
# 3. Universe Selection (Large Cap Liquid Stocks)
self.UniverseSettings.Resolution = self.resolution
tickers = [
"AAPL","MSFT","AMZN","NVDA","META","GOOGL","BRK.B","JPM","JNJ","V","PG","HD","UNH","MA",
"XOM","CVX","MRK","KO","PEP","ABBV","AVGO","COST","WMT","MCD","CSCO","IBM","CAT","BA","DIS","GS",
"AMD","TSLA","GOOG","SMCI","PLTR","NFLX","CRM","ADBE","INTU",
"NOW","ORCL","QCOM","TXN","MU","PANW", "ABT", "T", "TMUS", "GEV", "ANET", "ACN", "PGR", "CEG", "ASML",
"SCHW", "UL", "COF", "TD", "PFE", "SYK", "BBVA", "BN", "ARM", "PLD", "MDT", "CME", "SBUX", "DASH"
]
symbols = [Symbol.Create(t, SecurityType.Equity, Market.USA) for t in tickers]
self.SetUniverseSelection(ManualUniverseSelectionModel(symbols))
# 4. Alpha Model: Systematic Beta Selection
self.SetAlpha(TridentBetaNeutralAlpha(
reference_ticker="SPY",
lookback=252, # 1 Year of trading days
sma_period=200, # Market Regime Filter
resolution=self.resolution,
n_each_side=2, # Top 2 Long, Top 2 Short
emit_after_open_min=245
))
# 5. Portfolio Construction: Equal Weighting (10% Gross Exposure)
self.SetPortfolioConstruction(GrossCappedInsightWeightingPCM(max_gross=0.10))
self.set_warm_up(252*3)
# 6. Execution Model: 45-minute delay on short entries
self.SetExecution(StaggeredLongShortExecutionModel(delay_minutes=45))
# 7. Charting: Real-time P/L visualization
pnl_chart = Chart('Trade Profits')
pnl_chart.AddSeries(Series('Realized P/L ($)', SeriesType.Bar, '$'))
self.AddChart(pnl_chart)
def OnOrderEvent(self, orderEvent: OrderEvent):
"""Logs Realized P/L and correctly identifies Long vs Short sleeves."""
if orderEvent.Status != OrderStatus.Filled:
return
symbol = orderEvent.Symbol
# We only log when the position is fully closed (not invested)
if not self.Portfolio[symbol].Invested:
closed_trades = list(self.TradeBuilder.ClosedTrades)
if not closed_trades:
return
last_t = closed_trades[-1]
# Ensure we are looking at the trade for this specific symbol
if last_t.Symbol == symbol:
# 1. Get the P/L
pnl = self.Portfolio[symbol].LastTradeProfit
if pnl == 0: pnl = getattr(last_t, 'Profit', 0)
# 2. THE FIX: Correctly identify the sleeve using TradeDirection
# TradeDirection.Long means you bought first.
# TradeDirection.Short means you sold first.
if last_t.Direction == TradeDirection.Long:
sleeve = "LONG (Low Beta)"
else:
sleeve = "SHORT (High Beta)"
# 3. Calculate Math for the log
entry_p = last_t.EntryPrice
exit_p = last_t.ExitPrice
total_cost = entry_p * abs(last_t.Quantity)
pnl_pct = (pnl / total_cost) * 100 if total_cost > 0 else 0
# 4. Final Accurate Log
if pnl != 0:
log_msg = (f"[{sleeve}] Closed {symbol} | "
f"P/L: ${pnl:.2f} ({pnl_pct:.2f}%) | "
f"Entry: {entry_p:.2f} | Exit: {exit_p:.2f}")
self.Log(log_msg)
self.Plot('Trade Profits', 'Realized P/L ($)', pnl)
class StaggeredLongShortExecutionModel(ExecutionModel):
def __init__(self, delay_minutes: int = 20):
self.delay = timedelta(minutes=int(delay_minutes))
self.pending = {}
def Execute(self, algorithm: QCAlgorithm, targets):
now = algorithm.Time
# Handle delayed short entries
due = [sym for sym, rec in self.pending.items() if rec["time"] <= now]
for sym in due:
rec = self.pending.pop(sym, None)
if rec: algorithm.SetHoldings(sym, float(rec["percent"]), tag=rec["tag"])
for t in targets:
sym = t.Symbol
if sym not in algorithm.Securities: continue
price = algorithm.Securities[sym].Price
total_value = algorithm.Portfolio.TotalPortfolioValue
if price <= 0 or total_value <= 0: continue
target_percent = (float(t.Quantity) * float(price)) / float(total_value)
# Liquidation
if abs(target_percent) < 1e-6:
if algorithm.Portfolio[sym].Invested:
algorithm.Liquidate(sym, tag="Exit: Selection Changed")
if sym in self.pending: self.pending.pop(sym, None)
continue
insight_tag = "Alpha Trade"
active_insights = algorithm.Insights.GetActiveInsights(now)
for insight in active_insights:
if insight.Symbol == sym:
insight_tag = insight.Tag
break
if target_percent < 0: # Delayed Shorts
self.pending[sym] = {"time": now + self.delay, "percent": float(target_percent), "tag": insight_tag}
else: # Immediate Longs
algorithm.SetHoldings(sym, float(target_percent), tag=insight_tag)
class TridentBetaNeutralAlpha(AlphaModel):
def __init__(self, reference_ticker, lookback, sma_period, resolution, n_each_side=2, emit_after_open_min=245):
super().__init__()
self.reference_ticker = reference_ticker
self.lookback = lookback
self.sma_period = sma_period
self.resolution = resolution
self.n_each_side = n_each_side
self.emit_after_open_min = emit_after_open_min
self.cache = {}
self.reference = None
self.last_emit_date = None
self.ref_sma200 = None
def OnSecuritiesChanged(self, algorithm, changes):
if self.reference is None:
ref_sec = algorithm.AddEquity(self.reference_ticker, self.resolution)
self.reference = ref_sec.Symbol
self.ref_sma200 = algorithm.SMA(self.reference, self.sma_period, Resolution.Daily)
self._init_symboldata_from_history(algorithm, [x.Symbol for x in changes.AddedSecurities])
def Update(self, algorithm, data):
if algorithm.IsWarmingUp or not self._after_open(algorithm): return []
today = algorithm.Time.date()
if self.last_emit_date == today: return []
regime = "BULL" if algorithm.Securities[self.reference].Price >= self.ref_sma200.Current.Value else "BEAR"
ref_sd = self.cache.get(self.reference)
if not ref_sd: return []
ref_series = ref_sd.GetSeries()
betas = {}
for sym, sd in self.cache.items():
if sym == self.reference: continue
beta = sd.compute_beta_vs(ref_series)
if beta is not None: betas[sym] = beta
sorted_betas = sorted(betas.items(), key=lambda x: x[1])
long_candidates = sorted_betas[:self.n_each_side]
short_candidates = sorted_betas[-self.n_each_side:]
insights = []
period = timedelta(days=1)
for sym, beta in long_candidates:
tag = f"LONG | Beta: {beta:.2f} | {regime}"
insights.append(Insight.Price(sym, period, InsightDirection.Up, None, None, None, 1.0, tag))
for sym, beta in short_candidates:
tag = f"SHORT | Beta: {beta:.2f} | {regime}"
insights.append(Insight.Price(sym, period, InsightDirection.Down, None, None, None, 1.0, tag))
self.last_emit_date = today
return insights
def _after_open(self, algorithm):
sec = algorithm.Securities[self.reference]
open_time = sec.Exchange.Hours.GetNextMarketOpen(algorithm.Time - timedelta(days=1), False)
return algorithm.Time >= open_time + timedelta(minutes=self.emit_after_open_min)
def _init_symboldata_from_history(self, algorithm, symbols):
hist = algorithm.History(symbols, self.lookback + self.sma_period, self.resolution)
if hist.empty: return
close_df = hist["close"].unstack(level=0)
for sym in symbols:
if sym in close_df.columns:
self.cache[sym] = SymbolData(algorithm, sym, self.lookback, self.sma_period, self.resolution, close_df[sym])
class GrossCappedInsightWeightingPCM(PortfolioConstructionModel):
def __init__(self, max_gross=0.10):
super().__init__()
self.max_gross = max_gross
def CreateTargets(self, algorithm, insights):
if not insights: return []
weight = self.max_gross / len(insights)
return [PortfolioTarget.Percent(algorithm, i.Symbol, (1 if i.Direction == InsightDirection.Up else -1) * weight) for i in insights]
class SymbolData:
def __init__(self, algorithm, symbol, lookback, sma_period, resolution, history_series):
self.window = RollingWindow[IndicatorDataPoint](lookback)
self.sma = algorithm.SMA(symbol, sma_period, resolution)
self.sma.Updated += lambda s, u: self.window.Add(u)
for t, p in history_series.ffill().items(): self.sma.Update(t, p)
def GetSeries(self):
if not self.window.IsReady: return None
return pd.Series({x.EndTime: x.Value for x in self.window}).iloc[::-1]
def compute_beta_vs(self, reference_series):
sym_series = self.GetSeries()
if sym_series is None or reference_series is None: return None
df = pd.concat([sym_series.rename("s"), reference_series.rename("r")], axis=1).dropna()
if len(df) < 50: return None
s_ret, r_ret = np.diff(df["s"]) / df["s"][:-1], np.diff(df["r"]) / df["r"][:-1]
n = min(len(s_ret), len(r_ret))
A = np.vstack([s_ret[:n], np.ones(n)]).T
beta, _ = np.linalg.lstsq(A, r_ret[:n], rcond=None)[0]
return float(beta)# region imports
from AlgorithmImports import *
# endregion
OrderTypeKeys = [
'Market', 'Limit', 'StopMarket', 'StopLimit', 'MarketOnOpen',
'MarketOnClose', 'OptionExercise',
]
OrderTypeCodes = dict(zip(range(len(OrderTypeKeys)), OrderTypeKeys))