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)
|
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:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user