Overall Statistics
Total Orders
616
Average Win
0.90%
Average Loss
-0.26%
Compounding Annual Return
-3.177%
Drawdown
21.600%
Expectancy
0.755
Start Equity
100000
End Equity
91504.5
Net Profit
-8.496%
Sharpe Ratio
-0.999
Sortino Ratio
-1.242
Probabilistic Sharpe Ratio
0.398%
Loss Rate
61%
Win Rate
39%
Profit-Loss Ratio
3.49
Alpha
-0.061
Beta
0.017
Annual Standard Deviation
0.061
Annual Variance
0.004
Information Ratio
-0.578
Tracking Error
0.158
Treynor Ratio
-3.644
Total Fees
$380.00
Estimated Strategy Capacity
$80000000.00
Lowest Capacity Asset
SPY 32KZCCA3SV3TY|SPY R735QTJ8XC9X
Portfolio Turnover
12.10%
Drawdown Recovery
0
from AlgorithmImports import *
from datetime import timedelta

class OptimizedLongGammaStrategy(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  # Shares of delta required to trigger a hedge
        self.straddle_entry_value = 0
        self.total_straddles = 0
        self.total_hedges = 0
        self.next_hedge_time = self.time

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

        # 1. 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
        )
        
        # 2. Close straddle on Friday afternoon to avoid weekend theta & expiration
        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:
        if self.is_warming_up or not self.contracts:
            return
            
        # 3. Intraday Gamma Scalping: Check delta every 30 minutes
        if self.time >= self.next_hedge_time:
            self.hedge_delta()
            self.next_hedge_time = self.time + timedelta(minutes=30)

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

        self.liquidate_options()

        # Get this week's option chain
        chain = self.option_chain(self.spy)
        week_end = Expiry.end_of_week(self.time)
        
        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
        atm_strike = min(
            set(x.id.strike_price for x in valid_contracts),
            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

        # 4. Add contracts and explicitly set the pricing model for accurate Greeks
        for contract in [atm_call, atm_put]:
            symbol = self.add_option_contract(contract).symbol
            self.securities[symbol].set_price_model(OptionPriceModels.BlackScholes())
            self.contracts.append(symbol)

        self.market_order(self.contracts[0], 1)
        self.market_order(self.contracts[1], 1)
        self.total_straddles += 1

        cost = (self.securities[self.contracts[0]].price + self.securities[self.contracts[1]].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:
        total_delta = 0
        
        for contract in self.contracts:
            if not self.portfolio.contains_key(contract) or not self.portfolio[contract].invested:
                continue
                
            sec = self.securities[contract]



            if sec.price > 0:
                # Try to access greeks through different attributes
                greeks = None
                if hasattr(sec, 'option_greeks') and sec.option_greeks:
                    greeks = sec.option_greeks
                elif hasattr(sec, 'greeks') and sec.greeks:
                    greeks = sec.greeks

                if greeks and hasattr(greeks, 'delta'):
                    total_delta += greeks.delta * self.portfolio[contract].quantity * 100

            if sec.price > 0:
                # Try to access greeks through different attributes
                greeks = None
                if hasattr(sec, 'option_greeks') and sec.option_greeks:
                    greeks = sec.option_greeks
                elif hasattr(sec, 'greeks') and sec.greeks:
                    greeks = sec.greeks

                if greeks and hasattr(greeks, 'delta'):
                    total_delta += greeks.delta * self.portfolio[contract].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 we need to buy/sell to neutralize
        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 liquidate_options(self) -> None:
        """Closes options and zeroes out the underlying hedge."""
        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 = []
        self.portfolio_delta = 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}"
        )