market-data-analyzer 2.0.1 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/utils/api.ts DELETED
@@ -1,396 +0,0 @@
1
- /**
2
- * API helpers for Yahoo Finance and CoinGecko free endpoints.
3
- *
4
- * Yahoo Finance: Uses the v7/v8 finance endpoints with cookie+crumb auth.
5
- * CoinGecko: Uses the free /api/v3 endpoints (no API key needed, rate-limited).
6
- */
7
-
8
- import { priceCache } from "./cache.js";
9
- import type {
10
- YahooQuote,
11
- YahooChartPoint,
12
- CoinGeckoMarketData,
13
- CoinGeckoGlobalData,
14
- } from "../types.js";
15
-
16
- // ---------------------------------------------------------------------------
17
- // User-Agent to avoid blocks
18
- // ---------------------------------------------------------------------------
19
-
20
- const UA =
21
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
22
-
23
- // ---------------------------------------------------------------------------
24
- // Yahoo Finance cookie + crumb authentication
25
- // ---------------------------------------------------------------------------
26
-
27
- let yahooCookie: string | null = null;
28
- let yahooCrumb: string | null = null;
29
- let yahooAuthExpiry = 0;
30
-
31
- /** Sleep helper for retry backoff. */
32
- function sleep(ms: number): Promise<void> {
33
- return new Promise((resolve) => setTimeout(resolve, ms));
34
- }
35
-
36
- /**
37
- * Obtain a cookie + crumb pair for Yahoo Finance API access.
38
- * Cookies are cached for 1 hour. Retries up to 3 times on rate limits.
39
- */
40
- async function ensureYahooAuth(): Promise<{ cookie: string; crumb: string }> {
41
- if (yahooCookie && yahooCrumb && Date.now() < yahooAuthExpiry) {
42
- return { cookie: yahooCookie, crumb: yahooCrumb };
43
- }
44
-
45
- const maxRetries = 3;
46
- for (let attempt = 0; attempt < maxRetries; attempt++) {
47
- if (attempt > 0) {
48
- await sleep(2000 * attempt);
49
- }
50
-
51
- try {
52
- // Step 1: Get cookies from fc.yahoo.com
53
- const resp1 = await fetch("https://fc.yahoo.com", {
54
- headers: { "User-Agent": UA },
55
- redirect: "manual",
56
- });
57
- const setCookies = resp1.headers.getSetCookie?.() ?? [];
58
- const cookieStr = setCookies.map((c) => c.split(";")[0]).join("; ");
59
- if (!cookieStr) {
60
- continue;
61
- }
62
-
63
- // Step 2: Get crumb using the cookies
64
- const resp2 = await fetch(
65
- "https://query2.finance.yahoo.com/v1/test/getcrumb",
66
- {
67
- headers: { "User-Agent": UA, Cookie: cookieStr },
68
- },
69
- );
70
- if (!resp2.ok) {
71
- continue;
72
- }
73
- const crumb = await resp2.text();
74
- if (!crumb || crumb.includes("Too Many")) {
75
- continue;
76
- }
77
-
78
- yahooCookie = cookieStr;
79
- yahooCrumb = crumb;
80
- yahooAuthExpiry = Date.now() + 3_600_000; // 1 hour
81
-
82
- return { cookie: cookieStr, crumb };
83
- } catch {
84
- // Network error, retry
85
- continue;
86
- }
87
- }
88
-
89
- throw new Error(
90
- "Failed to authenticate with Yahoo Finance after multiple attempts. " +
91
- "The API may be rate-limiting requests. Try again in a few minutes.",
92
- );
93
- }
94
-
95
- // ---------------------------------------------------------------------------
96
- // Generic fetch helpers
97
- // ---------------------------------------------------------------------------
98
-
99
- async function fetchJSON<T>(url: string): Promise<T> {
100
- const res = await fetch(url, {
101
- headers: {
102
- "User-Agent": UA,
103
- Accept: "application/json",
104
- },
105
- });
106
- if (!res.ok) {
107
- throw new Error(`HTTP ${res.status} from ${url}: ${res.statusText}`);
108
- }
109
- return res.json() as Promise<T>;
110
- }
111
-
112
- async function fetchYahooJSON<T>(url: string): Promise<T> {
113
- const maxRetries = 2;
114
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
115
- const { cookie, crumb } = await ensureYahooAuth();
116
- const separator = url.includes("?") ? "&" : "?";
117
- const fullUrl = `${url}${separator}crumb=${encodeURIComponent(crumb)}`;
118
- const res = await fetch(fullUrl, {
119
- headers: {
120
- "User-Agent": UA,
121
- Accept: "application/json",
122
- Cookie: cookie,
123
- },
124
- });
125
- if (res.status === 429 && attempt < maxRetries) {
126
- // Invalidate cached auth and retry
127
- yahooAuthExpiry = 0;
128
- await sleep(2000 * (attempt + 1));
129
- continue;
130
- }
131
- if (!res.ok) {
132
- throw new Error(`HTTP ${res.status} from ${url}: ${res.statusText}`);
133
- }
134
- return res.json() as Promise<T>;
135
- }
136
- throw new Error(`Failed to fetch ${url} after retries`);
137
- }
138
-
139
- // ---------------------------------------------------------------------------
140
- // Yahoo Finance helpers
141
- // ---------------------------------------------------------------------------
142
-
143
- /**
144
- * Fetch a quote for one or more symbols via Yahoo Finance v7 quote endpoint.
145
- * Uses cookie+crumb authentication via query2 subdomain.
146
- */
147
- export async function yahooQuote(symbols: string[]): Promise<YahooQuote[]> {
148
- const cacheKey = `yq:${symbols.sort().join(",")}`;
149
- const cached = priceCache.get<YahooQuote[]>(cacheKey);
150
- if (cached) return cached;
151
-
152
- const joined = symbols.join(",");
153
- const url = `https://query2.finance.yahoo.com/v7/finance/quote?symbols=${encodeURIComponent(joined)}`;
154
-
155
- const data = await fetchYahooJSON<any>(url);
156
- const quotes: YahooQuote[] = data?.quoteResponse?.result ?? [];
157
- priceCache.set(cacheKey, quotes);
158
- return quotes;
159
- }
160
-
161
- /**
162
- * Fetch historical chart data from Yahoo Finance.
163
- * @param symbol Ticker symbol
164
- * @param range "1mo", "3mo", "6mo", "1y", "5y"
165
- * @param interval "1d", "1wk", "1mo"
166
- */
167
- export async function yahooChart(
168
- symbol: string,
169
- range: string = "6mo",
170
- interval: string = "1d",
171
- ): Promise<YahooChartPoint[]> {
172
- const cacheKey = `yc:${symbol}:${range}:${interval}`;
173
- const cached = priceCache.get<YahooChartPoint[]>(cacheKey);
174
- if (cached) return cached;
175
-
176
- const url = `https://query2.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(symbol)}?range=${range}&interval=${interval}&includePrePost=false`;
177
- const data = await fetchYahooJSON<any>(url);
178
- const result = data?.chart?.result?.[0];
179
- if (!result) {
180
- throw new Error(`No chart data returned for ${symbol}`);
181
- }
182
-
183
- const timestamps: number[] = result.timestamp ?? [];
184
- const ohlcv = result.indicators?.quote?.[0] ?? {};
185
- const adjClose = result.indicators?.adjclose?.[0]?.adjclose;
186
-
187
- const points: YahooChartPoint[] = [];
188
- for (let i = 0; i < timestamps.length; i++) {
189
- const close = adjClose?.[i] ?? ohlcv.close?.[i];
190
- const open = ohlcv.open?.[i];
191
- if (close == null || open == null) continue;
192
- points.push({
193
- date: timestamps[i]!,
194
- open,
195
- high: ohlcv.high?.[i] ?? open,
196
- low: ohlcv.low?.[i] ?? open,
197
- close,
198
- volume: ohlcv.volume?.[i] ?? 0,
199
- });
200
- }
201
-
202
- priceCache.set(cacheKey, points);
203
- return points;
204
- }
205
-
206
- /**
207
- * Screen stocks using Yahoo Finance's screener or quote list.
208
- * Fetches quotes for a predefined set of popular tickers and filters them.
209
- */
210
- export async function yahooScreener(
211
- symbols: string[],
212
- ): Promise<YahooQuote[]> {
213
- // Batch into groups of 20 to stay within URL limits
214
- const results: YahooQuote[] = [];
215
- for (let i = 0; i < symbols.length; i += 20) {
216
- const batch = symbols.slice(i, i + 20);
217
- const quotes = await yahooQuote(batch);
218
- results.push(...quotes);
219
- }
220
- return results;
221
- }
222
-
223
- // ---------------------------------------------------------------------------
224
- // CoinGecko helpers
225
- // ---------------------------------------------------------------------------
226
-
227
- const CG_BASE = "https://api.coingecko.com/api/v3";
228
-
229
- /**
230
- * Fetch market data for one or more coins by ID.
231
- */
232
- export async function coingeckoMarkets(
233
- ids: string[],
234
- vsCurrency: string = "usd",
235
- ): Promise<CoinGeckoMarketData[]> {
236
- const cacheKey = `cg:${ids.sort().join(",")}:${vsCurrency}`;
237
- const cached = priceCache.get<CoinGeckoMarketData[]>(cacheKey);
238
- if (cached) return cached;
239
-
240
- const url = `${CG_BASE}/coins/markets?vs_currency=${vsCurrency}&ids=${ids.join(",")}&order=market_cap_desc&per_page=100&page=1&sparkline=false&price_change_percentage=7d,30d`;
241
- const data = await fetchJSON<CoinGeckoMarketData[]>(url);
242
- priceCache.set(cacheKey, data);
243
- return data;
244
- }
245
-
246
- /**
247
- * Fetch global crypto market data.
248
- */
249
- export async function coingeckoGlobal(): Promise<CoinGeckoGlobalData> {
250
- const cacheKey = "cg:global";
251
- const cached = priceCache.get<CoinGeckoGlobalData>(cacheKey);
252
- if (cached) return cached;
253
-
254
- const data = await fetchJSON<{ data: CoinGeckoGlobalData }>(`${CG_BASE}/global`);
255
- priceCache.set(cacheKey, data.data);
256
- return data.data;
257
- }
258
-
259
- /**
260
- * Map common symbols to CoinGecko IDs.
261
- */
262
- export function symbolToCoinGeckoId(symbol: string): string {
263
- const map: Record<string, string> = {
264
- BTC: "bitcoin",
265
- ETH: "ethereum",
266
- SOL: "solana",
267
- BNB: "binancecoin",
268
- XRP: "ripple",
269
- ADA: "cardano",
270
- DOGE: "dogecoin",
271
- DOT: "polkadot",
272
- AVAX: "avalanche-2",
273
- MATIC: "matic-network",
274
- LINK: "chainlink",
275
- UNI: "uniswap",
276
- ATOM: "cosmos",
277
- LTC: "litecoin",
278
- NEAR: "near",
279
- APT: "aptos",
280
- ARB: "arbitrum",
281
- OP: "optimism",
282
- SUI: "sui",
283
- TRX: "tron",
284
- SHIB: "shiba-inu",
285
- PEPE: "pepe",
286
- };
287
- const upper = symbol.toUpperCase().replace("-USD", "").replace("USD", "");
288
- return map[upper] ?? symbol.toLowerCase();
289
- }
290
-
291
- // ---------------------------------------------------------------------------
292
- // Popular stock symbols for screening
293
- // ---------------------------------------------------------------------------
294
-
295
- export const POPULAR_SYMBOLS = [
296
- // Mega-cap tech
297
- "AAPL", "MSFT", "NVDA", "GOOGL", "AMZN", "META", "TSLA", "AVGO", "TSM", "ORCL",
298
- // More tech
299
- "CRM", "AMD", "INTC", "ADBE", "NFLX", "CSCO", "QCOM", "TXN", "AMAT", "NOW",
300
- // Financials
301
- "JPM", "V", "MA", "BAC", "GS", "MS", "BRK-B", "AXP", "BLK", "SCHW",
302
- // Healthcare
303
- "UNH", "JNJ", "LLY", "PFE", "ABBV", "MRK", "TMO", "ABT", "DHR", "BMY",
304
- // Consumer
305
- "WMT", "PG", "KO", "PEP", "COST", "MCD", "NKE", "HD", "LOW", "SBUX",
306
- // Energy
307
- "XOM", "CVX", "COP", "SLB", "EOG",
308
- // Industrials
309
- "CAT", "GE", "UNP", "HON", "BA", "RTX", "DE", "LMT",
310
- // Other
311
- "DIS", "NEE", "AMT", "PLD", "LIN", "SPGI",
312
- ];
313
-
314
- // ---------------------------------------------------------------------------
315
- // Static sector/industry mapping for popular symbols
316
- // (v7 quote endpoint does not return sector/industry)
317
- // ---------------------------------------------------------------------------
318
-
319
- export const SYMBOL_SECTORS: Record<string, { sector: string; industry: string }> = {
320
- // Technology
321
- AAPL: { sector: "Technology", industry: "Consumer Electronics" },
322
- MSFT: { sector: "Technology", industry: "Software - Infrastructure" },
323
- NVDA: { sector: "Technology", industry: "Semiconductors" },
324
- GOOGL: { sector: "Technology", industry: "Internet Content & Information" },
325
- AMZN: { sector: "Technology", industry: "Internet Retail" },
326
- META: { sector: "Technology", industry: "Internet Content & Information" },
327
- TSLA: { sector: "Technology", industry: "Auto Manufacturers" },
328
- AVGO: { sector: "Technology", industry: "Semiconductors" },
329
- TSM: { sector: "Technology", industry: "Semiconductors" },
330
- ORCL: { sector: "Technology", industry: "Software - Infrastructure" },
331
- CRM: { sector: "Technology", industry: "Software - Application" },
332
- AMD: { sector: "Technology", industry: "Semiconductors" },
333
- INTC: { sector: "Technology", industry: "Semiconductors" },
334
- ADBE: { sector: "Technology", industry: "Software - Application" },
335
- NFLX: { sector: "Technology", industry: "Entertainment" },
336
- CSCO: { sector: "Technology", industry: "Communication Equipment" },
337
- QCOM: { sector: "Technology", industry: "Semiconductors" },
338
- TXN: { sector: "Technology", industry: "Semiconductors" },
339
- AMAT: { sector: "Technology", industry: "Semiconductor Equipment" },
340
- NOW: { sector: "Technology", industry: "Software - Application" },
341
- // Financials
342
- JPM: { sector: "Financial Services", industry: "Banks - Diversified" },
343
- V: { sector: "Financial Services", industry: "Credit Services" },
344
- MA: { sector: "Financial Services", industry: "Credit Services" },
345
- BAC: { sector: "Financial Services", industry: "Banks - Diversified" },
346
- GS: { sector: "Financial Services", industry: "Capital Markets" },
347
- MS: { sector: "Financial Services", industry: "Capital Markets" },
348
- "BRK-B": { sector: "Financial Services", industry: "Insurance - Diversified" },
349
- AXP: { sector: "Financial Services", industry: "Credit Services" },
350
- BLK: { sector: "Financial Services", industry: "Asset Management" },
351
- SCHW: { sector: "Financial Services", industry: "Capital Markets" },
352
- SPGI: { sector: "Financial Services", industry: "Financial Data & Stock Exchanges" },
353
- // Healthcare
354
- UNH: { sector: "Healthcare", industry: "Healthcare Plans" },
355
- JNJ: { sector: "Healthcare", industry: "Drug Manufacturers" },
356
- LLY: { sector: "Healthcare", industry: "Drug Manufacturers" },
357
- PFE: { sector: "Healthcare", industry: "Drug Manufacturers" },
358
- ABBV: { sector: "Healthcare", industry: "Drug Manufacturers" },
359
- MRK: { sector: "Healthcare", industry: "Drug Manufacturers" },
360
- TMO: { sector: "Healthcare", industry: "Diagnostics & Research" },
361
- ABT: { sector: "Healthcare", industry: "Medical Devices" },
362
- DHR: { sector: "Healthcare", industry: "Diagnostics & Research" },
363
- BMY: { sector: "Healthcare", industry: "Drug Manufacturers" },
364
- // Consumer Discretionary / Staples
365
- WMT: { sector: "Consumer Defensive", industry: "Discount Stores" },
366
- PG: { sector: "Consumer Defensive", industry: "Household & Personal Products" },
367
- KO: { sector: "Consumer Defensive", industry: "Beverages - Non-Alcoholic" },
368
- PEP: { sector: "Consumer Defensive", industry: "Beverages - Non-Alcoholic" },
369
- COST: { sector: "Consumer Defensive", industry: "Discount Stores" },
370
- MCD: { sector: "Consumer Cyclical", industry: "Restaurants" },
371
- NKE: { sector: "Consumer Cyclical", industry: "Footwear & Accessories" },
372
- HD: { sector: "Consumer Cyclical", industry: "Home Improvement Retail" },
373
- LOW: { sector: "Consumer Cyclical", industry: "Home Improvement Retail" },
374
- SBUX: { sector: "Consumer Cyclical", industry: "Restaurants" },
375
- DIS: { sector: "Communication Services", industry: "Entertainment" },
376
- // Energy
377
- XOM: { sector: "Energy", industry: "Oil & Gas Integrated" },
378
- CVX: { sector: "Energy", industry: "Oil & Gas Integrated" },
379
- COP: { sector: "Energy", industry: "Oil & Gas E&P" },
380
- SLB: { sector: "Energy", industry: "Oil & Gas Equipment & Services" },
381
- EOG: { sector: "Energy", industry: "Oil & Gas E&P" },
382
- // Industrials
383
- CAT: { sector: "Industrials", industry: "Farm & Heavy Construction Machinery" },
384
- GE: { sector: "Industrials", industry: "Specialty Industrial Machinery" },
385
- UNP: { sector: "Industrials", industry: "Railroads" },
386
- HON: { sector: "Industrials", industry: "Conglomerates" },
387
- BA: { sector: "Industrials", industry: "Aerospace & Defense" },
388
- RTX: { sector: "Industrials", industry: "Aerospace & Defense" },
389
- DE: { sector: "Industrials", industry: "Farm & Heavy Construction Machinery" },
390
- LMT: { sector: "Industrials", industry: "Aerospace & Defense" },
391
- // Real Estate / Utilities / Materials
392
- NEE: { sector: "Utilities", industry: "Utilities - Regulated Electric" },
393
- AMT: { sector: "Real Estate", industry: "REIT - Specialty" },
394
- PLD: { sector: "Real Estate", industry: "REIT - Industrial" },
395
- LIN: { sector: "Basic Materials", industry: "Specialty Chemicals" },
396
- };
@@ -1,65 +0,0 @@
1
- /**
2
- * In-memory cache with configurable TTL.
3
- * Default TTL is 5 minutes (300_000 ms) for price data.
4
- */
5
-
6
- interface CacheEntry<T> {
7
- value: T;
8
- expiresAt: number;
9
- }
10
-
11
- export class MemoryCache {
12
- private store = new Map<string, CacheEntry<unknown>>();
13
- private defaultTTL: number;
14
-
15
- constructor(defaultTTLMs: number = 300_000) {
16
- this.defaultTTL = defaultTTLMs;
17
- }
18
-
19
- get<T>(key: string): T | undefined {
20
- const entry = this.store.get(key);
21
- if (!entry) return undefined;
22
- if (Date.now() > entry.expiresAt) {
23
- this.store.delete(key);
24
- return undefined;
25
- }
26
- return entry.value as T;
27
- }
28
-
29
- set<T>(key: string, value: T, ttlMs?: number): void {
30
- this.store.set(key, {
31
- value,
32
- expiresAt: Date.now() + (ttlMs ?? this.defaultTTL),
33
- });
34
- }
35
-
36
- has(key: string): boolean {
37
- return this.get(key) !== undefined;
38
- }
39
-
40
- delete(key: string): void {
41
- this.store.delete(key);
42
- }
43
-
44
- clear(): void {
45
- this.store.clear();
46
- }
47
-
48
- /** Remove all expired entries. */
49
- prune(): void {
50
- const now = Date.now();
51
- for (const [key, entry] of this.store) {
52
- if (now > entry.expiresAt) {
53
- this.store.delete(key);
54
- }
55
- }
56
- }
57
-
58
- get size(): number {
59
- this.prune();
60
- return this.store.size;
61
- }
62
- }
63
-
64
- /** Shared price cache instance -- 5 minute TTL */
65
- export const priceCache = new MemoryCache(300_000);