Overall Statistics
Total Orders
1982
Average Win
0.14%
Average Loss
-0.11%
Compounding Annual Return
2.112%
Drawdown
13.000%
Expectancy
0.011
Start Equity
300000
End Equity
332858.5
Net Profit
10.953%
Sharpe Ratio
-0.201
Sortino Ratio
-0.221
Probabilistic Sharpe Ratio
3.005%
Loss Rate
56%
Win Rate
44%
Profit-Loss Ratio
1.30
Alpha
-0.03
Beta
0.224
Annual Standard Deviation
0.053
Annual Variance
0.003
Information Ratio
-0.705
Tracking Error
0.14
Treynor Ratio
-0.048
Total Fees
$1968.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
QQQ RIWIV7K5Z9LX
Portfolio Turnover
0.27%
#region imports
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using System.Globalization;
    using System.Drawing;
    using QuantConnect;
    using QuantConnect.Algorithm.Framework;
    using QuantConnect.Algorithm.Framework.Selection;
    using QuantConnect.Algorithm.Framework.Alphas;
    using QuantConnect.Algorithm.Framework.Portfolio;
    using QuantConnect.Algorithm.Framework.Portfolio.SignalExports;
    using QuantConnect.Algorithm.Framework.Execution;
    using QuantConnect.Algorithm.Framework.Risk;
    using QuantConnect.Algorithm.Selection;
    using QuantConnect.Api;
    using QuantConnect.Parameters;
    using QuantConnect.Benchmarks;
    using QuantConnect.Brokerages;
    using QuantConnect.Commands;
    using QuantConnect.Configuration;
    using QuantConnect.Util;
    using QuantConnect.Interfaces;
    using QuantConnect.Algorithm;
    using QuantConnect.Indicators;
    using QuantConnect.Data;
    using QuantConnect.Data.Auxiliary;
    using QuantConnect.Data.Consolidators;
    using QuantConnect.Data.Custom;
    using QuantConnect.Data.Custom.IconicTypes;
    using QuantConnect.DataSource;
    using QuantConnect.Data.Fundamental;
    using QuantConnect.Data.Market;
    using QuantConnect.Data.Shortable;
    using QuantConnect.Data.UniverseSelection;
    using QuantConnect.Notifications;
    using QuantConnect.Orders;
    using QuantConnect.Orders.Fees;
    using QuantConnect.Orders.Fills;
    using QuantConnect.Orders.OptionExercise;
    using QuantConnect.Orders.Slippage;
    using QuantConnect.Orders.TimeInForces;
    using QuantConnect.Python;
    using QuantConnect.Scheduling;
    using QuantConnect.Securities;
    using QuantConnect.Securities.Equity;
    using QuantConnect.Securities.Future;
    using QuantConnect.Securities.Option;
    using QuantConnect.Securities.Positions;
    using QuantConnect.Securities.Forex;
    using QuantConnect.Securities.Crypto;
    using QuantConnect.Securities.CryptoFuture;
    using QuantConnect.Securities.IndexOption;
    using QuantConnect.Securities.Interfaces;
    using QuantConnect.Securities.Volatility;
    using QuantConnect.Storage;
    using QuantConnect.Statistics;
    using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
    using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
    using Calendar = QuantConnect.Data.Consolidators.Calendar;
#endregion
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Indicators;
using QuantConnect.Orders;
using System;
using System.Collections.Generic;
using System.Linq;
using Google.OrTools.LinearSolver;
using Newtonsoft.Json.Serialization;

namespace QuantConnect.Algorithm.CSharp
{
    public class SummrFlagshipOptionsSpreadModel : QCAlgorithm
    {
        private bool _boolDoSpreads = false;
        private bool _boolDoBonds = true;
        private Dictionary<string, RelativeStrengthIndex> _rsiIndicators;
        private Dictionary<string, RelativeStrengthIndex> _rsiIndicators_Old;
        private Dictionary<string, decimal> _highestPrices;  // Track highest price for trailing stop
 
        private int _bias = 0, _prevBias = 0;                               // direction for strategy determined by RSI's
        private List<string> _symbols;
        private List<string> _optionsSymbols;
        private Symbol _symbVIX;
        private Symbol _symbSPY, _symbLQD, _symbHYG;
        private Symbol _symbQQQ;
        private Symbol _symbRSP;
        private Symbol _symbBIL;
        private Symbol _optionSPY;
        private Symbol _optionQQQ;
        private Symbol _optionRSP;
        private const decimal _decStrikeStep = 5m;
        private string _strLeveragedEquLrgCap;
        private string _strShrtTermFixedIncome;
        private string _strIntermediateTermFixedIncome;
        private DateTime _dateLeveragedLongStart = new DateTime(2020, 2, 7);  // HIBL (start + 90) ==> UDOW
        private DateTime _dateShrtTermFixedIncomeStart = new DateTime(2021, 12, 21);  // SBND (start + 90days) ==> PCEF
        private DateTime _dateIntermediateTermFixedIncome = new DateTime(2020, 10, 14); // IBTK (start + 90days) ==> VGIT
        private decimal _capitalForOptions;
        private decimal _capitalForBonds;
        private int _numETFShares;
        private int _numOptionContracts;
        private Dictionary<Symbol, int> _optionChainRetries = new Dictionary<Symbol, int>();
        private const int MaxRetries = 3;



