Files
ftdt-quant-lab/microstructure/signals.py
T
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

179 lines
5.8 KiB
Python

"""
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"