@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.
- package/dist/cli/commands/account.js +2 -2
- package/dist/cli/commands/algo.js +2 -2
- package/dist/cli/commands/market.js +2 -2
- package/dist/cli/commands/order.js +6 -4
- package/dist/cli/commands/position.js +2 -2
- package/dist/cli/commands/trade.js +3 -2
- package/dist/cli/envelope.js +2 -1
- package/dist/core/client/capability-executor.js +89 -14
- package/dist/core/client/order-id.js +24 -0
- package/dist/core/client/rapid-x-client.js +45 -9
- package/dist/core/client/symbol.js +6 -2
- package/dist/core/contracts/capabilities.js +4 -1
- package/dist/core/contracts/compatibility.js +1 -1
- package/dist/core/contracts/input-schema.js +53 -13
- package/dist/core/errors/product-error.js +17 -0
- package/dist/core/index.js +2 -0
- package/dist/core/safety/policy.js +6 -3
- package/dist/core/self-check/live-trading-verification-probes.js +8 -1
- package/dist/core/trading/preview-preflight.js +333 -0
- package/dist/core/trading/preview.js +88 -14
- package/dist/core/trading/trading-verification.js +56 -11
- package/dist/core/version.js +1 -1
- package/dist/mcp/tool-runner.js +27 -2
- package/package.json +1 -1
- package/packages/distribution/docs/cli.md +11 -1
- package/packages/distribution/docs/mcp.md +7 -2
- package/packages/distribution/docs/quickstart.md +9 -1
- package/packages/distribution/docs/tools.md +38 -2
- package/packages/distribution/manifests/offline-manifest.json +4 -4
- package/packages/distribution/registry/rapidx.mcp.json +1 -1
|
@@ -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.
|
|
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: ["
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
332
|
-
status: "
|
|
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;
|
package/dist/core/index.js
CHANGED
|
@@ -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 ?? "
|
|
82
|
-
return { allowed: false, code: "RCORE20002", reason: "Market orders are blocked by
|
|
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
|
+
}
|