ml-data-access 0.1.0__tar.gz

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.
@@ -0,0 +1,33 @@
1
+ # Publishes to PyPI on a version tag (v0.1.0) through trusted publishing.
2
+ # One-time setup on pypi.org: add a trusted publisher for this repository
3
+ # with workflow `publish.yml` and environment `pypi`. No token is stored here.
4
+ name: publish
5
+
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v5
16
+ - run: uv build
17
+ - uses: actions/upload-artifact@v4
18
+ with:
19
+ name: dist
20
+ path: dist/
21
+
22
+ publish:
23
+ needs: build
24
+ runs-on: ubuntu-latest
25
+ environment: pypi
26
+ permissions:
27
+ id-token: write
28
+ steps:
29
+ - uses: actions/download-artifact@v4
30
+ with:
31
+ name: dist
32
+ path: dist/
33
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,16 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v5
14
+ - run: uv sync --all-extras
15
+ - run: uv run ruff check .
16
+ - run: uv run pytest -q
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .env
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .DS_Store
@@ -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,68 @@
1
+ # ml-data-access
2
+
3
+ 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.
4
+
5
+ ```bash
6
+ pip install ml-data-access
7
+ ```
8
+
9
+ ```python
10
+ import datetime as dt
11
+
12
+ import ml_data_access
13
+
14
+ db = ml_data_access.connect() # ML_DATA_STORE, or connect(path)
15
+ start, end = dt.date(2018, 7, 2), dt.date(2025, 6, 30)
16
+
17
+ reference_df = ml_data_access.load_reference_returns(db, start, end)
18
+ scores_df = ml_data_access.load_signals(db, "vrp", start, end)
19
+ loadings_df = ml_data_access.load_factor_loadings(db, start, end)
20
+ chain_df = ml_data_access.load_option_greeks(db, "AAPL", dt.date(2025, 1, 1), dt.date(2025, 3, 31))
21
+ ```
22
+
23
+ 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).
24
+
25
+ The frames slot straight into malatium:
26
+
27
+ ```python
28
+ from malatium.providers import PanelProvider, TradingCalendar
29
+
30
+ calendar = TradingCalendar(ml_data_access.load_sessions(db, start, end))
31
+ reference = PanelProvider(reference_df)
32
+ scores = PanelProvider(scores_df)
33
+ ```
34
+
35
+ ## Loaders
36
+
37
+ | loader | table |
38
+ | --- | --- |
39
+ | `load_calendar`, `load_sessions` | exchange sessions, as a frame or a list of dates |
40
+ | `load_universe` | point-in-time S&P 500 membership, `ticker` and `symbol` |
41
+ | `load_sectors` | GICS sector snapshot |
42
+ | `load_indices`, `load_yields`, `load_rates` | index levels (2024 on), the CBOE yield curve, SOFR |
43
+ | `load_corporate_actions`, `load_earnings` | splits and dividends; announcement dates with a session |
44
+ | `load_underlying` | EOD stock OHLCV, 2023-06 on |
45
+ | `load_option_greeks(db, symbol, ...)`, `load_index_greeks`, `load_open_interest` | one name's chain, index chain, open interest |
46
+ | `load_symbology_check`, `usable_symbol_years` | which symbol-years are the right company |
47
+ | `load_reference_returns` | the reference straddle's per-vega P&L, `SPX` included |
48
+ | `load_factor_returns`, `load_factor_loadings`, `load_factor_covariances`, `load_idio_vol` | the vol risk model |
49
+ | `load_surface`, `load_realized_vol`, `load_forecast`, `load_stock_features` | the derived panels |
50
+ | `load_signals(db, name, ...)` | one signal's `(date, symbol, score)` |
51
+ | `in_universe` | semi-join a `(date, symbol)` panel to membership |
52
+
53
+ `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.
54
+
55
+ ## Things to know before trusting a number
56
+
57
+ - **Eighteen symbol-years are another company's chain.** Semi-join against `usable_symbol_years(db)` for any multi-year study.
58
+ - **Open interest is one day stale by construction** and joins on the same `date`.
59
+ - **`iv` is null, not wrong**, on the ~3% of contract-days that failed to invert.
60
+ - **Repaired index sessions** (mostly 2020-21) carry null gamma and a 15:59 underlying.
61
+
62
+ ## Development
63
+
64
+ ```bash
65
+ uv sync
66
+ uv run pytest # builds a synthetic store in a temp dir
67
+ uv run ruff check . && uv run ruff format .
68
+ ```
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "ml-data-access"
3
+ version = "0.1.0"
4
+ description = "Read side of the malatium data store: one loader per bear-lake table."
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "bear-lake>=0.1.5",
9
+ "polars>=1.30",
10
+ "python-dotenv>=1.0",
11
+ ]
12
+
13
+ [project.urls]
14
+ Repository = "https://github.com/Atium-Research/ml-data-access"
15
+
16
+ [dependency-groups]
17
+ dev = [
18
+ "pytest>=8",
19
+ "ruff>=0.6",
20
+ ]
21
+
22
+ [build-system]
23
+ requires = ["hatchling"]
24
+ build-backend = "hatchling.build"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["src/ml_data_access"]
28
+
29
+ [tool.pytest.ini_options]
30
+ testpaths = ["tests"]
31
+
32
+ [tool.ruff]
33
+ line-length = 100
34
+ target-version = "py313"
35
+
36
+ [tool.ruff.lint]
37
+ select = ["E", "F", "I", "UP", "B"]
@@ -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")