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:
@@ -404,8 +404,8 @@ def _handle_tick(
|
||||
"""
|
||||
snap: Snapshot = snapshot or (lambda _k, _l: None)
|
||||
|
||||
# Late start: the very first accepted color is already at FIRE phase.
|
||||
# User came online after the trade signal fired — warn and skip FSM feed.
|
||||
# Pornire târzie: prima culoare acceptată e deja în faza FIRE.
|
||||
# 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:
|
||||
direction = "BUY" if color == "light_green" else "SELL"
|
||||
audit.log({
|
||||
@@ -415,24 +415,23 @@ def _handle_tick(
|
||||
})
|
||||
notifier.send(Alert(
|
||||
kind="late_start",
|
||||
title=f"ATM started late — {direction} already fired",
|
||||
body=f"Observed {color} at startup. Check chart manually.",
|
||||
title=f"ATM pornit târziu — {direction} deja declanșat",
|
||||
body=f"Detectat {color} la pornire. Verifică graficul manual.",
|
||||
image_path=snap("late_start", f"late_start_{direction.lower()}"),
|
||||
direction=direction,
|
||||
))
|
||||
return None
|
||||
|
||||
# Catchup synth-arm: first accepted color is already at PRIME phase.
|
||||
# Drive FSM through a synthetic arm so the real PRIME transition fires a
|
||||
# normal prime alert below. Audit entry is tagged catchup:true.
|
||||
# Recuperare synth-arm: prima culoare acceptată e deja în faza PRIME.
|
||||
# Forțează FSM printr-un arm sintetic ca tranziția reală PRIME să emită
|
||||
# alertă normală mai jos. Intrarea audit e marcată catchup:true.
|
||||
#
|
||||
# Guard against post-FIRE residual dark_* dots: after a light_green fires
|
||||
# the cycle, FSM returns to IDLE but dark_green dots continue for the rest
|
||||
# of the 15m window. Those are tail noise, NOT a new prime signal. The
|
||||
# direction-scoped fired_in_session() check suppresses synth-arm in that
|
||||
# case — the tick falls through to the normal _from_idle feed below which
|
||||
# classifies it as noise. (A genuine new cycle always comes with fresh
|
||||
# turquoise/yellow first and drives _from_idle → ARMED normally.)
|
||||
# Gardă contra punctelor dark_* reziduale post-FIRE: după ce light_green
|
||||
# declanșează ciclul, FSM revine la IDLE dar punctele dark_green continuă
|
||||
# restul ferestrei de 15m. Sunt zgomot, NU un semnal nou de prime.
|
||||
# Verificarea fired_in_session(direction) suprimă synth-arm în acest caz —
|
||||
# tick-ul trece la feed-ul normal _from_idle care îl clasifică drept zgomot.
|
||||
# (Un ciclu nou autentic vine întotdeauna cu turquoise/yellow proaspăt.)
|
||||
catchup = False
|
||||
if color in ("dark_green", "dark_red") and fsm.state == State.IDLE:
|
||||
direction = "BUY" if color == "dark_green" else "SELL"
|
||||
@@ -450,8 +449,8 @@ def _handle_tick(
|
||||
})
|
||||
notifier.send(Alert(
|
||||
kind="arm",
|
||||
title=f"{direction} armed ({arm_color}) — catchup",
|
||||
body=f"catchup — session already armed at startup "
|
||||
title=f"{direction} armat ({arm_color}) — recuperare",
|
||||
body=f"recuperare — sesiunea era deja armată la pornire "
|
||||
f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}",
|
||||
image_path=snap("catchup", f"catchup_arm_{direction.lower()}"),
|
||||
direction=direction,
|
||||
@@ -471,25 +470,25 @@ def _handle_tick(
|
||||
tick_event["catchup"] = True
|
||||
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":
|
||||
direction = "BUY" if tr.next == State.ARMED_BUY else "SELL"
|
||||
notifier.send(Alert(
|
||||
kind="arm",
|
||||
title=f"{direction} armed ({color})",
|
||||
title=f"{direction} armat ({color})",
|
||||
body=f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}",
|
||||
image_path=snap("arm", f"arm_{direction.lower()}"),
|
||||
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":
|
||||
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_label = f"prime_{direction.lower()}_catchup" if catchup else f"prime_{direction.lower()}"
|
||||
notifier.send(Alert(
|
||||
kind="prime",
|
||||
title=f"{direction} primed ({color}){suffix}",
|
||||
title=f"{direction} pregătit ({color}){suffix}",
|
||||
body=f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}",
|
||||
image_path=snap(prime_kind, prime_label),
|
||||
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:
|
||||
"""Audit queue-overflow drops so the silent failure becomes loud."""
|
||||
"""Audit la depășire coadă — face eșecul silențios vizibil."""
|
||||
audit.log({
|
||||
"ts": time.time(),
|
||||
"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)
|
||||
|
||||
# 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()
|
||||
if first_frame is None:
|
||||
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"
|
||||
notifier.send(Alert(
|
||||
kind="heartbeat",
|
||||
title="ATM started",
|
||||
title="ATM pornit",
|
||||
body=(
|
||||
f"cfg={cfg.config_version}{dur_note} @ {datetime.now().isoformat(timespec='seconds')}\n"
|
||||
f"canary: {canary_status}"
|
||||
@@ -612,10 +611,10 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
|
||||
snapshot=_snapshot,
|
||||
)
|
||||
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)
|
||||
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:
|
||||
ts_str = datetime.fromtimestamp(now).strftime("%Y%m%d_%H%M%S")
|
||||
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:
|
||||
audit.log({"ts": now, "event": "sample_save_failed", "error": str(exc)})
|
||||
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:
|
||||
fire_path: "Path | None" = None
|
||||
if cfg.attach_screenshots.trigger:
|
||||
@@ -633,7 +632,7 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
|
||||
)
|
||||
notifier.send(Alert(
|
||||
kind="trigger",
|
||||
title=f"{tr.trigger} signal",
|
||||
title=f"Semnal {tr.trigger}",
|
||||
body=f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}",
|
||||
image_path=fire_path,
|
||||
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:
|
||||
notifier.send(Alert(
|
||||
kind="levels",
|
||||
title="Levels",
|
||||
title="Niveluri",
|
||||
body=(
|
||||
f"SL={lr.levels.sl} "
|
||||
f"TP1={lr.levels.tp1} "
|
||||
@@ -654,8 +653,8 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
|
||||
),
|
||||
))
|
||||
levels_extractor = None
|
||||
# heartbeat — include per-backend dispatch stats so silent failures
|
||||
# surface every 30 min without waiting for shutdown.
|
||||
# heartbeat — include statistici per-backend ca eșecurile silențioase
|
||||
# să apară la fiecare 30 min fără să aștepte oprirea.
|
||||
if time.time() > heartbeat_due:
|
||||
try:
|
||||
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"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:
|
||||
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
|
||||
time.sleep(cfg.loop_interval_s)
|
||||
finally:
|
||||
try:
|
||||
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():
|
||||
lines.append(
|
||||
f"{name}: sent={s['sent']} failed={s['failed']} "
|
||||
f"dropped={s['dropped']} retries={s['retries']}"
|
||||
)
|
||||
notifier.send(Alert(
|
||||
kind="heartbeat", title="ATM stopped",
|
||||
kind="heartbeat", title="ATM oprit",
|
||||
body="\n".join(lines),
|
||||
))
|
||||
except Exception:
|
||||
|
||||
@@ -92,8 +92,7 @@ def test_handle_tick_prime_buy():
|
||||
assert notif.alerts[0].kind == "arm"
|
||||
assert notif.alerts[1].kind == "prime"
|
||||
assert notif.alerts[1].direction == "BUY"
|
||||
# Non-catchup prime alert must not mention catchup
|
||||
assert "catchup" not in notif.alerts[1].title.lower()
|
||||
assert "recuperare" not in notif.alerts[1].title.lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -133,10 +132,10 @@ def test_handle_tick_catchup_dark_green():
|
||||
assert len(notif.alerts) == 2
|
||||
assert notif.alerts[0].kind == "arm"
|
||||
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].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
|
||||
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 notif.alerts[0].kind == "arm"
|
||||
# Decision 3A: cannot distinguish fresh arm from catchup-at-arm-phase
|
||||
assert "catchup" not in notif.alerts[0].title.lower()
|
||||
assert "catchup" not in notif.alerts[0].body.lower()
|
||||
assert "recuperare" not in notif.alerts[0].title.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 notif.alerts[0].kind == "arm"
|
||||
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].direction == "SELL"
|
||||
|
||||
@@ -341,7 +340,7 @@ def test_handle_tick_opposite_direction_catchup_after_fire():
|
||||
assert len(notif.alerts) == baseline_alerts + 2
|
||||
assert notif.alerts[baseline_alerts].kind == "arm"
|
||||
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].direction == "SELL"
|
||||
|
||||
|
||||
@@ -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 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 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 trigger[0].direction == "SELL"
|
||||
|
||||
Reference in New Issue
Block a user