Avi-L commited on
Commit
672df78
·
verified ·
1 Parent(s): c45a387

dataset ingest script

Browse files
Files changed (1) hide show
  1. ingest.py +169 -0
ingest.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build the curated eval dataset repo: Avi-L/crypto-quant-eval-data.
2
+
3
+ Sources (all verified to resolve on the HF Hub, 2026-10-23):
4
+ bars : vaquum/binance_btcusdt_1m_klines (BTCUSDT spot 1m, 2020->present)
5
+ fund : Torch-Trade/btcusdt_perp_funding_8h_05_2021_to_02_2026 (MIT)
6
+ Torch-Trade/ethusdt_perp_funding_8h_05_2021_to_02_2026 (MIT)
7
+ chain : coinmetrics-community btc.csv (GitHub, no usable HF on-chain repo)
8
+
9
+ ETH spot 1m is NOT ingested: the only verified ETH 1m repo
10
+ (HaianCao/Binance-Public-OHLCV) is 34.6 GB — out of scope for this job;
11
+ ETH enters via funding. Documented in the dataset README.
12
+
13
+ Outputs (uploaded to the dataset repo):
14
+ bars_BTCUSDT_1h.parquet, bars_BTCUSDT_1d.parquet (datetime index, OHLCV
15
+ + taker_volume_share when available, funding_rate_last, funding_tick)
16
+ funding_BTCUSDT.parquet, funding_ETHUSDT.parquet (funding_rate, 8h)
17
+ onchain_btc_daily.parquet
18
+ manifest.json (sha256 of every output + source file hashes)
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import hashlib
24
+ import json
25
+ import tempfile
26
+ from pathlib import Path
27
+
28
+ import pandas as pd
29
+ import pyarrow.parquet as pq
30
+ from huggingface_hub import HfApi, snapshot_download
31
+
32
+ DEST_REPO = "Avi-L/crypto-quant-eval-data"
33
+ BARS_REPO = "vaquum/binance_btcusdt_1m_klines"
34
+ FUNDING_REPOS = {
35
+ "BTCUSDT": "Torch-Trade/btcusdt_perp_funding_8h_05_2021_to_02_2026",
36
+ "ETHUSDT": "Torch-Trade/ethusdt_perp_funding_8h_05_2021_to_02_2026",
37
+ }
38
+ ONCHAIN_URL = (
39
+ "https://raw.githubusercontent.com/coinmetrics/data/master/csv/btc.csv"
40
+ )
41
+
42
+ MANIFEST: dict = {"generated": "2026-10-23", "sources": {}, "outputs": {}}
43
+
44
+
45
+ def sha256_file(path: Path) -> str:
46
+ h = hashlib.sha256()
47
+ with open(path, "rb") as f:
48
+ for chunk in iter(lambda: f.read(1 << 20), b""):
49
+ h.update(chunk)
50
+ return h.hexdigest()
51
+
52
+
53
+ def load_bars_streaming(cache: Path) -> pd.DataFrame:
54
+ """Read the vaquum 1m parquet files one at a time, resample to 1h/1d."""
55
+ d = Path(snapshot_download(repo_id=BARS_REPO, repo_type="dataset",
56
+ local_dir=cache / "bars"))
57
+ files = sorted(d.rglob("*.parquet"))
58
+ if not files:
59
+ raise ValueError(f"no parquet files under {d}")
60
+ daily, hourly = [], []
61
+ for f in files:
62
+ df = pd.read_parquet(f)
63
+ # vaquum schema: datetime (ts[ms,UTC]) + OHLCV-ish columns
64
+ tcol = "datetime" if "datetime" in df.columns else df.columns[0]
65
+ ts = pd.to_datetime(df[tcol], utc=True)
66
+ df = df.set_index(ts).sort_index()
67
+ agg = {"open": "first", "high": "max", "low": "min", "close": "last",
68
+ "volume": "sum"}
69
+ # optional columns present in some schemas
70
+ for col, how in (("quote_volume", "sum"), ("count", "sum"),
71
+ ("no_of_trades", "sum"),
72
+ ("taker_buy_volume", "sum")):
73
+ if col in df.columns:
74
+ agg[col] = how
75
+ agg_df = df[list(agg)].resample("1h").agg(agg).dropna(
76
+ subset=["open", "close"])
77
+ hourly.append(agg_df)
78
+ daily.append(df[list(agg)].resample("1d").agg(agg).dropna(
79
+ subset=["open", "close"]))
80
+ h = pd.concat(hourly)
81
+ dd = pd.concat(daily)
82
+ return h[~h.index.duplicated()].sort_index(), dd[~dd.index.duplicated()].sort_index()
83
+
84
+
85
+ def load_funding(cache: Path, repo_id: str) -> pd.DataFrame:
86
+ d = Path(snapshot_download(repo_id=repo_id, repo_type="dataset",
87
+ local_dir=cache / repo_id.replace("/", "_")))
88
+ csvs = sorted(d.rglob("*.csv"))
89
+ pqs = sorted(d.rglob("*.parquet"))
90
+ if csvs:
91
+ df = pd.concat(pd.read_csv(f) for f in csvs)
92
+ elif pqs:
93
+ df = pd.concat(pd.read_parquet(f) for f in pqs)
94
+ else:
95
+ raise ValueError(f"no csv/parquet under {d}")
96
+ if "timestamp" in df.columns:
97
+ df.index = pd.to_datetime(df["timestamp"], utc=True, unit="ms")
98
+ elif "datetime" in df.columns:
99
+ df.index = pd.to_datetime(df["datetime"], utc=True)
100
+ else:
101
+ raise ValueError(f"funding has no time column: {list(df.columns)}")
102
+ if "funding_rate" not in df.columns:
103
+ raise ValueError(f"funding has no funding_rate: {list(df.columns)}")
104
+ return df[["funding_rate"]].sort_index()
105
+
106
+
107
+ def merge_funding(bars: pd.DataFrame, funding: pd.DataFrame) -> pd.DataFrame:
108
+ out = bars.copy()
109
+ f = funding.reindex(out.index, method="ffill")["funding_rate"]
110
+ out["funding_rate_last"] = f
111
+ out["funding_tick"] = out.index.isin(funding.index)
112
+ return out
113
+
114
+
115
+ def onchain_daily(out: Path) -> None:
116
+ df = pd.read_csv(ONCHAIN_URL)
117
+ df["time"] = pd.to_datetime(df["time"], utc=True)
118
+ want = [c for c in ("time", "AdrActCnt", "BlkCnt", "CapMrktCurUSD",
119
+ "FeeMeanNtv", "IssuanceNtv", "TxCnt", "NVTAdj")
120
+ if c in df.columns]
121
+ df[want].set_index("time").to_parquet(out / "onchain_btc_daily.parquet")
122
+ MANIFEST["sources"]["coinmetrics-community btc.csv"] = ONCHAIN_URL
123
+
124
+
125
+ def main() -> None:
126
+ with tempfile.TemporaryDirectory() as tmp:
127
+ cache = Path(tmp)
128
+ out = cache / "curated"
129
+ out.mkdir()
130
+
131
+ print("[ingest] bars: streaming + resampling", flush=True)
132
+ hourly, daily = load_bars_streaming(cache)
133
+ funding_btc = load_funding(cache, FUNDING_REPOS["BTCUSDT"])
134
+ funding_eth = load_funding(cache, FUNDING_REPOS["ETHUSDT"])
135
+
136
+ for freq, bars in (("1h", hourly), ("1d", daily)):
137
+ merged = merge_funding(bars, funding_btc)
138
+ p = out / f"bars_BTCUSDT_{freq}.parquet"
139
+ merged.reset_index(names="datetime").to_parquet(p)
140
+ MANIFEST["outputs"][p.name] = {"sha256": sha256_file(p),
141
+ "rows": len(merged), "freq": freq}
142
+ print(f"[ingest] {p.name}: {len(merged):,} rows", flush=True)
143
+
144
+ for sym, df in (("BTCUSDT", funding_btc), ("ETHUSDT", funding_eth)):
145
+ p = out / f"funding_{sym}.parquet"
146
+ df.to_parquet(p)
147
+ MANIFEST["outputs"][p.name] = {"sha256": sha256_file(p),
148
+ "rows": len(df), "freq": "8h"}
149
+ print(f"[ingest] {p.name}: {len(df):,} ticks", flush=True)
150
+
151
+ print("[ingest] on-chain metrics", flush=True)
152
+ onchain_daily(out)
153
+ p = out / "onchain_btc_daily.parquet"
154
+ MANIFEST["outputs"][p.name] = {"sha256": sha256_file(p),
155
+ "rows": pq.ParquetFile(p).metadata.num_rows,
156
+ "freq": "1d"}
157
+
158
+ (out / "manifest.json").write_text(json.dumps(MANIFEST, indent=2))
159
+ api = HfApi()
160
+ api.upload_folder(
161
+ repo_id=DEST_REPO, repo_type="dataset",
162
+ folder_path=str(out), commit_message="curated eval dataset v1",
163
+ )
164
+ print(f"[ingest] uploaded {len(MANIFEST['outputs'])} files + manifest "
165
+ f"to {DEST_REPO}", flush=True)
166
+
167
+
168
+ if __name__ == "__main__":
169
+ main()