2026-06-29 21:50:27 -04:00
|
|
|
// Investor Flow — yfinance-adjustments
|
|
|
|
|
// Pure parser for yahoo-finance2 v3 chart `events` payloads into PriceAdjustment rows.
|
|
|
|
|
// Zero runtime dependencies: this module only reads JSON-shaped input and emits objects.
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A single corporate-action adjustment row produced by parseAdjustments().
|
|
|
|
|
*
|
|
|
|
|
* - `symbol` is the ticker the chart was fetched for.
|
|
|
|
|
* - `exDate` is the event date in YYYY-MM-DD (UTC, derived from ISO input).
|
|
|
|
|
* - `type` is either "split" or "dividend".
|
|
|
|
|
* - `ratio`:
|
|
|
|
|
* split → numerator / denominator (e.g. 4.0 for a 4:1 split).
|
|
|
|
|
* dividend → amount per share in USD (e.g. 0.01).
|
|
|
|
|
*/
|
|
|
|
|
export interface PriceAdjustment {
|
|
|
|
|
symbol: string;
|
|
|
|
|
exDate: string;
|
|
|
|
|
type: "split" | "dividend";
|
|
|
|
|
ratio: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type SplitEvent = { date: string; numerator: number; denominator: number; splitRatio?: string };
|
|
|
|
|
type DividendEvent = { date: string; amount: number };
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Parse a yahoo-finance2 v3 chart response's `events` object into a flat list of
|
|
|
|
|
* PriceAdjustment rows, sorted by exDate ascending.
|
|
|
|
|
*
|
|
|
|
|
* The function is defensive:
|
|
|
|
|
* - `chartJson.events` may be absent or null/undefined → return [].
|
|
|
|
|
* - `splits` / `dividends` may be absent, null, an array, or a map of keyed objects.
|
|
|
|
|
* Both shapes are handled via `Object.values()`.
|
|
|
|
|
* - Entries missing a `date` are skipped.
|
|
|
|
|
*/
|
|
|
|
|
export function parseAdjustments(
|
|
|
|
|
symbol: string,
|
|
|
|
|
chartJson: Record<string, unknown>,
|
|
|
|
|
): PriceAdjustment[] {
|
|
|
|
|
const result: PriceAdjustment[] = [];
|
|
|
|
|
|
|
|
|
|
const events = (chartJson.events as Record<string, unknown>) ?? {};
|
|
|
|
|
emitFrom(events.splits, symbol, "split", result);
|
|
|
|
|
emitFrom(events.dividends, symbol, "dividend", result);
|
|
|
|
|
|
|
|
|
|
result.sort((a, b) => (a.exDate < b.exDate ? -1 : a.exDate > b.exDate ? 1 : 0));
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function emitFrom(
|
|
|
|
|
source: unknown,
|
|
|
|
|
symbol: string,
|
|
|
|
|
type: "split" | "dividend",
|
|
|
|
|
out: PriceAdjustment[],
|
|
|
|
|
): void {
|
|
|
|
|
if (!source) return;
|
|
|
|
|
|
|
|
|
|
const entries: Record<string, unknown>[] = Array.isArray(source)
|
|
|
|
|
? (source as Record<string, unknown>[])
|
2026-07-23 18:02:24 -04:00
|
|
|
: (Object.values(source as Record<string, unknown>) as Record<string, unknown>[]);
|
2026-06-29 21:50:27 -04:00
|
|
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
if (!entry || typeof entry !== "object") continue;
|
|
|
|
|
|
|
|
|
|
if (type === "split") {
|
|
|
|
|
const e = entry as SplitEvent;
|
|
|
|
|
if (!e.date) continue;
|
|
|
|
|
const numerator = typeof e.numerator === "number" ? e.numerator : 1;
|
|
|
|
|
const denominator = typeof e.denominator === "number" && e.denominator !== 0 ? e.denominator : 1;
|
|
|
|
|
out.push({
|
|
|
|
|
symbol,
|
|
|
|
|
exDate: e.date.slice(0, 10),
|
|
|
|
|
type: "split",
|
|
|
|
|
ratio: numerator / denominator,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
const e = entry as DividendEvent;
|
|
|
|
|
if (!e.date) continue;
|
|
|
|
|
const amount = typeof e.amount === "number" ? e.amount : 0;
|
|
|
|
|
out.push({
|
|
|
|
|
symbol,
|
|
|
|
|
exDate: e.date.slice(0, 10),
|
|
|
|
|
type: "dividend",
|
|
|
|
|
ratio: amount,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|