Python · multi-source signal fusion

Three real signal sources, fused into one buying-trigger score

SEC EDGAR filings, GitHub release activity and Hacker News mentions — real entity resolution, a documented Severity × Confidence × Recency model, and retry logic added after two genuine live-run failures.

What this is, and what it isn't

A composite scoring engine that ingests real signals from three independent, free, public APIs — SEC EDGAR full-text search, GitHub (REST + Search), and Hacker News (Algolia) — resolves them to canonical entities, and combines them with a documented model. No synthetic data anywhere in this repo; every signal is a real, dereferenceable record from a live API call.

Not a real prospect list. The 14 entities scored below are large public SaaS companies, chosen for exactly one reason: verifiable coverage across all three signal sources. They are not realistic outbound targets for a solo GTM consulting practice — they exist purely so the fusion mechanism could be proven against real, independently-checkable data instead of invented records.

The three sources, and why each is real

SourceWhat it givesConfidence
SEC EDGAR full-text search8-K filings, keyed by CIK, ranked by disclosed item0.95 — CIK unambiguous
GitHub REST + Search APIRelease-cadence change for the org's highest-starred repo0.90 — org slug unambiguous
HN Algolia Search APIStory mentions, severity = the story's own point count0.60 — name-string matching, see below

No API key for any of the three. SEC asks only for a descriptive User-Agent, which this repo sends.

The scoring model — every constant explained, none tuned

This rule has not been validated against real outcomes — it's the documented starting point, not a proven one. That validation is exactly what correlate_with_replies.py is waiting to do — see below.

Entity resolution — the one place this can go wrong, and how it's handled

SEC and GitHub signals are keyed to an unambiguous identifier before a request is even made. HN starts from free text, so dedup.py matches a story's title against each entity's canonical name (whole-word, case-sensitive). If zero or more than one entity matches, the hit is dropped, not guessed — a two-company story doesn't get arbitrarily assigned to one of them.

HN's search matches far more than titles. Inspecting Okta's zero-resolved run by hand: Algolia's query=Okta returned real stories where the literal word "Okta" never appeared in the title — the match came from body text, URL, or comments. Short or common company names (Okta, Twilio, Asana) see a much higher resolution-drop rate than distinctive ones (Cloudflare, GitLab) — not because they're less newsworthy, but because Algolia's search surface is broader than the field this engine actually checks. The conservative choice (drop rather than guess from body text) was kept deliberately; real drop rates are reported per-entity in the fetch log, not averaged away.

What broke on the first real runs

SEC EDGAR bare-500'd on 3 of 14 entities mid-run, confirmed transient — the exact failing request, re-issued by hand seconds later, succeeded every time. The first run's script had no retry and would have silently shipped an 11/14 dataset.

A later run hit widespread connection resets across both api.github.com and hn.algolia.com simultaneously, while a plain curl loop against the same hosts succeeded 5/5 immediately after — consistent with transient local network conditions, not a server-side outage.

Both incidents are fixed with the same mechanism: retry.py, bounded exponential backoff, applied uniformly to all three sources' network calls, not just the one that failed first. The third rerun came back with 456 signals across 14/14 entities and zero errors. All three runs' outcomes are described in this repo, not just the clean one.

Verified run — clean, zero errors

456 signals across 14/14 entities, zero fetch failures logged. HN: 573 hits returned, 266 dropped on entity resolution (46%) — the conservative-resolution tradeoff above, measured, not hidden.

RankTickerCompositeSignals (SEC / GitHub / HN)
1NET (Cloudflare)1.000018 / 1 / 51
2SNOW (Snowflake)0.90398 / 1 / 35
3MNDY (monday.com)0.89830 / 1 / 0 — one high-severity GitHub signal alone
12MDB (MongoDB)0.41657 / 0 / 38
13CRWD (CrowdStrike)0.313710 / 0 / 3

Full table in output/scored-entities-2026-08-23.csv. Raw per-signal evidence (every SEC filing URL, GitHub releases page, HN story link) in output/all-signals-2026-08-23.json.

Reproduce it

python3 -m venv .venv
./.venv/bin/pip install -e ".[dev]"
./.venv/bin/pytest -q                        # 44 tests, offline, against committed fixtures
./.venv/bin/python scripts/fetch_live_signals.py   # real live run, ~3-4 min

Committed fixtures (real API responses captured 2026-08-23) let the test suite run fully offline; fetch_live_signals.py is the only thing that touches the network, and it's what produced every file in output/.

What this doesn't prove

Files

src/signal_engine/entities.py      canonical entity list — real tickers/CIKs/GitHub orgs
src/signal_engine/scoring.py       Signal dataclass, severity rubrics, decay, noisy-OR aggregation
src/signal_engine/dedup.py         HN title -> canonical ticker resolution, ambiguity handling
src/signal_engine/retry.py         bounded exponential backoff (added after a real SEC 500)
src/signal_engine/correlation.py   Spearman rho, stdlib only
src/signal_engine/engine.py        aggregates Signals into a ranked entity table
scripts/fetch_live_signals.py      the only network-touching script; produces everything in output/
scripts/correlate_with_replies.py  blocked on real reply data — fails loudly, on purpose
fixtures/                          real API responses, captured 2026-08-23, offline tests
output/                            real committed run output — scored table, raw signals, fetch log
tests/                             44 tests: scoring math, dedup, retry, correlation, fixture-parsing

Built something similar for your stack?

I build signal-detection, enrichment and outbound systems inside your own tools. You own everything — nothing is locked to me.