RecessionAlert SP-500 seasonal trends — exploratory proxy study¶
This notebook is an offline, inferred/proxy experiment. The checked-in RecessionAlert page (SHA256 sha256:7e34ccc16114249e7652c0bf1080f6df0f14b49e48d1009f05f9b8a33fb369cd) discloses four seasonal cycle families, ranked monthly characteristics, and short/long/2x text, but no source-linked signal history, scale conversion, publication clock, or complete portfolio rule.
We use only checked-in Yahoo snapshots from data/market.sqlite: ^GSPC as the longest direct SP500 price proxy (1927-12-30 onward) and actual SPY for the requested overlay (1993-01-29 onward). Scores use prior observations only; lag 1 is causal, while lag 0 is a research-only sensitivity.
import math
import sys
from pathlib import Path
import pandas as pd
import matplotlib.pyplot as plt
for candidate in [Path.cwd(), *Path.cwd().resolve().parents]:
if (candidate / 'research').is_dir():
sys.path.insert(0, str(candidate))
break
from research.research_campaign.recessionalert_sp500_seasonal import (
SOURCE_SHA256, SOURCE_URL, evaluate_seasonal_spec, load_proxy_panel, proxy_specs
)
panel = load_proxy_panel()
driver = panel['GSPC']
spy = panel['SPY'].dropna().astype(float).sort_index()
print({'source_sha256': SOURCE_SHA256, 'source_url': SOURCE_URL, 'driver_span': [str(driver.index.min().date()), str(driver.index.max().date())], 'overlay_span': [str(spy.index.min().date()), str(spy.index.max().date())], 'driver_rows': len(driver), 'overlay_rows': len(spy)})
Four labeled variants¶
The variants below are not official RecessionAlert settings. They make the missing choices explicit: expanding month-of-year best guess, documented-inspired 12/24/36/48 cycle composite, nearby threshold/lag/unlevered sensitivity, and a short-cycle long-short sensitivity.
rows = []
evaluated = []
benchmark = spy.pct_change().fillna(0.0)
for spec in proxy_specs():
out = evaluate_seasonal_spec(spec, driver, spy)
evaluated.append((spec, out))
returns = out['strategy_returns'].reindex(spy.index).fillna(0.0)
equity = (1.0 + returns).cumprod()
years = (returns.index[-1] - returns.index[0]).days / 365.25
rows.append({
'variant': spec['id'],
'family': spec['semantics']['family'],
'lag_bars': spec['semantics']['lag_bars'],
'overlay': spec['parameters']['overlay']['default'],
'scores': int(out['monthly_score'].notna().sum()),
'end_equity': float(equity.iloc[-1]),
'cagr': float(equity.iloc[-1] ** (1.0 / years) - 1.0),
'max_drawdown': float((equity / equity.cummax() - 1.0).min()),
})
summary = pd.DataFrame(rows)
summary
fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
for spec, out in evaluated:
label = spec['semantics']['family'] + ' lag' + str(spec['semantics']['lag_bars'])
axes[0].plot(out['monthly_score'].index, out['monthly_score'], label=label, linewidth=1.0)
axes[1].plot(out['strategy_equity'].index, out['strategy_equity'], label=label, linewidth=1.0)
axes[0].axhline(37, color='black', linestyle='--', linewidth=0.7, label='source-like 37')
axes[0].axhline(74, color='black', linestyle=':', linewidth=0.7, label='source-like 74')
axes[0].set_ylabel('Inferred score (0–100)')
axes[0].set_title('Inferred/proxy seasonal score; not native RecessionAlert data')
axes[0].legend(loc='upper left', fontsize=8, ncol=2)
axes[1].plot(spy.index, (1.0 + benchmark).cumprod(), color='black', linewidth=1.5, label='SPY benchmark')
axes[1].set_ylabel('Growth of $1')
axes[1].set_title('Causal daily SPY overlays; lag 1 is primary')
axes[1].legend(loc='upper left', fontsize=8)
fig.tight_layout()
plt.show()
Interpretation¶
All four variants are retained even where they underperform. The full-window proxy results trail SPY CAGR, vary by regime, and depend on inferred scale and execution choices. The source-fidelity disposition remains separate: no native indicator registry admission is justified. Raw cached HTML, media, workbook bytes, credentials, and private account data are not included.