diff --git a/QuantConnect.BybitBrokerage.Tests/BybitBrokerageTests.cs b/QuantConnect.BybitBrokerage.Tests/BybitBrokerageTests.cs
index e6179a9..8e226c9 100644
--- a/QuantConnect.BybitBrokerage.Tests/BybitBrokerageTests.cs
+++ b/QuantConnect.BybitBrokerage.Tests/BybitBrokerageTests.cs
@@ -21,6 +21,7 @@
using QuantConnect.Brokerages.Bybit.Api;
using QuantConnect.Brokerages.Bybit.Models.Enums;
using QuantConnect.Configuration;
+using QuantConnect.Data;
using QuantConnect.Interfaces;
using QuantConnect.Lean.Engine.DataFeeds;
using QuantConnect.Logging;
@@ -62,8 +63,16 @@ protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISec
var websocketUrl = Config.Get("bybit-websocket-url", "wss://stream-testnet.bybit.com");
_client = CreateRestApiClient(apiKey, apiSecret, apiUrl);
- return new BybitBrokerage(apiKey, apiSecret, apiUrl, websocketUrl, algorithm.Object, orderProvider,
- securityProvider, new AggregationManager(), null);
+
+ return CreateBrokerage(apiKey, apiSecret, apiUrl, websocketUrl, algorithm.Object, orderProvider, securityProvider, new AggregationManager());
+ }
+
+ protected virtual IBrokerage CreateBrokerage(string apiKey, string apiSecret, string apiUrl,
+ string websocketUrl, IAlgorithm algorithm, IOrderProvider orderProvider, ISecurityProvider securityProvider,
+ IDataAggregator aggregator)
+ {
+ return new BybitBrokerage(apiKey, apiSecret, apiUrl, websocketUrl, algorithm, orderProvider, securityProvider, new AggregationManager(), null);
+
}
protected virtual decimal TakerFee => BybitFeeModel.TakerNonVIPFee;
diff --git a/QuantConnect.BybitBrokerage.Tests/BybitInverseFuturesBrokerageHistoryProviderTests.cs b/QuantConnect.BybitBrokerage.Tests/BybitInverseFuturesBrokerageHistoryProviderTests.cs
new file mode 100644
index 0000000..ae51f28
--- /dev/null
+++ b/QuantConnect.BybitBrokerage.Tests/BybitInverseFuturesBrokerageHistoryProviderTests.cs
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+namespace QuantConnect.Brokerages.Bybit.Tests
+{
+ [TestFixture, Explicit("Requires valid credentials to be setup and run outside USA")]
+ public class BybitInverseFuturesBrokerageHistoryProviderTests : BybitBrokerageHistoryProviderTests
+ {
+ private static readonly Symbol ETHUSD = Symbol.Create("ETHUSD", SecurityType.CryptoFuture, Market.Bybit);
+
+ private static TestCaseData[] ValidHistory
+ {
+ get
+ {
+ return new[]
+ {
+ // valid
+ new TestCaseData(ETHUSD, Resolution.Tick, Time.OneMinute, TickType.Trade),
+ new TestCaseData(ETHUSD, Resolution.Minute, Time.OneHour, TickType.Trade),
+ new TestCaseData(ETHUSD, Resolution.Hour, Time.OneDay, TickType.Trade),
+ new TestCaseData(ETHUSD, Resolution.Daily, TimeSpan.FromDays(15), TickType.Trade),
+ new TestCaseData(ETHUSD, Resolution.Hour, Time.OneDay, TickType.OpenInterest)
+ };
+ }
+ }
+
+ [Test, TestCaseSource(nameof(ValidHistory))]
+ public override void GetsHistory(Symbol symbol, Resolution resolution, TimeSpan period, TickType tickType)
+ {
+ base.GetsHistory(symbol, resolution, period, tickType);
+ }
+
+ [Ignore("The brokerage is shared between different product categories, therefore this test is only required in the base class")]
+ [TestCase(default, default, default, default)]
+ public override void ReturnsNullOnInvalidHistoryRequest(
+ Symbol symbol, Resolution resolution, TimeSpan period, TickType tickType)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/QuantConnect.BybitBrokerage.Tests/BybitInverseFuturesBrokerageTests.Stream.cs b/QuantConnect.BybitBrokerage.Tests/BybitInverseFuturesBrokerageTests.Stream.cs
new file mode 100644
index 0000000..3c745aa
--- /dev/null
+++ b/QuantConnect.BybitBrokerage.Tests/BybitInverseFuturesBrokerageTests.Stream.cs
@@ -0,0 +1,43 @@
+/*
+ * 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 NUnit.Framework;
+
+namespace QuantConnect.Brokerages.Bybit.Tests
+{
+ [TestFixture]
+ public partial class BybitInverseFuturesBrokerageTests
+ {
+ private static TestCaseData[] TestParameters
+ {
+ get
+ {
+ return new[]
+ {
+ // valid parameters, for example
+ new TestCaseData(BTCUSD, Resolution.Tick, false),
+ new TestCaseData(BTCUSD, Resolution.Minute, true),
+ new TestCaseData(BTCUSD, Resolution.Second, true),
+ };
+ }
+ }
+
+ [Test, TestCaseSource(nameof(TestParameters))]
+ public override void StreamsData(Symbol symbol, Resolution resolution, bool throwsException)
+ {
+ base.StreamsData(symbol, resolution, throwsException);
+ }
+ }
+}
\ No newline at end of file
diff --git a/QuantConnect.BybitBrokerage.Tests/BybitInverseFuturesBrokerageTests.cs b/QuantConnect.BybitBrokerage.Tests/BybitInverseFuturesBrokerageTests.cs
new file mode 100644
index 0000000..7ac78dd
--- /dev/null
+++ b/QuantConnect.BybitBrokerage.Tests/BybitInverseFuturesBrokerageTests.cs
@@ -0,0 +1,138 @@
+/*
+ * 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 System.Linq;
+using System.Threading;
+using NUnit.Framework;
+using QuantConnect.Brokerages.Bybit.Models.Enums;
+using QuantConnect.Data;
+using QuantConnect.Interfaces;
+using QuantConnect.Lean.Engine.DataFeeds;
+using QuantConnect.Logging;
+using QuantConnect.Orders;
+using QuantConnect.Securities;
+using QuantConnect.Tests.Brokerages;
+using QuantConnect.Util;
+
+namespace QuantConnect.Brokerages.Bybit.Tests;
+
+[TestFixture, Explicit("Requires valid credentials to be setup and run outside USA")]
+public partial class BybitInverseFuturesBrokerageTests : BybitBrokerageTests
+{
+ private static Symbol BTCUSD = Symbol.Create("BTCUSD", SecurityType.CryptoFuture, "bybit");
+ protected override Symbol Symbol { get; } = BTCUSD;
+
+ protected override SecurityType SecurityType => SecurityType.Future;
+ protected override BybitProductCategory Category => BybitProductCategory.Inverse;
+ protected override decimal TakerFee => 0.0000015m;
+
+ protected override decimal GetDefaultQuantity() => 10m;
+
+ protected override IBrokerage CreateBrokerage(string apiKey, string apiSecret, string apiUrl,
+ string websocketUrl, IAlgorithm algorithm, IOrderProvider orderProvider, ISecurityProvider securityProvider,
+ IDataAggregator aggregator)
+ {
+ return new BybitBrokerage(apiKey, apiSecret, apiUrl, websocketUrl, algorithm, orderProvider, securityProvider, new AggregationManager(), null);
+
+ }
+
+ ///
+ /// Provides the data required to test each order type in various cases
+ ///
+ private static TestCaseData[] OrderParameters()
+ {
+ return new[]
+ {
+ new TestCaseData(new MarketOrderTestParameters(BTCUSD)).SetName("MarketOrder"),
+ new TestCaseData(new LimitOrderTestParameters(BTCUSD, 50000m, 10000m)).SetName("LimitOrder"),
+ new TestCaseData(new StopMarketOrderTestParameters(BTCUSD, 50000m, 10000m)).SetName("StopMarketOrder"),
+ new TestCaseData(new StopLimitOrderTestParameters(BTCUSD, 50000m, 10000m)).SetName("StopLimitOrder"),
+ new TestCaseData(new LimitIfTouchedOrderTestParameters(BTCUSD, 50000m, 20000)).SetName(
+ "LimitIfTouchedOrder")
+ };
+ }
+
+ [Test, TestCaseSource(nameof(OrderParameters))]
+ public override void CancelOrders(OrderTestParameters parameters)
+ {
+ base.CancelOrders(parameters);
+ }
+
+ [Test, TestCaseSource(nameof(OrderParameters))]
+ public override void LongFromZero(OrderTestParameters parameters)
+ {
+ base.LongFromZero(parameters);
+ }
+
+ [Test, TestCaseSource(nameof(OrderParameters))]
+ public override void CloseFromLong(OrderTestParameters parameters)
+ {
+ base.CloseFromLong(parameters);
+ }
+
+ [Test, TestCaseSource(nameof(OrderParameters))]
+ public override void ShortFromZero(OrderTestParameters parameters)
+ {
+ base.ShortFromZero(parameters);
+ }
+
+ [Test, TestCaseSource(nameof(OrderParameters))]
+ public override void CloseFromShort(OrderTestParameters parameters)
+ {
+ base.CloseFromShort(parameters);
+ }
+
+ [Test, TestCaseSource(nameof(OrderParameters))]
+ public override void ShortFromLong(OrderTestParameters parameters)
+ {
+ base.ShortFromLong(parameters);
+ }
+
+ [Test, TestCaseSource(nameof(OrderParameters))]
+ public override void LongFromShort(OrderTestParameters parameters)
+ {
+ base.LongFromShort(parameters);
+ }
+
+
+ [Test]
+ public override void GetAccountHoldings()
+ {
+ Log.Trace("");
+ Log.Trace("GET ACCOUNT HOLDINGS");
+ Log.Trace("");
+ var before = Brokerage.GetCashBalance();
+
+ var order = new MarketOrder(Symbol, GetDefaultQuantity(), DateTime.UtcNow);
+ PlaceOrderWaitForStatus(order);
+
+ Thread.Sleep(3000);
+
+ var after = Brokerage.GetCashBalance();
+
+ CurrencyPairUtil.DecomposeCurrencyPair(Symbol, out var baseCurrency, out _);
+ var beforeHoldings = before.FirstOrDefault(x => x.Currency == baseCurrency);
+ var afterHoldings = after.FirstOrDefault(x => x.Currency == baseCurrency);
+
+ var beforeQuantity = beforeHoldings == null ? 0 : beforeHoldings.Amount;
+ var afterQuantity = afterHoldings == null ? 0 : afterHoldings.Amount;
+
+ var fee = 0.00000015m;
+
+ Assert.AreEqual(0, afterQuantity - beforeQuantity + fee);
+ }
+
+}
\ No newline at end of file
diff --git a/QuantConnect.BybitBrokerage/Api/BybitAccountApiEndpoint.cs b/QuantConnect.BybitBrokerage/Api/BybitAccountApiEndpoint.cs
index d486e98..c022d32 100644
--- a/QuantConnect.BybitBrokerage/Api/BybitAccountApiEndpoint.cs
+++ b/QuantConnect.BybitBrokerage/Api/BybitAccountApiEndpoint.cs
@@ -40,7 +40,6 @@ public BybitAccountApiEndpoint(ISymbolMapper symbolMapper, string apiPrefix, ISe
///
/// Obtain wallet balance, query asset information of each currency, and account risk rate information
///
- /// The product category
/// The wallet balances
public BybitBalance GetWalletBalances()
{
diff --git a/QuantConnect.BybitBrokerage/Api/BybitPositionApiEndpoint.cs b/QuantConnect.BybitBrokerage/Api/BybitPositionApiEndpoint.cs
index c3e01ce..2e43e6c 100644
--- a/QuantConnect.BybitBrokerage/Api/BybitPositionApiEndpoint.cs
+++ b/QuantConnect.BybitBrokerage/Api/BybitPositionApiEndpoint.cs
@@ -48,10 +48,13 @@ public IEnumerable GetPositions(BybitProductCategory category
{
if (category == BybitProductCategory.Spot) return Array.Empty();
- var parameters = new KeyValuePair[]
+ var parameters = new List>();
+
+ if (category == BybitProductCategory.Linear)
{
- new("settleCoin", "USDT")
- };
+ parameters.Add(KeyValuePair.Create("settleCoin", "USDT"));
+ }
+
return FetchAll("/position/list", category, 200, parameters, true);
}
}
\ No newline at end of file
diff --git a/QuantConnect.BybitBrokerage/BybitBrokerage.Brokerage.cs b/QuantConnect.BybitBrokerage/BybitBrokerage.Brokerage.cs
index df57756..96e1383 100644
--- a/QuantConnect.BybitBrokerage/BybitBrokerage.Brokerage.cs
+++ b/QuantConnect.BybitBrokerage/BybitBrokerage.Brokerage.cs
@@ -43,7 +43,7 @@ public partial class BybitBrokerage
public override List GetOpenOrders()
{
var orders = new List();
- foreach (var category in SupportedBybitProductCategories)
+ foreach (var category in GetWorkingProductCategories(_algorithm.BrokerageName))
{
orders.AddRange(ApiClient.Trade.GetOpenOrders(category)
.Select(bybitOrder =>
@@ -98,7 +98,7 @@ public override List GetOpenOrders()
public override List GetAccountHoldings()
{
var holdings = new List();
- foreach (var category in SupportedBybitProductCategories)
+ foreach (var category in GetWorkingProductCategories(_algorithm.BrokerageName))
{
holdings.AddRange(ApiClient.Position.GetPositions(category)
.Select(bybitPosition => new Holding
@@ -121,9 +121,13 @@ public override List GetAccountHoldings()
/// The current cash balance for each currency available for trading
public override List GetCashBalance()
{
- return ApiClient.Account
- .GetWalletBalances().Assets
- .Select(x => new CashAmount(x.WalletBalance, x.Asset)).ToList();
+ var balances = ApiClient.Account.GetWalletBalances();
+ if (GetWorkingProductCategories(_algorithm.BrokerageName).Contains(BybitProductCategory.Inverse))
+ {
+ return [new CashAmount(balances.TotalAvailableBalance ?? 0, "USD")];
+ }
+
+ return balances.Assets.Select(x => new CashAmount(x.WalletBalance, x.Asset)).ToList();
}
///
diff --git a/QuantConnect.BybitBrokerage/BybitBrokerage.Messaging.cs b/QuantConnect.BybitBrokerage/BybitBrokerage.Messaging.cs
index f13ddd0..fa1dd48 100644
--- a/QuantConnect.BybitBrokerage/BybitBrokerage.Messaging.cs
+++ b/QuantConnect.BybitBrokerage/BybitBrokerage.Messaging.cs
@@ -155,7 +155,7 @@ private void HandleOrderExecution(JToken message)
var currency = tradeUpdate.Category switch
{
BybitProductCategory.Linear => "USDT",
- BybitProductCategory.Inverse => GetBaseCurrency(symbol),
+ BybitProductCategory.Inverse => GetBaseCurrency(leanSymbol),
BybitProductCategory.Spot => GetSpotFeeCurrency(leanSymbol, tradeUpdate),
_ => throw new NotSupportedException($"category {tradeUpdate.Category} not implemented")
};
@@ -186,7 +186,7 @@ static string GetSpotFeeCurrency(Symbol symbol, BybitTradeUpdate tradeUpdate)
return tradeUpdate.Side == OrderSide.Buy ? quote : @base;
}
- static string GetBaseCurrency(string pair)
+ static string GetBaseCurrency(Symbol pair)
{
CurrencyPairUtil.DecomposeCurrencyPair(pair, out var baseCurrency, out _);
return baseCurrency;
diff --git a/QuantConnect.BybitBrokerage/BybitBrokerage.cs b/QuantConnect.BybitBrokerage/BybitBrokerage.cs
index b7534cc..b042af1 100644
--- a/QuantConnect.BybitBrokerage/BybitBrokerage.cs
+++ b/QuantConnect.BybitBrokerage/BybitBrokerage.cs
@@ -47,7 +47,7 @@ namespace QuantConnect.Brokerages.Bybit;
[BrokerageFactory(typeof(BybitBrokerageFactory))]
public partial class BybitBrokerage : BaseWebsocketsBrokerage, IDataQueueHandler
{
- private static readonly List SupportedBybitProductCategories = new() { BybitProductCategory.Spot, BybitProductCategory.Linear };
+ private static readonly List SupportedBybitProductCategories = new() { BybitProductCategory.Spot, BybitProductCategory.Linear, BybitProductCategory.Inverse };
private static readonly List SuppotedSecurityTypes = new() { SecurityType.Crypto, SecurityType.CryptoFuture };
@@ -91,7 +91,7 @@ public partial class BybitBrokerage : BaseWebsocketsBrokerage, IDataQueueHandler
///
/// Parameterless constructor for brokerage
///
- public BybitBrokerage() : base(MarketName)
+ public BybitBrokerage() : base(Market.Bybit)
{
}
@@ -130,7 +130,7 @@ public BybitBrokerage(string apiKey, string apiSecret, string restApiUrl, string
public BybitBrokerage(string apiKey, string apiSecret, string restApiUrl, string webSocketBaseUrl,
IAlgorithm algorithm, IOrderProvider orderProvider, ISecurityProvider securityProvider,
IDataAggregator aggregator, LiveNodePacket job, BybitVIPLevel vipLevel = BybitVIPLevel.VIP0)
- : base(MarketName)
+ : base(Market.Bybit)
{
Initialize(
webSocketBaseUrl,
@@ -340,8 +340,9 @@ protected virtual bool CanSubscribe(Symbol symbol)
if (baseCanSubscribe && symbol.SecurityType == SecurityType.CryptoFuture)
{
- //Can only subscribe to non-inverse pairs
- return CurrencyPairUtil.TryDecomposeCurrencyPair(symbol, out _, out var quoteCurrency) && quoteCurrency == "USDT";
+ return CurrencyPairUtil.TryDecomposeCurrencyPair(symbol, out _, out var quoteCurrency) &&
+ (quoteCurrency is "USDT" || SupportedBybitProductCategories.Contains(BybitProductCategory.Inverse) &&
+ quoteCurrency is "USD");
}
return baseCanSubscribe;
@@ -526,19 +527,34 @@ private static BybitProductCategory GetBybitProductCategory(Symbol symbol)
return BybitProductCategory.Spot;
case SecurityType.CryptoFuture:
- if (!CurrencyPairUtil.TryDecomposeCurrencyPair(symbol, out _, out var quoteCurrency) ||
- quoteCurrency != "USDT")
+ if (CurrencyPairUtil.TryDecomposeCurrencyPair(symbol, out _, out var quoteCurrency))
{
- throw new ArgumentException($"Invalid symbol: {symbol}. Only linear futures are supported.");
+ if (quoteCurrency == "USDT")
+ {
+ return BybitProductCategory.Linear;
+ }
+ if (quoteCurrency == "USD")
+ {
+ return BybitProductCategory.Inverse;
+ }
}
-
- return BybitProductCategory.Linear;
+ throw new ArgumentException($"Invalid symbol: {symbol}. Only linear futures are supported.");
default:
throw new ArgumentOutOfRangeException(nameof(symbol), symbol, "Not supported security type");
}
}
+ private IEnumerable GetWorkingProductCategories(BrokerageName brokerageName)
+ {
+ if (brokerageName == BrokerageName.BybitInverseFutures)
+ {
+ return [BybitProductCategory.Inverse];
+ }
+
+ return [BybitProductCategory.Spot, BybitProductCategory.Linear];
+ }
+
///
/// Validate the user of this project has permission to be using it via our web API.
///
diff --git a/QuantConnect.BybitBrokerage/Models/ByBitResponse.cs b/QuantConnect.BybitBrokerage/Models/ByBitResponse.cs
index fa2f32e..4ccfd47 100644
--- a/QuantConnect.BybitBrokerage/Models/ByBitResponse.cs
+++ b/QuantConnect.BybitBrokerage/Models/ByBitResponse.cs
@@ -22,8 +22,7 @@ namespace QuantConnect.Brokerages.Bybit.Models
///
/// Bybits default http response message
///
- ///
- public class ByBitResponse
+ public class ByBitResponse
{
///
/// Success/Error code
@@ -43,16 +42,25 @@ public class ByBitResponse
///
[JsonProperty("retExtInfo")]
public object ExtendedInfo { get; set; }
-
- ///
- /// Business data result
- ///
- public T Result { get; set; }
-
+
///
/// Current time
///
[JsonConverter(typeof(BybitTimeConverter))]
public DateTime Time { get; set; }
}
+
+ ///
+ /// Bybits default http data response message
+ ///
+ ///
+ public class ByBitResponse : ByBitResponse
+ {
+ ///
+ /// Business data result
+ ///
+ public T Result { get; set; }
+
+ }
+
}
\ No newline at end of file
diff --git a/QuantConnect.BybitBrokerage/Models/Enums/PositionMode.cs b/QuantConnect.BybitBrokerage/Models/Enums/PositionMode.cs
new file mode 100644
index 0000000..cb88dd6
--- /dev/null
+++ b/QuantConnect.BybitBrokerage/Models/Enums/PositionMode.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.Brokerages.Bybit.Models.Enums;
+
+///
+/// Bybit position mode
+///
+public enum PositionMode
+{
+ ///
+ /// One way mode
+ ///
+ MergedSingle = 0,
+
+ ///
+ /// Hedge mode
+ ///
+ BothSides = 3,
+}
\ No newline at end of file