from dataclasses import dataclass from app.analysis.confluence import Cluster @dataclass(slots=True) class Alert: cluster: Cluster message: str class AlertEngine: def __init__(self, min_score: float, cooldown_seconds: int = 900): self.min_score = min_score self.cooldown_seconds = cooldown_seconds self._fired_at: dict[str, int] = {} def evaluate( self, clusters: list[Cluster], current_price: float, atr15: float, now: int, symbol: str, ) -> list[Alert]: tolerance = 0.5 * atr15 if tolerance <= 0: return [] alerts: list[Alert] = [] active_ids = {cluster.id for cluster in clusters} for cluster_id, fired_at in list(self._fired_at.items()): cluster = next((item for item in clusters if item.id == cluster_id), None) separated = cluster is None or abs(cluster.center - current_price) > 2 * tolerance if separated and now - fired_at >= self.cooldown_seconds: del self._fired_at[cluster_id] elif cluster_id not in active_ids and now - fired_at >= self.cooldown_seconds: del self._fired_at[cluster_id] for cluster in clusters: if ( cluster.score < self.min_score or abs(cluster.center - current_price) > tolerance or cluster.id in self._fired_at ): continue self._fired_at[cluster.id] = now direction = "BEARISH" if cluster.side.value == "resistance" else "BULLISH" timeframes = ", ".join(dict.fromkeys(member.tf.value for member in cluster.members)) message = ( f"{direction} ZONE {symbol} {current_price:.2f}\n" f"{cluster.side.value.title()} confluence {cluster.score:g} " f"@ {cluster.low:.2f}-{cluster.high:.2f}\n{timeframes}" ) alerts.append(Alert(cluster, message)) return alerts