Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1359.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**`Form4.shares_traded` raised `AttributeError` on a derivative-only filing.** `market_trades` is None when the non-derivative table has no transactions, and the accessor read `.Shares` off it without the None/empty guard its neighbours already had: Vertex's 2020-10-19 Form 4 parsed and returned its derivative transaction but crashed on `shares_traded`, which now reports 0 market trades. (GH #1359)
107 changes: 107 additions & 0 deletions data/ownership/VertexForm4.derivative-only.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?xml version="1.0"?>
<ownershipDocument>

<schemaVersion>X0306</schemaVersion>

<documentType>4</documentType>

<periodOfReport>2020-10-15</periodOfReport>

<notSubjectToSection16>0</notSubjectToSection16>

<issuer>
<issuerCik>0000875320</issuerCik>
<issuerName>VERTEX PHARMACEUTICALS INC / MA</issuerName>
<issuerTradingSymbol>VRTX</issuerTradingSymbol>
</issuer>

<reportingOwner>
<reportingOwnerId>
<rptOwnerCik>0001197036</rptOwnerCik>
<rptOwnerName>SACHS BRUCE I</rptOwnerName>
</reportingOwnerId>
<reportingOwnerAddress>
<rptOwnerStreet1>C/O VERTEX PHARMACEUTICALS INCORPORATED</rptOwnerStreet1>
<rptOwnerStreet2>50 NORTHERN AVENUE</rptOwnerStreet2>
<rptOwnerCity>BOSTON</rptOwnerCity>
<rptOwnerState>MA</rptOwnerState>
<rptOwnerZipCode>02210</rptOwnerZipCode>
<rptOwnerStateDescription></rptOwnerStateDescription>
</reportingOwnerAddress>
<reportingOwnerRelationship>
<isDirector>1</isDirector>
<isOfficer>0</isOfficer>
<isTenPercentOwner>0</isTenPercentOwner>
<isOther>0</isOther>
</reportingOwnerRelationship>
</reportingOwner>

<derivativeTable>
<derivativeTransaction>
<securityTitle>
<value>Deferred Stock Units</value>
</securityTitle>
<conversionOrExercisePrice>
<footnoteId id="F1"/>
</conversionOrExercisePrice>
<transactionDate>
<value>2020-10-15</value>
</transactionDate>
<deemedExecutionDate></deemedExecutionDate>
<transactionCoding>
<transactionFormType>4</transactionFormType>
<transactionCode>A</transactionCode>
<equitySwapInvolved>0</equitySwapInvolved>
</transactionCoding>
<transactionTimeliness>
<value></value>
</transactionTimeliness>
<transactionAmounts>
<transactionShares>
<value>196.356</value>
</transactionShares>
<transactionPricePerShare>
<value>222.81</value>
</transactionPricePerShare>
<transactionAcquiredDisposedCode>
<value>A</value>
</transactionAcquiredDisposedCode>
</transactionAmounts>
<exerciseDate>
<footnoteId id="F1"/>
</exerciseDate>
<expirationDate>
<footnoteId id="F1"/>
</expirationDate>
<underlyingSecurity>
<underlyingSecurityTitle>
<value>Common Stock</value>
</underlyingSecurityTitle>
<underlyingSecurityShares>
<value>196.356</value>
</underlyingSecurityShares>
</underlyingSecurity>
<postTransactionAmounts>
<sharesOwnedFollowingTransaction>
<value>12629.042</value>
</sharesOwnedFollowingTransaction>
</postTransactionAmounts>
<ownershipNature>
<directOrIndirectOwnership>
<value>D</value>
</directOrIndirectOwnership>
</ownershipNature>
</derivativeTransaction>
</derivativeTable>

<footnotes>
<footnote id="F1">Each deferred stock unit represents one share of common stock and is paid out in common stock upon the earliest to occur of (i) termination of Mr. Sachs' service on our board of directors, (ii) a change of control of our company and (iii) Mr. Sachs' disability or death.</footnote>
</footnotes>

<remarks></remarks>

<ownerSignature>
<signatureName>/s/ Sabrina Yohai, Attorney-in-Fact</signatureName>
<signatureDate>2020-10-19</signatureDate>
</ownerSignature>
</ownershipDocument>
18 changes: 16 additions & 2 deletions edgar/ownership/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,23 @@ def derivative_trades(self):

