Skip to content

Commit ebc014b

Browse files
Johnny A. dos Santosvslee
authored andcommitted
BL3P Websockets (Orderbook) implementation and tests (#470)
* BL3P Websockets and tests Add Orderbook via WebSockets Made OnGetMarketSymbolsMetadataAsync protected internal to be able to mock in tests Add unit tests and timestamp Rename folder and namespace * Update BL3P implementation capabilities
1 parent 7ede01c commit ebc014b

31 files changed

+494
-169
lines changed

ExchangeSharp/API/Exchanges/BL3P/ExchangeBL3PAPI.cs

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Threading.Tasks;
5+
using ExchangeSharp.API.Exchanges.BL3P;
6+
using ExchangeSharp.API.Exchanges.BL3P.Models;
7+
using Newtonsoft.Json;
58
using Newtonsoft.Json.Linq;
69

710
// ReSharper disable once CheckNamespace
@@ -10,10 +13,16 @@ namespace ExchangeSharp
1013
// ReSharper disable once InconsistentNaming
1114
public sealed class ExchangeBL3PAPI : ExchangeAPI
1215
{
13-
public override string BaseUrl { get; set; } = "https://api.bl3p.eu/1/";
16+
public override string BaseUrl { get; set; } = "https://api.bl3p.eu/";
17+
18+
public override string BaseUrlWebSocket { get; set; } = "wss://api.bl3p.eu/1/";
1419

1520
public ExchangeBL3PAPI()
1621
{
22+
MarketSymbolIsUppercase = true;
23+
MarketSymbolIsReversed = true;
24+
MarketSymbolSeparator = string.Empty;
25+
WebSocketOrderBookType = WebSocketOrderBookType.FullBookAlways;
1726
}
1827

1928
public ExchangeBL3PAPI(ref string publicApiKey, ref string privateApiKey)
@@ -29,21 +38,49 @@ public ExchangeBL3PAPI(ref string publicApiKey, ref string privateApiKey)
2938
privateApiKey = null;
3039
}
3140

32-
protected override Task<IEnumerable<string>> OnGetMarketSymbolsAsync()
41+
protected override async Task<IEnumerable<string>> OnGetMarketSymbolsAsync()
3342
{
34-
return Task.FromResult(new[]
35-
{
36-
// For now we only support these two coins
37-
"BTCEUR",
38-
"LTCEUR"
39-
} as IEnumerable<string>);
43+
return (await OnGetMarketSymbolsMetadataAsync().ConfigureAwait(false))
44+
.Select(em => em.MarketSymbol);
4045
}
4146

4247
protected override async Task<ExchangeTicker> OnGetTickerAsync(string marketSymbol)
4348
{
44-
var result = await MakeJsonRequestAsync<JObject>($"/{marketSymbol}/ticker");
49+
var result = await MakeJsonRequestAsync<JObject>($"/{marketSymbol}/ticker")
50+
.ConfigureAwait(false);
4551

46-
return ParseTickerResponse(result, marketSymbol);
52+
return await this.ParseTickerAsync(
53+
result,
54+
marketSymbol,
55+
askKey: "ask",
56+
bidKey: "bid",
57+
lastKey: "last",
58+
baseVolumeKey: "volume.24h",
59+
timestampKey: "timestamp",
60+
timestampType: TimestampType.UnixSeconds
61+
).ConfigureAwait(false);
62+
}
63+
64+
protected internal override Task<IEnumerable<ExchangeMarket>> OnGetMarketSymbolsMetadataAsync()
65+
{
66+
return Task.FromResult(new[]
67+
{
68+
// For now we only support these two coins
69+
new ExchangeMarket
70+
{
71+
BaseCurrency = "BTC",
72+
IsActive = true,
73+
MarketSymbol = "BTCEUR",
74+
QuoteCurrency = "EUR"
75+
},
76+
new ExchangeMarket
77+
{
78+
BaseCurrency = "LTC",
79+
IsActive = true,
80+
MarketSymbol = "LTCEUR",
81+
QuoteCurrency = "EUR"
82+
}
83+
} as IEnumerable<ExchangeMarket>);
4784
}
4885

4986
protected override Task<IEnumerable<KeyValuePair<string, ExchangeTicker>>> OnGetTickersAsync()
@@ -57,26 +94,53 @@ protected override Task<IEnumerable<KeyValuePair<string, ExchangeTicker>>> OnGet
5794
);
5895
}
5996

