feat(run): auto-save corpus samples + annotated FIRE screenshots

- samples/: full frame saved every time accepted colour changes (enough
  diversity for the labelled corpus, no constant-N-seconds flood).
- logs/fires/: annotated frame saved on every BUY/SELL trigger, attached
  to the Discord/Telegram Alert so the push includes a visual.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-16 11:33:54 +00:00
parent dec3b03d53
commit ec86f52f1f

View File

@@ -377,6 +377,12 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
start = time.monotonic()
heartbeat_due = start + cfg.heartbeat_min * 60
levels_extractor = None
last_saved_color: str | None = None
samples_dir = Path("samples")
samples_dir.mkdir(exist_ok=True)
fires_dir = Path("logs") / "fires"
fires_dir.mkdir(parents=True, exist_ok=True)
import cv2 # type: ignore[import-untyped]
try:
while duration_s is None or (time.monotonic() - start) < duration_s:
@@ -400,11 +406,31 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
"ts": now, "event": "tick",
"color": res.color, "state": tr.next.value, "reason": tr.reason,
})
# corpus: save full frame on each new distinct color for later labeling
if res.color != last_saved_color:
ts_str = datetime.fromtimestamp(now).strftime("%Y%m%d_%H%M%S")
sample_path = samples_dir / f"{ts_str}_{res.color}.png"
try:
cv2.imwrite(str(sample_path), frame)
except Exception as exc:
audit.log({"ts": now, "event": "sample_save_failed", "error": str(exc)})
last_saved_color = res.color
# FIRE: annotate frame + save, attach to alert
if tr.trigger and not tr.locked:
ts_str = datetime.fromtimestamp(now).strftime("%Y%m%d_%H%M%S")
fire_path = fires_dir / f"{ts_str}_{tr.trigger}.png"
try:
annotated = frame.copy()
x, y, w, h = cfg.dot_roi.x, cfg.dot_roi.y, cfg.dot_roi.w, cfg.dot_roi.h
cv2.rectangle(annotated, (x, y), (x + w, y + h), (0, 255, 255), 2)
cv2.imwrite(str(fire_path), annotated)
except Exception:
fire_path = None
notifier.send(Alert(
kind="trigger",
title=f"{tr.trigger} signal",
body=f"@ {now}",
body=f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}",
image_path=fire_path,
direction=tr.trigger,
))
levels_extractor = LevelsExtractor(cfg, tr.trigger, now)