generated from bennycode/ts-node-starter
-
-
Notifications
You must be signed in to change notification settings - Fork 120
Open
Description
- Pull the S&P 500 constituents.
- Get each stock’s intraday percent change.
- Compute: Breadth: % of constituents up on the day, Cap-weighted return: market-cap-weighted average return.
- Classify the day:
- Red if cap-weighted return < −0.30% and breadth < 45%
- Green if cap-weighted return > +0.30% and breadth > 55%
- Mixed otherwise
// sp500-breadth.ts
// npm i yahoo-finance2 cheerio node-fetch @types/node --save
// ts-node sp500-breadth.ts
import yf from "yahoo-finance2";
import fetch from "node-fetch";
import * as cheerio from "cheerio";
type Snap = {
ticker: string;
pctChange: number; // intraday return as decimal
marketCap: number; // in USD
};
async function getSP500Tickers(): Promise<string[]> {
const url = "https://en.wikipedia.org/wiki/List_of_S%26P_500_companies";
const html = await (await fetch(url)).text();
const $ = cheerio.load(html);
// First table on the page holds the index members
const tickers = new Set<string>();
$("table.wikitable tbody tr").each((_i, tr) => {
const sym = $(tr).find("td:nth-child(1)").text().trim();
if (sym) {
// Yahoo uses - instead of . for class shares like BRK-B
const t = sym.replace(/\./g, "-");
tickers.add(t);
}
});
// Basic sanity filter
return Array.from(tickers).filter(t => /^[A-Z\-]+$/.test(t));
}
function chunk<T>(arr: T[], n: number): T[][] {
const out: T[][] = [];
for (let i = 0; i < arr.length; i += n) out.push(arr.slice(i, i + n));
return out;
}
async function fetchSnapshots(tickers: string[]): Promise<Snap[]> {
const snaps: Snap[] = [];
const batches = chunk(tickers, 50);
for (const batch of batches) {
try {
// yahoo-finance2 can fetch multiple quotes in one call
const quotes = await yf.quote(batch);
for (const q of quotes) {
const last = q.regularMarketPrice as number | undefined;
const prev = q.previousClose as number | undefined;
const mcap = q.marketCap as number | undefined;
if (
typeof last === "number" &&
typeof prev === "number" &&
prev > 0 &&
typeof mcap === "number" &&
mcap > 0
) {
snaps.push({
ticker: q.symbol!,
pctChange: (last - prev) / prev,
marketCap: mcap,
});
}
}
} catch {
// Continue on batch failure
continue;
}
}
return snaps;
}
function classify(breadth: number, capWeightedReturn: number): "green" | "red" | "mixed" {
if (capWeightedReturn < -0.003 && breadth < 0.45) return "red";
if (capWeightedReturn > 0.003 && breadth > 0.55) return "green";
return "mixed";
}
async function main() {
const tickers = await getSP500Tickers();
if (!tickers.length) {
console.error("Could not load S&P 500 tickers");
process.exit(1);
}
const snaps = await fetchSnapshots(tickers);
if (!snaps.length) {
console.error("No snapshots retrieved");
process.exit(1);
}
const advancers = snaps.filter(s => s.pctChange > 0).length;
const breadth = advancers / snaps.length;
const totalMcap = snaps.reduce((a, b) => a + b.marketCap, 0);
const capWeightedReturn =
snaps.reduce((a, b) => a + (b.marketCap / totalMcap) * b.pctChange, 0);
const verdict = classify(breadth, capWeightedReturn);
const pct = (x: number, d = 2) => (x * 100).toFixed(d);
const result = {
sp500_constituents_used: snaps.length,
breadth_up_pct: Number(pct(breadth, 1)),
cap_weighted_return_pct: Number(pct(capWeightedReturn, 2)),
verdict,
};
console.log(JSON.stringify(result, null, 2));
}
main().catch(err => {
console.error(err);
process.exit(1);
});Metadata
Metadata
Assignees
Labels
No labels