Short Rate Models (Part 14: Macro-Finance II)

2026-03-17

1 Introduction

The macro-finance branch combines two ideas developed earlier in the series. The first is the Gaussian state-space term-structure scaffold. The second is the policy-rule and macro-observation logic. The reduced implementation uses alphaforge to build one monthly yield panel and one monthly macro block, then fits a joint macro-yield model in the package.

The resulting model is not the full Rudebusch-Wu system. It is a reduced state-space model with three observed macro states and one latent term-premium factor extracted from the yield panel.

Code
import os
import sys
from pathlib import Path

import numpy as np
import pandas as pd


def locate_workspace() -> Path:
    cwd = Path.cwd().resolve()
    for candidate in [cwd, *cwd.parents]:
        if (candidate / 'alphaforge').exists() and (candidate / 'short-rate-models').exists():
            return candidate
    raise RuntimeError('Could not locate the steveya workspace from the current working directory.')


WORKSPACE = locate_workspace()
sys.path.insert(0, str(WORKSPACE / 'alphaforge'))
sys.path.insert(0, str(WORKSPACE / 'short-rate-models'))

from alphaforge import (
    DataContext,
    DuckDBParquetStore,
    FREDDataSource,
    TradingCalendar,
    build_macro_finance_dataset,
)
from short_rate_models import MacroFinanceTermStructureModel
Code
fred_api_key = os.environ.get('FRED_API_KEY')
if not fred_api_key:
    raise RuntimeError('Set FRED_API_KEY before running this notebook.')

ctx = DataContext(
    sources={'fred': FREDDataSource(api_key=fred_api_key)},
    calendars={'XNYS': TradingCalendar('XNYS', tz='UTC')},
    store=DuckDBParquetStore(root=WORKSPACE / '.alphaforge_store' / 'short_rate_models'),
)

dataset = build_macro_finance_dataset(
    ctx,
    start=pd.Timestamp('1990-01-01', tz='UTC'),
    end=pd.Timestamp('2024-12-31', tz='UTC'),
)

dataset.yields.tail(), dataset.macro.tail()

2 Reduced Fit

The fit stacks the observed macro block with a latent term-premium factor extracted from the yield panel. The transition law is then estimated as a VAR(1), yields are regressed on the combined state vector, and the Kalman filter is run on the joint macro-yield system.

Code
model, fit = MacroFinanceTermStructureModel.fit(
    yields=dataset.yields,
    macro=dataset.macro,
)

fit['smoothed_states'].tail()
Code
channels = pd.DataFrame(model.policy_channel_decomposition())
channels
Code
latent_factor = fit['latent_term_premium_factor']
summary = pd.concat([dataset.macro, latent_factor], axis=1).dropna()
summary.tail()

3 Findings

The reduced model is useful when it does three things at once. It should let the macro block explain the broad policy and business-cycle component of the yield curve, leave room for an additional latent term-premium factor, and produce a channel decomposition that can be interpreted economically rather than as a purely statistical rotation.

That is the standard to apply here. The notebook is not trying to win a forecasting competition. It is trying to show how a joint macro-yield state-space model can be built from the same data and estimation infrastructure used in the earlier branches.

4 Limitations

The reduced implementation omits several structural details from the full macro-finance literature. The macro block is small, the latent factor count is low, and the state transition is estimated with a restricted public-data calibration rather than the full structural system. Those are conscious simplifications. The value of the notebook is that it makes the macro-finance architecture executable inside the same series workflow, not that it claims a journal-grade replication.