Files
edgartools/venv/lib/python3.10/site-packages/edgar/thirteenf/models.py
2025-12-09 12:13:01 +01:00

485 lines
18 KiB
Python

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))