@liquiditytech/rapidx-cli 1.0.28 → 1.0.29

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.
@@ -0,0 +1,338 @@
1
+ import { RapidXClient } from "../client/rapid-x-client.js";
2
+ import { parseRapidXSymbol } from "../client/symbol.js";
3
+ import { ProductError } from "../errors/product-error.js";
4
+ export async function runPreviewPreflight(capabilityId, input, options = {}) {
5
+ switch (capabilityId) {
6
+ case "order.preview":
7
+ case "order.place-preview":
8
+ case "order.place":
9
+ case "algo.place":
10
+ return validateOrderPlacementPreflight(input, options);
11
+ case "order.cancel":
12
+ return validateOrderMutationPreflight(input, "cancel", options);
13
+ case "order.amend":
14
+ return validateOrderMutationPreflight(input, "amend", options);
15
+ case "position.close":
16
+ return validatePositionClosePreflight(input, options);
17
+ default:
18
+ return {};
19
+ }
20
+ }
21
+ async function validateOrderPlacementPreflight(input, options) {
22
+ const symbolInput = requiredString(input.symbol, "symbol");
23
+ const parsed = parseRapidXSymbol(symbolInput);
24
+ const client = options.client ?? new RapidXClient();
25
+ const response = await client.get("/api/v1/trading/sym/info", { sym: parsed.canonicalSymbol });
26
+ const rules = extractSymbolRules(response, parsed.canonicalSymbol);
27
+ validateQuantity(input, rules);
28
+ validatePrice(input, rules);
29
+ validateNotional(input, rules);
30
+ return {
31
+ marketRules: compactObject({
32
+ symbol: parsed.canonicalSymbol,
33
+ inputSymbol: parsed.inputSymbol !== parsed.canonicalSymbol ? parsed.inputSymbol : undefined,
34
+ minNotional: rules.minNotional,
35
+ minSize: rules.minSize,
36
+ lotSize: rules.lotSize,
37
+ qtyPrecision: rules.qtyPrecision,
38
+ tickSize: rules.tickSize,
39
+ pricePrecision: rules.pricePrecision
40
+ })
41
+ };
42
+ }
43
+ async function validateOrderMutationPreflight(input, action, options) {
44
+ const client = options.client ?? new RapidXClient();
45
+ const params = orderLookupParams(input);
46
+ const response = await client.get("/api/v1/trading/order", params);
47
+ const order = extractOrderReadback(response);
48
+ if (!order) {
49
+ throw new ProductError({
50
+ code: "RCORE22004",
51
+ status: "BLOCKED",
52
+ message: "ORDER_NOT_FOUND: order.get did not return an order for preview preflight."
53
+ });
54
+ }
55
+ const state = normalizeOrderState(order.orderState ?? order.state ?? order.status);
56
+ if (!isOpenOrderState(state)) {
57
+ throw new ProductError({
58
+ code: "RCORE22005",
59
+ status: "BLOCKED",
60
+ message: `ORDER_NOT_OPEN: order is ${state || "UNKNOWN"} and cannot be ${action}ed.`
61
+ });
62
+ }
63
+ return {
64
+ orderReadback: {
65
+ ...order,
66
+ orderState: state || order.orderState
67
+ }
68
+ };
69
+ }
70
+ async function validatePositionClosePreflight(input, options) {
71
+ const symbolInput = requiredString(input.symbol, "symbol");
72
+ const parsed = parseRapidXSymbol(symbolInput);
73
+ const client = options.client ?? new RapidXClient();
74
+ const response = await client.get("/api/v1/trading/position", { sym: parsed.canonicalSymbol });
75
+ const positions = extractPositions(response);
76
+ const positionSide = typeof input.positionSide === "string" && input.positionSide.length > 0 ? input.positionSide.toUpperCase() : undefined;
77
+ const position = positions.find((item) => {
78
+ const itemSymbol = stringField(item, "sym") ?? stringField(item, "symbol");
79
+ const itemSide = stringField(item, "positionSide")?.toUpperCase();
80
+ const qty = Number(stringField(item, "positionQty") ?? stringField(item, "quantity") ?? stringField(item, "qty") ?? "0");
81
+ return itemSymbol === parsed.canonicalSymbol && Math.abs(qty) > 0 && (!positionSide || !itemSide || itemSide === positionSide);
82
+ });
83
+ if (!position) {
84
+ throw new ProductError({
85
+ code: "RCORE24003",
86
+ status: "BLOCKED",
87
+ message: `NO_POSITION_TO_CLOSE: no non-zero position found for ${parsed.canonicalSymbol}.`
88
+ });
89
+ }
90
+ return {
91
+ positionReadback: {
92
+ ...position,
93
+ sym: stringField(position, "sym") ?? parsed.canonicalSymbol
94
+ }
95
+ };
96
+ }
97
+ function extractSymbolRules(response, symbol) {
98
+ const data = unwrapData(response);
99
+ const candidates = [
100
+ isRecord(data) ? data[symbol] : undefined,
101
+ isRecord(data) && isRecord(data.data) ? data.data[symbol] : undefined,
102
+ isRecord(data) && Array.isArray(data.list) ? data.list.find((item) => isRecord(item) && (item.sym === symbol || item.symbol === symbol)) : undefined,
103
+ data
104
+ ];
105
+ const record = candidates.find((item) => isSymbolRuleRecord(item, symbol));
106
+ if (!record) {
107
+ throw new ProductError({
108
+ code: "RCORE12001",
109
+ status: "BLOCKED",
110
+ message: `INVALID_SYMBOL: ${symbol} was not found in RapidX symbol-info.`
111
+ });
112
+ }
113
+ const rules = { symbol };
114
+ assignIfDefined(rules, "minNotional", stringField(record, "minNotional"));
115
+ assignIfDefined(rules, "minSize", stringField(record, "minSize") ?? stringField(record, "minQty") ?? stringField(record, "minOrderQty"));
116
+ assignIfDefined(rules, "lotSize", stringField(record, "lotSize") ?? stringField(record, "qtyStep") ?? stringField(record, "stepSize"));
117
+ assignIfDefined(rules, "qtyPrecision", stringField(record, "qtyPrecision") ?? stringField(record, "quantityPrecision"));
118
+ assignIfDefined(rules, "tickSize", stringField(record, "tickSize") ?? stringField(record, "priceTick") ?? stringField(record, "priceStep"));
119
+ assignIfDefined(rules, "pricePrecision", stringField(record, "pricePrecision"));
120
+ return rules;
121
+ }
122
+ function assignIfDefined(target, key, value) {
123
+ if (value !== undefined) {
124
+ target[key] = value;
125
+ }
126
+ }
127
+ function isSymbolRuleRecord(value, symbol) {
128
+ if (!isRecord(value) || Object.keys(value).length === 0) {
129
+ return false;
130
+ }
131
+ const recordSymbol = stringField(value, "sym") ?? stringField(value, "symbol");
132
+ if (recordSymbol !== undefined) {
133
+ return recordSymbol === symbol;
134
+ }
135
+ return [
136
+ "minNotional",
137
+ "minSize",
138
+ "minQty",
139
+ "minOrderQty",
140
+ "lotSize",
141
+ "qtyStep",
142
+ "stepSize",
143
+ "qtyPrecision",
144
+ "quantityPrecision",
145
+ "tickSize",
146
+ "priceTick",
147
+ "priceStep",
148
+ "pricePrecision"
149
+ ].some((field) => stringField(value, field) !== undefined);
150
+ }
151
+ function validateNotional(input, rules) {
152
+ const minNotional = positiveNumber(rules.minNotional);
153
+ if (minNotional === undefined) {
154
+ return;
155
+ }
156
+ const notional = estimateNotional(input);
157
+ if (notional !== undefined && notional < minNotional - 1e-12) {
158
+ throw new ProductError({
159
+ code: "RCORE24005",
160
+ status: "BLOCKED",
161
+ message: `MIN_NOTIONAL_NOT_MET: estimatedNotional ${formatNumber(notional)} is below minNotional ${rules.minNotional}.`
162
+ });
163
+ }
164
+ }
165
+ function validateQuantity(input, rules) {
166
+ const quantityValue = input.quantity ?? input.qty;
167
+ if (quantityValue === undefined || quantityValue === null || quantityValue === "") {
168
+ return;
169
+ }
170
+ const quantityText = String(quantityValue);
171
+ const quantity = positiveNumber(quantityText);
172
+ if (quantity === undefined) {
173
+ return;
174
+ }
175
+ const minSize = positiveNumber(rules.minSize);
176
+ if (minSize !== undefined && quantity < minSize - 1e-12) {
177
+ throw new ProductError({
178
+ code: "RCORE24006",
179
+ status: "BLOCKED",
180
+ message: `INVALID_QUANTITY: quantity ${quantityText} is below minSize ${rules.minSize}.`
181
+ });
182
+ }
183
+ const lotSize = positiveNumber(rules.lotSize);
184
+ if (lotSize !== undefined && !isAlignedToStep(quantity, lotSize)) {
185
+ throw new ProductError({
186
+ code: "RCORE24006",
187
+ status: "BLOCKED",
188
+ message: `INVALID_QUANTITY: quantity ${quantityText} is not aligned to lotSize ${rules.lotSize}.`
189
+ });
190
+ }
191
+ const qtyPrecision = integerValue(rules.qtyPrecision);
192
+ if (qtyPrecision !== undefined && decimalPlaces(quantityText) > qtyPrecision) {
193
+ throw new ProductError({
194
+ code: "RCORE24006",
195
+ status: "BLOCKED",
196
+ message: `INVALID_QUANTITY: quantity ${quantityText} exceeds qtyPrecision ${rules.qtyPrecision}.`
197
+ });
198
+ }
199
+ }
200
+ function validatePrice(input, rules) {
201
+ if (input.price === undefined || input.price === null || input.price === "") {
202
+ return;
203
+ }
204
+ const priceText = String(input.price);
205
+ const price = positiveNumber(priceText);
206
+ if (price === undefined) {
207
+ return;
208
+ }
209
+ const tickSize = positiveNumber(rules.tickSize);
210
+ if (tickSize !== undefined && !isAlignedToStep(price, tickSize)) {
211
+ throw new ProductError({
212
+ code: "RCORE24007",
213
+ status: "BLOCKED",
214
+ message: `INVALID_PRICE_TICK: price ${priceText} is not aligned to tickSize ${rules.tickSize}.`
215
+ });
216
+ }
217
+ const pricePrecision = integerValue(rules.pricePrecision);
218
+ if (pricePrecision !== undefined && decimalPlaces(priceText) > pricePrecision) {
219
+ throw new ProductError({
220
+ code: "RCORE24007",
221
+ status: "BLOCKED",
222
+ message: `INVALID_PRICE_TICK: price ${priceText} exceeds pricePrecision ${rules.pricePrecision}.`
223
+ });
224
+ }
225
+ }
226
+ function estimateNotional(input) {
227
+ const direct = positiveNumber(input.amount ?? input.notional);
228
+ if (direct !== undefined) {
229
+ return direct;
230
+ }
231
+ const price = positiveNumber(input.price);
232
+ const quantity = positiveNumber(input.quantity ?? input.qty);
233
+ if (price === undefined || quantity === undefined) {
234
+ return undefined;
235
+ }
236
+ return price * quantity;
237
+ }
238
+ function orderLookupParams(input) {
239
+ const params = compactObject({
240
+ orderId: input.orderId,
241
+ clientOrderId: input.clientOrderId,
242
+ symbol: input.symbol
243
+ });
244
+ if (!params.orderId && !params.clientOrderId) {
245
+ throw new ProductError({
246
+ code: "RCORE00001",
247
+ status: "FAIL",
248
+ message: "orderId or clientOrderId is required."
249
+ });
250
+ }
251
+ return params;
252
+ }
253
+ function extractOrderReadback(response) {
254
+ const data = unwrapData(response);
255
+ if (isRecord(data) && Array.isArray(data.list)) {
256
+ return data.list.find(isRecord);
257
+ }
258
+ if (isRecord(data) && Array.isArray(data.orders)) {
259
+ return data.orders.find(isRecord);
260
+ }
261
+ return isRecord(data) && Object.keys(data).length > 0 ? data : undefined;
262
+ }
263
+ function extractPositions(response) {
264
+ const data = unwrapData(response);
265
+ if (Array.isArray(data)) {
266
+ return data.filter(isRecord);
267
+ }
268
+ if (isRecord(data) && Array.isArray(data.list)) {
269
+ return data.list.filter(isRecord);
270
+ }
271
+ if (isRecord(data) && Array.isArray(data.positions)) {
272
+ return data.positions.filter(isRecord);
273
+ }
274
+ return isRecord(data) ? [data] : [];
275
+ }
276
+ function unwrapData(response) {
277
+ if (isRecord(response) && "data" in response) {
278
+ return response.data;
279
+ }
280
+ return response;
281
+ }
282
+ function normalizeOrderState(value) {
283
+ return value === undefined || value === null ? "" : String(value).toUpperCase();
284
+ }
285
+ function isOpenOrderState(state) {
286
+ return ["NEW", "OPEN", "ACCEPTED", "PARTIALLY_FILLED"].includes(state);
287
+ }
288
+ function requiredString(value, field) {
289
+ if (value === undefined || value === null || value === "") {
290
+ throw new ProductError({
291
+ code: "RCORE00001",
292
+ status: "FAIL",
293
+ message: `${field} is required.`
294
+ });
295
+ }
296
+ return String(value);
297
+ }
298
+ function stringField(record, field) {
299
+ const value = record[field];
300
+ return value === undefined || value === null || value === "" ? undefined : String(value);
301
+ }
302
+ function positiveNumber(value) {
303
+ if (value === undefined || value === null || value === "") {
304
+ return undefined;
305
+ }
306
+ const number = Number(value);
307
+ return Number.isFinite(number) && number > 0 ? number : undefined;
308
+ }
309
+ function integerValue(value) {
310
+ if (value === undefined || value === null || value === "") {
311
+ return undefined;
312
+ }
313
+ const number = Number(value);
314
+ return Number.isInteger(number) && number >= 0 ? number : undefined;
315
+ }
316
+ function isAlignedToStep(value, step) {
317
+ const ratio = value / step;
318
+ return Math.abs(ratio - Math.round(ratio)) < 1e-9;
319
+ }
320
+ function decimalPlaces(value) {
321
+ const normalized = value.toLowerCase();
322
+ if (normalized.includes("e")) {
323
+ const [mantissa, exponentRaw] = normalized.split("e");
324
+ const exponent = Number(exponentRaw);
325
+ return Math.max(0, decimalPlaces(mantissa ?? "0") - exponent);
326
+ }
327
+ const [, fractional = ""] = normalized.split(".");
328
+ return fractional.replace(/0+$/, "").length;
329
+ }
330
+ function formatNumber(value) {
331
+ return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(12)));
332
+ }
333
+ function compactObject(input) {
334
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== null && value !== ""));
335
+ }
336
+ function isRecord(value) {
337
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
338
+ }
@@ -4,6 +4,7 @@ import { dirname, join, resolve } from "node:path";
4
4
  import { assertInputMatchesSchema } from "../contracts/input-schema.js";
