Issue 1345 — RecessionAlert Recession Fear Indicator

Offline, public-safe exploratory research. The checked-in RecessionAlert snapshot describes global Google search volume for recession as a fear gauge, but does not provide source-linked observations, exact query settings, a native span, publication clock, threshold, or buy/sell rule. This notebook never requests the live site, raw media, credentials, or private account data.

In [1]:
from pathlib import Path
import hashlib
import json
import sqlite3

root = Path.cwd()
while root != root.parent and not (root / 'data' / 'recessionalert.sqlite').exists():
    root = root.parent
page_url = 'https://recessionalert.com/a-recession-fear-indicator/'
page_sha = 'sha256:2cb8464ee21e8b28af0f8dcbf7bacbd3ddeffc040c53717a198c0b701fa019dd'
manifest = json.loads((root / 'research/artifacts/recessionalert/manifest.json').read_text())
manifest_page = next(item for item in manifest if item.get('redacted_url') == page_url)
with sqlite3.connect(f'file:{root / "data/recessionalert.sqlite"}?mode=ro', uri=True) as connection:
    row = connection.execute('SELECT fetched_at, status, sha256, content_length, body FROM pages WHERE url=?', (page_url,)).fetchone()
    history = connection.execute('SELECT fetched_at, status, sha256 FROM crawl_log WHERE url=? ORDER BY id', (page_url,)).fetchall()
fetched_at, status, digest, content_length, body = row
assert manifest_page['sha256'] == page_sha == digest == 'sha256:' + hashlib.sha256(body).hexdigest()
assert len(body) == content_length == 83313
assert status == 200 and fetched_at == '2026-08-22T23:39:13Z'
print({'source_sha256': digest, 'bytes': content_length, 'fetched_at': fetched_at, 'same_url_crawl_rows': len(history), 'database_mode': 'ro'})
{'source_sha256': 'sha256:2cb8464ee21e8b28af0f8dcbf7bacbd3ddeffc040c53717a198c0b701fa019dd', 'bytes': 83313, 'fetched_at': '2026-08-22T23:39:13Z', 'same_url_crawl_rows': 5, 'database_mode': 'ro'}

Disclosed rule and gaps

The article says that global Google search volume for recession is a promising recession indicator and fear gauge. It reports a peak within roughly two weeks of the 2008 NBER recession start, mentions an August 2011 spike near a forming S&P 500 bottom, and describes a later SuperIndex fall. These are contextual claims, not an executable market rule. The page gives no query geography/category/normalization, machine-readable observations, first/last dates, recurring release clock, threshold, smoothing, state semantics, target, sizing, rebalance, cost, exit, or re-entry rule. The article-to-archive delay and two-week NBER comparison are not publication lags.

In [2]:
import sys
sys.path.insert(0, str(root))
from research.research_campaign.recessionalert_1345 import run_campaign

campaign = run_campaign(db_path=root / 'data/market.sqlite')
print({'task_id': campaign['task_id'], 'source_fidelity': campaign['source_fidelity'], 'tested_variants': len(campaign['variants']), 'macro_window': campaign['window']['macro_signal']})
for variant in campaign['variants']:
    ief = variant['metrics_spy_ief']
    bil = variant['metrics_spy_bil']
    print({
        'variant': variant['id'],
        'lookback': variant['lookback'],
        'threshold': variant['threshold'],
        'lag_bars': variant['lag_bars'],
        'risk_off_fraction': round(variant['risk_off_fraction'], 4),
        'ief_total_vs_spy': (round(ief['total_return'], 6), round(ief['benchmark_total_return'], 6)),
        'bil_total_vs_spy': (round(bil['total_return'], 6), round(bil['benchmark_total_return'], 6)),
    })
{'task_id': '1345', 'source_fidelity': 'not_native_recessionalert_google_trends', 'tested_variants': 6, 'macro_window': 'UNRATE 1948-01/2026-06 after monthly alignment and family warm-up'}
{'variant': 'recessionalert.1345.unrate_regime.lb3.lag0.t0p0', 'lookback': 3, 'threshold': 0.0, 'lag_bars': 0, 'risk_off_fraction': 0.3337, 'ief_total_vs_spy': (9.192164, 11.397426), 'bil_total_vs_spy': (5.356712, 5.775363)}
{'variant': 'recessionalert.1345.unrate_regime.lb6.lag1.t0p0', 'lookback': 6, 'threshold': 0.0, 'lag_bars': 1, 'risk_off_fraction': 0.3497, 'ief_total_vs_spy': (6.345757, 11.397426), 'bil_total_vs_spy': (3.25542, 5.775363)}
{'variant': 'recessionalert.1345.unrate_regime.lb12.lag2.t0p5', 'lookback': 12, 'threshold': 0.5, 'lag_bars': 2, 'risk_off_fraction': 0.3477, 'ief_total_vs_spy': (6.747123, 11.397426), 'bil_total_vs_spy': (3.041096, 5.775363)}
{'variant': 'recessionalert.1345.unrate_regime.lb6.lag1.t0p5', 'lookback': 6, 'threshold': 0.5, 'lag_bars': 1, 'risk_off_fraction': 0.3497, 'ief_total_vs_spy': (6.345757, 11.397426), 'bil_total_vs_spy': (3.25542, 5.775363)}
{'variant': 'recessionalert.1345.unrate_regime.lb12.lag1.t1p0', 'lookback': 12, 'threshold': 1.0, 'lag_bars': 1, 'risk_off_fraction': 0.3488, 'ief_total_vs_spy': (6.825907, 11.397426), 'bil_total_vs_spy': (3.037258, 5.775363)}
{'variant': 'recessionalert.1345.unrate_regime.lb21.lag1.tm0p5', 'lookback': 21, 'threshold': -0.5, 'lag_bars': 1, 'risk_off_fraction': 0.388, 'ief_total_vs_spy': (3.921532, 11.397426), 'bil_total_vs_spy': (1.747567, 5.775363)}

