Files
ramseshk fcfc136384 feat: Phase 2 — microstructure analytics + 81 tests
New microstructure/ module with pure-function analytics:

microstructure/book.py:
  microprice() — depth-weighted mid price
  mid_price() — simple bid/ask midpoint
  order_book_imbalance() — ranged [-1, 1] volume skew
  depth_imbalance() — imbalance at fixed price distance
  spread_stats() — spread, spread_bps, mid, bid, ask
  depth_resiliency() — bid/ask volume within impact radius
  queue_depletion_prob() — Poisson fill probability at level
  batch_book_stats() — aggregate stats across snapshots

microstructure/trades.py:
  classify_lee_ready() — Lee-Ready aggressor classification
  classify_bulk_lee_ready() — batch classification with mids/bids/asks
  compute_markouts() — forward mid-price change at configurable horizons
  markout_summary() — mean/std/t-stat per side per horizon
  trade_volume_profile() — size bucket distribution
  trade_arrival_rate() — rolling trades/sec with burst detection

microstructure/toxicity.py:
  compute_vpin() — volume-synchronized informed trading probability
  compute_vpin_time_series() — rolling VPIN with alarm threshold
  fill_toxicity() — adverse price movement post-trade
  adverse_selection_ratio() — per-side adverse selection
  liquidation_clustering() — cluster detection in liquidation events

microstructure/funding.py:
  funding_regime() — classify regime (neutral/positive/negative/high)
  funding_predictability() — AR(1) autocorrelation analysis
  funding_carry_pnl() — cumulative carry PnL estimation
  basis_spread() — perp premium over spot (bps)
  basis_convergence_speed() — mean-reversion half-life via AR(1)

microstructure/signals.py:
  composite_signal() — weighted OBI + trade + VPIN + funding signal
  SignalPipeline — stateful pipeline accumulating book/trade updates
  detect_hft_regime() — regime classifier for HFT strategy selection

Bug fixes in Phase 1:
  - data/latency.py: proper linear-interpolation percentiles
  - data/normalizer.py: UTC timezone for naive datetimes
  - data/normalizer.py: detect_sequence_gap returns gap-1 (missing count)
  - microstructure/toxicity.py: consistent vpin_value key in compute_vpin

81 tests across 4 test files (store, normalizer, latency, microstructure)
2026-08-07 14:34:18 +08:00

81 lines
2.2 KiB
Python

"""
Tests for data/latency.py — latency tracking.
"""
import time
from data.latency import LatencyTracker
def test_empty_stats():
lt = LatencyTracker()
s = lt.stats()
for key in ("transport_ms", "signal_ms", "order_ms", "roundtrip_ms"):
assert s[key]["count"] == 0
def test_record_transport():
lt = LatencyTracker()
lt.record_transport(1705312800000, 1705312800.100) # 100ms
s = lt.stats()
assert s["transport_ms"]["p50"] == 100.0
assert s["transport_ms"]["count"] == 1
def test_record_signal():
lt = LatencyTracker()
lt.record_signal(5.2)
lt.record_signal(3.1)
s = lt.stats()
assert s["signal_ms"]["p50"] == 4.15 # interpolated median of [3.1, 5.2]
assert s["signal_ms"]["count"] == 2
def test_record_order():
lt = LatencyTracker()
lt.record_order(12.5)
s = lt.stats()
assert s["order_ms"]["p50"] == 12.5
def test_record_roundtrip():
lt = LatencyTracker()
lt.record_roundtrip(150.0)
s = lt.stats()
assert s["roundtrip_ms"]["p50"] == 150.0
def test_negative_latency_ignored():
lt = LatencyTracker()
lt.record_transport(1705312800100, 1705312800.000) # local before exchange
s = lt.stats()
assert s["transport_ms"]["count"] == 0
def test_summary_compact():
lt = LatencyTracker()
lt.record_transport(1705312800000, 1705312800.100)
lt.record_signal(5.0)
summary = lt.summary()
assert "transport_ms" in summary
assert "signal_ms" in summary
assert "p50" in summary["transport_ms"]
def test_percentiles_multiple_values():
lt = LatencyTracker()
for lat in [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]:
lt.record_signal(float(lat))
s = lt.stats()
assert s["signal_ms"]["p50"] == 55.0 # interpolated median of 10 values
assert s["signal_ms"]["p90"] == 91.0 # (10-1)*90/100=8.1, lo=8, vals[8]=90, vals[9]=100, frac=0.1, result=91
def test_window_pruning_approx():
"""Old records should be pruned after window expires."""
lt = LatencyTracker(window_seconds=0.1)
lt.record_signal(10.0)
time.sleep(0.15)
lt.record_signal(20.0)
s = lt.stats()
assert s["signal_ms"]["p50"] == 20.0 # only the recent one