You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
203 lines
6.9 KiB
Python
203 lines
6.9 KiB
Python
"""
|
|
V29 Attention Cache Backfill
|
|
|
|
Calls Oracle admin endpoints to collect Wikipedia pageview data for the midlarge
|
|
universe over 200 trading days, then fetches and caches attention features so the
|
|
V29 backtest has real wiki_spike values instead of soft-miss Nones.
|
|
|
|
Usage:
|
|
python scripts/v29/attention_backfill.py [--days 200] [--limit-tickers N]
|
|
|
|
Steps per (ticker, date) pair:
|
|
1. resolve_entity — ensure ticker→Wikipedia entity mapping (once per ticker)
|
|
2. collect_wiki — trigger Oracle to fetch Wikipedia pageview data
|
|
3. get_event_attention — fetch results and write to cache
|
|
|
|
Cache format: data/cache/orb_attention/<TICKER>.json.gz
|
|
Skip logic: skip pairs where cache already has non-None wiki_spike_10d.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import gzip
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import date, timedelta
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
BASE_DIR = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(BASE_DIR))
|
|
|
|
from libs.common.config import Settings
|
|
from libs.intraday.catalyst import AttentionEventCache, _extract_attention_features
|
|
from libs.oracle_client.attention import AttentionService
|
|
from libs.oracle_client.client import OracleClient
|
|
from libs.common.time_utils import is_trading_day, trading_days_between
|
|
|
|
|
|
UNIVERSE_YAML = BASE_DIR / "configs" / "symbols_midlarge_snapshot_exact.yaml"
|
|
CACHE_DIR = BASE_DIR / "data" / "cache" / "orb_attention"
|
|
V23_RUN = BASE_DIR / "runs" / "intraday_orb" / "intraday_20260419_195924_f325f91a.json"
|
|
CONCURRENCY_ENTITY = 20
|
|
CONCURRENCY_COLLECT = 40
|
|
CONCURRENCY_FETCH = 40
|
|
|
|
|
|
def load_universe() -> list[str]:
|
|
with open(UNIVERSE_YAML) as f:
|
|
data = yaml.safe_load(f)
|
|
if isinstance(data, dict):
|
|
tickers = sorted(data.get("symbols", data.get("tickers", list(data.keys()))))
|
|
else:
|
|
tickers = sorted(data)
|
|
|
|
# Sort by V23 trade frequency — high-ORB-activity tickers first (most likely to have wiki spikes)
|
|
try:
|
|
with open(V23_RUN) as f:
|
|
v23 = json.load(f)
|
|
from collections import Counter
|
|
freq = Counter(t["ticker"] for t in v23.get("trades", []))
|
|
tickers.sort(key=lambda t: -freq.get(t, 0))
|
|
except Exception:
|
|
pass
|
|
|
|
return tickers
|
|
|
|
|
|
def get_trading_days(lookback: int) -> list[str]:
|
|
today = date.today()
|
|
start = today - timedelta(days=lookback * 2)
|
|
days = [d.isoformat() for d in trading_days_between(start, today)]
|
|
return days[-lookback:]
|
|
|
|
|
|
def has_real_wiki(cache: AttentionEventCache, ticker: str, event_date: str) -> bool:
|
|
entry = cache.get(ticker, event_date)
|
|
if entry is None:
|
|
return False
|
|
return entry.get("attention_wiki_spike_10d") is not None
|
|
|
|
|
|
async def resolve_entities_bulk(
|
|
tickers: list[str],
|
|
svc: AttentionService,
|
|
) -> dict[str, bool]:
|
|
semaphore = asyncio.Semaphore(CONCURRENCY_ENTITY)
|
|
results: dict[str, bool] = {}
|
|
|
|
async def resolve_one(ticker: str) -> None:
|
|
async with semaphore:
|
|
try:
|
|
await svc.resolve_entity(ticker)
|
|
results[ticker] = True
|
|
except Exception:
|
|
results[ticker] = False
|
|
|
|
print(f" Resolving {len(tickers)} entities...")
|
|
await asyncio.gather(*(resolve_one(t) for t in tickers))
|
|
resolved = sum(1 for v in results.values() if v)
|
|
print(f" Resolved: {resolved}/{len(tickers)}")
|
|
return results
|
|
|
|
|
|
async def collect_and_fetch_bulk(
|
|
pairs: list[tuple[str, str]],
|
|
svc: AttentionService,
|
|
cache: AttentionEventCache,
|
|
) -> tuple[int, int]:
|
|
semaphore = asyncio.Semaphore(CONCURRENCY_COLLECT)
|
|
filled = 0
|
|
errors = 0
|
|
completed = 0
|
|
total = len(pairs)
|
|
last_pct = [-1]
|
|
|
|
def _progress() -> None:
|
|
pct = int(completed / total * 10) * 10 if total > 0 else 0
|
|
if pct > last_pct[0] or completed == total:
|
|
last_pct[0] = pct
|
|
bar = "█" * (pct // 5) + "░" * (20 - pct // 5)
|
|
print(f"\r [{bar}] {completed}/{total} ({pct}%) filled={filled} errors={errors}", end="", flush=True)
|
|
|
|
async def collect_one(ticker: str, event_date: str) -> None:
|
|
nonlocal filled, errors, completed
|
|
async with semaphore:
|
|
try:
|
|
payload = await svc.get_event_attention(ticker, event_date)
|
|
features = _extract_attention_features(payload)
|
|
await asyncio.to_thread(cache.put, ticker, event_date, features)
|
|
if features.get("attention_wiki_spike_10d") is not None:
|
|
filled += 1
|
|
except Exception as exc:
|
|
errors += 1
|
|
_ = exc
|
|
completed += 1
|
|
_progress()
|
|
|
|
_progress()
|
|
await asyncio.gather(*(collect_one(t, d) for t, d in pairs))
|
|
print()
|
|
return filled, errors
|
|
|
|
|
|
async def main(days: int = 200, limit_tickers: int | None = None) -> None:
|
|
settings = Settings()
|
|
cache = AttentionEventCache(str(CACHE_DIR))
|
|
trading_days = get_trading_days(days)
|
|
universe = load_universe()
|
|
if limit_tickers:
|
|
universe = universe[:limit_tickers]
|
|
|
|
print(f"V29 Attention Backfill")
|
|
print(f" Universe: {len(universe)} tickers")
|
|
print(f" Trading days: {trading_days[0]} → {trading_days[-1]} ({len(trading_days)} days)")
|
|
print(f" Cache dir: {CACHE_DIR}")
|
|
|
|
# Count what's already filled
|
|
total_pairs = len(universe) * len(trading_days)
|
|
already_filled = sum(
|
|
1 for t in universe for d in trading_days if has_real_wiki(cache, t, d)
|
|
)
|
|
print(f"\n Pre-check: {already_filled}/{total_pairs} pairs already have real wiki data")
|
|
|
|
# Pairs that need backfill
|
|
missing_pairs = [
|
|
(t, d) for t in universe for d in trading_days if not has_real_wiki(cache, t, d)
|
|
]
|
|
print(f" To fill: {len(missing_pairs)} pairs")
|
|
|
|
if not missing_pairs:
|
|
print("\n Nothing to do — cache already complete.")
|
|
return
|
|
|
|
async with OracleClient(
|
|
base_url=settings.stock_oracle_url,
|
|
timeout=max(float(settings.stock_oracle_timeout), 120.0),
|
|
) as client:
|
|
svc = AttentionService(client)
|
|
|
|
print(f"\n[1/2] Resolving entities for {len(universe)} tickers...")
|
|
await resolve_entities_bulk(universe, svc)
|
|
|
|
print(f"\n[2/2] Fetching attention for {len(missing_pairs)} pairs...")
|
|
filled, errors = await collect_and_fetch_bulk(missing_pairs, svc, cache)
|
|
|
|
print(f"\n Done. Filled={filled} Errors={errors}")
|
|
final_filled = sum(
|
|
1 for t in universe for d in trading_days if has_real_wiki(cache, t, d)
|
|
)
|
|
print(f" Final coverage: {final_filled}/{total_pairs} = {final_filled/total_pairs*100:.1f}%")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description="V29 wiki attention backfill")
|
|
parser.add_argument("--days", type=int, default=200, help="Trading days to backfill")
|
|
parser.add_argument("--limit-tickers", type=int, default=None, help="Test mode: limit to N tickers")
|
|
args = parser.parse_args()
|
|
asyncio.run(main(days=args.days, limit_tickers=args.limit_tickers))
|