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: """ Common stock Virtu Financial
One Liberty Plaza 165 Broadway New York NY 10006
17087 1282000.00 161514066 08/27/2022 CHX
""" 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: """ Common stock - 2 01/01/1933 Employee Stock Award -1 Issuer-1 Y 01/01/1933 17087 08/15/2021 CASH-25 """ 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: """ Virtu Financial
One Liberty Plaza 165 Broadway New York NY 10006
Common Stock 08/27/2022 0 0.00
""" 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: """ 09/08/2022 08/15/2022 08/15/2022 01/02/1933 /s/ Jan van der Velden """ 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])