sec-edgar-toolkit
Version:
Open source toolkit to facilitate working with the SEC EDGAR database
449 lines • 19 kB
JavaScript
"use strict";
/**
* Parser for SEC ownership forms (Form 3, 4, and 5) XML documents.
*
* These forms contain information about insider transactions and holdings
* by officers, directors, and significant shareholders.
*
* Form 3: Initial statement of beneficial ownership
* Form 4: Changes in beneficial ownership
* Form 5: Annual statement of changes in beneficial ownership
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Form5Parser = exports.Form4Parser = exports.OwnershipFormParser = exports.OwnershipFormParseError = void 0;
const fast_xml_parser_1 = require("fast-xml-parser");
const ownership_forms_1 = require("../types/ownership-forms");
Object.defineProperty(exports, "OwnershipFormParseError", { enumerable: true, get: function () { return ownership_forms_1.OwnershipFormParseError; } });
class OwnershipFormParser {
/**
* Initialize the ownership form parser.
* @param xmlContent Raw XML content of the form
* @throws OwnershipFormParseError If XML parsing fails
*/
constructor(xmlContent) {
this.xmlContent = xmlContent;
try {
const parser = new fast_xml_parser_1.XMLParser({
ignoreAttributes: false,
parseTagValue: false, // Don't auto-parse numbers to preserve leading zeros
trimValues: true,
});
const xmlString = typeof xmlContent === 'string'
? xmlContent
: xmlContent.toString('utf-8');
this.root = parser.parse(xmlString);
}
catch (error) {
throw new ownership_forms_1.OwnershipFormParseError(`Failed to parse XML: ${error.message}`);
}
this.formType = this.extractFormType();
console.info(`Initialized parser for Form ${this.formType}`);
}
/**
* Extract the form type from the XML document.
*/
extractFormType() {
// Navigate through possible wrapper elements
const doc = this.root.ownershipDocument || this.root;
// Try multiple possible locations for form type
const documentType = this.findNestedValue(doc, ['documentType']);
if (documentType) {
return documentType.toString().trim();
}
// Fallback: check schemaVersion or other indicators
const schemaVersion = this.findNestedValue(doc, ['schemaVersion']);
if (schemaVersion) {
// Assume it's a Form 4 if we can't find explicit type
return '4';
}
throw new ownership_forms_1.OwnershipFormParseError('Could not determine form type from XML');
}
/**
* Find a nested value in the XML structure
*/
findNestedValue(obj, path) {
let current = obj;
for (const key of path) {
if (!current)
return null;
// Direct property
if (current[key] !== undefined) {
current = current[key];
continue;
}
// Search in all properties (case insensitive)
let found = false;
for (const prop in current) {
if (prop.toLowerCase() === key.toLowerCase()) {
current = current[prop];
found = true;
break;
}
}
if (!found)
return null;
}
return current;
}
/**
* Safely extract text from an XML element.
*/
getText(element, defaultValue = '') {
if (!element)
return defaultValue;
if (typeof element === 'string' || typeof element === 'number') {
return element.toString().trim();
}
if (element.value !== undefined) {
return this.getText(element.value, defaultValue);
}
if (element['#text'] !== undefined) {
return element['#text'].toString().trim();
}
return defaultValue;
}
/**
* Safely extract float value from an XML element.
*/
getFloat(element, defaultValue = 0.0) {
const text = this.getText(element);
if (!text)
return defaultValue;
const parsed = parseFloat(text);
return isNaN(parsed) ? defaultValue : parsed;
}
/**
* Extract date from XML element and convert to Date object.
*/
getDate(element) {
const dateText = this.getText(element);
if (!dateText)
return null;
// Try different date formats
const dateFormats = [
/^\d{4}-\d{2}-\d{2}$/, // 2024-01-15
/^\d{1,2}\/\d{1,2}\/\d{4}$/, // 01/15/2024 or 1/15/2024
/^\d{1,2}-\d{1,2}-\d{4}$/, // 01-15-2024 or 1-15-2024
];
// Check which format matches
if (dateFormats[0].test(dateText)) {
return new Date(dateText);
}
else if (dateFormats[1].test(dateText)) {
const [month, day, year] = dateText.split('/');
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
}
else if (dateFormats[2].test(dateText)) {
const [month, day, year] = dateText.split('-');
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
}
console.warn(`Could not parse date: ${dateText}`);
return null;
}
/**
* Parse document-level information from the form.
*/
parseDocumentInfo() {
const doc = this.root.ownershipDocument || this.root;
const periodOfReport = this.findNestedValue(doc, ['periodOfReport']);
const dateOfOriginalSubmission = this.findNestedValue(doc, ['dateOfOriginalSubmission']);
const notSubjectToSection16 = this.findNestedValue(doc, ['notSubjectToSection16']);
return {
formType: this.formType,
schemaVersion: this.getText(this.findNestedValue(doc, ['schemaVersion'])),
documentType: this.getText(this.findNestedValue(doc, ['documentType'])),
periodOfReport: this.getDate(periodOfReport),
dateOfOriginalSubmission: this.getDate(dateOfOriginalSubmission),
...(notSubjectToSection16 !== null && {
notSubjectToSection16: this.getText(notSubjectToSection16).toLowerCase() === 'true'
})
};
}
/**
* Parse information about the issuer (company) from the form.
*/
parseIssuerInfo() {
const doc = this.root.ownershipDocument || this.root;
const issuer = this.findNestedValue(doc, ['issuer']);
if (!issuer) {
return { cik: '', name: '', tradingSymbol: '' };
}
return {
cik: this.getText(issuer.issuerCik || ''),
name: this.getText(issuer.issuerName || ''),
tradingSymbol: this.getText(issuer.issuerTradingSymbol || ''),
};
}
/**
* Parse information about the reporting owner (insider) from the form.
*/
parseReportingOwnerInfo() {
const doc = this.root.ownershipDocument || this.root;
const reportingOwner = this.findNestedValue(doc, ['reportingOwner']);
if (!reportingOwner) {
return {
cik: '',
name: '',
street1: '',
street2: '',
city: '',
state: '',
zipCode: '',
stateDescription: '',
};
}
const ownerInfo = {
cik: '',
name: '',
street1: '',
street2: '',
city: '',
state: '',
zipCode: '',
stateDescription: '',
};
// Parse owner identification
const ownerId = reportingOwner.reportingOwnerId;
if (ownerId) {
ownerInfo.cik = this.getText(ownerId.rptOwnerCik || '');
ownerInfo.name = this.getText(ownerId.rptOwnerName || '');
ownerInfo.street1 = this.getText(ownerId.rptOwnerStreet1 || '');
ownerInfo.street2 = this.getText(ownerId.rptOwnerStreet2 || '');
ownerInfo.city = this.getText(ownerId.rptOwnerCity || '');
ownerInfo.state = this.getText(ownerId.rptOwnerState || '');
ownerInfo.zipCode = this.getText(ownerId.rptOwnerZipCode || '');
ownerInfo.stateDescription = this.getText(ownerId.rptOwnerStateDescription || '');
}
// Parse owner relationship
const relationship = reportingOwner.reportingOwnerRelationship;
if (relationship) {
ownerInfo.relationship = {
isDirector: this.getText(relationship.isDirector || '').toLowerCase() === 'true',
isOfficer: this.getText(relationship.isOfficer || '').toLowerCase() === 'true',
isTenPercentOwner: this.getText(relationship.isTenPercentOwner || '').toLowerCase() === 'true',
isOther: this.getText(relationship.isOther || '').toLowerCase() === 'true',
officerTitle: this.getText(relationship.officerTitle || ''),
otherText: this.getText(relationship.otherText || ''),
};
}
return ownerInfo;
}
/**
* Parse non-derivative transactions from the form.
*/
parseNonDerivativeTransactions() {
const doc = this.root.ownershipDocument || this.root;
// Look for transactions in nonDerivativeTable or directly in the document
const nonDerivativeTable = this.findNestedValue(doc, ['nonDerivativeTable']);
let transactionElements;
if (nonDerivativeTable) {
transactionElements = nonDerivativeTable.nonDerivativeTransaction || [];
}
else {
// Try to find transactions directly in the document
transactionElements = doc.nonDerivativeTransaction || [];
}
const transactions = [];
const transactionArray = Array.isArray(transactionElements) ? transactionElements : [transactionElements];
for (const transElem of transactionArray) {
if (!transElem)
continue;
const transaction = {
securityTitle: '',
transactionDate: null,
shares: 0,
pricePerShare: 0,
acquiredDisposedCode: '',
};
// Security title
const security = transElem.securityTitle;
if (security) {
transaction.securityTitle = this.getText(security.value || security);
}
// Transaction date
const transDate = transElem.transactionDate;
if (transDate) {
transaction.transactionDate = this.getDate(transDate.value || transDate);
}
// Transaction amounts
const amounts = transElem.transactionAmounts;
if (amounts) {
transaction.shares = this.getFloat(amounts.transactionShares?.value || amounts.transactionShares);
transaction.pricePerShare = this.getFloat(amounts.transactionPricePerShare?.value || amounts.transactionPricePerShare);
transaction.acquiredDisposedCode = this.getText(amounts.transactionAcquiredDisposedCode?.value || amounts.transactionAcquiredDisposedCode);
}
// Transaction coding
const coding = transElem.transactionCoding;
if (coding) {
transaction.formType = this.getText(coding.transactionFormType);
transaction.code = this.getText(coding.transactionCode);
transaction.equitySwapInvolved = this.getText(coding.equitySwapInvolved).toLowerCase() === 'true';
}
// Post-transaction amounts
const postTrans = transElem.postTransactionAmounts;
if (postTrans) {
transaction.sharesOwnedFollowingTransaction = this.getFloat(postTrans.sharesOwnedFollowingTransaction?.value || postTrans.sharesOwnedFollowingTransaction);
transaction.directOrIndirectOwnership = this.getText(postTrans.directOrIndirectOwnership?.value || postTrans.directOrIndirectOwnership);
}
// Ownership nature
const ownership = transElem.ownershipNature;
if (ownership) {
transaction.natureOfOwnership = this.getText(ownership.value || ownership);
}
transactions.push(transaction);
}
return transactions;
}
/**
* Parse non-derivative holdings from the form.
*/
parseNonDerivativeHoldings() {
const doc = this.root.ownershipDocument || this.root;
const nonDerivativeTable = this.findNestedValue(doc, ['nonDerivativeTable']);
if (!nonDerivativeTable)
return [];
const holdings = [];
const holdingElements = nonDerivativeTable.nonDerivativeHolding || [];
const holdingArray = Array.isArray(holdingElements) ? holdingElements : [holdingElements];
for (const holdingElem of holdingArray) {
if (!holdingElem)
continue;
const holding = {
securityTitle: '',
sharesOwned: 0,
directOrIndirectOwnership: '',
};
// Security title
const security = holdingElem.securityTitle;
if (security) {
holding.securityTitle = this.getText(security.value || security);
}
// Shares owned
const shares = holdingElem.sharesOwned;
if (shares) {
holding.sharesOwned = this.getFloat(shares.value || shares);
}
// Direct or indirect ownership
const ownershipType = holdingElem.directOrIndirectOwnership;
if (ownershipType) {
holding.directOrIndirectOwnership = this.getText(ownershipType.value || ownershipType);
}
// Nature of ownership
const nature = holdingElem.ownershipNature;
if (nature) {
holding.natureOfOwnership = this.getText(nature.value || nature);
}
holdings.push(holding);
}
return holdings;
}
/**
* Parse derivative transactions (options, warrants, etc.) from the form.
*/
parseDerivativeTransactions() {
const doc = this.root.ownershipDocument || this.root;
const derivativeTable = this.findNestedValue(doc, ['derivativeTable']);
if (!derivativeTable)
return [];
const transactions = [];
const transactionElements = derivativeTable.derivativeTransaction || [];
const transactionArray = Array.isArray(transactionElements) ? transactionElements : [transactionElements];
for (const transElem of transactionArray) {
if (!transElem)
continue;
const transaction = {
securityTitle: '',
conversionOrExercisePrice: 0,
transactionDate: null,
shares: 0,
totalValue: 0,
acquiredDisposedCode: '',
};
// Security title
const security = transElem.securityTitle;
if (security) {
transaction.securityTitle = this.getText(security.value || security);
}
// Conversion or exercise price
const conversion = transElem.conversionOrExercisePrice;
if (conversion) {
transaction.conversionOrExercisePrice = this.getFloat(conversion.value || conversion);
}
// Transaction date
const transDate = transElem.transactionDate;
if (transDate) {
transaction.transactionDate = this.getDate(transDate.value || transDate);
}
// Transaction amounts
const amounts = transElem.transactionAmounts;
if (amounts) {
transaction.shares = this.getFloat(amounts.transactionShares?.value || amounts.transactionShares);
transaction.totalValue = this.getFloat(amounts.transactionTotalValue?.value || amounts.transactionTotalValue);
transaction.acquiredDisposedCode = this.getText(amounts.transactionAcquiredDisposedCode?.value || amounts.transactionAcquiredDisposedCode);
}
// Exercise date and expiration date
const exerciseDate = transElem.exerciseDate;
if (exerciseDate) {
transaction.exerciseDate = this.getDate(exerciseDate.value || exerciseDate);
}
const expirationDate = transElem.expirationDate;
if (expirationDate) {
transaction.expirationDate = this.getDate(expirationDate.value || expirationDate);
}
// Underlying security
const underlying = transElem.underlyingSecurity;
if (underlying) {
transaction.underlyingSecurity = {
title: this.getText(underlying.underlyingSecurityTitle?.value || underlying.underlyingSecurityTitle || ''),
shares: this.getFloat(underlying.underlyingSecurityShares?.value || underlying.underlyingSecurityShares),
};
}
transactions.push(transaction);
}
return transactions;
}
/**
* Parse all information from the ownership form.
*/
parseAll() {
return {
documentInfo: this.parseDocumentInfo(),
issuerInfo: this.parseIssuerInfo(),
reportingOwnerInfo: this.parseReportingOwnerInfo(),
nonDerivativeTransactions: this.parseNonDerivativeTransactions(),
nonDerivativeHoldings: this.parseNonDerivativeHoldings(),
derivativeTransactions: this.parseDerivativeTransactions(),
};
}
}
exports.OwnershipFormParser = OwnershipFormParser;
/**
* Specialized parser for Form 4 (Changes in Beneficial Ownership).
* Form 4 must be filed within 2 business days of a transaction.
*/
class Form4Parser extends OwnershipFormParser {
constructor(xmlContent) {
super(xmlContent);
const form = this.parseDocumentInfo();
if (form.formType !== '4') {
console.warn(`Expected Form 4, but found Form ${form.formType}`);
}
}
}
exports.Form4Parser = Form4Parser;
/**
* Specialized parser for Form 5 (Annual Statement of Changes in Beneficial Ownership).
* Form 5 is filed annually and reports transactions that were exempt from
* Form 4 reporting requirements.
*/
class Form5Parser extends OwnershipFormParser {
constructor(xmlContent) {
super(xmlContent);
const form = this.parseDocumentInfo();
if (form.formType !== '5') {
console.warn(`Expected Form 5, but found Form ${form.formType}`);
}
}
}
exports.Form5Parser = Form5Parser;
//# sourceMappingURL=ownership-forms.js.map