UNPKG

sec-edgar-api

Version:

Fetch and parse SEC earnings reports and other filings. Useful for financial analysis.

301 lines (300 loc) 14.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var constants_1 = require("../../util/constants"); /** * Gets the fiscal period for a given date. does this by checking when the FY end periods are, * Then measures the offset from the end date to the next/previous fiscal year end. */ var FactFiscalCalculator = /** @class */ (function () { function FactFiscalCalculator(params) { var _this = this; this.endDateByYear = new Map(); this.fiscalsByEndDate = new Map(); this.datesByFiscals = new Map(); /// these get cleared after resolve this.startDateCountMap = new Map(); this.endDateCountMap = new Map(); this.filedDateCountByEndDate = new Map(); this.didResolve = false; this.fiscalYearEnd = null; var _a = params !== null && params !== void 0 ? params : {}, _b = _a.facts, facts = _b === void 0 ? [] : _b, _c = _a.filings, filings = _c === void 0 ? [] : _c, fiscalYearEnd = _a.fiscalYearEnd; this.fiscalYearEnd = fiscalYearEnd !== null && fiscalYearEnd !== void 0 ? fiscalYearEnd : null; this.useFilingsForDates({ filings: filings }); facts.forEach(function (fact) { return _this.add(fact); }); } FactFiscalCalculator.prototype.useFilingsForDates = function (params) { var _this = this; var filings = params.filings; var endDateByYear = new Map(); filings.forEach(function (_a) { var reportDate = _a.reportDate, form = _a.form; if (constants_1.FORMS_EARNINGS_ANNUAL.includes(form)) { endDateByYear.set(Number(reportDate.substring(0, 4)), new Date(reportDate)); } }); filings.forEach(function (filing) { if (constants_1.FORMS_EARNINGS.includes(filing.form)) { var _a = _this.getFiscalYearQuarter({ dateStr: filing.reportDate, endDateByYear: endDateByYear, }), quarter = _a.quarter, year = _a.year; _this.setReportDates({ accn: filing.accessionNumber, end: filing.reportDate, filed: filing.filingDate, isAnnual: filing.form === '10-K' || filing.form === '20-F', quarter: quarter, year: year, }); } }); }; FactFiscalCalculator.prototype.add = function (fact) { var _a, _b, _c, _d; var end = fact.end, filed = fact.filed, start = fact.start; if (this.didResolve) { throw new Error('Cannot add fact after resolving'); } if (this.isAnnualReportFact(fact)) { this.addAnnualReportDate(fact.end); } this.endDateCountMap.set(end, ((_a = this.endDateCountMap.get(end)) !== null && _a !== void 0 ? _a : 0) + 1); if (start) { this.startDateCountMap.set(start, ((_b = this.startDateCountMap.get(start)) !== null && _b !== void 0 ? _b : 0) + 1); } // don't record filed dates for restated facts if (this.getDaysBefore(filed, end) < 60) { var bucket = (_c = this.filedDateCountByEndDate.get(end)) !== null && _c !== void 0 ? _c : this.filedDateCountByEndDate.set(end, new Map()).get(end); bucket.set(filed, ((_d = bucket.get(filed)) !== null && _d !== void 0 ? _d : 0) + 1); } }; FactFiscalCalculator.prototype.getDaysBefore = function (dateStrAfter, dateStrBefore) { var dateAfter = new Date(dateStrAfter); var dateBefore = new Date(dateStrBefore); var dayMS = 86400000; var differenceMS = dateAfter.getTime() - dateBefore.getTime(); return differenceMS / dayMS; }; FactFiscalCalculator.prototype.setReportDates = function (params) { var year = params.year, quarter = params.quarter, end = params.end, filed = params.filed, isAnnual = params.isAnnual; var key = "".concat(year, "_").concat(quarter); if (!this.datesByFiscals.has(key)) { if (year.toString().length !== 4) return; this.datesByFiscals.set(key, { filed: filed, end: end }); } var dates = this.datesByFiscals.get(key); dates.filed = filed; dates.end = end; this.filedDateCountByEndDate.set(end, new Map([[filed, Infinity]])); this.endDateCountMap.set(end, Infinity); if (isAnnual) { this.endDateByYear.set(Number(end.split('-', 1)[0]), new Date(end)); } }; FactFiscalCalculator.prototype.addAnnualReportDate = function (reportDate) { var year = Number(reportDate.split('-', 1)[0]); var dateEnd = new Date(reportDate); this.endDateByYear.set(year, dateEnd); }; FactFiscalCalculator.prototype.isAnnualReportFact = function (params) { var end = params.end, start = params.start, filed = params.filed, period = params.period, frame = params.frame; if (frame) { // FY frames will look like CY2023 vs CY2023Q2 or CY2023Q2I return frame.length === 6; } if (period !== undefined) return period === 12; // TODO: This was breaking for TSM. they file reports way late. if (!start || this.getDaysBefore(end, start) < 315) return false; return this.getDaysBefore(filed, end) < 60; // return start && this.getDaysBefore(end, start) < 315 }; FactFiscalCalculator.prototype.resolve = function () { var _this = this; var _a; this.endDateCountMap.forEach(function (count, date) { var _a; var _b = _this.getFiscalYearQuarter({ dateStr: date }), quarter = _b.quarter, year = _b.year; if (year.toString().length !== 4) return; var key = "".concat(year, "_").concat(quarter); if (!_this.datesByFiscals.has(key)) { _this.datesByFiscals.set(key, { filed: '', end: '' }); } var dateMap = _this.datesByFiscals.get(key); var countPrevDate = (_a = _this.endDateCountMap.get(dateMap.end)) !== null && _a !== void 0 ? _a : 0; if (count >= countPrevDate) { dateMap.end = date; } }); // set to the most commonly used filed date for each end date this.filedDateCountByEndDate.forEach(function (countByFiledDate, endDate) { var maxCount = 0; var filedDate = ''; countByFiledDate.forEach(function (count, filed) { if (count > maxCount) { maxCount = count; filedDate = filed; } }); var _a = _this.getFiscalYearQuarter({ dateStr: endDate }), quarter = _a.quarter, year = _a.year; var key = "".concat(year, "_").concat(quarter); if (!_this.datesByFiscals.has(key)) { if (year.toString().length !== 4) return; _this.datesByFiscals.set(key, { filed: '', end: '' }); } var dateMap = _this.datesByFiscals.get(key); dateMap.filed = filedDate; }); // some reports are missing filed dates, this guesses dates based on other years. var monthDayByQuarter = new Map(); // find dates for each quarter to fill in missing. var foundCount = 0; for (var _i = 0, _b = Array.from(this.datesByFiscals.entries()); _i < _b.length; _i++) { var _c = _b[_i], key = _c[0], dates = _c[1]; var quarter = Number(key.split('_')[1]); var bucket = (_a = monthDayByQuarter.get(quarter)) !== null && _a !== void 0 ? _a : { filedMonthDay: '', endMonthDay: '' }; monthDayByQuarter.set(quarter, bucket); if (dates.filed && !bucket.filedMonthDay) { bucket.filedMonthDay = dates.filed.substring(5); foundCount++; } if (dates.end && !bucket.endMonthDay) { bucket.endMonthDay = dates.end.substring(5); foundCount++; } if (foundCount === 8) { break; } } // if date falls on a weekend, move it to the nearest weekday var ensureNoWeekends = function (dateStr) { var date = new Date(dateStr); if (date.getDay() === 6) date.setDate(date.getDate() - 1); if (date.getDay() === 0) date.setDate(date.getDate() + 1); return date.toISOString().split('T')[0]; }; // fill in missing dates this.datesByFiscals.forEach(function (dates, key) { var _a; var quarter = Number(key.split('_')[1]); var datesFound = (_a = monthDayByQuarter.get(quarter)) !== null && _a !== void 0 ? _a : { filedMonthDay: '', endMonthDay: '' }; // if filing month comes after report month, it's the same year, otherwise it's the next year if (!dates.filed && dates.end && datesFound.filedMonthDay) { var _b = dates.end.split('-'), yearEnd = _b[0], month = _b[1], day = _b[2]; var yearFiled = datesFound.filedMonthDay >= "".concat(month, "-").concat(day) ? yearEnd : Number(yearEnd) + 1; dates.filed = ensureNoWeekends("".concat(yearFiled, "-").concat(datesFound.filedMonthDay)); } if (dates.filed && !dates.end && datesFound.endMonthDay) { var _c = dates.filed.split('-'), yearFiled = _c[0], month = _c[1], day = _c[2]; var yearEnd = "".concat(month, "-").concat(day) >= datesFound.endMonthDay ? yearFiled : Number(yearFiled) - 1; dates.end = ensureNoWeekends("".concat(yearEnd, "-").concat(datesFound.endMonthDay)); } }); this.ensureEndDateByYear(); this.endDateCountMap.clear(); this.filedDateCountByEndDate.clear(); this.didResolve = true; }; FactFiscalCalculator.prototype.getDatesByYearQuarter = function (params) { var _a; if (!this.didResolve) this.resolve(); this.didResolve = true; return (_a = this.datesByFiscals.get("".concat(params.year, "_").concat(params.quarter))) !== null && _a !== void 0 ? _a : null; }; /** * Assumes year end from start dates if not provided */ FactFiscalCalculator.prototype.ensureEndDateByYear = function () { if (this.endDateByYear.size > 0 || this.fiscalYearEnd || this.startDateCountMap.size === 0) return; var maxStartDateCount = 0; var maxStartDate = ''; this.startDateCountMap.forEach(function (count, date) { if (count > maxStartDateCount) { maxStartDateCount = count; maxStartDate = date; } }); this.endDateByYear.set(Number(maxStartDate.split('-', 1)[0]) - 1, new Date(maxStartDate)); this.startDateCountMap.clear(); }; FactFiscalCalculator.prototype.getFiscalYearQuarter = function (params) { var dateStr = params.dateStr, _a = params.endDateByYear, endDateByYear = _a === void 0 ? this.endDateByYear : _a, _b = params.fiscalYearEnd, fiscalYearEnd = _b === void 0 ? this.fiscalYearEnd : _b; this.ensureEndDateByYear(); if (this.fiscalsByEndDate.has(dateStr)) { return this.fiscalsByEndDate.get(dateStr); } var fiscals = FactFiscalCalculator.getFiscalYearQuarter({ dateStr: dateStr, endDateByYear: endDateByYear, fiscalYearEnd: fiscalYearEnd }); this.fiscalsByEndDate.set(dateStr, fiscals); return fiscals; }; FactFiscalCalculator.getFiscalYearQuarter = function (params) { var _a; var dateStr = params.dateStr, endDateByYearProp = params.endDateByYear, fiscalYearEnd = params.fiscalYearEnd; var endDateByYear = endDateByYearProp !== null && endDateByYearProp !== void 0 ? endDateByYearProp : new Map(); if (!dateStr) { throw new Error('No date provided, cannot determine fiscal year and quarter'); } if (fiscalYearEnd) { var month = fiscalYearEnd.month, day = fiscalYearEnd.day; var year = new Date().getFullYear(); var yearEndDate_1 = new Date("".concat(year, "-").concat(month, "-").concat(day)); endDateByYear.set(year, (_a = endDateByYear.get(year)) !== null && _a !== void 0 ? _a : yearEndDate_1); } if (!(endDateByYear === null || endDateByYear === void 0 ? void 0 : endDateByYear.size)) { throw new Error('No annual report dates provided'); } var getYearEndDate = function (year) { var _a; var YEAR_MS = 31536000000; var date = endDateByYear.get(year); var datePrev = endDateByYear.get(year - 1); var dateNext = endDateByYear.get(year + 1); if (date) return date; if (datePrev) return new Date(datePrev.getTime() + YEAR_MS); if (dateNext) return new Date(dateNext.getTime() - YEAR_MS); var prevKnownYear = Math.max.apply(Math, Array.from(endDateByYear.keys())); var prevKnownDate = endDateByYear.get(prevKnownYear); var years = (year - prevKnownYear) * YEAR_MS; return new Date(((_a = prevKnownDate === null || prevKnownDate === void 0 ? void 0 : prevKnownDate.getTime()) !== null && _a !== void 0 ? _a : 0) + years); }; var getDaysBefore = function (dateA, dateB) { return (dateB.getTime() - dateA.getTime()) / 86400000; }; var getFiscalQuarter = function (daysBeforeYearEnd) { var quarter = 1; if (daysBeforeYearEnd < 45) { quarter = 4; } else if (daysBeforeYearEnd < 135) { quarter = 3; } else if (daysBeforeYearEnd < 225) { quarter = 2; } return quarter; }; var yearQuarter = Number(dateStr.split('-')[0]); var dateQuarter = new Date(dateStr); var yearEndDate = getYearEndDate(yearQuarter); var daysBefore = getDaysBefore(dateQuarter, yearEndDate); var isCurFiscalYear = daysBefore > -30; var fiscalYear = isCurFiscalYear ? yearQuarter : yearQuarter + 1; var fiscalYearEndDate = isCurFiscalYear ? yearEndDate : getYearEndDate(fiscalYear); var daysFromYearEnd = (fiscalYearEndDate.getTime() - dateQuarter.getTime()) / 86400000; var quarter = getFiscalQuarter(daysFromYearEnd); return { quarter: quarter, year: fiscalYear }; }; return FactFiscalCalculator; }()); exports.default = FactFiscalCalculator;