60-
private ExchangeTicker ParseTickerResponse(JObject result, string marketSymbol)
97+
protected override async Task<IWebSocket> OnGetDeltaOrderBookWebSocketAsync(
98+
Action<ExchangeOrderBook> callback,
99+
int maxCount = 20,
100+
params string[] marketSymbols
101+
)
61102
{
62-
if (result == null)
63-
return null;
64-
65-
return new ExchangeTicker
103+
Task MessageCallback(IWebSocket _, byte[] msg)
66104
{
67-
Ask = result["ask"].ConvertInvariant<decimal>(),
68-
Bid = result["bid"].ConvertInvariant<decimal>(),
69-
Last = result["last"].ConvertInvariant<decimal>(),
70-
Volume = new ExchangeVolume
105+
var bl3POrderBook = JsonConvert.DeserializeObject<BL3POrderBook>(msg.ToStringFromUTF8());
106+
107+
var exchangeOrderBook = new ExchangeOrderBook
71108
{
72-
Timestamp = CryptoUtility.UtcNow,
73-
BaseCurrency = marketSymbol.Substring(0, 3),
74-
BaseCurrencyVolume = result["volume"]["24h"].ConvertInvariant<decimal>()
75-
},
76-
MarketSymbol = marketSymbol
77-
};
109+
MarketSymbol = bl3POrderBook.MarketSymbol,
110+
LastUpdatedUtc = CryptoUtility.UtcNow
111+
};
112+
113+
var asks = bl3POrderBook.Asks
114+
.OrderBy(b => b.Price, exchangeOrderBook.Asks.Comparer)
115+
.Take(maxCount);
116+
foreach (var ask in asks)
117+
{
118+
exchangeOrderBook.Asks.Add(ask.Price, ask.ToExchangeOrder());
119+
}
120+
121+
var bids = bl3POrderBook.Bids
122+
.OrderBy(b => b.Price, exchangeOrderBook.Bids.Comparer)
123+
.Take(maxCount);
124+
foreach (var bid in bids)
125+
{
126+
exchangeOrderBook.Bids.Add(bid.Price, bid.ToExchangeOrder());
127+
}
128+
129+
callback(exchangeOrderBook);
130+
131+
return Task.CompletedTask;
132+
}
133+
134+
return new MultiWebsocketWrapper(
135+
await Task.WhenAll(
136+
marketSymbols.Select(ms => ConnectWebSocketAsync($"{ms}/orderbook", MessageCallback))
137+
).ConfigureAwait(false)
138+
);
78139
}
79-
}
80140

