ml-data-access 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ml_data_access/__init__.py +84 -0
- ml_data_access/loaders.py +239 -0
- ml_data_access/store.py +128 -0
- ml_data_access-0.1.0.dist-info/METADATA +79 -0
- ml_data_access-0.1.0.dist-info/RECORD +6 -0
- ml_data_access-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Read side of the malatium data store.
|
|
2
|
+
|
|
3
|
+
import ml_data_access
|
|
4
|
+
|
|
5
|
+
db = ml_data_access.connect()
|
|
6
|
+
reference_df = ml_data_access.load_reference_returns(db, start, end)
|
|
7
|
+
chain_df = ml_data_access.load_option_greeks(db, "AAPL", start, end)
|
|
8
|
+
|
|
9
|
+
One `load_*` per table `ml-data-pipelines` writes, every one taking the
|
|
10
|
+
database and an inclusive `[start, end]` window and returning a collected
|
|
11
|
+
DataFrame. Nothing is screened, joined or derived: the store already holds
|
|
12
|
+
canonical tables, and the two screens a multi-year study needs are named
|
|
13
|
+
functions, `usable_symbol_years` and `in_universe`.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from ml_data_access.loaders import (
|
|
17
|
+
in_universe,
|
|
18
|
+
load_calendar,
|
|
19
|
+
load_corporate_actions,
|
|
20
|
+
load_earnings,
|
|
21
|
+
load_factor_covariances,
|
|
22
|
+
load_factor_loadings,
|
|
23
|
+
load_factor_returns,
|
|
24
|
+
load_forecast,
|
|
25
|
+
load_idio_vol,
|
|
26
|
+
load_index_greeks,
|
|
27
|
+
load_indices,
|
|
28
|
+
load_open_interest,
|
|
29
|
+
load_option_greeks,
|
|
30
|
+
load_rates,
|
|
31
|
+
load_realized_vol,
|
|
32
|
+
load_reference_returns,
|
|
33
|
+
load_sectors,
|
|
34
|
+
load_sessions,
|
|
35
|
+
load_signals,
|
|
36
|
+
load_stock_features,
|
|
37
|
+
load_surface,
|
|
38
|
+
load_symbology_check,
|
|
39
|
+
load_underlying,
|
|
40
|
+
load_universe,
|
|
41
|
+
load_yields,
|
|
42
|
+
usable_symbol_years,
|
|
43
|
+
)
|
|
44
|
+
from ml_data_access.store import (
|
|
45
|
+
available_symbols,
|
|
46
|
+
available_years,
|
|
47
|
+
connect,
|
|
48
|
+
describe,
|
|
49
|
+
scan,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"available_symbols",
|
|
54
|
+
"available_years",
|
|
55
|
+
"connect",
|
|
56
|
+
"describe",
|
|
57
|
+
"in_universe",
|
|
58
|
+
"load_calendar",
|
|
59
|
+
"load_corporate_actions",
|
|
60
|
+
"load_earnings",
|
|
61
|
+
"load_factor_covariances",
|
|
62
|
+
"load_factor_loadings",
|
|
63
|
+
"load_factor_returns",
|
|
64
|
+
"load_forecast",
|
|
65
|
+
"load_idio_vol",
|
|
66
|
+
"load_index_greeks",
|
|
67
|
+
"load_indices",
|
|
68
|
+
"load_open_interest",
|
|
69
|
+
"load_option_greeks",
|
|
70
|
+
"load_rates",
|
|
71
|
+
"load_realized_vol",
|
|
72
|
+
"load_reference_returns",
|
|
73
|
+
"load_sectors",
|
|
74
|
+
"load_sessions",
|
|
75
|
+
"load_signals",
|
|
76
|
+
"load_stock_features",
|
|
77
|
+
"load_surface",
|
|
78
|
+
"load_symbology_check",
|
|
79
|
+
"load_underlying",
|
|
80
|
+
"load_universe",
|
|
81
|
+
"load_yields",
|
|
82
|
+
"scan",
|
|
83
|
+
"usable_symbol_years",
|
|
84
|
+
]
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""One loader per table. Every loader is `load_x(db, start=None, end=None)`.
|
|
2
|
+
|
|
3
|
+
The per-symbol chain loaders take the symbol first, since it selects the
|
|
4
|
+
partition rather than filtering rows. `db` is the connected bear-lake
|
|
5
|
+
database; it is passed for the same reason at-research passes it, so a
|
|
6
|
+
study can hold two stores open.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import datetime as dt
|
|
10
|
+
|
|
11
|
+
import bear_lake as bl
|
|
12
|
+
import polars as pl
|
|
13
|
+
|
|
14
|
+
from ml_data_access.store import in_window, scan, years_in
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_window(
|
|
18
|
+
db: bl.Database, name: str, start: dt.date | None, end: dt.date | None, sort: list[str]
|
|
19
|
+
) -> pl.DataFrame:
|
|
20
|
+
frame = scan(name, years=years_in(start, end, name))
|
|
21
|
+
return db.query(in_window(frame, start, end).sort(sort))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# --- raw ---------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_calendar(
|
|
28
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
29
|
+
) -> pl.DataFrame:
|
|
30
|
+
"""Exchange sessions, one `date` per row."""
|
|
31
|
+
return load_window(db, "calendar", start, end, ["date"])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_sessions(
|
|
35
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
36
|
+
) -> list[dt.date]:
|
|
37
|
+
"""The calendar as a list of dates, the shape `malatium.providers.TradingCalendar` takes."""
|
|
38
|
+
return load_calendar(db, start, end)["date"].to_list()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_universe(
|
|
42
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
43
|
+
) -> pl.DataFrame:
|
|
44
|
+
"""Point-in-time membership: `date`, `ticker` (Wikipedia), `symbol` (option root)."""
|
|
45
|
+
return load_window(db, "universe", start, end, ["date", "ticker"])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_sectors(db: bl.Database) -> pl.DataFrame:
|
|
49
|
+
"""`ticker`, `symbol`, `sector`, `sub_industry`; a snapshot of today's constituents."""
|
|
50
|
+
return db.query(scan("sectors").sort("ticker"))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_indices(
|
|
54
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
55
|
+
) -> pl.DataFrame:
|
|
56
|
+
"""EOD levels for SPX, RUT, OEX, XSP and the VIX complex, 2024 on."""
|
|
57
|
+
return load_window(db, "indices", start, end, ["date", "symbol"])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_yields(
|
|
61
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
62
|
+
) -> pl.DataFrame:
|
|
63
|
+
"""CBOE treasury yield indices (`13w`, `5y`, `10y`, `30y`) as decimals."""
|
|
64
|
+
return load_window(db, "yields", start, end, ["date", "tenor"])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_rates(
|
|
68
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
69
|
+
) -> pl.DataFrame:
|
|
70
|
+
"""Overnight SOFR as a decimal, 2024 on."""
|
|
71
|
+
return load_window(db, "rates", start, end, ["date", "symbol"])
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def load_corporate_actions(
|
|
75
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
76
|
+
) -> pl.DataFrame:
|
|
77
|
+
"""Splits (ex-date ratios) and dividends, one row per (symbol, date, action)."""
|
|
78
|
+
return load_window(db, "corporate_actions", start, end, ["symbol", "date", "action"])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_earnings(
|
|
82
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
83
|
+
) -> pl.DataFrame:
|
|
84
|
+
"""Announcement dates with a `bmo` / `amc` / `unknown` session."""
|
|
85
|
+
return load_window(db, "earnings", start, end, ["symbol", "date"])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def load_underlying(
|
|
89
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
90
|
+
) -> pl.DataFrame:
|
|
91
|
+
"""EOD stock OHLCV, 2023-06 on, `symbol` in option-root spelling."""
|
|
92
|
+
return load_window(db, "underlying", start, end, ["date", "symbol"])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def load_chain(
|
|
96
|
+
db: bl.Database, name: str, symbol: str, start: dt.date | None, end: dt.date | None
|
|
97
|
+
) -> pl.DataFrame:
|
|
98
|
+
frame = scan(name, years=years_in(start, end, name), symbols=[symbol.upper()])
|
|
99
|
+
return db.query(in_window(frame, start, end).sort("date", "expiration", "strike", "right"))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def load_option_greeks(
|
|
103
|
+
db: bl.Database, symbol: str, start: dt.date | None = None, end: dt.date | None = None
|
|
104
|
+
) -> pl.DataFrame:
|
|
105
|
+
"""One name's EOD chain: quotes, `iv` (null where the inversion failed), greeks with
|
|
106
|
+
`vega` per vol point, and `underlying`, the spot the greeks were struck against."""
|
|
107
|
+
return load_chain(db, "option_greeks", symbol, start, end)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def load_index_greeks(
|
|
111
|
+
db: bl.Database, symbol: str, start: dt.date | None = None, end: dt.date | None = None
|
|
112
|
+
) -> pl.DataFrame:
|
|
113
|
+
"""The same for an index root (SPX, SPXW, XSP, VIX); repaired sessions carry null gamma."""
|
|
114
|
+
return load_chain(db, "index_greeks", symbol, start, end)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def load_open_interest(
|
|
118
|
+
db: bl.Database, symbol: str, start: dt.date | None = None, end: dt.date | None = None
|
|
119
|
+
) -> pl.DataFrame:
|
|
120
|
+
"""One name's EOD open interest; stamped pre-open, so it joins the same session's chain."""
|
|
121
|
+
return load_chain(db, "open_interest", symbol, start, end)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def load_symbology_check(db: bl.Database) -> pl.DataFrame:
|
|
125
|
+
"""Per symbol-year: is the stored chain the company the universe names?"""
|
|
126
|
+
return db.query(scan("symbology_check").sort("year", "symbol"))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# --- derived -------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def load_reference_returns(
|
|
133
|
+
db: bl.Database,
|
|
134
|
+
start: dt.date | None = None,
|
|
135
|
+
end: dt.date | None = None,
|
|
136
|
+
symbols: list[str] | None = None,
|
|
137
|
+
) -> pl.DataFrame:
|
|
138
|
+
"""The reference straddle's per-vega P&L per (date, symbol); `SPX` is the market."""
|
|
139
|
+
frame = scan(
|
|
140
|
+
"reference_returns", years=years_in(start, end, "reference_returns"), symbols=symbols
|
|
141
|
+
)
|
|
142
|
+
return db.query(in_window(frame, start, end).sort("date", "symbol"))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def load_factor_returns(
|
|
146
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
147
|
+
) -> pl.DataFrame:
|
|
148
|
+
"""`(date, factor, ret)`: `market` and one GICS sector factor each."""
|
|
149
|
+
return load_window(db, "factor_returns", start, end, ["date", "factor"])
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def load_factor_loadings(
|
|
153
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
154
|
+
) -> pl.DataFrame:
|
|
155
|
+
"""`(date, symbol, factor, loading)`, trailing 250 sessions."""
|
|
156
|
+
return load_window(db, "factor_loadings", start, end, ["date", "symbol", "factor"])
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def load_factor_covariances(
|
|
160
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
161
|
+
) -> pl.DataFrame:
|
|
162
|
+
"""`(date, factor_1, factor_2, covariance)`, Ledoit-Wolf shrunk."""
|
|
163
|
+
return load_window(db, "factor_covariances", start, end, ["date", "factor_1", "factor_2"])
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def load_idio_vol(
|
|
167
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
168
|
+
) -> pl.DataFrame:
|
|
169
|
+
"""`(date, symbol, idio_vol)`, daily residual std per dollar of vega."""
|
|
170
|
+
return load_window(db, "idio_vol", start, end, ["date", "symbol"])
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def load_surface(
|
|
174
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
175
|
+
) -> pl.DataFrame:
|
|
176
|
+
"""Constant-maturity ATM IV per (date, symbol): `iv30`, `iv60`, `iv90`."""
|
|
177
|
+
return load_window(db, "surface", start, end, ["date", "symbol"])
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def load_realized_vol(
|
|
181
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
182
|
+
) -> pl.DataFrame:
|
|
183
|
+
"""Close-to-close realized vol: trailing `rv_1`, `rv_5`, `rv_22` and the forward `rv_fwd`."""
|
|
184
|
+
return load_window(db, "realized_vol", start, end, ["date", "symbol"])
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def load_forecast(
|
|
188
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
189
|
+
) -> pl.DataFrame:
|
|
190
|
+
"""The HAR forecast of the forward 60-session vol, `rv_fcst`."""
|
|
191
|
+
return load_window(db, "forecast", start, end, ["date", "symbol"])
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def load_stock_features(
|
|
195
|
+
db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
|
|
196
|
+
) -> pl.DataFrame:
|
|
197
|
+
"""`ret`, `beta`, `idio_vol`, `gap_freq` per (date, symbol)."""
|
|
198
|
+
return load_window(db, "stock_features", start, end, ["date", "symbol"])
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def load_signals(
|
|
202
|
+
db: bl.Database,
|
|
203
|
+
signal: str,
|
|
204
|
+
start: dt.date | None = None,
|
|
205
|
+
end: dt.date | None = None,
|
|
206
|
+
) -> pl.DataFrame:
|
|
207
|
+
"""`(date, symbol, score)` for one named signal (`vrp`, `iv_zscore`, `momentum`)."""
|
|
208
|
+
frame = scan("signals", years=years_in(start, end, "signals")).filter(
|
|
209
|
+
pl.col("signal") == signal
|
|
210
|
+
)
|
|
211
|
+
return db.query(
|
|
212
|
+
in_window(frame, start, end).select("date", "symbol", "score").sort("date", "symbol")
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# --- screens -------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def usable_symbol_years(db: bl.Database) -> pl.DataFrame:
|
|
220
|
+
"""`(symbol, year)` pairs whose chain is not known to be another company's.
|
|
221
|
+
|
|
222
|
+
Only `wrong_instrument` is excluded. `thin_overlap` means the check could
|
|
223
|
+
not run, and it falls on names later delisted or acquired, so excluding
|
|
224
|
+
it would be a survivorship filter. Semi-join a panel against this.
|
|
225
|
+
"""
|
|
226
|
+
return db.query(
|
|
227
|
+
scan("symbology_check")
|
|
228
|
+
.filter(pl.col("status") != "wrong_instrument")
|
|
229
|
+
.select("symbol", "year")
|
|
230
|
+
.unique()
|
|
231
|
+
.sort("symbol", "year")
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def in_universe(db: bl.Database, panel_df: pl.DataFrame) -> pl.DataFrame:
|
|
236
|
+
"""Keep the (date, symbol) rows that were index members that day."""
|
|
237
|
+
start, end = panel_df["date"].min(), panel_df["date"].max()
|
|
238
|
+
members_df = load_universe(db, start, end).select("date", "symbol").unique()
|
|
239
|
+
return panel_df.join(members_df, on=["date", "symbol"], how="semi")
|
ml_data_access/store.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Connecting to the store and scanning only the partitions a query needs.
|
|
2
|
+
|
|
3
|
+
bear-lake lays a partitioned table out as `<store>/<table>/<year>/<symbol>.parquet`
|
|
4
|
+
with a `metadata.json` beside it. `bl.table()` globs the whole table, which
|
|
5
|
+
is right for the small tables and wrong for a per-symbol read of a 60 GB
|
|
6
|
+
chain table, so `scan` builds the path list from the partition keys in the
|
|
7
|
+
table's metadata and falls back to the glob otherwise.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import datetime as dt
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import bear_lake as bl
|
|
16
|
+
import polars as pl
|
|
17
|
+
from dotenv import load_dotenv
|
|
18
|
+
|
|
19
|
+
load_dotenv()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def connect(path: str | Path | None = None) -> bl.Database:
|
|
23
|
+
"""Connect to the store at `path`, or at `ML_DATA_STORE`."""
|
|
24
|
+
path = path or os.getenv("ML_DATA_STORE")
|
|
25
|
+
if not path:
|
|
26
|
+
raise RuntimeError("set ML_DATA_STORE to the bear-lake directory, or pass a path")
|
|
27
|
+
return bl.connect(str(Path(path).expanduser()))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def store_path() -> Path:
|
|
31
|
+
if not bl.CONNECTED:
|
|
32
|
+
raise RuntimeError("not connected: call ml_data_access.connect() first")
|
|
33
|
+
return Path(bl.DATABASE_PATH)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def table_dir(name: str) -> Path:
|
|
37
|
+
return store_path() / name
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def partition_keys(name: str) -> list[str]:
|
|
41
|
+
metadata_path = table_dir(name) / "metadata.json"
|
|
42
|
+
if not metadata_path.exists():
|
|
43
|
+
raise FileNotFoundError(
|
|
44
|
+
f"no table {name!r} in {store_path()}; run `uv run pipelines {name.replace('_', '-')}`"
|
|
45
|
+
" in ml-data-pipelines"
|
|
46
|
+
)
|
|
47
|
+
return json.loads(metadata_path.read_text()).get("partition_keys") or []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def available_years(name: str) -> list[int]:
|
|
51
|
+
directory = table_dir(name)
|
|
52
|
+
if not directory.is_dir():
|
|
53
|
+
return []
|
|
54
|
+
return sorted(int(child.stem) for child in directory.iterdir() if child.stem.isdigit())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def available_symbols(name: str, year: int) -> list[str]:
|
|
58
|
+
directory = table_dir(name) / str(year)
|
|
59
|
+
if not directory.is_dir():
|
|
60
|
+
return []
|
|
61
|
+
return sorted(path.stem for path in directory.glob("*.parquet"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def scan(
|
|
65
|
+
name: str,
|
|
66
|
+
years: list[int] | None = None,
|
|
67
|
+
symbols: list[str] | None = None,
|
|
68
|
+
) -> pl.LazyFrame:
|
|
69
|
+
"""Lazy scan of `name`, opening only the partitions for `years` and `symbols`."""
|
|
70
|
+
keys = partition_keys(name)
|
|
71
|
+
if keys == ["year", "symbol"] and (years is not None or symbols is not None):
|
|
72
|
+
years = years if years is not None else available_years(name)
|
|
73
|
+
paths = []
|
|
74
|
+
for year in years:
|
|
75
|
+
names = symbols if symbols is not None else available_symbols(name, year)
|
|
76
|
+
paths.extend(
|
|
77
|
+
table_dir(name) / str(year) / f"{symbol}.parquet"
|
|
78
|
+
for symbol in names
|
|
79
|
+
if (table_dir(name) / str(year) / f"{symbol}.parquet").exists()
|
|
80
|
+
)
|
|
81
|
+
elif keys == ["year"] and years is not None:
|
|
82
|
+
paths = [
|
|
83
|
+
table_dir(name) / f"{year}.parquet"
|
|
84
|
+
for year in years
|
|
85
|
+
if (table_dir(name) / f"{year}.parquet").exists()
|
|
86
|
+
]
|
|
87
|
+
else:
|
|
88
|
+
paths = sorted(table_dir(name).glob("**/*.parquet"))
|
|
89
|
+
if not paths:
|
|
90
|
+
raise FileNotFoundError(f"no {name} partitions for years={years} symbols={symbols}")
|
|
91
|
+
frame = pl.scan_parquet(paths)
|
|
92
|
+
if symbols is not None and "symbol" in frame.collect_schema().names():
|
|
93
|
+
frame = frame.filter(pl.col("symbol").is_in(symbols))
|
|
94
|
+
return frame
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def years_in(start: dt.date | None, end: dt.date | None, name: str) -> list[int] | None:
|
|
98
|
+
"""The years a window touches, clipped to what is on disk; None means every year."""
|
|
99
|
+
if start is None and end is None:
|
|
100
|
+
return None
|
|
101
|
+
on_disk = available_years(name)
|
|
102
|
+
first = start.year if start else (on_disk[0] if on_disk else None)
|
|
103
|
+
last = end.year if end else (on_disk[-1] if on_disk else None)
|
|
104
|
+
if first is None or last is None:
|
|
105
|
+
return None
|
|
106
|
+
return [year for year in on_disk if first <= year <= last]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def in_window(
|
|
110
|
+
frame: pl.LazyFrame, start: dt.date | None, end: dt.date | None, column: str = "date"
|
|
111
|
+
) -> pl.LazyFrame:
|
|
112
|
+
if start is not None:
|
|
113
|
+
frame = frame.filter(pl.col(column) >= start)
|
|
114
|
+
if end is not None:
|
|
115
|
+
frame = frame.filter(pl.col(column) <= end)
|
|
116
|
+
return frame
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def describe() -> str:
|
|
120
|
+
"""One line per table: partitions and size."""
|
|
121
|
+
lines = []
|
|
122
|
+
for directory in sorted(store_path().iterdir()):
|
|
123
|
+
if not (directory / "metadata.json").exists():
|
|
124
|
+
continue
|
|
125
|
+
files = list(directory.glob("**/*.parquet"))
|
|
126
|
+
size = sum(file.stat().st_size for file in files)
|
|
127
|
+
lines.append(f"{directory.name:<20} {len(files):>5} files {size / 1e9:7.2f} GB")
|
|
128
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ml-data-access
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read side of the malatium data store: one loader per bear-lake table.
|
|
5
|
+
Project-URL: Repository, https://github.com/Atium-Research/ml-data-access
|
|
6
|
+
Requires-Python: >=3.13
|
|
7
|
+
Requires-Dist: bear-lake>=0.1.5
|
|
8
|
+
Requires-Dist: polars>=1.30
|
|
9
|
+
Requires-Dist: python-dotenv>=1.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# ml-data-access
|
|
13
|
+
|
|
14
|
+
Read side of the malatium data store. One `load_*` per table that [ml-data-pipelines](https://github.com/Atium-Research/ml-data-pipelines) writes, over a [bear-lake](https://github.com/andrewhall1124/bear-lake) database, in the shape of `at-research`'s data helpers.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install ml-data-access
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import datetime as dt
|
|
22
|
+
|
|
23
|
+
import ml_data_access
|
|
24
|
+
|
|
25
|
+
db = ml_data_access.connect() # ML_DATA_STORE, or connect(path)
|
|
26
|
+
start, end = dt.date(2018, 7, 2), dt.date(2025, 6, 30)
|
|
27
|
+
|
|
28
|
+
reference_df = ml_data_access.load_reference_returns(db, start, end)
|
|
29
|
+
scores_df = ml_data_access.load_signals(db, "vrp", start, end)
|
|
30
|
+
loadings_df = ml_data_access.load_factor_loadings(db, start, end)
|
|
31
|
+
chain_df = ml_data_access.load_option_greeks(db, "AAPL", dt.date(2025, 1, 1), dt.date(2025, 3, 31))
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Every loader is `load_x(db, start=None, end=None)` and returns a collected DataFrame for the inclusive window; the three per-symbol chain loaders take the symbol first. Nothing is screened, joined or derived: the store holds canonical tables (`symbol` is the option root, `right` is `C`/`P`, `iv` is null where the vendor's inversion failed, `vega` is per vol point).
|
|
35
|
+
|
|
36
|
+
The frames slot straight into malatium:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from malatium.providers import PanelProvider, TradingCalendar
|
|
40
|
+
|
|
41
|
+
calendar = TradingCalendar(ml_data_access.load_sessions(db, start, end))
|
|
42
|
+
reference = PanelProvider(reference_df)
|
|
43
|
+
scores = PanelProvider(scores_df)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Loaders
|
|
47
|
+
|
|
48
|
+
| loader | table |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `load_calendar`, `load_sessions` | exchange sessions, as a frame or a list of dates |
|
|
51
|
+
| `load_universe` | point-in-time S&P 500 membership, `ticker` and `symbol` |
|
|
52
|
+
| `load_sectors` | GICS sector snapshot |
|
|
53
|
+
| `load_indices`, `load_yields`, `load_rates` | index levels (2024 on), the CBOE yield curve, SOFR |
|
|
54
|
+
| `load_corporate_actions`, `load_earnings` | splits and dividends; announcement dates with a session |
|
|
55
|
+
| `load_underlying` | EOD stock OHLCV, 2023-06 on |
|
|
56
|
+
| `load_option_greeks(db, symbol, ...)`, `load_index_greeks`, `load_open_interest` | one name's chain, index chain, open interest |
|
|
57
|
+
| `load_symbology_check`, `usable_symbol_years` | which symbol-years are the right company |
|
|
58
|
+
| `load_reference_returns` | the reference straddle's per-vega P&L, `SPX` included |
|
|
59
|
+
| `load_factor_returns`, `load_factor_loadings`, `load_factor_covariances`, `load_idio_vol` | the vol risk model |
|
|
60
|
+
| `load_surface`, `load_realized_vol`, `load_forecast`, `load_stock_features` | the derived panels |
|
|
61
|
+
| `load_signals(db, name, ...)` | one signal's `(date, symbol, score)` |
|
|
62
|
+
| `in_universe` | semi-join a `(date, symbol)` panel to membership |
|
|
63
|
+
|
|
64
|
+
`ml_data_access.describe()` lists what the store holds. `ml_data_access.scan(name, years, symbols)` is the lazy scan under every loader; it opens only the partitions asked for, using bear-lake's `<table>/<year>/<symbol>.parquet` layout.
|
|
65
|
+
|
|
66
|
+
## Things to know before trusting a number
|
|
67
|
+
|
|
68
|
+
- **Eighteen symbol-years are another company's chain.** Semi-join against `usable_symbol_years(db)` for any multi-year study.
|
|
69
|
+
- **Open interest is one day stale by construction** and joins on the same `date`.
|
|
70
|
+
- **`iv` is null, not wrong**, on the ~3% of contract-days that failed to invert.
|
|
71
|
+
- **Repaired index sessions** (mostly 2020-21) carry null gamma and a 15:59 underlying.
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
uv sync
|
|
77
|
+
uv run pytest # builds a synthetic store in a temp dir
|
|
78
|
+
uv run ruff check . && uv run ruff format .
|
|
79
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
ml_data_access/__init__.py,sha256=mE-N1q7pCV1M2u9s_wH3PagH-ThCZKhJIAu8lR9FPUk,2042
|
|
2
|
+
ml_data_access/loaders.py,sha256=KkOYxJ7I2kxCaz87-FAqa4JudWAsfBrV5pkqqub-LAk,9059
|
|
3
|
+
ml_data_access/store.py,sha256=tflQQcJ-1MTyEWSsNuEcyRa9pXtaMB4Y9ztUndttaXk,4617
|
|
4
|
+
ml_data_access-0.1.0.dist-info/METADATA,sha256=NaM2Z9iugDZJ23OMs37uTkuFaIFaiQv66JiB5nx2YJ0,3795
|
|
5
|
+
ml_data_access-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
6
|
+
ml_data_access-0.1.0.dist-info/RECORD,,
|