# 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)
├── calibration/          # corpus auto-suficient pentru validare + regresie
│   ├── calibration_labels.json    # etichete per-frame pentru atm validate-calibration
│   ├── scenarios.json             # secvențe FSM (arm→prime→trigger etc.) pentru test_scenarios_regression.py
│   ├── frames/                    # PNG-uri numite {ts}_{color}.png, izolate de logs/fires și samples
│   └── README.md
├── 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 (tranzitoriu, se poate goli)
│   └── calibrate_capture_*.png / debug_*.png  # artefacte debug (gitignored)
├── samples/              # frame complet salvat automat la fiecare schimbare de culoare (tranzitoriu)
├── 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/                # 192 teste pytest (184 core + 8 scenarii regresie)
└── 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.

---

## Secrets

Credențialele Discord/Telegram NU se țin în TOML — trăiesc în `.env` la rădăcina proiectului:

```bash
cp .env.example .env
# apoi editezi .env cu valorile reale
```

Variabile necesare:

| Variabilă | Ce e |
|---|---|
| `ATM_DISCORD_URL` | Webhook URL-ul Discord (canalul unde vin alertele) |
| `ATM_TG_TOKEN` | Token-ul bot-ului Telegram (de la `@BotFather`) |
| `ATM_TG_CHAT` | Chat ID (group-ul sau user-ul; prefix `-` pentru group) |

`.env` e în `.gitignore` — secretele nu ajung pe git. `configs/*.toml` **pot** fi comise pe git (calibrare pură, safe to version).

La pornire, dacă `.env` e găsit, loader-ul printează pe stderr:

```
[atm.config] .env: loaded 3 vars (0 overridden by shell)
```

**⚠️ Shell env wins peste `.env`.** Dacă ai făcut `export ATM_TG_TOKEN=...` cândva în `.bashrc` / profil, aceasta override-uiește `.env` — verifică cu `printenv | grep ATM_`. Mesajul `(N overridden by shell)` te avertizează când se întâmplă.

Config-ul se refuză să pornească dacă:
- lipsește oricare din cele 3 variabile (mesaj cu numele variabilei + hint către `.env.example`);
- `ATM_DISCORD_URL` sau `ATM_TG_TOKEN` conține `REPLACE_ME` (ai copiat `.env.example` dar n-ai editat);
- `ATM_TG_CHAT` nu-i numeric (opțional cu `-` la început pentru group).

---

## Dev

```bash
pytest -q                        # toate testele (192+)
pytest tests/test_commands.py    # un modul specific
pytest tests/test_scenarios_regression.py -v   # scenarii FSM pe imagini reale
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ă |
| `test_scenarios_regression.py` | secvențe FSM pe frame-uri reale (arm→prime→trigger, phase_skip, catchup, post-fire suppression) |

---

## 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`. TOML-ul conține doar calibrare — secretele Discord/Telegram se țin în `.env` la rădăcina proiectului (vezi secțiunea **Secrets** de mai jos).

### ⚠️ 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 calibration/calibration_labels.json
```

Format input (`calibration/calibration_labels.json`):
```json
[
  {"path": "calibration/frames/20260420_171501_yellow.png",    "expected": "yellow"},
  {"path": "calibration/frames/20260420_172104_dark_red.png",  "expected": "dark_red"},
  {"path": "calibration/frames/20260420_173004_light_red.png", "expected": "light_red"}
]
```

Frame-urile sunt copiate în `calibration/frames/` cu numele `{timestamp}_{culoare}.png`
— numele reflectă ground truth-ul vizibil pe dot, nu label-ul de eveniment din
`logs/fires/`. Directorul e auto-suficient: `samples/` și `logs/fires/` se pot
goli oricând fără să afecteze validarea.

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ă

### Trei surse de frame-uri, roluri distincte

| Sursă | Unde se salvează | Cum se populează | Folosit de |
|---|---|---|---|
| `calibration/frames/` | PNG-uri curate `{ts}_{color}.png` | **manual** — copii din `logs/fires/` doar cele verificate | `atm validate-calibration` + `test_scenarios_regression.py` |
| `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 | sursă pentru `calibration/frames/` |

`calibration/` e singurul director **persistent**. Celelalte două se pot goli
după ce ai extras ce-ți trebuie — tranzitorii prin natură.

### Regresie FSM pe frame-uri reale

