@tradejs/cli 1.0.6 → 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.
- package/dist/cli.js +11012 -5970
- package/dist/lib/aiTrainMetrics.js +23 -0
- package/dist/lib/candlesProviderMigration.js +266 -0
- package/dist/lib/cliArgs.js +40 -0
- package/dist/lib/derivativesContextBackfill.js +487 -0
- package/dist/lib/researchAgent.js +195 -0
- package/dist/lib/runtimeParity.js +234 -0
- package/dist/lib/telegramReports.js +91 -0
- package/dist/lib/timeWindow.js +101 -0
- package/dist/scripts/agentRun.js +895 -0
- package/dist/scripts/aiTrain.js +113 -31
- package/dist/scripts/backtest.js +794 -93
- package/dist/scripts/candlesMigrateProvider.js +5302 -0
- package/dist/scripts/continuity.js +1 -1
- package/dist/scripts/migration.js +1 -1
- package/dist/scripts/researchAuto.js +845 -0
- package/dist/scripts/results.js +46 -22
- package/dist/scripts/runtimeParity.js +1651 -0
- package/dist/scripts/signals.js +710 -57
- package/dist/scripts/signalsSummary.js +655 -0
- package/dist/scripts/user-add.js +0 -9
- package/package.json +16 -12
package/dist/scripts/signals.js
CHANGED
|
@@ -35,17 +35,446 @@ __export(signals_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(signals_exports);
|
|
36
36
|
var import_config = require("dotenv/config");
|
|
37
37
|
var import_args = __toESM(require("args"));
|
|
38
|
-
var
|
|
38
|
+
var import_progress2 = __toESM(require("progress"));
|
|
39
39
|
var import_connectors = require("@tradejs/connectors");
|
|
40
|
-
var
|
|
40
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
41
41
|
var import_connectors2 = require("@tradejs/node/connectors");
|
|
42
42
|
var import_cli = require("@tradejs/node/cli");
|
|
43
|
-
var
|
|
44
|
-
var
|
|
43
|
+
var import_async2 = require("@tradejs/core/async");
|
|
44
|
+
var import_constants2 = require("@tradejs/core/constants");
|
|
45
45
|
var import_strategies = require("@tradejs/node/strategies");
|
|
46
46
|
var import_time = require("@tradejs/core/time");
|
|
47
47
|
var import_logger = require("@tradejs/infra/logger");
|
|
48
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
|
|
49
478
|
import_args.default.option(["t", "tickers"], "Selected tickers");
|
|
50
479
|
import_args.default.option(["e", "exclude"], "Exclude tickers from tests");
|
|
51
480
|
import_args.default.option(["l", "tickersLimit"], "Tickers limit");
|
|
@@ -68,11 +497,50 @@ import_args.default.option(
|
|
|
68
497
|
"Connector provider or name for signals (e.g. bybit, binance, coinbase, custom)",
|
|
69
498
|
"bybit"
|
|
70
499
|
);
|
|
71
|
-
var PRELOAD_START = (0, import_time.getTimestamp)(
|
|
500
|
+
var PRELOAD_START = (0, import_time.getTimestamp)(import_constants2.SIGNALS_CLI_PRELOAD_DAYS);
|
|
72
501
|
var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
|
|
73
502
|
var flags = import_args.default.parse(process.argv);
|
|
74
503
|
var interval = flags.timeframe.toString();
|
|
75
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
|
+
};
|
|
76
544
|
var resolveStrategyNameByConfigKey = (userName, key) => {
|
|
77
545
|
const parts = key.split(":");
|
|
78
546
|
if (parts.length !== 5) {
|
|
@@ -121,15 +589,77 @@ var createStrategySkipStats = (runtimeStrategies) => new Map(
|
|
|
121
589
|
}
|
|
122
590
|
])
|
|
123
591
|
);
|
|
124
|
-
var
|
|
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") => {
|
|
125
621
|
const stats = strategyStats.get(strategyName);
|
|
126
622
|
if (!stats) {
|
|
127
623
|
return;
|
|
128
624
|
}
|
|
129
|
-
|
|
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
|
+
);
|
|
130
660
|
};
|
|
131
661
|
var logStrategySkipStats = (runtimeStrategies, strategyStats) => {
|
|
132
|
-
import_logger.logger.info(
|
|
662
|
+
import_logger.logger.info(import_chalk2.default.yellow("skip stats:"));
|
|
133
663
|
for (const { strategyName } of runtimeStrategies) {
|
|
134
664
|
const stats = strategyStats.get(strategyName);
|
|
135
665
|
if (!stats) {
|
|
@@ -209,20 +739,72 @@ var findSignals = async (symbol, connector, btcBinanceData, btcCoinbaseData, run
|
|
|
209
739
|
});
|
|
210
740
|
const signal = await strategy(lastCandle, btcLastCandle);
|
|
211
741
|
if (!signal || typeof signal === "string") {
|
|
742
|
+
const reason = typeof signal === "string" && signal.trim() ? signal : "NO_SIGNAL";
|
|
212
743
|
if (typeof signal === "string") {
|
|
213
|
-
recordStrategyReason(strategyStats, strategyName, signal);
|
|
744
|
+
recordStrategyReason(strategyStats, strategyName, signal, "core");
|
|
214
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
|
+
});
|
|
215
761
|
continue;
|
|
216
762
|
}
|
|
217
763
|
if (stats) {
|
|
218
764
|
stats.signals += 1;
|
|
219
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
|
+
}
|
|
220
774
|
strategySignals.push(signal);
|
|
221
775
|
await (0, import_redis.setData)(import_redis.redisKeys.signal(symbol, signal.signalId), signal, {
|
|
222
|
-
expire:
|
|
776
|
+
expire: import_constants2.TTL_1D
|
|
223
777
|
});
|
|
224
778
|
await (0, import_redis.setData)(import_redis.redisKeys.storeSignal(symbol, signal.signalId), signal, {
|
|
225
|
-
expire:
|
|
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 } : {}
|
|
226
808
|
});
|
|
227
809
|
import_logger.logger.info("Signal found %s by strategy %s", symbol, strategyName);
|
|
228
810
|
}
|
|
@@ -232,6 +814,8 @@ var signals = async () => {
|
|
|
232
814
|
const startedAt = Date.now();
|
|
233
815
|
const signals2 = new Array();
|
|
234
816
|
let status = "completed";
|
|
817
|
+
let projectHooks;
|
|
818
|
+
let afterSignalsHookContext = null;
|
|
235
819
|
try {
|
|
236
820
|
const connectorName = await resolveSignalsConnectorName(flags.connector);
|
|
237
821
|
const connectorFactory = await (0, import_connectors2.getConnectorCreatorByName)(
|
|
@@ -276,61 +860,91 @@ var signals = async () => {
|
|
|
276
860
|
);
|
|
277
861
|
}
|
|
278
862
|
}
|
|
279
|
-
const tickers = await (
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
+
)
|
|
285
872
|
);
|
|
286
873
|
if (flags.showTickersList) {
|
|
287
|
-
console.log(
|
|
874
|
+
console.log(import_chalk2.default.gray(JSON.stringify(tickers.sort(), null, 2)));
|
|
288
875
|
return;
|
|
289
876
|
}
|
|
877
|
+
const projectConfig = await (0, import_cli.loadTradejsConfig)(projectRoot);
|
|
878
|
+
projectHooks = projectConfig.hooks;
|
|
290
879
|
if (!flags.cacheOnly) {
|
|
291
|
-
await (
|
|
292
|
-
|
|
293
|
-
interval,
|
|
294
|
-
|
|
295
|
-
|
|
880
|
+
await timeOperation(
|
|
881
|
+
`update ${connectorName}`,
|
|
882
|
+
() => (0, import_cli.update)(marketConnector, interval, tickers, import_constants2.SIGNALS_CLI_PRELOAD_DAYS, {
|
|
883
|
+
connectorLabel: connectorName
|
|
884
|
+
})
|
|
296
885
|
);
|
|
297
886
|
if (btcBinanceConnector !== marketConnector) {
|
|
298
|
-
await (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
+
)
|
|
303
896
|
);
|
|
304
897
|
}
|
|
305
898
|
if (btcCoinbaseConnector !== marketConnector) {
|
|
306
|
-
await (
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
+
)
|
|
311
908
|
);
|
|
312
909
|
}
|
|
313
910
|
}
|
|
314
911
|
const currentTimestamp = (0, import_time.getTimestamp)();
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
})
|
|
330
|
-
]);
|
|
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
|
+
}
|
|
331
926
|
if (flags.updateOnly) {
|
|
332
927
|
return;
|
|
333
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
|
+
);
|
|
334
948
|
const runtimeStrategies = await loadRuntimeStrategies(flags.user);
|
|
335
949
|
if (!runtimeStrategies.length) {
|
|
336
950
|
import_logger.logger.warn(
|
|
@@ -340,20 +954,44 @@ var signals = async () => {
|
|
|
340
954
|
return;
|
|
341
955
|
}
|
|
342
956
|
import_logger.logger.info(
|
|
343
|
-
|
|
344
|
-
`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(", ")}`
|
|
345
959
|
)
|
|
346
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
|
+
})
|
|
972
|
+
)
|
|
973
|
+
};
|
|
974
|
+
const beforeSignalsResult = await invokeBeforeSignalsHooks(
|
|
975
|
+
projectHooks,
|
|
976
|
+
afterSignalsHookContext
|
|
977
|
+
);
|
|
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
|
+
}
|
|
347
985
|
const strategyStats = createStrategySkipStats(runtimeStrategies);
|
|
348
|
-
const bar = new
|
|
986
|
+
const bar = new import_progress2.default(
|
|
349
987
|
":current/:total [:bar][:percent] :found :eta(s) :symbol",
|
|
350
988
|
{
|
|
351
989
|
total: tickers.length,
|
|
352
990
|
width: 30
|
|
353
991
|
}
|
|
354
992
|
);
|
|
355
|
-
import_logger.logger.info(
|
|
356
|
-
await (0,
|
|
993
|
+
import_logger.logger.info(import_chalk2.default.yellow(`tickers: ${tickers.length}`));
|
|
994
|
+
await (0, import_async2.runWithConcurrency)(tickers, 5, async (symbol) => {
|
|
357
995
|
const strategySignals = await findSignals(
|
|
358
996
|
symbol,
|
|
359
997
|
marketConnector,
|
|
@@ -366,8 +1004,8 @@ var signals = async () => {
|
|
|
366
1004
|
signals2.push(...strategySignals);
|
|
367
1005
|
}
|
|
368
1006
|
bar.tick(1, {
|
|
369
|
-
found:
|
|
370
|
-
symbol:
|
|
1007
|
+
found: import_chalk2.default.cyan(signals2.length),
|
|
1008
|
+
symbol: import_chalk2.default.gray(symbol)
|
|
371
1009
|
});
|
|
372
1010
|
});
|
|
373
1011
|
if (!flags.skipScreenshots) {
|
|
@@ -394,8 +1032,23 @@ var signals = async () => {
|
|
|
394
1032
|
);
|
|
395
1033
|
throw error;
|
|
396
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
|
+
}
|
|
397
1050
|
import_logger.logger.info(
|
|
398
|
-
|
|
1051
|
+
import_chalk2.default.yellow(
|
|
399
1052
|
`signals ${status} in ${formatElapsed(startedAt)} (found=${signals2.length})`
|
|
400
1053
|
)
|
|
401
1054
|
);
|