81-
public partial class ExchangeName { public const string BL3P = "BL3P"; }
141+
public partial class ExchangeName
142+
{
143+
public const string BL3P = "BL3P";
144+
}
145+
}
82146
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Newtonsoft.Json;
2+
3+
namespace ExchangeSharp.API.Exchanges.BL3P.Models
4+
{
5+
// ReSharper disable once InconsistentNaming
6+
public class BL3POrder
7+
{
8+
[JsonProperty("price_int")]
9+
[JsonConverter(typeof(FixedIntDecimalConverter), 5)]
10+
public decimal Price { get; set; }
11+
12+
13+
[JsonProperty("amount_int")]
14+
[JsonConverter(typeof(FixedIntDecimalConverter), 8)]
15+
public decimal Amount { get; set; }
16+
17+
public ExchangeOrderPrice ToExchangeOrder()
18+
{
19+
return new ExchangeOrderPrice
20+
{
21+
Amount = Amount,
22+
Price = Price
23+
};
24+
}
25+
}
26+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Newtonsoft.Json;
2+
3+
namespace ExchangeSharp.API.Exchanges.BL3P.Models
4+
{
5+
// ReSharper disable once InconsistentNaming
6+
public class BL3POrderBook
7+
{
8+
[JsonProperty("marketplace")]
9+
public string MarketSymbol { get; set; }
10+
11+
[JsonProperty("asks")]
12+
public BL3POrder[] Asks { get; set; }
13+
14+
[JsonProperty("bids")]
15+
public BL3POrder[] Bids { get; set; }
16+
}
17+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
6+
namespace ExchangeSharp.API.Exchanges.BL3P
7+
{
8+
internal sealed class MultiWebsocketWrapper : IWebSocket
9+
{
10+
private readonly Dictionary<IWebSocket, WsStatus> webSockets;
11+
12+
private enum WsStatus : byte
13+
{
14+
Unknown = 0,
15+
Connected,
16+
Disconnected
17+
}
18+
19+
public MultiWebsocketWrapper(params IWebSocket[] webSockets)
20+
{
21+
this.webSockets = webSockets.ToDictionary(k => k, _ => WsStatus.Unknown);
22+
InstallEventListeners();
23+
}
24+
25+
private void InstallEventListeners()
26+
{
27+
foreach (var ws in webSockets)
28+
{
29+
ws.Key.Connected += socket =>
30+
{
31+
webSockets[socket] = WsStatus.Connected;
32+
33+
if (webSockets.Values.All(v => v == WsStatus.Connected))
34+
{
35+
OnConnected();
36+
}
37+
38+
return Task.CompletedTask;
39+
};
40+
ws.Key.Disconnected += socket =>
41+
{
42+
webSockets[socket] = WsStatus.Disconnected;
43+
44+
foreach (var otherWs in webSockets.Keys)
45+
{
46+
if (!socket.Equals(otherWs))
47+
{
48+
otherWs.Dispose();
49+
}
50+
}
51+
52+
OnDisconnected();
53+
54+
return Task.CompletedTask;
55+
};
56+
}
57+
}
58+
59+
public void Dispose()
60+
{
61+
foreach (var webSocket in webSockets)
62+
{
63+
webSocket.Key?.Dispose();
64+
}
65+
}
66+
67+
public TimeSpan ConnectInterval { get; set; }
68+
69+
public TimeSpan KeepAlive { get; set; }
70+
71+
public event WebSocketConnectionDelegate Connected;
72+
73+
public event WebSocketConnectionDelegate Disconnected;
74+
75+
public async Task<bool> SendMessageAsync(object message)
76+
{
77+
var tasks = await Task.WhenAll(webSockets.Select(ws => ws.Key.SendMessageAsync(message)))
78+
.ConfigureAwait(false);
79+
80+
return tasks.All(r => r);
81+
}
82+
83+
private void OnConnected()
84+
{
85+
Connected?.Invoke(this);
86+
}
87+
88+
private void OnDisconnected()
89+
{
90+
Disconnected?.Invoke(this);
91+
}
92+
}
93+
}

ExchangeSharp/API/Exchanges/Binance/ExchangeBinanceAPI.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ protected override async Task<IEnumerable<string>> OnGetMarketSymbolsAsync()
117117
return symbols;
118118
}
119119

120-
protected override async Task<IEnumerable<ExchangeMarket>> OnGetMarketSymbolsMetadataAsync()
120+
protected internal override async Task<IEnumerable<ExchangeMarket>> OnGetMarketSymbolsMetadataAsync()
121121
{
122122
/*
123123
* {

ExchangeSharp/API/Exchanges/BitMEX/ExchangeBitMEXAPI.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ protected override async Task<IEnumerable<string>> OnGetMarketSymbolsAsync()
8585
}
8686

8787

88-
protected override async Task<IEnumerable<ExchangeMarket>> OnGetMarketSymbolsMetadataAsync()
88+
protected internal override async Task<IEnumerable<ExchangeMarket>> OnGetMarketSymbolsMetadataAsync()
8989
{
9090
/*
9191
{{
@@ -556,10 +556,10 @@ private void AddOrderToPayload(ExchangeOrderRequest order, Dictionary<string, ob
556556
payload["ordType"] = order.OrderType.ToStringInvariant();
557557
payload["side"] = order.IsBuy ? "Buy" : "Sell";
558558
payload["orderQty"] = order.Amount;
559-
559+
560560
if(order.OrderType!=OrderType.Market)
561561
payload["price"] = order.Price;
562-
562+
563563
if (order.ExtraParameters.TryGetValue("execInst", out var execInst))
564564
{
565565
payload["execInst"] = execInst;

ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ protected override async Task<IEnumerable<string>> OnGetMarketSymbolsAsync()
7676
return m.Select(x => NormalizeMarketSymbol(x.MarketSymbol));
7777
}
7878

79-
protected override async Task<IEnumerable<ExchangeMarket>> OnGetMarketSymbolsMetadataAsync()
79+
protected internal override async Task<IEnumerable<ExchangeMarket>> OnGetMarketSymbolsMetadataAsync()
8080
{
8181
var markets = new List<ExchangeMarket>();
8282
JToken allPairs = await MakeJsonRequestAsync<JToken>("/symbols_details", BaseUrlV1);
@@ -394,7 +394,7 @@ protected override async Task<Dictionary<string, decimal>> OnGetAmountsAsync()
394394
{
395395
return await OnGetAmountsAsync("exchange");
396396
}
397-
397+
398398
public async Task<Dictionary<string, decimal>> OnGetAmountsAsync(string type)
399399
{
400400
Dictionary<string, decimal> lookup = new Dictionary<string, decimal>(StringComparer.OrdinalIgnoreCase);
@@ -412,7 +412,7 @@ public async Task<Dictionary<string, decimal>> OnGetAmountsAsync(string type)
412412
}
413413
return lookup;
414414
}
415-
415+
416416
protected override async Task<Dictionary<string, decimal>> OnGetMarginAmountsAvailableToTradeAsync(
417417
bool includeZeroBalances = false)
418418
{
@@ -435,7 +435,7 @@ protected override async Task<Dictionary<string, decimal>> OnGetAmountsAvailable
435435
}
436436
}
437437
return lookup;
438-
}
438+
}
439439

440440
protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrderRequest order)
441441
{
@@ -532,7 +532,7 @@ protected override Task<IWebSocket> OnGetCompletedOrderDetailsWebSocketAsync(Act
532532
await _socket.SendMessageAsync(payloadJSON);
533533
});
534534
}
535-
535+
536536
protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null)
537537
{
538538
Dictionary<string, object> payload = await GetNoncePayloadAsync();

0 commit comments

Comments
 (0)