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); + } } }