Overall Statistics
Total Orders
324
Average Win
0.94%
Average Loss
-1.24%
Compounding Annual Return
26.122%
Drawdown
30.600%
Expectancy
0.114
Start Equity
100000
End Equity
118048.4
Net Profit
18.048%
Sharpe Ratio
0.594
Sortino Ratio
0.68
Probabilistic Sharpe Ratio
33.857%
Loss Rate
37%
Win Rate
63%
Profit-Loss Ratio
0.76
Alpha
0.46
Beta
-1.753
Annual Standard Deviation
0.508
Annual Variance
0.258
Information Ratio
0.332
Tracking Error
0.636
Treynor Ratio
-0.172
Total Fees
$1296.60
Estimated Strategy Capacity
$7000.00
Lowest Capacity Asset
SPY 32W71MM2CAT46|SPY R735QTJ8XC9X
Portfolio Turnover
1.51%
Drawdown Recovery
69
#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
namespace QuantConnect.Algorithm.CSharp
{
    /// <summary>
    /// Updated QuantConnect algorithm implementing a long straddle strategy targeted ~6-month expiration
    /// rolled monthly (close and reopen new ~6-month straddle), sized to 50% of portfolio.
    /// Overlaid with daily selling of 0DTE (or closest) ~20-delta naked puts, matching straddle quantity.
    ///
    /// Key Updates:
    /// - Straddle: Combo ticket for lower margin; roll monthly by closing old and opening new ~180-day target.
    /// - Put Sales: Market orders; allow intra-day reopen after ITM close (always ~20-delta; single open at a time).
    /// - Strictly cap short put qty at straddle qty; try to get as close as possible based on margin (floor max affordable, min with straddle, >=1).
    /// - Intraday: If short put ITM, close immediately and reopen same day with ~20-delta.
    /// - EOD: Always close any remaining short put 5 min before close.
    /// - Data Resolution: Minute for pricing.
    ///
    /// Assumptions:
    /// - SPY underlying.
    /// - Adjust dates/cash as needed.
    /// </summary>
    public class SixMonthStraddleRolledMonthlyWithDailyPuts : QCAlgorithm
    {
        private Symbol _symbol;
        private Symbol _equitySymbol;
        private Symbol _straddleCallSymbol;
        private Symbol _straddlePutSymbol;
        private Symbol _shortPutSymbol;
        private int _straddleQuantity = 0;
        private DateTime _lastReopenDate = DateTime.MinValue;
        private decimal _targetDelta = 0.20m;
        private bool _allowSameDayReopen = false;
        private OptionChain _latestOptionChain;
        public override void Initialize()
        {
            // Backtest period (adjust as needed; 0DTE data post-2019)
            SetStartDate(2024, 12, 31);//Current date
            SetCash(100000);
            // Set to Margin account type (Interactive Brokers brokerage model)
            SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin);
            // Add SPY equity for underlying price access
            var equity = AddEquity("SPY", Resolution.Minute);
            _equitySymbol = equity.Symbol;
            // Add SPY options universe with minute resolution for accurate pricing
            var option = AddOption("SPY", Resolution.Minute);
            _symbol = option.Symbol;
            // Set price model for Greeks computation
            // option.PriceModel = OptionPriceModels.BlackScholes();
            // Filter: Weeklies, strikes ±5% of price, expirations 0-200 days (for 6-month target)
            option.SetFilter(universe => universe
                .IncludeWeeklys()
                .Strikes(-5, 5)
                .Expiration(TimeSpan.FromDays(0), TimeSpan.FromDays(200))
            );
            // Benchmark to SPY
            SetBenchmark("SPY");
            // Toggle for allowing same-day reopen of 0DTE put after ITM close
            // _allowSameDayReopen = GetParameter("AllowSameDayReopen", true);
            Log($"Toggle: Allow same-day 0DTE reopen = {_allowSameDayReopen}");
            // Schedule monthly straddle roll on 1st trading day
            Schedule.On(DateRules.MonthStart("SPY"), TimeRules.AfterMarketOpen("SPY", 5), RollStraddle);
            // Schedule daily trading logic at market open (5 min after)
            Schedule.On(DateRules.EveryDay("SPY"), TimeRules.AfterMarketOpen("SPY", 5), DailyTradingLogic);
            // Schedule EOD close for any remaining short put
            Schedule.On(DateRules.EveryDay("SPY"), TimeRules.BeforeMarketClose("SPY", 5), CloseShortPutEOD);
        }
        public override void OnData(Slice slice)
        {
            // Update latest option chain for use in scheduled events
            if (slice.OptionChains.TryGetValue(_symbol, out var chain))
            {
                _latestOptionChain = chain;
            }
            // Intraday ITM check for short put (skip if reopened today to avoid loops)
            if (Time.Date == _lastReopenDate)
            {
                return;
            }
            if (_shortPutSymbol != null && Portfolio[_shortPutSymbol].Invested && Portfolio[_shortPutSymbol].Quantity < 0)
            {
                var option = Securities[_shortPutSymbol] as Option;
                var underlyingPrice = Securities[_equitySymbol].Price;
                if (underlyingPrice < option.Symbol.ID.StrikePrice)
                {
                    // Close immediately if ITM
                    var quantity = Portfolio[_shortPutSymbol].Quantity;
                    var pnl = Portfolio[_shortPutSymbol].UnrealizedProfit;
                    Liquidate(_shortPutSymbol);
                    _shortPutSymbol = null;
                    Log($"*** EARLY CLOSE: SHORT PUT ITM *** | Quantity: {Math.Abs(quantity)} contracts | Strike: {option.Symbol.ID.StrikePrice} | Underlying: {underlyingPrice} | P/L: {pnl:C}");
                    if (_allowSameDayReopen)
                    {
                        Log($"*** REOPENING SAME-DAY 0DTE PUT (toggle enabled) ***");
                        // Reopen same day with ~20-delta
                        AttemptPutSale(underlyingPrice);
                        _lastReopenDate = Time.Date;
                    }
                    else
                    {
                        Log($"*** SKIPPING SAME-DAY REOPEN (toggle disabled) ***");
                    }
                }
            }
        }
        private void RollStraddle()
        {
            var today = Time.Date;
            if (_straddleQuantity == 0 || _straddleCallSymbol == null)
            {
                _longStraddleBought = false;
                return; // DailyTradingLogic will handle initial purchase
            }
            var currentExpiry = _straddleCallSymbol.ID.Date;
            var daysLeft = (currentExpiry.Date - today).Days;
            if (daysLeft >= 90)
            {
                Log($"*** STRADDLE ROLL SKIPPED *** | Days left: {daysLeft} >= 90");
                return;
            }
            // Proceed with roll
            if (Portfolio[_straddleCallSymbol].Invested) Liquidate(_straddleCallSymbol);
            if (_straddlePutSymbol != null && Portfolio[_straddlePutSymbol].Invested) Liquidate(_straddlePutSymbol);
            Log($"*** STRADDLE ROLLED *** | Closed old position on {today:yyyy-MM-dd} (original days left: {daysLeft})");
            _straddleQuantity = 0;
            _straddleCallSymbol = null;
            _straddlePutSymbol = null;
            _longStraddleBought = false;
            // DailyTradingLogic will handle new purchase
        }
        private bool _longStraddleBought = false;
        private void DailyTradingLogic()
        {
            var today = Time.Date;
            // Reset for new day
            _lastReopenDate = DateTime.MinValue;
            _targetDelta = 0.20m;
            if (_latestOptionChain == null || !_latestOptionChain.Any())
            {
                return;
            }
            var underlyingPrice = _latestOptionChain.Underlying.Price;
            var portfolioValue = Portfolio.TotalPortfolioValue;
            // Log account status daily
            Log($"Date: {today:yyyy-MM-dd} | Portfolio Value: {portfolioValue:C} | Margin Remaining: {Portfolio.MarginRemaining:C} | Buying Power: {Portfolio.MarginRemaining + Portfolio.TotalHoldingsValue:C} | Delta Target: {_targetDelta:F2}");
            var contracts = _latestOptionChain.Contracts.Values.ToList();
            // Step 1: Buy/roll long straddle, scaled to 50% of portfolio value (monthly or first time)
            if (!_longStraddleBought)
            {
                // Find expiration closest to 180 days out for 6-month target
                var expiryDays = contracts.Select(c => (c.Expiry.Date - today).Days).Where(d => d >= 0).ToList();
                if (!expiryDays.Any()) return;
               
                var targetDays = 180;
                var closestIdx = Enumerable.Range(0, expiryDays.Count)
                    .OrderBy(i => Math.Abs(expiryDays[i] - targetDays))
                    .First();
                var targetExpiry = contracts.Where(c => (c.Expiry.Date - today).Days == expiryDays[closestIdx]).First().Expiry;
                // Filter contracts for target expiry
                var targetContracts = contracts.Where(c => c.Expiry == targetExpiry).ToList();
                // Find ATM call and put
                var calls = targetContracts
                    .Where(c => c.Right == OptionRight.Call)
                    .OrderBy(c => Math.Abs(underlyingPrice - c.Strike))
                    .ToList();
                var puts = targetContracts
                    .Where(c => c.Right == OptionRight.Put)
                    .OrderBy(c => Math.Abs(underlyingPrice - c.Strike))
                    .ToList();
                if (calls.Any() && puts.Any())
                {
                    var atmCall = calls.First();
                    var atmPut = puts.First();
                    var atmStrike = atmCall.Strike;
                    // Cost estimate using ask prices (conservative)
                    var callAsk = atmCall.AskPrice > 0 ? atmCall.AskPrice : atmCall.LastPrice;
                    var putAsk = atmPut.AskPrice > 0 ? atmPut.AskPrice : atmPut.LastPrice;
                    if (callAsk <= 0 || putAsk <= 0) {
                        Log("Invalid ask prices for straddle—skipping");
                        return;
                    }
                    var straddleCostPerContract = (callAsk + putAsk) * 100m; // Per straddle (call + put)
                    // Target 50% of portfolio value
                    var targetCost = 0.5m * portfolioValue;
                    var quantity = (int)Math.Floor(targetCost / straddleCostPerContract);
                    quantity = Math.Max(1, quantity); // Minimum 1 contract
                    // Place market order for straddle combo (lower margin)
                    var longStraddle = OptionStrategies.Straddle(_symbol, atmStrike, targetExpiry);
                    Buy(longStraddle, quantity); // Market order by default in QC for strategies
                    var totalStraddleCost = straddleCostPerContract * quantity;
                    var allocationPct = (totalStraddleCost / portfolioValue) * 100m;
                    _straddleCallSymbol = atmCall.Symbol;
                    _straddlePutSymbol = atmPut.Symbol;
                    Log($"*** STRADDLE PURCHASE/ROLL *** | Quantity: {quantity} contracts | Strike: {atmStrike} | Expiry: {targetExpiry:yyyy-MM-dd} ({expiryDays[closestIdx]} days) | Est. Cost per Straddle: {straddleCostPerContract:C} | Total Cost: {totalStraddleCost:C} ({allocationPct:F1}% of portfolio) | Call Delta: {atmCall.Greeks.Delta:F3} | Put Delta: {atmPut.Greeks.Delta:F3} | Net Delta: {(atmCall.Greeks.Delta + atmPut.Greeks.Delta) * quantity:F3}");
                    _straddleQuantity = quantity;
                    _longStraddleBought = true;
                }
            }
            // Step 2: Attempt daily put sale
            // Close any lingering open short put
            if (_shortPutSymbol != null && Portfolio[_shortPutSymbol].Invested)
            {
                Liquidate(_shortPutSymbol);
                _shortPutSymbol = null;
            }
            if (!contracts.Any() || _straddleQuantity == 0) return;
            AttemptPutSale(underlyingPrice);
        }
        private void AttemptPutSale(decimal underlyingPrice)
        {
            var today = Time.Date;
            var contracts = _latestOptionChain.Contracts.Values.ToList();
            // Find shortest expiration (>=0 DTE)
            var validContracts = contracts.Where(c => (c.Expiry.Date - today).Days >= 0);
            if (!validContracts.Any()) return;
           
            var minDays = validContracts.Min(c => (c.Expiry.Date - today).Days);
            var targetShortExpiry = validContracts.Where(c => (c.Expiry.Date - today).Days == minDays).First().Expiry;
            // Filter puts for target expiry
            var shortPuts = contracts.Where(c => c.Expiry == targetShortExpiry && c.Right == OptionRight.Put).ToList();
            if (shortPuts.Any())
            {
                // Select closest to target delta put (|delta| ≈ target, since put delta negative)
                var deltaCandidates = shortPuts
                    .Where(p => p.Greeks.Delta < 0 && Math.Abs(p.Greeks.Delta) <= 0.5m) // Reasonable OTM puts
                    .Select(p => new { Contract = p, DeltaDiff = Math.Abs(p.Greeks.Delta + _targetDelta) })
                    .OrderBy(x => x.DeltaDiff)
                    .ToList();
                if (deltaCandidates.Any())
                {
                    var selectedPut = deltaCandidates.First().Contract;
                    var putDelta = selectedPut.Greeks.Delta;
                    var strikeDistancePct = ((underlyingPrice - selectedPut.Strike) / underlyingPrice) * 100m;
                    // Calculate maximum possible short quantity based on margin/buying power
                    var targetShortPercentage = -1.0m; // Attempt to short 100% to get max possible
                    var maxShortQuantity = CalculateOrderQuantity(selectedPut.Symbol, targetShortPercentage);
                    var maxAffordableQty = (int) Math.Floor(Math.Abs(maxShortQuantity));
                    if (maxAffordableQty < 1)
                    {
                        Log($"Insufficient margin for put sale: max affordable {maxAffordableQty} < 1");
                        return;
                    }
                    // Strictly cap at straddle quantity; try to get as close as possible
                    var tradeQty = Math.Min(_straddleQuantity, maxAffordableQty);
                    if (tradeQty < 1)
                    {
                        Log($"Cannot sell put: tradeQty {tradeQty} < 1 (straddle qty: {_straddleQuantity}, max affordable: {maxAffordableQty})");
                        return;
                    }
                    // Ensure no open position
                    if (_shortPutSymbol != null && Portfolio[_shortPutSymbol].Invested && Portfolio[_shortPutSymbol].Quantity < 0)
                    {
                        return; // Already open
                    }
                    // Place market sell order (negative quantity for short)
                    MarketOrder(selectedPut.Symbol, -tradeQty); // Explicit market order
                    // Est. premium (credit received)
                    var putBid = selectedPut.BidPrice > 0 ? selectedPut.BidPrice : selectedPut.LastPrice;
                    var premiumPerContract = putBid * 100m;
                    var totalPremium = premiumPerContract * tradeQty;
                    var reopenStr = (Time.Date == _lastReopenDate) ? " (REOPEN)" : "";
                    Log($"*** DAILY PUT SALE{reopenStr} (HEDGE) *** | Quantity: {tradeQty} contracts (max affordable: {maxAffordableQty}, capped at straddle: {_straddleQuantity}) | Strike: {selectedPut.Strike} | Expiry: {targetShortExpiry:yyyy-MM-dd} ({minDays} DTE) | Delta: {putDelta:F3} (target ~ -{_targetDelta:F2}) | Est. Premium per Contract: {premiumPerContract:C} | Total Credit: {totalPremium:C} | OTM %: {strikeDistancePct:F1}%");
                    _shortPutSymbol = selectedPut.Symbol;
                }
                else
                {
                    Log($"No suitable {_targetDelta:F2}-delta put found for {targetShortExpiry:yyyy-MM-dd} (min DTE: {minDays})");
                }
            }
        }
        private void CloseShortPutEOD()
        {
            if (_shortPutSymbol == null || !Portfolio[_shortPutSymbol].Invested || Portfolio[_shortPutSymbol].Quantity >= 0)
            {
                return;
            }
            var quantity = Portfolio[_shortPutSymbol].Quantity; // negative
            var pnl = Portfolio[_shortPutSymbol].UnrealizedProfit;
            var option = Securities[_shortPutSymbol] as Option;
            var underlyingPrice = Securities[_equitySymbol].Price;
            Liquidate(_shortPutSymbol);
            _shortPutSymbol = null;
            Log($"*** CLOSING SHORT PUT EOD *** | Quantity: {Math.Abs(quantity)} contracts | Strike: {option.Symbol.ID.StrikePrice} | Underlying: {underlyingPrice} | P/L: {pnl:C}");
        }
        public override void OnOrderEvent(OrderEvent orderEvent)
        {
            if (orderEvent.Status == OrderStatus.Filled)
            {
                var direction = orderEvent.FillQuantity > 0 ? "Bought" : "Sold";
                var cashImpact = orderEvent.FillQuantity * orderEvent.FillPrice * 100m;
                Log($"FILL: {direction} {Math.Abs(orderEvent.FillQuantity)} x {orderEvent.Symbol} @ {orderEvent.FillPrice:F2} | Cash Impact: {cashImpact:C}");
            }
        }
    }
}