ATM — Monitor Automat de Trading

Tool personal pentru strategia M2D. Urmărește banda de puncte colorate M2D MAPS de pe un chart TradeStation, rulează o mașină de stări pe faze (ARMED → PRIMED → FIRE) și trimite alerte pe Discord + Telegram cu screenshot adnotat la fiecare semnal BUY/SELL. Execuția trade-ului o faci tu manual în TradeLocker.

Fără execuție automată. Faza 2 (auto-execute) e blocată de auditul TOS prop-firm — vezi docs/phase2-prop-firm-audit.md.


Cum e organizat proiectul

atm/
├── configs/              # calibrări + current.txt (marcaj care config e activ)
├── logs/
│   ├── YYYY-MM-DD.jsonl  # audit zilnic, se rotește la miezul nopții local
│   ├── dead_letter.jsonl # alerte care au eșuat după retries
│   ├── fires/            # screenshot-uri adnotate, unul per trigger BUY/SELL
│   └── calibrate_capture_*.png / debug_*.png  # artefacte debug (gitignored)
├── samples/              # frame complet salvat automat la fiecare schimbare de culoare
├── src/atm/              # pachetul Python
│   ├── config.py         # dataclass + loader TOML
│   ├── vision.py         # crop ROI, phash, pixel↔preț, Hough, componente conectate
│   ├── state_machine.py  # FSM 5 stări + lockout per direcție
│   ├── detector.py       # capture → crop → găsește dot-ul rightmost → clasifică → debounce
│   ├── canary.py         # watchdog layout via phash drift + flag de pauză
│   ├── levels.py         # extracție SL/TP pe Faza-B
│   ├── notifier/         # FanoutNotifier + webhook Discord + bot Telegram
│   ├── audit.py          # JSONL line-buffered, rotație zilnică
│   ├── calibrate.py      # wizard Tk (selectează regiune + click pe culori)
│   ├── labeler.py        # UI Tk → labels.json
│   ├── dryrun.py         # replay pe corpus, gate precision/recall
│   ├── validate.py       # gate offline de clasificare a culorilor
│   ├── journal.py        # înregistrări trade-uri
│   ├── report.py         # raport săptămânal PnL în R
│   └── main.py           # CLI unificat
├── tests/                # 184 teste pytest
└── TODOS.md              # backlog P1/P2/P3

Instalare

Python 3.11+.

pip install -e ".[windows]"    # Windows: capture live + focus fereastră
pip install -e ".[dev]"        # Linux/macOS/WSL: doar dev + teste (fără capture)
atm --help

WSL/Linux: recomandat să folosești un virtualenv local:

python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

[windows] aduce mss, pygetwindow, pywin32 (nu le pune pe WSL).


Calibrare

Se face o singură dată per layout de chart. Trebuie să ruleze pe mașina pe care face capture live (Windows, fizic — nu RDP/virtual).

atm calibrate           # countdown 3s default; pune --delay 10 dacă vrei mai mult timp

Flow:

  1. Dialog: substring din titlul ferestrei chart-ului (ex. TradeStation sau DIA). Se salvează în config pentru auto-focus ulterior.
  2. Mesaj "Ready?" → click OK → countdown 3s în terminal. Alt-tab pe TradeStation, minimizează tot ce-l acoperă.
  3. Se face screenshot full-desktop, apare o fereastră Tk scalată.
  4. Trage un dreptunghi peste chart (include și banda M2D MAPS). Enter = confirmă. Esc = anulează.
  5. Click pas cu pas pe regiunea selectată:
    • M2D MAPS strip: colț stânga-sus + colț dreapta-jos
    • Un click pe fiecare culoare: turquoise, yellow, dark_green, dark_red, light_green, light_red, gray + background (8 total — "Skip" dacă o culoare nu-i vizibilă acum)
    • Chart: colț stânga-sus + colț dreapta-jos (pentru detecția de linii în Faza-B)
    • Două prețuri cunoscute pe axa Y (pixel y → introduci prețul)
    • Canary: colț stânga-sus + colț dreapta-jos pe un element UI stabil (etichetă axă, bară titlu)
  6. Save → scrie configs/YYYY-MM-DD-HHMM.toml + marcaj configs/current.txt. Preia credențialele Discord/Telegram din env (ATM_DISCORD_URL, ATM_TG_TOKEN, ATM_TG_CHAT) dacă sunt setate; altfel pune REPLACE_ME — editezi TOML-ul manual.

