"""Time budgeting for the nightly run. Each agent gets a wall-clock deadline = its fair share of the remaining window, capped by the per-agent config, and never past the hard stop. If an agent finishes early the freed time rolls over to those remaining (bounded by the cap), and the hard stop always holds. """ from __future__ import annotations from datetime import datetime, timedelta from zoneinfo import ZoneInfo def local_hard_stop_ts(hard_stop_hhmm: str, local_tz: str) -> float: """Epoch seconds of the next HH:MM in local time (today if still ahead, else tomorrow).""" tz = ZoneInfo(local_tz) now = datetime.now(tz) hh, mm = (int(x) for x in hard_stop_hhmm.split(":")) target = now.replace(hour=hh, minute=mm, second=0, microsecond=0) if target <= now: target += timedelta(days=1) return target.timestamp() def next_deadline( now_ts: float, hard_stop_ts: float, agents_remaining: int, per_agent_cap_s: float ) -> float: """Deadline for the current agent: min(fair share, cap), never past hard stop.""" if agents_remaining <= 0 or now_ts >= hard_stop_ts: return hard_stop_ts fair_share = (hard_stop_ts - now_ts) / agents_remaining span = min(fair_share, per_agent_cap_s) if per_agent_cap_s else fair_share return now_ts + max(span, 60.0) def next_market_trade_date(market_tz: str) -> str: """Best-effort next trading date (ET), rolling weekends to Monday. Holidays handled in Phase 3 via Alpaca's calendar; this is only for naming the nightly output folder.""" tz = ZoneInfo(market_tz) d = datetime.now(tz).date() while d.weekday() >= 5: # 5 = Sat, 6 = Sun d += timedelta(days=1) return d.isoformat()