Securities Anomalies — feature-extraction architecture
Design for the scheme-centric view that backs https://sec.atsignhandle.xyz/anomalies and (proposed) https://sec.atsignhandle.xyz/anomalies/docs. The corpus today stores documents (press releases, litigation releases, complaints, indictments, judgments) as flat rows. This doc lays out how those rows roll up into a scheme node, what features we extract from EDGAR to score scheme exposure before indictment, and how we measure detection lead-time against the historical record.
Why a scheme node
A securities-fraud prosecution typically generates 5–40 documents: an SEC press release, an SEC litigation release, a DOJ press release, a criminal complaint or indictment (sometimes superseding), a parallel civil complaint, judgments, sentencings, restitution orders, and one or more administrative orders. Today every one of those is a row in documents. To answer "what schemes exist, who was involved, how much money, and how was it caught" we need one row per scheme, with documents attached.
LLM backend routing:
anomaly-classifyand the scheme/feature passes share the samepool-postcrime-*aliases as the enrichment pipeline. How those route to NIM vs LAN GPUs (and why NIM usage is low) is in llm-routing.
Scheme identity
scheme_id = uuid_v5(
namespace = "fraud-heuristics/scheme/v1",
name = canonical_caption || "|" || primary_filing_date
)
canonical_caption— DOJ/SEC caption normalized: drop "v." vs "v", drop "et al.", drop "United States of America" / "Securities and Exchange Commission" prefix, lowercase, ASCII-fold.primary_filing_date— earliest of (criminal complaint, indictment, SEC civil complaint).- A criminal case and its parallel civil case fold to one scheme (same caption family, dates within ~30 days, ≥1 shared defendant).
- A superseding indictment shares the scheme of the original; a separate prosecution after acquittal does not.
Scheme table — fields
| Field | Type | Source | Pass | Notes |
|---|---|---|---|---|
scheme_id |
uuid |
derived | — | PK |
canonical_caption |
text | DOJ/SEC | regex | display title |
scheme_type |
enum | scheme-classify |
LLM | pump-dump · insider · unregistered · mkt-manip · accounting · other |
scheme_subtype |
text | scheme-classify |
LLM | "matched trade", "free-riding", "shell-merger", etc. |
date_complaint |
date | first filing | regex | filing date of complaint/information/indictment |
date_scheme_start |
date | pleading overt acts | LLM | earliest "on or about" inside indictment |
date_scheme_end |
date | pleading overt acts | LLM | latest "on or about" |
total_amount_usd |
numeric | pleadings + PR | LLM | victim losses, sometimes equals proceeds |
proceeds_usd |
numeric | pleadings | LLM | defendant gain, may differ from victim loss |
disposition |
enum | docket | regex+LLM | open · plea · trial-conviction · trial-acquittal · settled · dismissed |
disposition_date |
date | docket | regex | latest terminal action |
participants_jsonb |
jsonb | entities + cross-ref |
both | array of {role, name, edgar_cik?, role_source} |
tickers_jsonb |
jsonb | pleadings + PR | regex | e.g. ["CDS","FORX",…] |
evidence_source |
enum | pleadings | LLM | wiretap · ci · cw · whistleblower · sar · market-surveillance · self-report · parallel-civil · unknown |
evidence_start_date |
date | pleadings | LLM | earliest court-authorized intercept / cooperator agreement |
agencies_jsonb |
jsonb | pleadings + PR | regex | DOJ-SDNY · SEC · FBI · IRS-CI · USPIS · CFTC · FINRA · state |
cases_jsonb |
jsonb | dockets | regex | [{court, docket_no, kind}] (criminal + civil parallels) |
wiretap_orders_jsonb |
jsonb | pleadings | LLM | [{authorizing_judge, start_date, end_date, target_phones?}] |
cooperators_count |
int | pleadings | LLM | counted CW/CI/CS references |
cooperator_names_jsonb |
jsonb | pleadings | LLM | named cooperators only |
confidence |
float | derived | — | post-extraction QA score |
Document role (per-document join)
documents.scheme_id (nullable uuid) and documents.scheme_role (enum) are the two new columns wired in via a regex+LLM pass. Roles:
scheme_role |
Trigger heuristic | Counts toward |
|---|---|---|
indictment |
Form: indictment / information / complaint (criminal) | dates, participants, overt acts |
superseding_indictment |
"Superseding" in title | overrides original facts |
civil_complaint |
SEC v. … (civil) | participants, amounts |
press_release |
DOJ-PR / SEC-PR with caption match | tickers, amounts |
litigation_release |
SEC litigation release | amounts, disposition |
judgment_order |
judgment, final order, consent order | disposition |
sentencing |
sentencing transcript, J&C | disposition |
restitution_order |
restitution order | victim totals |
plea_agreement |
plea agreement | disposition |
motion_or_misc |
other | ignored for top-level fields |
Pipeline — document → scheme
- Pre-cluster (regex pass
scheme-cluster)- Normalize caption from
documents.title. - Stable-hash → candidate
scheme_id. - Materialize
documents.scheme_idfor the easy 80%.
- Normalize caption from
- LLM disambiguation (
scheme-link, qwen3:8b)- Run only on residual rows that didn't bind in step 1.
- Input: title + 2 KB of body summary + 5 nearest neighbors (by ticker + date window).
- Output: existing
scheme_idornull(creates new).
- Field extraction (per
scheme_role× per-pass)- Regex for: dates, dollar amounts, tickers, agencies, docket numbers, judge names.
- LLM for: scheme type/subtype, overt-act window, evidence source, wiretap dates, cooperator counts.
- Cross-reference (regex)
- For each participant name resolved by
entitiesenrichment, look upcikinedgar-universe.dbviaedgar-cik-resolver. Annotateparticipants_jsonb[i].edgar_cik.
- For each participant name resolved by
- Materialize scheme row
- Insert/update
schemestable;stats-minerpanels read from it.
- Insert/update
All five pass-types are added to the existing fraud-publisher queue. None require new infrastructure beyond corpus.db migrations.
Entity universe — EDGAR cross-reference
After step 4, participants_jsonb is hydrated. From there we expand the universe:
- Individual → all CIKs they're listed on (Form 4 reporting persons, S-1 officers/directors, 10-K certifications).
- Entity → all CIKs in the corporate family (sub-CIKs of same parent, predecessor name-change chain, address cluster).
- Counsel → all CIKs they signed exhibits on (filing-agent / preparer index already lives in
edgar-cik-cli/counsel-index/). - Address → all CIKs at that address (already in
address-clusters.json).
The expanded universe goes back into participants_jsonb as role: "expansion-neighbor" so we keep the original-vs-derived split clean. This is how a single scheme node ends up touching the full Cane Clark / Cane O'Neill Taylor network without the LLM having to know about it.
Confidence scoring — features from EDGAR alone
Goal: given only a CIK and an accession, produce a probability that this CIK is part of an active securities-fraud scheme. Then test against historical schemes where we know the indictment date.
Feature categories (all extractable from EDGAR filings + edgar-universe.db):
| Feature | Source | Direction | Weight class |
|---|---|---|---|
| SIC blank-check (6770) | submissions.json | + | strong |
| Predecessor name-change count ≥ 3 | namechange-index | + | strong |
| Reg-S issuance present | filings body | + | strong |
| Promissory note exhibit count | filings exhibits | + | medium |
| Opinion-letter present | filings exhibits | + | medium |
| Edgarizer fingerprint | filing_agent_id | + | medium |
| Cane-Clark / Diane-Dalmy counsel signature | counsel-index | + | strong |
| Officer turnover ≥ 3 in 2 years | Form 4 / 10-K | + | medium |
| Address shared with ≥ 3 other shells | address-clusters | + | strong |
| Form 4 cluster (≥ 5 insider buys within 30 days) | Form 4 | + | medium |
| Filing-cadence anomaly (silence → burst pre-pump) | filings.db | + | medium |
| Ticker volume / market-cap spike | edgar-pipelines | + | weak (needs market data) |
| 144 filings cluster | filings.db | + | medium |
| Active S-8 with insider-controlled trust | S-8 + Form 4 | + | strong |
| Recent CIK (< 24 months) + ALL of: SIC 6770, blank-officers, no revenue | submissions.json | + | strong |
Scoring: simple linear combination at first (no ML), weights set by hand. Per-CIK score = sum(w_i × present_i) / max_possible. The point isn't a perfect classifier — it's reproducible scoring that we can re-run as features are added.
Historical validation — two test sets
Test set A — blank-check unregistered-securities universe
- Filter:
submissions.sic = '6770' AND first_filing_date BETWEEN 2010 AND 2018 AND state IN ('NV','DE'). - For each CIK, compute confidence at each accession in chronological order.
- For known-bad CIKs (cross-reference DOJ/SEC schemes already in our corpus), record the accession date at which
confidence ≥ 0.6("first flag"). - Lead-time =
date_complaint − date_first_flag. - Metric: median lead-time + recall at
confidence ≥ 0.6over the known-bad set.
Test set B — known-associate expansion
- Seed: defendants in 10 already-prosecuted schemes (Discala/Cane, Honig, Lebed, Beasley, etc.).
- Expand via the entity universe above.
- For each expansion-neighbor CIK, compute confidence trajectory.
- Metric: how many expansion-neighbors hit confidence threshold before their own prosecution (if any).
Both sets are read-only experiments against corpus.db + edgar-universe.db; no new ingest required to start.
Evidence-provenance extraction
How did the government find the scheme? This is buried in the pleadings and matters because (a) it tells us which signals to weight in future scoring, and (b) it gives us the evidence_start_date lower bound — the earliest date the government had probable cause.
| Provenance class | Regex anchors | LLM disambiguation |
|---|---|---|
wiretap |
"Title III", "wire interception", "court-authorized interception of wire communications", "intercepted", "target telephone" | confirm + extract start_date / end_date + authorizing_judge |
ci / cw |
"Confidential Source", "CS-[0-9]", "CW-[0-9]", "Confidential Witness", "Cooperating Witness" | count distinct identifiers |
cs_named |
"[Person] pleaded guilty … and is cooperating" | extract person, cooperation date |
whistleblower |
"Section 21F", "Dodd-Frank whistleblower", "tip submitted under" | confirm; extract reward if disclosed |
sar |
"Suspicious Activity Report", "SAR filed", "bank's compliance" | confirm + bank name |
market_surveillance |
"FINRA referral", "trade surveillance", "exchange flagged" | confirm + venue |
self_report |
"voluntarily disclosed", "self-reported", "internal investigation" | confirm + party |
parallel_civil |
"parallel civil action", "SEC investigation began" | extract SEC inception date if stated |
evidence_start_date is the earliest dated anchor across all classes — typically the wiretap authorization date or the first dated cooperator agreement. This is the closest proxy we have to "when did the government have a viable case?".
Detection lead-time — the metric that matters
For each scheme we now have three timestamps:
T_first_flag ← earliest accession where our confidence ≥ 0.6
T_evidence_start ← from pleadings (wiretap / cooperator / SAR / etc.)
T_indictment ← date_complaint
Two gaps to minimize:
Δ_detection_lead = T_indictment − T_first_flag (positive = we'd have flagged first)
Δ_evidence_lead = T_evidence_start − T_first_flag (positive = we'd have flagged before the wiretap began)
Δ_detection_lead is the headline. Δ_evidence_lead is the harder target — it asks whether public EDGAR data alone could have driven a SAR or a surveillance referral before the gov spent a year on a wiretap.
Per-scheme + aggregate medians populate the new /anomalies/lead-time panel.
Reference case — US v. Discala et al. (SDNY 14-cr-399 / 14-cv-5074)
A worked example used for both regression and design review.
| Field | Value | Source |
|---|---|---|
scheme_id |
uuid_v5("us v. discala|2014-07-17") | derived |
canonical_caption |
"us v. discala et al." | DOJ press release |
scheme_type |
pump-and-dump (matched-trade variant) | indictment |
date_complaint |
2014-07-17 | indictment filing |
date_scheme_start |
2012-10 (approx) | indictment overt acts |
date_scheme_end |
2014-06 | indictment + civil complaint |
total_amount_usd |
$300M+ (alleged) | DOJ PR |
disposition |
trial conviction (2017) | docket |
evidence_source |
wiretap (+ cooperators) |
indictment ¶¶ describing intercepts |
evidence_start_date |
2013-06 (T-III order) | indictment |
cooperators_count |
≥ 2 (CW-1, CW-2 referenced) | indictment |
agencies_jsonb |
["DOJ-EDNY", "SEC", "FBI"] |
PR |
participants_jsonb |
Discala, Hatfield, Cane Clark LLP (counsel) | indictment + counsel-index |
tickers_jsonb |
CDS, FORX, NRTI, SCRC | indictment |
For each ticker we walk forward through EDGAR accessions and ask: at what point would our feature catalog have flagged the issuer? The answer feeds Δ_detection_lead. For CDS specifically the predecessor-name-change chain + Cane-Clark counsel + S-8 trust pattern are all present years before 2014; the prediction is that confidence ≥ 0.6 on or before 2012-Q4 — two years before indictment, one year before the wiretap.
Roadmap
- M1 — schema + scheme-cluster regex pass (1 week)
- Migration:
schemestable,documents.scheme_id/scheme_rolecolumns. - Regex
scheme-clusterpass binds the easy 80%. - Single
/anomalies/schemespanel listing all schemes by date.
- Migration:
- M2 — LLM disambiguation + role + field extraction (1 week)
scheme-link,scheme-fields,scheme-evidenceenrichment passes.- Backfill across the existing corpus.
- M3 — entity universe expansion (3 days)
- Cross-reference
participants_jsonbagainstedgar-universe.db. - Address / counsel / officer expansion.
- Cross-reference
- M4 — feature catalog v1 + confidence column on
edgar-universe.entities(1 week)- 15 features above, hand-weighted, materialized per-CIK.
/anomalies/feature-catalogdoc + per-feature drill panels.
- M5 — historical validation (1 week)
- Test set A (blank-check) and test set B (known-associate expansion).
- Δ_detection_lead and Δ_evidence_lead per scheme + aggregates.
- M6 —
/anomalies/lead-time+ iteration loop (ongoing)- Each new feature is judged on whether it improves Δ_detection_lead median without dropping recall.
Open questions
- Market data: ticker-price + volume features would dramatically improve the catalog but require a new data feed (FINRA OTC market data, polygon.io, or similar). Today we only have EDGAR.
- Sealed docket detection: when a wiretap is still under seal we can't extract
evidence_start_date. Need a fallback: infer wiretap presence from cooperator-density and prosecution narrative. - De-duplication of agency PRs: DOJ and SEC sometimes issue near-identical press releases on the same scheme. They should fold to the same
scheme_idbut their content shouldn't double-count amounts. Thescheme_roleenum captures this; field extraction needs to prefer indictment > civil complaint > PR. - Restoring sealed defendants: indictments redact sometimes; we may not see all participants. Cross-reference against the docket lists helps but isn't complete.
- Weighting: hand-weighting features is fine for v1 but eventually we want a logistic regression on known-bad/known-good. The training set is small (~100 prosecuted schemes); regularization heavy.
Where this lives
| Artifact | Path |
|---|---|
| This document | docs/application/anomalies.md → https://sec.atsignhandle.xyz/docs/anomalies.html |
| Public alias | https://sec.atsignhandle.xyz/anomalies/docs (SvelteKit route, redirects to the rendered HTML) |
| Schema migrations | broker/src/migrations/2026-05-23-schemes-table.sql (to be added in M1) |
| Cluster pass | processor/src/extract/scheme-cluster.ts (to be added in M1) |
| Link/fields/evidence passes | processor/src/extract/scheme-link.ts, scheme-fields.ts, scheme-evidence.ts (M2) |
| Feature catalog | processor/src/features/ directory (M4) |
| Lead-time experiment | processor/scripts/lead-time-backtest.ts (M5) |