@okx_ai/okx-trade-cli 1.3.0-beta.3 → 1.3.0-beta.4

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
@@ -866,6 +866,9 @@ function stringify(obj, { maxDepth = 1e3, numbersAsFloat = false } = {}) {
866
866
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, existsSync as existsSync4 } from "fs";
867
867
  import { join as join7 } from "path";
868
868
  import { homedir as homedir5 } from "os";
869
+ import fs2 from "fs";
870
+ import path2 from "path";
871
+ import os2 from "os";
869
872
  import * as fs3 from "fs";
870
873
  import * as path3 from "path";
871
874
  import * as os3 from "os";
@@ -939,16 +942,8 @@ function writeCache(hostname, entry, cachePath = DOH_CACHE_PATH) {
939
942
  }
940
943
  }
941
944
  var FAILED_NODE_TTL_MS = 60 * 60 * 1e3;
942
- function classifyAndCache(node, hostname, failedNodes) {
945
+ function classifyAndCache(node, hostname, failedNodes, cachePath) {
943
946
  if (!node) {
944
- if (failedNodes.length > 0) {
945
- writeCache(hostname, {
946
- mode: "direct",
947
- node: null,
948
- failedNodes,
949
- updatedAt: Date.now()
950
- });
951
- }
952
947
  return { mode: null, node: null };
953
948
  }
954
949
  if (node.ip === hostname || node.host === hostname) {
@@ -957,7 +952,7 @@ function classifyAndCache(node, hostname, failedNodes) {
957
952
  node: null,
958
953
  failedNodes,
959
954
  updatedAt: Date.now()
960
- });
955
+ }, cachePath);
961
956
  return { mode: "direct", node: null };
962
957
  }
963
958
  writeCache(hostname, {
@@ -965,7 +960,7 @@ function classifyAndCache(node, hostname, failedNodes) {
965
960
  node,
966
961
  failedNodes,
967
962
  updatedAt: Date.now()
968
- });
963
+ }, cachePath);
969
964
  return { mode: "proxy", node };
970
965
  }
