Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 43 additions & 11 deletions Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using System;
using System.Linq;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;

namespace QuantConnect.Securities.CryptoFuture
{
Expand Down Expand Up @@ -55,24 +56,32 @@ public override MaintenanceMargin GetMaintenanceMargin(MaintenanceMarginParamete
return new MaintenanceMargin(GetInitialMarginRequirement(new InitialMarginParameters(parameters.Security, parameters.Quantity)));
}

/// <summary>
/// Gets the total margin required to execute the specified order in units of the account currency including fees
/// </summary>
/// <param name="parameters">An object containing the portfolio, the security and the order</param>
/// <returns>The total margin in terms of the currency quoted in the order</returns>
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);
}

/// <summary>
/// The margin that must be held in order to increase the position by the provided quantity
/// </summary>
/// <param name="parameters">An object containing the security and quantity of shares</param>
/// <returns>The initial margin required for the option (i.e. the equity required to enter a position for this option)</returns>
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));
}

/// <summary>
Expand Down Expand Up @@ -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;
}
}
}
116 changes: 110 additions & 6 deletions Tests/Common/Securities/CryptoFuture/CryptoFutureMarginModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}

Expand All @@ -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)
Expand Down Expand Up @@ -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);
}
}
}