@dshtrading/connector-okx 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { BAR_MAP, NormalizedSize, OKX_INTERVAL_VOCABULARY, OkxCredentials, OkxFundingRate, OkxInstrument, OkxOpenInterest, OkxPlaceOrderParams, OkxRestClient, OkxRestOptions, SignedAuth, TradingServiceError, barDurationMs, buildAuthHeaders, isoTimestamp, normalizeOkxSymbol, normalizeSize, signPayload, signaturePrehash, toBar, toCanonicalOkxSymbol, toOkxSwapInstId } from "./rest.js";
1
+ import { BAR_MAP, NormalizedSize, OKX_INTERVAL_VOCABULARY, OkxCredentials, OkxFundingRate, OkxInstrument, OkxOpenInterest, OkxPlaceOrderParams, OkxRestClient, OkxRestOptions, OkxSetLeverageParams, SignedAuth, TradingServiceError, barDurationMs, buildAuthHeaders, coinsToContracts, contractsToCoins, isoTimestamp, normalizeOkxSymbol, normalizeSize, okxAssetClassOf, okxContractOf, signPayload, signaturePrehash, toBar, toCanonicalOkxSymbol, toOkxSwapInstId } from "./rest.js";
2
2
  import { Context, Service } from "@deepseek-ai/cordis";
3
3
  import Schema from "@deepseek-ai/schemastery";
4
- import { AccountBalance, DerivativesData, DerivativesHistory, Disposable, Interval, Kline, MarketDataService, Order, OrderRequest, OrderStatus, Orderbook, Position, Ticker, TradeFill, TradeService, TradeTick } from "@dshtrading/api";
4
+ import { AccountBalance, DerivativesData, DerivativesHistory, Disposable, InstrumentRef, Interval, Kline, LeverageSetting, MarginMode, MarketDataService, Order, OrderRequest, OrderStatus, Orderbook, Position, SetLeverageRequest, Ticker, TradeFill, TradeService, TradeTick } from "@dshtrading/api";
5
5
  //#region src/index.d.ts
6
6
  /**
7
7
  * Cordis 插件名 = preset 行 id(TEMPLATES §8):`dsh-trading-crypto-*` 市场命名空间,
@@ -55,7 +55,9 @@ interface ResolvedCredentialRefs {
55
55
  declare function credentialRefsFor(config: Config, env?: 'demo' | 'live'): ResolvedCredentialRefs;
56
56
  /**
57
57
  * 三 ref 凭证解析:每次操作调用(ctx.credentials 的设计意图——换 key 无需重启插件)。
58
- * 无 credentials seam 时回落启动环境变量(llm-deepseek 同款降级)。
58
+ * **有 credentials seam 即 fail-closed**:seam 存在时只认 seam,某 slot 未命中不再回退
59
+ * 启动环境变量——显式配置与 ambient 凭据混用会让 demo 路径拿到 live key(反之亦然);
60
+ * 仅当 seam 完全缺席时才回落启动环境变量(llm-deepseek 同款降级)。
59
61
  * 任何一处未命中/无效 → TRADING_CREDENTIALS_MISSING,消息只带 ref 名(绝不带值)。
60
62
  */
61
63
  declare function resolveCredentials(ctx: CredentialsContext, config: Config): Promise<OkxCredentials>;
@@ -68,10 +70,7 @@ declare class OkxMarketDataService extends Service implements MarketDataService
68
70
  constructor(ctx: Context, options?: OkxRestOptions, client?: OkxRestClient, serviceName?: string);
69
71
  getTicker(instId: string): Promise<Ticker>;
70
72
  getKlines(instId: string, interval: Interval, limit?: number): Promise<Kline[]>;
71
- listInstruments(): Promise<Array<{
72
- symbol: string;
73
- name?: string;
74
- }>>;
73
+ listInstruments(): Promise<InstrumentRef[]>;
75
74
  /** OKX 专属扩展(MarketDataService 契约之外):SWAP 资金费率。 */
76
75
  getFundingRate(instId: string): Promise<OkxFundingRate>;
77
76
  /** 盘口快照(api 可选契约 getOrderbook,issue #39):books 20 档透传。 */
@@ -137,11 +136,11 @@ declare class OkxTradeService extends Service implements TradeService {
137
136
  * 未来任何新消费面)同样 fail-closed;工具层 evaluateOrderGate + base 审批闸门保留
138
137
  * (双保险),工具层只做参数预检与富回执。
139
138
  *
140
- * - 闸门 ① reject(dryRun=false 请求实盘而 liveTrading=false)→ 结构化错误抛出
139
+ * - 闸门 ① reject(dryRun=false 请求实盘而未获实盘授权)→ 结构化错误抛出
141
140
  * (TRADING_LIVE_TRADING_DISABLED,api TradingError 契约);
142
141
  * - 闸门 ② simulate(dryRun 缺省/true,或 config.dryRun 强制模拟)→ 本地模拟回执
143
142
  * (Order.dryRun=true,不触网;工具层另有带市价参照的富回执);
144
- * - 闸门 ③ live(dryRun=false 且 liveTrading=true)→ 真实签名下单(env=demo 加模拟盘头)。
143
+ * - 闸门 ③ live(dryRun=false 且实盘已获授权)→ 真实签名下单(env=demo 加模拟盘头)。
145
144
  *
146
145
  * **sz 单位纪律(调研 §4,实现期最重要的换算)**:
147
146
  * - api `OrderRequest.quantity` 语义恒为 base 币数;
