From b06b12a0fc68d5302a9c95074999703d6c61dfe6 Mon Sep 17 00:00:00 2001 From: Mariusz Kotas Date: Tue, 20 Feb 2024 10:55:37 +0100 Subject: [PATCH] Implement Binance order source --- Directory.Build.props | 2 +- .../Orders/CryptoOrders.cs | 27 ++- .../Orders/ICryptoOrders.cs | 14 +- .../Orders/Models/CryptoOrder.cs | 28 ++- .../Utils/CryptoPairsHelper.cs | 4 +- .../Crypto.Websocket.Extensions.csproj | 2 +- .../Orders/Sources/BinanceOrderSource.cs | 201 ++++++++++++++++++ .../Orders/Sources/BitmexOrderSource.cs | 2 +- .../OrdersExample.cs | 31 ++- .../Program.cs | 5 + 10 files changed, 283 insertions(+), 33 deletions(-) create mode 100644 src/Crypto.Websocket.Extensions/Orders/Sources/BinanceOrderSource.cs diff --git a/Directory.Build.props b/Directory.Build.props index c52c13e..ef44980 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ - 2.7.0 + 2.7.1 diff --git a/src/Crypto.Websocket.Extensions.Core/Orders/CryptoOrders.cs b/src/Crypto.Websocket.Extensions.Core/Orders/CryptoOrders.cs index c270215..7be42eb 100644 --- a/src/Crypto.Websocket.Extensions.Core/Orders/CryptoOrders.cs +++ b/src/Crypto.Websocket.Extensions.Core/Orders/CryptoOrders.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using System.Reactive.Subjects; @@ -31,7 +32,7 @@ public class CryptoOrders : ICryptoOrders /// Orders source /// Select prefix if you want to distinguish orders /// Select target pair, if you want to filter monitored orders - public CryptoOrders(IOrderSource source, long? orderPrefix = null, string targetPair = null) + public CryptoOrders(IOrderSource source, long? orderPrefix = null, string? targetPair = null) { CryptoValidations.ValidateInput(source, nameof(source)); @@ -82,17 +83,17 @@ public CryptoOrders(IOrderSource source, long? orderPrefix = null, string target /// /// Originally provided target pair for this orders data /// - public string TargetPairOriginal { get; private set; } + public string? TargetPairOriginal { get; private set; } /// /// Last executed (or partially filled) buy order /// - public CryptoOrder LastExecutedBuyOrder { get; private set; } + public CryptoOrder? LastExecutedBuyOrder { get; private set; } /// /// Last executed (or partially filled) sell order /// - public CryptoOrder LastExecutedSellOrder { get; private set; } + public CryptoOrder? LastExecutedSellOrder { get; private set; } /// @@ -145,7 +146,7 @@ public CryptoOrderCollectionReadonly GetAllOrders() /// /// Find active order by provided unique id /// - public CryptoOrder FindActiveOrder(string id) + public CryptoOrder? FindActiveOrder(string id) { if (GetActiveOrders().ContainsKey(id)) return _idToOrder[id]; @@ -155,11 +156,9 @@ public CryptoOrder FindActiveOrder(string id) /// /// Find order by provided unique id /// - public CryptoOrder FindOrder(string id) + public CryptoOrder? FindOrder(string id) { - if (_idToOrder.ContainsKey(id)) - return _idToOrder[id]; - return null; + return _idToOrder.GetValueOrDefault(id); } /// @@ -191,7 +190,7 @@ public bool IsOurOrder(CryptoOrder order) /// /// Returns true if client id matches prefix /// - public bool IsOurOrder(string clientId) + public bool IsOurOrder(string? clientId) { if (string.IsNullOrWhiteSpace(ClientIdPrefixString)) return true; @@ -235,7 +234,7 @@ private void Subscribe() _source.OrderUpdatedStream.Subscribe(OnOrderUpdated); } - private void OnOrdersUpdated(CryptoOrder[] orders) + private void OnOrdersUpdated(CryptoOrder[]? orders) { if (orders == null) return; @@ -246,7 +245,7 @@ private void OnOrdersUpdated(CryptoOrder[] orders) } } - private void OnOrderCreated(CryptoOrder order) + private void OnOrderCreated(CryptoOrder? order) { if (order == null) return; @@ -254,7 +253,7 @@ private void OnOrderCreated(CryptoOrder order) HandleOrderUpdated(order); } - private void OnOrderUpdated(CryptoOrder order) + private void OnOrderUpdated(CryptoOrder? order) { if (order == null) return; @@ -291,7 +290,7 @@ private void HandleOrderUpdated(CryptoOrder order) _ourOrderChanged.OnNext(order); } - private bool IsFilteredOut(CryptoOrder order) + private bool IsFilteredOut(CryptoOrder? order) { if (order == null) return true; diff --git a/src/Crypto.Websocket.Extensions.Core/Orders/ICryptoOrders.cs b/src/Crypto.Websocket.Extensions.Core/Orders/ICryptoOrders.cs index e99ee95..857d510 100644 --- a/src/Crypto.Websocket.Extensions.Core/Orders/ICryptoOrders.cs +++ b/src/Crypto.Websocket.Extensions.Core/Orders/ICryptoOrders.cs @@ -45,17 +45,17 @@ public interface ICryptoOrders /// /// Originally provided target pair for this orders data /// - string TargetPairOriginal { get; } + string? TargetPairOriginal { get; } /// /// Last executed (or partially filled) buy order /// - CryptoOrder LastExecutedBuyOrder { get; } + CryptoOrder? LastExecutedBuyOrder { get; } /// /// Last executed (or partially filled) sell order /// - CryptoOrder LastExecutedSellOrder { get; } + CryptoOrder? LastExecutedSellOrder { get; } /// /// Generate a new client id (with prefix) @@ -80,22 +80,22 @@ public interface ICryptoOrders /// /// Find active order by provided unique id /// - CryptoOrder FindActiveOrder(string id); + CryptoOrder? FindActiveOrder(string id); /// /// Find order by provided unique id /// - CryptoOrder FindOrder(string id); + CryptoOrder? FindOrder(string id); /// /// Find active order by provided client id /// - CryptoOrder FindActiveOrderByClientId(string clientId); + CryptoOrder? FindActiveOrderByClientId(string clientId); /// /// Find order by provided client id /// - CryptoOrder FindOrderByClientId(string clientId); + CryptoOrder? FindOrderByClientId(string clientId); /// /// Returns true if client id matches prefix diff --git a/src/Crypto.Websocket.Extensions.Core/Orders/Models/CryptoOrder.cs b/src/Crypto.Websocket.Extensions.Core/Orders/Models/CryptoOrder.cs index 3fe53ba..1c8589f 100644 --- a/src/Crypto.Websocket.Extensions.Core/Orders/Models/CryptoOrder.cs +++ b/src/Crypto.Websocket.Extensions.Core/Orders/Models/CryptoOrder.cs @@ -22,22 +22,22 @@ public class CryptoOrder /// /// Unique order id (provided by exchange) /// - public string Id { get; set; } + public string Id { get; set; } = null!; /// /// Group id of related orders (provided by client, supported only by a few exchanges) /// - public string GroupId { get; set; } + public string? GroupId { get; set; } /// /// Unique client order id (provided by client) /// - public string ClientId { get; set; } + public string? ClientId { get; set; } /// /// Pair to which this order belongs /// - public string Pair { get; set; } + public string Pair { get; set; } = null!; /// /// Pair to which this order belongs (cleaned) @@ -143,6 +143,11 @@ public double? AmountOrigQuote /// public CryptoOrderStatus OrderStatus { get; set; } + /// + /// Current order's status in a raw format + /// + public string OrderStatusRaw { get; set; } = string.Empty; + /// /// Order's price /// @@ -172,8 +177,21 @@ public double? AmountOrigQuote /// Whenever order was executed on margin /// public bool OnMargin { get; set; } + + /// + /// Reason why order was canceled/rejected + /// + public string? CanceledReason { get; set; } - + /// + /// Paid fee + /// + public double Fee { get; set; } + + /// + /// Currency of the paid fee + /// + public string? FeeCurrency { get; set; } private double? WithCorrectSign(double? value) diff --git a/src/Crypto.Websocket.Extensions.Core/Utils/CryptoPairsHelper.cs b/src/Crypto.Websocket.Extensions.Core/Utils/CryptoPairsHelper.cs index 66053be..26cc9fe 100644 --- a/src/Crypto.Websocket.Extensions.Core/Utils/CryptoPairsHelper.cs +++ b/src/Crypto.Websocket.Extensions.Core/Utils/CryptoPairsHelper.cs @@ -8,7 +8,7 @@ public static class CryptoPairsHelper /// /// Clean pair from any unnecessary characters and make lowercase /// - public static string Clean(string pair) + public static string Clean(string? pair) { return (pair ?? string.Empty) .Trim() @@ -21,7 +21,7 @@ public static string Clean(string pair) /// /// Compare two pairs, clean them before /// - public static bool AreSame(string firstPair, string secondPair) + public static bool AreSame(string? firstPair, string? secondPair) { var first = Clean(firstPair); var second = Clean(secondPair); diff --git a/src/Crypto.Websocket.Extensions/Crypto.Websocket.Extensions.csproj b/src/Crypto.Websocket.Extensions/Crypto.Websocket.Extensions.csproj index 9f759ba..c50f2f2 100644 --- a/src/Crypto.Websocket.Extensions/Crypto.Websocket.Extensions.csproj +++ b/src/Crypto.Websocket.Extensions/Crypto.Websocket.Extensions.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/Crypto.Websocket.Extensions/Orders/Sources/BinanceOrderSource.cs b/src/Crypto.Websocket.Extensions/Orders/Sources/BinanceOrderSource.cs new file mode 100644 index 0000000..8fad0d4 --- /dev/null +++ b/src/Crypto.Websocket.Extensions/Orders/Sources/BinanceOrderSource.cs @@ -0,0 +1,201 @@ +using System; +using System.Linq; +using Binance.Client.Websocket.Client; +using Binance.Client.Websocket.Responses.Orders; +using Crypto.Websocket.Extensions.Core.Models; +using Crypto.Websocket.Extensions.Core.Orders; +using Crypto.Websocket.Extensions.Core.Orders.Models; +using Crypto.Websocket.Extensions.Core.Orders.Sources; +using Crypto.Websocket.Extensions.Core.Validations; +using Microsoft.Extensions.Logging; + +namespace Crypto.Websocket.Extensions.Orders.Sources +{ + /// + /// Binance orders source + /// + public class BinanceOrderSource : OrderSourceBase + { + private readonly CryptoOrderCollection _partiallyFilledOrders = new CryptoOrderCollection(); + private BinanceWebsocketClient _client = null!; + private IDisposable? _subscription; + + /// + public BinanceOrderSource(BinanceWebsocketClient client) : base(client.Logger) + { + ChangeClient(client); + } + + /// + public override string ExchangeName => "binance"; + + /// + /// Change client and resubscribe to the new streams + /// + public void ChangeClient(BinanceWebsocketClient client) + { + CryptoValidations.ValidateInput(client, nameof(client)); + + _client = client; + _subscription?.Dispose(); + Subscribe(); + } + + private void Subscribe() + { + _subscription = _client.Streams.OrderUpdateStream.Subscribe(HandleOrdersSafe); + } + + private void HandleOrdersSafe(OrderUpdate response) + { + try + { + HandleOrders(response); + } + catch (Exception e) + { + _client.Logger.LogError(e, "[Binance] Failed to handle order info, error: '{error}'", e.Message); + } + } + + private void HandleOrders(OrderUpdate response) + { + var order = ConvertOrder(response); + OrderUpdatedSubject.OnNext(order); + } + + /// + /// Convert Binance orders to crypto orders + /// + public CryptoOrder[] ConvertOrders(OrderUpdate[] orders) + { + return orders + .Select(ConvertOrder) + .ToArray(); + } + + /// + /// Convert Binance order to crypto order + /// + public CryptoOrder ConvertOrder(OrderUpdate order) + { + var id = order.Id.ToString(); + var existingCurrent = ExistingOrders.ContainsKey(id) ? ExistingOrders[id] : null; + var existingPartial = _partiallyFilledOrders.ContainsKey(id) ? _partiallyFilledOrders[id] : null; + var existing = existingPartial ?? existingCurrent; + + var price = Math.Abs(FirstNonZero(order.LastPriceFilled, order.Price, existing?.Price) ?? 0); + + var priceAvgBasedOnQuantity = + order.QuantityFilled > 0 ? order.QuoteQuantityFilled / order.QuantityFilled : 0; + var priceAvg = Math.Abs(FirstNonZero(priceAvgBasedOnQuantity, order.LastPriceFilled, order.Price, existing?.PriceAverage) ?? 0); + + var amountQuote = + FirstNonZero(order.QuoteQuantity, order.Quantity * order.Price, existing?.AmountOrigQuote); + + var currentStatus = ConvertOrderStatus(order); + + var newOrder = new CryptoOrder + { + Id = id, + GroupId = order.TradeGroupId?.ToString() ?? existing?.GroupId ?? null, + ClientId = !string.IsNullOrWhiteSpace(order.ClientOrderId) ? + order.ClientOrderId : + order.OriginalClientOrderId ?? existing?.ClientId, + Pair = order.Symbol ?? existing?.Pair ?? string.Empty, + Side = order.Side == OrderSide.Sell ? CryptoOrderSide.Ask : CryptoOrderSide.Bid, + AmountFilled = order.LastQuantityFilled, + AmountFilledCumulative = order.QuantityFilled, + AmountOrig = order.Quantity, + AmountFilledQuote = order.LastQuoteQuantity, + AmountFilledCumulativeQuote = order.QuoteQuantityFilled, + AmountOrigQuote = amountQuote, + Created = order.CreateTime ?? existing?.Created, + Updated = order.UpdateTime ?? order.EventTime ?? existing?.Updated, + Price = price, + PriceAverage = priceAvg, + Fee = order.Fee, + FeeCurrency = order.FeeAsset, + OrderStatus = currentStatus, + OrderStatusRaw = order.Status.ToString(), + CanceledReason = order.RejectReason.ToString(), + Type = ConvertOrderType(order.Type) ?? existing?.Type ?? CryptoOrderType.Undefined, + TypePrev = existing?.TypePrev ?? existing?.Type ?? ConvertOrderType(order.Type) ?? CryptoOrderType.Undefined, + OnMargin = existing?.OnMargin ?? false + }; + + + if (currentStatus == CryptoOrderStatus.PartiallyFilled) + { + // save partially filled orders + _partiallyFilledOrders[newOrder.Id] = newOrder; + } + + return newOrder; + } + + + /// + /// Convert order type + /// + public static CryptoOrderType? ConvertOrderType(OrderType type) + { + switch (type) + { + case OrderType.Market: + return CryptoOrderType.Market; + case OrderType.StopLoss: + return CryptoOrderType.Stop; + case OrderType.StopLossLimit: + return CryptoOrderType.StopLimit; + case OrderType.Limit: + case OrderType.LimitMaker: + return CryptoOrderType.Limit; + case OrderType.TakeProfitLimit: + return CryptoOrderType.TakeProfitLimit; + case OrderType.TakeProfit: + return CryptoOrderType.TakeProfitMarket; + default: + return null; + } + } + + /// + /// Convert order status + /// + public static CryptoOrderStatus ConvertOrderStatus(OrderUpdate order) + { + var status = order.Status; + switch (status) + { + case OrderStatus.New: + return order.IsWorking ? CryptoOrderStatus.Active : CryptoOrderStatus.New; + case OrderStatus.PartiallyFilled: + return CryptoOrderStatus.PartiallyFilled; + case OrderStatus.Filled: + return CryptoOrderStatus.Executed; + default: + return CryptoOrderStatus.Canceled; + } + } + + + private static double? FirstNonZero(params double?[] numbers) + { + foreach (var number in numbers) + { + if (number.HasValue && Math.Abs(number.Value) > 0) + return number.Value; + } + + return null; + } + + private static double? Abs(double? value) + { + if (!value.HasValue) + return null; + return Math.Abs(value.Value); + } + } +} diff --git a/src/Crypto.Websocket.Extensions/Orders/Sources/BitmexOrderSource.cs b/src/Crypto.Websocket.Extensions/Orders/Sources/BitmexOrderSource.cs index 0a3993c..3e1c843 100644 --- a/src/Crypto.Websocket.Extensions/Orders/Sources/BitmexOrderSource.cs +++ b/src/Crypto.Websocket.Extensions/Orders/Sources/BitmexOrderSource.cs @@ -173,7 +173,7 @@ public CryptoOrder ConvertOrder(Order order) ClientId = !string.IsNullOrWhiteSpace(order.ClOrdId) ? order.ClOrdId : existing?.ClientId, - Pair = order.Symbol ?? existing?.Pair, + Pair = order.Symbol ?? existing?.Pair ?? string.Empty, Side = order.Side == BitmexSide.Sell ? CryptoOrderSide.Ask : CryptoOrderSide.Bid, AmountFilled = amountFilled, AmountFilledCumulative = amountFilledCumulative, diff --git a/test_integration/Crypto.Websocket.Extensions.Sample/OrdersExample.cs b/test_integration/Crypto.Websocket.Extensions.Sample/OrdersExample.cs index 3a3ee51..fdd33bb 100644 --- a/test_integration/Crypto.Websocket.Extensions.Sample/OrdersExample.cs +++ b/test_integration/Crypto.Websocket.Extensions.Sample/OrdersExample.cs @@ -1,5 +1,9 @@ using System; using System.Threading.Tasks; +using Binance.Client.Websocket; +using Binance.Client.Websocket.Client; +using Binance.Client.Websocket.Signing; +using Binance.Client.Websocket.Websockets; using Bitmex.Client.Websocket; using Bitmex.Client.Websocket.Client; using Bitmex.Client.Websocket.Websockets; @@ -22,7 +26,8 @@ public static class OrdersExample public static async Task RunEverything() { - var ordBitmex = await StartBitmex(false, HandleOrderChanged, HandleWalletsChanged, HandlePositionsChanged); + //var ordBitmex = await StartBitmex(false, HandleOrderChanged, HandleWalletsChanged, HandlePositionsChanged); + var ordBinance = await StartBinance(HandleOrderChanged); Log.Information("Waiting for orders..."); } @@ -33,7 +38,7 @@ private static void HandleOrderChanged(CryptoOrder order) $"Price: {order.PriceGrouped}, Amount: {order.AmountOrig:#.#####}/{order.AmountOrigQuote}, " + $"cumulative: {order.AmountFilledCumulative:#.#####}/{order.AmountFilledCumulativeQuote}, " + $"filled: {order.AmountFilled:#.#####}/{order.AmountFilledQuote}, " + - $"Status: {order.OrderStatus}"); + $"Status: {order.OrderStatus} ({order.OrderStatusRaw})"); } private static void HandleWalletsChanged(CryptoWallet[] wallets) @@ -98,12 +103,34 @@ private static async Task StartBitmex(bool isTestnet, Action { + Log.Information("[Bitmex] Reconnected, type: {type}", x.Type); client.Authenticate(ApiKey, ApiSecret); }); await communicator.Start(); + return orders; + } + + private static async Task StartBinance(Action handler) + { + var url = BinanceValues.ApiWebsocketUrl; + var communicator = new BinanceWebsocketCommunicator(url, Program.Logger.CreateLogger()) { Name = "Binance" }; + var client = new BinanceWebsocketClient(communicator, Program.Logger.CreateLogger()); + + var source = new BinanceOrderSource(client); + var orders = new CryptoOrders(source); + orders.OrderChangedStream.Subscribe(handler); + + communicator.ReconnectionHappened.Subscribe(x => + { + Log.Information("[Binance] Reconnected, type: {type}", x.Type); + }); + + await communicator.Authenticate(ApiKey, new BinanceHmac(ApiSecret)); + await communicator.Start(); + return orders; } } diff --git a/test_integration/Crypto.Websocket.Extensions.Sample/Program.cs b/test_integration/Crypto.Websocket.Extensions.Sample/Program.cs index b46b6b1..4238799 100644 --- a/test_integration/Crypto.Websocket.Extensions.Sample/Program.cs +++ b/test_integration/Crypto.Websocket.Extensions.Sample/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.IO; using System.Reflection; using System.Runtime; @@ -17,6 +18,10 @@ class Program static void Main(string[] args) { + var defaultCulture = CultureInfo.InvariantCulture; + CultureInfo.DefaultThreadCurrentCulture = defaultCulture; + CultureInfo.DefaultThreadCurrentUICulture = defaultCulture; + GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency; Logger = InitLogging();