Skip to content
This repository was archived by the owner on Jun 8, 2020. It is now read-only.

Commit 09a7cf6

Browse files
authored
Merge pull request #227 from badgerwithagun/binance-orderbook-initial-snapshot
[Binance] Orderbook initial snapshot
2 parents 9df133e + cc532a5 commit 09a7cf6

File tree

5 files changed

+193
-28
lines changed

5 files changed

+193
-28
lines changed

xchange-binance/src/main/java/info/bitrich/xchangestream/binance/BinanceStreamingExchange.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import info.bitrich.xchangestream.core.StreamingMarketDataService;
66
import io.reactivex.Completable;
77
import org.knowm.xchange.binance.BinanceExchange;
8+
import org.knowm.xchange.binance.service.BinanceMarketDataService;
89
import org.knowm.xchange.currency.CurrencyPair;
910

1011
import java.util.List;
@@ -38,7 +39,7 @@ public Completable connect(ProductSubscription... args) {
3839

3940
ProductSubscription subscriptions = args[0];
4041
streamingService = createStreamingService(subscriptions);
41-
streamingMarketDataService = new BinanceStreamingMarketDataService(streamingService);
42+
streamingMarketDataService = new BinanceStreamingMarketDataService(streamingService, (BinanceMarketDataService) marketDataService);
4243
return streamingService.connect()
4344
.doOnComplete(() -> streamingMarketDataService.openSubscriptions(subscriptions));
4445
}

xchange-binance/src/main/java/info/bitrich/xchangestream/binance/BinanceStreamingMarketDataService.java

+115-16
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.knowm.xchange.binance.BinanceAdapters;
1717
import org.knowm.xchange.binance.dto.marketdata.BinanceOrderbook;
1818
import org.knowm.xchange.binance.dto.marketdata.BinanceTicker24h;
19+
import org.knowm.xchange.binance.service.BinanceMarketDataService;
1920
import org.knowm.xchange.currency.CurrencyPair;
2021
import org.knowm.xchange.dto.Order.OrderType;
2122
import org.knowm.xchange.dto.marketdata.OrderBook;
@@ -27,10 +28,10 @@
2728
import org.slf4j.LoggerFactory;
2829

2930
import java.io.IOException;
30-
import java.util.ArrayList;
3131
import java.util.Date;
3232
import java.util.HashMap;
3333
import java.util.Map;
34+
import java.util.concurrent.atomic.AtomicLong;
3435

3536
import static info.bitrich.xchangestream.binance.dto.BaseBinanceWebSocketTransaction.BinanceWebSocketTypes.DEPTH_UPDATE;
3637
import static info.bitrich.xchangestream.binance.dto.BaseBinanceWebSocketTransaction.BinanceWebSocketTypes.TICKER_24_HR;
@@ -40,16 +41,17 @@ public class BinanceStreamingMarketDataService implements StreamingMarketDataSer
4041
private static final Logger LOG = LoggerFactory.getLogger(BinanceStreamingMarketDataService.class);
4142

4243
private final BinanceStreamingService service;
43-
private final Map<CurrencyPair, OrderBook> orderbooks = new HashMap<>();
44+
private final Map<CurrencyPair, OrderbookSubscription> orderbooks = new HashMap<>();
4445

4546
private final Map<CurrencyPair, Observable<BinanceTicker24h>> tickerSubscriptions = new HashMap<>();
4647
private final Map<CurrencyPair, Observable<OrderBook>> orderbookSubscriptions = new HashMap<>();
4748
private final Map<CurrencyPair, Observable<BinanceRawTrade>> tradeSubscriptions = new HashMap<>();
4849
private final ObjectMapper mapper = new ObjectMapper();
50+
private final BinanceMarketDataService marketDataService;
4951

50-
51-
public BinanceStreamingMarketDataService(BinanceStreamingService service) {
52+
public BinanceStreamingMarketDataService(BinanceStreamingService service, BinanceMarketDataService marketDataService) {
5253
this.service = service;
54+
this.marketDataService = marketDataService;
5355
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
5456
}
5557

@@ -126,34 +128,131 @@ private Observable<BinanceTicker24h> rawTickerStream(CurrencyPair currencyPair)
126128
.map(transaction -> transaction.getData().getTicker());
127129
}
128130

