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