@okx_ai/okx-trade-cli 1.3.0-beta.1 → 1.3.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -854,6 +854,9 @@ function stringify(obj, { maxDepth = 1e3, numbersAsFloat = false } = {}) {
854
854
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync4 } from "fs";
855
855
  import { join as join5 } from "path";
856
856
  import { homedir as homedir3 } from "os";
857
+ import fs2 from "fs";
858
+ import path2 from "path";
859
+ import os2 from "os";
857
860
  import * as fs3 from "fs";
858
861
  import * as path3 from "path";
859
862
  import * as os3 from "os";
@@ -2322,58 +2325,111 @@ function registerAccountTools() {
2322
2325
  }
2323
2326
  ];
2324
2327
  }
2325
- async function resolveQuoteCcySz(instId, sz, tgtCcy, instType, client) {
2326
- if (tgtCcy !== "quote_ccy") {
2327
- return { sz, tgtCcy, conversionNote: void 0 };
2328
- }
2329
- const [instrumentsRes, tickerRes] = await Promise.all([
2330
- client.publicGet("/api/v5/public/instruments", {
2331
- instType,
2332
- instId
2333
- }),
2334
- client.publicGet("/api/v5/market/ticker", { instId })
2335
- ]);
2336
- const instruments = Array.isArray(instrumentsRes.data) ? instrumentsRes.data : [];
2328
+ function extractInstrumentParams(instId, data) {
2329
+ const instruments = Array.isArray(data) ? data : [];
2337
2330
  if (instruments.length === 0) {
2338
- throw new Error(
2339
- `Failed to fetch instrument info for ${instId}: empty instrument list. Cannot determine ctVal for quote_ccy conversion.`
2340
- );
2331
+ throw new Error(`Failed to fetch instrument info for ${instId}: empty instrument list. Cannot determine ctVal for conversion.`);
2341
2332
  }
2342
- const ctValStr = String(instruments[0].ctVal ?? "");
2333
+ const inst = instruments[0];
2334
+ const ctValStr = String(inst.ctVal ?? "");
2343
2335
  const ctVal = parseFloat(ctValStr);
2344
2336
  if (!isFinite(ctVal) || ctVal <= 0) {
2345
- throw new Error(
2346
- `Invalid ctVal "${ctValStr}" for ${instId}. ctVal must be a positive number for quote_ccy conversion.`
2347
- );
2337
+ throw new Error(`Invalid ctVal "${ctValStr}" for ${instId}. ctVal must be a positive number for conversion.`);
2338
+ }
2339
+ const minSzStr = String(inst.minSz ?? "1");
2340
+ const minSz = parseFloat(minSzStr);
2341
+ if (!isFinite(minSz) || minSz <= 0) {
2342
+ throw new Error(`Invalid minSz "${minSzStr}" for ${instId}. minSz must be a positive number for conversion.`);
2343
+ }
2344
+ const lotSzStr = String(inst.lotSz ?? "1");
2345
+ const lotSz = parseFloat(lotSzStr);
2346
+ if (!isFinite(lotSz) || lotSz <= 0) {
2347
+ throw new Error(`Invalid lotSz "${lotSzStr}" for ${instId}. lotSz must be a positive number for conversion.`);
2348
2348
  }
2349
- const tickers = Array.isArray(tickerRes.data) ? tickerRes.data : [];
2349
+ return { ctVal, ctValStr, minSz, minSzStr, lotSz, lotSzStr };
2350
+ }
2351
+ function extractLastPx(instId, data) {
2352
+ const tickers = Array.isArray(data) ? data : [];
2350
2353
  if (tickers.length === 0) {
2351
- throw new Error(
2352
- `Failed to fetch ticker price for ${instId}: empty ticker response. Cannot determine last price for quote_ccy conversion.`
2353
- );
2354
+ throw new Error(`Failed to fetch ticker price for ${instId}: empty ticker response. Cannot determine last price for conversion.`);
2354
2355
  }
2355
2356
  const lastStr = String(tickers[0].last ?? "");
2356
2357
  const lastPx = parseFloat(lastStr);
2357
2358
  if (!isFinite(lastPx) || lastPx <= 0) {
2359
+ throw new Error(`Invalid last price "${lastStr}" for ${instId}. Last price must be a positive number for conversion.`);
2360
+ }
2361
+ return { lastPx, lastStr };
2362
+ }
2363
+ function extractLeverage(instId, mgnMode, data) {
2364
+ const leverageData = Array.isArray(data) ? data : [];
2365
+ if (leverageData.length === 0) {
2358
2366
  throw new Error(
2359
- `Invalid last price "${lastStr}" for ${instId}. Last price must be a positive number for quote_ccy conversion.`
2367
+ `Failed to fetch leverage info for ${instId} (mgnMode=${mgnMode}): empty response. Cannot determine leverage for margin conversion. Please set leverage first using set_leverage.`
2360
2368
  );
2361
2369
  }
2362
- const usdtAmount = parseFloat(sz);
2370
+ const leverStr = String(leverageData[0].lever ?? "1");
2371
+ const lever = parseFloat(leverStr);
2372
+ if (!isFinite(lever) || lever <= 0) {
2373
+ throw new Error(`Invalid leverage "${leverStr}" for ${instId}. Leverage must be a positive number for margin conversion.`);
2374
+ }
2375
+ return { lever, leverStr };
2376
+ }
2377
+ function computeContracts(p) {
2378
+ const { instId, sz, isMarginMode, inst, lastPx, lastStr, lever, leverStr } = p;
2379
+ const { ctVal, ctValStr, minSz, minSzStr, lotSz, lotSzStr } = inst;
2380
+ const userAmount = parseFloat(sz);
2363
2381
  const contractValue = ctVal * lastPx;
2364
- const contracts = Math.floor(usdtAmount / contractValue);
2365
- if (contracts <= 0) {
2366
- const minUsdt = contractValue.toFixed(2);
2382
+ const effectiveNotional = isMarginMode ? userAmount * lever : userAmount;
2383
+ const lotSzDecimals = lotSzStr.includes(".") ? lotSzStr.split(".")[1].length : 0;
2384
+ const precision = 10 ** (lotSzDecimals + 4);
2385
+ const rawContracts = Math.round(effectiveNotional / contractValue * precision) / precision;
2386
+ const rawLots = Math.round(rawContracts / lotSz * precision) / precision;
2387
+ const contractsRounded = parseFloat((Math.floor(rawLots) * lotSz).toFixed(lotSzDecimals));
2388
+ if (contractsRounded < minSz) {
2389
+ const minAmount = isMarginMode ? (minSz * contractValue / lever).toFixed(2) : (minSz * contractValue).toFixed(2);
2390
+ const unit = isMarginMode ? "USDT margin" : "USDT";
2367
2391
  throw new Error(
2368
- `sz=${sz} USDT is too small to buy even 1 contract of ${instId}. Minimum amount required is at least ${minUsdt} USDT (ctVal=${ctValStr}, lastPx=${lastStr}, 1 contract = ${minUsdt} USDT).`
2392
+ `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}` : "") + `).
2393
+ `
2369
2394
  );
2370
2395
  }
2371
- const conversionNote = `Converting ${sz} USDT \u2192 ${contracts} contracts (ctVal=${ctValStr}, lastPx=${lastStr}, formula: floor(${sz} / (${ctValStr} \xD7 ${lastStr})) = ${contracts})`;
2372
- return {
2373
- sz: String(contracts),
2374
- tgtCcy: void 0,
2375
- conversionNote
2376
- };
2396
+ const contractsStr = contractsRounded.toFixed(lotSzDecimals);
2397
+ 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})`;
2398
+ return { contractsStr, conversionNote };
2399
+ }
2400
+ async function resolveQuoteCcySz(instId, sz, tgtCcy, instType, client, tdMode) {
2401
+ if (tgtCcy !== "quote_ccy" && tgtCcy !== "margin") {
2402
+ return { sz, tgtCcy, conversionNote: void 0 };
2403
+ }
2404
+ const isMarginMode = tgtCcy === "margin";
2405
+ if (isMarginMode && !tdMode) {
2406
+ throw new Error(
2407
+ "tdMode (cross or isolated) is required when tgtCcy=margin. Cannot determine leverage without knowing the margin mode."
2408
+ );
2409
+ }
2410
+ const mgnMode = tdMode === "cross" ? "cross" : "isolated";
2411
+ const fetchPromises = [
2412
+ client.publicGet("/api/v5/public/instruments", { instType, instId }),
2413
+ client.publicGet("/api/v5/market/ticker", { instId })
2414
+ ];
2415
+ if (isMarginMode) {
2416
+ fetchPromises.push(client.privateGet("/api/v5/account/leverage-info", { instId, mgnMode }));
2417
+ }
2418
+ const results = await Promise.all(fetchPromises);
2419
+ const inst = extractInstrumentParams(instId, results[0].data);
2420
+ const { lastPx, lastStr } = extractLastPx(instId, results[1].data);
2421
+ const { lever, leverStr } = isMarginMode ? extractLeverage(instId, mgnMode, results[2].data) : { lever: 1, leverStr: "1" };
2422
+ const { contractsStr, conversionNote } = computeContracts({
2423
+ instId,
2424
+ sz,
2425
+ isMarginMode,
2426
+ inst,
2427
+ lastPx,
2428
+ lastStr,
2429
+ lever,
2430
+ leverStr
2431
+ });
2432
+ return { sz: contractsStr, tgtCcy: void 0, conversionNote };
2377
2433
  }
2378
2434
  function registerAlgoTradeTools() {
2379
2435
  return [
@@ -2453,8 +2509,8 @@ function registerAlgoTradeTools() {
2453
2509
  },
2454
2510
  tgtCcy: {
2455
2511
  type: "string",
2456
- enum: ["base_ccy", "quote_ccy"],
2457
- description: "Size unit. base_ccy(default): sz in contracts, quote_ccy: sz in USDT"
2512
+ enum: ["base_ccy", "quote_ccy", "margin"],
2513
+ 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)"
2458
2514
  },
2459
2515
  reduceOnly: {
2460
2516
  type: "boolean",
@@ -2475,7 +2531,8 @@ function registerAlgoTradeTools() {
2475
2531
  requireString(args, "sz"),
2476
2532
  readString(args, "tgtCcy"),
2477
2533
  "SWAP",
2478
- context.client
2534
+ context.client,
2535
+ readString(args, "tdMode")
2479
2536
  );
2480
2537
  const response = await context.client.privatePost(
2481
2538
  "/api/v5/trade/order-algo",
@@ -2797,8 +2854,8 @@ function registerFuturesAlgoTools() {
2797
2854
  },
2798
2855
  tgtCcy: {
2799
2856
  type: "string",
2800
- enum: ["base_ccy", "quote_ccy"],
2801
- description: "Size unit. base_ccy(default): sz in contracts, quote_ccy: sz in USDT"
2857
+ enum: ["base_ccy", "quote_ccy", "margin"],
2858
+ 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)"
2802
2859
  },
2803
2860
  reduceOnly: {
2804
2861
  type: "boolean",
@@ -2819,7 +2876,8 @@ function registerFuturesAlgoTools() {
2819
2876
  requireString(args, "sz"),
2820
2877
  readString(args, "tgtCcy"),
2821
2878
  "FUTURES",
2822
- context.client
2879
+ context.client,
2880
+ readString(args, "tdMode")
2823
2881
  );
2824
2882
  const response = await context.client.privatePost(
2825
2883
  "/api/v5/trade/order-algo",
@@ -4021,7 +4079,7 @@ function registerEarnTools() {
4021
4079
  {
4022
4080
  name: "earn_get_savings_balance",
4023
4081
  module: "earn.savings",
4024
- 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.",
4082
+ 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.",
4025
4083
  isWrite: false,
4026
4084
  inputSchema: {
4027
4085
  type: "object",
@@ -4042,6 +4100,43 @@ function registerEarnTools() {
4042
4100
  return normalizeResponse(response);
4043
4101
  }
4044
4102
  },
4103
+ {
4104
+ name: "earn_get_fixed_order_list",
4105
+ module: "earn.savings",
4106
+ 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.",
4107
+ isWrite: false,
4108
+ inputSchema: {
4109
+ type: "object",
4110
+ properties: {
4111
+ ccy: {
4112
+ type: "string",
4113
+ description: "Currency, e.g. USDT. Omit for all."
4114
+ },
4115
+ state: {
4116
+ type: "string",
4117
+ 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."
4118
+ }
4119
+ }
4120
+ },
4121
+ handler: async (rawArgs, context) => {
4122
+ const args = asRecord(rawArgs);
4123
+ const response = await context.client.privateGet(
4124
+ "/api/v5/finance/simple-earn-fixed/order-list",
4125
+ compactObject({
4126
+ ccy: readString(args, "ccy"),
4127
+ state: readString(args, "state")
4128
+ }),
4129
+ privateRateLimit("earn_get_fixed_order_list", 3)
4130
+ );
4131
+ const result = normalizeResponse(response);
4132
+ if (Array.isArray(result["data"])) {
4133
+ result["data"] = result["data"].map(
4134
+ ({ finalSettlementDate: _, ...rest }) => rest
4135
+ );
4136
+ }
4137
+ return result;
4138
+ }
4139
+ },
4045
4140
  {
4046
4141
  name: "earn_savings_purchase",
4047
4142
  module: "earn.savings",
@@ -4186,10 +4281,112 @@ function registerEarnTools() {
4186
4281
  return normalizeResponse(response);
4187
4282
  }
4188
4283
  },
4284
+ {
4285
+ name: "earn_fixed_purchase",
4286
+ module: "earn.savings",
4287
+ 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.",
4288
+ isWrite: true,
4289
+ inputSchema: {
4290
+ type: "object",
4291
+ properties: {
4292
+ ccy: {
4293
+ type: "string",
4294
+ description: "Currency, e.g. USDT"
4295
+ },
4296
+ amt: {
4297
+ type: "string",
4298
+ description: "Purchase amount"
4299
+ },
4300
+ term: {
4301
+ type: "string",
4302
+ description: "Term, e.g. 90D"
4303
+ },
4304
+ confirm: {
4305
+ type: "boolean",
4306
+ 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."
4307
+ }
4308
+ },
4309
+ required: ["ccy", "amt", "term"]
4310
+ },
4311
+ handler: async (rawArgs, context) => {
4312
+ const args = asRecord(rawArgs);
4313
+ const ccy = requireString(args, "ccy");
4314
+ const amt = requireString(args, "amt");
4315
+ const term = requireString(args, "term");
4316
+ const confirm = readBoolean(args, "confirm") ?? false;
4317
+ if (!confirm) {
4318
+ const [rateResponse, fixedResponse] = await Promise.all([
4319
+ context.client.publicGet(
4320
+ "/api/v5/finance/savings/lending-rate-history",
4321
+ compactObject({ ccy, limit: 1 }),
4322
+ publicRateLimit("earn_get_lending_rate_history", 6)
4323
+ ),
4324
+ context.client.privateGet(
4325
+ "/api/v5/finance/simple-earn-fixed/offers",
4326
+ compactObject({ ccy }),
4327
+ privateRateLimit("earn_fixed_purchase_preview_offers", 2)
4328
+ ).catch(() => null)
4329
+ ]);
4330
+ const rateResult = normalizeResponse(rateResponse);
4331
+ const fixedResult = fixedResponse ? normalizeResponse(fixedResponse) : { data: [] };
4332
+ const rateArr = Array.isArray(rateResult["data"]) ? rateResult["data"] : [];
4333
+ const allOffers = Array.isArray(fixedResult["data"]) ? fixedResult["data"] : [];
4334
+ const matchedOffer = allOffers.find(
4335
+ (o) => o["term"] === term && o["ccy"] === ccy
4336
+ );
4337
+ const { borrowingOrderQuota: _, ...offerWithoutTotal } = matchedOffer ?? {};
4338
+ const offerWithSoldOut = matchedOffer ? { ...offerWithoutTotal, soldOut: offerWithoutTotal["lendQuota"] === "0" } : null;
4339
+ return {
4340
+ preview: true,
4341
+ ccy,
4342
+ amt,
4343
+ term,
4344
+ offer: offerWithSoldOut,
4345
+ currentFlexibleRate: rateArr[0]?.["lendingRate"] ?? null,
4346
+ 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."
4347
+ };
4348
+ }
4349
+ assertNotDemo(context.config, "earn_fixed_purchase");
4350
+ const response = await context.client.privatePost(
4351
+ "/api/v5/finance/simple-earn-fixed/purchase",
4352
+ { ccy, amt, term },
4353
+ privateRateLimit("earn_fixed_purchase", 2)
4354
+ );
4355
+ return normalizeResponse(response);
4356
+ }
4357
+ },
4358
+ {
4359
+ name: "earn_fixed_redeem",
4360
+ module: "earn.savings",
4361
+ 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.",
4362
+ isWrite: true,
4363
+ inputSchema: {
4364
+ type: "object",
4365
+ properties: {
4366
+ reqId: {
4367
+ type: "string",
4368
+ description: "Request ID of the fixed-term order to redeem"
4369
+ }
4370
+ },
4371
+ required: ["reqId"]
4372
+ },
4373
+ handler: async (rawArgs, context) => {
4374
+ assertNotDemo(context.config, "earn_fixed_redeem");
4375
+ const args = asRecord(rawArgs);
4376
+ const response = await context.client.privatePost(
4377
+ "/api/v5/finance/simple-earn-fixed/redeem",
4378
+ {
4379
+ reqId: requireString(args, "reqId")
4380
+ },
4381
+ privateRateLimit("earn_fixed_redeem", 2)
4382
+ );
4383
+ return normalizeResponse(response);
4384
+ }
4385
+ },
4189
4386
  {
4190
4387
  name: "earn_get_lending_rate_history",
4191
4388
  module: "earn.savings",
4192
- 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.",
4389
+ 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.",
4193
4390
  isWrite: false,
4194
4391
  inputSchema: {
4195
4392
  type: "object",
@@ -4208,23 +4405,45 @@ function registerEarnTools() {
4208
4405
  },
4209
4406
  limit: {
4210
4407
  type: "number",
4211
- description: "Max results (default 100)"
4408
+ description: "Max results (default 7)"
4212
4409
  }
4213
4410
  }
4214
4411
  },
4215
4412
  handler: async (rawArgs, context) => {
4216
4413
  const args = asRecord(rawArgs);
4217
- const response = await context.client.publicGet(
4218
- "/api/v5/finance/savings/lending-rate-history",
4219
- compactObject({
4220
- ccy: readString(args, "ccy"),
4221
- after: readString(args, "after"),
4222
- before: readString(args, "before"),
4223
- limit: readNumber(args, "limit")
4224
- }),
4225
- publicRateLimit("earn_get_lending_rate_history", 6)
4226
- );
4227
- return normalizeResponse(response);
4414
+ const ccy = readString(args, "ccy");
4415
+ const [rateResponse, fixedResponse] = await Promise.all([
4416
+ context.client.publicGet(
4417
+ "/api/v5/finance/savings/lending-rate-history",
4418
+ compactObject({
4419
+ ccy,
4420
+ after: readString(args, "after"),
4421
+ before: readString(args, "before"),
4422
+ limit: readNumber(args, "limit") ?? 7
4423
+ }),
4424
+ publicRateLimit("earn_get_lending_rate_history", 6)
4425
+ ),
4426
+ context.client.privateGet(
4427
+ "/api/v5/finance/simple-earn-fixed/offers",
4428
+ compactObject({ ccy }),
4429
+ privateRateLimit("earn_get_lending_rate_history_fixed", 2)
4430
+ ).catch(() => null)
4431
+ ]);
4432
+ const rateResult = normalizeResponse(rateResponse);
4433
+ const rateData = Array.isArray(rateResult["data"]) ? rateResult["data"].map(
4434
+ ({ rate: _, ...rest }) => rest
4435
+ ) : [];
4436
+ const fixedResult = fixedResponse ? normalizeResponse(fixedResponse) : { data: [] };
4437
+ const allOffers = fixedResult["data"] ?? [];
4438
+ const fixedOffers = allOffers.map(({ borrowingOrderQuota: _, ...rest }) => ({
4439
+ ...rest,
4440
+ soldOut: rest["lendQuota"] === "0"
4441
+ }));
4442
+ return {
4443
+ ...rateResult,
4444
+ data: rateData,
4445
+ fixedOffers
4446
+ };
4228
4447
  }
4229
4448
  }
4230
4449
  ];
@@ -4861,7 +5080,7 @@ function registerAutoEarnTools() {
4861
5080
  }
4862
5081
  var EARN_DEMO_MESSAGE = "Earn features (savings, DCD, on-chain staking, auto-earn) are not available in simulated trading mode.";
4863
5082
  var EARN_DEMO_SUGGESTION = "Switch to a live account to use Earn features.";
4864
- var DEMO_GUARD_SKIP = /* @__PURE__ */ new Set(["dcd_redeem"]);
5083
+ var DEMO_GUARD_SKIP = /* @__PURE__ */ new Set(["dcd_redeem", "earn_fixed_purchase"]);
4865
5084
  function withDemoGuard(tool) {
4866
5085
  if (!tool.isWrite || DEMO_GUARD_SKIP.has(tool.name)) return tool;
4867
5086
  const originalHandler = tool.handler;
@@ -4922,12 +5141,12 @@ function buildContractTradeTools(cfg) {
4922
5141
  },
4923
5142
  sz: {
4924
5143
  type: "string",
4925
- 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."
5144
+ 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)."
4926
5145
  },
4927
5146
  tgtCcy: {
4928
5147
  type: "string",
4929
- enum: ["base_ccy", "quote_ccy"],
4930
- description: "Size unit. base_ccy(default): sz in contracts, quote_ccy: sz in USDT"
5148
+ enum: ["base_ccy", "quote_ccy", "margin"],
5149
+ 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)"
4931
5150
  },
4932
5151
  px: { type: "string", description: "Required for limit/post_only/fok/ioc" },
4933
5152
  reduceOnly: {
@@ -4951,7 +5170,8 @@ function buildContractTradeTools(cfg) {
4951
5170
  requireString(args, "sz"),
4952
5171
  readString(args, "tgtCcy"),
4953
5172
  defaultType,
4954
- context.client
5173
+ context.client,
5174
+ readString(args, "tdMode")
4955
5175
  );
4956
5176
  const response = await context.client.privatePost(
4957
5177
  "/api/v5/trade/order",
@@ -6049,8 +6269,8 @@ function registerOptionAlgoTools() {
6049
6269
  },
6050
6270
  tgtCcy: {
6051
6271
  type: "string",
6052
- enum: ["base_ccy", "quote_ccy"],
6053
- description: "Size unit. base_ccy(default): sz in contracts, quote_ccy: sz in USDT (auto-converted to contracts)"
6272
+ enum: ["base_ccy", "quote_ccy", "margin"],
6273
+ 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)"
6054
6274
  },
6055
6275
  reduceOnly: {
6056
6276
  type: "boolean",
@@ -6071,7 +6291,8 @@ function registerOptionAlgoTools() {
6071
6291
  requireString(args, "sz"),
6072
6292
  readString(args, "tgtCcy"),
6073
6293
  "OPTION",
6074
- context.client
6294
+ context.client,
6295
+ readString(args, "tdMode")
6075
6296
  );
6076
6297
  const response = await context.client.privatePost(
6077
6298
  "/api/v5/trade/order-algo",
@@ -6288,12 +6509,12 @@ function registerOptionTools() {
6288
6509
  },
6289
6510
  sz: {
6290
6511
  type: "string",
6291
- description: "Contracts count by default. Set tgtCcy=quote_ccy to use USDT amount instead."
6512
+ 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)."
6292
6513
  },
6293
6514
  tgtCcy: {
6294
6515
  type: "string",
6295
- enum: ["base_ccy", "quote_ccy"],
6296
- description: "Size unit. base_ccy(default): sz in contracts, quote_ccy: sz in USDT (auto-converted to contracts)"
6516
+ enum: ["base_ccy", "quote_ccy", "margin"],
6517
+ 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)"
6297
6518
  },
6298
6519
  px: {
6299
6520
  type: "string",
@@ -6335,7 +6556,8 @@ function registerOptionTools() {
6335
6556
  requireString(args, "sz"),
6336
6557
  readString(args, "tgtCcy"),
6337
6558
  "OPTION",
6338
- context.client
6559
+ context.client,
6560
+ readString(args, "tdMode")
6339
6561
  );
6340
6562
  const response = await context.client.privatePost(
6341
6563
  "/api/v5/trade/order",
@@ -7735,6 +7957,69 @@ Run: npm install -g ${packageName}
7735
7957
  refreshCacheInBackground(packageName);
7736
7958
  }
7737
7959
  }
7960
+ var LOG_LEVEL_PRIORITY = {
7961
+ error: 0,
7962
+ warn: 1,
7963
+ info: 2,
7964
+ debug: 3
7965
+ };
7966
+ var SENSITIVE_KEY_PATTERN = /apiKey|secretKey|passphrase|password|secret/i;
7967
+ function sanitize(value) {
7968
+ if (value === null || value === void 0) {
7969
+ return value;
7970
+ }
7971
+ if (Array.isArray(value)) {
7972
+ return value.map(sanitize);
7973
+ }
7974
+ if (typeof value === "object") {
7975
+ const result = {};
7976
+ for (const [k, v] of Object.entries(value)) {
7977
+ if (SENSITIVE_KEY_PATTERN.test(k)) {
7978
+ result[k] = "[REDACTED]";
7979
+ } else {
7980
+ result[k] = sanitize(v);
7981
+ }
7982
+ }
7983
+ return result;
7984
+ }
7985
+ return value;
7986
+ }
7987
+ var TradeLogger = class {
7988
+ logLevel;
7989
+ logDir;
7990
+ constructor(logLevel = "info", logDir) {
7991
+ this.logLevel = logLevel;
7992
+ this.logDir = logDir ?? path2.join(os2.homedir(), ".okx", "logs");
7993
+ }
7994
+ getLogPath(date) {
7995
+ const d = date ?? /* @__PURE__ */ new Date();
7996
+ const yyyy = d.getUTCFullYear();
7997
+ const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
7998
+ const dd = String(d.getUTCDate()).padStart(2, "0");
7999
+ return path2.join(this.logDir, `trade-${yyyy}-${mm}-${dd}.log`);
8000
+ }
8001
+ log(level, tool, params, result, durationMs) {
8002
+ if (LOG_LEVEL_PRIORITY[level] > LOG_LEVEL_PRIORITY[this.logLevel]) {
8003
+ return;
8004
+ }
8005
+ const entry = {
8006
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8007
+ level: level.toUpperCase(),
8008
+ tool,
8009
+ durationMs,
8010
+ params: sanitize(params),
8011
+ result: sanitize(result)
8012
+ };
8013
+ try {
8014
+ fs2.mkdirSync(this.logDir, { recursive: true });
8015
+ fs2.appendFileSync(this.getLogPath(), JSON.stringify(entry) + "\n", "utf8");
8016
+ } catch {
8017
+ }
8018
+ }
8019
+ static sanitize(params) {
8020
+ return sanitize(params);
8021
+ }
8022
+ };
7738
8023
  var CLIENT_NAMES = {
7739
8024
  "claude-desktop": "Claude Desktop",
7740
8025
  cursor: "Cursor",
@@ -7884,11 +8169,11 @@ function runSetup(options) {
7884
8169
  // src/commands/diagnose.ts
7885
8170
  import dns from "dns/promises";
7886
8171
  import net from "net";
7887
- import os4 from "os";
8172
+ import os5 from "os";
7888
8173
  import tls from "tls";
7889
8174
 
7890
8175
  // src/commands/diagnose-utils.ts
7891
- import fs2 from "fs";
8176
+ import fs4 from "fs";
7892
8177
  import { createRequire } from "module";
7893
8178
 
7894
8179
  // src/formatter.ts
@@ -7917,8 +8202,12 @@ function outputLine(message) {
7917
8202
  function errorLine(message) {
7918
8203
  activeOutput.err(message + EOL);
7919
8204
  }
8205
+ var jsonEnvEnabled = false;
8206
+ function setJsonEnvEnabled(enabled) {
8207
+ jsonEnvEnabled = enabled;
8208
+ }
7920
8209
  function printJson(data) {
7921
- const payload = envContext ? {
8210
+ const payload = jsonEnvEnabled && envContext ? {
7922
8211
  env: envContext.demo ? "demo" : "live",
7923
8212
  profile: envContext.profile,
7924
8213
  data
@@ -8006,7 +8295,7 @@ var Report = class {
8006
8295
  lines.push(`${key.padEnd(14)} ${value}`);
8007
8296
  }
8008
8297
  lines.push(sep2, "");
8009
- fs2.writeFileSync(filePath, lines.join("\n"), "utf8");
8298
+ fs4.writeFileSync(filePath, lines.join("\n"), "utf8");
8010
8299
  return true;
8011
8300
  } catch (_e) {
8012
8301
  return false;
@@ -8041,7 +8330,7 @@ function writeReportIfRequested(report, outputPath) {
8041
8330
  errorLine(` Warning: failed to write report to: ${outputPath}`);
8042
8331
  }
8043
8332
  }
8044
- function sanitize(value) {
8333
+ function sanitize2(value) {
8045
8334
  value = value.replace(
8046
8335
  /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi,
8047
8336
  "****-uuid-****"
@@ -8052,14 +8341,14 @@ function sanitize(value) {
8052
8341
  }
8053
8342
 
8054
8343
  // src/commands/diagnose-mcp.ts
8055
- import fs4 from "fs";
8056
- import path2 from "path";
8057
- import os2 from "os";
8344
+ import fs5 from "fs";
8345
+ import path4 from "path";
8346
+ import os4 from "os";
8058
8347
  import { spawnSync, spawn } from "child_process";
8059
8348
  import { createRequire as createRequire2 } from "module";
8060
8349
  import { fileURLToPath } from "url";
8061
8350
  var _require2 = createRequire2(import.meta.url);
8062
- var __dirname = path2.dirname(fileURLToPath(import.meta.url));
8351
+ var __dirname = path4.dirname(fileURLToPath(import.meta.url));
8063
8352
  function readMcpVersion() {
8064
8353
  const candidates = [
8065
8354
  // Installed as global or local dependency
@@ -8112,13 +8401,13 @@ function checkMcpEntryPoint(report) {
8112
8401
  if (!entryPath) {
8113
8402
  const candidates = [
8114
8403
  // Installed locally
8115
- path2.join(process.cwd(), "node_modules", ".bin", "okx-trade-mcp"),
8404
+ path4.join(process.cwd(), "node_modules", ".bin", "okx-trade-mcp"),
8116
8405
  // Monorepo workspace (e.g. running from source)
8117
- path2.join(__dirname, "..", "..", "..", "..", "mcp", "dist", "index.js")
8406
+ path4.join(__dirname, "..", "..", "..", "..", "mcp", "dist", "index.js")
8118
8407
  ];
8119
8408
  for (const candidate of candidates) {
8120
8409
  try {
8121
- fs4.accessSync(candidate, fs4.constants.X_OK | fs4.constants.R_OK);
8410
+ fs5.accessSync(candidate, fs5.constants.X_OK | fs5.constants.R_OK);
8122
8411
  entryPath = candidate;
8123
8412
  break;
8124
8413
  } catch (_e) {
@@ -8143,9 +8432,9 @@ var CLIENT_LIMITS = {
8143
8432
  cursor: { perServer: 40, total: 80 }
8144
8433
  };
8145
8434
  function checkJsonMcpConfig(configPath) {
8146
- if (!fs4.existsSync(configPath)) return "missing";
8435
+ if (!fs5.existsSync(configPath)) return "missing";
8147
8436
  try {
8148
- const raw = fs4.readFileSync(configPath, "utf8");
8437
+ const raw = fs5.readFileSync(configPath, "utf8");
8149
8438
  const parsed = JSON.parse(raw);
8150
8439
  const mcpServers = parsed["mcpServers"];
8151
8440
  if (!mcpServers) return "not-configured";
@@ -8163,15 +8452,15 @@ function checkJsonMcpConfig(configPath) {
8163
8452
  }
8164
8453
  }
8165
8454
  function checkClaudeCodeConfig() {
8166
- const home = os2.homedir();
8455
+ const home = os4.homedir();
8167
8456
  const candidates = [
8168
- path2.join(home, ".claude", "settings.json"),
8169
- path2.join(home, ".claude.json")
8457
+ path4.join(home, ".claude", "settings.json"),
8458
+ path4.join(home, ".claude.json")
8170
8459
  ];
8171
8460
  let anyFound = false;
8172
8461
  let anyParseError = false;
8173
8462
  for (const cfgPath of candidates) {
8174
- if (!fs4.existsSync(cfgPath)) continue;
8463
+ if (!fs5.existsSync(cfgPath)) continue;
8175
8464
  anyFound = true;
8176
8465
  const result = checkJsonMcpConfig(cfgPath);
8177
8466
  if (result === "found") return "found";
@@ -8188,8 +8477,8 @@ function handleJsonClient(clientId, report, configuredClients) {
8188
8477
  const status = checkJsonMcpConfig(configPath);
8189
8478
  if (status === "missing") return false;
8190
8479
  if (status === "found") {
8191
- ok(name, `configured (${sanitize(configPath)})`);
8192
- report.add(`client_${clientId}`, `OK ${sanitize(configPath)}`);
8480
+ ok(name, `configured (${sanitize2(configPath)})`);
8481
+ report.add(`client_${clientId}`, `OK ${sanitize2(configPath)}`);
8193
8482
  configuredClients.push(clientId);
8194
8483
  return false;
8195
8484
  }
@@ -8197,8 +8486,8 @@ function handleJsonClient(clientId, report, configuredClients) {
8197
8486
  fail(name, "okx-trade-mcp not found in mcpServers", [`Run: okx setup --client ${clientId}`]);
8198
8487
  report.add(`client_${clientId}`, "NOT_CONFIGURED");
8199
8488
  } else {
8200
- fail(name, `JSON parse error in ${sanitize(configPath)}`, [
8201
- `Check ${sanitize(configPath)} for JSON syntax errors`,
8489
+ fail(name, `JSON parse error in ${sanitize2(configPath)}`, [
8490
+ `Check ${sanitize2(configPath)} for JSON syntax errors`,
8202
8491
  `Then run: okx setup --client ${clientId}`
8203
8492
  ]);
8204
8493
  report.add(`client_${clientId}`, "PARSE_ERROR");
@@ -8291,37 +8580,37 @@ function checkToolCount(report, configuredClients, getSpecs = allToolSpecs) {
8291
8580
  }
8292
8581
  }
8293
8582
  function readLogTail(logPath) {
8294
- const stat = fs4.statSync(logPath);
8583
+ const stat = fs5.statSync(logPath);
8295
8584
  const readSize = Math.min(8192, stat.size);
8296
8585
  const buffer = Buffer.alloc(readSize);
8297
- const fd = fs4.openSync(logPath, "r");
8586
+ const fd = fs5.openSync(logPath, "r");
8298
8587
  try {
8299
- fs4.readSync(fd, buffer, 0, readSize, Math.max(0, stat.size - readSize));
8588
+ fs5.readSync(fd, buffer, 0, readSize, Math.max(0, stat.size - readSize));
8300
8589
  } finally {
8301
- fs4.closeSync(fd);
8590
+ fs5.closeSync(fd);
8302
8591
  }
8303
8592
  return buffer.toString("utf8").split("\n").filter((l) => l.trim()).slice(-5);
8304
8593
  }
8305
8594
  function getMcpLogCandidates() {
8306
8595
  if (process.platform === "darwin") {
8307
- const logsDir = path2.join(os2.homedir(), "Library", "Logs", "Claude");
8596
+ const logsDir = path4.join(os4.homedir(), "Library", "Logs", "Claude");
8308
8597
  const candidates = [
8309
- path2.join(logsDir, "mcp.log"),
8310
- path2.join(logsDir, "mcp-server-okx-trade-mcp.log")
8598
+ path4.join(logsDir, "mcp.log"),
8599
+ path4.join(logsDir, "mcp-server-okx-trade-mcp.log")
8311
8600
  ];
8312
8601
  try {
8313
- const extra = fs4.readdirSync(logsDir).filter((f) => f.startsWith("mcp") && f.endsWith(".log")).map((f) => path2.join(logsDir, f));
8602
+ const extra = fs5.readdirSync(logsDir).filter((f) => f.startsWith("mcp") && f.endsWith(".log")).map((f) => path4.join(logsDir, f));
8314
8603
  candidates.push(...extra);
8315
8604
  } catch (_e) {
8316
8605
  }
8317
8606
  return candidates;
8318
8607
  }
8319
8608
  if (process.platform === "win32") {
8320
- const appData2 = process.env.APPDATA ?? path2.join(os2.homedir(), "AppData", "Roaming");
8321
- return [path2.join(appData2, "Claude", "logs", "mcp.log")];
8609
+ const appData2 = process.env.APPDATA ?? path4.join(os4.homedir(), "AppData", "Roaming");
8610
+ return [path4.join(appData2, "Claude", "logs", "mcp.log")];
8322
8611
  }
8323
- const configHome = process.env.XDG_CONFIG_HOME ?? path2.join(os2.homedir(), ".config");
8324
- return [path2.join(configHome, "Claude", "logs", "mcp.log")];
8612
+ const configHome = process.env.XDG_CONFIG_HOME ?? path4.join(os4.homedir(), ".config");
8613
+ return [path4.join(configHome, "Claude", "logs", "mcp.log")];
8325
8614
  }
8326
8615
  function checkMcpLogs(report) {
8327
8616
  section("MCP Server Logs (recent)");
@@ -8335,7 +8624,7 @@ function checkMcpLogs(report) {
8335
8624
  report.add("mcp_log", logPath);
8336
8625
  if (lines.length > 0) {
8337
8626
  ok("last lines", `(${lines.length} shown)`);
8338
- for (const line of lines) outputLine(` ${sanitize(line)}`);
8627
+ for (const line of lines) outputLine(` ${sanitize2(line)}`);
8339
8628
  } else {
8340
8629
  ok("last lines", "(empty log)");
8341
8630
  }
@@ -8461,11 +8750,11 @@ function checkModuleLoading(entryPath, report) {
8461
8750
  return true;
8462
8751
  } else {
8463
8752
  const errMsg = result.stderr?.trim() || result.error?.message || "non-zero exit";
8464
- fail("module load", `failed: ${sanitize(errMsg)}`, [
8753
+ fail("module load", `failed: ${sanitize2(errMsg)}`, [
8465
8754
  "MCP server may have import errors or missing dependencies",
8466
8755
  `Try: node ${entryPath} --version`
8467
8756
  ]);
8468
- report.add("module_load", `FAIL ${sanitize(errMsg)}`);
8757
+ report.add("module_load", `FAIL ${sanitize2(errMsg)}`);
8469
8758
  return false;
8470
8759
  }
8471
8760
  }
@@ -8476,7 +8765,7 @@ async function cmdDiagnoseMcp(options = {}) {
8476
8765
  const report = new Report();
8477
8766
  report.add("ts", (/* @__PURE__ */ new Date()).toISOString());
8478
8767
  report.add("mode", "mcp");
8479
- report.add("os", `${process.platform} ${process.arch} ${os2.release()}`);
8768
+ report.add("os", `${process.platform} ${process.arch} ${os4.release()}`);
8480
8769
  checkMcpPackageVersion(report);
8481
8770
  const nodePassed = checkNodeCompat(report);
8482
8771
  const { entryPath, passed: entryPassed } = checkMcpEntryPoint(report);
@@ -8508,7 +8797,7 @@ async function cmdDiagnoseMcp(options = {}) {
8508
8797
 
8509
8798
  // src/commands/diagnose.ts
8510
8799
  var CLI_VERSION = readCliVersion();
8511
- var GIT_HASH = true ? "ab11ece" : "dev";
8800
+ var GIT_HASH = true ? "0a5911f" : "dev";
8512
8801
  function maskKey2(key) {
8513
8802
  if (!key) return "(not set)";
8514
8803
  if (key.length <= 8) return "****";
@@ -8588,14 +8877,14 @@ function checkEnvironment(report) {
8588
8877
  }
8589
8878
  ok("CLI", `v${CLI_VERSION} (${GIT_HASH})`);
8590
8879
  ok("OS", `${process.platform} ${process.arch}`);
8591
- ok("OS release", os4.release());
8880
+ ok("OS release", os5.release());
8592
8881
  ok("Shell", process.env.SHELL ?? "(unknown)");
8593
8882
  ok("Locale", `${process.env.LANG ?? process.env.LC_ALL ?? "(unknown)"}`);
8594
8883
  ok("Timezone", Intl.DateTimeFormat().resolvedOptions().timeZone);
8595
8884
  report.add("cli", `${CLI_VERSION} (${GIT_HASH})`);
8596
8885
  report.add("node", `${nodeVersion} ${process.platform} ${process.arch}`);
8597
- const machine = typeof os4.machine === "function" ? os4.machine() : process.arch;
8598
- report.add("os", `${os4.type()} ${os4.release()} ${machine}`);
8886
+ const machine = typeof os5.machine === "function" ? os5.machine() : process.arch;
8887
+ report.add("os", `${os5.type()} ${os5.release()} ${machine}`);
8599
8888
  report.add("shell", process.env.SHELL ?? "-");
8600
8889
  report.add("locale", process.env.LANG ?? process.env.LC_ALL ?? "-");
8601
8890
  report.add("tz", Intl.DateTimeFormat().resolvedOptions().timeZone);
@@ -8752,10 +9041,10 @@ async function cmdDiagnose(config, profile, options = {}) {
8752
9041
  }
8753
9042
  function checkConfigFile(report) {
8754
9043
  section("Config File");
8755
- const path5 = configFilePath();
9044
+ const path6 = configFilePath();
8756
9045
  try {
8757
9046
  readFullConfig();
8758
- ok("Config parse", `${path5} OK`);
9047
+ ok("Config parse", `${path6} OK`);
8759
9048
  report.add("config_parse", "OK");
8760
9049
  return true;
8761
9050
  } catch (e) {
@@ -8925,7 +9214,7 @@ function loadProfileConfig(opts) {
8925
9214
  import { EOL as EOL2 } from "os";
8926
9215
 
8927
9216
  // src/commands/client-setup.ts
8928
- import * as fs5 from "fs";
9217
+ import * as fs6 from "fs";
8929
9218
  var DETECTABLE_CLIENTS = ["claude-desktop", "cursor", "windsurf"];
8930
9219
  function cmdSetupClient(options) {
8931
9220
  runSetup(options);
@@ -8934,14 +9223,14 @@ function cmdSetupClients() {
8934
9223
  const detected = [];
8935
9224
  for (const id of DETECTABLE_CLIENTS) {
8936
9225
  const p = getConfigPath(id);
8937
- if (p && fs5.existsSync(p)) {
9226
+ if (p && fs6.existsSync(p)) {
8938
9227
  detected.push({ id, path: p });
8939
9228
  }
8940
9229
  }
8941
9230
  if (detected.length > 0) {
8942
9231
  outputLine("Detected clients:");
8943
- for (const { id, path: path5 } of detected) {
8944
- outputLine(` ${id.padEnd(16)} ${path5}`);
9232
+ for (const { id, path: path6 } of detected) {
9233
+ outputLine(` ${id.padEnd(16)} ${path6}`);
8945
9234
  }
8946
9235
  outputLine("");
8947
9236
  }
@@ -9325,7 +9614,7 @@ var HELP_TREE = {
9325
9614
  description: "Earn products \u2014 Simple Earn, On-chain Earn, and DCD (Dual Currency Deposit)",
9326
9615
  subgroups: {
9327
9616
  savings: {
9328
- description: "Simple Earn \u2014 flexible savings and lending",
9617
+ description: "Simple Earn \u2014 flexible savings, fixed-term, and lending",
9329
9618
  commands: {
9330
9619
  balance: {
9331
9620
  usage: "okx earn savings balance [<ccy>]",
@@ -9349,7 +9638,19 @@ var HELP_TREE = {
9349
9638
  },
9350
9639
  "rate-history": {
9351
9640
  usage: "okx earn savings rate-history [--ccy <ccy>] [--limit <n>]",
9352
- description: "Query Simple Earn lending rates (public, no auth needed)"
9641
+ description: "Query Simple Earn lending rates and fixed-term offers (requires auth)"
9642
+ },
9643
+ "fixed-orders": {
9644
+ usage: "okx earn savings fixed-orders [--ccy <ccy>] [--state <pending|earning|expired|settled|cancelled>]",
9645
+ description: "List fixed-term earn orders"
9646
+ },
9647
+ "fixed-purchase": {
9648
+ usage: "okx earn savings fixed-purchase --ccy <ccy> --amt <n> --term <term> [--confirm]",
9649
+ description: "Purchase Simple Earn Fixed (\u5B9A\u671F). Preview by default; add --confirm to execute. Funds locked until maturity"
9650
+ },
9651
+ "fixed-redeem": {
9652
+ usage: "okx earn savings fixed-redeem <reqId>",
9653
+ description: "Redeem a fixed-term earn order (full amount)"
9353
9654
  }
9354
9655
  }
9355
9656
  },
@@ -9529,6 +9830,7 @@ function printGlobalHelp() {
9529
9830
  " --demo Use simulated trading (demo) mode",
9530
9831
  " --live Force live trading mode (overrides profile demo=true; mutually exclusive with --demo)",
9531
9832
  " --json Output raw JSON",
9833
+ " --env With --json, wrap output as {env, profile, data}",
9532
9834
  " --verbose Show detailed network request/response info (stderr)",
9533
9835
  " --version, -v Show version",
9534
9836
  " --help Show this help",
@@ -9643,8 +9945,8 @@ function printCommandList(lines, commands) {
9643
9945
  lines.push("");
9644
9946
  }
9645
9947
  }
9646
- function printHelp(...path5) {
9647
- const [moduleName, subgroupName] = path5;
9948
+ function printHelp(...path6) {
9949
+ const [moduleName, subgroupName] = path6;
9648
9950
  if (!moduleName) {
9649
9951
  printGlobalHelp();
9650
9952
  } else if (!subgroupName) {
@@ -9660,6 +9962,7 @@ var CLI_OPTIONS = {
9660
9962
  profile: { type: "string" },
9661
9963
  demo: { type: "boolean", default: false },
9662
9964
  json: { type: "boolean", default: false },
9965
+ env: { type: "boolean", default: false },
9663
9966
  help: { type: "boolean", default: false },
9664
9967
  version: { type: "boolean", short: "v", default: false },
9665
9968
  // setup command
@@ -9765,6 +10068,8 @@ var CLI_OPTIONS = {
9765
10068
  orders: { type: "string" },
9766
10069
  // earn
9767
10070
  rate: { type: "string" },
10071
+ reqId: { type: "string" },
10072
+ confirm: { type: "boolean", default: false },
9768
10073
  // audit
9769
10074
  since: { type: "string" },
9770
10075
  tool: { type: "string" },
@@ -10141,9 +10446,9 @@ async function cmdMarketStockTokens(run, opts) {
10141
10446
  }
10142
10447
 
10143
10448
  // src/commands/account.ts
10144
- import * as fs6 from "fs";
10145
- import * as path4 from "path";
10146
- import * as os5 from "os";
10449
+ import * as fs7 from "fs";
10450
+ import * as path5 from "path";
10451
+ import * as os6 from "os";
10147
10452
  function getData2(result) {
10148
10453
  return result.data;
10149
10454
  }
@@ -10354,10 +10659,10 @@ function readAuditLogs(logDir, days = 7) {
10354
10659
  const yyyy = d.getUTCFullYear();
10355
10660
  const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
10356
10661
  const dd = String(d.getUTCDate()).padStart(2, "0");
10357
- const filePath = path4.join(logDir, `trade-${yyyy}-${mm}-${dd}.log`);
10662
+ const filePath = path5.join(logDir, `trade-${yyyy}-${mm}-${dd}.log`);
10358
10663
  let content;
10359
10664
  try {
10360
- content = fs6.readFileSync(filePath, "utf8");
10665
+ content = fs7.readFileSync(filePath, "utf8");
10361
10666
  } catch {
10362
10667
  continue;
10363
10668
  }
@@ -10373,7 +10678,7 @@ function readAuditLogs(logDir, days = 7) {
10373
10678
  return entries;
10374
10679
  }
10375
10680
  function cmdAccountAudit(opts) {
10376
- const logDir = path4.join(os5.homedir(), ".okx", "logs");
10681
+ const logDir = path5.join(os6.homedir(), ".okx", "logs");
10377
10682
  const limit = Math.min(Number(opts.limit) || 20, 100);
10378
10683
  let entries = readAuditLogs(logDir);
10379
10684
  if (opts.tool) entries = entries.filter((e) => e.tool === opts.tool);
@@ -11829,6 +12134,22 @@ async function cmdEarnSavingsBalance(run, ccy, json) {
11829
12134
  pendingAmt: r["pendingAmt"]
11830
12135
  }));
11831
12136
  }
12137
+ async function cmdEarnFixedOrderList(run, opts) {
12138
+ const data = extractData(await run("earn_get_fixed_order_list", {
12139
+ ccy: opts.ccy,
12140
+ state: opts.state
12141
+ }));
12142
+ printDataList(data, opts.json, "No fixed earn orders", (r) => ({
12143
+ reqId: r["reqId"],
12144
+ ccy: r["ccy"],
12145
+ amt: r["amt"],
12146
+ rate: r["rate"],
12147
+ term: r["term"],
12148
+ state: r["state"],
12149
+ accruedInterest: r["accruedInterest"],
12150
+ cTime: new Date(Number(r["cTime"])).toLocaleString()
12151
+ }));
12152
+ }
11832
12153
  async function cmdEarnSavingsPurchase(run, opts) {
11833
12154
  const data = extractData(await run("earn_savings_purchase", { ccy: opts.ccy, amt: opts.amt, rate: opts.rate }));
11834
12155
  if (opts.json) {
@@ -11874,14 +12195,107 @@ async function cmdEarnLendingHistory(run, opts) {
11874
12195
  ts: new Date(Number(r["ts"])).toLocaleString()
11875
12196
  }));
11876
12197
  }
12198
+ function printFixedPurchasePreview(rec) {
12199
+ const offer = rec["offer"];
12200
+ outputLine("");
12201
+ outputLine("\u{1F4CB} Fixed Earn Purchase Preview");
12202
+ outputLine(` Currency: ${rec["ccy"]}`);
12203
+ outputLine(` Amount: ${rec["amt"]}`);
12204
+ outputLine(` Term: ${rec["term"]}`);
12205
+ if (rec["currentFlexibleRate"]) {
12206
+ outputLine(` Current flexible rate: ${rec["currentFlexibleRate"]}`);
12207
+ }
12208
+ if (offer) {
12209
+ printKv({
12210
+ rate: offer["rate"],
12211
+ minLend: offer["minLend"],
12212
+ remainingQuota: offer["lendQuota"],
12213
+ soldOut: offer["soldOut"] ? "Yes" : "No"
12214
+ });
12215
+ } else {
12216
+ outputLine(" \u26A0\uFE0F No matching offer found for this term.");
12217
+ }
12218
+ outputLine("");
12219
+ outputLine(rec["warning"] ?? "");
12220
+ outputLine("");
12221
+ outputLine("Re-run with --confirm to execute.");
12222
+ }
12223
+ async function cmdEarnFixedPurchase(run, opts) {
12224
+ const result = await run("earn_fixed_purchase", {
12225
+ ccy: opts.ccy,
12226
+ amt: opts.amt,
12227
+ term: opts.term,
12228
+ confirm: opts.confirm
12229
+ });
12230
+ if (!result || typeof result !== "object") {
12231
+ outputLine("No response data");
12232
+ return;
12233
+ }
12234
+ const rec = result;
12235
+ if (rec["preview"]) {
12236
+ if (opts.json) {
12237
+ printJson(rec);
12238
+ return;
12239
+ }
12240
+ printFixedPurchasePreview(rec);
12241
+ return;
12242
+ }
12243
+ const data = extractData(result);
12244
+ if (opts.json) {
12245
+ printJson(data);
12246
+ return;
12247
+ }
12248
+ const r = data[0];
12249
+ if (!r) {
12250
+ outputLine("No response data");
12251
+ return;
12252
+ }
12253
+ printKv({ reqId: r["reqId"], ccy: r["ccy"], amt: r["amt"], term: r["term"] });
12254
+ }
12255
+ async function cmdEarnFixedRedeem(run, opts) {
12256
+ const data = extractData(await run("earn_fixed_redeem", { reqId: opts.reqId }));
12257
+ if (opts.json) {
12258
+ printJson(data);
12259
+ return;
12260
+ }
12261
+ if (!data.length) {
12262
+ outputLine("No response data");
12263
+ return;
12264
+ }
12265
+ printTable(data.map((r) => ({ reqId: r["reqId"] })));
12266
+ }
11877
12267
  async function cmdEarnLendingRateHistory(run, opts) {
11878
- const data = extractData(await run("earn_get_lending_rate_history", { ccy: opts.ccy, limit: opts.limit }));
11879
- printDataList(data, opts.json, "No rate history data", (r) => ({
12268
+ const result = await run("earn_get_lending_rate_history", { ccy: opts.ccy, limit: opts.limit });
12269
+ const data = extractData(result);
12270
+ const fixedOffers = extractFixedOffers(result);
12271
+ if (opts.json) {
12272
+ printJson({ data, fixedOffers });
12273
+ return;
12274
+ }
12275
+ printDataList(data, false, "No rate history data", (r) => ({
11880
12276
  ccy: r["ccy"],
11881
12277
  lendingRate: r["lendingRate"],
11882
- rate: r["rate"],
11883
12278
  ts: new Date(Number(r["ts"])).toLocaleString()
11884
12279
  }));
12280
+ if (fixedOffers.length > 0) {
12281
+ outputLine("");
12282
+ outputLine("Fixed-term offers:");
12283
+ printTable(fixedOffers.map((r) => ({
12284
+ ccy: r["ccy"],
12285
+ term: r["term"],
12286
+ rate: r["rate"],
12287
+ minLend: r["minLend"],
12288
+ remainingQuota: r["lendQuota"],
12289
+ soldOut: r["soldOut"] ? "Yes" : "No"
12290
+ })));
12291
+ }
12292
+ }
12293
+ function extractFixedOffers(result) {
12294
+ if (result && typeof result === "object") {
12295
+ const offers = result["fixedOffers"];
12296
+ if (Array.isArray(offers)) return offers;
12297
+ }
12298
+ return [];
11885
12299
  }
11886
12300
 
11887
12301
  // src/commands/auto-earn.ts
@@ -12716,7 +13130,7 @@ function printSkillInstallResult(meta, json) {
12716
13130
  // src/index.ts
12717
13131
  var _require3 = createRequire3(import.meta.url);
12718
13132
  var CLI_VERSION2 = _require3("../package.json").version;
12719
- var GIT_HASH2 = true ? "ab11ece" : "dev";
13133
+ var GIT_HASH2 = true ? "0a5911f" : "dev";
12720
13134
  function handleConfigCommand(action, rest, json, lang, force) {
12721
13135
  if (action === "init") return cmdConfigInit(lang === "zh" ? "zh" : "en");
12722
13136
  if (action === "show") return cmdConfigShow(json);
@@ -13416,6 +13830,9 @@ function handleEarnSavingsCommand(run, action, rest, v, json) {
13416
13830
  if (action === "set-rate") return cmdEarnSetLendingRate(run, { ccy: v.ccy, rate: v.rate, json });
13417
13831
  if (action === "lending-history") return cmdEarnLendingHistory(run, { ccy: v.ccy, limit, json });
13418
13832
  if (action === "rate-history") return cmdEarnLendingRateHistory(run, { ccy: v.ccy, limit, json });
13833
+ if (action === "fixed-orders") return cmdEarnFixedOrderList(run, { ccy: v.ccy, state: v.state, json });
13834
+ if (action === "fixed-purchase") return cmdEarnFixedPurchase(run, { ccy: v.ccy, amt: v.amt, term: v.term, confirm: v.confirm ?? false, json });
13835
+ if (action === "fixed-redeem") return cmdEarnFixedRedeem(run, { reqId: v.reqId, json });
13419
13836
  errorLine(`Unknown earn savings command: ${action}`);
13420
13837
  process.exitCode = 1;
13421
13838
  }
@@ -13534,6 +13951,23 @@ function printHelpForLevel(positionals) {
13534
13951
  else if (!subgroup) printHelp(module);
13535
13952
  else printHelp(module, subgroup);
13536
13953
  }
13954
+ function wrapRunnerWithLogger(baseRunner, logger) {
13955
+ const writeToolNames = new Set(allToolSpecs().filter((t) => t.isWrite).map((t) => t.name));
13956
+ return async (toolName, args) => {
13957
+ const startTime = Date.now();
13958
+ try {
13959
+ const result = await baseRunner(toolName, args);
13960
+ if (writeToolNames.has(toolName)) {
13961
+ markFailedIfSCodeError(result.data);
13962
+ }
13963
+ logger.log("info", toolName, args, result, Date.now() - startTime);
13964
+ return result;
13965
+ } catch (error) {
13966
+ logger.log("error", toolName, args, error, Date.now() - startTime);
13967
+ throw error;
13968
+ }
13969
+ };
13970
+ }
13537
13971
  async function runDiagnose(v) {
13538
13972
  let config;
13539
13973
  try {
@@ -13566,16 +14000,11 @@ async function main() {
13566
14000
  if (module === "diagnose") return runDiagnose(v);
13567
14001
  const config = loadProfileConfig({ profile: v.profile, demo: v.demo, live: v.live, verbose: v.verbose, userAgent: `okx-trade-cli/${CLI_VERSION2}`, sourceTag: "CLI" });
13568
14002
  setEnvContext({ demo: config.demo, profile: v.profile ?? "default" });
14003
+ setJsonEnvEnabled(v.env ?? false);
13569
14004
  const client = new OkxRestClient(config);
13570
14005
  const baseRunner = createToolRunner(client, config);
13571
- const writeToolNames = new Set(allToolSpecs().filter((t) => t.isWrite).map((t) => t.name));
13572
- const run = async (toolName, args) => {
13573
- const result = await baseRunner(toolName, args);
13574
- if (writeToolNames.has(toolName)) {
13575
- markFailedIfSCodeError(result.data);
13576
- }
13577
- return result;
13578
- };
14006
+ const logger = new TradeLogger("info");
14007
+ const run = wrapRunnerWithLogger(baseRunner, logger);
13579
14008
  const moduleHandlers = {
13580
14009
  market: () => handleMarketCommand(run, action, rest, v, json),
13581
14010
  account: () => handleAccountCommand(run, action, rest, v, json),
@@ -13620,7 +14049,8 @@ export {
13620
14049
  handleSpotCommand,
13621
14050
  handleSwapAlgoCommand,
13622
14051
  handleSwapCommand,
13623
- printHelp
14052
+ printHelp,
14053
+ wrapRunnerWithLogger
13624
14054
  };
13625
14055
  /*! Bundled license information:
13626
14056