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

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
@@ -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) {
@@ -1583,7 +1584,7 @@ var OkxRestClient = class _OkxRestClient {
1583
1584
  vlog2(`Network failure, refreshing DoH: ${cause}`);
1584
1585
  }
1585
1586
  const shouldRetry = await this.doh.handleNetworkFailure();
1586
- if (shouldRetry && reqConfig.method === "GET") {
1587
+ if (shouldRetry && (reqConfig.method === "GET" || reqConfig.retryOnNetworkError)) {
1587
1588
  return this.request(reqConfig);
1588
1589
  }
1589
1590
  }
@@ -2059,6 +2060,7 @@ var MODULES = [
2059
2060
  "account",
2060
2061
  "event",
2061
2062
  "news",
2063
+ "smartmoney",
2062
2064
  ...EARN_SUB_MODULE_IDS,
2063
2065
  ...BOT_SUB_MODULE_IDS,
2064
2066
  "skills"
@@ -3904,10 +3906,133 @@ function registerGridTools() {
3904
3906
  return normalizeWrite(response);
3905
3907
  }
3906
3908
  },
3909
+ {
3910
+ name: "grid_amend_order",
3911
+ module: "bot.grid",
3912
+ 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.",
3913
+ isWrite: true,
3914
+ inputSchema: {
3915
+ type: "object",
3916
+ properties: {
3917
+ algoId: {
3918
+ type: "string",
3919
+ description: "Grid bot algo order ID (required)"
3920
+ },
3921
+ // ── Price-range mode ──────────────────────────────────────────────
3922
+ maxPx: {
3923
+ type: "string",
3924
+ description: "[Price-range mode] New upper price boundary. Triggers amend-algo-basic-param when provided."
3925
+ },
3926
+ minPx: {
3927
+ type: "string",
3928
+ description: "[Price-range mode] New lower price boundary. Required when maxPx is set."
3929
+ },
3930
+ gridNum: {
3931
+ type: "string",
3932
+ description: "[Price-range mode] New number of grid intervals (integer). Required when maxPx is set."
3933
+ },
3934
+ // ── TP/SL mode ────────────────────────────────────────────────────
3935
+ instId: {
3936
+ type: "string",
3937
+ description: "[TP/SL mode] Instrument ID, e.g. BTC-USDT. Required when setting TP/SL."
3938
+ },
3939
+ tpTriggerPx: {
3940
+ type: "string",
3941
+ description: "[TP/SL mode] Take-profit trigger price (absolute). Pass '-1' to clear."
3942
+ },
3943
+ slTriggerPx: {
3944
+ type: "string",
3945
+ description: "[TP/SL mode] Stop-loss trigger price (absolute). Pass '-1' to clear."
3946
+ },
3947
+ tpRatio: {
3948
+ type: "string",
3949
+ description: "[TP/SL mode] Take-profit ratio, e.g. '0.1' = 10% profit. Pass '-1' to clear."
3950
+ },
3951
+ slRatio: {
3952
+ type: "string",
3953
+ description: "[TP/SL mode] Stop-loss ratio, e.g. '0.1' = 10% drawdown. Pass '-1' to clear."
3954
+ },
3955
+ // ── Shared optional ───────────────────────────────────────────────
3956
+ topUpAmt: {
3957
+ type: "string",
3958
+ 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."
3959
+ }
3960
+ },
3961
+ required: ["algoId"]
3962
+ },
3963
+ handler: async (rawArgs, context) => {
3964
+ const args = asRecord(rawArgs);
3965
+ const algoId = requireString(args, "algoId");
3966
+ const maxPx = readString(args, "maxPx");
3967
+ const instId = readString(args, "instId");
3968
+ const hasTpsl = readString(args, "tpTriggerPx") || readString(args, "slTriggerPx") || readString(args, "tpRatio") || readString(args, "slRatio");
3969
+ if (!maxPx && !hasTpsl) {
3970
+ throw new OkxApiError(
3971
+ "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).",
3972
+ { code: "", endpoint: "grid_amend_order" }
3973
+ );
3974
+ }
3975
+ if (hasTpsl && !instId) {
3976
+ throw new OkxApiError(
3977
+ "TP/SL mode requires instId. Provide instId alongside the TP/SL parameters.",
3978
+ { code: "", endpoint: "grid_amend_order" }
3979
+ );
3980
+ }
3981
+ const results = [];
3982
+ if (maxPx) {
3983
+ results.push(normalizeWrite(await context.client.privatePost(
3984
+ "/api/v5/tradingBot/grid/amend-algo-basic-param",
3985
+ compactObject({
3986
+ algoId,
3987
+ maxPx,
3988
+ minPx: requireString(args, "minPx"),
3989
+ gridNum: requireString(args, "gridNum"),
3990
+ // API field is "topupAmount" (lowercase u) — different from TP/SL mode's "topUpAmt"
3991
+ // Contract grid only; omitting lets the API use the minimum required
3992
+ topupAmount: readString(args, "topUpAmt")
3993
+ }),
3994
+ privateRateLimit("grid_amend_order", 20),
3995
+ true
3996
+ // retryOnNetworkError: amend sets fixed values, safe to retry
3997
+ )));
3998
+ }
3999
+ if (hasTpsl) {
4000
+ try {
4001
+ results.push(normalizeWrite(await context.client.privatePost(
4002
+ "/api/v5/tradingBot/grid/amend-order-algo",
4003
+ compactObject({
4004
+ algoId,
4005
+ instId,
4006
+ tpTriggerPx: readString(args, "tpTriggerPx"),
4007
+ slTriggerPx: readString(args, "slTriggerPx"),
4008
+ tpRatio: readString(args, "tpRatio"),
4009
+ slRatio: readString(args, "slRatio"),
4010
+ topUpAmt: readString(args, "topUpAmt")
4011
+ // API field is "topUpAmt" (uppercase U) — different from price-range mode's "topupAmount"
4012
+ }),
4013
+ privateRateLimit("grid_amend_order", 20),
4014
+ true
4015
+ // retryOnNetworkError: amend sets fixed values, safe to retry
4016
+ )));
4017
+ } catch (err) {
4018
+ if (results.length > 0) {
4019
+ const msg = err instanceof Error ? err.message : String(err);
4020
+ throw new OkxApiError(
4021
+ `TP/SL amend failed (price-range amend already succeeded): ${msg}`,
4022
+ { code: "", endpoint: "grid_amend_order" }
4023
+ );
4024
+ }
4025
+ throw err;
4026
+ }
4027
+ }
4028
+ const merged = results.flatMap((r) => Array.isArray(r.data) ? r.data : [r.data]);
4029
+ return { endpoint: results[0].endpoint, requestTime: results[0].requestTime, data: merged };
4030
+ }
4031
+ },
3907
4032
  {
3908
4033
  name: "grid_stop_order",
3909
4034
  module: "bot.grid",
3910
- description: "Stop a grid bot. [CAUTION] Closes or cancels orders. For contract: stopType controls close ('1') vs cancel-only ('2').",
4035
+ 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
4036
  isWrite: true,
3912
4037
  inputSchema: {
3913
4038
  type: "object",
@@ -3916,13 +4041,13 @@ function registerGridTools() {
3916
4041
  algoOrdType: {
3917
4042
  type: "string",
3918
4043
  enum: ["grid", "contract_grid"],
3919
- description: "grid=Spot, contract_grid=Contract"
4044
+ description: "grid=Spot grid, contract_grid=Contract grid"
3920
4045
  },
3921
- instId: { type: "string", description: "e.g. BTC-USDT, BTC-USD-SWAP" },
4046
+ instId: { type: "string", description: "Instrument ID, e.g. BTC-USDT, BTC-USDT-SWAP" },
3922
4047
  stopType: {
3923
4048
  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"
4049
+ enum: ["1", "2"],
4050
+ 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
4051
  }
3927
4052
  },
3928
4053
  required: ["algoId", "algoOrdType", "instId"]
@@ -3937,7 +4062,9 @@ function registerGridTools() {
3937
4062
  instId: requireString(args, "instId"),
3938
4063
  stopType: readString(args, "stopType") ?? "1"
3939
4064
  })],
3940
- privateRateLimit("grid_stop_order", 20)
4065
+ privateRateLimit("grid_stop_order", 20),
4066
+ true
4067
+ // retryOnNetworkError: safe to retry — already-stopped returns an error but does not harm state
3941
4068
  );
