Overall Statistics
Total Orders
256
Average Win
10.85%
Average Loss
-8.91%
Compounding Annual Return
57.508%
Drawdown
47.400%
Expectancy
0.215
Start Equity
30000
End Equity
42190.2
Net Profit
40.634%
Sharpe Ratio
1.012
Sortino Ratio
0.969
Probabilistic Sharpe Ratio
41.314%
Loss Rate
45%
Win Rate
55%
Profit-Loss Ratio
1.22
Alpha
0.223
Beta
2.383
Annual Standard Deviation
0.458
Annual Variance
0.21
Information Ratio
1.194
Tracking Error
0.304
Treynor Ratio
0.195
Total Fees
$153.50
Estimated Strategy Capacity
$0
Lowest Capacity Asset
SPY YVXOP93Y1PGM|SPY R735QTJ8XC9X
Portfolio Turnover
65.76%
Drawdown Recovery
153
using QuantConnect.Orders;
using QuantConnect.Securities;

namespace QuantConnect.Algorithm.CSharp
{
    // Variant 1 from the support ticket: a custom BuyingPowerModel that approves every
    // order and nothing else. This is the FIRST thing I tried and it did not help - QC
    // still charged maintenance margin against open positions in the background and
    // eventually blocked new combo orders anyway. Word-for-word the same override that
    // is live in MayaOPTIONS-LIVE_TradeStation/Main.cs.
    public class AlwaysApproveBuyingPowerModel : BuyingPowerModel
    {
        public override HasSufficientBuyingPowerForOrderResult HasSufficientBuyingPowerForOrder(HasSufficientBuyingPowerForOrderParameters parameters)
        {
            return new HasSufficientBuyingPowerForOrderResult(true);
        }
    }

    // Variant 2 from the support ticket: same always-true approval, PLUS zeroing out
    // GetInitialMarginRequiredForOrder / GetMaintenanceMargin / GetReservedBuyingPowerForPosition
    // and reporting decimal.MaxValue buying power. This is the exact model I run LIVE
    // (Library/libCommon/Misc/CustomBuyingPowerModel.cs) - it works fine there, but does
    // not fix the backtest insolvency.
    public class AlwaysApproveZeroMarginBuyingPowerModel : BuyingPowerModel
    {
        public override HasSufficientBuyingPowerForOrderResult HasSufficientBuyingPowerForOrder(HasSufficientBuyingPowerForOrderParameters parameters)
        {
            return new HasSufficientBuyingPowerForOrderResult(true);
        }

        public override BuyingPower GetBuyingPower(BuyingPowerParameters parameters)
        {
            return new BuyingPower(decimal.MaxValue);
        }

        public override InitialMargin GetInitialMarginRequiredForOrder(InitialMarginRequiredForOrderParameters parameters)
        {
            return new InitialMargin(0m);
        }

        public override MaintenanceMargin GetMaintenanceMargin(MaintenanceMarginParameters parameters)
        {
            return new MaintenanceMargin(0m);
        }

        public override ReservedBuyingPowerForPosition GetReservedBuyingPowerForPosition(ReservedBuyingPowerForPositionParameters parameters)
        {
            return new ReservedBuyingPowerForPosition(0m);
        }
    }

    // Variant 3 from the support ticket needs no custom class - it is just:
    //   option.SetBuyingPowerModel(QuantConnect.Securities.BuyingPowerModel.Null);
    //   option.SetMarginModel(QuantConnect.Securities.SecurityMarginModel.Null);
    // (wired up as a commented-out block in Main.cs's Initialize()).
}
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Data;
using QuantConnect.Orders;
using QuantConnect.Securities.Option;

namespace QuantConnect.Algorithm.CSharp
{
    // Minimal repro for QC support (Bronze ticket, ref project 33904557 / backtest
    // 1c91a751539073afe6dabd67133d00c0): ComboMarketOrder does not appear to be checked
    // against buying power in backtest mode the way single-leg orders are.
    //
    // No custom BuyingPowerModel or MarginModel is set anywhere in this file. Everything
    // here uses QuantConnect's stock defaults for an equity option. A new $5-wide SPY ATM
    // bull call debit spread (long 1 / short 1) is opened every trading day and never
    // closed, starting from $30,000 cash, with no other risk controls.
    //
    // Expected: buying power should run out after a handful of spreads and new
    // ComboMarketOrder calls should get rejected (or the account should hit a margin call
    // event) once required margin exceeds account equity.
    //
    // Actual: orders keep filling regardless of available buying power and
    // Portfolio.Cash / Portfolio.TotalPortfolioValue go deeply negative.
    public class ComboOrderMarginRepro : QCAlgorithm
    {
        private Symbol _optionSymbol;
        private DateTime _lastTradeDate = DateTime.MinValue;