⚠️ Reguli critice la calibrare (evită incidentul 2026-04-17)

1. Click EXCLUSIV pe dot-ul din DREAPTA al strip-ului. Banda M2D MAPS e istoric: dot-ul din dreapta = activ/curent, restul sunt mai vechi. TradeStation desenează dot-ul activ mai strălucitor decât cele vechi. Detector-ul live citește MEREU dot-ul din dreapta. Dacă dai click pe unul din stânga, culoarea calibrată e mai întunecată decât realitatea → clasificare greșită live (dark_red poate ajunge citit ca light_red, de exemplu).

2. Canary pe un pixel STATIC. NU pune regiunea canary peste: volume bar, preț curent, ceas/timestamp. Orice se schimbă natural în acea zonă declanșează drift-pause silent → bot-ul se oprește din detecție fără alertă vizibilă (asta s-a întâmplat la 22:25 pe 17.04, drift=129). Alege: o etichetă de axă, un titlu de panel, un colț de bordură.

3. Calibrează în mijlocul unei sesiuni active, nu dimineața înainte de deschidere. Dot-urile sunt clar vizibile și reflectă exact aceleași setări de rendering ca la live.

Ce scrie în TOML

  • chart_window_region = {x, y, w, h} — dreptunghi absolut virtual-desktop. Capture-ul la runtime crop-ează exact aceeași cutie, deci fereastra nu trebuie mutată după calibrare.
  • dot_roi, chart_roi, canary.roi — coordonate relative la regiunea selectată.
  • RGB per culoare (eșantionat cu saturation-snap într-o rază de 15px de click, media unui box 5x5 în jurul pixelului snapped).
  • y_axis — pereche de interpolare liniară.
  • canary.baseline_phash al ROI-ului canary.

Tips de sampling:

  • Click pe culori chiar vizibile acum în istoricul dot-urilor. Dacă o culoare nu-i vizibilă, skip — atm dryrun îți zice dacă valoarea ratată nu se potrivește cu dot-uri reale.
  • Tolerance default: 60 pentru dot-uri, 25 pentru background. Strângi în TOML după dryrun dacă apar misclasificări.

Smoke-test după calibrare

atm debug --delay 5

Ia un frame. Salvează logs/debug_full_<ts>.png, logs/debug_dot_roi_<ts>.png, logs/debug_annotated_<ts>.png. Tipărește:

window_found: True
dot_found:    True
rgb:          (114, 114, 114)
classified:   gray  distance=24  confidence=0.79
accepted:     True  color=gray

Deschizi PNG-ul adnotat: dreptunghi galben = dot_roi, cerc roșu = dot detectat. Cercul trebuie să pice pe dot-ul colorat cel mai din dreapta din banda M2D MAPS. Dacă nu:

  • Cerc la mijloc de strip → alt window e sub regiunea de capture (adu TradeStation în față).
  • Cerc pe element UI non-dot → dot_roi prea larg; recalibrează mai îngust.
  • color=None + UNKNOWN → tolerances prea strânse SAU RGB-urile eșantionate nu se potrivesc cu dot-urile reale; recalibrează cu click pe dot-uri reale.

Validare offline a calibrării

Verifici dacă calibrarea actuală clasifică corect un set de frame-uri etichetate manual, fără să aștepți sesiunea live. Esențial după orice recalibrare.

atm validate-calibration samples/calibration_labels.json

Format input (samples/calibration_labels.json):

