| Overall Statistics |
|
Total Orders 32276 Average Win 0.05% Average Loss -0.05% Compounding Annual Return 0.593% Drawdown 10.100% Expectancy 0.009 Start Equity 100000 End Equity 110147.47 Net Profit 10.147% Sharpe Ratio -0.532 Sortino Ratio -0.556 Probabilistic Sharpe Ratio 0.002% Loss Rate 51% Win Rate 49% Profit-Loss Ratio 1.07 Alpha -0.015 Beta -0.009 Annual Standard Deviation 0.03 Annual Variance 0.001 Information Ratio -0.7 Tracking Error 0.147 Treynor Ratio 1.763 Total Fees $0.00 Estimated Strategy Capacity $210000000.00 Lowest Capacity Asset TQQQ UK280CGTCB51 Portfolio Turnover 127.76% Drawdown Recovery 1087 |
from AlgorithmImports import *
class SpyTqqqSpreadReversion(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2010, 3, 5)
self.set_cash(100000)
# Number of contracts (each contract = 1 share SPY + 1 share TQQQ, opposite signs)
self._n_contracts = 100
# Take-profit / stop-loss per contract (±$10 per contract), scaled by N
self._tp_sl = 10.0 * self._n_contracts
# Subscribe to SPY and TQQQ at hourly resolution (default ADJUSTED normalization)
self._spy = self.add_equity("SPY", Resolution.HOUR)
self._tqqq = self.add_equity("TQQQ", Resolution.HOUR)
self._spy.set_fee_model(ConstantFeeModel(0))
self._tqqq.set_fee_model(ConstantFeeModel(0))
# Manual SMA for the 74-hour rolling mean of first difference of spread
self._spread_sma = SimpleMovingAverage(74)
# Track previous spread for first difference
self._prev_spread = None
# Track current signal state (0=flat, +1=long spread, -1=short spread)
self._prev_signal = 0
# Warm up: need 75+ hourly bars (1 for prev_spread + 74 first differences for SMA)
self.set_warm_up(80, Resolution.HOUR)
def on_data(self, data: Slice) -> None:
# Need both SPY and TQQQ bars
if not data.bars.contains_key(self._spy.symbol) or not data.bars.contains_key(self._tqqq.symbol):
return
spy_bar = data.bars[self._spy.symbol]
tqqq_bar = data.bars[self._tqqq.symbol]
spy_price = spy_bar.close
tqqq_price = tqqq_bar.close
# Compute spread = AdjClose(SPY) - AdjClose(TQQQ)
spread = spy_price - tqqq_price
# Compute first difference d = Spread_t - Spread_{t-1} and feed SMA
d = 0.0
if self._prev_spread is not None:
d = spread - self._prev_spread
self._spread_sma.update(spy_bar.end_time, d)
self._prev_spread = spread
# During warm-up, just build indicator state
if self.is_warming_up:
return
# Need SMA to be ready
if not self._spread_sma.is_ready:
return
# Threshold: rolling 74-hour mean of first difference
tau = self._spread_sma.current.value
# Signal: +1 if d > tau, -1 if d < -tau, 0 otherwise
signal = 0
if d > tau:
signal = 1
elif d < -tau:
signal = -1
# Signal-driven position management
if signal != self._prev_signal:
if signal == 0:
self._set_position(0)
else:
self._set_position(signal)
self._prev_signal = signal
return
# ±$10 take-profit / stop-loss overlay (only when holding)
if signal != 0:
pnl = (self.portfolio[self._spy.symbol].unrealized_profit +
self.portfolio[self._tqqq.symbol].unrealized_profit)
if pnl >= self._tp_sl or pnl <= -self._tp_sl:
self._set_position(0)
self._prev_signal = 0
return
self._prev_signal = signal
def _set_position(self, target_direction: int) -> None:
"""Set position to target direction: +1=long spread, -1=short spread, 0=flat."""
target_spy = target_direction * self._n_contracts
target_tqqq = -target_direction * self._n_contracts
current_spy = self.portfolio[self._spy.symbol].quantity
current_tqqq = self.portfolio[self._tqqq.symbol].quantity
spy_order_qty = target_spy - current_spy
tqqq_order_qty = target_tqqq - current_tqqq
if spy_order_qty != 0:
self.market_order(self._spy.symbol, spy_order_qty)
if tqqq_order_qty != 0:
self.market_order(self._tqqq.symbol, tqqq_order_qty)