@tradejs/cli 1.0.5 → 1.0.8

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.
Files changed (34) hide show
  1. package/README.md +4 -1
  2. package/dist/cli.js +13791 -7652
  3. package/dist/lib/aiTrainMetrics.js +173 -0
  4. package/dist/lib/candlesProviderMigration.js +266 -0
  5. package/dist/lib/cliArgs.js +40 -0
  6. package/dist/lib/derivativesContextBackfill.js +487 -0
  7. package/dist/lib/researchAgent.js +195 -0
  8. package/dist/lib/runtimeParity.js +234 -0
  9. package/dist/lib/telegramReports.js +91 -0
  10. package/dist/lib/timeWindow.js +101 -0
  11. package/dist/scripts/agentRun.js +895 -0
  12. package/dist/scripts/aiExport.js +192 -0
  13. package/dist/scripts/aiExportSelect.js +135 -0
  14. package/dist/scripts/aiTrain.js +2321 -0
  15. package/dist/scripts/backtest.js +998 -111
  16. package/dist/scripts/candlesMigrateProvider.js +5302 -0
  17. package/dist/scripts/continuity.js +1 -1
  18. package/dist/scripts/derivativesIngest.js +5 -0
  19. package/dist/scripts/derivativesIngestCoinalyzeAll.js +21 -12
  20. package/dist/scripts/migration.js +1 -1
  21. package/dist/scripts/mlExport.js +112 -13
  22. package/dist/scripts/mlExportSelect.js +43 -9
  23. package/dist/scripts/mlTrainLatestSelect.js +4 -3
  24. package/dist/scripts/researchAuto.js +845 -0
  25. package/dist/scripts/resolveExportStrategy.js +133 -0
  26. package/dist/scripts/results.js +46 -22
  27. package/dist/scripts/runtimeParity.js +1651 -0
  28. package/dist/scripts/selectStrategy.js +4 -3
  29. package/dist/scripts/signals.js +799 -62
  30. package/dist/scripts/signalsSummary.js +655 -0
  31. package/dist/scripts/spawnSiblingScript.js +71 -0
  32. package/dist/scripts/user-add.js +0 -9
  33. package/dist/workers/testerWorker.js +13 -2
  34. package/package.json +18 -12