[
  {"path": "logs/fires/20260417_201500_arm_sell.png", "expected": "yellow", "note": "primul arm"},
  {"path": "logs/fires/20260417_205302_ss.png",       "expected": "dark_red"},
  {"path": "logs/fires/20260417_210441_ss.png",       "expected": "light_red"}
]

Output: per fiecare frame PASS/FAIL + culoarea detectată + top 3 candidați după distanță RGB + sugestii de pixel pentru misclasificări.

Exit code:

  • 0 — 100% PASS (poți porni live în siguranță)
  • 1 — cel puțin un FAIL
  • 2 — input invalid/lipsă

Workflow de corectare iterativă (când apare o alertă greșită live)

Scenariu: ai rulat o sesiune live, ai văzut pe chart o culoare pe care bot-ul n-a detectat-o (sau a detectat greșit).

  1. În timpul sesiunii, când observi o culoare nouă pe chart, trimite /ss în Telegram. Asta salvează un screenshot în logs/fires/ cu timestamp.
  2. După sesiune, deschizi samples/calibration_labels.json și adaugi o intrare nouă pentru fiecare screenshot relevant:
    {"path": "logs/fires/20260420_151234_ss.png", "expected": "dark_green", "note": "văzut live, ratat de bot"}
    
    Câmpul expected = culoarea pe care TU ai văzut-o pe chart (nu ce a zis bot-ul).
  3. Rulează validarea:
    atm validate-calibration samples/calibration_labels.json
    
  4. Interpretează rezultatul:
    • Toate PASS → calibrarea ține, continui live fără modificări.
    • Măcar un FAIL → output-ul îți arată pixelul real (ex. RGB(128, 0, 0)), centrul curent din TOML (ex. dark_red RGB(83, 0, 0)) și distanța. Două opțiuni:
      • Fix tactic rapid: editezi TOML-ul direct, muți centrul culorii aproape de pixelul observat. Rulezi iar validate-calibration. Te oprești când e PASS.
      • Fix complet: la următoarea sesiune live completă, rulezi atm calibrate de la zero pe Windows, cu disciplina cele 3 reguli critice de mai sus (rightmost dot, pixel static pentru canary, în timpul unei sesiuni active).
  5. Acumulezi mai multe samples în timp. Obiectiv: 2-3 intrări per culoare în calibration_labels.json. Cu cât fișierul are mai multe etichete, cu atât calibrarea următoare e validată mai solid.

Exemplu real — incidentul 2026-04-17

La 20:53 s-a afișat un dark_red pe chart dar bot-ul l-a citit ca light_red (alertă ratată). Root cause: calibrarea anterioară (2026-04-16-0703.toml) a fost făcută dând click pe dot-uri istorice (mai întunecate), nu pe dot-ul activ din dreapta.

Fix aplicat în 2026-04-18-1220.toml, pe bază de evidență live:

Culoare Centru vechi Pixel live observat Centru nou
dark_red (83, 0, 0) (128, 0, 0) (128, 0, 0)
light_red (153, 0, 0) (171, 0, 0) (171, 0, 0)
dark_green (0, 77, 0) (0, 122, 0) (ajustat proporțional: +45 pe G)
light_green (0, 153, 0) (0, 171, 0) (ajustat proporțional: +18 pe G)

yellow, turquoise, gray, background — lăsate neschimbate (nu am dovezi live care să justifice ajustarea).

După fix: atm validate-calibration → 3/3 PASS, confidence 1.00 pe ambele roșuri.

Rollback dacă ceva merge prost:

echo "2026-04-16-0703.toml" > configs/current.txt

Sesiunea live

# Sesiunea de azi 16:3023:00 România local
atm run --start-at 16:30 --stop-at 23:00

# Fără limită
atm run

# Durată fixă (ore)
atm run --duration 2

# Linux/WSL smoke (rulează pe fișiere din samples/)
atm run --capture-stub --duration 0.05

