| """Tests for the Level-2 large-resting-order factor.""" |
|
|
| from __future__ import annotations |
|
|
| import math |
|
|
| import pytest |
|
|
| from scanner.factor_sources import StubDataSource |
| from scanner.l2_factor import ( |
| BIG_SIZE, |
| CLIP_RANGE, |
| SPOOF_AGE_THRESH, |
| TOP_LEVELS, |
| _book_lying, |
| compute_l2_factor, |
| compute_l2_factors, |
| ) |
|
|
|
|
| def _book(bids, asks, age=5.0): |
| """Wrap bid/ask lists in the dict schema the factor expects.""" |
| return { |
| "ticker": "TEST", |
| "ts": "2026-06-02T14:30:00Z", |
| "bids": [[p, s, "NSDQ", age] for p, s in bids], |
| "asks": [[p, s, "NSDQ", age] for p, s in asks], |
| } |
|
|
|
|
| class _StaticSource: |
| """Data source that returns the same book for every ticker.""" |
| def __init__(self, book): |
| self._book = book |
| def get_l2_snapshot(self, ticker): |
| return self._book |
|
|
|
|
| def test_balanced_book_is_near_zero(): |
| book = _book( |
| [(100.0, 1000), (99.99, 800), (99.98, 600)], |
| [(100.01, 1000), (100.02, 800), (100.03, 600)], |
| ) |
| f = compute_l2_factor("X", source=_StaticSource(book)) |
| assert -0.5 < f < 0.5, f"expected near-zero for balanced book, got {f}" |
|
|
|
|
| def test_bid_heavy_book_is_positive(): |
| book = _book( |
| [(100.0, 50000), (99.99, 40000), (99.98, 30000), (99.97, 20000), (99.96, 10000)], |
| [(100.01, 200), (100.02, 200), (100.03, 200), (100.04, 200), (100.05, 200)], |
| ) |
| f = compute_l2_factor("X", source=_StaticSource(book)) |
| assert f > 0.3, f"expected positive for bid-heavy book, got {f}" |
|
|
|
|
| def test_ask_heavy_book_is_negative(): |
| book = _book( |
| [(100.0, 200), (99.99, 200), (99.98, 200), (99.97, 200), (99.96, 200)], |
| [(100.01, 50000), (100.02, 40000), (100.03, 30000), (100.04, 20000), (100.05, 10000)], |
| ) |
| f = compute_l2_factor("X", source=_StaticSource(book)) |
| assert f < -0.3, f"expected negative for ask-heavy book, got {f}" |
|
|
|
|
| def test_clipped_to_range(): |
| book = _book( |
| [(100.0, 1_000_000)] * TOP_LEVELS, |
| [(100.01, 1)] * TOP_LEVELS, |
| ) |
| f = compute_l2_factor("X", source=_StaticSource(book)) |
| assert -CLIP_RANGE <= f <= CLIP_RANGE |
|
|
|
|
| def test_spoofed_orders_ignored(): |
| """Orders with age < SPOOF_AGE_THRESH should be filtered out.""" |
| book = _book( |
| [(100.0, 50000)], |
| [(100.01, 200)], |
| ) |
| |
| f_normal = compute_l2_factor("X", source=_StaticSource(book)) |
| assert f_normal > 0.2 |
| |
| book_spoofed = { |
| "ticker": "X", |
| "ts": "2026-06-02T14:30:00Z", |
| "bids": [[100.0, 50000, "NSDQ", 0.1]], |
| "asks": [[100.01, 200, "NSDQ", 5.0]], |
| } |
| f_spoofed = compute_l2_factor("X", source=_StaticSource(book_spoofed)) |
| |
| assert abs(f_spoofed) < abs(f_normal) |
|
|
|
|
| def test_book_lying_detection(): |
| assert _book_lying(0.7, -0.005) is True |
| assert _book_lying(0.7, 0.0) is False |
| assert _book_lying(0.3, 0.005) is True |
| assert _book_lying(0.5, 0.0) is False |
|
|
|
|
| def test_book_lying_discounts_factor(): |
| book = _book( |
| [(100.0, 50000), (99.99, 40000), (99.98, 30000), (99.97, 20000), (99.96, 10000)], |
| [(100.01, 200), (100.02, 200), (100.03, 200), (100.04, 200), (100.05, 200)], |
| ) |
| f_honest = compute_l2_factor("X", recent_return=0.001, source=_StaticSource(book)) |
| f_lying = compute_l2_factor("X", recent_return=-0.01, source=_StaticSource(book)) |
| |
| assert abs(f_lying) < abs(f_honest) |
|
|
|
|
| def test_empty_book_returns_zero(): |
| f = compute_l2_factor("X", source=_StaticSource(None)) |
| assert f == 0.0 |
| f = compute_l2_factor("X", source=_StaticSource({"bids": [], "asks": []})) |
| assert f == 0.0 |
|
|
|
|
| def test_batch_returns_all_tickers(): |
| book = _book( |
| [(100.0, 1000), (99.99, 800)], |
| [(100.01, 1000), (100.02, 800)], |
| ) |
| src = _StaticSource(book) |
| out = compute_l2_factors(["A", "B", "C"], source=src) |
| assert set(out.keys()) == {"A", "B", "C"} |
| for v in out.values(): |
| assert -CLIP_RANGE <= v <= CLIP_RANGE |
|
|
|
|
| def test_stub_data_source_synthesises(): |
| src = StubDataSource(stub_dir="/nonexistent") |
| book = src.get_l2_snapshot("AAPL") |
| assert book is not None |
| assert "bids" in book and "asks" in book |
| assert len(book["bids"]) == 10 |
| assert all(len(b) == 4 for b in book["bids"]) |
|
|