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%
8.3 KB · 163 lines python
Raw Blame History
1"""Build a tiny synthetic Parquet lake with the exact layout produced by frd_downloader.py.23    parquet/{stock|etf|crypto|index|fx}/{timeframe}/{adjustment}/{TICKER}_{timeframe}.parquet4    parquet/futures/{timeframe}/{contin_UNadj|contin_adj_ratio|contin_adj_absolute}/{ROOT}_{timeframe}.parquet5    parquet/futures_contracts/{timeframe}/{archive|update}/{ROOT}_{MonthCode}{YY}_{timeframe}.parquet6    parquet/options/{year}_{quarter}/{TICKER}_month_option_chain.parquet7    meta/futures/futures.csv89Every bar file carries a `ticker` column (root only for futures contracts — the contract identity is10in the file name, exactly like the real lake). Futures contracts get a realistic life-cycle: volume and11open interest ramp up towards expiry and collapse in the last sessions (so volume/OI rolls are testable),12daily data ends on the exchange expiry (3rd Friday for ES/E6, 20th of the preceding month for CL/NG),13intraday bars cover RTH 09:30–16:00 plus a pre-market ETH block 08:00–09:29 (Eastern, naive).14"""15from __future__ import annotations1617import csv18from datetime import date, timedelta19from pathlib import Path2021import numpy as np22import pandas as pd2324TIMEFRAMES = ["1min", "5min", "30min", "1hour", "1day"]25TICKERS = {"stock": ["AAPL", "MSFT", "SMCP", "SHAK", "GOOG", "GOOGL"], "etf": ["SPY"], "crypto": ["BTCUSD"],26           "index": ["SPX"], "fx": ["EURUSD"]}27ADJ = {"stock": ["adj_split", "adj_splitdiv", "UNADJUSTED"], "etf": ["adj_split", "adj_splitdiv", "UNADJUSTED"],28       "crypto": ["none"], "index": ["none"], "fx": ["none"]}29ROOTS = ["ES", "CL", "NG", "E6"]30MONTHS = {"F": 1, "G": 2, "H": 3, "J": 4, "K": 5, "M": 6, "N": 7, "Q": 8, "U": 9, "V": 10, "X": 11, "Z": 12}31CYCLE = {"ES": "HMUZ", "CL": "FJNV", "NG": "FJNV", "E6": "HMUZ"}      # CL/NG: quarterly subset to keep the lake small32INTRADAY_DAYS = {"ES": 30}                                             # sessions of intraday history per contract (default 5)333435def _third_friday(y: int, m: int) -> date:36    d = date(y, m, 15)37    return d + timedelta(days=(4 - d.weekday()) % 7)383940def contract_expiry(root: str, yy: int, month_code: str) -> date:41    y, m = 2000 + yy, MONTHS[month_code]42    if root in ("ES", "E6"):43        return _third_friday(y, m)44    pm = m - 1 or 1245    return date(y - (m == 1), pm, 20)464748def _lifecycle(dte: np.ndarray, rng: np.random.Generator) -> tuple[np.ndarray, np.ndarray]:49    """(volume, open_interest) as a function of days to expiry: ramp up, collapse in the last sessions."""50    w = np.where(dte > 8, (150.0 - dte) / 100.0, 1.42 * (dte / 8.0) ** 2 + 0.02)51    w = np.clip(w, 0.02, None) * rng.uniform(0.97, 1.03, len(dte))52    vol = np.round(10_000 * w)53    oi = np.clip((200.0 - dte) / 150.0, 0.05, 1.0) * np.where(dte > 10, 1.0, dte / 10.0)54    oi = np.round(100_000 * oi * rng.uniform(0.98, 1.02, len(dte)))55    return vol, oi565758def _bars(ticker: str, start: date, end: date, tf: str, seed: int, oi: bool = False, base: float = 100.0,59          expiry: date | None = None, eth: bool = False, days: int = 5) -> pd.DataFrame:60    rng = np.random.default_rng(seed)61    if tf == "1day":62        idx = pd.bdate_range(start, end)63    else:64        step = {"1min": 1, "5min": 5, "30min": 30, "1hour": 60}[tf]65        sessions = pd.bdate_range(start, end)[-days:]66        starts = [timedelta(hours=9, minutes=30)]67        counts = [int(390 / step)]68        if eth and step <= 30:                     # pre-market block 08:00–09:2969            starts.insert(0, timedelta(hours=8))70            counts.insert(0, int(90 / step))71        idx = pd.DatetimeIndex([d + s + timedelta(minutes=step * i) for d in sessions for s, n in zip(starts, counts) for i in range(n)])72    n = len(idx)73    close = base + np.cumsum(rng.normal(0, 1, n))74    df = pd.DataFrame({"ticker": ticker, "datetime": idx, "open": close + rng.normal(0, .2, n),75                       "high": close + abs(rng.normal(0, .5, n)), "low": close - abs(rng.normal(0, .5, n)),76                       "close": close})77    if expiry is not None:78        dte = np.array([(expiry - d.date()).days for d in idx], dtype=float)79        vol, oi_v = _lifecycle(dte, rng)80        df["volume"] = vol if tf == "1day" else np.round(vol / 400)81        if oi:82            df["open_interest"] = oi_v83    else:84        df["volume"] = rng.integers(100, 10_000, n).astype(float)85        if oi:86            df["open_interest"] = rng.integers(1_000, 100_000, n).astype(float)87    return df888990def build_lake(root: Path, start: date = date(2023, 1, 2), end: date = date(2025, 6, 30)) -> None:91    pq = root / "parquet"92    seed = 193    for asset, tickers in TICKERS.items():94        for tf in TIMEFRAMES:95            for adj in ADJ[asset]:96                if tf not in ("1min", "1day") and adj == "UNADJUSTED":97                    continue98                d = pq / asset / tf / adj99                d.mkdir(parents=True, exist_ok=True)100                for t in tickers:101                    seed += 1102                    _bars(t, start, end, tf, seed).to_parquet(d / f"{t}_{tf}.parquet", index=False)103    for tf in TIMEFRAMES:104        for adj in ("contin_UNadj", "contin_adj_ratio", "contin_adj_absolute"):105            d = pq / "futures" / tf / adj106            d.mkdir(parents=True, exist_ok=True)107            for r in ROOTS:108                seed += 1109                _bars(r, start, end, tf, seed, oi=(tf == "1day")).to_parquet(d / f"{r}_{tf}.parquet", index=False)110    # individual contracts: archive = up to 2024, update = 2025+ (with overlap for 2025 contracts)111    for tf in TIMEFRAMES:112        for r in ROOTS:113            base = {"ES": 5000.0, "CL": 70.0, "NG": 3.0, "E6": 1.08}[r]114            for yy in (23, 24, 25, 26):115                for mc in CYCLE[r]:116                    exp = contract_expiry(r, yy, mc)117                    first = exp - timedelta(days=400)118                    last = min(exp, end)119                    if first > end:120                        continue121                    bucket = "update" if yy >= 25 else "archive"122                    d = pq / "futures_contracts" / tf / bucket123                    d.mkdir(parents=True, exist_ok=True)124                    seed += 1125                    kw = dict(oi=(tf == "1day"), base=base + (yy * 4 + MONTHS[mc] / 3) * base / 400, expiry=exp, eth=True,126                              days=INTRADAY_DAYS.get(r, 5))127                    _bars(r, max(first, date(2022, 1, 3)), last, tf, seed, **kw).to_parquet(d / f"{r}_{mc}{yy}_{tf}.parquet", index=False)128                    if bucket == "update" and yy == 25:  # archive also holds the first part of 2025 contracts129                        d2 = pq / "futures_contracts" / tf / "archive"130                        d2.mkdir(parents=True, exist_ok=True)131                        _bars(r, max(first, date(2022, 1, 3)), min(last, date(2024, 12, 31)), tf, seed, **kw).to_parquet(132                            d2 / f"{r}_{mc}{yy}_{tf}.parquet", index=False)133    # options: one quarter, one ticker134    d = pq / "options" / "2025_q2"135    d.mkdir(parents=True, exist_ok=True)136    rows = []137    for td in pd.bdate_range("2025-04-01", "2025-04-10"):138        for k in (180, 190, 200, 210):139            for cp in ("c", "p"):140                rows.append({"ticker": "AAPL", "trade_date": td.date(), "strike": float(k), "expiry": date(2025, 6, 20),141                             "call_put": cp, "bid": 1.0, "ask": 1.2, "last": 1.1, "volume": 10.0, "open_interest": 100.0,142                             "iv": 0.25, "delta": 0.5 if cp == "c" else -0.5, "gamma": 0.01, "theta": -0.02, "vega": 0.1,143                             "rho": 0.01, "underlying_price": 195.0})144    pd.DataFrame(rows).to_parquet(d / "AAPL_month_option_chain.parquet", index=False)145    # metadata146    m = root / "meta" / "futures"147    m.mkdir(parents=True, exist_ok=True)148    with open(m / "futures.csv", "w", newline="") as f:149        w = csv.writer(f)150        w.writerow(["Ticker", "Name", "First Date", "Last Date"])151        w.writerow(["ES", "E-mini S&P 500 (CME) ", "2008-01-02", str(end)])152        w.writerow(["CL", "Crude Oil WTI (NYMEX) ", "2008-01-02", str(end)])153        w.writerow(["NG", "Natural Gas (NYMEX) ", "2008-01-02", str(end)])154        w.writerow(["E6", "Euro FX Futures (CME) ", "2008-01-02", str(end)])155        w.writerow(["ZK", "Unknown Product (XXX) ", "2008-01-02", str(end)])156    (root / "state").mkdir(exist_ok=True)157158159if __name__ == "__main__":160    import sys161    build_lake(Path(sys.argv[1] if len(sys.argv) > 1 else "/tmp/hfmd-lake"))162    print("fixture lake built")163