feat(notify): mesaje alertă și comentarii business în română

Toate alertele Discord/Telegram traduse: armat, pregătit, recuperare,
semnal, activ, niveluri, pornit/oprit. Comentariile de business-logic
din main.py traduse în română.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-16 23:09:20 +00:00
parent 51e98ae3d3
commit eca2b39e64
3 changed files with 43 additions and 45 deletions

View File

@@ -404,8 +404,8 @@ def _handle_tick(
""" """
snap: Snapshot = snapshot or (lambda _k, _l: None) snap: Snapshot = snapshot or (lambda _k, _l: None)
# Late start: the very first accepted color is already at FIRE phase. # Pornire târzie: prima culoare acceptată e deja în faza FIRE.
# User came online after the trade signal fired — warn and skip FSM feed. # Utilizatorul a venit online după ce semnalul s-a declanșat — avertizare fără feed FSM.
if first_accepted and color in ("light_green", "light_red") and fsm.state == State.IDLE: if first_accepted and color in ("light_green", "light_red") and fsm.state == State.IDLE:
direction = "BUY" if color == "light_green" else "SELL" direction = "BUY" if color == "light_green" else "SELL"
audit.log({ audit.log({
@@ -415,24 +415,23 @@ def _handle_tick(
}) })
notifier.send(Alert( notifier.send(Alert(
kind="late_start", kind="late_start",
title=f"ATM started late{direction} already fired", title=f"ATM pornit târziu{direction} deja declanșat",
body=f"Observed {color} at startup. Check chart manually.", body=f"Detectat {color} la pornire. Verifică graficul manual.",
image_path=snap("late_start", f"late_start_{direction.lower()}"), image_path=snap("late_start", f"late_start_{direction.lower()}"),
direction=direction, direction=direction,
)) ))
return None return None
# Catchup synth-arm: first accepted color is already at PRIME phase. # Recuperare synth-arm: prima culoare acceptată e deja în faza PRIME.
# Drive FSM through a synthetic arm so the real PRIME transition fires a # Forțează FSM printr-un arm sintetic ca tranziția reală PRIME să emită
# normal prime alert below. Audit entry is tagged catchup:true. # alertă normală mai jos. Intrarea audit e marcată catchup:true.
# #
# Guard against post-FIRE residual dark_* dots: after a light_green fires # Gardă contra punctelor dark_* reziduale post-FIRE: după ce light_green
# the cycle, FSM returns to IDLE but dark_green dots continue for the rest # declanșează ciclul, FSM revine la IDLE dar punctele dark_green continuă
# of the 15m window. Those are tail noise, NOT a new prime signal. The # restul ferestrei de 15m. Sunt zgomot, NU un semnal nou de prime.
# direction-scoped fired_in_session() check suppresses synth-arm in that # Verificarea fired_in_session(direction) suprimă synth-arm în acest caz —
# case — the tick falls through to the normal _from_idle feed below which # tick-ul trece la feed-ul normal _from_idle care îl clasifică drept zgomot.
# classifies it as noise. (A genuine new cycle always comes with fresh # (Un ciclu nou autentic vine întotdeauna cu turquoise/yellow proaspăt.)
# turquoise/yellow first and drives _from_idle → ARMED normally.)
catchup = False catchup = False
if color in ("dark_green", "dark_red") and fsm.state == State.IDLE: if color in ("dark_green", "dark_red") and fsm.state == State.IDLE:
direction = "BUY" if color == "dark_green" else "SELL" direction = "BUY" if color == "dark_green" else "SELL"
@@ -450,8 +449,8 @@ def _handle_tick(
}) })
notifier.send(Alert( notifier.send(Alert(
kind="arm", kind="arm",
title=f"{direction} armed ({arm_color}) — catchup", title=f"{direction} armat ({arm_color}) — recuperare",
body=f"catchup — session already armed at startup " body=f"recuperare — sesiunea era deja armată la pornire "
f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}", f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}",
image_path=snap("catchup", f"catchup_arm_{direction.lower()}"), image_path=snap("catchup", f"catchup_arm_{direction.lower()}"),
direction=direction, direction=direction,
@@ -471,25 +470,25 @@ def _handle_tick(
tick_event["catchup"] = True tick_event["catchup"] = True
audit.log(tick_event) audit.log(tick_event)
# ARM: turquoise (BUY) / yellow (SELL) — only on fresh IDLE→ARMED # ARM: turquoise (BUY) / yellow (SELL) — doar la tranziție nouă IDLE→ARMED
if tr.reason == "arm": if tr.reason == "arm":
direction = "BUY" if tr.next == State.ARMED_BUY else "SELL" direction = "BUY" if tr.next == State.ARMED_BUY else "SELL"
notifier.send(Alert( notifier.send(Alert(
kind="arm", kind="arm",
title=f"{direction} armed ({color})", title=f"{direction} armat ({color})",
body=f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}", body=f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}",
image_path=snap("arm", f"arm_{direction.lower()}"), image_path=snap("arm", f"arm_{direction.lower()}"),
direction=direction, direction=direction,
)) ))
# PRIME: dark_green (BUY) / dark_red (SELL) — only on ARMED→PRIMED # PRIME: dark_green (BUY) / dark_red (SELL) — doar la ARMED→PRIMED
elif tr.reason == "prime": elif tr.reason == "prime":
direction = "BUY" if tr.next == State.PRIMED_BUY else "SELL" direction = "BUY" if tr.next == State.PRIMED_BUY else "SELL"
suffix = "catchup" if catchup else "" suffix = "recuperare" if catchup else ""
prime_kind = "catchup" if catchup else "prime" prime_kind = "catchup" if catchup else "prime"
prime_label = f"prime_{direction.lower()}_catchup" if catchup else f"prime_{direction.lower()}" prime_label = f"prime_{direction.lower()}_catchup" if catchup else f"prime_{direction.lower()}"
notifier.send(Alert( notifier.send(Alert(
kind="prime", kind="prime",
title=f"{direction} primed ({color}){suffix}", title=f"{direction} pregătit ({color}){suffix}",
body=f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}", body=f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}",
image_path=snap(prime_kind, prime_label), image_path=snap(prime_kind, prime_label),
direction=direction, direction=direction,
@@ -522,7 +521,7 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
] ]
def _on_drop(backend_name: str, dropped: Alert) -> None: def _on_drop(backend_name: str, dropped: Alert) -> None:
"""Audit queue-overflow drops so the silent failure becomes loud.""" """Audit la depășire coadă — face eșecul silențios vizibil."""
audit.log({ audit.log({
"ts": time.time(), "ts": time.time(),
"event": "queue_overflow_drop", "event": "queue_overflow_drop",
@@ -533,7 +532,7 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
notifier = FanoutNotifier(backends, Path(cfg.dead_letter_path), on_drop=_on_drop) notifier = FanoutNotifier(backends, Path(cfg.dead_letter_path), on_drop=_on_drop)
# Sanity check: capture one frame, confirm canary matches calibration. # Verificare inițială: captură un frame, confirmă că canary se potrivește cu calibrarea.
first_frame = capture() first_frame = capture()
if first_frame is None: if first_frame is None:
print("WARN: first capture returned None — window/region missing", flush=True) print("WARN: first capture returned None — window/region missing", flush=True)
@@ -548,7 +547,7 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
dur_note = f" dur=∞" if duration_s is None else f" dur={duration_s/3600:.2f}h" dur_note = f" dur=∞" if duration_s is None else f" dur={duration_s/3600:.2f}h"
notifier.send(Alert( notifier.send(Alert(
kind="heartbeat", kind="heartbeat",
title="ATM started", title="ATM pornit",
body=( body=(
f"cfg={cfg.config_version}{dur_note} @ {datetime.now().isoformat(timespec='seconds')}\n" f"cfg={cfg.config_version}{dur_note} @ {datetime.now().isoformat(timespec='seconds')}\n"
f"canary: {canary_status}" f"canary: {canary_status}"
@@ -612,10 +611,10 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
snapshot=_snapshot, snapshot=_snapshot,
) )
if tr is None: if tr is None:
# late_start short-circuit: FSM untouched, skip FIRE + corpus save # pornire târzie: FSM neatins, sari peste FIRE + salvare corpus
time.sleep(cfg.loop_interval_s) time.sleep(cfg.loop_interval_s)
continue continue
# corpus: save full frame on each new distinct color for later labeling # corpus: salvează frame complet la fiecare culoare nouă distinctă, pt etichetare ulterioară
if res.color != last_saved_color: if res.color != last_saved_color:
ts_str = datetime.fromtimestamp(now).strftime("%Y%m%d_%H%M%S") ts_str = datetime.fromtimestamp(now).strftime("%Y%m%d_%H%M%S")
sample_path = samples_dir / f"{ts_str}_{res.color}.png" sample_path = samples_dir / f"{ts_str}_{res.color}.png"
@@ -624,7 +623,7 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
except Exception as exc: except Exception as exc:
audit.log({"ts": now, "event": "sample_save_failed", "error": str(exc)}) audit.log({"ts": now, "event": "sample_save_failed", "error": str(exc)})
last_saved_color = res.color last_saved_color = res.color
# FIRE: annotate frame + save, attach to alert # FIRE: adnotează frame-ul + salvează, atașează la alertă
if tr.trigger and not tr.locked: if tr.trigger and not tr.locked:
fire_path: "Path | None" = None fire_path: "Path | None" = None
if cfg.attach_screenshots.trigger: if cfg.attach_screenshots.trigger:
@@ -633,7 +632,7 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
) )
notifier.send(Alert( notifier.send(Alert(
kind="trigger", kind="trigger",
title=f"{tr.trigger} signal", title=f"Semnal {tr.trigger}",
body=f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}", body=f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}",
image_path=fire_path, image_path=fire_path,
direction=tr.trigger, direction=tr.trigger,
@@ -646,7 +645,7 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
if lr.status == "complete" and lr.levels: if lr.status == "complete" and lr.levels:
notifier.send(Alert( notifier.send(Alert(
kind="levels", kind="levels",
title="Levels", title="Niveluri",
body=( body=(
f"SL={lr.levels.sl} " f"SL={lr.levels.sl} "
f"TP1={lr.levels.tp1} " f"TP1={lr.levels.tp1} "
@@ -654,8 +653,8 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
), ),
)) ))
levels_extractor = None levels_extractor = None
# heartbeat — include per-backend dispatch stats so silent failures # heartbeat — include statistici per-backend ca eșecurile silențioase
# surface every 30 min without waiting for shutdown. # să apară la fiecare 30 min fără să aștepte oprirea.
if time.time() > heartbeat_due: if time.time() > heartbeat_due:
try: try:
stats = notifier.stats() stats = notifier.stats()
@@ -666,22 +665,22 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
f"{name}: sent={s['sent']} failed={s['failed']} " f"{name}: sent={s['sent']} failed={s['failed']} "
f"dropped={s['dropped']} retries={s['retries']}" f"dropped={s['dropped']} retries={s['retries']}"
) )
notifier.send(Alert(kind="heartbeat", title="alive", body="\n".join(body_lines))) notifier.send(Alert(kind="heartbeat", title="activ", body="\n".join(body_lines)))
except Exception: except Exception:
notifier.send(Alert(kind="heartbeat", title="alive", body="confidence ok")) notifier.send(Alert(kind="heartbeat", title="activ", body="încredere ok"))
heartbeat_due = time.time() + cfg.heartbeat_min * 60 heartbeat_due = time.time() + cfg.heartbeat_min * 60
time.sleep(cfg.loop_interval_s) time.sleep(cfg.loop_interval_s)
finally: finally:
try: try:
stats = notifier.stats() stats = notifier.stats()
lines = [f"after {time.monotonic() - start:.0f}s"] lines = [f"după {time.monotonic() - start:.0f}s"]
for name, s in stats.items(): for name, s in stats.items():
lines.append( lines.append(
f"{name}: sent={s['sent']} failed={s['failed']} " f"{name}: sent={s['sent']} failed={s['failed']} "
f"dropped={s['dropped']} retries={s['retries']}" f"dropped={s['dropped']} retries={s['retries']}"
) )
notifier.send(Alert( notifier.send(Alert(
kind="heartbeat", title="ATM stopped", kind="heartbeat", title="ATM oprit",
body="\n".join(lines), body="\n".join(lines),
)) ))
except Exception: except Exception:

View File

@@ -92,8 +92,7 @@ def test_handle_tick_prime_buy():
assert notif.alerts[0].kind == "arm" assert notif.alerts[0].kind == "arm"
assert notif.alerts[1].kind == "prime" assert notif.alerts[1].kind == "prime"
assert notif.alerts[1].direction == "BUY" assert notif.alerts[1].direction == "BUY"
# Non-catchup prime alert must not mention catchup assert "recuperare" not in notif.alerts[1].title.lower()
assert "catchup" not in notif.alerts[1].title.lower()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -133,10 +132,10 @@ def test_handle_tick_catchup_dark_green():
assert len(notif.alerts) == 2 assert len(notif.alerts) == 2
assert notif.alerts[0].kind == "arm" assert notif.alerts[0].kind == "arm"
assert notif.alerts[0].direction == "BUY" assert notif.alerts[0].direction == "BUY"
assert "catchup" in notif.alerts[0].title.lower() or "catchup" in notif.alerts[0].body.lower() assert "recuperare" in notif.alerts[0].title.lower() or "recuperare" in notif.alerts[0].body.lower()
assert notif.alerts[1].kind == "prime" assert notif.alerts[1].kind == "prime"
assert notif.alerts[1].direction == "BUY" assert notif.alerts[1].direction == "BUY"
assert "catchup" in notif.alerts[1].title.lower() or "catchup" in notif.alerts[1].body.lower() assert "recuperare" in notif.alerts[1].title.lower() or "recuperare" in notif.alerts[1].body.lower()
# Audit: both tick entries tagged catchup:true # Audit: both tick entries tagged catchup:true
catchup_events = [e for e in audit.events if e.get("catchup")] catchup_events = [e for e in audit.events if e.get("catchup")]
@@ -216,8 +215,8 @@ def test_handle_tick_first_turquoise_no_catchup_label():
assert len(notif.alerts) == 1 assert len(notif.alerts) == 1
assert notif.alerts[0].kind == "arm" assert notif.alerts[0].kind == "arm"
# Decision 3A: cannot distinguish fresh arm from catchup-at-arm-phase # Decision 3A: cannot distinguish fresh arm from catchup-at-arm-phase
assert "catchup" not in notif.alerts[0].title.lower() assert "recuperare" not in notif.alerts[0].title.lower()
assert "catchup" not in notif.alerts[0].body.lower() assert "recuperare" not in notif.alerts[0].body.lower()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -238,7 +237,7 @@ def test_handle_tick_catchup_dark_red_when_not_first_accepted():
assert len(notif.alerts) == 2 assert len(notif.alerts) == 2
assert notif.alerts[0].kind == "arm" assert notif.alerts[0].kind == "arm"
assert notif.alerts[0].direction == "SELL" assert notif.alerts[0].direction == "SELL"
assert "catchup" in (notif.alerts[0].title + notif.alerts[0].body).lower() assert "recuperare" in (notif.alerts[0].title + notif.alerts[0].body).lower()
assert notif.alerts[1].kind == "prime" assert notif.alerts[1].kind == "prime"
assert notif.alerts[1].direction == "SELL" assert notif.alerts[1].direction == "SELL"
@@ -341,7 +340,7 @@ def test_handle_tick_opposite_direction_catchup_after_fire():
assert len(notif.alerts) == baseline_alerts + 2 assert len(notif.alerts) == baseline_alerts + 2
assert notif.alerts[baseline_alerts].kind == "arm" assert notif.alerts[baseline_alerts].kind == "arm"
assert notif.alerts[baseline_alerts].direction == "SELL" assert notif.alerts[baseline_alerts].direction == "SELL"
assert "catchup" in (notif.alerts[baseline_alerts].title + notif.alerts[baseline_alerts].body).lower() assert "recuperare" in (notif.alerts[baseline_alerts].title + notif.alerts[baseline_alerts].body).lower()
assert notif.alerts[baseline_alerts + 1].kind == "prime" assert notif.alerts[baseline_alerts + 1].kind == "prime"
assert notif.alerts[baseline_alerts + 1].direction == "SELL" assert notif.alerts[baseline_alerts + 1].direction == "SELL"

View File

@@ -247,11 +247,11 @@ def test_run_live_catchup_sell_from_gray_then_dark_red(monkeypatch, tmp_path):
assert len(arm) == 1, f"expected 1 arm alert, got {len(arm)} ({[a.title for a in captured]})" assert len(arm) == 1, f"expected 1 arm alert, got {len(arm)} ({[a.title for a in captured]})"
assert arm[0].direction == "SELL" assert arm[0].direction == "SELL"
assert "catchup" in (arm[0].title + arm[0].body).lower() assert "recuperare" in (arm[0].title + arm[0].body).lower()
assert len(prime) == 1 assert len(prime) == 1
assert prime[0].direction == "SELL" assert prime[0].direction == "SELL"
assert "catchup" in (prime[0].title + prime[0].body).lower() assert "recuperare" in (prime[0].title + prime[0].body).lower()
assert len(trigger) == 1 assert len(trigger) == 1
assert trigger[0].direction == "SELL" assert trigger[0].direction == "SELL"