@fin.cx/opendata 3.1.0 → 3.2.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_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/stocks/classes.baseproviderservice.d.ts +129 -0
- package/dist_ts/stocks/classes.baseproviderservice.js +204 -0
- package/dist_ts/stocks/classes.stockdataservice.d.ts +121 -0
- package/dist_ts/stocks/classes.stockdataservice.js +495 -0
- package/dist_ts/stocks/index.d.ts +3 -0
- package/dist_ts/stocks/index.js +5 -1
- package/dist_ts/stocks/interfaces/stockdata.d.ts +57 -0
- package/dist_ts/stocks/interfaces/stockdata.js +2 -0
- package/package.json +1 -1
- package/readme.md +133 -61
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/stocks/classes.baseproviderservice.ts +296 -0
- package/ts/stocks/classes.stockdataservice.ts +647 -0
- package/ts/stocks/index.ts +5 -0
- package/ts/stocks/interfaces/stockdata.ts +65 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import type { IStockProvider, IProviderConfig } from './interfaces/provider.js';
|
|
3
|
+
import type { IFundamentalsProvider, IFundamentalsProviderConfig, IStockFundamentals } from './interfaces/fundamentals.js';
|
|
4
|
+
import type { IStockPrice, IStockDataRequest as IPriceRequest } from './interfaces/stockprice.js';
|
|
5
|
+
import type { IStockData, IStockDataServiceConfig, ICompleteStockDataRequest, ICompleteStockDataBatchRequest } from './interfaces/stockdata.js';
|
|
6
|
+
|
|
7
|
+
interface IProviderEntry<T> {
|
|
8
|
+
provider: T;
|
|
9
|
+
config: IProviderConfig | IFundamentalsProviderConfig;
|
|
10
|
+
lastError?: Error;
|
|
11
|
+
lastErrorTime?: Date;
|
|
12
|
+
successCount: number;
|
|
13
|
+
errorCount: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ICacheEntry<T> {
|
|
17
|
+
data: T;
|
|
18
|
+
timestamp: Date;
|
|
19
|
+
ttl: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Unified service for managing both stock prices and fundamentals
|
|
24
|
+
* Provides automatic enrichment and convenient combined data access
|
|
25
|
+
*/
|
|
26
|
+
export class StockDataService {
|
|
27
|
+
private priceProviders = new Map<string, IProviderEntry<IStockProvider>>();
|
|
28
|
+
private fundamentalsProviders = new Map<string, IProviderEntry<IFundamentalsProvider>>();
|
|
29
|
+
|
|
30
|
+
private priceCache = new Map<string, ICacheEntry<IStockPrice | IStockPrice[]>>();
|
|
31
|
+
private fundamentalsCache = new Map<string, ICacheEntry<IStockFundamentals | IStockFundamentals[]>>();
|
|
32
|
+
|
|
33
|
+
private logger = console;
|
|
34
|
+
|
|
35
|
+
private config: Required<IStockDataServiceConfig> = {
|
|
36
|
+
cache: {
|
|
37
|
+
priceTTL: 24 * 60 * 60 * 1000, // 24 hours
|
|
38
|
+
fundamentalsTTL: 90 * 24 * 60 * 60 * 1000, // 90 days
|
|
39
|
+
maxEntries: 10000
|
|
40
|
+
},
|
|
41
|
+
timeout: {
|
|
42
|
+
price: 10000, // 10 seconds
|
|
43
|
+
fundamentals: 30000 // 30 seconds
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
constructor(config?: IStockDataServiceConfig) {
|
|
48
|
+
if (config) {
|
|
49
|
+
this.config = {
|
|
50
|
+
cache: { ...this.config.cache, ...config.cache },
|
|
51
|
+
timeout: { ...this.config.timeout, ...config.timeout }
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ========== Provider Management ==========
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Register a price provider
|
|
60
|
+
*/
|
|
61
|
+
public registerPriceProvider(provider: IStockProvider, config?: IProviderConfig): void {
|
|
62
|
+
const defaultConfig: IProviderConfig = {
|
|
63
|
+
enabled: true,
|
|
64
|
+
priority: provider.priority,
|
|
65
|
+
timeout: this.config.timeout.price,
|
|
66
|
+
retryAttempts: 2,
|
|
67
|
+
retryDelay: 1000
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const mergedConfig = { ...defaultConfig, ...config };
|
|
71
|
+
|
|
72
|
+
this.priceProviders.set(provider.name, {
|
|
73
|
+
provider,
|
|
74
|
+
config: mergedConfig,
|
|
75
|
+
successCount: 0,
|
|
76
|
+
errorCount: 0
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
console.log(`Registered price provider: ${provider.name}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Register a fundamentals provider
|
|
84
|
+
*/
|
|
85
|
+
public registerFundamentalsProvider(
|
|
86
|
+
provider: IFundamentalsProvider,
|
|
87
|
+
config?: IFundamentalsProviderConfig
|
|
88
|
+
): void {
|
|
89
|
+
const defaultConfig: IFundamentalsProviderConfig = {
|
|
90
|
+
enabled: true,
|
|
91
|
+
priority: provider.priority,
|
|
92
|
+
timeout: this.config.timeout.fundamentals,
|
|
93
|
+
retryAttempts: 2,
|
|
94
|
+
retryDelay: 1000,
|
|
95
|
+
cacheTTL: this.config.cache.fundamentalsTTL
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const mergedConfig = { ...defaultConfig, ...config };
|
|
99
|
+
|
|
100
|
+
this.fundamentalsProviders.set(provider.name, {
|
|
101
|
+
provider,
|
|
102
|
+
config: mergedConfig,
|
|
103
|
+
successCount: 0,
|
|
104
|
+
errorCount: 0
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
console.log(`Registered fundamentals provider: ${provider.name}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Unregister a price provider
|
|
112
|
+
*/
|
|
113
|
+
public unregisterPriceProvider(providerName: string): void {
|
|
114
|
+
this.priceProviders.delete(providerName);
|
|
115
|
+
console.log(`Unregistered price provider: ${providerName}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Unregister a fundamentals provider
|
|
120
|
+
*/
|
|
121
|
+
public unregisterFundamentalsProvider(providerName: string): void {
|
|
122
|
+
this.fundamentalsProviders.delete(providerName);
|
|
123
|
+
console.log(`Unregistered fundamentals provider: ${providerName}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get all registered price providers
|
|
128
|
+
*/
|
|
129
|
+
public getPriceProviders(): IStockProvider[] {
|
|
130
|
+
return Array.from(this.priceProviders.values()).map(entry => entry.provider);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get all registered fundamentals providers
|
|
135
|
+
*/
|
|
136
|
+
public getFundamentalsProviders(): IFundamentalsProvider[] {
|
|
137
|
+
return Array.from(this.fundamentalsProviders.values()).map(entry => entry.provider);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get enabled price providers sorted by priority
|
|
142
|
+
*/
|
|
143
|
+
private getEnabledPriceProviders(): IStockProvider[] {
|
|
144
|
+
return Array.from(this.priceProviders.values())
|
|
145
|
+
.filter(entry => entry.config.enabled)
|
|
146
|
+
.sort((a, b) => (b.config.priority || 0) - (a.config.priority || 0))
|
|
147
|
+
.map(entry => entry.provider);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get enabled fundamentals providers sorted by priority
|
|
152
|
+
*/
|
|
153
|
+
private getEnabledFundamentalsProviders(): IFundamentalsProvider[] {
|
|
154
|
+
return Array.from(this.fundamentalsProviders.values())
|
|
155
|
+
.filter(entry => entry.config.enabled)
|
|
156
|
+
.sort((a, b) => (b.config.priority || 0) - (a.config.priority || 0))
|
|
157
|
+
.map(entry => entry.provider);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ========== Data Fetching Methods ==========
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get current price for a single ticker
|
|
164
|
+
*/
|
|
165
|
+
public async getPrice(ticker: string): Promise<IStockPrice> {
|
|
166
|
+
const cacheKey = `price:${ticker}`;
|
|
167
|
+
const cached = this.getFromCache(this.priceCache, cacheKey);
|
|
168
|
+
|
|
169
|
+
if (cached) {
|
|
170
|
+
console.log(`Cache hit for price: ${ticker}`);
|
|
171
|
+
return cached as IStockPrice;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const providers = this.getEnabledPriceProviders();
|
|
175
|
+
if (providers.length === 0) {
|
|
176
|
+
throw new Error('No price providers available');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let lastError: Error | undefined;
|
|
180
|
+
|
|
181
|
+
for (const provider of providers) {
|
|
182
|
+
const entry = this.priceProviders.get(provider.name)!;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const result = await this.fetchWithRetry(
|
|
186
|
+
() => provider.fetchData({ type: 'current', ticker }),
|
|
187
|
+
entry.config
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
entry.successCount++;
|
|
191
|
+
|
|
192
|
+
const price = result as IStockPrice;
|
|
193
|
+
this.addToCache(this.priceCache, cacheKey, price, this.config.cache.priceTTL);
|
|
194
|
+
|
|
195
|
+
console.log(`Successfully fetched price for ${ticker} from ${provider.name}`);
|
|
196
|
+
return price;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
entry.errorCount++;
|
|
199
|
+
entry.lastError = error as Error;
|
|
200
|
+
entry.lastErrorTime = new Date();
|
|
201
|
+
lastError = error as Error;
|
|
202
|
+
|
|
203
|
+
console.warn(`Provider ${provider.name} failed for ${ticker}: ${error.message}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Failed to fetch price for ${ticker} from all providers. Last error: ${lastError?.message}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get current prices for multiple tickers
|
|
214
|
+
*/
|
|
215
|
+
public async getPrices(tickers: string[]): Promise<IStockPrice[]> {
|
|
216
|
+
const cacheKey = `prices:${tickers.sort().join(',')}`;
|
|
217
|
+
const cached = this.getFromCache(this.priceCache, cacheKey);
|
|
218
|
+
|
|
219
|
+
if (cached) {
|
|
220
|
+
console.log(`Cache hit for prices: ${tickers.length} tickers`);
|
|
221
|
+
return cached as IStockPrice[];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const providers = this.getEnabledPriceProviders();
|
|
225
|
+
if (providers.length === 0) {
|
|
226
|
+
throw new Error('No price providers available');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let lastError: Error | undefined;
|
|
230
|
+
|
|
231
|
+
for (const provider of providers) {
|
|
232
|
+
const entry = this.priceProviders.get(provider.name)!;
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const result = await this.fetchWithRetry(
|
|
236
|
+
() => provider.fetchData({ type: 'batch', tickers }),
|
|
237
|
+
entry.config
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
entry.successCount++;
|
|
241
|
+
|
|
242
|
+
const prices = result as IStockPrice[];
|
|
243
|
+
this.addToCache(this.priceCache, cacheKey, prices, this.config.cache.priceTTL);
|
|
244
|
+
|
|
245
|
+
console.log(`Successfully fetched ${prices.length} prices from ${provider.name}`);
|
|
246
|
+
return prices;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
entry.errorCount++;
|
|
249
|
+
entry.lastError = error as Error;
|
|
250
|
+
entry.lastErrorTime = new Date();
|
|
251
|
+
lastError = error as Error;
|
|
252
|
+
|
|
253
|
+
console.warn(`Provider ${provider.name} failed for batch prices: ${error.message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
throw new Error(
|
|
258
|
+
`Failed to fetch prices for ${tickers.length} tickers from all providers. Last error: ${lastError?.message}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get fundamentals for a single ticker
|
|
264
|
+
*/
|
|
265
|
+
public async getFundamentals(ticker: string): Promise<IStockFundamentals> {
|
|
266
|
+
const cacheKey = `fundamentals:${ticker}`;
|
|
267
|
+
const cached = this.getFromCache(this.fundamentalsCache, cacheKey);
|
|
268
|
+
|
|
269
|
+
if (cached) {
|
|
270
|
+
console.log(`Cache hit for fundamentals: ${ticker}`);
|
|
271
|
+
return cached as IStockFundamentals;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const providers = this.getEnabledFundamentalsProviders();
|
|
275
|
+
if (providers.length === 0) {
|
|
276
|
+
throw new Error('No fundamentals providers available');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let lastError: Error | undefined;
|
|
280
|
+
|
|
281
|
+
for (const provider of providers) {
|
|
282
|
+
const entry = this.fundamentalsProviders.get(provider.name)!;
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const result = await this.fetchWithRetry(
|
|
286
|
+
() => provider.fetchData({ type: 'fundamentals-current', ticker }),
|
|
287
|
+
entry.config
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
entry.successCount++;
|
|
291
|
+
|
|
292
|
+
const fundamentals = result as IStockFundamentals;
|
|
293
|
+
const ttl = (entry.config as IFundamentalsProviderConfig).cacheTTL || this.config.cache.fundamentalsTTL;
|
|
294
|
+
this.addToCache(this.fundamentalsCache, cacheKey, fundamentals, ttl);
|
|
295
|
+
|
|
296
|
+
console.log(`Successfully fetched fundamentals for ${ticker} from ${provider.name}`);
|
|
297
|
+
return fundamentals;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
entry.errorCount++;
|
|
300
|
+
entry.lastError = error as Error;
|
|
301
|
+
entry.lastErrorTime = new Date();
|
|
302
|
+
lastError = error as Error;
|
|
303
|
+
|
|
304
|
+
console.warn(`Provider ${provider.name} failed for ${ticker} fundamentals: ${error.message}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Failed to fetch fundamentals for ${ticker} from all providers. Last error: ${lastError?.message}`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get fundamentals for multiple tickers
|
|
315
|
+
*/
|
|
316
|
+
public async getBatchFundamentals(tickers: string[]): Promise<IStockFundamentals[]> {
|
|
317
|
+
const cacheKey = `fundamentals-batch:${tickers.sort().join(',')}`;
|
|
318
|
+
const cached = this.getFromCache(this.fundamentalsCache, cacheKey);
|
|
319
|
+
|
|
320
|
+
if (cached) {
|
|
321
|
+
console.log(`Cache hit for batch fundamentals: ${tickers.length} tickers`);
|
|
322
|
+
return cached as IStockFundamentals[];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const providers = this.getEnabledFundamentalsProviders();
|
|
326
|
+
if (providers.length === 0) {
|
|
327
|
+
throw new Error('No fundamentals providers available');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let lastError: Error | undefined;
|
|
331
|
+
|
|
332
|
+
for (const provider of providers) {
|
|
333
|
+
const entry = this.fundamentalsProviders.get(provider.name)!;
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const result = await this.fetchWithRetry(
|
|
337
|
+
() => provider.fetchData({ type: 'fundamentals-batch', tickers }),
|
|
338
|
+
entry.config
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
entry.successCount++;
|
|
342
|
+
|
|
343
|
+
const fundamentals = result as IStockFundamentals[];
|
|
344
|
+
const ttl = (entry.config as IFundamentalsProviderConfig).cacheTTL || this.config.cache.fundamentalsTTL;
|
|
345
|
+
this.addToCache(this.fundamentalsCache, cacheKey, fundamentals, ttl);
|
|
346
|
+
|
|
347
|
+
console.log(`Successfully fetched ${fundamentals.length} fundamentals from ${provider.name}`);
|
|
348
|
+
return fundamentals;
|
|
349
|
+
} catch (error) {
|
|
350
|
+
entry.errorCount++;
|
|
351
|
+
entry.lastError = error as Error;
|
|
352
|
+
entry.lastErrorTime = new Date();
|
|
353
|
+
lastError = error as Error;
|
|
354
|
+
|
|
355
|
+
console.warn(`Provider ${provider.name} failed for batch fundamentals: ${error.message}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
throw new Error(
|
|
360
|
+
`Failed to fetch fundamentals for ${tickers.length} tickers from all providers. Last error: ${lastError?.message}`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* ✨ Get complete stock data (price + fundamentals) with automatic enrichment
|
|
366
|
+
*/
|
|
367
|
+
public async getStockData(request: string | ICompleteStockDataRequest): Promise<IStockData> {
|
|
368
|
+
const normalizedRequest = typeof request === 'string'
|
|
369
|
+
? { ticker: request, includeFundamentals: true, enrichFundamentals: true }
|
|
370
|
+
: { includeFundamentals: true, enrichFundamentals: true, ...request };
|
|
371
|
+
|
|
372
|
+
const price = await this.getPrice(normalizedRequest.ticker);
|
|
373
|
+
|
|
374
|
+
let fundamentals: IStockFundamentals | undefined;
|
|
375
|
+
|
|
376
|
+
if (normalizedRequest.includeFundamentals) {
|
|
377
|
+
try {
|
|
378
|
+
fundamentals = await this.getFundamentals(normalizedRequest.ticker);
|
|
379
|
+
|
|
380
|
+
// Enrich fundamentals with price calculations
|
|
381
|
+
if (normalizedRequest.enrichFundamentals && fundamentals) {
|
|
382
|
+
fundamentals = this.enrichWithPrice(fundamentals, price.price);
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.warn(`Failed to fetch fundamentals for ${normalizedRequest.ticker}: ${error.message}`);
|
|
386
|
+
// Continue without fundamentals
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
ticker: normalizedRequest.ticker,
|
|
392
|
+
price,
|
|
393
|
+
fundamentals,
|
|
394
|
+
fetchedAt: new Date()
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* ✨ Get complete stock data for multiple tickers with automatic enrichment
|
|
400
|
+
*/
|
|
401
|
+
public async getBatchStockData(request: string[] | ICompleteStockDataBatchRequest): Promise<IStockData[]> {
|
|
402
|
+
const normalizedRequest = Array.isArray(request)
|
|
403
|
+
? { tickers: request, includeFundamentals: true, enrichFundamentals: true }
|
|
404
|
+
: { includeFundamentals: true, enrichFundamentals: true, ...request };
|
|
405
|
+
|
|
406
|
+
const prices = await this.getPrices(normalizedRequest.tickers);
|
|
407
|
+
const priceMap = new Map(prices.map(p => [p.ticker, p]));
|
|
408
|
+
|
|
409
|
+
let fundamentalsMap = new Map<string, IStockFundamentals>();
|
|
410
|
+
|
|
411
|
+
if (normalizedRequest.includeFundamentals) {
|
|
412
|
+
try {
|
|
413
|
+
const fundamentals = await this.getBatchFundamentals(normalizedRequest.tickers);
|
|
414
|
+
|
|
415
|
+
// Enrich with prices if requested
|
|
416
|
+
if (normalizedRequest.enrichFundamentals) {
|
|
417
|
+
for (const fund of fundamentals) {
|
|
418
|
+
const price = priceMap.get(fund.ticker);
|
|
419
|
+
if (price) {
|
|
420
|
+
fundamentalsMap.set(fund.ticker, this.enrichWithPrice(fund, price.price));
|
|
421
|
+
} else {
|
|
422
|
+
fundamentalsMap.set(fund.ticker, fund);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
fundamentalsMap = new Map(fundamentals.map(f => [f.ticker, f]));
|
|
427
|
+
}
|
|
428
|
+
} catch (error) {
|
|
429
|
+
console.warn(`Failed to fetch batch fundamentals: ${error.message}`);
|
|
430
|
+
// Continue without fundamentals
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return normalizedRequest.tickers.map(ticker => ({
|
|
435
|
+
ticker,
|
|
436
|
+
price: priceMap.get(ticker)!,
|
|
437
|
+
fundamentals: fundamentalsMap.get(ticker),
|
|
438
|
+
fetchedAt: new Date()
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ========== Helper Methods ==========
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Enrich fundamentals with calculated metrics using current price
|
|
446
|
+
*/
|
|
447
|
+
private enrichWithPrice(fundamentals: IStockFundamentals, price: number): IStockFundamentals {
|
|
448
|
+
const enriched = { ...fundamentals };
|
|
449
|
+
|
|
450
|
+
// Calculate market cap: price × shares outstanding
|
|
451
|
+
if (fundamentals.sharesOutstanding) {
|
|
452
|
+
enriched.marketCap = price * fundamentals.sharesOutstanding;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Calculate P/E ratio: price / EPS
|
|
456
|
+
if (fundamentals.earningsPerShareDiluted && fundamentals.earningsPerShareDiluted > 0) {
|
|
457
|
+
enriched.priceToEarnings = price / fundamentals.earningsPerShareDiluted;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Calculate price-to-book: market cap / stockholders equity
|
|
461
|
+
if (enriched.marketCap && fundamentals.stockholdersEquity && fundamentals.stockholdersEquity > 0) {
|
|
462
|
+
enriched.priceToBook = enriched.marketCap / fundamentals.stockholdersEquity;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return enriched;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Fetch with retry logic
|
|
470
|
+
*/
|
|
471
|
+
private async fetchWithRetry<T>(
|
|
472
|
+
fetchFn: () => Promise<T>,
|
|
473
|
+
config: IProviderConfig | IFundamentalsProviderConfig
|
|
474
|
+
): Promise<T> {
|
|
475
|
+
const maxAttempts = config.retryAttempts || 1;
|
|
476
|
+
let lastError: Error | undefined;
|
|
477
|
+
|
|
478
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
479
|
+
try {
|
|
480
|
+
return await fetchFn();
|
|
481
|
+
} catch (error) {
|
|
482
|
+
lastError = error as Error;
|
|
483
|
+
|
|
484
|
+
if (attempt < maxAttempts) {
|
|
485
|
+
const delay = (config.retryDelay || 1000) * attempt;
|
|
486
|
+
console.log(`Retry attempt ${attempt} after ${delay}ms`);
|
|
487
|
+
await plugins.smartdelay.delayFor(delay);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
throw lastError || new Error('Unknown error during fetch');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Get from cache if not expired
|
|
497
|
+
*/
|
|
498
|
+
private getFromCache<T>(cache: Map<string, ICacheEntry<T>>, key: string): T | null {
|
|
499
|
+
const entry = cache.get(key);
|
|
500
|
+
|
|
501
|
+
if (!entry) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Check if cache entry has expired
|
|
506
|
+
const age = Date.now() - entry.timestamp.getTime();
|
|
507
|
+
if (entry.ttl !== Infinity && age > entry.ttl) {
|
|
508
|
+
cache.delete(key);
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return entry.data;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Add to cache with TTL
|
|
517
|
+
*/
|
|
518
|
+
private addToCache<T>(cache: Map<string, ICacheEntry<T>>, key: string, data: T, ttl: number): void {
|
|
519
|
+
// Enforce max entries limit
|
|
520
|
+
if (cache.size >= this.config.cache.maxEntries) {
|
|
521
|
+
// Remove oldest entry
|
|
522
|
+
const oldestKey = cache.keys().next().value;
|
|
523
|
+
if (oldestKey) {
|
|
524
|
+
cache.delete(oldestKey);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
cache.set(key, {
|
|
529
|
+
data,
|
|
530
|
+
timestamp: new Date(),
|
|
531
|
+
ttl
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ========== Health & Statistics ==========
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Check health of all providers (both price and fundamentals)
|
|
539
|
+
*/
|
|
540
|
+
public async checkProvidersHealth(): Promise<Map<string, boolean>> {
|
|
541
|
+
const health = new Map<string, boolean>();
|
|
542
|
+
|
|
543
|
+
// Check price providers
|
|
544
|
+
for (const [name, entry] of this.priceProviders) {
|
|
545
|
+
if (!entry.config.enabled) {
|
|
546
|
+
health.set(`${name} (price)`, false);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
const isAvailable = await entry.provider.isAvailable();
|
|
552
|
+
health.set(`${name} (price)`, isAvailable);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
health.set(`${name} (price)`, false);
|
|
555
|
+
console.error(`Health check failed for ${name}:`, error);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Check fundamentals providers
|
|
560
|
+
for (const [name, entry] of this.fundamentalsProviders) {
|
|
561
|
+
if (!entry.config.enabled) {
|
|
562
|
+
health.set(`${name} (fundamentals)`, false);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
const isAvailable = await entry.provider.isAvailable();
|
|
568
|
+
health.set(`${name} (fundamentals)`, isAvailable);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
health.set(`${name} (fundamentals)`, false);
|
|
571
|
+
console.error(`Health check failed for ${name}:`, error);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return health;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Get statistics for all providers
|
|
580
|
+
*/
|
|
581
|
+
public getProviderStats(): Map<
|
|
582
|
+
string,
|
|
583
|
+
{
|
|
584
|
+
type: 'price' | 'fundamentals';
|
|
585
|
+
successCount: number;
|
|
586
|
+
errorCount: number;
|
|
587
|
+
lastError?: string;
|
|
588
|
+
lastErrorTime?: Date;
|
|
589
|
+
}
|
|
590
|
+
> {
|
|
591
|
+
const stats = new Map();
|
|
592
|
+
|
|
593
|
+
// Price provider stats
|
|
594
|
+
for (const [name, entry] of this.priceProviders) {
|
|
595
|
+
stats.set(name, {
|
|
596
|
+
type: 'price',
|
|
597
|
+
successCount: entry.successCount,
|
|
598
|
+
errorCount: entry.errorCount,
|
|
599
|
+
lastError: entry.lastError?.message,
|
|
600
|
+
lastErrorTime: entry.lastErrorTime
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Fundamentals provider stats
|
|
605
|
+
for (const [name, entry] of this.fundamentalsProviders) {
|
|
606
|
+
stats.set(name, {
|
|
607
|
+
type: 'fundamentals',
|
|
608
|
+
successCount: entry.successCount,
|
|
609
|
+
errorCount: entry.errorCount,
|
|
610
|
+
lastError: entry.lastError?.message,
|
|
611
|
+
lastErrorTime: entry.lastErrorTime
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return stats;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Clear all caches
|
|
620
|
+
*/
|
|
621
|
+
public clearCache(): void {
|
|
622
|
+
this.priceCache.clear();
|
|
623
|
+
this.fundamentalsCache.clear();
|
|
624
|
+
console.log('All caches cleared');
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Get cache statistics
|
|
629
|
+
*/
|
|
630
|
+
public getCacheStats(): {
|
|
631
|
+
priceCache: { size: number; ttl: number };
|
|
632
|
+
fundamentalsCache: { size: number; ttl: number };
|
|
633
|
+
maxEntries: number;
|
|
634
|
+
} {
|
|
635
|
+
return {
|
|
636
|
+
priceCache: {
|
|
637
|
+
size: this.priceCache.size,
|
|
638
|
+
ttl: this.config.cache.priceTTL
|
|
639
|
+
},
|
|
640
|
+
fundamentalsCache: {
|
|
641
|
+
size: this.fundamentalsCache.size,
|
|
642
|
+
ttl: this.config.cache.fundamentalsTTL
|
|
643
|
+
},
|
|
644
|
+
maxEntries: this.config.cache.maxEntries
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
}
|
package/ts/stocks/index.ts
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
export * from './interfaces/stockprice.js';
|
|
3
3
|
export * from './interfaces/provider.js';
|
|
4
4
|
export * from './interfaces/fundamentals.js';
|
|
5
|
+
export * from './interfaces/stockdata.js';
|
|
5
6
|
|
|
6
7
|
// Export main services
|
|
7
8
|
export * from './classes.stockservice.js';
|
|
8
9
|
export * from './classes.fundamentalsservice.js';
|
|
10
|
+
export * from './classes.stockdataservice.js'; // ✨ New unified service
|
|
11
|
+
|
|
12
|
+
// Export base service (for advanced use cases)
|
|
13
|
+
export * from './classes.baseproviderservice.js';
|
|
9
14
|
|
|
10
15
|
// Export providers
|
|
11
16
|
export * from './providers/provider.yahoo.js';
|