"""Backfill the track record by forecasting through recent history, honestly. The point of a backfill is to give a new model a track record without waiting months for one. The danger is that a backfill is trivially easy to fake: run the model over history it has already seen, or let it peek at the bar it is predicting, and it will look extraordinary. So this script forecasts *as of* each historical timestamp. The context is sliced at that timestamp by `runtime.load_context`, and the adapter re-checks the slice against `issued_ts` on every call -- a context bar dated after the issue moment raises rather than producing a flattering number. Nothing here opts out of that check; there is no fast path that skips it. Every row written carries `backfilled: true`, and the UI badges those entries wherever they appear. A backfilled record is evidence of a kind, but it is not the same evidence as a forecast made before the outcome existed, and the difference is on the page rather than in a footnote. HF_TOKEN=$(cat ../../key.txt) python scripts/seed_trackrecord.py python scripts/seed_trackrecord.py --dry-run --models chronos-bolt-tiny """ from __future__ import annotations import argparse import json import logging import os import sys from pathlib import Path import pandas as pd sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from src import config, runtime, trackrecord # noqa: E402 from src.store import ArenaStore, now_utc # noqa: E402 logging.basicConfig(level=logging.INFO, format="%(levelname)-7s %(message)s") log = logging.getLogger("backfill") # How many forecasts to seed per (model, asset, timeframe). TARGET_PER_SERIES = 60 # Space the issue points out so the windows do not all overlap: consecutive # forecasts sharing 95% of their context would give 60 nearly identical # observations and a coverage figure with far less evidence behind it than the # count suggests. STRIDE = {"1h": 12, "1d": 3} def checkpoint_path(root: Path) -> Path: return root / "backfill_checkpoint.json" def load_checkpoint(root: Path) -> dict: p = checkpoint_path(root) if p.exists(): try: return json.loads(p.read_text()) except Exception: log.warning("unreadable checkpoint; starting fresh") return {"done": {}} def save_checkpoint(root: Path, state: dict) -> None: checkpoint_path(root).parent.mkdir(parents=True, exist_ok=True) checkpoint_path(root).write_text(json.dumps(state, indent=2, sort_keys=True)) def series_key(model: str, asset: str, tf: str) -> str: return f"{model}|{asset}|{tf}" def backfill_series(store: ArenaStore, model_slug: str, asset: str, tf: str, registry: dict, target: int, horizon: int, seed: int, dry_run: bool) -> int: """Issue `target` as-of forecasts, oldest first. Returns rows archived.""" prices = store.get_prices(asset, tf) if not len(prices): log.warning(" no prices for %s %s", asset, tf) return 0 ts = pd.to_datetime(prices["ts"], utc=True).reset_index(drop=True) stride = STRIDE.get(tf, 6) # Only issue points whose horizon has already elapsed -- an unresolved # backfill row would sit in the archive forever without ever scoring. latest_resolvable = len(ts) - horizon - 1 earliest = config.DEFAULT_CONTEXT_BARS + 1 if latest_resolvable <= earliest: log.warning(" not enough history for %s %s (%d bars)", asset, tf, len(ts)) return 0 points = list(range(latest_resolvable, earliest, -stride))[:target] points.reverse() written = 0 for i, idx in enumerate(points): as_of = ts.iloc[idx] try: run = runtime.run_forecast( store, model_slug, asset, tf, horizon=horizon, n_samples=config.DEFAULT_N_SAMPLES, seed=seed, as_of=as_of, registry=registry, archive=not dry_run, backfilled=True) except runtime.ForecastUnavailable as e: log.warning(" %s: %s", as_of.date(), e) continue written += run.archived_rows if (i + 1) % 10 == 0: log.info(" %d/%d issued (%s)", i + 1, len(points), as_of.date()) return written def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--dry-run", action="store_true") ap.add_argument("--models", nargs="*", default=None, help="model slugs; default is every CPU-tier enrolled model") ap.add_argument("--assets", nargs="*", default=list(config.SEED_ASSETS)) ap.add_argument("--timeframes", nargs="*", default=list(config.SEED_TIMEFRAMES)) ap.add_argument("--target", type=int, default=TARGET_PER_SERIES) ap.add_argument("--seed", type=int, default=42) ap.add_argument("--include-gpu", action="store_true", help="also seed GPU-tier models (slow on CPU)") ap.add_argument("--local-root", default=None) args = ap.parse_args() store = ArenaStore(local_root=args.local_root, offline=not os.environ.get("HF_TOKEN")) registry = store.get_registry() models = registry.get("models", {}) if not models: log.error("registry is empty; run scripts/bootstrap_registry.py first") return 1 if args.models: chosen = [m for m in args.models if m in models] missing = set(args.models) - set(chosen) if missing: log.error("not enrolled: %s", ", ".join(sorted(missing))) return 1 else: chosen = [slug for slug, entry in sorted(models.items()) if args.include_gpu or entry.get("capabilities", {}).get("hardware") == "cpu"] root = Path(store.local_root) checkpoint = load_checkpoint(root) total = 0 for model_slug in chosen: log.info("model %s", model_slug) for asset in args.assets: for tf in args.timeframes: key = series_key(model_slug, asset, tf) if checkpoint["done"].get(key): log.info(" %s %s already done, skipping", asset, tf) continue horizon = config.DEFAULT_HORIZON.get(tf, 24) log.info(" %s %s (h=%d, target=%d)", asset, tf, horizon, args.target) written = backfill_series(store, model_slug, asset, tf, registry, args.target, horizon, args.seed, args.dry_run) total += written if not args.dry_run: checkpoint["done"][key] = { "rows": written, "ts": now_utc().isoformat()} save_checkpoint(root, checkpoint) # Commit per series rather than at the end: a run that dies # halfway must not throw away hours of forecasting. store.flush(f"arena: backfill {model_slug} {asset} {tf}") log.info("archived %d forecast rows", total) if not args.dry_run: log.info("resolving") for model_slug in chosen: for asset in args.assets: for tf in args.timeframes: trackrecord.resolve(store, model_slug, asset, tf) trackrecord.regenerate_standings(store) store.flush("arena: resolve backfill and rebuild standings") log.info("standings rebuilt") return 0 if __name__ == "__main__": raise SystemExit(main())