CashFlow / tests /test_l2_factor.py
lhllamlam's picture
feat: wire 4 institutional flow factors (L2, options, ticks, intraday); stub data committed
6b66ac0 verified
Raw
History Blame Contribute Delete
4.58 kB
"""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)], # one big bid
[(100.01, 200)],
)
# With normal ages - all included, factor should be bid-heavy
f_normal = compute_l2_factor("X", source=_StaticSource(book))
assert f_normal > 0.2
# With spoof ages (0.1s) - the big bid is filtered, book looks thin
book_spoofed = {
"ticker": "X",
"ts": "2026-06-02T14:30:00Z",
"bids": [[100.0, 50000, "NSDQ", 0.1]], # spoof!
"asks": [[100.01, 200, "NSDQ", 5.0]],
}
f_spoofed = compute_l2_factor("X", source=_StaticSource(book_spoofed))
# Spoofed book should be weaker (closer to 0)
assert abs(f_spoofed) < abs(f_normal)
def test_book_lying_detection():
assert _book_lying(0.7, -0.005) is True # bid-heavy but falling
assert _book_lying(0.7, 0.0) is False # bid-heavy and flat
assert _book_lying(0.3, 0.005) is True # ask-heavy but rising
assert _book_lying(0.5, 0.0) is False # neutral
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))
# "Lying" book should produce a smaller absolute factor
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"])