Compare commits
3 Commits
66ffa4bb9a
...
5ebe26e5d5
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ebe26e5d5 | |||
| 75a17f9640 | |||
| ebc986abd3 |
11
CLAUDE.md
@@ -13,19 +13,18 @@ atm debug --delay 5 # one-shot capture + detect
|
|||||||
atm validate-calibration calibration/calibration_labels.json # offline color gate
|
atm validate-calibration calibration/calibration_labels.json # offline color gate
|
||||||
atm run --start-at 16:30 --stop-at 23:00 # live session
|
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 run --tz America/New_York --oh-start 09:30 --oh-stop 16:00 # NYSE window override
|
||||||
atm dryrun samples # corpus gate
|
pytest -q # 230+ tests (core + 8 scenarii regresie + env loader)
|
||||||
pytest -q # 210+ tests (core + 8 scenarii regresie + env loader)
|
|
||||||
pytest tests/test_scenarios_regression.py -v # FSM pe imagini reale
|
pytest tests/test_scenarios_regression.py -v # FSM pe imagini reale
|
||||||
```
|
```
|
||||||
|
|
||||||
## Calibration corpus
|
## Calibration corpus
|
||||||
|
|
||||||
`calibration/` — persistent, auto-suficient, safe to keep când `samples/` și `logs/fires/` se golesc. Conține:
|
`calibration/` — persistent, auto-suficient. Conține:
|
||||||
- `frames/` — PNG-uri `{ts}_{color}.png` (ground truth în nume)
|
- `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` — gate offline pentru `atm validate-calibration`
|
- `calibration_labels.json` — ground truth **manual** (gate offline pentru `atm validate-calibration`)
|
||||||
- `scenarios.json` — secvențe FSM pentru `tests/test_scenarios_regression.py`
|
- `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)
|
## Telegram commands (live)
|
||||||
|
|
||||||
|
|||||||
@@ -78,5 +78,45 @@
|
|||||||
"path": "calibration/frames/20260420_185702_gray.png",
|
"path": "calibration/frames/20260420_185702_gray.png",
|
||||||
"expected": "gray",
|
"expected": "gray",
|
||||||
"note": "idle gray dot in poll; rgb=(128,128,128)"
|
"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)"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
BIN
calibration/frames/20260421_164210_gray.png
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
calibration/frames/20260421_164452_gray.png
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
calibration/frames/20260421_165209_gray.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
calibration/frames/20260421_170045_dark_green.png
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
calibration/frames/20260421_174502_yellow.png
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
calibration/frames/20260421_174804_gray.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
calibration/frames/20260421_220346_dark_red.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
calibration/frames/20260421_222108_gray.png
Normal file
|
After Width: | Height: | Size: 243 KiB |
@@ -244,5 +244,95 @@
|
|||||||
"expected_scheduler_running": false
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1382,8 +1382,12 @@ async def run_live_async(cfg, duration_s=None, capture_stub: bool = False) -> No
|
|||||||
|
|
||||||
start = time.monotonic()
|
start = time.monotonic()
|
||||||
heartbeat_due = time.monotonic() + cfg.heartbeat_min * 60
|
heartbeat_due = time.monotonic() + cfg.heartbeat_min * 60
|
||||||
samples_dir = Path("samples")
|
# Dump raw frames direct în corpusul de calibrare — fără pas manual
|
||||||
samples_dir.mkdir(exist_ok=True)
|
# 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 = Path("logs") / "fires"
|
||||||
fires_dir.mkdir(parents=True, exist_ok=True)
|
fires_dir.mkdir(parents=True, exist_ok=True)
|
||||||
try:
|
try:
|
||||||
@@ -1496,7 +1500,7 @@ def _build_capture(cfg, capture_stub: bool = False):
|
|||||||
|
|
||||||
if use_stub:
|
if use_stub:
|
||||||
import itertools
|
import itertools
|
||||||
samples_dir = Path("samples")
|
samples_dir = Path("calibration") / "frames"
|
||||||
pngs = sorted(samples_dir.glob("*.png")) if samples_dir.exists() else []
|
pngs = sorted(samples_dir.glob("*.png")) if samples_dir.exists() else []
|
||||||
_cycle = itertools.cycle(pngs) if pngs else None
|
_cycle = itertools.cycle(pngs) if pngs else None
|
||||||
|
|
||||||
|
|||||||