fcfc136384
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)
81 lines
2.2 KiB
Python
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
|