diff --git a/src/atm/main.py b/src/atm/main.py index 69bdd9e..8034246 100644 --- a/src/atm/main.py +++ b/src/atm/main.py @@ -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: diff --git a/tests/test_handle_tick.py b/tests/test_handle_tick.py index 6ecbab3..958ce70 100644 --- a/tests/test_handle_tick.py +++ b/tests/test_handle_tick.py @@ -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" diff --git a/tests/test_main.py b/tests/test_main.py index 96d855d..ee8e18e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -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"