# Plan: ATM — Automated Trading Monitor (M2D, Faza 1) — ENG-REVIEWED **Source plan:** `/home/claude/.claude/plans/swirling-drifting-starfish.md` **CEO plan artifact:** `~/.gstack/projects/romfast-workspace/ceo-plans/2026-04-15-atm-trading.md` **Eng review mode:** FULL_REVIEW (4 decisions made, 0 unresolved) **Design doc:** `~/.gstack/projects/romfast-workspace/claude-master-design-20260415-atm-trading.md` (APPROVED) **Eng test plan:** `~/.gstack/projects/romfast-workspace/claude-master-eng-review-test-plan-20260415-212932.md` --- ## Context User trades M2D strategy manually on DIA (TradeStation) with execution on TradeLocker US30 CFD (prop firm). Same strategy on GLD → XAUUSD. 4h/evening dual-screen monitoring. Faza 1 goal: bot auto-detects M2D trigger, sends Discord/Telegram notification with screenshot + SL/TP1/TP2 levels; user executes manually in TradeLocker. Faza 2 (auto-execution) deferred until prop firm TOS verified and Faza 1 proven over 20+ sessions. **Review changed two things from the original plan:** 1. **State machine spec corrected.** Original "last 3 consecutive non-gray dots" is wrong. Actual M2D is phased: Phase 1 arming (turquoise → gray/dark-green) → Phase 2 trigger (light-green). 2. **Levels extraction corrected.** Original plan had levels.py extracting SL/TP at trigger. But those lines only appear on TradeStation chart *after* user enters trade in TradeLocker. Corrected to two-phase: spec-math at trigger, chart-scan after entry. Plus 5 accepted expansions (labeled corpus, level fallback, layout canary, trade journal, TOS checklist). --- ## Approach: B (Structured Python service, dry-run, audit log) + CEO-reviewed additions Runs on Windows machine alongside TradeStation. `mss` screenshots → ROI color-sample on M2D MAPS strip → phased state machine → Discord webhook + Telegram bot → JSONL audit + trade journal → dry-run replay against labeled corpus. --- ## State Machine Spec (corrected + exhaustive) States: - `IDLE` - `ARMED_BUY` — turquoise seen - `PRIMED_BUY` — turquoise + at least one dark-green seen - `ARMED_SELL` — yellow seen - `PRIMED_SELL` — yellow + at least one dark-red seen **Default rule:** any (state, event) pair not listed below → stay in current state, no action, log as `noise`. Transitions — BUY side: | From | Event | To | Action | |------|-------|-----|--------| | IDLE | turquoise | ARMED_BUY | log arm_ts | | IDLE | yellow | ARMED_SELL | log arm_ts (sell) | | IDLE | dark-green / dark-red / light-green / light-red / gray | IDLE | noise (log phase-skip if light-green/light-red) | | ARMED_BUY | gray | ARMED_BUY | persist | | ARMED_BUY | turquoise | ARMED_BUY | refresh arm_ts | | ARMED_BUY | dark-green | PRIMED_BUY | log prime_ts | | ARMED_BUY | yellow | ARMED_SELL | opposite rearm | | ARMED_BUY | dark-red | ARMED_BUY | ignore (minority noise) | | ARMED_BUY | light-green | IDLE | **skip detected** — no FIRE, log phase_skip | | ARMED_BUY | light-red | IDLE | skip detected, log | | PRIMED_BUY | dark-green | PRIMED_BUY | accumulate | | PRIMED_BUY | dark-red | PRIMED_BUY | ignore (minority noise) | | PRIMED_BUY | **light-green** | IDLE | **FIRE BUY**, lockout(BUY)=4min | | PRIMED_BUY | light-red | IDLE | skip detected (wrong trigger) | | PRIMED_BUY | gray | IDLE | **COOLED** — signal dead, log | | PRIMED_BUY | turquoise | ARMED_BUY | rearm fresh | | PRIMED_BUY | yellow | ARMED_SELL | opposite rearm | SELL side mirrors exactly: swap turquoise↔yellow, dark-green↔dark-red, light-green↔light-red, BUY↔SELL. Notes: - No time-based TTL on ARMED/PRIMED. State persists until trigger fires, cooled by gray after PRIMED, opposite-color rearm, or process restart (Windows Task Scheduler stops bot at session end → natural session-boundary reset). - Cooling rule: "gray after dark-green" = signal racit (user's term). Gray during ARMED_BUY (before any dark-green) is OK. - After FIRE: 4-minute lockout per-direction. BUY lockout doesn't block SELL and vice versa. Single timestamp per direction. - Opposite-color-Phase-1 triggers rearm to opposite side (captures direction flip). - Phase-skip (arming color → trigger color with no phase-2 step) → IDLE, no FIRE, logged. Would be legitimate only if indicator collapses phases, which it doesn't per observed behavior. --- ## Detection Details - **Loop interval:** 5 seconds (36 cycles per 3-min bar; stays well inside notification-latency target). - **Rightmost-dot detection:** scan ROI from right edge leftward, find first non-background pixel cluster → that's the rightmost dot. Don't hardcode x-pixel positions (chart scrolls; hardcoded positions drift). - **Debounce:** configurable `debounce_depth` in config.toml (default `1` — single-read acceptance). Increase if future sessions show mid-bar color flicker. Screenshot-in-notification is the user's visual verification on top. - **Rolling window:** keep last 20 classified dots with their detection timestamps. State machine consumes the newest *accepted* (post-debounce) dot per cycle. - **Classification:** nearest-color match in RGB Euclidean distance, per-color tolerance from calibration. Report confidence = `1 - distance_nearest / distance_second_nearest`. Log confidence every cycle. If all distances > tolerance → `UNKNOWN`, state unchanged. --- ## Levels Extraction (two-phase, simplified) **Phase A — at trigger (immediate alert to Discord + Telegram):** - No entry-price compute. No spec-math SL/TP. User places a manual 0.6% SL in TradeLocker at entry; actual TP1/TP2/SL come in Phase B from the chart. - Notification: `🟢 BUY signal DIA→US30 | 22:47:03` + annotated screenshot (detected dot highlighted). **Phase B — after user trades (chart-scan confirmation):** - After Phase A fires, detector keeps watching the chart ROI for horizontal colored lines (red=SL, green=TP1/TP2). - When lines appear (user has entered trade in TradeLocker and TradeStation drew them) → scan y-pixels via Hough + color mask, convert via y-axis calibration → send second alert to both channels: `✅ Levels: SL=484.35 | TP1=485.20 | TP2=485.88`. - If chart-line scan times out (no lines in 10 min) → silent (user didn't trade). - If only 2 lines detected (user didn't set TP2 or line not rendered yet) → partial-result alert. - Phase B overlap with next signal: guarded by per-direction lockout + Phase-B completion flag; a new FIRE cannot issue until prior Phase B closes (timeout or success). --- ## Dedup / Lockout - Time-based lockout: after any FIRE, block re-fire for 4 minutes (one 3-min bar + 1 min safety). - Tracked per-direction: BUY lockout doesn't block SELL. - Stored as single timestamp per direction (not pixel-keyed). --- ## Observability - **Heartbeat:** every 30 min to a separate Discord thread (not main alerts channel): `🟢 22:00 alive | 0 triggers | confidence avg 0.85 | chart OK`. Silence >35 min = watchdog concern (user notices). - **Layout canary:** every 60 cycles (5 min), hash a stable reference region (axis labels, chart border). Stored baseline in config. On significant divergence (>threshold) → `⚠️ Layout changed — auto-paused, recalibrate` to alerts channel. Bot pauses detection until operator acknowledges (touch a pause-file or restart). - **Low-confidence alert:** 3+ consecutive cycles with confidence below threshold → `⚠️ Bot lost sight` (already in original plan). - **Window-lost alert:** TradeStation window not found for 60s → `⚠️ Cannot find chart`. - **Audit JSONL:** per-cycle, daily rotation (`logs/YYYY-MM-DD.jsonl`), fields: `{ts, window_found, roi_ok, rightmost_dot_color, confidence, state, transition, trigger, notified, reason}`. --- ## Files to Create - `/workspace/atm/pyproject.toml` — Python 3.11+ required. Deps: `mss`, `opencv-python`, `numpy`, `requests`, `pygetwindow`, `pywin32` (DPI + window capture), `rich` (CLI), `pillow` (screenshot annotation). **No `tomli` — use stdlib `tomllib`.** - `/workspace/atm/config.toml` — populated by calibration tool (ROI coords, per-color RGB + tolerance, `debounce_depth`, y-axis scale, canary-region baseline hash, Discord webhook URL, Telegram bot token + chat_id) - `/workspace/atm/src/atm/config.py` — **[ENG-REVIEW]** `@dataclass Config` with `Config.load(path)` that validates on load (RGB tuples, positive tolerances, both notifier credentials present, y-axis 2-point pair). Fail fast at startup. - `/workspace/atm/src/atm/vision.py` — **[ENG-REVIEW]** shared primitives: ROI crop, perceptual hash, pixel-to-price linear interp, Hough line detection with color mask. Used by detector/canary/levels to avoid drift. - `/workspace/atm/src/atm/detector.py` — screenshot loop, rightmost-dot scan, color classification, rolling window, debounce - `/workspace/atm/src/atm/state_machine.py` — explicit phased state machine (spec above), exhaustive transition table - `/workspace/atm/src/atm/levels.py` — Phase B chart-scan only (Phase A entry-price compute removed after ENG-REVIEW) - `/workspace/atm/src/atm/canary.py` — layout fingerprint hash + drift check + auto-pause - `/workspace/atm/src/atm/notifier/__init__.py` — abstract `Notifier` protocol: `send_alert()`, `send_heartbeat()`, `send_levels_confirm()` - `/workspace/atm/src/atm/notifier/fanout.py` — **[ENG-REVIEW]** `FanoutNotifier` wraps N backends, each with its own worker thread + bounded queue (size 50, drop-oldest on overflow) + retry with exponential backoff + dead-letter file on total failure. Main loop never blocks. - `/workspace/atm/src/atm/notifier/discord.py` — webhook POST, annotated screenshot upload (multipart) - `/workspace/atm/src/atm/notifier/telegram.py` — **[ENG-REVIEW]** built in parallel with Discord (no longer deferred); bot API, photo upload - `/workspace/atm/src/atm/audit.py` — JSONL logger with daily local-midnight rotation, line-buffered write for crash safety - `/workspace/atm/src/atm/calibrate.py` — Tkinter: window pick → DPI check → ROI corners → per-color sample → y-axis scale → canary region → save versioned config - `/workspace/atm/src/atm/labeler.py` — **[EXPANSION]** Tkinter label UI → `labels.json` - `/workspace/atm/src/atm/dryrun.py` — replay with precision/recall/confusion matrix when labels present - `/workspace/atm/src/atm/journal.py` — **[EXPANSION]** `atm journal` CLI → `trades.jsonl` - `/workspace/atm/src/atm/report.py` — **[EXPANSION]** weekly aggregation - `/workspace/atm/src/atm/main.py` — CLI: `atm calibrate`, `atm label