SPB Git forge

spb/hfmarketdata

Public

Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)

127commits 1branches 0releases
24.7 MBsize
maindefault branch
11 days agolast push
JavaScript 53.7% Python 38.3% CSS 4.6% TypeScript 3.1%
5.1 KB · 119 lines python
Raw Blame History
1#!/usr/bin/env python32"""Build server-side continuous futures series (v2) with several roll/adjust choices and compare them3to each other and to the vendor continuous series (v1).45  continuous_compare.py --root CL --start 2018-01-01 --rolls volume,calendar --adjusts back_adjusted,ratio \6                        [--depth 1] [--timeframe 1day] [--vendor contin_adj_ratio] [--out cl.csv] [--plot cl.png]7"""8from __future__ import annotations910import argparse11import sys12from pathlib import Path1314import numpy as np15import pandas as pd1617sys.path.insert(0, str(Path(__file__).parent))18import hfmd  # noqa: E402192021def describe_rolls(name: str, df: pd.DataFrame, meta: dict) -> None:22    rolls = meta.get("roll_dates") or []23    if df.empty:24        print(f"\n[{name}] no data")25        return26    years = max((df["datetime"].iloc[-1] - df["datetime"].iloc[0]).days / 365.25, 1e-9)27    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)"28    print(f"\n[{name}] {len(df):,} bars {df['datetime'].iloc[0].date()} → {df['datetime'].iloc[-1].date()} · {roll_txt}")29    if rolls:30        shown = rolls if len(rolls) <= 12 else rolls[:6] + ["…"] + rolls[-6:]31        print("  roll dates:", ", ".join(str(r) if isinstance(r, str) else str(r.get("date", r)) for r in shown))32    if "close" in df and len(df) > 1:33        print(f"  last close {df['close'].iloc[-1]:.4f} · first close {df['close'].iloc[0]:.4f}"34              + ("  (adjusted levels are NOT tradable prices)" if "adjust=none" not in name else ""))353637def main() -> int:38    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)39    ap.add_argument("--root", required=True)40    ap.add_argument("--start", default="2015-01-01")41    ap.add_argument("--end")42    ap.add_argument("--timeframe", default="1day")43    ap.add_argument("--depth", type=int, default=1)44    ap.add_argument("--rolls", default="volume", help="comma list: volume, open_interest, calendar")45    ap.add_argument("--adjusts", default="back_adjusted", help="comma list: none, back_adjusted, ratio")46    ap.add_argument("--vendor", help="also fetch the v1 vendor continuous with this adjustment: contin_UNadj, contin_adj_ratio, contin_adj_absolute")47    ap.add_argument("--out", help="CSV with all series' closes aligned on datetime")48    ap.add_argument("--plot")49    a = ap.parse_args()50    root = a.root.upper()5152    series: dict[str, pd.Series] = {}53    for roll in a.rolls.split(","):54        for adj in a.adjusts.split(","):55            name = f"v2 roll={roll} adjust={adj} depth={a.depth}"56            try:57                df, meta = hfmd.continuous(root, roll=roll, adjust=adj, depth=a.depth, timeframe=a.timeframe, start=a.start, end=a.end)58            except hfmd.HfmdError as e:59                print(f"\n[{name}] {e}", file=sys.stderr)60                if e.status == 404:61                    print("  → the v2 continuous endpoint is not deployed on this server yet; use scripts/stitch_local.py", file=sys.stderr)62                continue63            describe_rolls(name, df, meta)64            if not df.empty:65                series[name] = df.set_index("datetime")["close"].astype(float)6667    if a.vendor:68        name = f"vendor {a.vendor}"69        try:70            vdf = hfmd.bars("futures", root, a.timeframe, a.start, a.end, a.vendor)71            describe_rolls(name, vdf, {})72            if not vdf.empty:73                series[name] = vdf.set_index("datetime")["close"].astype(float)74        except hfmd.HfmdError as e:75            print(f"\n[{name}] {e}", file=sys.stderr)7677    if not series:78        print("\nno series retrieved", file=sys.stderr)79        return 18081    wide = pd.concat(series, axis=1).sort_index()82    rets = wide.pct_change()83    print("\nreturn correlation between series (daily pct changes; 'none'/UNadj variants include roll gaps):")84    with pd.option_context("display.width", 200):85        print(rets.corr().round(4).to_string())86    if len(series) > 1:87        names = list(series)88        base = names[-1]89        print(f"\nannualised tracking difference of log-returns vs '{base}':")90        for n in names[:-1]:91            d = (np.log1p(rets[n]) - np.log1p(rets[base])).dropna()92            print(f"  {n:<45} mean={d.mean() * 252:+.4%}/yr  std={d.std() * np.sqrt(252):.4%}  n={len(d)}")9394    if a.out:95        wide.to_csv(a.out)96        print(f"\nwrote {a.out} ({len(wide):,} rows × {wide.shape[1]} series)")97    if a.plot:98        try:99            import matplotlib100            matplotlib.use("Agg")101            import matplotlib.pyplot as plt102        except ImportError:103            print("matplotlib not installed", file=sys.stderr)104            return 0105        fig, ax = plt.subplots(figsize=(11, 5))106        for n, s in series.items():107            ax.plot(s.index, s / s.dropna().iloc[0] * 100, lw=1.0, label=n)108        ax.set_title(f"{root} continuous series rebased to 100 (differences = roll/adjust methodology)")109        ax.grid(alpha=0.25)110        ax.legend(fontsize=8)111        fig.tight_layout()112        fig.savefig(a.plot, dpi=130)113        print(f"wrote {a.plot}")114    return 0115116117if __name__ == "__main__":118    sys.exit(main())119