Files
atm/README.md
Marius Mutu 42a1a0e7fd feat: /help command, atm.bat launcher, tzdata fix pentru Windows
- Telegram /h /help — listă comenzi în română
- atm.bat — pornire cu venv local automat, pip install la primul run
- tzdata adăugat în deps principale cu marker sys_platform==win32
- README: secțiuni dev, instalare Windows, flow-uri calibrare

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 13:11:44 +03:00

439 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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+.
### Windows (producție)
```powershell
python -m venv .venv
.venv\Scripts\activate
pip install -e ".[windows]"
# → creează .venv\Scripts\atm.exe
atm --help
```
`[windows]` aduce `mss`, `pygetwindow`, `pywin32`. Fără venv, `pip install -e ".[windows]"` direct în Python-ul global funcționează la fel.
Pornire rapidă cu scriptul inclus — instalează automat la primul run:
```powershell
atm.bat # prima rulare: pip install + atm run
atm.bat run --stop-at 23:00
atm.bat debug
```
### WSL / Linux (dev + teste)
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```
`[dev]` aduce `pytest`, `pytest-cov`, `pytest-asyncio`. Nu include dependențele Windows (`mss`, `pygetwindow`, `pywin32`) — nu rulează capture live.
---
## Dev
```bash
pytest -q # toate testele (184+)
pytest tests/test_commands.py # un modul specific
pytest -q --cov=atm --cov-report=term-missing # cu coverage
```
Smoke-test fără Windows (stub de captură din `samples/`):
```bash
atm run --capture-stub --duration 0.05
```
Structura testelor:
| Fișier | Ce acoperă |
|---|---|
| `test_commands.py` | parsing comenzi Telegram |
| `test_config.py` | loader TOML, attach_screenshots |
| `test_handle_tick.py` | loop principal, snapshot, FSM |
| `test_main.py` | lifecycle, operating hours, canary, dispatcher |
| `test_validate.py` | gate offline clasificare culori |
| `test_canary.py` | drift + callback pauză |
---
## 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).
```powershell
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
```powershell
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.
```bash
atm validate-calibration samples/calibration_labels.json
```
Format input (`samples/calibration_labels.json`):
```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ă
### Două corpus-uri, două scopuri
| Corpus | Unde se salvează | Cum se populează | Folosit de |
|---|---|---|---|
| `samples/` | frame complet la fiecare **schimbare de culoare** detectată | automat de `atm run` | `atm label` + `atm dryrun` |
| `logs/fires/` | screenshot adnotat la fiecare alertă BUY/SELL, `/ss` manual, **interval automat `/3`** | manual sau scheduler | `atm validate-calibration` |
**Flow A — calibrare fină cu screenshots automate (`/3`)**
Util când vrei să acumulezi repede frame-uri din culori reale, fără să aștepți schimbări de culoare.
1. **În sesiunea live**, trimite `/3` în Telegram → bot-ul face screenshot automat la 3 minute și îl salvează în `logs/fires/*_ss.png`. Oprești cu `/stop`.
2. **După sesiune**, adaugi intrări în `samples/calibration_labels.json` pentru fiecare screenshot relevant, cu culoarea pe care ai văzut-o TU pe chart:
```json
{"path": "logs/fires/20260420_151234_ss.png", "expected": "dark_green", "note": "văzut live, ratat de bot"}
```
3. **Rulează validarea:**
```bash
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.
**Flow B — gate de precizie pe corpus de schimbări de culoare**
`atm run` salvează automat în `samples/` un frame complet la fiecare schimbare de culoare detectată. După sesiune:
```powershell
atm label samples # UI Tk — etichetezi fiecare frame cu culoarea reală văzută pe chart
atm dryrun samples # replay prin detector + FSM; exit 0 dacă precision=100%, recall≥95%
```
Dacă gate-ul pică, ajustezi `tolerance` per culoare în TOML sau corectezi eșantioanele nepotrivite, apoi rulezi iar `atm dryrun` până trece.
### 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** — două opțiuni pentru a captura dovezi:
- `/ss` în Telegram → un screenshot instant în `logs/fires/`
- `/3` în Telegram → screenshots automate la 3 min în `logs/fires/` (util dacă nu ești la monitor continuu); oprești cu `/stop`
2. **După sesiune**, adaugi intrările relevante în `samples/calibration_labels.json` cu culoarea corectă și rulezi `atm validate-calibration` (Flow A de mai sus).
3. Dacă apar FAIL-uri, aplici fix tactic în TOML sau recalibrezi complet.
### 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:
```bash
echo "2026-04-16-0703.toml" > configs/current.txt
```
---
## Sesiunea live
```powershell
# 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):
```toml
[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 |
| `/h` sau `/help` | Listă scurtă a tuturor comenzilor disponibile |
Doar `allowed_chat_ids` sunt acceptate. După 3 `401` consecutive, poller-ul intră în mod degradat.
---
## După sesiune
```powershell
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.**
Pentru calibrare fină a clasificării de culori (Flow A cu `/3`), vezi secțiunea **Validare offline a calibrării** de mai sus.
Evidență trade-uri:
```powershell
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.jsonl` — `event=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) |