#!/usr/bin/env python3 """Build server-side continuous futures series (v2) with several roll/adjust choices and compare them to each other and to the vendor continuous series (v1). continuous_compare.py --root CL --start 2018-01-01 --rolls volume,calendar --adjusts back_adjusted,ratio \ [--depth 1] [--timeframe 1day] [--vendor contin_adj_ratio] [--out cl.csv] [--plot cl.png] """ from __future__ import annotations import argparse import sys from pathlib import Path import numpy as np import pandas as pd sys.path.insert(0, str(Path(__file__).parent)) import hfmd # noqa: E402 def describe_rolls(name: str, df: pd.DataFrame, meta: dict) -> None: rolls = meta.get("roll_dates") or [] if df.empty: print(f"\n[{name}] no data") return years = max((df["datetime"].iloc[-1] - df["datetime"].iloc[0]).days / 365.25, 1e-9) roll_txt = f"{len(rolls)} rolls ({len(rolls) / years:.1f}/yr)" if rolls or not name.startswith("vendor") else "roll dates not exposed (vendor construction)" print(f"\n[{name}] {len(df):,} bars {df['datetime'].iloc[0].date()} → {df['datetime'].iloc[-1].date()} · {roll_txt}") if rolls: shown = rolls if len(rolls) <= 12 else rolls[:6] + ["…"] + rolls[-6:] print(" roll dates:", ", ".join(str(r) if isinstance(r, str) else str(r.get("date", r)) for r in shown)) if "close" in df and len(df) > 1: print(f" last close {df['close'].iloc[-1]:.4f} · first close {df['close'].iloc[0]:.4f}" + (" (adjusted levels are NOT tradable prices)" if "adjust=none" not in name else "")) def main() -> int: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--root", required=True) ap.add_argument("--start", default="2015-01-01") ap.add_argument("--end") ap.add_argument("--timeframe", default="1day") ap.add_argument("--depth", type=int, default=1) ap.add_argument("--rolls", default="volume", help="comma list: volume, open_interest, calendar") ap.add_argument("--adjusts", default="back_adjusted", help="comma list: none, back_adjusted, ratio") ap.add_argument("--vendor", help="also fetch the v1 vendor continuous with this adjustment: contin_UNadj, contin_adj_ratio, contin_adj_absolute") ap.add_argument("--out", help="CSV with all series' closes aligned on datetime") ap.add_argument("--plot") a = ap.parse_args() root = a.root.upper() series: dict[str, pd.Series] = {} for roll in a.rolls.split(","): for adj in a.adjusts.split(","): name = f"v2 roll={roll} adjust={adj} depth={a.depth}" try: df, meta = hfmd.continuous(root, roll=roll, adjust=adj, depth=a.depth, timeframe=a.timeframe, start=a.start, end=a.end) except hfmd.HfmdError as e: print(f"\n[{name}] {e}", file=sys.stderr) if e.status == 404: print(" → the v2 continuous endpoint is not deployed on this server yet; use scripts/stitch_local.py", file=sys.stderr) continue describe_rolls(name, df, meta) if not df.empty: series[name] = df.set_index("datetime")["close"].astype(float) if a.vendor: name = f"vendor {a.vendor}" try: vdf = hfmd.bars("futures", root, a.timeframe, a.start, a.end, a.vendor) describe_rolls(name, vdf, {}) if not vdf.empty: series[name] = vdf.set_index("datetime")["close"].astype(float) except hfmd.HfmdError as e: print(f"\n[{name}] {e}", file=sys.stderr) if not series: print("\nno series retrieved", file=sys.stderr) return 1 wide = pd.concat(series, axis=1).sort_index() rets = wide.pct_change() print("\nreturn correlation between series (daily pct changes; 'none'/UNadj variants include roll gaps):") with pd.option_context("display.width", 200): print(rets.corr().round(4).to_string()) if len(series) > 1: names = list(series) base = names[-1] print(f"\nannualised tracking difference of log-returns vs '{base}':") for n in names[:-1]: d = (np.log1p(rets[n]) - np.log1p(rets[base])).dropna() print(f" {n:<45} mean={d.mean() * 252:+.4%}/yr std={d.std() * np.sqrt(252):.4%} n={len(d)}") if a.out: wide.to_csv(a.out) print(f"\nwrote {a.out} ({len(wide):,} rows × {wide.shape[1]} series)") if a.plot: try: import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt except ImportError: print("matplotlib not installed", file=sys.stderr) return 0 fig, ax = plt.subplots(figsize=(11, 5)) for n, s in series.items(): ax.plot(s.index, s / s.dropna().iloc[0] * 100, lw=1.0, label=n) ax.set_title(f"{root} continuous series rebased to 100 (differences = roll/adjust methodology)") ax.grid(alpha=0.25) ax.legend(fontsize=8) fig.tight_layout() fig.savefig(a.plot, dpi=130) print(f"wrote {a.plot}") return 0 if __name__ == "__main__": sys.exit(main())