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",
|
"--capture-stub", action="store_true",
|
||||||
help="Use stub capture (reads PNGs from samples/); useful for smoke-testing on Linux",
|
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
|
# journal
|
||||||
p_journal = sub.add_parser("journal", help="Add a trade journal entry interactively")
|
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"))
|
cfg = Config.load_current(Path("configs"))
|
||||||
duration_s = args.duration * 3600 if args.duration is not None else None
|
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"))
|
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)
|
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))
|
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
|
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(
|
notifier.send(Alert(
|
||||||
kind="heartbeat",
|
kind="heartbeat",
|
||||||
title="ATM started",
|
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()
|
start = time.monotonic()
|
||||||
heartbeat_due = start + cfg.heartbeat_min * 60
|
heartbeat_due = start + cfg.heartbeat_min * 60
|
||||||
|
|||||||
Reference in New Issue
Block a user