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.
@@ -0,0 +1,507 @@
1
+ /**
2
+ * Minimal interface that any cache backend must implement.
3
+ * Implement this interface to plug in Redis, Upstash, filesystem, etc.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import type { CacheDriver } from 'market-feed';
8
+ * import { createClient } from 'redis';
9
+ *
10
+ * const redis = createClient();
11
+ * const driver: CacheDriver = {
12
+ * async get(key) { const v = await redis.get(key); return v ? JSON.parse(v) : undefined; },
13
+ * async set(key, value, ttl) { await redis.set(key, JSON.stringify(value), { EX: ttl }); },
14
+ * async delete(key) { await redis.del(key); },
15
+ * async clear() { await redis.flushDb(); },
16
+ * };
17
+ * ```
18
+ */
19
+ interface CacheDriver {
20
+ get<T>(key: string): Promise<T | undefined>;
21
+ set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
22
+ delete(key: string): Promise<void>;
23
+ clear(): Promise<void>;
24
+ }
25
+ interface CacheConfig {
26
+ /** Default TTL in seconds for cached responses. Defaults to 60. */
27
+ ttl?: number;
28
+ /** Maximum number of entries in the memory cache. Defaults to 500. */
29
+ maxSize?: number;
30
+ /**
31
+ * Custom cache driver. When provided, `maxSize` is ignored
32
+ * (the driver manages its own eviction).
33
+ */
34
+ driver?: CacheDriver;
35
+ /**
36
+ * Per-method TTL overrides (in seconds).
37
+ * Keys are method names: "quote", "historical", "company", "news", "search", "marketStatus".
38
+ */
39
+ ttlOverrides?: Partial<Record<CacheMethod, number>>;
40
+ }
41
+ type CacheMethod = "quote" | "historical" | "company" | "news" | "search" | "marketStatus";
42
+
43
+ interface CompanyProfile {
44
+ symbol: string;
45
+ name: string;
46
+ description?: string;
47
+ sector?: string;
48
+ industry?: string;
49
+ /** ISO 3166-1 alpha-2 country code, e.g. "US" */
50
+ country?: string;
51
+ employees?: number;
52
+ website?: string;
53
+ ceo?: string;
54
+ /** Market cap in USD */
55
+ marketCap?: number;
56
+ /** Price-to-earnings ratio */
57
+ peRatio?: number;
58
+ /** Forward PE ratio */
59
+ forwardPE?: number;
60
+ /** Price-to-book ratio */
61
+ priceToBook?: number;
62
+ /** Annual dividend yield as a decimal, e.g. 0.015 = 1.5% */
63
+ dividendYield?: number;
64
+ /** Beta (5Y monthly) */
65
+ beta?: number;
66
+ /** Exchange identifier */
67
+ exchange?: string;
68
+ /** Currency */
69
+ currency?: string;
70
+ /** IPO date */
71
+ ipoDate?: Date;
72
+ provider: string;
73
+ raw?: unknown;
74
+ }
75
+ interface CompanyOptions {
76
+ raw?: boolean;
77
+ }
78
+
79
+ type HistoricalInterval = "1m" | "2m" | "5m" | "15m" | "30m" | "60m" | "90m" | "1h" | "1d" | "5d" | "1wk" | "1mo" | "3mo";
80
+ interface HistoricalOptions {
81
+ /**
82
+ * Start date — ISO 8601 string ("2024-01-01") or a Date object.
83
+ * Defaults to 1 year ago.
84
+ */
85
+ period1?: string | Date;
86
+ /**
87
+ * End date — ISO 8601 string or a Date object.
88
+ * Defaults to today.
89
+ */
90
+ period2?: string | Date;
91
+ /** Bar interval. Defaults to "1d". */
92
+ interval?: HistoricalInterval;
93
+ /** Include the raw provider response on each bar */
94
+ raw?: boolean;
95
+ }
96
+ interface HistoricalBar {
97
+ date: Date;
98
+ open: number;
99
+ high: number;
100
+ low: number;
101
+ close: number;
102
+ /** Adjusted close price (splits + dividends). May be absent for intraday. */
103
+ adjClose?: number;
104
+ volume: number;
105
+ raw?: unknown;
106
+ }
107
+
108
+ type TradingSession = "pre" | "regular" | "post" | "closed";
109
+ interface MarketStatus {
110
+ /** Whether the primary session is currently open */
111
+ isOpen: boolean;
112
+ /** Current trading session type */
113
+ session: TradingSession;
114
+ /** Next session open time */
115
+ nextOpen?: Date;
116
+ /** Current (or next) session close time */
117
+ nextClose?: Date;
118
+ /** Market/exchange identifier, e.g. "US", "LSE" */
119
+ market?: string;
120
+ /** Timezone of the exchange, e.g. "America/New_York" */
121
+ timezone?: string;
122
+ provider: string;
123
+ raw?: unknown;
124
+ }
125
+ interface MarketStatusOptions {
126
+ raw?: boolean;
127
+ }
128
+
129
+ interface NewsItem {
130
+ /** Provider-specific unique ID */
131
+ id: string;
132
+ title: string;
133
+ summary?: string;
134
+ url: string;
135
+ /** Publisher / source name */
136
+ source: string;
137
+ publishedAt: Date;
138
+ /** Ticker symbols mentioned in this article */
139
+ symbols: string[];
140
+ /** Thumbnail image URL */
141
+ thumbnail?: string;
142
+ provider: string;
143
+ raw?: unknown;
144
+ }
145
+ interface NewsOptions {
146
+ /** Maximum number of articles to return. Defaults to 10. */
147
+ limit?: number;
148
+ raw?: boolean;
149
+ }
150
+
151
+ interface Quote {
152
+ /** Ticker symbol, e.g. "AAPL" */
153
+ symbol: string;
154
+ /** Full company or asset name */
155
+ name: string;
156
+ /** Current or last-traded price */
157
+ price: number;
158
+ /** Absolute change from previous close */
159
+ change: number;
160
+ /** Percentage change from previous close */
161
+ changePercent: number;
162
+ /** Opening price for current/last session */
163
+ open: number;
164
+ /** Intraday high */
165
+ high: number;
166
+ /** Intraday low */
167
+ low: number;
168
+ /** Closing price of last completed session */
169
+ close: number;
170
+ /** Previous session close */
171
+ previousClose: number;
172
+ /** Volume traded in current/last session */
173
+ volume: number;
174
+ /** Average volume (30-day) */
175
+ avgVolume?: number;
176
+ /** Market capitalisation in quote currency */
177
+ marketCap?: number;
178
+ /** 52-week high */
179
+ fiftyTwoWeekHigh?: number;
180
+ /** 52-week low */
181
+ fiftyTwoWeekLow?: number;
182
+ /** Currency the price is quoted in, e.g. "USD" */
183
+ currency: string;
184
+ /** Exchange identifier, e.g. "NASDAQ" */
185
+ exchange: string;
186
+ /** Timestamp of the quote data */
187
+ timestamp: Date;
188
+ /** Which provider delivered this data */
189
+ provider: string;
190
+ /** Raw provider response — available when `raw: true` is passed to the client */
191
+ raw?: unknown;
192
+ }
193
+ interface QuoteOptions {
194
+ /** Include the raw provider response on each Quote object */
195
+ raw?: boolean;
196
+ }
197
+
198
+ type AssetType = "stock" | "etf" | "crypto" | "forex" | "index" | "mutual-fund" | "future" | "unknown";
199
+ interface SearchResult {
200
+ symbol: string;
201
+ name: string;
202
+ type: AssetType;
203
+ exchange?: string;
204
+ currency?: string;
205
+ /** ISIN when available */
206
+ isin?: string;
207
+ provider: string;
208
+ raw?: unknown;
209
+ }
210
+ interface SearchOptions {
211
+ /** Maximum number of results to return. Defaults to 10. */
212
+ limit?: number;
213
+ raw?: boolean;
214
+ }
215
+
216
+ /**
217
+ * The contract every provider adapter must satisfy.
218
+ *
219
+ * Methods marked optional (`?`) are not supported by all free-tier APIs.
220
+ * The MarketFeed client only calls them when the active provider declares them.
221
+ */
222
+ interface MarketProvider {
223
+ /** Human-readable provider name, e.g. "yahoo" */
224
+ readonly name: string;
225
+ /**
226
+ * Fetch real-time (or latest delayed) quotes for one or more symbols.
227
+ * Always returns an array — callers slice as needed.
228
+ */
229
+ quote(symbols: string[], options?: QuoteOptions): Promise<Quote[]>;
230
+ /**
231
+ * Fetch OHLCV bars for a single symbol over a date range.
232
+ */
233
+ historical(symbol: string, options?: HistoricalOptions): Promise<HistoricalBar[]>;
234
+ /**
235
+ * Search for tickers matching a query string.
236
+ */
237
+ search(query: string, options?: SearchOptions): Promise<SearchResult[]>;
238
+ /**
239
+ * Fetch company/asset profile. Optional — not all providers support this.
240
+ */
241
+ company?(symbol: string, options?: CompanyOptions): Promise<CompanyProfile>;
242
+ /**
243
+ * Fetch recent news for a symbol. Optional.
244
+ */
245
+ news?(symbol: string, options?: NewsOptions): Promise<NewsItem[]>;
246
+ /**
247
+ * Return current market status for a given market identifier (e.g. "US").
248
+ * Optional.
249
+ */
250
+ marketStatus?(market?: string, options?: MarketStatusOptions): Promise<MarketStatus>;
251
+ }
252
+
253
+ interface MarketFeedOptions {
254
+ /**
255
+ * Ordered list of provider adapters.
256
+ * When `fallback: true`, each is tried in order until one succeeds.
257
+ * Defaults to `[new YahooProvider()]`.
258
+ */
259
+ providers?: MarketProvider[];
260
+ /**
261
+ * Cache configuration. Pass `false` to disable caching entirely.
262
+ * Defaults to an in-memory LRU cache with 60s TTL.
263
+ */
264
+ cache?: CacheConfig | false;
265
+ /**
266
+ * When `true`, automatically tries the next provider if the current one fails.
267
+ * Defaults to `true`.
268
+ */
269
+ fallback?: boolean;
270
+ }
271
+ /**
272
+ * The unified MarketFeed client.
273
+ *
274
+ * Wraps one or more provider adapters under a single consistent API.
275
+ * Handles caching, fallback, and error aggregation transparently.
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * import { MarketFeed } from 'market-feed';
280
+ *
281
+ * const feed = new MarketFeed();
282
+ * const quote = await feed.quote('AAPL');
283
+ * console.log(quote.price); // 189.84
284
+ * ```
285
+ */
286
+ declare class MarketFeed {
287
+ private readonly providers;
288
+ private readonly cache;
289
+ private readonly fallback;
290
+ private readonly ttls;
291
+ constructor(options?: MarketFeedOptions);
292
+ /** Fetch a quote for a single symbol. */
293
+ quote(symbol: string, options?: QuoteOptions): Promise<Quote>;
294
+ /** Fetch quotes for multiple symbols in parallel. */
295
+ quote(symbols: string[], options?: QuoteOptions): Promise<Quote[]>;
296
+ historical(symbol: string, options?: HistoricalOptions): Promise<HistoricalBar[]>;
297
+ search(query: string, options?: SearchOptions): Promise<SearchResult[]>;
298
+ company(symbol: string, options?: CompanyOptions): Promise<CompanyProfile>;
299
+ news(symbol: string, options?: NewsOptions): Promise<NewsItem[]>;
300
+ marketStatus(market?: string, options?: MarketStatusOptions): Promise<MarketStatus>;
301
+ /** Invalidate all cached entries. */
302
+ clearCache(): Promise<void>;
303
+ /** Invalidate a specific cache key (exact match). */
304
+ invalidate(key: string): Promise<void>;
305
+ private withFallback;
306
+ private getCache;
307
+ private setCache;
308
+ }
309
+
310
+ interface YahooProviderOptions {
311
+ /** Request timeout in milliseconds. Defaults to 10 000. */
312
+ timeoutMs?: number;
313
+ /** Retry attempts on transient failures. Defaults to 2. */
314
+ retries?: number;
315
+ }
316
+ /**
317
+ * Yahoo Finance provider — no API key required.
318
+ *
319
+ * Uses the unofficial Yahoo Finance v8 chart API, which is free and publicly
320
+ * accessible but not officially supported. Use respectfully.
321
+ */
322
+ declare class YahooProvider implements MarketProvider {
323
+ readonly name = "yahoo";
324
+ private readonly http1;
325
+ private readonly http2;
326
+ constructor(options?: YahooProviderOptions);
327
+ quote(symbols: string[], options?: QuoteOptions): Promise<Quote[]>;
328
+ private fetchSingleQuote;
329
+ historical(symbol: string, options?: HistoricalOptions): Promise<HistoricalBar[]>;
330
+ search(query: string, options?: SearchOptions): Promise<SearchResult[]>;
331
+ company(symbol: string, options?: CompanyOptions): Promise<CompanyProfile>;
332
+ }
333
+
334
+ /**
335
+ * Token-bucket rate limiter.
336
+ *
337
+ * Tokens refill at `refillRate` tokens per second up to `capacity`.
338
+ * Call `consume()` before each API request; it throws `RateLimitError`
339
+ * if the bucket is empty.
340
+ */
341
+ declare class RateLimiter {
342
+ private readonly providerName;
343
+ private readonly capacity;
344
+ private readonly refillRate;
345
+ private tokens;
346
+ private lastRefill;
347
+ /**
348
+ * @param providerName Used in error messages
349
+ * @param capacity Maximum tokens in the bucket (= burst limit)
350
+ * @param refillRate Tokens added per second
351
+ */
352
+ constructor(providerName: string, capacity: number, refillRate: number);
353
+ /**
354
+ * Attempt to consume `count` tokens.
355
+ * Throws `RateLimitError` if insufficient tokens are available.
356
+ */
357
+ consume(count?: number): void;
358
+ /**
359
+ * Returns true if at least `count` tokens are available without consuming them.
360
+ */
361
+ canConsume(count?: number): boolean;
362
+ /** Milliseconds until the bucket has at least `count` tokens. 0 if already available. */
363
+ waitTimeMs(count?: number): number;
364
+ private refill;
365
+ }
366
+
367
+ interface AlphaVantageProviderOptions {
368
+ apiKey: string;
369
+ /** Request timeout in milliseconds. Defaults to 10 000. */
370
+ timeoutMs?: number;
371
+ /** Retry attempts on transient failures. Defaults to 2. */
372
+ retries?: number;
373
+ /**
374
+ * Override the rate limiter.
375
+ * Free tier: 5 calls/minute, 25 calls/day.
376
+ * Premium tier: higher — set refillRate accordingly.
377
+ */
378
+ rateLimiter?: RateLimiter;
379
+ }
380
+ /**
381
+ * Alpha Vantage provider.
382
+ *
383
+ * Free tier: 25 API calls/day, 5 calls/minute.
384
+ * The rate limiter is enforced client-side to avoid burning your daily quota.
385
+ */
386
+ declare class AlphaVantageProvider implements MarketProvider {
387
+ readonly name = "alpha-vantage";
388
+ private readonly http;
389
+ private readonly apiKey;
390
+ private readonly limiter;
391
+ constructor(options: AlphaVantageProviderOptions);
392
+ quote(symbols: string[], options?: QuoteOptions): Promise<Quote[]>;
393
+ private fetchSingleQuote;
394
+ historical(symbol: string, options?: HistoricalOptions): Promise<HistoricalBar[]>;
395
+ search(query: string, options?: SearchOptions): Promise<SearchResult[]>;
396
+ company(symbol: string, options?: CompanyOptions): Promise<CompanyProfile>;
397
+ private checkRateLimit;
398
+ }
399
+
400
+ interface PolygonProviderOptions {
401
+ apiKey: string;
402
+ /** Request timeout in milliseconds. Defaults to 10 000. */
403
+ timeoutMs?: number;
404
+ /** Retry attempts on transient failures. Defaults to 2. */
405
+ retries?: number;
406
+ /**
407
+ * Override the rate limiter.
408
+ * Free tier: 5 calls/minute. Unlimited for paid plans.
409
+ */
410
+ rateLimiter?: RateLimiter;
411
+ }
412
+ /**
413
+ * Polygon.io provider.
414
+ *
415
+ * Free tier provides 15-minute delayed data with 5 API calls per minute.
416
+ * Paid plans unlock real-time data and higher rate limits.
417
+ */
418
+ declare class PolygonProvider implements MarketProvider {
419
+ private readonly options;
420
+ readonly name = "polygon";
421
+ private readonly http;
422
+ private readonly limiter;
423
+ constructor(options: PolygonProviderOptions);
424
+ quote(symbols: string[], options?: QuoteOptions): Promise<Quote[]>;
425
+ historical(symbol: string, options?: HistoricalOptions): Promise<HistoricalBar[]>;
426
+ search(query: string, options?: SearchOptions): Promise<SearchResult[]>;
427
+ company(symbol: string, options?: CompanyOptions): Promise<CompanyProfile>;
428
+ news(symbol: string, options?: NewsOptions): Promise<NewsItem[]>;
429
+ private assertSuccess;
430
+ }
431
+
432
+ /**
433
+ * A simple LRU (Least-Recently-Used) in-memory cache with TTL support.
434
+ * No dependencies — uses a Map for O(1) access and insertion-order eviction.
435
+ */
436
+ declare class MemoryCacheDriver implements CacheDriver {
437
+ private readonly store;
438
+ private readonly maxSize;
439
+ constructor(maxSize?: number);
440
+ get<T>(key: string): Promise<T | undefined>;
441
+ set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
442
+ delete(key: string): Promise<void>;
443
+ clear(): Promise<void>;
444
+ /** Current number of cached entries (including potentially expired ones). */
445
+ get size(): number;
446
+ }
447
+
448
+ /**
449
+ * Base error class for all market-feed errors.
450
+ * Includes the provider name so callers can identify the source.
451
+ */
452
+ declare class MarketFeedError extends Error {
453
+ readonly provider: string;
454
+ readonly cause?: unknown | undefined;
455
+ constructor(message: string, provider: string, cause?: unknown | undefined);
456
+ }
457
+ /**
458
+ * Thrown when an upstream provider returns an HTTP error or unexpected payload.
459
+ */
460
+ declare class ProviderError extends MarketFeedError {
461
+ readonly statusCode?: number | undefined;
462
+ constructor(message: string, provider: string, statusCode?: number | undefined, cause?: unknown);
463
+ }
464
+ /**
465
+ * Thrown when a provider's rate limit is reached.
466
+ * Contains the earliest time the caller may retry.
467
+ */
468
+ declare class RateLimitError extends MarketFeedError {
469
+ readonly retryAfter?: Date | undefined;
470
+ constructor(provider: string, retryAfter?: Date | undefined);
471
+ }
472
+ /**
473
+ * Thrown when all configured providers have failed for a given operation.
474
+ */
475
+ declare class AllProvidersFailedError extends Error {
476
+ readonly errors: MarketFeedError[];
477
+ constructor(errors: MarketFeedError[], operation: string);
478
+ }
479
+ /**
480
+ * Thrown when a required feature is not supported by the active provider.
481
+ */
482
+ declare class UnsupportedOperationError extends MarketFeedError {
483
+ constructor(provider: string, operation: string);
484
+ }
485
+
486
+ /**
487
+ * Normalise a ticker symbol for use with a specific provider.
488
+ *
489
+ * Different providers use slightly different conventions:
490
+ * - Yahoo Finance: "BRK-B", "BTC-USD"
491
+ * - Alpha Vantage: "BRKB", "BTCUSD" (no separator for crypto pairs)
492
+ * - Polygon.io: "BRK/B", "X:BTCUSD"
493
+ */
494
+ /** Strip exchange suffixes like ".NASDAQ" or ".NYSE" */
495
+ declare function stripExchange(symbol: string): string;
496
+ /** Upper-case and trim a symbol */
497
+ declare function normalise(symbol: string): string;
498
+ /** Convert "BTC/USD" or "BTCUSD" → "BTC-USD" (Yahoo style) */
499
+ declare function toYahooSymbol(symbol: string): string;
500
+ /** Convert "BTC-USD" → "BTCUSD" (Alpha Vantage style for some endpoints) */
501
+ declare function toAlphaVantageSymbol(symbol: string): string;
502
+ /** Convert "BTC-USD" → "X:BTCUSD" for Polygon crypto, else leave as-is */
503
+ declare function toPolygonSymbol(symbol: string): string;
504
+ /** Deduplicate and normalise an array of symbols */
505
+ declare function dedupeSymbols(symbols: string[]): string[];
506
+
507
+ export { AllProvidersFailedError, AlphaVantageProvider, type AlphaVantageProviderOptions, type AssetType, type CacheConfig, type CacheDriver, type CacheMethod, type CompanyOptions, type CompanyProfile, type HistoricalBar, type HistoricalInterval, type HistoricalOptions, MarketFeed, MarketFeedError, type MarketFeedOptions, type MarketProvider, type MarketStatus, type MarketStatusOptions, MemoryCacheDriver, type NewsItem, type NewsOptions, PolygonProvider, type PolygonProviderOptions, ProviderError, type Quote, type QuoteOptions, RateLimitError, RateLimiter, type SearchOptions, type SearchResult, type TradingSession, UnsupportedOperationError, YahooProvider, type YahooProviderOptions, dedupeSymbols, normalise, stripExchange, toAlphaVantageSymbol, toPolygonSymbol, toYahooSymbol };