Claude Agent 0f7dd5dc84 fix(deps+tests): move httpx to prod deps; stub Poller+Scheduler in sync test
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>
2026-04-17 11:00:40 +00: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%