A Rules-Based Systematic Macro Strategy for Managing Inflation Risk

Executive Summary Inflation Regime Model Backtesting Strategy Performance Bottom Line Code Implementation & Appendix Reads: 169

Executive Summary

  • US long-duration Treasury yields remain elevated, reflecting market confidence in a sustained growth narrative. Meanwhile, inflation appears broadly contained, with headline CPI at 2.4% and core at 2.8%. Despite this moderation, the Fed held rates steady in the June FOMC meeting, maintaining a cautious stance.
  • Against this macro backdrop, we developed a rules-based asset allocation framework that classifies inflation regimes using CPI trends and rotates exposure across gold (GLD), Treasuries (TLT, IEF), and short-term cash equivalents (SHY). At Cordoba, we propose a systematic strategy that outperforms both passive and equal-weight portfolios by leveraging simple, transparent macro signals.
  • The backtest matches with our hypothesis: gold outperforms during inflationary acceleration, while Treasuries regain favour as inflation cools. This model empowers investors to actively manage rate sensitivity and duration risk throughout the economic cycle.

Inflation Regime Model

  • Our strategy classifies macroeconomic conditions into three distinct regimes based on monthly CPI trends. An Inflation Up regime is triggered when there are two consecutive months of month-over-month CPI increases above the 3-month moving average. Conversely, an Inflation Down regime occurs when CPI prints fall below the moving average for two consecutive months. All other conditions are categorised as Normal.
  • Within this framework, the model rotates between a focused set of ETFs: GLD (gold), IEF and TLT (intermediate and long-duration Treasuries), and SHY (short-duration Treasury, acting as a cash proxy). UUP (US Dollar Index) is also included for evaluating dollar strength.
  • Asset allocation is determined by combining the inflation regime with a 1-month momentum filter. During an Inflation Up regime with positive gold momentum, the model allocates to GLD. In an Inflation Down regime with upward-trending Treasuries, it shifts exposure to IEF. If no strong trend is detected or the regime is neutral, the model moves into SHY to preserve capital. Portfolio weights are normalised monthly and dynamically adjusted based on the prevailing macro signal.

Backtesting

  • As highlighted by the blue rectangle in the Correlation Matrix, CPI exhibits a strong positive correlation with GLD (0.88) and UUP (0.81), suggesting these assets tend to outperform during inflationary regimes. In contrast, Treasuries like IEF, TLT, and SHY show negative correlations with CPI, making them favourable in disinflationary environments.

Strategy Performance

  • To evaluate the effectiveness of our inflation regime model, we compare its return profile against two benchmarks:
  • Equal-Weighted Portfolio: A naive allocation across GLD, IEF, and SHY with fixed monthly rebalancing. This serves as a baseline to test whether macro signals improve upon passive diversification.
  • AGG (iShares Core U.S. Aggregate Bond ETF): A widely used benchmark for core bond exposure, including Treasuries, mortgage-backed securities, and investment-grade corporates. AGG reflects the performance of a broad, passive fixed income portfolio, commonly held by institutional and retail investors as a “safe” long-term anchor.
  • Our Regime-Based Strategy demonstrates clear risk-adjusted outperformance over the past 10 years. With a Sharpe Ratio of 0.47, it delivers the highest return per unit of volatility among all benchmarks considered. Importantly, it also maintains the lowest maximum drawdown at -16.78%, offering improved downside protection. Its Compound Annual Return (CAR) of 3.01% exceeds both the equal-weighted portfolio (1.83%) and AGG (-0.82%). While the 3.01% CAR may seem modest relative to equities or long bonds, it reflects a highly liquid, ETF-based strategy with superior flexibility. Unlike static portfolios, it enables real-time rebalancing and intraday execution, offering risk-adjusted efficiency and capital preservation in volatile macro regimes.
  • When inflation momentum is paired with a trend-following overlay, it is feasible to guide dynamic allocations that can improve the outperformance of regular passive ETFs.

Bottom Line

  • As US inflation softens and labour markets show cracks, duration exposure offers attractive asymmetry. While timing the Fed pivot is difficult, using macro regime classification allows investors to act proactively in managing risk and positioning.
  • At Cordoba, we continue to advocate for a gradual rotation away from US-centric exposures toward select emerging markets, where disinflation and easing cycles are already in motion. In this context, our rules-based US macro strategy focuses more on structural risk mitigation that enhances resilience in a late-cycle environment.

Code Implementation & Appendix

Python
from fredapi import Fred
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from ib_insync import *
import ssl, time
import seaborn as sns

# --- Setup ---
ssl._create_default_https_context = ssl._create_unverified_context
fred = Fred(api_key="")  # Add FRED API key

