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

Commit ed4f8ae

Browse files
2 parents f8c0ac0 + 890eba9 commit ed4f8ae

File tree

4 files changed

+165
-24
lines changed

4 files changed

+165
-24
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.reactivex.Completable;
77
import io.reactivex.Observable;
88
import org.knowm.xchange.binance.BinanceExchange;
9+
import org.knowm.xchange.binance.service.BinanceMarketDataService;
910
import org.knowm.xchange.currency.CurrencyPair;
1011

1112
import java.util.List;
@@ -39,7 +40,7 @@ public Completable connect(ProductSubscription... args) {
3940

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

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

+116-12
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414
import info.bitrich.xchangestream.service.netty.StreamingObjectMapperHelper;
1515
import io.reactivex.Observable;
1616
import io.reactivex.functions.Consumer;
17+
import io.reactivex.observables.ConnectableObservable;
18+
1719
import org.knowm.xchange.binance.BinanceAdapters;
1820
import org.knowm.xchange.binance.dto.marketdata.BinanceOrderbook;
1921
import org.knowm.xchange.binance.dto.marketdata.BinanceTicker24h;
22+
import org.knowm.xchange.binance.service.BinanceMarketDataService;
2023
import org.knowm.xchange.currency.CurrencyPair;
2124
import org.knowm.xchange.dto.Order.OrderType;
2225
import org.knowm.xchange.dto.marketdata.OrderBook;
@@ -32,6 +35,7 @@
3235
import java.util.Date;
3336
import java.util.HashMap;
3437
import java.util.Map;
38+
import java.util.concurrent.atomic.AtomicLong;
3539

3640
import static info.bitrich.xchangestream.binance.dto.BaseBinanceWebSocketTransaction.BinanceWebSocketTypes.DEPTH_UPDATE;
3741
import static info.bitrich.xchangestream.binance.dto.BaseBinanceWebSocketTransaction.BinanceWebSocketTypes.TICKER_24_HR;
@@ -41,16 +45,18 @@ public class BinanceStreamingMarketDataService implements StreamingMarketDataSer
4145
private static final Logger LOG = LoggerFactory.getLogger(BinanceStreamingMarketDataService.class);
4246

4347
private final BinanceStreamingService service;
44-
private final Map<CurrencyPair, OrderBook> orderbooks = new HashMap<>();
48+
private final Map<CurrencyPair, OrderbookSubscription> orderbooks = new HashMap<>();
4549

4650
private final Map<CurrencyPair, Observable<BinanceTicker24h>> tickerSubscriptions = new HashMap<>();
4751
private final Map<CurrencyPair, Observable<OrderBook>> orderbookSubscriptions = new HashMap<>();
4852
private final Map<CurrencyPair, Observable<BinanceRawTrade>> tradeSubscriptions = new HashMap<>();
4953
private final ObjectMapper mapper = StreamingObjectMapperHelper.getObjectMapper();
54+
private final BinanceMarketDataService marketDataService;
5055

51-
52-
public BinanceStreamingMarketDataService(BinanceStreamingService service) {
56+
public BinanceStreamingMarketDataService(BinanceStreamingService service, BinanceMarketDataService marketDataService) {
5357
this.service = service;
58+
this.marketDataService = marketDataService;
59+
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
5460
}
5561

5662
@Override
@@ -126,34 +132,132 @@ private Observable<BinanceTicker24h> rawTickerStream(CurrencyPair currencyPair)
126132
.map(transaction -> transaction.getData().getTicker());
127133
}
128134

129-
private Observable<OrderBook> orderBookStream(CurrencyPair currencyPair) {
130-
return service.subscribeChannel(channelFromCurrency(currencyPair, "depth"))
135+
private final class OrderbookSubscription {
136+
long snapshotlastUpdateId;
137+
AtomicLong lastUpdateId = new AtomicLong(0L);
138+
OrderBook orderBook;
139+
ConnectableObservable<BinanceWebsocketTransaction<DepthBinanceWebSocketTransaction>> stream;
140+
AtomicLong lastSyncTime = new AtomicLong(0L);
141+
142+
void invalidateSnapshot() {
143+
snapshotlastUpdateId = 0L;
144+
}
145+
146+
void initSnapshotIfInvalid(CurrencyPair currencyPair) {
147+
148+
if (snapshotlastUpdateId != 0L)
149+
return;
150+
151+
// Don't attempt reconnects too often to avoid bans. 3 seconds will do it.
152+
long now = System.currentTimeMillis();
153+
if (now - lastSyncTime.get() < 3000) {
154+
return;
155+
}
156+
157+
try {
158+
LOG.info("Fetching initial orderbook snapshot for {} ", currencyPair);
159+
BinanceOrderbook book = marketDataService.getBinanceOrderbook(currencyPair, 1000);
160+
snapshotlastUpdateId = book.lastUpdateId;
161+
lastUpdateId.set(book.lastUpdateId);
162+
orderBook = BinanceMarketDataService.convertOrderBook(book, currencyPair);
163+
} catch (Throwable e) {
164+
LOG.error("Failed to fetch initial order book for " + currencyPair, e);
165+
snapshotlastUpdateId = 0L;
166+
lastUpdateId.set(0L);
167+
orderBook = new OrderBook(null, new ArrayList<>(), new ArrayList<>());
168+
}
169+
lastSyncTime.set(now);
170+
}
171+
}
172+
173+
private OrderbookSubscription connectOrderBook(CurrencyPair currencyPair) {
174+
OrderbookSubscription subscription = new OrderbookSubscription();
175+
176+
// 1. Open a stream to wss://stream.binance.com:9443/ws/bnbbtc@depth
177+
subscription.stream = service.subscribeChannel(channelFromCurrency(currencyPair, "depth"))
131178
.map((JsonNode s) -> depthTransaction(s.toString()))
132179
.filter(transaction ->
133180
transaction.getData().getCurrencyPair().equals(currencyPair) &&
134181
transaction.getData().getEventType() == DEPTH_UPDATE)
135-
.map(transaction -> {
136-
DepthBinanceWebSocketTransaction depth = transaction.getData();
137182

138-
OrderBook currentOrderBook = orderbooks.computeIfAbsent(currencyPair, orderBook ->
139-
new OrderBook(null, new ArrayList<>(), new ArrayList<>()));
183+
// 2.Buffer the events you receive from the stream.
184+
// This is solely to allow room for us to periodically fetch a fresh snapshot
185+
// in the event that binance sends events out of sequence or skips events.
186+
.replay();
187+
subscription.stream.connect();
188+
189+
return subscription;
190+
}
191+
192+
private Observable<OrderBook> orderBookStream(CurrencyPair currencyPair) {
193+
OrderbookSubscription subscription = orderbooks.computeIfAbsent(currencyPair, pair -> connectOrderBook(pair));
194+
195+
return subscription.stream
196+
197+
// 3. Get a depth snapshot from https://www.binance.com/api/v1/depth?symbol=BNBBTC&limit=1000
198+
// (we do this if we don't already have one or we've invalidated a previous one)
199+
.doOnNext(transaction -> subscription.initSnapshotIfInvalid(currencyPair))
200+
201+
.map(BinanceWebsocketTransaction::getData)
202+
203+
// 4. Drop any event where u is <= lastUpdateId in the snapshot
204+
.filter(depth -> depth.getLastUpdateId() > subscription.snapshotlastUpdateId)
205+
206+
// 5. The first processed should have U <= lastUpdateId+1 AND u >= lastUpdateId+1
207+
.filter(depth -> {
208+
long lastUpdateId = subscription.lastUpdateId.get();
209+
if (lastUpdateId == 0L) {
210+
return depth.getFirstUpdateId() <= lastUpdateId + 1 &&
211+
depth.getLastUpdateId() >= lastUpdateId + 1;
212+
} else {
213+
return true;
214+
}
215+
})
216+
217+
// 6. While listening to the stream, each new event's U should be equal to the previous event's u+1
218+
.filter(depth -> {
219+
long lastUpdateId = subscription.lastUpdateId.get();
220+
boolean result;
221+
if (lastUpdateId == 0L) {
222+
result = true;
223+
} else {
224+
result = depth.getFirstUpdateId() == lastUpdateId + 1;
225+
}
226+
if (result) {
227+
subscription.lastUpdateId.set(depth.getLastUpdateId());
228+
} else {
229+
// If not, we re-sync. This will commonly occur a few times when starting up, since
230+
// given update ids 1,2,3,4,5,6,7,8,9, Binance may sometimes return a snapshot
231+
// as of 5, but update events covering 1-3, 4-6 and 7-9. We can't apply the 4-6
232+
// update event without double-counting 5, and we can't apply the 7-9 update without
233+
// missing 6. The only thing we can do is to keep requesting a fresh snapshot until
234+
// we get to a situation where the snapshot and an update event precisely line up.
235+
LOG.info("Orderbook snapshot for {} out of date (last={}, U={}, u={}). This is normal. Re-syncing.", currencyPair, lastUpdateId, depth.getFirstUpdateId(), depth.getLastUpdateId());
236+
subscription.invalidateSnapshot();
237+
}
238+
return result;
239+
})
140240

241+
// 7. The data in each event is the absolute quantity for a price level
242+
// 8. If the quantity is 0, remove the price level
243+
// 9. Receiving an event that removes a price level that is not in your local order book can happen and is normal.
244+
.map(depth -> {
141245
BinanceOrderbook ob = depth.getOrderBook();
142-
ob.bids.forEach((key, value) -> currentOrderBook.update(new OrderBookUpdate(
246+
ob.bids.forEach((key, value) -> subscription.orderBook.update(new OrderBookUpdate(
143247
OrderType.BID,
144248
null,
145249
currencyPair,
146250
key,
147251
depth.getEventTime(),
148252
value)));
149-
ob.asks.forEach((key, value) -> currentOrderBook.update(new OrderBookUpdate(
253+
ob.asks.forEach((key, value) -> subscription.orderBook.update(new OrderBookUpdate(
150254
OrderType.ASK,
151255
null,
152256
currencyPair,
153257
key,
154258
depth.getEventTime(),
155259
value)));
156-
return currentOrderBook;
260+
return subscription.orderBook;
157261
});
158262
}
159263

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
}

0 commit comments

Comments
 (0)