Files
stefan-jansen--machine-lear…/08_financial_features/04_fundamentals_macro_calendar.ipynb
T
2026-07-13 13:26:28 +08:00

1855 lines
77 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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>&quot;AAPL&quot;</td><td>320193</td><td>&quot;Apple Inc.&quot;</td><td>2022-03-26</td><td>2022-04-29</td><td>&quot;0000320193-22-000059&quot;</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>&quot;AAPL&quot;</td><td>320193</td><td>&quot;Apple Inc.&quot;</td><td>2022-06-25</td><td>2022-07-29</td><td>&quot;0000320193-22-000070&quot;</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>&quot;AAPL&quot;</td><td>320193</td><td>&quot;Apple Inc.&quot;</td><td>2022-09-24</td><td>2023-11-03</td><td>&quot;0000320193-23-000106&quot;</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>&quot;AAPL&quot;</td><td>320193</td><td>&quot;Apple Inc.&quot;</td><td>2022-12-31</td><td>2023-02-03</td><td>&quot;0000320193-23-000006&quot;</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>&quot;AAPL&quot;</td><td>320193</td><td>&quot;Apple Inc.&quot;</td><td>2023-04-01</td><td>2023-05-05</td><td>&quot;0000320193-23-000064&quot;</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>&quot;XOM&quot;</td><td>2022-09-30</td><td>0.5</td><td>0.052821</td></tr><tr><td>&quot;XOM&quot;</td><td>2022-12-31</td><td>0.5</td><td>null</td></tr><tr><td>&quot;XOM&quot;</td><td>2023-03-31</td><td>0.5</td><td>0.028764</td></tr><tr><td>&quot;XOM&quot;</td><td>2023-06-30</td><td>0.5</td><td>0.019794</td></tr><tr><td>&quot;XOM&quot;</td><td>2023-09-30</td><td>0.5</td><td>0.022709</td></tr><tr><td>&quot;XOM&quot;</td><td>2023-12-31</td><td>0.5</td><td>null</td></tr><tr><td>&quot;XOM&quot;</td><td>2024-03-31</td><td>0.5</td><td>0.020024</td></tr><tr><td>&quot;XOM&quot;</td><td>2024-06-30</td><td>0.5</td><td>0.017213</td></tr><tr><td>&quot;XOM&quot;</td><td>2024-09-30</td><td>0.5</td><td>0.016028</td></tr><tr><td>&quot;XOM&quot;</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>&quot;XOM&quot;</td><td>2022-09-30</td><td>0.105642</td><td>0.053113</td><td>0.0</td></tr><tr><td>&quot;XOM&quot;</td><td>2022-12-31</td><td>null</td><td>null</td><td>0.0</td></tr><tr><td>&quot;XOM&quot;</td><td>2023-03-31</td><td>0.057528</td><td>0.030944</td><td>-0.013296</td></tr><tr><td>&quot;XOM&quot;</td><td>2023-06-30</td><td>0.039589</td><td>0.021693</td><td>0.0</td></tr><tr><td>&quot;XOM&quot;</td><td>2023-09-30</td><td>0.045417</td><td>0.024365</td><td>0.0</td></tr><tr><td>&quot;XOM&quot;</td><td>2023-12-31</td><td>null</td><td>null</td><td>0.0</td></tr><tr><td>&quot;XOM&quot;</td><td>2024-03-31</td><td>0.040049</td><td>0.021751</td><td>-0.017051</td></tr><tr><td>&quot;XOM&quot;</td><td>2024-06-30</td><td>0.034426</td><td>0.020056</td><td>0.0</td></tr><tr><td>&quot;XOM&quot;</td><td>2024-09-30</td><td>0.032056</td><td>0.01864</td><td>0.0</td></tr><tr><td>&quot;XOM&quot;</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>&quot;XOM&quot;</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>&quot;XOM&quot;</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>&quot;XOM&quot;</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>&quot;XOM&quot;</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>&quot;XOM&quot;</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>&quot;XOM&quot;</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>&quot;XOM&quot;</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>&quot;XOM&quot;</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>&quot;XOM&quot;</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>&quot;XOM&quot;</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
}