Overall Statistics
Total Orders
173
Average Win
1.19%
Average Loss
-1.54%
Compounding Annual Return
-3.662%
Drawdown
24.000%
Expectancy
-0.134
Start Equity
1000000
End Equity
829748
Net Profit
-17.025%
Sharpe Ratio
-0.895
Sortino Ratio
-0.955
Probabilistic Sharpe Ratio
0.012%
Loss Rate
51%
Win Rate
49%
Profit-Loss Ratio
0.77
Alpha
-0.03
Beta
-0.416
Annual Standard Deviation
0.067
Annual Variance
0.004
Information Ratio
-0.645
Tracking Error
0.203
Treynor Ratio
0.144
Total Fees
$0.00
Estimated Strategy Capacity
$21000.00
Lowest Capacity Asset
SPY YYM6IF2SCCH2|SPY R735QTJ8XC9X
Portfolio Turnover
0.18%
Drawdown Recovery
419
# region imports
from AlgorithmImports import *
import numpy as np
from datetime import timedelta
# endregion

class AssenagonDispersionAlgorithm(QCAlgorithm):

    def Initialize(self):
        # 1. Strategy Timelines
        self.SetStartDate(2021, 1, 1) 
        self.SetEndDate(2026, 1, 1)
        self.SetCash(1000000)

        # 2. WarmUp and Universe Settings
        self.SetWarmUp(30, Resolution.Daily)
        self.UniverseSettings.Resolution = Resolution.Minute
        self.AddUniverse(self.FundamentalSelectionFunction)
        
        # 3. Index Setup + Assign Price Model for Greeks
        self.index_symbol = self.AddEquity("SPY", Resolution.Minute).Symbol
        self.spy_option = self.AddOption("SPY", Resolution.Minute)
        self.spy_option.PriceModel = OptionPriceModels.BlackScholes()
        self.spy_option.SetFilter(-2, 2, 25, 45) 
        
        # 4. Strategy State
        self.basket_symbols = []
        self.target_vega = 1000  
        self.next_rebalance = self.Time
        self.entry_prices = {}   

        # 5. Monitoring Charts
        # These will appear in the "Custom Charts" tab of your backtest
        plot = Chart("Portfolio Risk")
        plot.AddSeries(Series("Net Delta", SeriesType.Line, 0))
        plot.AddSeries(Series("Net Vega", SeriesType.Line, 1))
        self.AddChart(plot)

        # 6. Daily Delta Hedge (At the Close)
        self.Schedule.On(self.DateRules.EveryDay(self.index_symbol), 
                         self.TimeRules.BeforeMarketClose(self.index_symbol, 10), 
                         self.DeltaHedge)

        self.set_brokerage_model(BrokerageName.ALPACA)

    def FundamentalSelectionFunction(self, fundamental):
        if self.Time < self.next_rebalance and len(self.basket_symbols) > 0:
            return self.basket_symbols
        # Financials (103), Energy (309), Not Tech (311)
        filtered = [x for x in fundamental if x.HasFundamentalData and x.Price > 15 
                    and x.AssetClassification.MorningstarSectorCode in [103, 309]
                    and x.AssetClassification.MorningstarSectorCode != 311
                    and x.ValuationRatios.PERatio > 0]
        sorted_by_value = sorted(filtered, key=lambda x: 1 / x.ValuationRatios.PERatio, reverse=True)
        self.basket_symbols = [x.Symbol for x in sorted_by_value[:10]]
        return self.basket_symbols

    def OnData(self, slice):
        if self.IsWarmingUp: return
        self.ApplyCappedRiskManagement()
        
        # Update Risk Logs every hour (optional, but good for monitoring)
        if self.Time.minute == 0:
            self.LogPortfolioRisk()

        if self.Time < self.next_rebalance: return
        
        index_chain = slice.OptionChains.get(self.spy_option.Symbol)
        if not index_chain or not self.basket_symbols: return

        if self.Portfolio.Invested:
            self.Liquidate()
            self.entry_prices.clear()

        # --- Entry ---
        expiry = self.GetTargetExpiry(index_chain)
        if not expiry: return
        
        # Short Index Leg
        contracts = [o for o in index_chain if o.Expiry == expiry]
        atm_index = sorted(contracts, key=lambda x: abs(x.Strike - index_chain.Underlying.Price))[0]
        
        if atm_index.Greeks.Vega > 0:
            index_qty = self.target_vega / (atm_index.Greeks.Vega * 100)
            self.Sell(atm_index.Symbol, index_qty)

        # Buy Basket Legs
        vega_per_stock = self.target_vega / len(self.basket_symbols)
        for symbol in self.basket_symbols:
            if not self.Securities.ContainsKey(symbol):
                opt = self.AddOption(symbol, Resolution.Minute)
                opt.PriceModel = OptionPriceModels.BlackScholes()
                opt.SetFilter(-2, 2, 25, 45)
            
            chain = slice.OptionChains.get(symbol)
            if chain:
                stock_contracts = [o for o in chain if o.Expiry == expiry]
                if not stock_contracts: continue
                atm_stock = sorted(stock_contracts, key=lambda x: abs(x.Strike - chain.Underlying.Price))[0]
                if atm_stock.Greeks.Vega > 0:
                    stock_qty = vega_per_stock / (atm_stock.Greeks.Vega * 100)
                    self.Buy(atm_stock.Symbol, stock_qty)
                    self.entry_prices[atm_stock.Symbol] = atm_stock.LastPrice

        self.next_rebalance = self.Time + timedelta(days=21)

    def LogPortfolioRisk(self):
        """Calculates Net Delta and Net Vega to confirm neutrality."""
        net_delta = 0
        net_vega = 0
        
        for symbol, holdings in self.Portfolio.items():
            if not holdings.Invested: continue
            
            if symbol.SecurityType == SecurityType.Option:
                security = self.Securities[symbol]
                if hasattr(security, "Greeks"):
                    # Option Delta (x100 multiplier)
                    if security.Greeks.Delta:
                        net_delta += security.Greeks.Delta * holdings.Quantity * 100
                    # Option Vega (x100 multiplier)
                    if security.Greeks.Vega:
                        net_vega += security.Greeks.Vega * holdings.Quantity * 100
            
            elif symbol.SecurityType == SecurityType.Equity:
                # Stock Delta (1 share = 1 delta)
                net_delta += holdings.Quantity

        self.Plot("Portfolio Risk", "Net Delta", net_delta)
        self.Plot("Portfolio Risk", "Net Vega", net_vega)

    def ApplyCappedRiskManagement(self):
        for symbol, entry_p in list(self.entry_prices.items()):
            if self.Portfolio[symbol].Invested:
                if self.Securities[symbol].Price >= (entry_p * 2.5):
                    self.Liquidate(symbol)
                    self.Liquidate(symbol.Underlying)
                    del self.entry_prices[symbol]

    def GetTargetExpiry(self, chain):
        expiries = sorted(list(set([x.Expiry for x in chain])))
        for e in expiries:
            if 25 <= (e - self.Time).days <= 45: return e
        return None

    def DeltaHedge(self):
        """Zeroes out Delta daily and logs the risk result."""
        if self.IsWarmingUp: return
        for opt_symbol in list(self.Portfolio.Keys):
            if opt_symbol.SecurityType == SecurityType.Option:
                holdings = self.Portfolio[opt_symbol]
                if holdings.Quantity == 0: continue
                security = self.Securities[opt_symbol]
                if hasattr(security, "Greeks") and security.Greeks.Delta is not None:
                    target_shares = round(-(security.Greeks.Delta * holdings.Quantity * 100))
                    current_shares = self.Portfolio[opt_symbol.Underlying].Quantity
                    diff = target_shares - current_shares
                    if diff != 0:
                        self.MarketOrder(opt_symbol.Underlying, diff)
        
        self.LogPortfolioRisk()