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