Skip to content

fix(sec): correct Form 4 date filtering and per-row footnote attribution - #7642

Open
mateosandoval10 wants to merge 1 commit into
OpenBB-finance:developfrom
mateosandoval10:hotfix/sec-form4-date-filter-and-footnotes
Open

fix(sec): correct Form 4 date filtering and per-row footnote attribution#7642
mateosandoval10 wants to merge 1 commit into
OpenBB-finance:developfrom
mateosandoval10:hotfix/sec-form4-date-filter-and-footnotes

Conversation

@mateosandoval10

Copy link
Copy Markdown

Pull Request the OpenBB Platform

Description

Two independent correctness bugs in the SEC Form 4 path behind equity.ownership.insider_trading. Both are in openbb_platform/providers/sec/openbb_sec/utils/form4.py. No linked issue — found while reading the parser.

1. A one-sided date range returns no data

get_form_4_urls filtered with:

if (
    (not start_date or not item.filing_date)
    or start_date
    and item.filing_date < start_date
):
    continue

(not start_date or not item.filing_date) short-circuits to True whenever start_date is None, so the loop continues on every filing and returns an empty list. The end_date guard below it has the same shape.

This is normally hidden because SecInsiderTradingFetcher.transform_query backfills a 120-day window — but only when both bounds are missing:

if not start_date and not end_date:
    params["start_date"] = (datetime.now() - timedelta(days=120)).date()
    params["end_date"] = datetime.now().date()

So supplying exactly one bound leaves the other None, no URLs come back, and get_form_4 raises No Form 4 data was returned for {symbol}:

from openbb import obb
obb.equity.ownership.insider_trading(symbol="AAPL", start_date="2026-01-01", provider="sec")
# OpenBBError: No Form 4 data was returned for AAPL.
obb.equity.ownership.insider_trading(symbol="AAPL", end_date="2026-05-01", provider="sec")
# OpenBBError: No Form 4 data was returned for AAPL.

The guards now test the bound first, so an absent bound simply doesn't filter. The both-bounds path is unchanged.

2. Footnote text leaks from one transaction to the next

parse_form_4_data builds footnotes as an id → text map, then rebinds that same name to a joined string inside the per-transaction loop:

footnotes = ("; ".join([footnotes.get(fid, "") for fid in ids]) if isinstance(footnotes, dict) else footnotes)
new_row["footnote"] = footnotes

After the first row that carries footnote references, footnotes is no longer a dict. The isinstance(footnotes, str) branches downstream then hand that same string to every remaining row in the filing. A three-transaction Form 4 whose rows reference F1+F2, F3 and F4 returns:

row 1  ->  'Sale under a Rule 10b5-1 trading plan.; Weighted average price.'
row 2  ->  'Sale under a Rule 10b5-1 trading plan.; Weighted average price.'   # should be F3
row 3  ->  'Sale under a Rule 10b5-1 trading plan.; Weighted average price.'   # should be F4

Form 4 footnotes carry the qualification that makes a transaction readable — whether a sale was under a Rule 10b5-1 plan, whether shares were withheld for taxes, whether a position is held indirectly through a trust — so attaching the wrong one to a row is misleading rather than merely cosmetic.

Footnote resolution now goes through a small resolve_footnotes helper that reads the map without mutating it, replacing four near-duplicate inline blocks. Building the map also tolerates an empty <footnote id="F1"/> element, which previously raised KeyError: '#text'.

Notes for the reviewer

  • Both bugs are present in develop and survive verbatim into feature/v5-sec ([V5] openbb-sec: Refactor for V5 #7525), so this isn't superseded by the V5 refactor.
  • Two things I deliberately left alone: get_form_4_urls filters start_date against filing_date but end_date against report_date, and the derivative-table branch never resolves footnotes at all. Both look like separate decisions rather than defects — happy to follow up if you'd like either changed.

How has this been tested?

New openbb_platform/providers/sec/tests/test_form4.py, 12 tests, no network access:

  • get_form_4_urls across both bounds, start-only, end-only and neither, plus ISO-string bounds.
  • resolve_footnotes for a single reference, multiple references, an unknown id, a missing reference and an absent footnote map — asserting the map is not mutated.
  • parse_form_4_data end to end on a three-transaction ownership document, asserting each row keeps its own footnote, and on a document with an empty footnote element.
pytest openbb_platform/providers/sec/tests/test_form4.py -q
12 passed

Full provider suite, pytest openbb_platform/providers/sec/tests/ -q: 233 passed, 2 failed. The two failures (test_sec_sic_search_fetcher, test_sec_equity_ftd_fetcher) reproduce identically on a clean checkout of develop and are unrelated to this change.

ruff check and black --check clean on both files.

  • Ensure all unit and integration tests pass.

Checklist

  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have adhered to the GitFlow naming convention and my branch name is in the format of feature/feature-name or hotfix/hotfix-name.
  • I ensure that I am following the CONTRIBUTING guidelines.

Two independent correctness bugs in the SEC Form 4 path behind
`equity.ownership.insider_trading`.

1. `get_form_4_urls` discarded every filing when only one date bound was
   supplied. `(not start_date or not item.filing_date)` short-circuits to
   True whenever `start_date` is None, so the loop skipped all items.
   `SecInsiderTradingFetcher.transform_query` only backfills defaults when
   both bounds are missing, so a request with just `start_date` (or just
   `end_date`) reached the helper with one bound set to None and returned
   no URLs, surfacing as "No Form 4 data was returned for {symbol}".

2. `parse_form_4_data` rebound the `footnotes` id-to-text map to a joined
   string while iterating transactions, so after the first row carrying a
   footnote every later row in the same filing inherited that row's text.
   Footnote resolution now goes through `resolve_footnotes`, which reads
   the map without mutating it. Building the map also tolerates an empty
   `<footnote id="F1"/>` element, which previously raised KeyError.

Adds unit tests covering both bounds, each bound alone, neither bound, and
per-row footnote resolution. No network access required.
@CLAassistant

CLAassistant commented Aug 19, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants