@tradejs/cli 1.0.6 → 1.0.9
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 +10049 -4860
- 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/runtimeSignalsLoader.js +153 -0
- package/dist/lib/runtimeSignalsStorage.js +162 -0
- package/dist/lib/telegramReports.js +91 -0
- package/dist/lib/timeWindow.js +101 -0
- package/dist/scripts/agentRun.js +909 -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 +847 -0
- package/dist/scripts/results.js +46 -22
- package/dist/scripts/runtimeParity.js +1688 -0
- package/dist/scripts/signals.js +774 -63
- package/dist/scripts/signalsSummary.js +712 -0
- package/dist/scripts/user-add.js +0 -9
- package/package.json +16 -12
package/dist/scripts/signals.js
CHANGED
|
@@ -35,17 +35,520 @@ __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/lib/runtimeSignalsStorage.ts
|
|
478
|
+
var DAY_MS2 = 864e5;
|
|
479
|
+
var RUNTIME_STORAGE_DAY_OFFSET_MS = 5 * 60 * 60 * 1e3;
|
|
480
|
+
var toIsoDayKey = (timestamp) => new Date(timestamp).toISOString().slice(0, 10);
|
|
481
|
+
var toRuntimeStorageDayTimestamp = (timestamp) => Math.floor((timestamp + RUNTIME_STORAGE_DAY_OFFSET_MS) / DAY_MS2) * DAY_MS2;
|
|
482
|
+
var getRuntimeStorageDayKey = (timestamp) => toIsoDayKey(toRuntimeStorageDayTimestamp(timestamp));
|
|
483
|
+
var toRuntimeSignalBucketRef = (signal) => ({
|
|
484
|
+
signalId: signal.signalId,
|
|
485
|
+
symbol: signal.symbol,
|
|
486
|
+
strategy: signal.strategy,
|
|
487
|
+
timestamp: signal.timestamp
|
|
488
|
+
});
|
|
489
|
+
var normalizeRuntimeSignalSkipReason = (reason, fallbackSource) => {
|
|
490
|
+
let source = fallbackSource;
|
|
491
|
+
let normalizedReason = reason;
|
|
492
|
+
if (reason.startsWith("AI_QUALITY_BELOW_MIN")) {
|
|
493
|
+
source = "AI";
|
|
494
|
+
normalizedReason = "MIN_AI_QUALITY";
|
|
495
|
+
} else if (reason === "AI_QUALITY_UNAVAILABLE") {
|
|
496
|
+
source = "AI";
|
|
497
|
+
normalizedReason = "QUALITY_UNAVAILABLE";
|
|
498
|
+
} else if (reason === "ML_RESULT_UNAVAILABLE") {
|
|
499
|
+
source = "ML";
|
|
500
|
+
normalizedReason = "RESULT_UNAVAILABLE";
|
|
501
|
+
} else if (reason.startsWith("ML_THRESHOLD_NOT_MET")) {
|
|
502
|
+
source = "ML";
|
|
503
|
+
normalizedReason = "ML_THRESHOLD";
|
|
504
|
+
} else if (reason.startsWith("HOOK_BEFORE_ENTRY_GATE:")) {
|
|
505
|
+
source = "hook";
|
|
506
|
+
normalizedReason = `BEFORE_ENTRY_GATE:${reason.slice(
|
|
507
|
+
"HOOK_BEFORE_ENTRY_GATE:".length
|
|
508
|
+
)}`;
|
|
509
|
+
} else if (reason === "HOOK_BEFORE_ENTRY_GATE") {
|
|
510
|
+
source = "hook";
|
|
511
|
+
normalizedReason = "BEFORE_ENTRY_GATE";
|
|
512
|
+
} else if (reason === "MAKE_ORDERS_DISABLED" || reason === "ENTRY_POLICY_BLOCKED") {
|
|
513
|
+
source = "policy";
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
source: `skip from ${source}`,
|
|
517
|
+
reason: normalizedReason
|
|
518
|
+
};
|
|
519
|
+
};
|
|
520
|
+
var buildStatsReasonField = (source, reason) => `reason:${source}:${reason}`;
|
|
521
|
+
var buildRuntimeSignalStatsIncrements = (evaluation) => {
|
|
522
|
+
const increments = {
|
|
523
|
+
evaluated: 1
|
|
524
|
+
};
|
|
525
|
+
if (evaluation.status === "signal") {
|
|
526
|
+
increments.signals = 1;
|
|
527
|
+
}
|
|
528
|
+
let skipReason = null;
|
|
529
|
+
let fallbackSource = "core";
|
|
530
|
+
if (evaluation.status === "skip") {
|
|
531
|
+
skipReason = evaluation.reason || "NO_SIGNAL";
|
|
532
|
+
fallbackSource = "core";
|
|
533
|
+
} else if (evaluation.status === "signal" && (evaluation.orderStatus === "skipped" || evaluation.orderStatus === "canceled")) {
|
|
534
|
+
skipReason = evaluation.orderSkipReason || evaluation.reason || `orderStatus=${evaluation.orderStatus}`;
|
|
535
|
+
fallbackSource = "runtime";
|
|
536
|
+
} else if (evaluation.status === "error") {
|
|
537
|
+
skipReason = evaluation.reason || "EVALUATION_ERROR";
|
|
538
|
+
fallbackSource = "runtime";
|
|
539
|
+
}
|
|
540
|
+
if (skipReason) {
|
|
541
|
+
const normalized = normalizeRuntimeSignalSkipReason(
|
|
542
|
+
skipReason,
|
|
543
|
+
fallbackSource
|
|
544
|
+
);
|
|
545
|
+
increments[buildStatsReasonField(normalized.source, normalized.reason)] = 1;
|
|
546
|
+
}
|
|
547
|
+
return increments;
|
|
548
|
+
};
|
|
549
|
+
var shouldStoreDetailedRuntimeSignalEvaluation = (evaluation) => evaluation.status !== "skip";
|
|
550
|
+
|
|
551
|
+
// src/scripts/signals.ts
|
|
49
552
|
import_args.default.option(["t", "tickers"], "Selected tickers");
|
|
50
553
|
import_args.default.option(["e", "exclude"], "Exclude tickers from tests");
|
|
51
554
|
import_args.default.option(["l", "tickersLimit"], "Tickers limit");
|
|
@@ -68,11 +571,50 @@ import_args.default.option(
|
|
|
68
571
|
"Connector provider or name for signals (e.g. bybit, binance, coinbase, custom)",
|
|
69
572
|
"bybit"
|
|
70
573
|
);
|
|
71
|
-
var PRELOAD_START = (0, import_time.getTimestamp)(
|
|
574
|
+
var PRELOAD_START = (0, import_time.getTimestamp)(import_constants2.SIGNALS_CLI_PRELOAD_DAYS);
|
|
72
575
|
var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
|
|
73
576
|
var flags = import_args.default.parse(process.argv);
|
|
74
577
|
var interval = flags.timeframe.toString();
|
|
75
578
|
var formatElapsed = (startedAt) => `${((Date.now() - startedAt) / 1e3).toFixed(1)}s`;
|
|
579
|
+
var formatDuration = (startedAt) => {
|
|
580
|
+
const seconds = (Date.now() - startedAt) / 1e3;
|
|
581
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
582
|
+
const minutes = Math.floor(seconds / 60);
|
|
583
|
+
const restSeconds = Math.round(seconds % 60);
|
|
584
|
+
return `${minutes}m ${restSeconds}s`;
|
|
585
|
+
};
|
|
586
|
+
var timeOperation = async (label, operation) => {
|
|
587
|
+
const startedAt = Date.now();
|
|
588
|
+
try {
|
|
589
|
+
return await operation();
|
|
590
|
+
} finally {
|
|
591
|
+
import_logger.logger.info(import_chalk2.default.gray(`${label}: done in ${formatDuration(startedAt)}`));
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
var normalizeHookList = (value) => {
|
|
595
|
+
if (Array.isArray(value)) {
|
|
596
|
+
return value;
|
|
597
|
+
}
|
|
598
|
+
return value ? [value] : [];
|
|
599
|
+
};
|
|
600
|
+
var invokeBeforeSignalsHooks = async (hooks, params) => {
|
|
601
|
+
for (const hook of normalizeHookList(
|
|
602
|
+
hooks?.beforeSignals
|
|
603
|
+
)) {
|
|
604
|
+
const result = await hook(params);
|
|
605
|
+
if (result?.abort === true) {
|
|
606
|
+
return result;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return void 0;
|
|
610
|
+
};
|
|
611
|
+
var invokeAfterSignalsHooks = async (hooks, params) => {
|
|
612
|
+
for (const hook of normalizeHookList(
|
|
613
|
+
hooks?.afterSignals
|
|
614
|
+
)) {
|
|
615
|
+
await hook(params);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
76
618
|
var resolveStrategyNameByConfigKey = (userName, key) => {
|
|
77
619
|
const parts = key.split(":");
|
|
78
620
|
if (parts.length !== 5) {
|
|
@@ -121,15 +663,56 @@ var createStrategySkipStats = (runtimeStrategies) => new Map(
|
|
|
121
663
|
}
|
|
122
664
|
])
|
|
123
665
|
);
|
|
124
|
-
var recordStrategyReason = (strategyStats, strategyName, reason) => {
|
|
666
|
+
var recordStrategyReason = (strategyStats, strategyName, reason, fallbackSource = "core") => {
|
|
125
667
|
const stats = strategyStats.get(strategyName);
|
|
126
668
|
if (!stats) {
|
|
127
669
|
return;
|
|
128
670
|
}
|
|
129
|
-
|
|
671
|
+
const normalized = normalizeRuntimeSignalSkipReason(reason, fallbackSource);
|
|
672
|
+
const normalizedReason = `${normalized.source} / ${normalized.reason}`;
|
|
673
|
+
stats.reasons.set(
|
|
674
|
+
normalizedReason,
|
|
675
|
+
(stats.reasons.get(normalizedReason) ?? 0) + 1
|
|
676
|
+
);
|
|
677
|
+
};
|
|
678
|
+
var buildRuntimeSignalEvaluationId = ({
|
|
679
|
+
strategyName,
|
|
680
|
+
symbol,
|
|
681
|
+
timestamp
|
|
682
|
+
}) => `${strategyName}:${symbol}:${timestamp}`;
|
|
683
|
+
var getTelegramDeliverableSignals = (signals2) => signals2.filter(
|
|
684
|
+
(signal) => signal.orderStatus !== "skipped" && signal.orderStatus !== "canceled"
|
|
685
|
+
);
|
|
686
|
+
var saveRuntimeSignalEvaluation = async (evaluation) => {
|
|
687
|
+
const dayKey = getRuntimeStorageDayKey(evaluation.timestamp);
|
|
688
|
+
if (shouldStoreDetailedRuntimeSignalEvaluation(evaluation)) {
|
|
689
|
+
await (0, import_redis.setHashJsonField)(
|
|
690
|
+
import_redis.redisKeys.runtimeSignalEvaluationBucket(
|
|
691
|
+
evaluation.userName,
|
|
692
|
+
dayKey,
|
|
693
|
+
evaluation.strategy
|
|
694
|
+
),
|
|
695
|
+
evaluation.evaluationId,
|
|
696
|
+
evaluation,
|
|
697
|
+
{
|
|
698
|
+
expire: import_constants2.TTL_10D
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
await (0, import_redis.incrHashFields)(
|
|
703
|
+
import_redis.redisKeys.runtimeSignalEvaluationStatsBucket(
|
|
704
|
+
evaluation.userName,
|
|
705
|
+
dayKey,
|
|
706
|
+
evaluation.strategy
|
|
707
|
+
),
|
|
708
|
+
buildRuntimeSignalStatsIncrements(evaluation),
|
|
709
|
+
{
|
|
710
|
+
expire: import_constants2.TTL_10D
|
|
711
|
+
}
|
|
712
|
+
);
|
|
130
713
|
};
|
|
131
714
|
var logStrategySkipStats = (runtimeStrategies, strategyStats) => {
|
|
132
|
-
import_logger.logger.info(
|
|
715
|
+
import_logger.logger.info(import_chalk2.default.yellow("skip stats:"));
|
|
133
716
|
for (const { strategyName } of runtimeStrategies) {
|
|
134
717
|
const stats = strategyStats.get(strategyName);
|
|
135
718
|
if (!stats) {
|
|
@@ -181,6 +764,8 @@ var findSignals = async (symbol, connector, btcBinanceData, btcCoinbaseData, run
|
|
|
181
764
|
interval
|
|
182
765
|
})
|
|
183
766
|
]);
|
|
767
|
+
cachedData.pop();
|
|
768
|
+
btcCachedData.pop();
|
|
184
769
|
const lastCandle = cachedData.pop();
|
|
185
770
|
const btcLastCandle = btcCachedData.pop();
|
|
186
771
|
if (!lastCandle || !btcLastCandle) {
|
|
@@ -209,20 +794,74 @@ var findSignals = async (symbol, connector, btcBinanceData, btcCoinbaseData, run
|
|
|
209
794
|
});
|
|
210
795
|
const signal = await strategy(lastCandle, btcLastCandle);
|
|
211
796
|
if (!signal || typeof signal === "string") {
|
|
797
|
+
const reason = typeof signal === "string" && signal.trim() ? signal : "NO_SIGNAL";
|
|
212
798
|
if (typeof signal === "string") {
|
|
213
|
-
recordStrategyReason(strategyStats, strategyName, signal);
|
|
799
|
+
recordStrategyReason(strategyStats, strategyName, signal, "core");
|
|
214
800
|
}
|
|
801
|
+
await saveRuntimeSignalEvaluation({
|
|
802
|
+
evaluationId: buildRuntimeSignalEvaluationId({
|
|
803
|
+
strategyName,
|
|
804
|
+
symbol,
|
|
805
|
+
timestamp: lastCandle.timestamp
|
|
806
|
+
}),
|
|
807
|
+
userName: flags.user,
|
|
808
|
+
strategy: strategyName,
|
|
809
|
+
symbol,
|
|
810
|
+
interval,
|
|
811
|
+
timestamp: lastCandle.timestamp,
|
|
812
|
+
evaluatedAt: Date.now(),
|
|
813
|
+
status: "skip",
|
|
814
|
+
reason
|
|
815
|
+
});
|
|
215
816
|
continue;
|
|
216
817
|
}
|
|
217
818
|
if (stats) {
|
|
218
819
|
stats.signals += 1;
|
|
219
820
|
}
|
|
821
|
+
if (signal.orderStatus === "skipped" && typeof signal.orderSkipReason === "string" && signal.orderSkipReason.trim()) {
|
|
822
|
+
recordStrategyReason(
|
|
823
|
+
strategyStats,
|
|
824
|
+
strategyName,
|
|
825
|
+
signal.orderSkipReason,
|
|
826
|
+
"runtime"
|
|
827
|
+
);
|
|
828
|
+
}
|
|
220
829
|
strategySignals.push(signal);
|
|
221
|
-
await (0, import_redis.setData)(import_redis.redisKeys.signal(symbol, signal.signalId), signal, {
|
|
222
|
-
expire: import_constants.TTL_1D
|
|
223
|
-
});
|
|
224
830
|
await (0, import_redis.setData)(import_redis.redisKeys.storeSignal(symbol, signal.signalId), signal, {
|
|
225
|
-
expire:
|
|
831
|
+
expire: import_constants2.TTL_10D
|
|
832
|
+
});
|
|
833
|
+
await (0, import_redis.setHashJsonField)(
|
|
834
|
+
import_redis.redisKeys.runtimeSignalBucket(
|
|
835
|
+
flags.user,
|
|
836
|
+
getRuntimeStorageDayKey(signal.timestamp),
|
|
837
|
+
signal.strategy
|
|
838
|
+
),
|
|
839
|
+
signal.signalId,
|
|
840
|
+
toRuntimeSignalBucketRef(signal),
|
|
841
|
+
{
|
|
842
|
+
expire: import_constants2.TTL_10D
|
|
843
|
+
}
|
|
844
|
+
);
|
|
845
|
+
await saveRuntimeSignalEvaluation({
|
|
846
|
+
evaluationId: buildRuntimeSignalEvaluationId({
|
|
847
|
+
strategyName,
|
|
848
|
+
symbol,
|
|
849
|
+
timestamp: lastCandle.timestamp
|
|
850
|
+
}),
|
|
851
|
+
userName: flags.user,
|
|
852
|
+
strategy: strategyName,
|
|
853
|
+
symbol,
|
|
854
|
+
interval,
|
|
855
|
+
timestamp: lastCandle.timestamp,
|
|
856
|
+
evaluatedAt: Date.now(),
|
|
857
|
+
status: "signal",
|
|
858
|
+
reason: signal.orderSkipReason || signal.orderStatus || "SIGNAL",
|
|
859
|
+
signalId: signal.signalId,
|
|
860
|
+
direction: signal.direction,
|
|
861
|
+
orderStatus: signal.orderStatus,
|
|
862
|
+
orderSkipReason: signal.orderSkipReason,
|
|
863
|
+
...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
|
|
864
|
+
...signal.ml ? { ml: signal.ml } : {}
|
|
226
865
|
});
|
|
227
866
|
import_logger.logger.info("Signal found %s by strategy %s", symbol, strategyName);
|
|
228
867
|
}
|
|
@@ -232,6 +871,8 @@ var signals = async () => {
|
|
|
232
871
|
const startedAt = Date.now();
|
|
233
872
|
const signals2 = new Array();
|
|
234
873
|
let status = "completed";
|
|
874
|
+
let projectHooks;
|
|
875
|
+
let afterSignalsHookContext = null;
|
|
235
876
|
try {
|
|
236
877
|
const connectorName = await resolveSignalsConnectorName(flags.connector);
|
|
237
878
|
const connectorFactory = await (0, import_connectors2.getConnectorCreatorByName)(
|
|
@@ -276,61 +917,91 @@ var signals = async () => {
|
|
|
276
917
|
);
|
|
277
918
|
}
|
|
278
919
|
}
|
|
279
|
-
const tickers = await (
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
920
|
+
const tickers = await timeOperation(
|
|
921
|
+
"tickers load",
|
|
922
|
+
() => (0, import_cli.getTickers)(
|
|
923
|
+
marketConnector,
|
|
924
|
+
flags.tickers,
|
|
925
|
+
flags.exclude,
|
|
926
|
+
flags.tickersLimit,
|
|
927
|
+
flags.chunk
|
|
928
|
+
)
|
|
285
929
|
);
|
|
286
930
|
if (flags.showTickersList) {
|
|
287
|
-
console.log(
|
|
931
|
+
console.log(import_chalk2.default.gray(JSON.stringify(tickers.sort(), null, 2)));
|
|
288
932
|
return;
|
|
289
933
|
}
|
|
934
|
+
const projectConfig = await (0, import_cli.loadTradejsConfig)(projectRoot);
|
|
935
|
+
projectHooks = projectConfig.hooks;
|
|
290
936
|
if (!flags.cacheOnly) {
|
|
291
|
-
await (
|
|
292
|
-
|
|
293
|
-
interval,
|
|
294
|
-
|
|
295
|
-
|
|
937
|
+
await timeOperation(
|
|
938
|
+
`update ${connectorName}`,
|
|
939
|
+
() => (0, import_cli.update)(marketConnector, interval, tickers, import_constants2.SIGNALS_CLI_PRELOAD_DAYS, {
|
|
940
|
+
connectorLabel: connectorName
|
|
941
|
+
})
|
|
296
942
|
);
|
|
297
943
|
if (btcBinanceConnector !== marketConnector) {
|
|
298
|
-
await (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
944
|
+
await timeOperation(
|
|
945
|
+
`update ${import_connectors.ConnectorNames.Binance}`,
|
|
946
|
+
() => (0, import_cli.update)(
|
|
947
|
+
btcBinanceConnector,
|
|
948
|
+
interval,
|
|
949
|
+
["BTCUSDT"],
|
|
950
|
+
import_constants2.SIGNALS_CLI_PRELOAD_DAYS,
|
|
951
|
+
{ connectorLabel: import_connectors.ConnectorNames.Binance }
|
|
952
|
+
)
|
|
303
953
|
);
|
|
304
954
|
}
|
|
305
955
|
if (btcCoinbaseConnector !== marketConnector) {
|
|
306
|
-
await (
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
956
|
+
await timeOperation(
|
|
957
|
+
`update ${import_connectors.ConnectorNames.Coinbase}`,
|
|
958
|
+
() => (0, import_cli.update)(
|
|
959
|
+
btcCoinbaseConnector,
|
|
960
|
+
interval,
|
|
961
|
+
["BTCUSDT"],
|
|
962
|
+
import_constants2.SIGNALS_CLI_PRELOAD_DAYS,
|
|
963
|
+
{ connectorLabel: import_connectors.ConnectorNames.Coinbase }
|
|
964
|
+
)
|
|
311
965
|
);
|
|
312
966
|
}
|
|
313
967
|
}
|
|
314
968
|
const currentTimestamp = (0, import_time.getTimestamp)();
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
})
|
|
330
|
-
]);
|
|
969
|
+
if (shouldBackfillDerivativesContextForSignals({
|
|
970
|
+
cacheOnly: Boolean(flags.cacheOnly)
|
|
971
|
+
})) {
|
|
972
|
+
await timeOperation(
|
|
973
|
+
"derivatives context backfill",
|
|
974
|
+
() => backfillDerivativesContextForSignals({
|
|
975
|
+
userName: flags.user,
|
|
976
|
+
symbols: tickers,
|
|
977
|
+
startMs: currentTimestamp,
|
|
978
|
+
endMs: currentTimestamp,
|
|
979
|
+
preloadStartMs: PRELOAD_START
|
|
980
|
+
})
|
|
981
|
+
);
|
|
982
|
+
}
|
|
331
983
|
if (flags.updateOnly) {
|
|
332
984
|
return;
|
|
333
985
|
}
|
|
986
|
+
const [btcBinanceData, btcCoinbaseData] = await timeOperation(
|
|
987
|
+
"reference candles load",
|
|
988
|
+
() => Promise.all([
|
|
989
|
+
btcBinanceConnector.kline({
|
|
990
|
+
symbol: "BTCUSDT",
|
|
991
|
+
start: PRELOAD_START,
|
|
992
|
+
end: currentTimestamp,
|
|
993
|
+
cacheOnly: true,
|
|
994
|
+
interval
|
|
995
|
+
}),
|
|
996
|
+
btcCoinbaseConnector.kline({
|
|
997
|
+
symbol: "BTCUSDT",
|
|
998
|
+
start: PRELOAD_START,
|
|
999
|
+
end: currentTimestamp,
|
|
1000
|
+
cacheOnly: true,
|
|
1001
|
+
interval
|
|
1002
|
+
})
|
|
1003
|
+
])
|
|
1004
|
+
);
|
|
334
1005
|
const runtimeStrategies = await loadRuntimeStrategies(flags.user);
|
|
335
1006
|
if (!runtimeStrategies.length) {
|
|
336
1007
|
import_logger.logger.warn(
|
|
@@ -340,20 +1011,44 @@ var signals = async () => {
|
|
|
340
1011
|
return;
|
|
341
1012
|
}
|
|
342
1013
|
import_logger.logger.info(
|
|
343
|
-
|
|
344
|
-
`loaded strategies: ${runtimeStrategies.map((s) => s.strategyName).join(", ")}`
|
|
1014
|
+
import_chalk2.default.yellow(
|
|
1015
|
+
`loaded strategies (user=${flags.user}): ${runtimeStrategies.map((s) => s.strategyName).join(", ")}`
|
|
1016
|
+
)
|
|
1017
|
+
);
|
|
1018
|
+
afterSignalsHookContext = {
|
|
1019
|
+
connector: marketConnector,
|
|
1020
|
+
connectorName,
|
|
1021
|
+
userName: flags.user,
|
|
1022
|
+
interval,
|
|
1023
|
+
tickers: [...tickers],
|
|
1024
|
+
runtimeStrategies: runtimeStrategies.map(
|
|
1025
|
+
({ strategyName, strategyConfig }) => ({
|
|
1026
|
+
strategyName,
|
|
1027
|
+
strategyConfig
|
|
1028
|
+
})
|
|
345
1029
|
)
|
|
1030
|
+
};
|
|
1031
|
+
const beforeSignalsResult = await invokeBeforeSignalsHooks(
|
|
1032
|
+
projectHooks,
|
|
1033
|
+
afterSignalsHookContext
|
|
346
1034
|
);
|
|
1035
|
+
if (beforeSignalsResult?.abort === true) {
|
|
1036
|
+
import_logger.logger.info(
|
|
1037
|
+
"signals aborted before ticker evaluation: %s",
|
|
1038
|
+
beforeSignalsResult.reason ?? "PROJECT_BEFORE_SIGNALS_ABORTED"
|
|
1039
|
+
);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
347
1042
|
const strategyStats = createStrategySkipStats(runtimeStrategies);
|
|
348
|
-
const bar = new
|
|
1043
|
+
const bar = new import_progress2.default(
|
|
349
1044
|
":current/:total [:bar][:percent] :found :eta(s) :symbol",
|
|
350
1045
|
{
|
|
351
1046
|
total: tickers.length,
|
|
352
1047
|
width: 30
|
|
353
1048
|
}
|
|
354
1049
|
);
|
|
355
|
-
import_logger.logger.info(
|
|
356
|
-
await (0,
|
|
1050
|
+
import_logger.logger.info(import_chalk2.default.yellow(`tickers: ${tickers.length}`));
|
|
1051
|
+
await (0, import_async2.runWithConcurrency)(tickers, 5, async (symbol) => {
|
|
357
1052
|
const strategySignals = await findSignals(
|
|
358
1053
|
symbol,
|
|
359
1054
|
marketConnector,
|
|
@@ -366,15 +1061,16 @@ var signals = async () => {
|
|
|
366
1061
|
signals2.push(...strategySignals);
|
|
367
1062
|
}
|
|
368
1063
|
bar.tick(1, {
|
|
369
|
-
found:
|
|
370
|
-
symbol:
|
|
1064
|
+
found: import_chalk2.default.cyan(signals2.length),
|
|
1065
|
+
symbol: import_chalk2.default.gray(symbol)
|
|
371
1066
|
});
|
|
372
1067
|
});
|
|
373
|
-
if (!flags.skipScreenshots) {
|
|
374
|
-
await (0, import_cli.makeScreenshots)(signals2, "15", flags.user);
|
|
375
|
-
}
|
|
376
1068
|
if (flags.notify) {
|
|
377
|
-
|
|
1069
|
+
const telegramSignals = getTelegramDeliverableSignals(signals2);
|
|
1070
|
+
if (!flags.skipScreenshots && telegramSignals.length > 0) {
|
|
1071
|
+
await (0, import_cli.makeScreenshots)(telegramSignals, "15", flags.user);
|
|
1072
|
+
}
|
|
1073
|
+
await (0, import_cli.sendToTG)(telegramSignals, "15", flags.user);
|
|
378
1074
|
}
|
|
379
1075
|
import_logger.logger.info(
|
|
380
1076
|
JSON.stringify(
|
|
@@ -394,8 +1090,23 @@ var signals = async () => {
|
|
|
394
1090
|
);
|
|
395
1091
|
throw error;
|
|
396
1092
|
} finally {
|
|
1093
|
+
if (projectHooks && afterSignalsHookContext) {
|
|
1094
|
+
try {
|
|
1095
|
+
await invokeAfterSignalsHooks(projectHooks, {
|
|
1096
|
+
...afterSignalsHookContext,
|
|
1097
|
+
signals: [...signals2],
|
|
1098
|
+
status,
|
|
1099
|
+
durationMs: Date.now() - startedAt
|
|
1100
|
+
});
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
import_logger.logger.error(
|
|
1103
|
+
"afterSignals hook failed: %s",
|
|
1104
|
+
error?.message || String(error)
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
397
1108
|
import_logger.logger.info(
|
|
398
|
-
|
|
1109
|
+
import_chalk2.default.yellow(
|
|
399
1110
|
`signals ${status} in ${formatElapsed(startedAt)} (found=${signals2.length})`
|
|
400
1111
|
)
|
|
401
1112
|
);
|