@pioneer-platform/markets 8.12.0 → 8.13.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,356 @@
1
+ "use strict";
2
+ /*
3
+ API Metrics Reporter
4
+
5
+ Collects and reports API usage metrics to Discord via alerts bridge
6
+ Runs hourly to track pricing API health and usage
7
+ */
8
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
9
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
10
+ return new (P || (P = Promise))(function (resolve, reject) {
11
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
12
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
13
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
14
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
15
+ });
16
+ };
17
+ var __generator = (this && this.__generator) || function (thisArg, body) {
18
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
19
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
20
+ function verb(n) { return function (v) { return step([n, v]); }; }
21
+ function step(op) {
22
+ if (f) throw new TypeError("Generator is already executing.");
23
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
24
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
25
+ if (y = 0, t) op = [op[0] & 2, t.value];
26
+ switch (op[0]) {
27
+ case 0: case 1: t = op; break;
28
+ case 4: _.label++; return { value: op[1], done: false };
29
+ case 5: _.label++; y = op[1]; op = [0]; continue;
30
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
31
+ default:
32
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
33
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
34
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
35
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
36
+ if (t[2]) _.ops.pop();
37
+ _.trys.pop(); continue;
38
+ }
39
+ op = body.call(thisArg, _);
40
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
41
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
42
+ }
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.sendHourlyMetricsReport = sendHourlyMetricsReport;
46
+ exports.getCurrentMetrics = getCurrentMetrics;
47
+ exports.getUnpriceableTokens = getUnpriceableTokens;
48
+ exports.clearUnpriceableToken = clearUnpriceableToken;
49
+ exports.startHourlyReporting = startHourlyReporting;
50
+ var log = require('@pioneer-platform/loggerdog')();
51
+ var redis = require('@pioneer-platform/default-redis').redis;
52
+ var axios = require('axios');
53
+ var TAG = ' | api-metrics-reporter | ';
54
+ // Discord webhook via alerts bridge
55
+ var ALERTS_BRIDGE_URL = process.env.ALERTS_BRIDGE_URL || 'http://localhost:9000/api/v1/alert';
56
+ var ALERTS_ENABLED = process.env.ALERTS_ENABLED !== 'false';
57
+ // Redis keys
58
+ var REDIS_API_METRICS_KEY = 'markets:api_metrics';
59
+ var REDIS_UNPRICEABLE_SET = 'markets:unpriceable_tokens';
60
+ var REDIS_LAST_REPORT_KEY = 'markets:last_metrics_report';
61
+ /**
62
+ * Calculate success rate percentage
63
+ */
64
+ function calculateSuccessRate(success, failures) {
65
+ var total = success + failures;
66
+ if (total === 0)
67
+ return '0%';
68
+ return ((success / total) * 100).toFixed(1) + '%';
69
+ }
70
+ /**
71
+ * Get API health emoji
72
+ */
73
+ function getHealthEmoji(successRate) {
74
+ if (successRate >= 95)
75
+ return '🟢';
76
+ if (successRate >= 80)
77
+ return '🟡';
78
+ if (successRate >= 50)
79
+ return '🟠';
80
+ return '🔴';
81
+ }
82
+ /**
83
+ * Send alert to Discord via alerts bridge
84
+ */
85
+ function sendDiscordAlert(title_1, message_1) {
86
+ return __awaiter(this, arguments, void 0, function (title, message, level) {
87
+ var tag, error_1;
88
+ if (level === void 0) { level = 'info'; }
89
+ return __generator(this, function (_a) {
90
+ switch (_a.label) {
91
+ case 0:
92
+ tag = TAG + 'sendDiscordAlert | ';
93
+ if (!ALERTS_ENABLED) {
94
+ log.debug(tag, 'Alerts disabled, skipping Discord notification');
95
+ return [2 /*return*/];
96
+ }
97
+ _a.label = 1;
98
+ case 1:
99
+ _a.trys.push([1, 3, , 4]);
100
+ return [4 /*yield*/, axios.post(ALERTS_BRIDGE_URL, {
101
+ service: 'pioneer-markets',
102
+ level: level,
103
+ title: title,
104
+ message: message,
105
+ timestamp: new Date().toISOString()
106
+ }, {
107
+ timeout: 5000
108
+ })];
109
+ case 2:
110
+ _a.sent();
111
+ log.info(tag, "Sent Discord alert: ".concat(title));
112
+ return [3 /*break*/, 4];
113
+ case 3:
114
+ error_1 = _a.sent();
115
+ log.error(tag, "Failed to send Discord alert: ".concat(error_1.message));
116
+ return [3 /*break*/, 4];
117
+ case 4: return [2 /*return*/];
118
+ }
119
+ });
120
+ });
121
+ }
122
+ /**
123
+ * Generate and send hourly metrics report
124
+ */
125
+ function sendHourlyMetricsReport() {
126
+ return __awaiter(this, void 0, void 0, function () {
127
+ var tag, lastReport, now, timeSinceLastReport, oneHour, metricsData, metrics, unpriceableCount, totalSuccess, totalFailures, criticalErrors, apiReport, _i, _a, _b, api, stats, success, failures, total, rate, totalRate, reportMessage, _c, apiReport_1, api, _d, criticalErrors_1, error, level, error_2;
128
+ return __generator(this, function (_e) {
129
+ switch (_e.label) {
130
+ case 0:
131
+ tag = TAG + 'sendHourlyMetricsReport | ';
132
+ _e.label = 1;
133
+ case 1:
134
+ _e.trys.push([1, 8, , 9]);
135
+ return [4 /*yield*/, redis.get(REDIS_LAST_REPORT_KEY)];
136
+ case 2:
137
+ lastReport = _e.sent();
138
+ now = Date.now();
139
+ if (lastReport) {
140
+ timeSinceLastReport = now - parseInt(lastReport);
141
+ oneHour = 60 * 60 * 1000;
142
+ if (timeSinceLastReport < oneHour) {
143
+ log.debug(tag, "Skipping report - last sent ".concat(Math.floor(timeSinceLastReport / 1000 / 60), " minutes ago"));
144
+ return [2 /*return*/];
145
+ }
146
+ }
147
+ return [4 /*yield*/, redis.get(REDIS_API_METRICS_KEY)];
148
+ case 3:
149
+ metricsData = _e.sent();
150
+ if (!metricsData) {
151
+ log.debug(tag, 'No metrics data available yet');
152
+ return [2 /*return*/];
153
+ }
154
+ metrics = JSON.parse(metricsData);
155
+ return [4 /*yield*/, redis.scard(REDIS_UNPRICEABLE_SET)];
156
+ case 4:
157
+ unpriceableCount = _e.sent();
158
+ totalSuccess = 0;
159
+ totalFailures = 0;
160
+ criticalErrors = [];
161
+ apiReport = [];
162
+ for (_i = 0, _a = Object.entries(metrics); _i < _a.length; _i++) {
163
+ _b = _a[_i], api = _b[0], stats = _b[1];
164
+ if (api === 'timestamp')
165
+ continue;
166
+ success = stats.success || 0;
167
+ failures = stats.failures || 0;
168
+ total = success + failures;
169
+ rate = total > 0 ? (success / total) * 100 : 0;
170
+ totalSuccess += success;
171
+ totalFailures += failures;
172
+ apiReport.push({
173
+ name: api.charAt(0).toUpperCase() + api.slice(1),
174
+ success: success,
175
+ failures: failures,
176
+ rate: rate,
177
+ emoji: getHealthEmoji(rate),
178
+ lastError: stats.lastError
179
+ });
180
+ // Track critical errors (< 50% success rate)
181
+ if (rate < 50 && total > 10) {
182
+ criticalErrors.push("".concat(api, ": ").concat(rate.toFixed(1), "% success rate"));
183
+ }
184
+ }
185
+ // Sort by success rate
186
+ apiReport.sort(function (a, b) { return b.rate - a.rate; });
187
+ totalRate = totalSuccess + totalFailures > 0
188
+ ? ((totalSuccess / (totalSuccess + totalFailures)) * 100).toFixed(1)
189
+ : '0.0';
190
+ reportMessage = "**\uD83D\uDCCA Pricing API Health Report**\n\n";
191
+ reportMessage += "**Period:** Last hour\n";
192
+ reportMessage += "**Overall Success Rate:** ".concat(totalRate, "%\n");
193
+ reportMessage += "**Total Requests:** ".concat(totalSuccess + totalFailures, "\n");
194
+ reportMessage += "**Unpriceable Tokens:** ".concat(unpriceableCount, "\n\n");
195
+ reportMessage += "**API Performance:**\n";
196
+ for (_c = 0, apiReport_1 = apiReport; _c < apiReport_1.length; _c++) {
197
+ api = apiReport_1[_c];
198
+ reportMessage += "".concat(api.emoji, " **").concat(api.name, "**: ").concat(api.rate.toFixed(1), "% ");
199
+ reportMessage += "(".concat(api.success, "\u2713 / ").concat(api.failures, "\u2717)\n");
200
+ if (api.lastError && api.failures > 0) {
201
+ reportMessage += " \u2514\u2500 Last error: `".concat(api.lastError.substring(0, 80), "`\n");
202
+ }
203
+ }
204
+ // Add warnings if any
205
+ if (criticalErrors.length > 0) {
206
+ reportMessage += "\n**\u26A0\uFE0F Critical Issues:**\n";
207
+ for (_d = 0, criticalErrors_1 = criticalErrors; _d < criticalErrors_1.length; _d++) {
208
+ error = criticalErrors_1[_d];
209
+ reportMessage += "\u2022 ".concat(error, "\n");
210
+ }
211
+ }
212
+ // Add unpriceable token warning
213
+ if (unpriceableCount > 100) {
214
+ reportMessage += "\n**\u26A0\uFE0F High number of unpriceable tokens detected**\n";
215
+ reportMessage += "".concat(unpriceableCount, " tokens marked as unpriceable (likely scam/spam)\n");
216
+ }
217
+ level = criticalErrors.length > 0 ? 'warn' : 'info';
218
+ // Send to Discord
219
+ return [4 /*yield*/, sendDiscordAlert("Pricing API Metrics - ".concat(new Date().toLocaleString()), reportMessage, level)];
220
+ case 5:
221
+ // Send to Discord
222
+ _e.sent();
223
+ // Update last report timestamp
224
+ return [4 /*yield*/, redis.set(REDIS_LAST_REPORT_KEY, now.toString())];
225
+ case 6:
226
+ // Update last report timestamp
227
+ _e.sent();
228
+ // Reset metrics for next hour
229
+ return [4 /*yield*/, redis.del(REDIS_API_METRICS_KEY)];
230
+ case 7:
231
+ // Reset metrics for next hour
232
+ _e.sent();
233
+ log.info(tag, "Sent hourly metrics report - Overall: ".concat(totalRate, "%, Alerts: ").concat(criticalErrors.length));
234
+ return [3 /*break*/, 9];
235
+ case 8:
236
+ error_2 = _e.sent();
237
+ log.error(tag, "Error generating metrics report:", error_2);
238
+ return [3 /*break*/, 9];
239
+ case 9: return [2 /*return*/];
240
+ }
241
+ });
242
+ });
243
+ }
244
+ /**
245
+ * Get current metrics (for API endpoint)
246
+ */
247
+ function getCurrentMetrics() {
248
+ return __awaiter(this, void 0, void 0, function () {
249
+ var tag, metricsData, unpriceableCount, metrics, error_3;
250
+ return __generator(this, function (_a) {
251
+ switch (_a.label) {
252
+ case 0:
253
+ tag = TAG + 'getCurrentMetrics | ';
254
+ _a.label = 1;
255
+ case 1:
256
+ _a.trys.push([1, 4, , 5]);
257
+ return [4 /*yield*/, redis.get(REDIS_API_METRICS_KEY)];
258
+ case 2:
259
+ metricsData = _a.sent();
260
+ return [4 /*yield*/, redis.scard(REDIS_UNPRICEABLE_SET)];
261
+ case 3:
262
+ unpriceableCount = _a.sent();
263
+ if (!metricsData) {
264
+ return [2 /*return*/, {
265
+ message: 'No metrics data available',
266
+ unpriceableTokens: unpriceableCount
267
+ }];
268
+ }
269
+ metrics = JSON.parse(metricsData);
270
+ return [2 /*return*/, {
271
+ metrics: metrics,
272
+ unpriceableTokens: unpriceableCount,
273
+ timestamp: new Date().toISOString()
274
+ }];
275
+ case 4:
276
+ error_3 = _a.sent();
277
+ log.error(tag, "Error getting metrics:", error_3);
278
+ return [2 /*return*/, { error: 'Failed to retrieve metrics' }];
279
+ case 5: return [2 /*return*/];
280
+ }
281
+ });
282
+ });
283
+ }
284
+ /**
285
+ * Get list of unpriceable tokens
286
+ */
287
+ function getUnpriceableTokens() {
288
+ return __awaiter(this, arguments, void 0, function (limit) {
289
+ var tag, tokens, error_4;
290
+ if (limit === void 0) { limit = 100; }
291
+ return __generator(this, function (_a) {
292
+ switch (_a.label) {
293
+ case 0:
294
+ tag = TAG + 'getUnpriceableTokens | ';
295
+ _a.label = 1;
296
+ case 1:
297
+ _a.trys.push([1, 3, , 4]);
298
+ return [4 /*yield*/, redis.smembers(REDIS_UNPRICEABLE_SET)];
299
+ case 2:
300
+ tokens = _a.sent();
301
+ return [2 /*return*/, tokens.slice(0, limit)];
302
+ case 3:
303
+ error_4 = _a.sent();
304
+ log.error(tag, "Error getting unpriceable tokens:", error_4);
305
+ return [2 /*return*/, []];
306
+ case 4: return [2 /*return*/];
307
+ }
308
+ });
309
+ });
310
+ }
311
+ /**
312
+ * Clear a token from unpriceable list (for manual recovery)
313
+ */
314
+ function clearUnpriceableToken(caip) {
315
+ return __awaiter(this, void 0, void 0, function () {
316
+ var tag, error_5;
317
+ return __generator(this, function (_a) {
318
+ switch (_a.label) {
319
+ case 0:
320
+ tag = TAG + 'clearUnpriceableToken | ';
321
+ _a.label = 1;
322
+ case 1:
323
+ _a.trys.push([1, 3, , 4]);
324
+ return [4 /*yield*/, redis.srem(REDIS_UNPRICEABLE_SET, caip)];
325
+ case 2:
326
+ _a.sent();
327
+ log.info(tag, "Removed ".concat(caip, " from unpriceable list"));
328
+ return [2 /*return*/, true];
329
+ case 3:
330
+ error_5 = _a.sent();
331
+ log.error(tag, "Error clearing unpriceable token:", error_5);
332
+ return [2 /*return*/, false];
333
+ case 4: return [2 /*return*/];
334
+ }
335
+ });
336
+ });
337
+ }
338
+ /**
339
+ * Start hourly reporting (call this on module initialization)
340
+ */
341
+ function startHourlyReporting() {
342
+ var tag = TAG + 'startHourlyReporting | ';
343
+ // Send initial report after 5 minutes
344
+ setTimeout(function () {
345
+ sendHourlyMetricsReport().catch(function (err) {
346
+ log.error(tag, 'Error in initial metrics report:', err);
347
+ });
348
+ }, 5 * 60 * 1000);
349
+ // Then send every hour
350
+ setInterval(function () {
351
+ sendHourlyMetricsReport().catch(function (err) {
352
+ log.error(tag, 'Error in hourly metrics report:', err);
353
+ });
354
+ }, 60 * 60 * 1000);
355
+ log.info(tag, '✅ Hourly metrics reporting started');
356
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Get price for a symbol from any available exchange
3
+ * Tries exchanges in order of preference until successful
4
+ */
5
+ export declare function getPriceFromCCXT(symbol: string): Promise<number>;
6
+ /**
7
+ * Get prices for multiple symbols in parallel
8
+ * More efficient than calling getPriceFromCCXT multiple times
9
+ */
10
+ export declare function getBatchPricesFromCCXT(symbols: string[]): Promise<Record<string, number>>;
11
+ /**
12
+ * Clear the price cache
13
+ * Useful for testing or when fresh prices are needed
14
+ */
15
+ export declare function clearPriceCache(): void;
16
+ /**
17
+ * Get cache statistics
18
+ */
19
+ export declare function getCacheStats(): {
20
+ size: number;
21
+ entries: Array<{
22
+ symbol: string;
23
+ price: number;
24
+ age: number;
25
+ source: string;
26
+ }>;
27
+ };
28
+ /**
29
+ * Health check - verify we can connect to exchanges
30
+ */
31
+ export declare function healthCheck(): Promise<{
32
+ healthy: boolean;
33
+ exchanges: Record<string, boolean>;
34
+ }>;