Shell Heuristics · application docs

publisher · enricher-v2 · corpus

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-classify and the scheme/feature passes share the same pool-postcrime-* aliases as the enrichment pipeline. How those route to NIM vs LAN GPUs (and why NIM usage is low) is in llm-routing.

securities anomalies — documents → scheme node → confidence → lead-time source regex pass LLM pass scheme table cross-ref feature / score DOJ + SEC press PR, litigation release already in corpus.db court dockets + pleadings indictments, complaints, judgments, sentencings EDGAR universe edgar-universe.db filings.db (6.9 GB) scheme-cluster (regex) caption-hash → scheme_id binds ~80% on first pass scheme-link (LLM) residual disambiguation qwen3:8b · k-nearest scheme-fields + evidence dates · amounts · participants wiretap · CI · whistleblower schemes table scheme_id · type · dates · amount disposition · participants_jsonb documents.scheme_id + scheme_role entity universe expansion participants → edgar-cik-resolver address · counsel · officer overlap expansion-neighbors back into scheme feature catalog (per CIK) SIC 6770 · name-chain · counsel officer-churn · address · cadence confidence scoring (linear v1) per-accession trajectory first crossing 0.6 = T_first_flag market data layer yahoo + penny-stock store volume spike · block sale · pump lead-time backtest Δ_detection = T_indictment - T_first_flag Δ_evidence = T_evidence_start - T_first_flag historical: blank-check + known-associates cluster link extract scheme_id link fields participants CIK + neighbors 15 features price · volume T_indictment + evidence_start T_first_flag routing per design: 1. schemes table → entity universe (lateral, participants resolve to CIKs) 2. entity universe → feature catalog (left fan-out via row-510 corridor, no overlap with scheme table) 3. schemes table → confidence scoring (down through middle row-510, anchors T_indictment + evidence_start) 4. confidence scoring → lead-time backtest (straight vertical down to centre of backtest box) historical test sets: A. blank-check universe — SIC 6770 NV/DE 2010-2018; recall + median lead-time on prosecuted CIKs B. known-associate expansion — defendants in 10 seed schemes (Discala/Cane, Honig, Lebed, Beasley, ...) market-data layer is the next-feature dependency; without it volume-spike + pump features stay stubbed.

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

  1. Pre-cluster (regex pass scheme-cluster)
    • Normalize caption from documents.title.
    • Stable-hash → candidate scheme_id.
    • Materialize documents.scheme_id for the easy 80%.
  2. 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_id or null (creates new).
  3. 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.
  4. Cross-reference (regex)
    • For each participant name resolved by entities enrichment, look up cik in edgar-universe.db via edgar-cik-resolver. Annotate participants_jsonb[i].edgar_cik.
  5. Materialize scheme row
    • Insert/update schemes table; stats-miner panels read from it.

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.6 over 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

  1. M1 — schema + scheme-cluster regex pass (1 week)
    • Migration: schemes table, documents.scheme_id/scheme_role columns.
    • Regex scheme-cluster pass binds the easy 80%.
    • Single /anomalies/schemes panel listing all schemes by date.
  2. M2 — LLM disambiguation + role + field extraction (1 week)
    • scheme-link, scheme-fields, scheme-evidence enrichment passes.
    • Backfill across the existing corpus.
  3. M3 — entity universe expansion (3 days)
    • Cross-reference participants_jsonb against edgar-universe.db.
    • Address / counsel / officer expansion.
  4. M4 — feature catalog v1 + confidence column on edgar-universe.entities (1 week)
    • 15 features above, hand-weighted, materialized per-CIK.
    • /anomalies/feature-catalog doc + per-feature drill panels.
  5. 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.
  6. 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_id but their content shouldn't double-count amounts. The scheme_role enum 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.mdhttps://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)