55 lines
2 KiB
Python
55 lines
2 KiB
Python
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
|