| Overall Statistics |
|
Total Orders 68 Average Win 0.86% Average Loss -0.33% Compounding Annual Return -0.284% Drawdown 1.600% Expectancy 0.004 Start Equity 100000 End Equity 99905.22 Net Profit -0.095% Sharpe Ratio -1.95 Sortino Ratio -1.767 Probabilistic Sharpe Ratio 23.318% Loss Rate 72% Win Rate 28% Profit-Loss Ratio 2.57 Alpha -0.061 Beta 0.082 Annual Standard Deviation 0.029 Annual Variance 0.001 Information Ratio -1.054 Tracking Error 0.101 Treynor Ratio -0.692 Total Fees $68.00 Estimated Strategy Capacity $320000.00 Lowest Capacity Asset SPY 32NVPQ72H2SVA|SPY R735QTJ8XC9X Portfolio Turnover 12.60% Drawdown Recovery 37 |
#region imports
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Option;
#endregion
namespace QuantConnect.Algorithm.CSharp
{
public class DeltaHedgedStraddleAlgorithm : QCAlgorithm
{
private Equity _spy;
private Symbol _canonicalOption;
private OptionStrategy _shortStraddle;
private StraddleSelector _straddleSelector;
private DeltaHedger _hedger;
private decimal _straddleWeight = 0.5m, _delta = 0m;
private DateTime? _exitDay;
public override void Initialize()
{
SetStartDate(2024, 9, 1);
SetEndDate(2024, 12, 31);
SetCash(100000);
Settings.SeedInitialPrices = true;
_spy = AddEquity("SPY", dataNormalizationMode: DataNormalizationMode.Raw);
// Initialize the straddle selector with 7-30 day DTE range.
_straddleSelector = new StraddleSelector(minDte: 7, maxDte: 30);
// Initialize the delta hedger with 30 share minimum, 20 share rehedge band, and 4 hour delay.
_hedger = new DeltaHedger(30, 20, TimeSpan.FromHours(4));
_canonicalOption = QuantConnect.Symbol.CreateCanonicalOption(_spy.Symbol);
// Schedule the rebalance method to run 30 minutes after market open.
Schedule.On(
DateRules.EveryDay(_spy.Symbol),
TimeRules.AfterMarketOpen(_spy.Symbol, 30),
Rebalance
);
// Schedule the close expiring method to run 60 minutes before market close.
Schedule.On(
DateRules.EveryDay(_spy.Symbol),
TimeRules.BeforeMarketClose(_spy.Symbol, 60),
CloseExpiring
);
SetWarmUp(TimeSpan.FromDays(45));
}
public override void OnData(Slice slice)
{
if (IsWarmingUp || !slice.Bars.Any()) return;
if (!slice.OptionChains.TryGetValue(_canonicalOption, out var chain) || _shortStraddle == null) return;
// Accumulate the delta from all option legs in the straddle.
foreach (var leg in _shortStraddle.OptionLegs)
{
if (chain.Contracts.TryGetValue(leg.Symbol, out var contract))
{
_delta += contract.Greeks.Delta;
}
}
// Calculate the net portfolio delta including underlying and options.
var netDelta = _hedger.ComputeNetDelta(this, _shortStraddle.OptionLegs, _spy.Holdings.Quantity, _delta);
// Reset the delta accumulator for the next iteration.
_delta = 0m;
// Exit if it's not time to hedge or net delta is zero.
if (!(_hedger.ShouldHedge(Time) && netDelta != 0)) return;
// Calculate the hedge quantity needed to neutralize delta.
var hedgeQty = _hedger.GetQuantity(netDelta);
if (hedgeQty.HasValue)
{
MarketOrder(_spy.Symbol, hedgeQty.Value, tag: $"Daily delta hedge ({netDelta:F2} share delta)");
// Record the hedge operation with timestamp and delta.
_hedger.RecordHedge(Time, netDelta);
// Update the net delta after the hedge.
netDelta += hedgeQty.Value;
}
Plot("Portfolio Delta", "Delta", netDelta);
}
private void Rebalance()
{
if (IsWarmingUp || Portfolio.Invested) return;
var chain = OptionChain(_spy.Symbol);
if (!chain.Any()) return;
// Use the straddle selector to find the best ATM straddle.
var result = _straddleSelector.Select(chain, _spy.Price, Time.Date);
if (result == null) return;
_delta = result.Value.Delta;
_shortStraddle = OptionStrategies.ShortStraddle(_canonicalOption, result.Value.Strike, result.Value.Expiry);
// Calculate the number of contracts based on portfolio value and weight.
Buy(_shortStraddle, Math.Max(1, (int)(Portfolio.TotalPortfolioValue * _straddleWeight / (_spy.Price * 100m))),
tag: $"Sell ATM straddle exp {result.Value.Expiry:d}");
_hedger.Reset();
_exitDay = result.Value.Expiry.Date;
}
private void CloseExpiring()
{
if (_shortStraddle == null || !_exitDay.HasValue || Time.Date < _exitDay.Value) return;
// Liquidate all positions before expiration.
Liquidate(tag: "Expiry hedge liquidation");
}
public override void OnOrderEvent(OrderEvent orderEvent)
{
// Handle option assignment events.
if (orderEvent.Status == OrderStatus.Filled && orderEvent.IsAssignment)
{
// Liquidate all option legs of the straddle.
foreach (var leg in _shortStraddle.OptionLegs)
{
Liquidate(symbol: leg.Symbol, tag: "Assignment liquidation");
}
// Determine the direction based on whether it's a call or put assignment.
var direction = Securities[orderEvent.Symbol].Symbol.ID.OptionRight == OptionRight.Call ? 1 : -1;
MarketOrder(_spy.Symbol, -_spy.Holdings.Quantity + orderEvent.FillQuantity * direction * 100);
_shortStraddle = null;
}
}
}
public class StraddleSelector
{
private readonly int _minDte, _maxDte;
public StraddleSelector(int minDte, int maxDte)
{
// Store the minimum days to expiration for contract selection.
_minDte = minDte;
// Store the maximum days to expiration for contract selection.
_maxDte = maxDte;
}
public (decimal Delta, DateTime Expiry, decimal Strike)? Select(IEnumerable<OptionContract> chain, decimal spotPrice, DateTime currentDate)
{
// Filter contracts to those within the specified DTE range.
var contracts = chain.Where(c =>
{
var dte = (c.Expiry.Date - currentDate).Days;
return dte >= _minDte && dte <= _maxDte;
}).ToList();
if (!contracts.Any()) return null;
// Select the farthest expiration date from the filtered contracts.
var expiry = contracts.Max(c => c.Expiry);
// Filter to only contracts with the selected expiration.
contracts = contracts.Where(c => c.Expiry == expiry).ToList();
// Find the strike price closest to the current spot price.
var strike = contracts.OrderBy(c => Math.Abs(c.Strike - spotPrice)).First().Strike;
// Filter to only contracts with the selected strike.
contracts = contracts.Where(c => c.Strike == strike).ToList();
// Ensure we have both call and put for a straddle.
if (contracts.Count < 2) return null;
// Return the combined delta, expiry, and strike for the straddle.
return (contracts.Sum(c => c.Greeks.Delta), expiry, strike);
}
}
public class DeltaHedger
{
private readonly int _minShares, _rehedgeBand;
private readonly TimeSpan _hedgeDelay;
private DateTime _nextHedgeTime;
private decimal? _lastHedgedDelta;
public DeltaHedger(int minShares, int rehedgeBand, TimeSpan hedgeDelay)
{
// Store the minimum number of shares required to execute a hedge.
_minShares = minShares;
// Store the rehedge band threshold to prevent excessive hedging.
_rehedgeBand = rehedgeBand;
// Store the time delay between hedging operations.
_hedgeDelay = hedgeDelay;
Reset();
}
public void Reset()
{
_nextHedgeTime = DateTime.MinValue;
_lastHedgedDelta = null;
}
public bool ShouldHedge(DateTime currentTime)
{
// Check if enough time has passed since the last hedge.
return currentTime >= _nextHedgeTime;
}
public decimal ComputeNetDelta(QCAlgorithm algorithm, IEnumerable<Leg> legs, decimal underlyingQuantity, decimal contractDelta)
{
// Calculate the net portfolio delta by combining underlying and option positions.
return underlyingQuantity + legs.Sum(leg =>
algorithm.Securities.TryGetValue(leg.Symbol, out var sec)
? sec.Holdings.Quantity * contractDelta * 100
: 0m);
}
public int? GetQuantity(decimal netDelta)
{
if (_lastHedgedDelta.HasValue
&& Math.Abs(netDelta - _lastHedgedDelta.Value) < _rehedgeBand
&& Math.Abs(netDelta) < _minShares + _rehedgeBand)
{
return null;
}
// Calculate the required hedge quantity to neutralize the net delta.
var hedgeQty = (int)Math.Round(-netDelta);
if (Math.Abs(hedgeQty) < _minShares) return null;
// Return the calculated hedge quantity.
return hedgeQty;
}
public void RecordHedge(DateTime currentTime, decimal netDelta)
{
// Record the delta value after this hedge operation.
_lastHedgedDelta = netDelta;
// Set the next allowed hedge time based on the configured delay.
_nextHedgeTime = currentTime + _hedgeDelay;
}
}
}