131+
private final class OrderbookSubscription {
132+
long snapshotlastUpdateId;
133+
AtomicLong lastUpdateId = new AtomicLong(0L);
134+
OrderBook orderBook;
135+
Observable<BinanceWebsocketTransaction<DepthBinanceWebSocketTransaction>> stream;
136+
AtomicLong lastSyncTime = new AtomicLong(0L);
137+
138+
void invalidateSnapshot() {
139+
snapshotlastUpdateId = 0L;
140+
}
141+
142+
void initSnapshotIfInvalid(CurrencyPair currencyPair) {
143+
144+
if (snapshotlastUpdateId != 0L)
145+
return;
146+
147+
// Don't attempt reconnects too often to avoid bans. 3 seconds will do it.
148+
long now = System.currentTimeMillis();
149+
if (now - lastSyncTime.get() < 3000) {
150+
return;
151+
}
152+
153+
try {
154+
LOG.info("Fetching initial orderbook snapshot for {} ", currencyPair);
155+
BinanceOrderbook book = marketDataService.getBinanceOrderbook(currencyPair, 1000);
156+
snapshotlastUpdateId = book.lastUpdateId;
157+
lastUpdateId.set(book.lastUpdateId);
158+
orderBook = BinanceMarketDataService.convertOrderBook(book, currencyPair);
159+
} catch (Throwable e) {
160+
LOG.error("Failed to fetch initial order book for " + currencyPair, e);
161+
snapshotlastUpdateId = 0L;
162+
lastUpdateId.set(0L);
163+
orderBook = null;
164+
}
165+
lastSyncTime.set(now);
166+
}
167+
}
168+
169+
private OrderbookSubscription connectOrderBook(CurrencyPair currencyPair) {
170+
OrderbookSubscription subscription = new OrderbookSubscription();
171+
172+
// 1. Open a stream to wss://stream.binance.com:9443/ws/bnbbtc@depth
173+
// 2. Buffer the events you receive from the stream.
174+
subscription.stream = service.subscribeChannel(channelFromCurrency(currencyPair, "depth"))
175+
.map((JsonNode s) -> depthTransaction(s.toString()))
176+
.filter(transaction ->
177+
transaction.getData().getCurrencyPair().equals(currencyPair) &&
178+
transaction.getData().getEventType() == DEPTH_UPDATE);
179+
180+
181+
return subscription;
182+
}
183+
129184
private Observable<OrderBook> orderBookStream(CurrencyPair currencyPair) {
130-
return service.subscribeChannel(channelFromCurrency(currencyPair, "depth"))
131-
.map((JsonNode s) -> depthTransaction(s.toString()))
132-
.filter(transaction ->
133-
transaction.getData().getCurrencyPair().equals(currencyPair) &&
134-
transaction.getData().getEventType() == DEPTH_UPDATE)
135-
.map(transaction -> {
136-
DepthBinanceWebSocketTransaction depth = transaction.getData();
185+
OrderbookSubscription subscription = orderbooks.computeIfAbsent(currencyPair, pair -> connectOrderBook(pair));
186+
187+
return subscription.stream
188+
189+
// 3. Get a depth snapshot from https://www.binance.com/api/v1/depth?symbol=BNBBTC&limit=1000
190+
// (we do this if we don't already have one or we've invalidated a previous one)
191+
.doOnNext(transaction -> subscription.initSnapshotIfInvalid(currencyPair))
192+
193+
// If we failed, don't return anything. Just keep trying until it works
194+
.filter(transaction -> subscription.snapshotlastUpdateId > 0L)
195+
196+
.map(BinanceWebsocketTransaction::getData)
197+
198+
// 4. Drop any event where u is <= lastUpdateId in the snapshot
199+
.filter(depth -> depth.getLastUpdateId() > subscription.snapshotlastUpdateId)
200+
201+
// 5. The first processed should have U <= lastUpdateId+1 AND u >= lastUpdateId+1
202+
.filter(depth -> {
203+
long lastUpdateId = subscription.lastUpdateId.get();
204+
if (lastUpdateId == 0L) {
205+
return depth.getFirstUpdateId() <= lastUpdateId + 1 &&
206+
depth.getLastUpdateId() >= lastUpdateId + 1;
207+
} else {
208+
return true;
209+
}
210+
})
137211

138-
OrderBook currentOrderBook = orderbooks.computeIfAbsent(currencyPair, orderBook ->
139-
new OrderBook(null, new ArrayList<>(), new ArrayList<>()));
212+
// 6. While listening to the stream, each new event's U should be equal to the previous event's u+1
213+
.filter(depth -> {
214+
long lastUpdateId = subscription.lastUpdateId.get();
215+
boolean result;
216+
if (lastUpdateId == 0L) {
217+
result = true;
218+
} else {
219+
result = depth.getFirstUpdateId() == lastUpdateId + 1;
220+
}
221+
if (result) {
222+
subscription.lastUpdateId.set(depth.getLastUpdateId());
223+
} else {
224+
// If not, we re-sync. This will commonly occur a few times when starting up, since
225+
// given update ids 1,2,3,4,5,6,7,8,9, Binance may sometimes return a snapshot
226+
// as of 5, but update events covering 1-3, 4-6 and 7-9. We can't apply the 4-6
227+
// update event without double-counting 5, and we can't apply the 7-9 update without
228+
// missing 6. The only thing we can do is to keep requesting a fresh snapshot until
229+
// we get to a situation where the snapshot and an update event precisely line up.
230+
LOG.info("Orderbook snapshot for {} out of date (last={}, U={}, u={}). This is normal. Re-syncing.", currencyPair, lastUpdateId, depth.getFirstUpdateId(), depth.getLastUpdateId());
231+
subscription.invalidateSnapshot();
232+
}
233+
return result;
234+
})
140235

236+
// 7. The data in each event is the absolute quantity for a price level
237+
// 8. If the quantity is 0, remove the price level
238+
// 9. Receiving an event that removes a price level that is not in your local order book can happen and is normal.
239+
.map(depth -> {
141240
BinanceOrderbook ob = depth.getOrderBook();
142-
ob.bids.forEach((key, value) -> currentOrderBook.update(new OrderBookUpdate(
241+
ob.bids.forEach((key, value) -> subscription.orderBook.update(new OrderBookUpdate(
143242
OrderType.BID,
144243
null,
145244
currencyPair,
146245
key,
147246
depth.getEventTime(),
148247
value)));
149-
ob.asks.forEach((key, value) -> currentOrderBook.update(new OrderBookUpdate(
248+
ob.asks.forEach((key, value) -> subscription.orderBook.update(new OrderBookUpdate(
150249
OrderType.ASK,
151250
null,
152251
currencyPair,
153252
key,
154253
depth.getEventTime(),
155254
value)));
156-
return currentOrderBook;
255+
return subscription.orderBook;
157256
});
158257
}
159258

xchange-binance/src/main/java/info/bitrich/xchangestream/binance/dto/DepthBinanceWebSocketTransaction.java

+13
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,33 @@
88
public class DepthBinanceWebSocketTransaction extends ProductBinanceWebSocketTransaction {
99

1010
private final BinanceOrderbook orderBook;
11+
private final long lastUpdateId;
12+
private final long firstUpdateId;
1113

1214
public DepthBinanceWebSocketTransaction(
1315
@JsonProperty("e") String eventType,
1416
@JsonProperty("E") String eventTime,
1517
@JsonProperty("s") String symbol,
18+
@JsonProperty("U") long firstUpdateId,
1619
@JsonProperty("u") long lastUpdateId,
1720
@JsonProperty("b") List<Object[]> _bids,
1821
@JsonProperty("a") List<Object[]> _asks
1922
) {
2023
super(eventType, eventTime, symbol);
24+
this.firstUpdateId = firstUpdateId;
25+
this.lastUpdateId = lastUpdateId;
2126
orderBook = new BinanceOrderbook(lastUpdateId, _bids, _asks);
2227
}
2328

2429
public BinanceOrderbook getOrderBook() {
2530
return orderBook;
2631
}
32+
33+
public long getFirstUpdateId() {
34+
return firstUpdateId;
35+
}
36+
37+
public long getLastUpdateId() {
38+
return lastUpdateId;
39+
}
2740
}

xchange-binance/src/test/java/info/bitrich/xchangestream/binance/BinanceManualExample.java

+34-11
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,18 @@
44
import info.bitrich.xchangestream.core.StreamingExchange;
55
import info.bitrich.xchangestream.core.StreamingExchangeFactory;
66
import org.knowm.xchange.currency.CurrencyPair;
7-
import org.knowm.xchange.dto.trade.LimitOrder;
87
import org.slf4j.Logger;
98
import org.slf4j.LoggerFactory;
109

11-
import java.util.List;
10+
import io.reactivex.disposables.Disposable;
1211

1312
/**
1413
* Created by Lukas Zaoralek on 15.11.17.
1514
*/
1615
public class BinanceManualExample {
1716
private static final Logger LOG = LoggerFactory.getLogger(BinanceManualExample.class);
1817

19-
public static void main(String[] args) {
18+
public static void main(String[] args) throws InterruptedException {
2019
StreamingExchange exchange = StreamingExchangeFactory.INSTANCE.createExchange(BinanceStreamingExchange.class.getName());
2120

2221
ProductSubscription subscription = ProductSubscription.create()
@@ -28,22 +27,46 @@ public static void main(String[] args) {
2827

2928
exchange.connect(subscription).blockingAwait();
3029

31-
exchange.getStreamingMarketDataService()
30+
Disposable tickers = exchange.getStreamingMarketDataService()
3231
.getTicker(CurrencyPair.ETH_BTC)
3332
.subscribe(ticker -> {
3433
LOG.info("Ticker: {}", ticker);
3534
}, throwable -> LOG.error("ERROR in getting ticker: ", throwable));
3635

37-
exchange.getStreamingMarketDataService()
38-
.getOrderBook(CurrencyPair.LTC_BTC)
39-
.subscribe(orderBook -> {
40-
LOG.info("Order Book: {}", orderBook);
41-
}, throwable -> LOG.error("ERROR in getting order book: ", throwable));
42-
43-
exchange.getStreamingMarketDataService()
36+
Disposable trades = exchange.getStreamingMarketDataService()
4437
.getTrades(CurrencyPair.BTC_USDT)
4538
.subscribe(trade -> {
4639
LOG.info("Trade: {}", trade);
4740
});
41+
42+
Disposable orderbooks = orderbooks(exchange, "one");
43+
Thread.sleep(5000);
44+
45+
Disposable orderbooks2 = orderbooks(exchange, "two");
46+
Thread.sleep(10000);
47+
48+
tickers.dispose();
49+
trades.dispose();
50+
orderbooks.dispose();
51+
orderbooks2.dispose();
52+
exchange.disconnect().blockingAwait();
53+
54+
}
55+
56+
private static Disposable orderbooks(StreamingExchange exchange, String identifier) {
57+
return exchange.getStreamingMarketDataService()
58+
.getOrderBook(CurrencyPair.LTC_BTC)
59+
.subscribe(orderBook -> {
60+
LOG.info(
61+
"Order Book ({}): askDepth={} ask={} askSize={} bidDepth={}. bid={}, bidSize={}",
62+
identifier,
63+
orderBook.getAsks().size(),
64+
orderBook.getAsks().get(0).getLimitPrice(),
65+
orderBook.getAsks().get(0).getRemainingAmount(),
66+
orderBook.getBids().size(),
67+
orderBook.getBids().get(0).getLimitPrice(),
68+
orderBook.getBids().get(0).getRemainingAmount()
69+
);
70+
}, throwable -> LOG.error("ERROR in getting order book: ", throwable));
4871
}
4972
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package info.bitrich.xchangestream.binance;
2+
3+
import org.knowm.xchange.ExchangeSpecification;
4+
import info.bitrich.xchangestream.binance.BinanceStreamingExchange;
5+
import info.bitrich.xchangestream.core.ProductSubscription;
6+
import info.bitrich.xchangestream.core.StreamingExchange;
7+
import info.bitrich.xchangestream.core.StreamingExchangeFactory;
8+
9+
/**
10+
* This is a useful test for profiling behaviour of the orderbook stream under load.
11+
* Run this with a profiler to ensure that processing is efficient and free of memory leaks
12+
*/
13+
public class BinanceOrderbookHighVolumeExample {
14+
15+
public static void main(String[] args) throws InterruptedException {
16+
final ExchangeSpecification exchangeSpecification = new ExchangeSpecification(BinanceStreamingExchange.class);
17+
exchangeSpecification.setShouldLoadRemoteMetaData(true);
18+
StreamingExchange exchange = StreamingExchangeFactory.INSTANCE.createExchange(exchangeSpecification);
19+
ProductSubscription subscription = exchange.getExchangeSymbols().stream().limit(50)
20+
.reduce(ProductSubscription.create(), ProductSubscription.ProductSubscriptionBuilder::addOrderbook,
21+
(productSubscriptionBuilder, productSubscriptionBuilder2) -> {
22+
throw new UnsupportedOperationException();
23+
})
24+
.build();
25+
exchange.connect(subscription).blockingAwait();
26+
Thread.sleep(Long.MAX_VALUE);
27+
}
28+
29+
}

0 commit comments

Comments
 (0)