        public override void Initialize()
        {
            // Set backtest start and end date
            SetStartDate(2020, 1, 1);
            SetEndDate(2025, 3, 15);

            // Set starting cash
            SetCash(300000);

            _boolDoSpreads = GetParameter("DoSpreads") == "FALSE" ? false : true;
            _boolDoBonds = GetParameter("DoBonds") == "FALSE" ? false : true;

            // Add VIX data for charting
            _symbVIX = AddData<CBOE>("VIX", Resolution.Daily).Symbol;

            // Create and add a custom chart
            var dailyCloseChart = new Chart("Daily VIX Close");
            dailyCloseChart.AddSeries(new Series("Close", SeriesType.Line, 0));
            AddChart(dailyCloseChart);


            // Define symbols to trade
            _symbols = new List<string> { "SPY",  "QQQ", "BIL", "IBTK", "SHY", "SOXL", "SQQQ", "SBND", "HIBL", "TECL", "SOXS", "VGIT", "PCEF", "UDOW", "LQD", "HYG" };        //RSP
            _optionsSymbols = new List<string> {"SPY", "QQQ"};                                                                                                                  // RSP
            _strLeveragedEquLrgCap = "UDOW";
            _strShrtTermFixedIncome = "PCEF";
            _strIntermediateTermFixedIncome = "VGIT";
            // _strLeveragedEquLrgCap = "HIBL";
            // _strShrtTermFixedIncome = "SBND";
            // _strIntermediateTermFixedIncome = "IBTK";
            
            // instantiate dictionary to track highest prices for trailing stop kill
            _highestPrices = new Dictionary<string, decimal>();


            // Add equity symbols and initialize RSI indicators
            _rsiIndicators = new Dictionary<string, RelativeStrengthIndex>();
            _rsiIndicators_Old = new Dictionary<string, RelativeStrengthIndex>();
            int rsiPeriod;
                
            foreach (var symbol in _symbols)
            {
                AddEquity(symbol, Resolution.Minute);                            /// was .Daily
                if(symbol == "LQD") _symbLQD = Securities[symbol].Symbol;
                if(symbol == "HYG") _symbHYG = Securities[symbol].Symbol;
                if(symbol == "BIL") _symbBIL = Securities[symbol].Symbol;
                // Add options for trading
                if(_optionsSymbols.Contains(symbol))
                {
                    var opt = AddOption(symbol, Resolution.Minute, Market.USA, true, 0m);

					opt.PriceModel = OptionPriceModels.BjerksundStensland();		/// necessary for Greeks
                    opt.SetFilter(u=>u.WeeklysOnly().Strikes(-20, 20).Expiration(10,21));
                    
                    AddOption(symbol, Resolution.Minute);
                    switch (symbol)
                    {
                        case "SPY":
                            _symbSPY = Securities[symbol].Symbol;
                            _optionSPY = opt.Symbol;
                            break;
                        case "QQQ":
                            _symbQQQ = Securities[symbol].Symbol;
                            _optionQQQ = opt.Symbol;
                            break;
                        case "RSP":
                            _symbRSP = Securities[symbol].Symbol;
                            _optionRSP = opt.Symbol;
                            break;
                        default:
                            break;
                    }
                }

                // Set RSI periods based on symbol
                switch (symbol)
                {
                    case "SPY":
                        rsiPeriod = 6;
                        break;
                    case "BIL":
                        rsiPeriod = 5;
                        break;
                    case "IBTK":
                        rsiPeriod = 7;
                        break;
                    case "VGIT":
                        rsiPeriod = 7;
                        break;
                    case "SBND":
                        rsiPeriod = 10;
                        break;
                    case "PCEF":
                        rsiPeriod = 10;
                        break;
                    case "HIBL":
                        rsiPeriod = 10;
                        break;
                    case "UDOW":
                        rsiPeriod = 10;
                        break;
                    default:
                        rsiPeriod = 7; // Default period
                        break;
                }
                //_rsiIndicators[symbol] = RSI(symbol, rsiPeriod, MovingAverageType.Wilders, Resolution.Daily);
                var validSymbols = new List<string> { "SPY", "BIL", "SHY", "SOXL", "SOXS", "SQQQ", "VGIT", "PCEF", "UDOW"};
                if (validSymbols.Contains(symbol))
                {
                    _rsiIndicators_Old[symbol] = RSI(symbol, rsiPeriod, MovingAverageType.Simple, Resolution.Daily);       //MovingAverageType.Wilders
                }
                _rsiIndicators[symbol] = RSI(symbol, rsiPeriod, MovingAverageType.Simple, Resolution.Daily);
            }

            // Initialize rebalance time to 30 minutes after market open
            //_nextRebalanceTime = DateTime.MinValue;
            // Schedule rebalance 30 minutes after market open
            Schedule.On(DateRules.EveryDay(), TimeRules.AfterMarketOpen("SPY", 30), () =>
                {
                    Rebalance();
                });

            // Schedule expiration liquidation
            Schedule.On(DateRules.EveryDay(), TimeRules.At(15, 45), () => LiquidateExpiredOptions());
            // Schedule Assignment Risk Check 
            
            /*
            Schedule.On(DateRules.EveryDay(), TimeRules.AfterMarketOpen("SPY", 20), () =>
            {
                CheckForAssignmentRisk();
            });
            */
        }
        public override void OnData(Slice data)
        {
            
            /// reset the ETF Symbols when the newer ETFs commence trading:
            if (!_strLeveragedEquLrgCap.Equals("HIBL") && data.Time.CompareTo(_dateLeveragedLongStart) > 0) 
            {
                _strLeveragedEquLrgCap = "HIBL";
                Log($" *** *** *** Setting Leveraged Equity ETF to HIBL");
            }
            if (!_strIntermediateTermFixedIncome.Equals("IBTK") &&  data.Time.CompareTo(_dateIntermediateTermFixedIncome) > 0)
            {
                _strIntermediateTermFixedIncome = "IBTK";
                Log($" *** *** *** Setting Short Term Fixed Income ETF to ITBK");
            }
            if (!_strShrtTermFixedIncome.Equals("SBND") && data.Time.CompareTo(_dateShrtTermFixedIncomeStart) > 0){
                 _strShrtTermFixedIncome = "SBND";
                Log($" *** *** *** Setting Leveraged Equity ETF to SBND");
            }


            if (!_rsiIndicators_Old.All(x => x.Value.IsReady)) return;

           // check for assignment here in OnData to capture outlyer prices
           foreach (var kvp in Portfolio)
            {
                var symbol = kvp.Key;
                var security = Securities[symbol];

                // Only check for short option positions (negative quantity)
                if (security.Type != SecurityType.Option || security.Holdings.Quantity >= 0)
                    continue;

                // Get option contract details
                OptionChain thisChain = GetOptionChainWithRetry(symbol);
                if (thisChain == null)
                {
                    Log($"[ERROR] {symbol.Value} option chain could not be retrieved. Skipping CheckForAssignmentRisk.");
                    return;
                }

                OptionContract option = thisChain.Contracts.ContainsKey(symbol) ? thisChain.Contracts[symbol] : null;
                if (option == null) continue;

                // Get underlying asset price
                //var underlying = Securities[option.Underlying];
                var underlyingSymbol = option.UnderlyingSymbol;
                var underlyingSecurity = Securities[underlyingSymbol];
                decimal intrinsicValue = option.Right == OptionRight.Call
                    ? Math.Max(0, underlyingSecurity.Price - option.Strike)   // Calls: ITM if price > strike
                    : Math.Max(0, option.Strike - underlyingSecurity.Price);  // Puts: ITM if price < strike
                
                decimal extrinsicValue = option.AskPrice - intrinsicValue;

                // Assignment risk condition: Deep ITM with low extrinsic value
                if (intrinsicValue > 0 && extrinsicValue < 0.05m)  // Adjust threshold if needed
                {
                    Log($"[ASSIGNMENT RISK] At {data.Time.ToShortDateString()}, {data.Time.ToShortTimeString()} {option.Symbol.Value} at risk of early assignment! Liquidating spread.");
                    LiquidateSpreadContaining(option.Symbol);
                }
            }

            // Update highest prices for trailing stop
            foreach (var kvp in Portfolio)
            {
                var symbol = kvp.Key;
                var security = Securities[symbol];

                if (security.Holdings.Quantity != 0)
                {
                    if (!_highestPrices.ContainsKey(symbol.Value))
                        _highestPrices[symbol.Value] = security.Price;

                    _highestPrices[symbol.Value] = Math.Max(_highestPrices[symbol.Value], security.Price);

                    // Check trailing stop: If price drops 2% from high, sell
                    if (!_boolDoSpreads && security.Type == SecurityType.Option &&  security.Price <= _highestPrices[symbol.Value] * 0.95m) // Liquidate Long Options
                    {
                        Log($"[TRAILING STOP] Selling {symbol.Value} at {security.Price:C} (was {_highestPrices[symbol.Value]:C})");
                        Liquidate(symbol);
                    }
                    // _bias = _prevBias = -5;
                }
            }
        }       
        private DateTime GetNextWeeklyExpiry(OptionChain optionChain)
        {
            var today = CurrentSlice.Time.Date;
            var minExpiryDate = today.AddDays(9);   // calculate the min Expiry Date

            return optionChain
                .Select(c => c.Expiry)
                .Where(exp => exp >= minExpiryDate)  // Ensure expiration is at least 9 days away
                .OrderBy(exp => exp)                 // Get the nearest valid expiration
                .FirstOrDefault();        
        }

