diff --git a/Common/Brokerages/BrokerageName.cs b/Common/Brokerages/BrokerageName.cs index 7f6edaab7a5e..e31cb11a6ff8 100644 --- a/Common/Brokerages/BrokerageName.cs +++ b/Common/Brokerages/BrokerageName.cs @@ -197,6 +197,11 @@ public enum BrokerageName /// /// Transaction and submit/execution rules will use dYdX models /// - DYDX + DYDX, + + /// + /// Bybit Inverse Futures COIN-Margined contracts are settled and collateralized in their base cryptocurrency. + /// + BybitInverseFutures, } } diff --git a/Common/Brokerages/BybitInverseFuturesBrokerageModel.cs b/Common/Brokerages/BybitInverseFuturesBrokerageModel.cs new file mode 100644 index 000000000000..11d9425654e3 --- /dev/null +++ b/Common/Brokerages/BybitInverseFuturesBrokerageModel.cs @@ -0,0 +1,71 @@ +/* + * 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.Benchmarks; +using QuantConnect.Orders.Fees; +using QuantConnect.Securities; +using QuantConnect.Securities.CryptoFuture; + +namespace QuantConnect.Brokerages; + +/// +/// Provides Bybit Inverse Futures specific properties. +/// Inverse (COIN-Margined) contracts are settled and collateralized in their base cryptocurrency (e.g. BTC for BTCUSD). +/// +public class BybitInverseFuturesBrokerageModel : BybitBrokerageModel +{ + /// + /// Initializes a new instance of the class + /// + /// The type of account to be modeled, defaults to + public BybitInverseFuturesBrokerageModel(AccountType accountType = AccountType.Margin) : base(accountType) + { + } + + /// + /// Get the benchmark for this model + /// + /// SecurityService to create the security with if needed + /// The benchmark for this brokerage + public override IBenchmark GetBenchmark(SecurityManager securities) + { + var symbol = Symbol.Create("BTCUSD", SecurityType.CryptoFuture, MarketName); + return SecurityBenchmark.CreateInstance(securities, symbol); + } + + /// + /// Provides Bybit Inverse Futures fee model + /// + /// The security to get a fee model for + /// The new fee model for this brokerage + public override IFeeModel GetFeeModel(Security security) + { + return new BybitFuturesFeeModel(); + } + + /// + /// Gets a new buying power model for the security + /// + /// The security to get a buying power model for + /// The buying power model for this brokerage/security + public override IBuyingPowerModel GetBuyingPowerModel(Security security) + { + if (security.Type == SecurityType.CryptoFuture) + { + return new BybitInverseFuturesMarginModel(GetLeverage(security)); + } + return base.GetBuyingPowerModel(security); + } +} diff --git a/Common/Brokerages/IBrokerageModel.cs b/Common/Brokerages/IBrokerageModel.cs index 4544c388d8c1..246f37140551 100644 --- a/Common/Brokerages/IBrokerageModel.cs +++ b/Common/Brokerages/IBrokerageModel.cs @@ -276,6 +276,9 @@ public static IBrokerageModel Create(IOrderProvider orderProvider, BrokerageName case BrokerageName.Bybit: return new BybitBrokerageModel(accountType); + case BrokerageName.BybitInverseFutures: + return new BybitInverseFuturesBrokerageModel(accountType); + case BrokerageName.Eze: return new EzeBrokerageModel(accountType); @@ -379,6 +382,9 @@ public static BrokerageName GetBrokerageName(IBrokerageModel brokerageModel) case RBIBrokerageModel _: return BrokerageName.RBI; + case BybitInverseFuturesBrokerageModel _: + return BrokerageName.BybitInverseFutures; + case BybitBrokerageModel _: return BrokerageName.Bybit; diff --git a/Common/Securities/CryptoFuture/BybitInverseFuturesMarginModel.cs b/Common/Securities/CryptoFuture/BybitInverseFuturesMarginModel.cs new file mode 100644 index 000000000000..84b729e95734 --- /dev/null +++ b/Common/Securities/CryptoFuture/BybitInverseFuturesMarginModel.cs @@ -0,0 +1,32 @@ +/* + * 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. +*/ + +namespace QuantConnect.Securities.CryptoFuture +{ + /// + /// Margin model for Bybit Inverse Futures using the Unified Trading Account (UTA). + /// In UTA, reports TotalAvailableBalance as USD, + /// so we use the quote currency (USD) as collateral instead of the base crypto (e.g. ADA). + /// + public class BybitInverseFuturesMarginModel : CryptoFutureMarginModel + { + public BybitInverseFuturesMarginModel(decimal leverage) : base(leverage) { } + + private protected override Cash GetCollateralCash(Security security) + { + return (security as CryptoFuture).QuoteCurrency; + } + } +} diff --git a/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs b/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs index 0b6ce4528adf..f8f2080c019c 100644 --- a/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs +++ b/Common/Securities/CryptoFuture/CryptoFutureMarginModel.cs @@ -142,7 +142,7 @@ protected override decimal GetMarginRemaining(SecurityPortfolioManager portfolio /// /// Helper method to determine what's the collateral currency for the given crypto future /// - private static Cash GetCollateralCash(Security security) + private protected virtual Cash GetCollateralCash(Security security) { var cryptoFuture = (CryptoFuture)security; diff --git a/Tests/Common/Brokerages/BybitInverseFuturesBrokerageModelTests.cs b/Tests/Common/Brokerages/BybitInverseFuturesBrokerageModelTests.cs new file mode 100644 index 000000000000..f01ded481cd9 --- /dev/null +++ b/Tests/Common/Brokerages/BybitInverseFuturesBrokerageModelTests.cs @@ -0,0 +1,108 @@ +/* + * 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 System; +using NUnit.Framework; +using QuantConnect.Brokerages; +using QuantConnect.Data.Market; +using QuantConnect.Orders; +using QuantConnect.Orders.Fees; +using QuantConnect.Securities; +using QuantConnect.Tests.Brokerages; +using QuantConnect.Tests.Engine.DataFeeds; + +namespace QuantConnect.Tests.Common.Brokerages +{ + [TestFixture, Parallelizable(ParallelScope.All)] + public class BybitInverseFuturesBrokerageModelTests + { + private static readonly Symbol BTCUSD_Future = Symbol.Create("BTCUSD", SecurityType.CryptoFuture, Market.Bybit); + private static readonly BybitInverseFuturesBrokerageModel Model = new(); + + [Test] + public void DefaultAccountTypeIsMargin() + { + Assert.AreEqual(AccountType.Margin, Model.AccountType); + } + + [Test] + public void GetFeeModelReturnsBybitFuturesFeeModel_ForCryptoFuture() + { + var security = TestsHelpers.GetSecurity(symbol: BTCUSD_Future.Value, + securityType: SecurityType.CryptoFuture, + market: Market.Bybit, + quoteCurrency: "USD"); + + Assert.IsInstanceOf(Model.GetFeeModel(security)); + } + + [Test] + public void GetBrokerageNameReturnsBybitInverseFutures() + { + Assert.AreEqual(BrokerageName.BybitInverseFutures, BrokerageModel.GetBrokerageName(new BybitInverseFuturesBrokerageModel())); + } + + [Test] + public void GetBrokerageModelReturnsInverseFuturesModel() + { + var model = BrokerageModel.Create(null, BrokerageName.BybitInverseFutures, AccountType.Margin); + Assert.IsInstanceOf(model); + } + + [TestCase(AccountType.Cash, 1)] + [TestCase(AccountType.Margin, 10)] + public void GetLeverageReturnsCorrectValue(AccountType accountType, decimal expectedLeverage) + { + var security = TestsHelpers.GetSecurity(symbol: BTCUSD_Future.Value, + securityType: SecurityType.CryptoFuture, + market: Market.Bybit, + quoteCurrency: "USD"); + + var model = new BybitInverseFuturesBrokerageModel(accountType); + Assert.AreEqual(expectedLeverage, model.GetLeverage(security)); + } + + [TestCase(10, 0.40, Description = "leverage=10 => initialMargin ≈ 4 / 0.267 / 10 * 0.267 = 0.40 USD")] + [TestCase(25, 0.16, Description = "leverage=25 => initialMargin ≈ 4 / 0.267 / 25 * 0.267 = 0.16 USD")] + public void GetBuyingPowerUsesUsdBalance_WithDifferentLeverage(decimal leverage, double expectedInitialMarginUsd) + { + // Reproduces the live trading scenario: Bybit UTA reports TotalAvailableBalance as USD + // (no ADA in account), so the margin model must use USD as collateral. + var algo = new AlgorithmStub(); + algo.SetBrokerageModel(BrokerageName.BybitInverseFutures, AccountType.Margin); + algo.SetFinishedWarmingUp(); + + var adaUsd = algo.AddCryptoFuture("ADAUSD"); + adaUsd.SetLeverage(leverage); + + const decimal adaPrice = 0.267m; + const decimal usdBalance = 100m; + + adaUsd.QuoteCurrency.SetAmount(usdBalance); // USD = 100 (from GetCashBalance) + adaUsd.BaseCurrency.SetAmount(0m); // ADA = 0 (no base asset in account) + adaUsd.BaseCurrency.ConversionRate = adaPrice; + adaUsd.QuoteCurrency.ConversionRate = 1m; + adaUsd.SetMarketPrice(new TradeBar(new DateTime(2026, 1, 1), adaUsd.Symbol, adaPrice, adaPrice, adaPrice, adaPrice, volume: 1m)); + + // Buying power = USD balance regardless of leverage + var buyingPower = adaUsd.BuyingPowerModel.GetBuyingPower(new BuyingPowerParameters(algo.Portfolio, adaUsd, OrderDirection.Buy)); + Assert.AreEqual((double)usdBalance, (double)buyingPower.Value, delta: 0.01); + + // Initial margin scales inversely with leverage + var initialMargin = adaUsd.BuyingPowerModel.GetInitialMarginRequirement(new InitialMarginParameters(adaUsd, 4)); + Assert.AreEqual(expectedInitialMarginUsd, (double)initialMargin.Value, delta: 0.05); + } + } +}