@tradejs/cli 1.0.8 → 1.0.10
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 +29748 -13641
- package/dist/lib/aiPocketSearch.js +1316 -0
- package/dist/lib/aiTrainCharts.js +711 -0
- package/dist/lib/aiTrainDataset.js +40 -0
- package/dist/lib/aiTrainEvaluationDump.js +76 -0
- package/dist/lib/aiTrainMetrics.js +148 -8
- package/dist/lib/aiTrainOptions.js +130 -0
- package/dist/lib/aiTrainQuarantine.js +199 -0
- package/dist/lib/aiTrainResearch.js +467 -0
- package/dist/lib/binanceMarketContextBackfill.js +841 -0
- package/dist/lib/binanceMarketData.js +392 -0
- package/dist/lib/coinMarketCapContextBackfill.js +1088 -0
- package/dist/lib/continuity.js +60 -0
- package/dist/lib/derivativesContextBackfill.js +490 -77
- package/dist/lib/executionCalibration.js +705 -0
- package/dist/lib/marketContextPrepare.js +4403 -0
- package/dist/lib/paritySummary.js +101 -0
- package/dist/lib/runBot.js +1 -0
- package/dist/lib/runEnvironment.js +2254 -0
- package/dist/lib/runFormatting.js +1657 -0
- package/dist/lib/runtimeDebugEvidence.js +526 -0
- package/dist/lib/runtimeModeConfig.js +53 -0
- package/dist/lib/runtimeParity.js +88 -11
- package/dist/lib/runtimeParityDetails.js +751 -0
- package/dist/lib/runtimeRedis.js +157 -0
- package/dist/lib/runtimeSignalsLoader.js +187 -0
- package/dist/lib/runtimeSignalsStorage.js +185 -0
- package/dist/lib/runtimeStrategyBacktest.js +134 -0
- package/dist/lib/runtimeTradeSync.js +263 -0
- package/dist/lib/telegramReports.js +9 -0
- package/dist/lib/tickerUniverseCache.js +116 -0
- package/dist/lib/timeWindow.js +3 -3
- package/dist/scripts/agentRun.js +28 -8
- package/dist/scripts/aiExport.js +1721 -7
- package/dist/scripts/aiExportSelect.js +16 -4
- package/dist/scripts/aiPocketSearch.js +4190 -0
- package/dist/scripts/aiTrain.js +1866 -87
- package/dist/scripts/backtest.js +4509 -818
- package/dist/scripts/binanceMarketIngest.js +565 -0
- package/dist/scripts/bot.js +16 -2
- package/dist/scripts/candlesMigrateProvider.js +32 -22
- package/dist/scripts/cleanDir.js +15 -2
- package/dist/scripts/cleanRedis.js +15 -2
- package/dist/scripts/cleanTests.js +38 -2
- package/dist/scripts/cleanupMarketContext.js +69 -0
- package/dist/scripts/continuity.js +148 -67
- package/dist/scripts/derivativesIngest.js +14 -4
- package/dist/scripts/derivativesIngestCoinalyzeAll.js +14 -4
- package/dist/scripts/doctor.js +14 -4
- package/dist/scripts/executionCalibration.js +792 -0
- package/dist/scripts/infraCommon.js +10 -0
- package/dist/scripts/infraDown.js +14 -4
- package/dist/scripts/infraInit.js +24 -4
- package/dist/scripts/infraUp.js +14 -4
- package/dist/scripts/marketWs.js +222 -0
- package/dist/scripts/migration.js +15 -2
- package/dist/scripts/mlExport.js +1698 -10
- package/dist/scripts/mlExportSelect.js +16 -4
- package/dist/scripts/mlInspect.js +13 -3
- package/dist/scripts/mlTrainLatestSelect.js +14 -4
- package/dist/scripts/replay.js +8824 -0
- package/dist/scripts/replayRunner.js +8827 -0
- package/dist/scripts/replayRuntimeEvidence.js +868 -0
- package/dist/scripts/researchAuto.js +289 -60
- package/dist/scripts/results.js +3 -3
- package/dist/scripts/runtimeEvidence.js +605 -0
- package/dist/scripts/runtimeParity.js +6360 -1004
- package/dist/scripts/serverHealth.js +588 -0
- package/dist/scripts/signals.js +5623 -486
- package/dist/scripts/signalsDaemon.js +6167 -0
- package/dist/scripts/signalsSummary.js +954 -312
- package/dist/scripts/test-ml.js +25 -3
- package/dist/scripts/test.js +26 -1
- package/dist/scripts/user-add.js +15 -2
- package/dist/workers/testerWorker.js +130 -21
- package/package.json +18 -12
|
@@ -30,16 +30,310 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/scripts/signalsSummary.ts
|
|
31
31
|
var signalsSummary_exports = {};
|
|
32
32
|
__export(signalsSummary_exports, {
|
|
33
|
+
main: () => main,
|
|
33
34
|
signalsSummary: () => signalsSummary
|
|
34
35
|
});
|
|
35
36
|
module.exports = __toCommonJS(signalsSummary_exports);
|
|
36
37
|
var import_config = require("dotenv/config");
|
|
37
38
|
var import_args = __toESM(require("args"));
|
|
38
|
-
var import_constants = require("@tradejs/core/constants");
|
|
39
39
|
var import_logger = require("@tradejs/infra/logger");
|
|
40
|
-
var import_redis = require("@tradejs/infra/redis");
|
|
41
40
|
var import_connectors = require("@tradejs/node/connectors");
|
|
42
41
|
|
|
42
|
+
// src/lib/runtimeRedis.ts
|
|
43
|
+
var import_redis = require("@tradejs/infra/redis");
|
|
44
|
+
var import_time = require("@tradejs/core/time");
|
|
45
|
+
var RESERVED_STRATEGY_NAMES = /* @__PURE__ */ new Set(["charts"]);
|
|
46
|
+
var RESERVED_CONFIG_IDS = /* @__PURE__ */ new Set(["results"]);
|
|
47
|
+
var resolveStrategyConfigIdentityByKey = (userName, key) => {
|
|
48
|
+
const parts = key.split(":");
|
|
49
|
+
if (parts.length !== 5) return null;
|
|
50
|
+
const [users, keyUserName, strategiesKey, strategyName, configId] = parts;
|
|
51
|
+
if (users !== "users" || keyUserName !== userName || strategiesKey !== "strategies" || !strategyName || !configId || RESERVED_STRATEGY_NAMES.has(strategyName) || RESERVED_CONFIG_IDS.has(configId) || !/^[a-zA-Z0-9_-]+$/.test(configId)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return { strategyName, configId };
|
|
55
|
+
};
|
|
56
|
+
var isRuntimeTradeRecord = (value) => {
|
|
57
|
+
if (!value || typeof value !== "object") {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const record = value;
|
|
61
|
+
return typeof record.orderId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.entryTimestamp === "number" && typeof record.entryPrice === "number" && typeof record.qty === "number" && (record.direction === "LONG" || record.direction === "SHORT");
|
|
62
|
+
};
|
|
63
|
+
var resolveStrategyNameByConfigKey = (userName, key) => {
|
|
64
|
+
return resolveStrategyConfigIdentityByKey(userName, key)?.strategyName ?? null;
|
|
65
|
+
};
|
|
66
|
+
var getRuntimeStrategyConfigKeys = async (userName) => {
|
|
67
|
+
const keys = await (0, import_redis.getKeys)(`${import_redis.redisKeys.strategies(userName)}:`);
|
|
68
|
+
return keys.filter((key) => resolveStrategyConfigIdentityByKey(userName, key)).sort((left, right) => left.localeCompare(right));
|
|
69
|
+
};
|
|
70
|
+
var loadRuntimeStrategyNames = async (userName) => [
|
|
71
|
+
...new Set(
|
|
72
|
+
(await getRuntimeStrategyConfigKeys(userName)).map((key) => resolveStrategyNameByConfigKey(userName, key)).filter((value) => Boolean(value))
|
|
73
|
+
)
|
|
74
|
+
].sort((left, right) => left.localeCompare(right));
|
|
75
|
+
var loadRuntimeTrades = async (userName, {
|
|
76
|
+
startTime,
|
|
77
|
+
endTime
|
|
78
|
+
} = {}) => {
|
|
79
|
+
const hasWindow = Number.isFinite(startTime) && Number.isFinite(endTime) && startTime != null && endTime != null;
|
|
80
|
+
const filterByWindow = (trade) => !hasWindow || trade.entryTimestamp >= startTime && trade.entryTimestamp <= endTime;
|
|
81
|
+
if (hasWindow) {
|
|
82
|
+
const dayKeys = (0, import_time.getRuntimeStorageDayKeys)(startTime, endTime);
|
|
83
|
+
const bucketTrades = (await Promise.all(
|
|
84
|
+
dayKeys.map(
|
|
85
|
+
(dayKey) => (0, import_redis.getHashJsonValues)(
|
|
86
|
+
import_redis.redisKeys.runtimeTradeBucket(userName, dayKey)
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
)).flat();
|
|
90
|
+
const dedupedBucketTrades = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const trade of bucketTrades) {
|
|
92
|
+
if (!isRuntimeTradeRecord(trade)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
dedupedBucketTrades.set(trade.orderId, trade);
|
|
96
|
+
}
|
|
97
|
+
const windowedTrades = [...dedupedBucketTrades.values()].filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
|
|
98
|
+
if (windowedTrades.length > 0 || dayKeys.length === 0) {
|
|
99
|
+
return windowedTrades;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const tradePrefix = import_redis.redisKeys.runtimeTrades(userName);
|
|
103
|
+
const bucketPrefix = import_redis.redisKeys.runtimeTradeBuckets(userName);
|
|
104
|
+
const keys = (await (0, import_redis.getKeys)(tradePrefix)).filter(
|
|
105
|
+
(key) => !key.startsWith(bucketPrefix)
|
|
106
|
+
);
|
|
107
|
+
const trades = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
|
|
108
|
+
return trades.filter(isRuntimeTradeRecord).filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/lib/runtimeTradeSync.ts
|
|
112
|
+
var import_constants = require("@tradejs/core/constants");
|
|
113
|
+
var import_time2 = require("@tradejs/core/time");
|
|
114
|
+
var import_redis2 = require("@tradejs/infra/redis");
|
|
115
|
+
var CLOSED_PNL_LIMIT = 100;
|
|
116
|
+
var EXCHANGE_HISTORY_MAX_RANGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
117
|
+
var splitExchangeHistoryTimeRange = ({
|
|
118
|
+
startTime,
|
|
119
|
+
endTime,
|
|
120
|
+
maxRangeMs = EXCHANGE_HISTORY_MAX_RANGE_MS
|
|
121
|
+
}) => {
|
|
122
|
+
if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || !Number.isFinite(maxRangeMs) || maxRangeMs <= 0 || endTime <= startTime) {
|
|
123
|
+
return [{ startTime, endTime }];
|
|
124
|
+
}
|
|
125
|
+
const chunks = [];
|
|
126
|
+
let chunkStart = Math.trunc(startTime);
|
|
127
|
+
const finalEnd = Math.trunc(endTime);
|
|
128
|
+
const maxRange = Math.trunc(maxRangeMs);
|
|
129
|
+
while (chunkStart < finalEnd) {
|
|
130
|
+
const chunkEnd = Math.min(finalEnd, chunkStart + maxRange);
|
|
131
|
+
chunks.push({ startTime: chunkStart, endTime: chunkEnd });
|
|
132
|
+
if (chunkEnd >= finalEnd) {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
chunkStart = chunkEnd + 1;
|
|
136
|
+
}
|
|
137
|
+
return chunks;
|
|
138
|
+
};
|
|
139
|
+
var formatRuntimeTradeSyncError = (error) => {
|
|
140
|
+
if (!error || typeof error !== "object") {
|
|
141
|
+
return String(error || "unknown error");
|
|
142
|
+
}
|
|
143
|
+
const record = error;
|
|
144
|
+
const message = typeof record.message === "string" && record.message.trim() ? record.message.trim() : String(error);
|
|
145
|
+
const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : null;
|
|
146
|
+
const isAxiosError = record.isAxiosError === true;
|
|
147
|
+
const config = record.config && typeof record.config === "object" ? record.config : null;
|
|
148
|
+
const response = record.response && typeof record.response === "object" ? record.response : null;
|
|
149
|
+
const method = typeof config?.method === "string" && config.method.trim() ? config.method.toUpperCase() : null;
|
|
150
|
+
const url = typeof config?.url === "string" && config.url.trim() ? config.url.trim() : null;
|
|
151
|
+
const timeout = typeof config?.timeout === "number" && Number.isFinite(config.timeout) ? config.timeout : null;
|
|
152
|
+
const status = typeof response?.status === "number" && Number.isFinite(response.status) ? response.status : null;
|
|
153
|
+
const details = [
|
|
154
|
+
isAxiosError ? "axios" : null,
|
|
155
|
+
code,
|
|
156
|
+
status == null ? null : `status=${status}`,
|
|
157
|
+
method && url ? `${method} ${url}` : url,
|
|
158
|
+
timeout == null ? null : `timeout=${timeout}ms`
|
|
159
|
+
].filter(Boolean);
|
|
160
|
+
return details.length > 0 ? `${message} (${details.join(", ")})` : message;
|
|
161
|
+
};
|
|
162
|
+
var loadClosedPnlRows = async ({
|
|
163
|
+
connector,
|
|
164
|
+
startTime,
|
|
165
|
+
endTime,
|
|
166
|
+
callbacks
|
|
167
|
+
}) => {
|
|
168
|
+
if (typeof connector.getClosedPnl !== "function") {
|
|
169
|
+
callbacks?.onUnsupported?.();
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
const rows = [];
|
|
173
|
+
const chunks = splitExchangeHistoryTimeRange({ startTime, endTime });
|
|
174
|
+
for (const chunk of chunks) {
|
|
175
|
+
try {
|
|
176
|
+
const chunkRows = await connector.getClosedPnl({
|
|
177
|
+
startTime: chunk.startTime,
|
|
178
|
+
endTime: chunk.endTime,
|
|
179
|
+
limit: CLOSED_PNL_LIMIT
|
|
180
|
+
});
|
|
181
|
+
if (chunkRows.length >= CLOSED_PNL_LIMIT) {
|
|
182
|
+
callbacks?.onCapped?.(chunkRows.length);
|
|
183
|
+
}
|
|
184
|
+
rows.push(...chunkRows);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
callbacks?.onError?.(error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return rows.sort((left, right) => left.closedAt - right.closedAt);
|
|
190
|
+
};
|
|
191
|
+
var consumeClosedPnlMatch = (buckets, trade) => {
|
|
192
|
+
const rows = buckets.get(trade.symbol);
|
|
193
|
+
if (!rows?.length) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const minimumClosedAt = trade.entryTimestamp - 5 * 6e4;
|
|
197
|
+
const matchIndex = rows.findIndex(
|
|
198
|
+
(row2) => Number.isFinite(row2.closedAt) && row2.closedAt >= minimumClosedAt
|
|
199
|
+
);
|
|
200
|
+
if (matchIndex < 0) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const [row] = rows.splice(matchIndex, 1);
|
|
204
|
+
return row ?? null;
|
|
205
|
+
};
|
|
206
|
+
var syncRuntimeTrades = async ({
|
|
207
|
+
userName,
|
|
208
|
+
connector,
|
|
209
|
+
trades,
|
|
210
|
+
startTime,
|
|
211
|
+
endTime,
|
|
212
|
+
closedPnlCallbacks,
|
|
213
|
+
openPositionCallbacks
|
|
214
|
+
}) => {
|
|
215
|
+
let openPositions = [];
|
|
216
|
+
let openPositionsReliable = true;
|
|
217
|
+
if (typeof connector.getOpenPositionPnl === "function") {
|
|
218
|
+
try {
|
|
219
|
+
openPositions = await connector.getOpenPositionPnl();
|
|
220
|
+
} catch (error) {
|
|
221
|
+
openPositionCallbacks?.onError?.(error);
|
|
222
|
+
openPositionsReliable = false;
|
|
223
|
+
openPositions = [];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const openPositionsBySymbol = new Map(
|
|
227
|
+
openPositions.map((position) => [position.symbol, position])
|
|
228
|
+
);
|
|
229
|
+
const activeOrderIdBySymbol = /* @__PURE__ */ new Map();
|
|
230
|
+
const symbols = [...new Set(trades.map((trade) => trade.symbol))];
|
|
231
|
+
await Promise.all(
|
|
232
|
+
symbols.map(async (symbol) => {
|
|
233
|
+
const activeRef = await (0, import_redis2.getData)(
|
|
234
|
+
import_redis2.redisKeys.runtimeActiveTrade(userName, symbol),
|
|
235
|
+
null
|
|
236
|
+
);
|
|
237
|
+
activeOrderIdBySymbol.set(
|
|
238
|
+
symbol,
|
|
239
|
+
typeof activeRef?.orderId === "string" ? activeRef.orderId : null
|
|
240
|
+
);
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
const closedPnlRows = await loadClosedPnlRows({
|
|
244
|
+
connector,
|
|
245
|
+
startTime,
|
|
246
|
+
endTime,
|
|
247
|
+
callbacks: closedPnlCallbacks
|
|
248
|
+
});
|
|
249
|
+
const closedPnlBuckets = /* @__PURE__ */ new Map();
|
|
250
|
+
for (const row of closedPnlRows) {
|
|
251
|
+
const bucket = closedPnlBuckets.get(row.symbol) ?? [];
|
|
252
|
+
bucket.push(row);
|
|
253
|
+
closedPnlBuckets.set(row.symbol, bucket);
|
|
254
|
+
}
|
|
255
|
+
const syncedTrades = [];
|
|
256
|
+
for (const trade of trades) {
|
|
257
|
+
if (trade.status !== "active") {
|
|
258
|
+
syncedTrades.push(trade);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const openPosition = openPositionsBySymbol.get(trade.symbol);
|
|
262
|
+
const activeOrderId = activeOrderIdBySymbol.get(trade.symbol);
|
|
263
|
+
const isCurrentActiveTrade = activeOrderId === trade.orderId;
|
|
264
|
+
if (!openPositionsReliable) {
|
|
265
|
+
syncedTrades.push({
|
|
266
|
+
...trade,
|
|
267
|
+
status: "active",
|
|
268
|
+
lastSyncedAt: endTime
|
|
269
|
+
});
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (isCurrentActiveTrade && openPosition && openPosition.direction === trade.direction) {
|
|
273
|
+
const nextTrade2 = {
|
|
274
|
+
...trade,
|
|
275
|
+
status: "active",
|
|
276
|
+
currentPrice: openPosition.currentPrice,
|
|
277
|
+
currentPnl: openPosition.unrealizedPnl,
|
|
278
|
+
lastSyncedAt: endTime
|
|
279
|
+
};
|
|
280
|
+
await (0, import_redis2.setData)(
|
|
281
|
+
import_redis2.redisKeys.runtimeTrade(userName, trade.orderId),
|
|
282
|
+
nextTrade2,
|
|
283
|
+
{
|
|
284
|
+
expire: 0
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
await (0, import_redis2.setHashJsonField)(
|
|
288
|
+
import_redis2.redisKeys.runtimeTradeBucket(
|
|
289
|
+
userName,
|
|
290
|
+
(0, import_time2.getRuntimeStorageDayKey)(trade.entryTimestamp)
|
|
291
|
+
),
|
|
292
|
+
trade.orderId,
|
|
293
|
+
nextTrade2,
|
|
294
|
+
{ expire: 0 }
|
|
295
|
+
);
|
|
296
|
+
syncedTrades.push(nextTrade2);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const matchedClosedPnl = consumeClosedPnlMatch(closedPnlBuckets, trade);
|
|
300
|
+
const nextTrade = {
|
|
301
|
+
...trade,
|
|
302
|
+
status: "closed",
|
|
303
|
+
currentPrice: matchedClosedPnl?.exitPrice ?? trade.currentPrice ?? null,
|
|
304
|
+
currentPnl: matchedClosedPnl?.closedPnl ?? trade.closedPnl ?? trade.currentPnl ?? null,
|
|
305
|
+
closedPnl: matchedClosedPnl?.closedPnl ?? trade.closedPnl ?? trade.currentPnl ?? null,
|
|
306
|
+
actualEntryPrice: matchedClosedPnl?.entryPrice ?? trade.actualEntryPrice ?? null,
|
|
307
|
+
exitPrice: matchedClosedPnl?.exitPrice ?? trade.exitPrice ?? null,
|
|
308
|
+
actualExitPrice: matchedClosedPnl?.exitPrice ?? trade.actualExitPrice ?? null,
|
|
309
|
+
exitTimestamp: matchedClosedPnl?.closedAt ?? trade.exitTimestamp ?? endTime,
|
|
310
|
+
exitType: trade.exitType ?? null,
|
|
311
|
+
openFee: matchedClosedPnl?.openFee ?? trade.openFee ?? null,
|
|
312
|
+
closeFee: matchedClosedPnl?.closeFee ?? trade.closeFee ?? null,
|
|
313
|
+
fundingFee: matchedClosedPnl?.fundingFee ?? trade.fundingFee ?? null,
|
|
314
|
+
totalFee: matchedClosedPnl?.totalFee ?? trade.totalFee ?? null,
|
|
315
|
+
lastSyncedAt: endTime
|
|
316
|
+
};
|
|
317
|
+
await Promise.all([
|
|
318
|
+
(0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
|
|
319
|
+
expire: import_constants.TTL_1M
|
|
320
|
+
}),
|
|
321
|
+
(0, import_redis2.setHashJsonField)(
|
|
322
|
+
import_redis2.redisKeys.runtimeTradeBucket(
|
|
323
|
+
userName,
|
|
324
|
+
(0, import_time2.getRuntimeStorageDayKey)(trade.entryTimestamp)
|
|
325
|
+
),
|
|
326
|
+
trade.orderId,
|
|
327
|
+
nextTrade,
|
|
328
|
+
{ expire: import_constants.TTL_1M }
|
|
329
|
+
),
|
|
330
|
+
...isCurrentActiveTrade ? [(0, import_redis2.delKey)(import_redis2.redisKeys.runtimeActiveTrade(userName, trade.symbol))] : []
|
|
331
|
+
]);
|
|
332
|
+
syncedTrades.push(nextTrade);
|
|
333
|
+
}
|
|
334
|
+
return syncedTrades;
|
|
335
|
+
};
|
|
336
|
+
|
|
43
337
|
// src/lib/telegramReports.ts
|
|
44
338
|
var import_cli = require("@tradejs/node/cli");
|
|
45
339
|
var TELEGRAM_REPORT_CHUNK_LIMIT = 3900;
|
|
@@ -84,8 +378,16 @@ ${line}` : line;
|
|
|
84
378
|
};
|
|
85
379
|
var sendTelegramReport = async (message, options = {}) => {
|
|
86
380
|
const chunks = splitTelegramReport(message);
|
|
381
|
+
const sendAttachments = async () => {
|
|
382
|
+
for (const attachment of options.attachments || []) {
|
|
383
|
+
await (0, import_cli.sendDocumentToTG)(attachment, {
|
|
384
|
+
userName: options.userName
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
};
|
|
87
388
|
if (chunks.length === 1) {
|
|
88
389
|
await (0, import_cli.sendTextToTG)(chunks[0], options);
|
|
390
|
+
await sendAttachments();
|
|
89
391
|
return chunks.length;
|
|
90
392
|
}
|
|
91
393
|
for (const [index, chunk] of chunks.entries()) {
|
|
@@ -97,9 +399,343 @@ ${chunk}`, {
|
|
|
97
399
|
markup: index === chunks.length - 1 ? options.markup : void 0
|
|
98
400
|
});
|
|
99
401
|
}
|
|
402
|
+
await sendAttachments();
|
|
100
403
|
return chunks.length;
|
|
101
404
|
};
|
|
102
405
|
|
|
406
|
+
// src/lib/runtimeSignalsLoader.ts
|
|
407
|
+
var import_redis3 = require("@tradejs/infra/redis");
|
|
408
|
+
|
|
409
|
+
// src/lib/runtimeSignalsStorage.ts
|
|
410
|
+
var import_constants2 = require("@tradejs/core/constants");
|
|
411
|
+
var import_time3 = require("@tradejs/core/time");
|
|
412
|
+
var isRuntimeSignalBucketRef = (value) => {
|
|
413
|
+
if (!value || typeof value !== "object") {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
const record = value;
|
|
417
|
+
return typeof record.signalId === "string" && typeof record.symbol === "string" && typeof record.strategy === "string" && typeof record.timestamp === "number";
|
|
418
|
+
};
|
|
419
|
+
var toCount = (value) => {
|
|
420
|
+
const parsed = Number.parseInt(String(value ?? "0"), 10);
|
|
421
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
422
|
+
};
|
|
423
|
+
var parseRuntimeSignalStatsBucket = (value) => {
|
|
424
|
+
const reasonGroups = /* @__PURE__ */ new Map();
|
|
425
|
+
for (const [field, rawCount] of Object.entries(value)) {
|
|
426
|
+
if (!field.startsWith("reason:")) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const [, source, ...reasonParts] = field.split(":");
|
|
430
|
+
const reason = reasonParts.join(":");
|
|
431
|
+
if (!source || !reason) {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const reasons = reasonGroups.get(source) ?? /* @__PURE__ */ new Map();
|
|
435
|
+
reasons.set(reason, toCount(rawCount));
|
|
436
|
+
reasonGroups.set(source, reasons);
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
evaluated: toCount(value.evaluated),
|
|
440
|
+
signals: toCount(value.signals),
|
|
441
|
+
reasonGroups
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// src/lib/runtimeSignalsLoader.ts
|
|
446
|
+
var isSignalRecord = (value) => {
|
|
447
|
+
if (!value || typeof value !== "object") {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
const record = value;
|
|
451
|
+
return typeof record.signalId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.timestamp === "number";
|
|
452
|
+
};
|
|
453
|
+
var sortByTimestamp = (records) => [...records].sort((left, right) => left.timestamp - right.timestamp);
|
|
454
|
+
var filterBucketKeysByWindow = (bucketKeys, prefix, startTime, endTime) => {
|
|
455
|
+
if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || startTime == null || endTime == null) {
|
|
456
|
+
return bucketKeys;
|
|
457
|
+
}
|
|
458
|
+
const allowedDayKeys = new Set((0, import_time3.getRuntimeStorageDayKeys)(startTime, endTime));
|
|
459
|
+
if (!allowedDayKeys.size) {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
return bucketKeys.filter((key) => {
|
|
463
|
+
const suffix = key.slice(prefix.length);
|
|
464
|
+
const firstColon = suffix.indexOf(":");
|
|
465
|
+
if (firstColon <= 0) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
return allowedDayKeys.has(suffix.slice(0, firstColon));
|
|
469
|
+
});
|
|
470
|
+
};
|
|
471
|
+
var loadRuntimeSignals = async (userName, {
|
|
472
|
+
startTime,
|
|
473
|
+
endTime
|
|
474
|
+
} = {}) => {
|
|
475
|
+
const bucketPrefix = import_redis3.redisKeys.runtimeSignalBuckets(userName);
|
|
476
|
+
const bucketKeys = filterBucketKeysByWindow(
|
|
477
|
+
await (0, import_redis3.getKeys)(bucketPrefix),
|
|
478
|
+
bucketPrefix,
|
|
479
|
+
startTime,
|
|
480
|
+
endTime
|
|
481
|
+
);
|
|
482
|
+
const refs = (await Promise.all(bucketKeys.map((key) => (0, import_redis3.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalBucketRef);
|
|
483
|
+
const seen = /* @__PURE__ */ new Set();
|
|
484
|
+
const payloads = await Promise.all(
|
|
485
|
+
refs.map(async (ref) => {
|
|
486
|
+
const dedupeKey = `${ref.symbol}:${ref.signalId}`;
|
|
487
|
+
if (seen.has(dedupeKey)) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
seen.add(dedupeKey);
|
|
491
|
+
const signal = await (0, import_redis3.getData)(
|
|
492
|
+
import_redis3.redisKeys.storeSignal(ref.symbol, ref.signalId),
|
|
493
|
+
null
|
|
494
|
+
);
|
|
495
|
+
return isSignalRecord(signal) ? signal : null;
|
|
496
|
+
})
|
|
497
|
+
);
|
|
498
|
+
return sortByTimestamp(payloads.filter(isSignalRecord));
|
|
499
|
+
};
|
|
500
|
+
var parseBucketKey = (prefix, key) => {
|
|
501
|
+
if (!key.startsWith(prefix)) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
const suffix = key.slice(prefix.length);
|
|
505
|
+
const firstColon = suffix.indexOf(":");
|
|
506
|
+
if (firstColon <= 0 || firstColon >= suffix.length - 1) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
dayKey: suffix.slice(0, firstColon),
|
|
511
|
+
strategy: suffix.slice(firstColon + 1)
|
|
512
|
+
};
|
|
513
|
+
};
|
|
514
|
+
var loadRuntimeSignalEvaluationStatsBuckets = async (userName) => {
|
|
515
|
+
const prefix = import_redis3.redisKeys.runtimeSignalEvaluationStatsBuckets(userName);
|
|
516
|
+
const keys = await (0, import_redis3.getKeys)(prefix);
|
|
517
|
+
const entries = await Promise.all(
|
|
518
|
+
keys.map(async (key) => {
|
|
519
|
+
const parsedKey = parseBucketKey(prefix, key);
|
|
520
|
+
if (!parsedKey) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
key,
|
|
525
|
+
...parsedKey,
|
|
526
|
+
stats: parseRuntimeSignalStatsBucket(await (0, import_redis3.getHashData)(key))
|
|
527
|
+
};
|
|
528
|
+
})
|
|
529
|
+
);
|
|
530
|
+
return entries.filter(Boolean);
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// src/lib/runtimeDebugEvidence.ts
|
|
534
|
+
var import_redis4 = require("@tradejs/infra/redis");
|
|
535
|
+
var RUNTIME_DEBUG_TIMEZONE = "Europe/Moscow";
|
|
536
|
+
var RUNTIME_DEBUG_TIMEZONE_LABEL = "MSK";
|
|
537
|
+
var formatRuntimeDebugDateTime = (timestamp) => new Intl.DateTimeFormat("ru-RU", {
|
|
538
|
+
timeZone: RUNTIME_DEBUG_TIMEZONE,
|
|
539
|
+
year: "numeric",
|
|
540
|
+
month: "2-digit",
|
|
541
|
+
day: "2-digit",
|
|
542
|
+
hour: "2-digit",
|
|
543
|
+
minute: "2-digit",
|
|
544
|
+
hour12: false
|
|
545
|
+
}).format(new Date(timestamp));
|
|
546
|
+
var getRuntimeDebugDateParts = (timestamp) => Object.fromEntries(
|
|
547
|
+
new Intl.DateTimeFormat("en-US", {
|
|
548
|
+
timeZone: RUNTIME_DEBUG_TIMEZONE,
|
|
549
|
+
year: "numeric",
|
|
550
|
+
month: "2-digit",
|
|
551
|
+
day: "2-digit"
|
|
552
|
+
}).formatToParts(new Date(timestamp)).map((part) => [part.type, part.value])
|
|
553
|
+
);
|
|
554
|
+
var formatRuntimeDebugIsoDayKey = (timestamp) => {
|
|
555
|
+
const parts = getRuntimeDebugDateParts(timestamp);
|
|
556
|
+
return `${parts.year}-${parts.month}-${parts.day}`;
|
|
557
|
+
};
|
|
558
|
+
var buildRuntimeDebugEvaluationId = ({
|
|
559
|
+
strategy,
|
|
560
|
+
symbol,
|
|
561
|
+
timestamp
|
|
562
|
+
}) => `${strategy}:${symbol}:${timestamp}`;
|
|
563
|
+
var buildRuntimeDebugReportPayload = async ({
|
|
564
|
+
userName,
|
|
565
|
+
startTime,
|
|
566
|
+
endTime,
|
|
567
|
+
signals,
|
|
568
|
+
evaluations,
|
|
569
|
+
trades,
|
|
570
|
+
strategyConfigs,
|
|
571
|
+
evaluationStatsBuckets
|
|
572
|
+
}) => {
|
|
573
|
+
const signalById = new Map(
|
|
574
|
+
signals.map((signal) => [signal.signalId, signal])
|
|
575
|
+
);
|
|
576
|
+
const evaluationById = new Map(
|
|
577
|
+
evaluations.map((evaluation) => [evaluation.evaluationId, evaluation])
|
|
578
|
+
);
|
|
579
|
+
const evaluationBySignalShape = new Map(
|
|
580
|
+
evaluations.map((evaluation) => [
|
|
581
|
+
buildRuntimeDebugEvaluationId(evaluation),
|
|
582
|
+
evaluation
|
|
583
|
+
])
|
|
584
|
+
);
|
|
585
|
+
const dayKeys = (0, import_time3.getRuntimeStorageDayKeys)(startTime, endTime);
|
|
586
|
+
const debugTrades = await Promise.all(
|
|
587
|
+
trades.map(async (trade) => {
|
|
588
|
+
const signal = trade.signalId != null ? signalById.get(trade.signalId) : void 0;
|
|
589
|
+
const evaluationId = buildRuntimeDebugEvaluationId({
|
|
590
|
+
strategy: trade.strategy,
|
|
591
|
+
symbol: trade.symbol,
|
|
592
|
+
timestamp: signal?.timestamp ?? trade.entryTimestamp
|
|
593
|
+
});
|
|
594
|
+
const knownEvaluation = evaluationById.get(evaluationId) ?? evaluationBySignalShape.get(evaluationId);
|
|
595
|
+
const tradeDayKey = (0, import_time3.getRuntimeStorageDayKey)(trade.entryTimestamp);
|
|
596
|
+
const signalDayKey = signal != null ? (0, import_time3.getRuntimeStorageDayKey)(signal.timestamp) : tradeDayKey;
|
|
597
|
+
const evaluationDayKey = knownEvaluation != null ? (0, import_time3.getRuntimeStorageDayKey)(knownEvaluation.timestamp) : signalDayKey;
|
|
598
|
+
const tradeKey = import_redis4.redisKeys.runtimeTrade(userName, trade.orderId);
|
|
599
|
+
const tradeBucketKey = import_redis4.redisKeys.runtimeTradeBucket(
|
|
600
|
+
userName,
|
|
601
|
+
tradeDayKey
|
|
602
|
+
);
|
|
603
|
+
const activeTradeKey = import_redis4.redisKeys.runtimeActiveTrade(
|
|
604
|
+
userName,
|
|
605
|
+
trade.symbol
|
|
606
|
+
);
|
|
607
|
+
const signalKey = trade.signalId != null ? import_redis4.redisKeys.storeSignal(trade.symbol, trade.signalId) : null;
|
|
608
|
+
const evaluationBucketKey = import_redis4.redisKeys.runtimeSignalEvaluationBucket(
|
|
609
|
+
userName,
|
|
610
|
+
evaluationDayKey,
|
|
611
|
+
knownEvaluation?.strategy ?? trade.strategy
|
|
612
|
+
);
|
|
613
|
+
const evaluationField = knownEvaluation?.evaluationId ?? evaluationId;
|
|
614
|
+
const evaluationKey = import_redis4.redisKeys.runtimeSignalEvaluation(
|
|
615
|
+
userName,
|
|
616
|
+
evaluationField
|
|
617
|
+
);
|
|
618
|
+
const [
|
|
619
|
+
tradeValue,
|
|
620
|
+
activeTradeValue,
|
|
621
|
+
signalValue,
|
|
622
|
+
directEvaluationValue,
|
|
623
|
+
bucketEvaluationValue
|
|
624
|
+
] = await Promise.all([
|
|
625
|
+
(0, import_redis4.getData)(tradeKey, null),
|
|
626
|
+
(0, import_redis4.getData)(activeTradeKey, null),
|
|
627
|
+
signalKey ? (0, import_redis4.getData)(signalKey, null) : Promise.resolve(null),
|
|
628
|
+
(0, import_redis4.getData)(evaluationKey, null),
|
|
629
|
+
knownEvaluation ? Promise.resolve(knownEvaluation) : (0, import_redis4.getHashJsonField)(
|
|
630
|
+
evaluationBucketKey,
|
|
631
|
+
evaluationField,
|
|
632
|
+
null
|
|
633
|
+
)
|
|
634
|
+
]);
|
|
635
|
+
const evaluationValue = directEvaluationValue ?? bucketEvaluationValue ?? knownEvaluation ?? null;
|
|
636
|
+
return {
|
|
637
|
+
redisDebug: {
|
|
638
|
+
trade: tradeKey,
|
|
639
|
+
tradeBucket: {
|
|
640
|
+
key: tradeBucketKey,
|
|
641
|
+
field: trade.orderId
|
|
642
|
+
},
|
|
643
|
+
activeTrade: activeTradeKey,
|
|
644
|
+
signal: signalKey,
|
|
645
|
+
evaluation: {
|
|
646
|
+
key: evaluationBucketKey,
|
|
647
|
+
field: evaluationField,
|
|
648
|
+
directKey: evaluationKey
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
trade,
|
|
652
|
+
redisValues: {
|
|
653
|
+
trade: tradeValue,
|
|
654
|
+
tradeBucket: trade,
|
|
655
|
+
activeTrade: activeTradeValue,
|
|
656
|
+
signal: signalValue ?? signal ?? null,
|
|
657
|
+
evaluation: evaluationValue ?? knownEvaluation ?? null
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
})
|
|
661
|
+
);
|
|
662
|
+
const linkedEvaluations = /* @__PURE__ */ new Map();
|
|
663
|
+
for (const trade of debugTrades) {
|
|
664
|
+
const evaluation = trade.redisValues.evaluation;
|
|
665
|
+
if (evaluation && typeof evaluation === "object" && "evaluationId" in evaluation && typeof evaluation.evaluationId === "string") {
|
|
666
|
+
linkedEvaluations.set(
|
|
667
|
+
evaluation.evaluationId,
|
|
668
|
+
evaluation
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const reportEvaluations = evaluations.length > 0 ? evaluations : [...linkedEvaluations.values()];
|
|
673
|
+
return {
|
|
674
|
+
reportType: "runtime-daily-debug",
|
|
675
|
+
generatedAt: Date.now(),
|
|
676
|
+
generatedAtMsk: formatRuntimeDebugDateTime(Date.now()),
|
|
677
|
+
userName,
|
|
678
|
+
window: {
|
|
679
|
+
startTime,
|
|
680
|
+
endTime,
|
|
681
|
+
startMsk: formatRuntimeDebugDateTime(startTime),
|
|
682
|
+
endMsk: formatRuntimeDebugDateTime(endTime),
|
|
683
|
+
dayKeys
|
|
684
|
+
},
|
|
685
|
+
redisPrefixes: {
|
|
686
|
+
runtimeTrades: import_redis4.redisKeys.runtimeTrades(userName),
|
|
687
|
+
runtimeTradeBuckets: import_redis4.redisKeys.runtimeTradeBuckets(userName),
|
|
688
|
+
runtimeActiveTrades: import_redis4.redisKeys.runtimeActiveTrades(userName),
|
|
689
|
+
runtimeSignals: import_redis4.redisKeys.runtimeSignalBuckets(userName),
|
|
690
|
+
runtimeSignalEvaluations: import_redis4.redisKeys.runtimeSignalEvaluationBuckets(userName),
|
|
691
|
+
runtimeSignalEvaluationStats: import_redis4.redisKeys.runtimeSignalEvaluationStatsBuckets(userName)
|
|
692
|
+
},
|
|
693
|
+
counts: {
|
|
694
|
+
trades: trades.length,
|
|
695
|
+
signals: signals.length,
|
|
696
|
+
evaluations: reportEvaluations.length,
|
|
697
|
+
evaluationStatsBuckets: evaluationStatsBuckets?.length ?? 0,
|
|
698
|
+
strategyConfigs: strategyConfigs?.length ?? 0
|
|
699
|
+
},
|
|
700
|
+
trades: debugTrades,
|
|
701
|
+
signals: signals.map((signal) => ({
|
|
702
|
+
redisKey: import_redis4.redisKeys.storeSignal(signal.symbol, signal.signalId),
|
|
703
|
+
signal
|
|
704
|
+
})),
|
|
705
|
+
evaluations: reportEvaluations.map((evaluation) => ({
|
|
706
|
+
redisDebug: {
|
|
707
|
+
key: import_redis4.redisKeys.runtimeSignalEvaluationBucket(
|
|
708
|
+
userName,
|
|
709
|
+
(0, import_time3.getRuntimeStorageDayKey)(evaluation.timestamp),
|
|
710
|
+
evaluation.strategy
|
|
711
|
+
),
|
|
712
|
+
field: evaluation.evaluationId,
|
|
713
|
+
directKey: import_redis4.redisKeys.runtimeSignalEvaluation(
|
|
714
|
+
userName,
|
|
715
|
+
evaluation.evaluationId
|
|
716
|
+
)
|
|
717
|
+
},
|
|
718
|
+
evaluation
|
|
719
|
+
})),
|
|
720
|
+
evaluationStatsBuckets: evaluationStatsBuckets ?? [],
|
|
721
|
+
strategyConfigs: strategyConfigs ?? []
|
|
722
|
+
};
|
|
723
|
+
};
|
|
724
|
+
var buildRuntimeDebugReportAttachment = async (params) => {
|
|
725
|
+
const payload = await buildRuntimeDebugReportPayload(params);
|
|
726
|
+
const dayKey = formatRuntimeDebugIsoDayKey(params.endTime);
|
|
727
|
+
return {
|
|
728
|
+
filename: `tradejs-runtime-debug-${params.userName}-${dayKey}.json`,
|
|
729
|
+
content: JSON.stringify(payload, null, 2),
|
|
730
|
+
caption: `Runtime debug ${dayKey} ${RUNTIME_DEBUG_TIMEZONE_LABEL}`,
|
|
731
|
+
summary: {
|
|
732
|
+
trades: payload.counts.trades,
|
|
733
|
+
signals: payload.counts.signals,
|
|
734
|
+
evaluations: payload.counts.evaluations
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
};
|
|
738
|
+
|
|
103
739
|
// src/scripts/signalsSummary.ts
|
|
104
740
|
import_args.default.option(["u", "user"], "Use user config", "root");
|
|
105
741
|
import_args.default.option(
|
|
@@ -109,6 +745,11 @@ import_args.default.option(
|
|
|
109
745
|
);
|
|
110
746
|
import_args.default.option(["H", "hours"], "Summary window in hours", 24);
|
|
111
747
|
import_args.default.option(["P", "printOnly"], "Print summary instead of Telegram", false);
|
|
748
|
+
import_args.default.option(
|
|
749
|
+
"debugAttachment",
|
|
750
|
+
"Attach runtime Redis debug JSON for replay diagnostics",
|
|
751
|
+
true
|
|
752
|
+
);
|
|
112
753
|
var flags = import_args.default.parse(process.argv);
|
|
113
754
|
var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
|
|
114
755
|
var SUMMARY_TIMEZONE = "Europe/Moscow";
|
|
@@ -124,6 +765,8 @@ var formatMskDateTime = (timestamp) => new Intl.DateTimeFormat("ru-RU", {
|
|
|
124
765
|
hour12: false
|
|
125
766
|
}).format(new Date(timestamp));
|
|
126
767
|
var formatSigned = (value) => `${value >= 0 ? "+" : ""}${value.toFixed(2)}`;
|
|
768
|
+
var formatPnlMoneyText = (value, knownCount) => knownCount > 0 ? `${formatSigned(value)}$` : "n/a";
|
|
769
|
+
var formatWinRateText = (wins, closedKnown) => closedKnown > 0 ? `${(wins / closedKnown * 100).toFixed(2)}% (${wins}/${closedKnown})` : "n/a";
|
|
127
770
|
var resolveSummaryTitle = (hours) => {
|
|
128
771
|
if (hours === 24) {
|
|
129
772
|
return "TradeJS daily summary";
|
|
@@ -131,6 +774,9 @@ var resolveSummaryTitle = (hours) => {
|
|
|
131
774
|
if (hours === 168) {
|
|
132
775
|
return "TradeJS weekly summary";
|
|
133
776
|
}
|
|
777
|
+
if (hours === 720) {
|
|
778
|
+
return "TradeJS monthly summary";
|
|
779
|
+
}
|
|
134
780
|
return `TradeJS ${hours}h summary`;
|
|
135
781
|
};
|
|
136
782
|
var normalizeStatus = (value) => {
|
|
@@ -139,57 +785,57 @@ var normalizeStatus = (value) => {
|
|
|
139
785
|
}
|
|
140
786
|
return "unknown";
|
|
141
787
|
};
|
|
142
|
-
var
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
source = "hook";
|
|
164
|
-
normalizedReason = "BEFORE_ENTRY_GATE";
|
|
165
|
-
} else if (reason === "MAKE_ORDERS_DISABLED" || reason === "ENTRY_POLICY_BLOCKED") {
|
|
166
|
-
source = "policy";
|
|
167
|
-
}
|
|
788
|
+
var shouldAttachDebugReport = (value) => value !== false && String(value).toLowerCase() !== "false";
|
|
789
|
+
var getSummaryDateParts = (timestamp) => Object.fromEntries(
|
|
790
|
+
new Intl.DateTimeFormat("en-US", {
|
|
791
|
+
timeZone: SUMMARY_TIMEZONE,
|
|
792
|
+
year: "numeric",
|
|
793
|
+
month: "2-digit",
|
|
794
|
+
day: "2-digit"
|
|
795
|
+
}).formatToParts(new Date(timestamp)).map((part) => [part.type, part.value])
|
|
796
|
+
);
|
|
797
|
+
var formatSummaryIsoDayKey = (timestamp) => {
|
|
798
|
+
const parts = getSummaryDateParts(timestamp);
|
|
799
|
+
return `${parts.year}-${parts.month}-${parts.day}`;
|
|
800
|
+
};
|
|
801
|
+
var unescapeHtml = (value) => value.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
802
|
+
var stripTelegramHtml = (value) => unescapeHtml(value.replace(/<\/?(?:b|code)>/g, ""));
|
|
803
|
+
var buildSignalsSummaryAttachment = ({
|
|
804
|
+
userName,
|
|
805
|
+
endTime,
|
|
806
|
+
content
|
|
807
|
+
}) => {
|
|
808
|
+
const dayKey = formatSummaryIsoDayKey(endTime);
|
|
168
809
|
return {
|
|
169
|
-
|
|
170
|
-
|
|
810
|
+
filename: `tradejs-signals-summary-${userName}-${dayKey}.txt`,
|
|
811
|
+
content: stripTelegramHtml(content),
|
|
812
|
+
caption: `Signals summary ${dayKey} ${SUMMARY_TIMEZONE_LABEL}`
|
|
171
813
|
};
|
|
172
814
|
};
|
|
173
|
-
var
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
815
|
+
var appendReportAttachmentSummary = ({
|
|
816
|
+
message,
|
|
817
|
+
signalsFilename,
|
|
818
|
+
filename,
|
|
819
|
+
tradesCount,
|
|
820
|
+
signalsCount,
|
|
821
|
+
evaluationsCount
|
|
822
|
+
}) => {
|
|
823
|
+
const lines = [
|
|
824
|
+
message,
|
|
825
|
+
"",
|
|
826
|
+
"\u{1F4CE} <b>Signal report file</b>",
|
|
827
|
+
`File: <code>${escapeHtml(signalsFilename)}</code>`
|
|
828
|
+
];
|
|
829
|
+
if (filename) {
|
|
830
|
+
lines.push(
|
|
831
|
+
"",
|
|
832
|
+
"\u{1F4CE} <b>Replay debug file</b>",
|
|
833
|
+
`File: <code>${escapeHtml(filename)}</code>`,
|
|
834
|
+
`Inside: trades=<b>${tradesCount ?? 0}</b>, signals=<b>${signalsCount ?? 0}</b>, evaluations=<b>${evaluationsCount ?? 0}</b>`,
|
|
835
|
+
"Redis refs: <code>trade</code>, <code>tradeBucket</code>, <code>activeTrade</code>, <code>signal</code>, <code>evaluation</code>"
|
|
836
|
+
);
|
|
190
837
|
}
|
|
191
|
-
|
|
192
|
-
return typeof record.orderId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.entryTimestamp === "number" && typeof record.entryPrice === "number" && typeof record.qty === "number";
|
|
838
|
+
return lines.join("\n");
|
|
193
839
|
};
|
|
194
840
|
var resolveSummaryConnectorName = async (value) => {
|
|
195
841
|
const connectorName = await (0, import_connectors.resolveConnectorName)(value, projectRoot);
|
|
@@ -203,168 +849,75 @@ var resolveSummaryConnectorName = async (value) => {
|
|
|
203
849
|
);
|
|
204
850
|
return import_connectors.DEFAULT_CONNECTOR_NAME;
|
|
205
851
|
};
|
|
206
|
-
var
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
};
|
|
211
|
-
var
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
const [users, keyUserName, strategiesKey, strategyName, configKey] = parts;
|
|
222
|
-
if (users !== "users" || keyUserName !== userName || strategiesKey !== "strategies" || configKey !== "config" || !strategyName) {
|
|
223
|
-
return null;
|
|
852
|
+
var createRuntimeSignalStats = () => ({
|
|
853
|
+
evaluated: 0,
|
|
854
|
+
signals: 0,
|
|
855
|
+
reasonGroups: /* @__PURE__ */ new Map()
|
|
856
|
+
});
|
|
857
|
+
var mergeRuntimeSignalStats = (target, source) => {
|
|
858
|
+
target.evaluated += source.evaluated;
|
|
859
|
+
target.signals += source.signals;
|
|
860
|
+
for (const [group, reasons] of source.reasonGroups.entries()) {
|
|
861
|
+
const targetReasons = target.reasonGroups.get(group) ?? /* @__PURE__ */ new Map();
|
|
862
|
+
for (const [reason, count] of reasons.entries()) {
|
|
863
|
+
targetReasons.set(reason, (targetReasons.get(reason) ?? 0) + count);
|
|
864
|
+
}
|
|
865
|
+
target.reasonGroups.set(group, targetReasons);
|
|
224
866
|
}
|
|
225
|
-
return strategyName;
|
|
226
|
-
};
|
|
227
|
-
var loadRuntimeStrategyNames = async (userName) => {
|
|
228
|
-
const keys = await (0, import_redis.getKeys)(`${import_redis.redisKeys.strategies(userName)}:`);
|
|
229
|
-
return keys.filter((key) => key.endsWith(":config")).map((key) => resolveStrategyNameByConfigKey(userName, key)).filter((value) => Boolean(value)).sort((left, right) => left.localeCompare(right));
|
|
230
|
-
};
|
|
231
|
-
var loadRuntimeTrades = async (userName) => {
|
|
232
|
-
const keys = await (0, import_redis.getKeys)(import_redis.redisKeys.runtimeTrades(userName));
|
|
233
|
-
const trades = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
|
|
234
|
-
return trades.filter(isRuntimeTradeRecord).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
|
|
235
867
|
};
|
|
236
|
-
var
|
|
237
|
-
|
|
868
|
+
var buildSummaryPrelude = ({
|
|
869
|
+
hours,
|
|
238
870
|
startTime,
|
|
239
|
-
endTime
|
|
871
|
+
endTime,
|
|
872
|
+
realizedWindowPnlText,
|
|
873
|
+
winRateText,
|
|
874
|
+
longCount,
|
|
875
|
+
shortCount,
|
|
876
|
+
openCount,
|
|
877
|
+
openPnlText
|
|
240
878
|
}) => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
error?.message || String(error)
|
|
255
|
-
);
|
|
256
|
-
return [];
|
|
257
|
-
}
|
|
879
|
+
const windowPnlLabel = hours === 24 ? "24h Realized PnL" : `${hours}h Realized PnL`;
|
|
880
|
+
return [
|
|
881
|
+
`\u{1F4CB} <b>${escapeHtml(resolveSummaryTitle(hours))}</b>`,
|
|
882
|
+
"",
|
|
883
|
+
"\u{1F552} <b>Window</b>",
|
|
884
|
+
`<b>${escapeHtml(formatMskDateTime(startTime))} - ${escapeHtml(formatMskDateTime(endTime))} ${SUMMARY_TIMEZONE_LABEL}</b>`,
|
|
885
|
+
"",
|
|
886
|
+
`\u23F1 Range: <b>${hours}h</b>`,
|
|
887
|
+
`\u{1F4B0} <b>${windowPnlLabel}:</b> <b>${escapeHtml(realizedWindowPnlText)}</b>`,
|
|
888
|
+
`\u{1F3C6} <b>WinRate:</b> <b>${escapeHtml(winRateText)}</b>`,
|
|
889
|
+
`\u2197\uFE0F <b>LONG:</b> <b>${longCount}</b>, \u2198\uFE0F <b>SHORT:</b> <b>${shortCount}</b>`,
|
|
890
|
+
`\u{1F4CD} <b>Open positions:</b> <b>${openCount}</b>, Unrealized PnL: <b>${escapeHtml(openPnlText)}</b>`
|
|
891
|
+
];
|
|
258
892
|
};
|
|
259
|
-
var
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return null;
|
|
893
|
+
var resolveTradeStatusEmoji = (status, pnl) => {
|
|
894
|
+
if (status === "active") {
|
|
895
|
+
return typeof pnl === "number" && Number.isFinite(pnl) && pnl < 0 ? "\u{1F534}" : "\u{1F7E2}";
|
|
263
896
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
(row2) => Number.isFinite(row2.closedAt) && row2.closedAt >= minimumClosedAt
|
|
267
|
-
);
|
|
268
|
-
if (matchIndex < 0) {
|
|
269
|
-
return null;
|
|
897
|
+
if (status === "closed") {
|
|
898
|
+
return typeof pnl === "number" && Number.isFinite(pnl) && pnl < 0 ? "\u274C" : "\u2705";
|
|
270
899
|
}
|
|
271
|
-
|
|
272
|
-
return row ?? null;
|
|
900
|
+
return "\u2754";
|
|
273
901
|
};
|
|
274
|
-
var
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
startTime,
|
|
279
|
-
endTime
|
|
280
|
-
}) => {
|
|
281
|
-
const openPositions = typeof connector.getOpenPositionPnl === "function" ? await connector.getOpenPositionPnl() : [];
|
|
282
|
-
const openPositionsBySymbol = new Map(
|
|
283
|
-
openPositions.map((position) => [position.symbol, position])
|
|
284
|
-
);
|
|
285
|
-
const activeOrderIdBySymbol = /* @__PURE__ */ new Map();
|
|
286
|
-
const symbols = [...new Set(trades.map((trade) => trade.symbol))];
|
|
287
|
-
await Promise.all(
|
|
288
|
-
symbols.map(async (symbol) => {
|
|
289
|
-
const activeRef = await (0, import_redis.getData)(
|
|
290
|
-
import_redis.redisKeys.runtimeActiveTrade(userName, symbol),
|
|
291
|
-
null
|
|
292
|
-
);
|
|
293
|
-
activeOrderIdBySymbol.set(
|
|
294
|
-
symbol,
|
|
295
|
-
typeof activeRef?.orderId === "string" ? activeRef.orderId : null
|
|
296
|
-
);
|
|
297
|
-
})
|
|
298
|
-
);
|
|
299
|
-
const closedPnlRows = await loadClosedPnlRows({
|
|
300
|
-
connector,
|
|
301
|
-
startTime,
|
|
302
|
-
endTime
|
|
303
|
-
});
|
|
304
|
-
const closedPnlBuckets = /* @__PURE__ */ new Map();
|
|
305
|
-
for (const row of closedPnlRows) {
|
|
306
|
-
const bucket = closedPnlBuckets.get(row.symbol) ?? [];
|
|
307
|
-
bucket.push(row);
|
|
308
|
-
closedPnlBuckets.set(row.symbol, bucket);
|
|
309
|
-
}
|
|
310
|
-
const syncedTrades = [];
|
|
311
|
-
for (const trade of trades) {
|
|
312
|
-
if (trade.status !== "active") {
|
|
313
|
-
syncedTrades.push(trade);
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
const openPosition = openPositionsBySymbol.get(trade.symbol);
|
|
317
|
-
const activeOrderId = activeOrderIdBySymbol.get(trade.symbol);
|
|
318
|
-
const isCurrentActiveTrade = activeOrderId === trade.orderId;
|
|
319
|
-
if (isCurrentActiveTrade && openPosition && openPosition.direction === trade.direction) {
|
|
320
|
-
const nextTrade2 = {
|
|
321
|
-
...trade,
|
|
322
|
-
status: "active",
|
|
323
|
-
currentPrice: openPosition.currentPrice,
|
|
324
|
-
currentPnl: openPosition.unrealizedPnl,
|
|
325
|
-
lastSyncedAt: endTime
|
|
326
|
-
};
|
|
327
|
-
await (0, import_redis.setData)(
|
|
328
|
-
import_redis.redisKeys.runtimeTrade(userName, trade.orderId),
|
|
329
|
-
nextTrade2,
|
|
330
|
-
{
|
|
331
|
-
expire: 0
|
|
332
|
-
}
|
|
333
|
-
);
|
|
334
|
-
syncedTrades.push(nextTrade2);
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
const matchedClosedPnl = consumeClosedPnlMatch(closedPnlBuckets, trade);
|
|
338
|
-
const nextTrade = {
|
|
339
|
-
...trade,
|
|
340
|
-
status: "closed",
|
|
341
|
-
currentPrice: matchedClosedPnl?.exitPrice ?? trade.currentPrice ?? null,
|
|
342
|
-
currentPnl: matchedClosedPnl?.closedPnl ?? trade.closedPnl ?? trade.currentPnl ?? null,
|
|
343
|
-
closedPnl: matchedClosedPnl?.closedPnl ?? trade.closedPnl ?? trade.currentPnl ?? null,
|
|
344
|
-
exitPrice: matchedClosedPnl?.exitPrice ?? trade.exitPrice ?? null,
|
|
345
|
-
exitTimestamp: matchedClosedPnl?.closedAt ?? trade.exitTimestamp ?? endTime,
|
|
346
|
-
lastSyncedAt: endTime
|
|
347
|
-
};
|
|
348
|
-
await Promise.all([
|
|
349
|
-
(0, import_redis.setData)(import_redis.redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
|
|
350
|
-
expire: import_constants.TTL_1M
|
|
351
|
-
}),
|
|
352
|
-
...isCurrentActiveTrade ? [(0, import_redis.delKey)(import_redis.redisKeys.runtimeActiveTrade(userName, trade.symbol))] : []
|
|
353
|
-
]);
|
|
354
|
-
syncedTrades.push(nextTrade);
|
|
902
|
+
var isTimestampInWindow = (timestamp, startTime, endTime) => typeof timestamp === "number" && Number.isFinite(timestamp) && timestamp >= startTime && timestamp < endTime;
|
|
903
|
+
var isRuntimeTradeClosedInWindow = (trade, startTime, endTime) => {
|
|
904
|
+
if (trade.status !== "closed" || !isTimestampInWindow(trade.exitTimestamp, startTime, endTime)) {
|
|
905
|
+
return false;
|
|
355
906
|
}
|
|
356
|
-
|
|
907
|
+
const looksLikeSyncFallbackClose = trade.exitTimestamp === trade.lastSyncedAt && trade.exitPrice == null && trade.actualExitPrice == null && trade.closeFee == null && trade.fundingFee == null && trade.totalFee == null;
|
|
908
|
+
return !looksLikeSyncFallbackClose;
|
|
357
909
|
};
|
|
358
|
-
var
|
|
910
|
+
var buildSummaryMessages = ({
|
|
359
911
|
hours,
|
|
360
912
|
startTime,
|
|
361
913
|
endTime,
|
|
362
914
|
configuredStrategyNames,
|
|
363
915
|
signals,
|
|
364
|
-
|
|
916
|
+
evaluationStatsByStrategy,
|
|
365
917
|
trades
|
|
366
918
|
}) => {
|
|
367
|
-
const
|
|
919
|
+
const signalLines = [];
|
|
920
|
+
const tradeLines = [];
|
|
368
921
|
const statusOrder = [
|
|
369
922
|
"completed",
|
|
370
923
|
"skipped",
|
|
@@ -374,7 +927,6 @@ var buildSummaryMessage = ({
|
|
|
374
927
|
];
|
|
375
928
|
const strategyNames = /* @__PURE__ */ new Set();
|
|
376
929
|
const signalStats = /* @__PURE__ */ new Map();
|
|
377
|
-
const evaluationStats = /* @__PURE__ */ new Map();
|
|
378
930
|
const tradeStats = /* @__PURE__ */ new Map();
|
|
379
931
|
for (const strategyName of configuredStrategyNames) {
|
|
380
932
|
if (strategyName) {
|
|
@@ -388,75 +940,49 @@ var buildSummaryMessage = ({
|
|
|
388
940
|
stats.set(status, (stats.get(status) ?? 0) + 1);
|
|
389
941
|
signalStats.set(signal.strategy, stats);
|
|
390
942
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (evaluation.status === "signal") {
|
|
400
|
-
stats.signals += 1;
|
|
401
|
-
}
|
|
402
|
-
const orderStatus = normalizeStatus(evaluation.orderStatus);
|
|
403
|
-
let skipReason = null;
|
|
404
|
-
let fallbackSource = "core";
|
|
405
|
-
if (evaluation.status === "skip") {
|
|
406
|
-
skipReason = evaluation.reason || "NO_SIGNAL";
|
|
407
|
-
fallbackSource = "core";
|
|
408
|
-
} else if (evaluation.status === "signal" && (orderStatus === "skipped" || orderStatus === "canceled")) {
|
|
409
|
-
skipReason = evaluation.orderSkipReason || evaluation.reason || `orderStatus=${orderStatus}`;
|
|
410
|
-
fallbackSource = "runtime";
|
|
411
|
-
} else if (evaluation.status === "error") {
|
|
412
|
-
skipReason = evaluation.reason || "EVALUATION_ERROR";
|
|
413
|
-
fallbackSource = "runtime";
|
|
414
|
-
}
|
|
415
|
-
if (skipReason) {
|
|
416
|
-
const normalized = normalizeSkipStatsReason(skipReason, fallbackSource);
|
|
417
|
-
const reasons = stats.reasonGroups.get(normalized.source) ?? /* @__PURE__ */ new Map();
|
|
418
|
-
reasons.set(normalized.reason, (reasons.get(normalized.reason) ?? 0) + 1);
|
|
419
|
-
stats.reasonGroups.set(normalized.source, reasons);
|
|
420
|
-
}
|
|
421
|
-
evaluationStats.set(evaluation.strategy, stats);
|
|
422
|
-
}
|
|
423
|
-
for (const trade of trades) {
|
|
943
|
+
const realizedTrades = trades.filter(
|
|
944
|
+
(trade) => isRuntimeTradeClosedInWindow(trade, startTime, endTime)
|
|
945
|
+
);
|
|
946
|
+
const openTrades = trades.filter(
|
|
947
|
+
(trade) => trade.status === "active" && typeof trade.entryTimestamp === "number" && Number.isFinite(trade.entryTimestamp) && trade.entryTimestamp < endTime
|
|
948
|
+
);
|
|
949
|
+
const reportTrades = [...realizedTrades, ...openTrades];
|
|
950
|
+
for (const trade of reportTrades) {
|
|
424
951
|
strategyNames.add(trade.strategy);
|
|
425
952
|
const stats = tradeStats.get(trade.strategy) ?? {
|
|
426
|
-
total: 0,
|
|
427
953
|
active: 0,
|
|
428
954
|
closed: 0,
|
|
429
955
|
activePnl: 0,
|
|
430
956
|
activePnlKnown: 0,
|
|
431
957
|
closedPnl: 0,
|
|
432
958
|
closedPnlKnown: 0,
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
959
|
+
trades: [],
|
|
960
|
+
closedWins: 0,
|
|
961
|
+
closedKnown: 0
|
|
436
962
|
};
|
|
437
963
|
const pnl = trade.status === "active" ? trade.currentPnl : trade.closedPnl ?? trade.currentPnl;
|
|
438
|
-
const pnlText = typeof pnl === "number" && Number.isFinite(pnl) ? formatSigned(pnl) : "n/a";
|
|
439
|
-
stats.total += 1;
|
|
964
|
+
const pnlText = typeof pnl === "number" && Number.isFinite(pnl) ? `${formatSigned(pnl)}$` : "n/a";
|
|
440
965
|
if (trade.status === "active") {
|
|
441
966
|
stats.active += 1;
|
|
442
967
|
if (typeof pnl === "number" && Number.isFinite(pnl)) {
|
|
443
968
|
stats.activePnl += pnl;
|
|
444
969
|
stats.activePnlKnown += 1;
|
|
445
|
-
stats.totalPnl += pnl;
|
|
446
|
-
stats.totalPnlKnown += 1;
|
|
447
970
|
}
|
|
448
971
|
} else {
|
|
449
972
|
stats.closed += 1;
|
|
450
973
|
if (typeof pnl === "number" && Number.isFinite(pnl)) {
|
|
451
974
|
stats.closedPnl += pnl;
|
|
452
975
|
stats.closedPnlKnown += 1;
|
|
453
|
-
stats.
|
|
454
|
-
|
|
976
|
+
stats.closedKnown += 1;
|
|
977
|
+
if (pnl > 0) {
|
|
978
|
+
stats.closedWins += 1;
|
|
979
|
+
}
|
|
455
980
|
}
|
|
456
981
|
}
|
|
457
982
|
stats.trades.push({
|
|
458
983
|
symbol: trade.symbol,
|
|
459
984
|
status: trade.status,
|
|
985
|
+
pnl: typeof pnl === "number" && Number.isFinite(pnl) ? pnl : null,
|
|
460
986
|
pnlText,
|
|
461
987
|
entryTimestamp: trade.entryTimestamp,
|
|
462
988
|
orderId: trade.orderId
|
|
@@ -466,39 +992,70 @@ var buildSummaryMessage = ({
|
|
|
466
992
|
const sortedStrategies = [...strategyNames].sort(
|
|
467
993
|
(left, right) => left.localeCompare(right)
|
|
468
994
|
);
|
|
469
|
-
const
|
|
470
|
-
(sum, stats) => sum + stats.
|
|
995
|
+
const realizedWindowPnl = [...tradeStats.values()].reduce(
|
|
996
|
+
(sum, stats) => sum + stats.closedPnl,
|
|
997
|
+
0
|
|
998
|
+
);
|
|
999
|
+
const realizedWindowPnlKnown = [...tradeStats.values()].reduce(
|
|
1000
|
+
(sum, stats) => sum + stats.closedPnlKnown,
|
|
1001
|
+
0
|
|
1002
|
+
);
|
|
1003
|
+
const realizedWindowPnlText = realizedWindowPnlKnown > 0 ? formatSigned(realizedWindowPnl) : "n/a";
|
|
1004
|
+
const longCount = realizedTrades.filter(
|
|
1005
|
+
(trade) => trade.direction === "LONG"
|
|
1006
|
+
).length;
|
|
1007
|
+
const shortCount = realizedTrades.filter(
|
|
1008
|
+
(trade) => trade.direction === "SHORT"
|
|
1009
|
+
).length;
|
|
1010
|
+
const openPnl = [...tradeStats.values()].reduce(
|
|
1011
|
+
(sum, stats) => sum + stats.activePnl,
|
|
471
1012
|
0
|
|
472
1013
|
);
|
|
473
|
-
const
|
|
474
|
-
(sum, stats) => sum + stats.
|
|
1014
|
+
const openPnlKnown = [...tradeStats.values()].reduce(
|
|
1015
|
+
(sum, stats) => sum + stats.activePnlKnown,
|
|
475
1016
|
0
|
|
476
1017
|
);
|
|
477
|
-
const
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
lines.push("\u{1F552} <b>Window</b>");
|
|
482
|
-
lines.push(
|
|
483
|
-
`<b>${escapeHtml(formatMskDateTime(startTime))} - ${escapeHtml(formatMskDateTime(endTime))} ${SUMMARY_TIMEZONE_LABEL}</b>`
|
|
1018
|
+
const openPnlText = openTrades.length > 0 ? formatPnlMoneyText(openPnl, openPnlKnown) : "n/a";
|
|
1019
|
+
const closedWins = [...tradeStats.values()].reduce(
|
|
1020
|
+
(sum, stats) => sum + stats.closedWins,
|
|
1021
|
+
0
|
|
484
1022
|
);
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
`\u{1F4B0} <b>${windowPnlLabel}:</b> <b>${escapeHtml(totalWindowPnlText)}</b>`
|
|
1023
|
+
const closedKnown = [...tradeStats.values()].reduce(
|
|
1024
|
+
(sum, stats) => sum + stats.closedKnown,
|
|
1025
|
+
0
|
|
489
1026
|
);
|
|
490
|
-
|
|
491
|
-
|
|
1027
|
+
const winRateText = formatWinRateText(closedWins, closedKnown);
|
|
1028
|
+
const prelude = buildSummaryPrelude({
|
|
1029
|
+
hours,
|
|
1030
|
+
startTime,
|
|
1031
|
+
endTime,
|
|
1032
|
+
realizedWindowPnlText,
|
|
1033
|
+
winRateText,
|
|
1034
|
+
longCount,
|
|
1035
|
+
shortCount,
|
|
1036
|
+
openCount: openTrades.length,
|
|
1037
|
+
openPnlText
|
|
1038
|
+
});
|
|
1039
|
+
signalLines.push(...prelude);
|
|
1040
|
+
signalLines.push("");
|
|
1041
|
+
signalLines.push("\u{1F4E1} <b>Signals</b>");
|
|
1042
|
+
tradeLines.push(...prelude);
|
|
1043
|
+
tradeLines.push("");
|
|
1044
|
+
tradeLines.push("\u{1F4BC} <b>Closed Trades</b>");
|
|
492
1045
|
if (!sortedStrategies.length) {
|
|
493
|
-
|
|
494
|
-
|
|
1046
|
+
signalLines.push("\u26A0\uFE0F No runtime data for this window.");
|
|
1047
|
+
tradeLines.push("\u26A0\uFE0F No runtime data for this window.");
|
|
1048
|
+
return {
|
|
1049
|
+
signalsMessage: signalLines.join("\n"),
|
|
1050
|
+
tradesMessage: tradeLines.join("\n")
|
|
1051
|
+
};
|
|
495
1052
|
}
|
|
496
1053
|
const appendEvaluationDetails = (strategyName) => {
|
|
497
|
-
const evaluation =
|
|
1054
|
+
const evaluation = evaluationStatsByStrategy.get(strategyName);
|
|
498
1055
|
if (!evaluation) {
|
|
499
1056
|
return;
|
|
500
1057
|
}
|
|
501
|
-
|
|
1058
|
+
signalLines.push(
|
|
502
1059
|
`evaluated=<b>${evaluation.evaluated}</b>, signals=<b>${evaluation.signals}</b>`
|
|
503
1060
|
);
|
|
504
1061
|
const sourceOrder = ["skip from core", "skip from AI", "skip from ML"];
|
|
@@ -512,21 +1069,21 @@ var buildSummaryMessage = ({
|
|
|
512
1069
|
}
|
|
513
1070
|
);
|
|
514
1071
|
for (const [source, reasons] of sortedReasonGroups) {
|
|
515
|
-
|
|
1072
|
+
signalLines.push(`<b>${escapeHtml(source)}</b>:`);
|
|
516
1073
|
const sortedReasons = [...reasons.entries()].sort(
|
|
517
1074
|
(left, right) => right[1] - left[1] || left[0].localeCompare(right[0])
|
|
518
1075
|
);
|
|
519
1076
|
for (const [reason, count] of sortedReasons) {
|
|
520
|
-
|
|
1077
|
+
signalLines.push(`${escapeHtml(reason)}: <b>${count}</b>`);
|
|
521
1078
|
}
|
|
522
1079
|
}
|
|
523
1080
|
};
|
|
524
1081
|
for (const strategyName of sortedStrategies) {
|
|
525
1082
|
const stats = signalStats.get(strategyName);
|
|
526
1083
|
if (!stats || stats.size === 0) {
|
|
527
|
-
const evaluation =
|
|
528
|
-
|
|
529
|
-
|
|
1084
|
+
const evaluation = evaluationStatsByStrategy.get(strategyName);
|
|
1085
|
+
signalLines.push("");
|
|
1086
|
+
signalLines.push(
|
|
530
1087
|
evaluation?.evaluated ? `<b>${escapeHtml(strategyName)}</b>
|
|
531
1088
|
signals=<b>0</b>` : `<b>${escapeHtml(strategyName)}</b>
|
|
532
1089
|
none`
|
|
@@ -538,42 +1095,62 @@ none`
|
|
|
538
1095
|
const count = stats.get(status) ?? 0;
|
|
539
1096
|
return count > 0 ? `${status}=<b>${count}</b>` : null;
|
|
540
1097
|
}).filter(Boolean);
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
1098
|
+
signalLines.push("");
|
|
1099
|
+
signalLines.push(`<b>${escapeHtml(strategyName)}</b>`);
|
|
1100
|
+
signalLines.push(parts.join(", "));
|
|
544
1101
|
appendEvaluationDetails(strategyName);
|
|
545
1102
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
1103
|
+
const sortedTradeStrategies = [...tradeStats.entries()].filter(([, stats]) => stats.closed > 0).map(([strategyName]) => strategyName).sort((left, right) => left.localeCompare(right));
|
|
1104
|
+
if (sortedTradeStrategies.length === 0) {
|
|
1105
|
+
tradeLines.push("none");
|
|
1106
|
+
}
|
|
1107
|
+
for (const strategyName of sortedTradeStrategies) {
|
|
549
1108
|
const stats = tradeStats.get(strategyName);
|
|
550
|
-
if (!stats)
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
1109
|
+
if (!stats) continue;
|
|
1110
|
+
tradeLines.push("");
|
|
1111
|
+
tradeLines.push(
|
|
1112
|
+
`<b>${escapeHtml(strategyName)}</b> (<b>${stats.closed}</b>)`
|
|
1113
|
+
);
|
|
1114
|
+
tradeLines.push(
|
|
1115
|
+
`Closed PnL: <b>${escapeHtml(formatPnlMoneyText(stats.closedPnl, stats.closedPnlKnown))}</b> (<b>${stats.closed}</b>)`
|
|
1116
|
+
);
|
|
1117
|
+
const sortedTrades = stats.trades.filter((trade) => trade.status === "closed").sort(
|
|
1118
|
+
(left, right) => left.entryTimestamp - right.entryTimestamp || left.symbol.localeCompare(right.symbol) || left.orderId.localeCompare(right.orderId)
|
|
1119
|
+
);
|
|
1120
|
+
for (const trade of sortedTrades) {
|
|
1121
|
+
tradeLines.push(
|
|
1122
|
+
`- ${escapeHtml(trade.symbol)}: PnL <b>${escapeHtml(trade.pnlText)}</b> ${resolveTradeStatusEmoji(trade.status, trade.pnl)}`
|
|
1123
|
+
);
|
|
555
1124
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
1125
|
+
}
|
|
1126
|
+
const sortedOpenStrategies = [...tradeStats.entries()].filter(([, stats]) => stats.active > 0).map(([strategyName]) => strategyName).sort((left, right) => left.localeCompare(right));
|
|
1127
|
+
if (sortedOpenStrategies.length > 0) {
|
|
1128
|
+
tradeLines.push("");
|
|
1129
|
+
tradeLines.push("\u{1F4CD} <b>Open Positions</b>");
|
|
1130
|
+
}
|
|
1131
|
+
for (const strategyName of sortedOpenStrategies) {
|
|
1132
|
+
const stats = tradeStats.get(strategyName);
|
|
1133
|
+
if (!stats) continue;
|
|
1134
|
+
tradeLines.push("");
|
|
1135
|
+
tradeLines.push(
|
|
1136
|
+
`<b>${escapeHtml(strategyName)}</b> (<b>${stats.active}</b>)`
|
|
563
1137
|
);
|
|
564
|
-
|
|
565
|
-
`
|
|
1138
|
+
tradeLines.push(
|
|
1139
|
+
`Unrealized PnL: <b>${escapeHtml(formatPnlMoneyText(stats.activePnl, stats.activePnlKnown))}</b> (<b>${stats.active}</b>)`
|
|
566
1140
|
);
|
|
567
|
-
const sortedTrades =
|
|
1141
|
+
const sortedTrades = stats.trades.filter((trade) => trade.status === "active").sort(
|
|
568
1142
|
(left, right) => left.entryTimestamp - right.entryTimestamp || left.symbol.localeCompare(right.symbol) || left.orderId.localeCompare(right.orderId)
|
|
569
1143
|
);
|
|
570
1144
|
for (const trade of sortedTrades) {
|
|
571
|
-
|
|
572
|
-
`- ${escapeHtml(trade.symbol)}: PnL <b>${escapeHtml(trade.pnlText)}</b
|
|
1145
|
+
tradeLines.push(
|
|
1146
|
+
`- ${escapeHtml(trade.symbol)}: PnL <b>${escapeHtml(trade.pnlText)}</b> ${resolveTradeStatusEmoji(trade.status, trade.pnl)}`
|
|
573
1147
|
);
|
|
574
1148
|
}
|
|
575
1149
|
}
|
|
576
|
-
return
|
|
1150
|
+
return {
|
|
1151
|
+
signalsMessage: signalLines.join("\n"),
|
|
1152
|
+
tradesMessage: tradeLines.join("\n")
|
|
1153
|
+
};
|
|
577
1154
|
};
|
|
578
1155
|
var signalsSummary = async () => {
|
|
579
1156
|
const hours = Math.max(
|
|
@@ -593,10 +1170,10 @@ var signalsSummary = async () => {
|
|
|
593
1170
|
const connector = await connectorFactory({
|
|
594
1171
|
userName: flags.user
|
|
595
1172
|
});
|
|
596
|
-
const [configuredStrategyNames, signals,
|
|
1173
|
+
const [configuredStrategyNames, signals, evaluationStatsBuckets, trades] = await Promise.all([
|
|
597
1174
|
loadRuntimeStrategyNames(flags.user),
|
|
598
|
-
loadRuntimeSignals(flags.user),
|
|
599
|
-
|
|
1175
|
+
loadRuntimeSignals(flags.user, { startTime, endTime }),
|
|
1176
|
+
loadRuntimeSignalEvaluationStatsBuckets(flags.user),
|
|
600
1177
|
loadRuntimeTrades(flags.user)
|
|
601
1178
|
]);
|
|
602
1179
|
const syncedTrades = await syncRuntimeTrades({
|
|
@@ -604,52 +1181,117 @@ var signalsSummary = async () => {
|
|
|
604
1181
|
connector,
|
|
605
1182
|
trades,
|
|
606
1183
|
startTime,
|
|
607
|
-
endTime
|
|
1184
|
+
endTime,
|
|
1185
|
+
openPositionCallbacks: {
|
|
1186
|
+
onError: (error) => {
|
|
1187
|
+
import_logger.logger.warn(
|
|
1188
|
+
"signals summary: getOpenPositionPnl failed: %s",
|
|
1189
|
+
formatRuntimeTradeSyncError(error)
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
closedPnlCallbacks: {
|
|
1194
|
+
onError: (error) => {
|
|
1195
|
+
import_logger.logger.warn(
|
|
1196
|
+
"signals summary: getClosedPnl failed: %s",
|
|
1197
|
+
formatRuntimeTradeSyncError(error)
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
608
1201
|
});
|
|
609
1202
|
const windowSignals = signals.filter(
|
|
610
|
-
(signal) => signal.timestamp >= startTime && signal.timestamp
|
|
1203
|
+
(signal) => signal.timestamp >= startTime && signal.timestamp < endTime
|
|
611
1204
|
);
|
|
612
|
-
const
|
|
613
|
-
|
|
1205
|
+
const windowDayKeys = new Set((0, import_time3.getRuntimeStorageDayKeys)(startTime, endTime));
|
|
1206
|
+
const windowEvaluationStats = /* @__PURE__ */ new Map();
|
|
1207
|
+
for (const entry of evaluationStatsBuckets) {
|
|
1208
|
+
if (!windowDayKeys.has(entry.dayKey)) {
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
const stats = windowEvaluationStats.get(entry.strategy) ?? createRuntimeSignalStats();
|
|
1212
|
+
mergeRuntimeSignalStats(stats, entry.stats);
|
|
1213
|
+
windowEvaluationStats.set(entry.strategy, stats);
|
|
1214
|
+
}
|
|
1215
|
+
const windowClosedTrades = syncedTrades.filter(
|
|
1216
|
+
(trade) => isRuntimeTradeClosedInWindow(trade, startTime, endTime)
|
|
1217
|
+
);
|
|
1218
|
+
const openSnapshotTrades = syncedTrades.filter(
|
|
1219
|
+
(trade) => trade.status === "active" && typeof trade.entryTimestamp === "number" && Number.isFinite(trade.entryTimestamp) && trade.entryTimestamp < endTime
|
|
614
1220
|
);
|
|
615
|
-
const
|
|
616
|
-
|
|
1221
|
+
const windowTrades = [...windowClosedTrades, ...openSnapshotTrades];
|
|
1222
|
+
const windowTradeSignalIds = new Set(
|
|
1223
|
+
windowTrades.map((trade) => trade.signalId).filter((signalId) => typeof signalId === "string")
|
|
617
1224
|
);
|
|
618
|
-
const
|
|
619
|
-
(
|
|
1225
|
+
const debugSignals = windowSignals.filter(
|
|
1226
|
+
(signal) => windowTradeSignalIds.has(signal.signalId)
|
|
620
1227
|
);
|
|
621
|
-
const
|
|
1228
|
+
const { signalsMessage, tradesMessage } = buildSummaryMessages({
|
|
622
1229
|
hours,
|
|
623
1230
|
startTime,
|
|
624
1231
|
endTime,
|
|
625
1232
|
configuredStrategyNames,
|
|
626
1233
|
signals: windowSignals,
|
|
627
|
-
|
|
1234
|
+
evaluationStatsByStrategy: windowEvaluationStats,
|
|
628
1235
|
trades: windowTrades
|
|
629
1236
|
});
|
|
1237
|
+
const windowEvaluationsCount = [...windowEvaluationStats.values()].reduce(
|
|
1238
|
+
(sum, stats) => sum + stats.evaluated,
|
|
1239
|
+
0
|
|
1240
|
+
);
|
|
630
1241
|
import_logger.logger.info(
|
|
631
1242
|
"signals summary window=%sh signals=%s evaluations=%s trades=%s connector=%s user=%s",
|
|
632
1243
|
hours,
|
|
633
1244
|
windowSignals.length,
|
|
634
|
-
|
|
635
|
-
|
|
1245
|
+
windowEvaluationsCount,
|
|
1246
|
+
windowClosedTrades.length,
|
|
636
1247
|
connectorName,
|
|
637
1248
|
flags.user
|
|
638
1249
|
);
|
|
1250
|
+
const debugAttachment = shouldAttachDebugReport(flags.debugAttachment) ? await buildRuntimeDebugReportAttachment({
|
|
1251
|
+
userName: flags.user,
|
|
1252
|
+
startTime,
|
|
1253
|
+
endTime,
|
|
1254
|
+
signals: debugSignals,
|
|
1255
|
+
evaluations: [],
|
|
1256
|
+
trades: windowTrades
|
|
1257
|
+
}) : null;
|
|
1258
|
+
const signalsAttachment = buildSignalsSummaryAttachment({
|
|
1259
|
+
userName: flags.user,
|
|
1260
|
+
endTime,
|
|
1261
|
+
content: signalsMessage
|
|
1262
|
+
});
|
|
1263
|
+
const tradesMessageWithAttachmentSummary = appendReportAttachmentSummary({
|
|
1264
|
+
message: tradesMessage,
|
|
1265
|
+
signalsFilename: signalsAttachment.filename,
|
|
1266
|
+
filename: debugAttachment?.filename,
|
|
1267
|
+
tradesCount: debugAttachment ? windowTrades.length : void 0,
|
|
1268
|
+
signalsCount: debugAttachment ? debugAttachment.summary?.signals ?? debugSignals.length : void 0,
|
|
1269
|
+
evaluationsCount: debugAttachment ? debugAttachment.summary?.evaluations ?? 0 : void 0
|
|
1270
|
+
});
|
|
1271
|
+
const attachments = [
|
|
1272
|
+
signalsAttachment,
|
|
1273
|
+
...debugAttachment ? [debugAttachment] : []
|
|
1274
|
+
];
|
|
639
1275
|
if (flags.printOnly) {
|
|
640
|
-
console.log(
|
|
1276
|
+
console.log(tradesMessageWithAttachmentSummary);
|
|
1277
|
+
console.log("");
|
|
1278
|
+
console.log(`Signals attachment: ${signalsAttachment.filename}`);
|
|
1279
|
+
console.log(signalsAttachment.content);
|
|
1280
|
+
if (debugAttachment) {
|
|
1281
|
+
console.log("");
|
|
1282
|
+
console.log(`Debug attachment: ${debugAttachment.filename}`);
|
|
1283
|
+
console.log(debugAttachment.content);
|
|
1284
|
+
}
|
|
641
1285
|
return;
|
|
642
1286
|
}
|
|
643
|
-
await sendTelegramReport(
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
signalsSummary().then(() => {
|
|
647
|
-
process.exit(0);
|
|
648
|
-
}).catch(() => {
|
|
649
|
-
process.exit(1);
|
|
1287
|
+
await sendTelegramReport(tradesMessageWithAttachmentSummary, {
|
|
1288
|
+
userName: flags.user,
|
|
1289
|
+
attachments
|
|
650
1290
|
});
|
|
651
|
-
}
|
|
1291
|
+
};
|
|
1292
|
+
var main = signalsSummary;
|
|
652
1293
|
// Annotate the CommonJS export names for ESM import in node:
|
|
653
1294
|
0 && (module.exports = {
|
|
1295
|
+
main,
|
|
654
1296
|
signalsSummary
|
|
655
1297
|
});
|