@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.
- package/README.md +4 -1
- package/dist/cli.js +13791 -7652
- package/dist/lib/aiTrainMetrics.js +173 -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/aiExport.js +192 -0
- package/dist/scripts/aiExportSelect.js +135 -0
- package/dist/scripts/aiTrain.js +2321 -0
- package/dist/scripts/backtest.js +998 -111
- package/dist/scripts/candlesMigrateProvider.js +5302 -0
- package/dist/scripts/continuity.js +1 -1
- package/dist/scripts/derivativesIngest.js +5 -0
- package/dist/scripts/derivativesIngestCoinalyzeAll.js +21 -12
- package/dist/scripts/migration.js +1 -1
- package/dist/scripts/mlExport.js +112 -13
- package/dist/scripts/mlExportSelect.js +43 -9
- package/dist/scripts/mlTrainLatestSelect.js +4 -3
- package/dist/scripts/researchAuto.js +845 -0
- package/dist/scripts/resolveExportStrategy.js +133 -0
- package/dist/scripts/results.js +46 -22
- package/dist/scripts/runtimeParity.js +1651 -0
- package/dist/scripts/selectStrategy.js +4 -3
- package/dist/scripts/signals.js +799 -62
- package/dist/scripts/signalsSummary.js +655 -0
- package/dist/scripts/spawnSiblingScript.js +71 -0
- package/dist/scripts/user-add.js +0 -9
- package/dist/workers/testerWorker.js +13 -2
- package/package.json +18 -12
package/dist/scripts/signals.js
CHANGED
|
@@ -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
|
|
38
|
+
var import_progress2 = __toESM(require("progress"));
|
|
29
39
|
var import_connectors = require("@tradejs/connectors");
|
|
30
|
-
var
|
|
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
|
|
34
|
-
var
|
|
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)(
|
|
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:
|
|
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:
|
|
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 (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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(
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
281
|
-
await (0,
|
|
282
|
-
const
|
|
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 (
|
|
290
|
-
signals2.push(
|
|
1003
|
+
if (strategySignals.length > 0) {
|
|
1004
|
+
signals2.push(...strategySignals);
|
|
291
1005
|
}
|
|
292
1006
|
bar.tick(1, {
|
|
293
|
-
found:
|
|
294
|
-
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
|
-
|
|
1051
|
+
import_chalk2.default.yellow(
|
|
321
1052
|
`signals ${status} in ${formatElapsed(startedAt)} (found=${signals2.length})`
|
|
322
1053
|
)
|
|
323
1054
|
);
|
|
324
1055
|
}
|
|
325
1056
|
};
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
});
|