UNPKG

sec-edgar-toolkit

Version:

Open source toolkit to facilitate working with the SEC EDGAR database

375 lines 16 kB
"use strict"; /** * Parser for 8-K current events documents. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CurrentEventParser = void 0; class CurrentEventParser { constructor(rawContent) { this.rawContent = rawContent; } /** * Parse all current events from the 8-K filing */ parseAll() { const header = this.parseHeader(); return { formType: header.formType, filingDate: header.filingDate, cik: header.cik, companyName: header.companyName, ticker: header.ticker, events: this.getCurrentEvents(), materialAgreements: this.getMaterialAgreements(), executiveChanges: this.getExecutiveChanges(), acquisitions: this.getAcquisitions(), earningsResults: this.getEarningsResults(), }; } /** * Extract current events from the filing */ getCurrentEvents() { const events = []; // Extract Item numbers and descriptions const itemPattern = /Item\s+(\d+\.\d+)\.?\s+([^\n\r]+)/gi; let match; while ((match = itemPattern.exec(this.rawContent)) !== null) { const itemNumber = match[1]; const description = match[2].trim(); events.push({ type: this.mapItemToEventType(itemNumber), description: description, date: this.extractFilingDate(), item: `Item ${itemNumber}`, significance: this.assessEventSignificance(description), details: { itemNumber: itemNumber, fullText: this.extractItemContent(itemNumber), }, }); } return events; } /** * Extract material agreements */ getMaterialAgreements() { const agreements = []; // Look for agreement-related content const agreementSection = this.extractSection('ITEM 1.01|MATERIAL AGREEMENT|DEFINITIVE AGREEMENT'); if (agreementSection) { // Extract the company name (from the filing) const companyMatch = this.rawContent.match(/(?:^|\n)([A-Z][A-Z\s]+(?:INC\.|CORP\.|CORPORATION|COMPANY|LLC))(?:\s|\n)/m); const companyName = companyMatch ? companyMatch[1].trim() : 'Unknown Company'; // Extract agreement type - skip the item header const contentAfterHeader = agreementSection.replace(/^ITEM\s+\d+\.\d+[^\n\r]*[\n\r]+/i, ''); const typeMatch = contentAfterHeader.match(/(?:entered into|executed|signed)\s+(?:a|an|the)?\s*([A-Za-z\s]+Agreement)/i); const agreementType = typeMatch ? typeMatch[1].trim() : 'Material Agreement'; // Find counterparty const withPattern = /(?:with|between.*?and)\s+([A-Za-z\s]+(?:Inc\.|Corp\.|Technologies|Company|LLC)?)[\s,.(]/i; const withMatch = agreementSection.match(withPattern); const counterparty = withMatch ? withMatch[1].trim() : ''; const parties = [companyName]; if (counterparty && !counterparty.toLowerCase().includes('the company')) { parties.push(counterparty); } // Try to extract the date from the agreement text const dateMatch = contentAfterHeader.match(/On\s+([A-Za-z]+ \d+, \d+)/); const effectiveDate = dateMatch ? new Date(dateMatch[1]) : this.extractFilingDate(); agreements.push({ type: agreementType, parties: parties, effectiveDate: effectiveDate, description: agreementSection.substring(0, 500), value: this.extractAgreementValue(agreementSection), currency: 'USD', }); } return agreements; } /** * Extract executive changes */ getExecutiveChanges() { const changes = []; // Look for executive change content const changeSection = this.extractSection('ITEM 5.02|DEPARTURE.*DIRECTOR|APPOINTMENT.*OFFICER'); if (changeSection) { // Look for departures/resignations const departurePattern = /(?:\(b\))\s*On\s+[A-Za-z]+ \d+, \d+,\s*([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+),.*?(?:notified|informed|announced|resignation|retire|departure)/gi; let match; while ((match = departurePattern.exec(changeSection)) !== null) { const name = match[1].trim(); const position = this.extractPosition(changeSection, name); const effectiveDate = this.extractDateAfterName(changeSection, name); changes.push({ type: 'resignation', person: { name: name, position: position, }, effectiveDate: effectiveDate || this.extractFilingDate(), }); } // Look for appointments const appointmentPattern = /(?:appointed|named)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)(?:,|\s+as|\s+to)/gi; while ((match = appointmentPattern.exec(changeSection)) !== null) { const name = match[1].trim(); const position = this.extractPosition(changeSection, name); const effectiveDate = this.extractDateAfterName(changeSection, name); changes.push({ type: 'appointment', person: { name: name, position: position, }, effectiveDate: effectiveDate || this.extractFilingDate(), }); } } return changes; } /** * Extract acquisitions and mergers */ getAcquisitions() { const acquisitions = []; // Look for acquisition content const acquisitionSection = this.extractSection('ITEM 2.01|ACQUISITION|MERGER|DIVESTITURE'); if (acquisitionSection) { acquisitions.push({ type: 'acquisition', target: { name: this.extractTargetName(acquisitionSection), description: acquisitionSection.substring(0, 300), }, value: this.extractTransactionValue(acquisitionSection), currency: 'USD', expectedClosingDate: null, status: 'announced', }); } return acquisitions; } /** * Extract earnings results */ getEarningsResults() { // Look for earnings-related content const earningsSection = this.extractSection('ITEM 2.02|RESULTS.*OPERATIONS|EARNINGS'); if (!earningsSection) { return null; } return { period: this.extractReportingPeriod(), revenue: this.extractMetric(earningsSection, 'revenue|sales'), netIncome: this.extractMetric(earningsSection, 'net income|earnings'), earningsPerShare: this.extractMetric(earningsSection, 'earnings per share|EPS'), guidance: this.extractGuidance(earningsSection), }; } // Helper methods parseHeader() { const cikMatch = this.rawContent.match(/CENTRAL INDEX KEY:\s*(\d+)/); const companyMatch = this.rawContent.match(/COMPANY CONFORMED NAME:\s*([^\n\r]+)/); const formTypeMatch = this.rawContent.match(/FORM TYPE:\s*([^\n\r]+)/); const filingDateMatch = this.rawContent.match(/FILED AS OF DATE:\s*(\d{8})/); return { cik: cikMatch?.[1] || '', companyName: companyMatch?.[1]?.trim() || '', formType: formTypeMatch?.[1]?.trim() || '', ticker: '', // Would need to extract from trading symbol filingDate: this.parseDate(filingDateMatch?.[1]), }; } extractSection(pattern) { const regex = new RegExp(pattern, 'i'); const match = this.rawContent.match(regex); if (!match) return ''; const startIndex = match.index || 0; // Look for next Item (case-insensitive) const nextItemMatch = this.rawContent.substring(startIndex + 1).match(/Item\s+\d+\.\d+/i); const endIndex = nextItemMatch ? startIndex + 1 + (nextItemMatch.index || 0) : -1; return this.rawContent.substring(startIndex, endIndex > 0 ? endIndex : startIndex + 10000); } mapItemToEventType(itemNumber) { const itemMap = { '1.01': 'Material Agreement', '1.02': 'Termination of Material Agreement', '2.01': 'Acquisition or Disposition', '2.02': 'Results of Operations', '3.01': 'Notice of Delisting', '5.02': 'Executive Changes', '8.01': 'Other Events', }; return itemMap[itemNumber] || 'Other Event'; } assessEventSignificance(description) { const highKeywords = ['acquisition', 'merger', 'bankruptcy', 'material adverse']; const mediumKeywords = ['agreement', 'executive', 'earnings', 'results']; const text = description.toLowerCase(); if (highKeywords.some(keyword => text.includes(keyword))) { return 'high'; } else if (mediumKeywords.some(keyword => text.includes(keyword))) { return 'medium'; } return 'low'; } extractItemContent(itemNumber) { const escapedNumber = itemNumber.replace('.', '\\.'); const itemPattern = new RegExp(`Item\\s+${escapedNumber}\\s+[^\\n\\r]+`, 'gi'); const match = this.rawContent.match(itemPattern); if (!match) return ''; // Just return first 10 words for brevity const words = match[0].split(/\s+/); return words.slice(0, 10).join(' '); } extractFilingDate() { const dateMatch = this.rawContent.match(/FILED AS OF DATE:\s*(\d{8})/); return this.parseDate(dateMatch?.[1]); } parseDate(dateStr) { if (!dateStr) return new Date(); const year = parseInt(dateStr.substring(0, 4)); const month = parseInt(dateStr.substring(4, 6)) - 1; const day = parseInt(dateStr.substring(6, 8)); return new Date(year, month, day); } extractAgreementValue(text) { const valuePattern = /\$([0-9,]+(?:\.[0-9]+)?)\s*(?:million|billion)?/i; const match = text.match(valuePattern); if (match) { let value = parseFloat(match[1].replace(/,/g, '')); if (text.toLowerCase().includes('million')) { value *= 1000000; } else if (text.toLowerCase().includes('billion')) { value *= 1000000000; } return value; } return null; } extractPosition(text, name) { // Replace newlines with spaces for easier matching const cleanText = text.replace(/\n/g, ' ').replace(/\s+/g, ' '); // Check if this is an appointment (contains "appointed" before the name) const isAppointment = cleanText.toLowerCase().indexOf('appointed') < cleanText.indexOf(name) && cleanText.toLowerCase().indexOf('appointed') > -1; // Look for patterns that include the full position title const patterns = isAppointment ? [ // For appointments: "Jane Doe, age 45, as the new Senior Vice President..." new RegExp(`${name}(?:,\\s*age\\s*\\d+,)?.*?as\\s+(?:the\\s+)?(?:new\\s+)?([^,\\.]+?)(?:,|\\seffective|\\.)`, 'i'), ] : [ // For departures: "John Smith, Senior Vice President..." new RegExp(`${name},\\s*([^,]+?)\\s*(?:,|notified|informed|announced)`, 'i'), // "Senior Vice President John Smith" new RegExp(`([^,]*(?:Vice President|President|Director|Officer|Manager)[^,]*),?\\s*${name}`, 'i') ]; for (const pattern of patterns) { const match = cleanText.match(pattern); if (match && match[1]) { const position = match[1].trim(); // Only return if it looks like a complete position if (position.length > 5 && position.match(/\w+\s+\w+/)) { return position; } } } // Fallback return 'Executive'; } extractTargetName(text) { const targetPattern = /(?:acquire|purchase|merger with)\s+([A-Z][^,.]{10,50})/i; const match = text.match(targetPattern); return match?.[1]?.trim() || 'Target Company'; } extractTransactionValue(text) { return this.extractAgreementValue(text); } extractReportingPeriod() { // Look for explicit period mention in earnings section const periodMatch = this.rawContent.match(/(?:quarter|period|year)\s+ended\s+([A-Za-z]+ \d+, \d+)/i); if (periodMatch) { return periodMatch[1]; } const conformedPeriodMatch = this.rawContent.match(/CONFORMED PERIOD OF REPORT:\s*(\d{8})/); if (conformedPeriodMatch) { const date = this.parseDate(conformedPeriodMatch[1]); return `Q${Math.ceil((date.getMonth() + 1) / 3)} ${date.getFullYear()}`; } return 'Current Period'; } extractMetric(text, pattern) { // Try different formats for monetary values const patterns = [ new RegExp(`(?:${pattern}).*?\\$([0-9,]+(?:\\.[0-9]+)?)\\s*billion`, 'i'), new RegExp(`(?:${pattern}).*?\\$([0-9,]+(?:\\.[0-9]+)?)\\s*million`, 'i'), new RegExp(`(?:${pattern}).*?\\$([0-9,]+(?:\\.[0-9]+)?)`, 'i'), new RegExp(`(?:${pattern}).*?([0-9,]+(?:\\.[0-9]+)?)\\s*billion`, 'i'), new RegExp(`(?:${pattern}).*?([0-9,]+(?:\\.[0-9]+)?)\\s*million`, 'i') ]; const multipliers = { billion: 1000000000, million: 1000000, default: 1 }; for (const patternRegex of patterns) { const match = text.match(patternRegex); if (match && match[1]) { const value = parseFloat(match[1].replace(/,/g, '')); const matchStr = match[0].toLowerCase(); if (matchStr.includes('billion')) { return value * multipliers.billion; } else if (matchStr.includes('million')) { return value * multipliers.million; } else { return value; } } } return null; } extractGuidance(text) { const guidance = []; const guidancePattern = /(?:guidance|expects?).*?([a-zA-Z\s]+).*?(\$[0-9,]+|\d+%)/gi; let match; while ((match = guidancePattern.exec(text)) !== null) { guidance.push({ metric: match[1].trim(), value: match[2], }); } return guidance; } extractDateAfterName(text, name) { // Look for "effective [date]" pattern after the name const patterns = [ new RegExp(`effective\\s+([A-Za-z]+ \\d+, \\d+)`, 'i'), new RegExp(`retire.*?effective\\s+([A-Za-z]+ \\d+, \\d+)`, 'i'), new RegExp(`resignation.*?effective\\s+([A-Za-z]+ \\d+, \\d+)`, 'i'), ]; // Find the position of the name const nameIndex = text.indexOf(name); if (nameIndex === -1) return null; // Search for date pattern after the name const textAfterName = text.substring(nameIndex); for (const pattern of patterns) { const match = textAfterName.match(pattern); if (match && match[1]) { return new Date(match[1]); } } return null; } } exports.CurrentEventParser = CurrentEventParser; //# sourceMappingURL=current-events.js.map