Compare commits

..

3 Commits

Author SHA1 Message Date
5ebe26e5d5 test(calibration): 8 labels + 3 scenarii noi (inclusiv regresie 2026-04-21)
calibration_labels.json: 16 → 24 entry-uri. Toate cele 8 frame-uri dumpate
automat de live loop azi (gray×5, dark_green×1, yellow×1, dark_red×1) trec
prin validate-calibration cu detector-ul curent (24/24 PASS).

scenarios.json: 8 → 11 scenarii:

1. buy_catchup_opposite_rearm_to_sell — REGRESSION pentru bug-ul de azi.
   Trace real din log: 17:00 catchup dark_green → PRIMED_BUY (synth arm+prime),
   apoi 17:45 yellow → ARMED_SELL via opposite_rearm. Verifică că dispatch-ul
   nou emite kind=opposite_rearm și că scheduler-ul se oprește.

2. buy_armed_gray_persist — gray între arm și prime ține ARMED_BUY
   (reason=persist). Acoperă o ramură FSM neacoperită.

3. buy_primed_gray_cooldown — gray după prime ucide ciclul (reason=cooled,
   IDLE, scheduler stop). Confirmă semantica M2D că chart-ul tăcut post-prime
   înseamnă setup expirat.

Total: 11/11 scenarii PASS, 238/238 teste (235 + 3 noi scenarii regresie).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:35:22 +03:00
75a17f9640 feat: auto-capture scrie direct în calibration/frames/ (elimină pasul manual)
Live loop-ul dumpa frame-uri pe schimbare de culoare în samples/, iar userul
copia manual cele utile în calibration/frames/ pentru labelling și regresie.
Pas inutil — acum scrie direct în corpus.

- samples_dir → calibration/frames/ (mkdir parents=True)
- stub capture (ATM_STUB_CAPTURE pentru smoke test Linux) citește din aceeași locație
- 8 PNG-uri orfane din samples/ (20260421_*) mutate în corpus
- CLAUDE.md clarifică: filename = culoarea detectată (poate fi greșită);
  calibration_labels.json rămâne singurul ground truth (manual)

Impact zero pe validate-calibration (iterează peste labels.json, ignoră fișiere
extra) și test_scenarios_regression.py (referă doar frame-uri curate din
scenarios.json).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:31:06 +03:00
ebc986abd3 docs(claude): scoate atm dryrun samples din quick-ref (corpus e calibration/)
samples/ nu mai e corpus-ul activ — calibration/frames/ + calibration_labels.json
au înlocuit workflow-ul vechi. `atm dryrun samples` rămâne comandă validă dar
necesită samples/labels.json care nu mai există în uzul curent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:26:51 +03:00
12 changed files with 142 additions and 9 deletions

View File