@cached_property
def shares_traded(self):
"""Total shares in the non-derivative open-market (P/S) trades.

``0`` means this filing reported no such trades. It does not mean the
filing is empty: a derivative-only Form 4 reports its activity through
``derivative_table`` and still has nothing to count here (GH #1359).
"""
trades = self.market_trades
# `market_trades` is None when the non-derivative table has no
# transactions at all, and empty when none of them is coded P or S.
# Every other caller of it guards both states; this one read `.Shares`
# straight off it and raised AttributeError on a derivative-only
# filing, which parses fine and reports its transaction elsewhere.
if trades is None or trades.empty:
return 0
# Sum the Shares if Shares is all numeric
if np.issubdtype(self.market_trades.Shares.dtype, np.number):
return self.market_trades.Shares.sum()
if np.issubdtype(trades.Shares.dtype, np.number):
return trades.Shares.sum()

@classmethod
def from_xml(cls,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""
Regression test for GitHub issue #1359: `shares_traded` on a derivative-only Form 4.

Vertex Pharmaceuticals Form 4 filed 2020-10-19 (accession 0001209191-20-055264)
reports one derivative transaction and no non-derivative table at all, so
`NonDerivativeTable.market_trades` is None. `Ownership.shares_traded` read
`.Shares` straight off it and raised `AttributeError: 'NoneType' object has no
attribute 'Shares'`, even though the filing parsed and `to_dataframe()` returned
the derivative transaction. Every neighbouring accessor already guarded None.

GitHub Issue: https://github.com/dgunning/edgartools/issues/1359
"""
from pathlib import Path

import pandas as pd
import pytest

import edgar.ownership.owners as owners_module
from edgar.ownership.forms import Form4

# Vertex Pharmaceuticals Form 4, filed 2020-10-19, accession 0001209191-20-055264.
# One derivative transaction (196.356 deferred stock units), no <nonDerivativeTable>.
DERIVATIVE_ONLY = Path('data/ownership/VertexForm4.derivative-only.xml')
# 374Water Form 4: a non-derivative award (code A) and a derivative award, so the
# non-derivative table has transactions but none of them is a P/S market trade.
AWARDS_ONLY = Path('data/ownership/374WaterForm4.xml')
# Snowflake Form 4: five non-derivative sales, 200,000 shares in total.
MARKET_TRADES = Path('data/form4.snow.xml')
SNOW_SHARES_TRADED = 200_000


@pytest.fixture
def offline_owners(monkeypatch):
"""Stub the reporting-owner CIK lookup, the only fetch in this parse path.

Same stub as `tests/ownership/test_insider_parse_contract.py`: it is what
keeps an `Ownership.parse_xml` assertion in the pull-request gate instead of
the post-merge network lane.
"""
class StubEntity:
def __init__(self, cik):
self.data = type("Data", (), {"is_company": False})()

monkeypatch.setattr(owners_module, "Entity", StubEntity)


def _form4(fixture: Path) -> Form4:
return Form4.parse_xml(fixture.read_text())


def test_derivative_only_filing_reports_zero_shares_traded(offline_owners):
"""The reported AttributeError. 0 is the count of P/S market trades, and the
derivative transaction has to remain reachable alongside it."""
form4 = _form4(DERIVATIVE_ONLY)

assert len(form4.non_derivative_table.transactions) == 0
assert len(form4.derivative_table.transactions) == 1
assert form4.market_trades is None

assert form4.shares_traded == 0

# 0 must not mean the filing was read as empty.
df = form4.to_dataframe()
assert len(df) == 1
assert df.iloc[0].Shares == 196.356


def test_award_only_filing_still_reports_zero_shares_traded(offline_owners):
"""Control for the empty-frame half of the guard: this table has
transactions, so `market_trades` is a frame, but no row is coded P or S."""
form4 = _form4(AWARDS_ONLY)

assert form4.non_derivative_table.has_transactions
assert form4.market_trades is not None
assert form4.market_trades.empty

assert form4.shares_traded == 0


def test_market_trades_are_still_summed(offline_owners):
"""Control for the path the guard must not shadow."""
form4 = _form4(MARKET_TRADES)

assert len(form4.market_trades) == 5
assert form4.shares_traded == SNOW_SHARES_TRADED


def test_nonnumeric_shares_are_not_reported_as_zero(offline_owners):
"""A nonempty frame whose Shares column is not numeric still falls through
to None. Returning 0 there would report "no shares traded" for a filing that
traded an amount we could not read, which is what the empty-frame branch
must not be allowed to shadow.

The column is forced to `object`, the dtype `np.issubdtype` is reached with
on pandas 2; nothing here depends on how pandas 3 infers a string column.
"""
form4 = _form4(MARKET_TRADES)
unreadable = form4.market_trades.copy()
unreadable['Shares'] = pd.Series(
['unknown'] * len(unreadable), index=unreadable.index, dtype=object)
assert not unreadable.empty
form4.__dict__['market_trades'] = unreadable

assert form4.shares_traded is None
Loading