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>
ATM — Automated Trading Monitor
Personal Faza-1 tool for the M2D strategy. Watches the M2D MAPS colored-dot strip on a TradeStation chart, runs a phased state machine (ARMED→PRIMED→FIRE), pushes Discord + Telegram alerts with an annotated screenshot on BUY/SELL. You execute the trade manually in TradeLocker.
No auto-execution. Faza 2 (auto-execute) is blocked on prop-firm TOS audit — see docs/phase2-prop-firm-audit.md.
Project layout
atm/
├── configs/ # calibration outputs + current.txt marker
├── logs/
│ ├── YYYY-MM-DD.jsonl # per-cycle audit log, rotates at local midnight
│ ├── dead_letter.jsonl # alerts that failed after retries
│ ├── fires/ # annotated screenshots, one per BUY/SELL trigger
│ └── calibrate_capture_*.png / debug_*.png # gitignored debug artifacts
├── samples/ # full frames saved automatically on each colour change
├── src/atm/ # package
│ ├── config.py # frozen dataclass + TOML loader
│ ├── vision.py # ROI crop, phash, pixel↔price, Hough, connected-components
│ ├── state_machine.py # 5-state phased FSM, per-direction lockout
│ ├── detector.py # capture → crop → find rightmost dot → classify → debounce
│ ├── canary.py # layout phash drift watchdog with pause-file gating
│ ├── levels.py # Phase-B SL/TP line extraction
│ ├── notifier/ # FanoutNotifier + Discord webhook + Telegram bot
│ ├── audit.py # line-buffered JSONL, daily rotation
│ ├── calibrate.py # Tk wizard (region-select + click-sample)
│ ├── labeler.py # Tk UI → labels.json
│ ├── dryrun.py # replay corpus, precision/recall gate
│ ├── journal.py # trade entries
│ ├── report.py # weekly R-multiple PnL
│ └── main.py # unified CLI
├── tests/ # 105 pytest cases
└── TODOS.md # P1/P2/P3 backlog, Faza 2 items
Install
Python 3.11+ required. Clone, then:
pip install -e ".[windows]" # Windows: live capture + window focus
pip install -e . # Linux / macOS: dev / dryrun only (no live)
atm --help
[windows] pulls mss, pygetwindow, pywin32.
Calibration
One-time per chart layout. Run on the machine that will do live capture.
atm calibrate # 3s default countdown; use --delay 10 if you want more time
Flow:
- Dialog: substring of the chart window title (e.g.
TradeStationorDIA). Stored in config for later auto-focus. - "Ready?" message → click OK → 3s countdown in terminal. Alt-tab TradeStation to the foreground and minimize anything covering it.
- Full-desktop screenshot is captured and shown in a scaled Tk window.
- Drag a rectangle over the chart (include the M2D MAPS strip). Enter = confirm. Esc = cancel.
- Step-by-step clicks on the selected region:
- M2D MAPS strip: top-left + bottom-right corners
- One click on each of: turquoise, yellow, dark_green, dark_red, light_green, light_red, gray dot + chart background (8 total — "Skip" if a colour isn't currently visible)
- Chart area: top-left + bottom-right (for Phase-B line detection)
- Two known price levels on the y-axis (pixel y → enter price)
- Canary region: top-left + bottom-right on a stable UI element (axis label, title bar)
- Save → writes
configs/YYYY-MM-DD-HHMM.toml+ markerconfigs/current.txt. Pulls Discord/Telegram creds from env (ATM_DISCORD_URL,ATM_TG_TOKEN,ATM_TG_CHAT) if set; otherwiseREPLACE_MEplaceholders — edit the TOML manually.
What gets written:
chart_window_region = {x, y, w, h}— virtual-desktop absolute rectangle. Runtime capture crops the same box, so the window must stay in that position.dot_roi,chart_roi,canary.roi— coords relative to the selected region.- Per-colour RGB (sampled via saturation-snap within 15px of the click, mean of 5x5 around the snapped centre).
y_axislinear-interp pair.canary.baseline_phashof the canary ROI.
Sampling tips:
- Click colours that are actually present in the current dot history. If a colour isn't visible, skip it —
atm dryrunwill tell you if the skipped value doesn't match real dots. - Default tolerance is 60 for dot colours, 25 for background. Tighten via TOML after dryrun if misclassifications creep in.
Smoke-test after calibration
atm debug --delay 5
Captures one frame. Saves logs/debug_full_<ts>.png, logs/debug_dot_roi_<ts>.png, logs/debug_annotated_<ts>.png. Prints:
window_found: True
dot_found: True
rgb: (114, 114, 114)
classified: gray distance=24 confidence=0.79
accepted: True color=gray
Open the annotated PNG: yellow rectangle = dot_roi, red circle = detected dot. The circle should land on the ACTUAL rightmost colored dot in the M2D MAPS strip. If not:
- Circle mid-strip → wrong window under the capture region (bring TradeStation to front).
- Circle on a non-dot UI element →
dot_roiboundaries capture too much; recalibrate narrower. color=None+UNKNOWN→ tolerances too tight OR sampled RGBs don't match real dots; recalibrate clicking on actual dots.
Live run
# Today's session 16:30–23:00 Romania local
atm run --start-at 16:30 --stop-at 23:00
# Indefinite
atm run
# Fixed duration (hours)
atm run --duration 2
# Linux / headless smoke (reads samples/*.png in a loop)
atm run --capture-stub --duration 0.05
Startup sequence:
- Wall-clock wait until
--start-at(if set). pygetwindow.activate()on the first window matchingcfg.window_title— brings TradeStation to the foreground automatically (restores if minimised).- 5s countdown (
--startup-delay). - Capture first frame + canary check. Status (
drift=X/Yorcapture_failed) is included in the startup ping. - "ATM started" ping on Discord + Telegram.
- Main loop: every
loop_interval_s(default 5s) — capture → canary → detect → state machine → maybe notify → maybe Phase-B. - At
--stop-at(or--duration): "ATM stopped" ping, then exit.
Per-cycle behaviour:
- Canary drift → auto-pause (logs
paused, skips detection). Clear by runningatm runagain with the pause-file removed. - Detector reports UNKNOWN → stays in current state (logged as
noise). - Colour change → full frame saved to
samples/YYYYMMDD_HHMMSS_<color>.png(for corpus). - FIRE (BUY/SELL, not locked) → annotated PNG saved to
logs/fires/, attached to the alert,LevelsExtractorarmed. - Phase-B complete → "Levels SL=… TP1=… TP2=…" push.
- Heartbeat every
heartbeat_minminutes.
Keep PowerShell minimized during the session so it doesn't cover TradeStation.
After the session
atm label samples # Tk UI — label each saved frame with true dot colour
atm dryrun samples # replay through detector + FSM; exits 0 if precision=100%, recall>=95%
If the gate fails, tune per-colour tolerance in configs/<current>.toml, or recalibrate colour samples that didn't match. Re-run atm dryrun until it passes. Only then do you trust live signals.
Trade record-keeping:
atm journal # interactive entry after a real trade
atm report --week 2026-16 # weekly win rate + R PnL + slippage
DPI / multi-monitor notes
- Calibration region is virtual-desktop-absolute; runtime capture uses the same rectangle. Don't move the TradeStation window after calibrating. Canary will catch drift and pause automatically.
- Changing DPI scaling or moving to a different monitor with different DPI → recalibrate.
- RDP / virtual desktops:
msscan return black frames over RDP. Run locally on the same physical machine as TradeStation.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
capture_failed in startup ping |
chart_window_region references coords off-screen (different monitor layout) |
Recalibrate. |
Startup canary drift=X/8 with X >> 8 |
Wrong window is in the capture region | Make sure TradeStation is the window at cfg.chart_window_region. Relaunch. |
WARN: no window contains 'xxx' at startup |
cfg.window_title substring matches nothing |
Edit window_title in TOML to a substring that's unique to TradeStation. |
| No alerts even after trigger ought to fire | Check logs/YYYY-MM-DD.jsonl for event=tick entries — are colours accepted? Is trigger ever set? |
If always UNKNOWN → tolerances too tight. If trigger but locked=true → lockout from prior fire, normal. |
| Discord OK, Telegram silent (or vice versa) | logs/dead_letter.jsonl contains failed alerts with error |
Fix credentials in TOML, restart. |
Heartbeat shows telegram: failed > 0 |
Telegram returned ok:false (bot blocked, invalid chat_id, parse error) |
Check logs/dead_letter.jsonl for the error_str / description field. Common: bot never started by user in Telegram, or wrong chat_id flavor (channel vs group vs DM). |
| Debug circle on mid-strip instead of right edge | Anti-aliasing bridges dots in the mask | Already fixed via erosion+connected-components — ensure git pull is current. |
| Wizard window is tiny / image not visible | Tk geometry default on Windows | Already fixed — git pull. Image is scaled to fit screen. |
Windows Task Scheduler (production)
For hands-off daily runs surviving reboots:
- Task Scheduler → Create Task → name
ATM M2D Monitor - General: "Run only when user is logged on", "Run with highest privileges"
- Triggers: New → Daily, Start
16:30 - Actions: New → Program
C:\path\to\python.exe, Arguments-m atm run --stop-at 23:00, Start inD:\PROIECTE\atm - Conditions: uncheck "Start only if AC power" (if laptop)
- Settings: "If task runs longer than 7 hours → stop"
Click-right → Run to test manually. Manual DST-change check twice a year (Mar / Oct first week).
Quick command reference
atm calibrate [--screenshot PATH] [--delay SEC] # Tk wizard
atm debug [--delay SEC] # one-shot capture + detect
atm label SAMPLES_DIR # Tk labeling
atm dryrun SAMPLES_DIR # corpus gate
atm run [--duration H] [--start-at HH:MM] [--stop-at HH:MM] [--startup-delay SEC] [--capture-stub]
atm journal [--file PATH] # interactive trade entry
atm report [--week YYYY-WW] [--file PATH] # weekly summary
Exit code: atm dryrun exits 0 if gate passes, 1 otherwise. Other commands follow standard convention.