@liquiditytech/rapidx-cli 1.0.26
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/README.md +81 -0
- package/dist/cli/audit.js +10 -0
- package/dist/cli/bin.js +41 -0
- package/dist/cli/commands/account.js +34 -0
- package/dist/cli/commands/algo.js +35 -0
- package/dist/cli/commands/auth.js +22 -0
- package/dist/cli/commands/config.js +46 -0
- package/dist/cli/commands/doctor.js +42 -0
- package/dist/cli/commands/index.js +73 -0
- package/dist/cli/commands/market.js +26 -0
- package/dist/cli/commands/order.js +81 -0
- package/dist/cli/commands/position.js +35 -0
- package/dist/cli/commands/schema.js +5 -0
- package/dist/cli/commands/self-check.js +24 -0
- package/dist/cli/commands/trade-gate.js +26 -0
- package/dist/cli/commands/trade.js +30 -0
- package/dist/cli/commands/update.js +27 -0
- package/dist/cli/envelope.js +29 -0
- package/dist/cli/help.js +34 -0
- package/dist/cli/invocation-checker.js +39 -0
- package/dist/cli/mcp-entry.js +4 -0
- package/dist/cli/parser.js +87 -0
- package/dist/core/audit/redaction.js +56 -0
- package/dist/core/audit/writer.js +27 -0
- package/dist/core/client/capability-executor.js +400 -0
- package/dist/core/client/rapid-x-client.js +156 -0
- package/dist/core/client/signing.js +24 -0
- package/dist/core/client/symbol.js +36 -0
- package/dist/core/config/credential.js +42 -0
- package/dist/core/config/resolve.js +24 -0
- package/dist/core/contracts/capabilities.js +77 -0
- package/dist/core/contracts/compatibility.js +65 -0
- package/dist/core/contracts/events.js +29 -0
- package/dist/core/contracts/evidence.js +7 -0
- package/dist/core/contracts/input-schema.js +370 -0
- package/dist/core/contracts/types.js +1 -0
- package/dist/core/errors/product-error.js +74 -0
- package/dist/core/index.js +24 -0
- package/dist/core/safety/policy.js +215 -0
- package/dist/core/safety/raw-api.js +19 -0
- package/dist/core/self-check/live-read-only-probes.js +25 -0
- package/dist/core/self-check/live-trading-verification-probes.js +252 -0
- package/dist/core/self-check/run-self-check.js +91 -0
- package/dist/core/trading/preview.js +330 -0
- package/dist/core/trading/trading-verification.js +137 -0
- package/dist/core/update/check-update.js +295 -0
- package/dist/core/version.js +1 -0
- package/dist/mcp/audit.js +10 -0
- package/dist/mcp/server.js +73 -0
- package/dist/mcp/tool-registry.js +31 -0
- package/dist/mcp/tool-runner.js +144 -0
- package/package.json +48 -0
- package/packages/distribution/docs/cli-only-agent.md +12 -0
- package/packages/distribution/docs/cli.md +49 -0
- package/packages/distribution/docs/index.md +58 -0
- package/packages/distribution/docs/mcp.md +36 -0
- package/packages/distribution/docs/quickstart.md +129 -0
- package/packages/distribution/docs/self-check.md +7 -0
- package/packages/distribution/docs/skills.md +140 -0
- package/packages/distribution/docs/tools.md +35 -0
- package/packages/distribution/docs/trading-verification.md +17 -0
- package/packages/distribution/docs/troubleshooting/index.md +27 -0
- package/packages/distribution/manifests/offline-manifest.json +26 -0
- package/packages/distribution/registry/rapidx.mcp.json +26 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { makeEvidence } from "../core/index.js";
|
|
2
|
+
export function ok(data, evidenceText, status = "PASS", source = "local_check", auditId) {
|
|
3
|
+
return {
|
|
4
|
+
ok: status === "PASS",
|
|
5
|
+
status,
|
|
6
|
+
data,
|
|
7
|
+
...(auditId ? { auditId } : {}),
|
|
8
|
+
evidence: [makeEvidence(evidenceText, source)]
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function fail(code, message, status = "FAIL", evidenceText = "rapidx cli") {
|
|
12
|
+
return {
|
|
13
|
+
ok: false,
|
|
14
|
+
status,
|
|
15
|
+
code,
|
|
16
|
+
message,
|
|
17
|
+
evidence: [makeEvidence(evidenceText)]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function failWithData(code, message, data, status = "FAIL", evidenceText = "rapidx cli") {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
status,
|
|
24
|
+
code,
|
|
25
|
+
message,
|
|
26
|
+
data,
|
|
27
|
+
evidence: [makeEvidence(evidenceText)]
|
|
28
|
+
};
|
|
29
|
+
}
|
package/dist/cli/help.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { RAPIDX_VERSION } from "../core/index.js";
|
|
2
|
+
export function formatCliHelp() {
|
|
3
|
+
return [
|
|
4
|
+
"RapidX CLI",
|
|
5
|
+
"",
|
|
6
|
+
"Usage: rapidx <domain> <action> [--input '<json>'] [--json]",
|
|
7
|
+
"",
|
|
8
|
+
"Examples:",
|
|
9
|
+
" rapidx schema --json",
|
|
10
|
+
" rapidx update check --json",
|
|
11
|
+
" rapidx self-check --input '{\"scope\":\"deep\"}' --json",
|
|
12
|
+
" rapidx market get-ticker --input '{\"symbol\":\"BINANCE_PERP_BTC_USDT\"}' --json",
|
|
13
|
+
" rapidx market get-ticker --symbol BINANCE_PERP_BTC_USDT --json",
|
|
14
|
+
" rapidx order preview --input '{\"symbol\":\"BINANCE_PERP_BTC_USDT\",\"side\":\"BUY\",\"orderType\":\"LIMIT\",\"price\":\"1\",\"quantity\":\"0.001\",\"maxNotional\":\"1\",\"clientOrderId\":\"example\"}' --json",
|
|
15
|
+
" rapidx order cancel-preview --input '{\"orderId\":\"<order-id>\"}' --json",
|
|
16
|
+
" rapidx trade verify-live --input '{\"symbol\":\"BINANCE_PERP_BTC_USDT\",\"side\":\"BUY\",\"maxNotional\":\"10\",\"clientOrderId\":\"verify-001\",\"explicitUserConsent\":true}' --json",
|
|
17
|
+
" rapidx mcp serve",
|
|
18
|
+
"",
|
|
19
|
+
"Domains:",
|
|
20
|
+
" schema, doctor, auth, config, update, self-check, invocation",
|
|
21
|
+
" market, account, order, trade, position, algo",
|
|
22
|
+
"",
|
|
23
|
+
"Use --input for structured JSON. Named options such as --symbol and --depth are also accepted for simple inputs."
|
|
24
|
+
].join("\n");
|
|
25
|
+
}
|
|
26
|
+
export function formatCliVersion() {
|
|
27
|
+
return RAPIDX_VERSION;
|
|
28
|
+
}
|
|
29
|
+
export function isHelpArg(arg) {
|
|
30
|
+
return arg === "--help" || arg === "-h";
|
|
31
|
+
}
|
|
32
|
+
export function isVersionArg(arg) {
|
|
33
|
+
return arg === "--version" || arg === "-v";
|
|
34
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const COMPLEX_SHELL_PATTERNS = [
|
|
2
|
+
/\bcd\s+\S+\s+&&\s+/,
|
|
3
|
+
/\s&&\s/,
|
|
4
|
+
/\s\|\s/,
|
|
5
|
+
/\s;\s/,
|
|
6
|
+
/<<\s*\w+/,
|
|
7
|
+
/\b(?:sh|bash|zsh)\s+-c\b/,
|
|
8
|
+
/\bbtc-mcp-call\.js\b/,
|
|
9
|
+
/(?:^|\s)(?:node|python|python3)\s+\/(?:tmp|private\/tmp)\//
|
|
10
|
+
];
|
|
11
|
+
export function checkInvocationMode(input) {
|
|
12
|
+
if (input.preflightError && /exec preflight|complex interpreter invocation/i.test(input.preflightError)) {
|
|
13
|
+
return {
|
|
14
|
+
compliant: false,
|
|
15
|
+
code: "RCLI25001",
|
|
16
|
+
reason: "Host exec preflight blocked the command; use direct MCP tools or the rapidx bin without shell chaining."
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const commandLine = input.commandLine ?? "";
|
|
20
|
+
const matched = COMPLEX_SHELL_PATTERNS.find((pattern) => pattern.test(commandLine));
|
|
21
|
+
if (matched) {
|
|
22
|
+
return {
|
|
23
|
+
compliant: false,
|
|
24
|
+
code: "RCLI13001",
|
|
25
|
+
reason: `Complex shell invocation is not allowed: ${matched.source}`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (/\bnode\s+.*bridge/i.test(commandLine)) {
|
|
29
|
+
return {
|
|
30
|
+
compliant: false,
|
|
31
|
+
code: "RCLI13002",
|
|
32
|
+
reason: "Bridge scripts are not part of the supported CLI contract."
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
compliant: true,
|
|
37
|
+
reason: "Invocation is direct."
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
export function parseArgv(argv, options = {}) {
|
|
3
|
+
const commandParts = [];
|
|
4
|
+
const parsedOptions = {};
|
|
5
|
+
let input = {};
|
|
6
|
+
const namedInput = {};
|
|
7
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
8
|
+
const token = argv[index];
|
|
9
|
+
if (!token) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (!token.startsWith("--")) {
|
|
13
|
+
commandParts.push(token);
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const name = token.slice(2);
|
|
17
|
+
if (name === "input") {
|
|
18
|
+
const value = argv[index + 1];
|
|
19
|
+
index += 1;
|
|
20
|
+
if (!value) {
|
|
21
|
+
throw new Error("--input requires a value.");
|
|
22
|
+
}
|
|
23
|
+
input = readInput(value);
|
|
24
|
+
parsedOptions.input = value;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const normalizedName = normalizeOptionName(name);
|
|
28
|
+
const next = argv[index + 1];
|
|
29
|
+
let parsedValue;
|
|
30
|
+
if (next && !next.startsWith("--")) {
|
|
31
|
+
parsedValue = next;
|
|
32
|
+
parsedOptions[normalizedName] = next;
|
|
33
|
+
index += 1;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
parsedValue = true;
|
|
37
|
+
parsedOptions[normalizedName] = true;
|
|
38
|
+
}
|
|
39
|
+
if (!RESERVED_OPTIONS.has(normalizedName)) {
|
|
40
|
+
namedInput[normalizedName] = coerceNamedInputValue(normalizedName, parsedValue);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (Object.keys(input).length === 0 && options.stdin && options.stdin.trim()) {
|
|
44
|
+
input = JSON.parse(options.stdin);
|
|
45
|
+
}
|
|
46
|
+
if (Object.keys(namedInput).length > 0) {
|
|
47
|
+
input = { ...input, ...namedInput };
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
commandParts,
|
|
51
|
+
command: commandParts.join(" "),
|
|
52
|
+
options: parsedOptions,
|
|
53
|
+
input
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function readInput(value) {
|
|
57
|
+
const raw = value.startsWith("@") ? readFileSync(value.slice(1), "utf8") : value;
|
|
58
|
+
const parsed = JSON.parse(raw);
|
|
59
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
60
|
+
throw new Error("CLI input must be a JSON object.");
|
|
61
|
+
}
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
const RESERVED_OPTIONS = new Set(["help", "version", "json", "input"]);
|
|
65
|
+
const NUMBER_OPTIONS = new Set(["depth", "limit", "pageSize", "leverage", "maxCacheAgeSeconds"]);
|
|
66
|
+
const BOOLEAN_OPTIONS = new Set(["postOnly", "reduceOnly", "readOnly", "explicitUserConsent", "force", "checkUpdates"]);
|
|
67
|
+
function normalizeOptionName(name) {
|
|
68
|
+
return name.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
69
|
+
}
|
|
70
|
+
function coerceNamedInputValue(name, value) {
|
|
71
|
+
if (typeof value === "boolean") {
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
if (NUMBER_OPTIONS.has(name)) {
|
|
75
|
+
const numeric = Number(value);
|
|
76
|
+
return Number.isFinite(numeric) ? numeric : value;
|
|
77
|
+
}
|
|
78
|
+
if (BOOLEAN_OPTIONS.has(name)) {
|
|
79
|
+
if (value === "true") {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (value === "false") {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { CORE_ERRORS, ProductError } from "../errors/product-error.js";
|
|
2
|
+
export const SENSITIVE_KEYS = [
|
|
3
|
+
"accessKey",
|
|
4
|
+
"secretKey",
|
|
5
|
+
"access_key",
|
|
6
|
+
"secret_key",
|
|
7
|
+
"token",
|
|
8
|
+
"apiKey",
|
|
9
|
+
"password",
|
|
10
|
+
"credential",
|
|
11
|
+
"credentials"
|
|
12
|
+
];
|
|
13
|
+
const SENSITIVE_KEY_SET = new Set(SENSITIVE_KEYS.map((key) => key.toLowerCase()));
|
|
14
|
+
const SECRET_LIKE_PATTERN = /(sk|pk|secret|token|key)_[A-Za-z0-9_-]{16,}|[A-Fa-f0-9]{48,}/;
|
|
15
|
+
export function isSensitiveKey(key) {
|
|
16
|
+
return SENSITIVE_KEY_SET.has(key.toLowerCase());
|
|
17
|
+
}
|
|
18
|
+
export function redactSecrets(input) {
|
|
19
|
+
let redactedCount = 0;
|
|
20
|
+
let riskDetected = false;
|
|
21
|
+
function visit(value, keyHint) {
|
|
22
|
+
if (keyHint && isSensitiveKey(keyHint)) {
|
|
23
|
+
redactedCount += 1;
|
|
24
|
+
return "[REDACTED]";
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === "string") {
|
|
27
|
+
if (SECRET_LIKE_PATTERN.test(value)) {
|
|
28
|
+
riskDetected = true;
|
|
29
|
+
return "[REDACTED]";
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
return value.map((item) => visit(item));
|
|
35
|
+
}
|
|
36
|
+
if (value && typeof value === "object") {
|
|
37
|
+
const entries = Object.entries(value).map(([key, child]) => [key, visit(child, key)]);
|
|
38
|
+
return Object.fromEntries(entries);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
value: visit(input),
|
|
44
|
+
redactedCount,
|
|
45
|
+
riskDetected
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function assertNoSensitiveText(text) {
|
|
49
|
+
if (SECRET_LIKE_PATTERN.test(text)) {
|
|
50
|
+
throw new ProductError({
|
|
51
|
+
code: CORE_ERRORS.SECRET_REDACTION_RISK,
|
|
52
|
+
status: "FAIL",
|
|
53
|
+
message: "Potential secret material detected in output."
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { redactSecrets } from "./redaction.js";
|
|
3
|
+
import { CORE_ERRORS, ProductError } from "../errors/product-error.js";
|
|
4
|
+
export class AuditWriter {
|
|
5
|
+
stream;
|
|
6
|
+
constructor(stream = process.stderr) {
|
|
7
|
+
this.stream = stream;
|
|
8
|
+
}
|
|
9
|
+
write(event) {
|
|
10
|
+
const auditId = randomUUID();
|
|
11
|
+
const redacted = redactSecrets({
|
|
12
|
+
auditId,
|
|
13
|
+
timestamp: event.timestamp ?? new Date().toISOString(),
|
|
14
|
+
...event
|
|
15
|
+
});
|
|
16
|
+
if (redacted.riskDetected) {
|
|
17
|
+
throw new ProductError({
|
|
18
|
+
code: CORE_ERRORS.SECRET_REDACTION_RISK,
|
|
19
|
+
status: "FAIL",
|
|
20
|
+
message: "Audit payload contained secret-like material."
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const line = JSON.stringify(redacted.value);
|
|
24
|
+
this.stream.write(`${line}\n`);
|
|
25
|
+
return { auditId, line };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { ProductError } from "../errors/product-error.js";
|
|
2
|
+
import { parseRapidXSymbol } from "./symbol.js";
|
|
3
|
+
import { RapidXClient } from "./rapid-x-client.js";
|
|
4
|
+
import { findCapabilityById } from "../contracts/capabilities.js";
|
|
5
|
+
import { assertInputMatchesSchema } from "../contracts/input-schema.js";
|
|
6
|
+
export async function executeRapidXCapability(capabilityId, input = {}, options = {}) {
|
|
7
|
+
const capability = findCapabilityById(capabilityId);
|
|
8
|
+
if (!capability) {
|
|
9
|
+
throw new ProductError({
|
|
10
|
+
code: "RCORE30002",
|
|
11
|
+
status: "FAIL",
|
|
12
|
+
message: `No capability registered for ${capabilityId}`
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
assertInputMatchesSchema(capability.inputSchema, input);
|
|
16
|
+
if (capabilityId.startsWith("market.")) {
|
|
17
|
+
return executeMarketCapability(capabilityId, input, options.fetchFn ?? fetch);
|
|
18
|
+
}
|
|
19
|
+
const client = options.client ?? new RapidXClient();
|
|
20
|
+
switch (capabilityId) {
|
|
21
|
+
case "account.overview":
|
|
22
|
+
return client.get("/api/v1/trading/account");
|
|
23
|
+
case "account.balance":
|
|
24
|
+
return executeAccountBalanceCapability(client, input);
|
|
25
|
+
case "account.set-position-mode":
|
|
26
|
+
return client.post("/api/v1/trading/account", {
|
|
27
|
+
positionMode: String(input.mode),
|
|
28
|
+
exchangeType: String(input.exchange)
|
|
29
|
+
});
|
|
30
|
+
case "order.place":
|
|
31
|
+
return client.post("/api/v1/trading/order", orderPlaceBody(input));
|
|
32
|
+
case "order.amend":
|
|
33
|
+
return client.put("/api/v1/trading/order", orderAmendBody(input));
|
|
34
|
+
case "order.cancel":
|
|
35
|
+
return client.delete("/api/v1/trading/order", orderCancelBody(input));
|
|
36
|
+
case "order.get":
|
|
37
|
+
return client.get("/api/v1/trading/order", optionalParams(input, ["orderId", "clientOrderId", "symbol"]));
|
|
38
|
+
case "order.list":
|
|
39
|
+
return client.get("/api/v1/trading/orders", { pageSize: stringValue(input.pageSize, "1000"), ...optionalParams(input, ["symbol"]) });
|
|
40
|
+
case "order.history":
|
|
41
|
+
return client.get("/api/v1/trading/history/orders", { pageSize: stringValue(input.pageSize, "100"), ...optionalParams(input, ["symbol", "startTime", "endTime"]) });
|
|
42
|
+
case "position.list":
|
|
43
|
+
return executePositionListCapability(client, input);
|
|
44
|
+
case "position.history":
|
|
45
|
+
return client.get("/api/v1/trading/history/position", {
|
|
46
|
+
pageSize: stringValue(input.pageSize, "100"),
|
|
47
|
+
...positionHistoryParams(input)
|
|
48
|
+
});
|
|
49
|
+
case "position.close":
|
|
50
|
+
return executePositionCloseCapability(client, input);
|
|
51
|
+
case "position.set-leverage":
|
|
52
|
+
return client.post("/api/v1/trading/position/leverage", {
|
|
53
|
+
sym: String(input.symbol),
|
|
54
|
+
leverage: String(input.leverage)
|
|
55
|
+
});
|
|
56
|
+
case "algo.place":
|
|
57
|
+
return client.post("/api/v1/algo/order", algoPlaceBody(input));
|
|
58
|
+
case "algo.amend":
|
|
59
|
+
return client.put("/api/v1/algo/order", algoAmendBody(input));
|
|
60
|
+
case "algo.cancel":
|
|
61
|
+
return client.delete("/api/v1/algo/order", optionalParams(input, ["algoOrderId", "clientOrderId"]));
|
|
62
|
+
case "algo.list":
|
|
63
|
+
return client.get("/api/v1/algo/openOrders", algoListParams(input));
|
|
64
|
+
default:
|
|
65
|
+
throw new ProductError({
|
|
66
|
+
code: "RCORE30002",
|
|
67
|
+
status: "FAIL",
|
|
68
|
+
message: `No RapidX executor registered for ${capabilityId}`
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function executeMarketCapability(capabilityId, input, fetchFn) {
|
|
73
|
+
const symbol = stringValue(input.symbol ?? input.sym, "BINANCE_PERP_BTC_USDT");
|
|
74
|
+
const parsed = parseRapidXSymbol(symbol);
|
|
75
|
+
if (capabilityId === "market.funding-rate" || capabilityId === "market.mark-price" || capabilityId === "market.symbol-info") {
|
|
76
|
+
const client = new RapidXClient({ fetchFn });
|
|
77
|
+
const path = capabilityId === "market.funding-rate"
|
|
78
|
+
? "/api/v1/market/fundingRate"
|
|
79
|
+
: capabilityId === "market.mark-price"
|
|
80
|
+
? "/api/v1/market/markPrice"
|
|
81
|
+
: "/api/v1/trading/sym/info";
|
|
82
|
+
return client.get(path, optionalParams({ sym: symbol }, ["sym"]));
|
|
83
|
+
}
|
|
84
|
+
if (parsed.exchange === "OKX") {
|
|
85
|
+
return executeOkxMarketCapability(capabilityId, symbol, parsed.okxInstId ?? "", input, fetchFn);
|
|
86
|
+
}
|
|
87
|
+
if (parsed.exchange !== "BINANCE") {
|
|
88
|
+
throw new ProductError({
|
|
89
|
+
code: "RCORE12001",
|
|
90
|
+
status: "FAIL",
|
|
91
|
+
message: `Unsupported market exchange: ${parsed.exchange}`
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const base = parsed.isPerp ? "https://fapi.binance.com" : "https://api.binance.com";
|
|
95
|
+
switch (capabilityId) {
|
|
96
|
+
case "market.ticker": {
|
|
97
|
+
const raw = await fetchJson(fetchFn, `${base}${parsed.isPerp ? "/fapi/v1/ticker/24hr" : "/api/v3/ticker/24hr"}`, { symbol: parsed.binanceSymbol });
|
|
98
|
+
return {
|
|
99
|
+
symbol,
|
|
100
|
+
lastPrice: raw.lastPrice,
|
|
101
|
+
priceChange: raw.priceChange,
|
|
102
|
+
priceChangePercent: raw.priceChangePercent,
|
|
103
|
+
highPrice: raw.highPrice,
|
|
104
|
+
lowPrice: raw.lowPrice,
|
|
105
|
+
volume: raw.volume,
|
|
106
|
+
quoteVolume: raw.quoteVolume
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
case "market.orderbook": {
|
|
110
|
+
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);
|
|
112
|
+
}
|
|
113
|
+
case "market.klines": {
|
|
114
|
+
const raw = await fetchJson(fetchFn, `${base}${parsed.isPerp ? "/fapi/v1/klines" : "/api/v3/klines"}`, {
|
|
115
|
+
symbol: parsed.binanceSymbol,
|
|
116
|
+
interval: stringValue(input.interval, "1h"),
|
|
117
|
+
limit: stringValue(input.limit, "100")
|
|
118
|
+
});
|
|
119
|
+
return { symbol, interval: stringValue(input.interval, "1h"), count: raw.length, candles: raw };
|
|
120
|
+
}
|
|
121
|
+
case "market.open-interest": {
|
|
122
|
+
if (!parsed.isPerp) {
|
|
123
|
+
throw new ProductError({ code: "RCORE00001", status: "FAIL", message: "open interest only supports perpetual symbols" });
|
|
124
|
+
}
|
|
125
|
+
return fetchJson(fetchFn, `${base}/fapi/v1/openInterest`, { symbol: parsed.binanceSymbol });
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
throw new ProductError({ code: "RCORE30002", status: "FAIL", message: `Unknown market capability: ${capabilityId}` });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function executeAccountBalanceCapability(client, input) {
|
|
132
|
+
const mode = String(input.mode ?? "portfolio").toLowerCase();
|
|
133
|
+
const params = optionalParams(input, ["currency"]);
|
|
134
|
+
if (mode === "account") {
|
|
135
|
+
return client.get("/api/v1/account/balance", params);
|
|
136
|
+
}
|
|
137
|
+
return client.get("/api/v1/trading/portfolio/assets", params);
|
|
138
|
+
}
|
|
139
|
+
async function executePositionListCapability(client, input) {
|
|
140
|
+
const params = {};
|
|
141
|
+
if (input.symbol !== undefined && input.symbol !== null && input.symbol !== "") {
|
|
142
|
+
params.sym = String(input.symbol);
|
|
143
|
+
}
|
|
144
|
+
const response = await client.get("/api/v1/trading/position", params);
|
|
145
|
+
return filterPositionResponse(response, params.sym);
|
|
146
|
+
}
|
|
147
|
+
async function executePositionCloseCapability(client, input) {
|
|
148
|
+
const response = await client.delete("/api/v1/trading/position", positionCloseBody(input));
|
|
149
|
+
return {
|
|
150
|
+
closePosition: true,
|
|
151
|
+
sourceCapability: "position.close",
|
|
152
|
+
reduceOnlyRequested: input.reduceOnly === true,
|
|
153
|
+
request: compactObject({
|
|
154
|
+
symbol: input.symbol,
|
|
155
|
+
positionSide: input.positionSide,
|
|
156
|
+
maxNotional: input.maxNotional
|
|
157
|
+
}),
|
|
158
|
+
readbackNote: "position.close uses RapidX close-position API; order.get reduceOnly may not reflect this close-position request. Verify final exposure with position.list.",
|
|
159
|
+
response
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async function executeOkxMarketCapability(capabilityId, symbol, instId, input, fetchFn) {
|
|
163
|
+
switch (capabilityId) {
|
|
164
|
+
case "market.ticker": {
|
|
165
|
+
const data = await fetchOkx(fetchFn, "/api/v5/market/ticker", { instId });
|
|
166
|
+
const raw = data[0] ?? {};
|
|
167
|
+
return { symbol, lastPrice: raw.last, highPrice: raw.high24h, lowPrice: raw.low24h, volume: raw.vol24h, quoteVolume: raw.volCcy24h };
|
|
168
|
+
}
|
|
169
|
+
case "market.orderbook": {
|
|
170
|
+
const data = await fetchOkx(fetchFn, "/api/v5/market/books", { instId, sz: stringValue(input.depth, "20") });
|
|
171
|
+
const raw = data[0] ?? { bids: [], asks: [] };
|
|
172
|
+
const bids = Array.isArray(raw.bids) ? raw.bids : [];
|
|
173
|
+
const asks = Array.isArray(raw.asks) ? raw.asks : [];
|
|
174
|
+
return normalizeOrderbook(symbol, bids, asks);
|
|
175
|
+
}
|
|
176
|
+
case "market.open-interest": {
|
|
177
|
+
const data = await fetchOkx(fetchFn, "/api/v5/public/open-interest", { instId });
|
|
178
|
+
return { symbol, ...(data[0] ?? {}) };
|
|
179
|
+
}
|
|
180
|
+
default:
|
|
181
|
+
throw new ProductError({ code: "RCORE30002", status: "FAIL", message: `OKX adapter not registered for ${capabilityId}` });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function fetchJson(fetchFn, url, params = {}) {
|
|
185
|
+
const parsed = new URL(url);
|
|
186
|
+
for (const [key, value] of Object.entries(params)) {
|
|
187
|
+
parsed.searchParams.set(key, value);
|
|
188
|
+
}
|
|
189
|
+
const response = await fetchFn(parsed.toString(), { method: "GET", signal: AbortSignal.timeout(15_000) });
|
|
190
|
+
const text = await response.text();
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
throw new ProductError({ code: "RCORE02001", status: "FAIL", message: `Public market API error ${response.status}` });
|
|
193
|
+
}
|
|
194
|
+
return JSON.parse(text);
|
|
195
|
+
}
|
|
196
|
+
async function fetchOkx(fetchFn, path, params) {
|
|
197
|
+
const payload = await fetchJson(fetchFn, `https://www.okx.com${path}`, params);
|
|
198
|
+
if (payload.code !== "0") {
|
|
199
|
+
throw new ProductError({ code: "RCORE22001", status: "FAIL", message: `OKX API error: ${payload.code} ${payload.msg ?? ""}`.trim() });
|
|
200
|
+
}
|
|
201
|
+
return payload.data ?? [];
|
|
202
|
+
}
|
|
203
|
+
function normalizeOrderbook(symbol, bidsRaw, asksRaw) {
|
|
204
|
+
const bids = bidsRaw.map(([price, quantity]) => ({ price, quantity }));
|
|
205
|
+
const asks = asksRaw.map(([price, quantity]) => ({ price, quantity }));
|
|
206
|
+
return {
|
|
207
|
+
symbol,
|
|
208
|
+
bestBid: bids[0] ?? null,
|
|
209
|
+
bestAsk: asks[0] ?? null,
|
|
210
|
+
bids,
|
|
211
|
+
asks
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function orderPlaceBody(input) {
|
|
215
|
+
const body = requiredParams(input, ["symbol", "side", "orderType", "clientOrderId"]);
|
|
216
|
+
const quantity = input.quantity ?? input.qty;
|
|
217
|
+
if (quantity !== undefined) {
|
|
218
|
+
body.orderQty = String(quantity);
|
|
219
|
+
}
|
|
220
|
+
if (input.amount !== undefined) {
|
|
221
|
+
body.quoteOrderQty = String(input.amount);
|
|
222
|
+
}
|
|
223
|
+
if (input.price !== undefined) {
|
|
224
|
+
body.limitPrice = String(input.price);
|
|
225
|
+
}
|
|
226
|
+
if (input.timeInForce !== undefined) {
|
|
227
|
+
body.timeInForce = String(input.timeInForce);
|
|
228
|
+
}
|
|
229
|
+
if (input.postOnly === true) {
|
|
230
|
+
body.timeInForce = "GTX";
|
|
231
|
+
}
|
|
232
|
+
if (input.reduceOnly !== undefined) {
|
|
233
|
+
body.reduceOnly = input.reduceOnly === true ? "TRUE" : "FALSE";
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
sym: String(body.symbol),
|
|
237
|
+
side: String(body.side),
|
|
238
|
+
orderType: String(body.orderType),
|
|
239
|
+
clientOrderId: String(body.clientOrderId),
|
|
240
|
+
...omitKeys(body, ["symbol"])
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function orderAmendBody(input) {
|
|
244
|
+
const body = optionalParams(input, ["orderId", "clientOrderId"]);
|
|
245
|
+
if (input.quantity !== undefined) {
|
|
246
|
+
body.replaceQty = String(input.quantity);
|
|
247
|
+
}
|
|
248
|
+
if (input.price !== undefined) {
|
|
249
|
+
body.replacePrice = String(input.price);
|
|
250
|
+
}
|
|
251
|
+
return body;
|
|
252
|
+
}
|
|
253
|
+
function orderCancelBody(input) {
|
|
254
|
+
return optionalParams(input, ["orderId", "clientOrderId"]);
|
|
255
|
+
}
|
|
256
|
+
function positionCloseBody(input) {
|
|
257
|
+
const body = {
|
|
258
|
+
sym: String(input.symbol)
|
|
259
|
+
};
|
|
260
|
+
if (input.positionSide !== undefined && input.positionSide !== null && input.positionSide !== "") {
|
|
261
|
+
body.positionSide = String(input.positionSide);
|
|
262
|
+
}
|
|
263
|
+
return body;
|
|
264
|
+
}
|
|
265
|
+
function algoPlaceBody(input) {
|
|
266
|
+
const body = {
|
|
267
|
+
...orderPlaceBody(input),
|
|
268
|
+
algoOrderType: String(input.algoType)
|
|
269
|
+
};
|
|
270
|
+
appendMappedOptionalParams(body, input, [
|
|
271
|
+
["positionSide", "positionSide"],
|
|
272
|
+
["conditionType", "conditionType"],
|
|
273
|
+
["triggerPrice", "conditionalTriggerPrice"],
|
|
274
|
+
["triggerType", "conditionalTriggerType"],
|
|
275
|
+
["takeProfitPrice", "tpTriggerPrice"],
|
|
276
|
+
["takeProfitLimitPrice", "tpPrice"],
|
|
277
|
+
["stopLossPrice", "slTriggerPrice"],
|
|
278
|
+
["stopLossLimitPrice", "slPrice"]
|
|
279
|
+
]);
|
|
280
|
+
return body;
|
|
281
|
+
}
|
|
282
|
+
function algoAmendBody(input) {
|
|
283
|
+
const body = optionalParams(input, ["algoOrderId", "clientOrderId"]);
|
|
284
|
+
appendMappedOptionalParams(body, input, [
|
|
285
|
+
["triggerPrice", "conditionalTriggerPrice"],
|
|
286
|
+
["triggerType", "conditionalTriggerType"],
|
|
287
|
+
["takeProfitPrice", "tpTriggerPrice"],
|
|
288
|
+
["takeProfitLimitPrice", "tpPrice"],
|
|
289
|
+
["stopLossPrice", "slTriggerPrice"],
|
|
290
|
+
["stopLossLimitPrice", "slPrice"]
|
|
291
|
+
]);
|
|
292
|
+
return body;
|
|
293
|
+
}
|
|
294
|
+
function algoListParams(input) {
|
|
295
|
+
const params = {};
|
|
296
|
+
if (input.symbol !== undefined && input.symbol !== null && input.symbol !== "") {
|
|
297
|
+
params.sym = String(input.symbol);
|
|
298
|
+
}
|
|
299
|
+
return params;
|
|
300
|
+
}
|
|
301
|
+
function positionHistoryParams(input) {
|
|
302
|
+
const params = optionalParams(input, ["startTime", "endTime"]);
|
|
303
|
+
if (input.symbol !== undefined && input.symbol !== null && input.symbol !== "") {
|
|
304
|
+
params.sym = String(input.symbol);
|
|
305
|
+
}
|
|
306
|
+
return params;
|
|
307
|
+
}
|
|
308
|
+
function requiredParams(input, keys) {
|
|
309
|
+
const result = {};
|
|
310
|
+
for (const key of keys) {
|
|
311
|
+
if (input[key] === undefined || input[key] === null || input[key] === "") {
|
|
312
|
+
throw new ProductError({ code: "RCORE00001", status: "FAIL", message: `${key} is required.` });
|
|
313
|
+
}
|
|
314
|
+
result[key] = String(input[key]);
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
function optionalParams(input, keys) {
|
|
319
|
+
const result = {};
|
|
320
|
+
for (const key of keys) {
|
|
321
|
+
if (input[key] !== undefined && input[key] !== null && input[key] !== "") {
|
|
322
|
+
result[key] = String(input[key]);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
function filterPositionResponse(response, symbol) {
|
|
328
|
+
if (!symbol) {
|
|
329
|
+
return response;
|
|
330
|
+
}
|
|
331
|
+
if (Array.isArray(response)) {
|
|
332
|
+
return {
|
|
333
|
+
data: response.filter((item) => positionMatchesSymbol(item, symbol)),
|
|
334
|
+
requestedSymbol: symbol,
|
|
335
|
+
filterApplied: "client"
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (!isRecord(response)) {
|
|
339
|
+
return response;
|
|
340
|
+
}
|
|
341
|
+
if (Array.isArray(response.data)) {
|
|
342
|
+
return {
|
|
343
|
+
...response,
|
|
344
|
+
data: response.data.filter((item) => positionMatchesSymbol(item, symbol)),
|
|
345
|
+
requestedSymbol: symbol,
|
|
346
|
+
filterApplied: "client"
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (isRecord(response.data) && Array.isArray(response.data.list)) {
|
|
350
|
+
return {
|
|
351
|
+
...response,
|
|
352
|
+
data: {
|
|
353
|
+
...response.data,
|
|
354
|
+
list: response.data.list.filter((item) => positionMatchesSymbol(item, symbol))
|
|
355
|
+
},
|
|
356
|
+
requestedSymbol: symbol,
|
|
357
|
+
filterApplied: "client"
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (Array.isArray(response.list)) {
|
|
361
|
+
return {
|
|
362
|
+
...response,
|
|
363
|
+
list: response.list.filter((item) => positionMatchesSymbol(item, symbol)),
|
|
364
|
+
requestedSymbol: symbol,
|
|
365
|
+
filterApplied: "client"
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
...response,
|
|
370
|
+
requestedSymbol: symbol,
|
|
371
|
+
filterApplied: "not_applicable"
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function positionMatchesSymbol(item, symbol) {
|
|
375
|
+
if (!isRecord(item)) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
return [item.sym, item.symbol, item.instrumentId, item.instrument].some((value) => value === symbol);
|
|
379
|
+
}
|
|
380
|
+
function compactObject(input) {
|
|
381
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== null && value !== ""));
|
|
382
|
+
}
|
|
383
|
+
function isRecord(value) {
|
|
384
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
385
|
+
}
|
|
386
|
+
function stringValue(value, fallback) {
|
|
387
|
+
return value === undefined || value === null || value === "" ? fallback : String(value);
|
|
388
|
+
}
|
|
389
|
+
function appendMappedOptionalParams(target, input, mappings) {
|
|
390
|
+
for (const [from, to] of mappings) {
|
|
391
|
+
const value = input[from];
|
|
392
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
393
|
+
target[to] = String(value);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function omitKeys(input, keys) {
|
|
398
|
+
const blocked = new Set(keys);
|
|
399
|
+
return Object.fromEntries(Object.entries(input).filter(([key]) => !blocked.has(key)));
|
|
400
|
+
}
|