Marius Mutu 8bae507bbd feat(cli): atm validate-calibration — offline color classification gate
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>
2026-04-18 12:02:48 +03:00

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:

  1. Dialog: substring of the chart window title (e.g. TradeStation or DIA). Stored in config for later auto-focus.
  2. "Ready?" message → click OK → 3s countdown in terminal. Alt-tab TradeStation to the foreground and minimize anything covering it.
  3. Full-desktop screenshot is captured and shown in a scaled Tk window.
  4. Drag a rectangle over the chart (include the M2D MAPS strip). Enter = confirm. Esc = cancel.
  5. 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)
  6. Save → writes configs/YYYY-MM-DD-HHMM.toml + marker configs/current.txt. Pulls Discord/Telegram creds from env (ATM_DISCORD_URL, ATM_TG_TOKEN, ATM_TG_CHAT) if set; otherwise REPLACE_ME placeholders — 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_axis linear-interp pair.
  • canary.baseline_phash of 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 dryrun will 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_roi boundaries 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:3023: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:

  1. Wall-clock wait until --start-at (if set).
  2. pygetwindow.activate() on the first window matching cfg.window_title — brings TradeStation to the foreground automatically (restores if minimised).
  3. 5s countdown (--startup-delay).
  4. Capture first frame + canary check. Status (drift=X/Y or capture_failed) is included in the startup ping.
  5. "ATM started" ping on Discord + Telegram.
  6. Main loop: every loop_interval_s (default 5s) — capture → canary → detect → state machine → maybe notify → maybe Phase-B.
  7. At --stop-at (or --duration): "ATM stopped" ping, then exit.

Per-cycle behaviour:

  • Canary drift → auto-pause (logs paused, skips detection). Clear by running atm run again 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, LevelsExtractor armed.
  • Phase-B complete → "Levels SL=… TP1=… TP2=…" push.
  • Heartbeat every heartbeat_min minutes.

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: mss can 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:

  1. Task Scheduler → Create Task → name ATM M2D Monitor
  2. General: "Run only when user is logged on", "Run with highest privileges"
  3. Triggers: New → Daily, Start 16:30
  4. Actions: New → Program C:\path\to\python.exe, Arguments -m atm run --stop-at 23:00, Start in D:\PROIECTE\atm
  5. Conditions: uncheck "Start only if AC power" (if laptop)
  6. 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.

Description
No description provided
Readme 10 MiB
Languages
Python 99.9%