@liquiditytech/rapidx-cli 1.0.26

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.
Files changed (64) hide show
  1. package/README.md +81 -0
  2. package/dist/cli/audit.js +10 -0
  3. package/dist/cli/bin.js +41 -0
  4. package/dist/cli/commands/account.js +34 -0
  5. package/dist/cli/commands/algo.js +35 -0
  6. package/dist/cli/commands/auth.js +22 -0
  7. package/dist/cli/commands/config.js +46 -0
  8. package/dist/cli/commands/doctor.js +42 -0
  9. package/dist/cli/commands/index.js +73 -0
  10. package/dist/cli/commands/market.js +26 -0
  11. package/dist/cli/commands/order.js +81 -0
  12. package/dist/cli/commands/position.js +35 -0
  13. package/dist/cli/commands/schema.js +5 -0
  14. package/dist/cli/commands/self-check.js +24 -0
  15. package/dist/cli/commands/trade-gate.js +26 -0
  16. package/dist/cli/commands/trade.js +30 -0
  17. package/dist/cli/commands/update.js +27 -0
  18. package/dist/cli/envelope.js +29 -0
  19. package/dist/cli/help.js +34 -0
  20. package/dist/cli/invocation-checker.js +39 -0
  21. package/dist/cli/mcp-entry.js +4 -0
  22. package/dist/cli/parser.js +87 -0
  23. package/dist/core/audit/redaction.js +56 -0
  24. package/dist/core/audit/writer.js +27 -0
  25. package/dist/core/client/capability-executor.js +400 -0
  26. package/dist/core/client/rapid-x-client.js +156 -0
  27. package/dist/core/client/signing.js +24 -0
  28. package/dist/core/client/symbol.js +36 -0
  29. package/dist/core/config/credential.js +42 -0
  30. package/dist/core/config/resolve.js +24 -0
  31. package/dist/core/contracts/capabilities.js +77 -0
  32. package/dist/core/contracts/compatibility.js +65 -0
  33. package/dist/core/contracts/events.js +29 -0
  34. package/dist/core/contracts/evidence.js +7 -0
  35. package/dist/core/contracts/input-schema.js +370 -0
  36. package/dist/core/contracts/types.js +1 -0
  37. package/dist/core/errors/product-error.js +74 -0
  38. package/dist/core/index.js +24 -0
  39. package/dist/core/safety/policy.js +215 -0
  40. package/dist/core/safety/raw-api.js +19 -0
  41. package/dist/core/self-check/live-read-only-probes.js +25 -0
  42. package/dist/core/self-check/live-trading-verification-probes.js +252 -0
  43. package/dist/core/self-check/run-self-check.js +91 -0
  44. package/dist/core/trading/preview.js +330 -0
  45. package/dist/core/trading/trading-verification.js +137 -0
  46. package/dist/core/update/check-update.js +295 -0
  47. package/dist/core/version.js +1 -0
  48. package/dist/mcp/audit.js +10 -0
  49. package/dist/mcp/server.js +73 -0
  50. package/dist/mcp/tool-registry.js +31 -0
  51. package/dist/mcp/tool-runner.js +144 -0
  52. package/package.json +48 -0
  53. package/packages/distribution/docs/cli-only-agent.md +12 -0
  54. package/packages/distribution/docs/cli.md +49 -0
  55. package/packages/distribution/docs/index.md +58 -0
  56. package/packages/distribution/docs/mcp.md +36 -0
  57. package/packages/distribution/docs/quickstart.md +129 -0
  58. package/packages/distribution/docs/self-check.md +7 -0
  59. package/packages/distribution/docs/skills.md +140 -0
  60. package/packages/distribution/docs/tools.md +35 -0
  61. package/packages/distribution/docs/trading-verification.md +17 -0
  62. package/packages/distribution/docs/troubleshooting/index.md +27 -0
  63. package/packages/distribution/manifests/offline-manifest.json +26 -0
  64. package/packages/distribution/registry/rapidx.mcp.json +26 -0
