@dshtrading/connector-binance 0.4.1 → 0.6.0

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 { BinanceRestClient, BinanceRestOptions, INTERVAL_VOCABULARY, TradingServiceError, normalizeBinanceFuturesSymbol } from "./rest.js";
1
+ import { BinanceRestClient, BinanceRestOptions, INTERVAL_VOCABULARY, TradingServiceError, binanceAssetClassOf, binanceContractOf, normalizeBinanceFuturesSymbol, resolveBinanceInstrument } from "./rest.js";
2
2
  import { Context, Service } from "@deepseek-ai/cordis";
3
3
  import Schema from "@deepseek-ai/schemastery";
4
- import { DerivativesData, DerivativesHistory, Disposable, Interval, Kline, MarketDataService, Orderbook, Ticker, TradeTick } from "@dshtrading/api";
4
+ import { DerivativesData, DerivativesHistory, Disposable, InstrumentRef, Interval, Kline, MarketDataService, Orderbook, Ticker, TradeTick } from "@dshtrading/api";
5
5
  //#region src/index.d.ts
6
6
  /**
7
7
  * Cordis 插件名 = patch 行 id(TEMPLATES §8):`dsh-trading-crypto-*` 市场命名空间,
@@ -13,7 +13,7 @@ interface Config {
13
13
  enabled: boolean;
14
14
  /** 交易安全闸门(铁律 #3):true 时下单类工具强制 dry-run。 */
15
15
  dryRun: boolean;
16
- /** 实盘总闸门:默认 false;false 时无论 dryRun 与否都拒绝实盘下单 [S4]。 */
16
+ /** 实盘镜像:默认 false。它是非权威镜像——只能收紧、永不能授予;实盘授权由 @dshtrading/authority 的人工签署平面裁定 [S4]。 */
17
17
  liveTrading: boolean;
18
18
  }
19
19
  declare const Config: Schema<Config>;
@@ -30,10 +30,7 @@ declare class BinanceMarketDataService extends Service implements MarketDataServ
30
30
  constructor(ctx: Context, options?: BinanceRestOptions, name?: string);
31
31
  getTicker(symbol: string): Promise<Ticker>;
32
32
  getKlines(symbol: string, interval: Interval, limit?: number): Promise<Kline[]>;
33
- listInstruments(): Promise<Array<{
34
- symbol: string;
35
- name?: string;
36
- }>>;
33
+ listInstruments(): Promise<InstrumentRef[]>;
37
34
  subscribeTicker(symbol: string, cb: (ticker: Ticker) => void, options?: SubscribeTickerOptions): Disposable;
38
35
  /**
39
36
  * 盘口快照(api 可选契约 getOrderbook,issue #39):现货 depth 20 档透传,
@@ -73,15 +70,15 @@ interface PlaceOrderArgs {
73
70
  }
74
71
  /**
75
72
  * 闸门判定结果(三条路径,顺序即铁律 #3 修订版的裁决顺序):
76
- * - `reject` —— ① 请求实盘(dryRun!==true)而 liveTrading=false:工具返回结构化
73
+ * - `reject` —— ① 请求实盘(dryRun!==true)而未获实盘授权:工具返回结构化
77
74
  * 拒绝(不抛异常,模型可读到明确原因与出路);
78
75
  * - `simulate` —— ② dryRun=true(显式、缺省,或被插件 config.dryRun 强制):
79
76
  * 返回 DRY-RUN 模拟成交回执;
80
- * - `live` —— ③ dryRun=false 且 liveTrading=true:本切片无签名下单能力,
77
+ * - `live` —— ③ dryRun=false 且实盘已获授权:本切片无签名下单能力,
81
78
  * 工具抛 TRADING_NOT_IMPLEMENTED 结构化错误。
82
79
  *
83
80
  * 优先级说明:config.dryRun=true 是「强制模拟」开关,只影响 ②/③ 的归类;
84
- * 显式 dryRun=false 的实盘意图在 liveTrading=false 时仍走 ① 明确拒绝,
81
+ * 显式 dryRun=false 的实盘意图在 未获实盘授权时仍走 ① 明确拒绝,
85
82
  * 不做静默降级(拒绝语义优先于强制模拟,调用方必须知道实盘意图被拒)。
86
83
  */