5
5
  import { evaluateSafety, stableParamsHash } from "../safety/policy.js";
6
6
  import { ProductError } from "../errors/product-error.js";
7
+ import { parseRapidXSymbol } from "../client/symbol.js";
7
8
  export function makePreviewStore() {
8
9
  return { records: new Map() };
9
10
  }
@@ -38,10 +39,11 @@ export function savePreviewStoreToFile(filePath, store) {
38
39
  }
39
40
  export function createTradePreview(capability, input, policy, state, store, ttlSeconds = 300) {
40
41
  assertInputMatchesSchema(capability.inputSchema, input, {
41
- allowedExtraFields: ["explicitUserConsent", "acceptedRiskText"]
42
+ allowedExtraFields: ["explicitUserConsent", "acceptedRiskText", "automationMode", "automationConsentText", "automationScope"]
42
43
  });
43
44
  const tradeParams = normalizeTradeParams(input);
44
- const previewDetails = makePreviewDetails(capability, tradeParams);
45
+ const automation = makeAutomationDetails(input, tradeParams);
46
+ const previewDetails = makePreviewDetails(capability, tradeParams, automation);
45
47
  const decision = evaluateSafety(capability, input, policy, state);
46
48
  if (!decision.allowed) {
47
49
  throw new ProductError({
@@ -59,7 +61,8 @@ export function createTradePreview(capability, input, policy, state, store, ttlS
59
61
  expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
60
62
  submitted: false,
61
63
  status: "PASS",
62
- confirmation: makeSubmitConfirmation(previewId)
64
+ confirmation: makeSubmitConfirmation(previewId, automation),
65
+ ...(automation ? { automation } : {})
63
66
  };
64
67
  store.records.set(previewId, record);
65
68
  return record;
@@ -73,7 +76,10 @@ export function createTargetedTradePreview(targetCapability, input, policy, stat
73
76
  "acceptedRiskText",
74
77
  "mcpSchemaVersion",
75
78
  "skillsVersion",
76
- "skillsSchemaVersion"
79
+ "skillsSchemaVersion",
80
+ "automationMode",
81
+ "automationConsentText",
82
+ "automationScope"
77
83
  ],
78
84
  ignoredRequiredFields: ["previewId", "continueConsentId"]
79
85
  });
@@ -92,7 +98,8 @@ export function createTargetedTradePreview(targetCapability, input, policy, stat
92
98
  });
93
99
  }
