Skip to content

Commit 94ad7ef

Browse files
Uncross OrderBook (#6)
* Handle crossed order book in `dYdXBrokerage`: add logic to uncross order book, validate bid-ask prices before emitting quote ticks, and improve illiquid asset handling. * Refactor `UncrossOrderBook`: simplify bid-ask comparison logic and improve order book state updates within processing loop. * Refactor `dYdXBrokerage`: remove `UncrossOrderBook` in favor of a new extension method, clean up bid-ask price logic, and update namespace resolution for consistency. * Add `UncrossOrderBook` extension method and unit tests to handle crossed order books in `dYdXBrokerage`. * Add license --------- Co-authored-by: Martin Molinero <[email protected]>
1 parent ae2bb2e commit 94ad7ef

File tree

4 files changed

+283
-2
lines changed

4 files changed

+283
-2
lines changed
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using NUnit.Framework;
17+
using QuantConnect.Brokerages.dYdX.Extensions;
18+
19+
namespace QuantConnect.Brokerages.dYdX.Tests.Extensions;
20+
21+
[TestFixture]
22+
public class OrderBookExtensionsTests
23+
{
24+
[Test]
25+
public void UncrossOrderBook_WhenBidGreaterThanAsk_RemovesBothWhenSizesEqual()
26+
{
27+
// Arrange
28+
var symbol = Symbol.Create("BTCUSD", SecurityType.Crypto, Market.dYdX);
29+
var orderBook = new DefaultOrderBook(symbol);
30+
31+
orderBook.UpdateBidRow(101m, 10m);
32+
orderBook.UpdateAskRow(100m, 10m);
33+
34+
// Act
35+
orderBook.UncrossOrderBook();
36+
37+
// Assert
38+
Assert.AreEqual(0, orderBook.BestBidPrice);
39+
Assert.AreEqual(0, orderBook.BestAskPrice);
40+
}
41+
42+
[Test]
43+
public void UncrossOrderBook_WhenBidGreaterThanAsk_ReducesBidWhenBidSizeLarger()
44+
{
45+
// Arrange
46+
var symbol = Symbol.Create("BTCUSD", SecurityType.Crypto, Market.dYdX);
47+
var orderBook = new DefaultOrderBook(symbol);
48+
49+
orderBook.UpdateBidRow(101m, 15m);
50+
orderBook.UpdateAskRow(100m, 10m);
51+
52+
// Act
53+
orderBook.UncrossOrderBook();
54+
55+
// Assert
56+
Assert.AreEqual(101m, orderBook.BestBidPrice);
57+
Assert.AreEqual(5m, orderBook.BestBidSize);
58+
Assert.AreEqual(0, orderBook.BestAskPrice);
59+
}
60+
61+
[Test]
62+
public void UncrossOrderBook_WhenBidGreaterThanAsk_ReducesAskWhenAskSizeLarger()
63+
{
64+
// Arrange
65+
var symbol = Symbol.Create("BTCUSD", SecurityType.Crypto, Market.dYdX);
66+
var orderBook = new DefaultOrderBook(symbol);
67+
68+
orderBook.UpdateBidRow(101m, 10m);
69+
orderBook.UpdateAskRow(100m, 15m);
70+
71+
// Act
72+
orderBook.UncrossOrderBook();
73+
74+
// Assert
75+
Assert.AreEqual(0, orderBook.BestBidPrice);
76+
Assert.AreEqual(100m, orderBook.BestAskPrice);
77+
Assert.AreEqual(5m, orderBook.BestAskSize);
78+
}
79+
80+
[Test]
81+
public void UncrossOrderBook_WhenBidLessThanAsk_DoesNothing()
82+
{
83+
// Arrange
84+
var symbol = Symbol.Create("BTCUSD", SecurityType.Crypto, Market.dYdX);
85+
var orderBook = new DefaultOrderBook(symbol);
86+
87+
orderBook.UpdateBidRow(99m, 10m);
88+
orderBook.UpdateAskRow(100m, 10m);
89+
90+
// Act
91+
orderBook.UncrossOrderBook();
92+
93+
// Assert
94+
Assert.AreEqual(99m, orderBook.BestBidPrice);
95+
Assert.AreEqual(10m, orderBook.BestBidSize);
96+
Assert.AreEqual(100m, orderBook.BestAskPrice);
97+
Assert.AreEqual(10m, orderBook.BestAskSize);
98+
}
99+
100+
[Test]
101+
public void UncrossOrderBook_WhenMultipleCrossedLevels_UncrossesAll()
102+
{
103+
// Arrange
104+
var symbol = Symbol.Create("BTCUSD", SecurityType.Crypto, Market.dYdX);
105+
var orderBook = new DefaultOrderBook(symbol);
106+
107+
// Add multiple crossed levels
108+
orderBook.UpdateBidRow(94m, 5m);
109+
orderBook.UpdateBidRow(102m, 10m);
110+
orderBook.UpdateBidRow(101m, 15m);
111+
orderBook.UpdateAskRow(100m, 8m);
112+
orderBook.UpdateAskRow(99m, 12m);
113+
orderBook.UpdateAskRow(98m, 20m);
114+
115+
// Act
116+
orderBook.UncrossOrderBook();
117+
118+
// Assert
119+
Assert.AreEqual(94m, orderBook.BestBidPrice);
120+
Assert.AreEqual(5m, orderBook.BestBidSize);
121+
Assert.AreEqual(99m, orderBook.BestAskPrice);
122+
Assert.AreEqual(7m, orderBook.BestAskSize);
123+
}
124+
125+
[Test]
126+
public void UncrossOrderBook_WhenSingleCrossedLevels_UncrossesAll()
127+
{
128+
// Arrange
129+
var symbol = Symbol.Create("BTCUSD", SecurityType.Crypto, Market.dYdX);
130+
var orderBook = new DefaultOrderBook(symbol);
131+
132+
// Add multiple crossed levels
133+
orderBook.UpdateBidRow(94m, 5m);
134+
orderBook.UpdateBidRow(95m, 10m);
135+
orderBook.UpdateBidRow(101m, 15m);
136+
orderBook.UpdateAskRow(100m, 8m);
137+
orderBook.UpdateAskRow(99m, 12m);
138+
orderBook.UpdateAskRow(98m, 20m);
139+
140+
// Act
141+
orderBook.UncrossOrderBook();
142+
143+
// Assert
144+
Assert.AreEqual(95m, orderBook.BestBidPrice);
145+
Assert.AreEqual(10m, orderBook.BestBidSize);
146+
Assert.AreEqual(98, orderBook.BestAskPrice);
147+
Assert.AreEqual(5, orderBook.BestAskSize);
148+
}
149+
150+
[Test]
151+
public void UncrossOrderBook_WhenOrderBookEmpty_DoesNothing()
152+
{
153+
// Arrange
154+
var symbol = Symbol.Create("BTCUSD", SecurityType.Crypto, Market.dYdX);
155+
var orderBook = new DefaultOrderBook(symbol);
156+
157+
// Act
158+
orderBook.UncrossOrderBook();
159+
160+
// Assert
161+
Assert.AreEqual(0, orderBook.BestBidPrice);
162+
Assert.AreEqual(0, orderBook.BestAskPrice);
163+
}
164+
165+
[Test]
166+
public void UncrossOrderBook_WhenOnlyBidsExist_DoesNothing()
167+
{
168+
// Arrange
169+
var symbol = Symbol.Create("BTCUSD", SecurityType.Crypto, Market.dYdX);
170+
var orderBook = new DefaultOrderBook(symbol);
171+
172+
orderBook.UpdateBidRow(100m, 10m);
173+
174+
// Act
175+
orderBook.UncrossOrderBook();
176+
177+
// Assert
178+
Assert.AreEqual(100m, orderBook.BestBidPrice);
179+
Assert.AreEqual(10m, orderBook.BestBidSize);
180+
Assert.AreEqual(0, orderBook.BestAskPrice);
181+
}
182+
183+
[Test]
184+
public void UncrossOrderBook_WhenOnlyAsksExist_DoesNothing()
185+
{
186+
// Arrange
187+
var symbol = Symbol.Create("BTCUSD", SecurityType.Crypto, Market.dYdX);
188+
var orderBook = new DefaultOrderBook(symbol);
189+
190+
orderBook.UpdateAskRow(100m, 10m);
191+
192+
// Act
193+
orderBook.UncrossOrderBook();
194+
195+
// Assert
196+
Assert.AreEqual(0, orderBook.BestBidPrice);
197+
Assert.AreEqual(100m, orderBook.BestAskPrice);
198+
Assert.AreEqual(10m, orderBook.BestAskSize);
199+
}
200+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
namespace QuantConnect.Brokerages.dYdX.Extensions;
17+
public static class OrderBookExtensions
18+
{
19+
/// <summary>
20+
/// Crossed prices where best bid > best ask may happen.
21+
/// This happens because the dydx network is decentralized, operated by 42 validators where the order book updates can be sent by any of the validators and therefore may arrive out of sequence to the full node/indexer
22+
/// see ref https://docs.dydx.xyz/interaction/data/watch-orderbook#uncrossing-the-orderbook
23+
/// </summary>
24+
/// <param name="orderBook">Order book to uncross</param>
25+
public static void UncrossOrderBook(this DefaultOrderBook orderBook)
26+
{
27+
// Get sorted lists: bids descending (highest first), asks ascending (lowest first)
28+
var bidPrice = orderBook.BestBidPrice;
29+
var askPrice = orderBook.BestAskPrice;
30+
31+
while (bidPrice != 0 && askPrice != 0 && bidPrice > askPrice)
32+
{
33+
var bidSize = orderBook.BestBidSize;
34+
var askSize = orderBook.BestAskSize;
35+
36+
if (bidSize > askSize)
37+
{
38+
orderBook.UpdateBidRow(bidPrice, bidSize - askSize);
39+
orderBook.RemoveAskRow(askPrice);
40+
}
41+
else if (bidSize < askSize)
42+
{
43+
orderBook.UpdateAskRow(askPrice, askSize - bidSize);
44+
orderBook.RemoveBidRow(bidPrice);
45+
}
46+
else
47+
{
48+
orderBook.RemoveAskRow(askPrice);
49+
orderBook.RemoveBidRow(bidPrice);
50+
}
51+
52+
bidPrice = orderBook.BestBidPrice;
53+
askPrice = orderBook.BestAskPrice;
54+
}
55+
}
56+
}

QuantConnect.dYdXBrokerage/dYdXBrokerage.Messaging.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using System.Collections.Generic;
1818
using System.Linq;
1919
using Newtonsoft.Json.Linq;
20+
using QuantConnect.Brokerages.dYdX.Extensions;
2021
using QuantConnect.Brokerages.dYdX.Models;
2122
using QuantConnect.Brokerages.dYdX.Models.WebSockets;
2223
using QuantConnect.Data.Market;
@@ -489,7 +490,31 @@ private void UpdateOrderBook(DefaultOrderBook orderBook, Orderbook orderbook)
489490

490491
private void OnBestBidAskUpdated(object sender, BestBidAskUpdatedEventArgs e)
491492
{
492-
EmitQuoteTick(e.Symbol, e.BestBidPrice, e.BestBidSize, e.BestAskPrice, e.BestAskSize);
493+
if (e.BestBidPrice < e.BestAskPrice)
494+
{
495+
EmitQuoteTick(e.Symbol, e.BestBidPrice, e.BestBidSize, e.BestAskPrice, e.BestAskSize);
496+
}
497+
498+
// Orderbook got crossed, uncross it and then emit quote tick
499+
if (sender is DefaultOrderBook orderBook)
500+
{
501+
orderBook.BestBidAskUpdated -= OnBestBidAskUpdated;
502+
503+
orderBook.UncrossOrderBook();
504+
505+
orderBook.BestBidAskUpdated += OnBestBidAskUpdated;
506+
if (orderBook.BestBidPrice == 0 && orderBook.BestAskPrice == 0)
507+
{
508+
// nothing to emit, can happen with illiquid assets
509+
return;
510+
}
511+
512+
EmitQuoteTick(e.Symbol,
513+
orderBook.BestBidPrice,
514+
orderBook.BestBidSize,
515+
orderBook.BestAskPrice,
516+
orderBook.BestAskSize);
517+
}
493518
}
494519

495520
private void EmitQuoteTick(Symbol symbol, decimal bidPrice, decimal bidSize, decimal askPrice, decimal askSize)

QuantConnect.dYdXBrokerage/dYdXBrokerage.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ private static Dictionary<Symbol, int> FetchSymbolWeights(
649649
string indexerRestUrl)
650650
{
651651
var weights = new Dictionary<Symbol, int>();
652-
var data = Extensions.DownloadData($"{indexerRestUrl}/perpetualMarkets");
652+
var data = QuantConnect.Extensions.DownloadData($"{indexerRestUrl}/perpetualMarkets");
653653
var markets = JsonConvert.DeserializeObject<ExchangeInfo>(data);
654654
var totalMarketVolume24H = markets.Symbols.Values
655655
.Select(x => x.Volume24H)

0 commit comments

Comments
 (0)