Overall Statistics
Total Orders
9802
Average Win
0.09%
Average Loss
-0.09%
Compounding Annual Return
11.144%
Drawdown
3.700%
Expectancy
0.137
Start Equity
1000000
End Equity
1695339.83
Net Profit
69.534%
Sharpe Ratio
1.601
Sortino Ratio
1.295
Probabilistic Sharpe Ratio
100.000%
Loss Rate
45%
Win Rate
55%
Profit-Loss Ratio
1.08
Alpha
0.035
Beta
0.034
Annual Standard Deviation
0.023
Annual Variance
0.001
Information Ratio
-0.129
Tracking Error
0.138
Treynor Ratio
1.106
Total Fees
$14724.35
Estimated Strategy Capacity
$42000000.00
Lowest Capacity Asset
SPY Z0C7OB8K08TI|SPY R735QTJ8XC9X
Portfolio Turnover
21.41%
Drawdown Recovery
144
from AlgorithmImports import *


class HedgeAction:
    NONE = 0
    HEDGE = 1
    ROLL = 2


class DeltaHedger:

    def __init__(self, min_shares=6, rehedge_band=3, hedge_delay=timedelta(minutes=60), roll_threshold=None):
        self._min_shares = min_shares
        self._rehedge_band = rehedge_band
        self._hedge_delay = hedge_delay
        self._roll_threshold = roll_threshold
        self.reset()

    def reset(self):
        self._next_hedge_time = datetime.min
        self._last_hedged_delta = None

    def is_ready(self, current_time):
        return current_time >= self._next_hedge_time

    def compute_net_delta(self, chain, legs, equity_quantity):
        net_delta = float(equity_quantity)
        for leg in legs:
            contract = chain.contracts.get(leg.symbol)
            if contract is None or contract.greeks is None:
                return None
            net_delta += float(contract.greeks.delta) * leg.holdings.quantity * 100.0
        return net_delta

    def evaluate(self, net_delta):
        if self._roll_threshold and abs(net_delta) >= self._roll_threshold:
            return HedgeAction.ROLL
        if (self._last_hedged_delta is not None
                and abs(net_delta - self._last_hedged_delta) < self._rehedge_band
                and abs(net_delta) < self._min_shares + self._rehedge_band):
            return HedgeAction.NONE
        if abs(int(round(-net_delta))) < self._min_shares:
            return HedgeAction.NONE
        return HedgeAction.HEDGE

    def record_hedge(self, current_time, net_delta, cooldown=None):
        self._last_hedged_delta = net_delta
        self._next_hedge_time = current_time + (cooldown or self._hedge_delay)
from AlgorithmImports import *
from straddle_select import StraddleSelector
from delta_hedge import HedgeAction, DeltaHedger