Best-effort diagnostics

The inferred proxy uses FRED UNRATE because it is the longest recession-relevant public series in the tracked cache. SPY is the risk-on benchmark and IEF/BIL are descriptive defensive sleeves; these are not a RecessionAlert allocation. Lag 1 is the causal baseline, lag 0 is research-only, and lag 2 is a conservative sensitivity.

In [3]:
from research.research_campaign.recessionalert_1345 import run_diagnostics

diagnostics = run_diagnostics(db_path=root / 'data/market.sqlite')
print('Timing sensitivity:')
for row in diagnostics['timing_sensitivity']:
    print({k: row[k] for k in ('variant', 'lag_bars', 'risk_off_fraction', 'risk_off_events', 'same_close_status')}, 'IEF_total=', round(row['metrics_spy_ief']['total_return'], 6), 'BIL_total=', round(row['metrics_spy_bil']['total_return'], 6))
print('Context windows:')
for row in diagnostics['regime_results']:
    print({k: row[k] for k in ('name', 'macro_mean_unrate', 'macro_max_unrate', 'risk_off_fraction')})
print('Failure modes:', [row['id'] for row in diagnostics['failure_modes']])
Timing sensitivity:
{'variant': 'lb6_t0_lag0', 'lag_bars': 0, 'risk_off_fraction': 0.3497326203208556, 'risk_off_events': 57, 'same_close_status': 'research_only_sensitivity'} IEF_total= 8.487782 BIL_total= 3.738237
{'variant': 'lb6_t0_lag1', 'lag_bars': 1, 'risk_off_fraction': 0.3497326203208556, 'risk_off_events': 58, 'same_close_status': 'not_used'} IEF_total= 6.345757 BIL_total= 3.25542
{'variant': 'lb6_t0_lag2', 'lag_bars': 2, 'risk_off_fraction': 0.3497326203208556, 'risk_off_events': 58, 'same_close_status': 'not_used'} IEF_total= 9.766429 BIL_total= 3.732513
Context windows:
{'name': 'gfc_context', 'macro_mean_unrate': 6.7, 'macro_max_unrate': 9.5, 'risk_off_fraction': 1.0}
{'name': 'article_august_2011_context', 'macro_mean_unrate': 9.0, 'macro_max_unrate': 9.0, 'risk_off_fraction': 0.0}
{'name': 'article_update_context', 'macro_mean_unrate': 7.8, 'macro_max_unrate': 7.8, 'risk_off_fraction': 0.0}
{'name': 'covid_context', 'macro_mean_unrate': 7.566666666666667, 'macro_max_unrate': 14.8, 'risk_off_fraction': 0.3333333333333333}
Failure modes: ['native_series_missing', 'publication_vintage_missing', 'market_mapping_undisclosed', 'proxy_underperformance', 'lookahead_and_warmup']

Conclusion

Six inferred variants were executed on the longest available offline proxy history. They all underperformed their aligned SPY total-return benchmark in the IEF and BIL descriptive overlays. This is an exploratory negative result, not a validation of the source claim. The native Google Trends series, exact publication lag, and disclosed buy/sell rule remain unavailable. Publish the derived charts and trial ledger as Other / Research evidence only; do not register an official indicator or present an inferred setting as disclosed.