Spaces:
Running on Zero
Running on Zero
File size: 7,510 Bytes
8028640 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | """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())
|