Issue 1362 — RecessionAlert Bull Market Safari

This analysis uses only the checked-in RecessionAlert snapshot. It verifies the selected page digest and records what the article actually discloses: eight bullish-context observations, a 2:1 bearish counterweight, and no complete playable signal. No live request, credential, private account data, or crawl-database mutation is used.

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
spec = json.loads((root / 'research/findings/specs/recessionalert-bull-market-safari.json').read_text(encoding='utf-8'))
page = spec['source']['page']
db = root / 'data' / 'recessionalert.sqlite'
connection = sqlite3.connect(f'file:{db}?mode=ro', uri=True)
connection.execute('PRAGMA query_only=ON')
row = connection.execute('SELECT sha256, content_length, fetched_at, body FROM pages WHERE url=? ORDER BY fetched_at DESC LIMIT 1', (page['url'],)).fetchone()
target_asset_rows = connection.execute('SELECT COUNT(*) FROM assets WHERE page_url=? OR url=?', (page['url'], page['url'])).fetchone()[0]
crawl_count = connection.execute('SELECT COUNT(*) FROM crawl_log WHERE url=?', (page['url'],)).fetchone()[0]
connection.close()
assert row is not None
page_digest = 'sha256:' + hashlib.sha256(row[3]).hexdigest()
assert page_digest == row[0] == page['sha256']
assert row[1] == page['content_length'] == 96394
assert row[2] == page['fetched_at'] == '2026-08-22T23:48:19Z'
assert target_asset_rows == 0
assert crawl_count == spec['fixture_expectations']['crawl_revision_count'] == 2
print('page_sha256', page_digest)
print('page_content_length', row[1])
print('page_fetched_at', row[2])
print('target_asset_rows', target_asset_rows)
print('same_url_crawl_vintages', crawl_count)
print('article_timestamp', spec['publication_lag']['article_timestamp'])
print('publication_lag', spec['publication_lag']['release_lag_status'])
print('decision', spec['disposition'])
page_sha256 sha256:10b2985e4d23ae06f1011ff5fb80bcef8e25adcd227910ac71e82cb8079da60a
page_content_length 96394
page_fetched_at 2026-08-22T23:48:19Z
target_asset_rows 0
same_url_crawl_vintages 2
article_timestamp 2023-05-22T17:53:36-0400
publication_lag unknown
decision insufficient_evidence

The documented rule, without invented weights

The article names eight observations: labor, earnings estimates, global leading data, S&P 500 200-day technicals, Zweig breadth, WLEI, a Gen2 probability model, and CMHI. They are context, not eight equal votes. The source gives no aggregation formula, state threshold, current value, or trade execution map.

In [2]:
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

components = spec['rules']['components']
for component in components:
    print(f"{component['number']}. {component['name']}: {component['documented_rule']}")

fig, ax = plt.subplots(figsize=(9, 4.8))
labels = [component['name'].replace('_', ' ').title() for component in components]
ax.barh(labels[::-1], [1] * len(labels), color='#4472a8')
ax.set_xlim(0, 1.25)
ax.set_xlabel('Source-described observation (not a signal score)')
ax.set_title('Bull Market Safari: eight narrative observations')
ax.set_xticks([0, 1])
ax.set_xticklabels(['Not present', 'Described on page'])
ax.text(0.02, -0.17, 'Each mark identifies an article observation; no weighting or aggregation is disclosed.', transform=ax.transAxes, fontsize=9)
fig.tight_layout()
display(fig)
1. unemployment: State-wide unemployment data used in the shown fashion is described as leading for future unemployment rises and recessions; this factor appears positive while seven other composite factors are negative.
2. earnings_estimates: A positive sighting is discussed, but estimates are given less weight than hard data because of subjectivity and error.
3. global_leading_data: Described as a useful global/U.S. leading measure; revisions can be material near turning points, with at least three months suggested for confirmation and two further months of observation for the current trend.
4. sp500_200ma: The S&P 500 had spent more than six weeks above its 200-day average and the average had turned up; a cited 1928 historical study found the index higher after an upturn in 20 of 20 cases with average gain 18%.
5. zweig_breadth_thrust: The article cites a classic thrust on 2023-03-31 and a Reduxed ZBT-A in early October 2022, describing the Redux as more useful for pinpointing major bottoms.
6. weekly_leading_economic_index: WLEI is described as having put in a bottom and nearly moved above water, while the Weekly SuperIndex and most monthly leading data had not followed.
7. gen2_sp500_probability_model: The article gives example bottom probabilities just under 80% by drawdown, over 95% by duration, and just under 90% when averaged.
8. composite_market_health_index: CMHI is described as recovering from classical bear-market levels but stalling; two metrics are price momentum, three breadth, one macro-economic, and one seasonal.
<Figure size 900x480 with 1 Axes>

Causal boundary and decision

The article timestamp is 2023-05-22T17:53:36-0400. Component release lag, vintages, revision calendars, and calculation cutoffs are unknown; the page warns that OECD leading data can be revised near turning points and suggests three months of confirmation. A future overlay must use point-in-time inputs and at least one available-bar execution lag; same-close execution is unsupported.

The result is insufficient evidence. Baseline returns, subperiod stability, adjacent parameters, false-signal behavior, and market-outcome metrics are not estimable without a source-linked component history and a disclosed state machine. The safe public action is an Other / Research evidence-gap report, with no registry change.