Initial commit
This commit is contained in:
484
venv/lib/python3.10/site-packages/edgar/thirteenf/models.py
Normal file
484
venv/lib/python3.10/site-packages/edgar/thirteenf/models.py
Normal file
@@ -0,0 +1,484 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from functools import lru_cache
|
||||
from typing import List, Union
|
||||
|
||||
import pyarrow.compute as pc
|
||||
|
||||
from edgar._party import Address
|
||||
|
||||
__all__ = [
|
||||
'FilingManager',
|
||||
'OtherManager',
|
||||
'CoverPage',
|
||||
'SummaryPage',
|
||||
'Signature',
|
||||
'PrimaryDocument13F',
|
||||
'ThirteenF',
|
||||
'THIRTEENF_FORMS',
|
||||
'format_date',
|
||||
]
|
||||
|
||||
THIRTEENF_FORMS = ['13F-HR', "13F-HR/A", "13F-NT", "13F-NT/A", "13F-CTR", "13F-CTR/A"]
|
||||
|
||||
|
||||
def format_date(date: Union[str, datetime]) -> str:
|
||||
if isinstance(date, str):
|
||||
return date
|
||||
return date.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FilingManager:
|
||||
name: str
|
||||
address: Address
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OtherManager:
|
||||
cik: str
|
||||
name: str
|
||||
file_number: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CoverPage:
|
||||
report_calendar_or_quarter: str
|
||||
report_type: str
|
||||
filing_manager: FilingManager
|
||||
other_managers: List[OtherManager]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SummaryPage:
|
||||
other_included_managers_count: int
|
||||
total_value: Decimal
|
||||
total_holdings: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Signature:
|
||||
name: str
|
||||
title: str
|
||||
phone: str
|
||||
signature: str
|
||||
city: str
|
||||
state_or_country: str
|
||||
date: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PrimaryDocument13F:
|
||||
report_period: datetime
|
||||
cover_page: CoverPage
|
||||
summary_page: SummaryPage
|
||||
signature: Signature
|
||||
additional_information: str
|
||||
|
||||
|
||||
class ThirteenF:
|
||||
"""
|
||||
A 13F-HR is a quarterly report filed by institutional investment managers that have over $100 million in qualifying
|
||||
assets under management. The report is filed with the Securities & Exchange Commission (SEC) and discloses all
|
||||
the firm's equity holdings that it held at the end of the quarter. The report is due within 45 days of the end
|
||||
of the quarter. The 13F-HR is a public document that is available on the SEC's website.
|
||||
"""
|
||||
|
||||
def __init__(self, filing, use_latest_period_of_report=False):
|
||||
from edgar.thirteenf.parsers.primary_xml import parse_primary_document_xml
|
||||
|
||||
assert filing.form in THIRTEENF_FORMS, f"Form {filing.form} is not a valid 13F form"
|
||||
# The filing might not be the filing for the current period. We need to use the related filing filed on the same
|
||||
# date as the current filing that has the latest period of report
|
||||
self._related_filings = filing.related_filings().filter(filing_date=filing.filing_date, form=filing.form)
|
||||
self._actual_filing = filing # The filing passed in
|
||||
if use_latest_period_of_report:
|
||||
# Use the last related filing.
|
||||
# It should also be the one that has the CONFORMED_PERIOD_OF_REPORT closest to filing_date
|
||||
self.filing = self._related_filings[-1]
|
||||
else:
|
||||
# Use the exact filing that was passed in
|
||||
self.filing = self._actual_filing
|
||||
|
||||
# Parse primary document if XML is available (2013+ filings)
|
||||
# For older TXT-only filings (2012 and earlier), primary_form_information will be None
|
||||
primary_xml = self.filing.xml()
|
||||
self.primary_form_information = parse_primary_document_xml(primary_xml) if primary_xml else None
|
||||
|
||||
def has_infotable(self):
|
||||
return self.filing.form in ['13F-HR', "13F-HR/A"]
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
return self.filing.form
|
||||
|
||||
@property
|
||||
@lru_cache(maxsize=1)
|
||||
def infotable_xml(self):
|
||||
"""Returns XML content if available (2013+ filings)"""
|
||||
if self.has_infotable():
|
||||
result = self._get_infotable_from_attachment()
|
||||
if result and result[0] and result[1] == 'xml' and "informationTable" in result[0]:
|
||||
return result[0]
|
||||
return None
|
||||
|
||||
def _get_infotable_from_attachment(self):
|
||||
"""
|
||||
Use the filing homepage to get the infotable file.
|
||||
Returns a tuple of (content, format) where format is 'xml' or 'txt'.
|
||||
"""
|
||||
if self.has_infotable():
|
||||
# Try XML format first (2013+)
|
||||
query = "document_type=='INFORMATION TABLE' and document.lower().endswith('.xml')"
|
||||
attachments = self.filing.attachments.query(query)
|
||||
if len(attachments) > 0:
|
||||
return (attachments.get_by_index(0).download(), 'xml')
|
||||
|
||||
# Fall back to TXT format (2012 and earlier)
|
||||
# The primary document itself contains the table in TXT format
|
||||
# Try various description patterns first
|
||||
query = "description=='FORM 13F' or description=='INFORMATION TABLE'"
|
||||
attachments = self.filing.attachments.query(query)
|
||||
if len(attachments) > 0:
|
||||
# Filter for .txt files only
|
||||
txt_attachments = [att for att in attachments if att.document.lower().endswith('.txt')]
|
||||
if txt_attachments:
|
||||
return (txt_attachments[0].download(), 'txt')
|
||||
|
||||
# Final fallback: For older filings, descriptions may be unreliable
|
||||
# Look for sequence number 1 with .txt extension
|
||||
try:
|
||||
att = self.filing.attachments.get_by_sequence(1)
|
||||
if att and att.document.lower().endswith('.txt'):
|
||||
return (att.download(), 'txt')
|
||||
except (KeyError, AttributeError):
|
||||
pass
|
||||
|
||||
return (None, None)
|
||||
|
||||
@property
|
||||
@lru_cache(maxsize=1)
|
||||
def infotable_txt(self):
|
||||
"""Returns TXT content if available (pre-2013 filings)"""
|
||||
if self.has_infotable():
|
||||
result = self._get_infotable_from_attachment()
|
||||
if result and result[0] and result[1] == 'txt':
|
||||
return result[0]
|
||||
|
||||
# Fallback: Some filings have the information table embedded in the main HTML
|
||||
# instead of as a separate attachment. Try to extract it from the main HTML.
|
||||
if not result or not result[0]:
|
||||
html = self.filing.html()
|
||||
if html and "Form 13F Information Table" in html:
|
||||
return html
|
||||
return None
|
||||
|
||||
@property
|
||||
@lru_cache(maxsize=1)
|
||||
def infotable_html(self):
|
||||
if self.has_infotable():
|
||||
query = "document_type=='INFORMATION TABLE' and document.lower().endswith('.html')"
|
||||
attachments = self.filing.attachments.query(query)
|
||||
return attachments[0].download()
|
||||
|
||||
@property
|
||||
@lru_cache(maxsize=1)
|
||||
def infotable(self):
|
||||
"""
|
||||
Returns the information table as a pandas DataFrame.
|
||||
Supports both XML format (2013+) and TXT format (2012 and earlier).
|
||||
"""
|
||||
from edgar.thirteenf.parsers.infotable_xml import parse_infotable_xml
|
||||
from edgar.thirteenf.parsers.infotable_txt import parse_infotable_txt
|
||||
|
||||
if self.has_infotable():
|
||||
# Try XML format first
|
||||
if self.infotable_xml:
|
||||
return parse_infotable_xml(self.infotable_xml)
|
||||
# Fall back to TXT format
|
||||
elif self.infotable_txt:
|
||||
return parse_infotable_txt(self.infotable_txt)
|
||||
return None
|
||||
|
||||
@property
|
||||
def accession_number(self):
|
||||
return self.filing.accession_no
|
||||
|
||||
@property
|
||||
def total_value(self):
|
||||
"""Total value of holdings in thousands of dollars"""
|
||||
if self.primary_form_information:
|
||||
return self.primary_form_information.summary_page.total_value
|
||||
# For TXT-only filings, calculate from infotable
|
||||
infotable = self.infotable
|
||||
if infotable is not None and len(infotable) > 0:
|
||||
return Decimal(int(infotable['Value'].sum()))
|
||||
return None
|
||||
|
||||
@property
|
||||
def total_holdings(self):
|
||||
"""Total number of holdings"""
|
||||
if self.primary_form_information:
|
||||
return self.primary_form_information.summary_page.total_holdings
|
||||
# For TXT-only filings, count from infotable
|
||||
infotable = self.infotable
|
||||
if infotable is not None:
|
||||
return len(infotable)
|
||||
return None
|
||||
|
||||
@property
|
||||
def report_period(self):
|
||||
"""Report period end date"""
|
||||
if self.primary_form_information:
|
||||
return format_date(self.primary_form_information.report_period)
|
||||
# For TXT-only filings, use CONFORMED_PERIOD_OF_REPORT from filing header
|
||||
if hasattr(self.filing, 'period_of_report') and self.filing.period_of_report:
|
||||
return format_date(self.filing.period_of_report)
|
||||
return None
|
||||
|
||||
@property
|
||||
def filing_date(self):
|
||||
return format_date(self.filing.filing_date)
|
||||
|
||||
@property
|
||||
def investment_manager(self):
|
||||
# This is really the firm e.g. Spark Growth Management Partners II, LLC
|
||||
if self.primary_form_information:
|
||||
return self.primary_form_information.cover_page.filing_manager
|
||||
return None
|
||||
|
||||
@property
|
||||
def signer(self):
|
||||
# This is the person who signed the filing. Could be the Reporting Manager but could be someone else
|
||||
# like the CFO
|
||||
if self.primary_form_information:
|
||||
return self.primary_form_information.signature.name
|
||||
return None
|
||||
|
||||
# Enhanced manager name properties for better clarity
|
||||
@property
|
||||
def management_company_name(self) -> str:
|
||||
"""
|
||||
The legal name of the investment management company that filed the 13F.
|
||||
|
||||
This is the institutional entity (e.g., "Berkshire Hathaway Inc", "Vanguard Group Inc")
|
||||
that is legally responsible for managing the assets, not an individual person's name.
|
||||
|
||||
Returns:
|
||||
str: The legal name of the management company, or company name from filing if not available
|
||||
|
||||
Example:
|
||||
>>> thirteen_f.management_company_name
|
||||
'Berkshire Hathaway Inc'
|
||||
"""
|
||||
if self.investment_manager:
|
||||
return self.investment_manager.name
|
||||
# For TXT-only filings, use company name from filing
|
||||
return self.filing.company
|
||||
|
||||
@property
|
||||
def filing_signer_name(self) -> str:
|
||||
"""
|
||||
The name of the individual who signed the 13F filing.
|
||||
|
||||
This is typically an administrative officer (CFO, CCO, Compliance Officer, etc.)
|
||||
rather than the famous portfolio manager. For example, Berkshire Hathaway's 13F
|
||||
is signed by "Marc D. Hamburg" (SVP), not Warren Buffett.
|
||||
|
||||
Returns:
|
||||
str: The name of the person who signed the filing
|
||||
|
||||
Example:
|
||||
>>> thirteen_f.filing_signer_name
|
||||
'Marc D. Hamburg'
|
||||
"""
|
||||
return self.signer
|
||||
|
||||
@property
|
||||
def filing_signer_title(self) -> str:
|
||||
"""
|
||||
The business title of the individual who signed the 13F filing.
|
||||
|
||||
Common titles include: CFO, CCO, Senior Vice President, Chief Compliance Officer,
|
||||
Secretary, Treasurer, etc. This helps distinguish administrative signers from
|
||||
portfolio managers.
|
||||
|
||||
Returns:
|
||||
str: The business title of the filing signer, or None if not available
|
||||
|
||||
Example:
|
||||
>>> thirteen_f.filing_signer_title
|
||||
'Senior Vice President'
|
||||
"""
|
||||
if self.primary_form_information:
|
||||
return self.primary_form_information.signature.title
|
||||
return None
|
||||
|
||||
@property
|
||||
def manager_name(self) -> str:
|
||||
"""
|
||||
DEPRECATED: Use management_company_name instead.
|
||||
|
||||
Returns the management company name for backwards compatibility.
|
||||
This property name was misleading as it suggested an individual manager's name.
|
||||
|
||||
Returns:
|
||||
str: The management company name
|
||||
|
||||
Warning:
|
||||
This property is deprecated and may be removed in future versions.
|
||||
Use management_company_name for the company name, or see get_portfolio_managers()
|
||||
if you need information about individual portfolio managers.
|
||||
"""
|
||||
import warnings
|
||||
warnings.warn(
|
||||
"manager_name is deprecated and misleading. Use management_company_name for the "
|
||||
"company name, or get_portfolio_managers() for individual manager information.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
return self.management_company_name
|
||||
|
||||
def get_portfolio_managers(self, include_approximate: bool = False) -> list[dict]:
|
||||
"""
|
||||
Get information about the actual portfolio managers for this fund.
|
||||
|
||||
Note: 13F filings do not contain individual portfolio manager names.
|
||||
This method provides a curated mapping for well-known funds based on
|
||||
public information. Results may not be current or complete.
|
||||
|
||||
Args:
|
||||
include_approximate (bool): If True, includes approximate/historical
|
||||
manager information even if not current
|
||||
|
||||
Returns:
|
||||
list[dict]: List of portfolio manager information with keys:
|
||||
'name', 'title', 'status', 'source', 'last_updated'
|
||||
|
||||
Example:
|
||||
>>> thirteen_f.get_portfolio_managers()
|
||||
[
|
||||
{
|
||||
'name': 'Warren Buffett',
|
||||
'title': 'Chairman & CEO',
|
||||
'status': 'active',
|
||||
'source': 'public_records',
|
||||
'last_updated': '2024-01-01'
|
||||
}
|
||||
]
|
||||
"""
|
||||
from edgar.thirteenf.manager_lookup import lookup_portfolio_managers
|
||||
return lookup_portfolio_managers(
|
||||
self.management_company_name,
|
||||
getattr(self.filing, 'cik', None),
|
||||
include_approximate=include_approximate
|
||||
)
|
||||
|
||||
def _lookup_portfolio_managers(self, company_name: str, include_approximate: bool = False) -> list[dict]:
|
||||
"""
|
||||
Private method for testing - looks up portfolio managers by company name.
|
||||
|
||||
Args:
|
||||
company_name: Name of the management company
|
||||
include_approximate: Whether to include approximate/historical data
|
||||
|
||||
Returns:
|
||||
list[dict]: List of portfolio manager information
|
||||
"""
|
||||
from edgar.thirteenf.manager_lookup import lookup_portfolio_managers
|
||||
return lookup_portfolio_managers(company_name, cik=None, include_approximate=include_approximate)
|
||||
|
||||
def get_manager_info_summary(self) -> dict:
|
||||
"""
|
||||
Get a comprehensive summary of all available manager information.
|
||||
|
||||
This provides a clear breakdown of what information is available from the 13F
|
||||
filing versus external sources, helping users understand the data limitations.
|
||||
|
||||
Returns:
|
||||
dict: Summary with keys 'from_13f_filing', 'external_sources', 'limitations'
|
||||
|
||||
Example:
|
||||
>>> thirteen_f.get_manager_info_summary()
|
||||
{
|
||||
'from_13f_filing': {
|
||||
'management_company': 'Berkshire Hathaway Inc',
|
||||
'filing_signer': 'Marc D. Hamburg',
|
||||
'signer_title': 'Senior Vice President'
|
||||
},
|
||||
'external_sources': {
|
||||
'portfolio_managers': [
|
||||
{'name': 'Warren Buffett', 'title': 'Chairman & CEO', 'status': 'active'}
|
||||
]
|
||||
},
|
||||
'limitations': [
|
||||
'13F filings do not contain individual portfolio manager names',
|
||||
'External manager data may not be current or complete',
|
||||
'Filing signer is typically an administrative officer, not the portfolio manager'
|
||||
]
|
||||
}
|
||||
"""
|
||||
portfolio_managers = self.get_portfolio_managers()
|
||||
|
||||
return {
|
||||
'from_13f_filing': {
|
||||
'management_company': self.management_company_name,
|
||||
'filing_signer': self.filing_signer_name,
|
||||
'signer_title': self.filing_signer_title,
|
||||
'form': self.form,
|
||||
'period_of_report': str(self.report_period)
|
||||
},
|
||||
'external_sources': {
|
||||
'portfolio_managers': portfolio_managers,
|
||||
'manager_count': len(portfolio_managers)
|
||||
},
|
||||
'limitations': [
|
||||
'13F filings do not contain individual portfolio manager names',
|
||||
'External manager data may not be current or complete',
|
||||
'Filing signer is typically an administrative officer, not the portfolio manager',
|
||||
'Portfolio manager information is sourced from public records and may be outdated'
|
||||
]
|
||||
}
|
||||
|
||||
def is_filing_signer_likely_portfolio_manager(self) -> bool:
|
||||
"""
|
||||
Determine if the filing signer is likely to be a portfolio manager.
|
||||
|
||||
This uses heuristics based on the signer's title to assess whether they
|
||||
might be involved in investment decisions rather than just administrative functions.
|
||||
|
||||
Returns:
|
||||
bool: True if signer appears to be investment-focused, False if administrative
|
||||
|
||||
Example:
|
||||
>>> thirteen_f.is_filing_signer_likely_portfolio_manager()
|
||||
False # For administrative titles like CFO, CCO, etc.
|
||||
"""
|
||||
from edgar.thirteenf.manager_lookup import is_filing_signer_likely_portfolio_manager
|
||||
return is_filing_signer_likely_portfolio_manager(self.filing_signer_title)
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def previous_holding_report(self):
|
||||
if len(self.report_period) == 1:
|
||||
return None
|
||||
# Look in the related filings data for the row with this accession number
|
||||
idx = pc.equal(self._related_filings.data['accession_number'], self.accession_number).index(True).as_py()
|
||||
if idx == 0:
|
||||
return None
|
||||
previous_filing = self._related_filings[idx - 1]
|
||||
return ThirteenF(previous_filing, use_latest_period_of_report=False)
|
||||
|
||||
def __rich__(self):
|
||||
from edgar.thirteenf.rendering import render_rich
|
||||
return render_rich(self)
|
||||
|
||||
def __repr__(self):
|
||||
from edgar.richtools import repr_rich
|
||||
return repr_rich(self.__rich__())
|
||||
|
||||
|
||||
# For backward compatibility, expose parse methods as static methods
|
||||
ThirteenF.parse_primary_document_xml = staticmethod(lambda xml: __import__('edgar.thirteenf.parsers.primary_xml', fromlist=['parse_primary_document_xml']).parse_primary_document_xml(xml))
|
||||
ThirteenF.parse_infotable_xml = staticmethod(lambda xml: __import__('edgar.thirteenf.parsers.infotable_xml', fromlist=['parse_infotable_xml']).parse_infotable_xml(xml))
|
||||
ThirteenF.parse_infotable_txt = staticmethod(lambda txt: __import__('edgar.thirteenf.parsers.infotable_txt', fromlist=['parse_infotable_txt']).parse_infotable_txt(txt))
|
||||
Reference in New Issue
Block a user