@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.
- package/dist/cli/commands/order.js +3 -1
- package/dist/cli/commands/trade.js +2 -1
- package/dist/core/client/capability-executor.js +81 -13
- package/dist/core/client/rapid-x-client.js +41 -8
- 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 +48 -11
- package/dist/core/errors/product-error.js +2 -0
- package/dist/core/index.js +1 -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 +338 -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 +25 -2
- package/package.json +1 -1
- package/packages/distribution/docs/cli.md +9 -1
- package/packages/distribution/docs/mcp.md +7 -2
- package/packages/distribution/docs/quickstart.md +9 -1
- package/packages/distribution/docs/tools.md +36 -2
- package/packages/distribution/manifests/offline-manifest.json +4 -4
- package/packages/distribution/registry/rapidx.mcp.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { consumePreview, createTargetedTradePreview, createTradePreview, defaultSafetyPolicy, executeRapidXCapability, findCapabilityById, loadPreviewStoreFromFile, makeSafetyState, normalizeUnknownError, resolvePreviewStoreFile, savePreviewStoreToFile, verifyPreview } from "../../core/index.js";
|
|
1
|
+
import { consumePreview, createTargetedTradePreview, createTradePreview, defaultSafetyPolicy, executeRapidXCapability, findCapabilityById, loadPreviewStoreFromFile, makeSafetyState, normalizeUnknownError, resolvePreviewStoreFile, runPreviewPreflight, savePreviewStoreToFile, verifyPreview } from "../../core/index.js";
|
|
2
2
|
import { fail, ok } from "../envelope.js";
|
|
3
3
|
import { writeCliAudit } from "../audit.js";
|
|
4
4
|
const safetyState = makeSafetyState();
|
|
@@ -12,6 +12,7 @@ export async function runOrderCommand(action, input) {
|
|
|
12
12
|
}
|
|
13
13
|
try {
|
|
14
14
|
const preview = createTradePreview(capability, input, { ...defaultSafetyPolicy(), tradingEnabled: true, readOnly: false }, safetyState, previewStore);
|
|
15
|
+
Object.assign(preview, await runPreviewPreflight("order.place", input));
|
|
15
16
|
savePreviewStoreToFile(previewStoreFile, previewStore);
|
|
16
17
|
return ok(preview, `rapidx order ${action}`);
|
|
17
18
|
}
|
|
@@ -28,6 +29,7 @@ export async function runOrderCommand(action, input) {
|
|
|
28
29
|
}
|
|
29
30
|
try {
|
|
30
31
|
const preview = createTargetedTradePreview(targetCapability, input, { ...defaultSafetyPolicy(), tradingEnabled: true, readOnly: false }, safetyState, previewStore);
|
|
32
|
+
Object.assign(preview, await runPreviewPreflight(previewTarget, input));
|
|
31
33
|
savePreviewStoreToFile(previewStoreFile, previewStore);
|
|
32
34
|
return ok(preview, `rapidx order ${action}`);
|
|
33
35
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTargetedTradePreview, defaultSafetyPolicy, findCapabilityById, loadPreviewStoreFromFile, makeSafetyState, normalizeUnknownError, resolvePreviewStoreFile, savePreviewStoreToFile } from "../../core/index.js";
|
|
1
|
+
import { createTargetedTradePreview, defaultSafetyPolicy, findCapabilityById, loadPreviewStoreFromFile, makeSafetyState, normalizeUnknownError, resolvePreviewStoreFile, runPreviewPreflight, savePreviewStoreToFile } from "../../core/index.js";
|
|
2
2
|
import { fail, ok } from "../envelope.js";
|
|
3
3
|
const safetyState = makeSafetyState();
|
|
4
4
|
export async function runTradeCommand(action, input) {
|
|
@@ -17,6 +17,7 @@ export async function runTradeCommand(action, input) {
|
|
|
17
17
|
const previewStore = loadPreviewStoreFromFile(previewStoreFile);
|
|
18
18
|
try {
|
|
19
19
|
const preview = createTargetedTradePreview(targetCapability, input, { ...defaultSafetyPolicy(), tradingEnabled: true, readOnly: false }, safetyState, previewStore);
|
|
20
|
+
Object.assign(preview, await runPreviewPreflight(targetCapability.capabilityId, input));
|
|
20
21
|
savePreviewStoreToFile(previewStoreFile, previewStore);
|
|
21
22
|
return ok(preview, "rapidx trade preview");
|
|
22
23
|
}
|
|
@@ -32,7 +32,7 @@ export async function executeRapidXCapability(capabilityId, input = {}, options
|
|
|
32
32
|
case "order.amend":
|
|
33
33
|
return client.put("/api/v1/trading/order", orderAmendBody(input));
|
|
34
34
|
case "order.cancel":
|
|
35
|
-
return client
|
|
35
|
+
return executeOrderCancelCapability(client, input);
|
|
36
36
|
case "order.get":
|
|
37
37
|
return client.get("/api/v1/trading/order", optionalParams(input, ["orderId", "clientOrderId", "symbol"]));
|
|
38
38
|
case "order.list":
|
|
@@ -49,10 +49,11 @@ export async function executeRapidXCapability(capabilityId, input = {}, options
|
|
|
49
49
|
case "position.close":
|
|
50
50
|
return executePositionCloseCapability(client, input);
|
|
51
51
|
case "position.set-leverage":
|
|
52
|
-
return client.post("/api/v1/trading/position/leverage", {
|
|
52
|
+
return client.post("/api/v1/trading/position/leverage", compactObject({
|
|
53
53
|
sym: String(input.symbol),
|
|
54
|
-
leverage: String(input.leverage)
|
|
55
|
-
|
|
54
|
+
leverage: String(input.leverage),
|
|
55
|
+
positionSide: input.positionSide
|
|
56
|
+
}));
|
|
56
57
|
case "algo.place":
|
|
57
58
|
return client.post("/api/v1/algo/order", algoPlaceBody(input));
|
|
58
59
|
case "algo.amend":
|
|
@@ -70,8 +71,9 @@ export async function executeRapidXCapability(capabilityId, input = {}, options
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
async function executeMarketCapability(capabilityId, input, fetchFn) {
|
|
73
|
-
const
|
|
74
|
-
const parsed = parseRapidXSymbol(
|
|
74
|
+
const inputSymbol = stringValue(input.symbol ?? input.sym, "BINANCE_PERP_BTC_USDT");
|
|
75
|
+
const parsed = parseRapidXSymbol(inputSymbol);
|
|
76
|
+
const symbol = parsed.canonicalSymbol;
|
|
75
77
|
if (capabilityId === "market.funding-rate" || capabilityId === "market.mark-price" || capabilityId === "market.symbol-info") {
|
|
76
78
|
const client = new RapidXClient({ fetchFn });
|
|
77
79
|
const path = capabilityId === "market.funding-rate"
|
|
@@ -96,6 +98,7 @@ async function executeMarketCapability(capabilityId, input, fetchFn) {
|
|
|
96
98
|
case "market.ticker": {
|
|
97
99
|
const raw = await fetchJson(fetchFn, `${base}${parsed.isPerp ? "/fapi/v1/ticker/24hr" : "/api/v3/ticker/24hr"}`, { symbol: parsed.binanceSymbol });
|
|
98
100
|
return {
|
|
101
|
+
...symbolMetadata(symbol, inputSymbol, parsed.binanceSymbol),
|
|
99
102
|
symbol,
|
|
100
103
|
lastPrice: raw.lastPrice,
|
|
101
104
|
priceChange: raw.priceChange,
|
|
@@ -108,7 +111,7 @@ async function executeMarketCapability(capabilityId, input, fetchFn) {
|
|
|
108
111
|
}
|
|
109
112
|
case "market.orderbook": {
|
|
110
113
|
const raw = await fetchJson(fetchFn, `${base}${parsed.isPerp ? "/fapi/v1/depth" : "/api/v3/depth"}`, { symbol: parsed.binanceSymbol, limit: stringValue(input.depth, "20") });
|
|
111
|
-
return normalizeOrderbook(symbol, raw.bids, raw.asks);
|
|
114
|
+
return normalizeOrderbook(symbol, raw.bids, raw.asks, symbolMetadata(symbol, inputSymbol, parsed.binanceSymbol));
|
|
112
115
|
}
|
|
113
116
|
case "market.klines": {
|
|
114
117
|
const raw = await fetchJson(fetchFn, `${base}${parsed.isPerp ? "/fapi/v1/klines" : "/api/v3/klines"}`, {
|
|
@@ -116,13 +119,14 @@ async function executeMarketCapability(capabilityId, input, fetchFn) {
|
|
|
116
119
|
interval: stringValue(input.interval, "1h"),
|
|
117
120
|
limit: stringValue(input.limit, "100")
|
|
118
121
|
});
|
|
119
|
-
return { symbol, interval: stringValue(input.interval, "1h"), count: raw.length, candles: raw };
|
|
122
|
+
return { ...symbolMetadata(symbol, inputSymbol, parsed.binanceSymbol), symbol, interval: stringValue(input.interval, "1h"), count: raw.length, candles: raw };
|
|
120
123
|
}
|
|
121
124
|
case "market.open-interest": {
|
|
122
125
|
if (!parsed.isPerp) {
|
|
123
126
|
throw new ProductError({ code: "RCORE00001", status: "FAIL", message: "open interest only supports perpetual symbols" });
|
|
124
127
|
}
|
|
125
|
-
|
|
128
|
+
const raw = await fetchJson(fetchFn, `${base}/fapi/v1/openInterest`, { symbol: parsed.binanceSymbol });
|
|
129
|
+
return { ...raw, ...symbolMetadata(symbol, inputSymbol, String(raw.symbol ?? parsed.binanceSymbol)), symbol };
|
|
126
130
|
}
|
|
127
131
|
default:
|
|
128
132
|
throw new ProductError({ code: "RCORE30002", status: "FAIL", message: `Unknown market capability: ${capabilityId}` });
|
|
@@ -159,23 +163,39 @@ async function executePositionCloseCapability(client, input) {
|
|
|
159
163
|
response
|
|
160
164
|
};
|
|
161
165
|
}
|
|
166
|
+
async function executeOrderCancelCapability(client, input) {
|
|
167
|
+
const response = await client.delete("/api/v1/trading/order", orderCancelBody(input));
|
|
168
|
+
const lastObservedOrderState = normalizeCancelOrderState(extractOrderState(response));
|
|
169
|
+
const terminalStateConfirmed = isCancelTerminalState(lastObservedOrderState);
|
|
170
|
+
return {
|
|
171
|
+
cancelAccepted: true,
|
|
172
|
+
terminalStateConfirmed,
|
|
173
|
+
...compactObject({ lastObservedOrderState }),
|
|
174
|
+
recommendedAction: terminalStateConfirmed
|
|
175
|
+
? "verify final status with order/get or order/list when needed"
|
|
176
|
+
: "poll order/get until CANCELED, REJECTED, EXPIRED, or timeout",
|
|
177
|
+
response
|
|
178
|
+
};
|
|
179
|
+
}
|
|
162
180
|
async function executeOkxMarketCapability(capabilityId, symbol, instId, input, fetchFn) {
|
|
181
|
+
const inputSymbol = stringValue(input.symbol ?? input.sym, symbol);
|
|
163
182
|
switch (capabilityId) {
|
|
164
183
|
case "market.ticker": {
|
|
165
184
|
const data = await fetchOkx(fetchFn, "/api/v5/market/ticker", { instId });
|
|
166
185
|
const raw = data[0] ?? {};
|
|
167
|
-
return { symbol, lastPrice: raw.last, highPrice: raw.high24h, lowPrice: raw.low24h, volume: raw.vol24h, quoteVolume: raw.volCcy24h };
|
|
186
|
+
return { ...symbolMetadata(symbol, inputSymbol, String(raw.instId ?? instId)), symbol, lastPrice: raw.last, highPrice: raw.high24h, lowPrice: raw.low24h, volume: raw.vol24h, quoteVolume: raw.volCcy24h };
|
|
168
187
|
}
|
|
169
188
|
case "market.orderbook": {
|
|
170
189
|
const data = await fetchOkx(fetchFn, "/api/v5/market/books", { instId, sz: stringValue(input.depth, "20") });
|
|
171
190
|
const raw = data[0] ?? { bids: [], asks: [] };
|
|
172
191
|
const bids = Array.isArray(raw.bids) ? raw.bids : [];
|
|
173
192
|
const asks = Array.isArray(raw.asks) ? raw.asks : [];
|
|
174
|
-
return normalizeOrderbook(symbol, bids, asks);
|
|
193
|
+
return normalizeOrderbook(symbol, bids, asks, symbolMetadata(symbol, inputSymbol, instId));
|
|
175
194
|
}
|
|
176
195
|
case "market.open-interest": {
|
|
177
196
|
const data = await fetchOkx(fetchFn, "/api/v5/public/open-interest", { instId });
|
|
178
|
-
|
|
197
|
+
const raw = data[0] ?? {};
|
|
198
|
+
return { ...raw, ...symbolMetadata(symbol, inputSymbol, String(raw.instId ?? instId)), symbol };
|
|
179
199
|
}
|
|
180
200
|
default:
|
|
181
201
|
throw new ProductError({ code: "RCORE30002", status: "FAIL", message: `OKX adapter not registered for ${capabilityId}` });
|
|
@@ -200,10 +220,11 @@ async function fetchOkx(fetchFn, path, params) {
|
|
|
200
220
|
}
|
|
201
221
|
return payload.data ?? [];
|
|
202
222
|
}
|
|
203
|
-
function normalizeOrderbook(symbol, bidsRaw, asksRaw) {
|
|
223
|
+
function normalizeOrderbook(symbol, bidsRaw, asksRaw, metadata = {}) {
|
|
204
224
|
const bids = bidsRaw.map(([price, quantity]) => ({ price, quantity }));
|
|
205
225
|
const asks = asksRaw.map(([price, quantity]) => ({ price, quantity }));
|
|
206
226
|
return {
|
|
227
|
+
...metadata,
|
|
207
228
|
symbol,
|
|
208
229
|
bestBid: bids[0] ?? null,
|
|
209
230
|
bestAsk: asks[0] ?? null,
|
|
@@ -211,6 +232,12 @@ function normalizeOrderbook(symbol, bidsRaw, asksRaw) {
|
|
|
211
232
|
asks
|
|
212
233
|
};
|
|
213
234
|
}
|
|
235
|
+
function symbolMetadata(symbol, inputSymbol, originalSymbol) {
|
|
236
|
+
return compactObject({
|
|
237
|
+
inputSymbol: inputSymbol !== symbol ? inputSymbol : undefined,
|
|
238
|
+
originalSymbol: originalSymbol !== symbol ? originalSymbol : undefined
|
|
239
|
+
});
|
|
240
|
+
}
|
|
214
241
|
function orderPlaceBody(input) {
|
|
215
242
|
const body = requiredParams(input, ["symbol", "side", "orderType", "clientOrderId"]);
|
|
216
243
|
const quantity = input.quantity ?? input.qty;
|
|
@@ -232,6 +259,9 @@ function orderPlaceBody(input) {
|
|
|
232
259
|
if (input.reduceOnly !== undefined) {
|
|
233
260
|
body.reduceOnly = input.reduceOnly === true ? "TRUE" : "FALSE";
|
|
234
261
|
}
|
|
262
|
+
if (input.positionSide !== undefined && input.positionSide !== null && input.positionSide !== "") {
|
|
263
|
+
body.positionSide = String(input.positionSide);
|
|
264
|
+
}
|
|
235
265
|
return {
|
|
236
266
|
sym: String(body.symbol),
|
|
237
267
|
side: String(body.side),
|
|
@@ -377,6 +407,44 @@ function positionMatchesSymbol(item, symbol) {
|
|
|
377
407
|
}
|
|
378
408
|
return [item.sym, item.symbol, item.instrumentId, item.instrument].some((value) => value === symbol);
|
|
379
409
|
}
|
|
410
|
+
function extractOrderState(value) {
|
|
411
|
+
if (!isRecord(value)) {
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
const directState = value.orderState ?? value.state ?? value.status;
|
|
415
|
+
if (directState !== undefined && directState !== null && directState !== "") {
|
|
416
|
+
return String(directState);
|
|
417
|
+
}
|
|
418
|
+
if (isRecord(value.data)) {
|
|
419
|
+
const nestedState = extractOrderState(value.data);
|
|
420
|
+
if (nestedState) {
|
|
421
|
+
return nestedState;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (isRecord(value.order)) {
|
|
425
|
+
const nestedState = extractOrderState(value.order);
|
|
426
|
+
if (nestedState) {
|
|
427
|
+
return nestedState;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
function normalizeCancelOrderState(state) {
|
|
433
|
+
if (!state) {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
const normalized = state.trim().toUpperCase().replace(/[-\s]+/g, "_");
|
|
437
|
+
if (["CANCELLED", "CANCEL_COMPLETE", "CANCEL_SUCCESS", "CANCELED"].includes(normalized)) {
|
|
438
|
+
return "CANCELED";
|
|
439
|
+
}
|
|
440
|
+
if (["CANCEL_PENDING", "PENDING_CANCEL", "CANCELING"].includes(normalized)) {
|
|
441
|
+
return "CANCEL_PENDING";
|
|
442
|
+
}
|
|
443
|
+
return normalized;
|
|
444
|
+
}
|
|
445
|
+
function isCancelTerminalState(state) {
|
|
446
|
+
return state === "CANCELED" || state === "REJECTED" || state === "EXPIRED";
|
|
447
|
+
}
|
|
380
448
|
function compactObject(input) {
|
|
381
449
|
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== null && value !== ""));
|
|
382
450
|
}
|
|
@@ -65,10 +65,10 @@ export class RapidXClient {
|
|
|
65
65
|
await delay(this.readOnlyRetryDelayMs * (2 ** (attempt - 1)));
|
|
66
66
|
continue;
|
|
67
67
|
}
|
|
68
|
-
const hint = upstreamHttpErrorHint(path, response.status);
|
|
68
|
+
const hint = upstreamHttpErrorHint(path, response.status, text);
|
|
69
69
|
throw new ProductError({
|
|
70
|
-
code: upstreamHttpErrorCode(response.status),
|
|
71
|
-
status:
|
|
70
|
+
code: upstreamHttpErrorCode(path, response.status, text),
|
|
71
|
+
status: upstreamHttpErrorStatus(path, response.status, text),
|
|
72
72
|
message: hint ? `RapidX upstream error ${response.status}. ${hint}` : `RapidX upstream error ${response.status}`,
|
|
73
73
|
details: { status: response.status, body: text.slice(0, 500), attempts: attempt, ...(hint ? { hint } : {}) }
|
|
74
74
|
});
|
|
@@ -111,13 +111,19 @@ function resolveBaseUrl(explicitBaseUrl) {
|
|
|
111
111
|
function firstNonEmpty(...values) {
|
|
112
112
|
return values.find((value) => value !== undefined && value.length > 0);
|
|
113
113
|
}
|
|
114
|
-
function upstreamHttpErrorHint(path, status) {
|
|
115
|
-
if (path
|
|
114
|
+
function upstreamHttpErrorHint(path, status, body) {
|
|
115
|
+
if (isAccountBalanceScopeError(path, status, body)) {
|
|
116
116
|
return "account balance mode=account requires account-level credentials; portfolio keys should use mode=portfolio.";
|
|
117
117
|
}
|
|
118
118
|
return undefined;
|
|
119
119
|
}
|
|
120
|
-
function upstreamHttpErrorCode(status) {
|
|
120
|
+
function upstreamHttpErrorCode(path, status, body) {
|
|
121
|
+
if (isAccountBalanceScopeError(path, status, body)) {
|
|
122
|
+
return CORE_ERRORS.PERMISSION_SCOPE_ERROR;
|
|
123
|
+
}
|
|
124
|
+
if (status === 404) {
|
|
125
|
+
return CORE_ERRORS.NOT_FOUND;
|
|
126
|
+
}
|
|
121
127
|
if (status === 429) {
|
|
122
128
|
return CORE_ERRORS.UPSTREAM_RATE_LIMITED;
|
|
123
129
|
}
|
|
@@ -126,6 +132,25 @@ function upstreamHttpErrorCode(status) {
|
|
|
126
132
|
}
|
|
127
133
|
return CORE_ERRORS.INVALID_CREDENTIAL;
|
|
128
134
|
}
|
|
135
|
+
function upstreamHttpErrorStatus(path, status, body) {
|
|
136
|
+
if (isAccountBalanceScopeError(path, status, body)) {
|
|
137
|
+
return "PERMISSION_SCOPE_ERROR";
|
|
138
|
+
}
|
|
139
|
+
if (status === 404) {
|
|
140
|
+
return "NOT_FOUND";
|
|
141
|
+
}
|
|
142
|
+
return "FAIL";
|
|
143
|
+
}
|
|
144
|
+
function isAccountBalanceScopeError(path, status, body) {
|
|
145
|
+
if (path !== "/api/v1/account/balance" || status < 400) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const lowerBody = body.toLowerCase();
|
|
149
|
+
return lowerBody.includes("account-level")
|
|
150
|
+
|| lowerBody.includes("permission scope")
|
|
151
|
+
|| lowerBody.includes("credential scope")
|
|
152
|
+
|| (lowerBody.includes("portfolio") && lowerBody.includes("account"));
|
|
153
|
+
}
|
|
129
154
|
function delay(ms) {
|
|
130
155
|
if (ms <= 0) {
|
|
131
156
|
return Promise.resolve();
|
|
@@ -144,9 +169,10 @@ function assertRapidXBusinessSuccess(payload) {
|
|
|
144
169
|
const upstreamMessage = typeof body.message === "string" && body.message.length > 0
|
|
145
170
|
? `: ${body.message}`
|
|
146
171
|
: "";
|
|
172
|
+
const classification = classifyBusinessError(code, typeof body.message === "string" ? body.message : "");
|
|
147
173
|
throw new ProductError({
|
|
148
|
-
code:
|
|
149
|
-
status:
|
|
174
|
+
code: classification.code,
|
|
175
|
+
status: classification.status,
|
|
150
176
|
message: `RapidX upstream business error ${code}${upstreamMessage}`,
|
|
151
177
|
details: {
|
|
152
178
|
upstreamCode: code,
|
|
@@ -154,3 +180,10 @@ function assertRapidXBusinessSuccess(payload) {
|
|
|
154
180
|
}
|
|
155
181
|
});
|
|
156
182
|
}
|
|
183
|
+
function classifyBusinessError(upstreamCode, upstreamMessage) {
|
|
184
|
+
const lowerMessage = upstreamMessage.toLowerCase();
|
|
185
|
+
if (lowerMessage.includes("not found") || lowerMessage.includes("not exist") || lowerMessage.includes("does not exist")) {
|
|
186
|
+
return { code: CORE_ERRORS.NOT_FOUND, status: "NOT_FOUND" };
|
|
187
|
+
}
|
|
188
|
+
return { code: CORE_ERRORS.UPSTREAM_REJECTED, status: "BUSINESS_ERROR" };
|
|
189
|
+
}
|
|
@@ -10,18 +10,22 @@ export function parseRapidXSymbol(symbol) {
|
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
12
|
const exchange = exchangeRaw.toUpperCase();
|
|
13
|
-
const
|
|
13
|
+
const requestedType = typeRaw.toUpperCase();
|
|
14
|
+
const type = requestedType === "SWAP" && exchange === "OKX" ? "PERP" : requestedType;
|
|
14
15
|
if (type !== "PERP" && type !== "SPOT") {
|
|
15
16
|
throw new ProductError({
|
|
16
17
|
code: "RCORE12001",
|
|
17
18
|
status: "FAIL",
|
|
18
|
-
message: `Unsupported RapidX symbol type: ${typeRaw}
|
|
19
|
+
message: `Unsupported RapidX symbol type: ${typeRaw}. Use PERP or SPOT; for OKX perpetual swaps, use OKX_PERP_<BASE>_<QUOTE> or OKX_SWAP_<BASE>_<QUOTE> as an input alias.`
|
|
19
20
|
});
|
|
20
21
|
}
|
|
21
22
|
const base = baseRaw.toUpperCase();
|
|
22
23
|
const quote = quoteRaw.toUpperCase();
|
|
23
24
|
const isPerp = type === "PERP";
|
|
25
|
+
const canonicalSymbol = `${exchange}_${type}_${base}_${quote}`;
|
|
24
26
|
const parsed = {
|
|
27
|
+
inputSymbol: symbol,
|
|
28
|
+
canonicalSymbol,
|
|
25
29
|
exchange,
|
|
26
30
|
base,
|
|
27
31
|
quote,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SCHEMA_VERSION } from "./types.js";
|
|
2
|
+
import { inputSchemaForName } from "./input-schema.js";
|
|
2
3
|
export const CAPABILITIES = [
|
|
3
4
|
{ capabilityId: "config.get", cliCommand: "rapidx config get", operationType: "READ", riskLevel: "read", inputSchema: "ConfigGetInput", outputSchema: "ConfigResult", previewRequired: false },
|
|
4
5
|
{ capabilityId: "config.set", cliCommand: "rapidx config set", operationType: "DIAGNOSTIC", riskLevel: "write-config", inputSchema: "ConfigSetInput", outputSchema: "ConfigResult", previewRequired: false },
|
|
@@ -43,9 +44,11 @@ export const CAPABILITIES = [
|
|
|
43
44
|
{ capabilityId: "invocation.check", cliCommand: "rapidx invocation check", operationType: "DIAGNOSTIC", riskLevel: "read", inputSchema: "InvocationCheckInput", outputSchema: "InvocationCompliance", previewRequired: false }
|
|
44
45
|
];
|
|
45
46
|
export function getSchemaResult() {
|
|
47
|
+
const schemaNames = [...new Set(CAPABILITIES.map((capability) => capability.inputSchema))].sort();
|
|
46
48
|
return {
|
|
47
49
|
schemaVersion: SCHEMA_VERSION,
|
|
48
|
-
capabilities: [...CAPABILITIES]
|
|
50
|
+
capabilities: [...CAPABILITIES],
|
|
51
|
+
inputSchemas: Object.fromEntries(schemaNames.map((name) => [name, inputSchemaForName(name)]))
|
|
49
52
|
};
|
|
50
53
|
}
|
|
51
54
|
export function listMcpCapabilities() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SCHEMA_VERSION } from "./types.js";
|
|
2
2
|
import { RAPIDX_VERSION } from "../version.js";
|
|
3
3
|
export const RAPIDX_SKILLS_DISTRIBUTION = "github";
|
|
4
|
-
export const RAPIDX_SKILLS_VERSION = "1.0.
|
|
4
|
+
export const RAPIDX_SKILLS_VERSION = "1.0.3";
|
|
5
5
|
export const RAPIDX_SKILLS_SCHEMA_VERSION = "1.0.0";
|
|
6
6
|
export function buildCompatibilityReport(input = {}) {
|
|
7
7
|
const checks = [
|
|
@@ -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.",
|
|
@@ -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 = {
|
|
@@ -121,6 +135,7 @@ export function inputSchemaForName(name) {
|
|
|
121
135
|
price: priceSchema,
|
|
122
136
|
quantity: quantitySchema,
|
|
123
137
|
amount: amountSchema,
|
|
138
|
+
positionSide: positionSideSchema,
|
|
124
139
|
timeInForce: stringSchema,
|
|
125
140
|
postOnly: booleanSchema,
|
|
126
141
|
reduceOnly: booleanSchema,
|
|
@@ -140,6 +155,7 @@ export function inputSchemaForName(name) {
|
|
|
140
155
|
takeProfitLimitPrice: stringSchema,
|
|
141
156
|
algoType: stringSchema,
|
|
142
157
|
consentId: stringSchema,
|
|
158
|
+
...automationProperties(),
|
|
143
159
|
...compatibilityProperties()
|
|
144
160
|
}, ["targetCapabilityId"]);
|
|
145
161
|
case "PreviewOrderInput":
|
|
@@ -147,6 +163,7 @@ export function inputSchemaForName(name) {
|
|
|
147
163
|
...objectSchema({
|
|
148
164
|
...tradeCreateProperties(),
|
|
149
165
|
consentId: stringSchema,
|
|
166
|
+
...automationProperties(),
|
|
150
167
|
...compatibilityProperties()
|
|
151
168
|
}, ["symbol", "side", "orderType", "maxNotional", "clientOrderId"]),
|
|
152
169
|
oneOf: [{ required: ["quantity"] }, { required: ["amount"] }]
|
|
@@ -166,7 +183,7 @@ export function inputSchemaForName(name) {
|
|
|
166
183
|
};
|
|
167
184
|
case "AmendOrderPreviewInput":
|
|
168
185
|
return {
|
|
169
|
-
...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, price: priceSchema, quantity: quantitySchema, ...compatibilityProperties() }),
|
|
186
|
+
...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, price: priceSchema, quantity: quantitySchema, ...automationProperties(), ...compatibilityProperties() }),
|
|
170
187
|
allOf: [
|
|
171
188
|
{ anyOf: [{ required: ["orderId"] }, { required: ["clientOrderId"] }] },
|
|
172
189
|
{ anyOf: [{ required: ["price"] }, { required: ["quantity"] }] }
|
|
@@ -174,7 +191,7 @@ export function inputSchemaForName(name) {
|
|
|
174
191
|
};
|
|
175
192
|
case "CancelOrderPreviewInput":
|
|
176
193
|
return {
|
|
177
|
-
...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, ...compatibilityProperties() }),
|
|
194
|
+
...objectSchema({ orderId: orderIdSchema, clientOrderId: clientOrderIdSchema, ...automationProperties(), ...compatibilityProperties() }),
|
|
178
195
|
anyOf: [{ required: ["orderId"] }, { required: ["clientOrderId"] }]
|
|
179
196
|
};
|
|
180
197
|
case "AmendOrderInput":
|
|
@@ -193,7 +210,7 @@ export function inputSchemaForName(name) {
|
|
|
193
210
|
case "ClosePositionInput":
|
|
194
211
|
return objectSchema({ symbol: symbolSchema, positionSide: positionSideSchema, reduceOnly: closePositionReduceOnlySchema, maxNotional: maxNotionalSchema, previewId: previewIdSchema, continueConsentId: continueConsentIdSchema }, ["symbol", "reduceOnly", "maxNotional", "previewId", "continueConsentId"]);
|
|
195
212
|
case "SetLeverageInput":
|
|
196
|
-
return objectSchema({ symbol: symbolSchema, leverage: numberSchema, positionSide:
|
|
213
|
+
return objectSchema({ symbol: symbolSchema, leverage: numberSchema, positionSide: positionSideSchema, previewId: previewIdSchema, continueConsentId: continueConsentIdSchema }, ["symbol", "leverage", "previewId", "continueConsentId"]);
|
|
197
214
|
case "SetPositionModeInput":
|
|
198
215
|
return objectSchema({ mode: { type: "string", enum: ["BOTH", "NET"] }, exchange: stringSchema, previewId: previewIdSchema, continueConsentId: continueConsentIdSchema }, ["mode", "exchange", "previewId", "continueConsentId"]);
|
|
199
216
|
case "AlgoPlaceInput":
|
|
@@ -201,7 +218,6 @@ export function inputSchemaForName(name) {
|
|
|
201
218
|
...objectSchema({
|
|
202
219
|
...tradeCreateProperties(),
|
|
203
220
|
algoType: stringSchema,
|
|
204
|
-
positionSide: stringSchema,
|
|
205
221
|
conditionType: algoConditionTypeSchema,
|
|
206
222
|
triggerPrice: stringSchema,
|
|
207
223
|
triggerType: triggerTypeSchema,
|
|
@@ -221,7 +237,7 @@ export function inputSchemaForName(name) {
|
|
|
221
237
|
"previewId",
|
|
222
238
|
"continueConsentId"
|
|
223
239
|
]),
|
|
224
|
-
|
|
240
|
+
anyOf: [{ required: ["quantity"] }, { required: ["amount"] }, { required: ["conditionType"] }]
|
|
225
241
|
};
|
|
226
242
|
case "AlgoAmendInput":
|
|
227
243
|
return objectSchema({
|
|
@@ -244,6 +260,7 @@ export function inputSchemaForName(name) {
|
|
|
244
260
|
side: sideSchema,
|
|
245
261
|
maxNotional: maxNotionalSchema,
|
|
246
262
|
clientOrderId: clientOrderIdSchema,
|
|
263
|
+
positionSide: positionSideSchema,
|
|
247
264
|
explicitUserConsent: explicitUserConsentSchema,
|
|
248
265
|
acceptedRiskText: acceptedRiskTextSchema
|
|
249
266
|
}, ["symbol", "side", "maxNotional", "clientOrderId", "explicitUserConsent", "acceptedRiskText"]);
|
|
@@ -326,15 +343,27 @@ export function assertInputMatchesSchema(schemaName, input, options = {}) {
|
|
|
326
343
|
collectAnyOfProblem(group.anyOf, input, problems);
|
|
327
344
|
}
|
|
328
345
|
}
|
|
346
|
+
collectSchemaSpecificProblems(schemaName, input, problems);
|
|
329
347
|
if (problems.length > 0) {
|
|
330
348
|
throw new ProductError({
|
|
331
|
-
code:
|
|
332
|
-
status: "
|
|
349
|
+
code: CORE_ERRORS.SCHEMA_INVALID,
|
|
350
|
+
status: "INVALID_INPUT",
|
|
333
351
|
message: `Input schema validation failed: ${problems.join("; ")}.`
|
|
334
352
|
});
|
|
335
353
|
}
|
|
336
354
|
void allowedFields;
|
|
337
355
|
}
|
|
356
|
+
function collectSchemaSpecificProblems(schemaName, input, problems) {
|
|
357
|
+
if (schemaName !== "AlgoPlaceInput") {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (String(input.conditionType ?? "").toUpperCase() !== "ENTIRE_CLOSE_POSITION") {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (!hasValue(input.stopLossPrice) && !hasValue(input.takeProfitPrice)) {
|
|
364
|
+
problems.push("TPSL ENTIRE_CLOSE_POSITION requires stopLossPrice or takeProfitPrice");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
338
367
|
function collectAnyOfProblem(rules, input, problems) {
|
|
339
368
|
if (!rules.some((candidate) => (candidate.required ?? []).every((field) => hasValue(input[field])))) {
|
|
340
369
|
const labels = rules.map((candidate) => (candidate.required ?? []).join(", ")).join(" or ");
|
|
@@ -362,7 +391,15 @@ function tradeCreateProperties() {
|
|
|
362
391
|
postOnly: booleanSchema,
|
|
363
392
|
reduceOnly: booleanSchema,
|
|
364
393
|
maxNotional: maxNotionalSchema,
|
|
365
|
-
clientOrderId: clientOrderIdSchema
|
|
394
|
+
clientOrderId: clientOrderIdSchema,
|
|
395
|
+
positionSide: positionSideSchema
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function automationProperties() {
|
|
399
|
+
return {
|
|
400
|
+
automationMode: automationModeSchema,
|
|
401
|
+
automationConsentText: automationConsentTextSchema,
|
|
402
|
+
automationScope: automationScopeSchema
|
|
366
403
|
};
|
|
367
404
|
}
|
|
368
405
|
function compatibilityProperties() {
|
|
@@ -27,6 +27,8 @@ export const CORE_ERRORS = {
|
|
|
27
27
|
MISSING_API_HOST: "RCORE01003",
|
|
28
28
|
SECRET_REDACTION_RISK: "RCORE90002",
|
|
29
29
|
UPSTREAM_REJECTED: "RCORE22001",
|
|
30
|
+
PERMISSION_SCOPE_ERROR: "RCORE01004",
|
|
31
|
+
NOT_FOUND: "RCORE22004",
|
|
30
32
|
UPSTREAM_TIMEOUT: "RCORE23002",
|
|
31
33
|
UPSTREAM_RATE_LIMITED: "RCORE23003",
|
|
32
34
|
SCHEMA_INVALID: "RCORE30001"
|
package/dist/core/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./client/symbol.js";
|
|
|
16
16
|
export * from "./safety/policy.js";
|
|
17
17
|
export * from "./safety/raw-api.js";
|
|
18
18
|
export * from "./trading/preview.js";
|
|
19
|
+
export * from "./trading/preview-preflight.js";
|
|
19
20
|
export * from "./update/check-update.js";
|
|
20
21
|
export * from "./self-check/run-self-check.js";
|
|
21
22
|
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)) {
|