Overall Statistics
from AlgorithmImports import *

class LongGammaStrategy(QCAlgorithm):
    def initialize(self) -> None:
        self.set_start_date(2022, 1, 1)
        self.set_end_date(2024, 9, 30)
        self.set_cash(100000)

        self.contracts = []
        self.portfolio_delta = 0
        self.hedge_threshold = 25  # hedge if delta exceeds 25 shares
        self.straddle_entry_value = 0
        self.total_straddles = 0
        self.total_hedges = 0

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

        # Enter new straddle every Monday
        self.schedule.on(
            self.date_rules.every(DayOfWeek.MONDAY),
            self.time_rules.after_market_open(self.spy, 30),
            self.enter_straddle
        )
        # Delta hedge once per day near close
        self.schedule.on(
            self.date_rules.every_day(self.spy),
            self.time_rules.before_market_close(self.spy, 15),
            self.hedge_delta
        )

    # ──────────────────────────────────────────────────────────────────────────
    def enter_straddle(self) -> None:
        if self.is_warming_up:
            return

        # Close existing positions
        for contract in self.contracts:
            if self.portfolio.contains_key(contract) and self.portfolio[contract].invested:
                self.liquidate(contract)
        if self.portfolio.contains_key(self.spy) and self.portfolio[self.spy].invested:
            self.liquidate(self.spy)
        self.contracts = []

        # Get this week's option chain
        chain = self.option_chain(self.spy)
        week_end = Expiry.end_of_week(self.time)
        self.contracts = [
            self.add_option_contract(x).symbol for x in chain
            if x.id.date < week_end
        ]

        spy_price = self.securities[self.spy].price
        if spy_price == 0 or not self.contracts:
            return

        # Find ATM strike
        atm_strike = min(
            set(self.securities[c].symbol.id.strike_price for c in self.contracts),
            key=lambda s: abs(s - spy_price)
        )

        atm_call = next((c for c in self.contracts
            if self.securities[c].symbol.id.strike_price == atm_strike
            and self.securities[c].symbol.id.option_right == OptionRight.CALL), None)

        atm_put = next((c for c in self.contracts
            if self.securities[c].symbol.id.strike_price == atm_strike
            and self.securities[c].symbol.id.option_right == OptionRight.PUT), None)

        if not atm_call or not atm_put:
            return

        self.market_order(atm_call, 1)
        self.market_order(atm_put, 1)
        self.total_straddles += 1

        cost = (self.securities[atm_call].price + self.securities[atm_put].price) * 100
        self.straddle_entry_value = cost
        self.log(f"[ENTRY #{self.total_straddles}] Strike={atm_strike} | SPY=${spy_price:.2f} | Cost=${cost:.2f}")

    # ──────────────────────────────────────────────────────────────────────────
    def hedge_delta(self) -> None:
        if self.is_warming_up:
            return

        total_delta = 0
        for contract in self.contracts:
            if not self.portfolio.contains_key(contract):
                continue
            if not self.portfolio[contract].invested:
                continue
            sec = self.securities[contract]
            if sec.price > 0 and hasattr(sec, 'option_greeks') and sec.option_greeks:
                total_delta += sec.option_greeks.delta * self.portfolio[contract].quantity * 100

        self.portfolio_delta = total_delta

        if abs(total_delta) < self.hedge_threshold:
            return

        target_spy   = -round(total_delta)
        current_spy  = self.portfolio[self.spy].quantity if self.portfolio.contains_key(self.spy) else 0
        shares_trade = target_spy - current_spy

        if shares_trade != 0:
            self.market_order(self.spy, shares_trade)
            self.total_hedges += 1
            self.log(f"[HEDGE #{self.total_hedges}] Delta={total_delta:.1f} | Traded {shares_trade:+d} SPY")

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