`calibration/scenarios.json` definește secvențe ordonate (arm → prime → trigger,
phase_skip, catchup, suprimare dark_* post-fire) care refolosesc aceleași frame-uri.
`tests/test_scenarios_regression.py` rulează fiecare secvență prin pipeline-ul real
`Detector → _handle_tick`, asertând per pas: culoarea detectată, tranziția FSM
(prev→next + reason + trigger), alertele emise prin notifier, și starea
scheduler-ului (running/stopped).

Extensii fără cod nou: adaugi un scenariu în JSON și pytest-ul îl consumă automat
(parametrizat pe `id`). Dacă scenariul cere o combinație de culori noi, copii
frame-ul în `calibration/frames/` cu numele `{timestamp}_{culoare}.png`.

**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 `calibration/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 calibration/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 `calibration/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.

### Exemplu real — incidentul 2026-04-20/21 (culori saturate)

User a observat screenshot-uri poll periodice după ce un trigger BUY/SELL părea deja declanșat. Dovadă: `logs/fires/20260420_214908_poll.png` avea pixel verde pur `(0, 255, 0)` (trigger light_green) dar detector-ul îl clasifica `UNKNOWN`. Investigație: 27/114 PNG-uri din corpus ieșeau UNKNOWN pentru că paleta din `2026-04-18-1220.toml` avea centrele celor patru culori luminoase **prea întunecate** — distanța până la pixelul real depășea toleranța de 60.

Fix aplicat în `2026-04-21-recalib.toml`:

| Culoare | Centru vechi | Pixel live observat | Centru nou | d(vechi) |
|---|---|---|---|---|
| turquoise | (0, 153, 153) | (0, 253, 253) | **(0, 253, 253)** | 141 |
| yellow | (153, 153, 0) | (253, 253, 0) | **(253, 253, 0)** | 141 |
| light_green | (0, 171, 0) | (0, 255, 0) | **(0, 255, 0)** | 84 |
| light_red | (171, 0, 0) | (255, 0, 0) | **(255, 0, 0)** | 84 |

dark_green, dark_red, gray, background — neschimbate (nu ieșeau UNKNOWN).

Consecință invizibilă pentru user: fără trigger acceptat de FSM, starea rămânea blocată în `PRIMED_*` → `ScreenshotScheduler` nu primea `reason=fire/cooled/phase_skip/opposite_rearm` → polling continuu la 3 min ore în șir.

După fix: corpus 27→0 UNKNOWN pe culorile luminoase (restul 9 sunt pixeli off-ROI crem, nu dot-uri). `atm validate-calibration calibration/calibration_labels.json` → 16/16 PASS.

**Lesson learned:** la recalibrare cu wizard-ul Tk, dacă folosești o imagine screenshot (nu captură live), pipeline-ul de saturation-snap poate rata pixelul cel mai saturat și să ia un dot ușor desaturat. Regulă: după wizard, verifică imediat cu `atm validate-calibration` pe un corpus cu toate 7 culorile. Dacă vreo culoare iese UNKNOWN, corectează manual în TOML cu pixelul real observat.

**Rollback** dacă ceva merge prost:
```bash
echo "2026-04-18-1220.toml" > configs/current.txt   # sau 2026-04-16-0703.toml
```

---

## Sesiunea live

```powershell
# Sesiunea de azi 16:30–23: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.

**Meniul slash (autocomplete pe `/` în client).** Se înregistrează **automat** la fiecare `atm run` (deci și prin `atm.bat`) — nu trebuie să faci nimic. Pentru (re)set/list/clear manual fără să pornești aplicația:

```powershell
.\.venv\Scripts\python.exe scripts\register_telegram_commands.py          # înregistrează
.\.venv\Scripts\python.exe scripts\register_telegram_commands.py --list   # ce e setat acum
.\.venv\Scripts\python.exe scripts\register_telegram_commands.py --clear  # șterge
```

Discord este **webhook outbound** în acest proiect (doar trimite alerte) — nu primește comenzi și nu poate avea slash commands fără un bot application separat.

---

## 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. |
| Poll-uri periodice continuă deși un trigger BUY/SELL s-a afișat pe chart | Trigger-ul a ieșit UNKNOWN (pixel saturat, paletă întunecată) → FSM blocat în PRIMED → scheduler nu primește `fire/cooled/phase_skip` | Rulează `atm validate-calibration calibration/calibration_labels.json`. Dacă vreo culoare luminoasă iese UNKNOWN, actualizezi centrul RGB în TOML la pixelul real observat. Vezi incidentul 2026-04-20/21. |

---

## 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) |
