@okx_ai/okx-trade-mcp 1.3.1 → 1.3.2-beta.2

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/index.js CHANGED
@@ -733,19 +733,19 @@ import * as os3 from "os";
733
733
  import { execFileSync } from "child_process";
734
734
  var EXEC_TIMEOUT_MS = 3e4;
735
735
  var ALLOWED_DOMAIN_RE = /^[\w.-]+\.okx\.com$/;
736
- var DOH_BIN_DIR = join(homedir(), ".okx", "bin");
737
- function getDohBinaryPath() {
738
- if (process.env.OKX_DOH_BINARY_PATH) {
739
- return process.env.OKX_DOH_BINARY_PATH;
736
+ var PILOT_BIN_DIR = join(homedir(), ".okx", "bin");
737
+ function getPilotBinaryPath() {
738
+ if (process.env.OKX_PILOT_BINARY_PATH) {
739
+ return process.env.OKX_PILOT_BINARY_PATH;
740
740
  }
741
741
  const ext = process.platform === "win32" ? ".exe" : "";
742
- return join(DOH_BIN_DIR, `okx-pilot${ext}`);
742
+ return join(PILOT_BIN_DIR, `okx-pilot${ext}`);
743
743
  }
744
- function execDohBinary(domain, exclude = [], userAgent) {
744
+ function execPilotBinary(domain, exclude = [], userAgent) {
745
745
  if (!ALLOWED_DOMAIN_RE.test(domain)) {
746
746
  return Promise.resolve(null);
747
747
  }
748
- const binPath = getDohBinaryPath();
748
+ const binPath = getPilotBinaryPath();
749
749
  const args = ["--domain", domain];
750
750
  if (exclude.length > 0) {
751
751
  args.push("--exclude", exclude.join(","));
@@ -778,7 +778,7 @@ function execDohBinary(domain, exclude = [], userAgent) {
778
778
  });
779
779
  }
780
780
  function getDefaultCachePath() {
781
- return process.env.OKX_DOH_CACHE_PATH || join2(homedir2(), ".okx", "doh-cache.json");
781
+ return process.env.OKX_PILOT_CACHE_PATH || join2(homedir2(), ".okx", "pilot-cache.json");
782
782
  }
783
783
  function readCache(hostname, cachePath = getDefaultCachePath()) {
784
784
  try {
@@ -832,7 +832,7 @@ function getActiveFailedNodes(nodes) {
832
832
  const now = Date.now();
833
833
  return nodes.filter((n) => now - n.failedAt < FAILED_NODE_TTL_MS);
834
834
  }
835
- function resolveDoh(hostname, cachePath) {
835
+ function resolvePilot(hostname, cachePath) {
836
836
  const entry = readCache(hostname, cachePath);
837
837
  if (entry) {
838
838
  if (entry.mode === "direct") {
@@ -844,53 +844,53 @@ function resolveDoh(hostname, cachePath) {
844
844
  }
845
845
  return { mode: null, node: null };
846
846
  }
847
- async function reResolveDoh(hostname, failedIp, userAgent, cachePath) {
847
+ async function reResolvePilot(hostname, failedIp, userAgent, cachePath) {
848
848
  const entry = readCache(hostname, cachePath);
849
849
  const active = getActiveFailedNodes(entry?.failedNodes);
850
850
  const now = Date.now();
851
851
  const alreadyFailed = failedIp && active.some((n) => n.ip === failedIp);
852
852
  const failedNodes = failedIp && !alreadyFailed ? [...active, { ip: failedIp, failedAt: now }] : active;
853
853
  const excludeIps = failedNodes.map((n) => n.ip);
854
- const node = await execDohBinary(hostname, excludeIps, userAgent);
854
+ const node = await execPilotBinary(hostname, excludeIps, userAgent);
855
855
  return classifyAndCache(node, hostname, failedNodes, cachePath);
856
856
  }
857
857
  function vlog(message) {
858
858
  process.stderr.write(`[verbose] ${message}
859
859
  `);
860
860
  }
861
- var DohManager = class {
861
+ var PilotManager = class {
862
862
  opts;
863
- // DoH proxy state (lazy-resolved on first request)
864
- dohResolved = false;
865
- dohRetried = false;
863
+ // Pilot proxy state (lazy-resolved on first request)
864
+ pilotResolved = false;
865
+ pilotRetried = false;
866
866
  directUnverified = false;
867
867
  // The first direct connection has not yet been verified
868
- dohNode = null;
869
- dohAgent = null;
870
- dohBaseUrl = null;
868
+ pilotNode = null;
869
+ pilotAgent = null;
870
+ pilotBaseUrl = null;
871
871
  constructor(opts) {
872
872
  this.opts = opts;
873
873
  }
874
874
  /**
875
- * Lazily resolve the DoH proxy node on the first request.
875
+ * Lazily resolve the Pilot proxy node on the first request.
876
876
  * Uses cache-first strategy via the resolver.
877
877
  */
878
- prepareDoh() {
879
- if (this.dohResolved || this.opts.hasCustomProxy) return;
880
- this.dohResolved = true;
878
+ preparePilot() {
879
+ if (this.pilotResolved || this.opts.hasCustomProxy) return;
880
+ this.pilotResolved = true;
881
881
  try {
882
882
  const { hostname, protocol } = new URL(this.opts.baseUrl);
883
- const result = resolveDoh(hostname);
883
+ const result = resolvePilot(hostname);
884
884
  if (!result.mode) {
885
885
  this.directUnverified = true;
886
886
  if (this.opts.verbose) {
887
- vlog("DoH: no cache, trying direct connection first");
887
+ vlog("Pilot: no cache, trying direct connection first");
888
888
  }
889
889
  return;
890
890
  }
891
891
  if (result.mode === "direct") {
892
892
  if (this.opts.verbose) {
893
- vlog("DoH: mode=direct (overseas or cached), using direct connection");
893
+ vlog("Pilot: mode=direct (overseas or cached), using direct connection");
894
894
  }
895
895
  return;
896
896
  }
@@ -900,57 +900,57 @@ var DohManager = class {
900
900
  } catch (err) {
901
901
  if (this.opts.verbose) {
902
902
  const cause = err instanceof Error ? err.message : String(err);
903
- vlog(`DoH resolution failed, falling back to direct: ${cause}`);
903
+ vlog(`Pilot resolution failed, falling back to direct: ${cause}`);
904
904
  }
905
905
  }
906
906
  }
907
907
  /** Get connection parameters for the current request. */
908
908
  getConnectionParams() {
909
- const baseUrl = this.dohNode ? this.dohBaseUrl : this.opts.baseUrl;
909
+ const baseUrl = this.pilotNode ? this.pilotBaseUrl : this.opts.baseUrl;
910
910
  const result = { baseUrl };
911
- if (this.dohAgent) {
912
- result.dispatcher = this.dohAgent;
911
+ if (this.pilotAgent) {
912
+ result.dispatcher = this.pilotAgent;
913
913
  }
914
- if (this.dohNode) {
915
- result.userAgent = this.dohUserAgent;
914
+ if (this.pilotNode) {
915
+ result.userAgent = this.pilotUserAgent;
916
916
  }
917
917
  return result;
918
918
  }
919
- /** Whether a DoH proxy node is currently active. */
919
+ /** Whether a Pilot proxy node is currently active. */
920
920
  get isProxyActive() {
921
- return this.dohNode !== null;
921
+ return this.pilotNode !== null;
922
922
  }
923
923
  /** Whether we have already retried after network failure. */
924
924
  get hasRetried() {
925
- return this.dohRetried;
925
+ return this.pilotRetried;
926
926
  }
927
927
  /**
928
928
  * Handle network failure: re-resolve with --exclude and retry once.
929
929
  * Returns true if retry should proceed, false if already retried.
930
930
  */
931
931
  async handleNetworkFailure() {
932
- if (this.dohRetried) return false;
933
- this.dohRetried = true;
934
- const failedIp = this.dohNode?.ip ?? "";
932
+ if (this.pilotRetried) return false;
933
+ this.pilotRetried = true;
934
+ const failedIp = this.pilotNode?.ip ?? "";
935
935
  const { hostname, protocol } = new URL(this.opts.baseUrl);
936
- this.dohNode = null;
937
- this.dohAgent = null;
938
- this.dohBaseUrl = null;
936
+ this.pilotNode = null;
937
+ this.pilotAgent = null;
938
+ this.pilotBaseUrl = null;
939
939
  if (!failedIp) this.directUnverified = false;
940
940
  if (this.opts.verbose) {
941
- vlog(failedIp ? `DoH: proxy node ${failedIp} failed, re-resolving with --exclude` : "DoH: direct connection failed, calling binary for DoH resolution");
941
+ vlog(failedIp ? `Pilot: proxy node ${failedIp} failed, re-resolving with --exclude` : "Pilot: direct connection failed, calling binary for Pilot resolution");
942
942
  }
943
943
  try {
944
- const result = await reResolveDoh(hostname, failedIp, this.dohUserAgent);
944
+ const result = await reResolvePilot(hostname, failedIp, this.pilotUserAgent);
945
945
  if (result.mode === "proxy" && result.node) {
946
946
  this.applyNode(result.node, protocol);
947
- this.dohRetried = false;
947
+ this.pilotRetried = false;
948
948
  return true;
949
949
  }
950
950
  } catch {
951
951
  }
952
952
  if (this.opts.verbose) {
953
- vlog("DoH: re-resolution failed or switched to direct, retrying with direct connection");
953
+ vlog("Pilot: re-resolution failed or switched to direct, retrying with direct connection");
954
954
  }
955
955
  return true;
956
956
  }
@@ -959,7 +959,7 @@ var DohManager = class {
959
959
  * (Even if the business response is an error, the network path is valid.)
960
960
  */
961
961
  cacheDirectIfNeeded() {
962
- if (!this.directUnverified || this.dohNode) return;
962
+ if (!this.directUnverified || this.pilotNode) return;
963
963
  this.directUnverified = false;
964
964
  const { hostname } = new URL(this.opts.baseUrl);
965
965
  writeCache(hostname, {
@@ -969,25 +969,25 @@ var DohManager = class {
969
969
  updatedAt: Date.now()
970
970
  });
971
971
  if (this.opts.verbose) {
972
- vlog("DoH: direct connection succeeded, cached mode=direct");
972
+ vlog("Pilot: direct connection succeeded, cached mode=direct");
973
973
  }
974
974
  }
975
- /** User-Agent for DoH proxy requests: OKX/@okx_ai/{packageName}/{version} */
976
- get dohUserAgent() {
975
+ /** User-Agent for Pilot proxy requests: OKX/@okx_ai/{packageName}/{version} */
976
+ get pilotUserAgent() {
977
977
  return `OKX/@okx_ai/${this.opts.packageUserAgent ?? "unknown"}`;
978
978
  }
979
979
  /**
980
- * Apply a DoH node: set up the custom Agent + base URL.
980
+ * Apply a Pilot node: set up the custom Agent + base URL.
981
981
  *
982
982
  * node.ip may be a real IP or a domain (CNAME like *.aliyunddos1021.com).
983
983
  * - Real IP → use directly in lookup callback
984
984
  * - Domain → dns.lookup on every connection to get a fresh IP
985
985
  */
986
986
  applyNode(node, protocol) {
987
- this.dohNode = node;
988
- this.dohBaseUrl = `${protocol}//${node.host}`;
987
+ this.pilotNode = node;
988
+ this.pilotBaseUrl = `${protocol}//${node.host}`;
989
989
  const nodeIpIsRealIp = !!isIP(node.ip);
990
- this.dohAgent = new Agent({
990
+ this.pilotAgent = new Agent({
991
991
  connect: {
992
992
  lookup: (_hostname, options, callback) => {
993
993
  if (nodeIpIsRealIp) {
@@ -1011,7 +1011,7 @@ var DohManager = class {
1011
1011
  }
1012
1012
  });
1013
1013
  if (this.opts.verbose) {
1014
- vlog(`DoH proxy active: \u2192 ${node.host} (${node.ip}), ttl=${node.ttl}s`);
1014
+ vlog(`Pilot proxy active: \u2192 ${node.host} (${node.ip}), ttl=${node.ttl}s`);
1015
1015
  }
1016
1016
  }
1017
1017
  };
@@ -1233,14 +1233,14 @@ var OkxRestClient = class _OkxRestClient {
1233
1233
  config;
1234
1234
  rateLimiter;
1235
1235
  dispatcher;
1236
- doh;
1236
+ pilot;
1237
1237
  constructor(config) {
1238
1238
  this.config = config;
1239
1239
  this.rateLimiter = new RateLimiter(3e4, config.verbose);
1240
1240
  if (config.proxyUrl) {
1241
1241
  this.dispatcher = new ProxyAgent(config.proxyUrl);
1242
1242
  }
1243
- this.doh = new DohManager({
1243
+ this.pilot = new PilotManager({
1244
1244
  baseUrl: config.baseUrl,
1245
1245
  packageUserAgent: config.userAgent,
1246
1246
  verbose: config.verbose,
@@ -1294,13 +1294,14 @@ var OkxRestClient = class _OkxRestClient {
1294
1294
  rateLimit
1295
1295
  });
1296
1296
  }
1297
- async privatePost(path4, body, rateLimit) {
1297
+ async privatePost(path4, body, rateLimit, retryOnNetworkError) {
1298
1298
  return this.request({
1299
1299
  method: "POST",
1300
1300
  path: path4,
1301
1301
  auth: "private",
1302
1302
  body,
1303
- rateLimit
1303
+ rateLimit,
1304
+ retryOnNetworkError
1304
1305
  });
1305
1306
  }
1306
1307
  setAuthHeaders(headers, method, requestPath, bodyJson, timestamp) {
@@ -1422,12 +1423,12 @@ var OkxRestClient = class _OkxRestClient {
1422
1423
  * Security: validates Content-Type and enforces maxBytes limit.
1423
1424
  */
1424
1425
  async privatePostBinary(path4, body, opts) {
1425
- this.doh.prepareDoh();
1426
+ this.pilot.preparePilot();
1426
1427
  const maxBytes = opts?.maxBytes ?? _OkxRestClient.DEFAULT_MAX_BYTES;
1427
1428
  const expectedCT = opts?.expectedContentType ?? "application/octet-stream";
1428
1429
  const bodyJson = body ? JSON.stringify(body) : "";
1429
1430
  const endpoint = `POST ${path4}`;
1430
- const conn = this.doh.getConnectionParams();
1431
+ const conn = this.pilot.getConnectionParams();
1431
1432
  this.logRequest("POST", `${conn.baseUrl}${path4}`, "private");
1432
1433
  const reqConfig = { method: "POST", path: path4, auth: "private" };
1433
1434
  const headers = this.buildHeaders(reqConfig, path4, bodyJson, getNow());
@@ -1439,13 +1440,15 @@ var OkxRestClient = class _OkxRestClient {
1439
1440
  try {
1440
1441
  response = await this.fetchBinary(path4, endpoint, headers, bodyJson, t0);
1441
1442
  } catch (error) {
1442
- this.doh.handleNetworkFailure().catch(() => {
1443
- });
1443
+ try {
1444
+ await this.pilot.handleNetworkFailure();
1445
+ } catch {
1446
+ }
1444
1447
  throw error;
1445
1448
  }
1446
1449
  const elapsed = Date.now() - t0;
1447
1450
  const traceId = extractTraceId(response.headers);
1448
- this.doh.cacheDirectIfNeeded();
1451
+ this.pilot.cacheDirectIfNeeded();
1449
1452
  if (!response.ok) {
1450
1453
  const text = await response.text();
1451
1454
  this.logResponse(response.status, text.length, elapsed, traceId, String(response.status));
@@ -1471,15 +1474,15 @@ var OkxRestClient = class _OkxRestClient {
1471
1474
  /**
1472
1475
  * Send an unauthenticated GET request and return the raw binary response.
1473
1476
  * Used for pre-signed download URLs where auth is embedded in the token.
1474
- * Inherits proxy, timeout, DoH, and verbose capabilities from the client.
1477
+ * Inherits proxy, timeout, Pilot, and verbose capabilities from the client.
1475
1478
  */
1476
1479
  async publicGetBinary(path4, query, opts) {
1477
- this.doh.prepareDoh();
1480
+ this.pilot.preparePilot();
1478
1481
  const maxBytes = opts?.maxBytes ?? _OkxRestClient.DEFAULT_MAX_BYTES;
1479
1482
  const expectedCT = opts?.expectedContentType ?? "application/octet-stream";
1480
1483
  const queryString = buildQueryString(query);
1481
1484
  const requestPath = queryString ? `${path4}?${queryString}` : path4;
1482
- const conn = this.doh.getConnectionParams();
1485
+ const conn = this.pilot.getConnectionParams();
1483
1486
  const url = `${conn.baseUrl}${requestPath}`;
1484
1487
  this.logRequest("GET", url, "public");
1485
1488
  const headers = new Headers({ Accept: "application/octet-stream" });
@@ -1495,13 +1498,15 @@ var OkxRestClient = class _OkxRestClient {
1495
1498
  dispatcher: this.dispatcher ?? conn.dispatcher
1496
1499
  });
1497
1500
  } catch (error) {
1498
- this.doh.handleNetworkFailure().catch(() => {
1499
- });
1501
+ try {
1502
+ await this.pilot.handleNetworkFailure();
1503
+ } catch {
1504
+ }
1500
1505
  throw new NetworkError(`Failed to call OKX endpoint GET ${path4}.`, `GET ${path4}`, error);
1501
1506
  }
1502
1507
  const elapsed = Date.now() - t0;
1503
1508
  const traceId = extractTraceId(response.headers);
1504
- this.doh.cacheDirectIfNeeded();
1509
+ this.pilot.cacheDirectIfNeeded();
1505
1510
  if (!response.ok) {
1506
1511
  const text = await response.text();
1507
1512
  this.logResponse(response.status, text.length, elapsed, traceId, String(response.status));
@@ -1526,7 +1531,7 @@ var OkxRestClient = class _OkxRestClient {
1526
1531
  }
1527
1532
  /** Execute fetch for binary endpoint, wrapping network errors. */
1528
1533
  async fetchBinary(path4, endpoint, headers, bodyJson, t0) {
1529
- const conn = this.doh.getConnectionParams();
1534
+ const conn = this.pilot.getConnectionParams();
1530
1535
  try {
1531
1536
  const fetchOptions = {
1532
1537
  method: "POST",
@@ -1573,17 +1578,17 @@ var OkxRestClient = class _OkxRestClient {
1573
1578
  // JSON request
1574
1579
  // ---------------------------------------------------------------------------
1575
1580
  /**
1576
- * Handle network error during a JSON request: refresh DoH and maybe retry.
1581
+ * Handle network error during a JSON request: refresh Pilot and maybe retry.
1577
1582
  * Always either returns a retry result or throws NetworkError.
1578
1583
  */
1579
1584
  async handleRequestNetworkError(error, reqConfig, requestPath, t0) {
1580
- if (!this.doh.hasRetried) {
1585
+ if (!this.pilot.hasRetried) {
1581
1586
  if (this.config.verbose) {
1582
1587
  const cause = error instanceof Error ? error.message : String(error);
1583
- vlog2(`Network failure, refreshing DoH: ${cause}`);
1588
+ vlog2(`Network failure, refreshing Pilot: ${cause}`);
1584
1589
  }
1585
- const shouldRetry = await this.doh.handleNetworkFailure();
1586
- if (shouldRetry && reqConfig.method === "GET") {
1590
+ const shouldRetry = await this.pilot.handleNetworkFailure();
1591
+ if (shouldRetry && (reqConfig.method === "GET" || reqConfig.retryOnNetworkError)) {
1587
1592
  return this.request(reqConfig);
1588
1593
  }
1589
1594
  }
@@ -1599,10 +1604,10 @@ var OkxRestClient = class _OkxRestClient {
1599
1604
  );
1600
1605
  }
1601
1606
  async request(reqConfig) {
1602
- this.doh.prepareDoh();
1607
+ this.pilot.preparePilot();
1603
1608
  const queryString = buildQueryString(reqConfig.query);
1604
1609
  const requestPath = queryString.length > 0 ? `${reqConfig.path}?${queryString}` : reqConfig.path;
1605
- const conn = this.doh.getConnectionParams();
1610
+ const conn = this.pilot.getConnectionParams();
1606
1611
  const url = `${conn.baseUrl}${requestPath}`;
1607
1612
  const bodyJson = reqConfig.body ? JSON.stringify(reqConfig.body) : "";
1608
1613
  const timestamp = getNow();
@@ -1631,7 +1636,7 @@ var OkxRestClient = class _OkxRestClient {
1631
1636
  const rawText = await response.text();
1632
1637
  const elapsed = Date.now() - t0;
1633
1638
  const traceId = extractTraceId(response.headers);
1634
- this.doh.cacheDirectIfNeeded();
1639
+ this.pilot.cacheDirectIfNeeded();
1635
1640
  return this.processResponse(rawText, response, elapsed, traceId, reqConfig, requestPath);
1636
1641
  }
1637
1642
  };
@@ -2059,6 +2064,7 @@ var MODULES = [
2059
2064
  "account",
2060
2065
  "event",
2061
2066
  "news",
2067
+ "smartmoney",
2062
2068
  ...EARN_SUB_MODULE_IDS,
2063
2069
  ...BOT_SUB_MODULE_IDS,
2064
2070
  "skills"
@@ -2209,6 +2215,10 @@ function registerAccountTools() {
2209
2215
  showValuation: {
2210
2216
  type: "boolean",
2211
2217
  description: "Include total asset valuation breakdown by account type (trading/funding/earn). Default false."
2218
+ },
2219
+ valuationCcy: {
2220
+ type: "string",
2221
+ description: "Currency used to denominate the total asset valuation (e.g. USDT, BTC). Default USDT. Only applies when showValuation=true."
2212
2222
  }
2213
2223
  }
2214
2224
  },
@@ -2216,6 +2226,7 @@ function registerAccountTools() {
2216
2226
  const args = asRecord(rawArgs);
2217
2227
  const ccy = readString(args, "ccy");
2218
2228
  const showValuation = readBoolean(args, "showValuation");
2229
+ const valuationCcy = readString(args, "valuationCcy") ?? "USDT";
2219
2230
  if (showValuation) {
2220
2231
  const balanceResp2 = await context.client.privateGet(
2221
2232
  "/api/v5/asset/balances",
@@ -2223,16 +2234,26 @@ function registerAccountTools() {
2223
2234
  privateRateLimit("account_get_asset_balance", 6)
2224
2235
  );
2225
2236
  let valuationData = null;
2237
+ let valuationError;
2226
2238
  try {
2227
2239
  const valuationResp = await context.client.privateGet(
2228
2240
  "/api/v5/asset/asset-valuation",
2229
- {},
2241
+ { ccy: valuationCcy },
2230
2242
  privateRateLimit("account_get_asset_valuation", 1)
2231
2243
  );
2232
2244
  valuationData = valuationResp.data;
2233
- } catch {
2245
+ } catch (err) {
2246
+ valuationError = err instanceof Error ? err.message : String(err);
2234
2247
  }
2235
- return { ...normalizeResponse(balanceResp2), valuation: valuationData };
2248
+ const valuationResult = {
2249
+ ...normalizeResponse(balanceResp2),
2250
+ valuation: valuationData,
2251
+ valuationCcy
2252
+ };
2253
+ if (valuationError !== void 0) {
2254
+ valuationResult["valuationError"] = valuationError;
2255
+ }
2256
+ return valuationResult;
2236
2257
  }
2237
2258
  const balanceResp = await context.client.privateGet(
2238
2259
  "/api/v5/asset/balances",
@@ -3904,10 +3925,133 @@ function registerGridTools() {
3904
3925
  return normalizeWrite(response);
3905
3926
  }
3906
3927
  },
3928
+ {
3929
+ name: "grid_amend_order",
3930
+ module: "bot.grid",
3931
+ description: "Amend a running grid bot. [CAUTION] Modifies a running bot. Use grid_list_orders to confirm the bot is running and obtain the algoId before calling.\nSupports two modes, which can be combined in a single call:\n\u2022 Price-range mode (maxPx+minPx+gridNum): change upper/lower price boundary and grid count. Contract grid: if new range requires more margin, pass topUpAmt; omit to auto-use the minimum required. Spot grid: topUpAmt is not supported.\n\u2022 TP/SL mode (instId + any of tpTriggerPx/slTriggerPx/tpRatio/slRatio): update take-profit and/or stop-loss. Pass '-1' to explicitly clear an existing TP or SL. tpTriggerPx/slTriggerPx are absolute prices; tpRatio/slRatio are profit ratios (e.g. '0.1' = 10%).\nWhen both sets of params are provided, both APIs are called sequentially.\nDo NOT use to create a new grid bot \u2014 use grid_create_order instead. Do NOT use to stop a grid bot \u2014 use grid_stop_order instead.",
3932
+ isWrite: true,
3933
+ inputSchema: {
3934
+ type: "object",
3935
+ properties: {
3936
+ algoId: {
3937
+ type: "string",
3938
+ description: "Grid bot algo order ID (required)"
3939
+ },
3940
+ // ── Price-range mode ──────────────────────────────────────────────
3941
+ maxPx: {
3942
+ type: "string",
3943
+ description: "[Price-range mode] New upper price boundary. Triggers amend-algo-basic-param when provided."
3944
+ },
3945
+ minPx: {
3946
+ type: "string",
3947
+ description: "[Price-range mode] New lower price boundary. Required when maxPx is set."
3948
+ },
3949
+ gridNum: {
3950
+ type: "string",
3951
+ description: "[Price-range mode] New number of grid intervals (integer). Required when maxPx is set."
3952
+ },
3953
+ // ── TP/SL mode ────────────────────────────────────────────────────
3954
+ instId: {
3955
+ type: "string",
3956
+ description: "[TP/SL mode] Instrument ID, e.g. BTC-USDT. Required when setting TP/SL."
3957
+ },
3958
+ tpTriggerPx: {
3959
+ type: "string",
3960
+ description: "[TP/SL mode] Take-profit trigger price (absolute). Pass '-1' to clear."
3961
+ },
3962
+ slTriggerPx: {
3963
+ type: "string",
3964
+ description: "[TP/SL mode] Stop-loss trigger price (absolute). Pass '-1' to clear."
3965
+ },
3966
+ tpRatio: {
3967
+ type: "string",
3968
+ description: "[TP/SL mode] Take-profit ratio, e.g. '0.1' = 10% profit. Pass '-1' to clear."
3969
+ },
3970
+ slRatio: {
3971
+ type: "string",
3972
+ description: "[TP/SL mode] Stop-loss ratio, e.g. '0.1' = 10% drawdown. Pass '-1' to clear."
3973
+ },
3974
+ // ── Shared optional ───────────────────────────────────────────────
3975
+ topUpAmt: {
3976
+ type: "string",
3977
+ description: "Top-up amount. In price-range mode maps to topupAmount (contract grid only; omit to use minimum required). In TP/SL mode maps to topUpAmt."
3978
+ }
3979
+ },
3980
+ required: ["algoId"]
3981
+ },
3982
+ handler: async (rawArgs, context) => {
3983
+ const args = asRecord(rawArgs);
3984
+ const algoId = requireString(args, "algoId");
3985
+ const maxPx = readString(args, "maxPx");
3986
+ const instId = readString(args, "instId");
3987
+ const hasTpsl = readString(args, "tpTriggerPx") || readString(args, "slTriggerPx") || readString(args, "tpRatio") || readString(args, "slRatio");
3988
+ if (!maxPx && !hasTpsl) {
3989
+ throw new OkxApiError(
3990
+ "Nothing to amend. Provide maxPx+minPx+gridNum for price-range mode, or any of tpTriggerPx/slTriggerPx/tpRatio/slRatio (instId also required) for TP/SL mode (both can be combined).",
3991
+ { code: "", endpoint: "grid_amend_order" }
3992
+ );
3993
+ }
3994
+ if (hasTpsl && !instId) {
3995
+ throw new OkxApiError(
3996
+ "TP/SL mode requires instId. Provide instId alongside the TP/SL parameters.",
3997
+ { code: "", endpoint: "grid_amend_order" }
3998
+ );
3999
+ }
4000
+ const results = [];
4001
+ if (maxPx) {
4002
+ results.push(normalizeWrite(await context.client.privatePost(
4003
+ "/api/v5/tradingBot/grid/amend-algo-basic-param",
4004
+ compactObject({
4005
+ algoId,
4006
+ maxPx,
4007
+ minPx: requireString(args, "minPx"),
4008
+ gridNum: requireString(args, "gridNum"),
4009
+ // API field is "topupAmount" (lowercase u) — different from TP/SL mode's "topUpAmt"
4010
+ // Contract grid only; omitting lets the API use the minimum required
4011
+ topupAmount: readString(args, "topUpAmt")
4012
+ }),
4013
+ privateRateLimit("grid_amend_order", 20),
4014
+ true
4015
+ // retryOnNetworkError: amend sets fixed values, safe to retry
4016
+ )));
4017
+ }
4018
+ if (hasTpsl) {
4019
+ try {
4020
+ results.push(normalizeWrite(await context.client.privatePost(
4021
+ "/api/v5/tradingBot/grid/amend-order-algo",
4022
+ compactObject({
4023
+ algoId,
4024
+ instId,
4025
+ tpTriggerPx: readString(args, "tpTriggerPx"),
4026
+ slTriggerPx: readString(args, "slTriggerPx"),
4027
+ tpRatio: readString(args, "tpRatio"),
4028
+ slRatio: readString(args, "slRatio"),
4029
+ topUpAmt: readString(args, "topUpAmt")
4030
+ // API field is "topUpAmt" (uppercase U) — different from price-range mode's "topupAmount"
4031
+ }),
4032
+ privateRateLimit("grid_amend_order", 20),
4033
+ true
4034
+ // retryOnNetworkError: amend sets fixed values, safe to retry
4035
+ )));
4036
+ } catch (err) {
4037
+ if (results.length > 0) {
4038
+ const msg = err instanceof Error ? err.message : String(err);
4039
+ throw new OkxApiError(
4040
+ `TP/SL amend failed (price-range amend already succeeded): ${msg}`,
4041
+ { code: "", endpoint: "grid_amend_order" }
4042
+ );
4043
+ }
4044
+ throw err;
4045
+ }
4046
+ }
4047
+ const merged = results.flatMap((r) => Array.isArray(r.data) ? r.data : [r.data]);
4048
+ return { endpoint: results[0].endpoint, requestTime: results[0].requestTime, data: merged };
4049
+ }
4050
+ },
3907
4051
  {
3908
4052
  name: "grid_stop_order",
3909
4053
  module: "bot.grid",
3910
- description: "Stop a grid bot. [CAUTION] Closes or cancels orders. For contract: stopType controls close ('1') vs cancel-only ('2').",
4054
+ description: "Stop a running grid bot. [CAUTION] This stops the strategy and handles open orders/positions according to stopType. Default (stopType='1') closes all positions immediately \u2014 use this for a clean exit. stopType='2' stops the strategy without selling: spot grid keeps all base assets as-is (no sell-back to quote); contract grid cancels all grid orders but leaves the position open for manual close later.",
3911
4055
  isWrite: true,
3912
4056
  inputSchema: {
3913
4057
  type: "object",
@@ -3916,13 +4060,13 @@ function registerGridTools() {
3916
4060
  algoOrdType: {
3917
4061
  type: "string",
3918
4062
  enum: ["grid", "contract_grid"],
3919
- description: "grid=Spot, contract_grid=Contract"
4063
+ description: "grid=Spot grid, contract_grid=Contract grid"
3920
4064
  },
3921
- instId: { type: "string", description: "e.g. BTC-USDT, BTC-USD-SWAP" },
4065
+ instId: { type: "string", description: "Instrument ID, e.g. BTC-USDT, BTC-USDT-SWAP" },
3922
4066
  stopType: {
3923
4067
  type: "string",
3924
- enum: ["1", "2", "3", "5", "6"],
3925
- description: "1=close all (default); 2=keep assets; 3=limit close; 5=partial; 6=no sell"
4068
+ enum: ["1", "2"],
4069
+ description: "'1' (default): stop strategy and sell \u2014 spot grid sells all base assets back to quote; contract grid market-closes all positions. '2': stop strategy without selling \u2014 spot grid keeps base assets as-is; contract grid cancels all grid orders but leaves the position open. After stopType='2', the remaining position can be closed manually from the Positions page."
3926
4070
  }
3927
4071
  },
3928
4072
  required: ["algoId", "algoOrdType", "instId"]
@@ -3937,7 +4081,9 @@ function registerGridTools() {
3937
4081
  instId: requireString(args, "instId"),
3938
4082
  stopType: readString(args, "stopType") ?? "1"
3939
4083
  })],
3940
- privateRateLimit("grid_stop_order", 20)
4084
+ privateRateLimit("grid_stop_order", 20),
4085
+ true
4086
+ // retryOnNetworkError: safe to retry — already-stopped returns an error but does not harm state
3941
4087
  );
3942
4088
  return normalizeWrite(response);
3943
4089
  }
@@ -6084,6 +6230,384 @@ function registerEventContractTools() {
6084
6230
  }
6085
6231
  ];
6086
6232
  }
6233
+ var PATH_LEADERBOARD = "/api/v5/orbit/public/leaderboard";
6234
+ var PATH_POSITION_CURRENT = "/api/v5/orbit/public/position-current";
6235
+ var PATH_TRADE_RECORDS = "/api/v5/orbit/public/trade-records";
6236
+ var PATH_OVERVIEW = "/api/v5/journal/smartmoney/overview";
6237
+ var PATH_SIGNAL = "/api/v5/journal/smartmoney/signal";
6238
+ var PATH_SIGNAL_HISTORY = "/api/v5/journal/smartmoney/signal-history";
6239
+ var SIGNAL_POOL_FILTER_PROPS = {
6240
+ sortType: {
6241
+ type: "string",
6242
+ description: "pnl or pnlRatio"
6243
+ },
6244
+ period: {
6245
+ type: "string",
6246
+ description: "3|7|30|90 days"
6247
+ },
6248
+ pnl: {
6249
+ type: "string",
6250
+ description: "PNL_ANY|PNL_TOP50|PNL_TOP20|PNL_TOP5"
6251
+ },
6252
+ winRatio: {
6253
+ type: "string",
6254
+ description: "WR_ANY|WR_GE_50|WR_GE_80"
6255
+ },
6256
+ maxRetreat: {
6257
+ type: "string",
6258
+ description: "MR_ANY|MR_LE_20|MR_LE_50"
6259
+ },
6260
+ asset: {
6261
+ type: "string",
6262
+ description: "AUM_ANY|AUM_TOP50|AUM_TOP20|AUM_TOP5"
6263
+ }
6264
+ };
6265
+ var LEADERBOARD_POOL_FILTER_PROPS = {
6266
+ sortType: {
6267
+ type: "string",
6268
+ description: "pnl or pnl_ratio"
6269
+ },
6270
+ period: {
6271
+ type: "string",
6272
+ description: "3|7|30|90 days, empty=all"
6273
+ },
6274
+ pnl: {
6275
+ type: "string",
6276
+ description: "Min PnL USD"
6277
+ },
6278
+ winRatio: {
6279
+ type: "string",
6280
+ description: "Min ratio (0.8=80%)"
6281
+ },
6282
+ maxRetreat: {
6283
+ type: "string",
6284
+ description: "Max DD (0.1=10%)"
6285
+ },
6286
+ asset: {
6287
+ type: "string",
6288
+ description: "Min AUM USD"
6289
+ }
6290
+ };
6291
+ var POOL_FILTER_KEYS = ["sortType", "period", "pnl", "winRatio", "maxRetreat", "asset"];
6292
+ function readPoolFilters(args) {
6293
+ const result = {};
6294
+ for (const key of POOL_FILTER_KEYS) {
6295
+ const val = readString(args, key);
6296
+ if (val) result[key] = val;
6297
+ }
6298
+ return result;
6299
+ }
6300
+ function extractLeaderboardData(data) {
6301
+ if (Array.isArray(data)) return data;
6302
+ if (data && typeof data === "object") {
6303
+ const inner = data.data;
6304
+ if (Array.isArray(inner)) return inner;
6305
+ }
6306
+ return [];
6307
+ }
6308
+ function registerSmartmoneyTools() {
6309
+ const tools = [
6310
+ /* ---------- 1. Overview ---------- */
6311
+ {
6312
+ name: "smartmoney_get_overview",
6313
+ module: "smartmoney",
6314
+ description: "Multi-currency smart money overview ranked by most-watched currencies. Pass ts=Date.now() for latest data, or dataVersion (yyyyMMddHHmm) from a prior call. For single-currency signal with entry prices and trend, use smartmoney_get_signal.",
6315
+ isWrite: false,
6316
+ inputSchema: {
6317
+ type: "object",
6318
+ properties: {
6319
+ dataVersion: {
6320
+ type: "string",
6321
+ description: "yyyyMMddHHmm UTC (or use ts)"
6322
+ },
6323
+ ts: {
6324
+ type: "string",
6325
+ description: "Timestamp ms (or use dataVersion)"
6326
+ },
6327
+ instType: {
6328
+ type: "string",
6329
+ description: "SPOT|MARGIN|FUTURES|SWAP|OPTION"
6330
+ },
6331
+ ...SIGNAL_POOL_FILTER_PROPS,
6332
+ lmtNum: {
6333
+ type: "string",
6334
+ description: "Trader pool size 1-500"
6335
+ },
6336
+ instCcyList: {
6337
+ type: "string",
6338
+ description: "Comma-separated e.g. BTC,ETH,SOL"
6339
+ },
6340
+ instCcy: {
6341
+ type: "string",
6342
+ description: "Single currency e.g. BTC"
6343
+ },
6344
+ topInstruments: {
6345
+ type: "string",
6346
+ description: "Top N instruments 1-100"
6347
+ }
6348
+ }
6349
+ },
6350
+ handler: async (rawArgs, context) => {
6351
+ const args = asRecord(rawArgs);
6352
+ const dv = readString(args, "dataVersion");
6353
+ const ts = readString(args, "ts");
6354
+ if (!dv && !ts) {
6355
+ throw new ValidationError('Either "dataVersion" or "ts" is required for smartmoney_get_overview.');
6356
+ }
6357
+ const response = await context.client.privateGet(
6358
+ PATH_OVERVIEW,
6359
+ compactObject({
6360
+ dataVersion: dv,
6361
+ ts,
6362
+ instType: readString(args, "instType"),
6363
+ ...readPoolFilters(args),
6364
+ lmtNum: readString(args, "lmtNum"),
6365
+ instCcyList: readString(args, "instCcyList"),
6366
+ instCcy: readString(args, "instCcy"),
6367
+ topInstruments: readString(args, "topInstruments")
6368
+ }),
6369
+ publicRateLimit("smartmoney_get_overview", 5)
6370
+ );
6371
+ return normalizeResponse(response);
6372
+ }
6373
+ },
6374
+ /* ---------- 2. Signal ---------- */
6375
+ {
6376
+ name: "smartmoney_get_signal",
6377
+ module: "smartmoney",
6378
+ description: "Single-currency consensus signal: long/short ratio, entry prices, trend, capital flow. Prefer instId (e.g. BTC-USDT-SWAP); instCcy is accepted but may return empty \u2014 use instId for reliable results. Pass ts=Date.now() for latest data, or dataVersion from a prior call. For multi-currency overview, use smartmoney_get_overview. For timeline, use smartmoney_get_signal_history.",
6379
+ isWrite: false,
6380
+ inputSchema: {
6381
+ type: "object",
6382
+ properties: {
6383
+ instId: {
6384
+ type: "string",
6385
+ description: "Recommended. e.g. BTC-USDT-SWAP"
6386
+ },
6387
+ instCcy: {
6388
+ type: "string",
6389
+ description: "e.g. BTC, SPOT/SWAP only. May return empty \u2014 prefer instId."
6390
+ },
6391
+ dataVersion: {
6392
+ type: "string",
6393
+ description: "yyyyMMddHHmm UTC (or use ts)"
6394
+ },
6395
+ ts: {
6396
+ type: "string",
6397
+ description: "Timestamp ms (or use dataVersion)"
6398
+ },
6399
+ ...SIGNAL_POOL_FILTER_PROPS,
6400
+ lmtNum: {
6401
+ type: "string",
6402
+ description: "Trader pool size 1-500"
6403
+ },
6404
+ authorIds: {
6405
+ type: "string",
6406
+ description: "Comma-separated user IDs e.g. 1001,1002"
6407
+ }
6408
+ }
6409
+ },
6410
+ handler: async (rawArgs, context) => {
6411
+ const args = asRecord(rawArgs);
6412
+ const instId = readString(args, "instId");
6413
+ const instCcy = readString(args, "instCcy");
6414
+ if (!instId && !instCcy) {
6415
+ throw new ValidationError('Either "instId" or "instCcy" is required for smartmoney_get_signal.');
6416
+ }
6417
+ const dv = readString(args, "dataVersion");
6418
+ const ts = readString(args, "ts");
6419
+ if (!dv && !ts) {
6420
+ throw new ValidationError('Either "dataVersion" or "ts" is required for smartmoney_get_signal.');
6421
+ }
6422
+ const response = await context.client.privateGet(
6423
+ PATH_SIGNAL,
6424
+ compactObject({
6425
+ instId,
6426
+ instCcy,
6427
+ dataVersion: dv,
6428
+ ts,
6429
+ ...readPoolFilters(args),
6430
+ lmtNum: readString(args, "lmtNum"),
6431
+ authorIds: readString(args, "authorIds")
6432
+ }),
6433
+ publicRateLimit("smartmoney_get_signal", 5)
6434
+ );
6435
+ return normalizeResponse(response);
6436
+ }
6437
+ },
6438
+ /* ---------- 3. Signal History ---------- */
6439
+ {
6440
+ name: "smartmoney_get_signal_history",
6441
+ module: "smartmoney",
6442
+ description: "Signal history timeline sorted by ts DESC for trend analysis. Requires instId. Pass ts=Date.now() for latest data, or dataVersion from a prior call. For current snapshot, use smartmoney_get_signal.",
6443
+ isWrite: false,
6444
+ inputSchema: {
6445
+ type: "object",
6446
+ properties: {
6447
+ instId: {
6448
+ type: "string",
6449
+ description: "e.g. BTC-USDT-SWAP"
6450
+ },
6451
+ dataVersion: {
6452
+ type: "string",
6453
+ description: "yyyyMMddHHmm UTC (or use ts)"
6454
+ },
6455
+ ts: {
6456
+ type: "string",
6457
+ description: "Timestamp ms (or use dataVersion)"
6458
+ },
6459
+ granularity: {
6460
+ type: "string",
6461
+ description: "1h or 1d (default 1h)"
6462
+ },
6463
+ limit: {
6464
+ type: "string",
6465
+ description: "Data points 1-500 (default 24)"
6466
+ },
6467
+ ...SIGNAL_POOL_FILTER_PROPS
6468
+ },
6469
+ required: ["instId"]
6470
+ },
6471
+ handler: async (rawArgs, context) => {
6472
+ const args = asRecord(rawArgs);
6473
+ const dv = readString(args, "dataVersion");
6474
+ const ts = readString(args, "ts");
6475
+ if (!dv && !ts) {
6476
+ throw new ValidationError('Either "dataVersion" or "ts" is required for smartmoney_get_signal_history.');
6477
+ }
6478
+ const response = await context.client.privateGet(
6479
+ PATH_SIGNAL_HISTORY,
6480
+ compactObject({
6481
+ instId: requireString(args, "instId"),
6482
+ dataVersion: dv,
6483
+ ts,
6484
+ granularity: readString(args, "granularity"),
6485
+ limit: readString(args, "limit"),
6486
+ ...readPoolFilters(args)
6487
+ }),
6488
+ publicRateLimit("smartmoney_get_signal_history", 5)
6489
+ );
6490
+ return normalizeResponse(response);
6491
+ }
6492
+ },
6493
+ /* ---------- 4. Traders (list) ---------- */
6494
+ {
6495
+ name: "smartmoney_get_traders",
6496
+ module: "smartmoney",
6497
+ description: "List/filter leaderboard traders. For single trader detail: smartmoney_get_trader_detail.",
6498
+ isWrite: false,
6499
+ inputSchema: {
6500
+ type: "object",
6501
+ properties: {
6502
+ dataVersion: {
6503
+ type: "string",
6504
+ description: "yyyyMMddHHmm, omit=latest"
6505
+ },
6506
+ ...LEADERBOARD_POOL_FILTER_PROPS,
6507
+ authorIds: {
6508
+ type: "string",
6509
+ description: "Comma-separated author IDs"
6510
+ },
6511
+ after: {
6512
+ type: "string",
6513
+ description: "Cursor after this authorId"
6514
+ },
6515
+ before: {
6516
+ type: "string",
6517
+ description: "Cursor before this authorId"
6518
+ },
6519
+ limit: {
6520
+ type: "string",
6521
+ description: "Max results 1-100"
6522
+ }
6523
+ }
6524
+ },
6525
+ handler: async (rawArgs, context) => {
6526
+ const args = asRecord(rawArgs);
6527
+ const response = await context.client.privateGet(
6528
+ PATH_LEADERBOARD,
6529
+ compactObject({
6530
+ dataVersion: readString(args, "dataVersion"),
6531
+ ...readPoolFilters(args),
6532
+ authorIds: readString(args, "authorIds"),
6533
+ after: readString(args, "after"),
6534
+ before: readString(args, "before"),
6535
+ limit: readString(args, "limit")
6536
+ }),
6537
+ publicRateLimit("smartmoney_get_traders", 5)
6538
+ );
6539
+ const normalized = normalizeResponse(response);
6540
+ return { ...normalized, data: extractLeaderboardData(normalized.data) };
6541
+ }
6542
+ },
6543
+ /* ---------- 5. Trader Detail (composite) ---------- */
6544
+ {
6545
+ name: "smartmoney_get_trader_detail",
6546
+ module: "smartmoney",
6547
+ description: "Trader portrait: profile + positions + trades. Requires authorId from smartmoney_get_traders. Do NOT use for listing \u2014 use smartmoney_get_traders.",
6548
+ isWrite: false,
6549
+ inputSchema: {
6550
+ type: "object",
6551
+ properties: {
6552
+ authorId: {
6553
+ type: "string",
6554
+ description: "Trader author ID"
6555
+ },
6556
+ period: {
6557
+ type: "string",
6558
+ description: "3|7|30|90 days, omit=all"
6559
+ },
6560
+ instCcy: {
6561
+ type: "string",
6562
+ description: "Currency filter e.g. BTC"
6563
+ },
6564
+ tradeLimit: {
6565
+ type: "string",
6566
+ description: "Max trades 1-100"
6567
+ }
6568
+ },
6569
+ required: ["authorId"]
6570
+ },
6571
+ handler: async (rawArgs, context) => {
6572
+ const args = asRecord(rawArgs);
6573
+ const authorId = requireString(args, "authorId");
6574
+ const period = readString(args, "period");
6575
+ const instCcy = readString(args, "instCcy");
6576
+ const tradeLimit = readString(args, "tradeLimit");
6577
+ const [profileRes, positionsRes, tradesRes] = await Promise.all([
6578
+ context.client.privateGet(
6579
+ PATH_LEADERBOARD,
6580
+ compactObject({ authorIds: authorId, period }),
6581
+ publicRateLimit("smartmoney_get_traders", 5)
6582
+ ),
6583
+ context.client.privateGet(
6584
+ PATH_POSITION_CURRENT,
6585
+ compactObject({ authorId, instCcy }),
6586
+ publicRateLimit("smartmoney_trader_positions", 5)
6587
+ ),
6588
+ context.client.privateGet(
6589
+ PATH_TRADE_RECORDS,
6590
+ compactObject({ authorId, instCcy, limit: tradeLimit }),
6591
+ publicRateLimit("smartmoney_trade_records", 5)
6592
+ )
6593
+ ]);
6594
+ const profileNorm = normalizeResponse(profileRes);
6595
+ const positionsNorm = normalizeResponse(positionsRes);
6596
+ const tradesNorm = normalizeResponse(tradesRes);
6597
+ return {
6598
+ endpoint: "smartmoney_get_trader_detail (composite)",
6599
+ requestTime: (/* @__PURE__ */ new Date()).toISOString(),
6600
+ data: {
6601
+ profile: extractLeaderboardData(profileNorm.data),
6602
+ positions: positionsNorm.data,
6603
+ trades: tradesNorm.data
6604
+ }
6605
+ };
6606
+ }
6607
+ }
6608
+ ];
6609
+ return tools;
6610
+ }
6087
6611
  function buildContractTradeTools(cfg) {
6088
6612
  const { prefix, module, label, instTypes, instIdExample } = cfg;
6089
6613
  const [defaultType, otherType] = instTypes;
@@ -6425,31 +6949,58 @@ function buildContractTradeTools(cfg) {
6425
6949
  {
6426
6950
  name: n("set_leverage"),
6427
6951
  module,
6428
- description: `Set leverage for a ${label} instrument or position. [CAUTION] Changes risk parameters.`,
6952
+ description: `Set leverage for a ${label} instrument or position. [CAUTION] Changes risk parameters.
6953
+ Scenarios (SWAP/FUTURES only):
6954
+ \u2022 cross + any instId under the index \u2192 sets leverage at the index level
6955
+ \u2022 isolated + buy-sell (net) posMode \u2192 instId only
6956
+ \u2022 isolated + long-short (hedge) posMode \u2192 instId + posSide=long|short (BOTH directions must be set separately)
6957
+ Not supported: PORTFOLIO MARGIN accounts cannot adjust cross leverage for SWAP/FUTURES \u2014 the request will be rejected by OKX. Use account_get_config first if unsure of the account's margin mode.`,
6429
6958
  isWrite: true,
6430
6959
  inputSchema: {
6431
6960
  type: "object",
6432
6961
  properties: {
6433
6962
  instId: { type: "string", description: instIdExample },
6434
- lever: { type: "string", description: "Leverage, e.g. '10'" },
6963
+ lever: {
6964
+ type: "string",
6965
+ description: "Leverage multiplier as a positive number string, e.g. '10'. Max value depends on the instrument (query market_get_instruments \u2192 lever)."
6966
+ },
6435
6967
  mgnMode: { type: "string", enum: ["cross", "isolated"] },
6436
6968
  posSide: {
6437
6969
  type: "string",
6438
- enum: ["long", "short", "net"],
6439
- description: "Required for isolated margin in hedge mode"
6970
+ enum: ["long", "short"],
6971
+ description: "REQUIRED when mgnMode=isolated AND the account is in hedge (long/short) position mode. Use 'long' or 'short' \u2014 setting one side does NOT auto-apply to the other. Omit entirely for one-way (net) position mode or for cross margin."
6440
6972
  }
6441
6973
  },
6442
6974
  required: ["instId", "lever", "mgnMode"]
6443
6975
  },
6444
6976
  handler: async (rawArgs, context) => {
6445
6977
  const args = asRecord(rawArgs);
6978
+ const instId = requireString(args, "instId");
6979
+ const leverRaw = requireString(args, "lever");
6980
+ const leverNum = Number(leverRaw);
6981
+ if (!Number.isFinite(leverNum) || leverNum <= 0) {
6982
+ throw new ValidationError(
6983
+ `Parameter "lever" must be a positive number string, got "${leverRaw}".`
6984
+ );
6985
+ }
6986
+ const mgnMode = requireString(args, "mgnMode");
6987
+ assertEnum(mgnMode, "mgnMode", ["cross", "isolated"]);
6988
+ const posSide = readString(args, "posSide");
6989
+ if (posSide !== void 0) {
6990
+ assertEnum(posSide, "posSide", ["long", "short"]);
6991
+ if (mgnMode === "cross") {
6992
+ throw new ValidationError(
6993
+ `posSide="${posSide}" is only valid with mgnMode="isolated" in hedge mode. Omit posSide for cross margin.`
6994
+ );
6995
+ }
6996
+ }
6446
6997
  const response = await context.client.privatePost(
6447
6998
  "/api/v5/account/set-leverage",
6448
6999
  compactObject({
6449
- instId: requireString(args, "instId"),
6450
- lever: requireString(args, "lever"),
6451
- mgnMode: requireString(args, "mgnMode"),
6452
- posSide: readString(args, "posSide")
7000
+ instId,
7001
+ lever: leverRaw,
7002
+ mgnMode,
7003
+ posSide
6453
7004
  }),
6454
7005
  privateRateLimit(n("set_leverage"), 20)
6455
7006
  );
@@ -7322,7 +7873,7 @@ function registerMarketFilterTools() {
7322
7873
  sortBy: {
7323
7874
  type: "string",
7324
7875
  enum: ["last", "chg24hPct", "marketCapUsd", "volUsd24h", "fundingRate", "oiUsd", "listTime"],
7325
- description: "Sort field. Default: volUsd24h. Note: marketCapUsd is only meaningful for SPOT (null for SWAP/FUTURES)."
7876
+ description: "Sort field. Default: volUsd24h. Note: marketCapUsd is only meaningful for SPOT (null for SWAP/FUTURES). To rank by OI *change* (oiDeltaPct / absOiDeltaPct), use market_filter_oi_change \u2014 market_filter only sorts by the current snapshot."
7326
7877
  },
7327
7878
  sortOrder: {
7328
7879
  type: "string",
@@ -7435,7 +7986,7 @@ function registerMarketFilterTools() {
7435
7986
  bar: {
7436
7987
  type: "string",
7437
7988
  enum: [...OI_BARS],
7438
- description: "Bar window for OI change computation: 5m, 15m, 1H, 4H, 1D. Default: 1H"
7989
+ description: "Bar window for OI change computation: 5m, 15m, 1H, 4H, 1D (case-insensitive on server, but send canonical form here). Default: 1H"
7439
7990
  },
7440
7991
  // Filters
7441
7992
  minOiUsd: {
@@ -7453,8 +8004,8 @@ function registerMarketFilterTools() {
7453
8004
  // Sort / pagination
7454
8005
  sortBy: {
7455
8006
  type: "string",
7456
- enum: ["oiUsd", "oiDeltaUsd", "oiDeltaPct", "volUsd24h", "last"],
7457
- description: "Sort field. Default: oiDeltaPct (largest movers first)"
8007
+ enum: ["oiUsd", "oiDeltaUsd", "oiDeltaPct", "absOiDeltaPct", "volUsd24h", "fundingRate", "last"],
8008
+ description: "Sort field. Default: oiDeltaPct (largest movers first, signed \u2014 longs and shorts separate). Use absOiDeltaPct to sort by |oiDeltaPct| (largest-magnitude moves regardless of direction). fundingRate is also supported for SWAP. Do NOT use the market_filter tool's sort fields (chg24hPct, marketCapUsd, listTime) here \u2014 they are not in the OI-change Row."
7458
8009
  },
7459
8010
  sortOrder: {
7460
8011
  type: "string",
@@ -7510,7 +8061,7 @@ var D_COINS_SENTIMENT = 'Comma-separated uppercase ticker symbols, max 20 (e.g.
7510
8061
  var D_LANGUAGE = "Content language: zh-CN or en-US. Infer from user's message. No server default.";
7511
8062
  var D_BEGIN = "Start time, Unix epoch milliseconds. API defaults to 72 hours ago when omitted. Pass explicitly for older topics (e.g. 'last 30 days'). Max range: 180 days. Parse relative time if given.";
7512
8063
  var D_END = "End time, Unix epoch milliseconds. Parse relative time if given. Omit for no upper bound.";
7513
- var D_IMPORTANCE = "Importance filter: high (server default) or low. Omit unless user wants broader coverage.";
8064
+ var D_IMPORTANCE = "Importance filter: 'low' returns all news (both low and high importance); 'high' narrows to major/breaking news only. Omitted \u2192 server default (high-only). Default to 'low' for broad browsing; pass 'high' only when the user explicitly asks for major news.";
7514
8065
  var D_PLATFORM = "Filter by news source. Use values from news_get_domains (e.g. blockbeats, odaily_flash). Omit for all sources.";
7515
8066
  var D_LIMIT = "Number of results (default 10, max 50).";
7516
8067
  function registerNewsTools() {
@@ -7521,7 +8072,7 @@ function registerNewsTools() {
7521
8072
  {
7522
8073
  name: "news_get_latest",
7523
8074
  module: "news",
7524
- description: "Get crypto news sorted by time. Omitting importance still returns only high-importance news (server default). Pass importance='low' explicitly to broaden results. Use when user asks 'what happened recently', 'latest news', 'any big news today', or wants to browse without a keyword. For coin-specific news, use news_get_by_coin instead.",
8075
+ description: "Get crypto news sorted by time. For broad browsing ('what happened recently', 'latest news', 'any big news today'), pass importance='low' to include both high and low importance. Server default (when importance omitted) returns only high-importance news. For coin-specific news, use news_get_by_coin instead.",
7525
8076
  isWrite: false,
7526
8077
  inputSchema: {
7527
8078
  type: "object",
@@ -9182,6 +9733,82 @@ function registerSpotTradeTools() {
9182
9733
  );
9183
9734
  return normalizeResponse(response);
9184
9735
  }
9736
+ },
9737
+ // ── set_leverage (SPOT margin: instId-level isolated OR ccy-level cross) ──
9738
+ // Covers OKX scenarios 1–5 (everything except SWAP/FUTURES, which are in
9739
+ // contract-trade.ts). Callers supply exactly one of {instId, ccy}:
9740
+ // • instId + isolated → scenario 1 (pair-level margin)
9741
+ // • instId + cross → scenario 3 (contract-mode pair-level cross margin)
9742
+ // • ccy + cross → scenarios 2 / 4 / 5 (spot/multi-ccy/PM currency-level cross)
9743
+ // Not applicable: posSide (spot has no long/short hedge).
9744
+ {
9745
+ name: "spot_set_leverage",
9746
+ module: "spot",
9747
+ description: "Set leverage for SPOT margin trading. Provide exactly ONE of instId (pair-level) or ccy (currency-level cross, requires borrow-enabled account / multi-ccy / portfolio margin). [CAUTION] Changes risk parameters.\nScenarios:\n \u2022 instId + mgnMode=isolated \u2192 pair-level isolated margin\n \u2022 instId + mgnMode=cross \u2192 pair-level cross margin (contract-mode account)\n \u2022 ccy + mgnMode=cross \u2192 currency-level cross margin (spot-with-borrow / multi-ccy / portfolio margin)\nWhen ccy is supplied, mgnMode MUST be cross. posSide is never applicable to spot margin.",
9748
+ isWrite: true,
9749
+ inputSchema: {
9750
+ type: "object",
9751
+ properties: {
9752
+ instId: {
9753
+ type: "string",
9754
+ description: "Spot pair, e.g. BTC-USDT. Provide instId OR ccy, not both."
9755
+ },
9756
+ ccy: {
9757
+ type: "string",
9758
+ description: "Margin currency, e.g. BTC. Required only for currency-level cross margin (borrow-enabled / multi-ccy / portfolio margin). Mutually exclusive with instId."
9759
+ },
9760
+ lever: {
9761
+ type: "string",
9762
+ description: "Leverage multiplier as a positive number string, e.g. '3'. Max depends on the pair (query market_get_instruments \u2192 lever) or the account policy for ccy-level."
9763
+ },
9764
+ mgnMode: {
9765
+ type: "string",
9766
+ enum: ["cross", "isolated"],
9767
+ description: "cross or isolated. Must be cross when ccy is supplied."
9768
+ }
9769
+ },
9770
+ required: ["lever", "mgnMode"]
9771
+ },
9772
+ handler: async (rawArgs, context) => {
9773
+ const args = asRecord(rawArgs);
9774
+ const instId = readString(args, "instId");
9775
+ const ccy = readString(args, "ccy");
9776
+ if (!instId && !ccy) {
9777
+ throw new ValidationError(
9778
+ `Missing required parameter: provide either "instId" (pair-level) or "ccy" (currency-level cross margin).`
9779
+ );
9780
+ }
9781
+ if (instId && ccy) {
9782
+ throw new ValidationError(
9783
+ `Parameters "instId" and "ccy" are mutually exclusive \u2014 provide only one. instId sets pair-level leverage; ccy sets currency-level cross margin leverage.`
9784
+ );
9785
+ }
9786
+ const leverRaw = requireString(args, "lever");
9787
+ const leverNum = Number(leverRaw);
9788
+ if (!Number.isFinite(leverNum) || leverNum <= 0) {
9789
+ throw new ValidationError(
9790
+ `Parameter "lever" must be a positive number string, got "${leverRaw}".`
9791
+ );
9792
+ }
9793
+ const mgnMode = requireString(args, "mgnMode");
9794
+ assertEnum(mgnMode, "mgnMode", ["cross", "isolated"]);
9795
+ if (ccy && mgnMode !== "cross") {
9796
+ throw new ValidationError(
9797
+ `When "ccy" is supplied, "mgnMode" must be "cross" (currency-level leverage only applies to cross margin).`
9798
+ );
9799
+ }
9800
+ const response = await context.client.privatePost(
9801
+ "/api/v5/account/set-leverage",
9802
+ compactObject({
9803
+ instId,
9804
+ ccy,
9805
+ lever: leverRaw,
9806
+ mgnMode
9807
+ }),
9808
+ privateRateLimit("spot_set_leverage", 20)
9809
+ );
9810
+ return normalizeResponse(response);
9811
+ }
9185
9812
  }
9186
9813
  ];
9187
9814
  }
@@ -9313,6 +9940,7 @@ function allToolSpecs() {
9313
9940
  ...registerNewsTools(),
9314
9941
  ...registerBotTools(),
9315
9942
  ...registerAllEarnTools(),
9943
+ ...registerSmartmoneyTools(),
9316
9944
  ...registerAuditTools(),
9317
9945
  ...registerSkillsTools()
9318
9946
  ];
@@ -9757,7 +10385,7 @@ var _require = createRequire(import.meta.url);
9757
10385
  var pkg = _require("../package.json");
9758
10386
  var SERVER_NAME = "okx-trade-mcp";
9759
10387
  var SERVER_VERSION = pkg.version;
9760
- var GIT_HASH = true ? "e9764c9" : "dev";
10388
+ var GIT_HASH = true ? "e0ee5a96" : "dev";
9761
10389
 
9762
10390
  // src/server.ts
9763
10391
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -9927,11 +10555,14 @@ Usage: okx-trade-mcp [options]
9927
10555
 
9928
10556
  Options:
9929
10557
  --modules <list> Comma-separated list of modules to load
9930
- Available: market, spot, swap, futures, option, account, news
10558
+ Available: market, spot, swap, futures, option, account,
10559
+ event, news, smartmoney, skills,
10560
+ earn.savings, earn.onchain, earn.dcd, earn.autoearn, earn.flash,
9931
10561
  bot.grid, bot.dca
9932
- Alias: "bot" = all bot sub-modules (bot.grid + bot.dca)
10562
+ Alias: "bot" = bot.grid + bot.dca
10563
+ "earn" / "earn.all" = all earn sub-modules
9933
10564
  Special: "all" loads all modules
9934
- Default: spot,swap,option,account,bot.grid
10565
+ Default: spot,swap,option,account,bot.grid,skills
9935
10566
 
9936
10567
  --profile <name> Profile to load from ${configFilePath()}
9937
10568
  Falls back to default_profile in config, then "default"