yahoo-finance2
Version:
JS API for Yahoo Finance
400 lines (386 loc) • 13.8 kB
JavaScript
/**
* Historical data module for retrieving price history, dividends, and stock splits.
*
* This module provides historical price data, dividend payments, and stock split
* information for financial instruments. While functional, many users prefer
* the {@link chart} module which offers more flexibility and features.
*
* @example Basic Price History
* ```typescript
* import YahooFinance from "yahoo-finance2";
* const yahooFinance = new YahooFinance();
*
* // Get 1 year of daily data
* const history = await yahooFinance.historical('AAPL', {
* period1: '2023-01-01',
* period2: '2024-01-01'
* });
*
* console.log(history[0]); // Most recent day
* // { date: Date, open: 150.5, high: 155.2, low: 149.8, close: 154.1, ... }
* ```
*
* @example Different Intervals
* ```typescript
* // Weekly data
* const weekly = await yahooFinance.historical('AAPL', {
* period1: '2023-01-01',
* interval: '1wk'
* });
*
* // Monthly data
* const monthly = await yahooFinance.historical('AAPL', {
* period1: '2022-01-01',
* interval: '1mo'
* });
* ```
*
* @example Dividends and Splits
* ```typescript
* // Get dividend history
* const dividends = await yahooFinance.historical('AAPL', {
* period1: '2023-01-01',
* events: 'dividends'
* });
* // Returns: [{ date: Date, dividends: 0.24 }, ...]
*
* // Get stock splits
* const splits = await yahooFinance.historical('AAPL', {
* period1: '2020-01-01',
* events: 'split'
* });
* // Returns: [{ date: Date, stockSplits: "4:1" }, ...]
* ```
*
* @remarks
* **Limitations**:
* - Intervals limited to daily ("1d"), weekly ("1wk"), monthly ("1mo")
* - Events (prices, dividends, splits) require separate requests
* - Consider using {@link chart} module for more flexibility
*
* **Performance**: The chart module often provides better performance and
* more features for historical data needs.
*
* @module historical
*/
import validateAndCoerceTypes from "../lib/validateAndCoerceTypes.js";
import { getTypedDefinitions } from "../lib/validate/index.js";
// @yf-schema: see the docs on how this file is automatically updated.
import historicalSchema from "./historical.schema.js";
import chartSchema from "./chart.schema.js";
const historicalDefinitions = getTypedDefinitions(historicalSchema);
const chartDefinitions = getTypedDefinitions(chartSchema);
const queryOptionsDefaults = {
interval: "1d",
events: "history",
includeAdjustedClose: true,
};
// Count number of null values in object (1-level deep)
function nullFieldCount(object) {
if (object == null) {
return;
}
let nullCount = 0;
for (const val of Object.values(object))
if (val === null)
nullCount++;
return nullCount;
}
/**
* Get historical price data, dividends, or stock splits for a financial instrument.
*
* This function retrieves historical data from Yahoo Finance. The type of data returned
* depends on the `events` parameter - price history (default), dividends, or stock splits.
*
* @example Price History
* ```typescript
* import YahooFinance from "yahoo-finance2";
* const yahooFinance = new YahooFinance();
*
* // Get 1 year of daily price data
* const prices = await yahooFinance.historical('AAPL', {
* period1: '2023-01-01',
* period2: '2024-01-01'
* });
*
* prices.forEach(day => {
* console.log(`${day.date.toISOString().split('T')[0]}: $${day.close}`);
* });
* ```
*
* @example Different Time Periods
* ```typescript
* // Last 30 days
* const recent = await yahooFinance.historical('TSLA', {
* period1: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
* });
*
* // Specific date range
* const range = await yahooFinance.historical('GOOGL', {
* period1: '2023-06-01',
* period2: '2023-12-31',
* interval: '1wk' // Weekly data
* });
* ```
*
* @example Dividends and Splits
* ```typescript
* // Get all dividends in 2023
* const dividends = await yahooFinance.historical('MSFT', {
* period1: '2023-01-01',
* period2: '2024-01-01',
* events: 'dividends'
* });
*
* // Find stock splits since 2020
* const splits = await yahooFinance.historical('AAPL', {
* period1: '2020-01-01',
* events: 'split'
* });
*
* console.log(splits); // [{ date: Date('2020-08-31'), stockSplits: "4:1" }]
* ```
*
* @param symbol - Stock, ETF, or other financial instrument symbol.
* Use search() to find valid symbols.
* @param queryOptionsOverrides - Required configuration:
* - `period1`: Start date (required)
* - `period2`: End date (optional, defaults to now)
* - `interval`: "1d", "1wk", or "1mo"
* - `events`: "history", "dividends", or "split"
* - `includeAdjustedClose`: Include adjusted prices
* @param moduleOptions - Optional module configuration (validateResult, etc.)
*
* @returns Promise that resolves to:
* - Array of HistoricalRowHistory (for price data)
* - Array of HistoricalRowDividend (for dividend data)
* - Array of HistoricalRowStockSplit (for split data)
*
* @throws Will throw an error if:
* - Network request fails
* - Invalid symbol or date range
* - Validation fails (if enabled)
*
* @remarks
* **Limitations:**
* - Limited to daily/weekly/monthly intervals only
* - Events (prices, dividends, splits) require separate API calls
* - Less flexible than the chart module
*
* **Alternative**: Consider using {@link chart} module for:
* - More interval options (1m, 5m, 15m, etc.)
* - Combined events in single request
* - Better performance for complex queries
*
* **Date Formats**: Accepts Date objects, ISO strings ("2023-01-01"),
* or Unix timestamps (milliseconds since epoch).
*
* @see {@link HistoricalOptions} for all available options
* @see {@link chart} for a more flexible alternative
*/
export default async function historical(symbol, queryOptionsOverrides, moduleOptions) {
let _schemaKey;
this._notices.show("ripHistorical");
if (!queryOptionsOverrides.events ||
queryOptionsOverrides.events === "history") {
_schemaKey = "#/definitions/HistoricalHistoryResult";
}
else if (queryOptionsOverrides.events === "dividends") {
_schemaKey = "#/definitions/HistoricalDividendsResult";
}
else if (queryOptionsOverrides.events === "split") {
_schemaKey = "#/definitions/HistoricalStockSplitsResult";
}
else
throw new Error("No such event type:" + queryOptionsOverrides.events);
const queryOpts = { ...queryOptionsDefaults, ...queryOptionsOverrides };
validateAndCoerceTypes({
source: "historical",
type: "options",
object: queryOpts,
definitions: historicalDefinitions,
schemaOrSchemaKey: "#/definitions/HistoricalOptions",
options: this._opts.validation,
logger: this._opts.logger,
logObj: this._logObj,
versionCheck: this._opts.versionCheck,
});
// Don't forget that queryOpts are already validated and safe-safe.
const eventsMap = { history: "", dividends: "div", split: "split" };
const chartQueryOpts = {
period1: queryOpts.period1,
period2: queryOpts.period2,
interval: queryOpts.interval,
events: eventsMap[queryOpts.events || "history"],
};
validateAndCoerceTypes({
source: "historical",
type: "options",
object: chartQueryOpts,
definitions: chartDefinitions,
schemaOrSchemaKey: "#/definitions/ChartOptions",
options: this._opts.validation,
logger: this._opts.logger,
logObj: this._logObj,
versionCheck: this._opts.versionCheck,
});
/*
throw new Error(
"Internal error, please report. historical() provided invalid chart() query options.",
);
*/
// TODO: do we even care?
if (queryOpts.includeAdjustedClose === false) {
/* */
}
const result = await this.chart(symbol, chartQueryOpts, {
...moduleOptions,
validateResult: true,
});
let out;
if (queryOpts.events === "dividends") {
out = (result.events?.dividends ?? []).map((d) => ({
date: d.date,
dividends: d.amount,
}));
}
else if (queryOpts.events === "split") {
out = (result.events?.splits ?? []).map((s) => ({
date: s.date,
stockSplits: s.splitRatio,
}));
}
else {
out = (result.quotes ?? [])
.filter((quote) => {
const fieldCount = Object.keys(quote).length;
const nullCount = nullFieldCount(quote);
if (nullCount === 0) {
// No nulls is a legit (regular) result
return true;
}
else if (nullCount !== fieldCount - 1 /* skip "date" */) {
// Unhandled case: some but not all values are null.
// Note: no need to check for null "date", validation does it for us
console.error(nullCount, quote);
throw new Error("Historical returned a result with SOME (but not " +
"all) null values. Please report this, and provide the " +
"query that caused it.");
}
else {
// All fields (except "date") are null
return false;
}
})
.map((quote) => {
if (!quote.adjclose)
return quote;
const { adjclose, ...rest } = quote;
return { ...rest, adjClose: adjclose };
});
}
const validateResult = !moduleOptions ||
moduleOptions.validateResult === undefined ||
moduleOptions.validateResult === true;
const validationOpts = {
...this._opts.validation,
// Set logErrors=false if validateResult=false
logErrors: validateResult ? this._opts.validation.logErrors : false,
};
validateAndCoerceTypes({
source: "historical",
type: "result",
object: out,
definitions: historicalDefinitions,
schemaOrSchemaKey: "#/definitions/HistoricalResult",
options: validationOpts,
logger: this._opts.logger,
logObj: this._logObj,
versionCheck: this._opts.versionCheck,
});
return out;
/*
// Original historical() retrieval code when Yahoo API still existed.
return this._moduleExec({
moduleName: "historical",
query: {
assertSymbol: symbol,
url: "https://${YF_QUERY_HOST}/v7/finance/download/" + symbol,
schemaKey: "#/definitions/HistoricalOptions",
defaults: queryOptionsDefaults,
overrides: queryOptionsOverrides,
fetchType: "csv",
transformWith(queryOptions: HistoricalOptions) {
if (!queryOptions.period2) queryOptions.period2 = new Date();
const dates = ["period1", "period2"] as const;
for (const fieldName of dates) {
const value = queryOptions[fieldName];
if (value instanceof Date)
queryOptions[fieldName] = Math.floor(value.getTime() / 1000);
else if (typeof value === "string") {
const timestamp = new Date(value as string).getTime();
if (isNaN(timestamp))
throw new Error(
"yahooFinance.historical() option '" +
fieldName +
"' invalid date provided: '" +
value +
"'",
);
queryOptions[fieldName] = Math.floor(timestamp / 1000);
}
}
if (queryOptions.period1 === queryOptions.period2) {
throw new Error(
"yahooFinance.historical() options `period1` and `period2` " +
"cannot share the same value.",
);
}
return queryOptions;
},
},
result: {
schemaKey,
transformWith(result: any) {
if (result.length === 0) return result;
const filteredResults = [];
const fieldCount = Object.keys(result[0]).length;
// Count number of null values in object (1-level deep)
function nullFieldCount(object: Object) {
let nullCount = 0;
for (const val of Object.values(object))
if (val === null) nullCount++;
return nullCount;
}
for (const row of result) {
const nullCount = nullFieldCount(row);
if (nullCount === 0) {
// No nulls is a legit (regular) result
filteredResults.push(row);
} else if (nullCount !== fieldCount - 1 /* skip "date" */
/*) {
// Unhandled case: some but not all values are null.
// Note: no need to check for null "date", validation does it for us
console.error(nullCount, row);
throw new Error(
"Historical returned a result with SOME (but not " +
"all) null values. Please report this, and provide the " +
"query that caused it.",
);
} else {
// All fields (except "date") are null: silently skip (no-op)
}
}
/*
* We may consider, for future optimization, to count rows and create
* new array in advance, and skip consecutive blocks of null results.
* Of doubtful utility.
*/
/*
return filteredResults;
},
},
moduleOptions,
});
*/
}