/rebase capturează + propune phash nou (screenshot adnotat cu red rect pe
canary.roi, old/new hash, distance, TTL 180s). /rebase confirm rescrie
baseline_phash în TOML-ul activ (regex line-match, păstrează comentariile),
mirror în cfg live via object.__setattr__ (CanaryRegion e frozen), clear
user_paused + drift_paused într-un singur shot — similar /resume.
Fix adiacent: _dispatch_ctx / _mock_config_class setează cfg.window_title=None
explicit; 5 teste _dispatch_command pre-existente eșuau pe MagicMock auto-
truthy care propaga în _focus_window_by_title.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/ss și /resume afișează acum markerii top-3 buline sub ROI (cercuri pline,
r=7, culoarea clasificată) cu tick vertical roșu pe pick-ul FSM (rightmost).
Caption compact: `N/3 STATE` header + `emoji c1/c2/c3: name ← pick`.
FIRE_{BUY|SELL} afișat ca 3/3 când fire_ts e în ultimele 30s.
/resume face capture ÎNAINTE de clearing state → zero race cu FSM tick
simultan. Capture fail → title marchează "⚠️ captură eșuată", resume-ul
rulează oricum.
config: <version> mutat din caption în /status (acolo are sens pentru
verificare de calibrare, nu la fiecare /ss).
Adaugă find_top_dots în vision.py (top-N variantă a find_rightmost_dot,
tie-break determinist pe y). 5 teste sintetice noi + 4 teste noi pentru
dispatcher resume (screenshot inline, capture-fail, order-of-ops,
parity /ss <-> fire path).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trei găuri observate în sesiunea 2026-04-21:
A. _handle_tick nu avea branch pentru reason=opposite_rearm (PRIMED_* ↔
ARMED_opus) sau reason=rearm (PRIMED_* → ARMED_* aceeași direcție). La
17:45 yellow a trecut FSM-ul PRIMED_BUY→ARMED_SELL corect, dar zero alert
pe Telegram. Adaugă helper _emit_arm_alert (DRY cu branch-ul arm existent)
și două branch-uri noi cu kind=opposite_rearm / kind=rearm.
B. Canary drift se curăța doar cu /resume force — user ușor confundă
/set_interval cu „relansare" și rămâne în drift-pause (cazul 18:09 azi).
/resume acum curăță user_paused + canary.resume() într-o singură comandă.
/resume force rămâne alias acceptat (muscle memory legacy).
C. Heartbeat-ul afișa „activ ARMED_SELL" deși detecția era oprită de 3 ore
(state FSM înghețat). Extract _build_heartbeat_alert care arată
„⚠️ pauzat (drift)" + „[drift-pause]" când canary.is_paused.
Guard-uri pentru comenzi când canary e paused:
- /set_interval: refuzat cu warn „Trimite /resume"
- /ss: screenshot trimis + body-ul include „⚠️ DETECȚIE OPRITĂ"
11 teste noi (1 critical regression pentru bug-ul A observat azi), plus
actualizarea test-ului /resume existent care aserta vechiul comportament.
Total: 235 passed + 8 scenarii regresie.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- _in_trading_window(): helper nou — suprima heartbeat in afara ferestrei de tranzactionare
- _heartbeat_loop: format compact romanian (STATE | semnale: N | Xh), fara statistici backend
- ATM pornit: "canary:" -> "senzor: ok/deviat (dist/thresh)"
- ATM oprit: simplificat la "durata: Xh | semnale: N"
- /status: 2-3 linii compacte, etichete pauza in romana, fara "Canary"
- _maybe_log_transition: parametru status_body optional; titluri "Piata deschisa/inchisa"
- _brief_status(): helper nou pentru mesajele de tranzitie piata
- 10 teste noi (trading_window, status compact, fereastra); 194 passed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add two new Telegram commands so the user can manage monitoring without
restarting the process:
- /pause sets lifecycle.user_paused = True. The detection loop then
short-circuits via _should_skip without touching FSM / canary state.
- /resume clears user_paused. R2 decision: drift-pause is NOT lifted by
plain /resume (the drift may be legit and require recalibration).
"/resume force" (value=1) also calls canary.resume(). The response
message adapts to context:
- drift active + plain resume → explains force requirement
- force + drift → confirms override, warns about recurrence
- out-of-window → explains monitor will resume at next open
- otherwise → plain "Monitorizare reluată"
- /status now shows "Activ: <pause_reason | activ>" and window state.
commands.py: extend CommandAction literal and _parse_command to accept
pause, resume, and "resume force" (value=1 signal).
Tests: test_commands.py parse coverage;
test_pause_command_sets_user_paused_and_skips_detection,
test_resume_clears_user_paused_and_canary_when_forced,
test_resume_during_drift_keeps_canary_paused_without_force (R2 #21),
test_resume_out_of_window_responds_with_pending_message,
test_status_command_reports_pause_reason,
test_lifecycle_with_drift_then_resume_then_fire (E2E #16).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add OperatingHoursCfg (enabled/timezone/weekdays/start_hhmm/stop_hhmm) so
the run loop can align with NYSE session hours instead of the user's
local wall clock (fixes DST drift between NY and Europe/Bucharest).
- Config parses [options.operating_hours] and resolves ZoneInfo at load,
fail-fast on invalid tz or weekday names. The tz is cached on
_tz_cache so the detection loop pays zero per-tick cost.
- LifecycleState tracks user_paused + last_window_state across ticks.
- Module-scope _should_skip(now, state, cfg, canary) returns skip reason
or None. Weekday check uses datetime.weekday() + a fixed MON..SUN list
(locale-free; strftime('%a') is localized).
- _maybe_log_transition emits market_open / market_closed once per edge.
R2: when last_window_state is None (startup), just seed — do not send
a spurious market_open alert when run_live_async launches in-window.
- _run_tick consults the lifecycle guard before scheduling the heavy
detection thread, so drain + transition logging still happen when the
tick is skipped.
- CLI flags --tz / --weekdays / --oh-start / --oh-stop override TOML.
(Kept distinct from the existing --start-at/--stop-at sleep-until-time
semantics to avoid breaking current deployments — deviation noted.)
- configs/example.toml documents the new [options.operating_hours] table.
Tests: parametrized window matrix (tests #8), transition logging (#9),
notification side-effect (#10), R2 #20 startup suppression, R2 #22
locale-independent weekday, plus guards for user_paused / canary
precedence and config-parse error paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Refactor _detection_loop by moving _run_tick, _handle_fsm_result,
_dispatch_command, and _drain_cmd_queue to module scope, passing
dependencies via a RunContext dataclass. This unblocks direct unit
testing of the drain path.
CRITICAL bug fix: the previous loop issued `continue` when the tick
returned res=None (canary paused or similar), which skipped the
drain block. Commands piled up in cmd_queue while detection was
paused — the hang observed on 2026-04-17 after canary drift-pause.
The refactored loop now runs _drain_cmd_queue UNCONDITIONALLY on
every iteration, after _handle_fsm_result, so pause-state never
starves the command channel.
Tests: test_drain_works_when_canary_paused,
test_drain_works_when_out_of_window,
test_drain_isolates_dispatch_exceptions (exception isolation +
audit/warn wiring).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
httpx was in dev deps only, causing ImportError for users doing `pip install -e .`
since atm.commands imports httpx at module level. Moved to main dependencies.
Also stubs TelegramPoller and ScreenshotScheduler in the sync catchup test to
prevent flaky CI failures from attempted real network connections.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AuditLog deadlock: log() held self._lock and called _open() which called
close() which tried to acquire self._lock again — RLock not needed,
refactored to _close_locked() (called while already holding lock).
pyproject.toml: pytest-asyncio + httpx in dev deps.
test_main.py:
- lifecycle integration test (MUST-HAVE): IDLE→ARMED→PRIMED→auto-poll
starts→FIRE→auto-poll stops, asserts scheduler event order
- asyncio import for async test marker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
run_live() is now a thin asyncio.run() wrapper. run_live_async():
- Blocking pipeline (capture→canary→detect→_handle_tick→snapshot) in
asyncio.to_thread() per decision 1 (_sync_detection_tick function)
- TelegramPoller + ScreenshotScheduler as background asyncio tasks
- asyncio.Queue[Command] for inter-task communication
- Auto-start scheduler on PRIMED, auto-stop on fire/cooled/phase_skip
- 7-step graceful shutdown sequence
- heartbeat_due uses time.monotonic() (prevents immediate-fire regression)
- Status command: FSM state, last detection, uptime, fire count, canary health
- "ss" command: one-shot capture+annotate+send via to_thread
- Price overlay in _save_annotated_frame (dot_pos_abs + canary_ok params)
- test_main.py: ScriptedDetector.step(ts, frame=None) for zero regression
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Catchup branch gated on first_accepted, but an earlier accepted gray tick
consumes the flag before a dark_* arrives, so the real prime-phase color
falls through to noise classification and no alert fires. Gate on
IDLE + dark_* alone — self-sufficient and correct.
Regression: 2 unit tests for _handle_tick + 1 integration test feeding
run_live a scripted gray→gray→dark_red→dark_red→light_red sequence.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>