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>
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.