@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 +67 -14
- package/lib/index.js +232 -22
- package/lib/rest.d.ts +54 -10
- package/lib/rest.js +123 -26
- package/package.json +5 -4
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
|
-
*
|
|
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<
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
224
|
+
* - `reject` —— ① 请求实盘(dryRun!==true)而未获实盘授权:结构化拒绝;
|
|
199
225
|
* - `simulate` —— ② dryRun=true(显式/缺省/被 config.dryRun 强制):本地模拟回执;
|
|
200
|
-
* - `live` —— ③ dryRun=false
|
|
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
|
|
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
|
-
*
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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" ?
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
200
|
-
*
|
|
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<
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
-
*
|
|
579
|
-
*
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
|
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.
|
|
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/
|
|
25
|
-
"@dshtrading/indicators": "^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.
|
|
36
|
+
"@dshtrading/connector-binance": "^0.6.1"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
39
|
"build": "tsdown",
|