Startup:

  1. Așteptare wall-clock până la --start-at (dacă e setat).
  2. pygetwindow.activate() pe prima fereastră care conține cfg.window_title — aduce TradeStation în față (restaurează dacă-i minimizată).
  3. Countdown 5s (--startup-delay).
  4. Primul frame + check canary. Status (drift=X/Y sau capture_failed) e inclus în ping-ul de start.
  5. Ping "ATM started" pe Discord + Telegram.
  6. Loop principal: la fiecare loop_interval_s (default 5s) — capture → canary → detect → FSM → poate notifică → poate Faza-B.
  7. La --stop-at (sau --duration): ping "ATM stopped", apoi exit.

Comportament per ciclu:

  • Drift canary → auto-pause + alertă Telegram single-shot (⚠️ Canary drift=N — monitorizare pauzată). Anulezi cu /resume force în Telegram, sau repornești cu flag-ul de pauză șters.
  • Detector raportează UNKNOWN → rămâne în starea curentă (loghează noise).
  • Schimbare de culoare → frame complet salvat în samples/YYYYMMDD_HHMMSS_<color>.png (pentru corpus).
  • FIRE (BUY/SELL, nu locked) → PNG adnotat salvat în logs/fires/, atașat la alertă, LevelsExtractor armed.
  • Phase-skip backstop (fire_on_phase_skip=true default) → ARMED → light_red/light_green direct (dark_* ratat) emite totuși alertă ⚠️ PHASE SKIP cu screenshot. Lockout-ul FSM previne spam.
  • Faza-B completă → push "Levels SL=… TP1=… TP2=…".
  • Heartbeat la fiecare heartbeat_min minute.

Ține PowerShell minimizat în timpul sesiunii ca să nu acopere TradeStation.

Fereastra orelor de trading

Configurezi din TOML (sursă adevăr: NYSE local, timezone-aware — DST-ul e gestionat automat):

[options.operating_hours]
enabled     = true
timezone    = "America/New_York"     # validat fail-fast la load
weekdays    = ["MON", "TUE", "WED", "THU", "FRI"]
start_hhmm  = "09:30"                # deschidere NYSE
stop_hhmm   = "16:00"                # închidere NYSE

Tick-urile din afara ferestrei sunt skipped (logged doar la tranziție). La traversarea boundary-ului bot-ul emite market_open / market_closed în Telegram — o singură dată per tranziție. Pornirea în-fereastră nu emite alertă spurioasă.

Override din CLI (bat TOML-ul):

atm run --tz America/New_York --weekdays MON,TUE,WED,THU,FRI --oh-start 09:30 --oh-stop 16:00

--oh-start / --oh-stop sunt diferite de --start-at / --stop-at. --start-at / --stop-at = wall-clock session bounds (când pornește procesul și când se oprește). --oh-start / --oh-stop = fereastra NYSE în care detecția rulează efectiv în interiorul sesiunii. Se combină.

Comenzi Telegram

Trimiți în chat-ul bot-ului:

Comandă Efect
/ss sau /screenshot Screenshot acum
/status Stare FSM + motiv pauză + fereastră open/closed
/pause Suspendă detecția (heartbeat-urile continuă)
/resume Elimină DOAR pauza user. Dacă Canary e drift-paused, rămâne paused — folosește /resume force
/resume force Elimină și drift-pause-ul canary (după recalibrare)
/3 sau /interval 3 Interval auto-screenshot = 3 min
/stop Oprește scheduler-ul de screenshot

Doar allowed_chat_ids sunt acceptate. După 3 401 consecutive, poller-ul intră în mod degradat.


După sesiune

atm label samples     # UI Tk — etichetezi fiecare frame salvat cu culoarea reală
atm dryrun samples    # replay prin detector + FSM; exit 0 dacă precision=100%, recall≥95%

Dacă gate-ul pică, ajustezi tolerance per culoare în configs/<current>.toml, sau recalibrezi eșantioanele care n-au potrivit. Rulezi iar atm dryrun până trece. Numai atunci ai încredere în semnalele live.

Evidență trade-uri:

atm journal                      # înregistrare interactivă după un trade real
atm report --week 2026-16        # win rate săptămânal + PnL în R + slippage

