Schema documentation
Two tables make up the data product: insidereu.filings (MAR Article 19 PDMR) and insidereu.substantial_holdings (Transparency Directive / DTR-5 / national major-holding notifications).
Schema stable since 2025-Q4; ingested_at added in 2026-Q1. No field has been renamed or dropped since. Other tables in the insidereu schema are application-internal and not part of the data product.
Table: insidereu.filings
PDMR (Person Discharging Managerial Responsibilities) and closely-related-person insider transactions filed under Article 19 of EU MAR (Regulation 596/2014) and UK/EEA equivalents.
| Column | Type | Null | Meaning |
|---|---|---|---|
| id | text PK | no | Stable cross-scrape dedup key, format {country}-{source-system-ref}. Re-ingesting the same record is idempotent. |
| country | text | no | ISO 3166-1 alpha-2 of the regulator's jurisdiction. Not the issuer's domicile. |
| insider_id | text | no | Stable insider key within a country. Cross-country join not guaranteed. Switzerland is the exception: the regulator publishes a role code, not a person, so CH keys are per role and issuer (see caveat 8). |
| insider_name | text | no | As reported by regulator. Nordic and other transliteration preserved. For CH it is synthetic, e.g. Board Member (Issuer AG). Some notices have no named person and read PDMR at <issuer>. |
| insider_role | text | yes | E.g. Chief Executive Officer, Member of the Board, Closely associated person, or the literal PDMR when no sub-role is provided. |
| company_name | text | no | Issuer name. |
| isin | text | yes | Coverage by country varies — see the coverage matrix. When null, join on (country, company_name). |
| issuer_lei | text | yes | GLEIF Legal Entity Identifier of the issuer, resolved from isin. Null where GLEIF has no mapping or the ISIN is missing. |
| ticker | text | yes | Yahoo Finance symbol, populated for about three quarters of rows. Resolved from the ISIN and accepted only if the exchange suffix matches the ISIN's country; existing values are never overwritten. UK rows from Investegate carry the raw LSE code (e.g. HAS), not a Yahoo symbol. |
| exchange | text | yes | Reporting venue. |
| transaction_date | date | no | Date the trade was executed. |
| filing_date | date | no | The date the market could first see the filing, where the regulator gives one: SE, DE, FI, DK and, for UK rows ingested from 2026-09-21, the RNS publication date. For most other countries it is the transaction date or the announcement date, and CH rows are flagged raw_data.filing_date_is_trade_date. Do not use it as a point-in-time stamp without reading caveat 2. |
| transaction_type | text | no | CHECK IN ('buy','sell','unknown'). 'unknown' means either the source carried no parseable direction, or the transaction is not an open-market trade (an award, grant, vesting or pledge release, flagged raw_data.non_market). Filter transaction_type IN ('buy','sell') for direction-confirmed rows only. |
| shares | numeric | yes | Volume of the trade. Set to NULL where the regulator's figure was found to be unreliable (raw_data.figures_suspect). |
| price | numeric | yes | Price per share in currency. For SE and CH the scraper converts the regulator's SEK / CHF price at a fixed rate and labels the row EUR; the original is kept in raw_data. NULL where the figure failed a plausibility check (caveat 9). |
| value_eur | numeric | yes | Trade size in EUR, computed at ingestion with a fixed approximate rate per currency (SEK 0.087, NOK 0.085-0.087, DKK 0.134, CHF 1.04, GBP 1.17, USD 0.92), not the ECB rate on the transaction date. Adequate for ranking and screening, not for precise P&L. |
| currency | text | yes | ISO 4217 of price. EUR for DE, CH, SE (converted) and Finland; DKK, NOK and GBP where the source price is kept native. |
| source_url | text | yes | Deep link to the regulator's original filing. Always populated for live-scraped rows. |
| raw_data | jsonb | yes | Source-system payload plus provenance flags added by our pipeline. Keys are listed under raw_data keys. |
| scraped_at | timestamptz | yes | When the scraper first hit the source page. |
| current_price_eur | numeric | yes | Most recent close, refreshed nightly for active tickers. |
| price_refreshed_at | timestamptz | yes | When current_price_eur was last refreshed. |
| ingested_at | timestamptz | no | When the row landed in our DB. Use this for ingestion-velocity / latency analysis. |
Dedup recommendation
A server-side enrichment-aware view is shipped: read from insidereu.filings_dedup rather than the base table. It collapses two enriched rows from different sources with matching (country, issuer-key, txn_date, insider, direction, shares, price) and signal-only rows whose enriched sibling exists in another source. It does not collapse two signal-only rows with no enriched sibling. Same schema, no extra columns. The public REST API and sample CSV exports already read from the view.
Awards, grants and other non-market transactions
Share-based awards, option grants and exercises, vestings and pledge releases are not mapped to buy or sell. Where the source labels them (FI, DK, GB) the row keeps transaction_type = 'unknown', with raw_data.non_market = true and the regulator's wording in raw_data.nature_raw. The Swedish and German feeds drop these natures at ingestion. In Finland more than half of PDMR notices are share-award receipts, which is why its buy/sell share is low. Some rows still carry price = 0 (for example French award notices); filter price > 0 to exclude them.
Swedish rows also carry raw_data.linked_to_programme = true when the regulator flags the transaction as linked to a share-option or incentive programme, so discretionary purchases can be separated from programme ones.
NULL ISIN handling
Across all rows ISIN coverage is about 93%, but that is driven by Sweden (99%), Switzerland (100%) and Germany (100%). Of the 26 countries with rows in the last 90 days, 11 have under 50% (measured 2026-09-21): NO, GR, HU, IT, BE, AU, LU, CY, MT, IS and SI. About 2,300 ISINs were filled by matching the company name to other rows of the same company in the same country when exactly one ISIN matched; those rows carry raw_data.isin_source = "company_name_propagation". When isin IS NULL, join on (country, company_name) against a reference universe.
raw_data keys
Beyond the source payload, our pipeline adds these keys. Absence of a flag means it did not apply or has not been evaluated for that row.
| non_market | true when the transaction is an award, grant, vesting, option exercise or pledge release, not an open-market trade. transaction_type stays unknown. |
| nature_raw | The regulator's own wording for the nature of the transaction, verbatim (FI, DK, GB, and Swedish rows ingested from 2026-09-21). |
| linked_to_programme | SE only: true when the regulator flags the transaction as linked to a share-option or incentive programme. |
| filing_date_is_trade_date | CH: SIX exposes no publication date, so filing_date equals transaction_date. |
| published_date | GB (Investegate): the RNS publication date shown in the announcement list, used as filing_date. |
| tx_date_from_publication | GB: the article body could not be parsed, so transaction_date is the publication date, not the trade date. |
| isin_source | Set to company_name_propagation when the ISIN was inferred from other rows of the same company. |
| signal_only | true when only a headline-level record exists (issuer, date, link) and no detail page or PDF has been parsed. |
| detail_enriched_at | When the detail page or PDF was parsed for this row. |
| figures_repaired | The stored price was the trade total entered as a unit price; corrected as total / shares. Holds the original figures. |
| figures_suspect | The price failed a plausibility check (more than 20 times the issuer's own median in a 180-day window, or an implausible unit) and price / value_eur were set to NULL. Holds the original figures. |
| figures_nulled | GB: shares, price and value were parsed from the wrong part of the announcement and were set to NULL. |
History windows
Earliest transaction date held per country, as of 2026-09-21. Where a regulator caps its own history, that is the limit; we cannot go further back.
| Country | Earliest | Rows | Limit |
|---|---|---|---|
| SE | 2021-01-01 | 66,898 | Walked in single-day windows; the 34 weekdays without rows are Swedish public holidays. |
| CH | 2023-09-21 | 6,708 | SIX keeps about three years. |
| GB | 2024-09 | 3,957 | Depends on the LSE and Investegate feeds; the daily job runs six times each weekday from 2026-09-21. |
| DE | 2025-04-15 | 3,640 | BaFin's portal holds nothing earlier. |
| FI | 2018-11 | 2,401 | Sparse before 2025; the Nasdaq news API returns roughly the latest 10,000 items. |
| AT | 2023-07 | 1,837 | Headline-level only. |
| NO, ES, FR, DK and others | 2025-12 to 2026-05 | under 1,800 each | Coverage began when each scraper was built; see the coverage matrix. |
Sample row
{
"id": "se-fi-example-person__example-ab-2026-05-16-4562-91246",
"country": "SE",
"insider_id": "example-person__example-ab",
"insider_name": "Example Person",
"insider_role": "Chief Executive Officer",
"company_name": "Example AB",
"isin": "SE0000000000",
"ticker": "EXMPL-B.ST",
"exchange": "Nasdaq Stockholm",
"transaction_date": "2026-05-16",
"filing_date": "2026-05-19",
"transaction_type": "buy",
"shares": 2000,
"price": 45.62,
"value_eur": 91246,
"currency": "EUR",
"source_url": "https://marknadssok.fi.se/Publiceringsklient/en-GB/Search/Search",
"raw_data": {
"original_currency": "SEK",
"original_price": 524.4,
"linked_to_programme": false,
"nature_raw": "Acquisition"
},
"scraped_at": "2026-05-20T02:30:35.068+00:00",
"ingested_at": "2026-05-20T02:30:35.068+00:00"
}Table: insidereu.substantial_holdings
Transparency Directive (Directive 2004/109/EC) major-holding notifications and national equivalents — DTR 5 in the UK, BaFin §33-§38a WpHG in Germany, AFM in NL, CNMV in ES. Triggered when any holder crosses 3% / 5% / 10% / 15% / 20% / 25% / 30% / 50% / 75% of voting rights in a listed issuer.
| Column | Type | Null | Meaning |
|---|---|---|---|
| id | text PK | no | Stable per-source key. |
| country | text | no | ISO 3166-1 alpha-2 of the regulator that received the notification. |
| filer_name | text | no | Notifying entity (institutional holder, individual, syndicate). |
| filer_id | text | no | Stable filer key within a country. |
| issuer_name | text | no | The company whose voting rights crossed a threshold. |
| issuer_isin | text | yes | ISIN. Coverage by country varies — see audit. |
| issuer_lei | text | yes | GLEIF LEI of the issuer, resolved from issuer_isin. Null where no ISIN or no GLEIF mapping. |
| issuer_ticker | text | yes | Yahoo Finance ticker (derived). |
| filing_date | date | no | Date the regulator published the notification. |
| event_date | date | yes | Date the threshold was crossed. Often 1-3 business days before filing_date. ES currently inverted (audit § 4a). |
| threshold_pct | numeric | yes | Threshold crossed, in percent. Currently 0% populated except NO (17%). Full population on the roadmap. |
| direction | text | no | CHECK IN ('crossed_above','crossed_below','disclosed'). ~99% of rows are disclosed today. Reliable directional values only for CH (98%) and NO (1.9%). |
| new_pct | numeric | yes | Filer's voting rights percentage after the event. |
| previous_pct | numeric | yes | Filer's voting rights percentage before the event. |
| voting_rights_count | bigint | yes | Absolute count of voting rights held after the event. |
| regulation | text | yes | The exact regulatory citation — e.g. Section 33 WpHG, DTR 5.1.2 R, AFM 5:38 Wft. |
| source_url | text | yes | Deep link to the original notification. |
| raw_data | jsonb | yes | Verbatim source payload. |
| created_at | timestamptz | no | Equivalent to filings.scraped_at. |
| ingested_at | timestamptz | no | When the row landed in our DB. Use for ingestion-velocity analysis. |
Dedup
Natural key: (country, filer_id, issuer_isin OR issuer_name, event_date, threshold_pct, direction). The id PK dedupes within a single source. No multi-source overlap observed today (each regulator publishes exactly once).
direction = 'disclosed'
Three regulators (FSMA-BE, AMF-FR, AFM-NL) publish “current holdings disclosure” notices that don't state whether the filer crossed up or down — they only restate the new position. Those rows carry direction = 'disclosed'. For strict directional whale events, filter direction <> 'disclosed'.
Sample row
{
"id": "DE-BAFIN-2026-05-19-77881",
"country": "DE",
"filer_name": "BlackRock, Inc.",
"filer_id": "DE-BAFIN-FILER-BLACKROCK",
"issuer_name": "Volkswagen AG",
"issuer_isin": "DE0007664039",
"issuer_ticker": "VOW3.DE",
"filing_date": "2026-05-19",
"event_date": "2026-05-15",
"threshold_pct": null,
"direction": "disclosed",
"new_pct": 5.12,
"previous_pct": 4.98,
"voting_rights_count": 15125400,
"regulation": "Section 33 WpHG",
"source_url": "https://www.bafin.de/.../77881.pdf",
"ingested_at": "2026-05-20T02:31:13.314+00:00"
}Known caveats — read before integrating
transaction_typeis ternary.'unknown'flags rows where the source did not carry parseable direction (signal-only). Filtertransaction_type IN ('buy','sell')for direction-confirmed rows only. Fully reliable buy/sell mix today: CH, DE, SE; smaller reliable tails: DK, FI, GB, IT, NO, ES.filing_dateis a real publication date only for some countries. The regulator's publication date is used for SE, DE, FI and DK, and for UK rows from Investegate ingested from 2026-09-21. For CH it equals the transaction date (SIX publishes none; flaggedraw_data.filing_date_is_trade_date). In the last 90 days it equals the transaction date for nearly every other country, and for UK rows before 2026-09-21 it is the transaction or ingestion date. A backtest that enters atfiling_datewill look ahead for those rows.ingested_atis reliable for “when we knew about it” on live-ingested rows only; rows loaded in a historical backfill carry the backfill date.threshold_pctis sparsely populated in holdings. Until full backfill, the most reliable holdings signal is the changenew_pct - previous_pct.- ISIN coverage is non-uniform. Tier 1-2 countries are >99%; Tier 4 countries can be 0%. See per-country detail in the coverage matrix →
- Dedup is applied server-side via
insidereu.filings_dedup. Read from the view, not the base table. current_price_euris refreshed nightly for tickers with confirmed Yahoo Finance mapping. Coverage trackstickercoverage.- Non-market transactions (awards, grants, vests, pledge releases) are not mapped to buy or sell; see the section above. Filter
price > 0to drop zero-price rows. - Swiss insiders are not individuals. SIX publishes a role code (board, executive, closely associated), not a name, so CH
insider_nameisBoard Member (Issuer)andinsider_idis per role and issuer. Counting distinct insiders (for example cluster detection) is not possible for CH; issuer-level analysis is. - Some source figures are wrong at the regulator and are corrected or blanked. Where a trade total was entered as a per-share price (for example a price 1.7 million times the issuer's normal level) the row is repaired if total / shares matches the issuer's usual price, otherwise
priceandvalue_eurare set to NULL and the original is kept inraw_data.figures_repairedorraw_data.figures_suspect. Around 190 Swedish, Swiss and other rows were handled this way on 2026-09-21. The check compares each price with the same issuer's trades within 180 days, so a few wrong figures on thinly traded issuers can remain. A handful of very large values (above EUR 1 billion) are genuine corporate actions, such as takeovers; treat single trades above EUR 100 million with care. value_euruses fixed FX.See the column definition. Do not use it as an exact P&L figure.- Programme purchases. Most feeds do not separate discretionary purchases from share-scheme or incentive-programme ones. Only Sweden flags this (
raw_data.linked_to_programme), and only from the data re-pulled on 2026-09-21. A “cluster” of managers buying may be several of them joining one programme. - History depth differs sharply by country. See the history windows table. Only Sweden has more than three years.
insidereu.pricesis an internal cache of daily closes used for our own analysis and is not part of the data product; prices are not redistributed.
API access
Filings and holdings are available via three delivery surfaces:
- REST:
https://insidereu.com/api/v1/filings?country=DE&since=2026-05-01 - CSV / Parquet drop: nightly to S3-compatible bucket (set up on request, pilot tier and above).
- Postgres read replica: enterprise tier.
API keys are provisioned per-seat via insidereu.api_keys with hashed key storage.