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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +332 -0
- package/dist/index.cjs +1141 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +507 -0
- package/dist/index.d.ts +507 -0
- package/dist/index.js +1123 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
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
|