Skip to content

Commit de70df7

Browse files
committed
Add own bid and ask quantity methods for OwnOrderBook
1 parent 1fa4f08 commit de70df7

File tree

4 files changed

+302
-0
lines changed

4 files changed

+302
-0
lines changed

crates/model/src/orderbook/own.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,34 @@ impl OwnOrderBook {
302302
.collect()
303303
}
304304

305+
/// Returns the aggregated own bid quantity at each price level.
306+
pub fn bid_quantity(&self) -> IndexMap<Decimal, Decimal> {
307+
self.bids()
308+
.map(|level| {
309+
(
310+
level.price.value.as_decimal(),
311+
level.orders.values().fold(Decimal::ZERO, |total, order| {
312+
total + order.size.as_decimal()
313+
}),
314+
)
315+
})
316+
.collect()
317+
}
318+
319+
/// Returns the aggregated own ask quantity at each price level.
320+
pub fn ask_quantity(&self) -> IndexMap<Decimal, Decimal> {
321+
self.asks()
322+
.map(|level| {
323+
(
324+
level.price.value.as_decimal(),
325+
level.orders.values().fold(Decimal::ZERO, |total, order| {
326+
total + order.size.as_decimal()
327+
}),
328+
)
329+
})
330+
.collect()
331+
}
332+
305333
/// Return a formatted string representation of the order book.
306334
#[must_use]
307335
pub fn pprint(&self, num_levels: usize) -> String {
@@ -587,6 +615,7 @@ impl Ord for OwnBookLevel {
587615
mod tests {
588616
use nautilus_core::UnixNanos;
589617
use rstest::{fixture, rstest};
618+
use rust_decimal_macros::dec;
590619

591620
use super::*;
592621

@@ -890,4 +919,97 @@ mod tests {
890919
assert_eq!(ask_orders.len(), 1);
891920
assert_eq!(ask_orders[0], order2);
892921
}
922+
923+
#[rstest]
924+
fn test_own_order_book_quantity_empty_levels() {
925+
let instrument_id = InstrumentId::from("AAPL.XNAS");
926+
let book = OwnOrderBook::new(instrument_id);
927+
928+
let bid_quantities = book.bid_quantity();
929+
let ask_quantities = book.ask_quantity();
930+
931+
assert!(bid_quantities.is_empty());
932+
assert!(ask_quantities.is_empty());
933+
}
934+
935+
#[rstest]
936+
fn test_own_order_book_bid_ask_quantity() {
937+
let instrument_id = InstrumentId::from("AAPL.XNAS");
938+
let mut book = OwnOrderBook::new(instrument_id);
939+
940+
// Add multiple orders at the same price level (bids)
941+
let bid_order1 = OwnBookOrder::new(
942+
ClientOrderId::from("O-1"),
943+
OrderSideSpecified::Buy,
944+
Price::from("100.00"),
945+
Quantity::from("10"),
946+
OrderType::Limit,
947+
TimeInForce::Gtc,
948+
OrderStatus::Accepted,
949+
UnixNanos::default(),
950+
UnixNanos::default(),
951+
);
952+
let bid_order2 = OwnBookOrder::new(
953+
ClientOrderId::from("O-2"),
954+
OrderSideSpecified::Buy,
955+
Price::from("100.00"),
956+
Quantity::from("15"),
957+
OrderType::Limit,
958+
TimeInForce::Gtc,
959+
OrderStatus::Accepted,
960+
UnixNanos::default(),
961+
UnixNanos::default(),
962+
);
963+
// Add an order at a different price level (bids)
964+
let bid_order3 = OwnBookOrder::new(
965+
ClientOrderId::from("O-3"),
966+
OrderSideSpecified::Buy,
967+
Price::from("99.50"),
968+
Quantity::from("20"),
969+
OrderType::Limit,
970+
TimeInForce::Gtc,
971+
OrderStatus::Accepted,
972+
UnixNanos::default(),
973+
UnixNanos::default(),
974+
);
975+
976+
// Add orders at different price levels (asks)
977+
let ask_order1 = OwnBookOrder::new(
978+
ClientOrderId::from("O-4"),
979+
OrderSideSpecified::Sell,
980+
Price::from("101.00"),
981+
Quantity::from("12"),
982+
OrderType::Limit,
983+
TimeInForce::Gtc,
984+
OrderStatus::Accepted,
985+
UnixNanos::default(),
986+
UnixNanos::default(),
987+
);
988+
let ask_order2 = OwnBookOrder::new(
989+
ClientOrderId::from("O-5"),
990+
OrderSideSpecified::Sell,
991+
Price::from("101.00"),
992+
Quantity::from("8"),
993+
OrderType::Limit,
994+
TimeInForce::Gtc,
995+
OrderStatus::Accepted,
996+
UnixNanos::default(),
997+
UnixNanos::default(),
998+
);
999+
1000+
book.add(bid_order1);
1001+
book.add(bid_order2);
1002+
book.add(bid_order3);
1003+
book.add(ask_order1);
1004+
book.add(ask_order2);
1005+
1006+
let bid_quantities = book.bid_quantity();
1007+
assert_eq!(bid_quantities.len(), 2);
1008+
assert_eq!(bid_quantities.get(&dec!(100.00)), Some(&dec!(25)));
1009+
assert_eq!(bid_quantities.get(&dec!(99.50)), Some(&dec!(20)));
1010+
1011+
let ask_quantities = book.ask_quantity();
1012+
assert_eq!(ask_quantities.len(), 1);
1013+
assert_eq!(ask_quantities.get(&dec!(101.00)), Some(&dec!(20)));
1014+
}
8931015
}

crates/model/src/python/orderbook/own.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,16 @@ impl OwnOrderBook {
213213
self.asks_as_map()
214214
}
215215

216+
#[pyo3(name = "bid_quantity")]
217+
fn py_bid_quantity(&self) -> IndexMap<Decimal, Decimal> {
218+
self.bid_quantity()
219+
}
220+
221+
#[pyo3(name = "ask_quantity")]
222+
fn py_ask_quantity(&self) -> IndexMap<Decimal, Decimal> {
223+
self.ask_quantity()
224+
}
225+
216226
#[pyo3(signature = (num_levels=3))]
217227
#[pyo3(name = "pprint")]
218228
fn py_pprint(&self, num_levels: usize) -> String {

nautilus_trader/core/nautilus_pyo3.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3212,6 +3212,8 @@ class OwnOrderBook:
32123212
def clear(self) -> None: ...
32133213
def bids_to_dict(self) -> dict[Decimal, list[OwnBookOrder]]: ...
32143214
def asks_to_dict(self) -> dict[Decimal, list[OwnBookOrder]]: ...
3215+
def bid_quantity(self) -> dict[Decimal, Decimal]: ...
3216+
def ask_quantity(self) -> dict[Decimal, Decimal]: ...
32153217
def pprint(self, num_levels: int = 3) -> str: ...
32163218

32173219
###################################################################################################

tests/unit_tests/model/test_own_order_book.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
# limitations under the License.
1414
# -------------------------------------------------------------------------------------------------
1515

16+
from decimal import Decimal
17+
1618
import pytest
1719

1820
from nautilus_trader.core.nautilus_pyo3 import ClientOrderId
@@ -361,3 +363,169 @@ def test_own_order_book_price_change():
361363
assert len(new_orders) == 1
362364
assert new_orders[0] == updated
363365
assert book.ts_last == 11
366+
367+
368+
def test_own_order_book_bid_ask_quantity():
369+
instrument_id = InstrumentId.from_str("AAPL.XNAS")
370+
book = OwnOrderBook(instrument_id)
371+
372+
# Add multiple orders at the same price level (bids)
373+
bid_order1 = OwnBookOrder(
374+
client_order_id=ClientOrderId("O-1"),
375+
side=OrderSide.BUY,
376+
price=Price(100.0, 2),
377+
size=Quantity(10.0, 0),
378+
order_type=OrderType.LIMIT,
379+
time_in_force=TimeInForce.GTC,
380+
status=OrderStatus.ACCEPTED,
381+
ts_last=0,
382+
ts_init=0,
383+
)
384+
bid_order2 = OwnBookOrder(
385+
client_order_id=ClientOrderId("O-2"),
386+
side=OrderSide.BUY,
387+
price=Price(100.0, 2),
388+
size=Quantity(15.0, 0),
389+
order_type=OrderType.LIMIT,
390+
time_in_force=TimeInForce.GTC,
391+
status=OrderStatus.ACCEPTED,
392+
ts_last=0,
393+
ts_init=0,
394+
)
395+
# Add an order at a different price level (bids)
396+
bid_order3 = OwnBookOrder(
397+
client_order_id=ClientOrderId("O-3"),
398+
side=OrderSide.BUY,
399+
price=Price(99.5, 2),
400+
size=Quantity(20.0, 0),
401+
order_type=OrderType.LIMIT,
402+
time_in_force=TimeInForce.GTC,
403+
status=OrderStatus.ACCEPTED,
404+
ts_last=0,
405+
ts_init=0,
406+
)
407+
408+
# Add orders at different price levels (asks)
409+
ask_order1 = OwnBookOrder(
410+
client_order_id=ClientOrderId("O-4"),
411+
side=OrderSide.SELL,
412+
price=Price(101.0, 2),
413+
size=Quantity(12.0, 0),
414+
order_type=OrderType.LIMIT,
415+
time_in_force=TimeInForce.GTC,
416+
status=OrderStatus.ACCEPTED,
417+
ts_last=0,
418+
ts_init=0,
419+
)
420+
ask_order2 = OwnBookOrder(
421+
client_order_id=ClientOrderId("O-5"),
422+
side=OrderSide.SELL,
423+
price=Price(101.0, 2),
424+
size=Quantity(8.0, 0),
425+
order_type=OrderType.LIMIT,
426+
time_in_force=TimeInForce.GTC,
427+
status=OrderStatus.ACCEPTED,
428+
ts_last=0,
429+
ts_init=0,
430+
)
431+
432+
book.add(bid_order1)
433+
book.add(bid_order2)
434+
book.add(bid_order3)
435+
book.add(ask_order1)
436+
book.add(ask_order2)
437+
438+
bid_quantities = book.bid_quantity()
439+
assert len(bid_quantities) == 2
440+
assert bid_quantities[Price(100.0, 2).as_decimal()] == Decimal("25")
441+
assert bid_quantities[Price(99.5, 2).as_decimal()] == Decimal("20")
442+
443+
ask_quantities = book.ask_quantity()
444+
assert len(ask_quantities) == 1
445+
assert ask_quantities[Price(101.0, 2).as_decimal()] == Decimal("20")
446+
447+
448+
def test_own_order_book_quantity_empty_levels():
449+
instrument_id = InstrumentId.from_str("AAPL.XNAS")
450+
book = OwnOrderBook(instrument_id)
451+
452+
# Test on empty book
453+
bid_quantities = book.bid_quantity()
454+
ask_quantities = book.ask_quantity()
455+
456+
assert len(bid_quantities) == 0
457+
assert len(ask_quantities) == 0
458+
459+
460+
@pytest.mark.parametrize(
461+
"orders,expected_bid_quantities,expected_ask_quantities",
462+
[
463+
# Test case 1: Multiple orders at same price level
464+
(
465+
[
466+
(OrderSide.BUY, 100.0, 10.0),
467+
(OrderSide.BUY, 100.0, 15.0),
468+
(OrderSide.SELL, 101.0, 20.0),
469+
],
470+
{Decimal("100.00"): Decimal("25")},
471+
{Decimal("101.00"): Decimal("20")},
472+
),
473+
# Test case 2: Multiple price levels
474+
(
475+
[
476+
(OrderSide.BUY, 100.0, 10.0),
477+
(OrderSide.BUY, 99.0, 5.0),
478+
(OrderSide.SELL, 101.0, 7.0),
479+
(OrderSide.SELL, 102.0, 3.0),
480+
],
481+
{Decimal("100.00"): Decimal("10"), Decimal("99.00"): Decimal("5")},
482+
{Decimal("101.00"): Decimal("7"), Decimal("102.00"): Decimal("3")},
483+
),
484+
# Test case 3: Only buy orders
485+
(
486+
[
487+
(OrderSide.BUY, 100.0, 10.0),
488+
(OrderSide.BUY, 99.0, 5.0),
489+
],
490+
{Decimal("100.00"): Decimal("10"), Decimal("99.00"): Decimal("5")},
491+
{},
492+
),
493+
# Test case 4: Only sell orders
494+
(
495+
[
496+
(OrderSide.SELL, 101.0, 7.0),
497+
(OrderSide.SELL, 102.0, 3.0),
498+
],
499+
{},
500+
{Decimal("101.00"): Decimal("7"), Decimal("102.00"): Decimal("3")},
501+
),
502+
],
503+
)
504+
def test_own_order_book_quantities_parametrized(
505+
orders,
506+
expected_bid_quantities,
507+
expected_ask_quantities,
508+
):
509+
instrument_id = InstrumentId.from_str("AAPL.XNAS")
510+
book = OwnOrderBook(instrument_id)
511+
512+
# Add orders based on the test parameters
513+
for i, (side, price, size) in enumerate(orders):
514+
order = OwnBookOrder(
515+
client_order_id=ClientOrderId(f"O-{i+1}"),
516+
side=side,
517+
price=Price(price, 2),
518+
size=Quantity(size, 0),
519+
order_type=OrderType.LIMIT,
520+
time_in_force=TimeInForce.GTC,
521+
status=OrderStatus.ACCEPTED,
522+
ts_last=0,
523+
ts_init=0,
524+
)
525+
book.add(order)
526+
527+
bid_quantities = book.bid_quantity()
528+
ask_quantities = book.ask_quantity()
529+
530+
assert dict(bid_quantities) == expected_bid_quantities
531+
assert dict(ask_quantities) == expected_ask_quantities

0 commit comments

Comments
 (0)