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.
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'])
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.
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)
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.