Files
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

182 lines
6.1 KiB
Python

"""
Footprint Engine
Aggregates tick data into price-level bid/ask volume buckets.
Detects imbalances, strong levels, and unfinished auction levels.
"""
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Optional
from orderflow_system.data.models import Tick, Candle, FootprintLevel, Side
@dataclass
class FootprintBar:
"""Complete footprint for a single time bar."""
timestamp_ms: int = 0
open: float = 0.0
high: float = 0.0
low: float = 0.0
close: float = 0.0
levels: dict[float, FootprintLevel] = field(default_factory=dict)
@property
def total_buy_volume(self) -> float:
return sum(lv.ask_volume for lv in self.levels.values())
@property
def total_sell_volume(self) -> float:
return sum(lv.bid_volume for lv in self.levels.values())
@property
def delta(self) -> float:
return self.total_buy_volume - self.total_sell_volume
@property
def total_volume(self) -> float:
return self.total_buy_volume + self.total_sell_volume
def imbalance_levels(self, threshold: float = 3.0) -> list[tuple[float, str]]:
"""
Find price levels with strong imbalance (buy/sell ratio > threshold).
These are one-side-print levels — key for initiative auction detection.
Returns list of (price, 'buy'|'sell') for imbalanced levels.
"""
results = []
for price, lv in sorted(self.levels.items()):
if lv.bid_volume > 0 and lv.ask_volume / lv.bid_volume >= threshold:
results.append((price, "buy"))
elif lv.ask_volume > 0 and lv.bid_volume / lv.ask_volume >= threshold:
results.append((price, "sell"))
elif lv.bid_volume == 0 and lv.ask_volume > 0:
results.append((price, "buy"))
elif lv.ask_volume == 0 and lv.bid_volume > 0:
results.append((price, "sell"))
return results
def max_volume_level(self) -> Optional[tuple[float, FootprintLevel]]:
"""Price level with highest total volume = POC of this bar."""
if not self.levels:
return None
return max(self.levels.items(), key=lambda x: x[1].total_volume)
def absorption_at_level(self, price: float, tolerance: float = 0.0) -> Optional[FootprintLevel]:
"""Get footprint data at a specific price level."""
if price in self.levels:
return self.levels[price]
# Check with tolerance
for p, lv in self.levels.items():
if abs(p - price) <= tolerance:
return lv
return None
class FootprintEngine:
"""
Builds and analyzes footprint data from ticks or candles.
The footprint shows executed buy and sell orders at each price level,
revealing aggression, absorption, and imbalance.
"""
def __init__(self, tick_size: float = 0.1):
self.tick_size = tick_size
self._bar_history: list[FootprintBar] = []
self._max_history = 200
def build_from_candle(self, candle: Candle) -> FootprintBar:
"""Build footprint bar from a candle that already has footprint data."""
bar = FootprintBar(
timestamp_ms=candle.timestamp_ms,
open=candle.open,
high=candle.high,
low=candle.low,
close=candle.close,
levels=dict(candle.footprint),
)
self._bar_history.append(bar)
if len(self._bar_history) > self._max_history:
self._bar_history = self._bar_history[-self._max_history:]
return bar
def build_from_ticks(self, ticks: list[Tick], timestamp_ms: int = 0) -> FootprintBar:
"""Build footprint bar from raw ticks."""
if not ticks:
return FootprintBar(timestamp_ms=timestamp_ms)
levels: dict[float, FootprintLevel] = {}
prices = []
for t in ticks:
rounded = round(round(t.price / self.tick_size) * self.tick_size, 10)
prices.append(t.price)
if rounded not in levels:
levels[rounded] = FootprintLevel(price=rounded)
if t.is_buy:
levels[rounded].ask_volume += t.size
else:
levels[rounded].bid_volume += t.size
bar = FootprintBar(
timestamp_ms=timestamp_ms or ticks[0].timestamp_ms,
open=prices[0],
high=max(prices),
low=min(prices),
close=prices[-1],
levels=levels,
)
self._bar_history.append(bar)
if len(self._bar_history) > self._max_history:
self._bar_history = self._bar_history[-self._max_history:]
return bar
@property
def history(self) -> list[FootprintBar]:
return self._bar_history
def get_recent_bars(self, n: int) -> list[FootprintBar]:
return self._bar_history[-n:]
def get_aggressive_volume_at_level(
self, price: float, lookback_bars: int = 5
) -> tuple[float, float]:
"""
Get total aggressive buy and sell volume at a price level
across the last N bars. Used for absorption detection.
Returns (buy_volume, sell_volume) at that level.
"""
total_buy = 0.0
total_sell = 0.0
tolerance = self.tick_size * 0.5
for bar in self._bar_history[-lookback_bars:]:
lv = bar.absorption_at_level(price, tolerance)
if lv:
total_buy += lv.ask_volume
total_sell += lv.bid_volume
return total_buy, total_sell
def count_consecutive_imbalances(
self, direction: str, lookback: int = 5, threshold: float = 3.0
) -> int:
"""
Count consecutive bars with one-sided imbalance in a direction.
Used for initiative auction detection — "constant aggression" signal.
"""
count = 0
for bar in reversed(self._bar_history[-lookback:]):
imbalances = bar.imbalance_levels(threshold)
has_directional = any(d == direction for _, d in imbalances)
if has_directional:
count += 1
else:
break
return count