Commit Graph

75 Commits

Author SHA1 Message Date
839caacc95 agents 2026-05-05 18:02:37 +03:00
c950a5a699 feat(multi-chart): refactor _run_multi_tick + fix alert spam pe oscilație strip
Bug critic: _strips_match(tol=10) trip pe pulsații naturale de lățime ~18px între
ticks (ex. 792↔810px). Fiecare trip → _commit_layout_change → reset FSM + alert
Telegram + scheduler stop. Logul 2026-05-04.jsonl arăta 576 evenimente
layout_change/zi, plus prime alerts repetate la dark_red/dark_green (FSM resetat
înghite lockout-ul) și sincronizare cross-chart pe ambele FSM-uri simultan.

Fix:
- main.py:1511 — gate doar pe count change (len(new) != len(current));
  count stabil → silent update sub_roi indiferent de jitter
- main.py:1438 — silent=True pe alert layout_change (Telegram fără sunet)
- 2 teste regresie noi: width oscillation 792↔810 + silent assertion
- 2 teste async reparate: bootstrap _detect_strips_for_ctx pentru ScriptedDetector
  (regresie după ce _run_multi_tick a devenit unica cale de detecție)

Plus refactor multi-chart pre-existent: layout.py modul nou, _detect_strips_for_ctx,
ChartState per-chart FSM/Detector, ROI per-strip pe screenshots, scripts/diag_*.

Verificat: 292 passed, 2 skipped în 10s.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-05 17:59:18 +03:00
8a1be979fe chore(calibration): 22 frame-uri auto-captured din sesiunea 2026-04-22
Output din live loop (auto-capture pe schimbare de culoare) pentru
sesiunea de azi. Filename = culoare detectată de FSM (poate fi greșită).
Următorul pas manual: review și label-uire în calibration_labels.json
pentru atm validate-calibration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 22:59:21 +03:00
248ad6b10e feat(telegram): /rebase + /rebase confirm pentru re-anchor canary baseline
/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>
2026-04-22 22:56:51 +03:00
45ed502b3d feat(telegram): /ss + /resume cu verify multi-bulină și header FSM step
/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>
2026-04-22 00:38:29 +03:00
5ebe26e5d5 test(calibration): 8 labels + 3 scenarii noi (inclusiv regresie 2026-04-21)
calibration_labels.json: 16 → 24 entry-uri. Toate cele 8 frame-uri dumpate
automat de live loop azi (gray×5, dark_green×1, yellow×1, dark_red×1) trec
prin validate-calibration cu detector-ul curent (24/24 PASS).

scenarios.json: 8 → 11 scenarii:

1. buy_catchup_opposite_rearm_to_sell — REGRESSION pentru bug-ul de azi.
   Trace real din log: 17:00 catchup dark_green → PRIMED_BUY (synth arm+prime),
   apoi 17:45 yellow → ARMED_SELL via opposite_rearm. Verifică că dispatch-ul
   nou emite kind=opposite_rearm și că scheduler-ul se oprește.

2. buy_armed_gray_persist — gray între arm și prime ține ARMED_BUY
   (reason=persist). Acoperă o ramură FSM neacoperită.

3. buy_primed_gray_cooldown — gray după prime ucide ciclul (reason=cooled,
   IDLE, scheduler stop). Confirmă semantica M2D că chart-ul tăcut post-prime
   înseamnă setup expirat.

Total: 11/11 scenarii PASS, 238/238 teste (235 + 3 noi scenarii regresie).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:35:22 +03:00
75a17f9640 feat: auto-capture scrie direct în calibration/frames/ (elimină pasul manual)
Live loop-ul dumpa frame-uri pe schimbare de culoare în samples/, iar userul
copia manual cele utile în calibration/frames/ pentru labelling și regresie.
Pas inutil — acum scrie direct în corpus.

- samples_dir → calibration/frames/ (mkdir parents=True)
- stub capture (ATM_STUB_CAPTURE pentru smoke test Linux) citește din aceeași locație
- 8 PNG-uri orfane din samples/ (20260421_*) mutate în corpus
- CLAUDE.md clarifică: filename = culoarea detectată (poate fi greșită);
  calibration_labels.json rămâne singurul ground truth (manual)