3942
4069
  return normalizeWrite(response);
3943
4070
  }
@@ -6084,6 +6211,401 @@ function registerEventContractTools() {
6084
6211
  }
6085
6212
  ];
6086
6213
  }
6214
+ var PATH_LEADERBOARD = "/api/v5/orbit/public/leaderboard";
6215
+ var PATH_POSITION_CURRENT = "/api/v5/orbit/public/position-current";
6216
+ var PATH_TRADE_RECORDS = "/api/v5/orbit/public/trade-records";
6217
+ var PATH_OVERVIEW = "/api/v5/journal/public/smartmoney/overview";
6218
+ var PATH_SIGNAL = "/api/v5/journal/public/smartmoney/signal";
6219
+ var PATH_SIGNAL_HISTORY = "/api/v5/journal/public/smartmoney/signal-history";
6220
+ var SIGNAL_POOL_FILTER_PROPS = {
6221
+ sortType: {
6222
+ type: "string",
6223
+ description: "pnl or pnlRatio"
6224
+ },
6225
+ period: {
6226
+ type: "string",
6227
+ description: "3|7|30|90 days"
6228
+ },
6229
+ pnl: {
6230
+ type: "string",
6231
+ description: "PNL_ANY|PNL_TOP50|PNL_TOP20|PNL_TOP5"
6232
+ },
6233
+ winRatio: {
6234
+ type: "string",
6235
+ description: "WR_ANY|WR_GE_50|WR_GE_80"
6236
+ },
6237
+ maxRetreat: {
6238
+ type: "string",
6239
+ description: "MR_ANY|MR_LE_20|MR_LE_50"
6240
+ },
6241
+ asset: {
6242
+ type: "string",
6243
+ description: "AUM_ANY|AUM_TOP50|AUM_TOP20|AUM_TOP5"
6244
+ }
6245
+ };
6246
+ var LEADERBOARD_POOL_FILTER_PROPS = {
6247
+ sortType: {
6248
+ type: "string",
6249
+ description: "pnl or pnl_ratio"
6250
+ },
6251
+ period: {
6252
+ type: "string",
6253
+ description: "3|7|30|90 days, empty=all"
6254
+ },
6255
+ pnl: {
6256
+ type: "string",
6257
+ description: "Min PnL USD"
6258
+ },
6259
+ winRatio: {
6260
+ type: "string",
6261
+ description: "Min ratio (0.8=80%)"
6262
+ },
6263
+ maxRetreat: {
6264
+ type: "string",
6265
+ description: "Max DD (0.1=10%)"
6266
+ },
6267
+ asset: {
6268
+ type: "string",
6269
+ description: "Min AUM USD"
6270
+ }
6271
+ };
6272
+ var POOL_FILTER_KEYS = ["sortType", "period", "pnl", "winRatio", "maxRetreat", "asset"];
6273
+ function readPoolFilters(args) {
6274
+ const result = {};
6275
+ for (const key of POOL_FILTER_KEYS) {
6276
+ const val = readString(args, key);
6277
+ if (val) result[key] = val;
6278
+ }
6279
+ return result;
6280
+ }
6281
+ function extractLeaderboardData(data) {
6282
+ if (Array.isArray(data)) return data;
6283
+ if (data && typeof data === "object") {
6284
+ const inner = data.data;
6285
+ if (Array.isArray(inner)) return inner;
6286
+ }
6287
+ return [];
6288
+ }
6289
+ var SMARTMONEY_DEMO_MESSAGE = "Smart Money features are not available in demo/simulated trading mode.";
6290
+ var SMARTMONEY_DEMO_SUGGESTION = "Switch to a live profile to use Smart Money features.";
6291
+ function withSmartmoneyDemoGuard(tool) {
6292
+ const originalHandler = tool.handler;
6293
+ return {
6294
+ ...tool,
6295
+ handler: async (args, context) => {
6296
+ if (context.config.demo) {
6297
+ throw new ConfigError(
6298
+ SMARTMONEY_DEMO_MESSAGE,
6299
+ SMARTMONEY_DEMO_SUGGESTION
6300
+ );
6301
+ }
6302
+ return originalHandler(args, context);
6303
+ }
6304
+ };
6305
+ }
6306
+ function registerSmartmoneyTools() {
6307
+ const tools = [
6308
+ /* ---------- 1. Overview ---------- */
6309
+ {
6310
+ name: "smartmoney_get_overview",
6311
+ module: "smartmoney",
6312
+ 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.",
6313
+ isWrite: false,
6314
+ inputSchema: {
6315
+ type: "object",
6316
+ properties: {
6317
+ dataVersion: {
6318
+ type: "string",
6319
+ description: "yyyyMMddHHmm UTC (or use ts)"
6320
+ },
6321
+ ts: {
6322
+ type: "string",
6323
+ description: "Timestamp ms (or use dataVersion)"
6324
+ },
6325
+ instType: {
6326
+ type: "string",
6327
+ description: "SPOT|MARGIN|FUTURES|SWAP|OPTION"
6328
+ },
6329
+ ...SIGNAL_POOL_FILTER_PROPS,
6330
+ lmtNum: {
6331
+ type: "string",
6332
+ description: "Trader pool size 1-500"
6333
+ },
6334
+ instCcyList: {
6335
+ type: "string",
6336
+ description: "Comma-separated e.g. BTC,ETH,SOL"
6337
+ },
6338
+ instCcy: {
6339
+ type: "string",
6340
+ description: "Single currency e.g. BTC"
6341
+ },
6342
+ topInstruments: {
6343
+ type: "string",
6344
+ description: "Top N instruments 1-100"
6345
+ }
6346
+ }
6347
+ },
6348
+ handler: async (rawArgs, context) => {
6349
+ const args = asRecord(rawArgs);
6350
+ const dv = readString(args, "dataVersion");
6351
+ const ts = readString(args, "ts");
6352
+ if (!dv && !ts) {
6353
+ throw new ValidationError('Either "dataVersion" or "ts" is required for smartmoney_get_overview.');
6354
+ }
6355
+ const response = await context.client.privateGet(
6356
+ PATH_OVERVIEW,
6357
+ compactObject({
6358
+ dataVersion: dv,
6359
+ ts,
6360
+ instType: readString(args, "instType"),
6361
+ ...readPoolFilters(args),
6362
+ lmtNum: readString(args, "lmtNum"),
6363
+ instCcyList: readString(args, "instCcyList"),
6364
+ instCcy: readString(args, "instCcy"),
6365
+ topInstruments: readString(args, "topInstruments")
6366
+ }),
6367
+ publicRateLimit("smartmoney_get_overview", 5)
6368
+ );
6369
+ return normalizeResponse(response);
6370
+ }
6371
+ },
6372
+ /* ---------- 2. Signal ---------- */
6373
+ {
6374
+ name: "smartmoney_get_signal",
6375
+ module: "smartmoney",
6376
+ description: "Single-currency consensus signal: long/short ratio, entry prices, trend, capital flow. Requires instId or instCcy. 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.",
6377
+ isWrite: false,
6378
+ inputSchema: {
6379
+ type: "object",
6380
+ properties: {
6381
+ instId: {
6382
+ type: "string",
6383
+ description: "e.g. BTC-USDT-SWAP (or use instCcy)"
6384
+ },
6385
+ instCcy: {
6386
+ type: "string",
6387
+ description: "e.g. BTC, SPOT/SWAP only (or use instId)"
6388
+ },
6389
+ dataVersion: {
6390
+ type: "string",
6391
+ description: "yyyyMMddHHmm UTC (or use ts)"
6392
+ },
6393
+ ts: {
6394
+ type: "string",
6395
+ description: "Timestamp ms (or use dataVersion)"
6396
+ },
6397
+ ...SIGNAL_POOL_FILTER_PROPS,
6398
+ lmtNum: {
6399
+ type: "string",
6400
+ description: "Trader pool size 1-500"
6401
+ },
6402
+ authorIds: {
6403
+ type: "string",
6404
+ description: "Comma-separated user IDs e.g. 1001,1002"
6405
+ }
6406
+ }
6407
+ },
6408
+ handler: async (rawArgs, context) => {
6409
+ const args = asRecord(rawArgs);
6410
+ const instId = readString(args, "instId");
6411
+ const instCcy = readString(args, "instCcy");
6412
+ if (!instId && !instCcy) {
6413
+ throw new ValidationError('Either "instId" or "instCcy" is required for smartmoney_get_signal.');
6414
+ }
6415
+ const dv = readString(args, "dataVersion");
6416
+ const ts = readString(args, "ts");
6417
+ if (!dv && !ts) {
6418
+ throw new ValidationError('Either "dataVersion" or "ts" is required for smartmoney_get_signal.');
6419
+ }
6420
+ const response = await context.client.privateGet(
6421
+ PATH_SIGNAL,
6422
+ compactObject({
6423
+ instId,
6424
+ instCcy,
6425
+ dataVersion: dv,
6426
+ ts,
6427
+ ...readPoolFilters(args),
6428
+ lmtNum: readString(args, "lmtNum"),
6429
+ authorIds: readString(args, "authorIds")
6430
+ }),
6431
+ publicRateLimit("smartmoney_get_signal", 5)
6432
+ );
6433
+ return normalizeResponse(response);
6434
+ }
6435
+ },
6436
+ /* ---------- 3. Signal History ---------- */
6437
+ {
6438
+ name: "smartmoney_get_signal_history",
6439
+ module: "smartmoney",
6440
+ 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.",
6441
+ isWrite: false,
6442
+ inputSchema: {
6443
+ type: "object",
6444
+ properties: {
6445
+ instId: {
6446
+ type: "string",
6447
+ description: "e.g. BTC-USDT-SWAP"
6448
+ },
6449
+ dataVersion: {
6450
+ type: "string",
6451
+ description: "yyyyMMddHHmm UTC (or use ts)"
6452
+ },
6453
+ ts: {
6454
+ type: "string",
6455
+ description: "Timestamp ms (or use dataVersion)"
6456
+ },
6457
+ granularity: {
6458
+ type: "string",
6459
+ description: "1h or 1d"
6460
+ },
6461
+ limit: {
6462
+ type: "string",
6463
+ description: "Data points 1-500"
6464
+ },
6465
+ ...SIGNAL_POOL_FILTER_PROPS
6466
+ },
6467
+ required: ["instId"]
6468
+ },
6469
+ handler: async (rawArgs, context) => {
6470
+ const args = asRecord(rawArgs);
6471
+ const dv = readString(args, "dataVersion");
6472
+ const ts = readString(args, "ts");
6473
+ if (!dv && !ts) {
6474
+ throw new ValidationError('Either "dataVersion" or "ts" is required for smartmoney_get_signal_history.');
6475
+ }
6476
+ const response = await context.client.privateGet(
6477
+ PATH_SIGNAL_HISTORY,
6478
+ compactObject({
6479
+ instId: requireString(args, "instId"),
6480
+ dataVersion: dv,
6481
+ ts,
6482
+ granularity: readString(args, "granularity"),
6483
+ limit: readString(args, "limit"),
6484
+ ...readPoolFilters(args)
6485
+ }),
6486
+ publicRateLimit("smartmoney_get_signal_history", 5)
6487
+ );
6488
+ return normalizeResponse(response);
6489
+ }
6490
+ },
6491
+ /* ---------- 4. Traders (list) ---------- */
6492
+ {
6493
+ name: "smartmoney_get_traders",
6494
+ module: "smartmoney",
6495
+ description: "List/filter leaderboard traders. For single trader detail: smartmoney_get_trader_detail.",
6496
+ isWrite: false,
6497
+ inputSchema: {
6498
+ type: "object",
6499
+ properties: {
6500
+ dataVersion: {
6501
+ type: "string",
6502
+ description: "yyyyMMddHHmm, omit=latest"
6503
+ },
6504
+ ...LEADERBOARD_POOL_FILTER_PROPS,
6505
+ authorIds: {
6506
+ type: "string",
6507
+ description: "Comma-separated author IDs"
6508
+ },
6509
+ after: {
6510
+ type: "string",
6511
+ description: "Cursor after this authorId"
6512
+ },
6513
+ before: {
6514
+ type: "string",
6515
+ description: "Cursor before this authorId"
6516
+ },
6517
+ limit: {
6518
+ type: "string",
6519
+ description: "Max results 1-100"
6520
+ }
6521
+ }
6522
+ },
6523
+ handler: async (rawArgs, context) => {
6524
+ const args = asRecord(rawArgs);
6525
+ const response = await context.client.privateGet(
6526
+ PATH_LEADERBOARD,
6527
+ compactObject({
6528
+ dataVersion: readString(args, "dataVersion"),
6529
+ ...readPoolFilters(args),
6530
+ authorIds: readString(args, "authorIds"),
6531
+ after: readString(args, "after"),
6532
+ before: readString(args, "before"),
6533
+ limit: readString(args, "limit")
6534
+ }),
6535
+ publicRateLimit("smartmoney_get_traders", 5)
6536
+ );
6537
+ const normalized = normalizeResponse(response);
6538
+ return { ...normalized, data: extractLeaderboardData(normalized.data) };
6539
+ }
6540
+ },
6541
+ /* ---------- 5. Trader Detail (composite) ---------- */
6542
+ {
6543
+ name: "smartmoney_get_trader_detail",
6544
+ module: "smartmoney",
6545
+ description: "Trader portrait: profile + positions + trades. Requires authorId from smartmoney_get_traders. Do NOT use for listing \u2014 use smartmoney_get_traders.",
6546
+ isWrite: false,
6547
+ inputSchema: {
6548
+ type: "object",
6549
+ properties: {
6550
+ authorId: {
6551
+ type: "string",
6552
+ description: "Trader author ID"
6553
+ },
6554
+ period: {
6555
+ type: "string",
6556
+ description: "3|7|30|90 days, omit=all"
6557
+ },
6558
+ instCcy: {
6559
+ type: "string",
6560
+ description: "Currency filter e.g. BTC"
6561
+ },
6562
+ tradeLimit: {
6563
+ type: "string",
6564
+ description: "Max trades 1-100"
6565
+ }
6566
+ },
6567
+ required: ["authorId"]
6568
+ },
6569
+ handler: async (rawArgs, context) => {
6570
+ const args = asRecord(rawArgs);
6571
+ const authorId = requireString(args, "authorId");
6572
+ const period = readString(args, "period");
6573
+ const instCcy = readString(args, "instCcy");
6574
+ const tradeLimit = readString(args, "tradeLimit");
6575
+ const [profileRes, positionsRes, tradesRes] = await Promise.all([
6576
+ context.client.privateGet(
6577
+ PATH_LEADERBOARD,
6578
+ compactObject({ authorIds: authorId, period }),
6579
+ publicRateLimit("smartmoney_get_traders", 5)
6580
+ ),
6581
+ context.client.privateGet(
6582
+ PATH_POSITION_CURRENT,
6583
+ compactObject({ authorId, instCcy }),
6584
+ publicRateLimit("smartmoney_trader_positions", 5)
6585
+ ),
6586
+ context.client.privateGet(
6587
+ PATH_TRADE_RECORDS,
6588
+ compactObject({ authorId, instCcy, limit: tradeLimit }),
6589
+ publicRateLimit("smartmoney_trade_records", 5)
6590
+ )
6591
+ ]);
6592
+ const profileNorm = normalizeResponse(profileRes);
6593
+ const positionsNorm = normalizeResponse(positionsRes);
6594
+ const tradesNorm = normalizeResponse(tradesRes);
6595
+ return {
6596
+ endpoint: "smartmoney_get_trader_detail (composite)",
6597
+ requestTime: (/* @__PURE__ */ new Date()).toISOString(),
6598
+ data: {
6599
+ profile: extractLeaderboardData(profileNorm.data),
6600
+ positions: positionsNorm.data,
6601
+ trades: tradesNorm.data
6602
+ }
6603
+ };
6604
+ }
6605
+ }
6606
+ ];
6607
+ return tools.map(withSmartmoneyDemoGuard);
6608
+ }
6087
6609
  function buildContractTradeTools(cfg) {
6088
6610
  const { prefix, module, label, instTypes, instIdExample } = cfg;
6089
6611
  const [defaultType, otherType] = instTypes;
@@ -6425,31 +6947,58 @@ function buildContractTradeTools(cfg) {
6425
6947
  {
6426
6948
  name: n("set_leverage"),
6427
6949
  module,
6428
- description: `Set leverage for a ${label} instrument or position. [CAUTION] Changes risk parameters.`,
6950
+ description: `Set leverage for a ${label} instrument or position. [CAUTION] Changes risk parameters.
6951
+ Scenarios (SWAP/FUTURES only):
6952
+ \u2022 cross + any instId under the index \u2192 sets leverage at the index level
6953
+ \u2022 isolated + buy-sell (net) posMode \u2192 instId only
6954
+ \u2022 isolated + long-short (hedge) posMode \u2192 instId + posSide=long|short (BOTH directions must be set separately)
6955
+ 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
6956
  isWrite: true,
6430
6957
  inputSchema: {
6431
6958
  type: "object",
6432
6959
  properties: {
6433
6960
  instId: { type: "string", description: instIdExample },
6434
- lever: { type: "string", description: "Leverage, e.g. '10'" },
6961
+ lever: {
6962
+ type: "string",
6963
+ description: "Leverage multiplier as a positive number string, e.g. '10'. Max value depends on the instrument (query market_get_instruments \u2192 lever)."
6964
+ },
6435
6965
  mgnMode: { type: "string", enum: ["cross", "isolated"] },
6436
6966
  posSide: {
6437
6967
  type: "string",
6438
- enum: ["long", "short", "net"],
6439
- description: "Required for isolated margin in hedge mode"
6968
+ enum: ["long", "short"],
6969
+ 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
6970
  }
6441
6971
  },
6442
6972
  required: ["instId", "lever", "mgnMode"]
6443
6973
  },
6444
6974
  handler: async (rawArgs, context) => {
6445
6975
  const args = asRecord(rawArgs);
6976
+ const instId = requireString(args, "instId");
6977
+ const leverRaw = requireString(args, "lever");
6978
+ const leverNum = Number(leverRaw);
6979
+ if (!Number.isFinite(leverNum) || leverNum <= 0) {
6980
+ throw new ValidationError(
6981
+ `Parameter "lever" must be a positive number string, got "${leverRaw}".`
6982
+ );
6983
+ }
6984
+ const mgnMode = requireString(args, "mgnMode");
6985
+ assertEnum(mgnMode, "mgnMode", ["cross", "isolated"]);
6986
+ const posSide = readString(args, "posSide");
6987
+ if (posSide !== void 0) {
6988
+ assertEnum(posSide, "posSide", ["long", "short"]);
6989
+ if (mgnMode === "cross") {
6990
+ throw new ValidationError(
6991
+ `posSide="${posSide}" is only valid with mgnMode="isolated" in hedge mode. Omit posSide for cross margin.`
6992
+ );
6993
+ }
6994
+ }
6446
6995
  const response = await context.client.privatePost(
6447
6996
  "/api/v5/account/set-leverage",
6448
6997
  compactObject({
6449
- instId: requireString(args, "instId"),
6450
- lever: requireString(args, "lever"),
6451
- mgnMode: requireString(args, "mgnMode"),
6452
- posSide: readString(args, "posSide")
6998
+ instId,
6999
+ lever: leverRaw,
7000
+ mgnMode,
7001
+ posSide
6453
7002
  }),
6454
7003
  privateRateLimit(n("set_leverage"), 20)
6455
7004
  );
@@ -7322,7 +7871,7 @@ function registerMarketFilterTools() {
7322
7871
  sortBy: {
7323
7872
  type: "string",
7324
7873
  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)."
7874
+ 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
7875
  },
7327
7876
  sortOrder: {
7328
7877
  type: "string",
@@ -7435,7 +7984,7 @@ function registerMarketFilterTools() {
7435
7984
  bar: {
7436
7985
  type: "string",
7437
7986
  enum: [...OI_BARS],
7438
- description: "Bar window for OI change computation: 5m, 15m, 1H, 4H, 1D. Default: 1H"
7987
+ description: "Bar window for OI change computation: 5m, 15m, 1H, 4H, 1D (case-insensitive on server, but send canonical form here). Default: 1H"
7439
7988
  },
7440
7989
  // Filters
7441
7990
  minOiUsd: {
@@ -7453,8 +8002,8 @@ function registerMarketFilterTools() {
7453
8002
  // Sort / pagination
7454
8003
  sortBy: {
7455
8004
  type: "string",
7456
- enum: ["oiUsd", "oiDeltaUsd", "oiDeltaPct", "volUsd24h", "last"],
7457
- description: "Sort field. Default: oiDeltaPct (largest movers first)"
8005
+ enum: ["oiUsd", "oiDeltaUsd", "oiDeltaPct", "absOiDeltaPct", "volUsd24h", "fundingRate", "last"],
8006
+ 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
8007
  },
7459
8008
  sortOrder: {
7460
8009
  type: "string",
@@ -7510,7 +8059,7 @@ var D_COINS_SENTIMENT = 'Comma-separated uppercase ticker symbols, max 20 (e.g.
7510
8059
  var D_LANGUAGE = "Content language: zh-CN or en-US. Infer from user's message. No server default.";
7511
8060
  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
8061
  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.";
8062
+ 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
8063
  var D_PLATFORM = "Filter by news source. Use values from news_get_domains (e.g. blockbeats, odaily_flash). Omit for all sources.";
7515
8064
  var D_LIMIT = "Number of results (default 10, max 50).";
7516
8065
  function registerNewsTools() {
@@ -7521,7 +8070,7 @@ function registerNewsTools() {
7521
8070
  {
7522
8071
  name: "news_get_latest",
7523
8072
  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.",
8073
+ 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
8074
  isWrite: false,
7526
8075
  inputSchema: {
7527
8076
  type: "object",
@@ -9182,6 +9731,82 @@ function registerSpotTradeTools() {
9182
9731
  );
9183
9732
  return normalizeResponse(response);
9184
9733
  }
9734
+ },
9735
+ // ── set_leverage (SPOT margin: instId-level isolated OR ccy-level cross) ──
9736
+ // Covers OKX scenarios 1–5 (everything except SWAP/FUTURES, which are in
9737
+ // contract-trade.ts). Callers supply exactly one of {instId, ccy}:
9738
+ // • instId + isolated → scenario 1 (pair-level margin)
9739
+ // • instId + cross → scenario 3 (contract-mode pair-level cross margin)
9740
+ // • ccy + cross → scenarios 2 / 4 / 5 (spot/multi-ccy/PM currency-level cross)
9741
+ // Not applicable: posSide (spot has no long/short hedge).
9742
+ {
9743
+ name: "spot_set_leverage",
9744
+ module: "spot",
9745
+ 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.",
9746
+ isWrite: true,
9747
+ inputSchema: {
9748
+ type: "object",
9749
+ properties: {
9750
+ instId: {
9751
+ type: "string",
9752
+ description: "Spot pair, e.g. BTC-USDT. Provide instId OR ccy, not both."
9753
+ },
9754
+ ccy: {
9755
+ type: "string",
9756
+ description: "Margin currency, e.g. BTC. Required only for currency-level cross margin (borrow-enabled / multi-ccy / portfolio margin). Mutually exclusive with instId."
9757
+ },
9758
+ lever: {
9759
+ type: "string",
9760
+ 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."
9761
+ },
9762
+ mgnMode: {
9763
+ type: "string",
9764
+ enum: ["cross", "isolated"],
9765
+ description: "cross or isolated. Must be cross when ccy is supplied."
9766
+ }
9767
+ },
9768
+ required: ["lever", "mgnMode"]
9769
+ },
9770
+ handler: async (rawArgs, context) => {
9771
+ const args = asRecord(rawArgs);
9772
+ const instId = readString(args, "instId");
9773
+ const ccy = readString(args, "ccy");
9774
+ if (!instId && !ccy) {
9775
+ throw new ValidationError(
9776
+ `Missing required parameter: provide either "instId" (pair-level) or "ccy" (currency-level cross margin).`
9777
+ );
9778
+ }
9779
+ if (instId && ccy) {
9780
+ throw new ValidationError(
9781
+ `Parameters "instId" and "ccy" are mutually exclusive \u2014 provide only one. instId sets pair-level leverage; ccy sets currency-level cross margin leverage.`
9782
+ );
9783
+ }
9784
+ const leverRaw = requireString(args, "lever");
9785
+ const leverNum = Number(leverRaw);
9786
+ if (!Number.isFinite(leverNum) || leverNum <= 0) {
9787
+ throw new ValidationError(
9788
+ `Parameter "lever" must be a positive number string, got "${leverRaw}".`
9789
+ );
9790
+ }
9791
+ const mgnMode = requireString(args, "mgnMode");
9792
+ assertEnum(mgnMode, "mgnMode", ["cross", "isolated"]);
9793
+ if (ccy && mgnMode !== "cross") {
9794
+ throw new ValidationError(
9795
+ `When "ccy" is supplied, "mgnMode" must be "cross" (currency-level leverage only applies to cross margin).`
9796
+ );
9797
+ }
9798
+ const response = await context.client.privatePost(
9799
+ "/api/v5/account/set-leverage",
9800
+ compactObject({
9801
+ instId,
9802
+ ccy,
9803
+ lever: leverRaw,
9804
+ mgnMode
9805
+ }),
9806
+ privateRateLimit("spot_set_leverage", 20)
9807
+ );
9808
+ return normalizeResponse(response);
9809
+ }
9185
9810
  }
9186
9811
  ];
9187
9812
  }
@@ -9313,6 +9938,7 @@ function allToolSpecs() {
9313
9938
  ...registerNewsTools(),
9314
9939
  ...registerBotTools(),
9315
9940
  ...registerAllEarnTools(),
9941
+ ...registerSmartmoneyTools(),
9316
9942
  ...registerAuditTools(),
9317
9943
  ...registerSkillsTools()
9318
9944
  ];
@@ -9757,7 +10383,7 @@ var _require = createRequire(import.meta.url);
9757
10383
  var pkg = _require("../package.json");
9758
10384
  var SERVER_NAME = "okx-trade-mcp";
9759
10385
  var SERVER_VERSION = pkg.version;
9760
- var GIT_HASH = true ? "e9764c9" : "dev";
10386
+ var GIT_HASH = true ? "d5c8ab1" : "dev";
9761
10387
 
9762
10388
  // src/server.ts
9763
10389
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -9927,11 +10553,14 @@ Usage: okx-trade-mcp [options]
9927
10553
 
9928
10554
  Options:
9929
10555
  --modules <list> Comma-separated list of modules to load
9930
- Available: market, spot, swap, futures, option, account, news
10556
+ Available: market, spot, swap, futures, option, account,
10557
+ event, news, smartmoney, skills,
10558
+ earn.savings, earn.onchain, earn.dcd, earn.autoearn, earn.flash,
9931
10559
  bot.grid, bot.dca
9932
- Alias: "bot" = all bot sub-modules (bot.grid + bot.dca)
10560
+ Alias: "bot" = bot.grid + bot.dca
10561
+ "earn" / "earn.all" = all earn sub-modules
9933
10562
  Special: "all" loads all modules
9934
- Default: spot,swap,option,account,bot.grid
10563
+ Default: spot,swap,option,account,bot.grid,skills
9935
10564
 
9936
10565
  --profile <name> Profile to load from ${configFilePath()}
9937
10566
  Falls back to default_profile in config, then "default"