README gets: operating-hours config + CLI override flags, Telegram command table with /pause /resume [force] semantics, validate-calibration usage + exit codes, new audit event reference, phase-skip backstop note, and test count bump. CLAUDE.md quick reference now lists the new subcommand, CLI flags, and Telegram commands so future sessions pick them up without re-reading main.py. TODOS.md marks the 2026-04-17 hang fix, canary drift notification, phase-skip backstop, operating-hours window, and validate-calibration as done with commit pointers; adds exchange-calendar holidays as known gap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
297 lines
14 KiB
Markdown
297 lines
14 KiB
Markdown
# 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/ # 184 pytest cases
|
||
└── TODOS.md # P1/P2/P3 backlog, Faza 2 items
|
||
```
|
||
|
||
---
|
||
|
||
## Install
|
||
|
||
Python 3.11+ required. Clone, then:
|
||
|
||
```bash
|
||
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.
|
||
|
||
```powershell
|
||
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
|
||
|
||
```powershell
|
||
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
|
||
|
||
```powershell
|
||
# 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:
|
||
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 + **single-shot Telegram alert** (`⚠️ Canary drift=N — monitorizare pauzată`). Clear via `/resume force` in Telegram, or restart with 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 skip backstop** (`fire_on_phase_skip=true` default) → ARMED → light_red/light_green direct (dark_red/dark_green missed) still emits `⚠️ PHASE SKIP` alert with screenshot. FSM lockout suppresses spam.
|
||
- Phase-B complete → "Levels SL=… TP1=… TP2=…" push.
|
||
- Heartbeat every `heartbeat_min` minutes.
|
||
|
||
### Operating hours window
|
||
|
||
Configure via `[options.operating_hours]` in TOML (source of truth: NYSE local time, timezone-aware so DST is handled automatically):
|
||
|
||
```toml
|
||
[options.operating_hours]
|
||
enabled = true
|
||
timezone = "America/New_York" # fail-fast validated at config load
|
||
weekdays = ["MON", "TUE", "WED", "THU", "FRI"]
|
||
start_hhmm = "09:30" # NYSE open
|
||
stop_hhmm = "16:00" # NYSE close
|
||
```
|
||
|
||
Out-of-window ticks are skipped (logged only on transition). On boundary crossings the bot emits `market_open` / `market_closed` Telegram status messages exactly once per transition. **Startup in-window does not emit a spurious `market_open` alert.**
|
||
|
||
CLI overrides (beat TOML):
|
||
|
||
```
|
||
atm run --tz America/New_York --weekdays MON,TUE,WED,THU,FRI --oh-start 09:30 --oh-stop 16:00
|
||
```
|
||
|
||
> `--oh-start / --oh-stop` are **different** from `--start-at / --stop-at`. The `--start-at / --stop-at` pair controls wall-clock session bounds (when the process starts and quits); `--oh-*` controls the NYSE trading window inside the session (what hours detection actually runs). They compose.
|
||
|
||
### Telegram commands
|
||
|
||
Send to the bot chat:
|
||
|
||
| Command | Effect |
|
||
|---|---|
|
||
| `/ss` or `/screenshot` | Take and send a screenshot now |
|
||
| `/status` | State + pause reason + window open/closed |
|
||
| `/pause` | Suspend detection (heartbeats continue) |
|
||
| `/resume` | Clear user pause only. If Canary is drift-paused it **stays paused** — use `/resume force` |
|
||
| `/resume force` | Also clear Canary drift-pause (use after recalibration) |
|
||
| `/3` or `/interval 3` | Set auto-screenshot interval to 3 min |
|
||
| `/stop` | Stop the scheduler |
|
||
|
||
Only `allowed_chat_ids` are accepted. After 3 consecutive `401`s the poller enters degraded mode.
|
||
|
||
### Calibration validation (offline gate)
|
||
|
||
Validate that the current calibration classifies known-labeled frames correctly **without waiting for a live session**:
|
||
|
||
```bash
|
||
atm validate-calibration samples/calibration_labels.json
|
||
```
|
||
|
||
Input JSON:
|
||
```json
|
||
[
|
||
{"path": "logs/fires/20260417_201500_arm_sell.png", "expected": "yellow", "note": "first arm"},
|
||
{"path": "logs/fires/20260417_205302_ss.png", "expected": "dark_red"},
|
||
{"path": "logs/fires/20260417_210441_ss.png", "expected": "light_red"}
|
||
]
|
||
```
|
||
|
||
Output: per-sample PASS/FAIL with detected color + top-3 candidates by RGB distance + suggestion pixels for misclassifications.
|
||
|
||
Exit code: `0` if 100% PASS, `1` on any FAIL, `2` on malformed/missing input. Suitable for CI or a pre-`atm run` sanity check.
|
||
|
||
Keep PowerShell minimized during the session so it doesn't cover TradeStation.
|
||
|
||
---
|
||
|
||
## After the session
|
||
|
||
```powershell
|
||
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:
|
||
|
||
```powershell
|
||
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 validate-calibration LABEL_FILE.json # offline color-classification gate
|
||
atm run [--duration H] [--start-at HH:MM] [--stop-at HH:MM] [--startup-delay SEC] [--capture-stub]
|
||
[--tz TZNAME] [--weekdays MON,TUE,...] [--oh-start HH:MM] [--oh-stop HH:MM]
|
||
atm journal [--file PATH] # interactive trade entry
|
||
atm report [--week YYYY-WW] [--file PATH] # weekly summary
|
||
```
|
||
|
||
Exit codes:
|
||
- `atm dryrun` — 0 pass, 1 fail.
|
||
- `atm validate-calibration` — 0 all PASS, 1 any FAIL, 2 bad input.
|
||
- Others: standard convention.
|
||
|
||
## Audit log events
|
||
|
||
Events written to `logs/YYYY-MM-DD.jsonl`. Added by the lifecycle+canary work:
|
||
|
||
| Event | Payload | When |
|
||
|---|---|---|
|
||
| `canary_drift_paused` | `distance` | First drift tick after clean; emits Telegram alert |
|
||
| `user_paused` | — | `/pause` received |
|
||
| `user_resumed` | `was_drift`, `was_user`, `force` | `/resume` or `/resume force` |
|
||
| `market_open` / `market_closed` | `reason` | Operating-hours window boundary (once per transition; **not** at startup) |
|
||
| `phase_skip_fire` | `direction` | Backstop alert when ARMED→light_* direct |
|
||
| `command_error` | `action`, `error` | Dispatch exception (isolated from detection loop) |
|