Files
OrderFlow-Analysis-Pro/orderflow_system/analytics/orderbook.py
T
BlackboxAI 0206ef7cbb Initial commit: orderflow analysis system with 5 pattern detectors
Real-time orderflow trading system with absorption, initiative, sweep,
exhaustion, and divergence detection. Features volume profile framing,
state machine trade lifecycle, MT5 + Bybit feeds, FastAPI dashboard,
and Telegram alerts for 30+ instruments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 21:38:25 +03:00

209 lines
7.4 KiB
Python

"""
Orderbook Tracker
Maintains real-time L2 orderbook state and detects structural features:
- Thin levels (low liquidity → sweep risk)
- Book imbalance (bid vs ask depth)
- Path of least resistance
- Levels being consumed (for sweep detection)
"""
from __future__ import annotations
import time
from collections import deque
from dataclasses import dataclass, field
from typing import Optional
from orderflow_system.data.models import OrderbookSnapshot, OrderbookLevel
@dataclass
class LevelConsumption:
"""Tracks consumption of orderbook levels for sweep detection."""
timestamp_ms: int
price: float
side: str # 'bid' or 'ask'
prev_quantity: float
consumed_quantity: float
@dataclass
class BookState:
"""Analyzed state of the orderbook at a point in time."""
timestamp_ms: int = 0
imbalance_ratio: float = 0.0 # +1 = all bids, -1 = all asks
bid_depth_5: float = 0.0 # Total qty in top 5 bid levels
ask_depth_5: float = 0.0 # Total qty in top 5 ask levels
bid_depth_10: float = 0.0
ask_depth_10: float = 0.0
thin_bids: list[float] = field(default_factory=list) # Thin bid prices
thin_asks: list[float] = field(default_factory=list) # Thin ask prices
path_of_least_resistance: str = "neutral" # 'up', 'down', 'neutral'
best_bid: float = 0.0
best_ask: float = 0.0
spread: float = 0.0
class OrderbookTracker:
"""
Tracks orderbook state changes for sweep and liquidity analysis.
From Fabio's teaching:
- "Path of least resistance": the side with less passive liquidity is
easier for aggressive orders to pierce through
- Thin levels → potential for book sweeps
- Tracking level consumption reveals how aggressive orders eat the book
"""
def __init__(self, thin_threshold: float = 10.0, max_consumption_history: int = 200):
self.thin_threshold = thin_threshold
self._prev_snapshot: Optional[OrderbookSnapshot] = None
self._consumption_history: deque[LevelConsumption] = deque(
maxlen=max_consumption_history
)
self._book_state_history: list[BookState] = []
self._max_history = 200
@property
def latest_snapshot(self) -> Optional[OrderbookSnapshot]:
"""Return the most recent orderbook snapshot."""
return self._prev_snapshot
def update(self, snapshot: OrderbookSnapshot) -> BookState:
"""Process a new orderbook snapshot and return analyzed state."""
# Detect consumed levels if we have a previous snapshot
if self._prev_snapshot is not None:
self._detect_consumptions(self._prev_snapshot, snapshot)
state = self._analyze(snapshot)
self._prev_snapshot = snapshot
self._book_state_history.append(state)
if len(self._book_state_history) > self._max_history:
self._book_state_history = self._book_state_history[-self._max_history:]
return state
def _analyze(self, snap: OrderbookSnapshot) -> BookState:
"""Analyze current orderbook structure."""
bid_5 = snap.bid_depth(5)
ask_5 = snap.ask_depth(5)
bid_10 = snap.bid_depth(10)
ask_10 = snap.ask_depth(10)
# Thin level detection
thin_bids = [
b.price for b in snap.bids[:20]
if b.quantity < self.thin_threshold
]
thin_asks = [
a.price for a in snap.asks[:20]
if a.quantity < self.thin_threshold
]
# Path of least resistance
total = bid_10 + ask_10
if total > 0:
ratio = (bid_10 - ask_10) / total
else:
ratio = 0.0
if ratio > 0.15:
polr = "up" # More bids than asks → harder to go down → easier up
elif ratio < -0.15:
polr = "down" # More asks → easier down
else:
polr = "neutral"
return BookState(
timestamp_ms=snap.timestamp_ms,
imbalance_ratio=snap.imbalance_ratio(5),
bid_depth_5=bid_5,
ask_depth_5=ask_5,
bid_depth_10=bid_10,
ask_depth_10=ask_10,
thin_bids=thin_bids,
thin_asks=thin_asks,
path_of_least_resistance=polr,
best_bid=snap.best_bid or 0.0,
best_ask=snap.best_ask or 0.0,
spread=snap.spread or 0.0,
)
def _detect_consumptions(
self, prev: OrderbookSnapshot, curr: OrderbookSnapshot
):
"""
Detect which book levels were consumed between snapshots.
If a bid/ask level existed before and now has less or zero quantity,
it was consumed by aggressive orders.
"""
ts = curr.timestamp_ms
# Check consumed asks (eaten by aggressive buyers going UP)
prev_asks = {a.price: a.quantity for a in prev.asks[:30]}
curr_asks = {a.price: a.quantity for a in curr.asks[:30]}
for price, prev_qty in prev_asks.items():
curr_qty = curr_asks.get(price, 0.0)
consumed = prev_qty - curr_qty
if consumed > prev_qty * 0.5 and consumed > 1.0:
self._consumption_history.append(LevelConsumption(
timestamp_ms=ts,
price=price,
side="ask",
prev_quantity=prev_qty,
consumed_quantity=consumed,
))
# Check consumed bids (eaten by aggressive sellers going DOWN)
prev_bids = {b.price: b.quantity for b in prev.bids[:30]}
curr_bids = {b.price: b.quantity for b in curr.bids[:30]}
for price, prev_qty in prev_bids.items():
curr_qty = curr_bids.get(price, 0.0)
consumed = prev_qty - curr_qty
if consumed > prev_qty * 0.5 and consumed > 1.0:
self._consumption_history.append(LevelConsumption(
timestamp_ms=ts,
price=price,
side="bid",
prev_quantity=prev_qty,
consumed_quantity=consumed,
))
def get_recent_consumptions(
self, time_window_ms: int = 5000, side: Optional[str] = None
) -> list[LevelConsumption]:
"""
Get recent level consumptions within a time window.
Used by sweep detector to count how many levels were eaten recently.
"""
now = int(time.time() * 1000)
cutoff = now - time_window_ms
result = [
c for c in self._consumption_history
if c.timestamp_ms >= cutoff
]
if side:
result = [c for c in result if c.side == side]
return result
def count_swept_levels(
self, time_window_ms: int = 3000, side: Optional[str] = None
) -> int:
"""Count distinct price levels consumed within time window."""
consumptions = self.get_recent_consumptions(time_window_ms, side)
return len(set(c.price for c in consumptions))
def total_consumed_volume(
self, time_window_ms: int = 3000, side: Optional[str] = None
) -> float:
"""Total volume consumed from the book within time window."""
consumptions = self.get_recent_consumptions(time_window_ms, side)
return sum(c.consumed_quantity for c in consumptions)
@property
def latest_state(self) -> Optional[BookState]:
return self._book_state_history[-1] if self._book_state_history else None