sec-edgar-api
Version:
Fetch and parse SEC earnings reports and other filings. Useful for financial analysis.
301 lines (300 loc) • 14.9 kB
JavaScript
"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;