feat(run): --startup-delay + canary sanity check at startup
- 5s countdown before the loop starts so user can alt-tab TradeStation to
the foreground and minimize whatever covers it.
- First frame triggers a canary phash check. Drift → WARN printed, clears
auto-pause so user can Ctrl+C without the loop going silent. Canary
status ('drift=X/Y' or 'capture_failed') is included in the startup
ping so it's visible on Discord/Telegram.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,10 @@ def main(argv=None) -> None:
|
||||
"--capture-stub", action="store_true",
|
||||
help="Use stub capture (reads PNGs from samples/); useful for smoke-testing on Linux",
|
||||
)
|
||||
p_run.add_argument(
|
||||
"--startup-delay", type=float, default=5.0, metavar="SEC",
|
||||
help="Seconds to wait before the loop starts (bring TradeStation to front). Default 5.",
|
||||
)
|
||||
|
||||
# journal
|
||||
p_journal = sub.add_parser("journal", help="Add a trade journal entry interactively")
|
||||
@@ -139,6 +143,15 @@ def _cmd_run(args) -> None:
|
||||
cfg = Config.load_current(Path("configs"))
|
||||
duration_s = args.duration * 3600 if args.duration is not None else None
|
||||
capture_stub = args.capture_stub or bool(os.environ.get("ATM_STUB_CAPTURE"))
|
||||
|
||||
delay = getattr(args, "startup_delay", 0.0)
|
||||
if delay > 0 and not capture_stub:
|
||||
print(f"Bring TradeStation to front, minimize PowerShell/VS Code. Starting in {delay:.0f}s...", flush=True)
|
||||
for i in range(int(delay), 0, -1):
|
||||
print(f" {i}...", flush=True)
|
||||
time.sleep(1.0)
|
||||
print("RUN", flush=True)
|
||||
|
||||
run_live(cfg, duration_s=duration_s, capture_stub=capture_stub)
|
||||
|
||||
|
||||
@@ -282,15 +295,29 @@ def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None:
|
||||
]
|
||||
notifier = FanoutNotifier(backends, Path(cfg.dead_letter_path))
|
||||
|
||||
# Startup ping — confirms both channels work before the first bar closes.
|
||||
# Sanity check: capture one frame, confirm canary matches calibration.
|
||||
from datetime import datetime
|
||||
dur_note = f" duration={cfg.heartbeat_min:d}m heartbeat" if duration_s is None else f" duration={duration_s/3600:.2f}h"
|
||||
first_frame = capture()
|
||||
if first_frame is None:
|
||||
print("WARN: first capture returned None — window/region missing", flush=True)
|
||||
canary_status = "capture_failed"
|
||||
else:
|
||||
first_check = canary.check(first_frame)
|
||||
canary_status = f"drift={first_check.distance}/{cfg.canary.drift_threshold}"
|
||||
if first_check.drifted:
|
||||
print(f"WARN: canary drift at startup ({canary_status}). Wrong window in front?", flush=True)
|
||||
canary.resume() # clear the auto-pause so user can Ctrl+C and fix
|
||||
|
||||
dur_note = f" dur=∞" if duration_s is None else f" dur={duration_s/3600:.2f}h"
|
||||
notifier.send(Alert(
|
||||
kind="heartbeat",
|
||||
title="ATM started",
|
||||
body=f"config={cfg.config_version}{dur_note} at {datetime.now().isoformat(timespec='seconds')}",
|
||||
body=(
|
||||
f"cfg={cfg.config_version}{dur_note} @ {datetime.now().isoformat(timespec='seconds')}\n"
|
||||
f"canary: {canary_status}"
|
||||
),
|
||||
))
|
||||
audit.log({"event": "started", "config": cfg.config_version})
|
||||
audit.log({"event": "started", "config": cfg.config_version, "canary": canary_status})
|
||||
|
||||
start = time.monotonic()
|
||||
heartbeat_due = start + cfg.heartbeat_min * 60
|
||||
|
||||
Reference in New Issue
Block a user