From 3c4bc887c3d36a6becf48dfcea97deb6d0771d75 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Thu, 16 Apr 2026 07:21:44 +0000 Subject: [PATCH] feat(run): --start-at HH:MM + --stop-at HH:MM for wall-clock scheduling Usage: atm run --start-at 16:30 --stop-at 23:00 Sleeps until next occurrence of the start time, runs until stop-at. If start-at is in the past today, rolls over to tomorrow. Duration flag is overridden when --stop-at is given. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/atm/main.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/atm/main.py b/src/atm/main.py index c6ce6c2..11e2ca6 100644 --- a/src/atm/main.py +++ b/src/atm/main.py @@ -62,6 +62,16 @@ def main(argv=None) -> None: "--startup-delay", type=float, default=5.0, metavar="SEC", help="Seconds to wait before the loop starts (bring TradeStation to front). Default 5.", ) + p_run.add_argument( + "--start-at", metavar="HH:MM", default=None, + help="Sleep until local wall-clock time HH:MM before starting. If the " + "time is in the past today, waits until tomorrow.", + ) + p_run.add_argument( + "--stop-at", metavar="HH:MM", default=None, + help="Stop at local HH:MM (overrides --duration). If the time is in " + "the past when the loop starts, rolls over to tomorrow.", + ) # journal p_journal = sub.add_parser("journal", help="Add a trade journal entry interactively") @@ -141,9 +151,32 @@ def _cmd_dryrun(args) -> None: 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")) + # --start-at HH:MM: sleep until the next occurrence of that local wall-clock time + if args.start_at: + from datetime import datetime, timedelta + hh, mm = (int(p) for p in args.start_at.split(":")) + now = datetime.now() + target = now.replace(hour=hh, minute=mm, second=0, microsecond=0) + if target <= now: + target += timedelta(days=1) + wait = (target - now).total_seconds() + print(f"Waiting until {target:%Y-%m-%d %H:%M} (in {wait/60:.1f} min) before starting...", flush=True) + time.sleep(wait) + + # --stop-at HH:MM: compute duration from NOW to the next HH:MM; overrides --duration + duration_s = args.duration * 3600 if args.duration is not None else None + if args.stop_at: + from datetime import datetime, timedelta + hh, mm = (int(p) for p in args.stop_at.split(":")) + now = datetime.now() + stop = now.replace(hour=hh, minute=mm, second=0, microsecond=0) + if stop <= now: + stop += timedelta(days=1) + duration_s = (stop - now).total_seconds() + print(f"Will stop at {stop:%Y-%m-%d %H:%M} (duration {duration_s/3600:.2f}h)", flush=True) + 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)