94
100
  const tradeParams = normalizeTradeParams(input);
95
- const previewDetails = makePreviewDetails(targetCapability, tradeParams);
101
+ const automation = makeAutomationDetails(input, tradeParams);
102
+ const previewDetails = makePreviewDetails(targetCapability, tradeParams, automation);
96
103
  const decision = evaluateSafety(targetCapability, input, policy, state);
97
104
  if (!decision.allowed) {
98
105
  throw new ProductError({
@@ -110,16 +117,19 @@ export function createTargetedTradePreview(targetCapability, input, policy, stat
110
117
  expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
111
118
  submitted: false,
112
119
  status: "PASS",
113
- confirmation: makeSubmitConfirmation(previewId)
120
+ confirmation: makeSubmitConfirmation(previewId, automation),
121
+ ...(automation ? { automation } : {})
114
122
  };
115
123
  store.records.set(previewId, record);
116
124
  return record;
117
125
  }
118
- function makeSubmitConfirmation(previewId) {
126
+ function makeSubmitConfirmation(previewId, automation) {
119
127
  return {
120
128
  submitToken: `confirm_${previewId}`,
121
129
  requiredFields: ["previewId", "continueConsentId"],
122
- instruction: "Pass submitToken as continueConsentId only after the user has confirmed this exact preview."
130
+ instruction: automation
131
+ ? "Automation mode is enabled for this preview. The agent may pass submitToken as continueConsentId without asking for another per-order chat confirmation, within the authorized scope."
132
+ : "Pass submitToken as continueConsentId only after the user has confirmed this exact preview."
123
133
  };
124
134
  }
125
135
  export function verifyPreview(store, previewId, capability, input, now = new Date()) {
@@ -135,7 +145,7 @@ export function verifyPreview(store, previewId, capability, input, now = new Dat
135
145
  throw new ProductError({
136
146
  code: "RCORE20002",
137
147
  status: "BLOCKED",
138
- message: "previewId was not found."
148
+ message: "previewId was not found or has expired. Re-run preview and submit within 5 minutes."
139
149
  });
140
150
  }
141
151
  if (new Date(record.expiresAt).getTime() < now.getTime()) {
@@ -172,16 +182,24 @@ function normalizeTradeParams(input) {
172
182
  const normalized = {};
173
183
  for (const [key, value] of Object.entries(input)) {
174
184
  if (!NON_TRADE_HASH_FIELDS.has(key) && value !== undefined) {
175
- normalized[key] = value;
185
+ normalized[key] = key === "symbol" && typeof value === "string" ? canonicalSymbolOrOriginal(value) : value;
176
186
  }
177
187
  }
178
188
  return normalized;
179
189
  }
180
- function makePreviewDetails(capability, tradeParams) {
190
+ function canonicalSymbolOrOriginal(value) {
191
+ try {
192
+ return parseRapidXSymbol(value).canonicalSymbol;
193
+ }
194
+ catch {
195
+ return value;
196
+ }
197
+ }
198
+ function makePreviewDetails(capability, tradeParams, automation) {
181
199
  return {
182
200
  businessParams: { ...tradeParams },
183
201
  requestSummary: makeRequestSummary(capability.capabilityId, tradeParams),
184
- riskNotes: makeRiskNotes(capability.capabilityId, tradeParams)
202
+ riskNotes: makeRiskNotes(capability.capabilityId, tradeParams, automation)
185
203
  };
186
204
  }
187
205
  function makeRequestSummary(capabilityId, params) {
@@ -190,6 +208,7 @@ function makeRequestSummary(capabilityId, params) {
190
208
  action: "place_order",
191
209
  symbol: params.symbol,
192
210
  side: params.side,
211
+ positionSide: params.positionSide,
193
212
  orderType: params.orderType,
194
213
  price: params.price,
195
214
  quantity: params.quantity,
@@ -240,11 +259,18 @@ function makeRequestSummary(capabilityId, params) {
240
259
  }
241
260
  return compactRecord({ action: capabilityId, ...params });
242
261
  }
243
- function makeRiskNotes(capabilityId, params) {
262
+ function makeRiskNotes(capabilityId, params, automation) {
244
263
  const notes = [];
264
+ if (automation) {
265
+ notes.push("Automation mode allows the agent to submit this preview with submitToken without asking for another per-order chat confirmation.");
266
+ }
245
267
  if (params.maxNotional !== undefined) {
246
268
  notes.push("maxNotional is a safety upper bound, not the target order size.");
247
269
  }
270
+ if (String(params.orderType ?? "").toUpperCase() === "MARKET") {
271
+ notes.push("MARKET orders may execute immediately.");
272
+ notes.push("MARKET order fill price is not guaranteed and may include slippage.");
273
+ }
248
274
  if (capabilityId === "position.close") {
249
275
  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
276
  notes.push("position.close uses the RapidX close-position API and may execute as a market close with slippage.");
@@ -252,6 +278,51 @@ function makeRiskNotes(capabilityId, params) {
252
278
  }
253
279
  return notes;
254
280
  }
281
+ function makeAutomationDetails(input, tradeParams) {
282
+ if (input.automationMode !== true) {
283
+ return undefined;
284
+ }
285
+ const acceptedRiskText = typeof input.automationConsentText === "string" ? input.automationConsentText.trim() : "";
286
+ const missing = automationConsentMissingFields(acceptedRiskText, tradeParams, input);
287
+ if (missing.length > 0) {
288
+ throw new ProductError({
289
+ code: "RCORE20003",
290
+ status: "BLOCKED",
291
+ message: `automationConsentText must include ${missing.join(", ")} for this exact automation scope.`
292
+ });
293
+ }
294
+ return {
295
+ enabled: true,
296
+ confirmationMode: "automation-preview",
297
+ scope: typeof input.automationScope === "string" && input.automationScope.trim().length > 0 ? input.automationScope.trim() : "single-preview",
298
+ ...(tradeParams.maxNotional !== undefined ? { maxNotional: String(tradeParams.maxNotional) } : {}),
299
+ acceptedRiskText
300
+ };
301
+ }
302
+ function automationConsentMissingFields(text, tradeParams, input) {
303
+ const missing = [];
304
+ const upperText = text.toUpperCase();
305
+ const lowerText = text.toLowerCase();
306
+ if (text.length < 20) {
307
+ missing.push("automationConsentText");
308
+ }
309
+ const symbol = typeof tradeParams.symbol === "string" ? tradeParams.symbol : undefined;
310
+ if (symbol && !upperText.includes(symbol.toUpperCase())) {
311
+ missing.push("symbol");
312
+ }
313
+ const maxNotional = tradeParams.maxNotional;
314
+ if (maxNotional !== undefined && !text.includes(String(maxNotional))) {
315
+ missing.push("maxNotional");
316
+ }
317
+ if (!lowerText.includes("automation") && !lowerText.includes("automated") && !text.includes("自动")) {
318
+ missing.push("automation intent");
319
+ }
320
+ const scope = typeof input.automationScope === "string" ? input.automationScope.trim() : "";
321
+ if (scope && scope !== "single-preview" && !lowerText.includes(scope.toLowerCase())) {
322
+ missing.push("automationScope");
323
+ }
324
+ return missing;
325
+ }
255
326
  function estimateNotional(params) {
256
327
  if (params.amount !== undefined && params.amount !== null && params.amount !== "") {
257
328
  return String(params.amount);
@@ -314,7 +385,10 @@ const NON_TRADE_HASH_FIELDS = new Set([
314
385
  "acceptedRiskText",
315
386
  "mcpSchemaVersion",
316
387
  "skillsVersion",
317
- "skillsSchemaVersion"
388
+ "skillsSchemaVersion",
389
+ "automationMode",
390
+ "automationConsentText",
391
+ "automationScope"
318
392
  ]);
319
393
  function isTradePreviewRecord(value) {
320
394
  if (!value || typeof value !== "object") {
@@ -13,7 +13,7 @@ export async function runTradingVerification(rawInput, probes = {}) {
13
13
  }
14
14
  catch (error) {
15
15
  if (error instanceof ProductError) {
16
- return { submittedRealOrder: false, status: "FAIL", cleanupStatus: "NOT_VERIFIED", steps, errorCode: error.code, errorMessage: error.message, errorStage: "input-validation" };
16
+ return { submittedRealOrder: false, status: error.status, cleanupStatus: "NOT_VERIFIED", steps, errorCode: error.code, errorMessage: error.message, errorStage: "input-validation" };
17
17
  }
18
18
  throw error;
19
19
  }
@@ -39,7 +39,7 @@ export async function runTradingVerification(rawInput, probes = {}) {
39
39
  errorCode: "RCORE20001",
40
40
  errorMessage: consentProblem,
41
41
  errorStage: "user-consent",
42
- recommendedAction: "Ask the human user to confirm the exact symbol, side, maxNotional, real-order risk, and cancel cleanup behavior before running verify-live."
42
+ recommendedAction: "Ask the human user to confirm the exact symbol, side, positionSide when provided, maxNotional, real-order risk, and cancel cleanup behavior before running verify-live."
43
43
  };
44
44
  }
45
45
  if (!probes.marketRules) {
@@ -48,8 +48,18 @@ export async function runTradingVerification(rawInput, probes = {}) {
48
48
  }
49
49
  const marketRules = await probes.marketRules(input);
50
50
  if (Number(marketRules.minNotional) > Number(input.maxNotional)) {
51
- steps.push({ name: "market", status: "BLOCKED", toolOrCommandEvidence: `minNotional=${marketRules.minNotional};maxNotional=${input.maxNotional}` });
52
- return { submittedRealOrder: false, status: "BLOCKED", cleanupStatus: "NOT_VERIFIED", steps, errorCode: "RCORE24001", errorStage: "market" };
51
+ const evidence = `minNotional=${marketRules.minNotional};maxNotional=${input.maxNotional}`;
52
+ steps.push({ name: "market", status: "BLOCKED", toolOrCommandEvidence: evidence });
53
+ return {
54
+ submittedRealOrder: false,
55
+ status: "BLOCKED",
56
+ cleanupStatus: "NOT_VERIFIED",
57
+ steps,
58
+ errorCode: "RCORE24001",
59
+ errorMessage: `Verification order blocked because ${evidence}.`,
60
+ errorStage: "market",
61
+ recommendedAction: `Increase maxNotional to at least ${marketRules.minNotional}, or choose a symbol whose minNotional is within the authorized maxNotional.`
62
+ };
53
63
  }
54
64
  steps.push({ name: "market", status: "PASS", toolOrCommandEvidence: `minNotional=${marketRules.minNotional};tickSize=${marketRules.tickSize}` });
55
65
  const capability = findCapabilityById("order.preview");
@@ -73,7 +83,7 @@ export async function runTradingVerification(rawInput, probes = {}) {
73
83
  return { submittedRealOrder: false, status: "NOT_VERIFIED", cleanupStatus: "NOT_VERIFIED", steps, previewId: preview.previewId, errorCode: "RCORE23001", errorStage: "place" };
74
84
  }
75
85
  const placed = await probes.placeOrder({ ...input, previewId: preview.previewId, orderType: "LIMIT", postOnly: true, price, quantity });
76
- steps.push({ name: "place", status: "PASS", toolOrCommandEvidence: placed.requestId ?? placed.orderId });
86
+ steps.push({ name: "place", status: "PASS", toolOrCommandEvidence: [placed.requestId ?? placed.orderId, input.positionSide ? `positionSide=${input.positionSide}` : undefined].filter(Boolean).join(";") });
77
87
  if (!probes.queryOrder) {
78
88
  steps.push({ name: "query", status: "NOT_VERIFIED", toolOrCommandEvidence: "queryOrder probe missing" });
79
89
  return { submittedRealOrder: true, status: "NOT_VERIFIED", cleanupStatus: "NOT_VERIFIED", steps, previewId: preview.previewId, errorCode: "RCORE23001", errorStage: "query" };
@@ -93,7 +103,11 @@ export async function runTradingVerification(rawInput, probes = {}) {
93
103
  }
94
104
  }
95
105
  else {
96
- steps.push({ name: "amend", status: "EXPECTED_ERROR", toolOrCommandEvidence: "amendOrder probe not supported" });
106
+ steps.push({
107
+ name: "amend",
108
+ status: "EXPECTED_ERROR",
109
+ toolOrCommandEvidence: "optional verify-live amend check skipped; order.amend remains available outside verify-live"
110
+ });
97
111
  }
98
112
  if (!probes.cancelOrder) {
99
113
  steps.push({ name: "cancel", status: "FAIL", toolOrCommandEvidence: "cancelOrder probe missing" });
@@ -110,12 +124,12 @@ export async function runTradingVerification(rawInput, probes = {}) {
110
124
  steps.push({ name: "cleanup-check", status: "NOT_VERIFIED", toolOrCommandEvidence: "cleanupCheck probe missing" });
111
125
  return { submittedRealOrder: true, status: "NOT_VERIFIED", cleanupStatus: "NOT_VERIFIED", steps, previewId: preview.previewId, errorCode: "RCORE24002", errorStage: "cleanup-check", recommendedAction: "Query order/list and position/list before retrying verify-live." };
112
126
  }
113
- const cleanup = await probes.cleanupCheck(currentOrder);
114
- if (cleanup.openOrders !== 0 || cleanup.unexpectedPositions !== 0) {
115
- steps.push({ name: "cleanup-check", status: "FAIL", toolOrCommandEvidence: `openOrders=${cleanup.openOrders};unexpectedPositions=${cleanup.unexpectedPositions}` });
116
- return cleanupFailureReport(steps, preview.previewId, "cleanup-check", currentOrder.status, `cleanup still shows openOrders=${cleanup.openOrders}; unexpectedPositions=${cleanup.unexpectedPositions}`);
127
+ const cleanupConfirmation = await confirmCleanup(currentOrder, probes);
128
+ if (!cleanupConfirmation.confirmed) {
129
+ steps.push({ name: "cleanup-check", status: "FAIL", toolOrCommandEvidence: cleanupConfirmation.evidence });
130
+ return cleanupFailureReport(steps, preview.previewId, "cleanup-check", currentOrder.status, `cleanup still shows ${cleanupEvidence(cleanupConfirmation.cleanup)}`);
117
131
  }
118
- steps.push({ name: "cleanup-check", status: "PASS", toolOrCommandEvidence: "openOrders=0;unexpectedPositions=0" });
132
+ steps.push({ name: "cleanup-check", status: "PASS", toolOrCommandEvidence: cleanupConfirmation.evidence });
119
133
  return {
120
134
  submittedRealOrder: true,
121
135
  status: "PASS",
@@ -136,6 +150,9 @@ function normalizeTradingVerificationInput(input) {
136
150
  if (typeof input.acceptedRiskText === "string") {
137
151
  normalized.acceptedRiskText = input.acceptedRiskText;
138
152
  }
153
+ if (input.positionSide === "LONG" || input.positionSide === "SHORT") {
154
+ normalized.positionSide = input.positionSide;
155
+ }
139
156
  return normalized;
140
157
  }
141
158
  function validateParameterBoundConsent(input) {
@@ -155,6 +172,9 @@ function validateParameterBoundConsent(input) {
155
172
  if (!upperText.includes(input.side)) {
156
173
  missing.push("side");
157
174
  }
175
+ if (input.positionSide && !upperText.includes(input.positionSide)) {
176
+ missing.push("positionSide");
177
+ }
158
178
  if (!text.includes(input.maxNotional)) {
159
179
  missing.push("maxNotional");
160
180
  }
@@ -194,6 +214,31 @@ async function confirmCancel(order, probes) {
194
214
  function isSafeCancelTerminalState(status) {
195
215
  return status === "CANCELED" || status === "EXPIRED" || status === "REJECTED";
196
216
  }
217
+ async function confirmCleanup(order, probes) {
218
+ const observations = [];
219
+ let current = await probes.cleanupCheck(order);
220
+ observations.push(cleanupEvidence(current));
221
+ if (isCleanupClean(current)) {
222
+ return { confirmed: true, cleanup: current, evidence: observations.join("->") };
223
+ }
224
+ const delays = probes.cleanupCheckDelaysMs ?? probes.cancelConfirmationDelaysMs ?? [100, 250, 500, 1_000, 2_000];
225
+ const wait = probes.wait ?? defaultWait;
226
+ for (const delay of delays) {
227
+ await wait(delay);
228
+ current = await probes.cleanupCheck(order);
229
+ observations.push(cleanupEvidence(current));
230
+ if (isCleanupClean(current)) {
231
+ return { confirmed: true, cleanup: current, evidence: observations.join("->") };
232
+ }
233
+ }
234
+ return { confirmed: false, cleanup: current, evidence: observations.join("->") };
235
+ }
236
+ function isCleanupClean(cleanup) {
237
+ return cleanup.openOrders === 0 && cleanup.unexpectedPositions === 0;
238
+ }
239
+ function cleanupEvidence(cleanup) {
240
+ return `openOrders=${cleanup.openOrders};unexpectedPositions=${cleanup.unexpectedPositions}`;
241
+ }
197
242
  function defaultWait(milliseconds) {
198
243
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
199
244
  }
@@ -1 +1 @@
1
- export const RAPIDX_VERSION = "1.0.28";
1
+ export const RAPIDX_VERSION = "1.0.29";