@@ -0,0 +1,330 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { assertInputMatchesSchema } from "../contracts/input-schema.js";
5
+ import { evaluateSafety, stableParamsHash } from "../safety/policy.js";
6
+ import { ProductError } from "../errors/product-error.js";
7
+ export function makePreviewStore() {
8
+ return { records: new Map() };
9
+ }
10
+ export function resolvePreviewStoreFile(env = process.env, cwd = process.cwd()) {
11
+ const stateDir = env.RAPIDX_STATE_DIR ? resolve(env.RAPIDX_STATE_DIR) : resolve(cwd, ".rapidx");
12
+ return join(stateDir, "preview-store.json");
13
+ }
14
+ export function loadPreviewStoreFromFile(filePath, now = new Date()) {
15
+ const store = makePreviewStore();
16
+ if (!existsSync(filePath)) {
17
+ return store;
18
+ }
19
+ const parsed = JSON.parse(readFileSync(filePath, "utf8"));
20
+ if (!Array.isArray(parsed)) {
21
+ return store;
22
+ }
23
+ for (const item of parsed) {
24
+ if (!isTradePreviewRecord(item)) {
25
+ continue;
26
+ }
27
+ if (new Date(item.expiresAt).getTime() < now.getTime()) {
28
+ continue;
29
+ }
30
+ store.records.set(item.previewId, item);
31
+ }
32
+ return store;
33
+ }
34
+ export function savePreviewStoreToFile(filePath, store) {
35
+ mkdirSync(dirname(filePath), { recursive: true });
36
+ const records = [...store.records.values()];
37
+ writeFileSync(filePath, `${JSON.stringify(records, null, 2)}\n`, { mode: 0o600 });
38
+ }
39
+ export function createTradePreview(capability, input, policy, state, store, ttlSeconds = 300) {
40
+ assertInputMatchesSchema(capability.inputSchema, input, {
41
+ allowedExtraFields: ["explicitUserConsent", "acceptedRiskText"]
42
+ });
43
+ const tradeParams = normalizeTradeParams(input);
44
+ const previewDetails = makePreviewDetails(capability, tradeParams);
45
+ const decision = evaluateSafety(capability, input, policy, state);
46
+ if (!decision.allowed) {
47
+ throw new ProductError({
48
+ code: decision.code ?? "RCORE20002",
49
+ status: "BLOCKED",
50
+ message: decision.reason ?? "Preview blocked by safety policy."
51
+ });
52
+ }
53
+ const previewId = `rpv_${randomUUID()}`;
54
+ const record = {
55
+ previewId,
56
+ capabilityId: capability.capabilityId,
57
+ paramsHash: stableParamsHash(tradeParams),
58
+ ...previewDetails,
59
+ expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
60
+ submitted: false,
61
+ status: "PASS",
62
+ confirmation: makeSubmitConfirmation(previewId)
63
+ };
64
+ store.records.set(previewId, record);
65
+ return record;
66
+ }
67
+ export function createTargetedTradePreview(targetCapability, input, policy, state, store, ttlSeconds = 300) {
68
+ assertInputMatchesSchema(targetCapability.inputSchema, input, {
69
+ allowedExtraFields: [
70
+ "targetCapabilityId",
71
+ "consentId",
72
+ "explicitUserConsent",
73
+ "acceptedRiskText",
74
+ "mcpSchemaVersion",
75
+ "skillsVersion",
76
+ "skillsSchemaVersion"
77
+ ],
78
+ ignoredRequiredFields: ["previewId", "continueConsentId"]
79
+ });
80
+ if (targetCapability.operationType !== "TRADE_WRITE") {
81
+ throw new ProductError({
82
+ code: "RCORE20002",
83
+ status: "BLOCKED",
84
+ message: "trade preview target must be a trade write capability."
85
+ });
86
+ }
87
+ if (isPreviewCapability(targetCapability.capabilityId)) {
88
+ throw new ProductError({
89
+ code: "RCORE20002",
90
+ status: "BLOCKED",
91
+ message: "trade preview target cannot be another preview capability."
92
+ });
93
+ }
94
+ const tradeParams = normalizeTradeParams(input);
95
+ const previewDetails = makePreviewDetails(targetCapability, tradeParams);
96
+ const decision = evaluateSafety(targetCapability, input, policy, state);
97
+ if (!decision.allowed) {
98
+ throw new ProductError({
99
+ code: decision.code ?? "RCORE20002",
100
+ status: "BLOCKED",
101
+ message: decision.reason ?? "Preview blocked by safety policy."
102
+ });
103
+ }
104
+ const previewId = `rpv_${randomUUID()}`;
105
+ const record = {
106
+ previewId,
107
+ capabilityId: targetCapability.capabilityId,
108
+ paramsHash: stableParamsHash(tradeParams),
109
+ ...previewDetails,
110
+ expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
111
+ submitted: false,
112
+ status: "PASS",
113
+ confirmation: makeSubmitConfirmation(previewId)
114
+ };
115
+ store.records.set(previewId, record);
116
+ return record;
117
+ }
118
+ function makeSubmitConfirmation(previewId) {
119
+ return {
120
+ submitToken: `confirm_${previewId}`,
121
+ requiredFields: ["previewId", "continueConsentId"],
122
+ instruction: "Pass submitToken as continueConsentId only after the user has confirmed this exact preview."
123
+ };
124
+ }
125
+ export function verifyPreview(store, previewId, capability, input, now = new Date()) {
126
+ if (!previewId) {
127
+ throw new ProductError({
128
+ code: "RCORE20002",
129
+ status: "BLOCKED",
130
+ message: "previewId is required before trade write submission."
131
+ });
132
+ }
133
+ const record = store.records.get(previewId);
134
+ if (!record) {
135
+ throw new ProductError({
136
+ code: "RCORE20002",
137
+ status: "BLOCKED",
138
+ message: "previewId was not found."
139
+ });
140
+ }
141
+ if (new Date(record.expiresAt).getTime() < now.getTime()) {
142
+ throw new ProductError({
143
+ code: "RCORE20002",
144
+ status: "BLOCKED",
145
+ message: "previewId has expired."
146
+ });
147
+ }
148
+ const capabilityMatches = record.capabilityId === capability.capabilityId
149
+ || ((record.capabilityId === "order.preview" || record.capabilityId === "order.place-preview") && capability.capabilityId === "order.place");
150
+ if (!capabilityMatches) {
151
+ throw new ProductError({
152
+ code: "RCORE20002",
153
+ status: "BLOCKED",
154
+ message: "previewId was created for a different capability."
155
+ });
156
+ }
157
+ if (record.paramsHash !== stableParamsHash(normalizeTradeParams(input))) {
158
+ throw new ProductError({
159
+ code: "RCORE20002",
160
+ status: "BLOCKED",
161
+ message: "Submitted trade parameters do not match the preview."
162
+ });
163
+ }
164
+ return record;
165
+ }
166
+ export function consumePreview(store, previewId, capability, input, now = new Date()) {
167
+ const record = verifyPreview(store, previewId, capability, input, now);
168
+ store.records.delete(record.previewId);
169
+ return record;
170
+ }
171
+ function normalizeTradeParams(input) {
172
+ const normalized = {};
173
+ for (const [key, value] of Object.entries(input)) {
174
+ if (!NON_TRADE_HASH_FIELDS.has(key) && value !== undefined) {
175
+ normalized[key] = value;
176
+ }
177
+ }
178
+ return normalized;
179
+ }
180
+ function makePreviewDetails(capability, tradeParams) {
181
+ return {
182
+ businessParams: { ...tradeParams },
183
+ requestSummary: makeRequestSummary(capability.capabilityId, tradeParams),
184
+ riskNotes: makeRiskNotes(capability.capabilityId, tradeParams)
185
+ };
186
+ }
187
+ function makeRequestSummary(capabilityId, params) {
188
+ if (capabilityId === "order.preview" || capabilityId === "order.place-preview" || capabilityId === "order.place") {
189
+ return compactRecord({
190
+ action: "place_order",
191
+ symbol: params.symbol,
192
+ side: params.side,
193
+ orderType: params.orderType,
194
+ price: params.price,
195
+ quantity: params.quantity,
196
+ amount: params.amount,
197
+ maxNotional: params.maxNotional,
198
+ clientOrderId: params.clientOrderId,
199
+ estimatedNotional: estimateNotional(params)
200
+ });
201
+ }
202
+ if (capabilityId === "position.close") {
203
+ return compactRecord({
204
+ action: "close_position",
205
+ symbol: params.symbol,
206
+ positionSide: params.positionSide,
207
+ quantity: params.quantity,
208
+ reduceOnly: params.reduceOnly,
209
+ maxNotional: params.maxNotional,
210
+ orderType: "MARKET",
211
+ closeSide: "determined_by_current_position"
212
+ });
213
+ }
214
+ if (capabilityId === "position.set-leverage") {
215
+ return compactRecord({
216
+ action: "set_leverage",
217
+ symbol: params.symbol,
218
+ leverage: params.leverage,
219
+ positionSide: params.positionSide
220
+ });
221
+ }
222
+ if (capabilityId === "account.set-position-mode") {
223
+ return compactRecord({
224
+ action: "set_position_mode",
225
+ mode: params.mode,
226
+ exchange: params.exchange
227
+ });
228
+ }
229
+ if (capabilityId.startsWith("order.") || capabilityId.startsWith("algo.")) {
230
+ return compactRecord({
231
+ action: capabilityId.replace(".", "_"),
232
+ symbol: params.symbol,
233
+ orderId: params.orderId,
234
+ algoOrderId: params.algoOrderId,
235
+ clientOrderId: params.clientOrderId,
236
+ price: params.price,
237
+ quantity: params.quantity,
238
+ maxNotional: params.maxNotional
239
+ });
240
+ }
241
+ return compactRecord({ action: capabilityId, ...params });
242
+ }
243
+ function makeRiskNotes(capabilityId, params) {
244
+ const notes = [];
245
+ if (params.maxNotional !== undefined) {
246
+ notes.push("maxNotional is a safety upper bound, not the target order size.");
247
+ }
248
+ if (capabilityId === "position.close") {
249
+ notes.push("Do not pass side or quantity for position.close; RapidX determines BUY or SELL from the current position and closes the target symbol/positionSide.");
250
+ notes.push("position.close uses the RapidX close-position API and may execute as a market close with slippage.");
251
+ notes.push("order.get reduceOnly may not reflect the close-position API request; verify final exposure with position.list.");
252
+ }
253
+ return notes;
254
+ }
255
+ function estimateNotional(params) {
256
+ if (params.amount !== undefined && params.amount !== null && params.amount !== "") {
257
+ return String(params.amount);
258
+ }
259
+ if (params.notional !== undefined && params.notional !== null && params.notional !== "") {
260
+ return String(params.notional);
261
+ }
262
+ if (params.price === undefined || params.quantity === undefined) {
263
+ return undefined;
264
+ }
265
+ return multiplyDecimalStrings(String(params.price), String(params.quantity));
266
+ }
267
+ function multiplyDecimalStrings(left, right) {
268
+ const parsedLeft = parseDecimal(left);
269
+ const parsedRight = parseDecimal(right);
270
+ if (!parsedLeft || !parsedRight) {
271
+ return undefined;
272
+ }
273
+ const negative = parsedLeft.negative !== parsedRight.negative;
274
+ const product = parsedLeft.value * parsedRight.value;
275
+ const scale = parsedLeft.scale + parsedRight.scale;
276
+ return formatScaledDecimal(product, scale, negative);
277
+ }
278
+ function parseDecimal(input) {
279
+ const trimmed = input.trim();
280
+ const match = /^([+-])?(\d+)(?:\.(\d+))?$/.exec(trimmed);
281
+ if (!match) {
282
+ return undefined;
283
+ }
284
+ const [, sign, whole, fraction = ""] = match;
285
+ const digits = `${whole}${fraction}`.replace(/^0+(?=\d)/, "");
286
+ return {
287
+ value: BigInt(digits || "0"),
288
+ scale: fraction.length,
289
+ negative: sign === "-"
290
+ };
291
+ }
292
+ function formatScaledDecimal(value, scale, negative) {
293
+ const sign = negative && value !== 0n ? "-" : "";
294
+ const raw = value.toString().padStart(scale + 1, "0");
295
+ if (scale === 0) {
296
+ return `${sign}${raw}`;
297
+ }
298
+ const whole = raw.slice(0, -scale) || "0";
299
+ const fraction = raw.slice(-scale).replace(/0+$/, "");
300
+ return fraction ? `${sign}${whole}.${fraction}` : `${sign}${whole}`;
301
+ }
302
+ function compactRecord(input) {
303
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== null && value !== ""));
304
+ }
305
+ function isPreviewCapability(capabilityId) {
306
+ return capabilityId.endsWith(".preview") || capabilityId.endsWith("-preview");
307
+ }
308
+ const NON_TRADE_HASH_FIELDS = new Set([
309
+ "targetCapabilityId",
310
+ "previewId",
311
+ "consentId",
312
+ "continueConsentId",
313
+ "explicitUserConsent",
314
+ "acceptedRiskText",
315
+ "mcpSchemaVersion",
316
+ "skillsVersion",
317
+ "skillsSchemaVersion"
318
+ ]);
319
+ function isTradePreviewRecord(value) {
320
+ if (!value || typeof value !== "object") {
321
+ return false;
322
+ }
323
+ const candidate = value;
324
+ return typeof candidate.previewId === "string"
325
+ && typeof candidate.capabilityId === "string"
326
+ && typeof candidate.paramsHash === "string"
327
+ && typeof candidate.expiresAt === "string"
328
+ && candidate.submitted === false
329
+ && (candidate.status === "PASS" || candidate.status === "BLOCKED");
330
+ }
@@ -0,0 +1,137 @@
1
+ import { findCapabilityById } from "../contracts/capabilities.js";
2
+ import { assertInputMatchesSchema } from "../contracts/input-schema.js";
3
+ import { ProductError } from "../errors/product-error.js";
4
+ import { createTradePreview, makePreviewStore } from "./preview.js";
5
+ import { defaultSafetyPolicy, makeSafetyState } from "../safety/policy.js";
6
+ import { runSelfCheck } from "../self-check/run-self-check.js";
7
+ import { makeResolvedCredential } from "../config/credential.js";
8
+ export async function runTradingVerification(rawInput, probes = {}) {
9
+ const steps = [];
10
+ let input;
11
+ try {
12
+ input = normalizeTradingVerificationInput(rawInput);
13
+ }
14
+ catch (error) {
15
+ if (error instanceof ProductError) {
16
+ return { submittedRealOrder: false, status: "FAIL", cleanupStatus: "NOT_VERIFIED", steps, errorCode: error.code, errorMessage: error.message };
17
+ }
18
+ throw error;
19
+ }
20
+ const selfCheckOptions = {
21
+ input: { scope: "deep", readOnly: true },
22
+ probes
23
+ };
24
+ const selfCheck = await runSelfCheck(probes.credentialAuth
25
+ ? { ...selfCheckOptions, credential: makeResolvedCredential("configured-access-key", "configured-secret-key") }
26
+ : selfCheckOptions);
27
+ steps.push({ name: "self-check", status: selfCheck.status, toolOrCommandEvidence: "rapidx self-check --read-only --json" });
28
+ if (selfCheck.status !== "PASS") {
29
+ return { submittedRealOrder: false, status: "NOT_VERIFIED", cleanupStatus: "NOT_VERIFIED", steps, errorCode: "RCORE10002" };
30
+ }
31
+ if (!input.explicitUserConsent) {
32
+ steps.push({ name: "preview", status: "BLOCKED", toolOrCommandEvidence: "user consent missing for small real trade verification" });
33
+ return { submittedRealOrder: false, status: "BLOCKED", cleanupStatus: "NOT_VERIFIED", steps, errorCode: "RCORE20001" };
34
+ }
35
+ if (!probes.marketRules) {
36
+ steps.push({ name: "market", status: "NOT_VERIFIED", toolOrCommandEvidence: "market rules probe missing" });
37
+ return { submittedRealOrder: false, status: "NOT_VERIFIED", cleanupStatus: "NOT_VERIFIED", steps, errorCode: "RCORE10002" };
38
+ }
39
+ const marketRules = await probes.marketRules(input);
40
+ if (Number(marketRules.minNotional) > Number(input.maxNotional)) {
41
+ steps.push({ name: "market", status: "BLOCKED", toolOrCommandEvidence: `minNotional=${marketRules.minNotional};maxNotional=${input.maxNotional}` });
42
+ return { submittedRealOrder: false, status: "BLOCKED", cleanupStatus: "NOT_VERIFIED", steps, errorCode: "RCORE24001" };
43
+ }
44
+ steps.push({ name: "market", status: "PASS", toolOrCommandEvidence: `minNotional=${marketRules.minNotional};tickSize=${marketRules.tickSize}` });
45
+ const capability = findCapabilityById("order.preview");
46
+ if (!capability) {
47
+ throw new Error("Missing order.preview capability");
48
+ }
49
+ const policy = { ...defaultSafetyPolicy(), tradingEnabled: true, readOnly: false, maxNotional: input.maxNotional };
50
+ const price = marketRules.orderPrice ?? (input.side === "BUY" ? marketRules.bestBid : marketRules.bestAsk);
51
+ const quantity = marketRules.orderQuantity ?? quantityForNotional(marketRules.minNotional, price);
52
+ const previewInput = {
53
+ ...input,
54
+ orderType: "LIMIT",
55
+ postOnly: true,
56
+ price,
57
+ quantity
58
+ };
59
+ const preview = createTradePreview(capability, previewInput, policy, makeSafetyState(), makePreviewStore());
60
+ steps.push({ name: "preview", status: "PASS", toolOrCommandEvidence: preview.previewId });
61
+ if (!probes.placeOrder) {
62
+ steps.push({ name: "place", status: "NOT_VERIFIED", toolOrCommandEvidence: "placeOrder probe missing" });
63
+ return { submittedRealOrder: false, status: "NOT_VERIFIED", cleanupStatus: "NOT_VERIFIED", steps, previewId: preview.previewId, errorCode: "RCORE23001" };
64
+ }
65
+ const placed = await probes.placeOrder({ ...input, previewId: preview.previewId, orderType: "LIMIT", postOnly: true, price, quantity });
66
+ steps.push({ name: "place", status: "PASS", toolOrCommandEvidence: placed.requestId ?? placed.orderId });
67
+ if (!probes.queryOrder) {
68
+ steps.push({ name: "query", status: "NOT_VERIFIED", toolOrCommandEvidence: "queryOrder probe missing" });
69
+ return { submittedRealOrder: true, status: "NOT_VERIFIED", cleanupStatus: "NOT_VERIFIED", steps, previewId: preview.previewId, errorCode: "RCORE23001" };
70
+ }
71
+ const queried = await probes.queryOrder(placed);
72
+ if (queried.status === "UNKNOWN") {
73
+ steps.push({ name: "query", status: "NOT_VERIFIED", toolOrCommandEvidence: queried.orderId });
74
+ return { submittedRealOrder: true, status: "NOT_VERIFIED", cleanupStatus: "NOT_VERIFIED", steps, previewId: preview.previewId, errorCode: "RCORE23001" };
75
+ }
76
+ steps.push({ name: "query", status: "PASS", toolOrCommandEvidence: `${queried.orderId}:${queried.status}` });
77
+ let currentOrder = queried;
78
+ if (probes.amendOrder) {
79
+ currentOrder = await probes.amendOrder(currentOrder);
80
+ steps.push({ name: "amend", status: currentOrder.status === "UNKNOWN" ? "NOT_VERIFIED" : "PASS", toolOrCommandEvidence: `${currentOrder.orderId}:${currentOrder.status}` });
81
+ if (currentOrder.status === "UNKNOWN") {
82
+ return { submittedRealOrder: true, status: "NOT_VERIFIED", cleanupStatus: "NOT_VERIFIED", steps, previewId: preview.previewId, errorCode: "RCORE23001" };
83
+ }
84
+ }
85
+ else {
86
+ steps.push({ name: "amend", status: "EXPECTED_ERROR", toolOrCommandEvidence: "amendOrder probe not supported" });
87
+ }
88
+ if (!probes.cancelOrder) {
89
+ steps.push({ name: "cancel", status: "FAIL", toolOrCommandEvidence: "cancelOrder probe missing" });
90
+ return { submittedRealOrder: true, status: "FAIL", cleanupStatus: "FAIL", steps, previewId: preview.previewId, errorCode: "RCORE24002" };
91
+ }
92
+ currentOrder = await probes.cancelOrder(currentOrder);
93
+ if (currentOrder.status !== "CANCELED") {
94
+ steps.push({ name: "cancel", status: "FAIL", toolOrCommandEvidence: `${currentOrder.orderId}:${currentOrder.status}` });
95
+ return { submittedRealOrder: true, status: "FAIL", cleanupStatus: "FAIL", steps, previewId: preview.previewId, errorCode: "RCORE24002" };
96
+ }
97
+ steps.push({ name: "cancel", status: "PASS", toolOrCommandEvidence: `${currentOrder.orderId}:${currentOrder.status}` });
98
+ if (!probes.cleanupCheck) {
99
+ steps.push({ name: "cleanup-check", status: "NOT_VERIFIED", toolOrCommandEvidence: "cleanupCheck probe missing" });
100
+ return { submittedRealOrder: true, status: "NOT_VERIFIED", cleanupStatus: "NOT_VERIFIED", steps, previewId: preview.previewId, errorCode: "RCORE24002" };
101
+ }
102
+ const cleanup = await probes.cleanupCheck(currentOrder);
103
+ if (cleanup.openOrders !== 0 || cleanup.unexpectedPositions !== 0) {
104
+ steps.push({ name: "cleanup-check", status: "FAIL", toolOrCommandEvidence: `openOrders=${cleanup.openOrders};unexpectedPositions=${cleanup.unexpectedPositions}` });
105
+ return { submittedRealOrder: true, status: "FAIL", cleanupStatus: "FAIL", steps, previewId: preview.previewId, errorCode: "RCORE24002" };
106
+ }
107
+ steps.push({ name: "cleanup-check", status: "PASS", toolOrCommandEvidence: "openOrders=0;unexpectedPositions=0" });
108
+ return {
109
+ submittedRealOrder: true,
110
+ status: "PASS",
111
+ cleanupStatus: "PASS",
112
+ steps,
113
+ previewId: preview.previewId
114
+ };
115
+ }
116
+ function normalizeTradingVerificationInput(input) {
117
+ assertInputMatchesSchema("TradingVerificationInput", input);
118
+ const normalized = {
119
+ symbol: String(input.symbol),
120
+ side: input.side === "SELL" ? "SELL" : "BUY",
121
+ maxNotional: String(input.maxNotional),
122
+ clientOrderId: String(input.clientOrderId),
123
+ explicitUserConsent: input.explicitUserConsent === true
124
+ };
125
+ if (typeof input.acceptedRiskText === "string") {
126
+ normalized.acceptedRiskText = input.acceptedRiskText;
127
+ }
128
+ return normalized;
129
+ }
130
+ function quantityForNotional(minNotional, price) {
131
+ const priceNumber = Number(price);
132
+ const minNotionalNumber = Number(minNotional);
133
+ if (!Number.isFinite(priceNumber) || priceNumber <= 0 || !Number.isFinite(minNotionalNumber) || minNotionalNumber <= 0) {
134
+ return minNotional;
135
+ }
136
+ return String(Math.ceil((minNotionalNumber / priceNumber) * 1_000_000) / 1_000_000);
137
+ }