1855 lines
77 KiB
Plaintext
1855 lines
77 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "6535ec44",
|
||
"metadata": {
|
||
"papermill": {
|
||
"duration": 0.001892,
|
||
"end_time": "2026-06-13T03:31:40.097070+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:40.095178+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"# Slow Features and Context: Fundamentals, Macro, Calendar\n",
|
||
"\n",
|
||
"**Chapter 8: Feature Engineering**\n",
|
||
"**Section Reference**: 8.4 - Contextual and Slow-Moving Features\n",
|
||
"**Docker image**: `ml4t`\n",
|
||
"\n",
|
||
"## Purpose\n",
|
||
"\n",
|
||
"This notebook covers **slow-moving features** that provide context for faster signals:\n",
|
||
"\n",
|
||
"1. **Fundamentals**: Value, quality, growth factors from financial statements\n",
|
||
"2. **Macro**: Economic indicators, yield curves, credit spreads, risk regimes\n",
|
||
"3. **Calendar**: Cyclical encodings for seasonal patterns\n",
|
||
"\n",
|
||
"## Key Principle\n",
|
||
"\n",
|
||
"Slow features update infrequently (quarterly, monthly, or by schedule) but\n",
|
||
"condition daily decisions. The binding constraint is **data integrity** —\n",
|
||
"ensuring each observation reflects only what was knowable at decision time.\n",
|
||
"\n",
|
||
"## Data Policy\n",
|
||
"\n",
|
||
"All examples use **real data** (SEC XBRL, FRED macro).\n",
|
||
"\n",
|
||
"## References\n",
|
||
"\n",
|
||
"- Fama and French (1992, 1993): Value, size, profitability factors\n",
|
||
"- Cochrane (2011): \"Presidential Address: Discount Rates\" — factor zoo\n",
|
||
"- Harvey, Liu, and Zhu (2016): \"...and the Cross-Section of Expected Returns\"\n",
|
||
"\n",
|
||
"## Case Study Mapping\n",
|
||
"\n",
|
||
"| Case Study | Relevant Features |\n",
|
||
"|------------|-------------------|\n",
|
||
"| ETFs (`etfs`) | Calendar encodings, macro regimes |\n",
|
||
"| US Firm Characteristics (`us_firm_characteristics`) | All fundamental factors |\n",
|
||
"| S&P 500 Equity+Options (`sp500_equity_option_analytics`) | Macro + VIX regime |"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 1,
|
||
"id": "7cfe716a",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:40.101505Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:40.101394Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:41.434226Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:41.433840Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 1.336014,
|
||
"end_time": "2026-06-13T03:31:41.435178+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:40.099164+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"\"\"\"Slow Features and Context: Fundamentals, Macro, Calendar — contextual features that condition faster signals.\"\"\"\n",
|
||
"\n",
|
||
"from __future__ import annotations\n",
|
||
"\n",
|
||
"import warnings\n",
|
||
"from datetime import date, datetime, timedelta\n",
|
||
"\n",
|
||
"import polars as pl\n",
|
||
"\n",
|
||
"warnings.filterwarnings(\"ignore\")\n",
|
||
"\n",
|
||
"from data import load_macro as _load_macro_canonical\n",
|
||
"from data import load_sec_xbrl_fundamentals"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 2,
|
||
"id": "b9943257",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:41.440054Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:41.439956Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:41.441667Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:41.441341Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.004626,
|
||
"end_time": "2026-06-13T03:31:41.441939+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.437313+00:00",
|
||
"status": "completed"
|
||
},
|
||
"tags": [
|
||
"parameters"
|
||
]
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"SEED = 42\n",
|
||
"CALENDAR_START_DATE = \"2015-01-01\""
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "0f585017",
|
||
"metadata": {
|
||
"papermill": {
|
||
"duration": 0.001435,
|
||
"end_time": "2026-06-13T03:31:41.444867+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.443432+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"---\n",
|
||
"\n",
|
||
"# Part 1: Fundamental Factors\n",
|
||
"\n",
|
||
"Fundamental factors update quarterly but inform daily trading decisions.\n",
|
||
"\n",
|
||
"**Key challenges**:\n",
|
||
"- Point-in-time accuracy (use announcement date, not period end)\n",
|
||
"- Forward-filling to daily frequency\n",
|
||
"- Factor staleness between announcements"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "f62a9f57",
|
||
"metadata": {
|
||
"papermill": {
|
||
"duration": 0.001424,
|
||
"end_time": "2026-06-13T03:31:41.447772+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.446348+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 1.1 Load Fundamental Data\n",
|
||
"\n",
|
||
"### Scope: scaffolding for the construction mechanics, not a real-data value pipeline\n",
|
||
"\n",
|
||
"`load_fundamentals()` reads SEC XBRL filings. XBRL publishes accounting numbers\n",
|
||
"(book equity, earnings, operating cash flow, capex) but does **not** publish\n",
|
||
"market capitalization — that comes from market prices on the announcement\n",
|
||
"date. To keep the value-factor cells below executable on the XBRL output\n",
|
||
"alone, this notebook approximates `market_cap = 2 × book_value`. This is a\n",
|
||
"**scaffolding** value: it lets the downstream `compute_value_factors()` cell\n",
|
||
"show the mechanics of book-to-market, earnings yield, and cash-flow yield,\n",
|
||
"but the resulting numbers are **not** the real-data factor values.\n",
|
||
"\n",
|
||
"The lookahead-safe, real-data version (XBRL fundamentals joined to daily\n",
|
||
"adjusted prices on the announcement date, with point-in-time discipline) is\n",
|
||
"demonstrated in the `us_firm_characteristics` case study and uses the\n",
|
||
"`load_firm_characteristics()` loader from `data/equities/loader.py`. See\n",
|
||
"Chapter 11's case study pipeline (`case_studies/us_firm_characteristics/`)\n",
|
||
"and the Chen-Pelger-Zhu (2020) panel for the production version."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"id": "d6accaa0",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:41.451289Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:41.451178Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:41.452866Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:41.452574Z"
|
||
},
|
||
"lines_to_next_cell": 2,
|
||
"papermill": {
|
||
"duration": 0.003854,
|
||
"end_time": "2026-06-13T03:31:41.453086+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.449232+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Denominator safety constant (used by all factor computations)\n",
|
||
"EPSILON = 1e-10"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"id": "f415ffae",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:41.456567Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:41.456453Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:41.482643Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:41.482376Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.028518,
|
||
"end_time": "2026-06-13T03:31:41.483084+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.454566+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Fundamental data: 240 rows, 20 symbols\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div><style>\n",
|
||
".dataframe > thead > tr,\n",
|
||
".dataframe > tbody > tr {\n",
|
||
" text-align: right;\n",
|
||
" white-space: pre-wrap;\n",
|
||
"}\n",
|
||
"</style>\n",
|
||
"<small>shape: (5, 20)</small><table border=\"1\" class=\"dataframe\"><thead><tr><th>symbol</th><th>cik</th><th>entity_name</th><th>fiscal_quarter_end</th><th>announcement_date</th><th>accession</th><th>assets</th><th>cashandcashequivalentsatcarryingvalue</th><th>grossprofit</th><th>liabilities</th><th>total_debt</th><th>operating_cf</th><th>earnings</th><th>operatingincomeloss</th><th>capex</th><th>revenue</th><th>book_value</th><th>market_cap</th><th>total_assets</th><th>accruals</th></tr><tr><td>str</td><td>i64</td><td>str</td><td>date</td><td>date</td><td>str</td><td>i64</td><td>i64</td><td>i64</td><td>i64</td><td>i64</td><td>i64</td><td>i64</td><td>i64</td><td>i64</td><td>i64</td><td>i64</td><td>f64</td><td>i64</td><td>f64</td></tr></thead><tbody><tr><td>"AAPL"</td><td>320193</td><td>"Apple Inc."</td><td>2022-03-26</td><td>2022-04-29</td><td>"0000320193-22-000059"</td><td>350662000000</td><td>28098000000</td><td>42559000000</td><td>283263000000</td><td>113000000000</td><td>null</td><td>25010000000</td><td>29979000000</td><td>null</td><td>null</td><td>67399000000</td><td>1.3480e11</td><td>350662000000</td><td>0.0</td></tr><tr><td>"AAPL"</td><td>320193</td><td>"Apple Inc."</td><td>2022-06-25</td><td>2022-07-29</td><td>"0000320193-22-000070"</td><td>336309000000</td><td>27502000000</td><td>35885000000</td><td>278202000000</td><td>108700000000</td><td>null</td><td>19442000000</td><td>23076000000</td><td>null</td><td>null</td><td>58107000000</td><td>1.1621e11</td><td>336309000000</td><td>0.0</td></tr><tr><td>"AAPL"</td><td>320193</td><td>"Apple Inc."</td><td>2022-09-24</td><td>2023-11-03</td><td>"0000320193-23-000106"</td><td>352755000000</td><td>23646000000</td><td>null</td><td>302083000000</td><td>110087000000</td><td>null</td><td>null</td><td>null</td><td>null</td><td>null</td><td>50672000000</td><td>1.0134e11</td><td>352755000000</td><td>0.0</td></tr><tr><td>"AAPL"</td><td>320193</td><td>"Apple Inc."</td><td>2022-12-31</td><td>2023-02-03</td><td>"0000320193-23-000006"</td><td>346747000000</td><td>20535000000</td><td>50332000000</td><td>290020000000</td><td>109400000000</td><td>34005000000</td><td>29998000000</td><td>36016000000</td><td>3787000000</td><td>null</td><td>56727000000</td><td>1.1345e11</td><td>346747000000</td><td>-4.0070e9</td></tr><tr><td>"AAPL"</td><td>320193</td><td>"Apple Inc."</td><td>2023-04-01</td><td>2023-05-05</td><td>"0000320193-23-000064"</td><td>332160000000</td><td>24687000000</td><td>41976000000</td><td>270002000000</td><td>107600000000</td><td>null</td><td>24160000000</td><td>28318000000</td><td>null</td><td>null</td><td>62158000000</td><td>1.2432e11</td><td>332160000000</td><td>0.0</td></tr></tbody></table></div>"
|
||
],
|
||
"text/plain": [
|
||
"shape: (5, 20)\n",
|
||
"┌────────┬────────┬────────────┬────────────┬───┬────────────┬────────────┬────────────┬───────────┐\n",
|
||
"│ symbol ┆ cik ┆ entity_nam ┆ fiscal_qua ┆ … ┆ book_value ┆ market_cap ┆ total_asse ┆ accruals │\n",
|
||
"│ --- ┆ --- ┆ e ┆ rter_end ┆ ┆ --- ┆ --- ┆ ts ┆ --- │\n",
|
||
"│ str ┆ i64 ┆ --- ┆ --- ┆ ┆ i64 ┆ f64 ┆ --- ┆ f64 │\n",
|
||
"│ ┆ ┆ str ┆ date ┆ ┆ ┆ ┆ i64 ┆ │\n",
|
||
"╞════════╪════════╪════════════╪════════════╪═══╪════════════╪════════════╪════════════╪═══════════╡\n",
|
||
"│ AAPL ┆ 320193 ┆ Apple Inc. ┆ 2022-03-26 ┆ … ┆ 6739900000 ┆ 1.3480e11 ┆ 3506620000 ┆ 0.0 │\n",
|
||
"│ ┆ ┆ ┆ ┆ ┆ 0 ┆ ┆ 00 ┆ │\n",
|
||
"│ AAPL ┆ 320193 ┆ Apple Inc. ┆ 2022-06-25 ┆ … ┆ 5810700000 ┆ 1.1621e11 ┆ 3363090000 ┆ 0.0 │\n",
|
||
"│ ┆ ┆ ┆ ┆ ┆ 0 ┆ ┆ 00 ┆ │\n",
|
||
"│ AAPL ┆ 320193 ┆ Apple Inc. ┆ 2022-09-24 ┆ … ┆ 5067200000 ┆ 1.0134e11 ┆ 3527550000 ┆ 0.0 │\n",
|
||
"│ ┆ ┆ ┆ ┆ ┆ 0 ┆ ┆ 00 ┆ │\n",
|
||
"│ AAPL ┆ 320193 ┆ Apple Inc. ┆ 2022-12-31 ┆ … ┆ 5672700000 ┆ 1.1345e11 ┆ 3467470000 ┆ -4.0070e9 │\n",
|
||
"│ ┆ ┆ ┆ ┆ ┆ 0 ┆ ┆ 00 ┆ │\n",
|
||
"│ AAPL ┆ 320193 ┆ Apple Inc. ┆ 2023-04-01 ┆ … ┆ 6215800000 ┆ 1.2432e11 ┆ 3321600000 ┆ 0.0 │\n",
|
||
"│ ┆ ┆ ┆ ┆ ┆ 0 ┆ ┆ 00 ┆ │\n",
|
||
"└────────┴────────┴────────────┴────────────┴───┴────────────┴────────────┴────────────┴───────────┘"
|
||
]
|
||
},
|
||
"execution_count": 4,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"# Map lowercase us-gaap concepts to the shorter names used downstream.\n",
|
||
"# The XBRL loader exposes one column per us-gaap concept in lowercase.\n",
|
||
"_XBRL_RENAMES = {\n",
|
||
" \"stockholdersequity\": \"book_value\",\n",
|
||
" \"netincomeloss\": \"earnings\",\n",
|
||
" \"revenues\": \"revenue\",\n",
|
||
" \"netcashprovidedbyusedinoperatingactivities\": \"operating_cf\",\n",
|
||
" \"longtermdebt\": \"total_debt\",\n",
|
||
" \"paymentstoacquirepropertyplantandequipment\": \"capex\",\n",
|
||
"}\n",
|
||
"\n",
|
||
"\n",
|
||
"def load_fundamentals() -> pl.DataFrame:\n",
|
||
" \"\"\"Load SEC XBRL fundamentals and normalize to factor-friendly names.\n",
|
||
"\n",
|
||
" Note: `market_cap` remains a SCAFFOLDING approximation (2× book value)\n",
|
||
" because XBRL does not publish market capitalization. Production systems\n",
|
||
" should join with actual price data on the announcement date.\n",
|
||
" \"\"\"\n",
|
||
" df = load_sec_xbrl_fundamentals().rename(_XBRL_RENAMES)\n",
|
||
"\n",
|
||
" # `assets` preserves its lowercase concept name; alias for downstream code.\n",
|
||
" df = df.with_columns(\n",
|
||
" [\n",
|
||
" # Market cap approximation — SCAFFOLDING only (XBRL has no market cap)\n",
|
||
" (pl.col(\"book_value\") * 2.0).alias(\"market_cap\"),\n",
|
||
" pl.col(\"assets\").alias(\"total_assets\"),\n",
|
||
" ]\n",
|
||
" )\n",
|
||
"\n",
|
||
" # Accruals (earnings - operating CF)\n",
|
||
" if \"operating_cf\" in df.columns:\n",
|
||
" df = df.with_columns(\n",
|
||
" pl.when(pl.col(\"operating_cf\").is_not_null())\n",
|
||
" .then(pl.col(\"earnings\") - pl.col(\"operating_cf\"))\n",
|
||
" .otherwise(0.0)\n",
|
||
" .alias(\"accruals\")\n",
|
||
" )\n",
|
||
"\n",
|
||
" return df\n",
|
||
"\n",
|
||
"\n",
|
||
"fundamentals = load_fundamentals()\n",
|
||
"print(f\"Fundamental data: {len(fundamentals):,} rows, {fundamentals['symbol'].n_unique()} symbols\")\n",
|
||
"fundamentals.head(5)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "802f4e11",
|
||
"metadata": {
|
||
"lines_to_next_cell": 2,
|
||
"papermill": {
|
||
"duration": 0.001572,
|
||
"end_time": "2026-06-13T03:31:41.486385+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.484813+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 1.2 Value Factors\n",
|
||
"\n",
|
||
"Value factors identify stocks trading at discounts relative to fundamentals.\n",
|
||
"\n",
|
||
"> **Reminder**: every factor below has `market_cap` in the denominator and\n",
|
||
"> `market_cap` is the `2 × book_value` scaffolding from §1.1. The cell\n",
|
||
"> demonstrates the *construction* of book-to-market, earnings yield, and\n",
|
||
"> cash-flow yield; the *values* are not the real-data factor values. See\n",
|
||
"> the `us_firm_characteristics` case study for the production version."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 5,
|
||
"id": "277e450b",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:41.490020Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:41.489938Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:41.493661Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:41.493387Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.005992,
|
||
"end_time": "2026-06-13T03:31:41.493919+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.487927+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Value factors computed:\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div><style>\n",
|
||
".dataframe > thead > tr,\n",
|
||
".dataframe > tbody > tr {\n",
|
||
" text-align: right;\n",
|
||
" white-space: pre-wrap;\n",
|
||
"}\n",
|
||
"</style>\n",
|
||
"<small>shape: (10, 4)</small><table border=\"1\" class=\"dataframe\"><thead><tr><th>symbol</th><th>fiscal_quarter_end</th><th>book_to_market</th><th>earnings_yield</th></tr><tr><td>str</td><td>date</td><td>f64</td><td>f64</td></tr></thead><tbody><tr><td>"XOM"</td><td>2022-09-30</td><td>0.5</td><td>0.052821</td></tr><tr><td>"XOM"</td><td>2022-12-31</td><td>0.5</td><td>null</td></tr><tr><td>"XOM"</td><td>2023-03-31</td><td>0.5</td><td>0.028764</td></tr><tr><td>"XOM"</td><td>2023-06-30</td><td>0.5</td><td>0.019794</td></tr><tr><td>"XOM"</td><td>2023-09-30</td><td>0.5</td><td>0.022709</td></tr><tr><td>"XOM"</td><td>2023-12-31</td><td>0.5</td><td>null</td></tr><tr><td>"XOM"</td><td>2024-03-31</td><td>0.5</td><td>0.020024</td></tr><tr><td>"XOM"</td><td>2024-06-30</td><td>0.5</td><td>0.017213</td></tr><tr><td>"XOM"</td><td>2024-09-30</td><td>0.5</td><td>0.016028</td></tr><tr><td>"XOM"</td><td>2024-12-31</td><td>0.5</td><td>null</td></tr></tbody></table></div>"
|
||
],
|
||
"text/plain": [
|
||
"shape: (10, 4)\n",
|
||
"┌────────┬────────────────────┬────────────────┬────────────────┐\n",
|
||
"│ symbol ┆ fiscal_quarter_end ┆ book_to_market ┆ earnings_yield │\n",
|
||
"│ --- ┆ --- ┆ --- ┆ --- │\n",
|
||
"│ str ┆ date ┆ f64 ┆ f64 │\n",
|
||
"╞════════╪════════════════════╪════════════════╪════════════════╡\n",
|
||
"│ XOM ┆ 2022-09-30 ┆ 0.5 ┆ 0.052821 │\n",
|
||
"│ XOM ┆ 2022-12-31 ┆ 0.5 ┆ null │\n",
|
||
"│ XOM ┆ 2023-03-31 ┆ 0.5 ┆ 0.028764 │\n",
|
||
"│ XOM ┆ 2023-06-30 ┆ 0.5 ┆ 0.019794 │\n",
|
||
"│ XOM ┆ 2023-09-30 ┆ 0.5 ┆ 0.022709 │\n",
|
||
"│ XOM ┆ 2023-12-31 ┆ 0.5 ┆ null │\n",
|
||
"│ XOM ┆ 2024-03-31 ┆ 0.5 ┆ 0.020024 │\n",
|
||
"│ XOM ┆ 2024-06-30 ┆ 0.5 ┆ 0.017213 │\n",
|
||
"│ XOM ┆ 2024-09-30 ┆ 0.5 ┆ 0.016028 │\n",
|
||
"│ XOM ┆ 2024-12-31 ┆ 0.5 ┆ null │\n",
|
||
"└────────┴────────────────────┴────────────────┴────────────────┘"
|
||
]
|
||
},
|
||
"execution_count": 5,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"def compute_value_factors(df: pl.DataFrame) -> pl.DataFrame:\n",
|
||
" \"\"\"\n",
|
||
" Compute value factors with denominator clipping for safety.\n",
|
||
" \"\"\"\n",
|
||
" return df.with_columns(\n",
|
||
" [\n",
|
||
" # Book-to-Market\n",
|
||
" (pl.col(\"book_value\") / pl.col(\"market_cap\").clip(EPSILON, None)).alias(\n",
|
||
" \"book_to_market\"\n",
|
||
" ),\n",
|
||
" # Earnings yield\n",
|
||
" (pl.col(\"earnings\") / pl.col(\"market_cap\").clip(EPSILON, None)).alias(\"earnings_yield\"),\n",
|
||
" # Cash flow yield\n",
|
||
" (pl.col(\"operating_cf\") / pl.col(\"market_cap\").clip(EPSILON, None)).alias(\"cf_yield\"),\n",
|
||
" # FCF yield\n",
|
||
" (\n",
|
||
" (pl.col(\"operating_cf\") - pl.col(\"capex\"))\n",
|
||
" / pl.col(\"market_cap\").clip(EPSILON, None)\n",
|
||
" ).alias(\"fcf_yield\"),\n",
|
||
" ]\n",
|
||
" )\n",
|
||
"\n",
|
||
"\n",
|
||
"value_df = compute_value_factors(fundamentals)\n",
|
||
"print(\"Value factors computed:\")\n",
|
||
"value_df.select([\"symbol\", \"fiscal_quarter_end\", \"book_to_market\", \"earnings_yield\"]).tail(10)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "5d55999f",
|
||
"metadata": {
|
||
"papermill": {
|
||
"duration": 0.001577,
|
||
"end_time": "2026-06-13T03:31:41.497132+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.495555+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"**Interpretation**: A book-to-market ratio of 0.5 means the stock trades at\n",
|
||
"2x its book value — the market assigns significant intangible/growth premium.\n",
|
||
"Earnings yield is the inverse of the P/E ratio, making higher values more\n",
|
||
"\"value-oriented.\""
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "befcf775",
|
||
"metadata": {
|
||
"lines_to_next_cell": 2,
|
||
"papermill": {
|
||
"duration": 0.001553,
|
||
"end_time": "2026-06-13T03:31:41.500279+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.498726+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 1.3 Quality Factors\n",
|
||
"\n",
|
||
"Quality factors identify financially healthy companies."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 6,
|
||
"id": "f7875ce8",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:41.503972Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:41.503903Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:41.507499Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:41.507158Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.006167,
|
||
"end_time": "2026-06-13T03:31:41.508073+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.501906+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Quality factors computed:\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div><style>\n",
|
||
".dataframe > thead > tr,\n",
|
||
".dataframe > tbody > tr {\n",
|
||
" text-align: right;\n",
|
||
" white-space: pre-wrap;\n",
|
||
"}\n",
|
||
"</style>\n",
|
||
"<small>shape: (10, 5)</small><table border=\"1\" class=\"dataframe\"><thead><tr><th>symbol</th><th>fiscal_quarter_end</th><th>roe</th><th>roa</th><th>accruals_ratio</th></tr><tr><td>str</td><td>date</td><td>f64</td><td>f64</td><td>f64</td></tr></thead><tbody><tr><td>"XOM"</td><td>2022-09-30</td><td>0.105642</td><td>0.053113</td><td>0.0</td></tr><tr><td>"XOM"</td><td>2022-12-31</td><td>null</td><td>null</td><td>0.0</td></tr><tr><td>"XOM"</td><td>2023-03-31</td><td>0.057528</td><td>0.030944</td><td>-0.013296</td></tr><tr><td>"XOM"</td><td>2023-06-30</td><td>0.039589</td><td>0.021693</td><td>0.0</td></tr><tr><td>"XOM"</td><td>2023-09-30</td><td>0.045417</td><td>0.024365</td><td>0.0</td></tr><tr><td>"XOM"</td><td>2023-12-31</td><td>null</td><td>null</td><td>0.0</td></tr><tr><td>"XOM"</td><td>2024-03-31</td><td>0.040049</td><td>0.021751</td><td>-0.017051</td></tr><tr><td>"XOM"</td><td>2024-06-30</td><td>0.034426</td><td>0.020056</td><td>0.0</td></tr><tr><td>"XOM"</td><td>2024-09-30</td><td>0.032056</td><td>0.01864</td><td>0.0</td></tr><tr><td>"XOM"</td><td>2024-12-31</td><td>null</td><td>null</td><td>0.0</td></tr></tbody></table></div>"
|
||
],
|
||
"text/plain": [
|
||
"shape: (10, 5)\n",
|
||
"┌────────┬────────────────────┬──────────┬──────────┬────────────────┐\n",
|
||
"│ symbol ┆ fiscal_quarter_end ┆ roe ┆ roa ┆ accruals_ratio │\n",
|
||
"│ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n",
|
||
"│ str ┆ date ┆ f64 ┆ f64 ┆ f64 │\n",
|
||
"╞════════╪════════════════════╪══════════╪══════════╪════════════════╡\n",
|
||
"│ XOM ┆ 2022-09-30 ┆ 0.105642 ┆ 0.053113 ┆ 0.0 │\n",
|
||
"│ XOM ┆ 2022-12-31 ┆ null ┆ null ┆ 0.0 │\n",
|
||
"│ XOM ┆ 2023-03-31 ┆ 0.057528 ┆ 0.030944 ┆ -0.013296 │\n",
|
||
"│ XOM ┆ 2023-06-30 ┆ 0.039589 ┆ 0.021693 ┆ 0.0 │\n",
|
||
"│ XOM ┆ 2023-09-30 ┆ 0.045417 ┆ 0.024365 ┆ 0.0 │\n",
|
||
"│ XOM ┆ 2023-12-31 ┆ null ┆ null ┆ 0.0 │\n",
|
||
"│ XOM ┆ 2024-03-31 ┆ 0.040049 ┆ 0.021751 ┆ -0.017051 │\n",
|
||
"│ XOM ┆ 2024-06-30 ┆ 0.034426 ┆ 0.020056 ┆ 0.0 │\n",
|
||
"│ XOM ┆ 2024-09-30 ┆ 0.032056 ┆ 0.01864 ┆ 0.0 │\n",
|
||
"│ XOM ┆ 2024-12-31 ┆ null ┆ null ┆ 0.0 │\n",
|
||
"└────────┴────────────────────┴──────────┴──────────┴────────────────┘"
|
||
]
|
||
},
|
||
"execution_count": 6,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"def compute_quality_factors(df: pl.DataFrame) -> pl.DataFrame:\n",
|
||
" \"\"\"\n",
|
||
" Compute quality factors with denominator safety.\n",
|
||
" \"\"\"\n",
|
||
" return df.with_columns(\n",
|
||
" [\n",
|
||
" # ROE\n",
|
||
" (pl.col(\"earnings\") / pl.col(\"book_value\").clip(EPSILON, None)).alias(\"roe\"),\n",
|
||
" # ROA\n",
|
||
" (pl.col(\"earnings\") / pl.col(\"total_assets\").clip(EPSILON, None)).alias(\"roa\"),\n",
|
||
" # Accruals ratio (lower = better quality)\n",
|
||
" (pl.col(\"accruals\") / pl.col(\"total_assets\").clip(EPSILON, None)).alias(\n",
|
||
" \"accruals_ratio\"\n",
|
||
" ),\n",
|
||
" # Leverage\n",
|
||
" (pl.col(\"total_debt\") / pl.col(\"total_assets\").clip(EPSILON, None)).alias(\n",
|
||
" \"debt_to_assets\"\n",
|
||
" ),\n",
|
||
" ]\n",
|
||
" )\n",
|
||
"\n",
|
||
"\n",
|
||
"quality_df = compute_quality_factors(value_df)\n",
|
||
"print(\"Quality factors computed:\")\n",
|
||
"quality_df.select([\"symbol\", \"fiscal_quarter_end\", \"roe\", \"roa\", \"accruals_ratio\"]).tail(10)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "6ff87e1c",
|
||
"metadata": {
|
||
"lines_to_next_cell": 2,
|
||
"papermill": {
|
||
"duration": 0.00164,
|
||
"end_time": "2026-06-13T03:31:41.512868+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.511228+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 1.4 Daily Alignment with Correct ASOF Join\n",
|
||
"\n",
|
||
"**Critical**: Both DataFrames must be sorted by the join keys.\n",
|
||
"\n",
|
||
"```python\n",
|
||
"# WRONG: Only sorting by date\n",
|
||
"daily_df.join_asof(fundamental_df.sort(\"timestamp\"), on=\"timestamp\")\n",
|
||
"\n",
|
||
"# CORRECT: Sort both by [symbol, date]\n",
|
||
"daily_df.sort([\"symbol\", \"timestamp\"]).join_asof(\n",
|
||
" fundamental_df.sort([\"symbol\", \"announcement_date\"]),\n",
|
||
" left_on=\"timestamp\",\n",
|
||
" right_on=\"announcement_date\",\n",
|
||
" by=\"symbol\",\n",
|
||
")\n",
|
||
"```"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 7,
|
||
"id": "4341c9a6",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:41.516704Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:41.516632Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:41.524990Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:41.524712Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.010813,
|
||
"end_time": "2026-06-13T03:31:41.525315+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.514502+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Daily aligned: 4,200 rows\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div><style>\n",
|
||
".dataframe > thead > tr,\n",
|
||
".dataframe > tbody > tr {\n",
|
||
" text-align: right;\n",
|
||
" white-space: pre-wrap;\n",
|
||
"}\n",
|
||
"</style>\n",
|
||
"<small>shape: (10, 6)</small><table border=\"1\" class=\"dataframe\"><thead><tr><th>timestamp</th><th>symbol</th><th>announcement_date</th><th>fiscal_quarter_end</th><th>roe</th><th>book_to_market</th></tr><tr><td>date</td><td>str</td><td>date</td><td>date</td><td>f64</td><td>f64</td></tr></thead><tbody><tr><td>2024-01-01</td><td>"XOM"</td><td>2023-10-31</td><td>2023-09-30</td><td>0.045417</td><td>0.5</td></tr><tr><td>2024-01-02</td><td>"XOM"</td><td>2023-10-31</td><td>2023-09-30</td><td>0.045417</td><td>0.5</td></tr><tr><td>2024-01-03</td><td>"XOM"</td><td>2023-10-31</td><td>2023-09-30</td><td>0.045417</td><td>0.5</td></tr><tr><td>2024-01-04</td><td>"XOM"</td><td>2023-10-31</td><td>2023-09-30</td><td>0.045417</td><td>0.5</td></tr><tr><td>2024-01-08</td><td>"XOM"</td><td>2023-10-31</td><td>2023-09-30</td><td>0.045417</td><td>0.5</td></tr><tr><td>2024-01-09</td><td>"XOM"</td><td>2023-10-31</td><td>2023-09-30</td><td>0.045417</td><td>0.5</td></tr><tr><td>2024-01-10</td><td>"XOM"</td><td>2023-10-31</td><td>2023-09-30</td><td>0.045417</td><td>0.5</td></tr><tr><td>2024-01-11</td><td>"XOM"</td><td>2023-10-31</td><td>2023-09-30</td><td>0.045417</td><td>0.5</td></tr><tr><td>2024-01-15</td><td>"XOM"</td><td>2023-10-31</td><td>2023-09-30</td><td>0.045417</td><td>0.5</td></tr><tr><td>2024-01-16</td><td>"XOM"</td><td>2023-10-31</td><td>2023-09-30</td><td>0.045417</td><td>0.5</td></tr></tbody></table></div>"
|
||
],
|
||
"text/plain": [
|
||
"shape: (10, 6)\n",
|
||
"┌────────────┬────────┬───────────────────┬────────────────────┬──────────┬────────────────┐\n",
|
||
"│ timestamp ┆ symbol ┆ announcement_date ┆ fiscal_quarter_end ┆ roe ┆ book_to_market │\n",
|
||
"│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n",
|
||
"│ date ┆ str ┆ date ┆ date ┆ f64 ┆ f64 │\n",
|
||
"╞════════════╪════════╪═══════════════════╪════════════════════╪══════════╪════════════════╡\n",
|
||
"│ 2024-01-01 ┆ XOM ┆ 2023-10-31 ┆ 2023-09-30 ┆ 0.045417 ┆ 0.5 │\n",
|
||
"│ 2024-01-02 ┆ XOM ┆ 2023-10-31 ┆ 2023-09-30 ┆ 0.045417 ┆ 0.5 │\n",
|
||
"│ 2024-01-03 ┆ XOM ┆ 2023-10-31 ┆ 2023-09-30 ┆ 0.045417 ┆ 0.5 │\n",
|
||
"│ 2024-01-04 ┆ XOM ┆ 2023-10-31 ┆ 2023-09-30 ┆ 0.045417 ┆ 0.5 │\n",
|
||
"│ 2024-01-08 ┆ XOM ┆ 2023-10-31 ┆ 2023-09-30 ┆ 0.045417 ┆ 0.5 │\n",
|
||
"│ 2024-01-09 ┆ XOM ┆ 2023-10-31 ┆ 2023-09-30 ┆ 0.045417 ┆ 0.5 │\n",
|
||
"│ 2024-01-10 ┆ XOM ┆ 2023-10-31 ┆ 2023-09-30 ┆ 0.045417 ┆ 0.5 │\n",
|
||
"│ 2024-01-11 ┆ XOM ┆ 2023-10-31 ┆ 2023-09-30 ┆ 0.045417 ┆ 0.5 │\n",
|
||
"│ 2024-01-15 ┆ XOM ┆ 2023-10-31 ┆ 2023-09-30 ┆ 0.045417 ┆ 0.5 │\n",
|
||
"│ 2024-01-16 ┆ XOM ┆ 2023-10-31 ┆ 2023-09-30 ┆ 0.045417 ┆ 0.5 │\n",
|
||
"└────────────┴────────┴───────────────────┴────────────────────┴──────────┴────────────────┘"
|
||
]
|
||
},
|
||
"execution_count": 7,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"def align_factors_to_daily(\n",
|
||
" factor_df: pl.DataFrame,\n",
|
||
" daily_dates: pl.DataFrame,\n",
|
||
" announcement_col: str = \"announcement_date\",\n",
|
||
") -> pl.DataFrame:\n",
|
||
" \"\"\"\n",
|
||
" Align quarterly factors to daily frequency using ASOF join.\n",
|
||
"\n",
|
||
" CRITICAL: Both frames must be sorted by join keys.\n",
|
||
" \"\"\"\n",
|
||
" # Ensure sorting on both frames (REQUIRED for join_asof)\n",
|
||
" factor_sorted = factor_df.sort([\"symbol\", announcement_col])\n",
|
||
" daily_sorted = daily_dates.sort([\"symbol\", \"timestamp\"])\n",
|
||
"\n",
|
||
" # ASOF join: each day gets most recent announced values\n",
|
||
" aligned = daily_sorted.join_asof(\n",
|
||
" factor_sorted,\n",
|
||
" left_on=\"timestamp\",\n",
|
||
" right_on=announcement_col,\n",
|
||
" by=\"symbol\",\n",
|
||
" strategy=\"backward\",\n",
|
||
" )\n",
|
||
"\n",
|
||
" return aligned\n",
|
||
"\n",
|
||
"\n",
|
||
"# Create daily dates for alignment demo\n",
|
||
"symbols = quality_df[\"symbol\"].unique().to_list()\n",
|
||
"daily_dates = (\n",
|
||
" pl.DataFrame(\n",
|
||
" {\"timestamp\": pl.date_range(date(2024, 1, 1), date(2024, 12, 31), \"1d\", eager=True)}\n",
|
||
" )\n",
|
||
" .filter(pl.col(\"timestamp\").dt.weekday() < 5) # Business days\n",
|
||
" .join(pl.DataFrame({\"symbol\": symbols}), how=\"cross\")\n",
|
||
")\n",
|
||
"\n",
|
||
"aligned = align_factors_to_daily(\n",
|
||
" quality_df.select(\n",
|
||
" [\"symbol\", \"announcement_date\", \"fiscal_quarter_end\", \"roe\", \"book_to_market\"]\n",
|
||
" ),\n",
|
||
" daily_dates,\n",
|
||
")\n",
|
||
"\n",
|
||
"print(f\"Daily aligned: {len(aligned):,} rows\")\n",
|
||
"aligned.filter(pl.col(\"symbol\") == symbols[0]).head(10)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "d8a0bc3b",
|
||
"metadata": {
|
||
"papermill": {
|
||
"duration": 0.001711,
|
||
"end_time": "2026-06-13T03:31:41.528848+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.527137+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"### 1.5 Fake Sample Size Warning\n",
|
||
"\n",
|
||
"Forward-filling quarterly data to daily frequency inflates the apparent\n",
|
||
"sample size. Each unique fundamental observation appears ~63 times (one\n",
|
||
"quarter of trading days), but carries the same information."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 8,
|
||
"id": "d1feb759",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:41.532739Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:41.532674Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:41.535533Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:41.535203Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.005413,
|
||
"end_time": "2026-06-13T03:31:41.535953+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.530540+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Daily rows: 4,200\n",
|
||
"Unique observations: 81\n",
|
||
"Inflation ratio: 52x\n",
|
||
"\n",
|
||
"Each fundamental observation is repeated ~63 times via forward-fill.\n",
|
||
"This inflates t-statistics if not accounted for.\n",
|
||
"See Section 7.2 on uniqueness weighting for the correction.\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Count unique fundamental observations vs total daily rows\n",
|
||
"if len(aligned) > 0:\n",
|
||
" n_daily = len(aligned)\n",
|
||
" # Approximate unique observations: distinct (symbol, fiscal_quarter_end) pairs\n",
|
||
" n_unique = (\n",
|
||
" aligned.drop_nulls([\"fiscal_quarter_end\"])\n",
|
||
" .select([\"symbol\", \"fiscal_quarter_end\"])\n",
|
||
" .unique()\n",
|
||
" .shape[0]\n",
|
||
" )\n",
|
||
" inflation_ratio = n_daily / max(n_unique, 1)\n",
|
||
"\n",
|
||
" print(f\"Daily rows: {n_daily:,}\")\n",
|
||
" print(f\"Unique observations: {n_unique:,}\")\n",
|
||
" print(f\"Inflation ratio: {inflation_ratio:.0f}x\")\n",
|
||
" print(\n",
|
||
" \"\\nEach fundamental observation is repeated ~63 times via forward-fill.\"\n",
|
||
" \"\\nThis inflates t-statistics if not accounted for.\"\n",
|
||
" \"\\nSee Section 7.2 on uniqueness weighting for the correction.\"\n",
|
||
" )"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "c199aa81",
|
||
"metadata": {
|
||
"papermill": {
|
||
"duration": 0.001679,
|
||
"end_time": "2026-06-13T03:31:41.539393+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.537714+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"---\n",
|
||
"\n",
|
||
"# Part 2: Macro Features\n",
|
||
"\n",
|
||
"Macro data comes at mixed frequencies (daily, weekly, monthly, quarterly).\n",
|
||
"\n",
|
||
"**Key considerations**:\n",
|
||
"- **Publication lag**: Monthly data has 2-4 week delay\n",
|
||
"- **Revisions**: Initial estimates are often revised\n",
|
||
"- **Forward-fill carefully**: Limit to avoid stale data"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "4e43dc4e",
|
||
"metadata": {
|
||
"lines_to_next_cell": 2,
|
||
"papermill": {
|
||
"duration": 0.001698,
|
||
"end_time": "2026-06-13T03:31:41.542876+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.541178+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 2.1 Load Macro Data\n",
|
||
"\n",
|
||
"> **Publication Lag Warning**: Macro data has significant publication delays.\n",
|
||
"> Conservative approach: Lag monthly data by 30+ days."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 9,
|
||
"id": "60f02e33",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:41.546888Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:41.546767Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:41.551409Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:41.551116Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.007099,
|
||
"end_time": "2026-06-13T03:31:41.551689+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.544590+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Macro data: 9,497 rows\n",
|
||
"Columns: ['dff', 'dgs1', 'dgs2', 'dgs3', 'dgs5', 'dgs7', 'dgs10', 'dgs20', 'dgs30', 't10y2y']\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"macro = _load_macro_canonical()\n",
|
||
"print(f\"Macro data: {len(macro):,} rows\")\n",
|
||
"print(f\"Columns: {[c for c in macro.columns if c != 'timestamp'][:10]}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "c02fd8cc",
|
||
"metadata": {
|
||
"lines_to_next_cell": 2,
|
||
"papermill": {
|
||
"duration": 0.00171,
|
||
"end_time": "2026-06-13T03:31:41.555213+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.553503+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 2.2 Trend Features with Publication Lag\n",
|
||
"\n",
|
||
"> **Conservative Lagging**: For monthly data, add 30-day lag to ensure\n",
|
||
"> the data was actually available at the trading date."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 10,
|
||
"id": "716eb299",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:41.559171Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:41.559097Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:41.564159Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:41.563764Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.007694,
|
||
"end_time": "2026-06-13T03:31:41.564628+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.556934+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Macro features: 38 columns\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"def create_macro_trend_features(\n",
|
||
" df: pl.DataFrame,\n",
|
||
" cols: list[str],\n",
|
||
" windows: list[int] = [21, 63, 252],\n",
|
||
" conservative_lag: int = 0, # Days to lag for publication delay\n",
|
||
") -> pl.DataFrame:\n",
|
||
" \"\"\"\n",
|
||
" Create trend features from macro data.\n",
|
||
"\n",
|
||
" Args:\n",
|
||
" df: Macro data\n",
|
||
" cols: Columns to process\n",
|
||
" windows: Rolling window sizes\n",
|
||
" conservative_lag: Days to lag for publication delay safety\n",
|
||
" \"\"\"\n",
|
||
" # Apply conservative lag if specified\n",
|
||
" if conservative_lag > 0:\n",
|
||
" lag_exprs = [\n",
|
||
" pl.col(c).shift(conservative_lag).alias(f\"{c}_lagged\") for c in cols if c in df.columns\n",
|
||
" ]\n",
|
||
" df = df.with_columns(lag_exprs)\n",
|
||
" cols = [f\"{c}_lagged\" for c in cols if c in df.columns]\n",
|
||
"\n",
|
||
" feature_exprs = []\n",
|
||
" for col in cols:\n",
|
||
" if col not in df.columns:\n",
|
||
" continue\n",
|
||
"\n",
|
||
" for w in windows:\n",
|
||
" # Z-score\n",
|
||
" feature_exprs.append(\n",
|
||
" (\n",
|
||
" (pl.col(col) - pl.col(col).rolling_mean(w))\n",
|
||
" / pl.col(col).rolling_std(w).clip(EPSILON, None)\n",
|
||
" ).alias(f\"{col}_zscore_{w}d\")\n",
|
||
" )\n",
|
||
" # Rate of change\n",
|
||
" feature_exprs.append(pl.col(col).pct_change(w).alias(f\"{col}_roc_{w}d\"))\n",
|
||
"\n",
|
||
" return df.with_columns(feature_exprs)\n",
|
||
"\n",
|
||
"\n",
|
||
"# Apply to VIX (daily, no lag needed)\n",
|
||
"daily_cols = [\"vixcls\", \"dgs10\", \"t10y2y\"]\n",
|
||
"macro_features = create_macro_trend_features(\n",
|
||
" macro,\n",
|
||
" [c for c in daily_cols if c in macro.columns],\n",
|
||
" windows=[21, 63],\n",
|
||
")\n",
|
||
"\n",
|
||
"print(f\"Macro features: {len(macro_features.columns)} columns\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "6f0aa03e",
|
||
"metadata": {
|
||
"papermill": {
|
||
"duration": 0.001758,
|
||
"end_time": "2026-06-13T03:31:41.568246+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.566488+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"**Interpretation**: Z-scored macro data measures whether the current indicator\n",
|
||
"level is unusual relative to its recent history. A VIX z-score of +2 means\n",
|
||
"fear is elevated relative to the last 21 or 63 days — this conditions how\n",
|
||
"momentum and carry signals perform."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "8e821bfd",
|
||
"metadata": {
|
||
"lines_to_next_cell": 2,
|
||
"papermill": {
|
||
"duration": 0.001721,
|
||
"end_time": "2026-06-13T03:31:41.571720+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.569999+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 2.3 Monthly Features with Correct Forward-Fill\n",
|
||
"\n",
|
||
"**Fix**: Use forward-filled version for YoY/3m changes, not raw monthly."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 11,
|
||
"id": "6b20ee85",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:41.575744Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:41.575668Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:41.578847Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:41.578486Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.005631,
|
||
"end_time": "2026-06-13T03:31:41.579094+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.573463+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"def create_monthly_features(\n",
|
||
" df: pl.DataFrame,\n",
|
||
" monthly_cols: list[str],\n",
|
||
" conservative_lag: int = 30, # Monthly data publication delay\n",
|
||
") -> pl.DataFrame:\n",
|
||
" \"\"\"\n",
|
||
" Create features from monthly macro data.\n",
|
||
"\n",
|
||
" Uses forward-filled version for change calculations.\n",
|
||
" Applies conservative lag for publication delay.\n",
|
||
" \"\"\"\n",
|
||
" feature_exprs = []\n",
|
||
"\n",
|
||
" for col in monthly_cols:\n",
|
||
" if col not in df.columns:\n",
|
||
" continue\n",
|
||
"\n",
|
||
" # Forward-fill with limit (avoid very stale data)\n",
|
||
" ffill_col = f\"{col}_ffill\"\n",
|
||
" df = df.with_columns(\n",
|
||
" pl.col(col).shift(conservative_lag).forward_fill(limit=45).alias(ffill_col)\n",
|
||
" )\n",
|
||
"\n",
|
||
" # YoY change (using forward-filled, lagged version)\n",
|
||
" feature_exprs.append(pl.col(ffill_col).pct_change(252).alias(f\"{col}_yoy\"))\n",
|
||
" # 3-month change\n",
|
||
" feature_exprs.append(pl.col(ffill_col).pct_change(63).alias(f\"{col}_3m_chg\"))\n",
|
||
"\n",
|
||
" if feature_exprs:\n",
|
||
" df = df.with_columns(feature_exprs)\n",
|
||
"\n",
|
||
" return df\n",
|
||
"\n",
|
||
"\n",
|
||
"# Example: unemployment (monthly)\n",
|
||
"if \"unrate\" in macro.columns:\n",
|
||
" macro_features = create_monthly_features(macro_features, [\"unrate\"], conservative_lag=30)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "4d0acb29",
|
||
"metadata": {
|
||
"lines_to_next_cell": 2,
|
||
"papermill": {
|
||
"duration": 0.001869,
|
||
"end_time": "2026-06-13T03:31:41.582843+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.580974+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 2.4 Relative Value Features\n",
|
||
"\n",
|
||
"**Naming fix**: Rolling median ≠ percentile rank. Be precise."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 12,
|
||
"id": "bbd61ea8",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:41.587152Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:41.587027Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:41.592534Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:41.592290Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.008187,
|
||
"end_time": "2026-06-13T03:31:41.592806+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.584619+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"def create_relative_value_features(df: pl.DataFrame) -> pl.DataFrame:\n",
|
||
" \"\"\"\n",
|
||
" Create relative value features with correct naming.\n",
|
||
"\n",
|
||
" Note: rolling_median is NOT a percentile rank - it's the median value.\n",
|
||
" Percentile rank would be: rank(current) / count (0-100 scale).\n",
|
||
" \"\"\"\n",
|
||
" feature_exprs = []\n",
|
||
"\n",
|
||
" # Credit spread (if available)\n",
|
||
" if \"bamlc0a0cm\" in df.columns:\n",
|
||
" feature_exprs.append(pl.col(\"bamlc0a0cm\").alias(\"credit_spread\"))\n",
|
||
"\n",
|
||
" # Term spread (if available)\n",
|
||
" if \"t10y2y\" in df.columns:\n",
|
||
" feature_exprs.append(pl.col(\"t10y2y\").alias(\"term_spread\"))\n",
|
||
"\n",
|
||
" if feature_exprs:\n",
|
||
" df = df.with_columns(feature_exprs)\n",
|
||
"\n",
|
||
" # Rolling MEDIAN (not percentile - be precise about naming)\n",
|
||
" median_cols = [\"vixcls\", \"credit_spread\", \"term_spread\"]\n",
|
||
" median_exprs = [\n",
|
||
" pl.col(c).rolling_median(252).alias(f\"{c}_rolling_median_252d\")\n",
|
||
" for c in median_cols\n",
|
||
" if c in df.columns\n",
|
||
" ]\n",
|
||
"\n",
|
||
" if median_exprs:\n",
|
||
" df = df.with_columns(median_exprs)\n",
|
||
"\n",
|
||
" return df\n",
|
||
"\n",
|
||
"\n",
|
||
"macro_features = create_relative_value_features(macro_features)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "6434d344",
|
||
"metadata": {
|
||
"papermill": {
|
||
"duration": 0.001721,
|
||
"end_time": "2026-06-13T03:31:41.596327+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.594606+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 2.4b Yield-Curve Slope Feature\n",
|
||
"\n",
|
||
"The yield-curve slope (10Y-2Y spread) is loaded as `t10y2y`, but the text\n",
|
||
"specifies additional processing: a 5-day EMA for smoothing and a 250-day\n",
|
||
"z-score for regime-relative positioning."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 13,
|
||
"id": "0be6deaa",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:41.600268Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:41.600202Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:41.603319Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:41.603063Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.00577,
|
||
"end_time": "2026-06-13T03:31:41.603846+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.598076+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Yield-curve slope feature:\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div><style>\n",
|
||
".dataframe > thead > tr,\n",
|
||
".dataframe > tbody > tr {\n",
|
||
" text-align: right;\n",
|
||
" white-space: pre-wrap;\n",
|
||
"}\n",
|
||
"</style>\n",
|
||
"<small>shape: (5, 4)</small><table border=\"1\" class=\"dataframe\"><thead><tr><th>timestamp</th><th>t10y2y</th><th>yc_slope_ema5</th><th>yc_slope_zscore_250d</th></tr><tr><td>date</td><td>f64</td><td>f64</td><td>f64</td></tr></thead><tbody><tr><td>2025-12-27</td><td>0.68</td><td>0.683052</td><td>2.852638</td></tr><tr><td>2025-12-28</td><td>0.68</td><td>0.682034</td><td>2.788564</td></tr><tr><td>2025-12-29</td><td>0.67</td><td>0.678023</td><td>2.66936</td></tr><tr><td>2025-12-30</td><td>0.69</td><td>0.682015</td><td>2.70253</td></tr><tr><td>2025-12-31</td><td>0.71</td><td>0.691344</td><td>2.82924</td></tr></tbody></table></div>"
|
||
],
|
||
"text/plain": [
|
||
"shape: (5, 4)\n",
|
||
"┌────────────┬────────┬───────────────┬──────────────────────┐\n",
|
||
"│ timestamp ┆ t10y2y ┆ yc_slope_ema5 ┆ yc_slope_zscore_250d │\n",
|
||
"│ --- ┆ --- ┆ --- ┆ --- │\n",
|
||
"│ date ┆ f64 ┆ f64 ┆ f64 │\n",
|
||
"╞════════════╪════════╪═══════════════╪══════════════════════╡\n",
|
||
"│ 2025-12-27 ┆ 0.68 ┆ 0.683052 ┆ 2.852638 │\n",
|
||
"│ 2025-12-28 ┆ 0.68 ┆ 0.682034 ┆ 2.788564 │\n",
|
||
"│ 2025-12-29 ┆ 0.67 ┆ 0.678023 ┆ 2.66936 │\n",
|
||
"│ 2025-12-30 ┆ 0.69 ┆ 0.682015 ┆ 2.70253 │\n",
|
||
"│ 2025-12-31 ┆ 0.71 ┆ 0.691344 ┆ 2.82924 │\n",
|
||
"└────────────┴────────┴───────────────┴──────────────────────┘"
|
||
]
|
||
},
|
||
"execution_count": 13,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"# Yield-curve slope: EMA smoothing + rolling z-score\n",
|
||
"macro_features = macro_features.with_columns(\n",
|
||
" pl.col(\"t10y2y\").ewm_mean(span=5, ignore_nulls=True).alias(\"yc_slope_ema5\"),\n",
|
||
").with_columns(\n",
|
||
" [\n",
|
||
" (\n",
|
||
" (pl.col(\"yc_slope_ema5\") - pl.col(\"yc_slope_ema5\").rolling_mean(250))\n",
|
||
" / pl.col(\"yc_slope_ema5\").rolling_std(250).clip(EPSILON, None)\n",
|
||
" ).alias(\"yc_slope_zscore_250d\"),\n",
|
||
" ]\n",
|
||
")\n",
|
||
"print(\"Yield-curve slope feature:\")\n",
|
||
"macro_features.select([\"timestamp\", \"t10y2y\", \"yc_slope_ema5\", \"yc_slope_zscore_250d\"]).tail(5)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "d17554d0",
|
||
"metadata": {
|
||
"papermill": {
|
||
"duration": 0.002079,
|
||
"end_time": "2026-06-13T03:31:41.608156+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.606077+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"**Interpretation**: The z-score centers the slope relative to its recent history.\n",
|
||
"Values above +2 indicate an unusually steep curve (risk-on, growth expectations);\n",
|
||
"below -2 indicates inversion (recession signal). The EMA removes daily noise\n",
|
||
"without introducing significant lag."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "b2f4800f",
|
||
"metadata": {
|
||
"lines_to_next_cell": 2,
|
||
"papermill": {
|
||
"duration": 0.002345,
|
||
"end_time": "2026-06-13T03:31:41.612727+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.610382+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 2.5 Risk Regime Features"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 14,
|
||
"id": "a7e82341",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:41.617034Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:41.616940Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:41.622626Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:41.622183Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.008453,
|
||
"end_time": "2026-06-13T03:31:41.623016+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.614563+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Risk regime features:\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div><style>\n",
|
||
".dataframe > thead > tr,\n",
|
||
".dataframe > tbody > tr {\n",
|
||
" text-align: right;\n",
|
||
" white-space: pre-wrap;\n",
|
||
"}\n",
|
||
"</style>\n",
|
||
"<small>shape: (5, 2)</small><table border=\"1\" class=\"dataframe\"><thead><tr><th>vix_regime</th><th>vix_relative_to_max</th></tr><tr><td>i32</td><td>f64</td></tr></thead><tbody><tr><td>0</td><td>0.402129</td></tr><tr><td>0</td><td>0.402129</td></tr><tr><td>0</td><td>0.464508</td></tr><tr><td>0</td><td>0.503691</td></tr><tr><td>0</td><td>0.56479</td></tr></tbody></table></div>"
|
||
],
|
||
"text/plain": [
|
||
"shape: (5, 2)\n",
|
||
"┌────────────┬─────────────────────┐\n",
|
||
"│ vix_regime ┆ vix_relative_to_max │\n",
|
||
"│ --- ┆ --- │\n",
|
||
"│ i32 ┆ f64 │\n",
|
||
"╞════════════╪═════════════════════╡\n",
|
||
"│ 0 ┆ 0.402129 │\n",
|
||
"│ 0 ┆ 0.402129 │\n",
|
||
"│ 0 ┆ 0.464508 │\n",
|
||
"│ 0 ┆ 0.503691 │\n",
|
||
"│ 0 ┆ 0.56479 │\n",
|
||
"└────────────┴─────────────────────┘"
|
||
]
|
||
},
|
||
"execution_count": 14,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"def create_risk_regime_features(df: pl.DataFrame) -> pl.DataFrame:\n",
|
||
" \"\"\"Create risk regime indicators.\"\"\"\n",
|
||
" feature_exprs = []\n",
|
||
"\n",
|
||
" # VIX regime (thresholds: <15 low, 15-25 normal, >25 high)\n",
|
||
" if \"vixcls\" in df.columns:\n",
|
||
" feature_exprs.append(\n",
|
||
" pl.when(pl.col(\"vixcls\") < 15)\n",
|
||
" .then(0)\n",
|
||
" .when(pl.col(\"vixcls\") < 25)\n",
|
||
" .then(1)\n",
|
||
" .otherwise(2)\n",
|
||
" .alias(\"vix_regime\")\n",
|
||
" )\n",
|
||
" # VIX ratio to 252-day max\n",
|
||
" feature_exprs.append(\n",
|
||
" (pl.col(\"vixcls\") / pl.col(\"vixcls\").rolling_max(252).clip(EPSILON, None)).alias(\n",
|
||
" \"vix_relative_to_max\"\n",
|
||
" )\n",
|
||
" )\n",
|
||
"\n",
|
||
" # Credit regime\n",
|
||
" if \"credit_spread\" in df.columns:\n",
|
||
" feature_exprs.append(\n",
|
||
" pl.when(pl.col(\"credit_spread\") < 1.0)\n",
|
||
" .then(0)\n",
|
||
" .when(pl.col(\"credit_spread\") < 2.0)\n",
|
||
" .then(1)\n",
|
||
" .otherwise(2)\n",
|
||
" .alias(\"credit_regime\")\n",
|
||
" )\n",
|
||
"\n",
|
||
" return df.with_columns(feature_exprs) if feature_exprs else df\n",
|
||
"\n",
|
||
"\n",
|
||
"macro_features = create_risk_regime_features(macro_features)\n",
|
||
"print(\"Risk regime features:\")\n",
|
||
"macro_features.select([c for c in macro_features.columns if \"regime\" in c or \"relative\" in c]).tail(\n",
|
||
" 5\n",
|
||
")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "11540a55",
|
||
"metadata": {
|
||
"papermill": {
|
||
"duration": 0.001895,
|
||
"end_time": "2026-06-13T03:31:41.626921+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.625026+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"---\n",
|
||
"\n",
|
||
"# Part 3: Calendar and Seasonal Encodings\n",
|
||
"\n",
|
||
"Calendar features encode **predictable clocks**: sessions, day-of-week,\n",
|
||
"month-of-year, and scheduled events. The key principle is to encode\n",
|
||
"**phase and proximity**, not outcomes."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "23203775",
|
||
"metadata": {
|
||
"papermill": {
|
||
"duration": 0.001849,
|
||
"end_time": "2026-06-13T03:31:41.630664+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.628815+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 3.1 Cyclical Encoding\n",
|
||
"\n",
|
||
"Encoding month as an integer (1-12) implies an ordinal relationship\n",
|
||
"(December > January). Cyclical sin/cos encoding removes this artifact:\n",
|
||
"\n",
|
||
"$$x_{\\sin} = \\sin\\left(\\frac{2\\pi \\cdot m}{12}\\right), \\quad x_{\\cos} = \\cos\\left(\\frac{2\\pi \\cdot m}{12}\\right)$$"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 15,
|
||
"id": "b9d46dcc",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:41.635215Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:41.635075Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:42.345362Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:42.344974Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.713132,
|
||
"end_time": "2026-06-13T03:31:42.345662+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:41.632530+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"2026-06-12 23:31:42 | mlquant.features.cyclical_encode | INFO | [cyclical_encode] Starting calculation with parameters: period=12, name_prefix=month (shape: (0,))\n"
|
||
]
|
||
},
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"2026-06-12 23:31:42 | mlquant.features.cyclical_encode | INFO | [cyclical_encode] Completed calculation (shape: (0,)) (0.02ms)\n"
|
||
]
|
||
},
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"2026-06-12 23:31:42 | mlquant.features.cyclical_encode | INFO | [cyclical_encode] Starting calculation with parameters: period=5, name_prefix=dow (shape: (0,))\n"
|
||
]
|
||
},
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"2026-06-12 23:31:42 | mlquant.features.cyclical_encode | INFO | [cyclical_encode] Completed calculation (shape: (0,)) (0.02ms)\n"
|
||
]
|
||
},
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Calendar encodings (last 10 rows):\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div><style>\n",
|
||
".dataframe > thead > tr,\n",
|
||
".dataframe > tbody > tr {\n",
|
||
" text-align: right;\n",
|
||
" white-space: pre-wrap;\n",
|
||
"}\n",
|
||
"</style>\n",
|
||
"<small>shape: (10, 5)</small><table border=\"1\" class=\"dataframe\"><thead><tr><th>timestamp</th><th>month_sin</th><th>month_cos</th><th>dow_sin</th><th>dow_cos</th></tr><tr><td>date</td><td>f64</td><td>f64</td><td>f64</td><td>f64</td></tr></thead><tbody><tr><td>2025-12-17</td><td>-2.4493e-16</td><td>1.0</td><td>-0.587785</td><td>-0.809017</td></tr><tr><td>2025-12-18</td><td>-2.4493e-16</td><td>1.0</td><td>-0.951057</td><td>0.309017</td></tr><tr><td>2025-12-19</td><td>-2.4493e-16</td><td>1.0</td><td>-2.4493e-16</td><td>1.0</td></tr><tr><td>2025-12-22</td><td>-2.4493e-16</td><td>1.0</td><td>0.951057</td><td>0.309017</td></tr><tr><td>2025-12-23</td><td>-2.4493e-16</td><td>1.0</td><td>0.587785</td><td>-0.809017</td></tr><tr><td>2025-12-24</td><td>-2.4493e-16</td><td>1.0</td><td>-0.587785</td><td>-0.809017</td></tr><tr><td>2025-12-26</td><td>-2.4493e-16</td><td>1.0</td><td>-2.4493e-16</td><td>1.0</td></tr><tr><td>2025-12-29</td><td>-2.4493e-16</td><td>1.0</td><td>0.951057</td><td>0.309017</td></tr><tr><td>2025-12-30</td><td>-2.4493e-16</td><td>1.0</td><td>0.587785</td><td>-0.809017</td></tr><tr><td>2025-12-31</td><td>-2.4493e-16</td><td>1.0</td><td>-0.587785</td><td>-0.809017</td></tr></tbody></table></div>"
|
||
],
|
||
"text/plain": [
|
||
"shape: (10, 5)\n",
|
||
"┌────────────┬─────────────┬───────────┬─────────────┬───────────┐\n",
|
||
"│ timestamp ┆ month_sin ┆ month_cos ┆ dow_sin ┆ dow_cos │\n",
|
||
"│ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n",
|
||
"│ date ┆ f64 ┆ f64 ┆ f64 ┆ f64 │\n",
|
||
"╞════════════╪═════════════╪═══════════╪═════════════╪═══════════╡\n",
|
||
"│ 2025-12-17 ┆ -2.4493e-16 ┆ 1.0 ┆ -0.587785 ┆ -0.809017 │\n",
|
||
"│ 2025-12-18 ┆ -2.4493e-16 ┆ 1.0 ┆ -0.951057 ┆ 0.309017 │\n",
|
||
"│ 2025-12-19 ┆ -2.4493e-16 ┆ 1.0 ┆ -2.4493e-16 ┆ 1.0 │\n",
|
||
"│ 2025-12-22 ┆ -2.4493e-16 ┆ 1.0 ┆ 0.951057 ┆ 0.309017 │\n",
|
||
"│ 2025-12-23 ┆ -2.4493e-16 ┆ 1.0 ┆ 0.587785 ┆ -0.809017 │\n",
|
||
"│ 2025-12-24 ┆ -2.4493e-16 ┆ 1.0 ┆ -0.587785 ┆ -0.809017 │\n",
|
||
"│ 2025-12-26 ┆ -2.4493e-16 ┆ 1.0 ┆ -2.4493e-16 ┆ 1.0 │\n",
|
||
"│ 2025-12-29 ┆ -2.4493e-16 ┆ 1.0 ┆ 0.951057 ┆ 0.309017 │\n",
|
||
"│ 2025-12-30 ┆ -2.4493e-16 ┆ 1.0 ┆ 0.587785 ┆ -0.809017 │\n",
|
||
"│ 2025-12-31 ┆ -2.4493e-16 ┆ 1.0 ┆ -0.587785 ┆ -0.809017 │\n",
|
||
"└────────────┴─────────────┴───────────┴─────────────┴───────────┘"
|
||
]
|
||
},
|
||
"execution_count": 15,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"from ml4t.engineer.features.ml import cyclical_encode\n",
|
||
"\n",
|
||
"from data import load_etfs\n",
|
||
"\n",
|
||
"etfs = load_etfs()\n",
|
||
"spy = etfs.filter(pl.col(\"symbol\") == \"SPY\").sort(\"timestamp\")\n",
|
||
"calendar_start_dt = datetime.fromisoformat(CALENDAR_START_DATE)\n",
|
||
"spy = spy.filter(pl.col(\"timestamp\") >= calendar_start_dt)\n",
|
||
"\n",
|
||
"# Cyclical encoding for month\n",
|
||
"month_encoded = cyclical_encode(pl.col(\"timestamp\").dt.month(), period=12, name_prefix=\"month\")\n",
|
||
"cal_df = spy.with_columns(**month_encoded)\n",
|
||
"\n",
|
||
"# Day-of-week encoding (Monday=1, Friday=5)\n",
|
||
"dow_encoded = cyclical_encode(pl.col(\"timestamp\").dt.weekday(), period=5, name_prefix=\"dow\")\n",
|
||
"cal_df = cal_df.with_columns(**dow_encoded)\n",
|
||
"\n",
|
||
"print(\"Calendar encodings (last 10 rows):\")\n",
|
||
"cal_df.select([\"timestamp\", \"month_sin\", \"month_cos\", \"dow_sin\", \"dow_cos\"]).tail(10)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "7cb6fc52",
|
||
"metadata": {
|
||
"papermill": {
|
||
"duration": 0.001977,
|
||
"end_time": "2026-06-13T03:31:42.349815+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:42.347838+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"**Usage**: Calendar features are primarily **state variables** for conditioning.\n",
|
||
"For example, momentum signals may behave differently in January (tax-loss selling\n",
|
||
"reversal) versus other months. Time-to-event encodings (e.g., days to next\n",
|
||
"earnings, days to FOMC) follow the same pattern.\n",
|
||
"\n",
|
||
"**Note**: Volatility state features (vol ratio, percentile, decile) and\n",
|
||
"price-derived regime indicators (variance ratio, fractal efficiency) are\n",
|
||
"covered in `01_price_volume_features` since they derive from price data.\n",
|
||
"Signal × state interactions and feasibility overlays are in\n",
|
||
"`06_robustness_sensitivity`."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "d84d094d",
|
||
"metadata": {
|
||
"papermill": {
|
||
"duration": 0.001905,
|
||
"end_time": "2026-06-13T03:31:42.353673+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:42.351768+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 3.2 Time-to-Event Encoding\n",
|
||
"\n",
|
||
"Time-to-event measures proximity to a known future event (earnings, FOMC,\n",
|
||
"rebalance). The text specifies:\n",
|
||
"\n",
|
||
"$$d_{t,a} = \\min(T_{\\text{next}} - t, \\; H_{\\max})$$\n",
|
||
"\n",
|
||
"where $T_{\\text{next}}$ is the next event date and $H_{\\max}$ caps the\n",
|
||
"feature to avoid extreme values far from events."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 16,
|
||
"id": "626ad1d9",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:42.358358Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:42.358192Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:42.360793Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:42.360415Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.005411,
|
||
"end_time": "2026-06-13T03:31:42.361059+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:42.355648+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Synthetic earnings calendar for demonstration\n",
|
||
"# Real systems would load from SEC EDGAR filing dates\n",
|
||
"earnings_dates = []\n",
|
||
"for symbol in [\"AAPL\", \"MSFT\", \"GOOGL\"]:\n",
|
||
" # Quarterly earnings approximately 45 days after quarter end\n",
|
||
" for q_end in [\n",
|
||
" date(2023, 3, 31),\n",
|
||
" date(2023, 6, 30),\n",
|
||
" date(2023, 9, 30),\n",
|
||
" date(2023, 12, 31),\n",
|
||
" date(2024, 3, 31),\n",
|
||
" date(2024, 6, 30),\n",
|
||
" date(2024, 9, 30),\n",
|
||
" date(2024, 12, 31),\n",
|
||
" ]:\n",
|
||
" ann_date = q_end + timedelta(days=45)\n",
|
||
" earnings_dates.append({\"symbol\": symbol, \"earnings_date\": ann_date})\n",
|
||
"\n",
|
||
"earnings_cal = pl.DataFrame(earnings_dates).sort([\"symbol\", \"earnings_date\"])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 17,
|
||
"id": "f1f77e9f",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:42.365554Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:42.365479Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:42.370007Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:42.369627Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.007636,
|
||
"end_time": "2026-06-13T03:31:42.370748+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:42.363112+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Create daily dates and compute time-to-event features\n",
|
||
"daily = (\n",
|
||
" pl.DataFrame(\n",
|
||
" {\"timestamp\": pl.date_range(date(2023, 1, 1), date(2024, 12, 31), \"1d\", eager=True)}\n",
|
||
" )\n",
|
||
" .filter(pl.col(\"timestamp\").dt.weekday() < 5)\n",
|
||
" .join(pl.DataFrame({\"symbol\": [\"AAPL\", \"MSFT\", \"GOOGL\"]}), how=\"cross\")\n",
|
||
" .sort([\"symbol\", \"timestamp\"])\n",
|
||
")\n",
|
||
"\n",
|
||
"# Rolling forward join: for each date, find next earnings date\n",
|
||
"H_MAX = 63 # Cap at 63 trading days\n",
|
||
"\n",
|
||
"daily_with_events = daily.join_asof(\n",
|
||
" earnings_cal.sort([\"symbol\", \"earnings_date\"]),\n",
|
||
" left_on=\"timestamp\",\n",
|
||
" right_on=\"earnings_date\",\n",
|
||
" by=\"symbol\",\n",
|
||
" strategy=\"forward\",\n",
|
||
").with_columns(\n",
|
||
" [\n",
|
||
" (pl.col(\"earnings_date\") - pl.col(\"timestamp\"))\n",
|
||
" .dt.total_days()\n",
|
||
" .clip(0, H_MAX)\n",
|
||
" .alias(\"days_to_earnings\"),\n",
|
||
" ]\n",
|
||
")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 18,
|
||
"id": "b2874a06",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-06-13T03:31:42.375613Z",
|
||
"iopub.status.busy": "2026-06-13T03:31:42.375530Z",
|
||
"iopub.status.idle": "2026-06-13T03:31:42.378559Z",
|
||
"shell.execute_reply": "2026-06-13T03:31:42.378209Z"
|
||
},
|
||
"papermill": {
|
||
"duration": 0.005819,
|
||
"end_time": "2026-06-13T03:31:42.378801+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:42.372982+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Time-to-event features:\n",
|
||
"shape: (15, 4)\n",
|
||
"┌────────────┬────────┬──────────────────┬─────────────────┐\n",
|
||
"│ timestamp ┆ symbol ┆ days_to_earnings ┆ event_proximity │\n",
|
||
"│ --- ┆ --- ┆ --- ┆ --- │\n",
|
||
"│ date ┆ str ┆ i64 ┆ str │\n",
|
||
"╞════════════╪════════╪══════════════════╪═════════════════╡\n",
|
||
"│ 2023-04-25 ┆ AAPL ┆ 20 ┆ normal │\n",
|
||
"│ 2023-04-26 ┆ AAPL ┆ 19 ┆ normal │\n",
|
||
"│ 2023-04-27 ┆ AAPL ┆ 18 ┆ normal │\n",
|
||
"│ 2023-05-01 ┆ AAPL ┆ 14 ┆ normal │\n",
|
||
"│ 2023-05-02 ┆ AAPL ┆ 13 ┆ normal │\n",
|
||
"│ … ┆ … ┆ … ┆ … │\n",
|
||
"│ 2023-05-11 ┆ AAPL ┆ 4 ┆ pre_5d │\n",
|
||
"│ 2023-05-15 ┆ AAPL ┆ 0 ┆ pre_2d │\n",
|
||
"│ 2023-05-16 ┆ AAPL ┆ 63 ┆ far │\n",
|
||
"│ 2023-05-17 ┆ AAPL ┆ 63 ┆ far │\n",
|
||
"│ 2023-05-18 ┆ AAPL ┆ 63 ┆ far │\n",
|
||
"└────────────┴────────┴──────────────────┴─────────────────┘\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Bin into pre/post windows\n",
|
||
"daily_with_events = daily_with_events.with_columns(\n",
|
||
" pl.when(pl.col(\"days_to_earnings\") <= 2)\n",
|
||
" .then(pl.lit(\"pre_2d\"))\n",
|
||
" .when(pl.col(\"days_to_earnings\") <= 5)\n",
|
||
" .then(pl.lit(\"pre_5d\"))\n",
|
||
" .when(pl.col(\"days_to_earnings\") > H_MAX - 1)\n",
|
||
" .then(pl.lit(\"far\"))\n",
|
||
" .otherwise(pl.lit(\"normal\"))\n",
|
||
" .alias(\"event_proximity\")\n",
|
||
")\n",
|
||
"\n",
|
||
"print(\"Time-to-event features:\")\n",
|
||
"# Show a sample around an earnings date\n",
|
||
"sample_symbol = \"AAPL\"\n",
|
||
"print(\n",
|
||
" daily_with_events.filter(\n",
|
||
" (pl.col(\"symbol\") == sample_symbol)\n",
|
||
" & (pl.col(\"timestamp\").is_between(date(2023, 4, 25), date(2023, 5, 20)))\n",
|
||
" ).select([\"timestamp\", \"symbol\", \"days_to_earnings\", \"event_proximity\"])\n",
|
||
")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "db918673",
|
||
"metadata": {
|
||
"papermill": {
|
||
"duration": 0.002057,
|
||
"end_time": "2026-06-13T03:31:42.383029+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:42.380972+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"**Interpretation**: Time-to-event serves as a **state variable** — a label\n",
|
||
"that partitions trading days into discrete proximity windows\n",
|
||
"(pre-2d, pre-5d, normal, far). These windows feed downstream signal × state\n",
|
||
"interactions (see `06_robustness_sensitivity` for the IC-conditioning\n",
|
||
"pattern); this notebook covers only the encoding step."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "0dcd8c4d",
|
||
"metadata": {
|
||
"papermill": {
|
||
"duration": 0.001951,
|
||
"end_time": "2026-06-13T03:31:42.386975+00:00",
|
||
"exception": false,
|
||
"start_time": "2026-06-13T03:31:42.385024+00:00",
|
||
"status": "completed"
|
||
}
|
||
},
|
||
"source": [
|
||
"## Summary\n",
|
||
"\n",
|
||
"### Fundamentals\n",
|
||
"- **Value**: Book-to-market, earnings yield, CF yield\n",
|
||
"- **Quality**: ROE, ROA, accruals ratio\n",
|
||
"- **Alignment**: ASOF join with both frames sorted by `[symbol, date]`\n",
|
||
"- **Scaffolding**: Market cap approximation is for teaching only\n",
|
||
"\n",
|
||
"### Macro\n",
|
||
"- **Publication lag**: Add 30-day lag for monthly data\n",
|
||
"- **Forward-fill**: Use filled version for YoY/3m changes\n",
|
||
"- **Naming**: Rolling median $\\neq$ percentile rank (be precise)\n",
|
||
"- **Risk regimes**: VIX thresholds, credit regime from spread levels\n",
|
||
"\n",
|
||
"### Calendar\n",
|
||
"- **Cyclical encoding**: sin/cos for month, day-of-week, time-to-event\n",
|
||
"- **Phase, not outcome**: Encode timing, not post-event realized moves\n",
|
||
"\n",
|
||
"### Key Patterns\n",
|
||
"\n",
|
||
"| Feature Type | Update Freq | Alignment | Use Case |\n",
|
||
"|--------------|-------------|-----------|----------|\n",
|
||
"| Fundamentals | Quarterly | ASOF by announcement | Factor signals |\n",
|
||
"| Macro | Daily/Monthly | Forward-fill + lag | Context, regime |\n",
|
||
"| Calendar | Deterministic | Direct encoding | Seasonality |\n",
|
||
"\n",
|
||
"### Next Notebooks\n",
|
||
"\n",
|
||
"- `05_feature_selection` — Feature selection and deduplication (§8.6)\n",
|
||
"- `06_robustness_sensitivity` — Regime conditioning, interactions (§8.6)"
|
||
]
|
||
}
|
||
],
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": "Python 3 (ipykernel)",
|
||
"language": "python",
|
||
"name": "python3"
|
||
},
|
||
"language_info": {
|
||
"codemirror_mode": {
|
||
"name": "ipython",
|
||
"version": 3
|
||
},
|
||
"file_extension": ".py",
|
||
"mimetype": "text/x-python",
|
||
"name": "python",
|
||
"nbconvert_exporter": "python",
|
||
"pygments_lexer": "ipython3",
|
||
"version": "3.14.3"
|
||
},
|
||
"papermill": {
|
||
"default_parameters": {},
|
||
"duration": 3.848967,
|
||
"end_time": "2026-06-13T03:31:43.304212+00:00",
|
||
"environment_variables": {},
|
||
"exception": null,
|
||
"input_path": "08_financial_features/04_fundamentals_macro_calendar.ipynb",
|
||
"output_path": "08_financial_features/04_fundamentals_macro_calendar.ipynb",
|
||
"parameters": {},
|
||
"start_time": "2026-06-13T03:31:39.455245+00:00",
|
||
"version": "2.7.0"
|
||
},
|
||
"jupytext": {
|
||
"cell_metadata_filter": "tags,-all"
|
||
},
|
||
"ml4t_provenance": {
|
||
"source_py_blob": "15e869d0331d4f93f140269fca75dc08157100ae",
|
||
"executed_at": "2026-06-13T03:31:43.561253+00:00",
|
||
"executor": "cpu-local",
|
||
"production": true,
|
||
"parameters": {},
|
||
"notes": "executed-state finalization batch (2026-06-13 run)"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 5
|
||
}
|