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