@tradejs/cli 1.0.9 → 1.0.11
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 +13 -4
- package/dist/cli.js +29531 -13520
- 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 +1126 -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 +4441 -0
- package/dist/lib/paritySummary.js +101 -0
- package/dist/lib/runBot.js +1 -0
- package/dist/lib/runEnvironment.js +2259 -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 +40 -6
- package/dist/lib/runtimeSignalsStorage.js +45 -22
- 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 +14 -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 +4534 -803
- 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 +18 -5
- package/dist/scripts/infraDown.js +14 -4
- package/dist/scripts/infraInit.js +32 -9
- package/dist/scripts/infraUp.js +24 -6
- 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 +8867 -0
- package/dist/scripts/replayRunner.js +8870 -0
- package/dist/scripts/replayRuntimeEvidence.js +868 -0
- package/dist/scripts/researchAuto.js +286 -59
- package/dist/scripts/results.js +3 -3
- package/dist/scripts/runtimeEvidence.js +605 -0
- package/dist/scripts/runtimeParity.js +6400 -1043
- package/dist/scripts/serverHealth.js +588 -0
- package/dist/scripts/signals.js +5628 -511
- package/dist/scripts/signalsDaemon.js +6205 -0
- package/dist/scripts/signalsSummary.js +833 -248
- 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_redis2 = 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,32 +399,16 @@ ${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
|
|
|
103
406
|
// src/lib/runtimeSignalsLoader.ts
|
|
104
|
-
var
|
|
407
|
+
var import_redis3 = require("@tradejs/infra/redis");
|
|
105
408
|
|
|
106
409
|
// src/lib/runtimeSignalsStorage.ts
|
|
107
|
-
var
|
|
108
|
-
var
|
|
109
|
-
var toIsoDayKey = (timestamp) => new Date(timestamp).toISOString().slice(0, 10);
|
|
110
|
-
var toRuntimeStorageDayTimestamp = (timestamp) => Math.floor((timestamp + RUNTIME_STORAGE_DAY_OFFSET_MS) / DAY_MS) * DAY_MS;
|
|
111
|
-
var getRuntimeStorageDayKeys = (startTime, endTime) => {
|
|
112
|
-
if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) {
|
|
113
|
-
return [];
|
|
114
|
-
}
|
|
115
|
-
const start = Math.min(startTime, endTime);
|
|
116
|
-
const endExclusive = Math.max(startTime, endTime);
|
|
117
|
-
const normalizedEnd = Math.max(start, endExclusive - 1);
|
|
118
|
-
const keys = [];
|
|
119
|
-
const startDay = toRuntimeStorageDayTimestamp(start);
|
|
120
|
-
const endDay = toRuntimeStorageDayTimestamp(normalizedEnd);
|
|
121
|
-
for (let current = startDay; current <= endDay; current += DAY_MS) {
|
|
122
|
-
keys.push(toIsoDayKey(current));
|
|
123
|
-
}
|
|
124
|
-
return keys;
|
|
125
|
-
};
|
|
410
|
+
var import_constants2 = require("@tradejs/core/constants");
|
|
411
|
+
var import_time3 = require("@tradejs/core/time");
|
|
126
412
|
var isRuntimeSignalBucketRef = (value) => {
|
|
127
413
|
if (!value || typeof value !== "object") {
|
|
128
414
|
return false;
|
|
@@ -165,9 +451,35 @@ var isSignalRecord = (value) => {
|
|
|
165
451
|
return typeof record.signalId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.timestamp === "number";
|
|
166
452
|
};
|
|
167
453
|
var sortByTimestamp = (records) => [...records].sort((left, right) => left.timestamp - right.timestamp);
|
|
168
|
-
var
|
|
169
|
-
|
|
170
|
-
|
|
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);
|
|
171
483
|
const seen = /* @__PURE__ */ new Set();
|
|
172
484
|
const payloads = await Promise.all(
|
|
173
485
|
refs.map(async (ref) => {
|
|
@@ -176,8 +488,8 @@ var loadRuntimeSignals = async (userName) => {
|
|
|
176
488
|
return null;
|
|
177
489
|
}
|
|
178
490
|
seen.add(dedupeKey);
|
|
179
|
-
const signal = await (0,
|
|
180
|
-
|
|
491
|
+
const signal = await (0, import_redis3.getData)(
|
|
492
|
+
import_redis3.redisKeys.storeSignal(ref.symbol, ref.signalId),
|
|
181
493
|
null
|
|
182
494
|
);
|
|
183
495
|
return isSignalRecord(signal) ? signal : null;
|
|
@@ -200,8 +512,8 @@ var parseBucketKey = (prefix, key) => {
|
|
|
200
512
|
};
|
|
201
513
|
};
|
|
202
514
|
var loadRuntimeSignalEvaluationStatsBuckets = async (userName) => {
|
|
203
|
-
const prefix =
|
|
204
|
-
const keys = await (0,
|
|
515
|
+
const prefix = import_redis3.redisKeys.runtimeSignalEvaluationStatsBuckets(userName);
|
|
516
|
+
const keys = await (0, import_redis3.getKeys)(prefix);
|
|
205
517
|
const entries = await Promise.all(
|
|
206
518
|
keys.map(async (key) => {
|
|
207
519
|
const parsedKey = parseBucketKey(prefix, key);
|
|
@@ -211,13 +523,219 @@ var loadRuntimeSignalEvaluationStatsBuckets = async (userName) => {
|
|
|
211
523
|
return {
|
|
212
524
|
key,
|
|
213
525
|
...parsedKey,
|
|
214
|
-
stats: parseRuntimeSignalStatsBucket(await (0,
|
|
526
|
+
stats: parseRuntimeSignalStatsBucket(await (0, import_redis3.getHashData)(key))
|
|
215
527
|
};
|
|
216
528
|
})
|
|
217
529
|
);
|
|
218
530
|
return entries.filter(Boolean);
|
|
219
531
|
};
|
|
220
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
|
+
|
|
221
739
|
// src/scripts/signalsSummary.ts
|
|
222
740
|
import_args.default.option(["u", "user"], "Use user config", "root");
|
|
223
741
|
import_args.default.option(
|
|
@@ -227,6 +745,11 @@ import_args.default.option(
|
|
|
227
745
|
);
|
|
228
746
|
import_args.default.option(["H", "hours"], "Summary window in hours", 24);
|
|
229
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
|
+
);
|
|
230
753
|
var flags = import_args.default.parse(process.argv);
|
|
231
754
|
var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
|
|
232
755
|
var SUMMARY_TIMEZONE = "Europe/Moscow";
|
|
@@ -242,6 +765,8 @@ var formatMskDateTime = (timestamp) => new Intl.DateTimeFormat("ru-RU", {
|
|
|
242
765
|
hour12: false
|
|
243
766
|
}).format(new Date(timestamp));
|
|
244
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";
|
|
245
770
|
var resolveSummaryTitle = (hours) => {
|
|
246
771
|
if (hours === 24) {
|
|
247
772
|
return "TradeJS daily summary";
|
|
@@ -249,6 +774,9 @@ var resolveSummaryTitle = (hours) => {
|
|
|
249
774
|
if (hours === 168) {
|
|
250
775
|
return "TradeJS weekly summary";
|
|
251
776
|
}
|
|
777
|
+
if (hours === 720) {
|
|
778
|
+
return "TradeJS monthly summary";
|
|
779
|
+
}
|
|
252
780
|
return `TradeJS ${hours}h summary`;
|
|
253
781
|
};
|
|
254
782
|
var normalizeStatus = (value) => {
|
|
@@ -257,12 +785,57 @@ var normalizeStatus = (value) => {
|
|
|
257
785
|
}
|
|
258
786
|
return "unknown";
|
|
259
787
|
};
|
|
260
|
-
var
|
|
261
|
-
|
|
262
|
-
|
|
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);
|
|
809
|
+
return {
|
|
810
|
+
filename: `tradejs-signals-summary-${userName}-${dayKey}.txt`,
|
|
811
|
+
content: stripTelegramHtml(content),
|
|
812
|
+
caption: `Signals summary ${dayKey} ${SUMMARY_TIMEZONE_LABEL}`
|
|
813
|
+
};
|
|
814
|
+
};
|
|
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
|
+
);
|
|
263
837
|
}
|
|
264
|
-
|
|
265
|
-
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");
|
|
266
839
|
};
|
|
267
840
|
var resolveSummaryConnectorName = async (value) => {
|
|
268
841
|
const connectorName = await (0, import_connectors.resolveConnectorName)(value, projectRoot);
|
|
@@ -276,26 +849,6 @@ var resolveSummaryConnectorName = async (value) => {
|
|
|
276
849
|
);
|
|
277
850
|
return import_connectors.DEFAULT_CONNECTOR_NAME;
|
|
278
851
|
};
|
|
279
|
-
var resolveStrategyNameByConfigKey = (userName, key) => {
|
|
280
|
-
const parts = key.split(":");
|
|
281
|
-
if (parts.length !== 5) {
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
const [users, keyUserName, strategiesKey, strategyName, configKey] = parts;
|
|
285
|
-
if (users !== "users" || keyUserName !== userName || strategiesKey !== "strategies" || configKey !== "config" || !strategyName) {
|
|
286
|
-
return null;
|
|
287
|
-
}
|
|
288
|
-
return strategyName;
|
|
289
|
-
};
|
|
290
|
-
var loadRuntimeStrategyNames = async (userName) => {
|
|
291
|
-
const keys = await (0, import_redis2.getKeys)(`${import_redis2.redisKeys.strategies(userName)}:`);
|
|
292
|
-
return keys.filter((key) => key.endsWith(":config")).map((key) => resolveStrategyNameByConfigKey(userName, key)).filter((value) => Boolean(value)).sort((left, right) => left.localeCompare(right));
|
|
293
|
-
};
|
|
294
|
-
var loadRuntimeTrades = async (userName) => {
|
|
295
|
-
const keys = await (0, import_redis2.getKeys)(import_redis2.redisKeys.runtimeTrades(userName));
|
|
296
|
-
const trades = await Promise.all(keys.map((key) => (0, import_redis2.getData)(key, null)));
|
|
297
|
-
return trades.filter(isRuntimeTradeRecord).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
|
|
298
|
-
};
|
|
299
852
|
var createRuntimeSignalStats = () => ({
|
|
300
853
|
evaluated: 0,
|
|
301
854
|
signals: 0,
|
|
@@ -312,129 +865,49 @@ var mergeRuntimeSignalStats = (target, source) => {
|
|
|
312
865
|
target.reasonGroups.set(group, targetReasons);
|
|
313
866
|
}
|
|
314
867
|
};
|
|
315
|
-
var
|
|
316
|
-
|
|
868
|
+
var buildSummaryPrelude = ({
|
|
869
|
+
hours,
|
|
317
870
|
startTime,
|
|
318
|
-
endTime
|
|
871
|
+
endTime,
|
|
872
|
+
realizedWindowPnlText,
|
|
873
|
+
winRateText,
|
|
874
|
+
longCount,
|
|
875
|
+
shortCount,
|
|
876
|
+
openCount,
|
|
877
|
+
openPnlText
|
|
319
878
|
}) => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
error?.message || String(error)
|
|
334
|
-
);
|
|
335
|
-
return [];
|
|
336
|
-
}
|
|
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
|
+
];
|
|
337
892
|
};
|
|
338
|
-
var
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
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}";
|
|
342
896
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
(row2) => Number.isFinite(row2.closedAt) && row2.closedAt >= minimumClosedAt
|
|
346
|
-
);
|
|
347
|
-
if (matchIndex < 0) {
|
|
348
|
-
return null;
|
|
897
|
+
if (status === "closed") {
|
|
898
|
+
return typeof pnl === "number" && Number.isFinite(pnl) && pnl < 0 ? "\u274C" : "\u2705";
|
|
349
899
|
}
|
|
350
|
-
|
|
351
|
-
return row ?? null;
|
|
900
|
+
return "\u2754";
|
|
352
901
|
};
|
|
353
|
-
var
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
startTime,
|
|
358
|
-
endTime
|
|
359
|
-
}) => {
|
|
360
|
-
const openPositions = typeof connector.getOpenPositionPnl === "function" ? await connector.getOpenPositionPnl() : [];
|
|
361
|
-
const openPositionsBySymbol = new Map(
|
|
362
|
-
openPositions.map((position) => [position.symbol, position])
|
|
363
|
-
);
|
|
364
|
-
const activeOrderIdBySymbol = /* @__PURE__ */ new Map();
|
|
365
|
-
const symbols = [...new Set(trades.map((trade) => trade.symbol))];
|
|
366
|
-
await Promise.all(
|
|
367
|
-
symbols.map(async (symbol) => {
|
|
368
|
-
const activeRef = await (0, import_redis2.getData)(
|
|
369
|
-
import_redis2.redisKeys.runtimeActiveTrade(userName, symbol),
|
|
370
|
-
null
|
|
371
|
-
);
|
|
372
|
-
activeOrderIdBySymbol.set(
|
|
373
|
-
symbol,
|
|
374
|
-
typeof activeRef?.orderId === "string" ? activeRef.orderId : null
|
|
375
|
-
);
|
|
376
|
-
})
|
|
377
|
-
);
|
|
378
|
-
const closedPnlRows = await loadClosedPnlRows({
|
|
379
|
-
connector,
|
|
380
|
-
startTime,
|
|
381
|
-
endTime
|
|
382
|
-
});
|
|
383
|
-
const closedPnlBuckets = /* @__PURE__ */ new Map();
|
|
384
|
-
for (const row of closedPnlRows) {
|
|
385
|
-
const bucket = closedPnlBuckets.get(row.symbol) ?? [];
|
|
386
|
-
bucket.push(row);
|
|
387
|
-
closedPnlBuckets.set(row.symbol, bucket);
|
|
388
|
-
}
|
|
389
|
-
const syncedTrades = [];
|
|
390
|
-
for (const trade of trades) {
|
|
391
|
-
if (trade.status !== "active") {
|
|
392
|
-
syncedTrades.push(trade);
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
const openPosition = openPositionsBySymbol.get(trade.symbol);
|
|
396
|
-
const activeOrderId = activeOrderIdBySymbol.get(trade.symbol);
|
|
397
|
-
const isCurrentActiveTrade = activeOrderId === trade.orderId;
|
|
398
|
-
if (isCurrentActiveTrade && openPosition && openPosition.direction === trade.direction) {
|
|
399
|
-
const nextTrade2 = {
|
|
400
|
-
...trade,
|
|
401
|
-
status: "active",
|
|
402
|
-
currentPrice: openPosition.currentPrice,
|
|
403
|
-
currentPnl: openPosition.unrealizedPnl,
|
|
404
|
-
lastSyncedAt: endTime
|
|
405
|
-
};
|
|
406
|
-
await (0, import_redis2.setData)(
|
|
407
|
-
import_redis2.redisKeys.runtimeTrade(userName, trade.orderId),
|
|
408
|
-
nextTrade2,
|
|
409
|
-
{
|
|
410
|
-
expire: 0
|
|
411
|
-
}
|
|
412
|
-
);
|
|
413
|
-
syncedTrades.push(nextTrade2);
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
const matchedClosedPnl = consumeClosedPnlMatch(closedPnlBuckets, trade);
|
|
417
|
-
const nextTrade = {
|
|
418
|
-
...trade,
|
|
419
|
-
status: "closed",
|
|
420
|
-
currentPrice: matchedClosedPnl?.exitPrice ?? trade.currentPrice ?? null,
|
|
421
|
-
currentPnl: matchedClosedPnl?.closedPnl ?? trade.closedPnl ?? trade.currentPnl ?? null,
|
|
422
|
-
closedPnl: matchedClosedPnl?.closedPnl ?? trade.closedPnl ?? trade.currentPnl ?? null,
|
|
423
|
-
exitPrice: matchedClosedPnl?.exitPrice ?? trade.exitPrice ?? null,
|
|
424
|
-
exitTimestamp: matchedClosedPnl?.closedAt ?? trade.exitTimestamp ?? endTime,
|
|
425
|
-
lastSyncedAt: endTime
|
|
426
|
-
};
|
|
427
|
-
await Promise.all([
|
|
428
|
-
(0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
|
|
429
|
-
expire: import_constants.TTL_1M
|
|
430
|
-
}),
|
|
431
|
-
...isCurrentActiveTrade ? [(0, import_redis2.delKey)(import_redis2.redisKeys.runtimeActiveTrade(userName, trade.symbol))] : []
|
|
432
|
-
]);
|
|
433
|
-
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;
|
|
434
906
|
}
|
|
435
|
-
|
|
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;
|
|
436
909
|
};
|
|
437
|
-
var
|
|
910
|
+
var buildSummaryMessages = ({
|
|
438
911
|
hours,
|
|
439
912
|
startTime,
|
|
440
913
|
endTime,
|
|
@@ -443,7 +916,8 @@ var buildSummaryMessage = ({
|
|
|
443
916
|
evaluationStatsByStrategy,
|
|
444
917
|
trades
|
|
445
918
|
}) => {
|
|
446
|
-
const
|
|
919
|
+
const signalLines = [];
|
|
920
|
+
const tradeLines = [];
|
|
447
921
|
const statusOrder = [
|
|
448
922
|
"completed",
|
|
449
923
|
"skipped",
|
|
@@ -466,43 +940,49 @@ var buildSummaryMessage = ({
|
|
|
466
940
|
stats.set(status, (stats.get(status) ?? 0) + 1);
|
|
467
941
|
signalStats.set(signal.strategy, stats);
|
|
468
942
|
}
|
|
469
|
-
|
|
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) {
|
|
470
951
|
strategyNames.add(trade.strategy);
|
|
471
952
|
const stats = tradeStats.get(trade.strategy) ?? {
|
|
472
|
-
total: 0,
|
|
473
953
|
active: 0,
|
|
474
954
|
closed: 0,
|
|
475
955
|
activePnl: 0,
|
|
476
956
|
activePnlKnown: 0,
|
|
477
957
|
closedPnl: 0,
|
|
478
958
|
closedPnlKnown: 0,
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
959
|
+
trades: [],
|
|
960
|
+
closedWins: 0,
|
|
961
|
+
closedKnown: 0
|
|
482
962
|
};
|
|
483
963
|
const pnl = trade.status === "active" ? trade.currentPnl : trade.closedPnl ?? trade.currentPnl;
|
|
484
|
-
const pnlText = typeof pnl === "number" && Number.isFinite(pnl) ? formatSigned(pnl) : "n/a";
|
|
485
|
-
stats.total += 1;
|
|
964
|
+
const pnlText = typeof pnl === "number" && Number.isFinite(pnl) ? `${formatSigned(pnl)}$` : "n/a";
|
|
486
965
|
if (trade.status === "active") {
|
|
487
966
|
stats.active += 1;
|
|
488
967
|
if (typeof pnl === "number" && Number.isFinite(pnl)) {
|
|
489
968
|
stats.activePnl += pnl;
|
|
490
969
|
stats.activePnlKnown += 1;
|
|
491
|
-
stats.totalPnl += pnl;
|
|
492
|
-
stats.totalPnlKnown += 1;
|
|
493
970
|
}
|
|
494
971
|
} else {
|
|
495
972
|
stats.closed += 1;
|
|
496
973
|
if (typeof pnl === "number" && Number.isFinite(pnl)) {
|
|
497
974
|
stats.closedPnl += pnl;
|
|
498
975
|
stats.closedPnlKnown += 1;
|
|
499
|
-
stats.
|
|
500
|
-
|
|
976
|
+
stats.closedKnown += 1;
|
|
977
|
+
if (pnl > 0) {
|
|
978
|
+
stats.closedWins += 1;
|
|
979
|
+
}
|
|
501
980
|
}
|
|
502
981
|
}
|
|
503
982
|
stats.trades.push({
|
|
504
983
|
symbol: trade.symbol,
|
|
505
984
|
status: trade.status,
|
|
985
|
+
pnl: typeof pnl === "number" && Number.isFinite(pnl) ? pnl : null,
|
|
506
986
|
pnlText,
|
|
507
987
|
entryTimestamp: trade.entryTimestamp,
|
|
508
988
|
orderId: trade.orderId
|
|
@@ -512,39 +992,70 @@ var buildSummaryMessage = ({
|
|
|
512
992
|
const sortedStrategies = [...strategyNames].sort(
|
|
513
993
|
(left, right) => left.localeCompare(right)
|
|
514
994
|
);
|
|
515
|
-
const
|
|
516
|
-
(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,
|
|
517
1001
|
0
|
|
518
1002
|
);
|
|
519
|
-
const
|
|
520
|
-
|
|
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,
|
|
521
1012
|
0
|
|
522
1013
|
);
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
lines.push("");
|
|
527
|
-
lines.push("\u{1F552} <b>Window</b>");
|
|
528
|
-
lines.push(
|
|
529
|
-
`<b>${escapeHtml(formatMskDateTime(startTime))} - ${escapeHtml(formatMskDateTime(endTime))} ${SUMMARY_TIMEZONE_LABEL}</b>`
|
|
1014
|
+
const openPnlKnown = [...tradeStats.values()].reduce(
|
|
1015
|
+
(sum, stats) => sum + stats.activePnlKnown,
|
|
1016
|
+
0
|
|
530
1017
|
);
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
|
1022
|
+
);
|
|
1023
|
+
const closedKnown = [...tradeStats.values()].reduce(
|
|
1024
|
+
(sum, stats) => sum + stats.closedKnown,
|
|
1025
|
+
0
|
|
535
1026
|
);
|
|
536
|
-
|
|
537
|
-
|
|
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>");
|
|
538
1045
|
if (!sortedStrategies.length) {
|
|
539
|
-
|
|
540
|
-
|
|
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
|
+
};
|
|
541
1052
|
}
|
|
542
1053
|
const appendEvaluationDetails = (strategyName) => {
|
|
543
1054
|
const evaluation = evaluationStatsByStrategy.get(strategyName);
|
|
544
1055
|
if (!evaluation) {
|
|
545
1056
|
return;
|
|
546
1057
|
}
|
|
547
|
-
|
|
1058
|
+
signalLines.push(
|
|
548
1059
|
`evaluated=<b>${evaluation.evaluated}</b>, signals=<b>${evaluation.signals}</b>`
|
|
549
1060
|
);
|
|
550
1061
|
const sourceOrder = ["skip from core", "skip from AI", "skip from ML"];
|
|
@@ -558,12 +1069,12 @@ var buildSummaryMessage = ({
|
|
|
558
1069
|
}
|
|
559
1070
|
);
|
|
560
1071
|
for (const [source, reasons] of sortedReasonGroups) {
|
|
561
|
-
|
|
1072
|
+
signalLines.push(`<b>${escapeHtml(source)}</b>:`);
|
|
562
1073
|
const sortedReasons = [...reasons.entries()].sort(
|
|
563
1074
|
(left, right) => right[1] - left[1] || left[0].localeCompare(right[0])
|
|
564
1075
|
);
|
|
565
1076
|
for (const [reason, count] of sortedReasons) {
|
|
566
|
-
|
|
1077
|
+
signalLines.push(`${escapeHtml(reason)}: <b>${count}</b>`);
|
|
567
1078
|
}
|
|
568
1079
|
}
|
|
569
1080
|
};
|
|
@@ -571,8 +1082,8 @@ var buildSummaryMessage = ({
|
|
|
571
1082
|
const stats = signalStats.get(strategyName);
|
|
572
1083
|
if (!stats || stats.size === 0) {
|
|
573
1084
|
const evaluation = evaluationStatsByStrategy.get(strategyName);
|
|
574
|
-
|
|
575
|
-
|
|
1085
|
+
signalLines.push("");
|
|
1086
|
+
signalLines.push(
|
|
576
1087
|
evaluation?.evaluated ? `<b>${escapeHtml(strategyName)}</b>
|
|
577
1088
|
signals=<b>0</b>` : `<b>${escapeHtml(strategyName)}</b>
|
|
578
1089
|
none`
|
|
@@ -584,42 +1095,62 @@ none`
|
|
|
584
1095
|
const count = stats.get(status) ?? 0;
|
|
585
1096
|
return count > 0 ? `${status}=<b>${count}</b>` : null;
|
|
586
1097
|
}).filter(Boolean);
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
1098
|
+
signalLines.push("");
|
|
1099
|
+
signalLines.push(`<b>${escapeHtml(strategyName)}</b>`);
|
|
1100
|
+
signalLines.push(parts.join(", "));
|
|
590
1101
|
appendEvaluationDetails(strategyName);
|
|
591
1102
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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) {
|
|
595
1108
|
const stats = tradeStats.get(strategyName);
|
|
596
|
-
if (!stats)
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
+
);
|
|
601
1124
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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>)`
|
|
609
1137
|
);
|
|
610
|
-
|
|
611
|
-
`
|
|
1138
|
+
tradeLines.push(
|
|
1139
|
+
`Unrealized PnL: <b>${escapeHtml(formatPnlMoneyText(stats.activePnl, stats.activePnlKnown))}</b> (<b>${stats.active}</b>)`
|
|
612
1140
|
);
|
|
613
|
-
const sortedTrades =
|
|
1141
|
+
const sortedTrades = stats.trades.filter((trade) => trade.status === "active").sort(
|
|
614
1142
|
(left, right) => left.entryTimestamp - right.entryTimestamp || left.symbol.localeCompare(right.symbol) || left.orderId.localeCompare(right.orderId)
|
|
615
1143
|
);
|
|
616
1144
|
for (const trade of sortedTrades) {
|
|
617
|
-
|
|
618
|
-
`- ${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)}`
|
|
619
1147
|
);
|
|
620
1148
|
}
|
|
621
1149
|
}
|
|
622
|
-
return
|
|
1150
|
+
return {
|
|
1151
|
+
signalsMessage: signalLines.join("\n"),
|
|
1152
|
+
tradesMessage: tradeLines.join("\n")
|
|
1153
|
+
};
|
|
623
1154
|
};
|
|
624
1155
|
var signalsSummary = async () => {
|
|
625
1156
|
const hours = Math.max(
|
|
@@ -641,7 +1172,7 @@ var signalsSummary = async () => {
|
|
|
641
1172
|
});
|
|
642
1173
|
const [configuredStrategyNames, signals, evaluationStatsBuckets, trades] = await Promise.all([
|
|
643
1174
|
loadRuntimeStrategyNames(flags.user),
|
|
644
|
-
loadRuntimeSignals(flags.user),
|
|
1175
|
+
loadRuntimeSignals(flags.user, { startTime, endTime }),
|
|
645
1176
|
loadRuntimeSignalEvaluationStatsBuckets(flags.user),
|
|
646
1177
|
loadRuntimeTrades(flags.user)
|
|
647
1178
|
]);
|
|
@@ -650,12 +1181,28 @@ var signalsSummary = async () => {
|
|
|
650
1181
|
connector,
|
|
651
1182
|
trades,
|
|
652
1183
|
startTime,
|
|
653
|
-
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
|
+
}
|
|
654
1201
|
});
|
|
655
1202
|
const windowSignals = signals.filter(
|
|
656
1203
|
(signal) => signal.timestamp >= startTime && signal.timestamp < endTime
|
|
657
1204
|
);
|
|
658
|
-
const windowDayKeys = new Set(getRuntimeStorageDayKeys(startTime, endTime));
|
|
1205
|
+
const windowDayKeys = new Set((0, import_time3.getRuntimeStorageDayKeys)(startTime, endTime));
|
|
659
1206
|
const windowEvaluationStats = /* @__PURE__ */ new Map();
|
|
660
1207
|
for (const entry of evaluationStatsBuckets) {
|
|
661
1208
|
if (!windowDayKeys.has(entry.dayKey)) {
|
|
@@ -665,13 +1212,20 @@ var signalsSummary = async () => {
|
|
|
665
1212
|
mergeRuntimeSignalStats(stats, entry.stats);
|
|
666
1213
|
windowEvaluationStats.set(entry.strategy, stats);
|
|
667
1214
|
}
|
|
668
|
-
const
|
|
669
|
-
|
|
1215
|
+
const windowClosedTrades = syncedTrades.filter(
|
|
1216
|
+
(trade) => isRuntimeTradeClosedInWindow(trade, startTime, endTime)
|
|
670
1217
|
);
|
|
671
|
-
const
|
|
672
|
-
(trade) => trade.
|
|
1218
|
+
const openSnapshotTrades = syncedTrades.filter(
|
|
1219
|
+
(trade) => trade.status === "active" && typeof trade.entryTimestamp === "number" && Number.isFinite(trade.entryTimestamp) && trade.entryTimestamp < endTime
|
|
673
1220
|
);
|
|
674
|
-
const
|
|
1221
|
+
const windowTrades = [...windowClosedTrades, ...openSnapshotTrades];
|
|
1222
|
+
const windowTradeSignalIds = new Set(
|
|
1223
|
+
windowTrades.map((trade) => trade.signalId).filter((signalId) => typeof signalId === "string")
|
|
1224
|
+
);
|
|
1225
|
+
const debugSignals = windowSignals.filter(
|
|
1226
|
+
(signal) => windowTradeSignalIds.has(signal.signalId)
|
|
1227
|
+
);
|
|
1228
|
+
const { signalsMessage, tradesMessage } = buildSummaryMessages({
|
|
675
1229
|
hours,
|
|
676
1230
|
startTime,
|
|
677
1231
|
endTime,
|
|
@@ -689,24 +1243,55 @@ var signalsSummary = async () => {
|
|
|
689
1243
|
hours,
|
|
690
1244
|
windowSignals.length,
|
|
691
1245
|
windowEvaluationsCount,
|
|
692
|
-
|
|
1246
|
+
windowClosedTrades.length,
|
|
693
1247
|
connectorName,
|
|
694
1248
|
flags.user
|
|
695
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
|
+
];
|
|
696
1275
|
if (flags.printOnly) {
|
|
697
|
-
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
|
+
}
|
|
698
1285
|
return;
|
|
699
1286
|
}
|
|
700
|
-
await sendTelegramReport(
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
signalsSummary().then(() => {
|
|
704
|
-
process.exit(0);
|
|
705
|
-
}).catch(() => {
|
|
706
|
-
process.exit(1);
|
|
1287
|
+
await sendTelegramReport(tradesMessageWithAttachmentSummary, {
|
|
1288
|
+
userName: flags.user,
|
|
1289
|
+
attachments
|
|
707
1290
|
});
|
|
708
|
-
}
|
|
1291
|
+
};
|
|
1292
|
+
var main = signalsSummary;
|
|
709
1293
|
// Annotate the CommonJS export names for ESM import in node:
|
|
710
1294
|
0 && (module.exports = {
|
|
1295
|
+
main,
|
|
711
1296
|
signalsSummary
|
|
712
1297
|
});
|