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>
Quick sanity check after calibration. Prints window/dot detection state,
classified colour + confidence, saves full/cropped/annotated frames to logs/.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Problem: region-select crop is in its own coord-frame, but runtime capture
used pygetwindow+mss with the window's coords → ROI coords mismatch, chart
not captured correctly.
Fix: region-select now returns (image, virtual-desktop region). Wizard saves
'chart_window_region' in config. At runtime, _build_capture() prefers region
mode: grabs the same virtual-desktop rectangle, so ROI coords line up.
Users who calibrated before this commit must re-run 'atm calibrate' OR add
chart_window_region to their TOML manually. If window moves, canary will
detect drift and auto-pause.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Window-level PrintWindow/mss returns blank on GPU-accelerated apps
(TradeStation, some trading terminals). Switch to full-desktop screenshot
+ drag-rectangle as the default method — reliable across all apps.
- Title prompt still asked (needed by 'atm run' to locate window at runtime).
- Captured region saved to logs/calibrate_capture_<ts>.png for verification.
- Old window-capture path kept as fallback if region-select is cancelled.
- Minor: silence pyright windll attr warning.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Enumerate all matching windows, let user pick the largest one if multiple.
- Try PrintWindow first (works with GPU-accelerated apps like TradeStation
that render blank under plain mss capture).
- Detect blank/uniform captures and fall back to mss automatically.
- Save captured frame to logs/calibrate_capture_<ts>.png for debugging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace skeletal wizard (window title only) with complete click-to-sample flow:
- Screenshot target window via mss+pygetwindow (Windows) or --screenshot PNG
- Single-canvas Tk wizard with scaled display + back/skip/save controls
- Stepwise clicks collect: dot_roi, all 7 dot colours (+ optional background),
chart_roi, 2 y-axis reference points with prices, canary region
- Auto-compute canary baseline phash at save time
- Pull Discord/Telegram creds from ATM_DISCORD_URL/ATM_TG_TOKEN/ATM_TG_CHAT env
- Add --screenshot flag to 'atm calibrate' for non-Windows dev testing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Cast res.color to DotColor in run_live loop.
- Add [tool.pyright] extraPaths to pyproject.toml for IDE resolution.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>