Back to Editorial
2026-08-16//InsiderAlpha Research//Updated 2026-08-16

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_table separates non-derivative securities from derivatives;
  • is_current prevents superseded amendment versions from being reused;
  • Shares and Price establish positive disclosed terms;
  • validation_status makes provenance explicit;
  • accepted_at establishes when an SEC-linked filing became public;
  • accession_number and transaction_sequence preserve 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

SQL
-- 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:

StageRemaining rows or eventsWhat changed
All publishable transaction rows1,633,946 rowsNo semantic filter
Transaction code P49,046 rowsGrants, exercises, tax withholding and sales removed
Valid ticker candidates48,384 rowsNull-like ticker sentinels removed
Current, positive-term, non-derivative purchase records48,260 rows124 derivative P rows removed; validated legacy history retained
Aggregated owner-date-security events21,031 eventsSame-event tranches collapsed
Strict SEC-linked execution sample18,972 rowsCanonical SEC provenance and public timestamp required
Aggregated strict SEC-linked events11,861 eventsOne 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:

SQL
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:

SQL
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:

  1. Ingestion: preserve raw XML identity, accession, table, sequence and timestamps.
  2. Canonicalization: resolve amendments and normalize issuer and reporting-owner identity.
  3. Semantic events: classify by SEC code and aggregate transaction lines at an explicit unit.
  4. 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
Download 10,000-row sample
Buy full dataset · $99Review full release details

One-time payment · instant download · regular price $199

Tags
Insider Trading ScreenerForm 4DuckDBSQLData Engineering