sec-edgar-toolkit
Version:
Open source toolkit to facilitate working with the SEC EDGAR database
404 lines • 17.5 kB
JavaScript
"use strict";
/**
* Parser for financial forms (10-K, 10-Q) documents.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.FinancialFormParser = void 0;
class FinancialFormParser {
constructor(rawContent) {
this.document = null;
this.rawContent = rawContent;
}
/**
* Parse financial statements from the filing
*/
getFinancialStatements() {
return {
balanceSheet: this.parseBalanceSheet(),
incomeStatement: this.parseIncomeStatement(),
cashFlowStatement: this.parseCashFlowStatement(),
};
}
/**
* Parse balance sheet data
*/
parseBalanceSheet() {
const balanceSheetSection = this.extractSection('BALANCE SHEET|CONSOLIDATED BALANCE SHEET');
return {
assets: {
currentAssets: this.extractBalanceSheetItems(balanceSheetSection, 'current.*assets'),
nonCurrentAssets: this.extractBalanceSheetItems(balanceSheetSection, 'non.?current.*assets|property.*plant.*equipment'),
totalAssets: this.extractBalanceSheetItem(balanceSheetSection, 'total.*assets'),
},
liabilities: {
currentLiabilities: this.extractBalanceSheetItems(balanceSheetSection, 'current.*liabilities'),
nonCurrentLiabilities: this.extractBalanceSheetItems(balanceSheetSection, 'non.?current.*liabilities|long.?term.*debt'),
totalLiabilities: this.extractBalanceSheetItem(balanceSheetSection, 'total.*liabilities'),
},
equity: {
totalEquity: this.extractBalanceSheetItem(balanceSheetSection, 'total.*equity|shareholders.*equity'),
retainedEarnings: this.extractBalanceSheetItem(balanceSheetSection, 'retained.*earnings'),
},
};
}
/**
* Parse income statement data
*/
parseIncomeStatement() {
const incomeSection = this.extractSection('INCOME STATEMENT|CONSOLIDATED STATEMENT.*OPERATIONS|STATEMENT.*EARNINGS');
return {
revenue: this.extractIncomeStatementItem(incomeSection, 'revenue|net.*sales|total.*revenue'),
grossProfit: this.extractIncomeStatementItem(incomeSection, 'gross.*profit|gross.*margin'),
operatingIncome: this.extractIncomeStatementItem(incomeSection, 'operating.*income|income.*from.*operations'),
netIncome: this.extractIncomeStatementItem(incomeSection, 'net.*income|net.*earnings'),
earningsPerShare: this.extractIncomeStatementItem(incomeSection, 'earnings.*per.*share|basic.*earnings.*per.*share'),
operatingExpenses: this.extractIncomeStatementItems(incomeSection, 'operating.*expenses|research.*development|sales.*marketing'),
};
}
/**
* Parse cash flow statement data
*/
parseCashFlowStatement() {
const cashFlowSection = this.extractSection('CASH FLOW|CONSOLIDATED STATEMENT.*CASH.*FLOW');
return {
operatingActivities: this.extractCashFlowItems(cashFlowSection, 'operating.*activities|cash.*from.*operations'),
investingActivities: this.extractCashFlowItems(cashFlowSection, 'investing.*activities|cash.*from.*investing'),
financingActivities: this.extractCashFlowItems(cashFlowSection, 'financing.*activities|cash.*from.*financing'),
netCashFlow: this.extractCashFlowItem(cashFlowSection, 'net.*cash.*flow|net.*increase.*decrease.*cash'),
};
}
/**
* Extract business segments information
*/
getBusinessSegments() {
const segmentSection = this.extractSection('SEGMENT|BUSINESS.*SEGMENT|GEOGRAPHIC.*INFORMATION');
const segments = [];
// Extract segment data using pattern matching
const segmentPattern = /(\w+(?:\s+\w+)*)\s+segment.*?revenue.*?\$?([\d,]+)/gi;
let match;
while ((match = segmentPattern.exec(segmentSection)) !== null) {
segments.push({
name: match[1].trim(),
revenue: this.parseNumber(match[2]),
operatingIncome: 0, // Would need more sophisticated parsing
assets: 0, // Would need more sophisticated parsing
description: '',
});
}
return segments;
}
/**
* Extract risk factors
*/
getRiskFactors() {
const riskSection = this.extractSection('RISK FACTORS|ITEM 1A');
const riskFactors = [];
// Split by common risk factor patterns
const factors = riskSection.split(/(?=•|·|\d+\.|\n\n)/).filter(f => f.trim().length > 50);
factors.forEach(factor => {
const severity = this.assessRiskSeverity(factor);
riskFactors.push({
category: this.extractRiskCategory(factor),
description: factor.trim().substring(0, 500),
severity,
});
});
return riskFactors.slice(0, 10); // Limit to top 10 risk factors
}
/**
* Extract Management Discussion & Analysis sections
*/
getManagementDiscussion() {
const mdaSection = this.extractSection('MANAGEMENT.*DISCUSSION|ITEM 2|MD&A');
const sections = [];
// Split into subsections
const subsections = mdaSection.split(/(?=OVERVIEW|RESULTS OF OPERATIONS|FINANCIAL CONDITION|LIQUIDITY)/i);
subsections.forEach(section => {
if (section.trim().length > 100) {
const title = this.extractSectionTitle(section);
sections.push({
title,
content: section.trim().substring(0, 1000),
keyMetrics: this.extractKeyMetrics(section),
});
}
});
return sections;
}
/**
* Extract XBRL facts (simplified version)
*/
getXBRLFacts() {
const xbrlFacts = [];
// Extract XBRL data if present
const xbrlPattern = /<ix:nonFraction[^>]*name="([^"]*)"[^>]*contextRef="([^"]*)"[^>]*decimals="([^"]*)"[^>]*unitRef="([^"]*)"[^>]*>([^<]*)</gi;
let match;
while ((match = xbrlPattern.exec(this.rawContent)) !== null) {
xbrlFacts.push({
name: match[1],
value: this.parseNumber(match[5]),
units: match[4],
contextRef: match[2],
decimals: parseInt(match[3]) || 0,
period: this.extractPeriodFromContext(match[2]),
});
}
return xbrlFacts;
}
/**
* Calculate financial metrics
*/
getFinancialMetrics() {
const balanceSheet = this.parseBalanceSheet();
const incomeStatement = this.parseIncomeStatement();
return {
marketCap: null, // Would need stock price data
peRatio: null, // Would need stock price data
debtToEquity: this.calculateDebtToEquity(balanceSheet),
returnOnEquity: this.calculateROE(incomeStatement, balanceSheet),
currentRatio: this.calculateCurrentRatio(balanceSheet),
quickRatio: this.calculateQuickRatio(balanceSheet),
};
}
/**
* Parse complete financial form
*/
parseAll() {
const header = this.parseHeader();
const financialStatements = this.getFinancialStatements();
return {
formType: header.formType,
filingDate: header.filingDate,
periodEndDate: header.periodEndDate,
cik: header.cik,
companyName: header.companyName,
ticker: header.ticker,
balanceSheet: financialStatements.balanceSheet,
incomeStatement: financialStatements.incomeStatement,
cashFlowStatement: financialStatements.cashFlowStatement,
businessSegments: this.getBusinessSegments(),
riskFactors: this.getRiskFactors(),
managementDiscussion: this.getManagementDiscussion(),
xbrlFacts: this.getXBRLFacts(),
financialMetrics: this.getFinancialMetrics(),
};
}
// Helper methods
extractSection(pattern) {
const regex = new RegExp(pattern, 'i');
const match = this.rawContent.match(regex);
if (!match)
return '';
const startIndex = match.index || 0;
const endIndex = this.rawContent.indexOf('</DOCUMENT>', startIndex);
return this.rawContent.substring(startIndex, endIndex > 0 ? endIndex : startIndex + 50000);
}
extractBalanceSheetItems(section, pattern) {
const items = [];
const regex = new RegExp(pattern + '.*?\\$([\\d,]+)', 'gi');
let match;
while ((match = regex.exec(section)) !== null) {
items.push({
label: match[0].split('$')[0].trim(),
value: this.parseNumber(match[1]),
units: 'USD',
period: this.extractPeriod(section),
filed: this.extractFilingDate(),
});
}
return items;
}
extractBalanceSheetItem(section, pattern) {
const items = this.extractBalanceSheetItems(section, pattern);
return items.length > 0 ? items[0] : null;
}
extractIncomeStatementItems(section, pattern) {
const items = [];
const regex = new RegExp(pattern + '.*?\\$([\\d,]+)', 'gi');
let match;
while ((match = regex.exec(section)) !== null) {
items.push({
label: match[0].split('$')[0].trim(),
value: this.parseNumber(match[1]),
units: 'USD',
period: this.extractPeriod(section),
filed: this.extractFilingDate(),
});
}
return items;
}
extractIncomeStatementItem(section, pattern) {
const items = this.extractIncomeStatementItems(section, pattern);
return items.length > 0 ? items[0] : null;
}
extractCashFlowItems(section, pattern) {
const items = [];
const regex = new RegExp(pattern + '.*?\\$([\\d,]+)', 'gi');
let match;
while ((match = regex.exec(section)) !== null) {
items.push({
label: match[0].split('$')[0].trim(),
value: this.parseNumber(match[1]),
units: 'USD',
period: this.extractPeriod(section),
filed: this.extractFilingDate(),
});
}
return items;
}
extractCashFlowItem(section, pattern) {
const items = this.extractCashFlowItems(section, pattern);
return items.length > 0 ? items[0] : null;
}
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})/);
const periodMatch = this.rawContent.match(/CONFORMED PERIOD OF REPORT:\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]),
periodEndDate: this.parseDate(periodMatch?.[1]),
};
}
parseNumber(value) {
if (!value)
return 0;
return parseFloat(value.replace(/[,$]/g, '')) || 0;
}
parseDate(dateStr) {
if (!dateStr)
return new Date();
// Parse YYYYMMDD format
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);
}
extractPeriod(section) {
const periodMatch = section.match(/(\d{4})/);
return periodMatch?.[1] || new Date().getFullYear().toString();
}
extractFilingDate() {
const dateMatch = this.rawContent.match(/FILED AS OF DATE:\s*(\d{8})/);
return this.parseDate(dateMatch?.[1]);
}
assessRiskSeverity(riskText) {
const highRiskKeywords = ['material adverse', 'significant risk', 'substantial risk', 'could result in'];
const mediumRiskKeywords = ['may affect', 'potential impact', 'could impact'];
const text = riskText.toLowerCase();
if (highRiskKeywords.some(keyword => text.includes(keyword))) {
return 'high';
}
else if (mediumRiskKeywords.some(keyword => text.includes(keyword))) {
return 'medium';
}
return 'low';
}
extractRiskCategory(riskText) {
const categories = [
{ keywords: ['market', 'competition', 'customer'], category: 'Market Risk' },
{ keywords: ['regulation', 'compliance', 'legal'], category: 'Regulatory Risk' },
{ keywords: ['technology', 'cyber', 'security'], category: 'Technology Risk' },
{ keywords: ['financial', 'credit', 'liquidity'], category: 'Financial Risk' },
{ keywords: ['operational', 'supply chain', 'manufacturing'], category: 'Operational Risk' },
];
const text = riskText.toLowerCase();
for (const cat of categories) {
if (cat.keywords.some(keyword => text.includes(keyword))) {
return cat.category;
}
}
return 'General Risk';
}
extractSectionTitle(section) {
const lines = section.split('\n');
for (const line of lines.slice(0, 5)) {
if (line.trim().length > 10 && line.trim().length < 100) {
return line.trim();
}
}
return 'Management Discussion';
}
extractKeyMetrics(section) {
const metrics = [];
// Extract percentage changes
const changePattern = /(\w+(?:\s+\w+)*)\s+(?:increased|decreased|changed)\s+by\s+([\d.]+%)/gi;
let match;
while ((match = changePattern.exec(section)) !== null) {
metrics.push({
metric: match[1].trim(),
value: '',
change: match[2],
});
}
return metrics.slice(0, 5);
}
extractPeriodFromContext(contextRef) {
const periodMatch = contextRef.match(/(\d{4})/);
return periodMatch?.[1] || new Date().getFullYear().toString();
}
calculateDebtToEquity(balanceSheet) {
const totalLiabilities = balanceSheet.liabilities.totalLiabilities?.value;
const totalEquity = balanceSheet.equity.totalEquity?.value;
if (totalLiabilities && totalEquity && totalEquity !== 0) {
return totalLiabilities / totalEquity;
}
return null;
}
calculateROE(incomeStatement, balanceSheet) {
const netIncome = incomeStatement.netIncome?.value;
const totalEquity = balanceSheet.equity.totalEquity?.value;
if (netIncome && totalEquity && totalEquity !== 0) {
return netIncome / totalEquity;
}
return null;
}
calculateCurrentRatio(balanceSheet) {
const currentAssets = balanceSheet.assets.currentAssets.reduce((sum, asset) => sum + asset.value, 0);
const currentLiabilities = balanceSheet.liabilities.currentLiabilities.reduce((sum, liability) => sum + liability.value, 0);
if (currentAssets && currentLiabilities && currentLiabilities !== 0) {
return currentAssets / currentLiabilities;
}
return null;
}
calculateQuickRatio(balanceSheet) {
// Calculate quick assets (current assets minus inventory and prepaid expenses)
const currentAssets = balanceSheet.assets.currentAssets;
let quickAssets = 0;
for (const asset of currentAssets) {
// Exclude inventory and prepaid expenses from quick assets
const label = asset.label.toLowerCase();
if (!label.includes('inventory') &&
!label.includes('prepaid') &&
!label.includes('deferred') &&
!label.includes('other current assets')) {
quickAssets += asset.value;
}
}
// If no specific quick assets found, use a conservative estimate
if (quickAssets === 0 && currentAssets.length > 0) {
const totalCurrentAssets = currentAssets.reduce((sum, asset) => sum + asset.value, 0);
// Find inventory value if available
const inventory = currentAssets.find(asset => asset.label.toLowerCase().includes('inventory'));
if (inventory) {
quickAssets = totalCurrentAssets - inventory.value;
}
else {
// Conservative estimate: assume 20% of current assets are inventory/prepaid
quickAssets = totalCurrentAssets * 0.8;
}
}
const currentLiabilities = balanceSheet.liabilities.currentLiabilities.reduce((sum, liability) => sum + liability.value, 0);
if (quickAssets && currentLiabilities && currentLiabilities !== 0) {
return quickAssets / currentLiabilities;
}
return null;
}
}
exports.FinancialFormParser = FinancialFormParser;
//# sourceMappingURL=financial-forms.js.map