Avi-L commited on
Commit
578ee58
·
verified ·
1 Parent(s): 25f1782

data loader with sanity assertions

Browse files
Files changed (1) hide show
  1. crypto_eval/data.py +113 -0
crypto_eval/data.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Data loading for the crypto-quant-eval dataset.
2
+
3
+ Loads curated parquet files from the dataset repo or a local copy,
4
+ asserts schema and no-lookahead invariants, and exposes simple accessors.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ import pandas as pd
15
+
16
+ REQUIRED_BARS = {
17
+ "datetime", "open", "high", "low", "close", "volume",
18
+ }
19
+ REQUIRED_FUNDING = {"timestamp", "funding_rate"}
20
+
21
+
22
+ def sha256_file(path: str | Path) -> str:
23
+ h = hashlib.sha256()
24
+ with open(path, "rb") as f:
25
+ for chunk in iter(lambda: f.read(1 << 20), b""):
26
+ h.update(chunk)
27
+ return h.hexdigest()
28
+
29
+
30
+ @dataclass
31
+ class EvalData:
32
+ """Curated bar data plus aligned funding ticks."""
33
+
34
+ bars: pd.DataFrame # index: UTC datetime, OHLCV columns
35
+ funding: pd.DataFrame # index: UTC datetime (8h ticks), funding_rate
36
+ symbol: str
37
+ freq: str
38
+
39
+ def assert_sane(self) -> None:
40
+ # bar start time may be carried either as the DatetimeIndex or as
41
+ # a 'datetime' column; the index form is what load_local produces.
42
+ bar_cols = set(self.bars.columns)
43
+ if isinstance(self.bars.index, pd.DatetimeIndex):
44
+ bar_cols = bar_cols | {"datetime"}
45
+ missing = REQUIRED_BARS - bar_cols
46
+ if missing:
47
+ raise ValueError(f"bars missing columns: {sorted(missing)}")
48
+ fund_cols = set(self.funding.columns)
49
+ if isinstance(self.funding.index, pd.DatetimeIndex):
50
+ fund_cols = fund_cols | {"timestamp"}
51
+ missing = REQUIRED_FUNDING - fund_cols
52
+ if missing:
53
+ raise ValueError(f"funding missing columns: {sorted(missing)}")
54
+ if not self.bars.index.is_monotonic_increasing:
55
+ raise ValueError("bars index is not sorted")
56
+ if self.bars.index.has_duplicates:
57
+ raise ValueError("bars index has duplicate timestamps")
58
+ # no-lookahead invariant: bars are closed bars only.
59
+ # The curated dataset stores bar START time; a bar starting at t is
60
+ # only usable at t+freq. load() shifts nothing here — consumers must
61
+ # use shift-based accessors below, never bars.loc[t] inside a
62
+ # signal computed at t.
63
+ nulls = self.bars[["open", "high", "low", "close"]].isna().sum().sum()
64
+ if nulls:
65
+ raise ValueError(f"bars contain {nulls} null OHLC values")
66
+ bad_ohlc = (
67
+ (self.bars["high"] < self.bars["low"])
68
+ | (self.bars["close"] > self.bars["high"])
69
+ | (self.bars["open"] > self.bars["high"])
70
+ | (self.bars["close"] < self.bars["low"])
71
+ | (self.bars["open"] < self.bars["low"])
72
+ ).sum()
73
+ if bad_ohlc:
74
+ raise ValueError(f"{bad_ohlc} bars violate OHLC ordering")
75
+
76
+ def check_funding_coverage(self, freq: str) -> int:
77
+ """Return count of funding ticks falling inside the bar range."""
78
+ mask = (self.funding.index >= self.bars.index[0]) & (
79
+ self.funding.index <= self.bars.index[-1]
80
+ )
81
+ return int(mask.sum())
82
+
83
+
84
+ def load_local(root: str | Path, symbol: str = "BTCUSDT", freq: str = "1h") -> EvalData:
85
+ """Load bars_<symbol>_<freq>.parquet and funding_<symbol>.parquet from root."""
86
+ root = Path(root)
87
+ bars = pd.read_parquet(root / f"bars_{symbol}_{freq}.parquet")
88
+ funding = pd.read_parquet(root / f"funding_{symbol}.parquet")
89
+ # curated parquet stores bar start time as a 'datetime' column; the
90
+ # synthetic/local layout keeps a DatetimeIndex. Accept both.
91
+ for df in (bars, funding):
92
+ if "datetime" in df.columns and not isinstance(df.index, pd.DatetimeIndex):
93
+ ts = pd.to_datetime(df["datetime"], utc=True)
94
+ df.index = ts
95
+ elif "timestamp" in df.columns and not isinstance(df.index, pd.DatetimeIndex):
96
+ df.index = pd.to_datetime(df["timestamp"], utc=True)
97
+ bars = bars.drop(columns=["datetime"], errors="ignore")
98
+ funding = funding.drop(columns=["datetime", "timestamp"], errors="ignore")
99
+ for df in (bars, funding):
100
+ if not isinstance(df.index, pd.DatetimeIndex):
101
+ raise ValueError("parquet files must carry a datetime index")
102
+ if df.index.tz is None:
103
+ df.index = df.index.tz_localize("UTC")
104
+ else:
105
+ df.index = df.index.tz_convert("UTC")
106
+ data = EvalData(bars=bars, funding=funding, symbol=symbol, freq=freq)
107
+ data.assert_sane()
108
+ return data
109
+
110
+
111
+ def load_manifest(root: str | Path) -> dict:
112
+ """Read the source-hash manifest; every eval run must record this."""
113
+ return json.loads((Path(root) / "manifest.json").read_text())