Skip to content

Commit 9b2a793

Browse files
authored
fix: support BNFCR alternative collateral in CryptoFutureMarginModel for Binance (#9373)
* fix: support BNFCR alternative collateral in CryptoFutureMarginModel for Binance (#9339) * refactor: use classic switch in BinanceFuturesBrokerageModel.GetBuyingPowerModel * fix: CryptoFutureMarginModel in BinanceCoinFuturesBrokerageModel * refactor: simplify BinanceCryptoFutureMarginModel collateral conversion * fix: restore IsCryptoCoinFuture guard in BinanceCryptoFutureMarginModel * refactor: simplify BinanceCryptoFutureMarginModel to direct BNFCR lookup Replace IsStableCoinWithoutPair foreach loop with a single TryGetValue check for BNFCR. BNFCR is EU/EEA-only (MiCA Credits Trading Mode) so the lookup is a no-op for all other users. Amount reflects availableBalance from the Binance API — the total cross-margin pool already aggregated by Binance — making the > 0 guard correct and avoiding CashBook iteration. * feat: add regression algorithm for BNFCR as sole collateral on Binance USDⓈ-M futures Asserts end-to-end that EU/MiCA accounts with zero USDT and BNFCR as the only collateral can open ADAUSDT positions. Verifies buying power, holdings AbsoluteHoldingsCost, TotalSaleVolume, TotalMarginUsed, maintenance margin consistency and TotalUnrealizedProfit accuracy. * refactor: BNFCR presence gate with CashBook iteration for supplementary collateral Replace IsStableCoinWithoutPair and hardcoded asset list with CashBook iteration gated by BNFCR presence. Binance controls which assets are in the account — all with non-zero walletBalance are valid collateral. Add tests for BNFCR zero balance and BTC collateral conversion. * refactor: aggregate all collateral without reference equality check * fix: shared collateral deduction across quote currencies for EU/MiCA BNFCR mode - Extract virtual SharesCollateral in CryptoFutureMarginModel - Override in BinanceCryptoFutureMarginModel: BNFCR present → all USDⓈ-M share pool - Add BNFCRCurrency const - Add SharedCollateralDeductsMaintenanceMarginAcrossQuoteCurrencies unit test - Refactor regression algorithm to assert shared collateral across ADAUSDT/ETHUSDC * refactor: remove dead IsCryptoCoinFuture guard from BinanceCryptoFutureMarginModel - Remove IsCryptoCoinFuture() check (coin futures use BinanceCoinFuturesBrokerageModel, not this model) - Call base.GetTotalCollateralAmount() instead of duplicating primaryCollateral.Amount - Remove CoinFutureDoesNotIncludeBnfcrAsCollateral test (tested wrong margin model) * refactor: make GetCollateralCash private in CryptoFutureMarginModel - No longer accessed by subclasses after removing direct collateral checks from BinanceCryptoFutureMarginModel
1 parent f0aefdb commit 9b2a793

6 files changed

Lines changed: 483 additions & 4 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System.Collections.Generic;
17+
using QuantConnect.Brokerages;
18+
using QuantConnect.Data;
19+
using QuantConnect.Data.Market;
20+
using QuantConnect.Interfaces;
21+
using QuantConnect.Orders;
22+
using QuantConnect.Securities;
23+
using QuantConnect.Securities.CryptoFuture;
24+
25+
namespace QuantConnect.Algorithm.CSharp
26+
{
27+
/// <summary>
28+
/// Regression algorithm asserting that BNFCR serves as collateral for Binance USDⓈ-M futures
29+
/// (EU/MiCA Credits Trading Mode) and that futures with different quote currencies (ADAUSDT, ETHUSDC)
30+
/// correctly share the BNFCR collateral pool.
31+
/// </summary>
32+
public class BinanceCryptoFutureBnfcrCollateralRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
33+
{
34+
private CryptoFuture _adaUsdt;
35+
private CryptoFuture _ethUsdc;
36+
private bool _orderPlaced;
37+
38+
public override void Initialize()
39+
{
40+
SetStartDate(2022, 12, 13);
41+
SetEndDate(2022, 12, 13);
42+
SetTimeZone(TimeZones.Utc);
43+
SetBrokerageModel(BrokerageName.BinanceFutures, AccountType.Margin);
44+
45+
_adaUsdt = AddCryptoFuture("ADAUSDT");
46+
_ethUsdc = AddCryptoFuture("ETHUSDC");
47+
48+
SetCash(0);
49+
SetCash("BNFCR", 200m, 1m);
50+
SetCash("ETH", 0, 1600);
51+
SetCash("USDC", 0, 1);
52+
}
53+
54+
public override void OnData(Slice slice)
55+
{
56+
if (_adaUsdt.Price == 0 || _orderPlaced)
57+
{
58+
return;
59+
}
60+
61+
// 1. BNFCR collateral must produce positive buying power (USDT is zero)
62+
var buyingPower = _adaUsdt.BuyingPowerModel.GetBuyingPower(new BuyingPowerParameters(Portfolio, _adaUsdt, OrderDirection.Buy));
63+
if (buyingPower.Value <= 0)
64+
{
65+
throw new RegressionTestException($"Expected positive buying power from BNFCR, got {buyingPower.Value}");
66+
}
67+
68+
// 2. Order must not be rejected
69+
var ticket = Buy(_adaUsdt.Symbol, 1000);
70+
_orderPlaced = true;
71+
if (ticket.Status == OrderStatus.Invalid)
72+
{
73+
throw new RegressionTestException("Order rejected — BNFCR collateral should cover margin");
74+
}
75+
76+
// 3. Margin must be tracked
77+
if (Portfolio.TotalMarginUsed <= 0)
78+
{
79+
throw new RegressionTestException($"Expected positive TotalMarginUsed, got {Portfolio.TotalMarginUsed}");
80+
}
81+
82+
// 4. Shared collateral: ETHUSDC (different quote currency) must deduct ADAUSDT margin
83+
_ethUsdc.SetMarketPrice(new TradeBar { Time = Time, Symbol = _ethUsdc.Symbol, Close = 1600 });
84+
85+
var ethBuyingPower = _ethUsdc.BuyingPowerModel.GetBuyingPower(new BuyingPowerParameters(Portfolio, _ethUsdc, OrderDirection.Buy));
86+
var adaBuyingPower = _adaUsdt.BuyingPowerModel.GetBuyingPower(new BuyingPowerParameters(Portfolio, _adaUsdt, OrderDirection.Buy));
87+
88+
// ETHUSDC must see less buying power than ADAUSDT - ADAUSDT maintenance margin
89+
// is deducted from ETHUSDC's shared pool, but ADAUSDT skips itself.
90+
if (ethBuyingPower.Value >= adaBuyingPower.Value)
91+
{
92+
throw new RegressionTestException(
93+
$"ETHUSDC buying power ({ethBuyingPower.Value}) must be less than ADAUSDT ({adaBuyingPower.Value}) " +
94+
$"— shared BNFCR pool must deduct ADAUSDT maintenance margin");
95+
}
96+
}
97+
98+
public override void OnEndOfAlgorithm()
99+
{
100+
if (!Portfolio.Invested)
101+
{
102+
throw new RegressionTestException("Expected an open position at end of algorithm");
103+
}
104+
}
105+
106+
public override void OnOrderEvent(OrderEvent orderEvent)
107+
{
108+
Debug($"{Time} {orderEvent}");
109+
}
110+
111+
/// <summary>
112+
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
113+
/// </summary>
114+
public bool CanRunLocally { get; } = true;
115+
116+
/// <summary>
117+
/// This is used by the regression test system to indicate which languages this algorithm is written in.
118+
/// </summary>
119+
public List<Language> Languages { get; } = new() { Language.CSharp };
120+
121+
/// <summary>
122+
/// Data Points count of all timeslices of algorithm
123+
/// </summary>
124+
public long DataPoints => 4322;
125+
126+
/// <summary>
127+
/// Data Points count of the algorithm history
128+
/// </summary>
129+
public int AlgorithmHistoryDataPoints => 0;
130+
131+
/// <summary>
132+
/// Final status of the algorithm
133+
/// </summary>
134+
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
135+
136+
/// <summary>
137+
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
138+
/// </summary>
139+
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
140+
{
141+
{"Total Orders", "1"},
142+
{"Average Win", "0%"},
143+
{"Average Loss", "0%"},
144+
{"Compounding Annual Return", "0%"},
145+
{"Drawdown", "0%"},
146+
{"Expectancy", "0"},
147+
{"Start Equity", "200"},
148+
{"End Equity", "206.86"},
149+
{"Net Profit", "0%"},
150+
{"Sharpe Ratio", "0"},
151+
{"Sortino Ratio", "0"},
152+
{"Probabilistic Sharpe Ratio", "0%"},
153+
{"Loss Rate", "0%"},
154+
{"Win Rate", "0%"},
155+
{"Profit-Loss Ratio", "0"},
156+
{"Alpha", "0"},
157+
{"Beta", "0"},
158+
{"Annual Standard Deviation", "0"},
159+
{"Annual Variance", "0"},
160+
{"Information Ratio", "0"},
161+
{"Tracking Error", "0"},
162+
{"Treynor Ratio", "0"},
163+
{"Total Fees", "$0.12"},
164+
{"Estimated Strategy Capacity", "$340000.00"},
165+
{"Lowest Capacity Asset", "ADAUSDT 18R"},
166+
{"Portfolio Turnover", "148.31%"},
167+
{"Drawdown Recovery", "0"},
168+
{"OrderListHash", "177ae917deb456790cfbcaaaf1ec1f5c"}
169+
};
170+
}
171+
}

Common/Brokerages/BinanceCoinFuturesBrokerageModel.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using QuantConnect.Benchmarks;
1717
using QuantConnect.Orders.Fees;
1818
using QuantConnect.Securities;
19+
using QuantConnect.Securities.CryptoFuture;
1920

2021
namespace QuantConnect.Brokerages
2122
{
@@ -51,5 +52,15 @@ public override IFeeModel GetFeeModel(Security security)
5152
{
5253
return new BinanceCoinFuturesFeeModel();
5354
}
55+
56+
/// <summary>
57+
/// Creates the crypto future margin model for the given security
58+
/// </summary>
59+
/// <param name="security">The security to create the margin model for</param>
60+
/// <returns>The margin model instance</returns>
61+
protected override IBuyingPowerModel CreateCryptoFutureMarginModel(Security security)
62+
{
63+
return new CryptoFutureMarginModel(GetLeverage(security));
64+
}
5465
}
5566
}

Common/Brokerages/BinanceFuturesBrokerageModel.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,32 @@ public override IMarginInterestRateModel GetMarginInterestRateModel(Security sec
7272
}
7373
return base.GetMarginInterestRateModel(security);
7474
}
75+
76+
/// <summary>
77+
/// Gets a new buying power model for the security.
78+
/// For <see cref="SecurityType.CryptoFuture"/>, returns a <see cref="BinanceCryptoFutureMarginModel"/>
79+
/// that recognizes supplementary stable coin collateral (e.g. BNFCR for EU Credits Trading Mode).
80+
/// </summary>
81+
/// <param name="security">The security to get a buying power model for</param>
82+
/// <returns>The buying power model for this brokerage/security</returns>
83+
public override IBuyingPowerModel GetBuyingPowerModel(Security security)
84+
{
85+
if (security?.Type == SecurityType.CryptoFuture)
86+
{
87+
return CreateCryptoFutureMarginModel(security);
88+
}
89+
90+
return base.GetBuyingPowerModel(security);
91+
}
92+
93+
/// <summary>
94+
/// Creates the crypto future margin model for the given security
95+
/// </summary>
96+
/// <param name="security">The security to create the margin model for</param>
97+
/// <returns>The margin model instance</returns>
98+
protected virtual IBuyingPowerModel CreateCryptoFutureMarginModel(Security security)
99+
{
100+
return new BinanceCryptoFutureMarginModel(GetLeverage(security));
101+
}
75102
}
76103
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
namespace QuantConnect.Securities.CryptoFuture
17+
{
18+
/// <summary>
19+
/// Binance-specific crypto future margin model that includes supplementary stable coin
20+
/// currencies as alternative collateral for non-coin (USDⓈ-M) futures.
21+
/// </summary>
22+
/// <remarks>
23+
/// EU/EEA users under MiCA Credits Trading Mode have BNFCR in their account.
24+
/// When BNFCR is present, this model aggregates all supplementary collateral assets
25+
/// using their walletBalance values converted to the primary collateral currency.
26+
/// Non-EU accounts don't have BNFCR — the check is a no-op for them.
27+
/// See: https://www.binance.com/en/support/faq/detail/0e857c392a2d47cebde0af762d9255ae
28+
/// </remarks>
29+
public class BinanceCryptoFutureMarginModel : CryptoFutureMarginModel
30+
{
31+
/// <summary>
32+
/// Binance Futures Credits currency symbol, present in EU/EEA accounts under MiCA Credits Trading Mode.
33+
/// </summary>
34+
private const string BNFCRCurrency = "BNFCR";
35+
36+
/// <summary>
37+
/// Creates a new instance
38+
/// </summary>
39+
/// <param name="leverage">The leverage to use, default 25x</param>
40+
public BinanceCryptoFutureMarginModel(decimal leverage = 25)
41+
: base(leverage)
42+
{
43+
}
44+
45+
/// <summary>
46+
/// Gets the total collateral amount for a Binance crypto future, including supplementary
47+
/// collateral assets for EU/EEA accounts in MiCA Credits Trading Mode.
48+
/// For coin futures (e.g. BTCUSD), only the primary collateral (base currency) is used.
49+
/// </summary>
50+
/// <param name="portfolio">The algorithm's portfolio</param>
51+
/// <param name="security">The crypto future security</param>
52+
/// <param name="primaryCollateral">The primary collateral cash (e.g. USDT)</param>
53+
/// <returns>Total collateral amount in terms of the primary collateral currency</returns>
54+
protected override decimal GetTotalCollateralAmount(
55+
SecurityPortfolioManager portfolio, Security security, Cash primaryCollateral)
56+
{
57+
// BNFCR presence means EU/EEA account in MiCA Credits Trading Mode.
58+
// Non-EU accounts don't have BNFCR in CashBook — skip entirely.
59+
var cashBook = portfolio.CashBook;
60+
if (!cashBook.ContainsKey(BNFCRCurrency))
61+
{
62+
return base.GetTotalCollateralAmount(portfolio, security, primaryCollateral);
63+
}
64+
65+
// Aggregate all collateral assets using walletBalance values.
66+
// Binance controls which assets are in the account — we sum everything
67+
// with a non-zero balance, converting to the primary collateral currency.
68+
// Negative amounts (e.g. BNFCR fees) correctly reduce the total.
69+
var total = 0m;
70+
foreach (var kvp in cashBook)
71+
{
72+
var cash = kvp.Value;
73+
if (cash.Amount == 0)
74+
{
75+
continue;
76+
}
77+
78+
total += cashBook.Convert(cash.Amount, cash.Symbol, primaryCollateral.Symbol);
79+
}
80+
81+
return total;
82+
}
83+
84+
/// <summary>
85+
/// When BNFCR is present (EU/MiCA mode), all USDⓈ-M futures share the same collateral
86+
/// pool regardless of quote currency (USDT, USDC, etc.).
87+
/// </summary>
88+
protected override bool SharesCollateral(SecurityPortfolioManager portfolio, Cash collateralCurrency, Security otherCryptoFuture)
89+
{
90+
return portfolio.CashBook.ContainsKey(BNFCRCurrency) || base.SharesCollateral(portfolio, collateralCurrency, otherCryptoFuture);
91+
}
92+
}
93+
}

Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,14 @@ public override InitialMargin GetInitialMarginRequirement(InitialMarginParameter
8686
protected override decimal GetMarginRemaining(SecurityPortfolioManager portfolio, Security security, OrderDirection direction)
8787
{
8888
var collateralCurrency = GetCollateralCash(security);
89-
var totalCollateralCurrency = collateralCurrency.Amount;
89+
var totalCollateralCurrency = GetTotalCollateralAmount(portfolio, security, collateralCurrency);
9090
var result = totalCollateralCurrency;
9191

9292
foreach (var kvp in portfolio.Where(holdings => holdings.Value.Invested && holdings.Value.Type == SecurityType.CryptoFuture && holdings.Value.Symbol != security.Symbol))
9393
{
9494
var otherCryptoFuture = portfolio.Securities[kvp.Key];
9595
// check if we share the collateral
96-
if (collateralCurrency == GetCollateralCash(otherCryptoFuture))
96+
if (SharesCollateral(portfolio, collateralCurrency, otherCryptoFuture))
9797
{
9898
// we reduce the available collateral based on total usage of all other positions too
9999
result -= otherCryptoFuture.BuyingPowerModel.GetMaintenanceMargin(MaintenanceMarginParameters.ForCurrentHoldings(otherCryptoFuture));
@@ -139,6 +139,18 @@ protected override decimal GetMarginRemaining(SecurityPortfolioManager portfolio
139139
return result < 0 ? 0 : result;
140140
}
141141

142+
/// <summary>
143+
/// Determines whether the given security shares collateral with another crypto future.
144+
/// </summary>
145+
/// <param name="portfolio">The algorithm's portfolio</param>
146+
/// <param name="collateralCurrency">The collateral cash for the current security</param>
147+
/// <param name="otherCryptoFuture">The other crypto future security to check</param>
148+
/// <returns>True if both securities share the same collateral</returns>
149+
protected virtual bool SharesCollateral(SecurityPortfolioManager portfolio, Cash collateralCurrency, Security otherCryptoFuture)
150+
{
151+
return collateralCurrency == GetCollateralCash(otherCryptoFuture);
152+
}
153+
142154
/// <summary>
143155
/// Helper method to determine what's the collateral currency for the given crypto future
144156
/// </summary>
@@ -154,5 +166,19 @@ private static Cash GetCollateralCash(Security security)
154166

155167
return collateralCurrency;
156168
}
169+
170+
/// <summary>
171+
/// Gets the total collateral amount for the given crypto future position.
172+
/// The base implementation returns only the primary collateral amount.
173+
/// Override in subclasses to include supplementary collateral currencies.
174+
/// </summary>
175+
/// <param name="portfolio">The algorithm's portfolio</param>
176+
/// <param name="security">The crypto future security</param>
177+
/// <param name="primaryCollateral">The primary collateral cash (e.g. USDT for non-coin futures, BTC for coin futures)</param>
178+
/// <returns>Total collateral amount in terms of the primary collateral currency</returns>
179+
protected virtual decimal GetTotalCollateralAmount(SecurityPortfolioManager portfolio, Security security, Cash primaryCollateral)
180+
{
181+
return primaryCollateral.Amount;
182+
}
157183
}
158184
}

0 commit comments

Comments
 (0)