Files
momentum-system/backfill_history.py
T
momentum devandClaude Opus 4.8 14cccc5d30 Add opening-range-breakout setup (the EARLY trigger) + history backfill
- structure.detect_opening_range_breakout: break of the first ~5 RTH bars' high
  on >= OR-average volume; entry=OR high, stop=OR low. Placed first in
  classify_setup so it fires minutes after the open instead of waiting for a
  pullback to form. Verified: RGNT triggers at bar 6 (~9:36 ET) q=1.0.
- evaluate_setup counts OPENING_RANGE_BREAK toward the structure criterion.
- backfill_history.py: pull ~3 weeks of minute + 60d daily bars for the sub-$20
  universe so learn_params optimizes over real sessions.

75/75 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:22:43 -04:00

73 lines
2.6 KiB
Python

#!/usr/bin/env python3
"""Backfill historical minute + daily bars for a universe of sub-$20 names, so
learn_params.py has enough real sessions to optimize over.
Pulls the current sub-$20 most-actives (liquid names that gap often) plus any
symbols already in the DB, then fetches ~3 weeks of 1-minute bars (one paginated,
batched range request) and 60 days of daily bars for the gap/RVOL baseline.
PYTHONPATH=. /home/philip/.venvs/momentum/bin/python backfill_history.py
"""
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from dotenv import load_dotenv
from alpaca_paper.client import AlpacaPaperClient
from alpaca_paper.settings import AlpacaPaperSettings
from research.ingestion.discovery import screen_universe
from research.ingestion.market_data import upsert_daily_bars, upsert_minute_bars
from research.multi_schema import open_research_db
load_dotenv()
LOOKBACK_CALENDAR_DAYS = 22 # ~15 trading days
def main():
client = AlpacaPaperClient(AlpacaPaperSettings.from_env())
con = open_research_db("market")
universe = screen_universe(client, 1.0, 20.0, 40)
existing = [r[0] for r in con.execute("SELECT DISTINCT symbol FROM minute_bars").fetchall()]
universe = sorted(set(universe) | set(existing))
print(f"universe ({len(universe)}): {universe}", flush=True)
if not universe:
print("no universe (screener empty?) — aborting")
return
now = datetime.now(timezone.utc)
# daily bars (baseline for gap% / RVOL)
daily = client.get_daily_bars(
universe, start_iso=(now - timedelta(days=60)).date().isoformat() + "T00:00:00Z"
)
drows = sum(upsert_daily_bars(con, s, b) for s, b in daily.items())
print(f"daily bars: {drows} rows", flush=True)
# minute bars: one paginated range request (the client batches symbols <=100)
start = now - timedelta(days=LOOKBACK_CALENDAR_DAYS)
end = now - timedelta(minutes=20)
print(f"fetching minute bars {start.date()} .. {end.date()} (this can take a few min)...", flush=True)
bars = client.get_minute_bars(
universe,
start_iso=start.isoformat().replace("+00:00", "Z"),
end_iso=end.isoformat().replace("+00:00", "Z"),
feed="iex",
)
total = 0
for sym, sym_bars in bars.items():
if sym_bars:
total += upsert_minute_bars(con, sym, sym_bars)
have = len([s for s in bars if bars[s]])
print(f"minute bars: {total} rows across {have} symbols", flush=True)
sessions = con.execute("SELECT COUNT(DISTINCT session_date) FROM minute_bars").fetchone()[0]
print(f"distinct sessions in DB now: {sessions}", flush=True)
if __name__ == "__main__":
main()