@liquiditytech/rapidx-cli 1.0.28 → 1.0.30

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.
@@ -1,10 +1,10 @@
1
- import { ProductError } from "../errors/product-error.js";
1
+ import { CORE_ERRORS, ProductError } from "../errors/product-error.js";
2
2
  const stringSchema = { type: "string" };
3
3
  const booleanSchema = { type: "boolean" };
4
4
  const numberSchema = { type: "number" };
5
5
  const symbolSchema = {
6
6
  type: "string",
7
- description: "RapidX symbol. Recommended format: BINANCE_PERP_<BASE>_<QUOTE>.",
7
+ description: "RapidX symbol. Recommended format: BINANCE_PERP_<BASE>_<QUOTE>. OKX_PERP_<BASE>_<QUOTE> is supported; OKX_SWAP_<BASE>_<QUOTE> is accepted as an input alias and normalized to OKX_PERP.",
8
8
  examples: ["BINANCE_PERP_BTC_USDT", "BINANCE_PERP_ETH_USDT"]
9
9
  };
10
10
  const sideSchema = {
@@ -15,7 +15,7 @@ const sideSchema = {
15
15
  const orderTypeSchema = {
16
16
  type: "string",
17
17
  enum: ["LIMIT", "MARKET"],
18
- description: "Order type. LIMIT is the expected default for agent-driven trading; MARKET may be blocked by safety policy."
18
+ description: "Order type. MARKET is allowed when the write operation passes preview and consent checks; preview includes immediate-execution and slippage risk notes."
19
19
  };
20
20
  const priceSchema = {
21
21
  type: "string",
@@ -55,6 +55,19 @@ const acceptedRiskTextSchema = {
55
55
  description: "Human confirmation text bound to the exact symbol, side, maxNotional, real-order risk, and cancel/cleanup behavior.",
56
56
  examples: ["I authorize a real verification order for BINANCE_PERP_BTC_USDT BUY maxNotional 10 with cancel cleanup."]
57
57
  };
58
+ const automationModeSchema = {
59
+ type: "boolean",
60
+ description: "Set true only when the human user has explicitly enabled RapidX automation mode in chat for this preview."
61
+ };
62
+ const automationConsentTextSchema = {
63
+ type: "string",
64
+ description: "Human chat authorization text for automation mode. The agent must not invent this text.",
65
+ examples: ["I enable RapidX automation mode for BINANCE_PERP_BTC_USDT with maxNotional 100 and accept automated preview-submit execution."]
66
+ };
67
+ const automationScopeSchema = {
68
+ type: "string",
69
+ description: "Optional automation scope label, for example single-preview, strategy-session, or the user's exact scope phrase."
70
+ };
58
71
  const previewIdSchema = {
59
72
  type: "string",
60
73
  description: "Preview id returned by the matching preview tool or command.",
@@ -73,7 +86,7 @@ const targetCapabilityIdSchema = {
73
86
  const orderIdSchema = {
74
87
  type: "string",
75
88
  description: "RapidX order id returned by order.place, order.list, or order.get.",
76
- examples: ["1234567890"]
89
+ examples: ["2173374713391199"]
77
90
  };
78
91
  const accountBalanceModeSchema = {
79
92
  type: "string",
@@ -82,6 +95,7 @@ const accountBalanceModeSchema = {
82
95
  };
83
96
  const positionSideSchema = {
84
97
  type: "string",
98
+ enum: ["LONG", "SHORT"],
85
99
  description: "Position side when the account is in hedge mode, for example LONG or SHORT. Omit in one-way NET mode unless RapidX returns a specific side."
86
100
  };
87
101
  const closePositionReduceOnlySchema = {
@@ -103,7 +117,10 @@ export function inputSchemaForName(name) {
103
117
  case "AccountBalanceInput":
104
118
  return objectSchema({ mode: accountBalanceModeSchema, currency: stringSchema });
105
119
  case "OrderLookupInput":
106
- return objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, symbol: symbolSchema });
120
+ return {
121
+ ...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, symbol: symbolSchema }),
122
+ anyOf: [{ required: ["orderId"] }, { required: ["clientOrderId"] }]
123
+ };
107
124
  case "OrderListInput":
108
125
  case "OrderHistoryInput":
109
126
  return objectSchema({ symbol: symbolSchema, pageSize: numberSchema, startTime: stringSchema, endTime: stringSchema });
@@ -121,6 +138,7 @@ export function inputSchemaForName(name) {
121
138
  price: priceSchema,
122
139
  quantity: quantitySchema,
123
140
  amount: amountSchema,
141
+ positionSide: positionSideSchema,
124
142
  timeInForce: stringSchema,
125
143
  postOnly: booleanSchema,
126
144
  reduceOnly: booleanSchema,
@@ -140,6 +158,7 @@ export function inputSchemaForName(name) {
140
158
  takeProfitLimitPrice: stringSchema,
141
159
  algoType: stringSchema,
142
160
  consentId: stringSchema,
161
+ ...automationProperties(),
143
162
  ...compatibilityProperties()
144
163
  }, ["targetCapabilityId"]);
145
164
  case "PreviewOrderInput":
@@ -147,6 +166,7 @@ export function inputSchemaForName(name) {
147
166
  ...objectSchema({
148
167
  ...tradeCreateProperties(),
149
168
  consentId: stringSchema,
169
+ ...automationProperties(),
150
170
  ...compatibilityProperties()
151
171
  }, ["symbol", "side", "orderType", "maxNotional", "clientOrderId"]),
152
172
  oneOf: [{ required: ["quantity"] }, { required: ["amount"] }]
@@ -166,7 +186,7 @@ export function inputSchemaForName(name) {
166
186
  };
167
187
  case "AmendOrderPreviewInput":
168
188
  return {
169
- ...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, price: priceSchema, quantity: quantitySchema, ...compatibilityProperties() }),
189
+ ...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, price: priceSchema, quantity: quantitySchema, ...automationProperties(), ...compatibilityProperties() }),
170
190
  allOf: [
171
191
  { anyOf: [{ required: ["orderId"] }, { required: ["clientOrderId"] }] },
172
192
  { anyOf: [{ required: ["price"] }, { required: ["quantity"] }] }
@@ -174,7 +194,7 @@ export function inputSchemaForName(name) {
174
194
  };
175
195
  case "CancelOrderPreviewInput":
176
196
  return {
177
- ...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, ...compatibilityProperties() }),
197
+ ...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, ...automationProperties(), ...compatibilityProperties() }),
178
198
  anyOf: [{ required: ["orderId"] }, { required: ["clientOrderId"] }]
