- 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>
73 lines
2.6 KiB
Python
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()
|