Skip to content

fix(ownership): shares_traded counts zero market trades instead of raising (#1359) - #1367

Merged
dgunning merged 1 commit into
dgunning:mainfrom
manantlerio:fix/1359-shares-traded-derivative-only
Sep 28, 2026
Merged

dgunning merged 1 commit into
dgunning:mainfrom
manantlerio:fix/1359-shares-traded-derivative-only

Conversation

@manantlerio

Copy link
Copy Markdown
Contributor

Fixes #1359

The defect

NonDerivativeTable.market_trades has two "nothing to count" states: None when the non-derivative table has no transactions at all, and an empty frame when it has transactions but none of them is coded P or S. Every caller guards both - the summary path, common_stock_purchases, common_stock_sales, get_ownership_summary - except Ownership.shares_traded, which read .Shares straight off it.

So a derivative-only Form 4 parsed fine and returned its derivative transaction through to_dataframe(), then raised on the accessor:

AttributeError: 'NoneType' object has no attribute 'Shares'

Reproduced on a clean checkout of b022ad29 (5.59.1) through the public filing.obj() path, with the filing the reporter named: Vertex Pharmaceuticals Form 4 filed 2020-10-19, accession 0001209191-20-055264. It is already the fixture behind tests/ownership/test_form4.py::test_form4_with_derivatives.

The change

shares_traded returns 0 for both states. Two notes on the contract, since the number is easy to misread:

  • 0 counts non-derivative open-market trades only. It says nothing about derivative activity, which this accessor has never counted. That is now in the docstring rather than implied.
  • The nonempty non-numeric fall-through to None is deliberately untouched. A filing that traded an amount we cannot parse must not be reported as having traded nothing. A test pins that.

One behaviour change beyond the crash: an empty frame whose Shares column is not numeric fell through to None before and now returns 0. The award-only case already returned 0 by summing an empty numeric column, so that answer is unchanged.

Tests

tests/issues/regression/test_issue_1359_shares_traded_derivative_only.py, four tests: the reproduction, plus the three controls the issue asked for.

Filing Before After
Vertex 0001209191-20-055264, derivative-only AttributeError 0
374WaterForm4.xml, awards only (empty frame) 0 0
form4.snow.xml, five market trades 200000 200000
Nonempty non-numeric Shares None None

The reproduction test fails on an unfixed tree and the three controls pass on it, so the file pins the fix rather than the fixture.

It reads a checked-in copy of the Vertex XML (data/ownership/VertexForm4.derivative-only.xml, 4 KB) and stubs the reporting-owner CIK lookup, the only fetch in Ownership.parse_xml, using the same stub as tests/ownership/test_insider_parse_contract.py. Verified offline under pytest -p tests._offline_harness, so it lands in the fast pull-request gate rather than the post-merge network lane and needs no entry in REGRESSION_NETWORK_*.

The fixture needed git add -f: /data/ is in .gitignore while the fixtures inside it are tracked. Say the word if you would rather it lived elsewhere.

Verification

Python 3.14.5, pandas 3.0.6, Windows.

  • pytest tests/ownership plus the three ownership regression files: 89 passed, exit 0.
  • The new file: 4 passed; 1 failed, 3 passed with edgar/ownership/forms.py reverted.
  • pytest tests/issues/regression -m fast: 13 failures, and the failure set is byte-identical with and without this change. They are pre-existing in my environment and unrelated (test_issue_1325_import_ordering is Importing edgar.reference on one thread while another thread imports edgar deadlocks (_DeadlockError, ImportError) #1325 itself; test_filing_text_baseline and the dt1f1 files want fixtures my checkout does not have).
  • ruff check edgar: output byte-identical to upstream/main, so no new findings. Note the tree is already red under ruff 0.16.9 (1091 findings on an untouched checkout), which looks like drift from whatever CI pins. ruff format is not enforced, so formatting is left alone.
  • scripts/check_regression_provenance.py and scripts/check_regression_skips.py: both OK.
  • Changelog fragment changelog.d/1359.fixed.md, 402 characters.

One thing noticed, not fixed

On pandas 3, a genuinely non-numeric Shares column is inferred as StringDtype, and np.issubdtype raises TypeError: Cannot interpret '<StringDtype(na_value=nan)>' as a data type rather than falling through to None. That is a separate defect from this one and out of scope here, so the non-numeric test forces object dtype to assert the documented fall-through without depending on it. Happy to file it if it is not already tracked.

🤖 Generated with Claude Code

…ising (dgunning#1359)

`NonDerivativeTable.market_trades` returns None when the non-derivative table
has no transactions, and an empty frame when it has transactions but none of
them is coded P or S. Every caller of it guards both states - `market_trades`
itself in the summary path, `common_stock_purchases`, `common_stock_sales`,
`get_ownership_summary` - except `Ownership.shares_traded`, which read `.Shares`
straight off it.

So a derivative-only Form 4 parsed fine, returned its derivative transaction
through `to_dataframe()`, and then raised `AttributeError: 'NoneType' object has
no attribute 'Shares'` on the accessor. Vertex Pharmaceuticals' Form 4 filed
2020-10-19 (accession 0001209191-20-055264) is one, and it is already the
fixture behind `tests/ownership/test_form4.py::test_form4_with_derivatives`.

`shares_traded` now returns 0 for both states. 0 is the count of non-derivative
open-market trades and says nothing about derivative activity, which this
accessor has never counted - the docstring says so, because the number is easy
to misread as "nothing happened". The award-only case already returned 0 by
summing an empty numeric column, so that answer is unchanged; the empty branch
now also covers an empty frame whose Shares column is not numeric, which fell
through to None before.

The nonempty non-numeric fall-through to None is deliberately untouched: a
filing that traded an amount we cannot parse must not be reported as having
traded nothing. A regression test pins that alongside the two controls.

The test reads a checked-in copy of the Vertex XML and stubs the reporting-owner
CIK lookup, the only fetch in `Ownership.parse_xml`, using the same stub as
`tests/ownership/test_insider_parse_contract.py`. Verified offline under
`pytest -p tests._offline_harness`, so it gates pull requests rather than
landing in the post-merge network lane.

Fixes dgunning#1359

Co-Authored-By: Claude <noreply@anthropic.com>
@manantlerio
manantlerio force-pushed the fix/1359-shares-traded-derivative-only branch from 911696c to fb882e0 Compare September 26, 2026 16:43
@dgunning
dgunning merged commit 72afa79 into dgunning:main Sep 28, 2026
11 checks passed
monody0007 added a commit to monody0007/edgartools that referenced this pull request Sep 28, 2026
Resolve the conflict with dgunning#1367 in Ownership.shares_traded: keep its
None/empty guard and `trades` local, and keep this branch's
pd.api.types.is_numeric_dtype check in place of np.issubdtype.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

Form4.shares_traded raises AttributeError on derivative-only filings

2 participants