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

291 lines
10 KiB
Python

"""
Core data models used across the entire system.
Tick, OrderbookSnapshot, Candle, Signal, TradeState — the common language.
"""
from __future__ import annotations
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
# ──────────────────────────────────────────────
# Enums
# ──────────────────────────────────────────────
class Side(Enum):
BUY = "buy"
SELL = "sell"
class SignalType(Enum):
ABSORPTION = "absorption"
INITIATIVE = "initiative_auction"
SWEEP = "book_sweep"
EXHAUSTION = "exhaustion"
DIVERGENCE = "delta_divergence"
class TradePhase(Enum):
"""State machine phases for the execution model."""
WATCHING = "watching" # Monitoring a qualified level
ABSORPTION_DETECTED = "absorption" # Entry signal seen
POSITION_OPEN = "position_open" # Trade entered
BREAK_EVEN = "break_even" # SL moved to BE after initiative
TRAILING = "trailing" # Trailing on initiative prints
CLOSED = "closed" # Trade finished
# ──────────────────────────────────────────────
# Raw Market Data
# ──────────────────────────────────────────────
@dataclass(slots=True)
class Tick:
"""Single executed trade from the exchange."""
timestamp_ms: int # Unix ms
price: float
size: float # Contracts / quantity
side: Side # Aggressor side (taker)
trade_id: str = ""
@property
def timestamp(self) -> float:
return self.timestamp_ms / 1000.0
@property
def is_buy(self) -> bool:
return self.side == Side.BUY
@dataclass(slots=True)
class OrderbookLevel:
"""Single price level in the orderbook."""
price: float
quantity: float
@dataclass
class OrderbookSnapshot:
"""L2 orderbook state at a point in time."""
timestamp_ms: int
bids: list[OrderbookLevel] = field(default_factory=list) # Sorted desc by price
asks: list[OrderbookLevel] = field(default_factory=list) # Sorted asc by price
@property
def best_bid(self) -> Optional[float]:
return self.bids[0].price if self.bids else None
@property
def best_ask(self) -> Optional[float]:
return self.asks[0].price if self.asks else None
@property
def mid_price(self) -> Optional[float]:
if self.best_bid and self.best_ask:
return (self.best_bid + self.best_ask) / 2.0
return None
@property
def spread(self) -> Optional[float]:
if self.best_bid and self.best_ask:
return self.best_ask - self.best_bid
return None
def bid_depth(self, levels: int = 5) -> float:
"""Total bid quantity in top N levels."""
return sum(b.quantity for b in self.bids[:levels])
def ask_depth(self, levels: int = 5) -> float:
"""Total ask quantity in top N levels."""
return sum(a.quantity for a in self.asks[:levels])
def imbalance_ratio(self, levels: int = 5) -> float:
"""Book imbalance: +1 = all bids, -1 = all asks."""
bd = self.bid_depth(levels)
ad = self.ask_depth(levels)
total = bd + ad
if total == 0:
return 0.0
return (bd - ad) / total
# ──────────────────────────────────────────────
# Aggregated Structures
# ──────────────────────────────────────────────
@dataclass
class FootprintLevel:
"""Bid/Ask volume at a single price level within a candle."""
price: float
bid_volume: float = 0.0 # Aggressive sell volume hitting this bid
ask_volume: float = 0.0 # Aggressive buy volume hitting this ask
@property
def delta(self) -> float:
"""Horizontal delta at this level."""
return self.ask_volume - self.bid_volume
@property
def total_volume(self) -> float:
return self.bid_volume + self.ask_volume
@property
def imbalance_ratio(self) -> float:
"""Buy/sell ratio. >3 = strong buy imbalance."""
if self.bid_volume == 0:
return float("inf") if self.ask_volume > 0 else 0.0
return self.ask_volume / self.bid_volume
@dataclass
class Candle:
"""OHLCV candle enriched with orderflow data."""
timestamp_ms: int
open: float
high: float
low: float
close: float
volume: float = 0.0
buy_volume: float = 0.0 # Aggressive buy volume
sell_volume: float = 0.0 # Aggressive sell volume
tick_count: int = 0
footprint: dict[float, FootprintLevel] = field(default_factory=dict)
@property
def delta(self) -> float:
"""Vertical delta for this candle."""
return self.buy_volume - self.sell_volume
@property
def is_green(self) -> bool:
return self.close >= self.open
@property
def body_size(self) -> float:
return abs(self.close - self.open)
@property
def range_size(self) -> float:
return self.high - self.low
# ──────────────────────────────────────────────
# Volume Profile
# ──────────────────────────────────────────────
@dataclass
class VolumeProfileResult:
"""Output of the volume profile engine for a session."""
session_date: str = "" # YYYY-MM-DD
poc: float = 0.0 # Point of Control
vah: float = 0.0 # Value Area High
val: float = 0.0 # Value Area Low
volume_at_price: dict[float, float] = field(default_factory=dict)
total_volume: float = 0.0
lvn_levels: list[float] = field(default_factory=list)
shape: str = "unknown" # p_shape, b_shape, d_shape, double_dist
poc_position_pct: float = 0.5 # POC position within range (0=bottom, 1=top)
@property
def value_area_range(self) -> float:
return self.vah - self.val
# ──────────────────────────────────────────────
# Signals
# ──────────────────────────────────────────────
@dataclass
class Signal:
"""Output from a pattern detector."""
timestamp_ms: int
signal_type: SignalType
direction: Side # Suggested direction
price_level: float # Key price
strength: float = 0.0 # 0-100 confidence score
details: dict = field(default_factory=dict)
@property
def is_bullish(self) -> bool:
return self.direction == Side.BUY
def __repr__(self) -> str:
dir_str = "LONG" if self.is_bullish else "SHORT"
return (
f"Signal({self.signal_type.value} {dir_str} "
f"@ {self.price_level:.2f}, strength={self.strength:.0f})"
)
# ──────────────────────────────────────────────
# Trade State (State Machine)
# ──────────────────────────────────────────────
@dataclass
class TradeState:
"""
Tracks the state machine for a single trade idea.
Qualified Level → Absorption → Position → Break-Even → Trail → Closed
"""
instrument: str
direction: Side
phase: TradePhase = TradePhase.WATCHING
qualified_level: float = 0.0 # Level from profile framing
entry_price: float = 0.0
stop_loss: float = 0.0
take_profit: float = 0.0
break_even_price: float = 0.0
trail_stop: float = 0.0
absorption_signals: list[Signal] = field(default_factory=list)
initiative_signals: list[Signal] = field(default_factory=list)
entry_time_ms: int = 0
rr_ratio: float = 0.0
pnl_ticks: float = 0.0
notes: str = ""
def advance_to_absorption(self, signal: Signal):
"""Absorption detected at qualified level — entry signal."""
self.phase = TradePhase.ABSORPTION_DETECTED
self.absorption_signals.append(signal)
def advance_to_position(self, entry_price: float, stop_loss: float, take_profit: float):
"""Trade entered."""
self.phase = TradePhase.POSITION_OPEN
self.entry_price = entry_price
self.stop_loss = stop_loss
self.take_profit = take_profit
self.break_even_price = entry_price
self.entry_time_ms = int(time.time() * 1000)
risk = abs(entry_price - stop_loss)
if risk > 0:
self.rr_ratio = abs(take_profit - entry_price) / risk
def advance_to_break_even(self, signal: Signal):
"""Initiative auction confirmed — move SL to break even."""
self.phase = TradePhase.BREAK_EVEN
self.stop_loss = self.break_even_price
self.trail_stop = self.break_even_price
self.initiative_signals.append(signal)
def update_trail(self, new_trail_level: float, signal: Signal):
"""New initiative print — trail stop to the candle's extreme."""
self.phase = TradePhase.TRAILING
if self.direction == Side.BUY:
self.trail_stop = max(self.trail_stop, new_trail_level)
else:
self.trail_stop = min(self.trail_stop, new_trail_level)
self.stop_loss = self.trail_stop
self.initiative_signals.append(signal)
def close_trade(self, exit_price: float, reason: str = ""):
"""Trade finished."""
self.phase = TradePhase.CLOSED
if self.direction == Side.BUY:
self.pnl_ticks = exit_price - self.entry_price
else:
self.pnl_ticks = self.entry_price - exit_price
self.notes = reason