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)
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Tests for data/normalizer.py — timestamp normalization and sequence gaps.
|
||||
"""
|
||||
import time
|
||||
|
||||
from data.normalizer import (
|
||||
normalize_timestamp,
|
||||
SequenceTracker,
|
||||
detect_sequence_gap,
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_ms_timestamp():
|
||||
"""Millisecond timestamps pass through unchanged."""
|
||||
ts = 1705312800000
|
||||
assert normalize_timestamp(ts) == 1705312800000
|
||||
|
||||
|
||||
def test_normalize_seconds_timestamp():
|
||||
"""Second timestamps get multiplied by 1000."""
|
||||
ts = 1705312800
|
||||
result = normalize_timestamp(ts)
|
||||
assert result == 1705312800000
|
||||
|
||||
|
||||
def test_normalize_float_timestamp():
|
||||
"""Float seconds get multiplied."""
|
||||
ts = 1705312800.5
|
||||
result = normalize_timestamp(ts)
|
||||
assert result == 1705312800500
|
||||
|
||||
|
||||
def test_normalize_large_float_is_ms():
|
||||
"""A float > 1e12 is already in ms."""
|
||||
ts = 1705312800000.123
|
||||
result = normalize_timestamp(ts)
|
||||
assert result == 1705312800000
|
||||
|
||||
|
||||
def test_normalize_iso_string():
|
||||
"""ISO 8601 Z string converted to ms."""
|
||||
result = normalize_timestamp("2024-01-15T12:00:00.000Z")
|
||||
assert result == 1705320000000
|
||||
|
||||
|
||||
def test_normalize_iso_no_z():
|
||||
"""ISO string without trailing Z."""
|
||||
result = normalize_timestamp("2024-01-15T12:00:00")
|
||||
expected = 1705320000000
|
||||
assert abs(result - expected) < 1000
|
||||
|
||||
|
||||
def test_normalize_none_returns_now():
|
||||
"""None returns current time (within 1s)."""
|
||||
now_ms = int(time.time() * 1000)
|
||||
result = normalize_timestamp(None)
|
||||
assert abs(result - now_ms) < 2000
|
||||
|
||||
|
||||
def test_normalize_datetime():
|
||||
"""datetime object converted to ms."""
|
||||
from datetime import datetime
|
||||
dt = datetime(2024, 1, 15, 12, 0, 0)
|
||||
result = normalize_timestamp(dt)
|
||||
assert result == 1705320000000
|
||||
|
||||
|
||||
class TestSequenceTracker:
|
||||
def test_initial_no_gap(self):
|
||||
st = SequenceTracker()
|
||||
assert st.check("l2", "BTC", 100) is None
|
||||
|
||||
def test_consecutive_no_gap(self):
|
||||
st = SequenceTracker()
|
||||
st.check("l2", "BTC", 100)
|
||||
assert st.check("l2", "BTC", 101) is None
|
||||
assert st.check("l2", "BTC", 102) is None
|
||||
|
||||
def test_gap_detected(self):
|
||||
st = SequenceTracker()
|
||||
st.check("l2", "BTC", 100)
|
||||
gap = st.check("l2", "BTC", 105)
|
||||
assert gap is not None
|
||||
assert gap["gap_size"] == 4
|
||||
assert gap["expected"] == 101
|
||||
|
||||
def test_multiple_channels_independent(self):
|
||||
st = SequenceTracker()
|
||||
st.check("l2", "BTC", 100)
|
||||
st.check("trades", "BTC", 50)
|
||||
assert st.check("l2", "BTC", 101) is None
|
||||
assert st.check("trades", "BTC", 51) is None
|
||||
|
||||
def test_reset_clears_state(self):
|
||||
st = SequenceTracker()
|
||||
st.check("l2", "BTC", 100)
|
||||
st.reset("l2", "BTC")
|
||||
assert st.check("l2", "BTC", 200) is None # fresh start
|
||||
|
||||
def test_gap_counts_accumulate(self):
|
||||
st = SequenceTracker()
|
||||
st.check("l2", "BTC", 100)
|
||||
st.check("l2", "BTC", 105)
|
||||
st.check("l2", "BTC", 110)
|
||||
gaps = st.gap_counts
|
||||
assert gaps["l2:BTC"] == 8 # 4 + 4
|
||||
|
||||
|
||||
def test_detect_sequence_gap_empty():
|
||||
assert detect_sequence_gap(1, None) == 0
|
||||
|
||||
|
||||
def test_detect_sequence_gap_ok():
|
||||
assert detect_sequence_gap(102, 101) == 0
|
||||
|
||||
|
||||
def test_detect_sequence_gap_found():
|
||||
gap = detect_sequence_gap(105, 101)
|
||||
assert gap == 3
|
||||
|
||||
|
||||
def test_detect_sequence_gap_negative():
|
||||
assert detect_sequence_gap(100, 101) == -1 # dupe or reset
|
||||
Reference in New Issue
Block a user