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.
- ml_data_access-0.1.0/.github/workflows/publish.yml +33 -0
- ml_data_access-0.1.0/.github/workflows/test.yml +16 -0
- ml_data_access-0.1.0/.gitignore +10 -0
- ml_data_access-0.1.0/PKG-INFO +79 -0
- ml_data_access-0.1.0/README.md +68 -0
- ml_data_access-0.1.0/pyproject.toml +37 -0
- ml_data_access-0.1.0/src/ml_data_access/__init__.py +84 -0
- ml_data_access-0.1.0/src/ml_data_access/loaders.py +239 -0
- ml_data_access-0.1.0/src/ml_data_access/store.py +128 -0
- ml_data_access-0.1.0/tests/test_loaders.py +144 -0
- ml_data_access-0.1.0/uv.lock +831 -0
|
@@ -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,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")
|