diff --git a/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs b/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs
index cc82bd6ffa1b..578609b44b06 100644
--- a/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs
+++ b/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs
@@ -16,6 +16,7 @@
using System;
using System.Linq;
using QuantConnect.Orders;
+using QuantConnect.Orders.Fees;
namespace QuantConnect.Securities.CryptoFuture
{
@@ -55,6 +56,24 @@ public override MaintenanceMargin GetMaintenanceMargin(MaintenanceMarginParamete
return new MaintenanceMargin(GetInitialMarginRequirement(new InitialMarginParameters(parameters.Security, parameters.Quantity)));
}
+ ///
+ /// Gets the total margin required to execute the specified order in units of the account currency including fees
+ ///
+ /// An object containing the portfolio, the security and the order
+ /// The total margin in terms of the currency quoted in the order
+ public override InitialMargin GetInitialMarginRequiredForOrder(InitialMarginRequiredForOrderParameters parameters)
+ {
+ var fees = parameters.Security.FeeModel.GetOrderFee(new OrderFeeParameters(parameters.Security, parameters.Order)).Value;
+ var feesInAccountCurrency = parameters.CurrencyConverter.ConvertToAccountCurrency(fees).Amount;
+
+ var orderMargin = GetInitialMarginRequirement(
+ parameters.Security,
+ parameters.Order.Quantity,
+ GetOrderMarginPrice(parameters.Security, parameters.Order));
+
+ return new InitialMargin(orderMargin + Math.Sign(orderMargin) * feesInAccountCurrency);
+ }
+
///
/// The margin that must be held in order to increase the position by the provided quantity
///
@@ -62,17 +81,7 @@ public override MaintenanceMargin GetMaintenanceMargin(MaintenanceMarginParamete
/// The initial margin required for the option (i.e. the equity required to enter a position for this option)
public override InitialMargin GetInitialMarginRequirement(InitialMarginParameters parameters)
{
- var security = parameters.Security;
- var quantity = parameters.Quantity;
- if (security?.GetLastData() == null || quantity == 0m)
- {
- return InitialMargin.Zero;
- }
-
- var positionValue = security.Holdings.GetQuantityValue(quantity, security.Price);
- var marginRequirementInCollateral = Math.Abs(positionValue.Amount) / GetLeverage(security);
-
- return new InitialMargin(marginRequirementInCollateral * positionValue.Cash.ConversionRate);
+ return new InitialMargin(GetInitialMarginRequirement(parameters.Security, parameters.Quantity, parameters.Security?.Price ?? 0m));
}
///
@@ -180,5 +189,28 @@ protected virtual decimal GetTotalCollateralAmount(SecurityPortfolioManager port
{
return primaryCollateral.Amount;
}
+
+ private decimal GetInitialMarginRequirement(Security security, decimal quantity, decimal price)
+ {
+ if (security?.GetLastData() == null || quantity == 0m)
+ {
+ return 0m;
+ }
+
+ var positionValue = security.Holdings.GetQuantityValue(quantity, price);
+ var marginRequirementInCollateral = Math.Abs(positionValue.Amount) / GetLeverage(security);
+ return marginRequirementInCollateral * positionValue.Cash.ConversionRate;
+ }
+
+ private static decimal GetOrderMarginPrice(Security security, Order order)
+ {
+ var denominator = Math.Abs(order.Quantity * security.SymbolProperties.ContractMultiplier * security.QuoteCurrency.ConversionRate);
+ if (denominator == 0m)
+ {
+ return security.Price;
+ }
+
+ return Math.Abs(order.GetValue(security)) / denominator;
+ }
}
}
diff --git a/Tests/Common/Securities/CryptoFuture/CryptoFutureMarginModelTests.cs b/Tests/Common/Securities/CryptoFuture/CryptoFutureMarginModelTests.cs
index 2f3d8cc246fb..18b08c343430 100644
--- a/Tests/Common/Securities/CryptoFuture/CryptoFutureMarginModelTests.cs
+++ b/Tests/Common/Securities/CryptoFuture/CryptoFutureMarginModelTests.cs
@@ -18,6 +18,7 @@
using QuantConnect.Algorithm;
using QuantConnect.Data.Market;
using QuantConnect.Orders;
+using QuantConnect.Orders.Fees;
using QuantConnect.Securities;
using QuantConnect.Securities.CryptoFuture;
using QuantConnect.Brokerages;
@@ -54,18 +55,96 @@ public void InitialMarginRequirement(string ticker, decimal quantity)
decimal marginRequirement;
if (ticker == "BTCUSD")
{
- // ((quantity * contract mutiplier * price) / leverage) * conversion rate (BTC -> USD)
+ // ((quantity * contract multiplier * price) / leverage) * conversion rate (BTC -> USD)
marginRequirement = ((parameters.Quantity * 100m * cryptoFuture.Price) / 25m) * 1 / cryptoFuture.Price;
}
else
{
- // ((quantity * contract mutiplier * price) / leverage) * conversion rate (USDT ~= USD)
+ // ((quantity * contract multiplier * price) / leverage) * conversion rate (USDT ~= USD)
marginRequirement = ((parameters.Quantity * 1m * cryptoFuture.Price) / 25m) * 1;
}
Assert.AreEqual(Math.Abs(marginRequirement), result.Value);
}
+ [TestCase("BTCUSDT", 10, 16000, 12000)]
+ [TestCase("BTCUSD", 15000, 31300, 30000)]
+ public void InitialMarginRequiredForOrderUsesLimitOrderPrice(string ticker, decimal quantity, decimal securityPrice, decimal limitPrice)
+ {
+ var algo = GetAlgorithm();
+ var cryptoFuture = algo.AddCryptoFuture(ticker);
+ SetPrice(cryptoFuture, securityPrice);
+
+ var limitOrder = new LimitOrder(cryptoFuture.Symbol, quantity, limitPrice, DateTime.UtcNow);
+ var marginModel = cryptoFuture.BuyingPowerModel;
+
+ var marginForLimitOrder = marginModel.GetInitialMarginRequiredForOrder(
+ new InitialMarginRequiredForOrderParameters(algo.Portfolio.CashBook, cryptoFuture, limitOrder)).Value;
+
+ var expectedLimitOrderPrice = GetOrderMarginPrice(quantity, limitPrice, securityPrice);
+ var expected = GetExpectedOrderInitialMargin(algo, cryptoFuture, limitOrder, expectedLimitOrderPrice);
+
+ Assert.AreEqual(expected, marginForLimitOrder);
+
+ var marketOrder = new MarketOrder(cryptoFuture.Symbol, quantity, DateTime.UtcNow);
+ var marginForMarketOrder = marginModel.GetInitialMarginRequiredForOrder(
+ new InitialMarginRequiredForOrderParameters(algo.Portfolio.CashBook, cryptoFuture, marketOrder)).Value;
+
+ Assert.AreNotEqual(marginForMarketOrder, marginForLimitOrder);
+ }
+
+ [TestCase("BTCUSDT", -10, 16000, 19000)]
+ [TestCase("BTCUSD", -15000, 31300, 34000)]
+ public void InitialMarginRequiredForOrderUsesLimitOrderPriceForShortOrders(string ticker, decimal quantity, decimal securityPrice, decimal limitPrice)
+ {
+ var algo = GetAlgorithm();
+ var cryptoFuture = algo.AddCryptoFuture(ticker);
+ SetPrice(cryptoFuture, securityPrice);
+
+ var limitOrder = new LimitOrder(cryptoFuture.Symbol, quantity, limitPrice, DateTime.UtcNow);
+ var marginModel = cryptoFuture.BuyingPowerModel;
+
+ var marginForLimitOrder = marginModel.GetInitialMarginRequiredForOrder(
+ new InitialMarginRequiredForOrderParameters(algo.Portfolio.CashBook, cryptoFuture, limitOrder)).Value;
+
+ var expectedLimitOrderPrice = GetOrderMarginPrice(quantity, limitPrice, securityPrice);
+ var expected = GetExpectedOrderInitialMargin(algo, cryptoFuture, limitOrder, expectedLimitOrderPrice);
+
+ Assert.AreEqual(expected, marginForLimitOrder);
+
+ var marketOrder = new MarketOrder(cryptoFuture.Symbol, quantity, DateTime.UtcNow);
+ var marginForMarketOrder = marginModel.GetInitialMarginRequiredForOrder(
+ new InitialMarginRequiredForOrderParameters(algo.Portfolio.CashBook, cryptoFuture, marketOrder)).Value;
+
+ Assert.AreNotEqual(marginForMarketOrder, marginForLimitOrder);
+ }
+
+ [TestCase("BTCUSDT", -10, 16000, 17000, 19000)]
+ [TestCase("BTCUSD", -15000, 31300, 32000, 34000)]
+ public void InitialMarginRequiredForOrderUsesStopLimitOrderPrice(string ticker, decimal quantity, decimal securityPrice, decimal stopPrice, decimal limitPrice)
+ {
+ var algo = GetAlgorithm();
+ var cryptoFuture = algo.AddCryptoFuture(ticker);
+ SetPrice(cryptoFuture, securityPrice);
+
+ var stopLimitOrder = new StopLimitOrder(cryptoFuture.Symbol, quantity, stopPrice, limitPrice, DateTime.UtcNow);
+ var marginModel = cryptoFuture.BuyingPowerModel;
+
+ var marginForStopLimitOrder = marginModel.GetInitialMarginRequiredForOrder(
+ new InitialMarginRequiredForOrderParameters(algo.Portfolio.CashBook, cryptoFuture, stopLimitOrder)).Value;
+
+ var expectedStopLimitOrderPrice = GetOrderMarginPrice(quantity, limitPrice, securityPrice);
+ var expected = GetExpectedOrderInitialMargin(algo, cryptoFuture, stopLimitOrder, expectedStopLimitOrderPrice);
+
+ Assert.AreEqual(expected, marginForStopLimitOrder);
+
+ var marketOrder = new MarketOrder(cryptoFuture.Symbol, quantity, DateTime.UtcNow);
+ var marginForMarketOrder = marginModel.GetInitialMarginRequiredForOrder(
+ new InitialMarginRequiredForOrderParameters(algo.Portfolio.CashBook, cryptoFuture, marketOrder)).Value;
+
+ Assert.AreNotEqual(marginForMarketOrder, marginForStopLimitOrder);
+ }
+
[Test]
public void MarginRemainingWithBnfcrOnlyCollateral()
{
@@ -134,7 +213,7 @@ public void BnfcrZeroBalanceIncludesSupplementaryCollateral()
var buyingPower = cryptoFuture.BuyingPowerModel.GetBuyingPower(
new BuyingPowerParameters(algo.Portfolio, cryptoFuture, OrderDirection.Buy));
- // BNFCR presence triggers supplementary collateral — USDC should be included
+ // BNFCR presence triggers supplementary collateral - USDC should be included
Assert.AreEqual(100m + algoCash, buyingPower.Value);
}
@@ -153,7 +232,7 @@ public void BtcCollateralConvertedToQuoteCurrency()
var buyingPower = cryptoFuture.BuyingPowerModel.GetBuyingPower(
new BuyingPowerParameters(algo.Portfolio, cryptoFuture, OrderDirection.Buy));
- // 0 (USDC) + 0.5 * 16000 (BTC → USDC via USD) = 8000
+ // 0 (USDC) + 0.5 * 16000 (BTC -> USDC via USD) = 8000
Assert.AreEqual(8000m + algoCash, buyingPower.Value);
}
@@ -163,13 +242,13 @@ public void SharedCollateralDeductsMaintenanceMarginAcrossQuoteCurrencies()
var algo = GetBinanceFuturesAlgorithm();
var algoCash = algo.Portfolio.Cash;
- // Two USDⓈ-M futures with DIFFERENT quote currencies
+ // Two USD-margined futures with DIFFERENT quote currencies
var btcUsdt = algo.AddCryptoFuture("BTCUSDT");
var ethUsdc = algo.AddCryptoFuture("ETHUSDC");
SetPrice(btcUsdt, 16000);
SetPrice(ethUsdc, 1600);
- // EU user: BNFCR present — all USDⓈ-M futures share collateral pool
+ // EU user: BNFCR present - all USD-margined futures share collateral pool
algo.SetCash("BNFCR", 10000, 1);
// Simulate an existing ETHUSDC position (10 ETH @ $1,600)
@@ -239,5 +318,30 @@ private static void SetPrice(Security security, decimal price)
Close = price
});
}
+
+ private static decimal GetExpectedOrderInitialMargin(QCAlgorithm algo, Security security, Order order, decimal orderPrice)
+ {
+ var marginModel = security.BuyingPowerModel;
+ var positionValue = security.Holdings.GetQuantityValue(order.Quantity, orderPrice);
+ var expected = Math.Abs(positionValue.Amount) / marginModel.GetLeverage(security)
+ * positionValue.Cash.ConversionRate;
+
+ var fees = security.FeeModel.GetOrderFee(new OrderFeeParameters(security, order)).Value;
+ var feesInAccountCurrency = algo.Portfolio.CashBook.ConvertToAccountCurrency(fees).Amount;
+
+ return expected + feesInAccountCurrency;
+ }
+
+ private static decimal GetOrderMarginPrice(decimal quantity, decimal orderLimitPrice, decimal securityPrice)
+ {
+ if (quantity == 0m)
+ {
+ return securityPrice;
+ }
+
+ return quantity > 0m
+ ? Math.Min(orderLimitPrice, securityPrice)
+ : Math.Max(orderLimitPrice, securityPrice);
+ }
}
}