market-feed 0.1.0

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/dist/index.cjs ADDED
@@ -0,0 +1,1141 @@
1
+ 'use strict';
2
+
3
+ // market-feed — Unified financial market data client
4
+ // https://github.com/piyushgupta344/market-feed
5
+
6
+ // src/cache/memory.ts
7
+ var MemoryCacheDriver = class {
8
+ store = /* @__PURE__ */ new Map();
9
+ maxSize;
10
+ constructor(maxSize = 500) {
11
+ this.maxSize = maxSize;
12
+ }
13
+ async get(key) {
14
+ const entry = this.store.get(key);
15
+ if (!entry) return void 0;
16
+ if (entry.expiresAt !== 0 && Date.now() > entry.expiresAt) {
17
+ this.store.delete(key);
18
+ return void 0;
19
+ }
20
+ this.store.delete(key);
21
+ this.store.set(key, entry);
22
+ return entry.value;
23
+ }
24
+ async set(key, value, ttlSeconds) {
25
+ if (this.store.size >= this.maxSize && !this.store.has(key)) {
26
+ const oldest = this.store.keys().next().value;
27
+ if (oldest !== void 0) {
28
+ this.store.delete(oldest);
29
+ }
30
+ }
31
+ const expiresAt = ttlSeconds !== void 0 && ttlSeconds > 0 ? Date.now() + ttlSeconds * 1e3 : 0;
32
+ this.store.set(key, { value, expiresAt });
33
+ }
34
+ async delete(key) {
35
+ this.store.delete(key);
36
+ }
37
+ async clear() {
38
+ this.store.clear();
39
+ }
40
+ /** Current number of cached entries (including potentially expired ones). */
41
+ get size() {
42
+ return this.store.size;
43
+ }
44
+ };
45
+
46
+ // src/errors.ts
47
+ var MarketFeedError = class extends Error {
48
+ constructor(message, provider, cause) {
49
+ super(message);
50
+ this.provider = provider;
51
+ this.cause = cause;
52
+ this.name = "MarketFeedError";
53
+ Object.setPrototypeOf(this, new.target.prototype);
54
+ }
55
+ };
56
+ var ProviderError = class extends MarketFeedError {
57
+ constructor(message, provider, statusCode, cause) {
58
+ super(message, provider, cause);
59
+ this.statusCode = statusCode;
60
+ this.name = "ProviderError";
61
+ Object.setPrototypeOf(this, new.target.prototype);
62
+ }
63
+ };
64
+ var RateLimitError = class extends MarketFeedError {
65
+ constructor(provider, retryAfter) {
66
+ const when = retryAfter ? ` Retry after ${retryAfter.toISOString()}.` : "";
67
+ super(`Rate limit reached for provider "${provider}".${when}`, provider);
68
+ this.retryAfter = retryAfter;
69
+ this.name = "RateLimitError";
70
+ Object.setPrototypeOf(this, new.target.prototype);
71
+ }
72
+ };
73
+ var AllProvidersFailedError = class extends Error {
74
+ constructor(errors, operation) {
75
+ const summary = errors.map((e) => `[${e.provider}] ${e.message}`).join("; ");
76
+ super(`All providers failed for "${operation}": ${summary}`);
77
+ this.errors = errors;
78
+ this.name = "AllProvidersFailedError";
79
+ Object.setPrototypeOf(this, new.target.prototype);
80
+ }
81
+ };
82
+ var UnsupportedOperationError = class extends MarketFeedError {
83
+ constructor(provider, operation) {
84
+ super(`Provider "${provider}" does not support "${operation}".`, provider);
85
+ this.name = "UnsupportedOperationError";
86
+ Object.setPrototypeOf(this, new.target.prototype);
87
+ }
88
+ };
89
+
90
+ // src/http/client.ts
91
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
92
+ function buildUrl(base, path, params) {
93
+ const url = new URL(path, base.endsWith("/") ? base : `${base}/`);
94
+ if (params) {
95
+ for (const [key, value] of Object.entries(params)) {
96
+ if (value !== void 0) {
97
+ url.searchParams.set(key, String(value));
98
+ }
99
+ }
100
+ }
101
+ return url.toString();
102
+ }
103
+ async function sleep(ms) {
104
+ return new Promise((resolve) => setTimeout(resolve, ms));
105
+ }
106
+ var HttpClient = class {
107
+ constructor(providerName, options = {}) {
108
+ this.providerName = providerName;
109
+ this.baseUrl = options.baseUrl ?? "";
110
+ this.defaultHeaders = {
111
+ "Accept": "application/json",
112
+ "User-Agent": "market-feed/0.1.0 (+https://github.com/piyushgupta344/market-feed)",
113
+ ...options.headers
114
+ };
115
+ this.timeoutMs = options.timeoutMs ?? 1e4;
116
+ this.retries = options.retries ?? 2;
117
+ this.retryDelayMs = options.retryDelayMs ?? 300;
118
+ }
119
+ baseUrl;
120
+ defaultHeaders;
121
+ timeoutMs;
122
+ retries;
123
+ retryDelayMs;
124
+ async get(path, options = {}) {
125
+ const url = buildUrl(this.baseUrl, path, options.params);
126
+ const headers = { ...this.defaultHeaders, ...options.headers };
127
+ const timeoutMs = options.timeoutMs ?? this.timeoutMs;
128
+ let lastError;
129
+ for (let attempt = 0; attempt <= this.retries; attempt++) {
130
+ if (attempt > 0) {
131
+ await sleep(this.retryDelayMs * 2 ** (attempt - 1));
132
+ }
133
+ const controller = new AbortController();
134
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
135
+ try {
136
+ const response = await fetch(url, {
137
+ method: "GET",
138
+ headers,
139
+ signal: controller.signal
140
+ });
141
+ clearTimeout(timer);
142
+ if (!response.ok) {
143
+ if (RETRYABLE_STATUSES.has(response.status) && attempt < this.retries) {
144
+ lastError = new ProviderError(
145
+ `HTTP ${response.status} from ${url}`,
146
+ this.providerName,
147
+ response.status
148
+ );
149
+ continue;
150
+ }
151
+ throw new ProviderError(
152
+ `HTTP ${response.status} ${response.statusText} from ${url}`,
153
+ this.providerName,
154
+ response.status
155
+ );
156
+ }
157
+ const contentType = response.headers.get("content-type") ?? "";
158
+ if (!contentType.includes("application/json") && !contentType.includes("text/plain")) {
159
+ }
160
+ return await response.json();
161
+ } catch (err) {
162
+ clearTimeout(timer);
163
+ if (err instanceof ProviderError) {
164
+ throw err;
165
+ }
166
+ if (err instanceof Error && err.name === "AbortError") {
167
+ throw new ProviderError(
168
+ `Request to ${url} timed out after ${timeoutMs}ms`,
169
+ this.providerName
170
+ );
171
+ }
172
+ if (attempt < this.retries) {
173
+ lastError = err;
174
+ continue;
175
+ }
176
+ throw new ProviderError(
177
+ `Network error fetching ${url}: ${err instanceof Error ? err.message : String(err)}`,
178
+ this.providerName,
179
+ void 0,
180
+ err
181
+ );
182
+ }
183
+ }
184
+ throw new ProviderError(
185
+ `Failed after ${this.retries + 1} attempts`,
186
+ this.providerName,
187
+ void 0,
188
+ lastError
189
+ );
190
+ }
191
+ };
192
+
193
+ // src/utils/symbol.ts
194
+ function stripExchange(symbol) {
195
+ return symbol.split(".")[0] ?? symbol;
196
+ }
197
+ function normalise(symbol) {
198
+ return symbol.trim().toUpperCase();
199
+ }
200
+ function toYahooSymbol(symbol) {
201
+ const s = normalise(stripExchange(symbol));
202
+ if (s.includes("/")) return s.replace("/", "-");
203
+ return s;
204
+ }
205
+ function toAlphaVantageSymbol(symbol) {
206
+ return normalise(stripExchange(symbol)).replace(/[-/]/g, "");
207
+ }
208
+ function toPolygonSymbol(symbol) {
209
+ const s = normalise(stripExchange(symbol));
210
+ if (s.includes("-")) {
211
+ const [base, quote] = s.split("-");
212
+ if (base && quote && quote.length === 3) {
213
+ return `X:${base}${quote}`;
214
+ }
215
+ }
216
+ return s;
217
+ }
218
+ function dedupeSymbols(symbols) {
219
+ return [...new Set(symbols.map(normalise))];
220
+ }
221
+
222
+ // src/providers/yahoo/transform.ts
223
+ var PROVIDER = "yahoo";
224
+ function transformQuote(result, raw) {
225
+ const meta = result.meta;
226
+ return {
227
+ symbol: meta.symbol,
228
+ name: meta.symbol,
229
+ // Yahoo chart API doesn't return company name — enriched separately
230
+ price: meta.regularMarketPrice,
231
+ change: meta.regularMarketPrice - meta.regularMarketPreviousClose,
232
+ changePercent: (meta.regularMarketPrice - meta.regularMarketPreviousClose) / meta.regularMarketPreviousClose * 100,
233
+ open: meta.regularMarketDayHigh,
234
+ // chart meta has dayHigh/Low not open — see below
235
+ high: meta.regularMarketDayHigh,
236
+ low: meta.regularMarketDayLow,
237
+ close: meta.regularMarketPrice,
238
+ previousClose: meta.regularMarketPreviousClose,
239
+ volume: meta.regularMarketVolume,
240
+ ...meta.fiftyTwoWeekHigh !== void 0 ? { fiftyTwoWeekHigh: meta.fiftyTwoWeekHigh } : {},
241
+ ...meta.fiftyTwoWeekLow !== void 0 ? { fiftyTwoWeekLow: meta.fiftyTwoWeekLow } : {},
242
+ currency: meta.currency,
243
+ exchange: meta.fullExchangeName ?? meta.exchangeName,
244
+ timestamp: new Date(meta.regularMarketTime * 1e3),
245
+ provider: PROVIDER,
246
+ ...raw !== void 0 ? { raw } : {}
247
+ };
248
+ }
249
+ function transformHistorical(result, raw) {
250
+ const timestamps = result.timestamp ?? [];
251
+ const quoteIndicators = result.indicators.quote[0];
252
+ const adjCloseIndicators = result.indicators.adjclose?.[0];
253
+ if (!quoteIndicators) return [];
254
+ return timestamps.map((ts, i) => {
255
+ const open = quoteIndicators.open[i];
256
+ const high = quoteIndicators.high[i];
257
+ const low = quoteIndicators.low[i];
258
+ const close = quoteIndicators.close[i];
259
+ const volume = quoteIndicators.volume[i];
260
+ const adjClose = adjCloseIndicators?.adjclose[i];
261
+ if (open == null || high == null || low == null || close == null || volume == null) {
262
+ return null;
263
+ }
264
+ const bar = {
265
+ date: new Date(ts * 1e3),
266
+ open,
267
+ high,
268
+ low,
269
+ close,
270
+ volume,
271
+ ...adjClose !== null && adjClose !== void 0 ? { adjClose } : {},
272
+ ...raw !== void 0 ? { raw } : {}
273
+ };
274
+ return bar;
275
+ }).filter((bar) => bar !== null);
276
+ }
277
+ function transformCompany(symbol, result, raw) {
278
+ const profile = result.assetProfile;
279
+ const summary = result.summaryDetail;
280
+ const price = result.price;
281
+ const officers = profile?.companyOfficers ?? [];
282
+ const ceo = officers.find(
283
+ (o) => (o.title ?? "").toLowerCase().includes("chief executive")
284
+ )?.name;
285
+ return {
286
+ symbol,
287
+ name: price?.longName ?? price?.shortName ?? symbol,
288
+ ...profile?.longBusinessSummary !== void 0 ? { description: profile.longBusinessSummary } : {},
289
+ ...profile?.sector !== void 0 ? { sector: profile.sector } : {},
290
+ ...profile?.industry !== void 0 ? { industry: profile.industry } : {},
291
+ ...profile?.country !== void 0 ? { country: profile.country } : {},
292
+ ...profile?.fullTimeEmployees !== void 0 ? { employees: profile.fullTimeEmployees } : {},
293
+ ...profile?.website !== void 0 ? { website: profile.website } : {},
294
+ ...ceo !== void 0 ? { ceo } : {},
295
+ ...summary?.marketCap?.raw !== void 0 ? { marketCap: summary.marketCap.raw } : {},
296
+ ...summary?.trailingPE?.raw !== void 0 ? { peRatio: summary.trailingPE.raw } : {},
297
+ ...summary?.forwardPE?.raw !== void 0 ? { forwardPE: summary.forwardPE.raw } : {},
298
+ ...summary?.priceToBook?.raw !== void 0 ? { priceToBook: summary.priceToBook.raw } : {},
299
+ ...summary?.dividendYield?.raw !== void 0 ? { dividendYield: summary.dividendYield.raw } : {},
300
+ ...summary?.beta?.raw !== void 0 ? { beta: summary.beta.raw } : {},
301
+ ...price?.exchangeName !== void 0 ? { exchange: price.exchangeName } : {},
302
+ ...price?.currency !== void 0 ? { currency: price.currency } : summary?.currency !== void 0 ? { currency: summary.currency } : {},
303
+ provider: PROVIDER,
304
+ ...raw !== void 0 ? { raw } : {}
305
+ };
306
+ }
307
+ var YAHOO_TYPE_MAP = {
308
+ EQUITY: "stock",
309
+ ETF: "etf",
310
+ CRYPTOCURRENCY: "crypto",
311
+ CURRENCY: "forex",
312
+ INDEX: "index",
313
+ MUTUALFUND: "mutual-fund",
314
+ FUTURE: "future"
315
+ };
316
+ function transformSearch(quote, raw) {
317
+ const exchange = quote.exchDisp ?? quote.exchange;
318
+ return {
319
+ symbol: quote.symbol,
320
+ name: quote.longname ?? quote.shortname ?? quote.symbol,
321
+ type: YAHOO_TYPE_MAP[quote.quoteType?.toUpperCase() ?? ""] ?? "unknown",
322
+ ...exchange !== void 0 ? { exchange } : {},
323
+ provider: PROVIDER,
324
+ ...raw !== void 0 ? { raw } : {}
325
+ };
326
+ }
327
+
328
+ // src/providers/yahoo/index.ts
329
+ var YahooProvider = class {
330
+ name = "yahoo";
331
+ http1;
332
+ http2;
333
+ constructor(options = {}) {
334
+ const shared = {
335
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
336
+ ...options.retries !== void 0 ? { retries: options.retries } : {},
337
+ headers: {
338
+ "Accept": "application/json",
339
+ "Accept-Language": "en-US,en;q=0.9"
340
+ }
341
+ };
342
+ this.http1 = new HttpClient("yahoo", {
343
+ ...shared,
344
+ baseUrl: "https://query1.finance.yahoo.com"
345
+ });
346
+ this.http2 = new HttpClient("yahoo", {
347
+ ...shared,
348
+ baseUrl: "https://query2.finance.yahoo.com"
349
+ });
350
+ }
351
+ // ---------------------------------------------------------------------------
352
+ // Quote
353
+ // ---------------------------------------------------------------------------
354
+ async quote(symbols, options) {
355
+ const results = await Promise.all(
356
+ symbols.map((symbol) => this.fetchSingleQuote(symbol, options))
357
+ );
358
+ return results;
359
+ }
360
+ async fetchSingleQuote(symbol, options) {
361
+ const s = toYahooSymbol(symbol);
362
+ const data = await this.http1.get(`/v8/finance/chart/${s}`, {
363
+ params: {
364
+ interval: "1d",
365
+ range: "1d",
366
+ includePrePost: false
367
+ }
368
+ });
369
+ const result = data.chart.result?.[0];
370
+ if (!result) {
371
+ const err = data.chart.error;
372
+ throw new ProviderError(
373
+ err?.description ?? `No data returned for symbol "${s}"`,
374
+ this.name
375
+ );
376
+ }
377
+ return transformQuote(result, options?.raw ? data : void 0);
378
+ }
379
+ // ---------------------------------------------------------------------------
380
+ // Historical
381
+ // ---------------------------------------------------------------------------
382
+ async historical(symbol, options) {
383
+ const s = toYahooSymbol(symbol);
384
+ const interval = options?.interval ?? "1d";
385
+ const period1 = toEpoch(options?.period1 ?? subtractOneYear());
386
+ const period2 = toEpoch(options?.period2 ?? /* @__PURE__ */ new Date());
387
+ const data = await this.http1.get(`/v8/finance/chart/${s}`, {
388
+ params: {
389
+ interval,
390
+ period1,
391
+ period2,
392
+ events: "div,splits",
393
+ includeAdjustedClose: true
394
+ }
395
+ });
396
+ const result = data.chart.result?.[0];
397
+ if (!result) {
398
+ const err = data.chart.error;
399
+ throw new ProviderError(
400
+ err?.description ?? `No historical data for symbol "${s}"`,
401
+ this.name
402
+ );
403
+ }
404
+ return transformHistorical(result, options?.raw ? data : void 0);
405
+ }
406
+ // ---------------------------------------------------------------------------
407
+ // Search
408
+ // ---------------------------------------------------------------------------
409
+ async search(query, options) {
410
+ const limit = options?.limit ?? 10;
411
+ const data = await this.http1.get("/v1/finance/search", {
412
+ params: {
413
+ q: query,
414
+ quotesCount: limit,
415
+ newsCount: 0,
416
+ enableFuzzyQuery: false,
417
+ enableEnhancedTrivialQuery: true
418
+ }
419
+ });
420
+ return (data.quotes ?? []).slice(0, limit).map((q) => transformSearch(q, options?.raw ? q : void 0));
421
+ }
422
+ // ---------------------------------------------------------------------------
423
+ // Company
424
+ // ---------------------------------------------------------------------------
425
+ async company(symbol, options) {
426
+ const s = toYahooSymbol(symbol);
427
+ const data = await this.http2.get(
428
+ `/v10/finance/quoteSummary/${s}`,
429
+ {
430
+ params: {
431
+ modules: "assetProfile,summaryDetail,price"
432
+ }
433
+ }
434
+ );
435
+ const result = data.quoteSummary.result?.[0];
436
+ if (!result) {
437
+ const err = data.quoteSummary.error;
438
+ throw new ProviderError(
439
+ err?.description ?? `No company data for symbol "${s}"`,
440
+ this.name
441
+ );
442
+ }
443
+ return transformCompany(symbol, result, options?.raw ? data : void 0);
444
+ }
445
+ };
446
+ function toEpoch(date) {
447
+ const d = date instanceof Date ? date : new Date(date);
448
+ return Math.floor(d.getTime() / 1e3);
449
+ }
450
+ function subtractOneYear() {
451
+ const d = /* @__PURE__ */ new Date();
452
+ d.setFullYear(d.getFullYear() - 1);
453
+ return d;
454
+ }
455
+
456
+ // src/client.ts
457
+ var DEFAULT_TTLS = {
458
+ quote: 60,
459
+ historical: 3600,
460
+ company: 86400,
461
+ news: 300,
462
+ search: 600,
463
+ marketStatus: 60
464
+ };
465
+ var MarketFeed = class {
466
+ providers;
467
+ cache;
468
+ fallback;
469
+ ttls;
470
+ constructor(options = {}) {
471
+ this.providers = options.providers && options.providers.length > 0 ? options.providers : [new YahooProvider()];
472
+ this.fallback = options.fallback ?? true;
473
+ if (options.cache === false) {
474
+ this.cache = null;
475
+ } else {
476
+ const cfg = options.cache ?? {};
477
+ this.cache = cfg.driver ?? new MemoryCacheDriver(cfg.maxSize ?? 500);
478
+ }
479
+ const overrides = options.cache !== false ? options.cache?.ttlOverrides ?? {} : {};
480
+ this.ttls = { ...DEFAULT_TTLS, ...overrides };
481
+ }
482
+ async quote(symbolOrSymbols, options) {
483
+ const isSingle = typeof symbolOrSymbols === "string";
484
+ const symbols = isSingle ? [symbolOrSymbols] : symbolOrSymbols;
485
+ const cacheKey = `quote:${symbols.join(",")}`;
486
+ const cached = await this.getCache(cacheKey);
487
+ if (cached) return isSingle ? cached[0] : cached;
488
+ const result = await this.withFallback(
489
+ "quote",
490
+ (provider) => provider.quote(symbols, options)
491
+ );
492
+ await this.setCache(cacheKey, result, "quote");
493
+ return isSingle ? result[0] : result;
494
+ }
495
+ // ---------------------------------------------------------------------------
496
+ // historical
497
+ // ---------------------------------------------------------------------------
498
+ async historical(symbol, options) {
499
+ const interval = options?.interval ?? "1d";
500
+ const p1 = normaliseDate(options?.period1);
501
+ const p2 = normaliseDate(options?.period2 ?? /* @__PURE__ */ new Date());
502
+ const cacheKey = `historical:${symbol}:${interval}:${p1}:${p2}`;
503
+ const cached = await this.getCache(cacheKey);
504
+ if (cached) return cached;
505
+ const result = await this.withFallback(
506
+ "historical",
507
+ (provider) => provider.historical(symbol, options)
508
+ );
509
+ await this.setCache(cacheKey, result, "historical");
510
+ return result;
511
+ }
512
+ // ---------------------------------------------------------------------------
513
+ // search
514
+ // ---------------------------------------------------------------------------
515
+ async search(query, options) {
516
+ const cacheKey = `search:${query}:${options?.limit ?? 10}`;
517
+ const cached = await this.getCache(cacheKey);
518
+ if (cached) return cached;
519
+ const result = await this.withFallback(
520
+ "search",
521
+ (provider) => provider.search(query, options)
522
+ );
523
+ await this.setCache(cacheKey, result, "search");
524
+ return result;
525
+ }
526
+ // ---------------------------------------------------------------------------
527
+ // company
528
+ // ---------------------------------------------------------------------------
529
+ async company(symbol, options) {
530
+ const cacheKey = `company:${symbol}`;
531
+ const cached = await this.getCache(cacheKey);
532
+ if (cached) return cached;
533
+ const result = await this.withFallback("company", (provider) => {
534
+ if (!provider.company) throw new UnsupportedOperationError(provider.name, "company");
535
+ return provider.company(symbol, options);
536
+ });
537
+ await this.setCache(cacheKey, result, "company");
538
+ return result;
539
+ }
540
+ // ---------------------------------------------------------------------------
541
+ // news
542
+ // ---------------------------------------------------------------------------
543
+ async news(symbol, options) {
544
+ const cacheKey = `news:${symbol}:${options?.limit ?? 10}`;
545
+ const cached = await this.getCache(cacheKey);
546
+ if (cached) return cached;
547
+ const result = await this.withFallback("news", (provider) => {
548
+ if (!provider.news) throw new UnsupportedOperationError(provider.name, "news");
549
+ return provider.news(symbol, options);
550
+ });
551
+ await this.setCache(cacheKey, result, "news");
552
+ return result;
553
+ }
554
+ // ---------------------------------------------------------------------------
555
+ // marketStatus
556
+ // ---------------------------------------------------------------------------
557
+ async marketStatus(market, options) {
558
+ const cacheKey = `marketStatus:${market ?? "default"}`;
559
+ const cached = await this.getCache(cacheKey);
560
+ if (cached) return cached;
561
+ const result = await this.withFallback("marketStatus", (provider) => {
562
+ if (!provider.marketStatus) {
563
+ throw new UnsupportedOperationError(provider.name, "marketStatus");
564
+ }
565
+ return provider.marketStatus(market, options);
566
+ });
567
+ await this.setCache(cacheKey, result, "marketStatus");
568
+ return result;
569
+ }
570
+ // ---------------------------------------------------------------------------
571
+ // Cache management
572
+ // ---------------------------------------------------------------------------
573
+ /** Invalidate all cached entries. */
574
+ async clearCache() {
575
+ await this.cache?.clear();
576
+ }
577
+ /** Invalidate a specific cache key (exact match). */
578
+ async invalidate(key) {
579
+ await this.cache?.delete(key);
580
+ }
581
+ // ---------------------------------------------------------------------------
582
+ // Internal helpers
583
+ // ---------------------------------------------------------------------------
584
+ async withFallback(operation, fn) {
585
+ const errors = [];
586
+ for (const provider of this.providers) {
587
+ try {
588
+ return await fn(provider);
589
+ } catch (err) {
590
+ if (err instanceof UnsupportedOperationError) {
591
+ if (!this.fallback) throw err;
592
+ errors.push(err);
593
+ continue;
594
+ }
595
+ if (err instanceof MarketFeedError) {
596
+ if (!this.fallback) throw err;
597
+ errors.push(err);
598
+ continue;
599
+ }
600
+ throw err;
601
+ }
602
+ }
603
+ throw new AllProvidersFailedError(errors, operation);
604
+ }
605
+ async getCache(key) {
606
+ if (!this.cache) return void 0;
607
+ return this.cache.get(key);
608
+ }
609
+ async setCache(key, value, method) {
610
+ if (!this.cache) return;
611
+ await this.cache.set(key, value, this.ttls[method]);
612
+ }
613
+ };
614
+ function normaliseDate(d) {
615
+ if (!d) return "";
616
+ const date = d instanceof Date ? d : new Date(d);
617
+ return date.toISOString().slice(0, 10);
618
+ }
619
+
620
+ // src/utils/rate-limiter.ts
621
+ var RateLimiter = class {
622
+ // epoch ms
623
+ /**
624
+ * @param providerName Used in error messages
625
+ * @param capacity Maximum tokens in the bucket (= burst limit)
626
+ * @param refillRate Tokens added per second
627
+ */
628
+ constructor(providerName, capacity, refillRate) {
629
+ this.providerName = providerName;
630
+ this.capacity = capacity;
631
+ this.refillRate = refillRate;
632
+ this.tokens = capacity;
633
+ this.lastRefill = Date.now();
634
+ }
635
+ tokens;
636
+ lastRefill;
637
+ /**
638
+ * Attempt to consume `count` tokens.
639
+ * Throws `RateLimitError` if insufficient tokens are available.
640
+ */
641
+ consume(count = 1) {
642
+ this.refill();
643
+ if (this.tokens < count) {
644
+ const deficit = count - this.tokens;
645
+ const waitSeconds = deficit / this.refillRate;
646
+ const retryAfter = new Date(Date.now() + waitSeconds * 1e3);
647
+ throw new RateLimitError(this.providerName, retryAfter);
648
+ }
649
+ this.tokens -= count;
650
+ }
651
+ /**
652
+ * Returns true if at least `count` tokens are available without consuming them.
653
+ */
654
+ canConsume(count = 1) {
655
+ this.refill();
656
+ return this.tokens >= count;
657
+ }
658
+ /** Milliseconds until the bucket has at least `count` tokens. 0 if already available. */
659
+ waitTimeMs(count = 1) {
660
+ this.refill();
661
+ if (this.tokens >= count) return 0;
662
+ const deficit = count - this.tokens;
663
+ return Math.ceil(deficit / this.refillRate * 1e3);
664
+ }
665
+ refill() {
666
+ const now = Date.now();
667
+ const elapsed = (now - this.lastRefill) / 1e3;
668
+ const newTokens = elapsed * this.refillRate;
669
+ this.tokens = Math.min(this.capacity, this.tokens + newTokens);
670
+ this.lastRefill = now;
671
+ }
672
+ };
673
+
674
+ // src/providers/alpha-vantage/transform.ts
675
+ var PROVIDER2 = "alpha-vantage";
676
+ function transformQuote2(raw, includeRaw) {
677
+ const price = parseFloat(raw["05. price"]);
678
+ const open = parseFloat(raw["02. open"]);
679
+ const high = parseFloat(raw["03. high"]);
680
+ const low = parseFloat(raw["04. low"]);
681
+ const previousClose = parseFloat(raw["08. previous close"]);
682
+ const change = parseFloat(raw["09. change"]);
683
+ const changePercent = parseFloat(raw["10. change percent"].replace("%", ""));
684
+ const volume = parseInt(raw["06. volume"], 10);
685
+ const latestDay = raw["07. latest trading day"];
686
+ return {
687
+ symbol: raw["01. symbol"],
688
+ name: raw["01. symbol"],
689
+ price,
690
+ change,
691
+ changePercent,
692
+ open,
693
+ high,
694
+ low,
695
+ close: price,
696
+ previousClose,
697
+ volume,
698
+ currency: "USD",
699
+ // AV free tier is USD only
700
+ exchange: "",
701
+ timestamp: /* @__PURE__ */ new Date(`${latestDay}T16:00:00-05:00`),
702
+ provider: PROVIDER2,
703
+ raw: includeRaw
704
+ };
705
+ }
706
+ function transformHistoricalBars(timeSeries, period1, period2, raw) {
707
+ return Object.entries(timeSeries).filter(([dateStr]) => {
708
+ const d = new Date(dateStr);
709
+ if (period1 && d < period1) return false;
710
+ if (period2 && d > period2) return false;
711
+ return true;
712
+ }).map(([dateStr, bar]) => ({
713
+ date: new Date(dateStr),
714
+ open: parseFloat(bar["1. open"]),
715
+ high: parseFloat(bar["2. high"]),
716
+ low: parseFloat(bar["3. low"]),
717
+ close: parseFloat(bar["4. close"]),
718
+ adjClose: parseFloat(bar["5. adjusted close"]),
719
+ volume: parseInt(bar["6. volume"], 10),
720
+ ...raw !== void 0 ? { raw } : {}
721
+ })).sort((a, b) => a.date.getTime() - b.date.getTime());
722
+ }
723
+ var AV_TYPE_MAP = {
724
+ equity: "stock",
725
+ etf: "etf",
726
+ "mutual fund": "mutual-fund",
727
+ crypto: "crypto",
728
+ forex: "forex",
729
+ index: "index"
730
+ };
731
+ function transformSearch2(match, raw) {
732
+ const rawType = match["3. type"].toLowerCase();
733
+ return {
734
+ symbol: match["1. symbol"],
735
+ name: match["2. name"],
736
+ type: AV_TYPE_MAP[rawType] ?? "unknown",
737
+ exchange: match["4. region"],
738
+ currency: match["8. currency"],
739
+ provider: PROVIDER2,
740
+ raw
741
+ };
742
+ }
743
+ function transformCompany2(data, raw) {
744
+ return {
745
+ symbol: data.Symbol ?? "",
746
+ name: data.Name ?? data.Symbol ?? "",
747
+ ...data.Description ? { description: data.Description } : {},
748
+ ...data.Sector ? { sector: data.Sector } : {},
749
+ ...data.Industry ? { industry: data.Industry } : {},
750
+ ...data.Country ? { country: data.Country } : {},
751
+ ...data.FullTimeEmployees ? { employees: parseInt(data.FullTimeEmployees, 10) } : {},
752
+ ...data.OfficialSite ? { website: data.OfficialSite } : {},
753
+ ...data.MarketCapitalization ? { marketCap: parseFloat(data.MarketCapitalization) } : {},
754
+ ...data.TrailingPE ? { peRatio: parseFloat(data.TrailingPE) } : {},
755
+ ...data.ForwardPE ? { forwardPE: parseFloat(data.ForwardPE) } : {},
756
+ ...data.PriceToBookRatio ? { priceToBook: parseFloat(data.PriceToBookRatio) } : {},
757
+ ...data.DividendYield ? { dividendYield: parseFloat(data.DividendYield) } : {},
758
+ ...data.Beta ? { beta: parseFloat(data.Beta) } : {},
759
+ ...data.Exchange ? { exchange: data.Exchange } : {},
760
+ ...data.Currency ? { currency: data.Currency } : {},
761
+ ...data.IPODate ? { ipoDate: new Date(data.IPODate) } : {},
762
+ provider: PROVIDER2,
763
+ ...raw !== void 0 ? { raw } : {}
764
+ };
765
+ }
766
+
767
+ // src/providers/alpha-vantage/index.ts
768
+ var AlphaVantageProvider = class {
769
+ name = "alpha-vantage";
770
+ http;
771
+ apiKey;
772
+ limiter;
773
+ constructor(options) {
774
+ this.apiKey = options.apiKey;
775
+ this.http = new HttpClient("alpha-vantage", {
776
+ baseUrl: "https://www.alphavantage.co",
777
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
778
+ ...options.retries !== void 0 ? { retries: options.retries } : {}
779
+ });
780
+ this.limiter = options.rateLimiter ?? new RateLimiter("alpha-vantage", 5, 5 / 60);
781
+ }
782
+ // ---------------------------------------------------------------------------
783
+ // Quote — uses GLOBAL_QUOTE function
784
+ // ---------------------------------------------------------------------------
785
+ async quote(symbols, options) {
786
+ const quotes = [];
787
+ for (const symbol of symbols) {
788
+ this.limiter.consume();
789
+ quotes.push(await this.fetchSingleQuote(symbol, options));
790
+ }
791
+ return quotes;
792
+ }
793
+ async fetchSingleQuote(symbol, options) {
794
+ const s = normalise(symbol);
795
+ const data = await this.http.get("/query", {
796
+ params: { function: "GLOBAL_QUOTE", symbol: s, apikey: this.apiKey }
797
+ });
798
+ this.checkRateLimit(data);
799
+ const gq = data["Global Quote"];
800
+ if (!gq || !gq["01. symbol"]) {
801
+ throw new ProviderError(`No quote data returned for "${s}"`, this.name);
802
+ }
803
+ return transformQuote2(gq, options?.raw ? data : void 0);
804
+ }
805
+ // ---------------------------------------------------------------------------
806
+ // Historical — uses TIME_SERIES_DAILY_ADJUSTED
807
+ // ---------------------------------------------------------------------------
808
+ async historical(symbol, options) {
809
+ this.limiter.consume();
810
+ const s = normalise(symbol);
811
+ const outputsize = "full";
812
+ const data = await this.http.get("/query", {
813
+ params: {
814
+ function: "TIME_SERIES_DAILY_ADJUSTED",
815
+ symbol: s,
816
+ outputsize,
817
+ apikey: this.apiKey
818
+ }
819
+ });
820
+ this.checkRateLimit(data);
821
+ const timeSeries = data["Time Series (Daily Adjusted)"] ?? data["Time Series (Daily)"];
822
+ if (!timeSeries) {
823
+ throw new ProviderError(`No historical data returned for "${s}"`, this.name);
824
+ }
825
+ const period1 = options?.period1 ? new Date(options.period1) : (() => {
826
+ const d = /* @__PURE__ */ new Date();
827
+ d.setFullYear(d.getFullYear() - 1);
828
+ return d;
829
+ })();
830
+ const period2 = options?.period2 ? new Date(options.period2) : /* @__PURE__ */ new Date();
831
+ return transformHistoricalBars(
832
+ timeSeries,
833
+ period1,
834
+ period2,
835
+ options?.raw ? data : void 0
836
+ );
837
+ }
838
+ // ---------------------------------------------------------------------------
839
+ // Search — uses SYMBOL_SEARCH
840
+ // ---------------------------------------------------------------------------
841
+ async search(query, options) {
842
+ this.limiter.consume();
843
+ const data = await this.http.get("/query", {
844
+ params: { function: "SYMBOL_SEARCH", keywords: query, apikey: this.apiKey }
845
+ });
846
+ this.checkRateLimit(data);
847
+ const limit = options?.limit ?? 10;
848
+ return (data.bestMatches ?? []).slice(0, limit).map((m) => transformSearch2(m, options?.raw ? m : void 0));
849
+ }
850
+ // ---------------------------------------------------------------------------
851
+ // Company — uses OVERVIEW
852
+ // ---------------------------------------------------------------------------
853
+ async company(symbol, options) {
854
+ this.limiter.consume();
855
+ const s = normalise(symbol);
856
+ const data = await this.http.get("/query", {
857
+ params: { function: "OVERVIEW", symbol: s, apikey: this.apiKey }
858
+ });
859
+ this.checkRateLimit(data);
860
+ if (!data.Symbol) {
861
+ throw new ProviderError(`No company overview returned for "${s}"`, this.name);
862
+ }
863
+ return transformCompany2(data, options?.raw ? data : void 0);
864
+ }
865
+ // ---------------------------------------------------------------------------
866
+ // Internal
867
+ // ---------------------------------------------------------------------------
868
+ checkRateLimit(data) {
869
+ const info = data["Information"];
870
+ const note = data["Note"];
871
+ const msg = info ?? note ?? "";
872
+ if (msg.toLowerCase().includes("rate limit") || msg.toLowerCase().includes("api call")) {
873
+ throw new RateLimitError(this.name);
874
+ }
875
+ }
876
+ };
877
+
878
+ // src/providers/polygon/transform.ts
879
+ var PROVIDER3 = "polygon";
880
+ function transformQuote3(ticker, raw) {
881
+ const day = ticker.day;
882
+ const prev = ticker.prevDay;
883
+ const price = ticker.lastTrade?.p ?? day.c;
884
+ return {
885
+ symbol: ticker.ticker,
886
+ name: ticker.ticker,
887
+ price,
888
+ change: ticker.todaysChange,
889
+ changePercent: ticker.todaysChangePerc,
890
+ open: day.o,
891
+ high: day.h,
892
+ low: day.l,
893
+ close: day.c,
894
+ previousClose: prev.c,
895
+ volume: day.v,
896
+ currency: "USD",
897
+ exchange: "",
898
+ timestamp: new Date(ticker.updated / 1e6),
899
+ // nanoseconds → ms
900
+ provider: PROVIDER3,
901
+ raw
902
+ };
903
+ }
904
+ function transformHistoricalBar(bar, raw) {
905
+ return {
906
+ date: new Date(bar.t),
907
+ open: bar.o,
908
+ high: bar.h,
909
+ low: bar.l,
910
+ close: bar.c,
911
+ volume: bar.v,
912
+ ...raw !== void 0 ? { raw } : {}
913
+ };
914
+ }
915
+ var POLYGON_TYPE_MAP = {
916
+ CS: "stock",
917
+ ETF: "etf",
918
+ ETN: "etf",
919
+ ETP: "etf",
920
+ CRYPTO: "crypto",
921
+ FX: "forex",
922
+ CURRENCY: "forex",
923
+ INDEX: "index",
924
+ MF: "mutual-fund",
925
+ FUND: "mutual-fund",
926
+ WARRANT: "unknown",
927
+ RIGHT: "unknown",
928
+ ADRC: "stock",
929
+ PFD: "stock"
930
+ };
931
+ function transformSearch3(ticker, raw) {
932
+ const type = ticker.type ? POLYGON_TYPE_MAP[ticker.type.toUpperCase()] ?? "unknown" : "unknown";
933
+ return {
934
+ symbol: ticker.ticker,
935
+ name: ticker.name,
936
+ type,
937
+ ...ticker.primary_exchange ? { exchange: ticker.primary_exchange } : {},
938
+ ...ticker.currency_name ? { currency: ticker.currency_name.toUpperCase() } : {},
939
+ provider: PROVIDER3,
940
+ ...raw !== void 0 ? { raw } : {}
941
+ };
942
+ }
943
+ function transformCompany3(details, raw) {
944
+ return {
945
+ symbol: details.ticker,
946
+ name: details.name,
947
+ ...details.description ? { description: details.description } : {},
948
+ ...details.sic_description ? { sector: details.sic_description } : {},
949
+ ...details.address?.country ? { country: details.address.country } : {},
950
+ ...details.total_employees !== void 0 ? { employees: details.total_employees } : {},
951
+ ...details.homepage_url ? { website: details.homepage_url } : {},
952
+ ...details.market_cap !== void 0 ? { marketCap: details.market_cap } : {},
953
+ ...details.primary_exchange ? { exchange: details.primary_exchange } : {},
954
+ ...details.list_date ? { ipoDate: new Date(details.list_date) } : {},
955
+ provider: PROVIDER3,
956
+ ...raw !== void 0 ? { raw } : {}
957
+ };
958
+ }
959
+ function transformNews(article, raw) {
960
+ return {
961
+ id: article.id,
962
+ title: article.title,
963
+ ...article.description ? { summary: article.description } : {},
964
+ url: article.article_url,
965
+ source: article.publisher?.name ?? "unknown",
966
+ publishedAt: new Date(article.published_utc),
967
+ symbols: article.tickers ?? [],
968
+ ...article.image_url ? { thumbnail: article.image_url } : {},
969
+ provider: PROVIDER3,
970
+ ...raw !== void 0 ? { raw } : {}
971
+ };
972
+ }
973
+
974
+ // src/providers/polygon/index.ts
975
+ var PolygonProvider = class {
976
+ constructor(options) {
977
+ this.options = options;
978
+ this.http = new HttpClient("polygon", {
979
+ baseUrl: "https://api.polygon.io",
980
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
981
+ ...options.retries !== void 0 ? { retries: options.retries } : {},
982
+ headers: { Authorization: `Bearer ${options.apiKey}` }
983
+ });
984
+ this.limiter = options.rateLimiter ?? new RateLimiter("polygon", 5, 5 / 60);
985
+ }
986
+ name = "polygon";
987
+ http;
988
+ limiter;
989
+ // ---------------------------------------------------------------------------
990
+ // Quote — snapshot endpoint
991
+ // ---------------------------------------------------------------------------
992
+ async quote(symbols, options) {
993
+ this.limiter.consume();
994
+ const tickers = symbols.map(normalise).join(",");
995
+ const data = await this.http.get(
996
+ `/v2/snapshot/locale/us/markets/stocks/tickers`,
997
+ { params: { tickers, apiKey: this.options.apiKey } }
998
+ );
999
+ this.assertSuccess(data);
1000
+ const tickerList = data.tickers ?? (data.ticker ? [data.ticker] : []);
1001
+ if (tickerList.length === 0) {
1002
+ throw new ProviderError(`No snapshot data for "${tickers}"`, this.name);
1003
+ }
1004
+ return tickerList.map((t) => transformQuote3(t, options?.raw ? t : void 0));
1005
+ }
1006
+ // ---------------------------------------------------------------------------
1007
+ // Historical — aggregates endpoint
1008
+ // ---------------------------------------------------------------------------
1009
+ async historical(symbol, options) {
1010
+ this.limiter.consume();
1011
+ const s = normalise(symbol);
1012
+ const interval = options?.interval ?? "1d";
1013
+ const { multiplier, timespan } = parseInterval(interval);
1014
+ const period1 = toDateString(options?.period1 ?? subtractOneYear2());
1015
+ const period2 = toDateString(options?.period2 ?? /* @__PURE__ */ new Date());
1016
+ const data = await this.http.get(
1017
+ `/v2/aggs/ticker/${s}/range/${multiplier}/${timespan}/${period1}/${period2}`,
1018
+ {
1019
+ params: {
1020
+ adjusted: true,
1021
+ sort: "asc",
1022
+ limit: 5e4,
1023
+ apiKey: this.options.apiKey
1024
+ }
1025
+ }
1026
+ );
1027
+ this.assertSuccess(data);
1028
+ return (data.results ?? []).map(
1029
+ (bar) => transformHistoricalBar(bar, options?.raw ? bar : void 0)
1030
+ );
1031
+ }
1032
+ // ---------------------------------------------------------------------------
1033
+ // Search — reference tickers endpoint
1034
+ // ---------------------------------------------------------------------------
1035
+ async search(query, options) {
1036
+ this.limiter.consume();
1037
+ const limit = options?.limit ?? 10;
1038
+ const data = await this.http.get("/v3/reference/tickers", {
1039
+ params: {
1040
+ search: query,
1041
+ active: true,
1042
+ limit,
1043
+ apiKey: this.options.apiKey
1044
+ }
1045
+ });
1046
+ this.assertSuccess(data);
1047
+ return (data.results ?? []).map((t) => transformSearch3(t, options?.raw ? t : void 0));
1048
+ }
1049
+ // ---------------------------------------------------------------------------
1050
+ // Company — ticker details v3
1051
+ // ---------------------------------------------------------------------------
1052
+ async company(symbol, options) {
1053
+ this.limiter.consume();
1054
+ const s = normalise(symbol);
1055
+ const data = await this.http.get(
1056
+ `/v3/reference/tickers/${s}`,
1057
+ { params: { apiKey: this.options.apiKey } }
1058
+ );
1059
+ this.assertSuccess(data);
1060
+ if (!data.results) {
1061
+ throw new ProviderError(`No company data for "${s}"`, this.name);
1062
+ }
1063
+ return transformCompany3(data.results, options?.raw ? data : void 0);
1064
+ }
1065
+ // ---------------------------------------------------------------------------
1066
+ // News — ticker news endpoint
1067
+ // ---------------------------------------------------------------------------
1068
+ async news(symbol, options) {
1069
+ this.limiter.consume();
1070
+ const s = normalise(symbol);
1071
+ const limit = options?.limit ?? 10;
1072
+ const data = await this.http.get("/v2/reference/news", {
1073
+ params: {
1074
+ ticker: s,
1075
+ limit,
1076
+ order: "desc",
1077
+ sort: "published_utc",
1078
+ apiKey: this.options.apiKey
1079
+ }
1080
+ });
1081
+ this.assertSuccess(data);
1082
+ return (data.results ?? []).map((a) => transformNews(a, options?.raw ? a : void 0));
1083
+ }
1084
+ // ---------------------------------------------------------------------------
1085
+ // Internal
1086
+ // ---------------------------------------------------------------------------
1087
+ assertSuccess(response) {
1088
+ if (response.status === "ERROR" || response.error) {
1089
+ throw new ProviderError(
1090
+ response.error ?? `Polygon returned status "${response.status}"`,
1091
+ this.name
1092
+ );
1093
+ }
1094
+ }
1095
+ };
1096
+ function parseInterval(interval) {
1097
+ const map = {
1098
+ "1m": { multiplier: 1, timespan: "minute" },
1099
+ "2m": { multiplier: 2, timespan: "minute" },
1100
+ "5m": { multiplier: 5, timespan: "minute" },
1101
+ "15m": { multiplier: 15, timespan: "minute" },
1102
+ "30m": { multiplier: 30, timespan: "minute" },
1103
+ "60m": { multiplier: 60, timespan: "minute" },
1104
+ "1h": { multiplier: 1, timespan: "hour" },
1105
+ "1d": { multiplier: 1, timespan: "day" },
1106
+ "5d": { multiplier: 5, timespan: "day" },
1107
+ "1wk": { multiplier: 1, timespan: "week" },
1108
+ "1mo": { multiplier: 1, timespan: "month" },
1109
+ "3mo": { multiplier: 3, timespan: "month" }
1110
+ };
1111
+ return map[interval] ?? { multiplier: 1, timespan: "day" };
1112
+ }
1113
+ function toDateString(date) {
1114
+ const d = date instanceof Date ? date : new Date(date);
1115
+ return d.toISOString().slice(0, 10);
1116
+ }
1117
+ function subtractOneYear2() {
1118
+ const d = /* @__PURE__ */ new Date();
1119
+ d.setFullYear(d.getFullYear() - 1);
1120
+ return d;
1121
+ }
1122
+
1123
+ exports.AllProvidersFailedError = AllProvidersFailedError;
1124
+ exports.AlphaVantageProvider = AlphaVantageProvider;
1125
+ exports.MarketFeed = MarketFeed;
1126
+ exports.MarketFeedError = MarketFeedError;
1127
+ exports.MemoryCacheDriver = MemoryCacheDriver;
1128
+ exports.PolygonProvider = PolygonProvider;
1129
+ exports.ProviderError = ProviderError;
1130
+ exports.RateLimitError = RateLimitError;
1131
+ exports.RateLimiter = RateLimiter;
1132
+ exports.UnsupportedOperationError = UnsupportedOperationError;
1133
+ exports.YahooProvider = YahooProvider;
1134
+ exports.dedupeSymbols = dedupeSymbols;
1135
+ exports.normalise = normalise;
1136
+ exports.stripExchange = stripExchange;
1137
+ exports.toAlphaVantageSymbol = toAlphaVantageSymbol;
1138
+ exports.toPolygonSymbol = toPolygonSymbol;
1139
+ exports.toYahooSymbol = toYahooSymbol;
1140
+ //# sourceMappingURL=index.cjs.map
1141
+ //# sourceMappingURL=index.cjs.map