Overall Statistics
Total Orders
199
Average Win
6.15%
Average Loss
-1.23%
Compounding Annual Return
39.320%
Drawdown
28.200%
Expectancy
1.573
Start Equity
1000000
End Equity
2699324.02
Net Profit
169.932%
Sharpe Ratio
1.03
Sortino Ratio
1.233
Probabilistic Sharpe Ratio
48.100%
Loss Rate
57%
Win Rate
43%
Profit-Loss Ratio
5.00
Alpha
0.219
Beta
1.115
Annual Standard Deviation
0.273
Annual Variance
0.074
Information Ratio
1.03
Tracking Error
0.219
Treynor Ratio
0.252
Total Fees
$1063.00
Estimated Strategy Capacity
$4300000.00
Lowest Capacity Asset
FB V6OIPNZEM8V9
Portfolio Turnover
1.88%
from AlgorithmImports import *

class DynamicProtectivePutStrategy(QCAlgorithm):

    def Initialize(self):
        # === STRATEGY SETUP ===
        self.SetStartDate(2021, 1, 1)
        self.SetEndDate(2024, 1, 1)
        self.cash = 1000000
        self.SetCash(self.cash)

        # === STRATEGY CONFIG ===
        self.top_n_marketcap = 35 # top market caps worldwide
        self.top_n_momentum = 5   # number of stocks in the portfolio
        self.roc_period = 252  # 12-month ROC
        self.delta_put = 40    # Target delta to be mu

        #evenly distribute cash across the shares while keeping margin for the options
        self.stock_investment = self.cash / self.top_n_momentum * 0.7  

        self.active_symbols = []        # currently invested stocks
        self.stock_data = {}            # stock symbol -> {"put": option symbol}
        self.pending_hedges = {}        # stock symbol -> shares waiting for put hedge

        # === UNIVERSE SETTINGS ===
        self.UniverseSettings.Resolution = Resolution.Daily
        self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw

        self.AddUniverse(self.CoarseSelection, self.FineSelection)

        # === MONTHLY REBALANCING SCHEDULE ===
        self.Schedule.On(
            self.DateRules.MonthStart("SPY"),
            self.TimeRules.At(9, 30),
            self.Rebalance
        )

    def CoarseSelection(self, coarse):
        # Filter for liquid stocks with fundamental data
        return [x.Symbol for x in coarse if x.HasFundamentalData and x.Price > 10 and x.Volume > 100000]

    def FineSelection(self, fine):
        # Select top 35 by market cap
        sorted_by_cap = sorted(fine, key=lambda x: x.MarketCap, reverse=True)
        self.market_cap_universe = [x.Symbol for x in sorted_by_cap[:self.top_n_marketcap]]
        return self.market_cap_universe

    def Momentum(self, symbol):
        # Compute 12-month rate of change
        history = self.History(symbol, self.roc_period, Resolution.Daily)
        if history.empty or len(history["close"]) < self.roc_period:
            return -1
        prices = history["close"]
        return (prices.iloc[-1] - prices.iloc[0]) / prices.iloc[0]

    def Rebalance(self):
        # Wait for universe to be ready
        if not hasattr(self, 'market_cap_universe') or not self.market_cap_universe:
            return

        # === SELECT TOP 5 MOMENTUM STOCKS ===
        ranked = sorted(
            self.market_cap_universe,
            key=lambda sym: self.Momentum(sym),
            reverse=True
        )
        new_top = ranked[:self.top_n_momentum]

        # === REMOVE OLD POSITIONS ===
        removed = [s for s in self.active_symbols if s not in new_top]
        for symbol in removed:
            self.Liquidate(symbol)
            if self.stock_data.get(symbol, {}).get("put"):
                self.Liquidate(self.stock_data[symbol]["put"])
                self.stock_data[symbol]["put"] = None

        self.active_symbols = new_top

        # === ENTER POSITIONS & SCHEDULE HEDGES ===
        for symbol in self.active_symbols:
            if symbol not in self.stock_data:
                self.stock_data[symbol] = {"put": None}

            price = self.Securities[symbol].Price
            if price == 0:
                continue

            # Buy ~$50K of stock in 100-share lots
            raw_qty = int(self.stock_investment / price)
            shares = int(round(raw_qty / 100.0)) * 100
            if shares < 100:
                self.Debug(f"Skipping {symbol.Value}: too expensive for 100 shares.")
                continue

            self.MarketOrder(symbol, shares)
            self.pending_hedges[symbol] = shares  # flag for delta-based put selection in OnData

    def OnData(self, data):
        if not self.pending_hedges:
            return

        for symbol, shares in list(self.pending_hedges.items()):
            if symbol not in data.OptionChains:
                continue

            chain = data.OptionChains[symbol]

            # Filter puts expiring in ~30-36 days
            puts = [x for x in chain if x.Right == OptionRight.Put and 29 <= (x.Expiry - self.Time).days <= 36]
            puts = [x for x in puts if x.Greeks and x.Greeks.Delta is not None]
            if not puts:
                continue

            # Select put with delta closest to -0.35
            target_delta = -self.delta_put / 100
            selected = sorted(puts, key=lambda x: abs(x.Greeks.Delta - target_delta))[0]

            if not selected:
                continue

            option_symbol = selected.Symbol

            if option_symbol not in self.Securities:
                self.AddOptionContract(option_symbol, Resolution.Daily)

            # Liquidate previous hedge if needed
            if self.stock_data[symbol]["put"] is not None:
                self.Liquidate(self.stock_data[symbol]["put"])

            contracts_to_buy = shares // 100
            self.Buy(option_symbol, contracts_to_buy)
            self.stock_data[symbol]["put"] = option_symbol

            # Mark as hedged
            del self.pending_hedges[symbol]