Impact zero pe validate-calibration (iterează peste labels.json, ignoră fișiere
extra) și test_scenarios_regression.py (referă doar frame-uri curate din
scenarios.json).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:31:06 +03:00
ebc986abd3 docs(claude): scoate atm dryrun samples din quick-ref (corpus e calibration/)
samples/ nu mai e corpus-ul activ — calibration/frames/ + calibration_labels.json
au înlocuit workflow-ul vechi. `atm dryrun samples` rămâne comandă validă dar
necesită samples/labels.json care nu mai există în uzul curent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:26:51 +03:00
66ffa4bb9a fix: opposite_rearm/rearm alerts + /resume unified + canary-pause UX guards
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>
2026-04-21 22:23:20 +03:00
a796e91e90 fix: forțează maximize TradeStation la focus + atm debug --canary
Canary drift imediat după /ss, /resume, market_open sau startup când
TradeStation nu era maximizat — chart_window_region e absolute, deci orice
dimensiune sub full-screen capturează alt conținut și phash-ul diverge.
SW_RESTORE pe fereastră zoomed o de-maximiza; acum IsIconic-unminimize +
SW_SHOWMAXIMIZED necondiționat.

Adaugă atm debug --canary: salvează crop-ul canary ROI + distance vs baseline
pentru diagnostic rapid când drift-ul apare în teren.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:48:45 +03:00
2c1dae14fc fix: forțează SetForegroundWindow prin ALT-key pentru anti focus-stealing
pygetwindow.activate() raporta succes dar Windows refuza silent ridicarea
ferestrei TS (confirmat în audit: window_focused cu titlul corect, dar
screenshot-ul manual tot prindea app-ul de deasupra). Trucul standard:
keybd_event(ALT) resetează focus-lock-ul, apoi SetForegroundWindow via
ctypes pe hwnd direct. Fallback la win.activate() dacă lipsește _hWnd.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:22:47 +03:00
9e0202c9ee feat: focus fereastra TradeStation pe /ss, /resume și market_open
Captura e region-based (mss), deci dacă alt app acoperă TS, screenshot-urile
manuale și cele scheduled după redeschiderea pieței erau inutile. Acum
aducem TS în față înainte de captură.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:19:40 +03:00
9c44eb6e31 feat: mută secretele Discord/Telegram din TOML în .env
TOML-urile din configs/ rămân 100% calibrare — safe to commit. Secretele
(ATM_DISCORD_URL, ATM_TG_TOKEN, ATM_TG_CHAT) trăiesc în .env la rădăcină
(ignored), cu loader stdlib (shell wins peste file). Validare fail-fast
pentru env lipsă, placeholder REPLACE_ME, chat_id non-numeric.

Include .env.example + secţiune README §Secrets. Tests: 19 noi (env loader +
missing-env + placeholder + chat_id + regression post-migrate snapshot).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:37:24 +03:00
9e8cbafbd4 feat: calibration/ corpus + scenarii regresie FSM
- calibration/frames/: 16 PNG-uri ground-truth numite {ts}_{color}.png,
  copiate din logs/fires (izolate de samples/ și logs/fires/ care se pot goli)
- calibration/calibration_labels.json: mutat din samples/, curățat de entries
  cu fișiere inexistente, extins la acoperire completă 7 culori → 16/16 PASS
- calibration/scenarios.json: 8 secvențe FSM (BUY/SELL full cycle, phase_skip,
  catchup, post-fire suppression) pe frame-uri reale
- tests/test_scenarios_regression.py: parametrizat pe scenarios.json, asertează
  color+state+reason+trigger+alerts+scheduler prin pipeline-ul
  Detector → _handle_tick
- docs: README + CLAUDE reflectă noua structură, incidentul 2026-04-20/21
  (pixel saturat UNKNOWN → FSM blocat în PRIMED → polling continuu) +
  troubleshooting pentru trigger UNKNOWN

