| 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}");
}
}
}
}