Skip to content

Commit 08aafc2

Browse files
committed
fix(sec): 13F parser - guard zero total_value, preserve XML entities
1 parent 3e071fc commit 08aafc2

1 file changed

Lines changed: 22 additions & 1 deletion

File tree

openbb_platform/providers/sec/openbb_sec/utils/parse_13f.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,21 @@ async def parse_13f_hr(filing: str):
134134
if filing.startswith("https://"):
135135
filing = await get_complete_submission(filing) # type: ignore
136136

137+
# A Complete Submission TXT file is SGML-wrapped and not well-formed XML.
138+
# Feeding it to the XML parser as-is triggers lxml's recover mode, which
139+
# silently drops character entities: "S&P500" becomes "SP500" and
140+
# "BABCOCK & WILCOX" loses its ampersand in nameOfIssuer/titleOfClass.
141+
# Extract the embedded well-formed <XML> blocks (form header + information
142+
# table) and reassemble them under a synthetic root so the strict XML path
143+
# is used and entities are preserved. Inputs that are already bare XML
144+
# (no <XML> wrapper) are passed through unchanged.
145+
import re as _re # noqa: PLC0415
146+
147+
_xml_blocks = _re.findall(r"<XML>(.*?)</XML>", filing, _re.DOTALL | _re.IGNORECASE)
148+
if _xml_blocks:
149+
_decl = _re.compile(r"<\?xml[^>]*\?>")
150+
filing = "<root>" + "".join(_decl.sub("", b) for b in _xml_blocks) + "</root>"
151+
137152
soup = BeautifulSoup(filing, "xml")
138153

139154
info_table = soup.find_all("informationTable")
@@ -221,7 +236,13 @@ async def parse_13f_hr(filing: str):
221236
df.drop(columns=col, inplace=True)
222237

223238
total_value = df.value.sum()
224-
df["weight"] = round(df.value.astype(float) / total_value, 6)
239+
# Guard against empty filings: managers with no reportable holdings file a
240+
# single placeholder row (value=0), making total_value 0. Without the guard,
241+
# 0/0 becomes NaN, which `.replace({nan: None})` below turns into None and
242+
# the required `weight: float` field then fails validation.
243+
df["weight"] = (
244+
round(df.value.astype(float) / total_value, 6) if total_value else 0.0
245+
)
225246

226247
return (
227248
df.reset_index()

0 commit comments

Comments
 (0)