179
199
  };
180
200
  case "AmendOrderInput":
@@ -193,7 +213,7 @@ export function inputSchemaForName(name) {
193
213
  case "ClosePositionInput":
194
214
  return objectSchema({ symbol: symbolSchema, positionSide: positionSideSchema, reduceOnly: closePositionReduceOnlySchema, maxNotional: maxNotionalSchema, previewId: previewIdSchema, continueConsentId: continueConsentIdSchema }, ["symbol", "reduceOnly", "maxNotional", "previewId", "continueConsentId"]);
195
215
  case "SetLeverageInput":
196
- return objectSchema({ symbol: symbolSchema, leverage: numberSchema, positionSide: stringSchema, previewId: previewIdSchema, continueConsentId: continueConsentIdSchema }, ["symbol", "leverage", "previewId", "continueConsentId"]);
216
+ return objectSchema({ symbol: symbolSchema, leverage: numberSchema, positionSide: positionSideSchema, previewId: previewIdSchema, continueConsentId: continueConsentIdSchema }, ["symbol", "leverage", "previewId", "continueConsentId"]);
197
217
  case "SetPositionModeInput":
198
218
  return objectSchema({ mode: { type: "string", enum: ["BOTH", "NET"] }, exchange: stringSchema, previewId: previewIdSchema, continueConsentId: continueConsentIdSchema }, ["mode", "exchange", "previewId", "continueConsentId"]);
199
219
  case "AlgoPlaceInput":
@@ -201,7 +221,6 @@ export function inputSchemaForName(name) {
201
221
  ...objectSchema({
202
222
  ...tradeCreateProperties(),
203
223
  algoType: stringSchema,
204
- positionSide: stringSchema,
205
224
  conditionType: algoConditionTypeSchema,
206
225
  triggerPrice: stringSchema,
207
226
  triggerType: triggerTypeSchema,
@@ -221,7 +240,7 @@ export function inputSchemaForName(name) {
221
240
  "previewId",
222
241
  "continueConsentId"
223
242
  ]),
224
- oneOf: [{ required: ["quantity"] }, { required: ["amount"] }]
243
+ anyOf: [{ required: ["quantity"] }, { required: ["amount"] }, { required: ["conditionType"] }]
225
244
  };
226
245
  case "AlgoAmendInput":
227
246
  return objectSchema({
@@ -244,6 +263,7 @@ export function inputSchemaForName(name) {
244
263
  side: sideSchema,
245
264
  maxNotional: maxNotionalSchema,
246
265
  clientOrderId: clientOrderIdSchema,
266
+ positionSide: positionSideSchema,
247
267
  explicitUserConsent: explicitUserConsentSchema,
248
268
  acceptedRiskText: acceptedRiskTextSchema
249
269
  }, ["symbol", "side", "maxNotional", "clientOrderId", "explicitUserConsent", "acceptedRiskText"]);
@@ -326,15 +346,27 @@ export function assertInputMatchesSchema(schemaName, input, options = {}) {
326
346
  collectAnyOfProblem(group.anyOf, input, problems);
327
347
  }
328
348
  }
349
+ collectSchemaSpecificProblems(schemaName, input, problems);
329
350
  if (problems.length > 0) {
330
351
  throw new ProductError({
331
- code: "RCORE00001",
332
- status: "FAIL",
352
+ code: CORE_ERRORS.SCHEMA_INVALID,
353
+ status: "INVALID_INPUT",
333
354
  message: `Input schema validation failed: ${problems.join("; ")}.`
334
355
  });
335
356
  }
336
357
  void allowedFields;
337
358
  }
359
+ function collectSchemaSpecificProblems(schemaName, input, problems) {
360
+ if (schemaName !== "AlgoPlaceInput") {
361
+ return;
362
+ }
363
+ if (String(input.conditionType ?? "").toUpperCase() !== "ENTIRE_CLOSE_POSITION") {
364
+ return;
365
+ }
366
+ if (!hasValue(input.stopLossPrice) && !hasValue(input.takeProfitPrice)) {
367
+ problems.push("TPSL ENTIRE_CLOSE_POSITION requires stopLossPrice or takeProfitPrice");
368
+ }
369
+ }
338
370
  function collectAnyOfProblem(rules, input, problems) {
339
371
  if (!rules.some((candidate) => (candidate.required ?? []).every((field) => hasValue(input[field])))) {
340
372
  const labels = rules.map((candidate) => (candidate.required ?? []).join(", ")).join(" or ");
@@ -362,7 +394,15 @@ function tradeCreateProperties() {
362
394
  postOnly: booleanSchema,
363
395
  reduceOnly: booleanSchema,
364
396
  maxNotional: maxNotionalSchema,
365
- clientOrderId: clientOrderIdSchema
397
+ clientOrderId: clientOrderIdSchema,
398
+ positionSide: positionSideSchema
399
+ };
400
+ }
401
+ function automationProperties() {
402
+ return {
403
+ automationMode: automationModeSchema,
404
+ automationConsentText: automationConsentTextSchema,
405
+ automationScope: automationScopeSchema
366
406
  };
367
407
  }
368
408
  function compatibilityProperties() {
@@ -12,11 +12,13 @@ export class ProductError extends Error {
12
12
  }
13
13
  }
14
14
  toEnvelope(evidence = []) {
15
+ const details = publicErrorDetails(this);
15
16
  return {
16
17
  ok: false,
17
18
  status: this.status,
18
19
  code: this.code,
19
20
  message: this.message,
21
+ ...(details ? { details } : {}),
20
22
  evidence
21
23
  };
22
24
  }
@@ -26,7 +28,10 @@ export const CORE_ERRORS = {
26
28
  INVALID_CREDENTIAL: "RCORE01002",
27
29
  MISSING_API_HOST: "RCORE01003",
28
30
  SECRET_REDACTION_RISK: "RCORE90002",
31
+ INVALID_ORDER_ID: "RCORE00002",
29
32
  UPSTREAM_REJECTED: "RCORE22001",
33
+ PERMISSION_SCOPE_ERROR: "RCORE01004",
34
+ NOT_FOUND: "RCORE22004",
30
35
  UPSTREAM_TIMEOUT: "RCORE23002",
31
36
  UPSTREAM_RATE_LIMITED: "RCORE23003",
32
37
  SCHEMA_INVALID: "RCORE30001"
@@ -49,6 +54,18 @@ export function normalizeUnknownError(error, fallbackCode = "RCORE00001") {
49
54
  message
50
55
  });
51
56
  }
57
+ export function publicErrorDetails(error) {
58
+ if (!error.details) {
59
+ return undefined;
60
+ }
61
+ const details = {};
62
+ for (const key of ["upstreamStatus", "upstreamCode", "upstreamMessage", "hint", "attempts"]) {
63
+ if (error.details[key] !== undefined) {
64
+ details[key] = error.details[key];
65
+ }
66
+ }
67
+ return Object.keys(details).length > 0 ? details : undefined;
68
+ }
52
69
  function isUpstreamNetworkError(error) {
53
70
  if (!(error instanceof Error)) {
54
71
  return false;
@@ -12,10 +12,12 @@ export * from "./errors/product-error.js";
12
12
  export * from "./client/signing.js";
13
13
  export * from "./client/rapid-x-client.js";
14
14
  export * from "./client/capability-executor.js";
15
+ export * from "./client/order-id.js";
15
16
  export * from "./client/symbol.js";
16
17
  export * from "./safety/policy.js";
17
18
  export * from "./safety/raw-api.js";
18
19
  export * from "./trading/preview.js";
20
+ export * from "./trading/preview-preflight.js";
19
21
  export * from "./update/check-update.js";
20
22
  export * from "./self-check/run-self-check.js";
21
23
  export * from "./self-check/live-read-only-probes.js";
@@ -78,8 +78,8 @@ export function evaluateSafety(capability, input, policy, state = makeSafetyStat
78
78
  return { allowed: false, code: "RCORE00001", reason: "mode must be BOTH or NET.", paramsHash };
79
79
  }
80
80
  }
81
- if (String(input.orderType ?? "").toUpperCase() === "MARKET" && (policy.marketOrderPolicy ?? "BLOCK_BY_DEFAULT") === "BLOCK_BY_DEFAULT") {
82
- return { allowed: false, code: "RCORE20002", reason: "Market orders are blocked by default.", paramsHash };
81
+ if (String(input.orderType ?? "").toUpperCase() === "MARKET" && (policy.marketOrderPolicy ?? "ALLOW") === "BLOCK_BY_DEFAULT") {
82
+ return { allowed: false, code: "RCORE20002", reason: "Market orders are blocked by safety policy.", paramsHash };
83
83
  }
84
84
  if (requiresNotionalLimit(capability)) {
85
85
  const maxNotional = input.maxNotional ?? policy.maxNotional;
@@ -114,7 +114,7 @@ function requiredTradeFields(capability, input) {
114
114
  requireString(input, "orderType", missing);
115
115
  requireString(input, "clientOrderId", missing);
116
116
  requireString(input, "maxNotional", missing);
117
- if (!hasString(input, "quantity") && !hasString(input, "amount")) {
117
+ if (!hasString(input, "quantity") && !hasString(input, "amount") && !isEntireCloseAlgo(capability, input)) {
118
118
  missing.push("quantity or amount");
119
119
  }
120
120
  if (String(input.orderType ?? "").toUpperCase() === "LIMIT") {
@@ -194,6 +194,9 @@ function stringValue(value) {
194
194
  function hasString(input, field) {
195
195
  return typeof input[field] === "string" && input[field].length > 0;
196
196
  }
197
+ function isEntireCloseAlgo(capability, input) {
198
+ return capability.capabilityId === "algo.place" && String(input.conditionType ?? "").toUpperCase() === "ENTIRE_CLOSE_POSITION";
199
+ }
197
200
  function requireString(input, field, missing) {
198
201
  if (!hasString(input, field)) {
199
202
  missing.push(field);
@@ -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 };
@@ -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)) {
@@ -0,0 +1,333 @@
1
+ import { RapidXClient } from "../client/rapid-x-client.js";
2
+ import { assertOrderLookupReference } from "../client/order-id.js";
3
+ import { parseRapidXSymbol } from "../client/symbol.js";
4
+ import { ProductError } from "../errors/product-error.js";
5
+ export async function runPreviewPreflight(capabilityId, input, options = {}) {
6
+ switch (capabilityId) {
7
+ case "order.preview":
8
+ case "order.place-preview":
9
+ case "order.place":
10
+ case "algo.place":
11
+ return validateOrderPlacementPreflight(input, options);
12
+ case "order.cancel":
13
+ return validateOrderMutationPreflight(input, "cancel", options);
14
+ case "order.amend":
15
+ return validateOrderMutationPreflight(input, "amend", options);
16
+ case "position.close":
17
+ return validatePositionClosePreflight(input, options);
18
+ default:
19
+ return {};
20
+ }
21
+ }
22
+ async function validateOrderPlacementPreflight(input, options) {
23
+ const symbolInput = requiredString(input.symbol, "symbol");
24
+ const parsed = parseRapidXSymbol(symbolInput);
25
+ const client = options.client ?? new RapidXClient();
26
+ const response = await client.get("/api/v1/trading/sym/info", { sym: parsed.canonicalSymbol });
27
+ const rules = extractSymbolRules(response, parsed.canonicalSymbol);
28
+ validateQuantity(input, rules);
29
+ validatePrice(input, rules);
30
+ validateNotional(input, rules);
31
+ return {
32
+ marketRules: compactObject({
33
+ symbol: parsed.canonicalSymbol,
34
+ inputSymbol: parsed.inputSymbol !== parsed.canonicalSymbol ? parsed.inputSymbol : undefined,
35
+ minNotional: rules.minNotional,
36
+ minSize: rules.minSize,
37
+ lotSize: rules.lotSize,
38
+ qtyPrecision: rules.qtyPrecision,
39
+ tickSize: rules.tickSize,
40
+ pricePrecision: rules.pricePrecision
41
+ })
42
+ };
43
+ }
44
+ async function validateOrderMutationPreflight(input, action, options) {
45
+ const client = options.client ?? new RapidXClient();
46
+ const params = orderLookupParams(input);
47
+ const response = await client.get("/api/v1/trading/order", params);
48
+ const order = extractOrderReadback(response);
49
+ if (!order) {
50
+ throw new ProductError({
51
+ code: "RCORE22004",
52
+ status: "BLOCKED",
53
+ message: "ORDER_NOT_FOUND: order.get did not return an order for preview preflight."
54
+ });
55
+ }
56
+ const state = normalizeOrderState(order.orderState ?? order.state ?? order.status);
57
+ if (!isOpenOrderState(state)) {
58
+ throw new ProductError({
59
+ code: "RCORE22005",
60
+ status: "BLOCKED",
61
+ message: `ORDER_NOT_OPEN: order is ${state || "UNKNOWN"} and cannot be ${action}ed.`
62
+ });
63
+ }
64
+ return {
65
+ orderReadback: {
66
+ ...order,
67
+ orderState: state || order.orderState
68
+ }
69
+ };
70
+ }
71
+ async function validatePositionClosePreflight(input, options) {
72
+ const symbolInput = requiredString(input.symbol, "symbol");
73
+ const parsed = parseRapidXSymbol(symbolInput);
74
+ const client = options.client ?? new RapidXClient();
75
+ const response = await client.get("/api/v1/trading/position", { sym: parsed.canonicalSymbol });
76
+ const positions = extractPositions(response);
77
+ const positionSide = typeof input.positionSide === "string" && input.positionSide.length > 0 ? input.positionSide.toUpperCase() : undefined;
78
+ const position = positions.find((item) => {
79
+ const itemSymbol = stringField(item, "sym") ?? stringField(item, "symbol");
80
+ const itemSide = stringField(item, "positionSide")?.toUpperCase();
81
+ const qty = Number(stringField(item, "positionQty") ?? stringField(item, "quantity") ?? stringField(item, "qty") ?? "0");
82
+ return itemSymbol === parsed.canonicalSymbol && Math.abs(qty) > 0 && (!positionSide || !itemSide || itemSide === positionSide);
83
+ });
84
+ if (!position) {
85
+ throw new ProductError({
86
+ code: "RCORE24003",
87
+ status: "BLOCKED",
88
+ message: `NO_POSITION_TO_CLOSE: no non-zero position found for ${parsed.canonicalSymbol}.`
89
+ });
90
+ }
91
+ return {
92
+ positionReadback: {
93
+ ...position,
94
+ sym: stringField(position, "sym") ?? parsed.canonicalSymbol
95
+ }
96
+ };
97
+ }
98
+ function extractSymbolRules(response, symbol) {
99
+ const data = unwrapData(response);
100
+ const candidates = [
101
+ isRecord(data) ? data[symbol] : undefined,
102
+ isRecord(data) && isRecord(data.data) ? data.data[symbol] : undefined,
103
+ isRecord(data) && Array.isArray(data.list) ? data.list.find((item) => isRecord(item) && (item.sym === symbol || item.symbol === symbol)) : undefined,
104
+ data
105
+ ];
106
+ const record = candidates.find((item) => isSymbolRuleRecord(item, symbol));
107
+ if (!record) {
108
+ throw new ProductError({
109
+ code: "RCORE12001",
110
+ status: "BLOCKED",
111
+ message: `INVALID_SYMBOL: ${symbol} was not found in RapidX symbol-info.`
112
+ });
113
+ }
114
+ const rules = { symbol };
115
+ assignIfDefined(rules, "minNotional", stringField(record, "minNotional"));
116
+ assignIfDefined(rules, "minSize", stringField(record, "minSize") ?? stringField(record, "minQty") ?? stringField(record, "minOrderQty"));
117
+ assignIfDefined(rules, "lotSize", stringField(record, "lotSize") ?? stringField(record, "qtyStep") ?? stringField(record, "stepSize"));
118
+ assignIfDefined(rules, "qtyPrecision", stringField(record, "qtyPrecision") ?? stringField(record, "quantityPrecision"));
119
+ assignIfDefined(rules, "tickSize", stringField(record, "tickSize") ?? stringField(record, "priceTick") ?? stringField(record, "priceStep"));
120
+ assignIfDefined(rules, "pricePrecision", stringField(record, "pricePrecision"));
121
+ return rules;
122
+ }
123
+ function assignIfDefined(target, key, value) {
124
+ if (value !== undefined) {
125
+ target[key] = value;
126
+ }
127
+ }
128
+ function isSymbolRuleRecord(value, symbol) {
129
+ if (!isRecord(value) || Object.keys(value).length === 0) {
130
+ return false;
131
+ }
132
+ const recordSymbol = stringField(value, "sym") ?? stringField(value, "symbol");
133
+ if (recordSymbol !== undefined) {
134
+ return recordSymbol === symbol;
135
+ }
136
+ return [
137
+ "minNotional",
138
+ "minSize",
139
+ "minQty",
140
+ "minOrderQty",
141
+ "lotSize",
142
+ "qtyStep",
143
+ "stepSize",
144
+ "qtyPrecision",
145
+ "quantityPrecision",
146
+ "tickSize",
147
+ "priceTick",
148
+ "priceStep",
149
+ "pricePrecision"
150
+ ].some((field) => stringField(value, field) !== undefined);
151
+ }
152
+ function validateNotional(input, rules) {
153
+ const minNotional = positiveNumber(rules.minNotional);
154
+ if (minNotional === undefined) {
155
+ return;
156
+ }
157
+ const notional = estimateNotional(input);
158
+ if (notional !== undefined && notional < minNotional - 1e-12) {
159
+ throw new ProductError({
160
+ code: "RCORE24005",
161
+ status: "BLOCKED",
162
+ message: `MIN_NOTIONAL_NOT_MET: estimatedNotional ${formatNumber(notional)} is below minNotional ${rules.minNotional}.`
163
+ });
164
+ }
165
+ }
166
+ function validateQuantity(input, rules) {
167
+ const quantityValue = input.quantity ?? input.qty;
168
+ if (quantityValue === undefined || quantityValue === null || quantityValue === "") {
169
+ return;
170
+ }
171
+ const quantityText = String(quantityValue);
172
+ const quantity = positiveNumber(quantityText);
173
+ if (quantity === undefined) {
174
+ return;
175
+ }
176
+ const minSize = positiveNumber(rules.minSize);
177
+ if (minSize !== undefined && quantity < minSize - 1e-12) {
178
+ throw new ProductError({
179
+ code: "RCORE24006",
180
+ status: "BLOCKED",
181
+ message: `INVALID_QUANTITY: quantity ${quantityText} is below minSize ${rules.minSize}.`
182
+ });
183
+ }
184
+ const lotSize = positiveNumber(rules.lotSize);
185
+ if (lotSize !== undefined && !isAlignedToStep(quantity, lotSize)) {
186
+ throw new ProductError({
187
+ code: "RCORE24006",
188
+ status: "BLOCKED",
189
+ message: `INVALID_QUANTITY: quantity ${quantityText} is not aligned to lotSize ${rules.lotSize}.`
190
+ });
191
+ }
192
+ const qtyPrecision = integerValue(rules.qtyPrecision);
193
+ if (qtyPrecision !== undefined && decimalPlaces(quantityText) > qtyPrecision) {
194
+ throw new ProductError({
195
+ code: "RCORE24006",
196
+ status: "BLOCKED",
197
+ message: `INVALID_QUANTITY: quantity ${quantityText} exceeds qtyPrecision ${rules.qtyPrecision}.`
198
+ });
199
+ }
200
+ }
201
+ function validatePrice(input, rules) {
202
+ if (input.price === undefined || input.price === null || input.price === "") {
203
+ return;
204
+ }
205
+ const priceText = String(input.price);
206
+ const price = positiveNumber(priceText);
207
+ if (price === undefined) {
208
+ return;
209
+ }
210
+ const tickSize = positiveNumber(rules.tickSize);
211
+ if (tickSize !== undefined && !isAlignedToStep(price, tickSize)) {
212
+ throw new ProductError({
213
+ code: "RCORE24007",
214
+ status: "BLOCKED",
215
+ message: `INVALID_PRICE_TICK: price ${priceText} is not aligned to tickSize ${rules.tickSize}.`
216
+ });
217
+ }
218
+ const pricePrecision = integerValue(rules.pricePrecision);
219
+ if (pricePrecision !== undefined && decimalPlaces(priceText) > pricePrecision) {
220
+ throw new ProductError({
221
+ code: "RCORE24007",
222
+ status: "BLOCKED",
223
+ message: `INVALID_PRICE_TICK: price ${priceText} exceeds pricePrecision ${rules.pricePrecision}.`
224
+ });
225
+ }
226
+ }
227
+ function estimateNotional(input) {
228
+ const direct = positiveNumber(input.amount ?? input.notional);
229
+ if (direct !== undefined) {
230
+ return direct;
231
+ }
232
+ const price = positiveNumber(input.price);
233
+ const quantity = positiveNumber(input.quantity ?? input.qty);
234
+ if (price === undefined || quantity === undefined) {
235
+ return undefined;
236
+ }
237
+ return price * quantity;
238
+ }
239
+ function orderLookupParams(input) {
240
+ assertOrderLookupReference(input);
241
+ const params = compactObject({
242
+ orderId: input.orderId,
243
+ clientOrderId: input.clientOrderId,
244
+ symbol: input.symbol
245
+ });
246
+ return params;
247
+ }
248
+ function extractOrderReadback(response) {
249
+ const data = unwrapData(response);
250
+ if (isRecord(data) && Array.isArray(data.list)) {
251
+ return data.list.find(isRecord);
252
+ }
253
+ if (isRecord(data) && Array.isArray(data.orders)) {
254
+ return data.orders.find(isRecord);
255
+ }
256
+ return isRecord(data) && Object.keys(data).length > 0 ? data : undefined;
257
+ }
258
+ function extractPositions(response) {
259
+ const data = unwrapData(response);
260
+ if (Array.isArray(data)) {
261
+ return data.filter(isRecord);
262
+ }
263
+ if (isRecord(data) && Array.isArray(data.list)) {
264
+ return data.list.filter(isRecord);
265
+ }
266
+ if (isRecord(data) && Array.isArray(data.positions)) {
267
+ return data.positions.filter(isRecord);
268
+ }
269
+ return isRecord(data) ? [data] : [];
270
+ }
271
+ function unwrapData(response) {
272
+ if (isRecord(response) && "data" in response) {
273
+ return response.data;
274
+ }
275
+ return response;
276
+ }
277
+ function normalizeOrderState(value) {
278
+ return value === undefined || value === null ? "" : String(value).toUpperCase();
279
+ }
280
+ function isOpenOrderState(state) {
281
+ return ["NEW", "OPEN", "ACCEPTED", "PARTIALLY_FILLED"].includes(state);
282
+ }
283
+ function requiredString(value, field) {
284
+ if (value === undefined || value === null || value === "") {
285
+ throw new ProductError({
286
+ code: "RCORE00001",
287
+ status: "FAIL",
288
+ message: `${field} is required.`
289
+ });
290
+ }
291
+ return String(value);
292
+ }
293
+ function stringField(record, field) {
294
+ const value = record[field];
295
+ return value === undefined || value === null || value === "" ? undefined : String(value);
296
+ }
297
+ function positiveNumber(value) {
298
+ if (value === undefined || value === null || value === "") {
299
+ return undefined;
300
+ }
301
+ const number = Number(value);
302
+ return Number.isFinite(number) && number > 0 ? number : undefined;
303
+ }
304
+ function integerValue(value) {
305
+ if (value === undefined || value === null || value === "") {
306
+ return undefined;
307
+ }
308
+ const number = Number(value);
309
+ return Number.isInteger(number) && number >= 0 ? number : undefined;
310
+ }
311
+ function isAlignedToStep(value, step) {
312
+ const ratio = value / step;
313
+ return Math.abs(ratio - Math.round(ratio)) < 1e-9;
314
+ }
315
+ function decimalPlaces(value) {
316
+ const normalized = value.toLowerCase();
317
+ if (normalized.includes("e")) {
318
+ const [mantissa, exponentRaw] = normalized.split("e");
319
+ const exponent = Number(exponentRaw);
320
+ return Math.max(0, decimalPlaces(mantissa ?? "0") - exponent);
321
+ }
322
+ const [, fractional = ""] = normalized.split(".");
323
+ return fractional.replace(/0+$/, "").length;
324
+ }
325
+ function formatNumber(value) {
326
+ return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(12)));
327
+ }
328
+ function compactObject(input) {
329
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== null && value !== ""));
330
+ }
331
+ function isRecord(value) {
332
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
333
+ }