@@ -13,19 +13,18 @@ atm debug --delay 5 # one-shot capture + detect
atm validate-calibration calibration/calibration_labels.json # offline color gate
atm run --start-at 16:30 --stop-at 23:00 # live session
atm run --tz America/New_York --oh-start 09:30 --oh-stop 16:00 # NYSE window override
atm dryrun samples # corpus gate
pytest -q # 210+ tests (core + 8 scenarii regresie + env loader)
pytest -q # 230+ tests (core + 8 scenarii regresie + env loader)
pytest tests/test_scenarios_regression.py -v # FSM pe imagini reale
```
## Calibration corpus
`calibration/` — persistent, auto-suficient, safe to keep când `samples/` și `logs/fires/` se golesc. Conține:
- `frames/` — PNG-uri `{ts}_{color}.png` (ground truth în nume)
- `calibration_labels.json` — gate offline pentru `atm validate-calibration`
`calibration/` — persistent, auto-suficient. Conține:
- `frames/` — PNG-uri raw `{ts}_{color}.png` scrise **automat** de live loop la fiecare schimbare de culoare (filename = culoarea detectată, poate fi greșită)
- `calibration_labels.json`ground truth **manual** (gate offline pentru `atm validate-calibration`)
- `scenarios.json` — secvențe FSM pentru `tests/test_scenarios_regression.py`
Când adaugi un frame: copiezi din `logs/fires/` → redenumești `{ts}_{color}.png` → adaugi entry în JSON. Validare după orice recalibrare.
Workflow după sesiune: review frame-urile noi din `frames/`, adaugi entry-uri în `calibration_labels.json` cu culoarea pe care ai văzut-o TU pe chart (nu neapărat cea din filename), rulezi `atm validate-calibration`.
## Telegram commands (live)

View File

@@ -78,5 +78,45 @@
"path": "calibration/frames/20260420_185702_gray.png",
"expected": "gray",
"note": "idle gray dot in poll; rgb=(128,128,128)"
},
{
"path": "calibration/frames/20260421_164210_gray.png",
"expected": "gray",
"note": "2026-04-21 16:42 poll — gray între sesiuni; rgb=(128,128,128)"
},
{
"path": "calibration/frames/20260421_164452_gray.png",
"expected": "gray",
"note": "2026-04-21 16:44 poll — gray idle; rgb=(128,128,128)"
},
{
"path": "calibration/frames/20260421_165209_gray.png",
"expected": "gray",
"note": "2026-04-21 16:52 poll — gray idle; rgb=(128,128,128)"
},
{
"path": "calibration/frames/20260421_170045_dark_green.png",
"expected": "dark_green",
"note": "2026-04-21 17:00 BUY prime catchup (synth arm+prime); rgb=(0,128,0)"
},
{
"path": "calibration/frames/20260421_174502_yellow.png",
"expected": "yellow",
"note": "2026-04-21 17:45 opposite_rearm PRIMED_BUY→ARMED_SELL — frame bug regresiv; rgb=(253,253,0)"
},
{
"path": "calibration/frames/20260421_174804_gray.png",
"expected": "gray",
"note": "2026-04-21 17:48 gray persist în ARMED_SELL; rgb=(128,128,128)"
},
{
"path": "calibration/frames/20260421_220346_dark_red.png",
"expected": "dark_red",
"note": "2026-04-21 22:03 SELL prime seara; rgb=(128,0,0)"
},
{
"path": "calibration/frames/20260421_222108_gray.png",
"expected": "gray",
"note": "2026-04-21 22:21 gray idle; rgb=(128,128,128)"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

View File

@@ -244,5 +244,95 @@
"expected_scheduler_running": false
}
]
},
{
"id": "buy_catchup_opposite_rearm_to_sell",
"description": "REGRESSION 2026-04-21: real trace — catchup pe dark_green la 17:00 → PRIMED_BUY (synth arm+prime), apoi yellow la 17:45 → ARMED_SELL via opposite_rearm. Înainte de fix, bug-ul era dispatch-ul tăcut pentru opposite_rearm (zero alert). Acum trebuie să emită kind=opposite_rearm și să oprească scheduler-ul.",
"steps": [
{
"frame": "calibration/frames/20260421_170045_dark_green.png",
"expected_color": "dark_green",
"expected_reason": "prime",
"expected_state": "PRIMED_BUY",
"expected_trigger": null,
"expected_new_alerts": ["arm", "prime"],
"expected_scheduler_running": true
},
{
"frame": "calibration/frames/20260421_174502_yellow.png",
"expected_color": "yellow",
"expected_reason": "opposite_rearm",
"expected_state": "ARMED_SELL",
"expected_trigger": null,
"expected_new_alerts": ["opposite_rearm"],
"expected_scheduler_running": false
}
]
},
{
"id": "buy_armed_gray_persist",
"description": "Gray între arm și prime nu pierde ARMED_BUY (reason=persist, scheduler inactiv).",
"steps": [
{
"frame": "calibration/frames/20260420_200002_turquoise.png",
"expected_color": "turquoise",
"expected_reason": "arm",
"expected_state": "ARMED_BUY",
"expected_trigger": null,
"expected_new_alerts": ["arm"],
"expected_scheduler_running": false
},
{
"frame": "calibration/frames/20260421_164210_gray.png",
"expected_color": "gray",
"expected_reason": "persist",
"expected_state": "ARMED_BUY",
"expected_trigger": null,
"expected_new_alerts": [],
"expected_scheduler_running": false
},
{
"frame": "calibration/frames/20260420_185102_dark_green.png",
"expected_color": "dark_green",
"expected_reason": "prime",
"expected_state": "PRIMED_BUY",
"expected_trigger": null,
"expected_new_alerts": ["prime"],
"expected_scheduler_running": true
}
]
},
{
"id": "buy_primed_gray_cooldown",
"description": "Gray după prime ucide ciclul (reason=cooled, IDLE, scheduler oprit). Design M2D: setup expiră dacă chart-ul tace după priming.",
"steps": [
{
"frame": "calibration/frames/20260420_200002_turquoise.png",
"expected_color": "turquoise",
"expected_reason": "arm",
"expected_state": "ARMED_BUY",
"expected_trigger": null,
"expected_new_alerts": ["arm"],
"expected_scheduler_running": false
},
{
"frame": "calibration/frames/20260420_185102_dark_green.png",
"expected_color": "dark_green",
"expected_reason": "prime",
"expected_state": "PRIMED_BUY",
"expected_trigger": null,
"expected_new_alerts": ["prime"],
"expected_scheduler_running": true
},
{
"frame": "calibration/frames/20260421_174804_gray.png",
"expected_color": "gray",
"expected_reason": "cooled",
"expected_state": "IDLE",
"expected_trigger": null,
"expected_new_alerts": [],
"expected_scheduler_running": false
}
]
}
]

View File

@@ -1382,8 +1382,12 @@ async def run_live_async(cfg, duration_s=None, capture_stub: bool = False) -> No
start = time.monotonic()
heartbeat_due = time.monotonic() + cfg.heartbeat_min * 60
samples_dir = Path("samples")
samples_dir.mkdir(exist_ok=True)
# Dump raw frames direct în corpusul de calibrare — fără pas manual
# de copy-paste din samples/. calibration_labels.json rămâne manual
# (filename-ul folosește culoarea detectată, care poate fi greșită;
# curarea labels.json e ce face validate-calibration truthful).
samples_dir = Path("calibration") / "frames"
samples_dir.mkdir(parents=True, exist_ok=True)
fires_dir = Path("logs") / "fires"
fires_dir.mkdir(parents=True, exist_ok=True)
try:
@@ -1496,7 +1500,7 @@ def _build_capture(cfg, capture_stub: bool = False):
if use_stub:
import itertools
samples_dir = Path("samples")
samples_dir = Path("calibration") / "frames"
pngs = sorted(samples_dir.glob("*.png")) if samples_dir.exists() else []
_cycle = itertools.cycle(pngs) if pngs else None