@@ -5,6 +5,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
8
12
  var __copyProps = (to, from, except, desc) => {
9
13
  if (from && typeof from === "object" || typeof from === "function") {
10
14
  for (let key of __getOwnPropNames(from))
@@ -21,21 +25,456 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
25
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
26
  mod
23
27
  ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
29
 
25
30
  // src/scripts/signals.ts
31
+ var signals_exports = {};
32
+ __export(signals_exports, {
33
+ signals: () => signals
34
+ });
35
+ module.exports = __toCommonJS(signals_exports);
26
36
  var import_config = require("dotenv/config");
27
37
  var import_args = __toESM(require("args"));
28
- var import_progress = __toESM(require("progress"));
38
+ var import_progress2 = __toESM(require("progress"));
29
39
  var import_connectors = require("@tradejs/connectors");
30
- var import_chalk = __toESM(require("chalk"));
40
+ var import_chalk2 = __toESM(require("chalk"));
31
41
  var import_connectors2 = require("@tradejs/node/connectors");
32
42
  var import_cli = require("@tradejs/node/cli");
33
- var import_async = require("@tradejs/core/async");
34
- var import_constants = require("@tradejs/core/constants");
43
+ var import_async2 = require("@tradejs/core/async");
44
+ var import_constants2 = require("@tradejs/core/constants");
35
45
  var import_strategies = require("@tradejs/node/strategies");
36
46
  var import_time = require("@tradejs/core/time");
37
47
  var import_logger = require("@tradejs/infra/logger");
38
48
  var import_redis = require("@tradejs/infra/redis");
49
+
50
+ // src/lib/derivativesContextBackfill.ts
51
+ var import_chalk = __toESM(require("chalk"));
52
+ var import_progress = __toESM(require("progress"));
53
+ var import_async = require("@tradejs/core/async");
54
+ var import_constants = require("@tradejs/core/constants");
55
+ var import_indicators = require("@tradejs/core/indicators");
56
+ var import_timescale = require("@tradejs/infra/timescale");
57
+ var import_userSettings = require("@tradejs/infra/userSettings");
58
+ var HOUR_MS = 60 * 60 * 1e3;
59
+ var DAY_MS = 24 * HOUR_MS;
60
+ var DEFAULT_LOOKBACK_HOURS = 48;
61
+ var DEFAULT_INTERVALS = ["15m", "1h"];
62
+ var coinalyzeIntervalMap = {
63
+ "15m": "15min",
64
+ "1h": "1hour"
65
+ };
66
+ var lastRequestTs = 0;
67
+ var asInt = (value, fallback) => {
68
+ const parsed = Number.parseInt(String(value ?? ""), 10);
69
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
70
+ };
71
+ var parseList = (value) => String(value ?? "").split(",").map((item) => item.trim().toUpperCase()).filter(Boolean);
72
+ var normalizeSymbols = (symbols) => [
73
+ ...new Set(
74
+ symbols.map(
75
+ (symbol) => String(symbol || "").trim().toUpperCase()
76
+ ).filter(Boolean)
77
+ )
78
+ ];
79
+ var resolveDerivativesContextBackfillSymbols = (_requestedSymbols = []) => [...import_constants.DERIVATIVES_CONTEXT_REFERENCE_SYMBOLS];
80
+ var chunkArray = (items, size) => {
81
+ const chunks = [];
82
+ for (let i = 0; i < items.length; i += size) {
83
+ chunks.push(items.slice(i, i + size));
84
+ }
85
+ return chunks;
86
+ };
87
+ var isSignalsDerivativesContextEnabled = () => isDerivativesContextBackfillEnabled("CRON");
88
+ var isDerivativesContextBackfillEnabled = (env) => {
89
+ const normalized = String(process.env.DERIVATIVES_CONTEXT_ENABLED ?? "").trim().toLowerCase();
90
+ if (!normalized) return false;
91
+ if (["1", "true", "yes", "on"].includes(normalized)) {
92
+ return true;
93
+ }
94
+ if (normalized === "backtest") {
95
+ return env === "BACKTEST";
96
+ }
97
+ if (normalized === "live") {
98
+ return env !== "BACKTEST";
99
+ }
100
+ return false;
101
+ };
102
+ var shouldBackfillDerivativesContextForSignals = (params) => !params.cacheOnly && isSignalsDerivativesContextEnabled();
103
+ var resolveDerivativesContextIntervals = () => {
104
+ const intervals = (0, import_indicators.normalizeDerivativesIntervals)(
105
+ process.env.DERIVATIVES_CONTEXT_INTERVALS
106
+ );
107
+ return intervals.length ? intervals : DEFAULT_INTERVALS;
108
+ };
109
+ var resolveDerivativesContextLookbackMs = () => {
110
+ const hours = Number(process.env.DERIVATIVES_CONTEXT_LOOKBACK_HOURS);
111
+ const normalizedHours = Number.isFinite(hours) && hours > 0 ? hours : DEFAULT_LOOKBACK_HOURS;
112
+ return normalizedHours * HOUR_MS;
113
+ };
114
+ var resolveDerivativesContextBackfillWindow = (params) => {
115
+ const { startMs, endMs, preloadStartMs, nowMs = Date.now() } = params;
116
+ const safeEndMs = Math.min(endMs, nowMs);
117
+ const safeStartMs = Math.max(0, Math.min(startMs, safeEndMs));
118
+ const lookbackFromMs = safeStartMs - resolveDerivativesContextLookbackMs();
119
+ const requestedFromMs = preloadStartMs != null ? Math.max(Number(preloadStartMs), lookbackFromMs) : lookbackFromMs;
120
+ const fromMs = Math.max(
121
+ 0,
122
+ Math.min(
123
+ Number.isFinite(requestedFromMs) ? requestedFromMs : safeStartMs,
124
+ safeStartMs
125
+ )
126
+ );
127
+ return {
128
+ fromMs: Math.trunc(fromMs),
129
+ toMs: Math.trunc(safeEndMs),
130
+ testStartMs: Math.trunc(safeStartMs)
131
+ };
132
+ };
133
+ var getCoinalyzeApiKey = async (userName) => {
134
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
135
+ return settings.COINALYZE_API_KEY.trim() || String(process.env.COINALYZE_API_KEY ?? "").trim();
136
+ };
137
+ var getCoinalyzeBaseUrl = () => process.env.COINALYZE_BASE_URL?.trim() || "https://api.coinalyze.net/v1";
138
+ var getRequestDelayMs = () => Math.max(
139
+ 100,
140
+ asInt(
141
+ process.env.DERIVATIVES_CONTEXT_BACKFILL_REQUEST_DELAY_MS ?? process.env.COINALYZE_MIN_REQUEST_DELAY_MS,
142
+ 200
143
+ )
144
+ );
145
+ var getRequestTimeoutMs = () => Math.max(5e3, asInt(process.env.COINALYZE_REQUEST_TIMEOUT_MS, 45e3));
146
+ var fetchJsonWithRateLimit = async (url, apiKey) => {
147
+ const requestDelayMs = getRequestDelayMs();
148
+ const requestTimeoutMs = getRequestTimeoutMs();
149
+ const maxRetries = asInt(process.env.COINALYZE_MAX_RETRIES, 4);
150
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
151
+ const waitMs = Math.max(0, lastRequestTs + requestDelayMs - Date.now());
152
+ if (waitMs > 0) {
153
+ await (0, import_async.delay)(waitMs);
154
+ }
155
+ lastRequestTs = Date.now();
156
+ const controller = new AbortController();
157
+ const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
158
+ let response;
159
+ try {
160
+ response = await fetch(url, {
161
+ headers: {
162
+ api_key: apiKey,
163
+ "x-api-key": apiKey,
164
+ Authorization: `Bearer ${apiKey}`
165
+ },
166
+ signal: controller.signal
167
+ });
168
+ } catch (error) {
169
+ clearTimeout(timer);
170
+ const abortError = error instanceof Error && (error.name === "AbortError" || String(error.message).toLowerCase().includes("aborted"));
171
+ if (attempt < maxRetries && abortError) {
172
+ await (0, import_async.delay)(Math.min(12e3, 800 * 2 ** attempt));
173
+ continue;
174
+ }
175
+ throw error;
176
+ } finally {
177
+ clearTimeout(timer);
178
+ }
179
+ if (response.ok) {
180
+ return response.json();
181
+ }
182
+ const text = await response.text();
183
+ const retryAfterRaw = Number(response.headers.get("retry-after") ?? "");
184
+ const retryAfterMs = Number.isFinite(retryAfterRaw) && retryAfterRaw > 0 ? retryAfterRaw * 1e3 : null;
185
+ const transient = response.status === 429 || response.status >= 500;
186
+ if (attempt < maxRetries && transient) {
187
+ await (0, import_async.delay)(retryAfterMs ?? Math.min(12e3, 800 * 2 ** attempt));
188
+ continue;
189
+ }
190
+ throw new Error(`Coinalyze ${response.status}: ${text}`);
191
+ }
192
+ throw new Error("Coinalyze request failed after retries");
193
+ };
194
+ var fetchCoinalyzeMarkets = async (apiKey) => {
195
+ const raw = await fetchJsonWithRateLimit(
196
+ `${getCoinalyzeBaseUrl()}/future-markets`,
197
+ apiKey
198
+ );
199
+ return Array.isArray(raw) ? raw : [];
200
+ };
201
+ var selectBestMarket = (candidates, exchangePriority) => {
202
+ const scored = candidates.map((item) => {
203
+ let score = 0;
204
+ if (item.is_perpetual) score += 50;
205
+ if (String(item.quote_asset || "").toUpperCase() === "USDT") score += 25;
206
+ if (item.symbol.includes("_PERP")) score += 10;
207
+ const exchange = String(item.exchange || "").toUpperCase();
208
+ const idx = exchangePriority.indexOf(exchange);
209
+ if (idx >= 0) {
210
+ score += (exchangePriority.length - idx) * 10;
211
+ }
212
+ return { item, score };
213
+ });
214
+ scored.sort((a, b) => {
215
+ if (b.score !== a.score) return b.score - a.score;
216
+ return a.item.symbol.localeCompare(b.item.symbol);
217
+ });
218
+ return scored[0]?.item ?? null;
219
+ };
220
+ var buildMatches = (tickers, markets, exchangePriority) => {
221
+ const marketByTicker = /* @__PURE__ */ new Map();
222
+ for (const market of markets) {
223
+ const key = String(market.symbol_on_exchange || "").trim().toUpperCase();
224
+ if (!key) continue;
225
+ const list = marketByTicker.get(key) ?? [];
226
+ list.push(market);
227
+ marketByTicker.set(key, list);
228
+ }
229
+ const matches = [];
230
+ const unmatched = [];
231
+ for (const ticker of tickers) {
232
+ const candidates = marketByTicker.get(ticker) ?? [];
233
+ const selected = selectBestMarket(candidates, exchangePriority);
234
+ if (!selected) {
235
+ unmatched.push(ticker);
236
+ continue;
237
+ }
238
+ matches.push({
239
+ symbol: ticker,
240
+ marketSymbol: selected.symbol,
241
+ exchange: selected.exchange
242
+ });
243
+ }
244
+ return { matches, unmatched };
245
+ };
246
+ var normalizeMetricPoint = (metric, point) => {
247
+ if (metric === "oi") {
248
+ return {
249
+ ...point,
250
+ open_interest: point.open_interest ?? point.openInterest ?? point.oi ?? point.c
251
+ };
252
+ }
253
+ if (metric === "funding") {
254
+ return {
255
+ ...point,
256
+ funding_rate: point.funding_rate ?? point.fundingRate ?? point.rate ?? point.c
257
+ };
258
+ }
259
+ return {
260
+ ...point,
261
+ liq_long: point.liq_long ?? point.long_liq ?? point.long ?? point.l,
262
+ liq_short: point.liq_short ?? point.short_liq ?? point.short ?? point.s
263
+ };
264
+ };
265
+ var toSeriesMap = (raw, metric) => {
266
+ const out = /* @__PURE__ */ new Map();
267
+ if (!Array.isArray(raw)) return out;
268
+ for (const item of raw) {
269
+ const symbol = String(item.symbol || "").trim().toUpperCase();
270
+ const history = Array.isArray(item.history) ? item.history : [];
271
+ if (!symbol) continue;
272
+ out.set(
273
+ symbol,
274
+ history.filter((point) => point && typeof point === "object").map((point) => normalizeMetricPoint(metric, point))
275
+ );
276
+ }
277
+ return out;
278
+ };
279
+ var fetchMetricBatch = async (params) => {
280
+ const { endpoint, metric, marketSymbols, apiKey, interval: interval2, fromMs, toMs } = params;
281
+ const url = new URL(`${getCoinalyzeBaseUrl()}${endpoint}`);
282
+ url.searchParams.set("symbols", marketSymbols.join(","));
283
+ url.searchParams.set("interval", coinalyzeIntervalMap[interval2] || interval2);
284
+ url.searchParams.set("from", String(Math.floor(fromMs / 1e3)));
285
+ url.searchParams.set("to", String(Math.floor(toMs / 1e3)));
286
+ const raw = await fetchJsonWithRateLimit(url.toString(), apiKey);
287
+ return toSeriesMap(raw, metric);
288
+ };
289
+ var skippedBackfillResult = () => ({
290
+ skipped: true,
291
+ rows: 0,
292
+ matchedSymbols: 0,
293
+ unmatchedSymbols: 0,
294
+ failedWindows: 0,
295
+ skippedWindows: 0
296
+ });
297
+ var backfillDerivativesContext = async (params, enabled) => {
298
+ const { userName, startMs, endMs } = params;
299
+ const requestedSymbols = normalizeSymbols(params.symbols);
300
+ const symbols = resolveDerivativesContextBackfillSymbols(requestedSymbols);
301
+ if (!enabled || !requestedSymbols.length || !symbols.length) {
302
+ return skippedBackfillResult();
303
+ }
304
+ const intervals = resolveDerivativesContextIntervals();
305
+ if (!intervals.length) {
306
+ return skippedBackfillResult();
307
+ }
308
+ const apiKey = await getCoinalyzeApiKey(userName);
309
+ if (!apiKey) {
310
+ throw new Error(
311
+ `Missing COINALYZE_API_KEY for derivatives context backfill (user=${userName})`
312
+ );
313
+ }
314
+ const {
315
+ fromMs,
316
+ toMs: safeEndMs,
317
+ testStartMs: safeStartMs
318
+ } = resolveDerivativesContextBackfillWindow({
319
+ startMs,
320
+ endMs,
321
+ preloadStartMs: params.preloadStartMs
322
+ });
323
+ if (safeEndMs <= fromMs) {
324
+ return skippedBackfillResult();
325
+ }
326
+ const exchangePriority = parseList(
327
+ process.env.COINALYZE_EXCHANGE_PRIORITY || process.env.DERIVATIVES_CONTEXT_EXCHANGE_PRIORITY || "A,6,0"
328
+ );
329
+ const markets = await fetchCoinalyzeMarkets(apiKey);
330
+ if (!markets.length) {
331
+ throw new Error("No markets returned by Coinalyze /future-markets");
332
+ }
333
+ const { matches, unmatched } = buildMatches(
334
+ symbols,
335
+ markets,
336
+ exchangePriority
337
+ );
338
+ if (!matches.length) {
339
+ throw new Error(
340
+ "No matched derivatives reference symbols between BTC/ETH and Coinalyze"
341
+ );
342
+ }
343
+ await (0, import_timescale.waitForDbReady)();
344
+ const batchDays = asInt(
345
+ process.env.DERIVATIVES_CONTEXT_BACKFILL_BATCH_DAYS,
346
+ 30
347
+ );
348
+ const symbolBatchSize = asInt(
349
+ process.env.DERIVATIVES_CONTEXT_BACKFILL_SYMBOL_BATCH_SIZE,
350
+ 8
351
+ );
352
+ const symbolBatches = chunkArray(matches, symbolBatchSize);
353
+ const windowsPerPair = Math.ceil((safeEndMs - fromMs) / (batchDays * DAY_MS));
354
+ const totalWindows = symbolBatches.length * intervals.length * windowsPerPair;
355
+ const bar = new import_progress.default(
356
+ ":current/:total [:bar][:percent] :eta(s) :batch rows=:rows fail=:fail skip=:skip",
357
+ {
358
+ total: Math.max(1, totalWindows),
359
+ width: 24
360
+ }
361
+ );
362
+ const oiPath = process.env.COINALYZE_OI_PATH?.trim() || "/open-interest-history";
363
+ const fundingPath = process.env.COINALYZE_FUNDING_PATH?.trim() || "/funding-rate-history";
364
+ const liqPath = process.env.COINALYZE_LIQ_PATH?.trim() || "/liquidation-history";
365
+ let totalRows = 0;
366
+ let failedWindows = 0;
367
+ let skippedWindows = 0;
368
+ console.log(
369
+ import_chalk.default.cyan(
370
+ `derivatives context backfill: referenceSymbols=${matches.length}, requestedSymbols=${requestedSymbols.length}, unmatched=${unmatched.length}, intervals=${intervals.join(",")}, window=${new Date(fromMs).toISOString()}..${new Date(safeEndMs).toISOString()}, testStart=${new Date(safeStartMs).toISOString()}`
371
+ )
372
+ );
373
+ for (const interval2 of intervals) {
374
+ const edgesBySymbol = await (0, import_timescale.getDerivativesDataEdgesForSymbols)(
375
+ matches.map((item) => item.symbol),
376
+ interval2
377
+ );
378
+ for (let batchIdx = 0; batchIdx < symbolBatches.length; batchIdx += 1) {
379
+ const batch = symbolBatches[batchIdx];
380
+ let cursor = fromMs;
381
+ while (cursor < safeEndMs) {
382
+ const toMs = Math.min(safeEndMs, cursor + batchDays * DAY_MS);
383
+ const missingBatch = batch.filter((item) => {
384
+ const edges = edgesBySymbol.get(item.symbol.toUpperCase());
385
+ return edges?.min == null || edges?.max == null || edges.min > cursor || edges.max < toMs;
386
+ });
387
+ try {
388
+ if (!missingBatch.length) {
389
+ skippedWindows += 1;
390
+ } else {
391
+ const marketSymbols = missingBatch.map((item) => item.marketSymbol);
392
+ const oiMap = await fetchMetricBatch({
393
+ endpoint: oiPath,
394
+ metric: "oi",
395
+ marketSymbols,
396
+ apiKey,
397
+ interval: interval2,
398
+ fromMs: cursor,
399
+ toMs
400
+ });
401
+ const fundingMap = await fetchMetricBatch({
402
+ endpoint: fundingPath,
403
+ metric: "funding",
404
+ marketSymbols,
405
+ apiKey,
406
+ interval: interval2,
407
+ fromMs: cursor,
408
+ toMs
409
+ });
410
+ const liqMap = await fetchMetricBatch({
411
+ endpoint: liqPath,
412
+ metric: "liq",
413
+ marketSymbols,
414
+ apiKey,
415
+ interval: interval2,
416
+ fromMs: cursor,
417
+ toMs
418
+ });
419
+ const rows = missingBatch.flatMap((item) => {
420
+ const marketSymbol = item.marketSymbol.toUpperCase();
421
+ const points = (0, import_indicators.mergeCoinalyzeMetrics)({
422
+ symbol: item.symbol,
423
+ oiRaw: oiMap.get(marketSymbol) ?? [],
424
+ fundingRaw: fundingMap.get(marketSymbol) ?? [],
425
+ liqRaw: liqMap.get(marketSymbol) ?? []
426
+ });
427
+ return (0, import_indicators.coinalyzePointsToRows)(points, interval2, "coinalyze");
428
+ });
429
+ if (rows.length) {
430
+ await (0, import_timescale.upsertDerivatives)(rows);
431
+ totalRows += rows.length;
432
+ }
433
+ }
434
+ } catch (error) {
435
+ failedWindows += 1;
436
+ console.error(
437
+ import_chalk.default.red(
438
+ `derivatives context backfill window failed batch=${batchIdx + 1}/${symbolBatches.length} interval=${interval2} ${new Date(cursor).toISOString()}..${new Date(toMs).toISOString()}: ${error}`
439
+ )
440
+ );
441
+ } finally {
442
+ bar.tick(1, {
443
+ batch: import_chalk.default.gray(
444
+ `${batchIdx + 1}/${symbolBatches.length} ${interval2}`
445
+ ),
446
+ rows: totalRows,
447
+ fail: failedWindows,
448
+ skip: skippedWindows
449
+ });
450
+ }
451
+ cursor = toMs + 1;
452
+ }
453
+ }
454
+ }
455
+ console.log("");
456
+ console.log(
457
+ import_chalk.default.green(
458
+ `derivatives context backfill done: rows=${totalRows}, failed_windows=${failedWindows}, skipped_windows=${skippedWindows}`
459
+ )
460
+ );
461
+ if (failedWindows > 0) {
462
+ throw new Error(
463
+ `Derivatives context backfill failed for ${failedWindows} window(s)`
464
+ );
465
+ }
466
+ return {
467
+ skipped: false,
468
+ rows: totalRows,
469
+ matchedSymbols: matches.length,
470
+ unmatchedSymbols: unmatched.length,
471
+ failedWindows,
472
+ skippedWindows
473
+ };
474
+ };
475
+ var backfillDerivativesContextForSignals = async (params) => backfillDerivativesContext(params, isSignalsDerivativesContextEnabled());
476
+
477
+ // src/scripts/signals.ts
39
478
  import_args.default.option(["t", "tickers"], "Selected tickers");
40
479
  import_args.default.option(["e", "exclude"], "Exclude tickers from tests");
41
480
  import_args.default.option(["l", "tickersLimit"], "Tickers limit");
@@ -46,6 +485,11 @@ import_args.default.option(["S", "skipScreenshots"], "Skip screenshot generation
46
485
  import_args.default.option(["u", "updateOnly"], "Only update tickers history", false);
47
486
  import_args.default.option(["C", "cacheOnly"], "Do not update tickers history", false);
48
487
  import_args.default.option(["L", "showTickersList"], "Just show only ticker list", false);
488
+ import_args.default.option(
489
+ ["R", "showSkipStats"],
490
+ "Show aggregated skip stats by strategy",
491
+ false
492
+ );
49
493
  import_args.default.option(["c", "chunk"], "Split by chunks, ex. 1/3");
50
494
  import_args.default.option(["U", "user"], "Use user confg", "root");
51
495
  import_args.default.option(
@@ -53,13 +497,50 @@ import_args.default.option(
53
497
  "Connector provider or name for signals (e.g. bybit, binance, coinbase, custom)",
54
498
  "bybit"
55
499
  );
56
- var PRELOAD_START = (0, import_time.getTimestamp)(import_constants.SIGNALS_PRELOAD_DAYS);
500
+ var PRELOAD_START = (0, import_time.getTimestamp)(import_constants2.SIGNALS_CLI_PRELOAD_DAYS);
57
501
  var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
58
502
  var flags = import_args.default.parse(process.argv);
59
- var minTouches = parseInt(flags.points);
60
- var offset = parseInt(flags.offset);
61
503
  var interval = flags.timeframe.toString();
62
504
  var formatElapsed = (startedAt) => `${((Date.now() - startedAt) / 1e3).toFixed(1)}s`;
505
+ var formatDuration = (startedAt) => {
506
+ const seconds = (Date.now() - startedAt) / 1e3;
507
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
508
+ const minutes = Math.floor(seconds / 60);
509
+ const restSeconds = Math.round(seconds % 60);
510
+ return `${minutes}m ${restSeconds}s`;
511
+ };
512
+ var timeOperation = async (label, operation) => {
513
+ const startedAt = Date.now();
514
+ try {
515
+ return await operation();
516
+ } finally {
517
+ import_logger.logger.info(import_chalk2.default.gray(`${label}: done in ${formatDuration(startedAt)}`));
518
+ }
519
+ };
520
+ var normalizeHookList = (value) => {
521
+ if (Array.isArray(value)) {
522
+ return value;
523
+ }
524
+ return value ? [value] : [];
525
+ };
526
+ var invokeBeforeSignalsHooks = async (hooks, params) => {
527
+ for (const hook of normalizeHookList(
528
+ hooks?.beforeSignals
529
+ )) {
530
+ const result = await hook(params);
531
+ if (result?.abort === true) {
532
+ return result;
533
+ }
534
+ }
535
+ return void 0;
536
+ };
537
+ var invokeAfterSignalsHooks = async (hooks, params) => {
538
+ for (const hook of normalizeHookList(
539
+ hooks?.afterSignals
540
+ )) {
541
+ await hook(params);
542
+ }
543
+ };
63
544
  var resolveStrategyNameByConfigKey = (userName, key) => {
64
545
  const parts = key.split(":");
65
546
  if (parts.length !== 5) {
@@ -98,6 +579,107 @@ var loadRuntimeStrategies = async (userName) => {
98
579
  );
99
580
  return strategyConfigs.filter(Boolean);
100
581
  };
582
+ var createStrategySkipStats = (runtimeStrategies) => new Map(
583
+ runtimeStrategies.map(({ strategyName }) => [
584
+ strategyName,
585
+ {
586
+ evaluated: 0,
587
+ signals: 0,
588
+ reasons: /* @__PURE__ */ new Map()
589
+ }
590
+ ])
591
+ );
592
+ var normalizeSkipStatsReason = (reason, fallbackSource) => {
593
+ let source = fallbackSource;
594
+ let normalizedReason = reason;
595
+ if (reason.startsWith("AI_QUALITY_BELOW_MIN")) {
596
+ source = "AI";
597
+ normalizedReason = "MIN_AI_QUALITY";
598
+ } else if (reason === "AI_QUALITY_UNAVAILABLE") {
599
+ source = "AI";
600
+ normalizedReason = "QUALITY_UNAVAILABLE";
601
+ } else if (reason === "ML_RESULT_UNAVAILABLE") {
602
+ source = "ML";
603
+ normalizedReason = "RESULT_UNAVAILABLE";
604
+ } else if (reason.startsWith("ML_THRESHOLD_NOT_MET")) {
605
+ source = "ML";
606
+ normalizedReason = "ML_THRESHOLD";
607
+ } else if (reason.startsWith("HOOK_BEFORE_ENTRY_GATE:")) {
608
+ source = "hook";
609
+ normalizedReason = `BEFORE_ENTRY_GATE:${reason.slice(
610
+ "HOOK_BEFORE_ENTRY_GATE:".length
611
+ )}`;
612
+ } else if (reason === "HOOK_BEFORE_ENTRY_GATE") {
613
+ source = "hook";
614
+ normalizedReason = "BEFORE_ENTRY_GATE";
615
+ } else if (reason === "MAKE_ORDERS_DISABLED" || reason === "ENTRY_POLICY_BLOCKED") {
616
+ source = "policy";
617
+ }
618
+ return `skip from ${source} / ${normalizedReason}`;
619
+ };
620
+ var recordStrategyReason = (strategyStats, strategyName, reason, fallbackSource = "core") => {
621
+ const stats = strategyStats.get(strategyName);
622
+ if (!stats) {
623
+ return;
624
+ }
625
+ const normalizedReason = normalizeSkipStatsReason(reason, fallbackSource);
626
+ stats.reasons.set(
627
+ normalizedReason,
628
+ (stats.reasons.get(normalizedReason) ?? 0) + 1
629
+ );
630
+ };
631
+ var buildRuntimeSignalEvaluationId = ({
632
+ strategyName,
633
+ symbol,
634
+ timestamp
635
+ }) => `${strategyName}:${symbol}:${timestamp}`;
636
+ var hasMeaningfulEvaluationReason = (value) => {
637
+ const reason = typeof value === "string" ? value.trim() : "";
638
+ return reason.length > 0 && reason !== "NO_SIGNAL";
639
+ };
640
+ var resolveRuntimeSignalEvaluationTtl = (evaluation) => {
641
+ if (evaluation.status === "signal" || evaluation.status === "error") {
642
+ return import_constants2.TTL_1M;
643
+ }
644
+ if (hasMeaningfulEvaluationReason(evaluation.reason) || hasMeaningfulEvaluationReason(evaluation.orderSkipReason)) {
645
+ return import_constants2.TTL_1M;
646
+ }
647
+ return import_constants2.TTL_3D;
648
+ };
649
+ var saveRuntimeSignalEvaluation = async (evaluation) => {
650
+ await (0, import_redis.setData)(
651
+ import_redis.redisKeys.runtimeSignalEvaluation(
652
+ evaluation.userName,
653
+ evaluation.evaluationId
654
+ ),
655
+ evaluation,
656
+ {
657
+ expire: resolveRuntimeSignalEvaluationTtl(evaluation)
658
+ }
659
+ );
660
+ };
661
+ var logStrategySkipStats = (runtimeStrategies, strategyStats) => {
662
+ import_logger.logger.info(import_chalk2.default.yellow("skip stats:"));
663
+ for (const { strategyName } of runtimeStrategies) {
664
+ const stats = strategyStats.get(strategyName);
665
+ if (!stats) {
666
+ continue;
667
+ }
668
+ import_logger.logger.info(
669
+ `${strategyName}: evaluated=${stats.evaluated}, signals=${stats.signals}`
670
+ );
671
+ const sortedReasons = [...stats.reasons.entries()].sort(
672
+ (left, right) => right[1] - left[1] || left[0].localeCompare(right[0])
673
+ );
674
+ if (!sortedReasons.length) {
675
+ import_logger.logger.info(" none");
676
+ continue;
677
+ }
678
+ for (const [reason, count] of sortedReasons) {
679
+ import_logger.logger.info(` ${reason}: ${count}`);
680
+ }
681
+ }
682
+ };
101
683
  var resolveSignalsConnectorName = async (value) => {
102
684
  const connectorName = await (0, import_connectors2.resolveConnectorName)(value, projectRoot);
103
685
  if (connectorName) {
@@ -110,13 +692,9 @@ var resolveSignalsConnectorName = async (value) => {
110
692
  );
111
693
  return import_connectors2.DEFAULT_CONNECTOR_NAME;
112
694
  };
113
- var findSignals = async (symbol, connector, btcBinanceData, btcCoinbaseData, runtimeStrategies) => {
114
- const prevSignals = await (0, import_redis.getKeys)(import_redis.redisKeys.signalsBySymbol(symbol));
115
- if (prevSignals.length) {
116
- import_logger.logger.info("Exit by signal exists %s", symbol);
117
- return null;
118
- }
695
+ var findSignals = async (symbol, connector, btcBinanceData, btcCoinbaseData, runtimeStrategies, strategyStats) => {
119
696
  const currentTimestamp = (0, import_time.getTimestamp)();
697
+ const strategySignals = [];
120
698
  const [cachedData, btcCachedData] = await Promise.all([
121
699
  connector.kline({
122
700
  symbol,
@@ -136,10 +714,14 @@ var findSignals = async (symbol, connector, btcBinanceData, btcCoinbaseData, run
136
714
  const lastCandle = cachedData.pop();
137
715
  const btcLastCandle = btcCachedData.pop();
138
716
  if (!lastCandle || !btcLastCandle) {
139
- return;
717
+ return strategySignals;
140
718
  }
141
719
  for (const runtimeStrategy of runtimeStrategies) {
142
720
  const { strategyName, strategyCreator, strategyConfig } = runtimeStrategy;
721
+ const stats = strategyStats.get(strategyName);
722
+ if (stats) {
723
+ stats.evaluated += 1;
724
+ }
143
725
  const strategy = await strategyCreator({
144
726
  userName: flags.user,
145
727
  connector,
@@ -157,22 +739,83 @@ var findSignals = async (symbol, connector, btcBinanceData, btcCoinbaseData, run
157
739
  });
158
740
  const signal = await strategy(lastCandle, btcLastCandle);
159
741
  if (!signal || typeof signal === "string") {
742
+ const reason = typeof signal === "string" && signal.trim() ? signal : "NO_SIGNAL";
743
+ if (typeof signal === "string") {
744
+ recordStrategyReason(strategyStats, strategyName, signal, "core");
745
+ }
746
+ await saveRuntimeSignalEvaluation({
747
+ evaluationId: buildRuntimeSignalEvaluationId({
748
+ strategyName,
749
+ symbol,
750
+ timestamp: lastCandle.timestamp
751
+ }),
752
+ userName: flags.user,
753
+ strategy: strategyName,
754
+ symbol,
755
+ interval,
756
+ timestamp: lastCandle.timestamp,
757
+ evaluatedAt: Date.now(),
758
+ status: "skip",
759
+ reason
760
+ });
160
761
  continue;
161
762
  }
763
+ if (stats) {
764
+ stats.signals += 1;
765
+ }
766
+ if (signal.orderStatus === "skipped" && typeof signal.orderSkipReason === "string" && signal.orderSkipReason.trim()) {
767
+ recordStrategyReason(
768
+ strategyStats,
769
+ strategyName,
770
+ signal.orderSkipReason,
771
+ "runtime"
772
+ );
773
+ }
774
+ strategySignals.push(signal);
162
775
  await (0, import_redis.setData)(import_redis.redisKeys.signal(symbol, signal.signalId), signal, {
163
- expire: import_constants.TTL_1D
776
+ expire: import_constants2.TTL_1D
164
777
  });
165
778
  await (0, import_redis.setData)(import_redis.redisKeys.storeSignal(symbol, signal.signalId), signal, {
166
- expire: import_constants.TTL_3M
779
+ expire: import_constants2.TTL_3M
780
+ });
781
+ await (0, import_redis.setData)(
782
+ import_redis.redisKeys.runtimeSignal(flags.user, signal.signalId),
783
+ signal,
784
+ {
785
+ expire: import_constants2.TTL_1M
786
+ }
787
+ );
788
+ await saveRuntimeSignalEvaluation({
789
+ evaluationId: buildRuntimeSignalEvaluationId({
790
+ strategyName,
791
+ symbol,
792
+ timestamp: lastCandle.timestamp
793
+ }),
794
+ userName: flags.user,
795
+ strategy: strategyName,
796
+ symbol,
797
+ interval,
798
+ timestamp: lastCandle.timestamp,
799
+ evaluatedAt: Date.now(),
800
+ status: "signal",
801
+ reason: signal.orderSkipReason || signal.orderStatus || "SIGNAL",
802
+ signalId: signal.signalId,
803
+ direction: signal.direction,
804
+ orderStatus: signal.orderStatus,
805
+ orderSkipReason: signal.orderSkipReason,
806
+ ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
807
+ ...signal.ml ? { ml: signal.ml } : {}
167
808
  });
168
809
  import_logger.logger.info("Signal found %s by strategy %s", symbol, strategyName);
169
- return signal;
170
810
  }
811
+ return strategySignals;
171
812
  };
172
813
  var signals = async () => {
173
814
  const startedAt = Date.now();
174
815
  const signals2 = new Array();
175
816
  let status = "completed";
817
+ let projectHooks;
818
+ let afterSignalsHookContext = null;
176
819
  try {
177
820
  const connectorName = await resolveSignalsConnectorName(flags.connector);
178
821
  const connectorFactory = await (0, import_connectors2.getConnectorCreatorByName)(
@@ -217,46 +860,91 @@ var signals = async () => {
217
860
  );
218
861
  }
219
862
  }
220
- const tickers = await (0, import_cli.getTickers)(
221
- marketConnector,
222
- flags.tickers,
223
- flags.exclude,
224
- flags.tickersLimit,
225
- flags.chunk
863
+ const tickers = await timeOperation(
864
+ "tickers load",
865
+ () => (0, import_cli.getTickers)(
866
+ marketConnector,
867
+ flags.tickers,
868
+ flags.exclude,
869
+ flags.tickersLimit,
870
+ flags.chunk
871
+ )
226
872
  );
227
873
  if (flags.showTickersList) {
228
- console.log(import_chalk.default.gray(JSON.stringify(tickers.sort(), null, 2)));
874
+ console.log(import_chalk2.default.gray(JSON.stringify(tickers.sort(), null, 2)));
229
875
  return;
230
876
  }
877
+ const projectConfig = await (0, import_cli.loadTradejsConfig)(projectRoot);
878
+ projectHooks = projectConfig.hooks;
231
879
  if (!flags.cacheOnly) {
232
- await (0, import_cli.update)(marketConnector, interval, tickers);
880
+ await timeOperation(
881
+ `update ${connectorName}`,
882
+ () => (0, import_cli.update)(marketConnector, interval, tickers, import_constants2.SIGNALS_CLI_PRELOAD_DAYS, {
883
+ connectorLabel: connectorName
884
+ })
885
+ );
233
886
  if (btcBinanceConnector !== marketConnector) {
234
- await (0, import_cli.update)(btcBinanceConnector, interval, ["BTCUSDT"]);
887
+ await timeOperation(
888
+ `update ${import_connectors.ConnectorNames.Binance}`,
889
+ () => (0, import_cli.update)(
890
+ btcBinanceConnector,
891
+ interval,
892
+ ["BTCUSDT"],
893
+ import_constants2.SIGNALS_CLI_PRELOAD_DAYS,
894
+ { connectorLabel: import_connectors.ConnectorNames.Binance }
895
+ )
896
+ );
235
897
  }
236
898
  if (btcCoinbaseConnector !== marketConnector) {
237
- await (0, import_cli.update)(btcCoinbaseConnector, interval, ["BTCUSDT"]);
899
+ await timeOperation(
900
+ `update ${import_connectors.ConnectorNames.Coinbase}`,
901
+ () => (0, import_cli.update)(
902
+ btcCoinbaseConnector,
903
+ interval,
904
+ ["BTCUSDT"],
905
+ import_constants2.SIGNALS_CLI_PRELOAD_DAYS,
906
+ { connectorLabel: import_connectors.ConnectorNames.Coinbase }
907
+ )
908
+ );
238
909
  }
239
910
  }
240
911
  const currentTimestamp = (0, import_time.getTimestamp)();
241
- const [btcBinanceData, btcCoinbaseData] = await Promise.all([
242
- btcBinanceConnector.kline({
243
- symbol: "BTCUSDT",
244
- start: PRELOAD_START,
245
- end: currentTimestamp,
246
- cacheOnly: true,
247
- interval
248
- }),
249
- btcCoinbaseConnector.kline({
250
- symbol: "BTCUSDT",
251
- start: PRELOAD_START,
252
- end: currentTimestamp,
253
- cacheOnly: true,
254
- interval
255
- })
256
- ]);
912
+ if (shouldBackfillDerivativesContextForSignals({
913
+ cacheOnly: Boolean(flags.cacheOnly)
914
+ })) {
915
+ await timeOperation(
916
+ "derivatives context backfill",
917
+ () => backfillDerivativesContextForSignals({
918
+ userName: flags.user,
919
+ symbols: tickers,
920
+ startMs: currentTimestamp,
921
+ endMs: currentTimestamp,
922
+ preloadStartMs: PRELOAD_START
923
+ })
924
+ );
925
+ }
257
926
  if (flags.updateOnly) {
258
927
  return;
259
928
  }
929
+ const [btcBinanceData, btcCoinbaseData] = await timeOperation(
930
+ "reference candles load",
931
+ () => Promise.all([
932
+ btcBinanceConnector.kline({
933
+ symbol: "BTCUSDT",
934
+ start: PRELOAD_START,
935
+ end: currentTimestamp,
936
+ cacheOnly: true,
937
+ interval
938
+ }),
939
+ btcCoinbaseConnector.kline({
940
+ symbol: "BTCUSDT",
941
+ start: PRELOAD_START,
942
+ end: currentTimestamp,
943
+ cacheOnly: true,
944
+ interval
945
+ })
946
+ ])
947
+ );
260
948
  const runtimeStrategies = await loadRuntimeStrategies(flags.user);
261
949
  if (!runtimeStrategies.length) {
262
950
  import_logger.logger.warn(
@@ -266,40 +954,65 @@ var signals = async () => {
266
954
  return;
267
955
  }
268
956
  import_logger.logger.info(
269
- import_chalk.default.yellow(
270
- `loaded strategies: ${runtimeStrategies.map((s) => s.strategyName).join(", ")}`
957
+ import_chalk2.default.yellow(
958
+ `loaded strategies (user=${flags.user}): ${runtimeStrategies.map((s) => s.strategyName).join(", ")}`
959
+ )
960
+ );
961
+ afterSignalsHookContext = {
962
+ connector: marketConnector,
963
+ connectorName,
964
+ userName: flags.user,
965
+ interval,
966
+ tickers: [...tickers],
967
+ runtimeStrategies: runtimeStrategies.map(
968
+ ({ strategyName, strategyConfig }) => ({
969
+ strategyName,
970
+ strategyConfig
971
+ })
271
972
  )
973
+ };
974
+ const beforeSignalsResult = await invokeBeforeSignalsHooks(
975
+ projectHooks,
976
+ afterSignalsHookContext
272
977
  );
273
- const bar = new import_progress.default(
978
+ if (beforeSignalsResult?.abort === true) {
979
+ import_logger.logger.info(
980
+ "signals aborted before ticker evaluation: %s",
981
+ beforeSignalsResult.reason ?? "PROJECT_BEFORE_SIGNALS_ABORTED"
982
+ );
983
+ return;
984
+ }
985
+ const strategyStats = createStrategySkipStats(runtimeStrategies);
986
+ const bar = new import_progress2.default(
274
987
  ":current/:total [:bar][:percent] :found :eta(s) :symbol",
275
988
  {
276
989
  total: tickers.length,
277
990
  width: 30
278
991
  }
279
992
  );
280
- import_logger.logger.info(import_chalk.default.yellow(`tickers: ${tickers.length}`));
281
- await (0, import_async.runWithConcurrency)(tickers, 5, async (symbol) => {
282
- const signal = await findSignals(
993
+ import_logger.logger.info(import_chalk2.default.yellow(`tickers: ${tickers.length}`));
994
+ await (0, import_async2.runWithConcurrency)(tickers, 5, async (symbol) => {
995
+ const strategySignals = await findSignals(
283
996
  symbol,
284
997
  marketConnector,
285
998
  btcBinanceData,
286
999
  btcCoinbaseData,
287
- runtimeStrategies
1000
+ runtimeStrategies,
1001
+ strategyStats
288
1002
  );
289
- if (signal) {
290
- signals2.push(signal);
1003
+ if (strategySignals.length > 0) {
1004
+ signals2.push(...strategySignals);
291
1005
  }
292
1006
  bar.tick(1, {
293
- found: import_chalk.default.cyan(signals2.length),
294
- symbol: import_chalk.default.gray(symbol)
1007
+ found: import_chalk2.default.cyan(signals2.length),
1008
+ symbol: import_chalk2.default.gray(symbol)
295
1009
  });
296
1010
  });
297
1011
  if (!flags.skipScreenshots) {
298
- await (0, import_cli.makeScreenshots)(signals2, "15");
299
- await (0, import_cli.makeScreenshots)(signals2, "60");
1012
+ await (0, import_cli.makeScreenshots)(signals2, "15", flags.user);
300
1013
  }
301
1014
  if (flags.notify) {
302
- await (0, import_cli.sendToTG)(signals2, "15");
1015
+ await (0, import_cli.sendToTG)(signals2, "15", flags.user);
303
1016
  }
304
1017
  import_logger.logger.info(
305
1018
  JSON.stringify(
@@ -308,6 +1021,9 @@ var signals = async () => {
308
1021
  2
309
1022
  )
310
1023
  );
1024
+ if (flags.showSkipStats) {
1025
+ logStrategySkipStats(runtimeStrategies, strategyStats);
1026
+ }
311
1027
  } catch (error) {
312
1028
  status = "failed";
313
1029
  import_logger.logger.error(
@@ -316,15 +1032,36 @@ var signals = async () => {
316
1032
  );
317
1033
  throw error;
318
1034
  } finally {
1035
+ if (projectHooks && afterSignalsHookContext) {
1036
+ try {
1037
+ await invokeAfterSignalsHooks(projectHooks, {
1038
+ ...afterSignalsHookContext,
1039
+ signals: [...signals2],
1040
+ status,
1041
+ durationMs: Date.now() - startedAt
1042
+ });
1043
+ } catch (error) {
1044
+ import_logger.logger.error(
1045
+ "afterSignals hook failed: %s",
1046
+ error?.message || String(error)
1047
+ );
1048
+ }
1049
+ }
319
1050
  import_logger.logger.info(
320
- import_chalk.default.yellow(
1051
+ import_chalk2.default.yellow(
321
1052
  `signals ${status} in ${formatElapsed(startedAt)} (found=${signals2.length})`
322
1053
  )
323
1054
  );
324
1055
  }
325
1056
  };
326
- signals().then(() => {
327
- process.exit(0);
328
- }).catch(() => {
329
- process.exit(1);
1057
+ if (process.env.NODE_ENV !== "test") {
1058
+ signals().then(() => {
1059
+ process.exit(0);
1060
+ }).catch(() => {
1061
+ process.exit(1);
1062
+ });
1063
+ }
1064
+ // Annotate the CommonJS export names for ESM import in node:
1065
+ 0 && (module.exports = {
1066
+ signals
330
1067
  });