Overall Statistics
Total Orders
510
Average Win
0.36%
Average Loss
-0.15%
Compounding Annual Return
-2.257%
Drawdown
6.200%
Expectancy
0.593
Start Equity
100000
End Equity
95188
Net Profit
-4.812%
Sharpe Ratio
-3.408
Sortino Ratio
-3.039
Probabilistic Sharpe Ratio
0.213%
Loss Rate
54%
Win Rate
46%
Profit-Loss Ratio
2.43
Alpha
-0.07
Beta
-0.01
Annual Standard Deviation
0.021
Annual Variance
0
Information Ratio
-1.166
Tracking Error
0.136
Treynor Ratio
7.03
Total Fees
$306.00
Estimated Strategy Capacity
$300000000.00
Lowest Capacity Asset
SPY 32YYHRZRITRNQ|SPY R735QTJ8XC9X
Portfolio Turnover
15.56%
Drawdown Recovery
97
from AlgorithmImports import *
from datetime import timedelta


class ContractData:
    """DTO to hold option contract references."""
    def __init__(self, symbol: Symbol, right: OptionRight):
        self.symbol = symbol
        self.right = right


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 = 30  # Higher threshold = fewer hedges
        self._straddle_entry_cost = 0.0
        self._total_straddles = 0
        self._total_hedges = 0
        self._hedge_order_ids: set[int] = set()

        # Risk management
        self._profit_target_multiple = 1.4   # Close at 40% profit
        self._stop_loss_multiple = 0.5       # Close at 50% loss
        self._vix_threshold = 30             # VIX filter (raised from 20)

        self.settings.seed_initial_prices = True
        self._spy = self.add_equity(
            "SPY", data_normalization_mode=DataNormalizationMode.RAW
        ).symbol

        # Add VIX for volatility filtering
        self._vix = self.add_equity(
            "VIX", data_normalization_mode=DataNormalizationMode.RAW
        ).symbol

        # Enter new straddle every Monday morning
        self.schedule.on(
            self.date_rules.every(DayOfWeek.MONDAY),
            self.time_rules.after_market_open(self._spy, 30),
            self._enter_straddle
        )

        # Hedge delta once per day at midday to minimize transaction costs
        self.schedule.on(
            self.date_rules.every_day(self._spy),
            self.time_rules.after_market_open(self._spy, 120),  # ~11:30 AM
            self._hedge_delta
        )

        # Close straddle on Friday afternoon
        self.schedule.on(
            self.date_rules.every(DayOfWeek.FRIDAY),
            self.time_rules.before_market_close(self._spy, 30),
            self._liquidate_options
        )

    def on_data(self, data: Slice) -> None:
        """Check profit/loss targets on each data update."""
        if self.is_warming_up or not self._contracts:
            return
        self._check_risk_limits()

    def _check_risk_limits(self) -> None:
        """Close straddle if profit target or stop loss is hit."""
        if not self._contracts or self._straddle_entry_cost == 0:
            return

        current_value = 0.0
        for cd in self._contracts:
            holding = self.portfolio[cd.symbol]
            if holding.invested:
                current_value += holding.holdings_value

        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()
        elif 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()

    def _enter_straddle(self) -> None:
        if self.is_warming_up:
            return

        # VIX filter: only enter when implied vol is reasonable
        vix_price = self.securities[self._vix].price
        if vix_price > self._vix_threshold:
            self.log(f"[SKIP ENTRY] VIX={vix_price:.2f} > {self._vix_threshold}")
            return

        self._liquidate_options()

        # Get this week's option chain
        chain = self.option_chain(self._spy)
        days_until_friday = (4 - self.time.weekday()) % 7
        if days_until_friday == 0:
            days_until_friday = 7
        week_end = self.time + timedelta(days=days_until_friday)

        valid_contracts = [x for x in chain if x.id.date < week_end]
        if not valid_contracts:
            return

        spy_price = self.securities[self._spy].price
        if spy_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 - spy_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),
            ContractData(put_symbol, OptionRight.PUT),
        ]

        call_price = self.securities[call_symbol].price
        put_price = self.securities[put_symbol].price

        self.limit_order(call_symbol, 1, call_price * 1.01)
        self.limit_order(put_symbol, 1, put_price * 1.01)
        self._total_straddles += 1

        cost = (call_price + put_price) * 100
        self._straddle_entry_cost = cost
        self.log(
            f"[ENTRY #{self._total_straddles}] Strike={atm_strike} "
            f"| SPY=${spy_price:.2f} | VIX={vix_price:.2f} | Cost=${cost:.2f}"
        )

    def _hedge_delta(self) -> None:
        """Calculate portfolio delta from options and hedge with underlying."""
        if not self._contracts:
            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_spy = -round(total_delta)
        current_spy = int(self.portfolio[self._spy].quantity)
        shares_trade = target_spy - current_spy

        if shares_trade != 0 and self.is_market_open(self._spy):
            ticket = self.market_order(self._spy, 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} SPY"
            )

    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(self._spy, 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)

        if self.portfolio[self._spy].invested:
            self.liquidate(self._spy)

        self._contracts = []
        self._portfolio_delta = 0.0
        self._straddle_entry_cost = 0.0

    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}"
        )