From 72aaa00734887973445d4b4559949ab6a24095fa Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 21:48:40 +0000 Subject: [PATCH 1/2] ADR book: log why no home session was found; probe the auction sleeve's path Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NsPrq53XXinQsUZsWqeKht --- .github/workflows/adr-probe.yml | 49 +++++++++++++++++++++++++++++++++ qtrade/intraday/adr_book.py | 5 ++++ 2 files changed, 54 insertions(+) create mode 100644 .github/workflows/adr-probe.yml diff --git a/.github/workflows/adr-probe.yml b/.github/workflows/adr-probe.yml new file mode 100644 index 00000000..c7445b87 --- /dev/null +++ b/.github/workflows/adr-probe.yml @@ -0,0 +1,49 @@ +name: adr-probe + +# One-off diagnosis: run the auction sleeve's own day_inputs (as the 16:30 run does) and see the ADR part. + +on: + push: + branches: ["claude/**"] + paths: [".github/workflows/adr-probe.yml"] + +permissions: + contents: read + +jobs: + probe: + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + ALPACA_API_KEY: ${{ secrets.ALPACA_API_KEY }} + ALPACA_SECRET_KEY: ${{ secrets.ALPACA_SECRET_KEY }} + SEC_USER_AGENT: ${{ secrets.SEC_USER_AGENT }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + - run: pip install -r requirements.txt + - run: | + git fetch -q --depth 1 origin results && mkdir -p data + git show origin/results:intraday/latest/model.json > data/auction_tickers.json + git show origin/results:intraday/edge/signs.json > data/auction_signs.json + - name: Probe + run: | + python - 2>&1 <<'PY' | grep -v -i "warning\|inplace\|whatsnew\|sort_index\|set_index\|rename" | tail -40 + import pandas as pd + from qtrade.config import Config + from qtrade.utils import setup_logging + from qtrade.intraday import auction_sheet as A, adr_book as B + from qtrade.intraday.auction_live import load_inputs + setup_logging() + A.ADR_TRIES = 1 + cfg = Config.load("config/auction_paper.yaml") + tickers, signs = load_inputs("data/auction_tickers.json", "data/auction_signs.json") + day = pd.Timestamp.now(tz="America/New_York").tz_localize(None).normalize() + print("day", day) + print("standalone home_moves:", len(B.home_moves(day, [t for t, (_, _, m) in B.ADRS.items() if m in cfg.auction.adr_markets]))) + out = A.day_inputs(cfg, tickers, signs, day) + print("day_inputs adr weights:", len(out["adr_weights"])) + PY diff --git a/qtrade/intraday/adr_book.py b/qtrade/intraday/adr_book.py index 4a21e637..d3bc35c2 100644 --- a/qtrade/intraday/adr_book.py +++ b/qtrade/intraday/adr_book.py @@ -125,6 +125,11 @@ def home_moves(today: pd.Timestamp, tickers: list[str] | None = None) -> pd.Data f = f[f.index.normalize() < day] # FX up to yesterday only rows.append({"adr": t, "market": mkt, "home_ret": float(np.log(c.iloc[-1] / c.iloc[-2])), "fx_ret_prev": float(f.iloc[-1]) if len(f) else np.nan}) + if not rows: # say why, so an empty ADR book is never a mystery + sample = [h for h in homes if h in md.close][:4] + LOG.warning("ADR book: no home session dated %s; index ends %s; last bars %s", day.date(), + [str(d.date()) for d in md.close.index[-3:]], + {h: [str(d.date()) for d in md.close[h].dropna().index[-2:]] for h in sample}) return pd.DataFrame(rows) From c6ed6fa527e629d8583cdf7b1b583aecaf22866f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 21:52:51 +0000 Subject: [PATCH 2/2] ADR book: run after Yahoo's Asian bars are back (16:55 NY) and retry for 30 minutes The ADR book was empty again on 2026-11-04 despite 4 retries. The auction sleeve's own day_inputs found 0 ADRs on every try between 16:31 and 16:38 New York on Nov 3 and Nov 4, and 34 at 16:50 (probe on Actions). Yahoo's Asian daily bars are missing in that window. - The clock now dispatches the auction sleeve at 16:55 NY. - The script's window opens at 16:50, so the backup crons can't book early. - Retries go up to 10 x 3 min; the job timeout is 75 min. - home_moves logs the dates Yahoo returned whenever it finds no home session. - The one-off probe workflow is removed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NsPrq53XXinQsUZsWqeKht --- .github/workflows/adr-probe.yml | 49 ----------------------------- .github/workflows/auction-sheet.yml | 7 +++-- qtrade/clock.py | 2 +- qtrade/intraday/auction_sheet.py | 8 ++--- 4 files changed, 9 insertions(+), 57 deletions(-) delete mode 100644 .github/workflows/adr-probe.yml diff --git a/.github/workflows/adr-probe.yml b/.github/workflows/adr-probe.yml deleted file mode 100644 index c7445b87..00000000 --- a/.github/workflows/adr-probe.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: adr-probe - -# One-off diagnosis: run the auction sleeve's own day_inputs (as the 16:30 run does) and see the ADR part. - -on: - push: - branches: ["claude/**"] - paths: [".github/workflows/adr-probe.yml"] - -permissions: - contents: read - -jobs: - probe: - runs-on: ubuntu-latest - timeout-minutes: 30 - env: - ALPACA_API_KEY: ${{ secrets.ALPACA_API_KEY }} - ALPACA_SECRET_KEY: ${{ secrets.ALPACA_SECRET_KEY }} - SEC_USER_AGENT: ${{ secrets.SEC_USER_AGENT }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - cache: pip - - run: pip install -r requirements.txt - - run: | - git fetch -q --depth 1 origin results && mkdir -p data - git show origin/results:intraday/latest/model.json > data/auction_tickers.json - git show origin/results:intraday/edge/signs.json > data/auction_signs.json - - name: Probe - run: | - python - 2>&1 <<'PY' | grep -v -i "warning\|inplace\|whatsnew\|sort_index\|set_index\|rename" | tail -40 - import pandas as pd - from qtrade.config import Config - from qtrade.utils import setup_logging - from qtrade.intraday import auction_sheet as A, adr_book as B - from qtrade.intraday.auction_live import load_inputs - setup_logging() - A.ADR_TRIES = 1 - cfg = Config.load("config/auction_paper.yaml") - tickers, signs = load_inputs("data/auction_tickers.json", "data/auction_signs.json") - day = pd.Timestamp.now(tz="America/New_York").tz_localize(None).normalize() - print("day", day) - print("standalone home_moves:", len(B.home_moves(day, [t for t, (_, _, m) in B.ADRS.items() if m in cfg.auction.adr_markets]))) - out = A.day_inputs(cfg, tickers, signs, day) - print("day_inputs adr weights:", len(out["adr_weights"])) - PY diff --git a/.github/workflows/auction-sheet.yml b/.github/workflows/auction-sheet.yml index 9fd52236..0aee9bdd 100644 --- a/.github/workflows/auction-sheet.yml +++ b/.github/workflows/auction-sheet.yml @@ -5,7 +5,8 @@ name: auction-sheet # closed at today's official open, today's ADR book is settled open -> close, and tonight's # book is opened at today's official close - all in the sheet's "Auction book" and # "Auction trades" tabs. Nothing is sent to Alpaca. The two crons cover ~16:25 New York in -# summer and winter time; the script itself only runs between 16:20 and midnight New York, once a day. +# summer and winter time; the script itself only runs between 16:50 and midnight New York (Yahoo's Asian bars, needed by +# the ADR book, are missing around 16:30-16:45), once a day. on: schedule: @@ -14,7 +15,7 @@ on: workflow_dispatch: inputs: force: - description: "run now even outside 16:20-24:00 New York" + description: "run now even outside 16:50-24:00 New York" default: "false" permissions: @@ -27,7 +28,7 @@ concurrency: jobs: sleeve: runs-on: ubuntu-latest - timeout-minutes: 40 + timeout-minutes: 75 env: ALPACA_API_KEY: ${{ secrets.ALPACA_API_KEY }} # market data only ALPACA_SECRET_KEY: ${{ secrets.ALPACA_SECRET_KEY }} diff --git a/qtrade/clock.py b/qtrade/clock.py index 3b39d273..edb49b88 100644 --- a/qtrade/clock.py +++ b/qtrade/clock.py @@ -36,7 +36,7 @@ class Job: window_min=20), Job("hft-shadow.yml", NY, ("09:26",), window_min=300), # its own chain then runs the session Job("flip-sleeve.yml", NY, ("15:45",), window_min=400), - Job("auction-sheet.yml", NY, ("16:30",), window_min=400), + Job("auction-sheet.yml", NY, ("16:55",), window_min=400), # after Yahoo's ~16:30-16:45 refresh of Asian bars Job("hoard-archive.yml", NY, ("16:45",), inputs=(("job", "eod"),), window_min=400), Job("foreign-sheet.yml", "UTC", ("17:15", "23:30"), window_min=200), # 23:30: Asian bars Yahoo posted late Job("hoard-archive.yml", "UTC", ("07:15",), weekdays=False, inputs=(("job", "archive"),), window_min=300), diff --git a/qtrade/intraday/auction_sheet.py b/qtrade/intraday/auction_sheet.py index bcee6c0b..ba111788 100644 --- a/qtrade/intraday/auction_sheet.py +++ b/qtrade/intraday/auction_sheet.py @@ -32,7 +32,7 @@ NY = "America/New_York" TAB, TRADES_TAB = "Auction book", "Auction trades" -ADR_TRIES, ADR_RETRY_S = 4, 120 +ADR_TRIES, ADR_RETRY_S = 10, 180 # Yahoo's Asian bars vanished ~16:30-16:40 NY (Nov 3-4), back by 16:50 LOG_HEADER = ["Date", "Overnight P&L $", "ADR P&L $", "Day P&L $", "Day %", "Equity $", "Overnight longs", "Overnight shorts", "Overnight gross $", "ADR names", "Capital $"] HOLD_HEADER = ["Symbol", "Shares", "Entry (official close)", "Position $"] @@ -106,7 +106,7 @@ def day_inputs(cfg, tickers: list[str], signs: dict, day: pd.Timestamp) -> dict: try: moves = pd.DataFrame() for attempt in range(ADR_TRIES): # Yahoo sometimes serves the Asian daily bars late - moves = home_moves(day, adr_names) # (2026-11-03: none at 21:31 UTC, 24 at 21:50) + moves = home_moves(day, adr_names) # (none at 16:31-16:38 NY on Nov 3-4, all back by 16:50) LOG.info("ADR book: %d ADRs with a home session today (try %d)", len(moves), attempt + 1) if len(moves) or day.weekday() >= 5: break @@ -252,11 +252,11 @@ def main(argv=None): ap.add_argument("--tickers", default="data/auction_tickers.json") ap.add_argument("--signs", default="data/auction_signs.json") ap.add_argument("--dry-run", action="store_true") - ap.add_argument("--force", action="store_true", help="run outside 16:20-24:00 New York") + ap.add_argument("--force", action="store_true", help="run outside 16:50-24:00 New York") args = ap.parse_args(argv) setup_logging() now = pd.Timestamp.now(tz=NY) - if not args.force and not (now.weekday() < 5 and 16 * 60 + 20 <= now.hour * 60 + now.minute < 24 * 60): + if not args.force and not (now.weekday() < 5 and 16 * 60 + 50 <= now.hour * 60 + now.minute < 24 * 60): print({"skipped": f"outside the window ({now:%H:%M} NY)"}) return sleeve = book_split().get("auction", 0.0)