Skip to content

Commit bb8a5d5

Browse files
authored
Rewrite Binance OnGetUserDataWebSocketAsync() to be multi-exchange friendly (#661)
* Rewrite BinanceGroupCommon.OnGetUserDataWebSocketAsync() to be multi-exchange friendly - this PR contains 3 breaking changes: - ExchangeOrderResult.FillDate has been changed to CompletionDate and made it nullable. This is because this can represent either the Filled date or the Cancelled, Rejected, or Expired date. And it doesn't necessarily have a value. -ExchangeOrderResult.AmountFilled changed to nullable. Not all exchanges provide this value on every call. -removed listenKey from GetUserDataWebSocketAsync() Other changes - inlined call to BinanceGroupCommon.GetListenKeyAsync() - made parse() methods in BinanceGroupCommon static - added ExchangeAPIOrderResultExtensions with Completed enums - added ExchangeBalances - fixed BinanceGroupCommon.OnGetMarketSymbolsAsync() (updated URL)
1 parent 41a56cd commit bb8a5d5

File tree

18 files changed

+252
-63
lines changed

18 files changed

+252
-63
lines changed

src/ExchangeSharp/API/Exchanges/BL3P/ExchangeBL3PAPI.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ protected override async Task<ExchangeOrderResult> OnGetOrderDetailsAsync(string
339339
AmountFilled = result.TotalAmount.Value,
340340
AveragePrice = result.AverageCost?.Value,
341341
FeesCurrency = result.TotalFee.Currency,
342-
FillDate = result.DateClosed ?? DateTime.MinValue,
342+
CompletedDate = result.DateClosed ?? DateTime.MinValue,
343343
IsBuy = result.Type == BL3POrderType.Bid,
344344
MarketSymbol = marketSymbol,
345345
OrderDate = result.Date,

src/ExchangeSharp/API/Exchanges/BinanceGroup/BinanceGroupCommon.cs

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ protected override async Task<IReadOnlyDictionary<string, ExchangeCurrency>> OnG
107107
protected override async Task<IEnumerable<string>> OnGetMarketSymbolsAsync()
108108
{
109109
List<string> symbols = new List<string>();
110-
JToken? obj = await MakeJsonRequestAsync<JToken>("/ticker/allPrices");
110+
JToken? obj = await MakeJsonRequestAsync<JToken>("/ticker/price", BaseUrlApi);
111111
if (!(obj is null))
112112
{
113113
foreach (JToken token in obj)
@@ -782,7 +782,7 @@ protected override async Task<ExchangeWithdrawalResponse> OnWithdrawAsync(Exchan
782782
return withdrawalResponse;
783783
}
784784

785-
private bool ParseMarketStatus(string status)
785+
private static bool ParseMarketStatus(string status)
786786
{
787787
bool isActive = false;
788788
if (!string.IsNullOrWhiteSpace(status))
@@ -817,7 +817,7 @@ private async Task<ExchangeTicker> ParseTickerWebSocketAsync(JToken token)
817817
return await this.ParseTickerAsync(token, marketSymbol, "a", "b", "c", "v", "q", "E", TimestampType.UnixMilliseconds);
818818
}
819819

820-
private ExchangeOrderResult ParseOrder(JToken token)
820+
private static ExchangeOrderResult ParseOrder(JToken token)
821821
{
822822
/*
823823
"symbol": "IOTABTC",
@@ -877,13 +877,13 @@ private ExchangeOrderResult ParseOrder(JToken token)
877877
};
878878

879879
result.ResultCode = token["status"].ToStringInvariant();
880-
result.Result = ParseExchangeAPIOrderResult(result.ResultCode, result.AmountFilled);
880+
result.Result = ParseExchangeAPIOrderResult(result.ResultCode, result.AmountFilled.Value);
881881
ParseAveragePriceAndFeesFromFills(result, token["fills"]);
882882

883883
return result;
884884
}
885885

886-
private ExchangeAPIOrderResult ParseExchangeAPIOrderResult(string status, decimal amountFilled)
886+
internal static ExchangeAPIOrderResult ParseExchangeAPIOrderResult(string status, decimal amountFilled)
887887
{
888888
switch (status)
889889
{
@@ -906,7 +906,7 @@ private ExchangeAPIOrderResult ParseExchangeAPIOrderResult(string status, decima
906906
}
907907
}
908908

909-
private ExchangeOrderResult ParseTrade(JToken token, string symbol)
909+
private static ExchangeOrderResult ParseTrade(JToken token, string symbol)
910910
{
911911
/*
912912
[
@@ -943,7 +943,7 @@ private ExchangeOrderResult ParseTrade(JToken token, string symbol)
943943
return result;
944944
}
945945

946-
private void ParseAveragePriceAndFeesFromFills(ExchangeOrderResult result, JToken fillsToken)
946+
private static void ParseAveragePriceAndFeesFromFills(ExchangeOrderResult result, JToken fillsToken)
947947
{
948948
decimal totalCost = 0;
949949
decimal totalQuantity = 0;
@@ -1065,26 +1065,74 @@ protected override async Task<IEnumerable<ExchangeTransaction>> OnGetDepositHist
10651065
return transactions;
10661066
}
10671067

1068-
protected override async Task<IWebSocket> OnUserDataWebSocketAsync(Action<object> callback, string listenKey)
1068+
protected override async Task<IWebSocket> OnUserDataWebSocketAsync(Action<object> callback)
10691069
{
1070+
var listenKey = await GetListenKeyAsync();
10701071
return await ConnectPublicWebSocketAsync($"/ws/{listenKey}", (_socket, msg) =>
10711072
{
10721073
JToken token = JToken.Parse(msg.ToStringFromUTF8());
10731074
var eventType = token["e"].ToStringInvariant();
1074-
if (eventType == "executionReport")
1075+
switch (eventType)
10751076
{
1076-
var update = JsonConvert.DeserializeObject<ExecutionReport>(token.ToStringInvariant());
1077-
callback(update);
1078-
}
1079-
else if (eventType == "outboundAccountInfo" || eventType == "outboundAccountPosition")
1080-
{
1081-
var update = JsonConvert.DeserializeObject<OutboundAccount>(token.ToStringInvariant());
1082-
callback(update);
1083-
}
1084-
else if (eventType == "listStatus")
1085-
{
1086-
var update = JsonConvert.DeserializeObject<ListStatus>(token.ToStringInvariant());
1087-
callback(update);
1077+
case "executionReport": // systematically check to make sure we are dealing with expected cases here
1078+
{
1079+
var update = JsonConvert.DeserializeObject<ExecutionReport>(token.ToStringInvariant());
1080+
switch (update.CurrentExecutionType)
1081+
{
1082+
case "NEW ": // The order has been accepted into the engine.
1083+
break;
1084+
case "CANCELED": // The order has been canceled by the user.
1085+
break;
1086+
case "REPLACED": // (currently unused)
1087+
throw new NotImplementedException($"ExecutionType {update.CurrentExecutionType} is currently unused"); ;
1088+
case "REJECTED": // The order has been rejected and was not processed. (This is never pushed into the User Data Stream)
1089+
throw new NotImplementedException($"ExecutionType {update.CurrentExecutionType} is never pushed into the User Data Stream"); ;
1090+
case "TRADE": // Part of the order or all of the order's quantity has filled.
1091+
break;
1092+
case "EXPIRED": // The order was canceled according to the order type's rules (e.g. LIMIT FOK orders with no fill, LIMIT IOC or MARKET orders that partially fill) or by the exchange, (e.g. orders canceled during liquidation, orders canceled during maintenance)
1093+
break;
1094+
default: throw new NotImplementedException($"Unexpected ExecutionType {update.CurrentExecutionType}");
1095+
}
1096+
callback(update.ExchangeOrderResult);
1097+
break;
1098+
}
1099+
case "outboundAccountInfo":
1100+
throw new NotImplementedException("has been removed (per binance 2021-01-01)");
1101+
case "outboundAccountPosition":
1102+
{
1103+
var update = JsonConvert.DeserializeObject<OutboundAccount>(token.ToStringInvariant());
1104+
callback(new ExchangeBalances()
1105+
{
1106+
EventTime = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(update.EventTime),
1107+
BalancesUpdateType = BalancesUpdateType.Total,
1108+
Balances = update.BalancesAsTotalDictionary
1109+
});
1110+
callback(new ExchangeBalances()
1111+
{
1112+
EventTime = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(update.EventTime),
1113+
BalancesUpdateType = BalancesUpdateType.AvailableToTrade,
1114+
Balances = update.BalancesAsAvailableToTradeDictionary
1115+
});
1116+
break;
1117+
}
1118+
case "listStatus":
1119+
{ // fired as part of OCO order update
1120+
// var update = JsonConvert.DeserializeObject<ListStatus>(token.ToStringInvariant());
1121+
// no need to parse or call callback(), since OCO updates also send "executionReport"
1122+
break;
1123+
}
1124+
case "balanceUpdate":
1125+
{
1126+
var update = JsonConvert.DeserializeObject<BalanceUpdate>(token.ToStringInvariant());
1127+
callback(new ExchangeBalances()
1128+
{
1129+
EventTime = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(update.EventTime),
1130+
BalancesUpdateType = BalancesUpdateType.Delta,
1131+
Balances = new Dictionary<string, decimal>() { { update.Asset, update.BalanceDelta } }
1132+
});
1133+
break;
1134+
}
1135+
default: throw new NotImplementedException($"Unexpected event type {eventType}");
10881136
}
10891137
return Task.CompletedTask;
10901138
});

src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/UserDataStream.cs

Lines changed: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ internal class ExecutionReport
5050
[JsonProperty("L")]
5151
public decimal LastExecutedPrice { get; set; }
5252
[JsonProperty("n")]
53-
public string CommissionAmount { get; set; }
53+
public decimal CommissionAmount { get; set; }
5454
[JsonProperty("N")]
5555
public string CommissionAsset { get; set; }
5656
[JsonProperty("T")]
@@ -62,7 +62,7 @@ internal class ExecutionReport
6262
[JsonProperty("m")]
6363
public string IsThisTradeTheMakerSide { get; set; }
6464
[JsonProperty("O")]
65-
public string OrderCreationTime { get; set; }
65+
public long OrderCreationTime { get; set; }
6666
[JsonProperty("Z")]
6767
public decimal CumulativeQuoteAssetTransactedQuantity { get; set; }
6868
[JsonProperty("Y")]
@@ -73,6 +73,34 @@ public override string ToString()
7373
return $"{nameof(Symbol)}: {Symbol}, {nameof(OrderType)}: {OrderType}, {nameof(OrderQuantity)}: {OrderQuantity}, {nameof(OrderPrice)}: {OrderPrice}, {nameof(CurrentOrderStatus)}: {CurrentOrderStatus}, {nameof(OrderId)}: {OrderId}";
7474
}
7575

76+
/// <summary>
77+
/// convert current instance to ExchangeOrderResult
78+
/// </summary>
79+
public ExchangeOrderResult ExchangeOrderResult
80+
{
81+
get
82+
{
83+
var status = BinanceGroupCommon.ParseExchangeAPIOrderResult(status: CurrentOrderStatus, amountFilled: CumulativeFilledQuantity);
84+
return new ExchangeOrderResult()
85+
{
86+
OrderId = OrderId.ToString(),
87+
ClientOrderId = ClientOrderId,
88+
Result = status,
89+
ResultCode = CurrentOrderStatus,
90+
Message = null, // can use for something in the future if needed
91+
Amount = CumulativeFilledQuantity,
92+
Price = OrderPrice,
93+
AveragePrice = CumulativeQuoteAssetTransactedQuantity / CumulativeFilledQuantity, // Average price can be found by doing Z divided by z.
94+
OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(OrderCreationTime),
95+
CompletedDate = status.IsCompleted() ? (DateTime?)CryptoUtility.UnixTimeStampToDateTimeMilliseconds(EventTime) : null,
96+
MarketSymbol = Symbol,
97+
// IsBuy is not provided here
98+
Fees = CommissionAmount,
99+
FeesCurrency = CommissionAsset,
100+
TradeId = TradeId,
101+
};
102+
}
103+
}
76104
}
77105

78106
internal class Order
@@ -121,6 +149,28 @@ public override string ToString()
121149
}
122150
}
123151

152+
/// <summary>
153+
/// For Binance User Data stream (different from Balance): Balance Update occurs during the following:
154+
/// - Deposits or withdrawals from the account
155+
/// - Transfer of funds between accounts(e.g.Spot to Margin)
156+
/// </summary>
157+
internal class BalanceUpdate
158+
{
159+
[JsonProperty("e")]
160+
public string EventType { get; set; }
161+
[JsonProperty("E")]
162+
public long EventTime { get; set; }
163+
[JsonProperty("a")]
164+
public string Asset { get; set; }
165+
[JsonProperty("d")]
166+
public decimal BalanceDelta { get; set; }
167+
[JsonProperty("T")]
168+
public long ClearTime { get; set; }
169+
}
170+
171+
/// <summary>
172+
/// As part of outboundAccountPosition from Binance User Data Stream (different from BalanceUpdate)
173+
/// </summary>
124174
internal class Balance
125175
{
126176
[JsonProperty("a")]
@@ -136,29 +186,48 @@ public override string ToString()
136186
}
137187
}
138188

189+
/// <summary>
190+
/// outboundAccountPosition is sent any time an account balance has changed and contains
191+
/// the assets that were possibly changed by the event that generated the balance change.
192+
/// </summary>
139193
internal class OutboundAccount
140194
{
141195
[JsonProperty("e")]
142196
public string EventType { get; set; }
143197
[JsonProperty("E")]
144198
public long EventTime { get; set; }
145-
[JsonProperty("m")]
146-
public int MakerCommissionRate { get; set; }
147-
[JsonProperty("t")]
148-
public int TakerCommissionRate { get; set; }
149-
[JsonProperty("b")]
150-
public int BuyerCommissionRate { get; set; }
151-
[JsonProperty("s")]
152-
public int SellerCommissionRate { get; set; }
153-
[JsonProperty("T")]
154-
public bool CanTrade { get; set; }
155-
[JsonProperty("W")]
156-
public bool CanWithdraw { get; set; }
157-
[JsonProperty("D")]
158-
public bool CanDeposit { get; set; }
159199
[JsonProperty("u")]
160200
public long LastAccountUpdate { get; set; }
161201
[JsonProperty("B")]
162202
public List<Balance> Balances { get; set; }
203+
204+
/// <summary> convert the Balances list to a dictionary of total amounts </summary>
205+
public Dictionary<string, decimal> BalancesAsTotalDictionary
206+
{
207+
get
208+
{
209+
var dict = new Dictionary<string, decimal>();
210+
foreach (var balance in Balances)
211+
{
212+
dict.Add(balance.Asset, balance.Free + balance.Locked);
213+
}
214+
return dict;
215+
}
216+
}
217+
218+
/// <summary> convert the Balances list to a dictionary of available to trade amounts </summary>
219+
public Dictionary<string, decimal> BalancesAsAvailableToTradeDictionary
220+
{
221+
get
222+
{
223+
var dict = new Dictionary<string, decimal>();
224+
foreach (var balance in Balances)
225+
{
226+
dict.Add(balance.Asset, balance.Free);
227+
}
228+
return dict;
229+
}
230+
}
231+
163232
}
164233
}

src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ private ExchangeOrderResult ParseOrderCore(JToken token)
402402
};
403403
res.Fees = token["type"].ToStringInvariant() == "limit" ? MakerFee * res.Amount : TakerFee * res.Amount;
404404
res.Price = token["price"].ConvertInvariant<decimal>();
405-
res.FillDate = token["executed_at"] == null ? default : token["executed_at"].ConvertInvariant<double>().UnixTimeStampToDateTimeMilliseconds();
405+
res.CompletedDate = token["executed_at"] == null ? default : token["executed_at"].ConvertInvariant<double>().UnixTimeStampToDateTimeMilliseconds();
406406
res.FeesCurrency = res.MarketSymbol.Substring(0, 3);
407407
return res;
408408
}

src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -987,7 +987,8 @@ FEE_CURRENCY string Fee currency
987987
foreach (JToken trade in kv.Value)
988988
{
989989
ExchangeOrderResult append = new ExchangeOrderResult { MarketSymbol = kv.Key, OrderId = trade[3].ToStringInvariant() };
990-
append.Amount = append.AmountFilled = Math.Abs(trade[4].ConvertInvariant<decimal>());
990+
append.Amount = Math.Abs(trade[4].ConvertInvariant<decimal>());
991+
append.AmountFilled = Math.Abs(trade[4].ConvertInvariant<decimal>());
991992
append.Price = trade[7].ConvertInvariant<decimal>();
992993
append.AveragePrice = trade[5].ConvertInvariant<decimal>();
993994
append.IsBuy = trade[4].ConvertInvariant<decimal>() >= 0m;

src/ExchangeSharp/API/Exchanges/Bitstamp/ExchangeBitstampAPI.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ protected override async Task<IEnumerable<ExchangeOrderResult>> OnGetCompletedOr
476476
List<ExchangeOrderResult> orders2 = new List<ExchangeOrderResult>();
477477
foreach (var group in groupings)
478478
{
479-
decimal spentQuoteCurrency = group.Sum(o => o.AveragePrice.Value * o.AmountFilled);
479+
decimal spentQuoteCurrency = group.Sum(o => o.AveragePrice.Value * o.AmountFilled.Value);
480480
ExchangeOrderResult order = group.First();
481481
order.AmountFilled = group.Sum(o => o.AmountFilled);
482482
order.AveragePrice = spentQuoteCurrency / order.AmountFilled;

src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ private ExchangeOrderResult ParseOrder(JToken result)
9494
AveragePrice = averagePrice,
9595
IsBuy = (result["side"].ToStringInvariant() == "buy"),
9696
OrderDate = result["created_at"].ToDateTimeInvariant(),
97-
FillDate = result["done_at"].ToDateTimeInvariant(),
97+
CompletedDate = result["done_at"].ToDateTimeInvariant(),
9898
MarketSymbol = marketSymbol,
9999
OrderId = result["id"].ToStringInvariant()
100100
};

src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ protected override async Task<IEnumerable<ExchangeOrderResult>> OnGetOpenOrderDe
319319
MarketSymbol = x["symbol"].ToStringUpperInvariant(),
320320
OrderId = x["order_id"].ToStringInvariant(),
321321
OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["created_date"].ConvertInvariant<long>()),
322-
FillDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["finished_date"].ConvertInvariant<long>()),
322+
CompletedDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["finished_date"].ConvertInvariant<long>()),
323323
Price = x["price"].ConvertInvariant<decimal>(),
324324
AveragePrice = x["avg_price"].ConvertInvariant<decimal>(),
325325
Amount = x["amount"].ConvertInvariant<decimal>(),
@@ -355,7 +355,7 @@ protected override async Task<IEnumerable<ExchangeOrderResult>> OnGetCompletedOr
355355
AmountFilled = x["amount"].ConvertInvariant<decimal>(),
356356
Fees = x["fee"].ConvertInvariant<decimal>(),
357357
FeesCurrency = x["fee_currency"].ToStringInvariant(),
358-
FillDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["timestamp"].ConvertInvariant<long>()),
358+
CompletedDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["timestamp"].ConvertInvariant<long>()),
359359
IsBuy = x["side"].ToStringLowerInvariant() == "buy",
360360
Result = ExchangeAPIOrderResult.Unknown,
361361
});
@@ -372,7 +372,7 @@ protected override async Task<ExchangeOrderResult> OnGetOrderDetailsAsync(string
372372
MarketSymbol = x["symbol"].ToStringUpperInvariant(),
373373
OrderId = x["order_id"].ToStringInvariant(),
374374
OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["created_date"].ConvertInvariant<long>()),
375-
FillDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["finished_date"].ConvertInvariant<long>()),
375+
CompletedDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["finished_date"].ConvertInvariant<long>()),
376376
Price = x["price"].ConvertInvariant<decimal>(),
377377
AveragePrice = x["avg_price"].ConvertInvariant<decimal>(),
378378
Amount = x["amount"].ConvertInvariant<decimal>(),

src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ private async Task<ExchangeOrderResult> ParseHistoryOrder(string orderId, JToken
242242
orderResult.TradeId = order["postxid"].ToStringInvariant(); //verify which is orderid & tradeid
243243
orderResult.OrderId = order["ordertxid"].ToStringInvariant(); //verify which is orderid & tradeid
244244
orderResult.AmountFilled = order["vol"].ConvertInvariant<decimal>();
245-
orderResult.FillDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(order["time"].ConvertInvariant<double>());
245+
orderResult.CompletedDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(order["time"].ConvertInvariant<double>());
246246

247247
string[] pairs = (await ExchangeMarketSymbolToGlobalMarketSymbolAsync(order["pair"].ToStringInvariant())).Split('-');
248248
orderResult.FeesCurrency = pairs[1];

src/ExchangeSharp/API/Exchanges/KuCoin/ExchangeKuCoinAPI.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,7 @@ private ExchangeOrderResult ParseOpenOrder(JToken token)
645645

646646
// Amount and Filled are returned as Sold and Pending, so we'll adjust
647647
order.AmountFilled = token["dealSize"].ConvertInvariant<decimal>();
648-
order.Amount = token["size"].ConvertInvariant<decimal>() + order.AmountFilled;
648+
order.Amount = token["size"].ConvertInvariant<decimal>() + order.AmountFilled.Value;
649649

650650
if (order.Amount == order.AmountFilled) order.Result = ExchangeAPIOrderResult.Filled;
651651
else if (order.AmountFilled == 0m) order.Result = ExchangeAPIOrderResult.Pending;

0 commit comments

Comments
 (0)