Layered SSOT data architecture: domain/marketdata (silver — Mbp10Snapshot/BidAskLevel, faithful port of
Rust data contracts incl. the load-bearing 1e-9 price scaling) -> domain/features (gold — MicropriceFeatures
Block N+V, a FAITHFUL port of Rust ml-features/microprice.rs, matching its test oracle bit-for-bit incl.
f64::signum(0)=1.0) -> ports/warehouse.FeatureStore (point-in-time) -> adapters/warehouse DuckDbFeatureStore
(as_of = no lookahead) -> application/feature_pipeline (silver->gold transform, causal). 11 new tests
(8 microprice vs Rust oracle + pipeline/as-of). 31/31 total.
Verified via omnisearch: the Rust formula is the weighted mid-price (commonly mis-called 'Stoikov
microprice'); Stoikov's TRUE micro-price (Quant Finance 2018) is a recursive Markov-chain estimator,
empirically a better future-price predictor — to be ADDED as a separate feature (not a silent oracle
divergence), so the gauntlet A/Bs them. Conservation-of-money ('every cent accounted for, follow the
money') adopted as a first-class principle: a warehouse/trading invariant AND the lens that prioritizes
the order-flow features (OFI/VPIN/book-deltas/Kyle-λ).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
74 lines
3.0 KiB
Python
74 lines
3.0 KiB
Python
"""Microprice port — TDD against the Rust ml-features `microprice.rs` test oracle. Exact DBN fixed-point
|
|
inputs (i64, 1e-9) so the float arithmetic is identical to the Rust."""
|
|
from __future__ import annotations
|
|
|
|
from fxhnt.domain.features import (
|
|
MicropriceFeatures,
|
|
multilevel_microprice,
|
|
stoikov_microprice,
|
|
)
|
|
from fxhnt.domain.marketdata import BidAskLevel, Mbp10Snapshot
|
|
|
|
_BID = 100_000_000_000 # 100.00 at DBN 1e-9 scaling
|
|
_ASK = 100_040_000_000 # 100.04
|
|
|
|
|
|
def _snap(*levels: BidAskLevel) -> Mbp10Snapshot:
|
|
return Mbp10Snapshot(symbol="ES.FUT", ts=0, levels=tuple(levels))
|
|
|
|
|
|
def _l1(bid_sz: int, ask_sz: int, bid_px: int = _BID, ask_px: int = _ASK) -> BidAskLevel:
|
|
return BidAskLevel(bid_px=bid_px, bid_sz=bid_sz, ask_px=ask_px, ask_sz=ask_sz)
|
|
|
|
|
|
def test_balanced_book_equals_mid() -> None:
|
|
snap = _snap(_l1(100, 100))
|
|
assert abs(stoikov_microprice(snap) - snap.mid_price()) < 1e-9 # == 100.02
|
|
|
|
|
|
def test_heavy_ask_tilts_toward_bid() -> None:
|
|
snap = _snap(_l1(10, 1000))
|
|
mp, mid = stoikov_microprice(snap), snap.mid_price()
|
|
assert mp < mid # heavy ask -> below mid
|
|
assert abs(mp - 100.0) < abs(mp - mid) # closer to the bid
|
|
assert abs(mp - 100.000_396_039_6) < 1e-6 # exact: (100·1000+100.04·10)/1010
|
|
|
|
|
|
def test_heavy_bid_tilts_toward_ask() -> None:
|
|
snap = _snap(_l1(1000, 10))
|
|
assert stoikov_microprice(snap) > snap.mid_price()
|
|
|
|
|
|
def test_zero_depth_falls_back_to_mid() -> None:
|
|
snap = _snap(_l1(0, 0))
|
|
assert stoikov_microprice(snap) == snap.mid_price()
|
|
|
|
|
|
def test_empty_book_is_zero() -> None:
|
|
assert stoikov_microprice(_snap()) == 0.0
|
|
|
|
|
|
def test_multilevel_k1_equals_stoikov() -> None:
|
|
snap = _snap(_l1(100, 200))
|
|
assert abs(multilevel_microprice(snap, 1) - stoikov_microprice(snap)) < 1e-9
|
|
|
|
|
|
def test_residual_sign_is_signum_never_zero() -> None:
|
|
feats = MicropriceFeatures().update(_snap(_l1(1000, 10))) # heavy bid -> residual > 0
|
|
assert feats["microprice_residual"] > 0 and feats["microprice_residual_sign"] == 1.0
|
|
feats = MicropriceFeatures().update(_snap(_l1(10, 1000))) # heavy ask -> residual < 0
|
|
assert feats["microprice_residual"] < 0 and feats["microprice_residual_sign"] == -1.0
|
|
|
|
|
|
def test_change_and_acceleration_streaming() -> None:
|
|
f = MicropriceFeatures()
|
|
r0 = f.update(_snap(_l1(100, 100), BidAskLevel(bid_px=_BID, bid_sz=100, ask_px=_ASK, ask_sz=100)))
|
|
assert r0["microprice_change"] == 0.0 and r0["microprice_acceleration"] == 0.0 # cold start
|
|
|
|
# a linear ramp of the balanced mid -> first difference constant, second difference ≈ 0
|
|
f = MicropriceFeatures()
|
|
s = [f.update(_snap(_l1(100, 100, bid_px=_BID + k * 10_000_000, ask_px=_ASK + k * 10_000_000)))
|
|
for k in range(3)]
|
|
assert abs(s[2]["microprice_change"] - s[1]["microprice_change"]) < 1e-6 # constant Δ
|
|
assert abs(s[2]["microprice_acceleration"]) < 1e-6 # Δ² ≈ 0
|