Note DPI / multi-monitor

  • Regiunea din calibrare e absolută virtual-desktop; runtime capture folosește același dreptunghi. Nu muta fereastra TradeStation după calibrare. Canary prinde drift-ul și pauzează automat.
  • Schimbi DPI scaling sau muți pe un alt monitor cu DPI diferit → recalibrezi.
  • RDP / desktop virtual: mss poate returna frame-uri negre peste RDP. Rulează local pe aceeași mașină fizică pe care e TradeStation.

Troubleshooting

Simptom Cauză probabilă Fix
capture_failed în ping-ul de start chart_window_region referă coords off-screen (alt layout monitor) Recalibrează.
Canary la startup arată drift=X/8 cu X ≫ 8 Alt window e în regiunea de capture TradeStation trebuie să fie ferestra la cfg.chart_window_region. Relansează.
WARN: no window contains 'xxx' la start cfg.window_title nu prinde nimic Editează window_title în TOML cu un substring unic pentru TradeStation.
Nu vin alerte deși ar trebui Verifică logs/YYYY-MM-DD.jsonlevent=frame au culoare acceptată? trigger setat? Dacă mereu UNKNOWN → tolerances prea strânse SAU RGB-urile calibrate nu se potrivesc. Rulează atm validate-calibration. Dacă trigger dar locked=true → lockout de la fire anterior, normal.
Alertă pe culoare greșită (ex. dark_red → light_red) Calibrarea a luat dot istoric, nu activ Rulează atm validate-calibration. Corectezi tactic în TOML sau recalibrezi cu regula rightmost dot.
Discord OK, Telegram tace (sau invers) logs/dead_letter.jsonl are alertele eșuate + eroarea Fixezi credențiale în TOML, restart.
Heartbeat arată telegram: failed > 0 Telegram a răspuns ok:false Check logs/dead_letter.jsonl pentru error_str / description. Comun: bot-ul nu-a fost pornit de user în Telegram, sau chat_id greșit (channel vs group vs DM).
Bot-ul "moare" după N ore, heartbeat merge dar comenzile nu răspund Era bug-ul de hang din 2026-04-17 — drain coadă de comenzi sărit când Canary paused Fixat în c5024ce. Update git pull.

Windows Task Scheduler (producție)

Pentru rulare automată zilnică care supraviețuiește reboot-urilor:

  1. Task Scheduler → Create Task → nume 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: debifează "Start only if AC power" (dacă e laptop)
  6. Settings: "If task runs longer than 7 hours → stop"

Click-right → Run, să testezi manual. Check DST schimbare de două ori pe an (prima săptămână din martie / octombrie).


Referință rapidă comenzi

atm calibrate [--screenshot PATH] [--delay SEC]      # wizard Tk
atm debug [--delay SEC]                              # one-shot capture + detect
atm label SAMPLES_DIR                                # etichetare Tk
atm dryrun SAMPLES_DIR                               # gate pe corpus
atm validate-calibration LABEL_FILE.json             # gate offline clasificare culori
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]                            # înregistrare interactivă
atm report [--week YYYY-WW] [--file PATH]            # raport săptămânal

Exit codes:

  • atm dryrun — 0 pass, 1 fail.
  • atm validate-calibration — 0 toate PASS, 1 orice FAIL, 2 input invalid.
  • Restul: standard.

Evenimente audit

Scrise în logs/YYYY-MM-DD.jsonl. Cele adăugate recent:

Event Payload Când
canary_drift_paused distance Primul tick cu drift după o stare curată; emite alertă Telegram
user_paused /pause primit
user_resumed was_drift, was_user, force /resume sau /resume force
market_open / market_closed reason Boundary fereastră operating-hours (o dată per tranziție; nu la startup)
phase_skip_fire direction Alertă backstop când ARMED→light_* direct
command_error action, error Excepție la dispatch (izolată de loop-ul de detecție)
Description
No description provided
Readme 10 MiB
Languages
Python 99.9%