# --- CPI Data ---
df_cpi = fred.get_series('CPIAUCSL').pct_change().to_frame('headline')
df_cpi['inf_ma_3m'] = df_cpi['headline'].rolling(3).mean()
df_cpi['inf_regime'] = np.where(df_cpi['headline'] > df_cpi['inf_ma_3m'], 'upward',
                                np.where(df_cpi['headline'] < df_cpi['inf_ma_3m'], 'downward', 'normal'))

# --- ETF Data from IBKR ---
ib = IB()
ib.connect('127.0.0.1', 7497, clientId=1)
tickers = {'GLD': 'Gold', 'IEF': '7-10Y Treasury', 'TLT': '20+Y Treasury', 'SHY': '1-3Y Treasury', 'UUP': 'USD Index'}
etf_data = pd.DataFrame()

for symbol in tickers:
    bars = ib.reqHistoricalData(Stock(symbol, 'ARCA', 'USD'), '', '10 Y', '1 month', 'TRADES', useRTH=True, formatDate=1)
    etf_data[symbol] = util.df(bars).set_index('date')['close']
    time.sleep(2)
ib.disconnect()

# --- Merge Data ---
df_cpi.index = pd.to_datetime(df_cpi.index)
etf_data.index = pd.to_datetime(etf_data.index)
merged = pd.merge_asof(df_cpi.reset_index().rename(columns={'index': 'date'}),
                       etf_data.reset_index(), on='date', direction='backward').set_index('date')

# --- Correlation ---
correlations = merged[['headline'] + list(tickers)].corr()
corrs = correlations['headline'][1:]

# --- Regime-Based Weights ---
weights = {
    'upward': {k: max(0, v) for k, v in corrs.items()},
    'downward': {k: -min(0, v) for k, v in corrs.items()},
    'normal': {k: 1 for k in corrs.index}
}
normalize = lambda w: {k: v / sum(w.values()) if sum(w.values()) else 0 for k, v in w.items()}

monthly_weights = pd.DataFrame(index=merged.index, columns=tickers)
for date, row in merged.iterrows():
    monthly_weights.loc[date] = normalize(weights.get(row['inf_regime'], {k: 0 for k in tickers}))
monthly_weights = monthly_weights.astype(float)

# --- Returns and NAVs ---
etf_returns = merged[tickers].pct_change().fillna(0)
strategy_nav = (1 + (etf_returns * monthly_weights).sum(axis=1)).cumprod().rename('Regime-Based NAV')
equal_nav = (1 + etf_returns.mean(axis=1)).cumprod().rename('Equal Weight NAV')

# --- AGG Benchmark NAV ---
ib = IB()
ib.connect('127.0.0.1', 7497, clientId=6)
bars = ib.reqHistoricalData(Stock('AGG', 'ARCA', 'USD'), '', '10 Y', '1 month', 'TRADES', useRTH=True, formatDate=1)
ib.disconnect()
agg_nav = util.df(bars).set_index('date')['close']
agg_nav = (agg_nav / agg_nav.iloc[0]).rename('AGG Benchmark NAV').reindex_like(strategy_nav, method='pad')

# --- Plot NAVs ---
plt.figure(figsize=(12, 6))
plt.style.use("Solarize_Light2")
strategy_nav.plot(label=strategy_nav.name)
equal_nav.plot(label=equal_nav.name, linestyle='--')
agg_nav.plot(label=agg_nav.name, linestyle=':')
plt.title('Backtest: Regime-Based vs Equal Weight vs AGG NAV')
plt.xlabel('Date'); plt.ylabel('NAV'); plt.legend(); plt.grid(); plt.tight_layout(); plt.show()

# --- Performance Statistics ---
def stats(ret):
    car = (1 + ret.mean()) ** 12 - 1
    vol = ret.std() * np.sqrt(12)
    sharpe = car / vol if vol else np.nan
    max_dd = (ret.cumsum() - ret.cumsum().cummax()).min()
    calmar = car / abs(max_dd) if max_dd else np.nan
    return [car, vol, sharpe, max_dd, calmar]

returns = {
    'Regime-Based': strategy_nav.pct_change().dropna(),
    'Equal-Weighted': equal_nav.pct_change().dropna(),
    'AGG': agg_nav.pct_change().dropna()
}

summary = pd.DataFrame({k: stats(v) for k, v in returns.items()},
                       index=['CAR', 'Volatility', 'Sharpe', 'Max DD', 'Calmar']).T
print(summary)

# --- Correlation Heatmap ---
mask = np.triu(np.ones_like(correlations, dtype=bool), k=1)
plt.figure(figsize=(8, 6))
ax = sns.heatmap(correlations, annot=True, cmap='RdYlGn', mask=mask)
rect = patches.Rectangle((0, 1), 1, 5, linewidth=2, edgecolor='blue', facecolor='none')
ax.add_patch(rect)
plt.title('Correlation Heatmap: CPI vs ETF Prices')
plt.tight_layout()
plt.show()

Continue reading our research

To continue reading the full note and explore the complete body of our work, visit the Research Library.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top