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,178 @@
|
||||
"""
|
||||
Composite signal construction from microstructure features.
|
||||
|
||||
Combines book imbalance, trade flow, toxicity, and funding signals
|
||||
into a single directional signal with confidence score.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
# ── Composite signal ────────────────────────────────────────
|
||||
|
||||
def composite_signal(
|
||||
obi: float, # [-1, 1] order book imbalance
|
||||
trade_imbalance: float = 0.0, # [-1, 1] recent trade aggressor skew
|
||||
vpin: float = 0.0, # [0, 1] flow toxicity (higher = toxic)
|
||||
funding_regime: str = "neutral", # "high_positive", "negative", etc.
|
||||
spread_bps: float = 1.0, # current spread
|
||||
weight_obi: float = 0.35,
|
||||
weight_trade: float = 0.25,
|
||||
weight_vpin: float = -0.20, # negative: high VPIN → reduce confidence
|
||||
weight_funding: float = 0.20,
|
||||
) -> dict:
|
||||
"""Combine microstructure features into a single directional signal.
|
||||
|
||||
Returns:
|
||||
signal: "buy", "sell", or "neutral"
|
||||
score: [-1, 1] raw composite (positive = buy pressure)
|
||||
confidence: [0, 1] confidence in the signal
|
||||
breakdown: per-component contributions
|
||||
"""
|
||||
obi_score = np.clip(obi, -1.0, 1.0)
|
||||
trade_score = np.clip(trade_imbalance, -1.0, 1.0)
|
||||
vpin_score = np.clip(vpin, 0.0, 1.0)
|
||||
|
||||
# Funding: positive funding → short gets paid → sell bias; negative → buy bias
|
||||
funding_score_map = {
|
||||
"high_positive": -0.8,
|
||||
"positive": -0.4,
|
||||
"neutral": 0.0,
|
||||
"negative": 0.4,
|
||||
"high_negative": 0.8,
|
||||
}
|
||||
funding_score = funding_score_map.get(funding_regime, 0.0)
|
||||
|
||||
raw = (
|
||||
weight_obi * obi_score +
|
||||
weight_trade * trade_score +
|
||||
weight_vpin * vpin_score +
|
||||
weight_funding * funding_score
|
||||
)
|
||||
|
||||
# Confidence: base from signal magnitude, reduced by VPIN and spread
|
||||
signal_magnitude = abs(raw)
|
||||
vpin_penalty = np.clip(vpin * 0.5, 0.0, 0.3) if raw != 0 else 0
|
||||
spread_penalty = min(spread_bps / 50.0, 0.3) # wide spread → lower confidence
|
||||
|
||||
confidence = max(0.0, min(1.0, signal_magnitude * 1.5 - vpin_penalty - spread_penalty))
|
||||
|
||||
if raw > 0.1:
|
||||
signal = "buy"
|
||||
elif raw < -0.1:
|
||||
signal = "sell"
|
||||
else:
|
||||
signal = "neutral"
|
||||
|
||||
return {
|
||||
"signal": signal,
|
||||
"score": round(raw, 4),
|
||||
"confidence": round(confidence, 4),
|
||||
"breakdown": {
|
||||
"obi": round(obi_score * weight_obi, 4),
|
||||
"trade": round(trade_score * weight_trade, 4),
|
||||
"vpin": round(vpin_score * weight_vpin, 4),
|
||||
"funding": round(funding_score * weight_funding, 4),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ── Signal pipeline ─────────────────────────────────────────
|
||||
|
||||
class SignalPipeline:
|
||||
"""Stateful pipeline that accumulates microstructure data and emits signals.
|
||||
|
||||
Usage:
|
||||
pipeline = SignalPipeline()
|
||||
pipeline.update_book(bids, asks)
|
||||
pipeline.update_trade(px, sz, mid)
|
||||
signal = pipeline.emit()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
obi_window: int = 100,
|
||||
trade_window: int = 500,
|
||||
vpin_volume_size: float = 100.0,
|
||||
vpin_buckets: int = 50,
|
||||
):
|
||||
self._obi_window = obi_window
|
||||
self._trade_window = trade_window
|
||||
self._vpin_volume_size = vpin_volume_size
|
||||
self._vpin_buckets = vpin_buckets
|
||||
|
||||
self._buy_vol: list[float] = []
|
||||
self._sell_vol: list[float] = []
|
||||
self._obuys: int = 0
|
||||
self._osells: int = 0
|
||||
self._obuy: int = 1
|
||||
|
||||
def update_book(self, bids: dict[float, float], asks: dict[float, float]):
|
||||
from microstructure.book import order_book_imbalance
|
||||
self._obuys = order_book_imbalance(bids, asks)
|
||||
|
||||
def update_trade(self, px: float, sz: float, mid: float):
|
||||
if px >= mid:
|
||||
self._buy_vol.append(sz)
|
||||
else:
|
||||
self._sell_vol.append(sz)
|
||||
|
||||
if len(self._buy_vol) > self._trade_window:
|
||||
self._buy_vol = self._buy_vol[-self._trade_window:]
|
||||
if len(self._sell_vol) > self._trade_window:
|
||||
self._sell_vol = self._sell_vol[-self._trade_window:]
|
||||
|
||||
def recent_trade_imbalance(self) -> float:
|
||||
bv = sum(self._buy_vol[-100:])
|
||||
sv = sum(self._sell_vol[-100:])
|
||||
total = bv + sv
|
||||
return (bv - sv) / total if total > 0 else 0.0
|
||||
|
||||
def current_vpin(self) -> float:
|
||||
from microstructure.toxicity import compute_vpin
|
||||
result = compute_vpin(
|
||||
self._buy_vol, self._sell_vol,
|
||||
volume_bucket_size=self._vpin_volume_size,
|
||||
n_buckets=self._vpin_buckets,
|
||||
)
|
||||
return result.get("vpin_value", 0.0)
|
||||
|
||||
def emit(self) -> dict:
|
||||
return composite_signal(
|
||||
obi=self._obuys,
|
||||
trade_imbalance=self.recent_trade_imbalance(),
|
||||
vpin=self.current_vpin(),
|
||||
)
|
||||
|
||||
|
||||
# ── Regime detection ─────────────────────────────────────────
|
||||
|
||||
def detect_hft_regime(
|
||||
obi_std: float,
|
||||
spread_mean_bps: float,
|
||||
trade_rate_per_sec: float,
|
||||
vpin: float,
|
||||
) -> str:
|
||||
"""Classify current market regime for HFT strategy selection.
|
||||
|
||||
Returns one of:
|
||||
- "trending" — directional, high OBI variance, low VPIN
|
||||
- "ranging" — low OBI variance, tight spread, active
|
||||
- "toxic" — high VPIN, wide spread → don't quote
|
||||
- "quiet" — low activity, avoid
|
||||
"""
|
||||
if vpin > 0.4:
|
||||
return "toxic"
|
||||
if trade_rate_per_sec < 0.1:
|
||||
return "quiet"
|
||||
if obi_std > 0.3 and spread_mean_bps < 5:
|
||||
return "trending"
|
||||
if obi_std < 0.15 and spread_mean_bps < 3:
|
||||
return "ranging"
|
||||
if spread_mean_bps > 10:
|
||||
return "toxic"
|
||||
return "quiet"
|
||||
Reference in New Issue
Block a user