How to Build an Insider Trading Screener That Does Not Count Grants as Buys
A reproducible Form 4 screening workflow uses SEC transaction codes, canonical filings and owner-event aggregation to separate reported purchases from grants, exercises and tax withholding.
An insider-trading screener can return technically valid rows and still be economically wrong.
The usual error is filtering Form 4 records where shares were “acquired.” That direction flag includes compensation grants, option exercises, gifts and conversions. It answers whether reported ownership increased on a row—not whether the insider spent capital to purchase a security.
In the July 29, 2026 InsiderAlpha Form 4 Research Database, 391,613 canonical rows carry an acquired direction flag. Only 19,078 of those rows use transaction code P. At least 361,166 are grants, exercises, gifts or conversions represented by codes A, M, G and C.
The correct first filter is transaction semantics. Everything else—dollar value, role, clusters and holdings—comes afterward.
This article builds a reproducible purchase screener in DuckDB and shows exactly what each stage removes.
Define what the screener means
The target is a reported non-derivative purchase event. It is not a guarantee of an exchange execution, a discretionary motive or a profitable trade.
Under the official SEC Form 4 instructions, code P means an open-market or private purchase. That makes P the correct purchase candidate, but additional fields are still required:
transaction_tableseparates non-derivative securities from derivatives;is_currentprevents superseded amendment versions from being reused;SharesandPriceestablish positive disclosed terms;validation_statusmakes provenance explicit;accepted_atestablishes when an SEC-linked filing became public;accession_numberandtransaction_sequencepreserve auditability.
The detailed empirical distribution behind this decision is available in Form 4 Transaction Codes in Practice.
Why common filters fail
Failure 1: treating every acquisition as a buy
-- Wrong: includes grants, exercises, gifts and conversions.
SELECT *
FROM transactions
WHERE acquired_disposed = 'A';
acquired_disposed is a direction field. It is not a transaction classifier.
Failure 2: treating every positive value as conviction
A grant can have shares and a price-like field. An exercise can create a large calculated value. Neither becomes a purchase because the numeric fields are populated.
Failure 3: counting filing lines as independent events
One owner can report several tranches for the same security and date. Ranking every line separately overweights complex filings and can put the same economic decision into the screener multiple times.
Failure 4: using the transaction date as public time
The transaction occurred before the market learned about it. A historical strategy that enters on Date can contain look-ahead bias. Use accepted_at for SEC-linked execution research.
The screening funnel
The complete release contains 1,633,946 rows. Applying semantic and data-quality rules produces the following funnel:
| Stage | Remaining rows or events | What changed |
|---|---|---|
| All publishable transaction rows | 1,633,946 rows | No semantic filter |
Transaction code P | 49,046 rows | Grants, exercises, tax withholding and sales removed |
| Valid ticker candidates | 48,384 rows | Null-like ticker sentinels removed |
| Current, positive-term, non-derivative purchase records | 48,260 rows | 124 derivative P rows removed; validated legacy history retained |
| Aggregated owner-date-security events | 21,031 events | Same-event tranches collapsed |
| Strict SEC-linked execution sample | 18,972 rows | Canonical SEC provenance and public timestamp required |
| Aggregated strict SEC-linked events | 11,861 events | One auditable owner event per date, security and ownership form |
The 48,260-row historical mode includes validated legacy records whose older source lineage does not expose the same structured SEC fields. The 18,972-row strict mode is smaller but is the appropriate base when public availability and filing-level auditability are required.
A production-oriented DuckDB query
The following query implements strict mode against the release's transactions table:
WITH eligible_rows AS (
SELECT
"Ticker" AS ticker,
"Date" AS transaction_date,
"Executive" AS executive,
COALESCE(
NULLIF(reporting_owner_cik, ''),
LOWER(TRIM("Executive"))
) AS owner_key,
standard_role,
security_title,
ownership_nature,
"Shares" AS shares,
calculated_total_value,
shares_owned_after,
cluster_size_30d,
notes,
accession_number,
accepted_at
FROM transactions
WHERE COALESCE(is_current, TRUE)
AND "Type" = 'P'
AND UPPER(TRIM("Ticker")) NOT IN ('NONE', 'NULL', 'N/A', '')
AND transaction_table = 'non_derivative'
AND validation_status = 'sec_verified'
AND "Shares" > 0
AND "Price" > 0
AND accepted_at IS NOT NULL
),
purchase_events AS (
SELECT
ticker,
transaction_date,
owner_key,
MAX(executive) AS executive,
MAX(standard_role) AS role,
security_title,
ownership_nature,
SUM(shares) AS purchased_shares,
SUM(calculated_total_value) AS disclosed_value,
MAX(shares_owned_after) AS shares_owned_after,
MAX(cluster_size_30d) AS cluster_size_30d,
BOOL_OR(
COALESCE(
REGEXP_MATCHES(notes, '(?i)(10b5|trading plan)'),
FALSE
)
) AS plan_language_detected,
MIN(accepted_at) AS public_at,
STRING_AGG(DISTINCT accession_number, ',') AS accessions
FROM eligible_rows
GROUP BY
ticker,
transaction_date,
owner_key,
security_title,
ownership_nature
)
SELECT
ticker,
transaction_date,
public_at,
executive,
role,
purchased_shares,
ROUND(disclosed_value, 2) AS disclosed_value,
ROUND(
100.0 * purchased_shares
/ NULLIF(shares_owned_after, 0),
2
) AS purchase_fraction_pct,
cluster_size_30d,
plan_language_detected,
accessions
FROM purchase_events
ORDER BY public_at DESC, disclosed_value DESC;
This query deliberately returns context instead of a single opaque score. A user can see the owner, role, disclosed value, position change, cluster count, detected plan language and source accession.
Why the query aggregates by owner event
The grouping key is:
ticker × transaction date × reporting owner × security × ownership nature
That unit answers a practical question: how much did one reporting owner acquire in one security and ownership form on one date?
It avoids two opposite errors:
- grouping only by ticker-day can merge separate insiders into one transaction;
- leaving every filing line separate can count multiple price tranches as independent decisions.
For company-level event studies, a second aggregation to ticker-day may be appropriate. For a screener showing who bought, the owner event should remain visible.
Historical mode versus strict mode
To include the validated historical population, replace the strict provenance conditions with:
AND validation_status IN ('sec_verified', 'validated_legacy')
AND (
transaction_table = 'non_derivative'
OR (
transaction_table IS NULL
AND validation_status = 'validated_legacy'
)
)
Historical mode increases coverage from 18,972 to 48,260 eligible rows and produces 21,031 aggregated events across 1,203 issuers. It is useful for descriptive research. It should not silently inherit canonical fields that the legacy source does not provide.
Strict mode produces 11,861 aggregated events across 678 issuers. Because every source row has accepted_at, it supports filing-time chronology and direct accession audits.
What to rank after the semantic filter
Once grants and mechanical records are removed, the screener can expose several evidence-based dimensions.
Disclosed value
Among the 21,031 historical owner events, 12,089 disclosed at least $100,000 in calculated value. That threshold can reduce a feed, but it should not be treated as a universal conviction boundary. Our earlier research found no monotonic improvement as nominal purchase value increased.
Purchase relative to reported holdings
purchased_shares / shares_owned_after adds context that dollars cannot. Holdings were available for 11,849 historical owner events. Missing holdings should remain null; they should not be converted to zero or interpreted as a new position.
The relative-purchase-size study found that this feature was useful descriptively but did not create a stable universal premium by itself.
Owner role
CEO, CFO, director and 10% owner transactions occur under different incentives. Keep the normalized role and the original position text. Do not assume one role always has superior timing: our CEO-versus-CFO study found no robust universal CFO advantage.
Cluster context
In historical mode, 9,452 owner events had at least two distinct purchasers in the associated 30-day issuer window. Cluster context is useful for ranking, but it should remain a feature rather than a binary trading rule.
Trading-plan language
The query detects 10b5 or trading plan language in footnotes. It found such language in 1,743 historical owner events. This is a text flag, not verified intent. Absence of the phrase does not prove that a transaction was discretionary, and presence should lead to footnote review rather than automatic deletion.
Recommended screener architecture
A serious implementation separates four layers:
- Ingestion: preserve raw XML identity, accession, table, sequence and timestamps.
- Canonicalization: resolve amendments and normalize issuer and reporting-owner identity.
- Semantic events: classify by SEC code and aggregate transaction lines at an explicit unit.
- Presentation and ranking: expose value, holdings, role, clusters, plan language and source links.
Keep classification deterministic and testable. Ranking rules can evolve, but a code A grant should never become a purchase because a model assigned it a high score.
Useful acceptance tests include:
- no output row has a transaction code other than
P; - no strict output row comes from the derivative table;
- every strict output row has a public timestamp and at least one accession;
- every event has positive shares and price;
- amendments cannot produce duplicate current transactions;
- aggregated shares and value reconcile to their source rows.
Limitations
Code P includes private purchases, so the query should be labeled a reported-purchase screener rather than an exchange-only screener. Form 4 does not reveal total personal wealth, every outside exposure or the insider's motive.
Footnote detection can produce false positives and false negatives. Owner-event aggregation depends on identity quality. The database's return fields describe later security-price changes and are not benchmark-adjusted alpha. None of these records constitutes investment advice.
Conclusion
The essential rule is not complicated: use the transaction code to decide what happened, then aggregate and enrich the resulting event.
In this release, that rule reduces 1.63 million raw rows to 49,046 code P records before any ranking begins. Strict SEC lineage, valid ticker filtering and owner-event aggregation produce 11,861 auditable purchase events. The result is smaller than an “all acquisitions” feed—and substantially more defensible.
Researchers can run the query against the DuckDB file in the complete research release, inspect the same schema in the free sample, or build a live integration with the InsiderAlpha API.
Inspect before purchasing
Preview 10,000 real rows and all 64 fields
Validate the schema and research workflow before buying the 1,633,946-row release. No email or account is required for the sample.
- Reproduce a published report
- Run the included queries with DuckDB
- CSV, Parquet and DuckDB in the full release
- Three months of API access included
One-time payment · instant download · regular price $199