Pytest: 184 → 192 passed (+8 scenarii regresie).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 08:32:11 +03:00
bed79fcc35 calibrare 2026-04-21 07:25:38 +03:00
Claude Agent
082361e3c7 Revert "fix: trimite alert Telegram la închidere fereastră CMD pe Windows"
This reverts commit e1572fd3e3.
2026-04-18 13:00:16 +00:00
Claude Agent
e1572fd3e3 fix: trimite alert Telegram la închidere fereastră CMD pe Windows
CTRL_CLOSE_EVENT omora procesul instant, înainte ca finally să ruleze.
_run_live_win32 instalează SetConsoleCtrlHandler care cancelează task-ul
asyncio → finally trimite "ATM oprit" și flushează notifier-ul (~5s fereastra).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 12:51:27 +00:00
Claude Agent
414ad69369 feat: heartbeat suprimate afara orelor, format compact, status simplificat
- _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>
2026-04-18 12:41:52 +00:00
42a1a0e7fd feat: /help command, atm.bat launcher, tzdata fix pentru Windows
- Telegram /h /help — listă comenzi în română
- atm.bat — pornire cu venv local automat, pip install la primul run
- tzdata adăugat în deps principale cu marker sys_platform==win32
- README: secțiuni dev, instalare Windows, flow-uri calibrare

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 13:11:44 +03:00
7b91cb0cd0 feat: telegram commands, autopoll, operating hours, validate-calibration, phase-skip backstop 2026-04-18 12:42:03 +03:00
92a4b377c2 readme 2026-04-18 12:41:39 +03:00
5b61bd7b60 readme romana 2026-04-18 12:30:57 +03:00
212f77f0ee docs(readme): rescriere completă în română + workflow validate-calibration
- Tradus întreg README-ul din engleză în română, simplu și clar.
- Secțiune nouă "Reguli critice la calibrare" care codifică cele 3 lecții
  din incidentul 2026-04-17: click pe rightmost dot, canary pe pixel static,
  calibrare în sesiune activă.
- Workflow detaliat de corectare iterativă cu validate-calibration: cum
  colectezi samples în timpul sesiunii live (prin /ss), cum actualizezi
  calibration_labels.json, ce faci când FAIL, rollback.
- Exemplu real cu tabelul de ajustări aplicate în 2026-04-18-1220.toml
  (dark_red, light_red din evidență live; dark_green, light_green
  ajustate proporțional +45/+18 pe canalul dominant).
- Troubleshooting: două linii noi (alertă pe culoare greșită, hang după
  N ore rezolvat în c5024ce).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 12:28:31 +03:00
37f0b14468 docs: reflect Telegram /pause/resume, operating hours, phase-skip backstop, validate-calibration
README gets: operating-hours config + CLI override flags, Telegram command table
with /pause /resume [force] semantics, validate-calibration usage + exit codes,
new audit event reference, phase-skip backstop note, and test count bump.

CLAUDE.md quick reference now lists the new subcommand, CLI flags, and
Telegram commands so future sessions pick them up without re-reading main.py.

TODOS.md marks the 2026-04-17 hang fix, canary drift notification,
phase-skip backstop, operating-hours window, and validate-calibration as
done with commit pointers; adds exchange-calendar holidays as known gap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 12:09:44 +03:00
40cc67b4c6 fix(run): _should_skip tz check uses isinstance, tolerates mock cfg
Existing lifecycle tests mock cfg via MagicMock; the attribute auto-return
made `cfg.operating_hours._tz_cache` truthy-but-not-a-tzinfo, crashing
datetime.fromtimestamp with TypeError. Guard with isinstance(tz, tzinfo)
so mock configs are silently skipped.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 12:07:07 +03:00
8bae507bbd feat(cli): atm validate-calibration — offline color classification gate
Adds `atm validate-calibration LABEL_FILE` subcommand that runs the Detector
on a set of labeled PNG frames and reports per-sample PASS/FAIL with top-3
candidate colors and RGB-distance suggestions for failures. Exits 0 on 100%
PASS, 1 on any FAIL, 2 on missing/malformed label file.

- New module src/atm/validate.py with ValidationReport + SampleRecord
  dataclasses; reuses Detector.step(frame), does not reimplement color
  classification.
- main.py: new `validate-calibration` subparser and _cmd_validate_calibration
  handler wired into the dispatch map.
- samples/calibration_labels.json seeded with 3 entries from the 2026-04-17
  incident, plus a README describing the schema.
- tests/test_validate.py covers the 3 planned cases: PASS, FAIL w/ top-3
  + suggestion, missing file (graceful error, no traceback).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 12:02:48 +03:00
23865776e3 feat(commands): /pause /resume + adaptive dispatch + richer /status
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>
2026-04-18 12:01:19 +03:00
54f55752c1 feat(run,config): operating hours window + timezone-aware lifecycle state
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>
2026-04-18 11:59:22 +03:00
8b53b8d3c9 feat(alerts): fire_on_phase_skip backstop + public FSM lockout API
When the FSM observes ARMED → light_{green,red} directly (the dark
prime was missed), the color classifier likely mis-labeled the dark
phase as gray/background. Missing a fire is worse than a noisy alert,
so the new [options.alerts] fire_on_phase_skip flag (default True)
emits a phase_skip_fire alert on that transition with the standard
240s lockout to dedupe detector bounces.

Adds public StateMachine.is_locked / record_fire so the handler does
not reach into private attrs. _handle_tick now accepts an optional
cfg to read the flag; falls back to True if cfg is absent (tests).

Config gains AlertBehaviorCfg (new alerts field), parsed from
[options.alerts]. Example TOML updated with an explanatory comment.

Tests: test_phase_skip_fire_when_flag_on,
test_phase_skip_no_fire_when_flag_off,
test_phase_skip_lockout_suppresses_spam,
test_state_machine_is_locked_and_record_fire_public_api.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 11:55:39 +03:00
9cf49caf8a feat(canary): single-shot on_pause_callback + wire Telegram drift alert
Canary auto-pause was silent: when drift > threshold the module flipped
to paused without any user-facing notification, leaving the user to
wonder why detection went dark. Add an optional on_pause_callback
invoked exactly once per not_paused→paused transition. Wrap the call
in try/except so a notifier failure can never break the detection
cycle.

main.py wires the callback to emit canary_drift_paused audit event
plus a warn Alert guiding the user toward /resume or recalibration.

Tests: test_canary_pause_callback_fires_once (idempotent),
test_canary_resume_allows_new_pause_notification (re-arms after
resume), test_canary_pause_callback_exception_does_not_crash_check
(safety).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 11:53:22 +03:00
c5024ce600 feat(run): extract detection loop helpers + unconditional cmd drain
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>
2026-04-18 11:52:28 +03:00
153196f762 chore(git): track logs dir; ignore runtime state files
Add logs/.gitkeep to track directory structure. Extend .gitignore with
logs/fires, logs/pause.flag, logs/detections/, and configs/current.txt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 10:10:21 +03:00
Claude Agent
3b40aed939 fix(run): isolate command dispatch exceptions from detection loop
Any exception in _dispatch_command (status, ss, etc.) was leaking out of the
asyncio.QueueEmpty try/except, crashing _detection_loop and cancelling the
poller — making the bot permanently unresponsive for the rest of the session.

Separate the queue-empty check from the dispatch into two try blocks.
Dispatch errors now log to audit + print to terminal + send a Telegram warn.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 11:29:42 +00:00
Claude Agent
0f7dd5dc84 fix(deps+tests): move httpx to prod deps; stub Poller+Scheduler in sync test
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>
2026-04-17 11:00:40 +00:00
Claude Agent
63642e71dd chore(todos): mark integration test done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 10:54:24 +00:00
Claude Agent
424437ceaf fix(audit)+test: deadlock fix + lifecycle test + pytest-asyncio
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>
2026-04-17 10:54:10 +00:00
Claude Agent
ca6e578175 feat(run): async refactor — run_live_async + 7-step shutdown
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>
2026-04-17 10:37:17 +00:00
Claude Agent
4123b31a22 feat(commands,scheduler): TelegramPoller + ScreenshotScheduler
TelegramPoller: httpx async long-poll, startup drain, chat_id filter,
degrade after 3×401, Command dataclass with minute→second conversion.

ScreenshotScheduler: asyncio task, capture+annotate in to_thread (decisions 9+13),
silent=True on periodic screenshots, explicit constructor params.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 10:18:08 +00:00
Claude Agent
c1b89ad6a9 feat(config,detector): TelegramCfg polling fields + Detector.step optional frame
TelegramCfg gains allowed_chat_ids (default: [chat_id]), poll_timeout_s=30,
auto_poll_interval_s=180. _from_dict reads from TOML; absent key defaults to
primary chat_id so existing configs need no changes.

Detector.step(ts, frame=None): when frame is provided the capture() call is
skipped — async loop pre-captures once, shares frame between canary+detection.
DetectionResult.dot_pos_abs carries absolute (x,y) for price overlay.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 10:17:17 +00:00
Claude Agent
fd04fcd5e6 fix(audit): threading.Lock on AuditLog.log + close (P1 bug)
detection thread and async heartbeat call log() concurrently.
Without a lock, two threads can both see today != _current_date
and double-open the file, corrupting the handle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 10:16:28 +00:00
Claude Agent
c6714e8d5e feat(notifier): Alert.silent + TelegramNotifier disable_notification
Silent screenshots for periodic auto-poll — Telegram param
disable_notification=True suppresses phone notification sound.
Discord ignores the field (no equivalent).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 10:16:17 +00:00
Claude Agent
238243b1ce chore: add gstack skill routing rules to CLAUDE.md 2026-04-17 08:32:50 +00:00
Claude Agent
8ff31ed241 fix(run): heartbeat fires immediately after start due to monotonic/time mismatch
heartbeat_due was initialized from time.monotonic() but compared against
time.time(), causing the first heartbeat to always trigger on the first
loop iteration (duplicate message at startup).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 23:12:28 +00:00
Claude Agent
eca2b39e64 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>
2026-04-16 23:09:20 +00:00
Claude Agent
51e98ae3d3 fix(notify): switch Telegram parse_mode from Markdown to HTML
Underscores in alert text (dark_green, FIRE_BUY) broke Telegram's
legacy Markdown parser, causing ok:false → retries exhausted → failed.
HTML parse_mode is more robust and doesn't treat _ as italic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 23:01:28 +00:00
Claude Agent
840c23f74c feat(run): screenshot attach, Telegram ok:false fix, post-FIRE catchup guard
Three bundled fixes on the dispatch + FSM + notifier triangle:

1. Telegram silent-success bug: parse JSON body after 200 OK, raise on
   ok:false so FanoutNotifier retries + DLQs + stats surface the failure.
   Previously Discord succeeded while Telegram silently dropped.

2. Per-kind screenshot attach: new AlertsCfg dataclass with per-kind toggle
   (late_start, catchup, arm, prime, trigger). _save_annotated_frame helper
   extracted from inline FIRE block, threaded via Snapshot closure into
   _handle_tick. Failures audit-logged, never silent.

3. Post-FIRE catchup regression (d7305fb): residual dark_green/dark_red dots
   after a FIRE cycle look like startup-catchup from (color, state) alone.
   New fsm.fired_in_session(direction) gate suppresses synth-arm after a
   cycle already fired in that direction. Opposite direction unaffected.

Also: queue-overflow on_drop audit callback, periodic + shutdown heartbeat
stats per-backend, config back-compat (bool or dict for attach_screenshots).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:40:17 +00:00
Claude Agent
d7305fbbfc fix(run): drop first_accepted gate from catchup synth-arm
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>
2026-04-16 18:54:03 +00:00
Claude Agent
f4b9000100 feat(run): per-frame detection log at logs/detections/YYYY-MM-DD.jsonl
Writes one JSONL line per detector.step() with ts, rgb, match_name,
distance, confidence, dot_found, window_found, accepted, color.
Captures UNKNOWN classifications and no-dot frames that today's
audit log skips, so the user can verify post-session what colors
the program actually saw.

Reuses AuditLog for daily rotation + buffering. Separate subdir
keeps audit.jsonl uncluttered.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:48:27 +00:00
Claude Agent
e7369ca632 feat(run): arm + prime alerts, mid-session catchup, late-start guard
Notify on IDLE→ARMED and ARMED→PRIMED so the user gets staged warnings
before FIRE. If atm run starts mid-cycle on dark_green/dark_red, synth
a catchup arm so the prime alert still fires (audit tagged catchup:true).
If it starts on light_green/light_red, emit one late_start warn and skip
the FSM feed — FIRE already passed.

Extracted _handle_tick() as a pure helper (takes fsm + duck-typed
notifier/audit Protocols) so the dispatch is unit-testable without
mocking FanoutNotifier. 9 new tests cover arm, prime, refresh silence,
catchup synth-arm (+ audit), and late-start on both directions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:30:01 +00:00
Claude Agent
34fde8328c docs: rewrite README to match current CLI + workflow
Reflects everything that shipped after the initial teammate pass:
calibrate region-select flow, startup delays, auto-focus, canary startup
check, --start-at/--stop-at, atm debug, samples/ auto-save, fires/
annotated screenshots, Task Scheduler setup, current troubleshooting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:06:12 +00:00