125 lines
4.4 KiB
Python
125 lines
4.4 KiB
Python
"""L2 book delta + registry tests."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from decimal import Decimal as D
|
|
|
|
import pytest
|
|
|
|
from arbitrage.book.l2 import BookRegistry, LevelChange, LiveBook, Side
|
|
|
|
|
|
class TestLiveBook:
|
|
def test_snapshot_sets_best_bid_ask(self) -> None:
|
|
b = LiveBook(token_id="t")
|
|
b.apply_snapshot(
|
|
bids=[(D("0.10"), D("100")), (D("0.12"), D("50"))],
|
|
asks=[(D("0.20"), D("80")), (D("0.19"), D("40"))],
|
|
)
|
|
assert b.best_bid() == (D("0.12"), D("50"))
|
|
assert b.best_ask() == (D("0.19"), D("40"))
|
|
|
|
def test_snapshot_drops_zero_size_levels(self) -> None:
|
|
b = LiveBook(token_id="t")
|
|
b.apply_snapshot(bids=[(D("0.10"), D("0"))], asks=[(D("0.20"), D("10"))])
|
|
assert b.best_bid() is None
|
|
|
|
def test_delta_removes_level_at_zero(self) -> None:
|
|
b = LiveBook(token_id="t")
|
|
b.apply_snapshot(bids=[], asks=[(D("0.50"), D("10")), (D("0.60"), D("5"))])
|
|
b.apply_delta([LevelChange(price=D("0.50"), size=D("0"), side=Side.ASK)])
|
|
assert b.best_ask() == (D("0.60"), D("5"))
|
|
|
|
def test_delta_replaces_size_at_level(self) -> None:
|
|
b = LiveBook(token_id="t")
|
|
b.apply_snapshot(bids=[], asks=[(D("0.50"), D("10"))])
|
|
b.apply_delta([LevelChange(price=D("0.50"), size=D("3"), side=Side.ASK)])
|
|
assert b.best_ask() == (D("0.50"), D("3"))
|
|
|
|
def test_vwap_buy_walks_multiple_levels(self) -> None:
|
|
b = LiveBook(token_id="t")
|
|
b.apply_snapshot(
|
|
bids=[],
|
|
asks=[(D("0.19"), D("40")), (D("0.20"), D("80")), (D("0.21"), D("120"))],
|
|
)
|
|
result = b.vwap_buy(D("100"))
|
|
assert result is not None
|
|
vwap, filled, levels = result
|
|
assert vwap == D("0.196") # (40*0.19 + 60*0.20) / 100
|
|
assert filled == D("100")
|
|
assert levels == 2
|
|
|
|
def test_vwap_buy_partial_if_insufficient_depth(self) -> None:
|
|
b = LiveBook(token_id="t")
|
|
b.apply_snapshot(bids=[], asks=[(D("0.50"), D("10"))])
|
|
result = b.vwap_buy(D("100"))
|
|
assert result is not None
|
|
_, filled, _ = result
|
|
assert filled == D("10")
|
|
|
|
def test_vwap_buy_none_on_empty_book(self) -> None:
|
|
b = LiveBook(token_id="t")
|
|
assert b.vwap_buy(D("10")) is None
|
|
|
|
def test_to_snapshot_orders_bids_descending(self) -> None:
|
|
b = LiveBook(token_id="t")
|
|
b.apply_snapshot(
|
|
bids=[(D("0.10"), D("1")), (D("0.12"), D("2")), (D("0.11"), D("3"))],
|
|
asks=[],
|
|
)
|
|
snap = b.to_snapshot()
|
|
assert [lvl.price for lvl in snap.bids] == [D("0.12"), D("0.11"), D("0.10")]
|
|
|
|
def test_sequence_increments_on_updates(self) -> None:
|
|
b = LiveBook(token_id="t")
|
|
assert b.snapshots_applied == 0 and b.deltas_applied == 0
|
|
b.apply_snapshot(bids=[], asks=[(D("0.5"), D("1"))])
|
|
b.apply_delta([LevelChange(price=D("0.5"), size=D("2"), side=Side.ASK)])
|
|
b.apply_delta([LevelChange(price=D("0.5"), size=D("0"), side=Side.ASK)])
|
|
assert b.snapshots_applied == 1
|
|
assert b.deltas_applied == 2
|
|
|
|
|
|
class TestBookRegistry:
|
|
async def test_registry_publishes_updates_to_subscribers(self) -> None:
|
|
reg = BookRegistry()
|
|
events: list[str] = []
|
|
|
|
async def reader() -> None:
|
|
async for u in reg.updates():
|
|
events.append(u.reason)
|
|
if len(events) >= 2:
|
|
return
|
|
|
|
task = asyncio.create_task(reader())
|
|
await asyncio.sleep(0)
|
|
reg.apply_snapshot("x", bids=[], asks=[(D("0.5"), D("1"))])
|
|
reg.apply_delta("x", [LevelChange(price=D("0.5"), size=D("0"), side=Side.ASK)])
|
|
await asyncio.wait_for(task, timeout=1.0)
|
|
assert events == ["snapshot", "delta"]
|
|
|
|
def test_get_returns_none_for_unknown_token(self) -> None:
|
|
reg = BookRegistry()
|
|
assert reg.get("missing") is None
|
|
|
|
def test_book_creates_and_returns_same_instance(self) -> None:
|
|
reg = BookRegistry()
|
|
a = reg.book("t")
|
|
b = reg.book("t")
|
|
assert a is b
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"raw,expected",
|
|
[
|
|
("BUY", Side.BID),
|
|
("buy", Side.BID),
|
|
("bid", Side.BID),
|
|
("SELL", Side.ASK),
|
|
("ASK", Side.ASK),
|
|
],
|
|
)
|
|
def test_level_change_from_raw(raw: str, expected: Side) -> None:
|
|
lc = LevelChange.from_raw({"price": "0.5", "size": "1", "side": raw})
|
|
assert lc.side is expected
|