Overall Statistics
Total Orders
177
Average Win
0.22%
Average Loss
-0.14%
Compounding Annual Return
-1.454%
Drawdown
1.600%
Expectancy
-0.034
Start Equity
1000000
End Equity
995129.50
Net Profit
-0.487%
Sharpe Ratio
-2.108
Sortino Ratio
-1.638
Probabilistic Sharpe Ratio
17.522%
Loss Rate
62%
Win Rate
38%
Profit-Loss Ratio
1.54
Alpha
-0.071
Beta
0.123
Annual Standard Deviation
0.031
Annual Variance
0.001
Information Ratio
-1.183
Tracking Error
0.097
Treynor Ratio
-0.525
Total Fees
$285.60
Estimated Strategy Capacity
$450000.00
Lowest Capacity Asset
SPY 32NVPQ72H2SVA|SPY R735QTJ8XC9X
Portfolio Turnover
13.29%
Drawdown Recovery
39
#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(1_000_000);
        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.
        var qty = (int)(Portfolio.TotalPortfolioValue * _straddleWeight / (_spy.Price * 100m));
        if (qty > 0)
            Buy(_shortStraddle, qty, 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[leg.Symbol].Holdings.Quantity * contractDelta * 100);
    }

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


}