feat: tick split, imbalance dots (desativado), session start detection

- Tick split via while-loop no process_tick: clusters fecham em +-800 delta sem overshoot
- Session start detection: busca maior gap em 3000 bars M1 para detectar abertura de sessao
- Imbalance dots: codigo presente mas desativado (6 abordagens testadas, todas geraram dots em todos os levels com dados CFD polling)
- ask_events/bid_events tracking por level (para futura implementacao de imbalance)
- Bottom panel redesign: volume bars + delta blocks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
rufinomec-afk
2026-06-07 16:05:50 -03:00
co-authored by Claude Sonnet 4.6
parent 54f269b0ce
commit 3f48ef27ed
+63 -36
View File
@@ -1,8 +1,11 @@
import uuid import uuid
import time import time
import logging
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from config import settings from config import settings
logger = logging.getLogger("aggregator")
class FootprintCluster: class FootprintCluster:
def __init__(self, tick_size: float = 1.0): def __init__(self, tick_size: float = 1.0):
self.tick_size = tick_size self.tick_size = tick_size
@@ -48,12 +51,14 @@ class FootprintCluster:
rounded_price = round(price / self.tick_size) * self.tick_size rounded_price = round(price / self.tick_size) * self.tick_size
if rounded_price not in self.levels: if rounded_price not in self.levels:
self.levels[rounded_price] = {"ask": 0.0, "bid": 0.0} self.levels[rounded_price] = {"ask": 0.0, "bid": 0.0, "ask_events": 0, "bid_events": 0}
if is_buy: if is_buy:
self.levels[rounded_price]["ask"] += volume self.levels[rounded_price]["ask"] += volume
self.levels[rounded_price]["ask_events"] += 1
else: else:
self.levels[rounded_price]["bid"] += volume self.levels[rounded_price]["bid"] += volume
self.levels[rounded_price]["bid_events"] += 1
# Update High/Low # Update High/Low
if self.high is None or rounded_price > self.high: if self.high is None or rounded_price > self.high:
@@ -104,29 +109,12 @@ class FootprintCluster:
imbalances_buy = {} imbalances_buy = {}
imbalances_sell = {} imbalances_sell = {}
# IMBALANCE DESATIVADO TEMPORARIAMENTE
# Todas as abordagens testadas geraram dots em todos os levels.
# Ver project_imbalance_research.md para histórico completo das tentativas.
# Reativar quando encontrar a abordagem correta.
for price in sorted_prices: for price in sorted_prices:
ask_val = self.levels[price]["ask"]
bid_val = self.levels[price]["bid"]
# Lower level price
lower_price = round((price - self.tick_size) / self.tick_size) * self.tick_size
bid_below = self.levels[lower_price]["bid"] if lower_price in self.levels else 0.0
# Upper level price
upper_price = round((price + self.tick_size) / self.tick_size) * self.tick_size
ask_above = self.levels[upper_price]["ask"] if upper_price in self.levels else 0.0
# Buy Imbalance (diagonal): ask vs bid_below
# Avoid triggering on 0 vs 0
if ask_val > 0 and ask_val >= R * bid_below:
imbalances_buy[price] = True
else:
imbalances_buy[price] = False imbalances_buy[price] = False
# Sell Imbalance (diagonal): bid vs ask_above
if bid_val > 0 and bid_val >= R * ask_above:
imbalances_sell[price] = True
else:
imbalances_sell[price] = False imbalances_sell[price] = False
# Detect stacked imbalances: 3+ consecutive levels with imbalance in the same direction # Detect stacked imbalances: 3+ consecutive levels with imbalance in the same direction
@@ -339,32 +327,71 @@ class Aggregator:
self.active_cluster = FootprintCluster(tick_size=self.tick_size) self.active_cluster = FootprintCluster(tick_size=self.tick_size)
self.history: List[Dict[str, Any]] = [] self.history: List[Dict[str, Any]] = []
def _close_active(self, reason: str) -> Dict[str, Any]:
self.active_cluster.close(reason)
closed = self.active_cluster.to_json()
from datetime import datetime
ts = closed.get('close_time') or closed.get('open_time') or 0
dt = datetime.fromtimestamp(ts / 1000.0).strftime("%H:%M:%S") if ts else "?"
logger.info(f"CLUSTER CLOSED [{dt}] vol={closed['total_volume']:.0f} delta={closed['total_delta']:.0f} reason={reason}")
self.history.append(closed)
if len(self.history) > settings.HISTORY_BUFFER_SIZE:
self.history.pop(0)
self.active_cluster = FootprintCluster(tick_size=self.tick_size)
return closed
def process_tick(self, price: float, volume: float, is_buy: bool, timestamp_msc: int) -> tuple[Dict[str, Any], Optional[Dict[str, Any]]]: def process_tick(self, price: float, volume: float, is_buy: bool, timestamp_msc: int) -> tuple[Dict[str, Any], Optional[Dict[str, Any]]]:
""" """
Process a single tick. Process a single tick.
In delta mode, splits ticks (via while loop) so no cluster ever overshoots ±CLUSTER_DELTA_MAX.
Returns: Returns:
(active_cluster_json, closed_cluster_json_if_just_closed) (active_cluster_json, closed_cluster_json_if_just_closed)
""" """
last_closed = None
if settings.CLUSTER_CLOSE_MODE != "delta":
self.active_cluster.add_tick(price, volume, is_buy, timestamp_msc) self.active_cluster.add_tick(price, volume, is_buy, timestamp_msc)
# Check if the active cluster should be closed
close_reason = self.active_cluster.should_close(timestamp_msc) close_reason = self.active_cluster.should_close(timestamp_msc)
closed_json = None
if close_reason: if close_reason:
self.active_cluster.close(close_reason) last_closed = self._close_active(close_reason)
closed_json = self.active_cluster.to_json() return self.active_cluster.to_json(), last_closed
# Save to history remaining = volume
self.history.append(closed_json)
if len(self.history) > settings.HISTORY_BUFFER_SIZE:
self.history.pop(0)
# Start new cluster while remaining > 0:
self.active_cluster = FootprintCluster(tick_size=self.tick_size) current_delta = self.active_cluster.total_delta
# The next tick will set the open_time of the new cluster.
return self.active_cluster.to_json(), closed_json # If cluster is already at/beyond threshold (from a previous leftover), close it first
if self.active_cluster.total_ticks > 0 and abs(current_delta) >= settings.CLUSTER_DELTA_MAX:
last_closed = self._close_active("delta")
continue
contribution = remaining if is_buy else -remaining
projected = current_delta + contribution
if abs(projected) <= settings.CLUSTER_DELTA_MAX:
# Remaining fits — add and check for any close condition
self.active_cluster.add_tick(price, remaining, is_buy, timestamp_msc)
close_reason = self.active_cluster.should_close(timestamp_msc)
if close_reason:
last_closed = self._close_active(close_reason)
break
# Split: how much fits before hitting the limit
if is_buy:
capacity = settings.CLUSTER_DELTA_MAX - current_delta # always > 0 here
else:
capacity = current_delta + settings.CLUSTER_DELTA_MAX # always > 0 here
capacity = max(capacity, 0.0)
if capacity > 0:
self.active_cluster.add_tick(price, capacity, is_buy, timestamp_msc)
last_closed = self._close_active("delta")
remaining -= capacity
return self.active_cluster.to_json(), last_closed
def classify_tick(last: float, bid: float, ask: float, flags: int) -> bool: def classify_tick(last: float, bid: float, ask: float, flags: int) -> bool: