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>
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>
- _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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- samples/: full frame saved every time accepted colour changes (enough
diversity for the labelled corpus, no constant-N-seconds flood).
- logs/fires/: annotated frame saved on every BUY/SELL trigger, attached
to the Discord/Telegram Alert so the push includes a visual.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pygetwindow.activate() brings the calibrated window to the foreground so
the user doesn't need to alt-tab during the startup-delay. Largest window
matching cfg.window_title (case-insensitive substring) wins. If minimized,
restore first. Failures are warnings, not errors — user can still focus
it manually during the countdown.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Usage: atm run --start-at 16:30 --stop-at 23:00
Sleeps until next occurrence of the start time, runs until stop-at. If
start-at is in the past today, rolls over to tomorrow. Duration flag is
overridden when --stop-at is given.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 5s countdown before the loop starts so user can alt-tab TradeStation to
the foreground and minimize whatever covers it.
- First frame triggers a canary phash check. Drift → WARN printed, clears
auto-pause so user can Ctrl+C without the loop going silent. Canary
status ('drift=X/Y' or 'capture_failed') is included in the startup
ping so it's visible on Discord/Telegram.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Answers 'is it even running?' within seconds of 'atm run' — no waiting
30 min for heartbeat + no need for a live trigger to occur.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
With tol=25 all dots in the strip fused into one blob via 1px anti-aliased
bridges between adjacent dots → centroid landed mid-strip instead of on the
rightmost dot. Erosion (3x3 kernel, 2 iters) cleanly separates discrete
dots before connected-components labelling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dots on M2D MAPS strip are so close that anti-aliased edge pixels bridge
adjacent dots column-counts → the previous walk-left approach merged the
entire strip into one cluster and picked its midpoint.
Connected components (8-connectivity) treats each dot as a separate blob
even when antialiased edges touch. We pick the blob with the largest
right-edge, then return its centroid. Robust, O(pixels), one opencv call.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Also: calibrate._sample_rgb now snaps to the most-saturated pixel within 15px
of the click, so rough clicks still pick up the dot's pure colour. Default
dot-colour tolerance bumped 30→60 to absorb anti-aliasing.
Test fixture _SAMPLED_RGB recomputed for the new 36/49 dilution (was 24/49
when sampling at the trailing edge).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User's chart background is pure black (0,0,0) but detector hardcoded (18,18,18)
with tol=15. Gap pixels between dots (0,0,0) fell outside background tolerance,
so find_rightmost_dot locked onto a gap pixel rather than a dot. Now falls back
to the config's background spec if defined.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same fix as atm debug: desktop snapshot happens several seconds after user
confirms, giving time to alt-tab TradeStation to the foreground and get
rid of terminal/IDE windows covering it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>