971
966
  function getActiveFailedNodes(nodes) {
@@ -973,8 +968,8 @@ function getActiveFailedNodes(nodes) {
973
968
  const now = Date.now();
974
969
  return nodes.filter((n) => now - n.failedAt < FAILED_NODE_TTL_MS);
975
970
  }
976
- async function resolveDoh(hostname) {
977
- const entry = readCache(hostname);
971
+ function resolveDoh(hostname, cachePath) {
972
+ const entry = readCache(hostname, cachePath);
978
973
  if (entry) {
979
974
  if (entry.mode === "direct") {
980
975
  return { mode: "direct", node: null };
@@ -985,14 +980,15 @@ async function resolveDoh(hostname) {
985
980
  }
986
981
  return { mode: null, node: null };
987
982
  }
988
- async function reResolveDoh(hostname, failedIp, userAgent) {
989
- const entry = readCache(hostname);
983
+ async function reResolveDoh(hostname, failedIp, userAgent, cachePath) {
984
+ const entry = readCache(hostname, cachePath);
990
985
  const active = getActiveFailedNodes(entry?.failedNodes);
991
986
  const now = Date.now();
992
- const failedNodes = failedIp ? active.some((n) => n.ip === failedIp) ? active : [...active, { ip: failedIp, failedAt: now }] : active;
987
+ const alreadyFailed = failedIp && active.some((n) => n.ip === failedIp);
988
+ const failedNodes = failedIp && !alreadyFailed ? [...active, { ip: failedIp, failedAt: now }] : active;
993
989
  const excludeIps = failedNodes.map((n) => n.ip);
994
990
  const node = await execDohBinary(hostname, excludeIps, userAgent);
995
- return classifyAndCache(node, hostname, failedNodes);
991
+ return classifyAndCache(node, hostname, failedNodes, cachePath);
996
992
  }
997
993
  function getNow() {
998
994
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -1231,12 +1227,12 @@ var OkxRestClient = class _OkxRestClient {
1231
1227
  * Lazily resolve the DoH proxy node on the first request.
1232
1228
  * Uses cache-first strategy via the resolver.
1233
1229
  */
1234
- async ensureDoh() {
1230
+ ensureDoh() {
1235
1231
  if (this.dohResolved || this.dispatcher) return;
1236
1232
  this.dohResolved = true;
1237
1233
  try {
1238
1234
  const { hostname, protocol } = new URL(this.config.baseUrl);
1239
- const result = await resolveDoh(hostname);
1235
+ const result = resolveDoh(hostname);
1240
1236
  if (!result.mode) {
1241
1237
  this.directUnverified = true;
1242
1238
  if (this.config.verbose) {
@@ -1299,6 +1295,7 @@ var OkxRestClient = class _OkxRestClient {
1299
1295
  const result = await reResolveDoh(hostname, failedIp, this.dohUserAgent);
1300
1296
  if (result.mode === "proxy" && result.node) {
1301
1297
  this.applyDohNode(result.node, protocol);
1298
+ this.dohRetried = false;
1302
1299
  return true;
1303
1300
  }
1304
1301
  } catch {
@@ -1487,7 +1484,7 @@ var OkxRestClient = class _OkxRestClient {
1487
1484
  * Security: validates Content-Type and enforces maxBytes limit.
1488
1485
  */
1489
1486
  async privatePostBinary(path42, body, opts) {
1490
- await this.ensureDoh();
1487
+ this.ensureDoh();
1491
1488
  const maxBytes = opts?.maxBytes ?? _OkxRestClient.DEFAULT_MAX_BYTES;
1492
1489
  const expectedCT = opts?.expectedContentType ?? "application/octet-stream";
1493
1490
  const bodyJson = body ? JSON.stringify(body) : "";
@@ -1564,8 +1561,52 @@ var OkxRestClient = class _OkxRestClient {
1564
1561
  // ---------------------------------------------------------------------------
1565
1562
  // JSON request
1566
1563
  // ---------------------------------------------------------------------------
1564
+ /**
1565
+ * Handle network error during a JSON request: refresh DoH and maybe retry.
1566
+ * Always either returns a retry result or throws NetworkError.
1567
+ */
1568
+ async handleRequestNetworkError(error, reqConfig, requestPath, t0) {
1569
+ if (!this.dohRetried) {
1570
+ if (this.config.verbose) {
1571
+ const cause = error instanceof Error ? error.message : String(error);
1572
+ vlog(`Network failure, refreshing DoH: ${cause}`);
1573
+ }
1574
+ const shouldRetry = await this.handleDohNetworkFailure();
1575
+ if (shouldRetry && reqConfig.method === "GET") {
1576
+ return this.request(reqConfig);
1577
+ }
1578
+ }
1579
+ if (this.config.verbose) {
1580
+ const elapsed = Date.now() - t0;
1581
+ const cause = error instanceof Error ? error.message : String(error);
1582
+ vlog(`\u2717 NetworkError after ${elapsed}ms: ${cause}`);
1583
+ }
1584
+ throw new NetworkError(
1585
+ `Failed to call OKX endpoint ${reqConfig.method} ${requestPath}.`,
1586
+ `${reqConfig.method} ${requestPath}`,
1587
+ error
1588
+ );
1589
+ }
1590
+ /**
1591
+ * After a successful HTTP response on direct connection, cache mode=direct.
1592
+ * (Even if the business response is an error, the network path is valid.)
1593
+ */
1594
+ cacheDirectConnectionIfNeeded() {
1595
+ if (!this.directUnverified || this.dohNode) return;
1596
+ this.directUnverified = false;
1597
+ const { hostname } = new URL(this.config.baseUrl);
1598
+ writeCache(hostname, {
1599
+ mode: "direct",
1600
+ node: null,
1601
+ failedNodes: [],
1602
+ updatedAt: Date.now()
1603
+ });
1604
+ if (this.config.verbose) {
1605
+ vlog("DoH: direct connection succeeded, cached mode=direct");
1606
+ }
1607
+ }
1567
1608
  async request(reqConfig) {
1568
- await this.ensureDoh();
1609
+ this.ensureDoh();
1569
1610
  const queryString = buildQueryString(reqConfig.query);
1570
1611
  const requestPath = queryString.length > 0 ? `${reqConfig.path}?${queryString}` : reqConfig.path;
1571
1612
  const url = `${this.activeBaseUrl}${requestPath}`;
@@ -1591,42 +1632,12 @@ var OkxRestClient = class _OkxRestClient {
1591
1632
  };
1592
1633
  response = await fetch(url, fetchOptions);
1593
1634
  } catch (error) {
1594
- if (!this.dohRetried) {
1595
- if (this.config.verbose) {
1596
- vlog(`Network failure, refreshing DoH: ${error instanceof Error ? error.message : String(error)}`);
1597
- }
1598
- const shouldRetry = await this.handleDohNetworkFailure();
1599
- if (shouldRetry && reqConfig.method === "GET") {
1600
- return this.request(reqConfig);
1601
- }
1602
- }
1603
- if (this.config.verbose) {
1604
- const elapsed2 = Date.now() - t0;
1605
- const cause = error instanceof Error ? error.message : String(error);
1606
- vlog(`\u2717 NetworkError after ${elapsed2}ms: ${cause}`);
1607
- }
1608
- throw new NetworkError(
1609
- `Failed to call OKX endpoint ${reqConfig.method} ${requestPath}.`,
1610
- `${reqConfig.method} ${requestPath}`,
1611
- error
1612
- );
1635
+ return await this.handleRequestNetworkError(error, reqConfig, requestPath, t0);
1613
1636
  }
1614
1637
  const rawText = await response.text();
1615
1638
  const elapsed = Date.now() - t0;
1616
1639
  const traceId = extractTraceId(response.headers);
1617
- if (this.directUnverified && !this.dohNode) {
1618
- this.directUnverified = false;
1619
- const { hostname: h } = new URL(this.config.baseUrl);
1620
- writeCache(h, {
1621
- mode: "direct",
1622
- node: null,
1623
- failedNodes: [],
1624
- updatedAt: Date.now()
1625
- });
1626
- if (this.config.verbose) {
1627
- vlog("DoH: direct connection succeeded, cached mode=direct");
1628
- }
1629
- }
1640
+ this.cacheDirectConnectionIfNeeded();
1630
1641
  return this.processResponse(rawText, response, elapsed, traceId, reqConfig, requestPath);
1631
1642
  }
1632
1643
  };
@@ -1766,14 +1777,137 @@ var INDICATOR_BARS = [
1766
1777
  "1Wutc"
1767
1778
  ];
1768
1779
  var INDICATOR_CODE_OVERRIDES = {
1780
+ // Aliases
1781
+ "boll": "BB",
1782
+ // server supports BB not BOLL
1783
+ // Names where default rule produces underscores but backend uses no separator
1769
1784
  "rainbow": "BTCRAINBOW",
1770
- "range-filter": "RANGEFILTER",
1785
+ // default: RAINBOW
1771
1786
  "stoch-rsi": "STOCHRSI",
1772
- "pi-cycle-top": "PI_CYCLE_TOP",
1773
- "pi-cycle-bottom": "PI_CYCLE_BOTTOM",
1774
- // boll is an alias for bb; server supports BB not BOLL
1775
- "boll": "BB"
1787
+ // default: STOCH_RSI
1788
+ "bull-engulf": "BULLENGULF",
1789
+ // default: BULL_ENGULF
1790
+ "bear-engulf": "BEARENGULF",
1791
+ // default: BEAR_ENGULF
1792
+ "bull-harami": "BULLHARAMI",
1793
+ // default: BULL_HARAMI
1794
+ "bear-harami": "BEARHARAMI",
1795
+ // default: BEAR_HARAMI
1796
+ "bull-harami-cross": "BULLHARAMICROSS",
1797
+ // default: BULL_HARAMI_CROSS
1798
+ "bear-harami-cross": "BEARHARAMICROSS",
1799
+ // default: BEAR_HARAMI_CROSS
1800
+ "three-soldiers": "THREESOLDIERS",
1801
+ // default: THREE_SOLDIERS
1802
+ "three-crows": "THREECROWS",
1803
+ // default: THREE_CROWS
1804
+ "hanging-man": "HANGINGMAN",
1805
+ // default: HANGING_MAN
1806
+ "inverted-hammer": "INVERTEDH",
1807
+ // default: INVERTED_HAMMER (backend uses INVERTEDH)
1808
+ "shooting-star": "SHOOTINGSTAR",
1809
+ // default: SHOOTING_STAR
1810
+ "nvi-pvi": "NVIPVI",
1811
+ // default: NVI_PVI
1812
+ "top-long-short": "TOPLONGSHORT"
1813
+ // default: TOP_LONG_SHORT
1814
+ // Note: range-filter → RANGE_FILTER is correct via the default rule; no override needed.
1776
1815
  };
1816
+ var KNOWN_INDICATORS = [
1817
+ // Moving Averages
1818
+ { name: "ma", description: "Simple Moving Average" },
1819
+ { name: "ema", description: "Exponential Moving Average" },
1820
+ { name: "wma", description: "Weighted Moving Average" },
1821
+ { name: "dema", description: "Double Exponential Moving Average" },
1822
+ { name: "tema", description: "Triple Exponential Moving Average" },
1823
+ { name: "zlema", description: "Zero-Lag Exponential Moving Average" },
1824
+ { name: "hma", description: "Hull Moving Average" },
1825
+ { name: "kama", description: "Kaufman Adaptive Moving Average" },
1826
+ // Trend
1827
+ { name: "macd", description: "MACD" },
1828
+ { name: "sar", description: "Parabolic SAR" },
1829
+ { name: "adx", description: "Average Directional Index" },
1830
+ { name: "aroon", description: "Aroon Indicator" },
1831
+ { name: "cci", description: "Commodity Channel Index" },
1832
+ { name: "dpo", description: "Detrended Price Oscillator" },
1833
+ { name: "envelope", description: "Envelope" },
1834
+ { name: "halftrend", description: "HalfTrend" },
1835
+ { name: "alphatrend", description: "AlphaTrend" },
1836
+ // Momentum
1837
+ { name: "rsi", description: "Relative Strength Index" },
1838
+ { name: "stoch-rsi", description: "Stochastic RSI" },
1839
+ { name: "stoch", description: "Stochastic Oscillator" },
1840
+ { name: "roc", description: "Rate of Change" },
1841
+ { name: "mom", description: "Momentum" },
1842
+ { name: "ppo", description: "Price Percentage Oscillator" },
1843
+ { name: "trix", description: "TRIX" },
1844
+ { name: "ao", description: "Awesome Oscillator" },
1845
+ { name: "uo", description: "Ultimate Oscillator" },
1846
+ { name: "wr", description: "Williams %R" },
1847
+ // Volatility
1848
+ { name: "bb", description: "Bollinger Bands" },
1849
+ { name: "boll", description: "Bollinger Bands (alias for bb)" },
1850
+ { name: "bbwidth", description: "Bollinger Band Width" },
1851
+ { name: "bbpct", description: "Bollinger Band %B" },
1852
+ { name: "atr", description: "Average True Range" },
1853
+ { name: "keltner", description: "Keltner Channel" },
1854
+ { name: "donchian", description: "Donchian Channel" },
1855
+ { name: "hv", description: "Historical Volatility" },
1856
+ { name: "stddev", description: "Standard Deviation" },
1857
+ // Volume
1858
+ { name: "obv", description: "On-Balance Volume" },
1859
+ { name: "vwap", description: "Volume Weighted Average Price" },
1860
+ { name: "mvwap", description: "Moving VWAP" },
1861
+ { name: "cmf", description: "Chaikin Money Flow" },
1862
+ { name: "mfi", description: "Money Flow Index" },
1863
+ { name: "ad", description: "Accumulation/Distribution" },
1864
+ // Statistical
1865
+ { name: "lr", description: "Linear Regression" },
1866
+ { name: "slope", description: "Linear Regression Slope" },
1867
+ { name: "angle", description: "Linear Regression Angle" },
1868
+ { name: "variance", description: "Variance" },
1869
+ { name: "meandev", description: "Mean Deviation" },
1870
+ { name: "sigma", description: "Sigma" },
1871
+ { name: "stderr", description: "Standard Error" },
1872
+ // Custom
1873
+ { name: "kdj", description: "KDJ Stochastic Oscillator" },
1874
+ { name: "supertrend", description: "Supertrend" },
1875
+ // Ichimoku
1876
+ { name: "tenkan", description: "Ichimoku Tenkan-sen (Conversion Line)" },
1877
+ { name: "kijun", description: "Ichimoku Kijun-sen (Base Line)" },
1878
+ { name: "senkoa", description: "Ichimoku Senkou Span A (Leading Span A)" },
1879
+ { name: "senkob", description: "Ichimoku Senkou Span B (Leading Span B)" },
1880
+ { name: "chikou", description: "Ichimoku Chikou Span (Lagging Span)" },
1881
+ // Candlestick Patterns
1882
+ { name: "doji", description: "Doji candlestick pattern" },
1883
+ { name: "bull-engulf", description: "Bullish Engulfing pattern" },
1884
+ { name: "bear-engulf", description: "Bearish Engulfing pattern" },
1885
+ { name: "bull-harami", description: "Bullish Harami pattern" },
1886
+ { name: "bear-harami", description: "Bearish Harami pattern" },
1887
+ { name: "bull-harami-cross", description: "Bullish Harami Cross pattern" },
1888
+ { name: "bear-harami-cross", description: "Bearish Harami Cross pattern" },
1889
+ { name: "three-soldiers", description: "Three White Soldiers pattern" },
1890
+ { name: "three-crows", description: "Three Black Crows pattern" },
1891
+ { name: "hanging-man", description: "Hanging Man pattern" },
1892
+ { name: "inverted-hammer", description: "Inverted Hammer pattern" },
1893
+ { name: "shooting-star", description: "Shooting Star pattern" },
1894
+ // Bitcoin On-Chain
1895
+ { name: "ahr999", description: "AHR999 Bitcoin accumulation index" },
1896
+ { name: "rainbow", description: "Bitcoin Rainbow Chart" },
1897
+ // Other
1898
+ { name: "fisher", description: "Fisher Transform" },
1899
+ { name: "nvi-pvi", description: "Negative/Positive Volume Index (returns both)" },
1900
+ { name: "pmax", description: "PMAX" },
1901
+ { name: "qqe", description: "QQE Mod" },
1902
+ { name: "tdi", description: "Traders Dynamic Index" },
1903
+ { name: "waddah", description: "Waddah Attar Explosion" },
1904
+ { name: "range-filter", description: "Range Filter" },
1905
+ { name: "cho", description: "Chande Momentum Oscillator" },
1906
+ { name: "tr", description: "True Range" },
1907
+ { name: "tp", description: "Typical Price" },
1908
+ { name: "mp", description: "Median Price" },
1909
+ { name: "top-long-short", description: "Top Trader Long/Short Ratio (timeframe-independent)" }
1910
+ ];
1777
1911
  function resolveIndicatorCode(name) {
1778
1912
  const lower = name.toLowerCase();
1779
1913
  return INDICATOR_CODE_OVERRIDES[lower] ?? name.toUpperCase().replace(/-/g, "_");
@@ -1800,7 +1934,7 @@ function registerIndicatorTools() {
1800
1934
  },
1801
1935
  indicator: {
1802
1936
  type: "string",
1803
- description: "Indicator name (case-insensitive). Examples: ma, ema, rsi, macd, bb, kdj, supertrend, ahr999, rainbow, pi-cycle-top, pi-cycle-bottom, mayer, envelope, halftrend, alphatrend, pmax, waddah, tdi, qqe, range-filter"
1937
+ description: "Indicator name (case-insensitive). Call market_list_indicators to see all supported names."
1804
1938
  },
1805
1939
  bar: {
1806
1940
  type: "string",
@@ -1857,6 +1991,14 @@ function registerIndicatorTools() {
1857
1991
  );
1858
1992
  return normalizeResponse(response);
1859
1993
  }
1994
+ },
1995
+ {
1996
+ name: "market_list_indicators",
1997
+ module: "market",
1998
+ description: "List all supported technical indicator names and descriptions. Call this before market_get_indicator to discover valid indicator names. No credentials required.",
1999
+ isWrite: false,
2000
+ inputSchema: { type: "object", properties: {} },
2001
+ handler: async () => ({ data: KNOWN_INDICATORS })
1860
2002
  }
1861
2003
  ];
1862
2004
  }
@@ -2454,6 +2596,118 @@ function registerAccountTools() {
2454
2596
  }
2455
2597
  ];
2456
2598
  }
2599
+ function extractInstrumentParams(instId, data) {
2600
+ const instruments = Array.isArray(data) ? data : [];
2601
+ if (instruments.length === 0) {
2602
+ throw new Error(`Failed to fetch instrument info for ${instId}: empty instrument list. Cannot determine ctVal for conversion.`);
2603
+ }
2604
+ const inst = instruments[0];
2605
+ const ctValStr = String(inst.ctVal ?? "");
2606
+ const ctVal = parseFloat(ctValStr);
2607
+ if (!isFinite(ctVal) || ctVal <= 0) {
2608
+ throw new Error(`Invalid ctVal "${ctValStr}" for ${instId}. ctVal must be a positive number for conversion.`);
2609
+ }
2610
+ const minSzStr = String(inst.minSz ?? "1");
2611
+ const minSz = parseFloat(minSzStr);
2612
+ if (!isFinite(minSz) || minSz <= 0) {
2613
+ throw new Error(`Invalid minSz "${minSzStr}" for ${instId}. minSz must be a positive number for conversion.`);
2614
+ }
2615
+ const lotSzStr = String(inst.lotSz ?? "1");
2616
+ const lotSz = parseFloat(lotSzStr);
2617
+ if (!isFinite(lotSz) || lotSz <= 0) {
2618
+ throw new Error(`Invalid lotSz "${lotSzStr}" for ${instId}. lotSz must be a positive number for conversion.`);
2619
+ }
2620
+ return { ctVal, ctValStr, minSz, minSzStr, lotSz, lotSzStr };
2621
+ }
2622
+ function extractLastPx(instId, data) {
2623
+ const tickers = Array.isArray(data) ? data : [];
2624
+ if (tickers.length === 0) {
2625
+ throw new Error(`Failed to fetch ticker price for ${instId}: empty ticker response. Cannot determine last price for conversion.`);
2626
+ }
2627
+ const lastStr = String(tickers[0].last ?? "");
2628
+ const lastPx = parseFloat(lastStr);
2629
+ if (!isFinite(lastPx) || lastPx <= 0) {
2630
+ throw new Error(`Invalid last price "${lastStr}" for ${instId}. Last price must be a positive number for conversion.`);
2631
+ }
2632
+ return { lastPx, lastStr };
2633
+ }
2634
+ function extractLeverage(instId, mgnMode, data) {
2635
+ const leverageData = Array.isArray(data) ? data : [];
2636
+ if (leverageData.length === 0) {
2637
+ throw new Error(
2638
+ `Failed to fetch leverage info for ${instId} (mgnMode=${mgnMode}): empty response. Cannot determine leverage for margin conversion. Please set leverage first using set_leverage.`
2639
+ );
2640
+ }
2641
+ const leverStr = String(leverageData[0].lever ?? "1");
2642
+ const lever = parseFloat(leverStr);
2643
+ if (!isFinite(lever) || lever <= 0) {
2644
+ throw new Error(`Invalid leverage "${leverStr}" for ${instId}. Leverage must be a positive number for margin conversion.`);
2645
+ }
2646
+ return { lever, leverStr };
2647
+ }
2648
+ function computeContracts(p) {
2649
+ const { instId, sz, isMarginMode, inst, lastPx, lastStr, lever, leverStr } = p;
2650
+ const { ctVal, ctValStr, minSz, minSzStr, lotSz, lotSzStr } = inst;
2651
+ const userAmount = parseFloat(sz);
2652
+ const contractValue = ctVal * lastPx;
2653
+ const effectiveNotional = isMarginMode ? userAmount * lever : userAmount;
2654
+ const lotSzDecimals = lotSzStr.includes(".") ? lotSzStr.split(".")[1].length : 0;
2655
+ const precision = 10 ** (lotSzDecimals + 4);
2656
+ const rawContracts = Math.round(effectiveNotional / contractValue * precision) / precision;
2657
+ const rawLots = Math.round(rawContracts / lotSz * precision) / precision;
2658
+ const contractsRounded = parseFloat((Math.floor(rawLots) * lotSz).toFixed(lotSzDecimals));
2659
+ if (contractsRounded < minSz) {
2660
+ const minAmount = isMarginMode ? (minSz * contractValue / lever).toFixed(2) : (minSz * contractValue).toFixed(2);
2661
+ const unit = isMarginMode ? "USDT margin" : "USDT";
2662
+ throw new Error(
2663
+ `sz=${sz} ${unit} is too small for ${instId}. Minimum order size is ${minSzStr} contracts (\u2248 ${minAmount} ${unit}). (ctVal=${ctValStr}, lastPx=${lastStr}, minSz=${minSzStr}, lotSz=${lotSzStr}` + (isMarginMode ? `, lever=${leverStr}` : "") + `).
2664
+ `
2665
+ );
2666
+ }
2667
+ const contractsStr = contractsRounded.toFixed(lotSzDecimals);
2668
+ const conversionNote = isMarginMode ? `Converting ${sz} USDT margin (${leverStr}x leverage) \u2192 ${contractsStr} contracts (notional value \u2248 ${(contractsRounded * contractValue).toFixed(2)} USDT, ctVal=${ctValStr}, lastPx=${lastStr}, lever=${leverStr}, minSz=${minSzStr}, lotSz=${lotSzStr})` : `Converting ${sz} USDT \u2192 ${contractsStr} contracts (ctVal=${ctValStr}, lastPx=${lastStr}, minSz=${minSzStr}, lotSz=${lotSzStr})`;
2669
+ return { contractsStr, conversionNote };
2670
+ }
2671
+ async function resolveQuoteCcySz(instId, sz, tgtCcy, instType, client, tdMode) {
2672
+ if (tgtCcy === void 0 || tgtCcy === "base_ccy") {
2673
+ return { sz, tgtCcy, conversionNote: void 0 };
2674
+ }
2675
+ if (tgtCcy !== "quote_ccy" && tgtCcy !== "margin") {
2676
+ throw new ValidationError(
2677
+ `Unknown tgtCcy value "${tgtCcy}". Valid values: base_ccy, quote_ccy, margin.`,
2678
+ `Check the --tgtCcy flag. Use base_ccy (default, sz in contracts), quote_ccy (sz in USDT notional), or margin (sz in USDT margin cost).`
2679
+ );
2680
+ }
2681
+ const isMarginMode = tgtCcy === "margin";
2682
+ if (isMarginMode && !tdMode) {
2683
+ throw new Error(
2684
+ "tdMode (cross or isolated) is required when tgtCcy=margin. Cannot determine leverage without knowing the margin mode."
2685
+ );
2686
+ }
2687
+ const mgnMode = tdMode === "cross" ? "cross" : "isolated";
2688
+ const fetchPromises = [
2689
+ client.publicGet("/api/v5/public/instruments", { instType, instId }),
2690
+ client.publicGet("/api/v5/market/ticker", { instId })
2691
+ ];
2692
+ if (isMarginMode) {
2693
+ fetchPromises.push(client.privateGet("/api/v5/account/leverage-info", { instId, mgnMode }));
2694
+ }
2695
+ const results = await Promise.all(fetchPromises);
2696
+ const inst = extractInstrumentParams(instId, results[0].data);
2697
+ const { lastPx, lastStr } = extractLastPx(instId, results[1].data);
2698
+ const { lever, leverStr } = isMarginMode ? extractLeverage(instId, mgnMode, results[2].data) : { lever: 1, leverStr: "1" };
2699
+ const { contractsStr, conversionNote } = computeContracts({
2700
+ instId,
2701
+ sz,
2702
+ isMarginMode,
2703
+ inst,
2704
+ lastPx,
2705
+ lastStr,
2706
+ lever,
2707
+ leverStr
2708
+ });
2709
+ return { sz: contractsStr, tgtCcy: void 0, conversionNote };
2710
+ }
2457
2711
  function registerAlgoTradeTools() {
2458
2712
  return [
2459
2713
  {
@@ -2532,8 +2786,8 @@ function registerAlgoTradeTools() {
2532
2786
  },
2533
2787
  tgtCcy: {
2534
2788
  type: "string",
2535
- enum: ["base_ccy", "quote_ccy"],
2536
- description: "Size unit. base_ccy(default): sz in contracts, quote_ccy: sz in USDT"
2789
+ enum: ["base_ccy", "quote_ccy", "margin"],
2790
+ description: "Size unit. base_ccy(default): sz in contracts; quote_ccy: sz in USDT notional value; margin: sz in USDT margin cost (actual position = sz * leverage)"
2537
2791
  },
2538
2792
  reduceOnly: {
2539
2793
  type: "boolean",
@@ -2549,6 +2803,14 @@ function registerAlgoTradeTools() {
2549
2803
  handler: async (rawArgs, context) => {
2550
2804
  const args = asRecord(rawArgs);
2551
2805
  const reduceOnly = args.reduceOnly;
2806
+ const resolved = await resolveQuoteCcySz(
2807
+ requireString(args, "instId"),
2808
+ requireString(args, "sz"),
2809
+ readString(args, "tgtCcy"),
2810
+ "SWAP",
2811
+ context.client,
2812
+ readString(args, "tdMode")
2813
+ );
2552
2814
  const response = await context.client.privatePost(
2553
2815
  "/api/v5/trade/order-algo",
2554
2816
  compactObject({
@@ -2557,8 +2819,8 @@ function registerAlgoTradeTools() {
2557
2819
  side: requireString(args, "side"),
2558
2820
  posSide: readString(args, "posSide"),
2559
2821
  ordType: requireString(args, "ordType"),
2560
- sz: requireString(args, "sz"),
2561
- tgtCcy: readString(args, "tgtCcy"),
2822
+ sz: resolved.sz,
2823
+ tgtCcy: resolved.tgtCcy,
2562
2824
  tpTriggerPx: readString(args, "tpTriggerPx"),
2563
2825
  tpOrdPx: readString(args, "tpOrdPx"),
2564
2826
  tpTriggerPxType: readString(args, "tpTriggerPxType"),
@@ -2574,7 +2836,11 @@ function registerAlgoTradeTools() {
2574
2836
  }),
2575
2837
  privateRateLimit("swap_place_algo_order", 20)
2576
2838
  );
2577
- return normalizeResponse(response);
2839
+ const result = normalizeResponse(response);
2840
+ if (resolved.conversionNote) {
2841
+ result._conversion = resolved.conversionNote;
2842
+ }
2843
+ return result;
2578
2844
  }
2579
2845
  },
2580
2846
  {
@@ -2865,8 +3131,8 @@ function registerFuturesAlgoTools() {
2865
3131
  },
2866
3132
  tgtCcy: {
2867
3133
  type: "string",
2868
- enum: ["base_ccy", "quote_ccy"],
2869
- description: "Size unit. base_ccy(default): sz in contracts, quote_ccy: sz in USDT"
3134
+ enum: ["base_ccy", "quote_ccy", "margin"],
3135
+ description: "Size unit. base_ccy(default): sz in contracts; quote_ccy: sz in USDT notional value; margin: sz in USDT margin cost (actual position = sz * leverage)"
2870
3136
  },
2871
3137
  reduceOnly: {
2872
3138
  type: "boolean",
@@ -2882,6 +3148,14 @@ function registerFuturesAlgoTools() {
2882
3148
  handler: async (rawArgs, context) => {
2883
3149
  const args = asRecord(rawArgs);
2884
3150
  const reduceOnly = args.reduceOnly;
3151
+ const resolved = await resolveQuoteCcySz(
3152
+ requireString(args, "instId"),
3153
+ requireString(args, "sz"),
3154
+ readString(args, "tgtCcy"),
3155
+ "FUTURES",
3156
+ context.client,
3157
+ readString(args, "tdMode")
3158
+ );
2885
3159
  const response = await context.client.privatePost(
2886
3160
  "/api/v5/trade/order-algo",
2887
3161
  compactObject({
@@ -2890,8 +3164,8 @@ function registerFuturesAlgoTools() {
2890
3164
  side: requireString(args, "side"),
2891
3165
  posSide: readString(args, "posSide"),
2892
3166
  ordType: requireString(args, "ordType"),
2893
- sz: requireString(args, "sz"),
2894
- tgtCcy: readString(args, "tgtCcy"),
3167
+ sz: resolved.sz,
3168
+ tgtCcy: resolved.tgtCcy,
2895
3169
  tpTriggerPx: readString(args, "tpTriggerPx"),
2896
3170
  tpOrdPx: readString(args, "tpOrdPx"),
2897
3171
  tpTriggerPxType: readString(args, "tpTriggerPxType"),
@@ -2907,7 +3181,11 @@ function registerFuturesAlgoTools() {
2907
3181
  }),
2908
3182
  privateRateLimit("futures_place_algo_order", 20)
2909
3183
  );
2910
- return normalizeResponse(response);
3184
+ const result = normalizeResponse(response);
3185
+ if (resolved.conversionNote) {
3186
+ result._conversion = resolved.conversionNote;
3187
+ }
3188
+ return result;
2911
3189
  }
2912
3190
  },
2913
3191
  {
@@ -3472,7 +3750,7 @@ function registerSkillsTools() {
3472
3750
  {
3473
3751
  name: "skills_download",
3474
3752
  module: "skills",
3475
- description: "Download a skill zip file from OKX Skills Marketplace to a local directory. Always call skills_search first to confirm the skill name exists. Downloads the latest approved version. NOTE: This only downloads the zip \u2014 it does NOT install to agents. For full installation use CLI: okx skill add <name>. Use when the user wants to inspect or manually install a skill package.",
3753
+ description: "Download a skill zip file from OKX Skills Marketplace to a local directory. Always call skills_search first to confirm the skill name exists. Downloads the latest approved version. NOTE: Downloads third-party developer content as a zip \u2014 does NOT install to agents. For full installation use CLI: okx skill add <name>. Use when the user wants to inspect or manually install a skill package.",
3476
3754
  inputSchema: {
3477
3755
  type: "object",
3478
3756
  properties: {
@@ -4078,7 +4356,7 @@ function registerEarnTools() {
4078
4356
  {
4079
4357
  name: "earn_get_savings_balance",
4080
4358
  module: "earn.savings",
4081
- description: "Get Simple Earn (savings/flexible earn) balance. Returns current holdings, lent amount, pending interest, and the user's set rate. Response fields: amt (total held), loanAmt (actively lent), pendingAmt (awaiting match), earnings (cumulative interest), rate (user's own minimum lending rate setting \u2014 NOT market yield, NOT APY). To get the actual market lending rate, call earn_get_lending_rate_history instead.",
4359
+ description: "Get Simple Earn (savings/flexible earn) balance. Returns current holdings for all currencies or a specific one. To show market rates alongside balance (\u5E02\u573A\u5747\u5229\u7387), call earn_get_lending_rate_history. earn_get_lending_rate_history also returns fixed-term (\u5B9A\u671F) product offers, so one call gives a complete view of both flexible and fixed options. Do NOT use for fixed-term (\u5B9A\u671F) order queries \u2014 use earn_get_fixed_order_list instead.",
4082
4360
  isWrite: false,
4083
4361
  inputSchema: {
4084
4362
  type: "object",
@@ -4099,6 +4377,43 @@ function registerEarnTools() {
4099
4377
  return normalizeResponse(response);
4100
4378
  }
4101
4379
  },
4380
+ {
4381
+ name: "earn_get_fixed_order_list",
4382
+ module: "earn.savings",
4383
+ description: "Get Simple Earn Fixed (\u5B9A\u671F\u8D5A\u5E01) lending order list. Returns orders sorted by creation time descending. Use this to check status of fixed-term lending orders (pending/earning/expired/settled/cancelled). Do NOT use for flexible earn balance \u2014 use earn_get_savings_balance instead. If the result is empty, do NOT display any fixed-term section in the output.",
4384
+ isWrite: false,
4385
+ inputSchema: {
4386
+ type: "object",
4387
+ properties: {
4388
+ ccy: {
4389
+ type: "string",
4390
+ description: "Currency, e.g. USDT. Omit for all."
4391
+ },
4392
+ state: {
4393
+ type: "string",
4394
+ description: "Order state: pending (\u5339\u914D\u4E2D), earning (\u8D5A\u5E01\u4E2D), expired (\u903E\u671F), settled (\u5DF2\u7ED3\u7B97), cancelled (\u5DF2\u64A4\u9500). Omit for all."
4395
+ }
4396
+ }
4397
+ },
4398
+ handler: async (rawArgs, context) => {
4399
+ const args = asRecord(rawArgs);
4400
+ const response = await context.client.privateGet(
4401
+ "/api/v5/finance/simple-earn-fixed/order-list",
4402
+ compactObject({
4403
+ ccy: readString(args, "ccy"),
4404
+ state: readString(args, "state")
4405
+ }),
4406
+ privateRateLimit("earn_get_fixed_order_list", 3)
4407
+ );
4408
+ const result = normalizeResponse(response);
4409
+ if (Array.isArray(result["data"])) {
4410
+ result["data"] = result["data"].map(
4411
+ ({ finalSettlementDate: _, ...rest }) => rest
4412
+ );
4413
+ }
4414
+ return result;
4415
+ }
4416
+ },
4102
4417
  {
4103
4418
  name: "earn_savings_purchase",
4104
4419
  module: "earn.savings",
@@ -4243,10 +4558,112 @@ function registerEarnTools() {
4243
4558
  return normalizeResponse(response);
4244
4559
  }
4245
4560
  },
4561
+ {
4562
+ name: "earn_fixed_purchase",
4563
+ module: "earn.savings",
4564
+ description: "Purchase Simple Earn Fixed (\u5B9A\u671F) product, two-step flow. First call (confirm omitted or false): returns purchase preview with product details and risk warning. Preview offer fields: lendQuota = remaining quota (\u5269\u4F59\u989D\u5EA6), soldOut = whether product is sold out (lendQuota is 0). YOU MUST display the 'warning' field from the preview response to the user VERBATIM before asking for confirmation \u2014 do NOT omit or summarize it. Second call (confirm=true): executes the purchase. Only proceed after the user explicitly confirms. IMPORTANT: Orders in 'pending' (\u5339\u914D\u4E2D) state can still be cancelled via earn_fixed_redeem; once the status changes to 'earning' (\u8D5A\u5E01\u4E2D), funds are LOCKED until maturity \u2014 no early redemption allowed.",
4565
+ isWrite: true,
4566
+ inputSchema: {
4567
+ type: "object",
4568
+ properties: {
4569
+ ccy: {
4570
+ type: "string",
4571
+ description: "Currency, e.g. USDT"
4572
+ },
4573
+ amt: {
4574
+ type: "string",
4575
+ description: "Purchase amount"
4576
+ },
4577
+ term: {
4578
+ type: "string",
4579
+ description: "Term, e.g. 90D"
4580
+ },
4581
+ confirm: {
4582
+ type: "boolean",
4583
+ description: "Omit or false on the first call to preview the purchase details; set to true on the second call to execute after user confirms."
4584
+ }
4585
+ },
4586
+ required: ["ccy", "amt", "term"]
4587
+ },
4588
+ handler: async (rawArgs, context) => {
4589
+ const args = asRecord(rawArgs);
4590
+ const ccy = requireString(args, "ccy");
4591
+ const amt = requireString(args, "amt");
4592
+ const term = requireString(args, "term");
4593
+ const confirm = readBoolean(args, "confirm") ?? false;
4594
+ if (!confirm) {
4595
+ const [rateResponse, fixedResponse] = await Promise.all([
4596
+ context.client.publicGet(
4597
+ "/api/v5/finance/savings/lending-rate-history",
4598
+ compactObject({ ccy, limit: 1 }),
4599
+ publicRateLimit("earn_get_lending_rate_history", 6)
4600
+ ),
4601
+ context.client.privateGet(
4602
+ "/api/v5/finance/simple-earn-fixed/offers",
4603
+ compactObject({ ccy }),
4604
+ privateRateLimit("earn_fixed_purchase_preview_offers", 2)
4605
+ ).catch(() => null)
4606
+ ]);
4607
+ const rateResult = normalizeResponse(rateResponse);
4608
+ const fixedResult = fixedResponse ? normalizeResponse(fixedResponse) : { data: [] };
4609
+ const rateArr = Array.isArray(rateResult["data"]) ? rateResult["data"] : [];
4610
+ const allOffers = Array.isArray(fixedResult["data"]) ? fixedResult["data"] : [];
4611
+ const matchedOffer = allOffers.find(
4612
+ (o) => o["term"] === term && o["ccy"] === ccy
4613
+ );
4614
+ const { borrowingOrderQuota: _, ...offerWithoutTotal } = matchedOffer ?? {};
4615
+ const offerWithSoldOut = matchedOffer ? { ...offerWithoutTotal, soldOut: offerWithoutTotal["lendQuota"] === "0" } : null;
4616
+ return {
4617
+ preview: true,
4618
+ ccy,
4619
+ amt,
4620
+ term,
4621
+ offer: offerWithSoldOut,
4622
+ currentFlexibleRate: rateArr[0]?.["lendingRate"] ?? null,
4623
+ warning: "\u26A0\uFE0F Orders still in 'pending' state can be cancelled before matching completes. Once the status changes to 'earning', funds are LOCKED until maturity \u2014 early redemption is NOT allowed. Please call again with confirm=true to proceed."
4624
+ };
4625
+ }
4626
+ assertNotDemo(context.config, "earn_fixed_purchase");
4627
+ const response = await context.client.privatePost(
4628
+ "/api/v5/finance/simple-earn-fixed/purchase",
4629
+ { ccy, amt, term },
4630
+ privateRateLimit("earn_fixed_purchase", 2)
4631
+ );
4632
+ return normalizeResponse(response);
4633
+ }
4634
+ },
4635
+ {
4636
+ name: "earn_fixed_redeem",
4637
+ module: "earn.savings",
4638
+ description: "Redeem Simple Earn Fixed (\u5B9A\u671F\u8D5A\u5E01) order. [CAUTION] Redeems a fixed-term lending order. Always redeems the full order amount. Only orders in 'pending' (\u5339\u914D\u4E2D) state can be redeemed \u2014 orders in 'earning' state are locked until maturity and cannot be redeemed early. Do NOT use for flexible earn redemption \u2014 use earn_savings_redeem instead.",
4639
+ isWrite: true,
4640
+ inputSchema: {
4641
+ type: "object",
4642
+ properties: {
4643
+ reqId: {
4644
+ type: "string",
4645
+ description: "Request ID of the fixed-term order to redeem"
4646
+ }
4647
+ },
4648
+ required: ["reqId"]
4649
+ },
4650
+ handler: async (rawArgs, context) => {
4651
+ assertNotDemo(context.config, "earn_fixed_redeem");
4652
+ const args = asRecord(rawArgs);
4653
+ const response = await context.client.privatePost(
4654
+ "/api/v5/finance/simple-earn-fixed/redeem",
4655
+ {
4656
+ reqId: requireString(args, "reqId")
4657
+ },
4658
+ privateRateLimit("earn_fixed_redeem", 2)
4659
+ );
4660
+ return normalizeResponse(response);
4661
+ }
4662
+ },
4246
4663
  {
4247
4664
  name: "earn_get_lending_rate_history",
4248
4665
  module: "earn.savings",
4249
- description: "Query Simple Earn lending rates. Public endpoint (no API key required). Use this tool when the user asks about current or historical lending rates for Simple Earn, or when displaying savings balance with market rate context. Response fields per record: rate (market lending rate \u2014 the rate borrowers pay this period; user's minimum setting must be \u2264 this to be eligible), lendingRate (actual yield received by lenders; stablecoins e.g. USDT/USDC only: subject to pro-rata dilution \u2014 when eligible supply exceeds borrowing demand total interest is shared so lendingRate < rate; non-stablecoins: lendingRate = rate, no dilution; always use lendingRate as the true APY to show users), ts (settlement timestamp ms). To get current APY: use limit=1 and read lendingRate.",
4666
+ description: "Query Simple Earn lending rates and fixed-term offers. Use this tool when the user asks about Simple Earn products, current or historical lending rates, or when displaying savings balance with market rate context (\u5E02\u573A\u5747\u5229\u7387). Returns lending rate history (lendingRate field, newest-first) AND available fixed-term (\u5B9A\u671F) offers with APR, term, min amount, and quota \u2014 one call gives a complete view of both flexible and fixed options. In fixedOffers: lendQuota = remaining quota (\u5269\u4F59\u989D\u5EA6), soldOut = whether product is sold out (lendQuota is 0). To get current flexible APY: use limit=1 and read lendingRate.",
4250
4667
  isWrite: false,
4251
4668
  inputSchema: {
4252
4669
  type: "object",
@@ -4265,23 +4682,45 @@ function registerEarnTools() {
4265
4682
  },
4266
4683
  limit: {
4267
4684
  type: "number",
4268
- description: "Max results (default 100)"
4685
+ description: "Max results (default 7)"
4269
4686
  }
4270
4687
  }
4271
4688
  },
4272
4689
  handler: async (rawArgs, context) => {
4273
4690
  const args = asRecord(rawArgs);
4274
- const response = await context.client.publicGet(
4275
- "/api/v5/finance/savings/lending-rate-history",
4276
- compactObject({
4277
- ccy: readString(args, "ccy"),
4278
- after: readString(args, "after"),
4279
- before: readString(args, "before"),
4280
- limit: readNumber(args, "limit")
4281
- }),
4282
- publicRateLimit("earn_get_lending_rate_history", 6)
4283
- );
4284
- return normalizeResponse(response);
4691
+ const ccy = readString(args, "ccy");
4692
+ const [rateResponse, fixedResponse] = await Promise.all([
4693
+ context.client.publicGet(
4694
+ "/api/v5/finance/savings/lending-rate-history",
4695
+ compactObject({
4696
+ ccy,
4697
+ after: readString(args, "after"),
4698
+ before: readString(args, "before"),
4699
+ limit: readNumber(args, "limit") ?? 7
4700
+ }),
4701
+ publicRateLimit("earn_get_lending_rate_history", 6)
4702
+ ),
4703
+ context.client.privateGet(
4704
+ "/api/v5/finance/simple-earn-fixed/offers",
4705
+ compactObject({ ccy }),
4706
+ privateRateLimit("earn_get_lending_rate_history_fixed", 2)
4707
+ ).catch(() => null)
4708
+ ]);
4709
+ const rateResult = normalizeResponse(rateResponse);
4710
+ const rateData = Array.isArray(rateResult["data"]) ? rateResult["data"].map(
4711
+ ({ rate: _, ...rest }) => rest
4712
+ ) : [];
4713
+ const fixedResult = fixedResponse ? normalizeResponse(fixedResponse) : { data: [] };
4714
+ const allOffers = fixedResult["data"] ?? [];
4715
+ const fixedOffers = allOffers.map(({ borrowingOrderQuota: _, ...rest }) => ({
4716
+ ...rest,
4717
+ soldOut: rest["lendQuota"] === "0"
4718
+ }));
4719
+ return {
4720
+ ...rateResult,
4721
+ data: rateData,
4722
+ fixedOffers
4723
+ };
4285
4724
  }
4286
4725
  }
4287
4726
  ];
@@ -4620,7 +5059,7 @@ function registerDcdTools() {
4620
5059
  {
4621
5060
  name: "dcd_get_products",
4622
5061
  module: "earn.dcd",
4623
- description: "Get DCD products with yield and quota info.",
5062
+ description: "Get DCD products with yield and quota info. Yields in response are decimal fractions, not percentages.",
4624
5063
  isWrite: false,
4625
5064
  inputSchema: {
4626
5065
  type: "object",
@@ -4674,7 +5113,7 @@ function registerDcdTools() {
4674
5113
  {
4675
5114
  name: "dcd_get_orders",
4676
5115
  module: "earn.dcd",
4677
- description: "Get DCD order history.",
5116
+ description: "Get DCD order history. Yields in response are decimal fractions, not percentages.",
4678
5117
  isWrite: false,
4679
5118
  inputSchema: {
4680
5119
  type: "object",
@@ -4718,7 +5157,7 @@ function registerDcdTools() {
4718
5157
  {
4719
5158
  name: "dcd_subscribe",
4720
5159
  module: "earn.dcd",
4721
- description: "Subscribe to a DCD product: get quote and execute atomically. Confirm product, amount, and currency with user before calling. Optional minAnnualizedYield (percent) rejects the order if quote yield falls below threshold. Returns order result with quote snapshot (annualizedYield, absYield).",
5160
+ description: "Subscribe to a DCD product: get quote and execute atomically. Confirm product, amount, and currency with user before calling. Optional minAnnualizedYield rejects the order if quote yield falls below threshold. Returns order result with quote snapshot (minAnnualizedYield is in percent; response yields are decimal fractions).",
4722
5161
  isWrite: true,
4723
5162
  inputSchema: {
4724
5163
  type: "object",
@@ -4757,13 +5196,23 @@ function registerDcdTools() {
4757
5196
  });
4758
5197
  }
4759
5198
  if (minAnnualizedYield !== void 0) {
4760
- const actualYield = parseFloat(quote["annualizedYield"]);
4761
- if (!isNaN(actualYield) && actualYield < minAnnualizedYield) {
5199
+ const rawYield = parseFloat(quote["annualizedYield"]);
5200
+ if (isNaN(rawYield)) {
4762
5201
  throw new OkxApiError(
4763
- `Quote yield ${actualYield}% is below the minimum threshold of ${minAnnualizedYield}%.`,
5202
+ "Quote returned non-numeric annualizedYield, cannot verify minimum yield threshold.",
5203
+ {
5204
+ code: "INVALID_YIELD_VALUE",
5205
+ suggestion: "Order not placed. The quote did not include a valid annualizedYield. Retry or pick a different product."
5206
+ }
5207
+ );
5208
+ }
5209
+ const actualYieldPct = rawYield * 100;
5210
+ if (actualYieldPct < minAnnualizedYield) {
5211
+ throw new OkxApiError(
5212
+ `Quote yield ${actualYieldPct.toFixed(2)}% is below the minimum threshold of ${minAnnualizedYield}%.`,
4764
5213
  {
4765
5214
  code: "YIELD_BELOW_MIN",
4766
- suggestion: `Order not placed. Actual: ${actualYield}%, required: >= ${minAnnualizedYield}%. Try a different product or lower your minimum yield.`
5215
+ suggestion: `Order not placed. Actual: ${actualYieldPct.toFixed(2)}%, required: >= ${minAnnualizedYield}%. Try a different product or lower your minimum yield.`
4767
5216
  }
4768
5217
  );
4769
5218
  }
@@ -4908,7 +5357,7 @@ function registerAutoEarnTools() {
4908
5357
  }
4909
5358
  var EARN_DEMO_MESSAGE = "Earn features (savings, DCD, on-chain staking, auto-earn) are not available in simulated trading mode.";
4910
5359
  var EARN_DEMO_SUGGESTION = "Switch to a live account to use Earn features.";
4911
- var DEMO_GUARD_SKIP = /* @__PURE__ */ new Set(["dcd_redeem"]);
5360
+ var DEMO_GUARD_SKIP = /* @__PURE__ */ new Set(["dcd_redeem", "earn_fixed_purchase"]);
4912
5361
  function withDemoGuard(tool) {
4913
5362
  if (!tool.isWrite || DEMO_GUARD_SKIP.has(tool.name)) return tool;
4914
5363
  const originalHandler = tool.handler;
@@ -4969,12 +5418,12 @@ function buildContractTradeTools(cfg) {
4969
5418
  },
4970
5419
  sz: {
4971
5420
  type: "string",
4972
- description: "Number of contracts. Each contract = ctVal units (e.g. 0.1 ETH for ETH-USDT-SWAP). Query market_get_instruments for exact ctVal. Set tgtCcy=quote_ccy to specify sz in USDT instead."
5421
+ description: "Number of contracts. Each contract = ctVal units (e.g. 0.1 ETH for ETH-USDT-SWAP). Query market_get_instruments for exact ctVal. Set tgtCcy=quote_ccy to specify sz in USDT notional value; set tgtCcy=margin to specify sz as margin cost (notional = sz * leverage)."
4973
5422
  },
4974
5423
  tgtCcy: {
4975
5424
  type: "string",
4976
- enum: ["base_ccy", "quote_ccy"],
4977
- description: "Size unit. base_ccy(default): sz in contracts, quote_ccy: sz in USDT"
5425
+ enum: ["base_ccy", "quote_ccy", "margin"],
5426
+ description: "Size unit. base_ccy(default): sz in contracts; quote_ccy: sz in USDT notional value; margin: sz in USDT margin cost (actual position = sz * leverage)"
4978
5427
  },
4979
5428
  px: { type: "string", description: "Required for limit/post_only/fok/ioc" },
4980
5429
  reduceOnly: {
@@ -4993,6 +5442,14 @@ function buildContractTradeTools(cfg) {
4993
5442
  const args = asRecord(rawArgs);
4994
5443
  const reduceOnly = args.reduceOnly;
4995
5444
  const attachAlgoOrds = buildAttachAlgoOrds(args);
5445
+ const resolved = await resolveQuoteCcySz(
5446
+ requireString(args, "instId"),
5447
+ requireString(args, "sz"),
5448
+ readString(args, "tgtCcy"),
5449
+ defaultType,
5450
+ context.client,
5451
+ readString(args, "tdMode")
5452
+ );
4996
5453
  const response = await context.client.privatePost(
4997
5454
  "/api/v5/trade/order",
4998
5455
  compactObject({
@@ -5001,8 +5458,8 @@ function buildContractTradeTools(cfg) {
5001
5458
  side: requireString(args, "side"),
5002
5459
  posSide: readString(args, "posSide"),
5003
5460
  ordType: requireString(args, "ordType"),
5004
- sz: requireString(args, "sz"),
5005
- tgtCcy: readString(args, "tgtCcy"),
5461
+ sz: resolved.sz,
5462
+ tgtCcy: resolved.tgtCcy,
5006
5463
  px: readString(args, "px"),
5007
5464
  reduceOnly: typeof reduceOnly === "boolean" ? String(reduceOnly) : void 0,
5008
5465
  clOrdId: readString(args, "clOrdId"),
@@ -5011,7 +5468,11 @@ function buildContractTradeTools(cfg) {
5011
5468
  }),
5012
5469
  privateRateLimit(n("place_order"), 60)
5013
5470
  );
5014
- return normalizeResponse(response);
5471
+ const result = normalizeResponse(response);
5472
+ if (resolved.conversionNote) {
5473
+ result._conversion = resolved.conversionNote;
5474
+ }
5475
+ return result;
5015
5476
  }
5016
5477
  },
5017
5478
  // ── cancel_order ─────────────────────────────────────────────────────────
@@ -6085,8 +6546,8 @@ function registerOptionAlgoTools() {
6085
6546
  },
6086
6547
  tgtCcy: {
6087
6548
  type: "string",
6088
- enum: ["base_ccy", "quote_ccy"],
6089
- description: "Size unit. base_ccy(default): sz in contracts, quote_ccy: sz in USDT (may not be supported for options)"
6549
+ enum: ["base_ccy", "quote_ccy", "margin"],
6550
+ description: "Size unit. base_ccy(default): sz in contracts; quote_ccy: sz in USDT notional value; margin: sz in USDT margin cost (actual position = sz * leverage)"
6090
6551
  },
6091
6552
  reduceOnly: {
6092
6553
  type: "boolean",
@@ -6102,6 +6563,14 @@ function registerOptionAlgoTools() {
6102
6563
  handler: async (rawArgs, context) => {
6103
6564
  const args = asRecord(rawArgs);
6104
6565
  const reduceOnly = readBoolean(args, "reduceOnly");
6566
+ const resolved = await resolveQuoteCcySz(
6567
+ requireString(args, "instId"),
6568
+ requireString(args, "sz"),
6569
+ readString(args, "tgtCcy"),
6570
+ "OPTION",
6571
+ context.client,
6572
+ readString(args, "tdMode")
6573
+ );
6105
6574
  const response = await context.client.privatePost(
6106
6575
  "/api/v5/trade/order-algo",
6107
6576
  compactObject({
@@ -6109,8 +6578,8 @@ function registerOptionAlgoTools() {
6109
6578
  tdMode: requireString(args, "tdMode"),
6110
6579
  side: requireString(args, "side"),
6111
6580
  ordType: requireString(args, "ordType"),
6112
- sz: requireString(args, "sz"),
6113
- tgtCcy: readString(args, "tgtCcy"),
6581
+ sz: resolved.sz,
6582
+ tgtCcy: resolved.tgtCcy,
6114
6583
  tpTriggerPx: readString(args, "tpTriggerPx"),
6115
6584
  tpOrdPx: readString(args, "tpOrdPx"),
6116
6585
  tpTriggerPxType: readString(args, "tpTriggerPxType"),
@@ -6317,7 +6786,12 @@ function registerOptionTools() {
6317
6786
  },
6318
6787
  sz: {
6319
6788
  type: "string",
6320
- description: "Contracts count (NOT USDT). Use market_get_instruments for ctVal."
6789
+ description: "Contracts count by default. Set tgtCcy=quote_ccy to specify USDT notional value; set tgtCcy=margin to specify USDT margin cost (notional = sz * leverage)."
6790
+ },
6791
+ tgtCcy: {
6792
+ type: "string",
6793
+ enum: ["base_ccy", "quote_ccy", "margin"],
6794
+ description: "Size unit. base_ccy(default): sz in contracts; quote_ccy: sz in USDT notional value; margin: sz in USDT margin cost (actual position = sz * leverage)"
6321
6795
  },
6322
6796
  px: {
6323
6797
  type: "string",
@@ -6354,6 +6828,14 @@ function registerOptionTools() {
6354
6828
  const args = asRecord(rawArgs);
6355
6829
  const reduceOnly = args.reduceOnly;
6356
6830
  const attachAlgoOrds = buildAttachAlgoOrds(args);
6831
+ const resolved = await resolveQuoteCcySz(
6832
+ requireString(args, "instId"),
6833
+ requireString(args, "sz"),
6834
+ readString(args, "tgtCcy"),
6835
+ "OPTION",
6836
+ context.client,
6837
+ readString(args, "tdMode")
6838
+ );
6357
6839
  const response = await context.client.privatePost(
6358
6840
  "/api/v5/trade/order",
6359
6841
  compactObject({
@@ -6361,7 +6843,8 @@ function registerOptionTools() {
6361
6843
  tdMode: requireString(args, "tdMode"),
6362
6844
  side: requireString(args, "side"),
6363
6845
  ordType: requireString(args, "ordType"),
6364
- sz: requireString(args, "sz"),
6846
+ sz: resolved.sz,
6847
+ tgtCcy: resolved.tgtCcy,
6365
6848
  px: readString(args, "px"),
6366
6849
  reduceOnly: typeof reduceOnly === "boolean" ? String(reduceOnly) : void 0,
6367
6850
  clOrdId: readString(args, "clOrdId"),
@@ -7751,6 +8234,69 @@ Run: npm install -g ${packageName}
7751
8234
  refreshCacheInBackground(packageName);
7752
8235
  }
7753
8236
  }
8237
+ var LOG_LEVEL_PRIORITY = {
8238
+ error: 0,
8239
+ warn: 1,
8240
+ info: 2,
8241
+ debug: 3
8242
+ };
8243
+ var SENSITIVE_KEY_PATTERN = /apiKey|secretKey|passphrase|password|secret/i;
8244
+ function sanitize(value) {
8245
+ if (value === null || value === void 0) {
8246
+ return value;
8247
+ }
8248
+ if (Array.isArray(value)) {
8249
+ return value.map(sanitize);
8250
+ }
8251
+ if (typeof value === "object") {
8252
+ const result = {};
8253
+ for (const [k, v] of Object.entries(value)) {
8254
+ if (SENSITIVE_KEY_PATTERN.test(k)) {
8255
+ result[k] = "[REDACTED]";
8256
+ } else {
8257
+ result[k] = sanitize(v);
8258
+ }
8259
+ }
8260
+ return result;
8261
+ }
8262
+ return value;
8263
+ }
8264
+ var TradeLogger = class {
8265
+ logLevel;
8266
+ logDir;
8267
+ constructor(logLevel = "info", logDir) {
8268
+ this.logLevel = logLevel;
8269
+ this.logDir = logDir ?? path2.join(os2.homedir(), ".okx", "logs");
8270
+ }
8271
+ getLogPath(date) {
8272
+ const d = date ?? /* @__PURE__ */ new Date();
8273
+ const yyyy = d.getUTCFullYear();
8274
+ const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
8275
+ const dd = String(d.getUTCDate()).padStart(2, "0");
8276
+ return path2.join(this.logDir, `trade-${yyyy}-${mm}-${dd}.log`);
8277
+ }
8278
+ log(level, tool, params, result, durationMs) {
8279
+ if (LOG_LEVEL_PRIORITY[level] > LOG_LEVEL_PRIORITY[this.logLevel]) {
8280
+ return;
8281
+ }
8282
+ const entry = {
8283
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8284
+ level: level.toUpperCase(),
8285
+ tool,
8286
+ durationMs,
8287
+ params: sanitize(params),
8288
+ result: sanitize(result)
8289
+ };
8290
+ try {
8291
+ fs2.mkdirSync(this.logDir, { recursive: true });
8292
+ fs2.appendFileSync(this.getLogPath(), JSON.stringify(entry) + "\n", "utf8");
8293
+ } catch {
8294
+ }
8295
+ }
8296
+ static sanitize(params) {
8297
+ return sanitize(params);
8298
+ }
8299
+ };
7754
8300
  var CLIENT_NAMES = {
7755
8301
  "claude-desktop": "Claude Desktop",
7756
8302
  cursor: "Cursor",
@@ -7900,11 +8446,11 @@ function runSetup(options) {
7900
8446
  // src/commands/diagnose.ts
7901
8447
  import dns from "dns/promises";
7902
8448
  import net from "net";
7903
- import os4 from "os";
8449
+ import os5 from "os";
7904
8450
  import tls from "tls";
7905
8451
 
7906
8452
  // src/commands/diagnose-utils.ts
7907
- import fs2 from "fs";
8453
+ import fs4 from "fs";
7908
8454
  import { createRequire } from "module";
7909
8455
 
7910
8456
  // src/formatter.ts
@@ -7917,6 +8463,10 @@ var activeOutput = stdioOutput;
7917
8463
  function setOutput(impl) {
7918
8464
  activeOutput = impl;
7919
8465
  }
8466
+ var envContext = null;
8467
+ function setEnvContext(ctx) {
8468
+ envContext = ctx;
8469
+ }
7920
8470
  function output(message) {
7921
8471
  activeOutput.out(message);
7922
8472
  }
@@ -7929,10 +8479,23 @@ function outputLine(message) {
7929
8479
  function errorLine(message) {
7930
8480
  activeOutput.err(message + EOL);
7931
8481
  }
8482
+ var jsonEnvEnabled = false;
8483
+ function setJsonEnvEnabled(enabled) {
8484
+ jsonEnvEnabled = enabled;
8485
+ }
7932
8486
  function printJson(data) {
7933
- activeOutput.out(JSON.stringify(data, null, 2) + EOL);
8487
+ const payload = jsonEnvEnabled && envContext ? {
8488
+ env: envContext.demo ? "demo" : "live",
8489
+ profile: envContext.profile,
8490
+ data
8491
+ } : data;
8492
+ activeOutput.out(JSON.stringify(payload, null, 2) + EOL);
7934
8493
  }
7935
8494
  function printTable(rows) {
8495
+ if (envContext) {
8496
+ const envLabel = envContext.demo ? "demo (simulated trading)" : "live";
8497
+ activeOutput.out(`Environment: ${envLabel}` + EOL + EOL);
8498
+ }
7936
8499
  if (rows.length === 0) {
7937
8500
  activeOutput.out("(no data)" + EOL);
7938
8501
  return;
@@ -8009,7 +8572,7 @@ var Report = class {
8009
8572
  lines.push(`${key.padEnd(14)} ${value}`);
8010
8573
  }
8011
8574
  lines.push(sep2, "");
8012
- fs2.writeFileSync(filePath, lines.join("\n"), "utf8");
8575
+ fs4.writeFileSync(filePath, lines.join("\n"), "utf8");
8013
8576
  return true;
8014
8577
  } catch (_e) {
8015
8578
  return false;
@@ -8044,7 +8607,7 @@ function writeReportIfRequested(report, outputPath) {
8044
8607
  errorLine(` Warning: failed to write report to: ${outputPath}`);
8045
8608
  }
8046
8609
  }
8047
- function sanitize(value) {
8610
+ function sanitize2(value) {
8048
8611
  value = value.replace(
8049
8612
  /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi,
8050
8613
  "****-uuid-****"
@@ -8055,14 +8618,14 @@ function sanitize(value) {
8055
8618
  }
8056
8619
 
8057
8620
  // src/commands/diagnose-mcp.ts
8058
- import fs4 from "fs";
8059
- import path2 from "path";
8060
- import os2 from "os";
8621
+ import fs5 from "fs";
8622
+ import path4 from "path";
8623
+ import os4 from "os";
8061
8624
  import { spawnSync, spawn } from "child_process";
8062
8625
  import { createRequire as createRequire2 } from "module";
8063
8626
  import { fileURLToPath } from "url";
8064
8627
  var _require2 = createRequire2(import.meta.url);
8065
- var __dirname = path2.dirname(fileURLToPath(import.meta.url));
8628
+ var __dirname = path4.dirname(fileURLToPath(import.meta.url));
8066
8629
  function readMcpVersion() {
8067
8630
  const candidates = [
8068
8631
  // Installed as global or local dependency
@@ -8115,13 +8678,13 @@ function checkMcpEntryPoint(report) {
8115
8678
  if (!entryPath) {
8116
8679
  const candidates = [
8117
8680
  // Installed locally
8118
- path2.join(process.cwd(), "node_modules", ".bin", "okx-trade-mcp"),
8681
+ path4.join(process.cwd(), "node_modules", ".bin", "okx-trade-mcp"),
8119
8682
  // Monorepo workspace (e.g. running from source)
8120
- path2.join(__dirname, "..", "..", "..", "..", "mcp", "dist", "index.js")
8683
+ path4.join(__dirname, "..", "..", "..", "..", "mcp", "dist", "index.js")
8121
8684
  ];
8122
8685
  for (const candidate of candidates) {
8123
8686
  try {
8124
- fs4.accessSync(candidate, fs4.constants.X_OK | fs4.constants.R_OK);
8687
+ fs5.accessSync(candidate, fs5.constants.X_OK | fs5.constants.R_OK);
8125
8688
  entryPath = candidate;
8126
8689
  break;
8127
8690
  } catch (_e) {
@@ -8146,9 +8709,9 @@ var CLIENT_LIMITS = {
8146
8709
  cursor: { perServer: 40, total: 80 }
8147
8710
  };
8148
8711
  function checkJsonMcpConfig(configPath) {
8149
- if (!fs4.existsSync(configPath)) return "missing";
8712
+ if (!fs5.existsSync(configPath)) return "missing";
8150
8713
  try {
8151
- const raw = fs4.readFileSync(configPath, "utf8");
8714
+ const raw = fs5.readFileSync(configPath, "utf8");
8152
8715
  const parsed = JSON.parse(raw);
8153
8716
  const mcpServers = parsed["mcpServers"];
8154
8717
  if (!mcpServers) return "not-configured";
@@ -8166,15 +8729,15 @@ function checkJsonMcpConfig(configPath) {
8166
8729
  }
8167
8730
  }
8168
8731
  function checkClaudeCodeConfig() {
8169
- const home = os2.homedir();
8732
+ const home = os4.homedir();
8170
8733
  const candidates = [
8171
- path2.join(home, ".claude", "settings.json"),
8172
- path2.join(home, ".claude.json")
8734
+ path4.join(home, ".claude", "settings.json"),
8735
+ path4.join(home, ".claude.json")
8173
8736
  ];
8174
8737
  let anyFound = false;
8175
8738
  let anyParseError = false;
8176
8739
  for (const cfgPath of candidates) {
8177
- if (!fs4.existsSync(cfgPath)) continue;
8740
+ if (!fs5.existsSync(cfgPath)) continue;
8178
8741
  anyFound = true;
8179
8742
  const result = checkJsonMcpConfig(cfgPath);
8180
8743
  if (result === "found") return "found";
@@ -8191,8 +8754,8 @@ function handleJsonClient(clientId, report, configuredClients) {
8191
8754
  const status = checkJsonMcpConfig(configPath);
8192
8755
  if (status === "missing") return false;
8193
8756
  if (status === "found") {
8194
- ok(name, `configured (${sanitize(configPath)})`);
8195
- report.add(`client_${clientId}`, `OK ${sanitize(configPath)}`);
8757
+ ok(name, `configured (${sanitize2(configPath)})`);
8758
+ report.add(`client_${clientId}`, `OK ${sanitize2(configPath)}`);
8196
8759
  configuredClients.push(clientId);
8197
8760
  return false;
8198
8761
  }
@@ -8200,8 +8763,8 @@ function handleJsonClient(clientId, report, configuredClients) {
8200
8763
  fail(name, "okx-trade-mcp not found in mcpServers", [`Run: okx setup --client ${clientId}`]);
8201
8764
  report.add(`client_${clientId}`, "NOT_CONFIGURED");
8202
8765
  } else {
8203
- fail(name, `JSON parse error in ${sanitize(configPath)}`, [
8204
- `Check ${sanitize(configPath)} for JSON syntax errors`,
8766
+ fail(name, `JSON parse error in ${sanitize2(configPath)}`, [
8767
+ `Check ${sanitize2(configPath)} for JSON syntax errors`,
8205
8768
  `Then run: okx setup --client ${clientId}`
8206
8769
  ]);
8207
8770
  report.add(`client_${clientId}`, "PARSE_ERROR");
@@ -8294,37 +8857,37 @@ function checkToolCount(report, configuredClients, getSpecs = allToolSpecs) {
8294
8857
  }
8295
8858
  }
8296
8859
  function readLogTail(logPath) {
8297
- const stat = fs4.statSync(logPath);
8860
+ const stat = fs5.statSync(logPath);
8298
8861
  const readSize = Math.min(8192, stat.size);
8299
8862
  const buffer = Buffer.alloc(readSize);
8300
- const fd = fs4.openSync(logPath, "r");
8863
+ const fd = fs5.openSync(logPath, "r");
8301
8864
  try {
8302
- fs4.readSync(fd, buffer, 0, readSize, Math.max(0, stat.size - readSize));
8865
+ fs5.readSync(fd, buffer, 0, readSize, Math.max(0, stat.size - readSize));
8303
8866
  } finally {
8304
- fs4.closeSync(fd);
8867
+ fs5.closeSync(fd);
8305
8868
  }
8306
8869
  return buffer.toString("utf8").split("\n").filter((l) => l.trim()).slice(-5);
8307
8870
  }
8308
8871
  function getMcpLogCandidates() {
8309
8872
  if (process.platform === "darwin") {
8310
- const logsDir = path2.join(os2.homedir(), "Library", "Logs", "Claude");
8873
+ const logsDir = path4.join(os4.homedir(), "Library", "Logs", "Claude");
8311
8874
  const candidates = [
8312
- path2.join(logsDir, "mcp.log"),
8313
- path2.join(logsDir, "mcp-server-okx-trade-mcp.log")
8875
+ path4.join(logsDir, "mcp.log"),
8876
+ path4.join(logsDir, "mcp-server-okx-trade-mcp.log")
8314
8877
  ];
8315
8878
  try {
8316
- const extra = fs4.readdirSync(logsDir).filter((f) => f.startsWith("mcp") && f.endsWith(".log")).map((f) => path2.join(logsDir, f));
8879
+ const extra = fs5.readdirSync(logsDir).filter((f) => f.startsWith("mcp") && f.endsWith(".log")).map((f) => path4.join(logsDir, f));
8317
8880
  candidates.push(...extra);
8318
8881
  } catch (_e) {
8319
8882
  }
8320
8883
  return candidates;
8321
8884
  }
8322
8885
  if (process.platform === "win32") {
8323
- const appData2 = process.env.APPDATA ?? path2.join(os2.homedir(), "AppData", "Roaming");
8324
- return [path2.join(appData2, "Claude", "logs", "mcp.log")];
8886
+ const appData2 = process.env.APPDATA ?? path4.join(os4.homedir(), "AppData", "Roaming");
8887
+ return [path4.join(appData2, "Claude", "logs", "mcp.log")];
8325
8888
  }
8326
- const configHome = process.env.XDG_CONFIG_HOME ?? path2.join(os2.homedir(), ".config");
8327
- return [path2.join(configHome, "Claude", "logs", "mcp.log")];
8889
+ const configHome = process.env.XDG_CONFIG_HOME ?? path4.join(os4.homedir(), ".config");
8890
+ return [path4.join(configHome, "Claude", "logs", "mcp.log")];
8328
8891
  }
8329
8892
  function checkMcpLogs(report) {
8330
8893
  section("MCP Server Logs (recent)");
@@ -8338,7 +8901,7 @@ function checkMcpLogs(report) {
8338
8901
  report.add("mcp_log", logPath);
8339
8902
  if (lines.length > 0) {
8340
8903
  ok("last lines", `(${lines.length} shown)`);
8341
- for (const line of lines) outputLine(` ${sanitize(line)}`);
8904
+ for (const line of lines) outputLine(` ${sanitize2(line)}`);
8342
8905
  } else {
8343
8906
  ok("last lines", "(empty log)");
8344
8907
  }
@@ -8464,11 +9027,11 @@ function checkModuleLoading(entryPath, report) {
8464
9027
  return true;
8465
9028
  } else {
8466
9029
  const errMsg = result.stderr?.trim() || result.error?.message || "non-zero exit";
8467
- fail("module load", `failed: ${sanitize(errMsg)}`, [
9030
+ fail("module load", `failed: ${sanitize2(errMsg)}`, [
8468
9031
  "MCP server may have import errors or missing dependencies",
8469
9032
  `Try: node ${entryPath} --version`
8470
9033
  ]);
8471
- report.add("module_load", `FAIL ${sanitize(errMsg)}`);
9034
+ report.add("module_load", `FAIL ${sanitize2(errMsg)}`);
8472
9035
  return false;
8473
9036
  }
8474
9037
  }
@@ -8479,7 +9042,7 @@ async function cmdDiagnoseMcp(options = {}) {
8479
9042
  const report = new Report();
8480
9043
  report.add("ts", (/* @__PURE__ */ new Date()).toISOString());
8481
9044
  report.add("mode", "mcp");
8482
- report.add("os", `${process.platform} ${process.arch} ${os2.release()}`);
9045
+ report.add("os", `${process.platform} ${process.arch} ${os4.release()}`);
8483
9046
  checkMcpPackageVersion(report);
8484
9047
  const nodePassed = checkNodeCompat(report);
8485
9048
  const { entryPath, passed: entryPassed } = checkMcpEntryPoint(report);
@@ -8511,7 +9074,7 @@ async function cmdDiagnoseMcp(options = {}) {
8511
9074
 
8512
9075
  // src/commands/diagnose.ts
8513
9076
  var CLI_VERSION = readCliVersion();
8514
- var GIT_HASH = true ? "19e8da3" : "dev";
9077
+ var GIT_HASH = true ? "caa6dae" : "dev";
8515
9078
  function maskKey2(key) {
8516
9079
  if (!key) return "(not set)";
8517
9080
  if (key.length <= 8) return "****";
@@ -8591,14 +9154,14 @@ function checkEnvironment(report) {
8591
9154
  }
8592
9155
  ok("CLI", `v${CLI_VERSION} (${GIT_HASH})`);
8593
9156
  ok("OS", `${process.platform} ${process.arch}`);
8594
- ok("OS release", os4.release());
9157
+ ok("OS release", os5.release());
8595
9158
  ok("Shell", process.env.SHELL ?? "(unknown)");
8596
9159
  ok("Locale", `${process.env.LANG ?? process.env.LC_ALL ?? "(unknown)"}`);
8597
9160
  ok("Timezone", Intl.DateTimeFormat().resolvedOptions().timeZone);
8598
9161
  report.add("cli", `${CLI_VERSION} (${GIT_HASH})`);
8599
9162
  report.add("node", `${nodeVersion} ${process.platform} ${process.arch}`);
8600
- const machine = typeof os4.machine === "function" ? os4.machine() : process.arch;
8601
- report.add("os", `${os4.type()} ${os4.release()} ${machine}`);
9163
+ const machine = typeof os5.machine === "function" ? os5.machine() : process.arch;
9164
+ report.add("os", `${os5.type()} ${os5.release()} ${machine}`);
8602
9165
  report.add("shell", process.env.SHELL ?? "-");
8603
9166
  report.add("locale", process.env.LANG ?? process.env.LC_ALL ?? "-");
8604
9167
  report.add("tz", Intl.DateTimeFormat().resolvedOptions().timeZone);
@@ -8755,10 +9318,10 @@ async function cmdDiagnose(config, profile, options = {}) {
8755
9318
  }
8756
9319
  function checkConfigFile(report) {
8757
9320
  section("Config File");
8758
- const path5 = configFilePath();
9321
+ const path6 = configFilePath();
8759
9322
  try {
8760
9323
  readFullConfig();
8761
- ok("Config parse", `${path5} OK`);
9324
+ ok("Config parse", `${path6} OK`);
8762
9325
  report.add("config_parse", "OK");
8763
9326
  return true;
8764
9327
  } catch (e) {
@@ -8928,7 +9491,7 @@ function loadProfileConfig(opts) {
8928
9491
  import { EOL as EOL2 } from "os";
8929
9492
 
8930
9493
  // src/commands/client-setup.ts
8931
- import * as fs5 from "fs";
9494
+ import * as fs6 from "fs";
8932
9495
  var DETECTABLE_CLIENTS = ["claude-desktop", "cursor", "windsurf"];
8933
9496
  function cmdSetupClient(options) {
8934
9497
  runSetup(options);
@@ -8937,14 +9500,14 @@ function cmdSetupClients() {
8937
9500
  const detected = [];
8938
9501
  for (const id of DETECTABLE_CLIENTS) {
8939
9502
  const p = getConfigPath(id);
8940
- if (p && fs5.existsSync(p)) {
9503
+ if (p && fs6.existsSync(p)) {
8941
9504
  detected.push({ id, path: p });
8942
9505
  }
8943
9506
  }
8944
9507
  if (detected.length > 0) {
8945
9508
  outputLine("Detected clients:");
8946
- for (const { id, path: path5 } of detected) {
8947
- outputLine(` ${id.padEnd(16)} ${path5}`);
9509
+ for (const { id, path: path6 } of detected) {
9510
+ outputLine(` ${id.padEnd(16)} ${path6}`);
8948
9511
  }
8949
9512
  outputLine("");
8950
9513
  }
@@ -9328,7 +9891,7 @@ var HELP_TREE = {
9328
9891
  description: "Earn products \u2014 Simple Earn, On-chain Earn, and DCD (Dual Currency Deposit)",
9329
9892
  subgroups: {
9330
9893
  savings: {
9331
- description: "Simple Earn \u2014 flexible savings and lending",
9894
+ description: "Simple Earn \u2014 flexible savings, fixed-term, and lending",
9332
9895
  commands: {
9333
9896
  balance: {
9334
9897
  usage: "okx earn savings balance [<ccy>]",
@@ -9352,7 +9915,19 @@ var HELP_TREE = {
9352
9915
  },
9353
9916
  "rate-history": {
9354
9917
  usage: "okx earn savings rate-history [--ccy <ccy>] [--limit <n>]",
9355
- description: "Query Simple Earn lending rates (public, no auth needed)"
9918
+ description: "Query Simple Earn lending rates and fixed-term offers (requires auth)"
9919
+ },
9920
+ "fixed-orders": {
9921
+ usage: "okx earn savings fixed-orders [--ccy <ccy>] [--state <pending|earning|expired|settled|cancelled>]",
9922
+ description: "List fixed-term earn orders"
9923
+ },
9924
+ "fixed-purchase": {
9925
+ usage: "okx earn savings fixed-purchase --ccy <ccy> --amt <n> --term <term> [--confirm]",
9926
+ description: "Purchase Simple Earn Fixed (\u5B9A\u671F). Preview by default; add --confirm to execute. Funds locked until maturity"
9927
+ },
9928
+ "fixed-redeem": {
9929
+ usage: "okx earn savings fixed-redeem <reqId>",
9930
+ description: "Redeem a fixed-term earn order (full amount)"
9356
9931
  }
9357
9932
  }
9358
9933
  },
@@ -9532,6 +10107,7 @@ function printGlobalHelp() {
9532
10107
  " --demo Use simulated trading (demo) mode",
9533
10108
  " --live Force live trading mode (overrides profile demo=true; mutually exclusive with --demo)",
9534
10109
  " --json Output raw JSON",
10110
+ " --env With --json, wrap output as {env, profile, data}",
9535
10111
  " --verbose Show detailed network request/response info (stderr)",
9536
10112
  " --version, -v Show version",
9537
10113
  " --help Show this help",
@@ -9646,8 +10222,8 @@ function printCommandList(lines, commands) {
9646
10222
  lines.push("");
9647
10223
  }
9648
10224
  }
9649
- function printHelp(...path5) {
9650
- const [moduleName, subgroupName] = path5;
10225
+ function printHelp(...path6) {
10226
+ const [moduleName, subgroupName] = path6;
9651
10227
  if (!moduleName) {
9652
10228
  printGlobalHelp();
9653
10229
  } else if (!subgroupName) {
@@ -9663,6 +10239,7 @@ var CLI_OPTIONS = {
9663
10239
  profile: { type: "string" },
9664
10240
  demo: { type: "boolean", default: false },
9665
10241
  json: { type: "boolean", default: false },
10242
+ env: { type: "boolean", default: false },
9666
10243
  help: { type: "boolean", default: false },
9667
10244
  version: { type: "boolean", short: "v", default: false },
9668
10245
  // setup command
@@ -9768,6 +10345,8 @@ var CLI_OPTIONS = {
9768
10345
  orders: { type: "string" },
9769
10346
  // earn
9770
10347
  rate: { type: "string" },
10348
+ reqId: { type: "string" },
10349
+ confirm: { type: "boolean", default: false },
9771
10350
  // audit
9772
10351
  since: { type: "string" },
9773
10352
  tool: { type: "string" },
@@ -10043,6 +10622,10 @@ async function cmdMarketCandles(run, instId, opts) {
10043
10622
  }))
10044
10623
  );
10045
10624
  }
10625
+ function cmdMarketIndicatorList(json) {
10626
+ if (json) return printJson(KNOWN_INDICATORS);
10627
+ printTable(KNOWN_INDICATORS.map(({ name, description }) => ({ name, description })));
10628
+ }
10046
10629
  async function cmdMarketIndicator(run, indicator, instId, opts) {
10047
10630
  const params = opts.params ? opts.params.split(",").map((p) => Number(p.trim())).filter((n) => !Number.isNaN(n)) : void 0;
10048
10631
  const result = await run("market_get_indicator", {
@@ -10140,9 +10723,9 @@ async function cmdMarketStockTokens(run, opts) {
10140
10723
  }
10141
10724
 
10142
10725
  // src/commands/account.ts
10143
- import * as fs6 from "fs";
10144
- import * as path4 from "path";
10145
- import * as os5 from "os";
10726
+ import * as fs7 from "fs";
10727
+ import * as path5 from "path";
10728
+ import * as os6 from "os";
10146
10729
  function getData2(result) {
10147
10730
  return result.data;
10148
10731
  }
@@ -10353,10 +10936,10 @@ function readAuditLogs(logDir, days = 7) {
10353
10936
  const yyyy = d.getUTCFullYear();
10354
10937
  const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
10355
10938
  const dd = String(d.getUTCDate()).padStart(2, "0");
10356
- const filePath = path4.join(logDir, `trade-${yyyy}-${mm}-${dd}.log`);
10939
+ const filePath = path5.join(logDir, `trade-${yyyy}-${mm}-${dd}.log`);
10357
10940
  let content;
10358
10941
  try {
10359
- content = fs6.readFileSync(filePath, "utf8");
10942
+ content = fs7.readFileSync(filePath, "utf8");
10360
10943
  } catch {
10361
10944
  continue;
10362
10945
  }
@@ -10372,7 +10955,7 @@ function readAuditLogs(logDir, days = 7) {
10372
10955
  return entries;
10373
10956
  }
10374
10957
  function cmdAccountAudit(opts) {
10375
- const logDir = path4.join(os5.homedir(), ".okx", "logs");
10958
+ const logDir = path5.join(os6.homedir(), ".okx", "logs");
10376
10959
  const limit = Math.min(Number(opts.limit) || 20, 100);
10377
10960
  let entries = readAuditLogs(logDir);
10378
10961
  if (opts.tool) entries = entries.filter((e) => e.tool === opts.tool);
@@ -11360,6 +11943,7 @@ async function cmdOptionPlace(run, opts) {
11360
11943
  side: opts.side,
11361
11944
  ordType: opts.ordType,
11362
11945
  sz: opts.sz,
11946
+ tgtCcy: opts.tgtCcy,
11363
11947
  px: opts.px,
11364
11948
  reduceOnly: opts.reduceOnly,
11365
11949
  clOrdId: opts.clOrdId,
@@ -11827,6 +12411,22 @@ async function cmdEarnSavingsBalance(run, ccy, json) {
11827
12411
  pendingAmt: r["pendingAmt"]
11828
12412
  }));
11829
12413
  }
12414
+ async function cmdEarnFixedOrderList(run, opts) {
12415
+ const data = extractData(await run("earn_get_fixed_order_list", {
12416
+ ccy: opts.ccy,
12417
+ state: opts.state
12418
+ }));
12419
+ printDataList(data, opts.json, "No fixed earn orders", (r) => ({
12420
+ reqId: r["reqId"],
12421
+ ccy: r["ccy"],
12422
+ amt: r["amt"],
12423
+ rate: r["rate"],
12424
+ term: r["term"],
12425
+ state: r["state"],
12426
+ accruedInterest: r["accruedInterest"],
12427
+ cTime: new Date(Number(r["cTime"])).toLocaleString()
12428
+ }));
12429
+ }
11830
12430
  async function cmdEarnSavingsPurchase(run, opts) {
11831
12431
  const data = extractData(await run("earn_savings_purchase", { ccy: opts.ccy, amt: opts.amt, rate: opts.rate }));
11832
12432
  if (opts.json) {
@@ -11872,14 +12472,107 @@ async function cmdEarnLendingHistory(run, opts) {
11872
12472
  ts: new Date(Number(r["ts"])).toLocaleString()
11873
12473
  }));
11874
12474
  }
12475
+ function printFixedPurchasePreview(rec) {
12476
+ const offer = rec["offer"];
12477
+ outputLine("");
12478
+ outputLine("\u{1F4CB} Fixed Earn Purchase Preview");
12479
+ outputLine(` Currency: ${rec["ccy"]}`);
12480
+ outputLine(` Amount: ${rec["amt"]}`);
12481
+ outputLine(` Term: ${rec["term"]}`);
12482
+ if (rec["currentFlexibleRate"]) {
12483
+ outputLine(` Current flexible rate: ${rec["currentFlexibleRate"]}`);
12484
+ }
12485
+ if (offer) {
12486
+ printKv({
12487
+ rate: offer["rate"],
12488
+ minLend: offer["minLend"],
12489
+ remainingQuota: offer["lendQuota"],
12490
+ soldOut: offer["soldOut"] ? "Yes" : "No"
12491
+ });
12492
+ } else {
12493
+ outputLine(" \u26A0\uFE0F No matching offer found for this term.");
12494
+ }
12495
+ outputLine("");
12496
+ outputLine(rec["warning"] ?? "");
12497
+ outputLine("");
12498
+ outputLine("Re-run with --confirm to execute.");
12499
+ }
12500
+ async function cmdEarnFixedPurchase(run, opts) {
12501
+ const result = await run("earn_fixed_purchase", {
12502
+ ccy: opts.ccy,
12503
+ amt: opts.amt,
12504
+ term: opts.term,
12505
+ confirm: opts.confirm
12506
+ });
12507
+ if (!result || typeof result !== "object") {
12508
+ outputLine("No response data");
12509
+ return;
12510
+ }
12511
+ const rec = result;
12512
+ if (rec["preview"]) {
12513
+ if (opts.json) {
12514
+ printJson(rec);
12515
+ return;
12516
+ }
12517
+ printFixedPurchasePreview(rec);
12518
+ return;
12519
+ }
12520
+ const data = extractData(result);
12521
+ if (opts.json) {
12522
+ printJson(data);
12523
+ return;
12524
+ }
12525
+ const r = data[0];
12526
+ if (!r) {
12527
+ outputLine("No response data");
12528
+ return;
12529
+ }
12530
+ printKv({ reqId: r["reqId"], ccy: r["ccy"], amt: r["amt"], term: r["term"] });
12531
+ }
12532
+ async function cmdEarnFixedRedeem(run, opts) {
12533
+ const data = extractData(await run("earn_fixed_redeem", { reqId: opts.reqId }));
12534
+ if (opts.json) {
12535
+ printJson(data);
12536
+ return;
12537
+ }
12538
+ if (!data.length) {
12539
+ outputLine("No response data");
12540
+ return;
12541
+ }
12542
+ printTable(data.map((r) => ({ reqId: r["reqId"] })));
12543
+ }
11875
12544
  async function cmdEarnLendingRateHistory(run, opts) {
11876
- const data = extractData(await run("earn_get_lending_rate_history", { ccy: opts.ccy, limit: opts.limit }));
11877
- printDataList(data, opts.json, "No rate history data", (r) => ({
12545
+ const result = await run("earn_get_lending_rate_history", { ccy: opts.ccy, limit: opts.limit });
12546
+ const data = extractData(result);
12547
+ const fixedOffers = extractFixedOffers(result);
12548
+ if (opts.json) {
12549
+ printJson({ data, fixedOffers });
12550
+ return;
12551
+ }
12552
+ printDataList(data, false, "No rate history data", (r) => ({
11878
12553
  ccy: r["ccy"],
11879
12554
  lendingRate: r["lendingRate"],
11880
- rate: r["rate"],
11881
12555
  ts: new Date(Number(r["ts"])).toLocaleString()
11882
12556
  }));
12557
+ if (fixedOffers.length > 0) {
12558
+ outputLine("");
12559
+ outputLine("Fixed-term offers:");
12560
+ printTable(fixedOffers.map((r) => ({
12561
+ ccy: r["ccy"],
12562
+ term: r["term"],
12563
+ rate: r["rate"],
12564
+ minLend: r["minLend"],
12565
+ remainingQuota: r["lendQuota"],
12566
+ soldOut: r["soldOut"] ? "Yes" : "No"
12567
+ })));
12568
+ }
12569
+ }
12570
+ function extractFixedOffers(result) {
12571
+ if (result && typeof result === "object") {
12572
+ const offers = result["fixedOffers"];
12573
+ if (Array.isArray(offers)) return offers;
12574
+ }
12575
+ return [];
11883
12576
  }
11884
12577
 
11885
12578
  // src/commands/auto-earn.ts
@@ -12406,7 +13099,7 @@ async function cmdDcdRedeemExecute(run, opts) {
12406
13099
  ordId: r["ordId"],
12407
13100
  state: r["state"],
12408
13101
  redeemSz: q["redeemSz"] ? `${parseFloat(q["redeemSz"]).toFixed(8)} ${q["redeemCcy"]}` : "\u2014",
12409
- termRate: q["termRate"] ? `${q["termRate"]}%` : "\u2014"
13102
+ termRate: q["termRate"] ? `${(parseFloat(q["termRate"]) * 100).toFixed(2)}%` : "\u2014"
12410
13103
  });
12411
13104
  }
12412
13105
  async function cmdDcdOrderState(run, opts) {
@@ -12459,7 +13152,7 @@ async function cmdDcdOrders(run, opts) {
12459
13152
  quoteCcy: r["quoteCcy"],
12460
13153
  strike: r["strike"],
12461
13154
  notionalSz: r["notionalSz"],
12462
- annualizedYield: r["annualizedYield"],
13155
+ annualizedYield: r["annualizedYield"] ? `${(parseFloat(r["annualizedYield"]) * 100).toFixed(2)}%` : "\u2014",
12463
13156
  yieldSz: r["yieldSz"],
12464
13157
  settleTime: r["settleTime"] ? new Date(Number(r["settleTime"])).toLocaleDateString() : "",
12465
13158
  // scheduled settlement time
@@ -12499,7 +13192,7 @@ async function cmdDcdQuoteAndBuy(run, opts) {
12499
13192
  outputLine("Quote:");
12500
13193
  printKv({
12501
13194
  quoteId: q["quoteId"],
12502
- annualizedYield: q["annualizedYield"] ? `${q["annualizedYield"]}%` : "\u2014",
13195
+ annualizedYield: q["annualizedYield"] ? `${(parseFloat(q["annualizedYield"]) * 100).toFixed(2)}%` : "\u2014",
12503
13196
  absYield: q["absYield"],
12504
13197
  notionalSz: q["notionalSz"],
12505
13198
  notionalCcy: q["notionalCcy"]
@@ -12533,6 +13226,7 @@ function resolveNpx() {
12533
13226
  if (existsSync7(sibling)) return sibling;
12534
13227
  return "npx";
12535
13228
  }
13229
+ var THIRD_PARTY_INSTALL_NOTICE = "Note: This skill was created by a third-party developer, not by OKX. Review SKILL.md before use.";
12536
13230
  async function cmdSkillSearch(run, opts) {
12537
13231
  const args = {};
12538
13232
  if (opts.keyword) args.keyword = opts.keyword;
@@ -12608,11 +13302,7 @@ async function cmdSkillAdd(name, config, json) {
12608
13302
  throw e;
12609
13303
  }
12610
13304
  upsertSkillRecord(meta);
12611
- if (json) {
12612
- outputLine(JSON.stringify({ name: meta.name, version: meta.version, status: "installed" }, null, 2));
12613
- } else {
12614
- outputLine(`\u2713 Skill "${meta.name}" v${meta.version} installed`);
12615
- }
13305
+ printSkillInstallResult(meta, json);
12616
13306
  } finally {
12617
13307
  rmSync(tmpBase, { recursive: true, force: true });
12618
13308
  }
@@ -12705,11 +13395,19 @@ function cmdSkillList(json) {
12705
13395
  outputLine("");
12706
13396
  outputLine(`${skills.length} skills installed.`);
12707
13397
  }
13398
+ function printSkillInstallResult(meta, json) {
13399
+ if (json) {
13400
+ outputLine(JSON.stringify({ name: meta.name, version: meta.version, status: "installed" }, null, 2));
13401
+ } else {
13402
+ outputLine(`\u2713 Skill "${meta.name}" v${meta.version} installed`);
13403
+ outputLine(` ${THIRD_PARTY_INSTALL_NOTICE}`);
13404
+ }
13405
+ }
12708
13406
 
12709
13407
  // src/index.ts
12710
13408
  var _require3 = createRequire3(import.meta.url);
12711
13409
  var CLI_VERSION2 = _require3("../package.json").version;
12712
- var GIT_HASH2 = true ? "19e8da3" : "dev";
13410
+ var GIT_HASH2 = true ? "caa6dae" : "dev";
12713
13411
  function handleConfigCommand(action, rest, json, lang, force) {
12714
13412
  if (action === "init") return cmdConfigInit(lang === "zh" ? "zh" : "en");
12715
13413
  if (action === "show") return cmdConfigShow(json);
@@ -12759,18 +13457,20 @@ function handleMarketPublicCommand(run, action, rest, v, json) {
12759
13457
  instId: v.instId,
12760
13458
  json
12761
13459
  });
12762
- if (action === "indicator") {
12763
- const limit = v.limit !== void 0 ? Number(v.limit) : void 0;
12764
- const backtestTime = v["backtest-time"] !== void 0 ? Number(v["backtest-time"]) : void 0;
12765
- return cmdMarketIndicator(run, rest[0], rest[1], {
12766
- bar: v.bar,
12767
- params: v.params,
12768
- list: v.list,
12769
- limit,
12770
- backtestTime,
12771
- json
12772
- });
12773
- }
13460
+ if (action === "indicator") return handleIndicatorAction(run, rest, v, json);
13461
+ }
13462
+ function handleIndicatorAction(run, rest, v, json) {
13463
+ if (rest[0] === "list") return cmdMarketIndicatorList(json);
13464
+ const limit = v.limit !== void 0 ? Number(v.limit) : void 0;
13465
+ const backtestTime = v["backtest-time"] !== void 0 ? Number(v["backtest-time"]) : void 0;
13466
+ return cmdMarketIndicator(run, rest[0], rest[1], {
13467
+ bar: v.bar,
13468
+ params: v.params,
13469
+ list: v.list,
13470
+ limit,
13471
+ backtestTime,
13472
+ json
13473
+ });
12774
13474
  }
12775
13475
  function handleMarketDataCommand(run, action, rest, v, json) {
12776
13476
  const limit = v.limit !== void 0 ? Number(v.limit) : void 0;
@@ -13116,6 +13816,7 @@ function handleOptionCommand(run, action, rest, v, json) {
13116
13816
  side: v.side,
13117
13817
  ordType: v.ordType,
13118
13818
  sz: v.sz,
13819
+ tgtCcy: v.tgtCcy,
13119
13820
  px: v.px,
13120
13821
  reduceOnly: v.reduceOnly,
13121
13822
  clOrdId: v.clOrdId,
@@ -13406,6 +14107,9 @@ function handleEarnSavingsCommand(run, action, rest, v, json) {
13406
14107
  if (action === "set-rate") return cmdEarnSetLendingRate(run, { ccy: v.ccy, rate: v.rate, json });
13407
14108
  if (action === "lending-history") return cmdEarnLendingHistory(run, { ccy: v.ccy, limit, json });
13408
14109
  if (action === "rate-history") return cmdEarnLendingRateHistory(run, { ccy: v.ccy, limit, json });
14110
+ if (action === "fixed-orders") return cmdEarnFixedOrderList(run, { ccy: v.ccy, state: v.state, json });
14111
+ if (action === "fixed-purchase") return cmdEarnFixedPurchase(run, { ccy: v.ccy, amt: v.amt, term: v.term, confirm: v.confirm ?? false, json });
14112
+ if (action === "fixed-redeem") return cmdEarnFixedRedeem(run, { reqId: v.reqId, json });
13409
14113
  errorLine(`Unknown earn savings command: ${action}`);
13410
14114
  process.exitCode = 1;
13411
14115
  }
@@ -13524,6 +14228,31 @@ function printHelpForLevel(positionals) {
13524
14228
  else if (!subgroup) printHelp(module);
13525
14229
  else printHelp(module, subgroup);
13526
14230
  }
14231
+ function wrapRunnerWithLogger(baseRunner, logger) {
14232
+ const writeToolNames = new Set(allToolSpecs().filter((t) => t.isWrite).map((t) => t.name));
14233
+ return async (toolName, args) => {
14234
+ const startTime = Date.now();
14235
+ try {
14236
+ const result = await baseRunner(toolName, args);
14237
+ if (writeToolNames.has(toolName)) {
14238
+ markFailedIfSCodeError(result.data);
14239
+ }
14240
+ logger.log("info", toolName, args, result, Date.now() - startTime);
14241
+ return result;
14242
+ } catch (error) {
14243
+ logger.log("error", toolName, args, error, Date.now() - startTime);
14244
+ throw error;
14245
+ }
14246
+ };
14247
+ }
14248
+ async function runDiagnose(v) {
14249
+ let config;
14250
+ try {
14251
+ config = loadProfileConfig({ profile: v.profile, demo: v.demo, live: v.live, verbose: v.verbose, userAgent: `okx-trade-cli/${CLI_VERSION2}`, sourceTag: "CLI" });
14252
+ } catch {
14253
+ }
14254
+ return cmdDiagnose(config, v.profile ?? "default", { mcp: v.mcp, cli: v.cli, all: v.all, output: v.output });
14255
+ }
13527
14256
  async function main() {
13528
14257
  setOutput({
13529
14258
  out: (m) => process.stdout.write(m),
@@ -13545,25 +14274,14 @@ async function main() {
13545
14274
  if (module === "config") return handleConfigCommand(action, rest, json, v.lang, v.force);
13546
14275
  if (module === "setup") return handleSetupCommand(v);
13547
14276
  if (module === "upgrade") return cmdUpgrade(CLI_VERSION2, { beta: v.beta, check: v.check, force: v.force }, json);
13548
- if (module === "diagnose") {
13549
- let config2;
13550
- try {
13551
- config2 = loadProfileConfig({ profile: v.profile, demo: v.demo, live: v.live, verbose: v.verbose, userAgent: `okx-trade-cli/${CLI_VERSION2}`, sourceTag: "CLI" });
13552
- } catch {
13553
- }
13554
- return cmdDiagnose(config2, v.profile ?? "default", { mcp: v.mcp, cli: v.cli, all: v.all, output: v.output });
13555
- }
14277
+ if (module === "diagnose") return runDiagnose(v);
13556
14278
  const config = loadProfileConfig({ profile: v.profile, demo: v.demo, live: v.live, verbose: v.verbose, userAgent: `okx-trade-cli/${CLI_VERSION2}`, sourceTag: "CLI" });
14279
+ setEnvContext({ demo: config.demo, profile: v.profile ?? "default" });
14280
+ setJsonEnvEnabled(v.env ?? false);
13557
14281
  const client = new OkxRestClient(config);
13558
14282
  const baseRunner = createToolRunner(client, config);
13559
- const writeToolNames = new Set(allToolSpecs().filter((t) => t.isWrite).map((t) => t.name));
13560
- const run = async (toolName, args) => {
13561
- const result = await baseRunner(toolName, args);
13562
- if (writeToolNames.has(toolName)) {
13563
- markFailedIfSCodeError(result.data);
13564
- }
13565
- return result;
13566
- };
14283
+ const logger = new TradeLogger("info");
14284
+ const run = wrapRunnerWithLogger(baseRunner, logger);
13567
14285
  const moduleHandlers = {
13568
14286
  market: () => handleMarketCommand(run, action, rest, v, json),
13569
14287
  account: () => handleAccountCommand(run, action, rest, v, json),
@@ -13608,7 +14326,8 @@ export {
13608
14326
  handleSpotCommand,
13609
14327
  handleSwapAlgoCommand,
13610
14328
  handleSwapCommand,
13611
- printHelp
14329
+ printHelp,
14330
+ wrapRunnerWithLogger
13612
14331
  };
13613
14332
  /*! Bundled license information:
13614
14333