| Overall Statistics |
|
Total Orders 1374 Average Win 1.06% Average Loss -0.05% Compounding Annual Return 0.016% Drawdown 10.400% Expectancy 0.020 Start Equity 100000 End Equity 100038.55 Net Profit 0.039% Sharpe Ratio -1.425 Sortino Ratio -1.506 Probabilistic Sharpe Ratio 3.493% Loss Rate 96% Win Rate 4% Profit-Loss Ratio 23.16 Alpha -0.05 Beta -0.042 Annual Standard Deviation 0.038 Annual Variance 0.001 Information Ratio -1.086 Tracking Error 0.143 Treynor Ratio 1.297 Total Fees $1374.00 Estimated Strategy Capacity $440000.00 Lowest Capacity Asset SPY 331K18YH876LI|SPY R735QTJ8XC9X Portfolio Turnover 45.82% Drawdown Recovery 224 |
from AlgorithmImports import *
from datetime import timedelta
import math
from collections import deque
class ContractData:
"""DTO to hold option contract references."""
def __init__(self, symbol: Symbol, right: OptionRight, expiry, underlying: Symbol):
self.symbol = symbol
self.right = right
self.expiry = expiry
self.underlying = underlying
class OptimizedLongGammaStrategy(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2024, 1, 1)
self.set_end_date(2026, 5, 16)
self.set_cash(100000)
self._contracts: list[ContractData] = []
self._portfolio_delta = 0.0
self._hedge_threshold = 15
self._straddle_entry_cost = 0.0
self._total_straddles = 0
self._total_hedges = 0
self._hedge_order_ids: set[int] = set()
self._entry_filled = False
self._num_contracts = 1
self._entry_date = None
# Risk management
self._profit_target_multiple = 3.0
self._stop_loss_multiple = 0.7
self._risk_per_trade = 0.02
self._holding_days_max = 18
# NEW: Trailing stop - once value hits 2x entry, floor at 1.5x
self._trailing_activation_multiple = 2.0
self._trailing_floor_multiple = 1.5
self._trailing_stop_activated = False
# NEW: Circuit breaker - pause trading after 5% drawdown
self._max_drawdown_threshold = 0.05 # 5% from peak
self._circuit_breaker_days = 5
self._peak_portfolio_value = 100000.0
self._circuit_breaker_until = None
# VIX history for percentile filter
self._vix_history: deque[float] = deque(maxlen=504)
# Track current underlying for this straddle
self._current_underlying = None
# Underlyings to trade for diversification
self._underlying_symbols: list[Symbol] = []
self.settings.seed_initial_prices = True
# Add SPY and QQQ for diversification
for ticker in ["SPY", "QQQ"]:
equity = self.add_equity(ticker, data_normalization_mode=DataNormalizationMode.RAW)
self._underlying_symbols.append(equity.symbol)
option = self.add_option(ticker, Resolution.MINUTE)
option.set_filter(-5, 5, 0, 45)
# VIX for volatility regime filtering
self._vix = self.add_index("VIX", Resolution.DAILY).symbol
# Round-robin index for rotating underlyings
self._underlying_index = 0
# Weekly entry
self.schedule.on(
self.date_rules.every(DayOfWeek.MONDAY),
self.time_rules.after_market_open(self._underlying_symbols[0], 30),
self._maybe_enter_straddle
)
# Hedge delta mid-morning and before close
self.schedule.on(
self.date_rules.every_day(self._underlying_symbols[0]),
self.time_rules.after_market_open(self._underlying_symbols[0], 120),
self._hedge_delta
)
self.schedule.on(
self.date_rules.every_day(self._underlying_symbols[0]),
self.time_rules.before_market_close(self._underlying_symbols[0], 60),
self._hedge_delta
)
# Check holding period exit daily
self.schedule.on(
self.date_rules.every_day(self._underlying_symbols[0]),
self.time_rules.before_market_close(self._underlying_symbols[0], 30),
self._check_holding_period
)
def on_data(self, data: Slice) -> None:
if self.is_warming_up:
return
# Track VIX history for percentile filter
if data.contains_key(self._vix):
vix_price = float(self.securities[self._vix].price)
if vix_price > 0:
self._vix_history.append(vix_price)
# NEW: Update peak portfolio value and check circuit breaker
current_value = self.portfolio.total_portfolio_value
if current_value > self._peak_portfolio_value:
self._peak_portfolio_value = current_value
drawdown = (self._peak_portfolio_value - current_value) / self._peak_portfolio_value
if drawdown >= self._max_drawdown_threshold and self._circuit_breaker_until is None:
self._circuit_breaker_until = self.time + timedelta(days=self._circuit_breaker_days)
self.log(f"[CIRCUIT BREAKER] Drawdown={drawdown:.2%}, pausing until {self._circuit_breaker_until}")
self._liquidate_options()
if not self._contracts:
return
# Wait for legs to fill, then record cost basis
if not self._entry_filled:
invested_count = sum(
1 for cd in self._contracts
if self.portfolio[cd.symbol].invested
)
if invested_count == len(self._contracts):
self._entry_filled = True
self._straddle_entry_cost = abs(sum(
self.portfolio[cd.symbol].holdings_value
for cd in self._contracts
))
self.log(f"[FILLED] Entry cost=${self._straddle_entry_cost:.2f}")
return
self._check_risk_limits()
def _get_realized_vol(self, underlying: Symbol) -> float:
"""Calculate 21-day annualized realized volatility."""
history = self.history(underlying, 30, Resolution.DAILY)
bars = list(history)
if len(bars) < 22:
return 0.0
closes = [float(bar.close) for bar in bars[-22:]]
returns = [math.log(closes[i] / closes[i - 1]) for i in range(1, len(closes))]
if len(returns) < 20:
return 0.0
mean_r = sum(returns) / len(returns)
var_r = sum((r - mean_r) ** 2 for r in returns) / (len(returns) - 1)
return math.sqrt(var_r * 252) * 100
def _get_vix_percentile(self) -> float:
"""Calculate current VIX percentile over the last 2 years."""
if len(self._vix_history) < 100:
return 50.0 # Default to median if not enough data
current_vix = self._vix_history[-1]
count_below = sum(1 for v in self._vix_history if v < current_vix)
return (count_below / len(self._vix_history)) * 100
def _check_risk_limits(self) -> None:
"""Close straddle if profit target, stop loss, or trailing stop is hit."""
if not self._contracts or self._straddle_entry_cost == 0:
return
current_value = abs(sum(
self.portfolio[cd.symbol].holdings_value
for cd in self._contracts
if self.portfolio[cd.symbol].invested
))
# Trailing stop logic
gain_multiple = current_value / self._straddle_entry_cost
if gain_multiple >= self._trailing_activation_multiple:
self._trailing_stop_activated = True
if self._trailing_stop_activated:
# Floor at 1.5x entry to lock in gains
floor_value = self._straddle_entry_cost * self._trailing_floor_multiple
if current_value <= floor_value:
self.log(
f"[TRAILING STOP] Value=${current_value:.2f} "
f"fell below floor=${floor_value:.2f} (peak was {gain_multiple:.2f}x)"
)
self._liquidate_options()
return
# Traditional stop loss
if current_value <= self._straddle_entry_cost * self._stop_loss_multiple:
self.log(
f"[STOP LOSS] Value=${current_value:.2f} "
f"vs Entry=${self._straddle_entry_cost:.2f}"
)
self._liquidate_options()
return
# Traditional profit target
if current_value >= self._straddle_entry_cost * self._profit_target_multiple:
self.log(
f"[PROFIT TARGET] Value=${current_value:.2f} "
f"vs Entry=${self._straddle_entry_cost:.2f}"
)
self._liquidate_options()
def _maybe_enter_straddle(self) -> None:
if self.is_warming_up or self._contracts:
return
# NEW: Check circuit breaker
if self._circuit_breaker_until and self.time < self._circuit_breaker_until:
return
vix_price = (
float(self.securities[self._vix].price)
if self.securities[self._vix].has_data
else 20.0
)
# VIX regime filter: avoid extremes
if vix_price < 12 or vix_price > 30:
self.log(f"[SKIP] VIX={vix_price:.2f} outside 12-30")
return
# VIX percentile filter: only enter when IV is relatively cheap
vix_percentile = self._get_vix_percentile()
if vix_percentile > 40:
self.log(f"[SKIP] VIX percentile={vix_percentile:.1f}% > 40%")
return
# Rotate underlying for diversification
self._underlying_index = (self._underlying_index + 1) % len(self._underlying_symbols)
underlying = self._underlying_symbols[self._underlying_index]
# Realized vol filter
realized_vol = self._get_realized_vol(underlying)
if realized_vol > 0 and realized_vol < vix_price * 0.7:
self.log(f"[SKIP] RVol={realized_vol:.1f}% < 70% VIX={vix_price:.1f}%")
return
# Clean up any stale positions
self._liquidate_options()
# Find options with 14-30 DTE
chain = self.option_chain(underlying)
today = self.time.date()
valid_contracts = [
x for x in chain
if 14 <= (x.id.date.date() - today).days <= 30
]
if not valid_contracts:
self.log("[SKIP] No contracts in 14-30 DTE range")
return
# Pick expiry closest to 21 DTE target
expiries = sorted(
set(x.id.date for x in valid_contracts),
key=lambda d: abs((d.date() - today).days - 21)
)
target_expiry = expiries[0]
target_expiry_date = target_expiry.date()
valid_contracts = [x for x in valid_contracts if x.id.date == target_expiry]
underlying_price = self.securities[underlying].price
if underlying_price == 0:
return
# Find ATM strike
strikes = set(x.id.strike_price for x in valid_contracts)
atm_strike = min(strikes, key=lambda s: abs(s - underlying_price))
atm_call = next(
(x for x in valid_contracts
if x.id.strike_price == atm_strike
and x.id.option_right == OptionRight.CALL),
None
)
atm_put = next(
(x for x in valid_contracts
if x.id.strike_price == atm_strike
and x.id.option_right == OptionRight.PUT),
None
)
if not atm_call or not atm_put:
return
call_symbol = self.add_option_contract(atm_call).symbol
put_symbol = self.add_option_contract(atm_put).symbol
self._contracts = [
ContractData(call_symbol, OptionRight.CALL, target_expiry, underlying),
ContractData(put_symbol, OptionRight.PUT, target_expiry, underlying),
]
self._current_underlying = underlying
self._entry_filled = False
self._straddle_entry_cost = 0.0
self._entry_date = self.time.date()
self._trailing_stop_activated = False # Reset trailing stop for new trade
call_price = self.securities[call_symbol].price
put_price = self.securities[put_symbol].price
straddle_price = call_price + put_price
if straddle_price == 0:
return
# Position sizing
portfolio_value = self.portfolio.total_portfolio_value
max_risk = portfolio_value * self._risk_per_trade
num_contracts = max(1, int(max_risk / (straddle_price * 100)))
num_contracts = min(num_contracts, 10)
self._num_contracts = num_contracts
if self.is_market_open(underlying):
self.market_order(call_symbol, num_contracts)
self.market_order(put_symbol, num_contracts)
self._total_straddles += 1
estimated_cost = straddle_price * 100 * num_contracts
dte = (target_expiry_date - today).days
ticker = underlying.value
self.log(
f"[ENTRY #{self._total_straddles}] {ticker} K={atm_strike} "
f"Price={underlying_price:.2f} VIX={vix_price:.1f} "
f"VIX%ile={vix_percentile:.0f}% "
f"RVol={realized_vol:.1f}% DTE={dte} "
f"Qty={num_contracts} Cost={estimated_cost:.0f}"
)
def _check_holding_period(self) -> None:
"""Exit after holding for max days."""
if self._entry_date and self._contracts and self._entry_filled:
days_held = (self.time.date() - self._entry_date).days
if days_held >= self._holding_days_max:
self.log(f"[HOLD EXPIRY] {days_held} days held, closing")
self._liquidate_options()
def _hedge_delta(self) -> None:
"""Calculate portfolio delta from options and hedge with underlying."""
if not self._contracts or not self._entry_filled:
return
underlying = self._current_underlying
if underlying is None:
return
total_delta = 0.0
for cd in self._contracts:
holding = self.portfolio[cd.symbol]
if not holding.invested:
continue
chain = self.current_slice.option_chains.get(cd.symbol.canonical)
if chain is None:
continue
contract = chain.contracts.get(cd.symbol)
if contract is None:
continue
total_delta += contract.greeks.delta * holding.quantity * 100
self._portfolio_delta = total_delta
if abs(total_delta) < self._hedge_threshold:
return
target_shares = -round(total_delta)
current_shares = int(self.portfolio[underlying].quantity)
shares_trade = target_shares - current_shares
if shares_trade != 0 and self.is_market_open(underlying):
ticket = self.market_order(underlying, shares_trade, tag="Delta hedge")
self._hedge_order_ids.add(ticket.order_id)
self._total_hedges += 1
self.log(
f"[HEDGE #{self._total_hedges}] Delta={total_delta:.1f} "
f"Traded {shares_trade:+d} {underlying.value}"
)
def on_order_event(self, order_event: OrderEvent) -> None:
"""Handle assignment by liquidating delivered shares."""
if (order_event.symbol.security_type == SecurityType.EQUITY
and order_event.status == OrderStatus.FILLED
and order_event.order_id not in self._hedge_order_ids):
self.liquidate(order_event.symbol, tag="Assignment liquidation")
def _liquidate_options(self) -> None:
"""Closes options and zeroes out the underlying hedge."""
for cd in self._contracts:
if self.portfolio[cd.symbol].invested:
self.liquidate(cd.symbol)
for underlying in self._underlying_symbols:
if self.portfolio[underlying].invested:
self.liquidate(underlying)
self._contracts = []
self._portfolio_delta = 0.0
self._straddle_entry_cost = 0.0
self._entry_filled = False
self._entry_date = None
self._current_underlying = None
def on_end_of_algorithm(self) -> None:
final = self.portfolio.total_portfolio_value
self.log(
f"\n{'='*50}\n"
f" Final Value : ${final:,.2f}\n"
f" Total Return : {(final - 100000) / 100000 * 100:+.2f}%\n"
f" Straddles : {self._total_straddles}\n"
f" Hedges : {self._total_hedges}\n"
f"{'='*50}"
)