@tradejs/cli 1.0.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,391 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/scripts/derivativesIngestCoinalyzeAll.ts
26
+ var import_config = require("dotenv/config");
27
+ var import_args = __toESM(require("args"));
28
+ var import_chalk = __toESM(require("chalk"));
29
+ var import_progress = __toESM(require("progress"));
30
+ var import_lodash = __toESM(require("lodash"));
31
+ var import_connectors = require("@tradejs/connectors");
32
+ var import_indicators = require("@tradejs/core/indicators");
33
+ var import_async = require("@tradejs/core/async");
34
+ var import_cli = require("@tradejs/node/cli");
35
+ var import_timescale = require("@tradejs/infra/timescale");
36
+ var coinalyzeIntervalMap = {
37
+ "15m": "15min",
38
+ "1h": "1hour"
39
+ };
40
+ import_args.default.example(
41
+ "yarn ts-node ./src/scripts/derivativesIngestCoinalyzeAll --days 120 --intervals 15m,1h",
42
+ "Fetch derivatives for all getTickers symbols matched to Coinalyze markets"
43
+ );
44
+ import_args.default.option(["U", "user"], "Bybit user config name from Redis", "root");
45
+ import_args.default.option(["t", "tickers"], "Comma-separated include symbols");
46
+ import_args.default.option(["e", "exclude"], "Comma-separated exclude symbols");
47
+ import_args.default.option(["l", "tickersLimit"], "Tickers limit");
48
+ import_args.default.option(["c", "chunk"], "Chunk selector, e.g. 1/4");
49
+ import_args.default.option(["i", "intervals"], "Intervals: 15m,1h", "15m,1h");
50
+ import_args.default.option(["d", "days"], "Lookback in days", 120);
51
+ import_args.default.option(["b", "batchDays"], "Request chunk size in days", 120);
52
+ import_args.default.option(
53
+ ["E", "exchangePriority"],
54
+ "Coinalyze exchange priority, comma-separated",
55
+ "A,6,0"
56
+ );
57
+ import_args.default.option(
58
+ ["S", "symbolBatchSize"],
59
+ "How many symbols to request in one Coinalyze call",
60
+ 8
61
+ );
62
+ import_args.default.option(
63
+ ["w", "requestDelayMs"],
64
+ "Delay between API requests to smooth rate limit",
65
+ 200
66
+ );
67
+ import_args.default.option(
68
+ ["T", "requestTimeoutMs"],
69
+ "HTTP request timeout in milliseconds",
70
+ 45e3
71
+ );
72
+ import_args.default.option(["L", "showTickersList"], "Print matched symbols only", false);
73
+ var flags = import_args.default.parse(process.argv);
74
+ var asInt = (value, fallback) => {
75
+ const parsed = Number.parseInt(String(value ?? ""), 10);
76
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
77
+ };
78
+ var parseList = (value) => String(value ?? "").split(",").map((item) => item.trim().toUpperCase()).filter(Boolean);
79
+ var apiKey = process.env.COINALYZE_API_KEY?.trim();
80
+ if (!apiKey) {
81
+ throw new Error("Missing COINALYZE_API_KEY");
82
+ }
83
+ var coinalyzeBaseUrl = process.env.COINALYZE_BASE_URL?.trim() || "https://api.coinalyze.net/v1";
84
+ var coinalyzeMaxRetries = asInt(process.env.COINALYZE_MAX_RETRIES, 4);
85
+ var lastRequestTs = 0;
86
+ var fetchJsonWithRateLimit = async (url) => {
87
+ const requestDelayMs = Math.max(100, asInt(flags.requestDelayMs, 200));
88
+ const requestTimeoutMs = Math.max(
89
+ 5e3,
90
+ asInt(flags.requestTimeoutMs, 45e3)
91
+ );
92
+ for (let attempt = 0; attempt <= coinalyzeMaxRetries; attempt += 1) {
93
+ const waitMs = Math.max(0, lastRequestTs + requestDelayMs - Date.now());
94
+ if (waitMs > 0) {
95
+ await (0, import_async.delay)(waitMs);
96
+ }
97
+ lastRequestTs = Date.now();
98
+ const controller = new AbortController();
99
+ const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
100
+ let response;
101
+ try {
102
+ response = await fetch(url, {
103
+ headers: {
104
+ api_key: apiKey,
105
+ "x-api-key": apiKey,
106
+ Authorization: `Bearer ${apiKey}`
107
+ },
108
+ signal: controller.signal
109
+ });
110
+ } catch (error) {
111
+ clearTimeout(timer);
112
+ const abortError = error instanceof Error && (error.name === "AbortError" || String(error.message).toLowerCase().includes("aborted"));
113
+ if (attempt < coinalyzeMaxRetries && abortError) {
114
+ const backoffMs = Math.min(12e3, 800 * 2 ** attempt);
115
+ await (0, import_async.delay)(backoffMs);
116
+ continue;
117
+ }
118
+ throw error;
119
+ } finally {
120
+ clearTimeout(timer);
121
+ }
122
+ if (response.ok) {
123
+ return response.json();
124
+ }
125
+ const text = await response.text();
126
+ const retryAfterRaw = Number(response.headers.get("retry-after") ?? "");
127
+ const retryAfterMs = Number.isFinite(retryAfterRaw) && retryAfterRaw > 0 ? retryAfterRaw * 1e3 : null;
128
+ const transient = response.status === 429 || response.status >= 500;
129
+ if (attempt < coinalyzeMaxRetries && transient) {
130
+ const backoffMs = Math.min(12e3, 800 * 2 ** attempt);
131
+ await (0, import_async.delay)(retryAfterMs ?? backoffMs);
132
+ continue;
133
+ }
134
+ throw new Error(`Coinalyze ${response.status}: ${text}`);
135
+ }
136
+ throw new Error("Coinalyze request failed after retries");
137
+ };
138
+ var fetchCoinalyzeMarkets = async () => {
139
+ const raw = await fetchJsonWithRateLimit(
140
+ `${coinalyzeBaseUrl}/future-markets`
141
+ );
142
+ return Array.isArray(raw) ? raw : [];
143
+ };
144
+ var selectBestMarket = (candidates, exchangePriority) => {
145
+ const scored = candidates.map((item) => {
146
+ let score = 0;
147
+ if (item.is_perpetual) score += 50;
148
+ if (String(item.quote_asset || "").toUpperCase() === "USDT") score += 25;
149
+ if (item.symbol.includes("_PERP")) score += 10;
150
+ const exchange = String(item.exchange || "").toUpperCase();
151
+ const idx = exchangePriority.indexOf(exchange);
152
+ if (idx >= 0) {
153
+ score += (exchangePriority.length - idx) * 10;
154
+ }
155
+ return { item, score };
156
+ });
157
+ scored.sort((a, b) => {
158
+ if (b.score !== a.score) return b.score - a.score;
159
+ return a.item.symbol.localeCompare(b.item.symbol);
160
+ });
161
+ return scored[0]?.item ?? null;
162
+ };
163
+ var buildMatches = (tickers, markets, exchangePriority) => {
164
+ const marketByTicker = /* @__PURE__ */ new Map();
165
+ for (const market of markets) {
166
+ const key = String(market.symbol_on_exchange || "").trim().toUpperCase();
167
+ if (!key) continue;
168
+ const list = marketByTicker.get(key) ?? [];
169
+ list.push(market);
170
+ marketByTicker.set(key, list);
171
+ }
172
+ const matches = [];
173
+ const unmatched = [];
174
+ for (const ticker of tickers) {
175
+ const candidates = marketByTicker.get(ticker) ?? [];
176
+ const selected = selectBestMarket(candidates, exchangePriority);
177
+ if (!selected) {
178
+ unmatched.push(ticker);
179
+ continue;
180
+ }
181
+ matches.push({
182
+ symbol: ticker,
183
+ marketSymbol: selected.symbol,
184
+ exchange: selected.exchange
185
+ });
186
+ }
187
+ return { matches, unmatched };
188
+ };
189
+ var normalizeMetricPoint = (metric, point) => {
190
+ if (metric === "oi") {
191
+ return {
192
+ ...point,
193
+ open_interest: point.open_interest ?? point.openInterest ?? point.oi ?? point.c
194
+ };
195
+ }
196
+ if (metric === "funding") {
197
+ return {
198
+ ...point,
199
+ funding_rate: point.funding_rate ?? point.fundingRate ?? point.rate ?? point.c
200
+ };
201
+ }
202
+ return {
203
+ ...point,
204
+ liq_long: point.liq_long ?? point.long_liq ?? point.long ?? point.l,
205
+ liq_short: point.liq_short ?? point.short_liq ?? point.short ?? point.s
206
+ };
207
+ };
208
+ var toSeriesMap = (raw, metric) => {
209
+ const out = /* @__PURE__ */ new Map();
210
+ if (!Array.isArray(raw)) return out;
211
+ for (const item of raw) {
212
+ const symbol = String(item.symbol || "").trim().toUpperCase();
213
+ const history = Array.isArray(item.history) ? item.history : [];
214
+ if (!symbol) continue;
215
+ out.set(
216
+ symbol,
217
+ history.filter((point) => point && typeof point === "object").map((point) => normalizeMetricPoint(metric, point))
218
+ );
219
+ }
220
+ return out;
221
+ };
222
+ var fetchMetricBatch = async (params) => {
223
+ const { endpoint, metric, marketSymbols, interval, fromMs, toMs } = params;
224
+ const url = new URL(`${coinalyzeBaseUrl}${endpoint}`);
225
+ url.searchParams.set("symbols", marketSymbols.join(","));
226
+ url.searchParams.set("interval", coinalyzeIntervalMap[interval] || interval);
227
+ url.searchParams.set("from", String(Math.floor(fromMs / 1e3)));
228
+ url.searchParams.set("to", String(Math.floor(toMs / 1e3)));
229
+ const raw = await fetchJsonWithRateLimit(url.toString());
230
+ return toSeriesMap(raw, metric);
231
+ };
232
+ var run = async () => {
233
+ const intervals = (0, import_indicators.normalizeDerivativesIntervals)(
234
+ flags.intervals
235
+ );
236
+ const days = asInt(flags.days, 120);
237
+ const batchDays = asInt(flags.batchDays, 120);
238
+ const symbolBatchSize = asInt(flags.symbolBatchSize, 25);
239
+ const tickersLimit = flags.tickersLimit !== void 0 ? asInt(flags.tickersLimit, 0) || void 0 : void 0;
240
+ const exchangePriority = parseList(flags.exchangePriority || "A,6,0");
241
+ if (!intervals.length) throw new Error("No intervals provided");
242
+ const connectorFactory = import_connectors.connectors[import_connectors.ConnectorNames.ByBit];
243
+ const bybit = await connectorFactory({
244
+ userName: String(flags.user || "root")
245
+ });
246
+ const tickers = await (0, import_cli.getTickers)(
247
+ bybit,
248
+ String(flags.tickers || ""),
249
+ String(flags.exclude || ""),
250
+ tickersLimit,
251
+ String(flags.chunk || "")
252
+ );
253
+ if (!tickers.length) {
254
+ throw new Error("No tickers loaded via getTickers");
255
+ }
256
+ const markets = await fetchCoinalyzeMarkets();
257
+ if (!markets.length) {
258
+ throw new Error("No markets returned by Coinalyze /future-markets");
259
+ }
260
+ const { matches, unmatched } = buildMatches(
261
+ tickers,
262
+ markets,
263
+ exchangePriority
264
+ );
265
+ if (!matches.length) {
266
+ throw new Error("No matched symbols between getTickers and Coinalyze");
267
+ }
268
+ console.log(
269
+ import_chalk.default.cyan(
270
+ `Tickers=${tickers.length}, matched=${matches.length}, unmatched=${unmatched.length}, intervals=${intervals.join(",")}, symbolBatch=${symbolBatchSize}`
271
+ )
272
+ );
273
+ if (unmatched.length) {
274
+ console.log(
275
+ import_chalk.default.yellow(
276
+ `Unmatched sample (${Math.min(20, unmatched.length)}): ${unmatched.slice(0, 20).join(", ")}`
277
+ )
278
+ );
279
+ }
280
+ if (flags.showTickersList) {
281
+ console.log(
282
+ JSON.stringify(
283
+ matches.map((item) => ({
284
+ symbol: item.symbol,
285
+ coinalyze: item.marketSymbol,
286
+ exchange: item.exchange
287
+ })),
288
+ null,
289
+ 2
290
+ )
291
+ );
292
+ return;
293
+ }
294
+ await (0, import_timescale.waitForDbReady)();
295
+ const now = Date.now();
296
+ const fromMs = now - days * 24 * 60 * 60 * 1e3;
297
+ const symbolBatches = import_lodash.default.chunk(matches, symbolBatchSize);
298
+ const windowsPerPair = Math.ceil(days / batchDays);
299
+ const totalWindows = symbolBatches.length * intervals.length * windowsPerPair;
300
+ const bar = new import_progress.default(
301
+ ":current/:total [:bar][:percent] :eta(s) :batch rows=:rows fail=:fail",
302
+ {
303
+ total: Math.max(1, totalWindows),
304
+ width: 30
305
+ }
306
+ );
307
+ const oiPath = process.env.COINALYZE_OI_PATH?.trim() || "/open-interest-history";
308
+ const fundingPath = process.env.COINALYZE_FUNDING_PATH?.trim() || "/funding-rate-history";
309
+ const liqPath = process.env.COINALYZE_LIQ_PATH?.trim() || "/liquidation-history";
310
+ let totalRows = 0;
311
+ let failedWindows = 0;
312
+ for (const interval of intervals) {
313
+ for (let batchIdx = 0; batchIdx < symbolBatches.length; batchIdx += 1) {
314
+ const batch = symbolBatches[batchIdx];
315
+ const marketSymbols = batch.map((item) => item.marketSymbol);
316
+ let cursor = fromMs;
317
+ while (cursor < now) {
318
+ const toMs = Math.min(now, cursor + batchDays * 24 * 60 * 60 * 1e3);
319
+ try {
320
+ const oiMap = await fetchMetricBatch({
321
+ endpoint: oiPath,
322
+ metric: "oi",
323
+ marketSymbols,
324
+ interval,
325
+ fromMs: cursor,
326
+ toMs
327
+ });
328
+ const fundingMap = await fetchMetricBatch({
329
+ endpoint: fundingPath,
330
+ metric: "funding",
331
+ marketSymbols,
332
+ interval,
333
+ fromMs: cursor,
334
+ toMs
335
+ });
336
+ const liqMap = await fetchMetricBatch({
337
+ endpoint: liqPath,
338
+ metric: "liq",
339
+ marketSymbols,
340
+ interval,
341
+ fromMs: cursor,
342
+ toMs
343
+ });
344
+ const rows = batch.flatMap((item) => {
345
+ const marketSymbol = item.marketSymbol.toUpperCase();
346
+ const points = (0, import_indicators.mergeCoinalyzeMetrics)({
347
+ symbol: item.symbol,
348
+ oiRaw: oiMap.get(marketSymbol) ?? [],
349
+ fundingRaw: fundingMap.get(marketSymbol) ?? [],
350
+ liqRaw: liqMap.get(marketSymbol) ?? []
351
+ });
352
+ return (0, import_indicators.coinalyzePointsToRows)(points, interval, "coinalyze");
353
+ });
354
+ if (rows.length) {
355
+ await (0, import_timescale.upsertDerivatives)(rows);
356
+ totalRows += rows.length;
357
+ }
358
+ } catch (error) {
359
+ failedWindows += 1;
360
+ console.error(
361
+ import_chalk.default.red(
362
+ `batch window failed batch=${batchIdx + 1}/${symbolBatches.length} interval=${interval} ${new Date(cursor).toISOString()}..${new Date(toMs).toISOString()}: ${error}`
363
+ )
364
+ );
365
+ } finally {
366
+ bar.tick(1, {
367
+ batch: import_chalk.default.gray(
368
+ `${batchIdx + 1}/${symbolBatches.length} ${interval} (${batch.length} symbols)`
369
+ ),
370
+ rows: totalRows,
371
+ fail: failedWindows
372
+ });
373
+ }
374
+ cursor = toMs + 1;
375
+ }
376
+ }
377
+ }
378
+ console.log("");
379
+ console.log(
380
+ import_chalk.default.green(
381
+ `Done. symbols=${matches.length} intervals=${intervals.join(",")} rows=${totalRows} failed_windows=${failedWindows}`
382
+ )
383
+ );
384
+ if (failedWindows > 0) {
385
+ process.exit(1);
386
+ }
387
+ };
388
+ run().catch((error) => {
389
+ console.error(import_chalk.default.red(`derivativesIngestCoinalyzeAll failed: ${error}`));
390
+ process.exit(1);
391
+ });