499 lines
20 KiB
Python
499 lines
20 KiB
Python
from dataclasses import dataclass
|
|
from typing import Dict, List
|
|
|
|
import pandas as pd
|
|
from bs4 import BeautifulSoup, Tag
|
|
from rich import box
|
|
from rich.console import Group
|
|
from rich.panel import Panel
|
|
from rich.table import Column, Table
|
|
from rich.text import Text
|
|
|
|
from edgar._party import Address, Contact, Filer
|
|
from edgar.entity import Company
|
|
from edgar.richtools import repr_rich
|
|
from edgar.xmltools import child_text, child_texts
|
|
|
|
__all__ = ['Form144',
|
|
'concat_securities_information',
|
|
'concat_securities_to_be_sold'
|
|
]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SecuritiesInformation:
|
|
"""
|
|
<securitiesInformation>
|
|
<securitiesClassTitle>Common stock</securitiesClassTitle>
|
|
<brokerOrMarketmakerDetails>
|
|
<name>Virtu Financial</name>
|
|
<address>
|
|
<com:street1>One Liberty Plaza</com:street1>
|
|
<com:street2>165 Broadway</com:street2>
|
|
<com:city>New York</com:city>
|
|
<com:stateOrCountry>NY</com:stateOrCountry>
|
|
<com:zipCode>10006</com:zipCode>
|
|
</address>
|
|
</brokerOrMarketmakerDetails>
|
|
<noOfUnitsSold>17087</noOfUnitsSold>
|
|
<aggregateMarketValue>1282000.00</aggregateMarketValue>
|
|
<noOfUnitsOutstanding>161514066</noOfUnitsOutstanding>
|
|
<approxSaleDate>08/27/2022</approxSaleDate>
|
|
<securitiesExchangeName>CHX</securitiesExchangeName>
|
|
</securitiesInformation>
|
|
"""
|
|
security_class: str
|
|
units_to_be_sold: int
|
|
aggregate_market_value: float
|
|
units_outstanding: int
|
|
approx_sale_date: str
|
|
exchange_name: str
|
|
broker_name: str
|
|
broker_address: Address
|
|
|
|
def to_dict(self):
|
|
# Convert this object to a dictionary
|
|
return {
|
|
'security_class': self.security_class,
|
|
'units_to_be_sold': self.units_to_be_sold,
|
|
'market_value': self.aggregate_market_value,
|
|
'units_outstanding': self.units_outstanding,
|
|
'approx_sale_date': self.approx_sale_date,
|
|
'exchange_name': self.exchange_name,
|
|
'broker_name': self.broker_name
|
|
}
|
|
|
|
@classmethod
|
|
def from_tag(cls, tag: Tag):
|
|
security_class = child_text(tag, 'securitiesClassTitle')
|
|
units_to_be_sold = child_text(tag, 'noOfUnitsSold')
|
|
aggregate_market_value = child_text(tag, 'aggregateMarketValue')
|
|
units_outstanding = child_text(tag, 'noOfUnitsOutstanding')
|
|
approx_sale_date = child_text(tag, 'approxSaleDate')
|
|
exchange_name = child_text(tag, 'securitiesExchangeName')
|
|
|
|
# Get the broker or market maker
|
|
broker_or_marketmaker_tag = tag.find('brokerOrMarketmakerDetails')
|
|
broker_name = child_text(broker_or_marketmaker_tag, 'name')
|
|
|
|
# Get the address
|
|
address_el = broker_or_marketmaker_tag.find('address')
|
|
address = Address(
|
|
street1=child_text(address_el, 'street1'),
|
|
street2=child_text(address_el, 'street2'),
|
|
city=child_text(address_el, 'city'),
|
|
state_or_country=child_text(address_el, 'stateOrCountry'),
|
|
zipcode=child_text(address_el, 'zipCode')
|
|
)
|
|
return cls(
|
|
security_class=security_class,
|
|
units_to_be_sold=int(units_to_be_sold),
|
|
aggregate_market_value=float(aggregate_market_value) if aggregate_market_value else None,
|
|
units_outstanding=int(units_outstanding),
|
|
approx_sale_date=approx_sale_date,
|
|
exchange_name=exchange_name,
|
|
broker_name=broker_name,
|
|
broker_address=address
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SecuritiesToBeSold:
|
|
"""
|
|
<securitiesToBeSold>
|
|
<securitiesClassTitle>Common stock - 2</securitiesClassTitle>
|
|
<acquiredDate>01/01/1933</acquiredDate>
|
|
<natureOfAcquisitionTransaction>Employee Stock Award -1</natureOfAcquisitionTransaction>
|
|
<nameOfPersonfromWhomAcquired>Issuer-1</nameOfPersonfromWhomAcquired>
|
|
<isGiftTransaction>Y</isGiftTransaction>
|
|
<donarAcquiredDate>01/01/1933</donarAcquiredDate>
|
|
<amountOfSecuritiesAcquired>17087</amountOfSecuritiesAcquired>
|
|
<paymentDate>08/15/2021</paymentDate>
|
|
<natureOfPayment>CASH-25</natureOfPayment>
|
|
</securitiesToBeSold>
|
|
"""
|
|
security_class: str
|
|
acquired_date: str
|
|
nature_of_acquisition_transaction: str
|
|
name_of_person_from_whom_acquired: str
|
|
is_gift_transaction: str
|
|
donar_acquired_date: str
|
|
amount_of_securities_acquired: int
|
|
payment_date: str
|
|
nature_of_payment: str
|
|
|
|
def to_dict(self):
|
|
# Convert this object to a dictionary
|
|
return {
|
|
'security_class': self.security_class,
|
|
'acquired_date': self.acquired_date,
|
|
'amount_acquired': self.amount_of_securities_acquired,
|
|
'nature_of_acquisition': self.nature_of_acquisition_transaction,
|
|
'acquired_from': self.name_of_person_from_whom_acquired,
|
|
'nature_of_payment': self.nature_of_payment,
|
|
'is_gift': self.is_gift_transaction,
|
|
'donar_acquired_date': self.donar_acquired_date,
|
|
'payment_date': self.payment_date
|
|
}
|
|
|
|
@classmethod
|
|
def from_tag(cls, tag: Tag):
|
|
security_class = child_text(tag, 'securitiesClassTitle')
|
|
acquired_date = child_text(tag, 'acquiredDate')
|
|
nature_of_acquisition_transaction = child_text(tag, 'natureOfAcquisitionTransaction')
|
|
name_of_person_from_whom_acquired = child_text(tag, 'nameOfPersonfromWhomAcquired')
|
|
is_gift_transaction = child_text(tag, 'isGiftTransaction')
|
|
donar_acquired_date = child_text(tag, 'donarAcquiredDate')
|
|
amount_of_securities_acquired = child_text(tag, 'amountOfSecuritiesAcquired')
|
|
payment_date = child_text(tag, 'paymentDate')
|
|
nature_of_payment = child_text(tag, 'natureOfPayment')
|
|
return cls(
|
|
security_class=security_class,
|
|
acquired_date=acquired_date,
|
|
nature_of_acquisition_transaction=nature_of_acquisition_transaction,
|
|
name_of_person_from_whom_acquired=name_of_person_from_whom_acquired,
|
|
is_gift_transaction=is_gift_transaction,
|
|
donar_acquired_date=donar_acquired_date,
|
|
amount_of_securities_acquired=int(amount_of_securities_acquired),
|
|
payment_date=payment_date,
|
|
nature_of_payment=nature_of_payment
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SecuritiesSoldPast3Months:
|
|
"""
|
|
<securitiesSoldInPast3Months>
|
|
<sellerDetails>
|
|
<name>Virtu Financial</name>
|
|
<address>
|
|
<com:street1>One Liberty Plaza</com:street1>
|
|
<com:street2>165 Broadway</com:street2>
|
|
<com:city>New York</com:city>
|
|
<com:stateOrCountry>NY</com:stateOrCountry>
|
|
<com:zipCode>10006</com:zipCode>
|
|
</address>
|
|
</sellerDetails>
|
|
<securitiesClassTitle>Common Stock</securitiesClassTitle>
|
|
<saleDate>08/27/2022</saleDate>
|
|
<amountOfSecuritiesSold>0</amountOfSecuritiesSold>
|
|
<grossProceeds>0.00</grossProceeds>
|
|
</securitiesSoldInPast3Months>
|
|
"""
|
|
seller_name: str
|
|
seller_address: Address
|
|
security_class: str
|
|
sale_date: str
|
|
amount_of_securities_sold: int
|
|
gross_proceeds: float
|
|
|
|
def to_dict(self):
|
|
# Convert this object to a dictionary
|
|
return {
|
|
'security_class': self.security_class,
|
|
'seller_name': self.seller_name,
|
|
'sale_date': self.sale_date,
|
|
'amount_sold': self.amount_of_securities_sold,
|
|
'gross_proceeds': self.gross_proceeds
|
|
}
|
|
|
|
@classmethod
|
|
def from_tag(cls, tag: Tag):
|
|
seller_details = tag.find('sellerDetails')
|
|
seller_name = child_text(seller_details, 'name')
|
|
# Get the address
|
|
address_el = seller_details.find('address')
|
|
seller_address = Address(
|
|
street1=child_text(address_el, 'street1'),
|
|
street2=child_text(address_el, 'street2'),
|
|
city=child_text(address_el, 'city'),
|
|
state_or_country=child_text(address_el, 'stateOrCountry'),
|
|
zipcode=child_text(address_el, 'zipCode')
|
|
)
|
|
security_class = child_text(tag, 'securitiesClassTitle')
|
|
sale_date = child_text(tag, 'saleDate')
|
|
amount_of_securities_sold = child_text(tag, 'amountOfSecuritiesSold')
|
|
gross_proceeds = child_text(tag, 'grossProceeds')
|
|
return cls(
|
|
seller_name=seller_name,
|
|
seller_address=seller_address,
|
|
security_class=security_class,
|
|
sale_date=sale_date,
|
|
amount_of_securities_sold=int(amount_of_securities_sold),
|
|
gross_proceeds=float(gross_proceeds) if gross_proceeds else None
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class NoticeSignature:
|
|
"""
|
|
<noticeSignature>
|
|
<noticeDate>09/08/2022</noticeDate>
|
|
<planAdoptionDates>
|
|
<planAdoptionDate>08/15/2022</planAdoptionDate>
|
|
<planAdoptionDate>08/15/2022</planAdoptionDate>
|
|
<planAdoptionDate>01/02/1933</planAdoptionDate>
|
|
</planAdoptionDates>
|
|
<signature>/s/ Jan van der Velden</signature>
|
|
</noticeSignature>
|
|
"""
|
|
notice_date: str
|
|
plan_adoption_dates: List[str]
|
|
signature: str
|
|
|
|
@classmethod
|
|
def from_tag(cls, tag: Tag):
|
|
notice_date = child_text(tag, 'noticeDate')
|
|
plan_adoption_dates = [child_text(d, 'planAdoptionDate') for d in tag.find_all('planAdoptionDate')]
|
|
signature = child_text(tag, 'signature')
|
|
return cls(
|
|
notice_date=notice_date,
|
|
plan_adoption_dates=plan_adoption_dates,
|
|
signature=signature
|
|
)
|
|
|
|
|
|
class Form144:
|
|
|
|
def __init__(self,
|
|
filing,
|
|
filer: Filer,
|
|
contact: Contact,
|
|
issuer_cik: str,
|
|
issuer_name: str,
|
|
sec_file_number: str,
|
|
issuer_contact_phone: str,
|
|
person_selling: str,
|
|
relationships: List[str],
|
|
address: Address,
|
|
securities_information: pd.DataFrame,
|
|
securities_to_be_sold: pd.DataFrame,
|
|
securities_sold_past_3_months: pd.DataFrame,
|
|
nothing_to_report: bool,
|
|
remarks: str,
|
|
notice_signature: NoticeSignature
|
|
):
|
|
assert filing.form in ['144', '144/A'], f"This form should be a Form 144 but was {filing.form}"
|
|
self._filing = filing
|
|
self.filer = filer
|
|
self.contact: Contact = contact
|
|
self.issuer_cik = issuer_cik
|
|
self.issuer_name = issuer_name
|
|
self.sec_file_number = sec_file_number
|
|
self.issuer_contact_phone = issuer_contact_phone
|
|
self.person_selling = person_selling
|
|
self.relationships = relationships
|
|
self.address = address
|
|
self.securities_information: pd.DataFrame = securities_information
|
|
self.securities_to_be_sold: pd.DataFram = securities_to_be_sold
|
|
self.securities_sold_past_3_months = securities_sold_past_3_months
|
|
self.nothing_to_report = nothing_to_report
|
|
self.remarks = remarks
|
|
self.notice_signature = notice_signature
|
|
|
|
@property
|
|
def company(self) -> Company:
|
|
return Company(self.issuer_cik)
|
|
|
|
@property
|
|
def units_to_be_sold(self) -> int:
|
|
return self.securities_information.loc[0].units_to_be_sold
|
|
|
|
@property
|
|
def market_value(self) -> int:
|
|
return self.securities_information.loc[0].market_value
|
|
|
|
@property
|
|
def approx_sale_date(self) -> int:
|
|
return self.securities_information.loc[0].approx_sale_date
|
|
|
|
@property
|
|
def security_class(self) -> int:
|
|
return self.securities_information.loc[0].security_class
|
|
|
|
@property
|
|
def broker_name(self) -> int:
|
|
return self.securities_information.loc[0].broker_name
|
|
|
|
@property
|
|
def exchange_name(self) -> int:
|
|
return self.securities_information.loc[0].exchange_name
|
|
|
|
@staticmethod
|
|
def parse_xml(xml: str) -> Dict[str, object]:
|
|
soup = BeautifulSoup(xml, 'xml')
|
|
|
|
root = soup.find('edgarSubmission')
|
|
|
|
form144 = {}
|
|
|
|
header_data = root.find('headerData')
|
|
filer_info_el = header_data.find('filerInfo')
|
|
|
|
filer_el = filer_info_el.find('filer')
|
|
filer_credentials_el = filer_el.find('filerCredentials')
|
|
form144['filer'] = Filer(
|
|
cik=child_text(filer_credentials_el, 'cik'),
|
|
entity_name=child_text(filer_credentials_el, 'name'),
|
|
file_number=child_text(filer_credentials_el, 'secFileNumber')
|
|
)
|
|
|
|
# Contact info
|
|
contact_el = filer_el.find('contact')
|
|
form144['contact'] = Contact(
|
|
name=child_text(contact_el, 'name'),
|
|
phone_number=child_text(contact_el, 'phone'),
|
|
email=child_text(contact_el, 'email')
|
|
) if contact_el else None
|
|
|
|
form_data = root.find('formData')
|
|
# Issuer
|
|
issuer_el = form_data.find('issuerInfo')
|
|
form144['issuer_cik'] = child_text(issuer_el, 'issuerCik')
|
|
form144['issuer_name'] = child_text(issuer_el, 'issuerName')
|
|
form144['sec_file_number'] = child_text(issuer_el, 'secFileNumber')
|
|
form144['issuer_contact_phone'] = child_text(issuer_el, 'issuerContactPhone')
|
|
form144['person_selling'] = child_text(issuer_el, 'nameOfPersonForWhoseAccountTheSecuritiesAreToBeSold')
|
|
|
|
relationship_el = issuer_el.find('relationshipsToIssuer')
|
|
form144['relationships'] = child_texts(relationship_el, 'relationshipToIssuer')
|
|
|
|
issuer_address_el = issuer_el.find("issuerAddress")
|
|
address: Address = Address(
|
|
street1=child_text(issuer_address_el, "street1"),
|
|
street2=child_text(issuer_address_el, "street2"),
|
|
city=child_text(issuer_address_el, "city"),
|
|
state_or_country=child_text(issuer_address_el, "stateOrCountry"),
|
|
state_or_country_description=child_text(issuer_address_el, "stateOrCountryDescription"),
|
|
zipcode=child_text(issuer_address_el, "zipCode")
|
|
)
|
|
form144['address'] = address
|
|
|
|
# Securities Information
|
|
form144['securities_information'] = pd.DataFrame([
|
|
SecuritiesInformation.from_tag(el).to_dict()
|
|
for el in form_data.find_all('securitiesInformation')
|
|
])
|
|
|
|
# Securities to be sold
|
|
form144['securities_to_be_sold'] = pd.DataFrame([
|
|
SecuritiesToBeSold.from_tag(el).to_dict()
|
|
for el in form_data.find_all('securitiesToBeSold')
|
|
])
|
|
# Nothing to report flag
|
|
form144['nothing_to_report'] = child_text(form_data, 'nothingToReportFlagOnSecuritiesSoldInPast3Months')
|
|
|
|
# Securities sold in past 3 months
|
|
form144['securities_sold_past_3_months'] = pd.DataFrame([
|
|
SecuritiesSoldPast3Months.from_tag(el).to_dict()
|
|
for el in form_data.find_all('securitiesSoldInPast3Months')
|
|
])
|
|
|
|
# Remarks
|
|
form144['remarks'] = child_text(form_data, 'remarks')
|
|
|
|
# Notice signature
|
|
form144['notice_signature'] = NoticeSignature.from_tag(form_data.find('noticeSignature'))
|
|
return form144
|
|
|
|
@classmethod
|
|
def from_filing(cls, filing):
|
|
assert filing.form in ['144', '144/A'], f"This form should be a Form 144 but was {filing.form}"
|
|
xml = filing.xml()
|
|
|
|
if xml:
|
|
form144 = cls.parse_xml(xml)
|
|
return cls(filing=filing, **form144)
|
|
|
|
def __rich__(self):
|
|
# Filer Information Table
|
|
filer_table = Table("Form", "Person Selling", "Relationship", "Company", "Issuer", box=box.SIMPLE,
|
|
row_styles=["", "dim"])
|
|
filer_table.add_row(f"Form {self._filing.form}", self.person_selling, ', '.join(self.relationships),
|
|
self._filing.company, self.issuer_name)
|
|
|
|
# Securities Information Table
|
|
securities_information_table = Table("Security Class",
|
|
"Date of Sale",
|
|
Column(header="Units To Be Sold", style="red1"),
|
|
"Market Value",
|
|
"Shares outstanding",
|
|
"Exchange",
|
|
"Broker",
|
|
box=box.SIMPLE, row_styles=["", "dim"])
|
|
for row in self.securities_information.itertuples():
|
|
securities_information_table.add_row(row.security_class,
|
|
row.approx_sale_date,
|
|
f"{row.units_to_be_sold:,}",
|
|
f"${row.market_value:,.0f}",
|
|
f"{row.units_outstanding:,}",
|
|
row.exchange_name,
|
|
row.broker_name)
|
|
|
|
# Securities to be sold
|
|
securities_to_be_sold_table = Table("Security Class",
|
|
"Date Acquired",
|
|
Column(header="Units Acquired", style="green"),
|
|
"Nature of acquistion",
|
|
"Acquired From",
|
|
"Gift",
|
|
"Payment Date",
|
|
"Nature of Payment",
|
|
box=box.SIMPLE,
|
|
row_styles=["", "dim"])
|
|
for row in self.securities_to_be_sold.itertuples():
|
|
securities_to_be_sold_table.add_row(row.security_class,
|
|
row.acquired_date,
|
|
f"{row.amount_acquired:,}",
|
|
row.nature_of_acquisition,
|
|
row.acquired_from,
|
|
row.is_gift,
|
|
row.payment_date,
|
|
row.nature_of_payment)
|
|
|
|
# Securities sold in past 3 months
|
|
securities_sold_past_3_months_table = Table("Security Class", "Sale Date",
|
|
Column(header="Amount Sold", style="red1"),
|
|
"Proceeds", "Seller Name", box=box.SIMPLE,
|
|
row_styles=["", "dim"])
|
|
for row in self.securities_sold_past_3_months.itertuples():
|
|
securities_sold_past_3_months_table.add_row(row.security_class,
|
|
row.sale_date,
|
|
f"{row.amount_sold:,}",
|
|
f"${row.gross_proceeds:,.2f}",
|
|
row.seller_name
|
|
)
|
|
|
|
# Notice signature
|
|
notice_signature_table = Table("Signature", "Date", box=box.SIMPLE, )
|
|
notice_signature_table.add_row(self.notice_signature.signature, self.notice_signature.notice_date)
|
|
|
|
# Plan adoption dates
|
|
plan_adoption_dates_table = Table("Date", box=box.SIMPLE, title="Plan Adoption Dates")
|
|
if len(self.notice_signature.plan_adoption_dates) == 0:
|
|
plan_adoption_dates_table.add_row(" " * 20)
|
|
else:
|
|
for date in self.notice_signature.plan_adoption_dates:
|
|
plan_adoption_dates_table.add_row(date)
|
|
return Panel(
|
|
Group(
|
|
filer_table,
|
|
Panel(securities_information_table, title="Securities Information"),
|
|
Panel(securities_to_be_sold_table, title="Securities To Be Sold"),
|
|
Panel(securities_sold_past_3_months_table, title="Securities Sold During The Past 3 Months"),
|
|
notice_signature_table,
|
|
Text(self.remarks or ""),
|
|
)
|
|
)
|
|
|
|
def __repr__(self):
|
|
return repr_rich(self.__rich__())
|
|
|
|
|
|
def concat_securities_information(form144_lst: List[Form144]):
|
|
return pd.concat([form144.securities_information for form144 in form144_lst])
|
|
|
|
|
|
def concat_securities_to_be_sold(form144_lst: List[Form144]):
|
|
return pd.concat([form144.securities_to_be_sold for form144 in form144_lst])
|