        private OptionChain GetOptionChainWithRetry(Symbol optionSymbol)
        {
            if (!_optionChainRetries.ContainsKey(optionSymbol))
            {
                _optionChainRetries[optionSymbol] = 0;  // Initialize retry count
            }

            if (CurrentSlice.OptionChains.TryGetValue(optionSymbol, out OptionChain optionChain))
            {
                _optionChainRetries[optionSymbol] = 0;  // Reset retry count on success
                return optionChain;
            }

            // Increment retry counter
            _optionChainRetries[optionSymbol]++;
            Log($" ** At {CurrentSlice.Time.ToShortTimeString()} [RETRYING {_optionChainRetries[optionSymbol]}] Option chain for {optionSymbol.Value} not found.");

            if (_optionChainRetries[optionSymbol] >= MaxRetries)
            {
                Log($"[FAILED] Unable to retrieve option chain for {optionSymbol.Value} after {MaxRetries} attempts.");
                _optionChainRetries[optionSymbol] = 0;  // Reset to allow future retries
                return null;
            }

            return null;  // Retry in the next OnData() call
        }
        

        public void Rebalance()
        {
            // Ensure RSI indicators are ready
            Log($"{CurrentSlice.Time.ToShortDateString()} @ {CurrentSlice.Time.ToShortTimeString()} INDICATOR READY CHECK ");
            if (!_rsiIndicators_Old.All(x => x.Value.IsReady)) return;
            if (CurrentSlice.Time.CompareTo(_dateShrtTermFixedIncomeStart)>1 && !_rsiIndicators.All(x => x.Value.IsReady)) 
            {
                Log($" *** *** *** -- Exiting Rebalance Because Young ETF's RSI not ready");
                return;
            }
            Log(" -- READY");

            // Plot VIX from previous day
            decimal vixPrice = Securities[_symbVIX].Price;
            Log($" ** Previous VIX Close: {vixPrice.ToString("0.00")}");
            Plot("Daily VIX Close", "Close", vixPrice);

            // Retrieve RSI values
            var rsiSPY = _rsiIndicators["SPY"].Current.Value;
            var rsiBIL = _rsiIndicators["BIL"].Current.Value;
            var rsiIBTK = _rsiIndicators[_strIntermediateTermFixedIncome].Current.Value;
            var rsiSBND = _rsiIndicators[_strShrtTermFixedIncome].Current.Value;
            var rsiHIBL = _rsiIndicators[_strLeveragedEquLrgCap].Current.Value;

            _capitalForOptions = Portfolio.TotalPortfolioValue * 0.1m;
            _capitalForBonds = Portfolio.TotalPortfolioValue * 0.9m;
            
            if(_boolDoBonds) SetHoldings(_symbLQD, 0.45);    // 45% to investment grade bonds
            if(_boolDoBonds) SetHoldings(_symbHYG, 0.45);    // 45% to high yield bonds

            OptionChain optionChainSPY = GetOptionChainWithRetry(_optionSPY);
            OptionChain optionChainQQQ = GetOptionChainWithRetry(_optionQQQ);
            //OptionChain optionChainRSP = GetOptionChainWithRetry(_optionRSP);

            if (optionChainSPY == null || optionChainQQQ == null)
            {
                Log("[ERROR] One or more option chains could not be retrieved. Skipping rebalance.");
                return;
            }

            DateTime nextExpiry = GetNextWeeklyExpiry(optionChainSPY);

            if (nextExpiry == DateTime.MinValue)
            {
                Log("No valid expiration found.");
                return;
            }

            decimal priceSPY = Securities[_symbSPY].Price;
            decimal priceQQQ = Securities[_symbQQQ].Price;
            //decimal priceRSP = Securities[_symbRSP].Price;
            decimal priceBIL = Securities[_symbBIL].Price;

            Log($" -- -- BIL PRICE:  {priceBIL.ToString("0.00")}  RSI VALUES BIL: {rsiBIL.ToString("0.00")} | IBTK: {rsiIBTK.ToString("0.00")} | SPY: {rsiSPY.ToString("0.00")} | SBND: {rsiSBND.ToString("0.00")} | HIBL: {rsiHIBL.ToString("0.00")}");

            // Implement strategy logic
            if (rsiBIL < rsiIBTK)
            {
                if (rsiSPY > 75)
                {
                    // Allocate to SHY (bonds)
                    // if (Portfolio["SHY"].Quantity == 0) SetHoldings("SHY", 1.0, true);
                    if (_bias != 0) Log($" -Allocating to SHY");
                    _bias = 0;
                }
                else
                {
                    // Allocate to SOXL (bullish semiconductors)
                    //if (Portfolio["SOXL"].Quantity == 0) SetHoldings("SOXL", 1.0, true);
                    // Write a Bear Call Spread
                    if (_bias != 1) Log($" -Allocating to SOXL equivalent {(_boolDoSpreads ? "selling SPY PUT CREDIT SPREAD" : "buying SPY Bull Calls")} ");
                    _bias = 1;
                }
            }
            else
            {
                if (rsiSBND < rsiHIBL)
                {
                    // Allocate to SOXS (bearish semiconductors) or SQQQ (short QQQ)
                    // SetHoldings("SOXS", 0.5, true);
                    // SetHoldings("SQQQ", 0.5);
                    if (_bias != -2) Log($" -Allocating to SOXS and SQQQ equivalents {(_boolDoSpreads ?  "selling SPY & QQQ CALL CREDIT SPREAD" : "buying SPY & QQQ BEAR PUTS")} ");
                    _bias = -2;
                }
                else
                {
                    // Allocate to SOXL (bullish semiconductors) or TECL (bullish tech)
                    // SetHoldings("SOXL", 0.5, true);
                    // SetHoldings("TECL", 0.5);
                    if (_bias != 2) Log($" -Allocating to SOXL and TECL equivalents {(_boolDoSpreads ? "selling SPY & QQQ PUT CREDIT SPREAD" : "buying SPY &N QQQ Bull Calls")}");
                    _bias = 2;
                }
            }

            Log($" -- -- Bias: {_bias.ToString()}");
            if (_bias != _prevBias) {
                Log ($" -- -- -- -- BIAS CHANGE LIQUIDATION");
                LiquidateOptions();
            } else return;
            
            // Select ATM Strike

            if (_bias == 1){
                var atmContractSPY = optionChainSPY
                    .Where(c => c.Expiry == nextExpiry && (_boolDoSpreads ? c.Right == OptionRight.Put : c.Right == OptionRight.Call))
                    .OrderBy(c => Math.Abs(c.Strike - priceSPY))
                    .FirstOrDefault();

                if (atmContractSPY == null)
                {
                    Log("*** *** *** No ATM SPY contracts found.");
                    return;
                }
        
                decimal atmStrikeSPY = atmContractSPY.Strike;
                decimal itmStrikeSPY = atmStrikeSPY + _decStrikeStep;

                OptionContract itmContractSPY = GetClosestOptionContract(optionChainSPY, itmStrikeSPY, (_boolDoSpreads ? OptionRight.Put : OptionRight.Call), nextExpiry);
                
                if (itmContractSPY == null || itmContractSPY.Strike == atmStrikeSPY)
                {
                    // If ITM contract is null or has the same strike as ATM, try adjusting further ITM
                    itmStrikeSPY = atmStrikeSPY + (_decStrikeStep * 2); // Move another strike step ITM
                    itmContractSPY = GetClosestOptionContract(optionChainSPY, itmStrikeSPY, (_boolDoSpreads ? OptionRight.Put : OptionRight.Call), nextExpiry);
                }
                
                
                if (itmContractSPY == null)
                {
                    Log("*** *** *** No ITM SPY contracts found.");
                    return;
                }
                _numETFShares = (int)(_capitalForOptions / priceSPY);
                _numOptionContracts = Math.Max(1, (int)Math.Round(_numETFShares / 100m));
                Log($"Capital for Options: {_capitalForOptions:C}, Share Price: {priceSPY:C}, NumShares: {_numETFShares}, NumContracts: {_numOptionContracts} of {atmContractSPY.Symbol.Value}");
                if (_boolDoSpreads){
                     Log($" SELLING {_numOptionContracts.ToString()} SPY PUT SPREADS {atmContractSPY.Symbol} AND {itmContractSPY.Symbol}"); 
                } else {
                    Log($" BUYING {_numOptionContracts.ToString()} SPY CALL Options {atmContractSPY.Symbol}");
                }
                MarketOrder(atmContractSPY.Symbol, _numOptionContracts);

                if(_boolDoSpreads) MarketOrder(itmContractSPY.Symbol, -_numOptionContracts);
            }

            if (_bias == -1){
                var atmContractSPY = optionChainSPY
                    .Where(c => c.Expiry == nextExpiry && (_boolDoSpreads ? c.Right == OptionRight.Call : c.Right == OptionRight.Put))
                    .OrderBy(c => Math.Abs(c.Strike - priceSPY))
                    .FirstOrDefault();
        
                if (atmContractSPY == null)
                {
                    Log("*** *** *** No ATM SPY contracts found.");
                    return;
                }
        
                decimal atmStrikeSPY = atmContractSPY.Strike;
                decimal itmStrikeSPY = atmStrikeSPY - _decStrikeStep;

                OptionContract itmContractSPY = GetClosestOptionContract(optionChainSPY, itmStrikeSPY, (_boolDoSpreads ? OptionRight.Call : OptionRight.Put), nextExpiry);

                if (itmContractSPY == null || itmContractSPY.Strike == atmStrikeSPY)
                {
                    // If ITM contract is null or has the same strike as ATM, try adjusting further ITM
                    itmStrikeSPY = atmStrikeSPY - (_decStrikeStep * 2); // Move another strike step ITM
                    itmContractSPY = GetClosestOptionContract(optionChainSPY, itmStrikeSPY, (_boolDoSpreads ? OptionRight.Call : OptionRight.Put), nextExpiry);
                }

                if (itmContractSPY == null)
                {
                    Log("*** *** *** No ITM SPY contracts found.");
                    return;
                }
                _numETFShares = (int)(_capitalForOptions / priceSPY);
                _numOptionContracts = Math.Max(1, (int)Math.Round(_numETFShares / 100m));
                Log($"Capital for Options: {_capitalForOptions:C}, Share Price: {priceSPY:C}, NumShares: {_numETFShares}, NumContracts: {_numOptionContracts} of {atmContractSPY.Symbol.Value}");
 
                if (_boolDoSpreads){
                     Log($" SELLING {_numOptionContracts.ToString()} SPY CALL SPREADS {atmContractSPY.Symbol} AND {itmContractSPY.Symbol}"); 
                } else {
                    Log($" BUYING {_numOptionContracts.ToString()} SPY PUT Options {atmContractSPY.Symbol}");
                }

                MarketOrder(atmContractSPY.Symbol, _numOptionContracts);
                if(_boolDoSpreads) MarketOrder(itmContractSPY.Symbol, -_numOptionContracts);

            }

            if (_bias == -2){
                var atmContractSPY = optionChainSPY
                    .Where(c => c.Expiry == nextExpiry && (_boolDoSpreads ? c.Right == OptionRight.Call : c.Right == OptionRight.Put))
                    .OrderBy(c => Math.Abs(c.Strike - priceSPY))
                    .FirstOrDefault();
        
                if (atmContractSPY == null)
                {
                    Log("*** *** *** No ATM SPY contracts found.");
                    return;
                }
        
                decimal atmStrikeSPY = atmContractSPY.Strike;
                decimal itmStrikeSPY = atmStrikeSPY - _decStrikeStep;

                OptionContract itmContractSPY = GetClosestOptionContract(optionChainSPY, itmStrikeSPY, (_boolDoSpreads ? OptionRight.Call : OptionRight.Put), nextExpiry);
                if (itmContractSPY == null || itmContractSPY.Strike == atmStrikeSPY)
                {
                    // If ITM contract is null or has the same strike as ATM, try adjusting further ITM
                    itmStrikeSPY = atmStrikeSPY - (_decStrikeStep * 2); // Move another strike step ITM
                    itmContractSPY = GetClosestOptionContract(optionChainSPY, itmStrikeSPY, (_boolDoSpreads ? OptionRight.Call : OptionRight.Put), nextExpiry);
                }

                if (itmContractSPY == null)
                {
                    Log("*** *** *** No ITM SPY contracts found.");
                    return;
                }
                var atmContractQQQ = optionChainQQQ
                    .Where(c => c.Expiry == nextExpiry && (_boolDoSpreads ? c.Right == OptionRight.Call : c.Right == OptionRight.Put))
                    .OrderBy(c => Math.Abs(c.Strike - priceQQQ))
                    .FirstOrDefault();
        
                if (atmContractQQQ == null)
                {
                    Log("*** *** *** No ATM QQQ contracts found.");
                    return;
                }
        
                decimal atmStrikeQQQ = atmContractQQQ.Strike;
                decimal itmStrikeQQQ = atmStrikeQQQ - _decStrikeStep;

                OptionContract itmContractQQQ = GetClosestOptionContract(optionChainQQQ, itmStrikeQQQ, (_boolDoSpreads ? OptionRight.Call : OptionRight.Put), nextExpiry);

                if (itmContractQQQ == null || itmContractQQQ.Strike == atmStrikeQQQ)
                {
                    // If ITM contract is null or has the same strike as ATM, try adjusting further ITM
                    itmStrikeQQQ = atmStrikeQQQ - (_decStrikeStep * 2); // Move another strike step ITM
                    itmContractQQQ = GetClosestOptionContract(optionChainQQQ, itmStrikeQQQ, (_boolDoSpreads ? OptionRight.Call : OptionRight.Put), nextExpiry);
                }

                if (itmContractQQQ == null)
                {
                    Log("*** *** *** No ITM QQQ contracts found.");
                    return;
                }
        
                _numETFShares = (int)(_capitalForOptions / priceSPY / 2);
                _numOptionContracts = Math.Max(1, (int)Math.Round(_numETFShares / 100m));
                Log($"Capital for Options: {_capitalForOptions:C}, Share Price: {priceSPY:C}, NumShares: {_numETFShares}, NumContracts: {_numOptionContracts} of {atmContractSPY.Symbol.Value}");
                if (_boolDoSpreads){
                     Log($" SELLING {_numOptionContracts.ToString()} SPY CALL SPREADS {atmContractSPY.Symbol} AND {itmContractSPY.Symbol}"); 
                } else {
                    Log($" BUYING {_numOptionContracts.ToString()} SPY PUT Options {atmContractSPY.Symbol}");
                }

                MarketOrder(atmContractSPY.Symbol, _numOptionContracts);
                if(_boolDoSpreads) MarketOrder(itmContractSPY.Symbol, -_numOptionContracts);

                _numETFShares = (int)(_capitalForOptions / priceQQQ / 2);
                _numOptionContracts = Math.Max(1, (int)Math.Round(_numETFShares / 100m));
                Log($"Capital for Options: {_capitalForOptions:C}, Share Price: {priceQQQ:C}, NumShares: {_numETFShares}, NumContracts: {_numOptionContracts} of {atmContractQQQ.Symbol.Value}");
                if (_boolDoSpreads){
                     Log($" SELLING {_numOptionContracts.ToString()} QQQ CALL SPREADS {atmContractQQQ.Symbol} AND {itmContractQQQ.Symbol}"); 
                } else {
                    Log($" BUYING {_numOptionContracts.ToString()} QQQ PUT Options {atmContractQQQ.Symbol}");
                }
                MarketOrder(atmContractQQQ.Symbol, _numOptionContracts);
                if(_boolDoSpreads) MarketOrder(itmContractQQQ.Symbol, -_numOptionContracts);
            }

            if (_bias == 2){
                var atmContractSPY = optionChainSPY
                    .Where(c => c.Expiry == nextExpiry && (_boolDoSpreads ? c.Right == OptionRight.Put : c.Right == OptionRight.Call))
                    .OrderBy(c => Math.Abs(c.Strike - priceSPY))
                    .FirstOrDefault();
                
                if (atmContractSPY == null)
                {
                    Log("*** *** *** No ATM SPY contracts found.");
                    return;
                }
        
                decimal atmStrikeSPY = atmContractSPY.Strike;
                decimal itmStrikeSPY = atmStrikeSPY + _decStrikeStep;

                OptionContract itmContractSPY = GetClosestOptionContract(optionChainSPY, itmStrikeSPY, (_boolDoSpreads ? OptionRight.Put : OptionRight.Call), nextExpiry);

                if (itmContractSPY == null || itmContractSPY.Strike == atmStrikeSPY)
                {
                    // If ITM contract is null or has the same strike as ATM, try adjusting further ITM
                    itmStrikeSPY = atmStrikeSPY + (_decStrikeStep * 2); // Move another strike step ITM
                    itmContractSPY = GetClosestOptionContract(optionChainSPY, itmStrikeSPY, (_boolDoSpreads ? OptionRight.Put : OptionRight.Call), nextExpiry);
                }
                
               if (itmContractSPY == null)
                {
                    Log("*** *** *** No ITM SPY contracts found.");
                    return;
                }
                var atmContractQQQ = optionChainQQQ
                    .Where(c => c.Expiry == nextExpiry && (_boolDoSpreads ? c.Right == OptionRight.Put : c.Right == OptionRight.Call))
                    .OrderBy(c => Math.Abs(c.Strike - priceQQQ))
                    .FirstOrDefault();
        
                if (atmContractQQQ == null)
                {
                    Log("*** *** *** No ATM QQQ contracts found.");
                    return;
                }
        
                decimal atmStrikeQQQ = atmContractQQQ.Strike;
                decimal itmStrikeQQQ = atmStrikeQQQ + _decStrikeStep;

                OptionContract itmContractQQQ = GetClosestOptionContract(optionChainQQQ, itmStrikeQQQ, (_boolDoSpreads ? OptionRight.Put : OptionRight.Call), nextExpiry);
                if (itmContractQQQ == null || itmContractQQQ.Strike == atmStrikeQQQ)
                {
                    // If ITM contract is null or has the same strike as ATM, try adjusting further ITM
                    itmStrikeQQQ = atmStrikeQQQ + (_decStrikeStep * 2); // Move another strike step ITM
                    itmContractQQQ = GetClosestOptionContract(optionChainQQQ, itmStrikeQQQ, (_boolDoSpreads ? OptionRight.Put : OptionRight.Call), nextExpiry);
                }

                if (itmContractQQQ == null)
                {
                    Log("*** *** *** No ITM QQQ contracts found.");
                    return;
                }
        
                _numETFShares = (int)(_capitalForOptions / priceSPY);
                _numOptionContracts = Math.Max(1, (int)Math.Round(_numETFShares / 100m / 2));
                Log($"Capital for Options: {_capitalForOptions:C}, Share Price: {priceSPY:C}, NumShares: {_numETFShares}, NumContracts: {_numOptionContracts} of {atmContractSPY.Symbol.Value}");
                if (_boolDoSpreads){
                     Log($" SELLING {_numOptionContracts.ToString()} SPY PUT SPREADS {atmContractSPY.Symbol} AND {itmContractSPY.Symbol}"); 
                } else {
                    Log($" BUYING {_numOptionContracts.ToString()} SPY CALL CALL Options {atmContractSPY.Symbol}");
                }
                MarketOrder(atmContractSPY.Symbol, _numOptionContracts);
                if(_boolDoSpreads) MarketOrder(itmContractSPY.Symbol, -_numOptionContracts);

                _numETFShares = (int)(_capitalForOptions / priceQQQ);
                _numOptionContracts = Math.Max(1, (int)Math.Round(_numETFShares / 100m / 2));
                Log($"Capital for Options: {_capitalForOptions:C}, Share Price: {priceQQQ:C}, NumShares: {_numETFShares}, NumContracts: {_numOptionContracts} of {atmContractQQQ.Symbol.Value}");
                if (_boolDoSpreads){
                     Log($" SELLING {_numOptionContracts.ToString()} QQQ PUT SPREADS {atmContractQQQ.Symbol} AND {itmContractQQQ.Symbol}"); 
                } else {
                    Log($" BUYING {_numOptionContracts.ToString()} QQQ CALL CALL Options {atmContractQQQ.Symbol}");
                }
                MarketOrder(atmContractQQQ.Symbol, _numOptionContracts);
                if(_boolDoSpreads) MarketOrder(itmContractQQQ.Symbol, -_numOptionContracts);
            }

            _prevBias = _bias;
        }
        private OptionContract GetClosestOptionContract(OptionChain optionChain, decimal targetStrike, OptionRight right, DateTime expiry)
        {
            return optionChain
                .Where(c => c.Expiry == expiry && c.Right == right)
                .OrderBy(c => Math.Abs(c.Strike - targetStrike)) // Sort by closest to target
                .FirstOrDefault();  // Select the closest available contract
        }
        private void LiquidateExpiredOptions()
        {
            //_bias = _prevBias = -5;
            foreach (var option in Portfolio.Keys.Where(x => x.SecurityType == SecurityType.Option))
            {
                var security = Securities[option];
                if (security.Holdings.Quantity != 0 && security.Symbol.ID.Date == Time.Date)
                {
                    Log($"[EXPIRATION] Liquidating {option.Value}");
                    Liquidate(option);
                }
            }
            _highestPrices.Clear();  // Clear highest prices after expiration liquidation

        }
        private void LiquidateOptions()
        {
            var options = Portfolio.Keys.Where(x => x.SecurityType == SecurityType.Option);
            foreach (var option in options)
            {
                // Log($"Liquidating option position: {option}");
                Liquidate(option);
            }
            _highestPrices.Clear();  // Clear highest prices after expiration liquidation

        }
        private void CheckForAssignmentRisk()
        {
            foreach (var kvp in Portfolio)
            {
                var symbol = kvp.Key;
                var security = Securities[symbol];

                // Only check for short option positions (negative quantity)
                if (security.Type != SecurityType.Option || security.Holdings.Quantity >= 0)
                    continue;

                // Get option contract details
                OptionChain thisChain = GetOptionChainWithRetry(symbol);
                if (thisChain == null)
                {
                    Log($"[ERROR] {symbol.Value} option chain could not be retrieved. Skipping CheckForAssignmentRisk.");
                    return;
                }

                OptionContract option = thisChain.Contracts.ContainsKey(symbol) ? thisChain.Contracts[symbol] : null;
                if (option == null) continue;

                // Get underlying asset price
                //var underlying = Securities[option.Underlying];
                var underlyingSymbol = option.UnderlyingSymbol;
                var underlyingSecurity = Securities[underlyingSymbol];
                decimal intrinsicValue = option.Right == OptionRight.Call
                    ? Math.Max(0, underlyingSecurity.Price - option.Strike)   // Calls: ITM if price > strike
                    : Math.Max(0, option.Strike - underlyingSecurity.Price);  // Puts: ITM if price < strike
                
                decimal extrinsicValue = option.AskPrice - intrinsicValue;

                // Assignment risk condition: Deep ITM with low extrinsic value
                if (intrinsicValue > 0 && extrinsicValue < 0.05m)  // Adjust threshold if needed
                {
                    Log($"[ASSIGNMENT RISK] {option.Symbol.Value} at risk of early assignment! Liquidating spread.");
                    LiquidateSpreadContaining(option.Symbol);
                }
            }
        }

        private void LiquidateSpreadContaining(Symbol shortOptionSymbol)
        {
            // Find and liquidate all legs of the spread containing this short option
            var spreadLegs = Portfolio.Keys
                .Where(s => s.SecurityType == SecurityType.Option && s.Value.Contains(shortOptionSymbol.Underlying.Value))  // Match underlying symbol
                .ToList();

            foreach (var leg in spreadLegs)
            {
                Log($"[SPREAD LIQUIDATION] Closing position: {leg.Value}");
                Liquidate(leg);
            }
        }
   
        public override void OnAssignmentOrderEvent(OrderEvent oe)
        {
            Log($"[ASSIGNMENT ORDER EVENT] : {oe.Message }");
        }
        public override void OnOrderEvent (OrderEvent oe)
        {
            //Log($"[ORDER EVENT] {oe.Id} is an {oe.ShortToString()}");
            if(oe.IsAssignment) Log($"[ORDER EVENT] {oe.Id} is an assignment for symbol: {oe.Symbol.Value}.  {oe.ShortToString()}");
        }
    
    }
}