@liquiditytech/rapidx-cli 1.0.27 → 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.
@@ -13,6 +13,7 @@ export function buildLiveTradingVerificationProbes(input = {}) {
13
13
  orderType: "LIMIT",
14
14
  price: orderInput.price,
15
15
  quantity: orderInput.quantity,
16
+ positionSide: orderInput.positionSide,
16
17
  maxNotional: orderInput.maxNotional,
17
18
  clientOrderId: orderInput.clientOrderId,
18
19
  postOnly: true,
@@ -34,7 +35,7 @@ export function buildLiveTradingVerificationProbes(input = {}) {
34
35
  previewId: `internal-${order.orderId}`,
35
36
  continueConsentId: continueConsentId(order.orderId, "cancel")
36
37
  });
37
- const data = extractDataObject(result);
38
+ const data = extractDataObject(extractWrappedResponse(result));
38
39
  const status = normalizeOrderStatus(stringField(data, "orderState") ?? stringField(data, "action") ?? "");
39
40
  if (status === "CANCELED") {
40
41
  return { orderId: order.orderId, status };
@@ -97,7 +98,7 @@ async function cleanupCheck(input) {
97
98
  openOrders: orders.filter((order) => {
98
99
  const state = normalizeOrderStatus(stringField(order, "orderState") ?? "OPEN");
99
100
  const orderSymbol = stringField(order, "sym") ?? stringField(order, "symbol") ?? symbol;
100
- return orderSymbol === symbol && state !== "CANCELED" && state !== "REJECTED" && state !== "FILLED";
101
+ return orderSymbol === symbol && state !== "CANCELED" && state !== "REJECTED" && state !== "EXPIRED" && state !== "FILLED";
101
102
  }).length,
102
103
  unexpectedPositions: positions.filter((position) => {
103
104
  const positionSymbol = stringField(position, "sym") ?? stringField(position, "symbol");
@@ -186,6 +187,12 @@ function extractDataObject(result) {
186
187
  }
187
188
  return {};
188
189
  }
190
+ function extractWrappedResponse(result) {
191
+ if (result && typeof result === "object" && !Array.isArray(result) && Object.prototype.hasOwnProperty.call(result, "response")) {
192
+ return result.response;
193
+ }
194
+ return result;
195
+ }
189
196
  function extractArrayData(result) {
190
197
  const data = extractData(result);
191
198
  if (Array.isArray(data)) {
@@ -244,6 +251,9 @@ function normalizeOrderStatus(state) {
244
251
  case "CANCELED":
245
252
  case "CANCEL_COMPLETE":
246
253
  return "CANCELED";
254
+ case "EXPIRED":
255
+ case "EXPIRE":
256
+ return "EXPIRED";
247
257
  case "REJECTED":
248
258
  return "REJECTED";
249
259
  default:
@@ -1,4 +1,4 @@
1
- import { getSchemaResult } from "../contracts/capabilities.js";
1
+ import { getSchemaResult, listMcpCapabilities } from "../contracts/capabilities.js";
2
2
  import { ProductError } from "../errors/product-error.js";
3
3
  import { checkForUpdate } from "../update/check-update.js";
4
4
  export async function runSelfCheck(options = {}) {
@@ -14,7 +14,9 @@ export async function runSelfCheck(options = {}) {
14
14
  timestamp: new Date().toISOString()
15
15
  });
16
16
  }
17
- add({ name: "discovery", status: "PASS", source: "local_check", message: `${getSchemaResult().capabilities.length} capabilities loaded.` });
17
+ const canonicalCapabilityCount = getSchemaResult().capabilities.length;
18
+ const mcpToolCount = new Set(listMcpCapabilities().map((capability) => capability.mcpTool)).size;
19
+ add({ name: "discovery", status: "PASS", source: "local_check", message: `${canonicalCapabilityCount} canonical capabilities loaded; ${mcpToolCount} MCP tools available.` });
18
20
  add({ name: "schema-compatibility", status: "PASS", source: "local_check", message: "Canonical schema is available." });
19
21
  let update;
20
22
  if (options.input?.checkUpdates) {
@@ -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") {