        private const int ContractsPerEntry = 1; // matches the real algo: 1 contract per trade, fixed ~$200-250 buying power per trade

        public override void Initialize()
        {
            SetStartDate(2025, 1, 1);
            SetEndDate(2025, 10, 1);
            SetCash(30000);

            AddEquity("SPY", Resolution.Minute);

            var option = AddOption("SPY", Resolution.Minute);
            _optionSymbol = option.Symbol;
            // Wide window + weeklys so there is always a candidate expiry in range every single
            // day (a narrower monthly-only window left multi-week gaps with zero valid contracts,
            // which was silently starving entries for reasons unrelated to buying power).
            option.SetFilter(u => u.IncludeWeeklys().Strikes(-10, 10).Expiration(20, 45));

            // ===============================================================
            // BUYING POWER MODEL VARIANTS FROM THE SUPPORT TICKET
            // Exactly one of the four should be active at a time. Nothing is
            // uncommented below, so the DEFAULT variant runs: QC's stock option
            // margin model, no override at all. That already reproduces the bug
            // with zero custom code (see BuyingPowerDiag chart / order log).
            // To try one of the other three I described attempting live,
            // uncomment ONE block below and comment the others back out.
            // ===============================================================

            // --- VARIANT 1: custom model, approves every order, nothing else ---
            // option.SetBuyingPowerModel(new AlwaysApproveBuyingPowerModel());

            // --- VARIANT 2 (what I run LIVE): approves every order AND zeroes
            //     out initial/maintenance/reserved margin + reports MaxValue
            //     buying power. Works fine live, does not fix the backtest. ---
            // option.SetBuyingPowerModel(new AlwaysApproveZeroMarginBuyingPowerModel());

            // --- VARIANT 3: infinite buying power, no margin model at all ---
            // option.SetBuyingPowerModel(QuantConnect.Securities.BuyingPowerModel.Null);
            // option.SetMarginModel(QuantConnect.Securities.SecurityMarginModel.Null);
        }

        public override void OnData(Slice slice)
        {
            if (Time.Date == _lastTradeDate.Date) return;
            if (!slice.OptionChains.TryGetValue(_optionSymbol, out var chain)) return;

            var calls = chain.Where(c => c.Right == OptionRight.Call).ToList();
            if (calls.Count == 0) return;

            var nearestExpiry = calls.Min(c => c.Expiry);
            var callsAtExpiry = calls.Where(c => c.Expiry == nearestExpiry).OrderBy(c => c.Strike).ToList();

            var underlyingPrice = chain.Underlying.Price;
            var longCall = callsAtExpiry.OrderBy(c => Math.Abs(c.Strike - underlyingPrice)).FirstOrDefault();
            if (longCall == null) return;

            var shortCall = callsAtExpiry.FirstOrDefault(c => c.Strike == longCall.Strike + 5m);
            if (shortCall == null) return;

            _lastTradeDate = Time;

            Log($"[PRE-ORDER] {Time:yyyy-MM-dd} Cash={Portfolio.Cash:F2} " +
                $"TotalPortfolioValue={Portfolio.TotalPortfolioValue:F2} " +
                $"MarginRemaining={Portfolio.MarginRemaining:F2} " +
                $"TotalMarginUsed={Portfolio.TotalMarginUsed:F2}");

            var legs = new List<Leg>
            {
                Leg.Create(longCall.Symbol, 1),
                Leg.Create(shortCall.Symbol, -1)
            };

            var tickets = ComboMarketOrder(legs, ContractsPerEntry, false, "ReproSpread");
            foreach (var t in tickets)
            {
                Log($"[ORDER] Id={t.OrderId} Status={t.Status} Symbol={t.Symbol} Qty={t.Quantity}");
            }

            // Daily account-health snapshot, independent of whether an order was attempted -
            // this is what makes the insolvency (or lack thereof) visible via the chart API,
            // since the backtests/log endpoint is not available for pulling Debug()/Log() text.
            Plot("BuyingPowerDiag", "Cash", Portfolio.Cash);
            Plot("BuyingPowerDiag", "TotalPortfolioValue", Portfolio.TotalPortfolioValue);
            Plot("BuyingPowerDiag", "TotalHoldingsValue", Portfolio.TotalHoldingsValue);
            Plot("BuyingPowerDiag", "MarginRemaining", Portfolio.MarginRemaining);
        }

        public override void OnOrderEvent(OrderEvent orderEvent)
        {
            Log($"[ORDER_EVENT] {orderEvent}");
        }

        public override void OnEndOfAlgorithm()
        {
            Log($"[FINAL] Cash={Portfolio.Cash:F2} TotalPortfolioValue={Portfolio.TotalPortfolioValue:F2} " +
                $"TotalMarginUsed={Portfolio.TotalMarginUsed:F2} TotalOrders={Transactions.GetOrders().Count()}");
        }
    }
}