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:
@@ -377,6 +377,12 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
|
|||||||
start = time.monotonic()
|
start = time.monotonic()
|
||||||
heartbeat_due = start + cfg.heartbeat_min * 60
|
heartbeat_due = start + cfg.heartbeat_min * 60
|
||||||
levels_extractor = None
|
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:
|
try:
|
||||||
while duration_s is None or (time.monotonic() - start) < duration_s:
|
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",
|
"ts": now, "event": "tick",
|
||||||
"color": res.color, "state": tr.next.value, "reason": tr.reason,
|
"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:
|
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(
|
notifier.send(Alert(
|
||||||
kind="trigger",
|
kind="trigger",
|
||||||
title=f"{tr.trigger} signal",
|
title=f"{tr.trigger} signal",
|
||||||
body=f"@ {now}",
|
body=f"@ {datetime.fromtimestamp(now).isoformat(timespec='seconds')}",
|
||||||
|
image_path=fire_path,
|
||||||
direction=tr.trigger,
|
direction=tr.trigger,
|
||||||
))
|
))
|
||||||
levels_extractor = LevelsExtractor(cfg, tr.trigger, now)
|
levels_extractor = LevelsExtractor(cfg, tr.trigger, now)
|
||||||
|
|||||||
Reference in New Issue
Block a user