@@ -152,6 +151,23 @@ declare class OkxTradeService extends Service implements TradeService {
152
151
  * ctVal/lotSz/minSz 换算并本地校验(向下取整,省一次 51000 往返)。
153
152
  */
154
153
  placeOrder(req: OrderRequest): Promise<Order>;
154
+ /**
155
+ * 设置合约杠杆与保证金模式(P7;api TradeService.setLeverage)。
156
+ *
157
+ * 与 placeOrder 共用同一套三态闸门(服务缝第一步——绕过工具层的直调同样 fail-closed):
158
+ * - ① dryRun=false 而未获实盘授权 → TRADING_LIVE_TRADING_DISABLED;
159
+ * - ② dryRun 缺省/true(或被 config.dryRun 强制)→ 本地模拟回执,不触网;
160
+ * - ③ dryRun=false 且已获授权 → 真实签名 POST /api/v5/account/set-leverage
161
+ * (env=demo 带 x-simulated-trading:1)。
162
+ *
163
+ * 本地判据(先于上游往返,给出可读原因、不静默改写调用方意图):
164
+ * - 仅永续(form=perp):现货传 setLeverage 报 TRADING_UNSUPPORTED_SYMBOL;
165
+ * - leverage > 交易所规格 lever(InstrumentContract.maxLeverage)→ 结构化拒绝,
166
+ * **不做上限截断**(静默改小杠杆会让调用方以为自己设定的数字已生效);
167
+ * - mgnMode 只认 cross/isolated;isolated + 双向持仓模式的 posSide 由调用方传,
168
+ * 本仓不猜账户持仓模式(缺席即原样透传,交交易所裁定)。
169
+ */
170
+ setLeverage(req: SetLeverageRequest): Promise<LeverageSetting>;
155
171
  /**
156
172
  * 撤单。OKX 按 instId + ordId 双键定位,symbol(instId)必填——api 契约的
157
173
  * cancelOrder(id) 单参形态对 OKX 不够,扩展第二可选参数(api 包 R3 修订)。
@@ -162,7 +178,12 @@ declare class OkxTradeService extends Service implements TradeService {
162
178
  cancelOrder(id: string, symbol?: string): Promise<void>;
163
179
  /** 查单(api TradeService R3 新增成员):state → OrderStatus 映射见 ORDER_STATE_MAP。 */
164
180
  getOrder(symbol: string, id: string): Promise<Order>;
165
- /** 只读持仓(SWAP 的 pos 单位是张 → 经 ctVal 换算成币;net 模式负 pos = short)。 */
181
+ /**
182
+ * 只读持仓(SWAP 的 pos 单位是张 → 经 ctVal 换算成币;net 模式负 pos = short)。
183
+ * 合约仓位附带交易所回传的强平价(liqPx)、维持保证金率(mgnRatio)、保证金模式
184
+ * (mgnMode)与名义价值(notionalUsd)——风控清单的「强平距离」判据读这些字段,
185
+ * 每条都是交易所口径,缺席即不展示(不本地补算)。
186
+ */
166
187
  getPositions(): Promise<Position[]>;
167
188
  /** 只读余额(TradeService 契约外扩展,crypto_get_balance 工具消费)。 */
168
189
  getBalances(): Promise<AccountBalance[]>;
@@ -190,14 +211,19 @@ interface PlaceOrderArgs {
190
211
  readonly quantity: number;
191
212
  /** LIMIT 单必填(schema 无法条件必填,execute 内校验),必须 > 0。 */
192
213
  readonly price?: number;
214
+ /**
215
+ * 合约订单的保证金模式(缺省 cross 全仓);仅永续有效,现货传该字段报
216
+ * TRADING_UNSUPPORTED_SYMBOL。isolated 需先用 crypto_set_leverage 设好该标的的逐仓杠杆。
217
+ */
218
+ readonly marginMode?: MarginMode;
193
219
  /** 缺省视为 true:仅模拟。显式 false 即实盘意图,进入三态闸门 ②/③。 */
194
220
  readonly dryRun?: boolean;
195
221
  }
196
222
  /**
197
223
  * 三态闸门判定(顺序即铁律 #3 修订版的裁决顺序;主 agent 裁决的三态环境映射):
198
- * - `reject` —— ① 请求实盘(dryRun!==true)而 liveTrading=false:结构化拒绝;
224
+ * - `reject` —— ① 请求实盘(dryRun!==true)而未获实盘授权:结构化拒绝;
199
225
  * - `simulate` —— ② dryRun=true(显式/缺省/被 config.dryRun 强制):本地模拟回执;
200
- * - `live` —— ③ dryRun=false 且 liveTrading=true:真实签名下单,environment
226
+ * - `live` —— ③ dryRun=false 且实盘已获授权:真实签名下单,environment
201
227
  * 决定是否带模拟盘头(demo=第一默认目标;live=实盘,base 闸门照旧 ask)。
202
228
  */
203
229
  type OrderGateVerdict = {
@@ -226,7 +252,7 @@ interface PlaceOrderToolDeps {
226
252
  readonly marketData: Pick<OkxMarketDataService, 'getTicker'>;
227
253
  /** 交易服务(闸门 ③ 的真实签名下单路径)。 */
228
254
  readonly trade: TradeService;
229
- /** 插件配置(dryRun 强制模拟 / liveTrading 总闸门 / env 三态)。 */
255
+ /** 插件配置(dryRun 强制模拟 / liveTrading 非权威镜像——实盘授权由 @dshtrading/authority 的人工签署平面裁定 / env 三态)。 */
230
256
  readonly config: Config;
231
257
  }
232
258
  /**
@@ -237,6 +263,33 @@ interface PlaceOrderToolDeps {
237
263
  * 工具内不再重复调 ctx.approval。
238
264
  */
239
265
  declare function createPlaceOrderTool(deps: PlaceOrderToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
266
+ /** crypto_set_leverage 参数契约(仅永续;dryRun 缺省 true)。 */
267
+ interface SetLeverageArgs {
268
+ /** 永续 instId:规范形(BTCUSDT-SWAP)或 OKX 原生形(BTC-USDT-SWAP)皆收。 */
269
+ readonly instId: string;
270
+ /** 目标杠杆倍数(> 0;上限以交易所规格为准,超出即拒绝,不静默截断)。 */
271
+ readonly leverage: number;
272
+ /** 保证金模式:'cross'(缺省,全仓)| 'isolated'(逐仓)。 */
273
+ readonly marginMode?: MarginMode;
274
+ /** 双向持仓模式(long/short)下的方向;净持仓模式(net)缺席。 */
275
+ readonly posSide?: 'long' | 'short';
276
+ /** 全仓多币种保证金模式下按币种设置时的保证金币。 */
277
+ readonly ccy?: string;
278
+ /** 缺省视为 true:仅模拟。显式 false 即实盘意图,进入与下单同门槛的闸门。 */
279
+ readonly dryRun?: boolean;
280
+ }
281
+ interface SetLeverageToolDeps {
282
+ /** 交易服务(服务缝三态闸门在实现内第一步)。 */
283
+ readonly trade: TradeService;
284
+ }
285
+ /**
286
+ * crypto_set_leverage 工具工厂(独立导出便于单测闸门与参数矩阵)。
287
+ *
288
+ * 审批不在这里做:dryRun!==true 的调用由 @dshtrading/base 的统一审批闸门在
289
+ * `tools/pre-execute` waterfall ask(headless 下 ask=deny,fail-closed);
290
+ * 工具内不再重复调 ctx.approval。
291
+ */
292
+ declare function createSetLeverageTool(deps: SetLeverageToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
240
293
  /** 本连接器的路由 provider slug(docs/exchange-routing.md §2.2)。 */
241
294
  declare const ROUTER_PROVIDER = "okx";
242
295
  /** 市场路由服务的最小消费面(api 包 MarketRouterService 同构;不定死接口)。 */
@@ -245,4 +298,4 @@ interface MarketRouterLike {
245
298
  }
246
299
  declare function apply(ctx: Context, config: Config): void;
247
300
  //#endregion
248
- export { BAR_MAP, Config, CredentialResolverLike, CredentialsContext, DryRunReference, MarketRouterLike, NormalizedSize, OKX_INTERVAL_VOCABULARY, OkxCredentials, OkxFundingRate, OkxInstrument, OkxMarketDataService, OkxOpenInterest, OkxPlaceOrderParams, OkxRestClient, OkxRestOptions, OkxTradeService, OkxTradeServiceOptions, OrderGateVerdict, PlaceOrderArgs, PlaceOrderToolDeps, ROUTER_PROVIDER, ResolvedCredentialRefs, SignedAuth, SubscribeTickerOptions, TRADING_CRYPTO_MARKET_DATA_KEY, TRADING_CRYPTO_TRADE_KEY, TradingServiceError, apply, barDurationMs, buildAuthHeaders, buildDryRunReceipt, createPlaceOrderTool, credentialRefsFor, evaluateOrderGate, inject, isoTimestamp, mapOrderState, name, normalizeOkxSymbol, normalizeSize, resolveCredentials, signPayload, signaturePrehash, toBar, toCanonicalOkxSymbol, toOkxSwapInstId };
301
+ export { BAR_MAP, Config, CredentialResolverLike, CredentialsContext, DryRunReference, MarketRouterLike, NormalizedSize, OKX_INTERVAL_VOCABULARY, OkxCredentials, OkxFundingRate, OkxInstrument, OkxMarketDataService, OkxOpenInterest, OkxPlaceOrderParams, OkxRestClient, OkxRestOptions, OkxSetLeverageParams, OkxTradeService, OkxTradeServiceOptions, OrderGateVerdict, PlaceOrderArgs, PlaceOrderToolDeps, ROUTER_PROVIDER, ResolvedCredentialRefs, SetLeverageArgs, SetLeverageToolDeps, SignedAuth, SubscribeTickerOptions, TRADING_CRYPTO_MARKET_DATA_KEY, TRADING_CRYPTO_TRADE_KEY, TradingServiceError, apply, barDurationMs, buildAuthHeaders, buildDryRunReceipt, coinsToContracts, contractsToCoins, createPlaceOrderTool, createSetLeverageTool, credentialRefsFor, evaluateOrderGate, inject, isoTimestamp, mapOrderState, name, normalizeOkxSymbol, normalizeSize, okxAssetClassOf, okxContractOf, resolveCredentials, signPayload, signaturePrehash, toBar, toCanonicalOkxSymbol, toOkxSwapInstId };
package/lib/index.js CHANGED
@@ -1,10 +1,46 @@
1
- import { BAR_MAP, OKX_INTERVAL_VOCABULARY, OkxRestClient, TradingServiceError, barDurationMs, buildAuthHeaders, isoTimestamp, normalizeOkxSymbol, normalizeSize, signPayload, signaturePrehash, toBar, toCanonicalOkxSymbol, toOkxSwapInstId } from "./rest.js";
1
+ import { BAR_MAP, OKX_INTERVAL_VOCABULARY, OkxRestClient, TradingServiceError, barDurationMs, buildAuthHeaders, coinsToContracts, contractsToCoins, isoTimestamp, normalizeOkxSymbol, normalizeSize, okxAssetClassOf, okxContractOf, signPayload, signaturePrehash, toBar, toCanonicalOkxSymbol, toOkxSwapInstId } from "./rest.js";
2
+ import { liveTradingEnabled } from "@dshtrading/authority";
2
3
  import { Service } from "@deepseek-ai/cordis";
3
4
  import { defineTool } from "@deepseek-ai/dsh-tools";
4
5
  import Schema from "@deepseek-ai/schemastery";
6
+ import { instrumentFormOf } from "@dshtrading/api";
5
7
  import { createGetIndicatorsTool } from "@dshtrading/indicators/tool";
6
8
  //#region src/index.ts
7
9
  /**
10
+ * OKX 连接器插件(dsh-trading okx 切片 R1-R3):真实 MarketDataService + crypto 市场
11
+ * 第一个真实 TradeService。
12
+ *
13
+ * 与 connector-binance 的关系(主 agent 裁决 2026-08-29,方案 B+C):
14
+ * - **互斥激活**:Config.enabled(默认 false)。false 时整个 apply() 静默退出——不
15
+ * provide 服务、不注册任何工具,`tradingCryptoMarketData` 与同名 `crypto_*` 工具
16
+ * 归 connector-binance(binance 不动);true 时由本插件独占。两连接器并存于同一
17
+ * 组合时必须恰好激活一个。为防御误配置,工具注册走 duplicate-safe 路径:名字已被
18
+ * 占用时跳过并 log(dsh-tools 对重复名直接抛错会炸 boot)。
19
+ * - **交易服务分离**:okx 额外 provide `tradingCryptoTrade`(api 包 TradeService 的
20
+ * 第一个真实实现;类型增强声明在 @dshtrading/api)。
21
+ *
22
+ * 三态环境语义(主 agent 裁决,映射铁律 #3 三段闸门):
23
+ * - dryRun=true(缺省)→ 本地模拟回执,不发任何请求;
24
+ * - dryRun=false + 未获实盘授权 → 结构化拒绝(headless 唯一防线);
25
+ * - dryRun=false + 实盘已获授权 + env='demo'(缺省)→ 真实签名下单 +
26
+ * `x-simulated-trading: 1`,成交在 OKX 模拟盘(获得实盘授权后的第一默认目标
27
+ * 是 demo 而非真钱);
28
+ * - env='live' → 真实实盘(无模拟盘头;base 统一审批闸门照旧 ask,headless fail-closed)。
29
+ *
30
+ * 合约交易语义(P7,2026-10-08):永续可下单(tdMode 缺省 cross,isolated 需先
31
+ * crypto_set_leverage 设好该标的的逐仓杠杆)、杠杆/保证金模式设置工具
32
+ * crypto_set_leverage(与下单同一套三态闸门 + base 审批闸门),持仓回带
33
+ * 强平价/维持保证金率/名义价值;张↔币换算的唯一实现在 rest.ts 的
34
+ * coinsToContracts(向下取整,绝不上取放大敞口)。
35
+ *
36
+ * 凭证(调研 §6 建议 4):三 ref(apiKeyRef/secretRef/passphraseRef)= 环境变量名;
37
+ * demo/live 用不同 ref 组(demo 默认 OKX_DEMO_*,live 默认 OKX_*),每次操作经
38
+ * ctx.credentials.resolve() 解析(换 key 无需重启);有 seam 时未命中即
39
+ * TRADING_CREDENTIALS_MISSING 带 ref 名(不回退启动环境变量),无 seam 才回落。
40
+ *
41
+ * @module @dshtrading/connector-okx
42
+ */
43
+ /**
8
44
  * Cordis 插件名 = preset 行 id(TEMPLATES §8):`dsh-trading-crypto-*` 市场命名空间,
9
45
  * 全仓唯一,绝不使用 `base` 等官方保留 id(insert-only 铁律 #1)。
10
46
  */
@@ -42,7 +78,9 @@ function credentialRefsFor(config, env = config.env) {
42
78
  }
43
79
  /**
44
80
  * 三 ref 凭证解析:每次操作调用(ctx.credentials 的设计意图——换 key 无需重启插件)。
45
- * 无 credentials seam 时回落启动环境变量(llm-deepseek 同款降级)。
81
+ * **有 credentials seam 即 fail-closed**:seam 存在时只认 seam,某 slot 未命中不再回退
82
+ * 启动环境变量——显式配置与 ambient 凭据混用会让 demo 路径拿到 live key(反之亦然);
83
+ * 仅当 seam 完全缺席时才回落启动环境变量(llm-deepseek 同款降级)。
46
84
  * 任何一处未命中/无效 → TRADING_CREDENTIALS_MISSING,消息只带 ref 名(绝不带值)。
47
85
  */
48
86
  async function resolveCredentials(ctx, config) {
@@ -65,17 +103,17 @@ async function resolveCredentials(ctx, config) {
65
103
  ["passphraseRef", refs.passphraseRef]
66
104
  ];
67
105
  const resolved = await Promise.all(entries.map(async ([slot, ref]) => {
68
- let value;
69
- if (resolver !== void 0) value = (await resolver.resolve(ref))?.value;
70
- if (value === void 0 || value === "") value = process.env[ref];
71
106
  return {
72
107
  slot,
73
108
  ref,
74
- value
109
+ value: resolver === void 0 ? process.env[ref] : (await resolver.resolve(ref))?.value
75
110
  };
76
111
  }));
77
112
  const missing = resolved.filter((part) => part.value === void 0 || part.value === "");
78
- if (missing.length > 0) throw new TradingServiceError("TRADING_CREDENTIALS_MISSING", `connector-okx: missing OKX ${config.env} credentials — provide ` + missing.map((part) => `${part.slot}=${part.ref}`).join(", ") + ` through the credentials service or the launching environment (env=${config.env}; demo and live API keys are separate and not interchangeable)`);
113
+ if (missing.length > 0) {
114
+ const sourceHint = resolver === void 0 ? "through the launching environment (no credentials seam is configured)" : "through the credentials service (a credentials seam is configured; the launching environment is not consulted)";
115
+ throw new TradingServiceError("TRADING_CREDENTIALS_MISSING", `connector-okx: missing OKX ${config.env} credentials — provide ` + missing.map((part) => `${part.slot}=${part.ref}`).join(", ") + ` ${sourceHint} (env=${config.env}; demo and live API keys are separate and not interchangeable)`);
116
+ }
79
117
  const [apiKey, secret, passphrase] = resolved.map((part) => part.value);
80
118
  return {
81
119
  key: apiKey,
@@ -230,7 +268,7 @@ var OkxTradeService = class extends Service {
230
268
  const bucket = `${this.simulated ? "demo" : "live"}:${instId}`;
231
269
  const cached = this.instruments.get(bucket);
232
270
  if (cached !== void 0 && Date.now() - cached.at < INSTRUMENT_TTL_MS) return cached.instrument;
233
- const instType = instId.endsWith("-SWAP") ? "SWAP" : "SPOT";
271
+ const instType = instrumentFormOf(instId) === "perp" ? "SWAP" : "SPOT";
234
272
  const row = (await this.client.getInstruments(instType, instId))[0];
235
273
  if (row === void 0) throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `OKX: unknown instId ${instId} (instType=${instType})`);
236
274
  this.instruments.set(bucket, {
@@ -255,7 +293,7 @@ var OkxTradeService = class extends Service {
255
293
  * 返回 Map 只含成功项:查不到的 instId 缺席,调用点按原语义保留张数原值(不再逐行重试)。
256
294
  */
257
295
  async prefetchInstruments(instIds) {
258
- const distinct = [...new Set([...instIds].filter((id) => id.endsWith("-SWAP")))];
296
+ const distinct = [...new Set([...instIds].filter((id) => instrumentFormOf(id) === "perp"))];
259
297
  const resolved = /* @__PURE__ */ new Map();
260
298
  for (let i = 0; i < distinct.length; i += INSTRUMENT_PREFETCH_CONCURRENCY) await Promise.all(distinct.slice(i, i + INSTRUMENT_PREFETCH_CONCURRENCY).map(async (id) => {
261
299
  try {
@@ -272,11 +310,11 @@ var OkxTradeService = class extends Service {
272
310
  * 未来任何新消费面)同样 fail-closed;工具层 evaluateOrderGate + base 审批闸门保留
273
311
  * (双保险),工具层只做参数预检与富回执。
274
312
  *
275
- * - 闸门 ① reject(dryRun=false 请求实盘而 liveTrading=false)→ 结构化错误抛出
313
+ * - 闸门 ① reject(dryRun=false 请求实盘而未获实盘授权)→ 结构化错误抛出
276
314
  * (TRADING_LIVE_TRADING_DISABLED,api TradingError 契约);
277
315
  * - 闸门 ② simulate(dryRun 缺省/true,或 config.dryRun 强制模拟)→ 本地模拟回执
278
316
  * (Order.dryRun=true,不触网;工具层另有带市价参照的富回执);
279
- * - 闸门 ③ live(dryRun=false 且 liveTrading=true)→ 真实签名下单(env=demo 加模拟盘头)。
317
+ * - 闸门 ③ live(dryRun=false 且实盘已获授权)→ 真实签名下单(env=demo 加模拟盘头)。
280
318
  *
281
319
  * **sz 单位纪律(调研 §4,实现期最重要的换算)**:
282
320
  * - api `OrderRequest.quantity` 语义恒为 base 币数;
@@ -287,6 +325,10 @@ var OkxTradeService = class extends Service {
287
325
  * ctVal/lotSz/minSz 换算并本地校验(向下取整,省一次 51000 往返)。
288
326
  */
289
327
  async placeOrder(req) {
328
+ if (req.marginMode !== void 0) {
329
+ if (req.marginMode !== "cross" && req.marginMode !== "isolated") throw new TradingServiceError("TRADING_NOT_IMPLEMENTED", `OKX place order: unknown marginMode ${JSON.stringify(req.marginMode)} — only 'cross' and 'isolated' are implemented`);
330
+ if (instrumentFormOf(req.symbol) !== "perp") throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `OKX place order: marginMode=${req.marginMode} is only valid for perpetual (${req.symbol}-SWAP shaped) symbols; spot orders must stay cash (spot margin trading is not implemented)`);
331
+ }
290
332
  const verdict = evaluateOrderGate(this.config, {
291
333
  instId: req.symbol,
292
334
  side: req.side,
@@ -313,9 +355,10 @@ var OkxTradeService = class extends Service {
313
355
  const instId = normalizeOkxSymbol(req.symbol);
314
356
  const instrument = await this.getInstrument(instId);
315
357
  const normalized = normalizeSize(instId, instrument, req.quantity);
358
+ const marginMode = req.marginMode ?? "cross";
316
359
  const params = {
317
360
  instId,
318
- tdMode: instrument.instType === "SWAP" ? "cross" : "cash",
361
+ tdMode: instrument.instType === "SWAP" ? marginMode : "cash",
319
362
  side: req.side,
320
363
  ordType: req.type,
321
364
  sz: normalized.sz,
@@ -339,6 +382,62 @@ var OkxTradeService = class extends Service {
339
382
  };
340
383
  }
341
384
  /**
385
+ * 设置合约杠杆与保证金模式(P7;api TradeService.setLeverage)。
386
+ *
387
+ * 与 placeOrder 共用同一套三态闸门(服务缝第一步——绕过工具层的直调同样 fail-closed):
388
+ * - ① dryRun=false 而未获实盘授权 → TRADING_LIVE_TRADING_DISABLED;
389
+ * - ② dryRun 缺省/true(或被 config.dryRun 强制)→ 本地模拟回执,不触网;
390
+ * - ③ dryRun=false 且已获授权 → 真实签名 POST /api/v5/account/set-leverage
391
+ * (env=demo 带 x-simulated-trading:1)。
392
+ *
393
+ * 本地判据(先于上游往返,给出可读原因、不静默改写调用方意图):
394
+ * - 仅永续(form=perp):现货传 setLeverage 报 TRADING_UNSUPPORTED_SYMBOL;
395
+ * - leverage > 交易所规格 lever(InstrumentContract.maxLeverage)→ 结构化拒绝,
396
+ * **不做上限截断**(静默改小杠杆会让调用方以为自己设定的数字已生效);
397
+ * - mgnMode 只认 cross/isolated;isolated + 双向持仓模式的 posSide 由调用方传,
398
+ * 本仓不猜账户持仓模式(缺席即原样透传,交交易所裁定)。
399
+ */
400
+ async setLeverage(req) {
401
+ const marginMode = req.marginMode;
402
+ if (marginMode !== "cross" && marginMode !== "isolated") throw new TradingServiceError("TRADING_NOT_IMPLEMENTED", `OKX set leverage: unknown marginMode ${JSON.stringify(marginMode)} — only 'cross' and 'isolated' are implemented`);
403
+ if (!Number.isFinite(req.leverage) || req.leverage <= 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX set leverage: leverage ${String(req.leverage)} must be a positive number`);
404
+ if (instrumentFormOf(req.symbol) !== "perp") throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `OKX set leverage: ${req.symbol} is not a perpetual — leverage/margin mode only applies to -SWAP symbols`);
405
+ const requestedDryRun = req.dryRun ?? true;
406
+ if (!requestedDryRun && !liveTradingEnabled(this.config.liveTrading)) throw new TradingServiceError("TRADING_LIVE_TRADING_DISABLED", `crypto_set_leverage rejected: the call requests real execution (dryRun=${String(req.dryRun)}) but live trading is disabled (no signed live-trading grant from the authority plane). Ask the operator to sign a live-trading grant after confirmation, or keep dryRun=true for a simulated receipt.`);
407
+ const canonical = toCanonicalOkxSymbol(normalizeOkxSymbol(req.symbol));
408
+ const optional = {
409
+ ...req.posSide !== void 0 ? { posSide: req.posSide } : {},
410
+ ...req.ccy !== void 0 ? { ccy: req.ccy } : {}
411
+ };
412
+ if (requestedDryRun || this.config.dryRun) return {
413
+ symbol: canonical,
414
+ leverage: req.leverage,
415
+ marginMode,
416
+ ...optional,
417
+ dryRun: true,
418
+ timestamp: Date.now()
419
+ };
420
+ const instId = normalizeOkxSymbol(req.symbol);
421
+ const max = (await this.getInstrument(instId)).maxLeverage;
422
+ if (max !== void 0 && Number.isFinite(max) && req.leverage > max) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX set leverage for ${instId}: requested ${req.leverage}x exceeds the exchange maximum lever ${max}x — refusing (no silent clamp)`);
423
+ const params = {
424
+ instId,
425
+ lever: String(req.leverage),
426
+ mgnMode: marginMode,
427
+ ...optional
428
+ };
429
+ const credentials = await this.getCredentials();
430
+ await this.client.setLeverage(params, this.auth(credentials));
431
+ return {
432
+ symbol: canonical,
433
+ leverage: req.leverage,
434
+ marginMode,
435
+ ...optional,
436
+ dryRun: false,
437
+ timestamp: Date.now()
438
+ };
439
+ }
440
+ /**
342
441
  * 撤单。OKX 按 instId + ordId 双键定位,symbol(instId)必填——api 契约的
343
442
  * cancelOrder(id) 单参形态对 OKX 不够,扩展第二可选参数(api 包 R3 修订)。
344
443
  *
@@ -346,7 +445,7 @@ var OkxTradeService = class extends Service {
346
445
  * 不存在)视作终态成功——撤单语义是「确保不再成交」,订单已终态即达成。
347
446
  */
348
447
  async cancelOrder(id, symbol) {
349
- if (!this.config.liveTrading || this.config.dryRun) throw new TradingServiceError("TRADING_LIVE_TRADING_DISABLED", "OKX cancelOrder rejected at the service seam: cancel is a live action and requires liveTrading=true with dryRun=false (keep liveTrading=false if the order was not placed through this service).");
448
+ if (!liveTradingEnabled(this.config.liveTrading) || this.config.dryRun) throw new TradingServiceError("TRADING_LIVE_TRADING_DISABLED", "OKX cancelOrder rejected at the service seam: cancel is a live action and requires a signed live-trading grant (see @dshtrading/authority) with dryRun=false (leave the live-trading authority ungranted if the order was not placed through this service).");
350
449
  if (symbol === void 0 || symbol === "") throw new TradingServiceError("TRADING_EXCHANGE_ERROR", "OKX cancelOrder requires the instId (symbol) together with the order id — OKX locates orders by (instId, ordId)");
351
450
  const instId = normalizeOkxSymbol(symbol);
352
451
  const credentials = await this.getCredentials();
@@ -390,7 +489,12 @@ var OkxTradeService = class extends Service {
390
489
  timestamp: Number.isFinite(timestamp) ? timestamp : Date.now()
391
490
  };
392
491
  }
393
- /** 只读持仓(SWAP 的 pos 单位是张 → 经 ctVal 换算成币;net 模式负 pos = short)。 */
492
+ /**
493
+ * 只读持仓(SWAP 的 pos 单位是张 → 经 ctVal 换算成币;net 模式负 pos = short)。
494
+ * 合约仓位附带交易所回传的强平价(liqPx)、维持保证金率(mgnRatio)、保证金模式
495
+ * (mgnMode)与名义价值(notionalUsd)——风控清单的「强平距离」判据读这些字段,
496
+ * 每条都是交易所口径,缺席即不展示(不本地补算)。
497
+ */
394
498
  async getPositions() {
395
499
  const credentials = await this.getCredentials();
396
500
  const rows = await this.client.getPositions(this.auth(credentials));
@@ -402,7 +506,7 @@ var OkxTradeService = class extends Service {
402
506
  const pos = typeof d.pos === "string" || typeof d.pos === "number" ? Number(d.pos) : NaN;
403
507
  if (instId === void 0 || !Number.isFinite(pos)) continue;
404
508
  let size = Math.abs(pos);
405
- if (instId.endsWith("-SWAP")) {
509
+ if (instrumentFormOf(instId) === "perp") {
406
510
  const instrument = instruments.get(instId);
407
511
  if (instrument?.ctVal !== void 0) size = size * instrument.ctVal;
408
512
  }
@@ -412,6 +516,10 @@ var OkxTradeService = class extends Service {
412
516
  const markPrice = typeof d.markPx === "string" ? Number(d.markPx) : void 0;
413
517
  const unrealizedPnl = typeof d.upl === "string" ? Number(d.upl) : void 0;
414
518
  const leverage = typeof d.lever === "string" ? Number(d.lever) : void 0;
519
+ const liquidationPrice = pickNumber(d.liqPx);
520
+ const marginRatio = pickNumber(d.mgnRatio);
521
+ const marginMode = d.mgnMode === "isolated" ? "isolated" : d.mgnMode === "cross" ? "cross" : void 0;
522
+ const notionalUsd = pickNumber(d.notionalUsd);
415
523
  const timestamp = typeof d.uTime === "string" ? Number(d.uTime) : Date.now();
416
524
  positions.push({
417
525
  symbol: toCanonicalOkxSymbol(instId),
@@ -421,6 +529,10 @@ var OkxTradeService = class extends Service {
421
529
  ...markPrice !== void 0 && Number.isFinite(markPrice) ? { markPrice } : {},
422
530
  ...unrealizedPnl !== void 0 && Number.isFinite(unrealizedPnl) ? { unrealizedPnl } : {},
423
531
  ...leverage !== void 0 && Number.isFinite(leverage) ? { leverage } : {},
532
+ ...liquidationPrice !== void 0 ? { liquidationPrice } : {},
533
+ ...marginRatio !== void 0 ? { marginRatio } : {},
534
+ ...marginMode !== void 0 ? { marginMode } : {},
535
+ ...notionalUsd !== void 0 ? { notionalUsd } : {},
424
536
  timestamp: Number.isFinite(timestamp) ? timestamp : Date.now()
425
537
  });
426
538
  }
@@ -456,7 +568,7 @@ var OkxTradeService = class extends Service {
456
568
  async toCoins(instId, exchangeAmount, instruments) {
457
569
  const n = typeof exchangeAmount === "string" || typeof exchangeAmount === "number" ? Number(exchangeAmount) : NaN;
458
570
  if (!Number.isFinite(n)) return void 0;
459
- if (!instId.endsWith("-SWAP")) return n;
571
+ if (instrumentFormOf(instId) !== "perp") return n;
460
572
  if (instruments !== void 0) {
461
573
  const instrument = instruments.get(instId);
462
574
  return instrument?.ctVal !== void 0 ? n * instrument.ctVal : n;
@@ -551,10 +663,10 @@ function mapOrderState(state) {
551
663
  }
552
664
  function evaluateOrderGate(config, args) {
553
665
  const requestedDryRun = args.dryRun ?? true;
554
- if (!requestedDryRun && !config.liveTrading) return {
666
+ if (!requestedDryRun && !liveTradingEnabled(config.liveTrading)) return {
555
667
  action: "reject",
556
668
  code: "TRADING_LIVE_TRADING_DISABLED",
557
- message: `crypto_place_order rejected: the call requests real execution (dryRun=${String(args.dryRun)}) but live trading is disabled (liveTrading=false). Ask the user to enable liveTrading explicitly after confirmation, or keep dryRun=true for a simulated fill.`
669
+ message: `crypto_place_order rejected: the call requests real execution (dryRun=${String(args.dryRun)}) but live trading is disabled (no signed live-trading grant from the authority plane). Ask the operator to sign a live-trading grant (packages/authority/bin/sign-live-trading.mjs) after confirmation, or keep dryRun=true for a simulated fill.`
558
670
  };
559
671
  if (requestedDryRun || config.dryRun) return { action: "simulate" };
560
672
  return {
@@ -610,6 +722,7 @@ async function buildDryRunReceipt(args, marketData) {
610
722
  quantity: args.quantity,
611
723
  quantityUnit: "base-asset coins (SWAP orders would be converted to contracts by ctVal)",
612
724
  ...args.type === "limit" ? { price: args.price } : {},
725
+ ...args.marginMode !== void 0 ? { marginMode: args.marginMode } : {},
613
726
  reference,
614
727
  timestamp: Date.now()
615
728
  });
@@ -624,7 +737,7 @@ async function buildDryRunReceipt(args, marketData) {
624
737
  function createPlaceOrderTool(deps) {
625
738
  return defineTool({
626
739
  name: "crypto_place_order",
627
- description: "Place an OKX spot or perpetual-swap (SWAP) order, or simulate one. instId accepts market-canonical (BTCUSDT, BTCUSDT-SWAP) or OKX native (BTC-USDT, BTC-USDT-SWAP) vocabulary. quantity is in BASE-ASSET coins: spot MARKET orders are sent with tgtCcy=base_ccy (OKX default for buys is quote-currency amount — a known trap), and SWAP quantities are converted to contracts via ctVal automatically. dryRun defaults to true and returns a DRY-RUN simulated fill receipt with the current market price as reference. Real execution (dryRun=false) requires the plugin liveTrading switch plus user approval; with env=demo (default) the order is signed and routed to the OKX demo exchange (simulated trading), env=live is real money.",
740
+ description: "Place an OKX spot or perpetual-swap order, or simulate one. instId accepts market-canonical (BTCUSDT, BTCUSDT-SWAP) or OKX native (BTC-USDT). quantity is in BASE-ASSET coins: spot MARKET orders are sent with tgtCcy=base_ccy (the OKX buy default is a quote-currency amount — a known trap) and SWAP quantities convert via ctVal. Perpetual orders default to cross margin; pass marginMode=isolated only after crypto_set_leverage has set the per-instrument isolated leverage. dryRun defaults to true and returns a DRY-RUN receipt; dryRun=false needs a signed live-trading grant plus approval. env=live is real money.",
628
741
  parameters: {
629
742
  instId: {
630
743
  type: "string",
@@ -652,9 +765,14 @@ function createPlaceOrderTool(deps) {
652
765
  type: "number",
653
766
  description: "Limit price; required when type=limit"
654
767
  },
768
+ marginMode: {
769
+ type: "string",
770
+ enum: ["cross", "isolated"],
771
+ description: "Perpetual margin mode (default cross). Isolated requires crypto_set_leverage for the instrument first; spot orders reject this field."
772
+ },
655
773
  dryRun: {
656
774
  type: "boolean",
657
- description: "true (default) = simulate only and return a DRY-RUN receipt; false = request real execution (gated by liveTrading, env and user approval)",
775
+ description: "true (default) = simulate only and return a DRY-RUN receipt; false = request real execution (gated by the signed live-trading authority, env and user approval)",
658
776
  default: true
659
777
  }
660
778
  },
@@ -681,12 +799,103 @@ function createPlaceOrderTool(deps) {
681
799
  type: args.type,
682
800
  quantity: args.quantity,
683
801
  ...args.type === "limit" ? { price: args.price } : {},
802
+ ...args.marginMode !== void 0 ? { marginMode: args.marginMode } : {},
684
803
  dryRun: false
685
804
  });
686
805
  return JSON.stringify(order);
687
806
  }
688
807
  });
689
808
  }
809
+ /** 参数校验(模型调用问题抛普通 Error,与 createPlaceOrderTool 先例一致)。 */
810
+ function validateSetLeverageArgs(args) {
811
+ try {
812
+ normalizeOkxSymbol(args.instId);
813
+ } catch {
814
+ throw new Error(`crypto_set_leverage: invalid instId ${JSON.stringify(args.instId)} — expected a perpetual id (BTCUSDT-SWAP canonical or BTC-USDT-SWAP native)`);
815
+ }
816
+ if (instrumentFormOf(args.instId) !== "perp") throw new Error(`crypto_set_leverage: ${args.instId} is not a perpetual — leverage/margin mode only applies to -SWAP symbols`);
817
+ if (typeof args.leverage !== "number" || !Number.isFinite(args.leverage) || args.leverage <= 0) throw new Error(`crypto_set_leverage: invalid leverage ${JSON.stringify(args.leverage)} — expected a positive multiplier`);
818
+ if (args.marginMode !== void 0 && args.marginMode !== "cross" && args.marginMode !== "isolated") throw new Error(`crypto_set_leverage: invalid marginMode ${JSON.stringify(args.marginMode)} — expected cross or isolated`);
819
+ if (args.posSide !== void 0 && args.posSide !== "long" && args.posSide !== "short") throw new Error(`crypto_set_leverage: invalid posSide ${JSON.stringify(args.posSide)} — expected long or short`);
820
+ }
821
+ /**
822
+ * crypto_set_leverage 工具工厂(独立导出便于单测闸门与参数矩阵)。
823
+ *
824
+ * 审批不在这里做:dryRun!==true 的调用由 @dshtrading/base 的统一审批闸门在
825
+ * `tools/pre-execute` waterfall ask(headless 下 ask=deny,fail-closed);
826
+ * 工具内不再重复调 ctx.approval。
827
+ */
828
+ function createSetLeverageTool(deps) {
829
+ return defineTool({
830
+ name: "crypto_set_leverage",
831
+ description: "Set the OKX perpetual leverage and margin mode for one instrument. This changes real account risk parameters, so it is gated exactly like an order: dryRun defaults to true and returns a DRY-RUN receipt without sending any request; dryRun=false needs a signed live-trading grant plus user approval (env=live is real money). The exchange maximum leverage is enforced on the real path; an out-of-range value is rejected, never silently clamped. Isolated margin also requires this call for the instrument before placing isolated orders.",
832
+ parameters: {
833
+ instId: {
834
+ type: "string",
835
+ required: true,
836
+ description: "Perpetual instrument id — market-canonical (BTCUSDT-SWAP) or OKX native (BTC-USDT-SWAP); spot symbols are rejected"
837
+ },
838
+ leverage: {
839
+ type: "number",
840
+ required: true,
841
+ description: "Target leverage multiplier (> 0). Must not exceed the exchange maximum (contract.maxLeverage); it is rejected, never clamped."
842
+ },
843
+ marginMode: {
844
+ type: "string",
845
+ enum: ["cross", "isolated"],
846
+ description: "Margin mode: cross (default, whole-account) or isolated (per-position; OKX also needs posSide in long/short position mode)",
847
+ default: "cross"
848
+ },
849
+ posSide: {
850
+ type: "string",
851
+ enum: ["long", "short"],
852
+ description: "Position direction — required by OKX in long/short position mode, omit in net mode"
853
+ },
854
+ ccy: {
855
+ type: "string",
856
+ description: "Margin currency for cross mode under multi-currency margin; omit otherwise"
857
+ },
858
+ dryRun: {
859
+ type: "boolean",
860
+ description: "true (default) = simulate only and return a DRY-RUN receipt; false = request real execution (gated by the signed live-trading authority, env and user approval)",
861
+ default: true
862
+ }
863
+ },
864
+ output: {
865
+ schema: { type: "string" },
866
+ render: (_args, value) => [{
867
+ type: "text",
868
+ text: value
869
+ }]
870
+ },
871
+ async execute(raw) {
872
+ const args = normalizeSetLeverageArgs(raw);
873
+ validateSetLeverageArgs(args);
874
+ const setLeverage = deps.trade.setLeverage;
875
+ if (typeof setLeverage !== "function") throw new TradingServiceError("TRADING_NOT_IMPLEMENTED", "crypto_set_leverage: the routed trade connector does not implement setLeverage");
876
+ const setting = await setLeverage.call(deps.trade, {
877
+ symbol: args.instId,
878
+ leverage: args.leverage,
879
+ marginMode: args.marginMode ?? "cross",
880
+ ...args.posSide !== void 0 ? { posSide: args.posSide } : {},
881
+ ...args.ccy !== void 0 ? { ccy: args.ccy } : {},
882
+ ...args.dryRun !== void 0 ? { dryRun: args.dryRun } : {}
883
+ });
884
+ return JSON.stringify(setting.dryRun ? {
885
+ ...setting,
886
+ note: "DRY-RUN — simulated leverage change; no request was sent to OKX. The exchange maximum leverage is validated only on a real (dryRun=false) request."
887
+ } : setting);
888
+ }
889
+ });
890
+ }
891
+ function normalizeSetLeverageArgs(raw) {
892
+ const args = raw ?? {};
893
+ const instId = typeof args.instId === "string" ? args.instId.trim().toUpperCase() : void 0;
894
+ return {
895
+ ...args,
896
+ instId
897
+ };
898
+ }
690
899
  /**
691
900
  * 互斥激活的注册面:dsh-tools 对同名重复注册直接抛错(会炸 boot/preset 挂载),
692
901
  * 而互斥纪律下「同时至多一个连接器激活」只是配置约定。这里把冲突降级为
@@ -817,6 +1026,7 @@ function apply(ctx, config) {
817
1026
  trade,
818
1027
  config
819
1028
  }), log);
1029
+ registerTool(ctx, createSetLeverageTool({ trade }), log);
820
1030
  registerTool(ctx, defineTool({
821
1031
  name: "crypto_cancel_order",
822
1032
  description: "Cancel an OKX order by (instId, ordId). Cancelling an already-terminal order (filled/canceled) is reported as already-terminal, not an error.",
@@ -898,7 +1108,7 @@ function apply(ctx, config) {
898
1108
  }), log);
899
1109
  registerTool(ctx, defineTool({
900
1110
  name: "crypto_get_positions",
901
- description: "Read the OKX account positions (size converted from contracts to coins for swaps, entry/mark price, unrealized PnL, leverage). Read-only.",
1111
+ description: "Read the OKX account positions: size is converted from contracts to coins for swaps, with entry/mark price, unrealized PnL and leverage. Perpetual positions also carry the exchange-reported liquidation price, maintenance margin ratio, margin mode (cross/isolated) and USD notional when OKX returns them — these drive the pre-trade risk checklist; absent means the exchange did not report it, never a locally computed value. Read-only.",
902
1112
  parameters: {},
903
1113
  output: {
904
1114
  schema: { type: "string" },
@@ -919,4 +1129,4 @@ function apply(ctx, config) {
919
1129
  });
920
1130
  }
921
1131
  //#endregion
922
- export { BAR_MAP, Config, OKX_INTERVAL_VOCABULARY, OkxMarketDataService, OkxRestClient, OkxTradeService, ROUTER_PROVIDER, TRADING_CRYPTO_MARKET_DATA_KEY, TRADING_CRYPTO_TRADE_KEY, TradingServiceError, apply, barDurationMs, buildAuthHeaders, buildDryRunReceipt, createPlaceOrderTool, credentialRefsFor, evaluateOrderGate, inject, isoTimestamp, mapOrderState, name, normalizeOkxSymbol, normalizeSize, resolveCredentials, signPayload, signaturePrehash, toBar, toCanonicalOkxSymbol, toOkxSwapInstId };
1132
+ export { BAR_MAP, Config, OKX_INTERVAL_VOCABULARY, OkxMarketDataService, OkxRestClient, OkxTradeService, ROUTER_PROVIDER, TRADING_CRYPTO_MARKET_DATA_KEY, TRADING_CRYPTO_TRADE_KEY, TradingServiceError, apply, barDurationMs, buildAuthHeaders, buildDryRunReceipt, coinsToContracts, contractsToCoins, createPlaceOrderTool, createSetLeverageTool, credentialRefsFor, evaluateOrderGate, inject, isoTimestamp, mapOrderState, name, normalizeOkxSymbol, normalizeSize, okxAssetClassOf, okxContractOf, resolveCredentials, signPayload, signaturePrehash, toBar, toCanonicalOkxSymbol, toOkxSwapInstId };
package/lib/rest.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { DerivativesPoint, Interval, Kline, Orderbook, Ticker, TradeTick, TradingErrorCode } from "@dshtrading/api";
1
+ import { DerivativesPoint, InstrumentAssetClass, InstrumentContract, InstrumentRef, Interval, Kline, Orderbook, Ticker, TradeTick, TradingErrorCode } from "@dshtrading/api";
2
2
  //#region src/rest.d.ts
3
3
  /** api 包 TradingError 契约的运行时 Error 实现(connector-binance 同款)。 */
4
4
  declare class TradingServiceError extends Error {
@@ -96,11 +96,18 @@ interface OkxInstrument {
96
96
  readonly settleCcy?: string;
97
97
  readonly baseCcy?: string;
98
98
  readonly quoteCcy?: string;
99
+ /** 最大杠杆(lever)。 */
100
+ readonly maxLeverage?: number;
101
+ /**
102
+ * 交易所分类标签(instCategory:1=加密、3=股票/ETF、4=大宗)。
103
+ * **无公开文档**(调研受限),语义靠真实网络实测反推;未登记即留空。
104
+ */
105
+ readonly instCategory?: number;
99
106
  }
100
- /** POST /api/v5/trade/order 请求体词汇(R3 只做 market/limit;tdMode 现货=cash、永续=cross)。 */
107
+ /** POST /api/v5/trade/order 请求体词汇(R3 只做 market/limit;tdMode 现货=cash、永续=cross/isolated)。 */
101
108
  interface OkxPlaceOrderParams {
102
109
  readonly instId: string;
103
- readonly tdMode: 'cash' | 'cross';
110
+ readonly tdMode: 'cash' | 'cross' | 'isolated';
104
111
  readonly side: 'buy' | 'sell';
105
112
  readonly ordType: 'market' | 'limit';
106
113
  /** 字符串数量:SPOT=base 币数(market 单显式 tgtCcy=base_ccy);SWAP=张(=coins/ctVal)。 */
@@ -108,6 +115,18 @@ interface OkxPlaceOrderParams {
108
115
  readonly px?: string;
109
116
  readonly tgtCcy?: 'base_ccy' | 'quote_ccy';
110
117
  }
118
+ /**
119
+ * POST /api/v5/account/set-leverage 请求体词汇(P7;仅 SWAP 走本仓接线)。
120
+ * `mgnMode` 与下单 `tdMode` 同词汇;逐仓/双向持仓模式下 `posSide` 必填(交易所裁定,
121
+ * 本仓不猜账户持仓模式:缺席即按原样透传,由交易所报错,不本地补位)。
122
+ */
123
+ interface OkxSetLeverageParams {
124
+ readonly instId: string;
125
+ readonly lever: string;
126
+ readonly mgnMode: 'cross' | 'isolated';
127
+ readonly posSide?: 'long' | 'short';
128
+ readonly ccy?: string;
129
+ }
111
130
  /** 客户端侧 sz 纪律校验结果:换算后的交易所数量字符串 + 提示(index.ts 组装请求体)。 */
112
131
  interface NormalizedSize {
113
132
  /** 发给交易所的 sz 字符串(SPOT=币数;SWAP=张)。 */
@@ -115,8 +134,24 @@ interface NormalizedSize {
115
134
  /** 现货市价单显式 tgtCcy=base_ccy(消除「buy 缺省按计价币金额」的坑,调研 §4)。 */
116
135
  readonly tgtCcy?: 'base_ccy';
117
136
  }
137
+ /**
138
+ * 张 ↔ 币换算(P7 的唯一换算实现,向量判据在 test/contract-trading.test.ts):
139
+ * 合约数量(张)= 币数 / ctVal,再按 lotSz 步进**向下**取整。
140
+ *
141
+ * 向下取整是安全方向:宁可少开,绝不因浮点误差把敞口放大一档。实现用
142
+ * **相对**容差吸收 `0.9999999999` 这类表示误差,并用一次硬性比较兜底
143
+ * (步进结果超过请求张数就退一档)——旧实现的 `Math.floor(x + 1e-9)`
144
+ * 在商刚好略小于整数时会向上跳一档,与「绝不上取」的注释不符。
145
+ */
146
+ declare function coinsToContracts(instId: string, instrument: OkxInstrument, quantityCoins: number): number;
147
+ /** 张 → 币(基币数量 = 张数 × ctVal;ctVal 缺席即拒绝,不虚构换算)。 */
148
+ declare function contractsToCoins(instId: string, instrument: OkxInstrument, contracts: number): number;
118
149
  /** 把 api 语义的 base 币数量换算成 OKX sz(本地精度校验:minSz/lotSz,省一次 51000 往返)。 */
119
150
  declare function normalizeSize(instId: string, instrument: OkxInstrument, quantityCoins: number): NormalizedSize;
151
+ /** instCategory(OKX 回字符串数字,实测;容忍 number 输入)→ 资产类别;未登记 = undefined。 */
152
+ declare function okxAssetClassOf(instCategory: unknown): InstrumentAssetClass | undefined;
153
+ /** SWAP 规格 → InstrumentContract(ctVal/tickSz/lotSz/lever/settleCcy 原样透传,不本地推断)。 */
154
+ declare function okxContractOf(inst: OkxInstrument): InstrumentContract | undefined;
120
155
  declare class OkxRestClient {
121
156
  #private;
122
157
  private readonly baseUrl;
@@ -196,19 +231,28 @@ declare class OkxRestClient {
196
231
  /** 合约/币对规格:GET /api/v5/public/instruments(sz 纪律的 ctVal/lotSz/minSz 来源)。 */
197
232
  getInstruments(instType: 'SPOT' | 'SWAP', instId?: string): Promise<OkxInstrument[]>;
198
233
  /**
199
- * 全部可交易现货标的名册(GET /api/v5/public/instruments?instType=SPOT,Issue #15)。
200
- * 输出 symbol 归一化为市场规范形(BTC-USDT → BTCUSDT),name 为 baseCcy/quoteCcy。
234
+ * 全部可交易标的名册 = 现货 ∪ 永续(GET /api/v5/public/instruments,Issue #15 扩展;
235
+ * 2026-10-08 形态维度落地)。
236
+ *
237
+ * - 现货:instType=SPOT → `BTCUSDT`(form=spot),name 取 baseCcy/quoteCcy。
238
+ * - 永续:instType=SWAP → `BTCUSDT-SWAP`(form=perp,与现货成对),name 取
239
+ * ctValCcy 与 instId 的计价段(SWAP 行的 baseCcy/quoteCcy 为空串,实测);
240
+ * assetClass 只由 instCategory 映射,contract 透传 ctVal/tickSz/lotSz/lever/settleCcy。
241
+ *
242
+ * 两半都拉取(Promise.all);任一半失败即抛结构化错误,不返回半份名册。
201
243
  */
202
- listInstruments(): Promise<Array<{
203
- symbol: string;
204
- name?: string;
205
- }>>;
244
+ listInstruments(): Promise<InstrumentRef[]>;
206
245
  /** 只读余额:GET /api/v5/account/balance(ccy 可选,逗号分隔 ≤20)。 */
207
246
  getBalance(auth: SignedAuth, ccy?: string): Promise<unknown[]>;
208
247
  /** 只读持仓:GET /api/v5/account/positions(instId 可选过滤)。 */
209
248
  getPositions(auth: SignedAuth, instId?: string): Promise<unknown[]>;
210
249
  /** 下单:POST /api/v5/trade/order(60 次/2s)。 */
211
250
  placeOrder(params: OkxPlaceOrderParams, auth: SignedAuth): Promise<unknown[]>;
251
+ /**
252
+ * 设置杠杆/保证金模式:POST /api/v5/account/set-leverage(P7;实盘动作,与下单同门槛)。
253
+ * 只发请求不判定闸门——三态闸门由 OkxTradeService.setLeverage 在调用本方法前执行。
254
+ */
255
+ setLeverage(params: OkxSetLeverageParams, auth: SignedAuth): Promise<unknown[]>;
212
256
  /** 撤单:POST /api/v5/trade/cancel-order(ordId 优先于 clOrdId,本切片只支持 ordId)。 */
213
257
  cancelOrder(instId: string, ordId: string, auth: SignedAuth): Promise<unknown[]>;
214
258
  /** 查单:GET /api/v5/trade/order(query 属于签名 requestPath)。 */
@@ -235,4 +279,4 @@ declare function toCanonicalOkxSymbol(symbol: string): string;
235
279
  */
236
280
  declare function toOkxSwapInstId(input: string): string;
237
281
  //#endregion
238
- export { BAR_MAP, NormalizedSize, OKX_INTERVAL_VOCABULARY, OkxCredentials, OkxFundingRate, OkxInstrument, OkxOpenInterest, OkxPlaceOrderParams, OkxRestClient, OkxRestOptions, SignedAuth, TradingServiceError, barDurationMs, buildAuthHeaders, isoTimestamp, normalizeOkxSymbol, normalizeSize, signPayload, signaturePrehash, toBar, toCanonicalOkxSymbol, toOkxSwapInstId };
282
+ export { BAR_MAP, NormalizedSize, OKX_INTERVAL_VOCABULARY, OkxCredentials, OkxFundingRate, OkxInstrument, OkxOpenInterest, OkxPlaceOrderParams, OkxRestClient, OkxRestOptions, OkxSetLeverageParams, SignedAuth, TradingServiceError, barDurationMs, buildAuthHeaders, coinsToContracts, contractsToCoins, isoTimestamp, normalizeOkxSymbol, normalizeSize, okxAssetClassOf, okxContractOf, signPayload, signaturePrehash, toBar, toCanonicalOkxSymbol, toOkxSwapInstId };
package/lib/rest.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { instrumentFormOf } from "@dshtrading/api";
1
2
  import { createHmac } from "node:crypto";
2
3
  //#region src/rest.ts
3
4
  /**
@@ -166,18 +167,51 @@ async function envelopeToError(res, path, parsedBody) {
166
167
  ].filter((part) => part !== void 0 && part !== "").join(" ");
167
168
  return new TradingServiceError(tradingCode, `OKX ${path}: ${detail}`, body);
168
169
  }
170
+ /**
171
+ * 张 ↔ 币换算(P7 的唯一换算实现,向量判据在 test/contract-trading.test.ts):
172
+ * 合约数量(张)= 币数 / ctVal,再按 lotSz 步进**向下**取整。
173
+ *
174
+ * 向下取整是安全方向:宁可少开,绝不因浮点误差把敞口放大一档。实现用
175
+ * **相对**容差吸收 `0.9999999999` 这类表示误差,并用一次硬性比较兜底
176
+ * (步进结果超过请求张数就退一档)——旧实现的 `Math.floor(x + 1e-9)`
177
+ * 在商刚好略小于整数时会向上跳一档,与「绝不上取」的注释不符。
178
+ */
179
+ function coinsToContracts(instId, instrument, quantityCoins) {
180
+ const ctVal = instrument.ctVal;
181
+ if (ctVal === void 0 || !Number.isFinite(ctVal) || ctVal <= 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX ${instId}: swap instrument has no usable ctVal (${String(ctVal)}) — refusing to guess a contract multiplier`);
182
+ const rawContracts = quantityCoins / ctVal;
183
+ if (!Number.isFinite(rawContracts) || rawContracts <= 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX ${instId}: quantity ${quantityCoins} coins does not convert to a positive contract amount (ctVal=${ctVal})`);
184
+ if (rawContracts < instrument.minSz) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX ${instId}: quantity ${quantityCoins} coins = ${rawContracts} contracts is below minSz ${instrument.minSz}`);
185
+ return stepDown(instId, rawContracts, instrument.lotSz, `coins=${quantityCoins}`);
186
+ }
187
+ /** 张 → 币(基币数量 = 张数 × ctVal;ctVal 缺席即拒绝,不虚构换算)。 */
188
+ function contractsToCoins(instId, instrument, contracts) {
189
+ const ctVal = instrument.ctVal;
190
+ if (ctVal === void 0 || !Number.isFinite(ctVal) || ctVal <= 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX ${instId}: swap instrument has no usable ctVal (${String(ctVal)})`);
191
+ return contracts * ctVal;
192
+ }
193
+ /**
194
+ * 按步进向下取整(安全方向:结果恒不超过 `amount`)。
195
+ * 容差是相对的(`Number.EPSILON` 量级),只吸收浮点表示误差;随后用一次
196
+ * 硬比较兜底,保证任何输入都不会被向上放大一档。
197
+ */
198
+ function stepDown(instId, amount, step, label) {
199
+ if (!Number.isFinite(step) || step <= 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX ${instId}: instrument has no usable lot step (${String(step)})`);
200
+ if (amount < step) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX ${instId}: ${label} is below the minimum lot step ${step}`);
201
+ const rawSteps = amount / step;
202
+ const tolerance = Math.max(Number.EPSILON * 8 * Math.abs(rawSteps), 1e-12);
203
+ let steps = Math.floor(rawSteps + tolerance);
204
+ if (steps * step > amount * (1 + 1e-12)) steps -= 1;
205
+ if (steps <= 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX ${instId}: ${label} rounds down to 0 at lotSz ${step}`);
206
+ return steps * step;
207
+ }
169
208
  /** 把 api 语义的 base 币数量换算成 OKX sz(本地精度校验:minSz/lotSz,省一次 51000 往返)。 */
170
209
  function normalizeSize(instId, instrument, quantityCoins) {
171
- const isSwap = instrument.instType === "SWAP";
172
- const amountInExchangeUnit = isSwap ? quantityCoins / (instrument.ctVal ?? NaN) : quantityCoins;
173
- if (!Number.isFinite(amountInExchangeUnit) || amountInExchangeUnit <= 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX ${instId}: quantity ${quantityCoins} does not convert to a positive exchange amount` + (isSwap ? ` (ctVal=${String(instrument.ctVal)})` : ""));
174
- if (amountInExchangeUnit < instrument.minSz) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX ${instId}: quantity ${quantityCoins} coins = ${amountInExchangeUnit} ${isSwap ? "contracts" : "base units"} is below minSz ${instrument.minSz}`);
175
- const step = instrument.lotSz;
176
- const units = Math.floor(amountInExchangeUnit / step + 1e-9) * step;
177
- if (units <= 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX ${instId}: quantity ${quantityCoins} coins rounds down to 0 at lotSz ${step}`);
178
- const sz = trimNumber(units);
179
- return isSwap ? { sz } : {
180
- sz,
210
+ if (instrument.instType === "SWAP") return { sz: trimNumber(coinsToContracts(instId, instrument, quantityCoins)) };
211
+ if (!Number.isFinite(quantityCoins) || quantityCoins <= 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX ${instId}: quantity ${quantityCoins} is not a positive base-asset amount`);
212
+ if (quantityCoins < instrument.minSz) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX ${instId}: quantity ${quantityCoins} coins is below minSz ${instrument.minSz}`);
213
+ return {
214
+ sz: trimNumber(stepDown(instId, quantityCoins, instrument.lotSz, `quantity=${quantityCoins} coins`)),
181
215
  tgtCcy: "base_ccy"
182
216
  };
183
217
  }
@@ -186,6 +220,37 @@ function trimNumber(n) {
186
220
  const fixed = n.toFixed(12).replace(/0+$/, "").replace(/\.$/, "");
187
221
  return fixed === "" ? "0" : fixed;
188
222
  }
223
+ /**
224
+ * OKX instCategory → 资产类别。
225
+ *
226
+ * **无公开文档**(调研受限),语义靠 2026-10-08 真实网络实测反推:SWAP 共 500 个
227
+ * (全部 state=live)中 1=加密 301、3=股票/ETF 190、4=大宗 9;SPOT 1144 个中同样
228
+ * 出现 3(127)/4(4)类。只登记实测到的取值;**未登记/缺失一律 undefined(留空),
229
+ * 禁止按符号猜**——反例硬证据:`SPX-USDT-SWAP` 的 instCategory=1(那是迷因币
230
+ * SPX6900),标普 500 是 `US500-USDT-SWAP`(instCategory=3)。
231
+ * 原始响应与计数见 spikes/impl-crypto-perp-tradfi/EVIDENCE.md。
232
+ */
233
+ const OKX_INST_CATEGORY_ASSET_CLASS = {
234
+ "1": "crypto",
235
+ "3": "equity",
236
+ "4": "commodity"
237
+ };
238
+ /** instCategory(OKX 回字符串数字,实测;容忍 number 输入)→ 资产类别;未登记 = undefined。 */
239
+ function okxAssetClassOf(instCategory) {
240
+ if (typeof instCategory !== "string" && typeof instCategory !== "number") return void 0;
241
+ return OKX_INST_CATEGORY_ASSET_CLASS[String(instCategory).trim()];
242
+ }
243
+ /** SWAP 规格 → InstrumentContract(ctVal/tickSz/lotSz/lever/settleCcy 原样透传,不本地推断)。 */
244
+ function okxContractOf(inst) {
245
+ if (inst.instType !== "SWAP") return void 0;
246
+ return {
247
+ tickSize: inst.tickSz,
248
+ lotSize: inst.lotSz,
249
+ ...inst.ctVal !== void 0 ? { multiplier: inst.ctVal } : {},
250
+ ...inst.maxLeverage !== void 0 ? { maxLeverage: inst.maxLeverage } : {},
251
+ ...inst.settleCcy !== void 0 ? { settleCcy: inst.settleCcy } : {}
252
+ };
253
+ }
189
254
  var OkxRestClient = class {
190
255
  baseUrl;
191
256
  timeoutMs;
@@ -279,7 +344,7 @@ var OkxRestClient = class {
279
344
  const d = (await this.request("/api/v5/market/ticker", { query: { instId: id } }))[0];
280
345
  const price = num(d?.last);
281
346
  if (price === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX ticker for ${id}: missing/invalid last price`);
282
- const volume = id.endsWith("-SWAP") ? num(d?.volCcy24h) ?? num(d?.vol24h) : num(d?.vol24h);
347
+ const volume = instrumentFormOf(id) === "perp" ? num(d?.volCcy24h) ?? num(d?.vol24h) : num(d?.vol24h);
283
348
  const prevClose = num(d?.open24h) ?? num(d?.sodUtc0);
284
349
  const changePercent = price !== void 0 && prevClose !== void 0 && prevClose > 0 ? (price - prevClose) / prevClose * 100 : void 0;
285
350
  return {
@@ -343,7 +408,7 @@ var OkxRestClient = class {
343
408
  /** 资金费率:GET /api/v5/public/funding-rate(仅 SWAP;10 次/2s)。 */
344
409
  async getFundingRate(instId) {
345
410
  const id = normalizeOkxSymbol(instId);
346
- if (!id.endsWith("-SWAP")) throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `OKX funding rate requires a perpetual swap instId (e.g. BTC-USDT-SWAP), got ${id}`);
411
+ if (instrumentFormOf(id) !== "perp") throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `OKX funding rate requires a perpetual swap instId (e.g. BTC-USDT-SWAP), got ${id}`);
347
412
  const d = (await this.request("/api/v5/public/funding-rate", { query: { instId: id } }))[0];
348
413
  const fundingRate = num(d?.fundingRate);
349
414
  const fundingTime = num(d?.fundingTime);
@@ -491,7 +556,7 @@ var OkxRestClient = class {
491
556
  }
492
557
  /** 未平仓合约量:GET /api/v5/public/open-interest(仅 SWAP;oi=张、oiCcy=币、oiUsd=USD)。 */ async getOpenInterest(instId) {
493
558
  const id = normalizeOkxSymbol(instId);
494
- if (!id.endsWith("-SWAP")) throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `OKX open interest requires a perpetual swap instId (e.g. BTC-USDT-SWAP), got ${id}`);
559
+ if (instrumentFormOf(id) !== "perp") throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `OKX open interest requires a perpetual swap instId (e.g. BTC-USDT-SWAP), got ${id}`);
495
560
  const d = (await this.request("/api/v5/public/open-interest", { query: {
496
561
  instType: "SWAP",
497
562
  instId: id
@@ -560,6 +625,8 @@ var OkxRestClient = class {
560
625
  const tickSz = num(d.tickSz);
561
626
  if (id === void 0 || lotSz === void 0 || minSz === void 0 || tickSz === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `OKX instruments: malformed row`);
562
627
  const ctVal = num(d.ctVal);
628
+ const maxLeverage = num(d.lever);
629
+ const instCategory = num(d.instCategory);
563
630
  return {
564
631
  instId: id,
565
632
  instType: str(d.instType) ?? instType,
@@ -570,23 +637,42 @@ var OkxRestClient = class {
570
637
  ...str(d.ctValCcy) !== void 0 ? { ctValCcy: str(d.ctValCcy) } : {},
571
638
  ...str(d.settleCcy) !== void 0 ? { settleCcy: str(d.settleCcy) } : {},
572
639
  ...str(d.baseCcy) !== void 0 ? { baseCcy: str(d.baseCcy) } : {},
573
- ...str(d.quoteCcy) !== void 0 ? { quoteCcy: str(d.quoteCcy) } : {}
640
+ ...str(d.quoteCcy) !== void 0 ? { quoteCcy: str(d.quoteCcy) } : {},
641
+ ...maxLeverage !== void 0 ? { maxLeverage } : {},
642
+ ...instCategory !== void 0 ? { instCategory } : {}
574
643
  };
575
644
  });
576
645
  }
577
646
  /**
578
- * 全部可交易现货标的名册(GET /api/v5/public/instruments?instType=SPOT,Issue #15)。
579
- * 输出 symbol 归一化为市场规范形(BTC-USDT → BTCUSDT),name 为 baseCcy/quoteCcy。
647
+ * 全部可交易标的名册 = 现货 ∪ 永续(GET /api/v5/public/instruments,Issue #15 扩展;
648
+ * 2026-10-08 形态维度落地)。
649
+ *
650
+ * - 现货:instType=SPOT → `BTCUSDT`(form=spot),name 取 baseCcy/quoteCcy。
651
+ * - 永续:instType=SWAP → `BTCUSDT-SWAP`(form=perp,与现货成对),name 取
652
+ * ctValCcy 与 instId 的计价段(SWAP 行的 baseCcy/quoteCcy 为空串,实测);
653
+ * assetClass 只由 instCategory 映射,contract 透传 ctVal/tickSz/lotSz/lever/settleCcy。
654
+ *
655
+ * 两半都拉取(Promise.all);任一半失败即抛结构化错误,不返回半份名册。
580
656
  */
581
657
  async listInstruments() {
582
- return (await this.getInstruments("SPOT")).map((inst) => {
583
- const canonical = toCanonicalOkxSymbol(inst.instId);
584
- const name = inst.baseCcy && inst.quoteCcy ? `${inst.baseCcy}/${inst.quoteCcy}` : void 0;
585
- return {
586
- symbol: canonical,
587
- ...name ? { name } : {}
588
- };
589
- });
658
+ const [spot, swap] = await Promise.all([this.getInstruments("SPOT"), this.getInstruments("SWAP")]);
659
+ return [...spot.map((inst) => this.#toInstrumentRef(inst, "spot")), ...swap.map((inst) => this.#toInstrumentRef(inst, "perp"))];
660
+ }
661
+ /** 名册条目 → InstrumentRef:形态由来源端点裁决,类别只信 instCategory(取不到留空)。 */
662
+ #toInstrumentRef(inst, form) {
663
+ const parts = inst.instId.split("-");
664
+ const base = inst.baseCcy ?? inst.ctValCcy ?? parts[0];
665
+ const quote = inst.quoteCcy ?? parts[1];
666
+ const name = base !== void 0 && base !== "" && quote !== void 0 && quote !== "" ? `${base}/${quote}` : void 0;
667
+ const assetClass = okxAssetClassOf(inst.instCategory);
668
+ const contract = form === "perp" ? okxContractOf(inst) : void 0;
669
+ return {
670
+ symbol: toCanonicalOkxSymbol(inst.instId),
671
+ ...name !== void 0 ? { name } : {},
672
+ form,
673
+ ...assetClass !== void 0 ? { assetClass } : {},
674
+ ...contract !== void 0 ? { contract } : {}
675
+ };
590
676
  }
591
677
  /** 只读余额:GET /api/v5/account/balance(ccy 可选,逗号分隔 ≤20)。 */
592
678
  async getBalance(auth, ccy) {
@@ -610,6 +696,17 @@ var OkxRestClient = class {
610
696
  auth
611
697
  });
612
698
  }
699
+ /**
700
+ * 设置杠杆/保证金模式:POST /api/v5/account/set-leverage(P7;实盘动作,与下单同门槛)。
701
+ * 只发请求不判定闸门——三态闸门由 OkxTradeService.setLeverage 在调用本方法前执行。
702
+ */
703
+ async setLeverage(params, auth) {
704
+ return this.request("/api/v5/account/set-leverage", {
705
+ method: "POST",
706
+ body: JSON.stringify(params),
707
+ auth
708
+ });
709
+ }
613
710
  /** 撤单:POST /api/v5/trade/cancel-order(ordId 优先于 clOrdId,本切片只支持 ordId)。 */
614
711
  async cancelOrder(instId, ordId, auth) {
615
712
  return this.request("/api/v5/trade/cancel-order", {
@@ -710,7 +807,7 @@ function toCanonicalOkxSymbol(symbol) {
710
807
  */
711
808
  function toOkxSwapInstId(input) {
712
809
  const id = normalizeOkxSymbol(input);
713
- return id.endsWith("-SWAP") ? id : `${id}-SWAP`;
810
+ return instrumentFormOf(id) === "perp" ? id : `${id}-SWAP`;
714
811
  }
715
812
  //#endregion
716
- export { BAR_MAP, OKX_INTERVAL_VOCABULARY, OkxRestClient, TradingServiceError, barDurationMs, buildAuthHeaders, isoTimestamp, normalizeOkxSymbol, normalizeSize, signPayload, signaturePrehash, toBar, toCanonicalOkxSymbol, toOkxSwapInstId };
813
+ export { BAR_MAP, OKX_INTERVAL_VOCABULARY, OkxRestClient, TradingServiceError, barDurationMs, buildAuthHeaders, coinsToContracts, contractsToCoins, isoTimestamp, normalizeOkxSymbol, normalizeSize, okxAssetClassOf, okxContractOf, signPayload, signaturePrehash, toBar, toCanonicalOkxSymbol, toOkxSwapInstId };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dshtrading/connector-okx",
3
3
  "description": "OKX market connector plugin for dsh-trading: public market data + signed demo/live trading (R1-R3), mutual-exclusion activation with connector-binance",
4
- "version": "0.5.0",
4
+ "version": "0.6.1",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -21,8 +21,9 @@
21
21
  ],
22
22
  "license": "PolyForm-Noncommercial-1.0.0",
23
23
  "dependencies": {
24
- "@dshtrading/api": "^0.5.0",
25
- "@dshtrading/indicators": "^0.5.0"
24
+ "@dshtrading/authority": "^0.6.1",
25
+ "@dshtrading/indicators": "^0.6.1",
26
+ "@dshtrading/api": "^0.6.1"
26
27
  },
27
28
  "peerDependencies": {
28
29
  "@deepseek-ai/cordis": ">=4.0.0",
@@ -32,7 +33,7 @@
32
33
  "devDependencies": {
33
34
  "tsdown": "^0.22.0",
34
35
  "vitest": "^3.0.0",
35
- "@dshtrading/connector-binance": "^0.5.0"
36
+ "@dshtrading/connector-binance": "^0.6.1"
36
37
  },
37
38
  "scripts": {
38
39
  "build": "tsdown",