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%
4.8 KB · 120 lines python
Raw Blame History
1#!/usr/bin/env python32"""Summary statistics for a bars file produced by fetch_bars.py (one or several tickers).34  analyze.py aapl.csv [--plot aapl.png] [--periods-per-year 252]56Prints, per ticker: rows, range, CAGR, annualised volatility, Sharpe (rf=0), max drawdown (+ dates),7best/worst bar, skew, kurtosis, number of gaps > 3 calendar days, monthly returns table.8"""9from __future__ import annotations1011import argparse12import sys1314import numpy as np15import pandas as pd161718def infer_periods_per_year(dt: pd.Series) -> int:19    if len(dt) < 3:20        return 25221    step = dt.diff().dropna().median()22    if step >= pd.Timedelta(days=1):23        return 252 if dt.dt.dayofweek.max() <= 4 else 36524    per_day = int(pd.Timedelta(hours=6.5) / step) if step < pd.Timedelta(hours=1) else int(pd.Timedelta(hours=24) / step)25    return max(per_day, 1) * 252262728def stats(df: pd.DataFrame, ppy: int | None) -> dict:29    df = df.sort_values("datetime").reset_index(drop=True)30    ppy = ppy or infer_periods_per_year(df["datetime"])31    close = df["close"].astype(float)32    ret = close.pct_change().dropna()33    years = max((df["datetime"].iloc[-1] - df["datetime"].iloc[0]).days / 365.25, 1e-9)34    equity = (1 + ret).cumprod()35    dd = equity / equity.cummax() - 136    trough = dd.idxmin() if len(dd) else None37    peak = equity.loc[:trough].idxmax() if trough is not None else None38    gaps = (df["datetime"].diff() > pd.Timedelta(days=3)).sum()39    out = {40        "rows": len(df),41        "first": str(df["datetime"].iloc[0]),42        "last": str(df["datetime"].iloc[-1]),43        "periods_per_year": ppy,44        "total_return": close.iloc[-1] / close.iloc[0] - 1,45        "cagr": (close.iloc[-1] / close.iloc[0]) ** (1 / years) - 1,46        "ann_vol": ret.std() * np.sqrt(ppy),47        "sharpe_rf0": (ret.mean() / ret.std() * np.sqrt(ppy)) if ret.std() > 0 else np.nan,48        "max_drawdown": dd.min() if len(dd) else np.nan,49        "dd_peak": str(df["datetime"].iloc[peak]) if peak is not None else None,50        "dd_trough": str(df["datetime"].iloc[trough]) if trough is not None else None,51        "best_bar": ret.max(),52        "worst_bar": ret.min(),53        "skew": ret.skew(),54        "kurtosis": ret.kurt(),55        "gaps_gt_3d": int(gaps),56        "avg_volume": float(df["volume"].mean()) if "volume" in df else None,57    }58    return out596061def monthly_table(df: pd.DataFrame) -> pd.DataFrame:62    s = df.set_index("datetime")["close"].astype(float).resample("ME").last().pct_change().dropna()63    t = s.to_frame("r")64    t["year"], t["month"] = t.index.year, t.index.month65    return (t.pivot(index="year", columns="month", values="r") * 100).round(1)666768def main() -> int:69    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)70    ap.add_argument("file")71    ap.add_argument("--plot", help="write a price + drawdown PNG (needs matplotlib)")72    ap.add_argument("--periods-per-year", type=int)73    a = ap.parse_args()7475    df = pd.read_parquet(a.file) if a.file.endswith(".parquet") else pd.read_csv(a.file)76    df["datetime"] = pd.to_datetime(df["datetime"])77    tickers = df["ticker"].unique() if "ticker" in df else ["?"]78    for t in tickers:79        sub = df[df["ticker"] == t] if "ticker" in df else df80        s = stats(sub, a.periods_per_year)81        print(f"\n=== {t} ===")82        for k, v in s.items():83            if isinstance(v, float):84                print(f"{k:>18}: {v:,.4f}" if abs(v) < 1000 else f"{k:>18}: {v:,.0f}")85            else:86                print(f"{k:>18}: {v}")87        if s["periods_per_year"] <= 365 and len(sub) > 40:88            print("\nmonthly returns (%):")89            print(monthly_table(sub).to_string())9091    if a.plot:92        try:93            import matplotlib94            matplotlib.use("Agg")95            import matplotlib.pyplot as plt96        except ImportError:97            print("matplotlib not installed: pip install matplotlib", file=sys.stderr)98            return 099        fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(11, 6.5), sharex=True, gridspec_kw={"height_ratios": [3, 1]})100        for t in tickers:101            sub = (df[df["ticker"] == t] if "ticker" in df else df).sort_values("datetime")102            close = sub["close"].astype(float)103            norm = close / close.iloc[0] * 100 if len(tickers) > 1 else close104            ax1.plot(sub["datetime"], norm, lw=1.1, label=t)105            eq = (1 + close.pct_change().fillna(0)).cumprod()106            ax2.fill_between(sub["datetime"], (eq / eq.cummax() - 1) * 100, 0, alpha=0.35)107        ax1.set_title(f"{', '.join(tickers)} — {'rebased to 100' if len(tickers) > 1 else 'close'}")108        ax1.grid(alpha=0.25)109        ax1.legend(loc="upper left")110        ax2.set_ylabel("drawdown %")111        ax2.grid(alpha=0.25)112        fig.tight_layout()113        fig.savefig(a.plot, dpi=130)114        print(f"\nwrote {a.plot}")115    return 0116117118if __name__ == "__main__":119    sys.exit(main())120