[Feature] Add form_13f Endpoint to equity.ownership (#6122)

* add form_13f endpoint

* review things

* option_type validator

* black

* codespell

* add some comments to the code

* remove duplicated process

* static file

* remove warning

* static why no one updates you :-(

---------

Co-authored-by: Pratyush Shukla <ps4534@nyu.edu>
Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com>
This commit is contained in:
Danglewood
2024-03-05 06:21:38 -08:00
committed by GitHub
parent e7896a839e
commit 6efab3fd15
13 changed files with 5705 additions and 4 deletions

View File

@@ -0,0 +1,107 @@
"""From 13F-HR Standard Model."""
from datetime import date as dateType
from typing import Literal, Optional
from pydantic import Field, field_validator
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.descriptions import (
QUERY_DESCRIPTIONS,
)
class Form13FHRQueryParams(QueryParams):
"""Form 13F-HR Query."""
__validator_dict__ = {"check_single": ("symbol")}
symbol: str = Field(
description=QUERY_DESCRIPTIONS.get("symbol", "")
+ " A CIK or Symbol can be used."
)
date: Optional[dateType] = Field(
default=None,
description=QUERY_DESCRIPTIONS.get("date", "")
+ " The date represents the end of the reporting period."
+ " All form 13F-HR filings are based on the calendar year"
+ " and are reported quarterly."
+ " If a date is not supplied, the most recent filing is returned."
+ " Submissions beginning 2013-06-30 are supported.",
)
limit: Optional[int] = Field(
default=1,
description=QUERY_DESCRIPTIONS.get("limit", "")
+ " The number of previous filings to return."
+ " The date parameter takes priority over this parameter.",
)
@field_validator("symbol", mode="before", check_fields=False)
@classmethod
def upper_symbol(cls, v: str):
"""Convert symbol to uppercase."""
return str(v).upper()
class Form13FHRData(Data):
"""
Form 13F-HR Data.
Detailed documentation of the filing can be found here:
https://www.sec.gov/pdf/form13f.pdf
"""
period_ending: dateType = Field(
description="The end-of-quarter date of the filing."
)
issuer: str = Field(description="The name of the issuer.")
cusip: str = Field(description="The CUSIP of the security.")
asset_class: str = Field(
description="The title of the asset class for the security."
)
security_type: Optional[Literal["SH", "PRN"]] = Field(
default=None,
description="The total number of shares of the class of security"
+ " or the principal amount of such class."
+ " 'SH' for shares. 'PRN' for principal amount."
+ " Convertible debt securities are reported as 'PRN'.",
)
option_type: Optional[Literal["call", "put"]] = Field(
default=None,
description="Defined when the holdings being reported are put or call options."
+ " Only long positions are reported.",
)
voting_authority_sole: Optional[int] = Field(
default=None,
description="The number of shares for which the Manager"
+ " exercises sole voting authority (none).",
)
voting_authority_shared: Optional[int] = Field(
default=None,
description="The number of shares for which the Manager"
+ " exercises a defined shared voting authority (none).",
)
voting_authority_other: Optional[int] = Field(
default=None,
description="The number of shares for which the Manager"
+ " exercises other shared voting authority (none).",
)
principal_amount: int = Field(
description="The total number of shares of the class of security"
+ " or the principal amount of such class."
+ " Only long positions are reported"
)
value: int = Field(
description="The fair market value of the holding of the particular class of security."
+ " The value reported for options is the fair market value of the underlying security"
+ " with respect to the number of shares controlled."
+ " Values are rounded to the nearest US dollar"
+ " and use the closing price of the last trading day of the calendar year or quarter.",
)
@field_validator("option_type", mode="before", check_fields=False)
@classmethod
def validate_option_type(cls, v: str):
"""Validate and convert to lower case."""
return v.lower() if v else None

View File

@@ -1775,3 +1775,27 @@ def test_equity_fundamental_reported_financials(params, headers):
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@parametrize(
"params",
[
(
{
"symbol": "NVDA",
"date": None,
"limit": 1,
"provider": "sec",
}
),
],
)
@pytest.mark.integration
def test_equity_ownership_form_13f(params, headers):
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/equity/ownership/form_13f?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200

View File

@@ -1664,3 +1664,26 @@ def test_equity_fundamental_reported_financials(params, obb):
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
@parametrize(
"params",
[
(
{
"symbol": "NVDA",
"date": None,
"limit": 1,
"provider": "sec",
}
),
],
)
@pytest.mark.integration
def test_equity_ownership_form_13f(params, obb):
params = {p: v for p, v in params.items() if v}
result = obb.equity.ownership.form_13f(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0

View File

@@ -57,3 +57,34 @@ async def share_statistics(
) -> OBBject:
"""Get data about share float for a given company."""
return await OBBject.from_query(Query(**locals()))
@router.command(
model="Form13FHR",
exclude_auto_examples=True,
examples=[
"### Enter the symbol as either the stock ticker or the CIK number as a string. ###",
'obb.equity.ownership.form_13f(symbol="NVDA").to_df()',
"### Enter a date (calendar quarter ending) for a specific report. ###",
'obb.equity.ownership.form_13f(symbol="BRK-A", date="2016-09-30")',
"### Use the `limit` parameter to return N number of reports from the most recent. ###",
"### Example finding Michael Burry's filings. ###",
'cik = obb.regulators.sec.institutions_search("Scion Asset Management").results[0].cik',
"obb.equity.ownership.form_13f(cik, limit=2).to_df()",
],
)
async def form_13f(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""
The Securities and Exchange Commission's (SEC) Form 13F is a quarterly report
that is required to be filed by all institutional investment managers with at least
$100 million in assets under management.
Managers are required to file Form 13F within 45 days after the last day of the calendar quarter.
Most funds wait until the end of this period in order to conceal
their investment strategy from competitors and the public.
"""
return await OBBject.from_query(Query(**locals()))

View File

@@ -82,6 +82,7 @@
"equity_fundamental_transcript": "/equity/fundamental/transcript",
"equity_market_snapshots": "/equity/market_snapshots",
"equity_ownership": "/equity/ownership",
"equity_ownership_form_13f": "/equity/ownership/form_13f",
"equity_ownership_insider_trading": "/equity/ownership/insider_trading",
"equity_ownership_institutional": "/equity/ownership/institutional",
"equity_ownership_major_holders": "/equity/ownership/major_holders",

View File

@@ -157,7 +157,7 @@ class ROUTER_economy_gdp(Container):
The provider to use for the query, by default None.
If None, the provider specified in defaults is selected or 'oecd' if there is
no default.
country : Literal['australia', 'austria', 'belgium', 'brazil', 'canada', 'chile', 'colombia', 'costa_rica', 'czech_republic', 'denmark', 'estonia', 'euro_area', 'european_union', 'finland', 'france', 'germany', 'greece', 'hungary', 'iceland', 'indonesia', 'ireland', 'israel', 'italy', 'japan', 'korea', 'latvia', 'lithuania', 'luxembourg', 'mexico', 'netherlands', 'new_zealand', 'norway', 'poland', 'portugal', 'russia', 'slovak_republic', 'slovenia', 'south_africa', 'spain', 'sweden', 'switzerland', 'turkey', 'united_kingdom', 'united_states']
country : Literal['australia', 'austria', 'belgium', 'brazil', 'canada', 'chile', 'colombia', 'costa_rica', 'czech_republic', 'denmark', 'estonia', 'euro_area', 'european_union', 'finland', 'france', 'germany', 'greece', 'hungary', 'iceland', 'indonesia', 'ireland', 'israel', 'italy', 'japan', 'korea', 'latvia', 'lithuania', 'luxembourg', 'mexico', 'netherlands', 'new_zealand', 'norway', 'poland', 'portugal', 'russia', 'slovak_republic', 'slovenia', 'south_africa', 'spain', 'sweden', 'switzerland', 'turkey', 'united_kingdom', 'united_states', 'all']
Country to get GDP for. (provider: oecd)
Returns
@@ -245,7 +245,7 @@ class ROUTER_economy_gdp(Container):
The provider to use for the query, by default None.
If None, the provider specified in defaults is selected or 'oecd' if there is
no default.
country : Literal['G20', 'G7', 'argentina', 'australia', 'austria', 'belgium', 'brazil', 'bulgaria', 'canada', 'chile', 'china', 'colombia', 'costa_rica', 'croatia', 'czech_republic', 'denmark', 'estonia', 'euro_area_19', 'europe', 'european_union_27', 'finland', 'france', 'germany', 'greece', 'hungary', 'iceland', 'india', 'indonesia', 'ireland', 'israel', 'italy', 'japan', 'korea', 'latvia', 'lithuania', 'luxembourg', 'mexico', 'netherlands', 'new_zealand', 'norway', 'oecd_total', 'poland', 'portugal', 'romania', 'russia', 'saudi_arabia', 'slovak_republic', 'slovenia', 'south_africa', 'spain', 'sweden', 'switzerland', 'turkey', 'united_kingdom', 'united_states']
country : Literal['G20', 'G7', 'argentina', 'australia', 'austria', 'belgium', 'brazil', 'bulgaria', 'canada', 'chile', 'china', 'colombia', 'costa_rica', 'croatia', 'czech_republic', 'denmark', 'estonia', 'euro_area_19', 'europe', 'european_union_27', 'finland', 'france', 'germany', 'greece', 'hungary', 'iceland', 'india', 'indonesia', 'ireland', 'israel', 'italy', 'japan', 'korea', 'latvia', 'lithuania', 'luxembourg', 'mexico', 'netherlands', 'new_zealand', 'norway', 'oecd_total', 'poland', 'portugal', 'romania', 'russia', 'saudi_arabia', 'slovak_republic', 'slovenia', 'south_africa', 'spain', 'sweden', 'switzerland', 'turkey', 'united_kingdom', 'united_states', 'all']
Country to get GDP for. (provider: oecd)
Returns

View File

@@ -13,6 +13,7 @@ from typing_extensions import Annotated
class ROUTER_equity_ownership(Container):
"""/equity/ownership
form_13f
insider_trading
institutional
major_holders
@@ -22,6 +23,125 @@ class ROUTER_equity_ownership(Container):
def __repr__(self) -> str:
return self.__doc__ or ""
@exception_handler
@validate
def form_13f(
self,
symbol: Annotated[
str,
OpenBBCustomParameter(
description="Symbol to get data for. A CIK or Symbol can be used."
),
],
date: Annotated[
Union[datetime.date, None, str],
OpenBBCustomParameter(
description="A specific date to get data for. The date represents the end of the reporting period. All form 13F-HR filings are based on the calendar year and are reported quarterly. If a date is not supplied, the most recent filing is returned. Submissions beginning 2013-06-30 are supported."
),
] = None,
limit: Annotated[
Optional[int],
OpenBBCustomParameter(
description="The number of data entries to return. The number of previous filings to return. The date parameter takes priority over this parameter."
),
] = 1,
provider: Optional[Literal["sec"]] = None,
**kwargs
) -> OBBject:
"""The Securities and Exchange Commission's (SEC) Form 13F is a quarterly report
that is required to be filed by all institutional investment managers with at least
$100 million in assets under management.
Managers are required to file Form 13F within 45 days after the last day of the calendar quarter.
Most funds wait until the end of this period in order to conceal
their investment strategy from competitors and the public.
Parameters
----------
symbol : str
Symbol to get data for. A CIK or Symbol can be used.
date : Union[datetime.date, None, str]
A specific date to get data for. The date represents the end of the reporting period. All form 13F-HR filings are based on the calendar year and are reported quarterly. If a date is not supplied, the most recent filing is returned. Submissions beginning 2013-06-30 are supported.
limit : Optional[int]
The number of data entries to return. The number of previous filings to return. The date parameter takes priority over this parameter.
provider : Optional[Literal['sec']]
The provider to use for the query, by default None.
If None, the provider specified in defaults is selected or 'sec' if there is
no default.
Returns
-------
OBBject
results : List[Form13FHR]
Serializable results.
provider : Optional[Literal['sec']]
Provider name.
warnings : Optional[List[Warning_]]
List of warnings.
chart : Optional[Chart]
Chart object.
extra : Dict[str, Any]
Extra info.
Form13FHR
---------
period_ending : date
The end-of-quarter date of the filing.
issuer : str
The name of the issuer.
cusip : str
The CUSIP of the security.
asset_class : str
The title of the asset class for the security.
security_type : Optional[Literal['SH', 'PRN']]
The total number of shares of the class of security or the principal amount of such class. 'SH' for shares. 'PRN' for principal amount. Convertible debt securities are reported as 'PRN'.
option_type : Optional[Literal['call', 'put']]
Defined when the holdings being reported are put or call options. Only long positions are reported.
voting_authority_sole : Optional[int]
The number of shares for which the Manager exercises sole voting authority (none).
voting_authority_shared : Optional[int]
The number of shares for which the Manager exercises a defined shared voting authority (none).
voting_authority_other : Optional[int]
The number of shares for which the Manager exercises other shared voting authority (none).
principal_amount : int
The total number of shares of the class of security or the principal amount of such class. Only long positions are reported
value : int
The fair market value of the holding of the particular class of security. The value reported for options is the fair market value of the underlying security with respect to the number of shares controlled. Values are rounded to the nearest US dollar and use the closing price of the last trading day of the calendar year or quarter.
weight : Optional[float]
The weight of the security relative to the market value of all securities in the filing , as a normalized percent. (provider: sec)
Example
-------
>>> from openbb import obb
>>> ### Enter the symbol as either the stock ticker or the CIK number as a string. ###
>>> obb.equity.ownership.form_13f(symbol="NVDA").to_df()
>>> ### Enter a date (calendar quarter ending) for a specific report. ###
>>> obb.equity.ownership.form_13f(symbol="BRK-A", date="2016-09-30")
>>> ### Use the `limit` parameter to return N number of reports from the most recent. ###
>>> ### Example finding Michael Burry's filings. ###
>>> cik = obb.regulators.sec.institutions_search("Scion Asset Management").results[0].cik
>>> obb.equity.ownership.form_13f(cik, limit=2).to_df()
""" # noqa: E501
return self._run(
"/equity/ownership/form_13f",
**filter_inputs(
provider_choices={
"provider": self._get_provider(
provider,
"/equity/ownership/form_13f",
("sec",),
)
},
standard_params={
"symbol": symbol,
"date": date,
"limit": limit,
},
extra_params=kwargs,
)
)
@exception_handler
@validate
def insider_trading(

View File

@@ -290,7 +290,7 @@ class ROUTER_etf(Container):
no default.
date : Optional[Union[str, datetime.date]]
A specific date to get data for. Entering a date will attempt to return the NPORT-P filing for the entered date. This needs to be _exactly_ the date of the filing. Use the holdings_date command/endpoint to find available filing dates for the ETF. (provider: fmp);
A specific date to get data for. The date represents the period ending. The date entered will return the closest filing. (provider: sec)
A specific date to get data for. The date represents the period ending. The date entered will return the closest filing. (provider: sec)
cik : Optional[str]
The CIK of the filing entity. Overrides symbol. (provider: fmp)
use_cache : bool
@@ -382,7 +382,7 @@ class ROUTER_etf(Container):
in_arrears : Optional[str]
If the debt security is in arrears. (provider: sec)
is_paid_kind : Optional[str]
If the debt security payments are are paid in kind. (provider: sec)
If the debt security payments are paid in kind. (provider: sec)
derivative_category : Optional[str]
The derivative category of the holding. (provider: sec)
counterparty : Optional[str]

View File

@@ -6,6 +6,7 @@ from openbb_sec.models.company_filings import SecCompanyFilingsFetcher
from openbb_sec.models.equity_ftd import SecEquityFtdFetcher
from openbb_sec.models.equity_search import SecEquitySearchFetcher
from openbb_sec.models.etf_holdings import SecEtfHoldingsFetcher
from openbb_sec.models.form_13FHR import SecForm13FHRFetcher
from openbb_sec.models.institutions_search import SecInstitutionsSearchFetcher
from openbb_sec.models.rss_litigation import SecRssLitigationFetcher
from openbb_sec.models.schema_files import SecSchemaFilesFetcher
@@ -24,6 +25,7 @@ sec_provider = Provider(
"EquitySearch": SecEquitySearchFetcher,
"EtfHoldings": SecEtfHoldingsFetcher,
"Filings": SecCompanyFilingsFetcher,
"Form13FHR": SecForm13FHRFetcher,
"InstitutionsSearch": SecInstitutionsSearchFetcher,
"RssLitigation": SecRssLitigationFetcher,
"SchemaFiles": SecSchemaFilesFetcher,

View File

@@ -0,0 +1,92 @@
"""SEC Form 13F-HR Model."""
# pylint: disable =unused-argument
import asyncio
from typing import Any, Dict, List, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.form_13FHR import (
Form13FHRData,
Form13FHRQueryParams,
)
from openbb_sec.utils import parse_13f
from pydantic import Field
class SecForm13FHRQueryParams(Form13FHRQueryParams):
"""SEC Form 13F-HR Query Params.
Source: https://www.sec.gov/Archives/edgar/data/
"""
class SecForm13FHRData(Form13FHRData):
"""SEC Form 13F-HR Data."""
__alias_dict__ = {
"issuer": "nameOfIssuer",
"asset_class": "titleOfClass",
"option_type": "putCall",
}
weight: float = Field(
description="The weight of the security relative to the market value of all securities in the filing"
+ " , as a normalized percent.",
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
)
class SecForm13FHRFetcher(Fetcher[SecForm13FHRQueryParams, List[SecForm13FHRData]]):
"""SEC Form 13F-HR Fetcher."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> SecForm13FHRQueryParams:
"""Transform the query."""
return SecForm13FHRQueryParams(**params)
@staticmethod
async def aextract_data(
query: SecForm13FHRQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the SEC endpoint."""
symbol = query.symbol
urls = []
cik = symbol.isnumeric()
filings = (
parse_13f.get_13f_candidates(symbol=symbol)
if cik is False
else parse_13f.get_13f_candidates(cik=symbol)
)
if query.limit and query.date is None:
urls = filings.iloc[: query.limit].to_list()
if query.date is not None:
date = parse_13f.date_to_quarter_end(query.date.strftime("%Y-%m-%d"))
urls = [filings.loc[date]]
results = []
async def get_filing(url):
"""Get a single 13F-HR filing and parse it."""
data = await parse_13f.parse_13f_hr(url)
if len(data) > 0:
results.extend(data.to_dict("records"))
await asyncio.gather(*[get_filing(url) for url in urls])
return sorted(
results, key=lambda d: [d["period_ending"], d["weight"]], reverse=True
)
@staticmethod
def transform_data(
query: SecForm13FHRQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[SecForm13FHRData]:
"""Transform the data."""
return [SecForm13FHRData.model_validate(d) for d in data]

View File

@@ -0,0 +1,209 @@
"""Utility functions for parsing SEC Form 13F-HR."""
from typing import Dict, Optional
import xmltodict
from bs4 import BeautifulSoup
from openbb_core.provider.utils.helpers import amake_request
from openbb_sec.models.company_filings import SecCompanyFilingsFetcher
from openbb_sec.utils.definitions import HEADERS
from pandas import DataFrame, offsets, to_datetime
def date_to_quarter_end(date: str) -> str:
"""Convert a date to the end of the calendar quarter."""
return (
(to_datetime(date).to_period("Q").to_timestamp("D") + offsets.QuarterEnd())
.date()
.strftime("%Y-%m-%d")
)
def get_13f_candidates(symbol: Optional[str] = None, cik: Optional[str] = None):
"""Get the 13F-HR filings for a given symbol or CIK."""
fetcher = SecCompanyFilingsFetcher()
params = {}
if cik is not None:
params["cik"] = str(cik)
if symbol is not None:
params["symbol"] = symbol
if cik is None and symbol is None:
raise ValueError("Either symbol or cik must be provided.")
params["use_cache"] = False
params["form_type"] = "13F-HR"
query = fetcher.transform_query(params)
filings = fetcher.extract_data(query, {})
if len(filings) == 0:
raise ValueError(f"No 13F-HR filings found for {symbol if symbol else cik}.")
# Filings before June 30, 2013 are non-structured and are not supported by downstream parsers.
return (
DataFrame(data=filings)
.query("`reportDate` >= '2013-06-30'")
.set_index("reportDate")["completeSubmissionUrl"]
)
async def complete_submission_callback(response, _):
"""Callback function for processing the response object."""
if response.status == 200:
return await response.text()
raise RuntimeError(f"Request failed with status code {response.status}")
async def get_complete_submission(url: str):
"""Get the Complete Submission TXT file string from the SEC API."""
return await amake_request(
url, headers=HEADERS, response_callback=complete_submission_callback
)
def parse_header(filing_str: str) -> Dict:
"""Parse the header of a Complete Submission TXT file string."""
header_dict = {}
try:
soup = BeautifulSoup(filing_str, "lxml-xml")
header_xml = soup.find("headerData")
header_dict = xmltodict.parse(str(header_xml))["headerData"]
except KeyError:
soup = BeautifulSoup(filing_str, features="lxml")
header_xml = soup.find("type")
header_dict = xmltodict.parse(str(header_xml)).get("type")
if header_dict:
return header_dict # type: ignore
raise ValueError(
"Failed to parse the form header."
+ " Check the `filing_str` to for the tag, 'headerData'."
)
def get_submission_type(filing_str: str):
"""Get the submission type of a Complete Submission TXT file string."""
header = parse_header(filing_str)
if header:
try:
form_type = header["submissionType"]
return form_type
except KeyError:
form_type = header["#text"]
return form_type
raise ValueError(
"Failed to get the submission type from the form header."
+ " Check the response from `parse_header`."
)
def get_period_ending(filing_str: str):
"""Get the report date from a Complete Submission TXT file string."""
header = parse_header(filing_str)
if header.get("filerInfo"):
return header["filerInfo"].get("periodOfReport")
raise ValueError(
"Failed to get the period of report from the form header."
+ " Check the response from `parse_header`."
)
async def parse_13f_hr(filing: str):
"""Parses a 13F-HR filing from the Complete Submission TXT file string."""
data = DataFrame()
# Check if the input string is a URL
if filing.startswith("https://"):
filing = await get_complete_submission(filing) # type: ignore
# Validate the submission so we know that we can parse it.
if get_submission_type(filing) not in ("13F-HR", "13F-HR/A"):
raise ValueError("Submission type is not 13F-HR.")
soup = BeautifulSoup(filing, "lxml-xml")
info_table = soup.find_all("informationTable")
if info_table == []:
info_table = soup.find_all("table")[-1]
parsed_xml = xmltodict.parse(
str(info_table[0]).replace("ns1:", "").replace("n1:", "")
)["informationTable"]["infoTable"]
if parsed_xml is None:
raise ValueError(
"Failed to parse the 13F-HR information table."
+ " Check the `filing_str` to make sure it is valid and contains the tag 'informationTable'."
+ " Documents filed before Q2 2013 are not supported."
)
period_ending = get_period_ending(filing)
data = (
DataFrame(parsed_xml)
if isinstance(parsed_xml, list)
else DataFrame([parsed_xml])
)
data.columns = data.columns.str.replace("ns1:", "")
data["value"] = data["value"].astype(int)
security_type = []
principal_amount = []
# Unpack the nested objects
try:
security_type = [d.get("sshPrnamtType") for d in data["shrsOrPrnAmt"]]
data["security_type"] = security_type
principal_amount = [d.get("sshPrnamt") for d in data["shrsOrPrnAmt"]]
data["principal_amount"] = principal_amount
data.pop("shrsOrPrnAmt")
except ValueError:
pass
try:
sole = [d.get("Sole") for d in data["votingAuthority"]]
shared = [d.get("Shared") for d in data["votingAuthority"]]
none = [d.get("None") for d in data["votingAuthority"]]
data["voting_authority_sole"] = [int(s) for s in sole]
data["voting_authority_shared"] = [int(s) for s in shared]
data["voting_authority_none"] = [int(s) for s in none]
data.pop("votingAuthority")
except ValueError:
pass
# Add the period ending so that the filing is identified when multiple are requested.
data["period_ending"] = to_datetime(period_ending, yearfirst=False).date()
df = DataFrame(data)
df["principal_amount"] = df["principal_amount"].astype(int)
# Aggregate the data because there are multiple entries for each security and we need the totals.
# We break it down by CUSIP, security type, and option type.
agg_index = ["cusip", "security_type", "putCall"]
agg_columns = {
"period_ending": "first",
"nameOfIssuer": "first",
"titleOfClass": "first",
"value": "sum",
"principal_amount": "sum",
"voting_authority_sole": "sum",
"voting_authority_shared": "sum",
"voting_authority_none": "sum",
}
# Only aggregate columns that exist in the DataFrame
agg_columns = {k: v for k, v in agg_columns.items() if k in df.columns}
agg_index = [k for k in agg_index if k in df.columns]
df = df.groupby([*agg_index]).agg(agg_columns)
for col in [
"voting_authority_sole",
"voting_authority_shared",
"voting_authority_none",
]:
if col in df.columns and all(df[col] == 0):
df.drop(columns=col, inplace=True)
total_value = df["value"].sum()
df["weight"] = round(df["value"] / total_value, 6)
return (
df.reset_index()
.fillna("N/A")
.sort_values(by="weight", ascending=False)
.replace("N/A", None)
)

View File

@@ -5,6 +5,7 @@ from openbb_sec.models.company_filings import SecCompanyFilingsFetcher
from openbb_sec.models.equity_ftd import SecEquityFtdFetcher
from openbb_sec.models.equity_search import SecEquitySearchFetcher
from openbb_sec.models.etf_holdings import SecEtfHoldingsFetcher
from openbb_sec.models.form_13FHR import SecForm13FHRFetcher
from openbb_sec.models.institutions_search import SecInstitutionsSearchFetcher
from openbb_sec.models.rss_litigation import SecRssLitigationFetcher
from openbb_sec.models.schema_files import SecSchemaFilesFetcher
@@ -112,3 +113,12 @@ def test_sec_cik_map_fetcher(credentials=test_credentials):
fetcher = SecCikMapFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_sec_form_13FHR_fetcher(credentials=test_credentials):
params = {"symbol": "NVDA"}
fetcher = SecForm13FHRFetcher()
result = fetcher.test(params, credentials)
assert result is None