Files
OrderFlow-Analysis-Pro/orderflow_system/data/candle_builder.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

134 lines
4.5 KiB
Python

"""
Candle Builder — aggregates ticks into OHLCV candles with footprint data.
Builds candles in real-time from the tick stream.
"""
from __future__ import annotations
import time
from typing import Callable, Optional
from orderflow_system.data.models import Tick, Candle, FootprintLevel, Side
class CandleBuilder:
"""
Builds time-based candles from a tick stream.
Each candle includes full footprint data (bid/ask volume at each price level).
"""
def __init__(
self,
interval_seconds: int = 60,
tick_size: float = 0.1,
on_candle_close: Optional[Callable] = None,
):
self.interval_ms = interval_seconds * 1000
self.tick_size = tick_size
self.on_candle_close = on_candle_close
self._current_candle: Optional[Candle] = None
self._candle_history: list[Candle] = []
self._max_history = 5000
def _round_price(self, price: float) -> float:
"""Round price to tick size for footprint grouping."""
return round(round(price / self.tick_size) * self.tick_size, 10)
def _candle_start_ms(self, timestamp_ms: int) -> int:
"""Align timestamp to candle interval boundary."""
return (timestamp_ms // self.interval_ms) * self.interval_ms
async def process_tick(self, tick: Tick) -> Optional[Candle]:
"""
Feed a tick into the builder. Returns a closed candle if interval completed.
"""
candle_start = self._candle_start_ms(tick.timestamp_ms)
closed_candle = None
# Check if we need to close current candle and start new one
if self._current_candle is not None:
if candle_start > self._current_candle.timestamp_ms:
closed_candle = self._current_candle
# Deduplicate: replace last entry if same timestamp
if (self._candle_history
and self._candle_history[-1].timestamp_ms
== closed_candle.timestamp_ms):
self._candle_history[-1] = closed_candle
else:
self._candle_history.append(closed_candle)
if len(self._candle_history) > self._max_history:
self._candle_history = self._candle_history[-self._max_history:]
if self.on_candle_close:
await self.on_candle_close(closed_candle)
self._current_candle = None
# Start new candle if needed
if self._current_candle is None:
self._current_candle = Candle(
timestamp_ms=candle_start,
open=tick.price,
high=tick.price,
low=tick.price,
close=tick.price,
)
# Update OHLCV
c = self._current_candle
c.high = max(c.high, tick.price)
c.low = min(c.low, tick.price)
c.close = tick.price
c.volume += tick.size
c.tick_count += 1
if tick.is_buy:
c.buy_volume += tick.size
else:
c.sell_volume += tick.size
# Update footprint at this price level
fp_price = self._round_price(tick.price)
if fp_price not in c.footprint:
c.footprint[fp_price] = FootprintLevel(price=fp_price)
if tick.is_buy:
c.footprint[fp_price].ask_volume += tick.size
else:
c.footprint[fp_price].bid_volume += tick.size
return closed_candle
@property
def current_candle(self) -> Optional[Candle]:
return self._current_candle
@property
def history(self) -> list[Candle]:
return self._candle_history
def get_recent_candles(self, n: int) -> list[Candle]:
"""Return last N closed candles."""
return self._candle_history[-n:]
def load_historical_candles(self, candles: list[Candle]) -> int:
"""
Bulk-load historical candles (e.g. from MT5 bars).
Prepends them before any real-time candles, deduplicating by timestamp.
Returns the number of candles actually added.
"""
if not candles:
return 0
# Existing timestamps for dedup
existing_ts = {c.timestamp_ms for c in self._candle_history}
new_candles = [c for c in candles if c.timestamp_ms not in existing_ts]
if not new_candles:
return 0
# Merge: historical first, then real-time, sorted by time
merged = sorted(new_candles + self._candle_history, key=lambda c: c.timestamp_ms)
self._candle_history = merged[-self._max_history:]
return len(new_candles)