class DeltaHedgedStraddleAlgo(QCAlgorithm):

    def initialize(self):
        self.set_start_date(self.end_date - timedelta(5*365))
        self.set_cash(1_000_000)
        self.settings.seed_initial_prices = True
        self._spy = self.add_equity("SPY", Resolution.HOUR, data_normalization_mode=DataNormalizationMode.RAW)
        self._straddle = StraddleSelector(min_dte=7, max_dte=30)
        self._hedger = DeltaHedger(min_shares=6, rehedge_band=3, hedge_delay=timedelta(minutes=60), roll_threshold=None)
        self._canonical_option = None
        self._straddle_weight = 1.0
        self._legs = []
        self._exit_day = None
        self._reentry_time = None
        self.schedule.on(
            self.date_rules.every_day(self._spy),
            self.time_rules.after_market_open(self._spy, 30),
            self._rebalance
        )
        self.schedule.on(
            self.date_rules.every_day(self._spy),
            self.time_rules.before_market_close(self._spy, 60),
            self._close_expiring
        )
        self.schedule.on(
            self.date_rules.every_day(self._spy),
            self.time_rules.before_market_close(self._spy, 60),
            self._eod_hedge
        )
        self.set_warm_up(timedelta(45))

    def _rebalance(self):
        chain = self.option_chain(self._spy.symbol, flatten=True)
        result = self._straddle.select(chain, self._spy.price, self.time.date()) if chain else None
        if (self.is_warming_up or self._active_legs() or self.transactions.get_open_orders() or self._spy.price <= 0 or not chain or not result):
            return
        call_symbol, put_symbol, expiry = result
        self._legs = [self.add_option_contract(s) for s in (call_symbol, put_symbol)]
        self._canonical_option = call_symbol.canonical
        self.combo_market_order(
            [Leg.create(leg.symbol, -1) for leg in self._legs],
            max(1, int(self.portfolio.total_portfolio_value * self._straddle_weight / (self._spy.price * 100.0))), 
            tag=f"Sell ATM straddle exp {expiry.date()}"
        )
        self._hedger.reset()
        exit_day = expiry.date()
        if not self._spy.exchange.hours.is_open(datetime(exit_day.year, exit_day.month, exit_day.day, 12, 0, 0), False):
            exit_day = self._spy.exchange.hours.get_previous_trading_day(exit_day)
        self._exit_day = exit_day

    def _close_expiring(self):
        legs = self._active_legs()
        if not legs or not self._exit_day or self.time.date() < self._exit_day:
            return
        self._legs, self._exit_day = [], None
        for leg in legs:
            self.liquidate(leg, tag="Expiry liquidation")
        if self._spy.holdings.invested:
            self.liquidate(self._spy, tag="Expiry hedge liquidation")

    def on_data(self, slice_):
        if self._reentry_time is not None:
            if self.transactions.get_open_orders() or self._active_legs():
                return
            self._reentry_time = None
            self._rebalance()
            return
        legs = self._active_legs()
        hedger = self._hedger
        chain = slice_.option_chains.get(self._canonical_option) if self._canonical_option else None
        net_delta = hedger.compute_net_delta(chain, legs, self._spy.holdings.quantity) if chain else None
        if (not legs or not self._spy.exchange.hours.is_open(self.time, False) or not hedger.is_ready(self.time) or self.transactions.get_open_orders(self._spy.symbol) or not chain or net_delta is None):
            return
        action = hedger.evaluate(net_delta)
        if action == HedgeAction.ROLL:
            for leg in legs:
                self.liquidate(leg, tag=f"Delta roll: net_delta={net_delta:.2f}")
            if self._spy.holdings.invested:
                self.liquidate(self._spy, tag=f"Delta roll hedge: net_delta={net_delta:.2f}")
            self._legs, self._exit_day = [], None
            self._reentry_time = self.time
            hedger.reset()
            return
        self.plot("Portfolio Delta", "Delta", net_delta)
        if action == HedgeAction.HEDGE:
            hedge_qty = int(round(-net_delta))
            self.market_order(self._spy, hedge_qty, tag=f"Daily delta hedge ({net_delta:.2f} share delta)")
            hedger.record_hedge(self.time, net_delta)

    def _eod_hedge(self):
        legs = self._active_legs()
        slice_ = self.current_slice
        chain = slice_.option_chains.get(self._canonical_option) if slice_ and self._canonical_option else None
        net_delta = self._hedger.compute_net_delta(chain, legs, self._spy.holdings.quantity) if chain else None
        hedge_qty = int(round(-net_delta)) if net_delta is not None else 0        
        if not legs or self.transactions.get_open_orders(self._spy.symbol) or not slice_ or not chain or net_delta is None or not hedge_qty:
            return
        self.market_order(self._spy, hedge_qty, tag=f"EOD delta hedge ({net_delta:.2f} share delta)")
        self._hedger.record_hedge(self.time, 0.0, cooldown=timedelta(minutes=1))

    def _active_legs(self):
        return [leg for leg in self._legs if leg.holdings.invested]

    def on_order_event(self, order_event):
        if order_event.status != OrderStatus.FILLED:
            return
        if order_event.symbol.security_type == SecurityType.OPTION:
            matching = [l for l in self._legs if l.symbol == order_event.symbol]
            leg = matching[0] if matching else None
            if leg and not leg.holdings.invested:
                for other in self._active_legs():
                    if other.symbol != order_event.symbol:
                        self.liquidate(other, tag="Close option after assignment")
                self._legs = [l for l in self._legs if l.holdings.invested]
        if (order_event.symbol.security_type == SecurityType.EQUITY and self._spy.holdings.invested and not self._active_legs()):
            self.liquidate(self._spy, tag="Assignment liquidation")
from AlgorithmImports import *


class StraddleSelector:

    def __init__(self, min_dte=7, max_dte=30):
        self._min_dte = min_dte
        self._max_dte = max_dte

    def select(self, chain, spot_price, current_date):
        contracts = [
            row for row in chain
            if self._min_dte <= (row.id.date.date() - current_date).days <= self._max_dte
        ]
        if not contracts:
            return None
        expiry = max(row.id.date for row in contracts)
        expiry_rows = [row for row in contracts if row.id.date == expiry]
        strike = min(expiry_rows, key=lambda r: abs(r.id.strike_price - spot_price)).id.strike_price
        symbol_by_right = {
            row.id.option_right: row.symbol
            for row in expiry_rows
            if row.id.strike_price == strike
            and row.id.option_right in (OptionRight.CALL, OptionRight.PUT)
        }
        if len(symbol_by_right) < 2:
            return None
        return symbol_by_right[OptionRight.CALL], symbol_by_right[OptionRight.PUT], expiry