From d83a666f308895a55ff841fce34b89deb10e9839 Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Wed, 22 Apr 2026 23:50:04 -0700 Subject: [PATCH] Paper trader Phase 1.B.3 + 1.C.1: market clock via Alpaca + scoring dispatch log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _is_market_open() now queries Alpaca's get_clock() so holidays, early closes, and halted markets no longer skip orders; falls back to weekday 9:30–16:00 ET only on broker error (1.B.3) - AlpacaBroker.get_clock() + MarketClock dataclass wrapping alpaca-py's TradingClient.get_clock() - PaperTradingEngine logs scoring_model at session startup (WARNING level) so multi-session daemon makes the live-path scorer dispatch explicit in every boot log — verifies v7.356 config's return_max_long_v13e actually reaches _compute_score (1.C.1) Co-Authored-By: Claude Sonnet 4.6 --- apps/paper_trader/alpaca_broker.py | 16 +++++++++++++ apps/paper_trader/engine.py | 37 ++++++++++++++++++++++-------- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/apps/paper_trader/alpaca_broker.py b/apps/paper_trader/alpaca_broker.py index 6600da8..ef8f554 100644 --- a/apps/paper_trader/alpaca_broker.py +++ b/apps/paper_trader/alpaca_broker.py @@ -61,6 +61,13 @@ class PortfolioHistory: profit_loss_pct: list[float] +@dataclass +class MarketClock: + is_open: bool + next_open: dt.datetime | None + next_close: dt.datetime | None + + class AlpacaBroker: """Thin wrapper around alpaca-py TradingClient for paper trading.""" @@ -99,6 +106,15 @@ class AlpacaBroker: # Account # ------------------------------------------------------------------ # + def get_clock(self) -> MarketClock: + """Return Alpaca's market clock (respects holidays, early closes, halts).""" + clk = self._trading.get_clock() + return MarketClock( + is_open=bool(clk.is_open), + next_open=clk.next_open, + next_close=clk.next_close, + ) + def get_account(self) -> AccountInfo: acct = self._trading.get_account() equity = float(acct.equity or 0) diff --git a/apps/paper_trader/engine.py b/apps/paper_trader/engine.py index 188991d..40a3772 100644 --- a/apps/paper_trader/engine.py +++ b/apps/paper_trader/engine.py @@ -134,6 +134,14 @@ class PaperTradingEngine: self._parking_brake_cooldown_remaining: int = 0 self._parking_brake_skip_buy_today: bool = False # skip same-day re-buy after brake fires + logger.warning( + "paper_trader_scoring_dispatch", + session_id=session.session_id, + session_name=session.session_name, + scoring_model=self._config.signal.scoring_model, + config_path=session.config_path, + ) + def _get_candidate_capital_bucket_id(self, candidate: Candidate) -> str | None: return candidate.engine_capital_bucket_id @@ -438,16 +446,25 @@ class PaperTradingEngine: ) return None, reason - @staticmethod - def _is_market_open() -> bool: - """Return True if US equity market is currently open (9:30–16:00 ET, weekdays).""" - import zoneinfo - now_et = dt.datetime.now(tz=zoneinfo.ZoneInfo("America/New_York")) - if now_et.weekday() >= 5: # Saturday=5, Sunday=6 - return False - market_open = now_et.replace(hour=9, minute=30, second=0, microsecond=0) - market_close = now_et.replace(hour=16, minute=0, second=0, microsecond=0) - return market_open <= now_et < market_close + def _is_market_open(self) -> bool: + """Return True if US equity market is currently open. + + Authoritative: Alpaca `get_clock()` — respects holidays, early closes, + halted markets. Falls back to a weekday 9:30–16:00 ET clock-only check + on any broker error (better to fail-open during rare API hiccups than + miss an event entirely; Alpaca will reject rejected orders downstream). + """ + try: + return self._broker.get_clock().is_open + except Exception as exc: + logger.warning("paper_engine_clock_fallback", error=str(exc)) + import zoneinfo + now_et = dt.datetime.now(tz=zoneinfo.ZoneInfo("America/New_York")) + if now_et.weekday() >= 5: + return False + market_open = now_et.replace(hour=9, minute=30, second=0, microsecond=0) + market_close = now_et.replace(hour=16, minute=0, second=0, microsecond=0) + return market_open <= now_et < market_close def _check_kill_switch(self, drawdown_pct: float, session_st: Any) -> bool: """Activate kill switch if drawdown exceeds threshold. Returns True if triggered."""