@tradejs/node 3.1.29-beta.261 → 3.1.29-beta.263
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backtest.js +189 -75
- package/dist/backtest.mjs +3 -3
- package/dist/{chunk-ZZQLTUTP.mjs → chunk-GHSLOJLE.mjs} +195 -69
- package/dist/chunk-JQOLTLEY.mjs +0 -0
- package/dist/{chunk-WGL7XJ3W.mjs → chunk-SKY2HNZU.mjs} +1 -1
- package/dist/{chunk-3BBXGZ7M.mjs → chunk-YR55Q355.mjs} +11 -11
- package/dist/chunk-ZUMUI37V.mjs +0 -0
- package/dist/connectors.mjs +1 -0
- package/dist/diagnostics.d.mts +182 -0
- package/dist/diagnostics.d.ts +182 -0
- package/dist/diagnostics.js +9060 -0
- package/dist/diagnostics.mjs +867 -0
- package/dist/jev.d.mts +64 -69
- package/dist/jev.d.ts +64 -69
- package/dist/jev.js +193 -75
- package/dist/jev.mjs +6 -2
- package/dist/{jevTraining-JYXUVAZG.mjs → jevTraining-J4Y4VCM4.mjs} +1 -1
- package/dist/mcp.d.mts +203 -0
- package/dist/mcp.d.ts +203 -0
- package/dist/mcp.js +9257 -0
- package/dist/mcp.mjs +584 -0
- package/dist/registry.js +189 -75
- package/dist/registry.mjs +4 -3
- package/dist/runtimeDashboard.js +189 -75
- package/dist/runtimeDashboard.mjs +7 -7
- package/dist/runtimeStrategies.js +189 -75
- package/dist/runtimeStrategies.mjs +3 -3
- package/dist/strategies.js +189 -75
- package/dist/strategies.mjs +3 -3
- package/package.json +17 -6
package/dist/mcp.mjs
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import "./chunk-JQOLTLEY.mjs";
|
|
2
|
+
import "./chunk-ZUMUI37V.mjs";
|
|
3
|
+
import {
|
|
4
|
+
getConnectorCreatorByProvider
|
|
5
|
+
} from "./chunk-QVKUROR2.mjs";
|
|
6
|
+
import {
|
|
7
|
+
listRuntimeDeployments,
|
|
8
|
+
loadResolvedRuntimeStrategies
|
|
9
|
+
} from "./chunk-BO42QWAB.mjs";
|
|
10
|
+
import "./chunk-SKY2HNZU.mjs";
|
|
11
|
+
import {
|
|
12
|
+
ensureIndicatorPluginsLoaded
|
|
13
|
+
} from "./chunk-UL5Z46JL.mjs";
|
|
14
|
+
import "./chunk-LZDXRXIU.mjs";
|
|
15
|
+
import "./chunk-YR55Q355.mjs";
|
|
16
|
+
import "./chunk-GHSLOJLE.mjs";
|
|
17
|
+
import "./chunk-Y6FXYEAI.mjs";
|
|
18
|
+
|
|
19
|
+
// src/mcp.ts
|
|
20
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
21
|
+
import { createIndicators } from "@tradejs/core/indicators";
|
|
22
|
+
import { intervalToMs } from "@tradejs/core/data";
|
|
23
|
+
import {
|
|
24
|
+
getDataStrict as getDataStrict2,
|
|
25
|
+
getHashData,
|
|
26
|
+
getHashJsonField,
|
|
27
|
+
mcpStorage as mcpStorage2,
|
|
28
|
+
redisKeys as redisKeys2,
|
|
29
|
+
scanDataKeys,
|
|
30
|
+
scanHashJson
|
|
31
|
+
} from "@tradejs/infra/redis";
|
|
32
|
+
import { getRuntimeDeploymentHeartbeat } from "@tradejs/infra/runtimeHeartbeats";
|
|
33
|
+
import { getTradingAccount } from "@tradejs/infra/tradingAccounts";
|
|
34
|
+
import { getRuntimeStorageDayKey } from "@tradejs/core/time";
|
|
35
|
+
|
|
36
|
+
// src/mcpJobs.ts
|
|
37
|
+
import { createHash } from "crypto";
|
|
38
|
+
import { readFile } from "fs/promises";
|
|
39
|
+
import path from "path";
|
|
40
|
+
import { getDataStrict, mcpStorage, redisKeys } from "@tradejs/infra/redis";
|
|
41
|
+
var MCP_JOB_TTL = 7 * 86400;
|
|
42
|
+
var hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
43
|
+
var mcpJobStorageId = (userName, id) => `jobs:${hash(userName)}:${id}`;
|
|
44
|
+
var mcpJobDirectory = (job) => path.join(
|
|
45
|
+
String(process.env.PROJECT_CWD || process.cwd()),
|
|
46
|
+
"data/mcp",
|
|
47
|
+
hash(job.userName),
|
|
48
|
+
job.id
|
|
49
|
+
);
|
|
50
|
+
var mcpPackageManifestHash = async () => hash(
|
|
51
|
+
await readFile(
|
|
52
|
+
path.join(
|
|
53
|
+
String(process.env.PROJECT_CWD || process.cwd()),
|
|
54
|
+
"runtime-package-manifest.json"
|
|
55
|
+
),
|
|
56
|
+
"utf8"
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
var assertWindow = (startTime, endTime, maxDays) => {
|
|
60
|
+
if (!Number.isSafeInteger(startTime) || !Number.isSafeInteger(endTime) || startTime <= 0 || endTime <= startTime || endTime > Date.now() || endTime - startTime > maxDays * 864e5)
|
|
61
|
+
throw new Error(`Select a past window of at most ${maxDays} days`);
|
|
62
|
+
};
|
|
63
|
+
var enqueueMcpJob = async (principal, kind, request, idempotencyKey) => {
|
|
64
|
+
if (!/^[A-Za-z0-9_-]{8,100}$/.test(idempotencyKey))
|
|
65
|
+
throw new Error(
|
|
66
|
+
"Provide an idempotencyKey of 8\u2013100 letters, digits, underscores or hyphens"
|
|
67
|
+
);
|
|
68
|
+
const id = hash(
|
|
69
|
+
`${principal.userName}:${principal.clientId}:${idempotencyKey}`
|
|
70
|
+
);
|
|
71
|
+
const storageId = mcpJobStorageId(principal.userName, id);
|
|
72
|
+
const existing = await mcpStorage.get(storageId);
|
|
73
|
+
if (existing) {
|
|
74
|
+
if (existing.kind !== kind || existing.request.requestFingerprint !== hash(JSON.stringify(request)))
|
|
75
|
+
throw new Error("Idempotency key was used for another request");
|
|
76
|
+
return existing;
|
|
77
|
+
}
|
|
78
|
+
const worker = await mcpStorage.get(
|
|
79
|
+
"worker:heartbeat"
|
|
80
|
+
);
|
|
81
|
+
if (!worker) throw new Error("MCP worker is unavailable; no job was queued");
|
|
82
|
+
if (!await mcpStorage.rateLimit(`jobs:${hash(principal.userName)}`, 10, 3600))
|
|
83
|
+
throw new Error("Hourly job limit reached");
|
|
84
|
+
const job = {
|
|
85
|
+
id,
|
|
86
|
+
userName: principal.userName,
|
|
87
|
+
clientId: principal.clientId,
|
|
88
|
+
kind,
|
|
89
|
+
status: "queued",
|
|
90
|
+
request: { ...request, requestFingerprint: hash(JSON.stringify(request)) },
|
|
91
|
+
args: [],
|
|
92
|
+
createdAt: Date.now(),
|
|
93
|
+
updatedAt: Date.now(),
|
|
94
|
+
logs: [],
|
|
95
|
+
outputId: id,
|
|
96
|
+
packageManifestSha256: await mcpPackageManifestHash()
|
|
97
|
+
};
|
|
98
|
+
const outputDir = mcpJobDirectory(job);
|
|
99
|
+
if (kind === "backtest") {
|
|
100
|
+
assertWindow(Number(request.startTime), Number(request.endTime), 90);
|
|
101
|
+
const configId = String(request.configId || "");
|
|
102
|
+
if (!/^[A-Za-z][A-Za-z0-9]*:[A-Za-z0-9_-]+$/.test(configId))
|
|
103
|
+
throw new Error("Select a named Strategy:config");
|
|
104
|
+
const grid = await getDataStrict(
|
|
105
|
+
redisKeys.backtestConfig(principal.userName, configId)
|
|
106
|
+
);
|
|
107
|
+
if (!grid || typeof grid !== "object" || Array.isArray(grid))
|
|
108
|
+
throw new Error("Backtest config not found");
|
|
109
|
+
const cells = Object.values(grid);
|
|
110
|
+
if (!cells.length || !cells.every((cell) => Array.isArray(cell) && cell.length > 0) || cells.reduce((n, cell) => n * cell.length, 1) > 8)
|
|
111
|
+
throw new Error("MCP backtests allow at most eight config combinations");
|
|
112
|
+
const symbols = request.symbols;
|
|
113
|
+
if (!Array.isArray(symbols) || !symbols.length || symbols.length > 20 || !symbols.every(
|
|
114
|
+
(symbol) => typeof symbol === "string" && /^[A-Z0-9._-]{1,40}$/.test(symbol)
|
|
115
|
+
))
|
|
116
|
+
throw new Error("Select 1\u201320 symbols");
|
|
117
|
+
const frozenConfigId = `${configId.split(":")[0]}:mcp_${id}`;
|
|
118
|
+
job.request = {
|
|
119
|
+
...job.request,
|
|
120
|
+
resolvedConfig: grid,
|
|
121
|
+
configSha256: hash(JSON.stringify(grid)),
|
|
122
|
+
frozenConfigId
|
|
123
|
+
};
|
|
124
|
+
job.args = [
|
|
125
|
+
"backtest",
|
|
126
|
+
"--user",
|
|
127
|
+
principal.userName,
|
|
128
|
+
"--config",
|
|
129
|
+
frozenConfigId,
|
|
130
|
+
"--connector",
|
|
131
|
+
String(request.provider),
|
|
132
|
+
"--timeframe",
|
|
133
|
+
String(request.interval),
|
|
134
|
+
"--startTime",
|
|
135
|
+
String(request.startTime),
|
|
136
|
+
"--endTime",
|
|
137
|
+
String(request.endTime),
|
|
138
|
+
"--tickers",
|
|
139
|
+
symbols.join(","),
|
|
140
|
+
"--parallel",
|
|
141
|
+
"1",
|
|
142
|
+
"--cacheOnly",
|
|
143
|
+
"--fast",
|
|
144
|
+
"--persistResults"
|
|
145
|
+
];
|
|
146
|
+
} else if (kind === "runtime-evidence") {
|
|
147
|
+
assertWindow(Number(request.startTime), Number(request.endTime), 7);
|
|
148
|
+
job.args = [
|
|
149
|
+
"runtime-evidence",
|
|
150
|
+
"--user",
|
|
151
|
+
principal.userName,
|
|
152
|
+
"--deployment",
|
|
153
|
+
String(request.deploymentId),
|
|
154
|
+
"--startTime",
|
|
155
|
+
String(request.startTime),
|
|
156
|
+
"--endTime",
|
|
157
|
+
String(request.endTime),
|
|
158
|
+
"--publishDir",
|
|
159
|
+
outputDir
|
|
160
|
+
];
|
|
161
|
+
} else if (kind === "runtime-feedback-replay") {
|
|
162
|
+
if (!worker.replayAvailable)
|
|
163
|
+
throw new Error("MCP replay worker requires an isolated Redis host");
|
|
164
|
+
if (typeof request.verifiedEvidencePath !== "string")
|
|
165
|
+
throw new Error("Select verified runtime evidence");
|
|
166
|
+
job.args = [
|
|
167
|
+
"runtime-feedback-replay",
|
|
168
|
+
"--runtimeEvidence",
|
|
169
|
+
request.verifiedEvidencePath,
|
|
170
|
+
"--outDir",
|
|
171
|
+
outputDir,
|
|
172
|
+
"--runId",
|
|
173
|
+
id
|
|
174
|
+
];
|
|
175
|
+
} else {
|
|
176
|
+
throw new Error(
|
|
177
|
+
"Unsupported diagnostic kind; use verified runtime feedback for deployment-bound parity"
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (!await mcpStorage.put(storageId, job, MCP_JOB_TTL, true)) {
|
|
181
|
+
const winner = await readMcpJob(principal.userName, id);
|
|
182
|
+
if (winner.kind !== kind || winner.request.requestFingerprint !== job.request.requestFingerprint)
|
|
183
|
+
throw new Error("Idempotency key was used for another request");
|
|
184
|
+
return winner;
|
|
185
|
+
}
|
|
186
|
+
return job;
|
|
187
|
+
};
|
|
188
|
+
var readMcpJob = async (userName, id) => {
|
|
189
|
+
if (!/^[a-f0-9]{64}$/.test(id)) throw new Error("Invalid job id");
|
|
190
|
+
const job = await mcpStorage.get(mcpJobStorageId(userName, id));
|
|
191
|
+
if (!job || job.userName !== userName) throw new Error("Job not found");
|
|
192
|
+
return job;
|
|
193
|
+
};
|
|
194
|
+
var cancelMcpJob = async (principal, id) => {
|
|
195
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
196
|
+
const job = await readMcpJob(principal.userName, id);
|
|
197
|
+
if (job.clientId !== principal.clientId)
|
|
198
|
+
throw new Error("Only the submitting client can cancel this job");
|
|
199
|
+
if (!["queued", "running"].includes(job.status)) return job;
|
|
200
|
+
const next = {
|
|
201
|
+
...job,
|
|
202
|
+
cancelRequested: true,
|
|
203
|
+
updatedAt: Date.now(),
|
|
204
|
+
...job.status === "queued" ? { status: "cancelled", finishedAt: Date.now() } : {}
|
|
205
|
+
};
|
|
206
|
+
if (await mcpStorage.compare(
|
|
207
|
+
mcpJobStorageId(job.userName, job.id),
|
|
208
|
+
job,
|
|
209
|
+
next,
|
|
210
|
+
MCP_JOB_TTL
|
|
211
|
+
))
|
|
212
|
+
return next;
|
|
213
|
+
}
|
|
214
|
+
throw new Error("Job changed; retry cancellation");
|
|
215
|
+
};
|
|
216
|
+
var publicMcpJob = (job) => {
|
|
217
|
+
const { args: _args, logs: _logs, request: _request, ...view } = job;
|
|
218
|
+
return {
|
|
219
|
+
...view,
|
|
220
|
+
request: Object.fromEntries(
|
|
221
|
+
Object.entries(job.request).filter(
|
|
222
|
+
([key]) => !["verifiedEvidencePath", "resolvedConfig"].includes(key)
|
|
223
|
+
)
|
|
224
|
+
),
|
|
225
|
+
logLines: job.logs.length
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// src/mcp.ts
|
|
230
|
+
var mcpProjectRoot = () => String(process.env.PROJECT_CWD || process.cwd());
|
|
231
|
+
var isMcpRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
232
|
+
var mcpId = (value) => createHash2("sha256").update(value).digest("hex");
|
|
233
|
+
var redactMcpData = (value) => {
|
|
234
|
+
if (Array.isArray(value)) return value.map(redactMcpData);
|
|
235
|
+
if (!isMcpRecord(value)) return value;
|
|
236
|
+
return Object.fromEntries(
|
|
237
|
+
Object.entries(value).filter(
|
|
238
|
+
([key]) => !/secret|password|authorization|api.?key|private.?key|access.?token|refresh.?token|bot.?token|cookie|credential/i.test(
|
|
239
|
+
key
|
|
240
|
+
)
|
|
241
|
+
).map(([key, nested]) => [key, redactMcpData(nested)])
|
|
242
|
+
);
|
|
243
|
+
};
|
|
244
|
+
var connectorFor = async (userName, provider, universe) => {
|
|
245
|
+
const creator = await getConnectorCreatorByProvider(
|
|
246
|
+
provider,
|
|
247
|
+
mcpProjectRoot()
|
|
248
|
+
);
|
|
249
|
+
if (!creator) throw new Error("Unknown market provider");
|
|
250
|
+
return creator({ userName, universe });
|
|
251
|
+
};
|
|
252
|
+
var readMcpMarket = async (userName, provider, universe, offset, limit, symbol) => {
|
|
253
|
+
const connector = await connectorFor(userName, provider, universe);
|
|
254
|
+
const tickers = await connector.getTickers();
|
|
255
|
+
const rows = symbol ? tickers.filter((row) => row.symbol === symbol) : tickers;
|
|
256
|
+
return {
|
|
257
|
+
provider,
|
|
258
|
+
universe,
|
|
259
|
+
items: rows.slice(offset, offset + limit),
|
|
260
|
+
total: rows.length,
|
|
261
|
+
nextOffset: offset + limit < rows.length ? offset + limit : null,
|
|
262
|
+
observedAt: Date.now(),
|
|
263
|
+
source: "connector"
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
var readMcpChart = async (userName, request) => {
|
|
267
|
+
const connector = await connectorFor(
|
|
268
|
+
userName,
|
|
269
|
+
request.provider,
|
|
270
|
+
request.universe
|
|
271
|
+
);
|
|
272
|
+
const width = intervalToMs(request.interval);
|
|
273
|
+
const end = Math.floor(Math.min(request.endTime, Date.now()) / width) * width;
|
|
274
|
+
const visibleStart = end - request.bars * width;
|
|
275
|
+
const start = Math.max(0, visibleStart - 200 * width);
|
|
276
|
+
const fetched = await connector.kline({
|
|
277
|
+
symbol: request.symbol,
|
|
278
|
+
interval: request.interval,
|
|
279
|
+
start,
|
|
280
|
+
end,
|
|
281
|
+
cacheOnly: request.cacheOnly
|
|
282
|
+
});
|
|
283
|
+
const candles = fetched.filter(
|
|
284
|
+
(candle) => candle.timestamp >= start && candle.timestamp < end
|
|
285
|
+
);
|
|
286
|
+
const btc = request.universe === "tradfi" ? [] : request.symbol === "BTCUSDT" ? candles : await connector.kline({
|
|
287
|
+
symbol: "BTCUSDT",
|
|
288
|
+
interval: request.interval,
|
|
289
|
+
start,
|
|
290
|
+
end,
|
|
291
|
+
cacheOnly: request.cacheOnly
|
|
292
|
+
});
|
|
293
|
+
const visibleCandles = candles.filter((candle) => candle.timestamp >= visibleStart).slice(-request.bars);
|
|
294
|
+
await ensureIndicatorPluginsLoaded(mcpProjectRoot());
|
|
295
|
+
const allIndicators = createIndicators(
|
|
296
|
+
candles,
|
|
297
|
+
btc.filter((candle) => candle.timestamp < end),
|
|
298
|
+
{
|
|
299
|
+
includeMlPayload: false,
|
|
300
|
+
pluginRegistryScope: mcpProjectRoot()
|
|
301
|
+
}
|
|
302
|
+
).result();
|
|
303
|
+
return {
|
|
304
|
+
...request,
|
|
305
|
+
startTime: visibleStart,
|
|
306
|
+
historyStartTime: start,
|
|
307
|
+
warmupBars: 200,
|
|
308
|
+
endTime: end,
|
|
309
|
+
candles: visibleCandles,
|
|
310
|
+
indicators: Object.fromEntries(
|
|
311
|
+
(request.indicatorNames || ["maFast", "maSlow", "atr", "macd"]).map(
|
|
312
|
+
(key) => [
|
|
313
|
+
key,
|
|
314
|
+
Array.isArray(allIndicators[key]) ? allIndicators[key].slice(
|
|
315
|
+
-request.bars
|
|
316
|
+
) : allIndicators[key] ?? null
|
|
317
|
+
]
|
|
318
|
+
)
|
|
319
|
+
),
|
|
320
|
+
availableIndicators: Object.keys(allIndicators),
|
|
321
|
+
observedAt: Date.now(),
|
|
322
|
+
source: request.cacheOnly ? "cached_history" : "connector",
|
|
323
|
+
closedCandlesOnly: true,
|
|
324
|
+
coverage: {
|
|
325
|
+
requestedBars: request.bars,
|
|
326
|
+
availableBars: candles.filter(
|
|
327
|
+
(candle) => candle.timestamp >= visibleStart
|
|
328
|
+
).length,
|
|
329
|
+
complete: candles.filter((candle) => candle.timestamp >= visibleStart).length >= request.bars
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
};
|
|
333
|
+
var readMcpDeployments = async (userName) => {
|
|
334
|
+
const deployments = await listRuntimeDeployments({
|
|
335
|
+
userName,
|
|
336
|
+
projectRoot: mcpProjectRoot()
|
|
337
|
+
});
|
|
338
|
+
const allowed = [];
|
|
339
|
+
for (const deployment of deployments) {
|
|
340
|
+
if (!await getTradingAccount(userName, deployment.accountId)) continue;
|
|
341
|
+
allowed.push({
|
|
342
|
+
...deployment,
|
|
343
|
+
heartbeat: await getRuntimeDeploymentHeartbeat(userName, deployment.id)
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
deployments: allowed,
|
|
348
|
+
observedAt: Date.now(),
|
|
349
|
+
source: "project_declaration_and_runtime_storage"
|
|
350
|
+
};
|
|
351
|
+
};
|
|
352
|
+
var requireMcpDeployment = async (userName, deploymentId) => {
|
|
353
|
+
const { deployments } = await readMcpDeployments(userName);
|
|
354
|
+
const deployment = deployments.find((row) => row.id === deploymentId);
|
|
355
|
+
if (!deployment) throw new Error("Deployment unavailable for this user");
|
|
356
|
+
return deployment;
|
|
357
|
+
};
|
|
358
|
+
var readMcpStrategies = async (userName, deploymentId) => {
|
|
359
|
+
const deployment = await requireMcpDeployment(userName, deploymentId);
|
|
360
|
+
const rows = await loadResolvedRuntimeStrategies({
|
|
361
|
+
userName,
|
|
362
|
+
projectRoot: mcpProjectRoot(),
|
|
363
|
+
deploymentId
|
|
364
|
+
});
|
|
365
|
+
return {
|
|
366
|
+
deploymentId,
|
|
367
|
+
accountId: deployment.accountId,
|
|
368
|
+
strategies: rows.map(({ strategyCreator: _creator, ...row }) => row),
|
|
369
|
+
observedAt: Date.now()
|
|
370
|
+
};
|
|
371
|
+
};
|
|
372
|
+
var readMcpRuntimePage = async (userName, kind, request) => {
|
|
373
|
+
const deployment = await requireMcpDeployment(userName, request.deploymentId);
|
|
374
|
+
const prefix = kind === "signals" ? redisKeys2.runtimeSignalBuckets(userName) : kind === "evaluations" ? redisKeys2.runtimeSignalEvaluationBuckets(userName) : redisKeys2.runtimeTrades(userName);
|
|
375
|
+
const { cursor: _cursor, ...filters } = request;
|
|
376
|
+
const binding = mcpId(JSON.stringify({ userName, kind, ...filters }));
|
|
377
|
+
let state = {
|
|
378
|
+
binding,
|
|
379
|
+
scan: "0",
|
|
380
|
+
keys: [],
|
|
381
|
+
hash: "0",
|
|
382
|
+
values: [],
|
|
383
|
+
scanned: false
|
|
384
|
+
};
|
|
385
|
+
if (request.cursor !== "0") {
|
|
386
|
+
const saved = await mcpStorage2.get(
|
|
387
|
+
`cursor:${request.cursor}`
|
|
388
|
+
);
|
|
389
|
+
if (!saved || saved.binding !== binding)
|
|
390
|
+
throw new Error("Invalid storage cursor");
|
|
391
|
+
state = saved;
|
|
392
|
+
}
|
|
393
|
+
if (!state.keys.length && !state.values.length && (!state.scanned || state.scan !== "0")) {
|
|
394
|
+
const page = await scanDataKeys(prefix, state.scan);
|
|
395
|
+
state = { ...state, scan: page.cursor, keys: page.keys, scanned: true };
|
|
396
|
+
}
|
|
397
|
+
if (!state.values.length && state.keys.length) {
|
|
398
|
+
const key = state.keys[0];
|
|
399
|
+
if (kind === "orders") {
|
|
400
|
+
state.keys.shift();
|
|
401
|
+
if (!key.slice(prefix.length).startsWith("days:"))
|
|
402
|
+
state.values = [await getDataStrict2(key)];
|
|
403
|
+
} else {
|
|
404
|
+
const page = await scanHashJson(key, state.hash);
|
|
405
|
+
state.values = page.values;
|
|
406
|
+
state.hash = page.cursor;
|
|
407
|
+
if (page.cursor === "0") state.keys.shift();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const items = [];
|
|
411
|
+
for (let row of state.values.splice(0, 50)) {
|
|
412
|
+
if (kind === "signals" && isMcpRecord(row) && typeof row.symbol === "string" && typeof row.signalId === "string")
|
|
413
|
+
row = await getDataStrict2(
|
|
414
|
+
redisKeys2.storeSignal(row.symbol, row.signalId)
|
|
415
|
+
);
|
|
416
|
+
if (!isMcpRecord(row) || row.deploymentId !== request.deploymentId || row.accountId !== deployment.accountId || row.userName !== void 0 && row.userName !== userName || request.strategy && row.strategy !== request.strategy || request.symbol && row.symbol !== request.symbol)
|
|
417
|
+
continue;
|
|
418
|
+
const timestamp = Number(
|
|
419
|
+
row.timestamp ?? row.entryTimestamp ?? row.createdAt
|
|
420
|
+
);
|
|
421
|
+
if (timestamp < request.startTime || timestamp >= request.endTime || !Number.isFinite(timestamp))
|
|
422
|
+
continue;
|
|
423
|
+
const {
|
|
424
|
+
figures: _figures,
|
|
425
|
+
indicators: _indicators,
|
|
426
|
+
additionalIndicators: _additional,
|
|
427
|
+
...summary
|
|
428
|
+
} = row;
|
|
429
|
+
items.push(summary);
|
|
430
|
+
}
|
|
431
|
+
let next = "0";
|
|
432
|
+
if (state.values.length || state.keys.length || state.scan !== "0") {
|
|
433
|
+
next = mcpId(randomUUID());
|
|
434
|
+
await mcpStorage2.put(`cursor:${next}`, state, 600);
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
items,
|
|
438
|
+
cursor: next,
|
|
439
|
+
deploymentId: request.deploymentId,
|
|
440
|
+
observedAt: Date.now(),
|
|
441
|
+
source: "runtime_storage",
|
|
442
|
+
window: { startTime: request.startTime, endTime: request.endTime },
|
|
443
|
+
coverage: "stored_records_only",
|
|
444
|
+
note: "Missing records do not prove no evaluation occurred. Continue until cursor is 0; cursors expire after 10 minutes."
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
var readMcpSignal = async (userName, signalId, deploymentId, timestamp, strategy) => {
|
|
448
|
+
const deployment = await requireMcpDeployment(userName, deploymentId);
|
|
449
|
+
const ref = await getHashJsonField(
|
|
450
|
+
redisKeys2.runtimeSignalBucket(
|
|
451
|
+
userName,
|
|
452
|
+
getRuntimeStorageDayKey(timestamp),
|
|
453
|
+
strategy
|
|
454
|
+
),
|
|
455
|
+
signalId,
|
|
456
|
+
null
|
|
457
|
+
);
|
|
458
|
+
if (!isMcpRecord(ref) || typeof ref.symbol !== "string")
|
|
459
|
+
throw new Error("Signal not found");
|
|
460
|
+
const signal = await getDataStrict2(
|
|
461
|
+
redisKeys2.storeSignal(ref.symbol, signalId)
|
|
462
|
+
);
|
|
463
|
+
if (!isMcpRecord(signal) || signal.deploymentId !== deploymentId || signal.accountId !== deployment.accountId || signal.userName !== void 0 && signal.userName !== userName)
|
|
464
|
+
throw new Error("Signal not found");
|
|
465
|
+
return { signal, observedAt: Date.now(), source: "runtime_storage" };
|
|
466
|
+
};
|
|
467
|
+
var readMcpEvaluationStats = async (userName, deploymentId, cursor) => {
|
|
468
|
+
await requireMcpDeployment(userName, deploymentId);
|
|
469
|
+
const page = await scanDataKeys(
|
|
470
|
+
redisKeys2.runtimeSignalEvaluationStatsBuckets(userName),
|
|
471
|
+
cursor
|
|
472
|
+
);
|
|
473
|
+
const buckets = [];
|
|
474
|
+
for (const key of page.keys) {
|
|
475
|
+
const parts = key.slice(redisKeys2.runtimeSignalEvaluationStatsBuckets(userName).length).split(":");
|
|
476
|
+
if (parts.slice(1, -1).join(":") !== deploymentId) continue;
|
|
477
|
+
buckets.push({
|
|
478
|
+
bucket: key.slice(
|
|
479
|
+
redisKeys2.runtimeSignalEvaluationStatsBuckets(userName).length
|
|
480
|
+
),
|
|
481
|
+
counters: await getHashData(key)
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
buckets,
|
|
486
|
+
cursor: page.cursor,
|
|
487
|
+
observedAt: Date.now(),
|
|
488
|
+
source: "debug_telemetry",
|
|
489
|
+
immutableEvidence: false,
|
|
490
|
+
note: "Aggregate counters are not composition-bound evidence. Detailed skip records may not have been captured."
|
|
491
|
+
};
|
|
492
|
+
};
|
|
493
|
+
var readMcpBacktestPage = async (userName, kind, cursor) => {
|
|
494
|
+
const prefix = kind === "configs" ? `users:${userName}:backtests:configs:` : `users:${userName}:backtests:results:`;
|
|
495
|
+
const page = await scanDataKeys(prefix, cursor);
|
|
496
|
+
const items = [];
|
|
497
|
+
for (const key of page.keys) {
|
|
498
|
+
const value = await getDataStrict2(key);
|
|
499
|
+
items.push({
|
|
500
|
+
id: key.slice(prefix.length),
|
|
501
|
+
value: kind === "results" && isMcpRecord(value) ? {
|
|
502
|
+
config: value.config,
|
|
503
|
+
mode: value.mode,
|
|
504
|
+
startedAt: value.startedAt,
|
|
505
|
+
finishedAt: value.finishedAt,
|
|
506
|
+
successTests: value.successTests,
|
|
507
|
+
errorTests: value.errorTests,
|
|
508
|
+
resultCount: Array.isArray(value.results) ? value.results.length : 0
|
|
509
|
+
} : value
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
items,
|
|
514
|
+
cursor: page.cursor,
|
|
515
|
+
observedAt: Date.now(),
|
|
516
|
+
source: "backtest_storage"
|
|
517
|
+
};
|
|
518
|
+
};
|
|
519
|
+
var readMcpBacktestResult = async (userName, id, offset = 0, limit = 20) => {
|
|
520
|
+
if (id.includes("*") || id.includes("?") || id.includes("["))
|
|
521
|
+
throw new Error("Invalid result identifier");
|
|
522
|
+
const result = await getDataStrict2(
|
|
523
|
+
`users:${userName}:backtests:results:${id}`
|
|
524
|
+
);
|
|
525
|
+
if (!result) throw new Error("Result not found");
|
|
526
|
+
const record = isMcpRecord(result) ? result : { results: result };
|
|
527
|
+
const groups = ["results", "resultsByTickers", "resultsByStrategies"];
|
|
528
|
+
const pageGroup = (group) => Array.isArray(record[group]) ? record[group].slice(offset, offset + limit) : record[group];
|
|
529
|
+
const paged = {
|
|
530
|
+
results: pageGroup("results"),
|
|
531
|
+
resultsByTickers: pageGroup("resultsByTickers"),
|
|
532
|
+
resultsByStrategies: pageGroup("resultsByStrategies")
|
|
533
|
+
};
|
|
534
|
+
const total = Math.max(
|
|
535
|
+
0,
|
|
536
|
+
...groups.map(
|
|
537
|
+
(group) => Array.isArray(record[group]) ? record[group].length : 0
|
|
538
|
+
)
|
|
539
|
+
);
|
|
540
|
+
return {
|
|
541
|
+
id,
|
|
542
|
+
result: {
|
|
543
|
+
config: record.config,
|
|
544
|
+
mode: record.mode,
|
|
545
|
+
startedAt: record.startedAt,
|
|
546
|
+
finishedAt: record.finishedAt,
|
|
547
|
+
successTests: record.successTests,
|
|
548
|
+
errorTests: record.errorTests,
|
|
549
|
+
bestConfig: record.bestConfig,
|
|
550
|
+
mergedConfig: record.mergedConfig,
|
|
551
|
+
runtimeComparison: record.runtimeComparison,
|
|
552
|
+
...paged
|
|
553
|
+
},
|
|
554
|
+
offset,
|
|
555
|
+
total,
|
|
556
|
+
nextOffset: offset + limit < total ? offset + limit : null,
|
|
557
|
+
observedAt: Date.now(),
|
|
558
|
+
source: "backtest_storage"
|
|
559
|
+
};
|
|
560
|
+
};
|
|
561
|
+
export {
|
|
562
|
+
MCP_JOB_TTL,
|
|
563
|
+
cancelMcpJob,
|
|
564
|
+
enqueueMcpJob,
|
|
565
|
+
isMcpRecord,
|
|
566
|
+
mcpId,
|
|
567
|
+
mcpJobDirectory,
|
|
568
|
+
mcpJobStorageId,
|
|
569
|
+
mcpPackageManifestHash,
|
|
570
|
+
mcpProjectRoot,
|
|
571
|
+
publicMcpJob,
|
|
572
|
+
readMcpBacktestPage,
|
|
573
|
+
readMcpBacktestResult,
|
|
574
|
+
readMcpChart,
|
|
575
|
+
readMcpDeployments,
|
|
576
|
+
readMcpEvaluationStats,
|
|
577
|
+
readMcpJob,
|
|
578
|
+
readMcpMarket,
|
|
579
|
+
readMcpRuntimePage,
|
|
580
|
+
readMcpSignal,
|
|
581
|
+
readMcpStrategies,
|
|
582
|
+
redactMcpData,
|
|
583
|
+
requireMcpDeployment
|
|
584
|
+
};
|