@tradejs/node 3.0.1 → 3.1.0
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 +2 -0
- package/dist/ai.d.mts +26 -4
- package/dist/ai.d.ts +26 -4
- package/dist/ai.js +161 -165
- package/dist/ai.mjs +3 -1
- package/dist/backtest.js +496 -188
- package/dist/backtest.mjs +3 -3
- package/dist/chunk-3TWULKHV.mjs +595 -0
- package/dist/chunk-FB5NUEOQ.mjs +295 -0
- package/dist/{chunk-VRXU4E4O.mjs → chunk-KOQSLFT2.mjs} +169 -169
- package/dist/chunk-LAJ7NA3Q.mjs +377 -0
- package/dist/{chunk-OGAWBO3Z.mjs → chunk-MKLTJQLY.mjs} +1 -1
- package/dist/cli.js +150 -161
- package/dist/cli.mjs +1 -1
- package/dist/connectors.d.mts +5 -2
- package/dist/connectors.d.ts +5 -2
- package/dist/connectors.js +329 -8
- package/dist/connectors.mjs +5 -1
- package/dist/registry.d.mts +2 -1
- package/dist/registry.d.ts +2 -1
- package/dist/registry.js +157 -161
- package/dist/registry.mjs +4 -2
- package/dist/runtimeDashboard.d.mts +12 -0
- package/dist/runtimeDashboard.d.ts +12 -0
- package/dist/runtimeDashboard.js +8433 -0
- package/dist/runtimeDashboard.mjs +955 -0
- package/dist/runtimeTrades.d.mts +31 -0
- package/dist/runtimeTrades.d.ts +31 -0
- package/dist/runtimeTrades.js +321 -0
- package/dist/runtimeTrades.mjs +11 -0
- package/dist/strategies.d.mts +2 -2
- package/dist/strategies.d.ts +2 -2
- package/dist/strategies.js +168 -165
- package/dist/strategies.mjs +13 -372
- package/package.json +18 -5
- package/dist/chunk-V3YMKE4I.mjs +0 -271
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildExchangeFallbackRuntimeTrades,
|
|
3
|
+
takeClosedPnlMatch
|
|
4
|
+
} from "./chunk-FB5NUEOQ.mjs";
|
|
5
|
+
import "./chunk-LAJ7NA3Q.mjs";
|
|
6
|
+
import {
|
|
7
|
+
getConnectorCreatorByProvider
|
|
8
|
+
} from "./chunk-3TWULKHV.mjs";
|
|
9
|
+
import "./chunk-MKLTJQLY.mjs";
|
|
10
|
+
import {
|
|
11
|
+
getAvailableStrategyNames
|
|
12
|
+
} from "./chunk-KOQSLFT2.mjs";
|
|
13
|
+
import "./chunk-WS5DYEVZ.mjs";
|
|
14
|
+
import "./chunk-Y6FXYEAI.mjs";
|
|
15
|
+
|
|
16
|
+
// src/runtimeDashboard.ts
|
|
17
|
+
import { getRuntimeStorageDayKeys } from "@tradejs/core/time";
|
|
18
|
+
import { logger } from "@tradejs/infra/logger";
|
|
19
|
+
import { strategyLogicConfigFingerprint } from "@tradejs/infra/strategyReleaseEvidence";
|
|
20
|
+
import {
|
|
21
|
+
listTradingAccounts,
|
|
22
|
+
resolveTradingAccount
|
|
23
|
+
} from "@tradejs/infra/tradingAccounts";
|
|
24
|
+
import { listRuntimeDeployments } from "@tradejs/infra/runtimeDeployments";
|
|
25
|
+
import { loadRuntimeStrategyConfigs as loadStoredRuntimeStrategyConfigs } from "@tradejs/infra/runtimeStrategyConfigs";
|
|
26
|
+
import {
|
|
27
|
+
getData as getData2,
|
|
28
|
+
getHashJsonValues,
|
|
29
|
+
getKeys,
|
|
30
|
+
redisKeys as redisKeys2
|
|
31
|
+
} from "@tradejs/infra/redis";
|
|
32
|
+
import {
|
|
33
|
+
buildRuntimeStrategyAnalytics,
|
|
34
|
+
isRuntimeTradeRecord,
|
|
35
|
+
selectTradesForWindow,
|
|
36
|
+
toRuntimeTradeView,
|
|
37
|
+
assignLegacyRuntimeTradeAccountScopes,
|
|
38
|
+
buildRuntimeStrategyIdentityKey
|
|
39
|
+
} from "@tradejs/core/runtimeTrades";
|
|
40
|
+
|
|
41
|
+
// src/strategyEvidenceTimeline.ts
|
|
42
|
+
import fs from "fs/promises";
|
|
43
|
+
import path from "path";
|
|
44
|
+
import {
|
|
45
|
+
canonicalStrategyEvidenceJson,
|
|
46
|
+
safeStrategyEvidenceSegment,
|
|
47
|
+
verifyStrategyEvidenceMarkerEnvelope
|
|
48
|
+
} from "@tradejs/infra/strategyReleaseEvidence";
|
|
49
|
+
var DEFAULT_MARKER_DIRECTORY = "data/strategy-release/markers";
|
|
50
|
+
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
51
|
+
var isNonEmptyString = (value) => typeof value === "string" && value.trim().length > 0;
|
|
52
|
+
var strategyEvidenceTimelineSelectorKey = (selector) => [
|
|
53
|
+
selector.strategy,
|
|
54
|
+
selector.compositionId ?? "",
|
|
55
|
+
selector.gitSha ?? "",
|
|
56
|
+
selector.gateFingerprint ?? "",
|
|
57
|
+
selector.configFingerprint ?? "",
|
|
58
|
+
selector.contextFingerprint ?? "",
|
|
59
|
+
selector.requireCompleteLineage ? "exact" : "partial"
|
|
60
|
+
].join(":");
|
|
61
|
+
var discoverJsonFiles = async (rootDir) => {
|
|
62
|
+
const files = [];
|
|
63
|
+
const visit = async (directory, depth) => {
|
|
64
|
+
if (depth > 12) return;
|
|
65
|
+
let entries;
|
|
66
|
+
try {
|
|
67
|
+
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error.code === "ENOENT") return;
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
await Promise.all(
|
|
73
|
+
entries.map(async (entry) => {
|
|
74
|
+
if (entry.name.startsWith(".") || entry.name.includes(".tmp-")) return;
|
|
75
|
+
const entryPath = path.join(directory, entry.name);
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
await visit(entryPath, depth + 1);
|
|
78
|
+
} else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
79
|
+
files.push(entryPath);
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
await visit(rootDir, 0);
|
|
85
|
+
return files.sort();
|
|
86
|
+
};
|
|
87
|
+
var inferMatchingStrategies = ({
|
|
88
|
+
filePath,
|
|
89
|
+
rootDir,
|
|
90
|
+
parsed,
|
|
91
|
+
strategies
|
|
92
|
+
}) => {
|
|
93
|
+
const payload = asRecord(asRecord(parsed)?.payload);
|
|
94
|
+
const declaredStrategy = isNonEmptyString(payload?.strategy) ? payload.strategy : null;
|
|
95
|
+
const directoryStrategy = path.relative(rootDir, filePath).split(path.sep)[0];
|
|
96
|
+
return strategies.filter(
|
|
97
|
+
(strategy) => strategy === declaredStrategy || directoryStrategy === safeStrategyEvidenceSegment(strategy)
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
var missingTimeline = () => ({
|
|
101
|
+
status: "missing",
|
|
102
|
+
observedFrom: null,
|
|
103
|
+
markers: []
|
|
104
|
+
});
|
|
105
|
+
var invalidTimeline = () => ({
|
|
106
|
+
status: "invalid",
|
|
107
|
+
observedFrom: null,
|
|
108
|
+
markers: []
|
|
109
|
+
});
|
|
110
|
+
var loadStrategyEvidenceTimelines = async ({
|
|
111
|
+
projectRoot,
|
|
112
|
+
markerDir,
|
|
113
|
+
selectors: requestedSelectors,
|
|
114
|
+
startTime,
|
|
115
|
+
endTime
|
|
116
|
+
}) => {
|
|
117
|
+
const selectors = [...requestedSelectors].filter((selector) => selector.strategy.trim().length > 0).sort(
|
|
118
|
+
(left, right) => strategyEvidenceTimelineSelectorKey(left).localeCompare(
|
|
119
|
+
strategyEvidenceTimelineSelectorKey(right)
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
const strategies = [...new Set(selectors.map(({ strategy }) => strategy))];
|
|
123
|
+
const timelines = new Map(
|
|
124
|
+
selectors.map((selector) => [
|
|
125
|
+
strategyEvidenceTimelineSelectorKey(selector),
|
|
126
|
+
missingTimeline()
|
|
127
|
+
])
|
|
128
|
+
);
|
|
129
|
+
if (!selectors.length) return timelines;
|
|
130
|
+
const configuredDir = markerDir?.trim() || DEFAULT_MARKER_DIRECTORY;
|
|
131
|
+
const rootDir = path.isAbsolute(configuredDir) ? configuredDir : path.resolve(projectRoot, configuredDir);
|
|
132
|
+
let files;
|
|
133
|
+
try {
|
|
134
|
+
files = await discoverJsonFiles(rootDir);
|
|
135
|
+
} catch {
|
|
136
|
+
for (const selector of selectors) {
|
|
137
|
+
timelines.set(
|
|
138
|
+
strategyEvidenceTimelineSelectorKey(selector),
|
|
139
|
+
invalidTimeline()
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return timelines;
|
|
143
|
+
}
|
|
144
|
+
const envelopesByStrategy = /* @__PURE__ */ new Map();
|
|
145
|
+
const invalidStrategies = /* @__PURE__ */ new Set();
|
|
146
|
+
for (const filePath of files) {
|
|
147
|
+
let parsed = null;
|
|
148
|
+
try {
|
|
149
|
+
parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
150
|
+
} catch {
|
|
151
|
+
for (const strategy of inferMatchingStrategies({
|
|
152
|
+
filePath,
|
|
153
|
+
rootDir,
|
|
154
|
+
parsed,
|
|
155
|
+
strategies
|
|
156
|
+
})) {
|
|
157
|
+
invalidStrategies.add(strategy);
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const matchingStrategies = inferMatchingStrategies({
|
|
162
|
+
filePath,
|
|
163
|
+
rootDir,
|
|
164
|
+
parsed,
|
|
165
|
+
strategies
|
|
166
|
+
});
|
|
167
|
+
if (!matchingStrategies.length) continue;
|
|
168
|
+
try {
|
|
169
|
+
const envelope = verifyStrategyEvidenceMarkerEnvelope(parsed);
|
|
170
|
+
for (const strategy of matchingStrategies) {
|
|
171
|
+
if (strategy !== envelope.payload.strategy) {
|
|
172
|
+
invalidStrategies.add(strategy);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!strategies.includes(envelope.payload.strategy)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const envelopes = envelopesByStrategy.get(envelope.payload.strategy) ?? [];
|
|
179
|
+
envelopes.push(envelope);
|
|
180
|
+
envelopesByStrategy.set(envelope.payload.strategy, envelopes);
|
|
181
|
+
} catch {
|
|
182
|
+
for (const strategy of matchingStrategies) {
|
|
183
|
+
invalidStrategies.add(strategy);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const selector of selectors) {
|
|
188
|
+
const strategy = selector.strategy;
|
|
189
|
+
const selectorKey = strategyEvidenceTimelineSelectorKey(selector);
|
|
190
|
+
if (invalidStrategies.has(strategy)) {
|
|
191
|
+
timelines.set(selectorKey, invalidTimeline());
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const envelopes = envelopesByStrategy.get(strategy) ?? [];
|
|
195
|
+
if (!envelopes.length) continue;
|
|
196
|
+
const hasCompleteSelector = Boolean(selector.compositionId) && Boolean(selector.gitSha) && Boolean(selector.gateFingerprint) && Boolean(selector.configFingerprint) && Boolean(selector.contextFingerprint);
|
|
197
|
+
if (selector.requireCompleteLineage && !hasCompleteSelector) continue;
|
|
198
|
+
const markersById = /* @__PURE__ */ new Map();
|
|
199
|
+
let hasConflict = false;
|
|
200
|
+
for (const envelope of envelopes) {
|
|
201
|
+
for (const marker of envelope.payload.markers) {
|
|
202
|
+
const existing = markersById.get(marker.id);
|
|
203
|
+
if (existing && canonicalStrategyEvidenceJson(existing) !== canonicalStrategyEvidenceJson(marker)) {
|
|
204
|
+
hasConflict = true;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
markersById.set(marker.id, marker);
|
|
208
|
+
}
|
|
209
|
+
if (hasConflict) break;
|
|
210
|
+
}
|
|
211
|
+
if (hasConflict) {
|
|
212
|
+
timelines.set(selectorKey, invalidTimeline());
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const matchingMarkers = [...markersById.values()].filter(
|
|
216
|
+
(marker) => (!selector.compositionId || marker.compositionId === selector.compositionId) && (!selector.gitSha || marker.gitSha === selector.gitSha) && (!selector.gateFingerprint || marker.gateFingerprint === selector.gateFingerprint) && (!selector.configFingerprint || marker.configFingerprint === selector.configFingerprint) && (!selector.contextFingerprint || marker.contextFingerprint === selector.contextFingerprint) && marker.timestamp >= startTime && marker.timestamp < endTime
|
|
217
|
+
).sort(
|
|
218
|
+
(left, right) => left.timestamp - right.timestamp || left.type.localeCompare(right.type) || left.id.localeCompare(right.id)
|
|
219
|
+
);
|
|
220
|
+
let lastLossValue;
|
|
221
|
+
let hasLastLossValue = false;
|
|
222
|
+
const markers = matchingMarkers.filter((marker) => {
|
|
223
|
+
if (marker.type !== "L") return true;
|
|
224
|
+
if (hasLastLossValue && marker.maxLossValue === lastLossValue) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
hasLastLossValue = true;
|
|
228
|
+
lastLossValue = marker.maxLossValue;
|
|
229
|
+
return true;
|
|
230
|
+
});
|
|
231
|
+
if (!markers.length && (selector.compositionId || selector.gitSha || selector.gateFingerprint || selector.configFingerprint || selector.contextFingerprint)) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
timelines.set(selectorKey, {
|
|
235
|
+
status: "verified",
|
|
236
|
+
observedFrom: markers.length ? Math.min(...markers.map((marker) => marker.timestamp)) : Math.min(...envelopes.map((envelope) => envelope.payload.createdAt)),
|
|
237
|
+
markers
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return timelines;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// src/runtimeTradeSync.ts
|
|
244
|
+
import { TTL_1M } from "@tradejs/core/constants";
|
|
245
|
+
import { getRuntimeStorageDayKey } from "@tradejs/core/time";
|
|
246
|
+
import {
|
|
247
|
+
delKey,
|
|
248
|
+
getData,
|
|
249
|
+
redisKeys,
|
|
250
|
+
setData,
|
|
251
|
+
setHashJsonField
|
|
252
|
+
} from "@tradejs/infra/redis";
|
|
253
|
+
var redisRuntimeTradeStore = {
|
|
254
|
+
async getActiveOrderId({ userName, symbol, scopeId }) {
|
|
255
|
+
const value = await getData(
|
|
256
|
+
redisKeys.runtimeActiveTrade(userName, symbol, scopeId),
|
|
257
|
+
null
|
|
258
|
+
);
|
|
259
|
+
return typeof value?.orderId === "string" ? value.orderId : null;
|
|
260
|
+
},
|
|
261
|
+
async saveTrade({ userName, trade, expire }) {
|
|
262
|
+
await Promise.all([
|
|
263
|
+
setData(redisKeys.runtimeTrade(userName, trade.orderId), trade, {
|
|
264
|
+
expire
|
|
265
|
+
}),
|
|
266
|
+
setHashJsonField(
|
|
267
|
+
redisKeys.runtimeTradeBucket(
|
|
268
|
+
userName,
|
|
269
|
+
getRuntimeStorageDayKey(trade.entryTimestamp)
|
|
270
|
+
),
|
|
271
|
+
trade.orderId,
|
|
272
|
+
trade,
|
|
273
|
+
{ expire }
|
|
274
|
+
)
|
|
275
|
+
]);
|
|
276
|
+
},
|
|
277
|
+
async saveClosedTrade({ userName, trade, expire }) {
|
|
278
|
+
await setHashJsonField(
|
|
279
|
+
redisKeys.runtimeClosedTradeBucket(
|
|
280
|
+
userName,
|
|
281
|
+
getRuntimeStorageDayKey(trade.exitTimestamp)
|
|
282
|
+
),
|
|
283
|
+
trade.orderId,
|
|
284
|
+
trade,
|
|
285
|
+
{ expire }
|
|
286
|
+
);
|
|
287
|
+
},
|
|
288
|
+
async deleteActiveTrade({ userName, symbol, scopeId }) {
|
|
289
|
+
await delKey(redisKeys.runtimeActiveTrade(userName, symbol, scopeId));
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
var getRuntimeTradeScopeId = (trade) => trade.deploymentId ?? trade.accountId;
|
|
293
|
+
var isRuntimeTradeInConnectorScope = (trade, connector) => {
|
|
294
|
+
if (trade.deploymentId && trade.deploymentId !== connector.deploymentId) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
if (trade.deploymentId && !connector.deploymentId) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
if (trade.accountId && trade.accountId !== connector.accountId) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
if (trade.accountId && !connector.accountId) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
return (trade.universe ?? "crypto") === connector.universe;
|
|
307
|
+
};
|
|
308
|
+
var buildRiskLevelsAnalysis = (position) => {
|
|
309
|
+
const takeProfitPrice = typeof position.takeProfitPrice === "number" && Number.isFinite(position.takeProfitPrice) ? position.takeProfitPrice : null;
|
|
310
|
+
const stopLossPrice = typeof position.stopLossPrice === "number" && Number.isFinite(position.stopLossPrice) ? position.stopLossPrice : null;
|
|
311
|
+
if (takeProfitPrice == null && stopLossPrice == null) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
...takeProfitPrice != null ? { takeProfitPrice } : {},
|
|
316
|
+
...stopLossPrice != null ? { stopLossPrice } : {}
|
|
317
|
+
};
|
|
318
|
+
};
|
|
319
|
+
var hasExchangeCloseDetails = (trade) => trade.status === "closed" && typeof trade.exitPrice === "number" && Number.isFinite(trade.exitPrice) && typeof trade.actualExitPrice === "number" && Number.isFinite(trade.actualExitPrice) && typeof trade.closedPnl === "number" && Number.isFinite(trade.closedPnl) && typeof trade.openFee === "number" && Number.isFinite(trade.openFee) && typeof trade.closeFee === "number" && Number.isFinite(trade.closeFee);
|
|
320
|
+
var syncRuntimeTrades = async ({
|
|
321
|
+
userName,
|
|
322
|
+
connector,
|
|
323
|
+
trades,
|
|
324
|
+
endTime,
|
|
325
|
+
openPositions,
|
|
326
|
+
openPositionsReliable,
|
|
327
|
+
closedPnlRows,
|
|
328
|
+
store = redisRuntimeTradeStore
|
|
329
|
+
}) => {
|
|
330
|
+
const openPositionsBySymbol = new Map(
|
|
331
|
+
openPositions.map((position) => [position.symbol, position])
|
|
332
|
+
);
|
|
333
|
+
const activeOrderIdByScope = /* @__PURE__ */ new Map();
|
|
334
|
+
const activeTradeScopes = [
|
|
335
|
+
...new Map(
|
|
336
|
+
trades.filter((trade) => isRuntimeTradeInConnectorScope(trade, connector)).map((trade) => {
|
|
337
|
+
const scope = {
|
|
338
|
+
symbol: trade.symbol,
|
|
339
|
+
scopeId: getRuntimeTradeScopeId(trade)
|
|
340
|
+
};
|
|
341
|
+
return [`${scope.scopeId ?? ""}:${scope.symbol}`, scope];
|
|
342
|
+
})
|
|
343
|
+
).values()
|
|
344
|
+
];
|
|
345
|
+
await Promise.all(
|
|
346
|
+
activeTradeScopes.map(async ({ symbol, scopeId }) => {
|
|
347
|
+
activeOrderIdByScope.set(
|
|
348
|
+
`${scopeId ?? ""}:${symbol}`,
|
|
349
|
+
await store.getActiveOrderId({ userName, symbol, scopeId })
|
|
350
|
+
);
|
|
351
|
+
})
|
|
352
|
+
);
|
|
353
|
+
const exactByOrderLinkId = new Map(
|
|
354
|
+
closedPnlRows.filter(
|
|
355
|
+
(row) => typeof row.orderLinkId === "string" && row.orderLinkId.length > 0
|
|
356
|
+
).map((row) => [row.orderLinkId, row])
|
|
357
|
+
);
|
|
358
|
+
const exactByOrderId = new Map(
|
|
359
|
+
closedPnlRows.filter(
|
|
360
|
+
(row) => typeof row.orderId === "string" && row.orderId.length > 0
|
|
361
|
+
).map((row) => [row.orderId, row])
|
|
362
|
+
);
|
|
363
|
+
const symbolBuckets = /* @__PURE__ */ new Map();
|
|
364
|
+
for (const row of closedPnlRows) {
|
|
365
|
+
const bucket = symbolBuckets.get(row.symbol) ?? [];
|
|
366
|
+
bucket.push(row);
|
|
367
|
+
symbolBuckets.set(row.symbol, bucket);
|
|
368
|
+
}
|
|
369
|
+
const syncedTrades = [];
|
|
370
|
+
for (const trade of trades) {
|
|
371
|
+
if (!isRuntimeTradeInConnectorScope(trade, connector)) {
|
|
372
|
+
syncedTrades.push(trade);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const scopeId = getRuntimeTradeScopeId(trade);
|
|
376
|
+
const isCurrentActiveTrade = activeOrderIdByScope.get(`${scopeId ?? ""}:${trade.symbol}`) === trade.orderId;
|
|
377
|
+
if (hasExchangeCloseDetails(trade)) {
|
|
378
|
+
if (isCurrentActiveTrade) {
|
|
379
|
+
await store.deleteActiveTrade({
|
|
380
|
+
userName,
|
|
381
|
+
symbol: trade.symbol,
|
|
382
|
+
scopeId
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
syncedTrades.push(trade);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const openPosition = openPositionsBySymbol.get(trade.symbol);
|
|
389
|
+
if (trade.status === "active" && !openPositionsReliable) {
|
|
390
|
+
syncedTrades.push(trade);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (trade.status === "active" && isCurrentActiveTrade && openPosition && openPosition.direction === trade.direction) {
|
|
394
|
+
const riskLevelsAnalysis = buildRiskLevelsAnalysis(openPosition);
|
|
395
|
+
const nextTrade2 = {
|
|
396
|
+
...trade,
|
|
397
|
+
status: "active",
|
|
398
|
+
currentPrice: openPosition.currentPrice,
|
|
399
|
+
currentPnl: openPosition.unrealizedPnl,
|
|
400
|
+
aiAnalysis: riskLevelsAnalysis ? { ...trade.aiAnalysis ?? {}, ...riskLevelsAnalysis } : trade.aiAnalysis,
|
|
401
|
+
lastSyncedAt: endTime
|
|
402
|
+
};
|
|
403
|
+
await store.saveTrade({ userName, trade: nextTrade2, expire: 0 });
|
|
404
|
+
syncedTrades.push(nextTrade2);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const matchedClosedPnl = takeClosedPnlMatch({
|
|
408
|
+
exactByOrderLinkId,
|
|
409
|
+
exactByOrderId,
|
|
410
|
+
symbolBuckets,
|
|
411
|
+
trade
|
|
412
|
+
});
|
|
413
|
+
if (!matchedClosedPnl) {
|
|
414
|
+
syncedTrades.push(trade);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const nextTrade = {
|
|
418
|
+
...trade,
|
|
419
|
+
status: "closed",
|
|
420
|
+
currentPrice: matchedClosedPnl.exitPrice ?? trade.currentPrice ?? null,
|
|
421
|
+
currentPnl: matchedClosedPnl.closedPnl,
|
|
422
|
+
closedPnl: matchedClosedPnl.closedPnl,
|
|
423
|
+
actualEntryPrice: matchedClosedPnl.entryPrice ?? trade.actualEntryPrice ?? null,
|
|
424
|
+
exitPrice: matchedClosedPnl.exitPrice ?? trade.exitPrice ?? null,
|
|
425
|
+
actualExitPrice: matchedClosedPnl.exitPrice ?? trade.actualExitPrice ?? null,
|
|
426
|
+
exitTimestamp: matchedClosedPnl.closedAt,
|
|
427
|
+
exitType: trade.exitType ?? null,
|
|
428
|
+
openFee: matchedClosedPnl.openFee ?? trade.openFee ?? null,
|
|
429
|
+
closeFee: matchedClosedPnl.closeFee ?? trade.closeFee ?? null,
|
|
430
|
+
fundingFee: matchedClosedPnl.fundingFee ?? trade.fundingFee ?? null,
|
|
431
|
+
totalFee: matchedClosedPnl.totalFee ?? trade.totalFee ?? null,
|
|
432
|
+
lastSyncedAt: endTime
|
|
433
|
+
};
|
|
434
|
+
await Promise.all([
|
|
435
|
+
store.saveTrade({ userName, trade: nextTrade, expire: TTL_1M }),
|
|
436
|
+
store.saveClosedTrade({ userName, trade: nextTrade, expire: TTL_1M }),
|
|
437
|
+
...isCurrentActiveTrade ? [
|
|
438
|
+
store.deleteActiveTrade({
|
|
439
|
+
userName,
|
|
440
|
+
symbol: trade.symbol,
|
|
441
|
+
scopeId
|
|
442
|
+
})
|
|
443
|
+
] : []
|
|
444
|
+
]);
|
|
445
|
+
syncedTrades.push(nextTrade);
|
|
446
|
+
}
|
|
447
|
+
return syncedTrades;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// src/runtimeDashboard.ts
|
|
451
|
+
var DEFAULT_PROVIDER = "bybit";
|
|
452
|
+
var DEFAULT_HOURS = 168;
|
|
453
|
+
var MIN_HOURS = 6;
|
|
454
|
+
var MAX_HOURS = 24 * 90;
|
|
455
|
+
var BYBIT_MAX_TIME_RANGE_MS = 7 * 24 * 60 * 60 * 1e3 - 1e3;
|
|
456
|
+
var EXCHANGE_REQUEST_TIMEOUT_MS = 15e3;
|
|
457
|
+
var coerceHours = (value) => {
|
|
458
|
+
const parsed = Number(value ?? Number.NaN);
|
|
459
|
+
if (!Number.isFinite(parsed)) {
|
|
460
|
+
return DEFAULT_HOURS;
|
|
461
|
+
}
|
|
462
|
+
return Math.min(MAX_HOURS, Math.max(MIN_HOURS, Math.trunc(parsed)));
|
|
463
|
+
};
|
|
464
|
+
var isRuntimeStrategyConfigEnabled = (config) => {
|
|
465
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
return config.ENABLE !== false;
|
|
469
|
+
};
|
|
470
|
+
var loadRuntimeStrategyConfigs = async (userName) => {
|
|
471
|
+
return (await loadStoredRuntimeStrategyConfigs(userName)).map(
|
|
472
|
+
({ strategyConfig, ...record }) => ({
|
|
473
|
+
...record,
|
|
474
|
+
config: strategyConfig
|
|
475
|
+
})
|
|
476
|
+
);
|
|
477
|
+
};
|
|
478
|
+
var loadConfiguredStrategyNames = async (projectRoot) => {
|
|
479
|
+
try {
|
|
480
|
+
return await getAvailableStrategyNames(projectRoot);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
logger.warn(
|
|
483
|
+
"strategies runtime: failed to load configured strategies: %s",
|
|
484
|
+
error?.message || String(error)
|
|
485
|
+
);
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
var resolveConnectorCreatorByProvider = async (provider, projectRoot) => await getConnectorCreatorByProvider(provider, projectRoot) ?? await getConnectorCreatorByProvider(DEFAULT_PROVIDER, projectRoot) ?? null;
|
|
490
|
+
var resolveConnectorAccountId = async ({
|
|
491
|
+
userName,
|
|
492
|
+
provider
|
|
493
|
+
}) => (await resolveTradingAccount({
|
|
494
|
+
userName,
|
|
495
|
+
provider,
|
|
496
|
+
universe: "crypto"
|
|
497
|
+
}))?.id;
|
|
498
|
+
var loadRuntimeTrades = async (userName, {
|
|
499
|
+
startTime,
|
|
500
|
+
endTime
|
|
501
|
+
}) => {
|
|
502
|
+
const filterByWindow = (trade) => trade.entryTimestamp >= startTime || typeof trade.exitTimestamp === "number" && trade.exitTimestamp >= startTime;
|
|
503
|
+
const dayKeys = getRuntimeStorageDayKeys(startTime, endTime);
|
|
504
|
+
const bucketTrades = (await Promise.all(
|
|
505
|
+
dayKeys.map(
|
|
506
|
+
(dayKey) => getHashJsonValues(
|
|
507
|
+
redisKeys2.runtimeTradeBucket(userName, dayKey)
|
|
508
|
+
)
|
|
509
|
+
)
|
|
510
|
+
)).flat();
|
|
511
|
+
const dedupedBucketTrades = /* @__PURE__ */ new Map();
|
|
512
|
+
for (const trade of bucketTrades) {
|
|
513
|
+
if (!isRuntimeTradeRecord(trade)) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
dedupedBucketTrades.set(trade.orderId, trade);
|
|
517
|
+
}
|
|
518
|
+
if (dedupedBucketTrades.size > 0 || dayKeys.length === 0) {
|
|
519
|
+
return [...dedupedBucketTrades.values()].filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
|
|
520
|
+
}
|
|
521
|
+
const keys = await getKeys(redisKeys2.runtimeTrades(userName));
|
|
522
|
+
const trades = await Promise.all(keys.map((key) => getData2(key, null)));
|
|
523
|
+
return trades.filter(isRuntimeTradeRecord).filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
|
|
524
|
+
};
|
|
525
|
+
var buildExchangeTimeRanges = (startTime, endTime) => {
|
|
526
|
+
const ranges = [];
|
|
527
|
+
let cursor = startTime;
|
|
528
|
+
while (cursor < endTime) {
|
|
529
|
+
const rangeEnd = Math.min(endTime, cursor + BYBIT_MAX_TIME_RANGE_MS);
|
|
530
|
+
ranges.push({ startTime: cursor, endTime: rangeEnd });
|
|
531
|
+
cursor = rangeEnd + 1;
|
|
532
|
+
}
|
|
533
|
+
return ranges;
|
|
534
|
+
};
|
|
535
|
+
var loadExchangeRange = async ({
|
|
536
|
+
label,
|
|
537
|
+
startTime,
|
|
538
|
+
endTime,
|
|
539
|
+
load,
|
|
540
|
+
errors
|
|
541
|
+
}) => {
|
|
542
|
+
try {
|
|
543
|
+
return await Promise.race([
|
|
544
|
+
load(),
|
|
545
|
+
new Promise((_, reject) => {
|
|
546
|
+
setTimeout(
|
|
547
|
+
() => reject(
|
|
548
|
+
new Error(
|
|
549
|
+
`${label} timed out for ${new Date(startTime).toISOString()} - ${new Date(endTime).toISOString()}`
|
|
550
|
+
)
|
|
551
|
+
),
|
|
552
|
+
EXCHANGE_REQUEST_TIMEOUT_MS
|
|
553
|
+
);
|
|
554
|
+
})
|
|
555
|
+
]);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
const message = error?.message || String(error);
|
|
558
|
+
errors?.push(`${label}: ${message}`);
|
|
559
|
+
logger.warn("strategies runtime: %s failed: %s", label, message);
|
|
560
|
+
return [];
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
var loadActiveRuntimeOrderIds = async (userName) => {
|
|
564
|
+
const keys = await getKeys(redisKeys2.runtimeActiveTrades(userName));
|
|
565
|
+
const refs = await Promise.all(keys.map((key) => getData2(key, null)));
|
|
566
|
+
return new Set(
|
|
567
|
+
refs.map(
|
|
568
|
+
(ref) => typeof ref?.orderId === "string" && ref.orderId.trim() ? ref.orderId.trim() : null
|
|
569
|
+
).filter((value) => Boolean(value))
|
|
570
|
+
);
|
|
571
|
+
};
|
|
572
|
+
var loadClosedPnlRows = async ({
|
|
573
|
+
connector,
|
|
574
|
+
startTime,
|
|
575
|
+
endTime,
|
|
576
|
+
errors
|
|
577
|
+
}) => {
|
|
578
|
+
if (typeof connector.getClosedPnl !== "function") {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
const rows = (await Promise.all(
|
|
583
|
+
buildExchangeTimeRanges(startTime, endTime).map(
|
|
584
|
+
(range) => loadExchangeRange({
|
|
585
|
+
label: "getClosedPnl",
|
|
586
|
+
...range,
|
|
587
|
+
errors,
|
|
588
|
+
load: () => connector.getClosedPnl?.({
|
|
589
|
+
...range,
|
|
590
|
+
limit: 100
|
|
591
|
+
}) ?? Promise.resolve([])
|
|
592
|
+
})
|
|
593
|
+
)
|
|
594
|
+
)).flatMap((items) => items ?? []);
|
|
595
|
+
return rows.sort((left, right) => left.closedAt - right.closedAt);
|
|
596
|
+
} catch (error) {
|
|
597
|
+
const message = error?.message || String(error);
|
|
598
|
+
errors?.push(`getClosedPnl: ${message}`);
|
|
599
|
+
logger.warn("strategies runtime: getClosedPnl failed: %s", message);
|
|
600
|
+
return [];
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
var loadExchangeEntryRows = async ({
|
|
604
|
+
connector,
|
|
605
|
+
startTime,
|
|
606
|
+
endTime,
|
|
607
|
+
errors
|
|
608
|
+
}) => {
|
|
609
|
+
if (typeof connector.getEntryExecutions !== "function") {
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
const rows = (await Promise.all(
|
|
614
|
+
buildExchangeTimeRanges(startTime, endTime).map(
|
|
615
|
+
(range) => loadExchangeRange({
|
|
616
|
+
label: "getEntryExecutions",
|
|
617
|
+
...range,
|
|
618
|
+
errors,
|
|
619
|
+
load: () => connector.getEntryExecutions?.({
|
|
620
|
+
...range,
|
|
621
|
+
limit: 100
|
|
622
|
+
}) ?? Promise.resolve([])
|
|
623
|
+
})
|
|
624
|
+
)
|
|
625
|
+
)).flatMap((items) => items ?? []);
|
|
626
|
+
return rows.sort(
|
|
627
|
+
(left, right) => left.entryTimestamp - right.entryTimestamp
|
|
628
|
+
);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
const message = error?.message || String(error);
|
|
631
|
+
errors?.push(`getEntryExecutions: ${message}`);
|
|
632
|
+
logger.warn("strategies runtime: getEntryExecutions failed: %s", message);
|
|
633
|
+
return [];
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
var loadOpenPositions = async (connector, errors) => {
|
|
637
|
+
if (typeof connector.getOpenPositionPnl !== "function") {
|
|
638
|
+
return { positions: [], reliable: false };
|
|
639
|
+
}
|
|
640
|
+
try {
|
|
641
|
+
return {
|
|
642
|
+
positions: await connector.getOpenPositionPnl(),
|
|
643
|
+
reliable: true
|
|
644
|
+
};
|
|
645
|
+
} catch (error) {
|
|
646
|
+
const message = error?.message || String(error);
|
|
647
|
+
errors?.push(`getOpenPositionPnl: ${message}`);
|
|
648
|
+
logger.warn("strategies runtime: getOpenPositionPnl failed: %s", message);
|
|
649
|
+
return { positions: [], reliable: false };
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
var loadRuntimeDashboard = async ({
|
|
653
|
+
userName,
|
|
654
|
+
provider: requestedProvider,
|
|
655
|
+
hours: requestedHours,
|
|
656
|
+
now,
|
|
657
|
+
projectRoot: requestedProjectRoot
|
|
658
|
+
}) => {
|
|
659
|
+
const provider = requestedProvider?.trim() || DEFAULT_PROVIDER;
|
|
660
|
+
const hours = coerceHours(requestedHours);
|
|
661
|
+
const endTime = now ?? Date.now();
|
|
662
|
+
const startTime = endTime - hours * 60 * 60 * 1e3;
|
|
663
|
+
const projectRoot = requestedProjectRoot?.trim() || String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
|
|
664
|
+
const exchangeErrors = [];
|
|
665
|
+
const connectorCreator = await resolveConnectorCreatorByProvider(
|
|
666
|
+
provider,
|
|
667
|
+
projectRoot
|
|
668
|
+
);
|
|
669
|
+
if (!connectorCreator) {
|
|
670
|
+
throw new Error(`No connector available for provider "${provider}"`);
|
|
671
|
+
}
|
|
672
|
+
const connectorAccountId = await resolveConnectorAccountId({
|
|
673
|
+
userName,
|
|
674
|
+
provider
|
|
675
|
+
});
|
|
676
|
+
const connector = await connectorCreator({
|
|
677
|
+
userName,
|
|
678
|
+
accountId: connectorAccountId,
|
|
679
|
+
universe: "crypto"
|
|
680
|
+
});
|
|
681
|
+
const [
|
|
682
|
+
runtimeStrategyConfigs,
|
|
683
|
+
configuredStrategyNames,
|
|
684
|
+
runtimeTrades,
|
|
685
|
+
activeOrderIds,
|
|
686
|
+
closedPnlRows,
|
|
687
|
+
entryRows,
|
|
688
|
+
openPositionsSnapshot,
|
|
689
|
+
runtimeDeployments,
|
|
690
|
+
tradingAccounts
|
|
691
|
+
] = await Promise.all([
|
|
692
|
+
loadRuntimeStrategyConfigs(userName),
|
|
693
|
+
loadConfiguredStrategyNames(projectRoot),
|
|
694
|
+
loadRuntimeTrades(userName, { startTime, endTime }),
|
|
695
|
+
loadActiveRuntimeOrderIds(userName),
|
|
696
|
+
loadClosedPnlRows({
|
|
697
|
+
connector,
|
|
698
|
+
startTime,
|
|
699
|
+
endTime,
|
|
700
|
+
errors: exchangeErrors
|
|
701
|
+
}),
|
|
702
|
+
loadExchangeEntryRows({
|
|
703
|
+
connector,
|
|
704
|
+
startTime,
|
|
705
|
+
endTime,
|
|
706
|
+
errors: exchangeErrors
|
|
707
|
+
}),
|
|
708
|
+
loadOpenPositions(connector, exchangeErrors),
|
|
709
|
+
listRuntimeDeployments(userName),
|
|
710
|
+
listTradingAccounts(userName)
|
|
711
|
+
]);
|
|
712
|
+
const relevantTrades = selectTradesForWindow(
|
|
713
|
+
runtimeTrades,
|
|
714
|
+
startTime,
|
|
715
|
+
activeOrderIds
|
|
716
|
+
);
|
|
717
|
+
const syncableTrades = relevantTrades.filter(
|
|
718
|
+
(trade) => isRuntimeTradeInConnectorScope(trade, connector)
|
|
719
|
+
);
|
|
720
|
+
const unsyncedTrades = relevantTrades.filter(
|
|
721
|
+
(trade) => !isRuntimeTradeInConnectorScope(trade, connector)
|
|
722
|
+
);
|
|
723
|
+
const syncedConnectorTrades = await syncRuntimeTrades({
|
|
724
|
+
userName,
|
|
725
|
+
connector,
|
|
726
|
+
trades: syncableTrades,
|
|
727
|
+
endTime,
|
|
728
|
+
openPositions: openPositionsSnapshot.positions,
|
|
729
|
+
openPositionsReliable: openPositionsSnapshot.reliable,
|
|
730
|
+
closedPnlRows
|
|
731
|
+
});
|
|
732
|
+
const syncedTrades = [...unsyncedTrades, ...syncedConnectorTrades];
|
|
733
|
+
const fallbackStrategyNames = [
|
|
734
|
+
.../* @__PURE__ */ new Set([
|
|
735
|
+
...runtimeStrategyConfigs.map(({ strategyName }) => strategyName),
|
|
736
|
+
...configuredStrategyNames
|
|
737
|
+
])
|
|
738
|
+
];
|
|
739
|
+
const fallbackTrades = buildExchangeFallbackRuntimeTrades({
|
|
740
|
+
entryRows,
|
|
741
|
+
closedPnlRows,
|
|
742
|
+
openPositions: openPositionsSnapshot.positions,
|
|
743
|
+
strategyNames: fallbackStrategyNames,
|
|
744
|
+
existingTrades: syncedTrades,
|
|
745
|
+
endTime
|
|
746
|
+
});
|
|
747
|
+
const allTrades = [...syncedTrades, ...fallbackTrades].filter(
|
|
748
|
+
isRuntimeTradeRecord
|
|
749
|
+
);
|
|
750
|
+
const connectedSet = new Set(
|
|
751
|
+
runtimeStrategyConfigs.map(
|
|
752
|
+
({ strategyName, configId }) => `${strategyName}:${configId}`
|
|
753
|
+
)
|
|
754
|
+
);
|
|
755
|
+
const accountsById = new Map(
|
|
756
|
+
tradingAccounts.map((account) => [account.id, account])
|
|
757
|
+
);
|
|
758
|
+
const runtimeIdentityKey = (trade) => buildRuntimeStrategyIdentityKey({
|
|
759
|
+
strategyName: trade.strategy,
|
|
760
|
+
configId: trade.runtimeConfigId,
|
|
761
|
+
universe: trade.universe,
|
|
762
|
+
accountId: trade.accountId,
|
|
763
|
+
deploymentId: trade.deploymentId,
|
|
764
|
+
policyProfileId: trade.policyProfileId
|
|
765
|
+
});
|
|
766
|
+
const identityByKey = /* @__PURE__ */ new Map();
|
|
767
|
+
const runtimeConfigAccountScopes = new Array();
|
|
768
|
+
for (const deployment of runtimeDeployments) {
|
|
769
|
+
for (const deploymentStrategy of deployment.strategies) {
|
|
770
|
+
const runtimeKey = buildRuntimeStrategyIdentityKey({
|
|
771
|
+
strategyName: deploymentStrategy.strategyName,
|
|
772
|
+
configId: `deployment-${deployment.id}`,
|
|
773
|
+
universe: deployment.universe,
|
|
774
|
+
accountId: deployment.accountId,
|
|
775
|
+
deploymentId: deployment.id,
|
|
776
|
+
policyProfileId: deploymentStrategy.policyProfileId
|
|
777
|
+
});
|
|
778
|
+
identityByKey.set(runtimeKey, {
|
|
779
|
+
strategyName: deploymentStrategy.strategyName,
|
|
780
|
+
configId: `deployment-${deployment.id}`,
|
|
781
|
+
interval: String(deployment.interval),
|
|
782
|
+
universe: deployment.universe,
|
|
783
|
+
accountId: deployment.accountId,
|
|
784
|
+
accountLabel: accountsById.get(deployment.accountId)?.label,
|
|
785
|
+
deploymentId: deployment.id,
|
|
786
|
+
policyProfileId: deploymentStrategy.policyProfileId,
|
|
787
|
+
releaseCompositionId: deploymentStrategy.releaseCompositionId,
|
|
788
|
+
enabled: deployment.enabled && deploymentStrategy.enabled !== false,
|
|
789
|
+
config: deploymentStrategy.config,
|
|
790
|
+
connected: false,
|
|
791
|
+
configFingerprint: strategyLogicConfigFingerprint(
|
|
792
|
+
deploymentStrategy.config
|
|
793
|
+
)
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
for (const runtimeConfig of runtimeStrategyConfigs) {
|
|
798
|
+
const universe = runtimeConfig.config.UNIVERSE === "tradfi" ? "tradfi" : "crypto";
|
|
799
|
+
const configuredAccountId = typeof runtimeConfig.config.ACCOUNT_ID === "string" && runtimeConfig.config.ACCOUNT_ID.trim() ? runtimeConfig.config.ACCOUNT_ID.trim() : void 0;
|
|
800
|
+
const resolvedAccount = await resolveTradingAccount({
|
|
801
|
+
userName,
|
|
802
|
+
accountId: configuredAccountId,
|
|
803
|
+
provider,
|
|
804
|
+
universe
|
|
805
|
+
}).catch(() => null);
|
|
806
|
+
const accountId = resolvedAccount?.id ?? configuredAccountId;
|
|
807
|
+
runtimeConfigAccountScopes.push({
|
|
808
|
+
strategyName: runtimeConfig.strategyName,
|
|
809
|
+
configId: runtimeConfig.configId,
|
|
810
|
+
universe,
|
|
811
|
+
accountId
|
|
812
|
+
});
|
|
813
|
+
const runtimeKey = buildRuntimeStrategyIdentityKey({
|
|
814
|
+
strategyName: runtimeConfig.strategyName,
|
|
815
|
+
configId: runtimeConfig.configId,
|
|
816
|
+
universe,
|
|
817
|
+
accountId
|
|
818
|
+
});
|
|
819
|
+
identityByKey.set(runtimeKey, {
|
|
820
|
+
strategyName: runtimeConfig.strategyName,
|
|
821
|
+
configId: runtimeConfig.configId,
|
|
822
|
+
interval: String(runtimeConfig.config.INTERVAL ?? "15"),
|
|
823
|
+
universe,
|
|
824
|
+
accountId,
|
|
825
|
+
accountLabel: accountId ? accountsById.get(accountId)?.label : void 0,
|
|
826
|
+
enabled: isRuntimeStrategyConfigEnabled(runtimeConfig.config),
|
|
827
|
+
config: runtimeConfig.config,
|
|
828
|
+
connected: true,
|
|
829
|
+
configFingerprint: strategyLogicConfigFingerprint(runtimeConfig.config)
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
const accountScopedTrades = assignLegacyRuntimeTradeAccountScopes(
|
|
833
|
+
allTrades,
|
|
834
|
+
runtimeConfigAccountScopes
|
|
835
|
+
);
|
|
836
|
+
for (const trade of accountScopedTrades) {
|
|
837
|
+
const key = runtimeIdentityKey(trade);
|
|
838
|
+
const configuredCompositionId = identityByKey.get(key)?.releaseCompositionId;
|
|
839
|
+
const observedCompositionId = trade.runtimeLineage?.compositionId;
|
|
840
|
+
const releaseCompositionId = configuredCompositionId && observedCompositionId && configuredCompositionId !== observedCompositionId ? void 0 : observedCompositionId ?? configuredCompositionId;
|
|
841
|
+
identityByKey.set(key, {
|
|
842
|
+
...identityByKey.get(key),
|
|
843
|
+
strategyName: trade.strategy,
|
|
844
|
+
configId: trade.runtimeConfigId ?? "config",
|
|
845
|
+
interval: String(trade.interval ?? "15"),
|
|
846
|
+
universe: trade.universe ?? "crypto",
|
|
847
|
+
accountId: trade.accountId,
|
|
848
|
+
accountLabel: trade.accountId ? accountsById.get(trade.accountId)?.label : void 0,
|
|
849
|
+
deploymentId: trade.deploymentId,
|
|
850
|
+
policyProfileId: trade.policyProfileId,
|
|
851
|
+
releaseCompositionId,
|
|
852
|
+
configFingerprint: trade.runtimeLineage?.configFingerprint,
|
|
853
|
+
gateFingerprint: trade.runtimeLineage?.gateFingerprint,
|
|
854
|
+
contextFingerprint: trade.runtimeLineage?.contextFingerprint,
|
|
855
|
+
gitSha: trade.runtimeLineage?.gitSha ?? void 0,
|
|
856
|
+
maxLossValue: trade.runtimeLineage?.maxLossValue ?? void 0
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
const evidenceTimelines = await loadStrategyEvidenceTimelines({
|
|
860
|
+
projectRoot,
|
|
861
|
+
markerDir: process.env.STRATEGY_RELEASE_MARKER_DIR,
|
|
862
|
+
selectors: [...identityByKey.values()].map((identity) => ({
|
|
863
|
+
strategy: identity.strategyName,
|
|
864
|
+
compositionId: identity.releaseCompositionId,
|
|
865
|
+
configFingerprint: identity.configFingerprint,
|
|
866
|
+
gateFingerprint: identity.gateFingerprint,
|
|
867
|
+
contextFingerprint: identity.contextFingerprint,
|
|
868
|
+
gitSha: identity.gitSha,
|
|
869
|
+
maxLossValue: identity.maxLossValue,
|
|
870
|
+
requireCompleteLineage: true
|
|
871
|
+
})),
|
|
872
|
+
startTime,
|
|
873
|
+
endTime
|
|
874
|
+
});
|
|
875
|
+
const strategies = await Promise.all(
|
|
876
|
+
[...identityByKey.entries()].map(async ([runtimeKey, identity]) => {
|
|
877
|
+
const { strategyName } = identity;
|
|
878
|
+
const strategyTrades = accountScopedTrades.filter((trade) => runtimeIdentityKey(trade) === runtimeKey).sort((left, right) => right.entryTimestamp - left.entryTimestamp);
|
|
879
|
+
const orders = strategyTrades.sort((left, right) => {
|
|
880
|
+
const leftDate = left.exitTimestamp ?? left.entryTimestamp;
|
|
881
|
+
const rightDate = right.exitTimestamp ?? right.entryTimestamp;
|
|
882
|
+
return rightDate - leftDate;
|
|
883
|
+
}).map((trade) => toRuntimeTradeView(trade, endTime));
|
|
884
|
+
const analytics = buildRuntimeStrategyAnalytics({
|
|
885
|
+
trades: strategyTrades,
|
|
886
|
+
startTime,
|
|
887
|
+
endTime
|
|
888
|
+
});
|
|
889
|
+
const effectiveStrategyConfig = identity.config ?? null;
|
|
890
|
+
return {
|
|
891
|
+
runtimeKey,
|
|
892
|
+
strategyName,
|
|
893
|
+
configId: identity.configId,
|
|
894
|
+
interval: identity.interval,
|
|
895
|
+
universe: identity.universe,
|
|
896
|
+
accountId: identity.accountId,
|
|
897
|
+
accountLabel: identity.accountLabel,
|
|
898
|
+
deploymentId: identity.deploymentId,
|
|
899
|
+
policyProfileId: identity.policyProfileId,
|
|
900
|
+
connected: identity.connected ?? connectedSet.has(`${strategyName}:${identity.configId}`),
|
|
901
|
+
enabled: identity.enabled ?? isRuntimeStrategyConfigEnabled(effectiveStrategyConfig),
|
|
902
|
+
config: effectiveStrategyConfig,
|
|
903
|
+
symbols: [...new Set(strategyTrades.map((trade) => trade.symbol))],
|
|
904
|
+
stat: analytics.stat,
|
|
905
|
+
summary: analytics.summary,
|
|
906
|
+
orderLog: analytics.orderLog,
|
|
907
|
+
evidenceTimeline: evidenceTimelines.get(
|
|
908
|
+
strategyEvidenceTimelineSelectorKey({
|
|
909
|
+
strategy: strategyName,
|
|
910
|
+
compositionId: identity.releaseCompositionId,
|
|
911
|
+
configFingerprint: identity.configFingerprint,
|
|
912
|
+
gateFingerprint: identity.gateFingerprint,
|
|
913
|
+
contextFingerprint: identity.contextFingerprint,
|
|
914
|
+
gitSha: identity.gitSha,
|
|
915
|
+
maxLossValue: identity.maxLossValue,
|
|
916
|
+
requireCompleteLineage: true
|
|
917
|
+
})
|
|
918
|
+
) ?? {
|
|
919
|
+
status: "missing",
|
|
920
|
+
observedFrom: null,
|
|
921
|
+
markers: []
|
|
922
|
+
},
|
|
923
|
+
recentTrades: strategyTrades.slice(0, 8).map((trade) => toRuntimeTradeView(trade, endTime)),
|
|
924
|
+
orders
|
|
925
|
+
};
|
|
926
|
+
})
|
|
927
|
+
);
|
|
928
|
+
strategies.sort((left, right) => {
|
|
929
|
+
if (left.stat.netProfit !== right.stat.netProfit) {
|
|
930
|
+
return right.stat.netProfit - left.stat.netProfit;
|
|
931
|
+
}
|
|
932
|
+
if (left.summary.totalPnl !== right.summary.totalPnl) {
|
|
933
|
+
return right.summary.totalPnl - left.summary.totalPnl;
|
|
934
|
+
}
|
|
935
|
+
if (left.connected !== right.connected) {
|
|
936
|
+
return left.connected ? -1 : 1;
|
|
937
|
+
}
|
|
938
|
+
return left.strategyName.localeCompare(right.strategyName);
|
|
939
|
+
});
|
|
940
|
+
const response = {
|
|
941
|
+
provider,
|
|
942
|
+
hours,
|
|
943
|
+
generatedAt: endTime,
|
|
944
|
+
dataSources: {
|
|
945
|
+
localTrades: syncedTrades.length,
|
|
946
|
+
exchangeFallbackTrades: fallbackTrades.length,
|
|
947
|
+
exchangeErrors: [...new Set(exchangeErrors)].sort()
|
|
948
|
+
},
|
|
949
|
+
strategies
|
|
950
|
+
};
|
|
951
|
+
return response;
|
|
952
|
+
};
|
|
953
|
+
export {
|
|
954
|
+
loadRuntimeDashboard
|
|
955
|
+
};
|