From 2b3e4aad037ba595428126887db84b78cf69fe1a Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 21 Jan 2026 18:15:08 -0400 Subject: [PATCH 1/4] Fix crypto future margin model to reflect margin used --- .../BasicTemplateCryptoFutureAlgorithm.cs | 9 +- ...asicTemplateCryptoFutureHourlyAlgorithm.cs | 7 +- .../BybitCryptoFuturesRegressionAlgorithm.cs | 9 +- ...eLeverageBasedMarginRegressionAlgorithm.cs | 170 ++++++++++++++++++ .../BasicTemplateCryptoFutureAlgorithm.py | 4 +- ...asicTemplateCryptoFutureHourlyAlgorithm.py | 2 +- .../BybitCryptoFuturesRegressionAlgorithm.py | 7 +- .../CryptoFuture/CryptoFutureMarginModel.cs | 12 +- 8 files changed, 185 insertions(+), 35 deletions(-) create mode 100644 Algorithm.CSharp/CryptoFutureLeverageBasedMarginRegressionAlgorithm.cs diff --git a/Algorithm.CSharp/BasicTemplateCryptoFutureAlgorithm.cs b/Algorithm.CSharp/BasicTemplateCryptoFutureAlgorithm.cs index bf375e5302d1..df1646052c75 100644 --- a/Algorithm.CSharp/BasicTemplateCryptoFutureAlgorithm.cs +++ b/Algorithm.CSharp/BasicTemplateCryptoFutureAlgorithm.cs @@ -119,9 +119,7 @@ public override void OnData(Slice slice) { throw new RegressionTestException($"Unexpected holdings cost {btcUsdHoldings.HoldingsCost}"); } - // margin used is based on the maintenance rate - if (Math.Abs(btcUsdHoldings.AbsoluteHoldingsCost * 0.05m - marginUsed) > 1 - || _btcUsd.BuyingPowerModel.GetMaintenanceMargin(_btcUsd) != marginUsed) + if (_btcUsd.BuyingPowerModel.GetMaintenanceMargin(_btcUsd) != marginUsed) { throw new RegressionTestException($"Unexpected margin used {marginUsed}"); } @@ -142,8 +140,7 @@ public override void OnData(Slice slice) { throw new RegressionTestException($"Unexpected holdings cost {adaUsdtHoldings.HoldingsCost}"); } - if (Math.Abs(adaUsdtHoldings.AbsoluteHoldingsCost * 0.05m - marginUsed) > 1 - || _adaUsdt.BuyingPowerModel.GetMaintenanceMargin(_adaUsdt) != marginUsed) + if (_adaUsdt.BuyingPowerModel.GetMaintenanceMargin(_adaUsdt) != marginUsed) { throw new RegressionTestException($"Unexpected margin used {marginUsed}"); } @@ -273,7 +270,7 @@ public override void OnOrderEvent(OrderEvent orderEvent) {"Tracking Error", "0"}, {"Treynor Ratio", "0"}, {"Total Fees", "$0.65"}, - {"Estimated Strategy Capacity", "$500000000.00"}, + {"Estimated Strategy Capacity", "$620000000.00"}, {"Lowest Capacity Asset", "ADAUSDT 18R"}, {"Portfolio Turnover", "0.16%"}, {"Drawdown Recovery", "0"}, diff --git a/Algorithm.CSharp/BasicTemplateCryptoFutureHourlyAlgorithm.cs b/Algorithm.CSharp/BasicTemplateCryptoFutureHourlyAlgorithm.cs index 811039363424..deea16fff636 100644 --- a/Algorithm.CSharp/BasicTemplateCryptoFutureHourlyAlgorithm.cs +++ b/Algorithm.CSharp/BasicTemplateCryptoFutureHourlyAlgorithm.cs @@ -93,7 +93,7 @@ public override void OnData(Slice slice) if (!Portfolio.Invested && Transactions.OrdersCount == 0) { var ticket = Buy(_adaUsdt.Symbol, 100000); - if(ticket.Status != OrderStatus.Invalid) + if (ticket.Status != OrderStatus.Invalid) { throw new RegressionTestException($"Unexpected valid order {ticket}, should fail due to margin not sufficient"); } @@ -114,8 +114,7 @@ public override void OnData(Slice slice) { throw new RegressionTestException($"Unexpected holdings cost {adaUsdtHoldings.HoldingsCost}"); } - if (Math.Abs(adaUsdtHoldings.AbsoluteHoldingsCost * 0.05m - marginUsed) > 1 - || _adaUsdt.BuyingPowerModel.GetMaintenanceMargin(_adaUsdt) != marginUsed) + if (_adaUsdt.BuyingPowerModel.GetMaintenanceMargin(_adaUsdt) != marginUsed) { throw new RegressionTestException($"Unexpected margin used {marginUsed}"); } @@ -236,7 +235,7 @@ public override void OnOrderEvent(OrderEvent orderEvent) {"Tracking Error", "0"}, {"Treynor Ratio", "0"}, {"Total Fees", "$0.61"}, - {"Estimated Strategy Capacity", "$370000000.00"}, + {"Estimated Strategy Capacity", "$460000000.00"}, {"Lowest Capacity Asset", "ADAUSDT 18R"}, {"Portfolio Turnover", "0.12%"}, {"Drawdown Recovery", "0"}, diff --git a/Algorithm.CSharp/BybitCryptoFuturesRegressionAlgorithm.cs b/Algorithm.CSharp/BybitCryptoFuturesRegressionAlgorithm.cs index ccda05848fda..6ce6ac6a6880 100644 --- a/Algorithm.CSharp/BybitCryptoFuturesRegressionAlgorithm.cs +++ b/Algorithm.CSharp/BybitCryptoFuturesRegressionAlgorithm.cs @@ -112,9 +112,7 @@ public override void OnData(Slice slice) { throw new RegressionTestException($"Unexpected holdings cost {btcUsdHoldings.HoldingsCost}"); } - // margin used is based on the maintenance rate - if (Math.Abs(btcUsdHoldings.AbsoluteHoldingsCost * 0.05m - marginUsed) > 1 - || _btcUsd.BuyingPowerModel.GetMaintenanceMargin(_btcUsd) != marginUsed) + if (_btcUsd.BuyingPowerModel.GetMaintenanceMargin(_btcUsd) != marginUsed) { throw new RegressionTestException($"Unexpected margin used {marginUsed}"); } @@ -135,8 +133,7 @@ public override void OnData(Slice slice) { throw new RegressionTestException($"Unexpected holdings cost {btcUsdtHoldings.HoldingsCost}"); } - if (Math.Abs(btcUsdtHoldings.AbsoluteHoldingsCost * 0.05m - marginUsed) > 1 - || _btcUsdt.BuyingPowerModel.GetMaintenanceMargin(_btcUsdt) != marginUsed) + if (_btcUsdt.BuyingPowerModel.GetMaintenanceMargin(_btcUsdt) != marginUsed) { throw new RegressionTestException($"Unexpected margin used {marginUsed}"); } @@ -261,7 +258,7 @@ public override void OnEndOfAlgorithm() {"Tracking Error", "0"}, {"Treynor Ratio", "0"}, {"Total Fees", "$0.60"}, - {"Estimated Strategy Capacity", "$200000000.00"}, + {"Estimated Strategy Capacity", "$100000000.00"}, {"Lowest Capacity Asset", "BTCUSDT 2V3"}, {"Portfolio Turnover", "1.08%"}, {"Drawdown Recovery", "0"}, diff --git a/Algorithm.CSharp/CryptoFutureLeverageBasedMarginRegressionAlgorithm.cs b/Algorithm.CSharp/CryptoFutureLeverageBasedMarginRegressionAlgorithm.cs new file mode 100644 index 000000000000..fbe81dbee969 --- /dev/null +++ b/Algorithm.CSharp/CryptoFutureLeverageBasedMarginRegressionAlgorithm.cs @@ -0,0 +1,170 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using QuantConnect.Brokerages; +using QuantConnect.Data; +using QuantConnect.Interfaces; +using QuantConnect.Securities.CryptoFuture; +using System.Collections.Generic; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm asserting that margin used and margin remaining update correctly when + /// changing leverage on a crypto future + /// + public class CryptoFutureLeverageBasedMarginRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private CryptoFuture _cryptoFuture; + + public override void Initialize() + { + SetStartDate(2022, 12, 13); + SetEndDate(2022, 12, 13); + + SetTimeZone(TimeZones.Utc); + + SetAccountCurrency("USDT"); + SetCash(200); + + SetBrokerageModel(BrokerageName.BinanceFutures, AccountType.Margin); + + _cryptoFuture = AddCryptoFuture("ADAUSDT"); + _cryptoFuture.SetLeverage(10); + } + + public override void OnData(Slice slice) + { + if (_cryptoFuture.Price == 0) + { + return; + } + + if (!Portfolio.Invested) + { + SetHoldings(_cryptoFuture.Symbol, 10); // Buy all we can with our margin (leverage is 10) + + var marginUsed = Portfolio.TotalMarginUsed; + var marginRemaining = Portfolio.MarginRemaining; + + if (marginRemaining > 0) + { + throw new RegressionTestException($"Expected no margin remaining after buying with full leverage. " + + $"Actual margin remaining is {marginRemaining}"); + } + + _cryptoFuture.SetLeverage(20); + + var newMarginUsed = Portfolio.TotalMarginUsed; + var newMarginRemaining = Portfolio.MarginRemaining; + + if (newMarginUsed >= marginUsed) + { + throw new RegressionTestException($"Expected margin used to decrease after increasing leverage. " + + $"Previous margin used: {marginUsed}, new margin used: {newMarginUsed}"); + } + + if (newMarginRemaining <= 0 || newMarginRemaining <= marginRemaining) + { + throw new RegressionTestException($"Expected margin remaining to increase after increasing leverage. " + + $"Previous margin remaining: {marginRemaining}, new margin remaining: {newMarginRemaining}"); + } + + var holdingsQuantity = _cryptoFuture.Holdings.AbsoluteQuantity; + + SetHoldings(_cryptoFuture.Symbol, 20); // Buy all we can with our margin (new leverage is 20) + + var newHoldingsQuantity = _cryptoFuture.Holdings.AbsoluteQuantity; + + if (newHoldingsQuantity <= holdingsQuantity) + { + throw new RegressionTestException($"Expected holdings quantity to increase after increasing leverage and buying more. " + + $"Previous holdings quantity: {holdingsQuantity}, new holdings quantity: {newHoldingsQuantity}"); + } + + newMarginRemaining = Portfolio.MarginRemaining; + + if (marginRemaining > 0) + { + throw new RegressionTestException($"Expected no margin remaining after buying with full leverage. " + + $"Actual margin remaining is {newMarginRemaining}"); + } + + // We are done testing, exit the algorithm + Quit(); + } + } + + /// + /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. + /// + public bool CanRunLocally { get; } = true; + + /// + /// This is used by the regression test system to indicate which languages this algorithm is written in. + /// + public List Languages { get; } = new() { Language.CSharp }; + + /// + /// Data Points count of all timeslices of algorithm + /// + public long DataPoints => 4; + + /// + /// Data Points count of the algorithm history + /// + public int AlgorithmHistoryDataPoints => 0; + + /// + /// Final status of the algorithm + /// + public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed; + + /// + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm + /// + public Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "2"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0%"}, + {"Drawdown", "0%"}, + {"Expectancy", "0"}, + {"Start Equity", "200"}, + {"End Equity", "195.58"}, + {"Net Profit", "0%"}, + {"Sharpe Ratio", "0"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "0%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "0"}, + {"Beta", "0"}, + {"Annual Standard Deviation", "0"}, + {"Annual Variance", "0"}, + {"Information Ratio", "0"}, + {"Tracking Error", "0"}, + {"Treynor Ratio", "0"}, + {"Total Fees", "₮1.57"}, + {"Estimated Strategy Capacity", "₮0"}, + {"Lowest Capacity Asset", "ADAUSDT 18R"}, + {"Portfolio Turnover", "2009.51%"}, + {"Drawdown Recovery", "0"}, + {"OrderListHash", "f92ad762f77fbf4ee13b1e89a78cb1eb"} + }; + } +} diff --git a/Algorithm.Python/BasicTemplateCryptoFutureAlgorithm.py b/Algorithm.Python/BasicTemplateCryptoFutureAlgorithm.py index c1949da1c55f..58e5150fa43f 100644 --- a/Algorithm.Python/BasicTemplateCryptoFutureAlgorithm.py +++ b/Algorithm.Python/BasicTemplateCryptoFutureAlgorithm.py @@ -82,7 +82,7 @@ def on_data(self, slice): raise AssertionError(f"Unexpected holdings cost {self.btc_usd_holdings.holdings_cost}") # margin used is based on the maintenance rate - if (abs(self.btc_usd_holdings.absolute_holdings_cost * 0.05 - self.margin_used) > 1) or (BuyingPowerModelExtensions.get_maintenance_margin(self.btc_usd.buying_power_model, self.btc_usd) != self.margin_used): + if BuyingPowerModelExtensions.get_maintenance_margin(self.btc_usd.buying_power_model, self.btc_usd) != self.margin_used: raise AssertionError(f"Unexpected margin used {self.margin_used}") self.buy(self.ada_usdt.symbol, 1000) @@ -99,7 +99,7 @@ def on_data(self, slice): if abs(self.ada_usdt_holdings.absolute_holdings_cost - self.holdings_value_usdt) > 1: raise AssertionError(f"Unexpected holdings cost {self.ada_usdt_holdings.holdings_cost}") - if (abs(self.ada_usdt_holdings.absolute_holdings_cost * 0.05 - self.margin_used) > 1) or (BuyingPowerModelExtensions.get_maintenance_margin(self.ada_usdt.buying_power_model, self.ada_usdt) != self.margin_used): + if BuyingPowerModelExtensions.get_maintenance_margin(self.ada_usdt.buying_power_model, self.ada_usdt) != self.margin_used: raise AssertionError(f"Unexpected margin used {self.margin_used}") # position just opened should be just spread here diff --git a/Algorithm.Python/BasicTemplateCryptoFutureHourlyAlgorithm.py b/Algorithm.Python/BasicTemplateCryptoFutureHourlyAlgorithm.py index 29cf8ceec6cb..7bc4efc55ae3 100644 --- a/Algorithm.Python/BasicTemplateCryptoFutureHourlyAlgorithm.py +++ b/Algorithm.Python/BasicTemplateCryptoFutureHourlyAlgorithm.py @@ -81,7 +81,7 @@ def on_data(self, slice): if abs(self.ada_usdt_holdings.absolute_holdings_cost - self.holdings_value_usdt) > 1: raise AssertionError(f"Unexpected holdings cost {self.ada_usdt_holdings.holdings_cost}") - if (abs(self.ada_usdt_holdings.absolute_holdings_cost * 0.05 - self.margin_used) > 1) or (BuyingPowerModelExtensions.get_maintenance_margin(self.ada_usdt.buying_power_model, self.ada_usdt) != self.margin_used): + if BuyingPowerModelExtensions.get_maintenance_margin(self.ada_usdt.buying_power_model, self.ada_usdt) != self.margin_used: raise AssertionError(f"Unexpected margin used {self.margin_used}") # position just opened should be just spread here diff --git a/Algorithm.Python/BybitCryptoFuturesRegressionAlgorithm.py b/Algorithm.Python/BybitCryptoFuturesRegressionAlgorithm.py index 5bb33c48fc56..c5afb4f02d47 100644 --- a/Algorithm.Python/BybitCryptoFuturesRegressionAlgorithm.py +++ b/Algorithm.Python/BybitCryptoFuturesRegressionAlgorithm.py @@ -79,9 +79,7 @@ def on_data(self, data): raise AssertionError(f"Unexpected TotalSaleVolume {btc_usd_holdings.total_sale_volume}") if abs(btc_usd_holdings.absolute_holdings_cost - holdings_value_btc_usd) > 1: raise AssertionError(f"Unexpected holdings cost {btc_usd_holdings.holdings_cost}") - # margin used is based on the maintenance rate - if (abs(btc_usd_holdings.absolute_holdings_cost * 0.05 - margin_used) > 1 or - not isclose(self.btc_usd.buying_power_model.get_maintenance_margin(MaintenanceMarginParameters.for_current_holdings(self.btc_usd)).value, margin_used)): + if not isclose(self.btc_usd.buying_power_model.get_maintenance_margin(MaintenanceMarginParameters.for_current_holdings(self.btc_usd)).value, margin_used): raise AssertionError(f"Unexpected margin used {margin_used}") self.buy(self.btc_usdt.symbol, 0.01) @@ -96,8 +94,7 @@ def on_data(self, data): raise AssertionError(f"Unexpected TotalSaleVolume {btc_usdt_holdings.total_sale_volume}") if abs(btc_usdt_holdings.absolute_holdings_cost - holdings_value_usdt) > 1: raise AssertionError(f"Unexpected holdings cost {btc_usdt_holdings.holdings_cost}") - if (abs(btc_usdt_holdings.absolute_holdings_cost * 0.05 - margin_used) > 1 or - not isclose(self.btc_usdt.buying_power_model.get_maintenance_margin(MaintenanceMarginParameters.for_current_holdings(self.btc_usdt)).value, margin_used)): + if not isclose(self.btc_usdt.buying_power_model.get_maintenance_margin(MaintenanceMarginParameters.for_current_holdings(self.btc_usdt)).value, margin_used): raise AssertionError(f"Unexpected margin used {margin_used}") # position just opened should be just spread here diff --git a/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs b/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs index 10bb425746af..efbf9b856a0e 100644 --- a/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs +++ b/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs @@ -47,17 +47,7 @@ public CryptoFutureMarginModel(decimal leverage = 25, decimal maintenanceMarginR /// The maintenance margin required for the option public override MaintenanceMargin GetMaintenanceMargin(MaintenanceMarginParameters parameters) { - var security = parameters.Security; - var quantity = parameters.Quantity; - if (security?.GetLastData() == null || quantity == 0m) - { - return MaintenanceMargin.Zero; - } - - var positionValue = security.Holdings.GetQuantityValue(quantity, security.Price); - var marginRequirementInCollateral = Math.Abs(positionValue.Amount) * _maintenanceMarginRate - _maintenanceAmount; - - return new MaintenanceMargin(marginRequirementInCollateral * positionValue.Cash.ConversionRate); + return new MaintenanceMargin(GetInitialMarginRequirement(new InitialMarginParameters(parameters.Security, parameters.Quantity))); } /// From e671082f33bae8fe233c455aa765f828addeb6ea Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 22 Jan 2026 10:39:50 -0400 Subject: [PATCH 2/4] Minor tests fixes --- .../CryptoFutureDailyMarginInterestRegressionAlgorithm.cs | 2 +- .../CryptoFutureHourlyMarginInterestRegressionAlgorithm.cs | 2 +- .../CryptoFuture/BybitCryptoFutureMarginModelTests.cs | 2 ++ .../Securities/CryptoFuture/CryptoFutureMarginModelTests.cs | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Algorithm.CSharp/CryptoFutureDailyMarginInterestRegressionAlgorithm.cs b/Algorithm.CSharp/CryptoFutureDailyMarginInterestRegressionAlgorithm.cs index 96f4b9667dc6..eea2b58ed588 100644 --- a/Algorithm.CSharp/CryptoFutureDailyMarginInterestRegressionAlgorithm.cs +++ b/Algorithm.CSharp/CryptoFutureDailyMarginInterestRegressionAlgorithm.cs @@ -63,7 +63,7 @@ public override void Initialize() {"Tracking Error", "0"}, {"Treynor Ratio", "0"}, {"Total Fees", "$0.15"}, - {"Estimated Strategy Capacity", "$3400000000.00"}, + {"Estimated Strategy Capacity", "$4300000000.00"}, {"Lowest Capacity Asset", "ADAUSDT 18R"}, {"Portfolio Turnover", "0.02%"}, {"Drawdown Recovery", "0"}, diff --git a/Algorithm.CSharp/CryptoFutureHourlyMarginInterestRegressionAlgorithm.cs b/Algorithm.CSharp/CryptoFutureHourlyMarginInterestRegressionAlgorithm.cs index c82bf7096a9c..f39159e591e5 100644 --- a/Algorithm.CSharp/CryptoFutureHourlyMarginInterestRegressionAlgorithm.cs +++ b/Algorithm.CSharp/CryptoFutureHourlyMarginInterestRegressionAlgorithm.cs @@ -167,7 +167,7 @@ public override void OnOrderEvent(OrderEvent orderEvent) {"Tracking Error", "0"}, {"Treynor Ratio", "0"}, {"Total Fees", "$0.15"}, - {"Estimated Strategy Capacity", "$330000000.00"}, + {"Estimated Strategy Capacity", "$410000000.00"}, {"Lowest Capacity Asset", "ADAUSDT 18R"}, {"Portfolio Turnover", "0.02%"}, {"Drawdown Recovery", "0"}, diff --git a/Tests/Common/Securities/CryptoFuture/BybitCryptoFutureMarginModelTests.cs b/Tests/Common/Securities/CryptoFuture/BybitCryptoFutureMarginModelTests.cs index 99b0b78c55ed..e517b36bb479 100644 --- a/Tests/Common/Securities/CryptoFuture/BybitCryptoFutureMarginModelTests.cs +++ b/Tests/Common/Securities/CryptoFuture/BybitCryptoFutureMarginModelTests.cs @@ -62,6 +62,8 @@ public void BybitInitialMarginRequirement(string ticker, decimal quantity, decim [TestCase("BTCUSD", -15000, 0.005, 75)] [TestCase("BTCUSD", 15000, 0.02, 300)] // Margin rate 2% [TestCase("BTCUSD", -15000, 0.02, 300)] + [Ignore("Maintenance margin calculation in Lean differs from the brokerage's one for crypto futures. " + + "In Lean this is the margin used by an open position, which is not the same as the maintenance margin in the brokerages.")] public void BybitMaintenanceMargin(string ticker, decimal quantity, decimal marginRate, decimal expectedMargin) { var algo = GetAlgorithm(); diff --git a/Tests/Common/Securities/CryptoFuture/CryptoFutureMarginModelTests.cs b/Tests/Common/Securities/CryptoFuture/CryptoFutureMarginModelTests.cs index 47b88f9aeb06..ed586d608b04 100644 --- a/Tests/Common/Securities/CryptoFuture/CryptoFutureMarginModelTests.cs +++ b/Tests/Common/Securities/CryptoFuture/CryptoFutureMarginModelTests.cs @@ -68,6 +68,8 @@ public void InitialMarginRequirement(string ticker, decimal quantity) [TestCase("BTCUSDT", 10)] [TestCase("BTCUSD", -10)] [TestCase("BTCUSDT", -10)] + [Ignore("Maintenance margin calculation in Lean differs from the brokerage's one for crypto futures. " + + "In Lean this is the margin used by an open position, which is not the same as the maintenance margin in the brokerages.")] public void GetMaintenanceMargin(string ticker, decimal quantity) { var algo = GetAlgorithm(); From da255aedd67d28df36e0bebd79f03b35c1e9d0c6 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 26 Jan 2026 12:49:34 -0400 Subject: [PATCH 3/4] Cleanup --- .../CryptoFuture/CryptoFutureMarginModel.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs b/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs index efbf9b856a0e..0b6ce4528adf 100644 --- a/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs +++ b/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs @@ -24,20 +24,25 @@ namespace QuantConnect.Securities.CryptoFuture /// public class CryptoFutureMarginModel : SecurityMarginModel { - private readonly decimal _maintenanceMarginRate; - private readonly decimal _maintenanceAmount; - /// /// Creates a new instance /// /// The leverage to use, used on initial margin requirements, default 25x /// The maintenance margin rate, default 5% /// The maintenance amount which will reduce maintenance margin requirements, default 0 - public CryptoFutureMarginModel(decimal leverage = 25, decimal maintenanceMarginRate = 0.05m, decimal maintenanceAmount = 0) + [Obsolete("This constructor is deprecated, please use the overload without maintenanceMarginRate and maintenanceAmount parameters.")] + public CryptoFutureMarginModel(decimal leverage, decimal maintenanceMarginRate = 0.05m, decimal maintenanceAmount = 0) + : base(leverage, 0) + { + } + + /// + /// Creates a new instance + /// + /// The leverage to use, used on initial margin requirements, default 25x + public CryptoFutureMarginModel(decimal leverage = 25) : base(leverage, 0) { - _maintenanceAmount = maintenanceAmount; - _maintenanceMarginRate = maintenanceMarginRate; } /// From 34f9792b8e888bf948df4ce912b3076a0df7c94d Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 27 Jan 2026 13:40:08 -0400 Subject: [PATCH 4/4] Cleanup --- .../BybitCryptoFutureMarginModelTests.cs | 24 -------------- .../CryptoFutureMarginModelTests.cs | 33 ------------------- 2 files changed, 57 deletions(-) diff --git a/Tests/Common/Securities/CryptoFuture/BybitCryptoFutureMarginModelTests.cs b/Tests/Common/Securities/CryptoFuture/BybitCryptoFutureMarginModelTests.cs index e517b36bb479..e6cfc2dca4bb 100644 --- a/Tests/Common/Securities/CryptoFuture/BybitCryptoFutureMarginModelTests.cs +++ b/Tests/Common/Securities/CryptoFuture/BybitCryptoFutureMarginModelTests.cs @@ -54,30 +54,6 @@ public void BybitInitialMarginRequirement(string ticker, decimal quantity, decim Assert.AreEqual((double)expectedMargin, (double)result.Value, (double)(0.05m * expectedMargin)); } - [TestCase("BTCUSDT", 0.5, 0.005, 87)] // Bybit value: 86.69. Margin rate 0.5% - [TestCase("BTCUSDT", -0.5, 0.005, 87)] - [TestCase("BTCUSDT", 0.5, 0.02, 320)] // Bybit value: 323.2. Margin rate 2% - [TestCase("BTCUSDT", -0.5, 0.02, 320)] - [TestCase("BTCUSD", 15000, 0.005, 75)] // Margin rate 0.5% - [TestCase("BTCUSD", -15000, 0.005, 75)] - [TestCase("BTCUSD", 15000, 0.02, 300)] // Margin rate 2% - [TestCase("BTCUSD", -15000, 0.02, 300)] - [Ignore("Maintenance margin calculation in Lean differs from the brokerage's one for crypto futures. " + - "In Lean this is the margin used by an open position, which is not the same as the maintenance margin in the brokerages.")] - public void BybitMaintenanceMargin(string ticker, decimal quantity, decimal marginRate, decimal expectedMargin) - { - var algo = GetAlgorithm(); - var cryptoFuture = algo.AddCryptoFuture(ticker); - cryptoFuture.SetBuyingPowerModel(new CryptoFutureMarginModel(25m, marginRate)); - SetPrice(cryptoFuture, 31300); - cryptoFuture.Holdings.SetHoldings(0.5m, quantity); - - var parameters = MaintenanceMarginParameters.ForCurrentHoldings(cryptoFuture); - var result = cryptoFuture.BuyingPowerModel.GetMaintenanceMargin(parameters); - - Assert.AreEqual((double)expectedMargin, (double)result.Value, (double)(0.15m * expectedMargin)); - } - private static QCAlgorithm GetAlgorithm() { var algo = new AlgorithmStub(); diff --git a/Tests/Common/Securities/CryptoFuture/CryptoFutureMarginModelTests.cs b/Tests/Common/Securities/CryptoFuture/CryptoFutureMarginModelTests.cs index ed586d608b04..f0b38193ac1b 100644 --- a/Tests/Common/Securities/CryptoFuture/CryptoFutureMarginModelTests.cs +++ b/Tests/Common/Securities/CryptoFuture/CryptoFutureMarginModelTests.cs @@ -64,39 +64,6 @@ public void InitialMarginRequirement(string ticker, decimal quantity) Assert.AreEqual(Math.Abs(marginRequirement), result.Value); } - [TestCase("BTCUSD", 10)] - [TestCase("BTCUSDT", 10)] - [TestCase("BTCUSD", -10)] - [TestCase("BTCUSDT", -10)] - [Ignore("Maintenance margin calculation in Lean differs from the brokerage's one for crypto futures. " + - "In Lean this is the margin used by an open position, which is not the same as the maintenance margin in the brokerages.")] - public void GetMaintenanceMargin(string ticker, decimal quantity) - { - var algo = GetAlgorithm(); - var cryptoFuture = algo.AddCryptoFuture(ticker); - SetPrice(cryptoFuture, 16000); - // entry price 1000, shouldn't matter - cryptoFuture.Holdings.SetHoldings(1000, quantity); - - var parameters = MaintenanceMarginParameters.ForCurrentHoldings(cryptoFuture); - var result = cryptoFuture.BuyingPowerModel.GetMaintenanceMargin(parameters); - - decimal marginRequirement; - if (ticker == "BTCUSD") - { - // ((quantity * contract mutiplier * price) * MaintenanceMarginRate) * conversion rate (BTC -> USD) - marginRequirement = ((parameters.Quantity * 100m * cryptoFuture.Price) * 0.05m) * 1 / cryptoFuture.Price; - } - else - { - // ((quantity * contract mutiplier * price) * MaintenanceMarginRate) * conversion rate (USDT ~= USD) - marginRequirement = ((parameters.Quantity * 1m * cryptoFuture.Price) * 0.05m) * 1; - } - - Assert.AreEqual(Math.Abs(marginRequirement), result.Value); - } - - private static QCAlgorithm GetAlgorithm() { // Initialize algorithm