@fin.cx/opendata 3.2.2 → 3.4.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/index.d.ts +1 -0
- package/dist_ts/stocks/index.js +2 -1
- package/dist_ts/stocks/providers/provider.coingecko.d.ts +101 -0
- package/dist_ts/stocks/providers/provider.coingecko.js +596 -0
- package/dist_ts/stocks/providers/provider.marketstack.d.ts +73 -12
- package/dist_ts/stocks/providers/provider.marketstack.js +336 -63
- package/package.json +1 -1
- package/readme.hints.md +45 -1
- package/readme.md +62 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/stocks/index.ts +2 -1
- package/ts/stocks/providers/provider.coingecko.ts +757 -0
- package/ts/stocks/providers/provider.marketstack.ts +386 -69
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
/**
|
|
3
|
+
* Custom error for rate limit exceeded responses
|
|
4
|
+
*/
|
|
5
|
+
class RateLimitError extends Error {
|
|
6
|
+
constructor(message, waitTime, retryAfter) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.waitTime = waitTime;
|
|
9
|
+
this.retryAfter = retryAfter;
|
|
10
|
+
this.name = 'RateLimitError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Rate limiter for CoinGecko API
|
|
15
|
+
* Free tier (Demo): 30 requests per minute
|
|
16
|
+
* Without registration: 5-15 requests per minute
|
|
17
|
+
*/
|
|
18
|
+
class RateLimiter {
|
|
19
|
+
constructor(maxRequestsPerMinute = 30) {
|
|
20
|
+
this.requestTimes = [];
|
|
21
|
+
this.consecutiveRateLimitErrors = 0;
|
|
22
|
+
this.maxRequestsPerMinute = maxRequestsPerMinute;
|
|
23
|
+
}
|
|
24
|
+
async waitForSlot() {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const oneMinuteAgo = now - 60000;
|
|
27
|
+
// Remove requests older than 1 minute
|
|
28
|
+
this.requestTimes = this.requestTimes.filter(time => time > oneMinuteAgo);
|
|
29
|
+
// If we've hit the limit, wait
|
|
30
|
+
if (this.requestTimes.length >= this.maxRequestsPerMinute) {
|
|
31
|
+
const oldestRequest = this.requestTimes[0];
|
|
32
|
+
const waitTime = 60000 - (now - oldestRequest) + 100; // +100ms buffer
|
|
33
|
+
await plugins.smartdelay.delayFor(waitTime);
|
|
34
|
+
return this.waitForSlot(); // Recursively check again
|
|
35
|
+
}
|
|
36
|
+
// Record this request
|
|
37
|
+
this.requestTimes.push(now);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get time in milliseconds until next request slot is available
|
|
41
|
+
*/
|
|
42
|
+
getTimeUntilNextSlot() {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const oneMinuteAgo = now - 60000;
|
|
45
|
+
// Clean old requests
|
|
46
|
+
const recentRequests = this.requestTimes.filter(time => time > oneMinuteAgo);
|
|
47
|
+
if (recentRequests.length < this.maxRequestsPerMinute) {
|
|
48
|
+
return 0; // Slot available now
|
|
49
|
+
}
|
|
50
|
+
// Calculate wait time until oldest request expires
|
|
51
|
+
const oldestRequest = recentRequests[0];
|
|
52
|
+
return Math.max(0, 60000 - (now - oldestRequest) + 100);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Handle rate limit error with exponential backoff
|
|
56
|
+
* Returns wait time in milliseconds
|
|
57
|
+
*/
|
|
58
|
+
handleRateLimitError() {
|
|
59
|
+
this.consecutiveRateLimitErrors++;
|
|
60
|
+
// Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 60s (max)
|
|
61
|
+
const baseWait = 1000; // 1 second
|
|
62
|
+
const exponent = this.consecutiveRateLimitErrors - 1;
|
|
63
|
+
const backoff = Math.min(baseWait * Math.pow(2, exponent), 60000 // max 60 seconds
|
|
64
|
+
);
|
|
65
|
+
// After 3 consecutive 429s, reduce rate limit to 80% as safety measure
|
|
66
|
+
if (this.consecutiveRateLimitErrors >= 3) {
|
|
67
|
+
const newLimit = Math.floor(this.maxRequestsPerMinute * 0.8);
|
|
68
|
+
if (newLimit < this.maxRequestsPerMinute) {
|
|
69
|
+
console.warn(`Adjusting rate limit from ${this.maxRequestsPerMinute} to ${newLimit} requests/min due to repeated 429 errors`);
|
|
70
|
+
this.maxRequestsPerMinute = newLimit;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return backoff;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Reset consecutive error count on successful request
|
|
77
|
+
*/
|
|
78
|
+
resetErrors() {
|
|
79
|
+
if (this.consecutiveRateLimitErrors > 0) {
|
|
80
|
+
this.consecutiveRateLimitErrors = 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* CoinGecko Crypto Price Provider
|
|
86
|
+
*
|
|
87
|
+
* Documentation: https://docs.coingecko.com/v3.0.1/reference/endpoint-overview
|
|
88
|
+
*
|
|
89
|
+
* Features:
|
|
90
|
+
* - Current crypto prices (single and batch)
|
|
91
|
+
* - Historical price data with OHLCV
|
|
92
|
+
* - 13M+ tokens, 240+ networks, 1600+ exchanges
|
|
93
|
+
* - Accepts both ticker symbols (BTC, ETH) and CoinGecko IDs (bitcoin, ethereum)
|
|
94
|
+
* - 24/7 market data (crypto never closes)
|
|
95
|
+
*
|
|
96
|
+
* Rate Limits:
|
|
97
|
+
* - Free tier (no key): 5-15 requests/minute
|
|
98
|
+
* - Demo plan (free with registration): ~30 requests/minute, 10,000/month
|
|
99
|
+
* - Paid plans: Higher limits
|
|
100
|
+
*
|
|
101
|
+
* API Authentication:
|
|
102
|
+
* - Optional API key for Demo/paid plans
|
|
103
|
+
* - Header: x-cg-demo-api-key (Demo) or x-cg-pro-api-key (paid)
|
|
104
|
+
*/
|
|
105
|
+
export class CoinGeckoProvider {
|
|
106
|
+
constructor(apiKey, config) {
|
|
107
|
+
this.config = config;
|
|
108
|
+
this.name = 'CoinGecko';
|
|
109
|
+
this.priority = 90; // High priority for crypto, between Yahoo (100) and Marketstack (80)
|
|
110
|
+
this.requiresAuth = false; // API key is optional
|
|
111
|
+
this.rateLimit = {
|
|
112
|
+
requestsPerMinute: 30, // Demo plan default
|
|
113
|
+
requestsPerDay: 10000 // Demo plan monthly quota / 30
|
|
114
|
+
};
|
|
115
|
+
this.logger = console;
|
|
116
|
+
this.baseUrl = 'https://api.coingecko.com/api/v3';
|
|
117
|
+
// Coin mapping cache
|
|
118
|
+
this.coinMapCache = new Map(); // ticker/id -> coingecko id
|
|
119
|
+
this.coinListLoadedAt = null;
|
|
120
|
+
this.coinListCacheTTL = 24 * 60 * 60 * 1000; // 24 hours
|
|
121
|
+
// Priority map for common crypto tickers (to avoid conflicts)
|
|
122
|
+
this.priorityTickerMap = new Map([
|
|
123
|
+
['btc', 'bitcoin'],
|
|
124
|
+
['eth', 'ethereum'],
|
|
125
|
+
['usdt', 'tether'],
|
|
126
|
+
['bnb', 'binancecoin'],
|
|
127
|
+
['sol', 'solana'],
|
|
128
|
+
['usdc', 'usd-coin'],
|
|
129
|
+
['xrp', 'ripple'],
|
|
130
|
+
['ada', 'cardano'],
|
|
131
|
+
['doge', 'dogecoin'],
|
|
132
|
+
['trx', 'tron'],
|
|
133
|
+
['dot', 'polkadot'],
|
|
134
|
+
['matic', 'matic-network'],
|
|
135
|
+
['ltc', 'litecoin'],
|
|
136
|
+
['shib', 'shiba-inu'],
|
|
137
|
+
['avax', 'avalanche-2'],
|
|
138
|
+
['link', 'chainlink'],
|
|
139
|
+
['atom', 'cosmos'],
|
|
140
|
+
['uni', 'uniswap'],
|
|
141
|
+
['etc', 'ethereum-classic'],
|
|
142
|
+
['xlm', 'stellar']
|
|
143
|
+
]);
|
|
144
|
+
this.apiKey = apiKey;
|
|
145
|
+
this.rateLimiter = new RateLimiter(this.rateLimit.requestsPerMinute);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Unified data fetching method supporting all request types
|
|
149
|
+
*/
|
|
150
|
+
async fetchData(request) {
|
|
151
|
+
switch (request.type) {
|
|
152
|
+
case 'current':
|
|
153
|
+
return this.fetchCurrentPrice(request);
|
|
154
|
+
case 'batch':
|
|
155
|
+
return this.fetchBatchCurrentPrices(request);
|
|
156
|
+
case 'historical':
|
|
157
|
+
return this.fetchHistoricalPrices(request);
|
|
158
|
+
case 'intraday':
|
|
159
|
+
return this.fetchIntradayPrices(request);
|
|
160
|
+
default:
|
|
161
|
+
throw new Error(`Unsupported request type: ${request.type}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Fetch current price for a single crypto
|
|
166
|
+
*/
|
|
167
|
+
async fetchCurrentPrice(request) {
|
|
168
|
+
return this.fetchWithRateLimitRetry(async () => {
|
|
169
|
+
// Resolve ticker to CoinGecko ID
|
|
170
|
+
const coinId = await this.resolveCoinId(request.ticker);
|
|
171
|
+
// Build URL
|
|
172
|
+
const params = new URLSearchParams({
|
|
173
|
+
ids: coinId,
|
|
174
|
+
vs_currencies: 'usd',
|
|
175
|
+
include_market_cap: 'true',
|
|
176
|
+
include_24hr_vol: 'true',
|
|
177
|
+
include_24hr_change: 'true',
|
|
178
|
+
include_last_updated_at: 'true'
|
|
179
|
+
});
|
|
180
|
+
const url = `${this.baseUrl}/simple/price?${params}`;
|
|
181
|
+
// Wait for rate limit slot
|
|
182
|
+
await this.rateLimiter.waitForSlot();
|
|
183
|
+
// Make request
|
|
184
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
185
|
+
.url(url)
|
|
186
|
+
.headers(this.buildHeaders())
|
|
187
|
+
.timeout(this.config?.timeout || 10000)
|
|
188
|
+
.get();
|
|
189
|
+
const responseData = await response.json();
|
|
190
|
+
// Check for rate limit error
|
|
191
|
+
if (this.isRateLimitError(responseData)) {
|
|
192
|
+
const waitTime = this.rateLimiter.handleRateLimitError();
|
|
193
|
+
throw new RateLimitError(`Rate limit exceeded for ${request.ticker}`, waitTime);
|
|
194
|
+
}
|
|
195
|
+
if (!responseData[coinId]) {
|
|
196
|
+
throw new Error(`No data found for ${request.ticker} (${coinId})`);
|
|
197
|
+
}
|
|
198
|
+
return this.mapToStockPrice(request.ticker, coinId, responseData[coinId], 'live');
|
|
199
|
+
}, `current price for ${request.ticker}`);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Fetch batch current prices for multiple cryptos
|
|
203
|
+
*/
|
|
204
|
+
async fetchBatchCurrentPrices(request) {
|
|
205
|
+
return this.fetchWithRateLimitRetry(async () => {
|
|
206
|
+
// Resolve all tickers to CoinGecko IDs
|
|
207
|
+
const coinIds = await Promise.all(request.tickers.map(ticker => this.resolveCoinId(ticker)));
|
|
208
|
+
// Build URL with comma-separated IDs
|
|
209
|
+
const params = new URLSearchParams({
|
|
210
|
+
ids: coinIds.join(','),
|
|
211
|
+
vs_currencies: 'usd',
|
|
212
|
+
include_market_cap: 'true',
|
|
213
|
+
include_24hr_vol: 'true',
|
|
214
|
+
include_24hr_change: 'true',
|
|
215
|
+
include_last_updated_at: 'true'
|
|
216
|
+
});
|
|
217
|
+
const url = `${this.baseUrl}/simple/price?${params}`;
|
|
218
|
+
// Wait for rate limit slot
|
|
219
|
+
await this.rateLimiter.waitForSlot();
|
|
220
|
+
// Make request
|
|
221
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
222
|
+
.url(url)
|
|
223
|
+
.headers(this.buildHeaders())
|
|
224
|
+
.timeout(this.config?.timeout || 15000)
|
|
225
|
+
.get();
|
|
226
|
+
const responseData = await response.json();
|
|
227
|
+
// Check for rate limit error
|
|
228
|
+
if (this.isRateLimitError(responseData)) {
|
|
229
|
+
const waitTime = this.rateLimiter.handleRateLimitError();
|
|
230
|
+
throw new RateLimitError(`Rate limit exceeded for batch request`, waitTime);
|
|
231
|
+
}
|
|
232
|
+
const prices = [];
|
|
233
|
+
// Map responses back to original tickers
|
|
234
|
+
for (let i = 0; i < request.tickers.length; i++) {
|
|
235
|
+
const ticker = request.tickers[i];
|
|
236
|
+
const coinId = coinIds[i];
|
|
237
|
+
if (responseData[coinId]) {
|
|
238
|
+
try {
|
|
239
|
+
prices.push(this.mapToStockPrice(ticker, coinId, responseData[coinId], 'live'));
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
this.logger.warn(`Failed to parse data for ${ticker}:`, error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
this.logger.warn(`No data returned for ${ticker} (${coinId})`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (prices.length === 0) {
|
|
250
|
+
throw new Error('No valid price data received from batch request');
|
|
251
|
+
}
|
|
252
|
+
return prices;
|
|
253
|
+
}, `batch prices for ${request.tickers.length} tickers`);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Fetch historical prices with OHLCV data
|
|
257
|
+
*/
|
|
258
|
+
async fetchHistoricalPrices(request) {
|
|
259
|
+
return this.fetchWithRateLimitRetry(async () => {
|
|
260
|
+
const coinId = await this.resolveCoinId(request.ticker);
|
|
261
|
+
// Calculate days between dates
|
|
262
|
+
const days = Math.ceil((request.to.getTime() - request.from.getTime()) / (1000 * 60 * 60 * 24));
|
|
263
|
+
// Build URL
|
|
264
|
+
const params = new URLSearchParams({
|
|
265
|
+
vs_currency: 'usd',
|
|
266
|
+
days: days.toString(),
|
|
267
|
+
interval: 'daily' // Explicit daily granularity for historical data
|
|
268
|
+
});
|
|
269
|
+
const url = `${this.baseUrl}/coins/${coinId}/market_chart?${params}`;
|
|
270
|
+
// Wait for rate limit slot
|
|
271
|
+
await this.rateLimiter.waitForSlot();
|
|
272
|
+
// Make request
|
|
273
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
274
|
+
.url(url)
|
|
275
|
+
.headers(this.buildHeaders())
|
|
276
|
+
.timeout(this.config?.timeout || 20000)
|
|
277
|
+
.get();
|
|
278
|
+
const responseData = await response.json();
|
|
279
|
+
// Check for rate limit error
|
|
280
|
+
if (this.isRateLimitError(responseData)) {
|
|
281
|
+
const waitTime = this.rateLimiter.handleRateLimitError();
|
|
282
|
+
throw new RateLimitError(`Rate limit exceeded for historical ${request.ticker}`, waitTime);
|
|
283
|
+
}
|
|
284
|
+
if (!responseData.prices || !Array.isArray(responseData.prices)) {
|
|
285
|
+
this.logger.error(`Invalid API response for ${request.ticker}:`, JSON.stringify(responseData).substring(0, 500));
|
|
286
|
+
throw new Error(`Invalid response format for ${request.ticker}: ${JSON.stringify(responseData).substring(0, 200)}`);
|
|
287
|
+
}
|
|
288
|
+
const prices = [];
|
|
289
|
+
const priceData = responseData.prices;
|
|
290
|
+
const marketCapData = responseData.market_caps || [];
|
|
291
|
+
const volumeData = responseData.total_volumes || [];
|
|
292
|
+
// Process each data point
|
|
293
|
+
for (let i = 0; i < priceData.length; i++) {
|
|
294
|
+
const [timestamp, price] = priceData[i];
|
|
295
|
+
const date = new Date(timestamp);
|
|
296
|
+
// Filter by date range
|
|
297
|
+
if (date < request.from || date > request.to)
|
|
298
|
+
continue;
|
|
299
|
+
const marketCap = marketCapData[i]?.[1];
|
|
300
|
+
const volume = volumeData[i]?.[1];
|
|
301
|
+
// Calculate previous close for change calculation
|
|
302
|
+
const previousClose = i > 0 ? priceData[i - 1][1] : price;
|
|
303
|
+
const change = price - previousClose;
|
|
304
|
+
const changePercent = previousClose !== 0 ? (change / previousClose) * 100 : 0;
|
|
305
|
+
prices.push({
|
|
306
|
+
ticker: request.ticker.toUpperCase(),
|
|
307
|
+
price: price,
|
|
308
|
+
currency: 'USD',
|
|
309
|
+
change: change,
|
|
310
|
+
changePercent: changePercent,
|
|
311
|
+
previousClose: previousClose,
|
|
312
|
+
timestamp: date,
|
|
313
|
+
provider: this.name,
|
|
314
|
+
marketState: 'REGULAR', // Crypto markets are always open
|
|
315
|
+
// OHLCV data (note: market_chart doesn't provide OHLC, only close prices)
|
|
316
|
+
volume: volume,
|
|
317
|
+
dataType: 'eod',
|
|
318
|
+
fetchedAt: new Date(),
|
|
319
|
+
companyName: coinId.charAt(0).toUpperCase() + coinId.slice(1)
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return prices;
|
|
323
|
+
}, `historical prices for ${request.ticker}`);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Fetch intraday prices with hourly intervals
|
|
327
|
+
*/
|
|
328
|
+
async fetchIntradayPrices(request) {
|
|
329
|
+
return this.fetchWithRateLimitRetry(async () => {
|
|
330
|
+
const coinId = await this.resolveCoinId(request.ticker);
|
|
331
|
+
// Map interval to days parameter (CoinGecko auto-granularity)
|
|
332
|
+
// For hourly data, request 1-7 days
|
|
333
|
+
let days = 1;
|
|
334
|
+
switch (request.interval) {
|
|
335
|
+
case '1min':
|
|
336
|
+
case '5min':
|
|
337
|
+
case '10min':
|
|
338
|
+
case '15min':
|
|
339
|
+
case '30min':
|
|
340
|
+
throw new Error('CoinGecko only supports hourly intervals in market_chart. Use interval: "1hour"');
|
|
341
|
+
case '1hour':
|
|
342
|
+
days = 1; // Last 24 hours with hourly granularity
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
// Build URL (omit interval param for automatic granularity based on days)
|
|
346
|
+
const params = new URLSearchParams({
|
|
347
|
+
vs_currency: 'usd',
|
|
348
|
+
days: days.toString()
|
|
349
|
+
});
|
|
350
|
+
const url = `${this.baseUrl}/coins/${coinId}/market_chart?${params}`;
|
|
351
|
+
// Wait for rate limit slot
|
|
352
|
+
await this.rateLimiter.waitForSlot();
|
|
353
|
+
// Make request
|
|
354
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
355
|
+
.url(url)
|
|
356
|
+
.headers(this.buildHeaders())
|
|
357
|
+
.timeout(this.config?.timeout || 15000)
|
|
358
|
+
.get();
|
|
359
|
+
const responseData = await response.json();
|
|
360
|
+
// Check for rate limit error
|
|
361
|
+
if (this.isRateLimitError(responseData)) {
|
|
362
|
+
const waitTime = this.rateLimiter.handleRateLimitError();
|
|
363
|
+
throw new RateLimitError(`Rate limit exceeded for intraday ${request.ticker}`, waitTime);
|
|
364
|
+
}
|
|
365
|
+
if (!responseData.prices || !Array.isArray(responseData.prices)) {
|
|
366
|
+
this.logger.error(`Invalid API response for ${request.ticker}:`, JSON.stringify(responseData).substring(0, 500));
|
|
367
|
+
throw new Error(`Invalid response format for ${request.ticker}: ${JSON.stringify(responseData).substring(0, 200)}`);
|
|
368
|
+
}
|
|
369
|
+
const prices = [];
|
|
370
|
+
const priceData = responseData.prices;
|
|
371
|
+
const marketCapData = responseData.market_caps || [];
|
|
372
|
+
const volumeData = responseData.total_volumes || [];
|
|
373
|
+
// Apply limit if specified
|
|
374
|
+
const limit = request.limit || priceData.length;
|
|
375
|
+
const dataToProcess = priceData.slice(-limit);
|
|
376
|
+
for (let i = 0; i < dataToProcess.length; i++) {
|
|
377
|
+
const actualIndex = priceData.length - limit + i;
|
|
378
|
+
const [timestamp, price] = dataToProcess[i];
|
|
379
|
+
const date = new Date(timestamp);
|
|
380
|
+
const marketCap = marketCapData[actualIndex]?.[1];
|
|
381
|
+
const volume = volumeData[actualIndex]?.[1];
|
|
382
|
+
const previousClose = i > 0 ? dataToProcess[i - 1][1] : price;
|
|
383
|
+
const change = price - previousClose;
|
|
384
|
+
const changePercent = previousClose !== 0 ? (change / previousClose) * 100 : 0;
|
|
385
|
+
prices.push({
|
|
386
|
+
ticker: request.ticker.toUpperCase(),
|
|
387
|
+
price: price,
|
|
388
|
+
currency: 'USD',
|
|
389
|
+
change: change,
|
|
390
|
+
changePercent: changePercent,
|
|
391
|
+
previousClose: previousClose,
|
|
392
|
+
timestamp: date,
|
|
393
|
+
provider: this.name,
|
|
394
|
+
marketState: 'REGULAR',
|
|
395
|
+
volume: volume,
|
|
396
|
+
dataType: 'intraday',
|
|
397
|
+
fetchedAt: new Date(),
|
|
398
|
+
companyName: coinId.charAt(0).toUpperCase() + coinId.slice(1)
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return prices;
|
|
402
|
+
}, `intraday prices for ${request.ticker}`);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Check if CoinGecko API is available
|
|
406
|
+
*/
|
|
407
|
+
async isAvailable() {
|
|
408
|
+
try {
|
|
409
|
+
const url = `${this.baseUrl}/simple/price?ids=bitcoin&vs_currencies=usd`;
|
|
410
|
+
await this.rateLimiter.waitForSlot();
|
|
411
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
412
|
+
.url(url)
|
|
413
|
+
.headers(this.buildHeaders())
|
|
414
|
+
.timeout(5000)
|
|
415
|
+
.get();
|
|
416
|
+
const responseData = await response.json();
|
|
417
|
+
return responseData.bitcoin?.usd !== undefined;
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
this.logger.warn('CoinGecko provider is not available:', error);
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Check if a market/network is supported
|
|
426
|
+
* CoinGecko supports 240+ networks
|
|
427
|
+
*/
|
|
428
|
+
supportsMarket(market) {
|
|
429
|
+
// CoinGecko has extensive crypto network coverage
|
|
430
|
+
const supportedNetworks = [
|
|
431
|
+
'CRYPTO', 'BTC', 'ETH', 'BSC', 'POLYGON', 'AVALANCHE',
|
|
432
|
+
'SOLANA', 'ARBITRUM', 'OPTIMISM', 'BASE'
|
|
433
|
+
];
|
|
434
|
+
return supportedNetworks.includes(market.toUpperCase());
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Check if a ticker format is supported
|
|
438
|
+
* Supports both ticker symbols (BTC) and CoinGecko IDs (bitcoin)
|
|
439
|
+
*/
|
|
440
|
+
supportsTicker(ticker) {
|
|
441
|
+
// Accept alphanumeric with hyphens (for coin IDs like 'wrapped-bitcoin')
|
|
442
|
+
return /^[A-Za-z0-9\-]{1,50}$/.test(ticker);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Resolve ticker symbol or CoinGecko ID to canonical CoinGecko ID
|
|
446
|
+
* Supports both formats: "BTC" -> "bitcoin", "bitcoin" -> "bitcoin"
|
|
447
|
+
*/
|
|
448
|
+
async resolveCoinId(tickerOrId) {
|
|
449
|
+
const normalized = tickerOrId.toLowerCase();
|
|
450
|
+
// Check priority map first (for common cryptos)
|
|
451
|
+
if (this.priorityTickerMap.has(normalized)) {
|
|
452
|
+
const coinId = this.priorityTickerMap.get(normalized);
|
|
453
|
+
this.coinMapCache.set(normalized, coinId);
|
|
454
|
+
return coinId;
|
|
455
|
+
}
|
|
456
|
+
// Check cache
|
|
457
|
+
if (this.coinMapCache.has(normalized)) {
|
|
458
|
+
return this.coinMapCache.get(normalized);
|
|
459
|
+
}
|
|
460
|
+
// Check if it's already a valid CoinGecko ID (contains hyphens or is all lowercase with original case)
|
|
461
|
+
if (normalized.includes('-') || normalized === tickerOrId) {
|
|
462
|
+
// Assume it's a CoinGecko ID, cache it
|
|
463
|
+
this.coinMapCache.set(normalized, normalized);
|
|
464
|
+
return normalized;
|
|
465
|
+
}
|
|
466
|
+
// Load coin list if needed
|
|
467
|
+
if (!this.coinListLoadedAt ||
|
|
468
|
+
Date.now() - this.coinListLoadedAt.getTime() > this.coinListCacheTTL) {
|
|
469
|
+
await this.loadCoinList();
|
|
470
|
+
}
|
|
471
|
+
// Try to find in cache after loading
|
|
472
|
+
if (this.coinMapCache.has(normalized)) {
|
|
473
|
+
return this.coinMapCache.get(normalized);
|
|
474
|
+
}
|
|
475
|
+
// Not found - return as-is and let API handle the error
|
|
476
|
+
this.logger.warn(`Could not resolve ticker ${tickerOrId} to CoinGecko ID, using as-is`);
|
|
477
|
+
return normalized;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Load complete coin list from CoinGecko API
|
|
481
|
+
*/
|
|
482
|
+
async loadCoinList() {
|
|
483
|
+
try {
|
|
484
|
+
const url = `${this.baseUrl}/coins/list`;
|
|
485
|
+
await this.rateLimiter.waitForSlot();
|
|
486
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
487
|
+
.url(url)
|
|
488
|
+
.headers(this.buildHeaders())
|
|
489
|
+
.timeout(10000)
|
|
490
|
+
.get();
|
|
491
|
+
const coinList = await response.json();
|
|
492
|
+
// Build mapping: symbol -> id
|
|
493
|
+
for (const coin of coinList) {
|
|
494
|
+
const symbol = coin.symbol.toLowerCase();
|
|
495
|
+
const id = coin.id.toLowerCase();
|
|
496
|
+
// Don't overwrite priority mappings or existing cache entries
|
|
497
|
+
if (!this.priorityTickerMap.has(symbol) && !this.coinMapCache.has(symbol)) {
|
|
498
|
+
this.coinMapCache.set(symbol, id);
|
|
499
|
+
}
|
|
500
|
+
// Always cache the ID mapping
|
|
501
|
+
this.coinMapCache.set(id, id);
|
|
502
|
+
}
|
|
503
|
+
this.coinListLoadedAt = new Date();
|
|
504
|
+
this.logger.info(`Loaded ${coinList.length} coins from CoinGecko`);
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
this.logger.error('Failed to load coin list from CoinGecko:', error);
|
|
508
|
+
// Don't throw - we can still work with direct IDs
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Map CoinGecko simple/price response to IStockPrice
|
|
513
|
+
*/
|
|
514
|
+
mapToStockPrice(ticker, coinId, data, dataType) {
|
|
515
|
+
const price = data.usd;
|
|
516
|
+
const change24h = data.usd_24h_change || 0;
|
|
517
|
+
// Calculate previous close from 24h change
|
|
518
|
+
const changePercent = change24h;
|
|
519
|
+
const change = (price * changePercent) / 100;
|
|
520
|
+
const previousClose = price - change;
|
|
521
|
+
// Parse last updated timestamp
|
|
522
|
+
const timestamp = data.last_updated_at
|
|
523
|
+
? new Date(data.last_updated_at * 1000)
|
|
524
|
+
: new Date();
|
|
525
|
+
return {
|
|
526
|
+
ticker: ticker.toUpperCase(),
|
|
527
|
+
price: price,
|
|
528
|
+
currency: 'USD',
|
|
529
|
+
change: change,
|
|
530
|
+
changePercent: changePercent,
|
|
531
|
+
previousClose: previousClose,
|
|
532
|
+
timestamp: timestamp,
|
|
533
|
+
provider: this.name,
|
|
534
|
+
marketState: 'REGULAR', // Crypto markets are 24/7
|
|
535
|
+
// Volume and market cap
|
|
536
|
+
volume: data.usd_24h_vol,
|
|
537
|
+
dataType: dataType,
|
|
538
|
+
fetchedAt: new Date(),
|
|
539
|
+
// Company identification (use coin name)
|
|
540
|
+
companyName: coinId.charAt(0).toUpperCase() + coinId.slice(1),
|
|
541
|
+
companyFullName: `${coinId.charAt(0).toUpperCase() + coinId.slice(1)} (${ticker.toUpperCase()})`
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Build HTTP headers with optional API key
|
|
546
|
+
*/
|
|
547
|
+
buildHeaders() {
|
|
548
|
+
const headers = {
|
|
549
|
+
'Accept': 'application/json'
|
|
550
|
+
};
|
|
551
|
+
if (this.apiKey) {
|
|
552
|
+
// Use Demo or Pro API key header
|
|
553
|
+
// CoinGecko accepts both x-cg-demo-api-key and x-cg-pro-api-key
|
|
554
|
+
headers['x-cg-demo-api-key'] = this.apiKey;
|
|
555
|
+
}
|
|
556
|
+
return headers;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Check if response indicates a rate limit error (429)
|
|
560
|
+
*/
|
|
561
|
+
isRateLimitError(responseData) {
|
|
562
|
+
return responseData?.status?.error_code === 429;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Wrapper for fetch operations with automatic rate limit retry and exponential backoff
|
|
566
|
+
*/
|
|
567
|
+
async fetchWithRateLimitRetry(fetchFn, operationName, maxRetries = 3) {
|
|
568
|
+
let lastError;
|
|
569
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
570
|
+
try {
|
|
571
|
+
const result = await fetchFn();
|
|
572
|
+
this.rateLimiter.resetErrors();
|
|
573
|
+
return result;
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
lastError = error;
|
|
577
|
+
if (error instanceof RateLimitError) {
|
|
578
|
+
const attemptInfo = `${attempt + 1}/${maxRetries}`;
|
|
579
|
+
this.logger.warn(`Rate limit hit for ${operationName}, waiting ${error.waitTime}ms before retry ${attemptInfo}`);
|
|
580
|
+
if (attempt < maxRetries - 1) {
|
|
581
|
+
await plugins.smartdelay.delayFor(error.waitTime);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
this.logger.error(`Max retries (${maxRetries}) exceeded for ${operationName} due to rate limiting`);
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// Non-rate-limit errors: throw immediately
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
throw lastError;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJvdmlkZXIuY29pbmdlY2tvLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vdHMvc3RvY2tzL3Byb3ZpZGVycy9wcm92aWRlci5jb2luZ2Vja28udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxrQkFBa0IsQ0FBQztBQVc1Qzs7R0FFRztBQUNILE1BQU0sY0FBZSxTQUFRLEtBQUs7SUFDaEMsWUFDRSxPQUFlLEVBQ1IsUUFBZ0IsRUFDaEIsVUFBbUI7UUFFMUIsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBSFIsYUFBUSxHQUFSLFFBQVEsQ0FBUTtRQUNoQixlQUFVLEdBQVYsVUFBVSxDQUFTO1FBRzFCLElBQUksQ0FBQyxJQUFJLEdBQUcsZ0JBQWdCLENBQUM7SUFDL0IsQ0FBQztDQUNGO0FBRUQ7Ozs7R0FJRztBQUNILE1BQU0sV0FBVztJQUtmLFlBQVksdUJBQStCLEVBQUU7UUFKckMsaUJBQVksR0FBYSxFQUFFLENBQUM7UUFFNUIsK0JBQTBCLEdBQVcsQ0FBQyxDQUFDO1FBRzdDLElBQUksQ0FBQyxvQkFBb0IsR0FBRyxvQkFBb0IsQ0FBQztJQUNuRCxDQUFDO0lBRU0sS0FBSyxDQUFDLFdBQVc7UUFDdEIsTUFBTSxHQUFHLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDO1FBQ3ZCLE1BQU0sWUFBWSxHQUFHLEdBQUcsR0FBRyxLQUFLLENBQUM7UUFFakMsc0NBQXNDO1FBQ3RDLElBQUksQ0FBQyxZQUFZLEdBQUcsSUFBSSxDQUFDLFlBQVksQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLEdBQUcsWUFBWSxDQUFDLENBQUM7UUFFMUUsK0JBQStCO1FBQy9CLElBQUksSUFBSSxDQUFDLFlBQVksQ0FBQyxNQUFNLElBQUksSUFBSSxDQUFDLG9CQUFvQixFQUFFLENBQUM7WUFDMUQsTUFBTSxhQUFhLEdBQUcsSUFBSSxDQUFDLFlBQVksQ0FBQyxDQUFD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
|