Overall Statistics
Total Orders
726
Average Win
0.27%
Average Loss
-0.09%
Compounding Annual Return
-1.889%
Drawdown
4.700%
Expectancy
0.178
Start Equity
100000
End Equity
95963.25
Net Profit
-4.037%
Sharpe Ratio
-4.446
Sortino Ratio
-3.044
Probabilistic Sharpe Ratio
0.055%
Loss Rate
72%
Win Rate
28%
Profit-Loss Ratio
3.15
Alpha
-0.068
Beta
0.001
Annual Standard Deviation
0.015
Annual Variance
0
Information Ratio
-1.166
Tracking Error
0.134
Treynor Ratio
-73.9
Total Fees
$598.00
Estimated Strategy Capacity
$850000000.00
Lowest Capacity Asset
SPY 32YYHRZRITRNQ|SPY R735QTJ8XC9X
Portfolio Turnover
20.56%
Drawdown Recovery
14
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 = 25  # Increased from 10 to reduce trades
        self._straddle_entry_cost = 0.0
        self._total_straddles = 0
        self._total_hedges = 0
        self._hedge_order_ids: set[int] = set()
        
        # Profit target: close straddle if it reaches this multiple of entry cost
        self._profit_target_multiple = 1.5

        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 only 2x per day to reduce transaction costs
        self.schedule.on(
            self.date_rules.every_day(self._spy),
            self.time_rules.after_market_open(self._spy, 90),  # 10:30 AM
            self._hedge_delta
        )
        self.schedule.on(
            self.date_rules.every_day(self._spy),
            self.time_rules.before_market_close(self._spy, 90),  # 2:30 PM
            self._hedge_delta
        )

        # Check for profit targets and close on Friday
        self.schedule.on(
            self.date_rules.every(DayOfWeek.FRIDAY),
            self.time_rules.before_market_close(self._spy, 15),
            self._liquidate_options
        )

    def on_data(self, data: Slice) -> None:
        """Check profit targets on each data update."""
        if self.is_warming_up or not self._contracts:
            return
        
        # Check if we've hit profit target
        self._check_profit_target()

    def _check_profit_target(self) -> None:
        """Close straddle if it reaches profit target."""
        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 >= 1.5x entry cost, take profit
        if current_value >= self._straddle_entry_cost * self._profit_target_multiple:
            self.log(f"[PROFIT TARGET] Current=${current_value:.2f} 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 VIX < 20 (options are relatively cheap)
        vix_price = self.securities[self._vix].price
        if vix_price > 20:
            self.log(f"[SKIP ENTRY] VIX={vix_price:.2f} > 20, options too expensive")
            return

        self._liquidate_options()

        # Get this week's option chain
        chain = self.option_chain(self._spy)
        # Calculate end of this week (next Friday)
        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),
        ]

        # Use limit orders to reduce slippage
        call_price = self.securities[call_symbol].price
        put_price = self.securities[put_symbol].price
        
        self.limit_order(call_symbol, 1, call_price * 1.02)  # 2% above ask
        self.limit_order(put_symbol, 1, put_price * 1.02)
        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

        # Check if the delta breach exceeds our threshold
        if abs(total_delta) < self._hedge_threshold:
            return

        # Calculate how many underlying shares to buy/sell to neutralize
        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}"
        )