87
84
  type OrderGateVerdict = {
@@ -107,7 +104,7 @@ declare function buildDryRunReceipt(args: PlaceOrderArgs, marketData: Pick<Marke
107
104
  interface PlaceOrderToolDeps {
108
105
  /** 行情服务( dry-run 回执的市价参照),按接口取用,不直连 REST。 */
109
106
  readonly marketData: Pick<MarketDataService, 'getTicker'>;
110
- /** 插件配置(dryRun 强制模拟 / liveTrading 总闸门)。 */
107
+ /** 插件配置(dryRun 强制模拟 / liveTrading 非权威镜像——实盘授权由 @dshtrading/authority 的人工签署平面裁定)。 */
111
108
  readonly config: Config;
112
109
  }
113
110
  /**
@@ -132,4 +129,4 @@ interface MarketRouterLike {
132
129
  declare function routeAllows(ctx: Context, config: Config, market: string): boolean;
133
130
  declare function apply(ctx: Context, config: Config): void;
134
131
  //#endregion
135
- export { BinanceMarketDataService, BinanceRestClient, BinanceRestOptions, Config, DryRunReference, INTERVAL_VOCABULARY, MarketRouterLike, OrderGateVerdict, PlaceOrderArgs, PlaceOrderToolDeps, ROUTER_PROVIDER, SubscribeTickerOptions, TRADING_CRYPTO_MARKET_DATA_KEY, TradingServiceError, apply, buildDryRunReceipt, createPlaceOrderTool, evaluateOrderGate, inject, name, normalizeBinanceFuturesSymbol, routeAllows };
132
+ export { BinanceMarketDataService, BinanceRestClient, BinanceRestOptions, Config, DryRunReference, INTERVAL_VOCABULARY, MarketRouterLike, OrderGateVerdict, PlaceOrderArgs, PlaceOrderToolDeps, ROUTER_PROVIDER, SubscribeTickerOptions, TRADING_CRYPTO_MARKET_DATA_KEY, TradingServiceError, apply, binanceAssetClassOf, binanceContractOf, buildDryRunReceipt, createPlaceOrderTool, evaluateOrderGate, inject, name, normalizeBinanceFuturesSymbol, resolveBinanceInstrument, routeAllows };
package/lib/index.js CHANGED
@@ -1,10 +1,25 @@
1
- import { BinanceRestClient, INTERVAL_VOCABULARY, TradingServiceError, normalizeBinanceFuturesSymbol } from "./rest.js";
1
+ import { BinanceRestClient, INTERVAL_VOCABULARY, TradingServiceError, binanceAssetClassOf, binanceContractOf, normalizeBinanceFuturesSymbol, resolveBinanceInstrument } 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";
5
6
  import { createGetIndicatorsTool } from "@dshtrading/indicators/tool";
7
+ import { instrumentFormOf } from "@dshtrading/api";
6
8
  //#region src/index.ts
7
9
  /**
10
+ * Binance 连接器插件(dsh-trading crypto 切片):真实 MarketDataService。
11
+ *
12
+ * 能力三角色:
13
+ * - 声明:ctx 键 `tradingCryptoMarketData` 由 @dshtrading/api 的模块增强统一声明;
14
+ * - provide:BinanceMarketDataService(Service 基类,随插件 fiber 自动注销);
15
+ * - inject:工具经 ctx.inject 等待服务就绪后注册,一律经服务执行,不直连 REST。
16
+ *
17
+ * REST 数据面见 ./rest.ts(可独立单测/脚本消费);错误词汇映射 @dshtrading/api
18
+ * 的 TradingErrorCode(TradingServiceError.code)。
19
+ *
20
+ * @module @dshtrading/connector-binance
21
+ */
22
+ /**
8
23
  * 互斥激活的注册面(与 connector-okx 的 registerTool 同款,2026-08-31 补齐):
9
24
  * dsh-tools 对同名重复注册直接抛错,互斥纪律下冲突降级为「先到先得 + warn」。
10
25
  * 没有这层时,inject 回调就绪顺序不定会让同名工具(含 crypto_get_indicators)
@@ -135,18 +150,19 @@ var BinanceMarketDataService = class extends Service {
135
150
  };
136
151
  function evaluateOrderGate(config, args) {
137
152
  const requestedDryRun = args.dryRun ?? true;
138
- if (!requestedDryRun && !config.liveTrading) return {
153
+ if (!requestedDryRun && !liveTradingEnabled(config.liveTrading)) return {
139
154
  action: "reject",
140
155
  code: "TRADING_LIVE_TRADING_DISABLED",
141
- 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.`
156
+ 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.`
142
157
  };
143
158
  if (requestedDryRun || config.dryRun) return { action: "simulate" };
144
159
  return { action: "live" };
145
160
  }
146
161
  /** Binance 现货符号形如 BTCUSDT / ETHUSDT:大写字母数字(kit 同款词汇)。 */
147
162
  const SPOT_SYMBOL_PATTERN = /^[A-Z0-9]{4,20}$/;
148
- /** 参数校验(模型调用问题抛普通 Error,与 kit 先例一致;服务故障才用错误词汇)。 */
163
+ /** 参数校验(模型调用错误词汇:合约输入必须显式拒绝,不能落到现货端点)。 */
149
164
  function validatePlaceOrderArgs(args) {
165
+ if (instrumentFormOf(args.symbol) === "perp") throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `crypto_place_order: ${args.symbol} is a perpetual — Binance contract order placement is not implemented in this connector (market data only); use a spot symbol here or route crypto trades to the OKX connector. This path never falls back to the spot endpoint.`);
150
166
  if (!SPOT_SYMBOL_PATTERN.test(args.symbol)) throw new Error(`crypto_place_order: invalid symbol ${JSON.stringify(args.symbol)} — expected an uppercase Binance symbol like BTCUSDT`);
151
167
  if (args.side !== "BUY" && args.side !== "SELL") throw new Error(`crypto_place_order: invalid side ${JSON.stringify(args.side)} — expected BUY or SELL`);
152
168
  if (args.type !== "MARKET" && args.type !== "LIMIT") throw new Error(`crypto_place_order: invalid type ${JSON.stringify(args.type)} — expected MARKET or LIMIT`);
@@ -202,7 +218,7 @@ async function buildDryRunReceipt(args, marketData) {
202
218
  function createPlaceOrderTool(deps) {
203
219
  return defineTool({
204
220
  name: "crypto_place_order",
205
- description: "Place a Binance spot order, or simulate one. 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 to be enabled plus user approval, and is not implemented yet in this slice.",
221
+ description: "Place a Binance SPOT order, or simulate one. dryRun defaults to true and returns a DRY-RUN simulated fill receipt with the current market price as reference. Real execution (dryRun=false) requires a signed live-trading grant plus user approval, and is not implemented yet in this slice. Perpetual (-SWAP) symbols are rejected with TRADING_UNSUPPORTED_SYMBOL — Binance contract order placement is not implemented; only contract market data is available.",
206
222
  parameters: {
207
223
  symbol: {
208
224
  type: "string",
@@ -232,7 +248,7 @@ function createPlaceOrderTool(deps) {
232
248
  },
233
249
  dryRun: {
234
250
  type: "boolean",
235
- description: "true (default) = simulate only and return a DRY-RUN receipt; false = request real execution (gated by liveTrading and user approval)",
251
+ description: "true (default) = simulate only and return a DRY-RUN receipt; false = request real execution (gated by the signed live-trading authority and user approval)",
236
252
  default: true
237
253
  }
238
254
  },
@@ -356,4 +372,4 @@ function apply(ctx, config) {
356
372
  });
357
373
  }
358
374
  //#endregion
359
- export { BinanceMarketDataService, BinanceRestClient, Config, INTERVAL_VOCABULARY, ROUTER_PROVIDER, TRADING_CRYPTO_MARKET_DATA_KEY, TradingServiceError, apply, buildDryRunReceipt, createPlaceOrderTool, evaluateOrderGate, inject, name, normalizeBinanceFuturesSymbol, routeAllows };
375
+ export { BinanceMarketDataService, BinanceRestClient, Config, INTERVAL_VOCABULARY, ROUTER_PROVIDER, TRADING_CRYPTO_MARKET_DATA_KEY, TradingServiceError, apply, binanceAssetClassOf, binanceContractOf, buildDryRunReceipt, createPlaceOrderTool, evaluateOrderGate, inject, name, normalizeBinanceFuturesSymbol, resolveBinanceInstrument, routeAllows };
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, InstrumentForm, InstrumentRef, Interval, Kline, Orderbook, Ticker, TradeTick, TradingErrorCode } from "@dshtrading/api";
2
2
  //#region src/rest.d.ts
3
3
  /** api 包 TradingError 契约的运行时 Error 实现。 */
4
4
  declare class TradingServiceError extends Error {
@@ -22,25 +22,59 @@ declare const INTERVAL_VOCABULARY: readonly string[];
22
22
  * OKX 原生 SWAP 形一律归一到 fapi 词汇——BTCUSDT / BTCUSDT-SWAP / BTC-USDT-SWAP → BTCUSDT。
23
23
  */
24
24
  declare function normalizeBinanceFuturesSymbol(raw: string): string;
25
+ /**
26
+ * 输入符号 → 形态 + 交易所词汇(2026-10-08 加密永续落地)。
27
+ *
28
+ * Binance 现货与永续在交易所侧**同形**(都叫 `BTCUSDT`),形态只能由规范后缀
29
+ * `-SWAP` 裁决——单一实现是 @dshtrading/api 的 `instrumentFormOf`,这里不另写
30
+ * 一份 endsWith。永续剥掉后缀后复用既有衍生品归一(同时容忍 OKX 原生形
31
+ * `BTC-USDT-SWAP`);现货保持原样交给交易所判存在性(未知符号仍报 -1121)。
32
+ */
33
+ declare function resolveBinanceInstrument(symbol: string): {
34
+ form: InstrumentForm;
35
+ venueSymbol: string;
36
+ };
37
+ /** underlyingType(含 underlyingSubType 逐项兜底)→ 资产类别;未登记/缺失 = undefined。 */
38
+ declare function binanceAssetClassOf(underlyingType: unknown, underlyingSubType: unknown): InstrumentAssetClass | undefined;
39
+ /** USDT-M 合约规格 → InstrumentContract(交易所元数据原样透传;字段缺失即缺省)。 */
40
+ declare function binanceContractOf(row: {
41
+ readonly contractSize?: unknown;
42
+ readonly marginAsset?: unknown;
43
+ readonly filters?: unknown;
44
+ }): InstrumentContract | undefined;
25
45
  declare class BinanceRestClient {
26
46
  #private;
27
47
  constructor(options?: BinanceRestOptions);
28
48
  /**
29
- * 最新行情:/api/v3/ticker/24hr 提供最新价与 24h 量,/api/v3/ticker/bookTicker 补充 bid/ask。
49
+ * 最新行情(2026-10-08 形态分流):现货 /api/v3/ticker/24hr + bookTicker;
50
+ * 永续(`-SWAP` 后缀)走 USDT-M /fapi/v1/ticker/24hr + bookTicker。
51
+ * 两条路径各自报结构化错误——绝不用现货报价冒充合约价(降级纪律)。
30
52
  */
31
53
  getTicker(symbol: string): Promise<Ticker>;
54
+ /** K 线(2026-10-08 形态分流):现货 /api/v3/klines;永续 /fapi/v1/klines,行结构同形。 */
32
55
  getKlines(symbol: string, interval: Interval, limit?: number): Promise<Kline[]>;
33
56
  /**
34
- * 全部可交易现货标的名册(GET /api/v3/exchangeInfo,status=TRADING 过滤,Issue #15)。
35
- * 输出 symbol 为规范形(BTCUSDT),name 为 baseAsset/quoteAsset(如 BTC/USDT)。
57
+ * 全部可交易标的名册 = 现货 ∪ USDT-M 永续(2026-10-08 形态维度落地,Issue #15 扩展)。
58
+ *
59
+ * - 现货:GET /api/v3/exchangeInfo,`status=TRADING` → `BTCUSDT`(form=spot,
60
+ * assetClass=crypto:Binance 现货名册无 TradFi 条目)。
61
+ * - 永续:GET /fapi/v1/exchangeInfo,`contractType` ∈ {PERPETUAL, TRADIFI_PERPETUAL}
62
+ * 且 `status=TRADING` → `BTCUSDT-SWAP`(form=perp,与现货成对);assetClass 取
63
+ * underlyingType/underlyingSubType(取不到留空),contract 取 contractSize/
64
+ * filters/marginAsset。TradFi 永续的字面量是 TRADIFI_PERPETUAL(实测 217 行),
65
+ * 只认 PERPETUAL 会把股票/大宗/外汇合约整批漏掉。
66
+ *
67
+ * 两半都是名册契约的一部分:任一半失败即抛结构化错误,**不返回半份名册**
68
+ * (静默半份会让检索排序与可用性判断失真,调用方按错误决定自己的兜底目录)。
69
+ */
70
+ listInstruments(): Promise<InstrumentRef[]>;
71
+ /**
72
+ * 盘口快照:GET /api/v3/depth(现货)或 /fapi/v1/depth(永续),limit=20 档;
73
+ * bids 降序 / asks 升序,Binance 原生序。永续不再回落现货 depth(合约盘口
74
+ * 落到现货端点等于把现货深度伪装成合约深度)。
36
75
  */
37
- listInstruments(): Promise<Array<{
38
- symbol: string;
39
- name?: string;
40
- }>>;
41
- /** 盘口快照:GET /api/v3/depth(limit=20 档;bids 降序 / asks 升序,Binance 原生序)。 */
42
76
  getOrderbook(symbol: string): Promise<Orderbook>;
43
- /** 最近逐笔成交:GET /api/v3/trades(时间升序;isBuyerMaker=true → 主动卖)。 */
77
+ /** 最近逐笔成交:GET /api/v3/trades(现货)或 /fapi/v1/trades(永续),时间升序。 */
44
78
  getRecentTrades(symbol: string, limit?: number): Promise<TradeTick[]>;
45
79
  /** 未平仓合约量:GET /fapi/v1/openInterest(openInterest 以 base 币计,time=快照 ms)。 */
46
80
  getFuturesOpenInterest(symbol: string): Promise<{
@@ -77,4 +111,4 @@ declare class BinanceRestClient {
77
111
  getFuturesOpenInterestHistory(symbol: string, limit?: number): Promise<DerivativesPoint[]>;
78
112
  }
79
113
  //#endregion
80
- export { BinanceRestClient, BinanceRestOptions, INTERVAL_VOCABULARY, TradingServiceError, normalizeBinanceFuturesSymbol };
114
+ export { BinanceRestClient, BinanceRestOptions, INTERVAL_VOCABULARY, TradingServiceError, binanceAssetClassOf, binanceContractOf, normalizeBinanceFuturesSymbol, resolveBinanceInstrument };
package/lib/rest.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { SWAP_SYMBOL_SUFFIX, instrumentFormOf } from "@dshtrading/api";
1
2
  //#region src/rest.ts
2
3
  /** api 包 TradingError 契约的运行时 Error 实现。 */
3
4
  var TradingServiceError = class extends Error {
@@ -48,6 +49,28 @@ function normalizeBinanceFuturesSymbol(raw) {
48
49
  const clean = requireSymbol(raw).replace(/[-_]/g, "");
49
50
  return clean.endsWith("SWAP") ? clean.slice(0, -4) : clean;
50
51
  }
52
+ /**
53
+ * 输入符号 → 形态 + 交易所词汇(2026-10-08 加密永续落地)。
54
+ *
55
+ * Binance 现货与永续在交易所侧**同形**(都叫 `BTCUSDT`),形态只能由规范后缀
56
+ * `-SWAP` 裁决——单一实现是 @dshtrading/api 的 `instrumentFormOf`,这里不另写
57
+ * 一份 endsWith。永续剥掉后缀后复用既有衍生品归一(同时容忍 OKX 原生形
58
+ * `BTC-USDT-SWAP`);现货保持原样交给交易所判存在性(未知符号仍报 -1121)。
59
+ */
60
+ function resolveBinanceInstrument(symbol) {
61
+ const upper = requireSymbol(symbol);
62
+ const form = instrumentFormOf(upper);
63
+ if (form === "spot") return {
64
+ form,
65
+ venueSymbol: upper
66
+ };
67
+ const venueSymbol = normalizeBinanceFuturesSymbol(upper);
68
+ if (venueSymbol === "") throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `Binance: ${JSON.stringify(symbol)} carries the ${SWAP_SYMBOL_SUFFIX} suffix but has no base symbol`);
69
+ return {
70
+ form,
71
+ venueSymbol
72
+ };
73
+ }
51
74
  /** Binance 返回数值均为字符串,宽松转 number(非有限值返回 undefined)。 */
52
75
  function num(value) {
53
76
  if (value === "") return void 0;
@@ -136,6 +159,63 @@ function parseTradeRow(row, symbol) {
136
159
  timestamp: num(d.time) ?? Date.now()
137
160
  };
138
161
  }
162
+ /**
163
+ * Binance 合约 underlying 元数据 → 资产类别标签。
164
+ *
165
+ * 只登记 Binance 公开字段的**实测取值**;未登记一律 undefined(留空),禁止按符号猜
166
+ * (硬不变量 #2:TradFi 归属只信交易所元数据)。
167
+ *
168
+ * 2026-10-08 可达环境实测取值(924 行 USDT-M exchangeInfo,见
169
+ * spikes/impl-crypto-perp-tradfi/EVIDENCE-reverify-2026-10-08.md):
170
+ * COIN 704 / EQUITY 179 / HK_EQUITY 15 / COMMODITY 8 / KR_EQUITY 8 / PREMARKET 4 /
171
+ * INDEX 3 / CN_EQUITY 2 / FX 1。
172
+ * - PREMARKET 是 Pre-IPO 股票合约(OPENAI/ANTHROPIC/MOONSHOT/OURA,underlyingSubType
173
+ * ["Pre-IPO","TradFi"])⇒ 归 equity;枚举里没有 premarket 成员,投影由字面量裁决而非符号。
174
+ * - FX(USDBRLUSDT)**故意不登记**:枚举没有外汇成员,硬塞进 equity/commodity 就是错标,
175
+ * 按「未登记即留空」兜底(漏标而非错标)。要不要加 FX 成员属契约层决定,不在本连接器内定。
176
+ */
177
+ const BINANCE_UNDERLYING_ASSET_CLASS = {
178
+ COIN: "crypto",
179
+ CRYPTO: "crypto",
180
+ STOCK: "equity",
181
+ EQUITY: "equity",
182
+ HK_EQUITY: "equity",
183
+ KR_EQUITY: "equity",
184
+ CN_EQUITY: "equity",
185
+ PREMARKET: "equity",
186
+ INDEX: "index",
187
+ COMMODITY: "commodity"
188
+ };
189
+ /**
190
+ * USDT-M 名册里的永续字面量:加密永续 `PERPETUAL`、TradFi 永续 `TRADIFI_PERPETUAL`
191
+ * (2026-10-08 实测 703 / 217)。只按 `PERPETUAL` 过滤会把全部 TradFi 永续整批漏掉。
192
+ */
193
+ const BINANCE_PERPETUAL_CONTRACT_TYPES = /* @__PURE__ */ new Set(["PERPETUAL", "TRADIFI_PERPETUAL"]);
194
+ /** underlyingType(含 underlyingSubType 逐项兜底)→ 资产类别;未登记/缺失 = undefined。 */
195
+ function binanceAssetClassOf(underlyingType, underlyingSubType) {
196
+ const candidates = [underlyingType];
197
+ if (Array.isArray(underlyingSubType)) candidates.push(...underlyingSubType);
198
+ for (const candidate of candidates) {
199
+ if (typeof candidate !== "string") continue;
200
+ const mapped = BINANCE_UNDERLYING_ASSET_CLASS[candidate.trim().toUpperCase()];
201
+ if (mapped !== void 0) return mapped;
202
+ }
203
+ }
204
+ /** USDT-M 合约规格 → InstrumentContract(交易所元数据原样透传;字段缺失即缺省)。 */
205
+ function binanceContractOf(row) {
206
+ const filters = Array.isArray(row.filters) ? row.filters : [];
207
+ const multiplier = num(row.contractSize);
208
+ const tickSize = num(filters.find((f) => f.filterType === "PRICE_FILTER")?.tickSize);
209
+ const lotSize = num(filters.find((f) => f.filterType === "LOT_SIZE")?.stepSize);
210
+ const settleCcy = typeof row.marginAsset === "string" && row.marginAsset !== "" ? row.marginAsset : void 0;
211
+ if (multiplier === void 0 && tickSize === void 0 && lotSize === void 0 && settleCcy === void 0) return;
212
+ return {
213
+ ...multiplier !== void 0 ? { multiplier } : {},
214
+ ...tickSize !== void 0 ? { tickSize } : {},
215
+ ...lotSize !== void 0 ? { lotSize } : {},
216
+ ...settleCcy !== void 0 ? { settleCcy } : {}
217
+ };
218
+ }
139
219
  var BinanceRestClient = class {
140
220
  #baseUrl;
141
221
  #fapiBaseUrl;
@@ -169,22 +249,28 @@ var BinanceRestClient = class {
169
249
  }
170
250
  }
171
251
  /**
172
- * 最新行情:/api/v3/ticker/24hr 提供最新价与 24h 量,/api/v3/ticker/bookTicker 补充 bid/ask。
252
+ * 最新行情(2026-10-08 形态分流):现货 /api/v3/ticker/24hr + bookTicker;
253
+ * 永续(`-SWAP` 后缀)走 USDT-M /fapi/v1/ticker/24hr + bookTicker。
254
+ * 两条路径各自报结构化错误——绝不用现货报价冒充合约价(降级纪律)。
173
255
  */
174
256
  async getTicker(symbol) {
175
- const sym = requireSymbol(symbol);
176
- const [day, book] = await Promise.all([this.#request("/api/v3/ticker/24hr", { symbol: sym }), this.#request("/api/v3/ticker/bookTicker", { symbol: sym })]);
257
+ const { form, venueSymbol } = resolveBinanceInstrument(symbol);
258
+ const perp = form === "perp";
259
+ const prefix = perp ? "/fapi/v1/ticker" : "/api/v3/ticker";
260
+ const base = perp ? this.#fapiBaseUrl : this.#baseUrl;
261
+ const [day, book] = await Promise.all([this.#request(`${prefix}/24hr`, { symbol: venueSymbol }, base), this.#request(`${prefix}/bookTicker`, { symbol: venueSymbol }, base)]);
177
262
  const d = day;
178
263
  const b = book;
179
264
  const price = num(d.lastPrice);
180
- if (price === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance ticker for ${sym}: missing/invalid lastPrice`);
265
+ if (price === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance ticker for ${venueSymbol}: missing/invalid lastPrice`);
181
266
  const bid = num(b.bidPrice);
182
267
  const ask = num(b.askPrice);
183
268
  const volume = num(d.volume);
184
269
  const prevClose = num(d.prevClosePrice);
185
270
  const changePercent = num(d.priceChangePercent);
271
+ const resolvedSymbol = typeof d.symbol === "string" && d.symbol ? d.symbol : venueSymbol;
186
272
  return {
187
- symbol: typeof d.symbol === "string" && d.symbol ? d.symbol : sym,
273
+ symbol: perp ? `${resolvedSymbol}${SWAP_SYMBOL_SUFFIX}` : resolvedSymbol,
188
274
  price,
189
275
  timestamp: Date.now(),
190
276
  ...bid !== void 0 ? { bid } : {},
@@ -194,53 +280,93 @@ var BinanceRestClient = class {
194
280
  ...changePercent !== void 0 ? { changePercent } : {}
195
281
  };
196
282
  }
283
+ /** K 线(2026-10-08 形态分流):现货 /api/v3/klines;永续 /fapi/v1/klines,行结构同形。 */
197
284
  async getKlines(symbol, interval, limit = 100) {
198
- const sym = requireSymbol(symbol);
285
+ const { form, venueSymbol } = resolveBinanceInstrument(symbol);
286
+ const perp = form === "perp";
199
287
  if (!isInterval(interval)) throw new TradingServiceError("TRADING_UNSUPPORTED_INTERVAL", `Binance klines: unsupported interval ${String(interval)}`);
200
288
  if (!Number.isInteger(limit) || limit < 1 || limit > 1e3) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance klines: limit must be an integer within 1..1000, got ${limit}`);
201
- const body = await this.#request("/api/v3/klines", {
202
- symbol: sym,
289
+ const body = await this.#request(perp ? "/fapi/v1/klines" : "/api/v3/klines", {
290
+ symbol: venueSymbol,
203
291
  interval,
204
292
  limit: String(limit)
205
- });
206
- if (!Array.isArray(body)) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance klines for ${sym}: unexpected response shape`);
207
- return body.map((row) => parseKlineRow(row, sym));
293
+ }, perp ? this.#fapiBaseUrl : this.#baseUrl);
294
+ if (!Array.isArray(body)) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance klines for ${venueSymbol}: unexpected response shape`);
295
+ return body.map((row) => parseKlineRow(row, venueSymbol));
208
296
  }
209
297
  /**
210
- * 全部可交易现货标的名册(GET /api/v3/exchangeInfo,status=TRADING 过滤,Issue #15)。
211
- * 输出 symbol 为规范形(BTCUSDT),name 为 baseAsset/quoteAsset(如 BTC/USDT)。
298
+ * 全部可交易标的名册 = 现货 ∪ USDT-M 永续(2026-10-08 形态维度落地,Issue #15 扩展)。
299
+ *
300
+ * - 现货:GET /api/v3/exchangeInfo,`status=TRADING` → `BTCUSDT`(form=spot,
301
+ * assetClass=crypto:Binance 现货名册无 TradFi 条目)。
302
+ * - 永续:GET /fapi/v1/exchangeInfo,`contractType` ∈ {PERPETUAL, TRADIFI_PERPETUAL}
303
+ * 且 `status=TRADING` → `BTCUSDT-SWAP`(form=perp,与现货成对);assetClass 取
304
+ * underlyingType/underlyingSubType(取不到留空),contract 取 contractSize/
305
+ * filters/marginAsset。TradFi 永续的字面量是 TRADIFI_PERPETUAL(实测 217 行),
306
+ * 只认 PERPETUAL 会把股票/大宗/外汇合约整批漏掉。
307
+ *
308
+ * 两半都是名册契约的一部分:任一半失败即抛结构化错误,**不返回半份名册**
309
+ * (静默半份会让检索排序与可用性判断失真,调用方按错误决定自己的兜底目录)。
212
310
  */
213
311
  async listInstruments() {
214
- const info = await this.#request("/api/v3/exchangeInfo", {});
215
- if (!Array.isArray(info?.symbols)) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", "Binance exchangeInfo: invalid response shape");
312
+ const [spotBody, futuresBody] = await Promise.all([this.#request("/api/v3/exchangeInfo", {}), this.#request("/fapi/v1/exchangeInfo", {}, this.#fapiBaseUrl)]);
313
+ const spotSymbols = spotBody.symbols;
314
+ if (!Array.isArray(spotSymbols)) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", "Binance exchangeInfo: invalid response shape");
315
+ const futuresSymbols = futuresBody.symbols;
316
+ if (!Array.isArray(futuresSymbols)) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", "Binance futures exchangeInfo: invalid response shape");
216
317
  const result = [];
217
- for (const item of info.symbols) if (item && item.status === "TRADING" && typeof item.symbol === "string" && item.symbol) {
218
- const name = item.baseAsset && item.quoteAsset ? `${item.baseAsset}/${item.quoteAsset}` : void 0;
318
+ for (const raw of spotSymbols) {
319
+ const item = raw;
320
+ if (item?.status !== "TRADING" || typeof item.symbol !== "string" || item.symbol === "") continue;
321
+ const name = typeof item.baseAsset === "string" && item.baseAsset !== "" && typeof item.quoteAsset === "string" && item.quoteAsset !== "" ? `${item.baseAsset}/${item.quoteAsset}` : void 0;
219
322
  result.push({
220
323
  symbol: item.symbol,
221
- ...name ? { name } : {}
324
+ ...name !== void 0 ? { name } : {},
325
+ form: "spot",
326
+ assetClass: "crypto"
327
+ });
328
+ }
329
+ for (const raw of futuresSymbols) {
330
+ const item = raw;
331
+ if (typeof item?.contractType !== "string" || !BINANCE_PERPETUAL_CONTRACT_TYPES.has(item.contractType) || item.status !== "TRADING" || typeof item.symbol !== "string" || item.symbol === "") continue;
332
+ const name = typeof item.baseAsset === "string" && item.baseAsset !== "" && typeof item.quoteAsset === "string" && item.quoteAsset !== "" ? `${item.baseAsset}/${item.quoteAsset}` : void 0;
333
+ const assetClass = binanceAssetClassOf(item.underlyingType, item.underlyingSubType);
334
+ const contract = binanceContractOf(item);
335
+ result.push({
336
+ symbol: `${item.symbol}${SWAP_SYMBOL_SUFFIX}`,
337
+ ...name !== void 0 ? { name } : {},
338
+ form: "perp",
339
+ ...assetClass !== void 0 ? { assetClass } : {},
340
+ ...contract !== void 0 ? { contract } : {}
222
341
  });
223
342
  }
224
343
  return result;
225
344
  }
226
- /** 盘口快照:GET /api/v3/depth(limit=20 档;bids 降序 / asks 升序,Binance 原生序)。 */
345
+ /**
346
+ * 盘口快照:GET /api/v3/depth(现货)或 /fapi/v1/depth(永续),limit=20 档;
347
+ * bids 降序 / asks 升序,Binance 原生序。永续不再回落现货 depth(合约盘口
348
+ * 落到现货端点等于把现货深度伪装成合约深度)。
349
+ */
227
350
  async getOrderbook(symbol) {
228
- const sym = normalizeBinanceFuturesSymbol(symbol);
229
- return parseDepthBody(await this.#request("/api/v3/depth", {
230
- symbol: sym,
351
+ const { form, venueSymbol } = resolveBinanceInstrument(symbol);
352
+ const perp = form === "perp";
353
+ return parseDepthBody(await this.#request(perp ? "/fapi/v1/depth" : "/api/v3/depth", {
354
+ symbol: venueSymbol,
231
355
  limit: "20"
232
- }), sym);
356
+ }, perp ? this.#fapiBaseUrl : this.#baseUrl), perp ? `${venueSymbol}${SWAP_SYMBOL_SUFFIX}` : venueSymbol);
233
357
  }
234
- /** 最近逐笔成交:GET /api/v3/trades(时间升序;isBuyerMaker=true → 主动卖)。 */
358
+ /** 最近逐笔成交:GET /api/v3/trades(现货)或 /fapi/v1/trades(永续),时间升序。 */
235
359
  async getRecentTrades(symbol, limit = 50) {
236
- const sym = normalizeBinanceFuturesSymbol(symbol);
360
+ const { form, venueSymbol } = resolveBinanceInstrument(symbol);
361
+ const perp = form === "perp";
362
+ const outputSymbol = perp ? `${venueSymbol}${SWAP_SYMBOL_SUFFIX}` : venueSymbol;
237
363
  const capped = Math.max(1, Math.min(Math.floor(limit) || 50, 100));
238
- const body = await this.#request("/api/v3/trades", {
239
- symbol: sym,
364
+ const body = await this.#request(perp ? "/fapi/v1/trades" : "/api/v3/trades", {
365
+ symbol: venueSymbol,
240
366
  limit: String(capped)
241
- });
242
- if (!Array.isArray(body)) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance trades for ${sym}: unexpected response shape`);
243
- return body.map((row) => parseTradeRow(row, sym));
367
+ }, perp ? this.#fapiBaseUrl : this.#baseUrl);
368
+ if (!Array.isArray(body)) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance trades for ${outputSymbol}: unexpected response shape`);
369
+ return body.map((row) => parseTradeRow(row, outputSymbol));
244
370
  }
245
371
  /** 未平仓合约量:GET /fapi/v1/openInterest(openInterest 以 base 币计,time=快照 ms)。 */
246
372
  async getFuturesOpenInterest(symbol) {
@@ -363,4 +489,4 @@ var BinanceRestClient = class {
363
489
  }
364
490
  };
365
491
  //#endregion
366
- export { BinanceRestClient, INTERVAL_VOCABULARY, TradingServiceError, normalizeBinanceFuturesSymbol };
492
+ export { BinanceRestClient, INTERVAL_VOCABULARY, TradingServiceError, binanceAssetClassOf, binanceContractOf, normalizeBinanceFuturesSymbol, resolveBinanceInstrument };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dshtrading/connector-binance",
3
3
  "description": "Binance market connector plugin for dsh-trading: public market data over REST+WS implementing the @dshtrading/api contracts",
4
- "version": "0.4.1",
4
+ "version": "0.6.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -21,12 +21,13 @@
21
21
  ],
22
22
  "license": "PolyForm-Noncommercial-1.0.0",
23
23
  "dependencies": {
24
- "@dshtrading/api": "^0.4.1",
25
- "@dshtrading/indicators": "^0.4.1"
24
+ "@dshtrading/authority": "^0.6.0",
25
+ "@dshtrading/indicators": "^0.6.0",
26
+ "@dshtrading/api": "^0.6.0"
26
27
  },
27
28
  "peerDependencies": {
28
29
  "@deepseek-ai/cordis": ">=4.0.0",
29
- "@deepseek-ai/dsh-tools": ">=0.1.5-rc.1",
30
+ "@deepseek-ai/dsh-tools": ">=0.2.0-rc.2",
30
31
  "@deepseek-ai/schemastery": ">=3.18.0"
31
32
  },
32
33
  "devDependencies": {