@dshtrading/connector-bybit 0.5.0 → 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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  node scripts/new-connector.mjs --slug bybit --title Bybit --market crypto
8
8
  ```
9
9
 
10
- 生成后按 [docs/connector-playbook.md](../../docs/connector-playbook.md) 的填写顺序
10
+ 生成后按 [docs/guides/connector-playbook.md](../../docs/guides/connector-playbook.md) 的填写顺序
11
11
  (src/rest.ts 头部清单 7 项)实现 TODO。每个 TODO 的参照段都在 connector-okx
12
12
  (packages/connector-okx/),它是本仓第一个真实 TradeService 连接器。
13
13
 
package/lib/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { AccountBalance, BybitRestClient, BybitRestOptions, INTERVAL_VOCABULARY, Interval, Kline, Order, Position, Ticker, TradingServiceError, normalizeCryptoSymbol, parseIntervalMs, toBybitInterval } from "./rest.js";
1
+ import { AccountBalance, BybitRestClient, BybitRestOptions, INTERVAL_VOCABULARY, Interval, Kline, Order, ParsedCryptoSymbol, Position, Ticker, TradingServiceError, bybitAssetClassOf, bybitCategory, normalizeCryptoSymbol, parseCryptoSymbol, parseIntervalMs, toBybitInterval } from "./rest.js";
2
2
  import { Context, Service } from "@deepseek-ai/cordis";
3
3
  import Schema from "@deepseek-ai/schemastery";
4
- import { AccountBalance as AccountBalance$1, DerivativesData, DerivativesHistory, Disposable, Interval as Interval$1, Kline as Kline$1, MarketDataService, Order as Order$1, OrderRequest, Orderbook, Position as Position$1, Ticker as Ticker$1, TradeFill, TradeService, TradeTick } from "@dshtrading/api";
4
+ import { AccountBalance as AccountBalance$1, DerivativesData, DerivativesHistory, Disposable, InstrumentRef, Interval as Interval$1, Kline as Kline$1, MarketDataService, Order as Order$1, OrderRequest, Orderbook, Position as Position$1, Ticker as Ticker$1, TradeFill, TradeService, TradeTick } from "@dshtrading/api";
5
5
  //#region src/index.d.ts
6
6
  declare const name = "dsh-trading-crypto-connector-bybit";
7
7
  interface Config {
@@ -20,6 +20,13 @@ declare class BybitMarketDataService extends Service implements MarketDataServic
20
20
  constructor(ctx: Context, options?: BybitRestOptions, serviceName?: string);
21
21
  getTicker(symbol: string): Promise<Ticker$1>;
22
22
  getKlines(symbol: string, interval?: Interval$1, limit?: number): Promise<Kline$1[]>;
23
+ /**
24
+ * 标的名册(P3):现货 ∪ 线性永续;永续输出规范形 `BTCUSDT-SWAP`(form=perp)。
25
+ * assetClass 只由交易所 `symbolType` 字面量裁决(stock/ETF/xstocks→equity、
26
+ * commodity→commodity、innovation/adventure→crypto);交易所没给(空串)或未登记
27
+ * (forex/mstocks)即留空,不本地推断(见 rest.ts bybitAssetClassOf)。
28
+ */
29
+ listInstruments(): Promise<InstrumentRef[]>;
23
30
  subscribeTicker(symbol: string, cb: (ticker: Ticker$1) => void, options?: {
24
31
  intervalMs?: number;
25
32
  }): Disposable;
@@ -43,7 +50,7 @@ declare class BybitMarketDataService extends Service implements MarketDataServic
43
50
  }
44
51
  declare class BybitTradeService extends Service implements TradeService {
45
52
  private readonly client;
46
- /** 插件配置(服务缝闸门 P0:dryRun 强制模拟 / liveTrading 总闸门)。 */
53
+ /** 插件配置(服务缝闸门 P0:dryRun 强制模拟 / liveTrading 非权威镜像——实盘授权由 @dshtrading/authority 的人工签署平面裁定)。 */
47
54
  private readonly config;
48
55
  constructor(ctx: Context, options: BybitRestOptions & {
49
56
  config: Config;
@@ -61,4 +68,4 @@ declare const ROUTER_PROVIDER = "bybit";
61
68
  declare function routeAllows(ctx: Context, config: Config, market: string): boolean;
62
69
  declare function apply(ctx: Context, config: Config): void;
63
70
  //#endregion
64
- export { type AccountBalance, BybitMarketDataService, BybitRestClient, BybitRestOptions, BybitTradeService, Config, INTERVAL_VOCABULARY, type Interval, type Kline, type Order, type Position, ROUTER_PROVIDER, TRADING_CRYPTO_MARKET_DATA_KEY, TRADING_CRYPTO_TRADE_KEY, type Ticker, TradingServiceError, apply, name, normalizeCryptoSymbol, parseIntervalMs, routeAllows, toBybitInterval };
71
+ export { type AccountBalance, BybitMarketDataService, BybitRestClient, BybitRestOptions, BybitTradeService, Config, INTERVAL_VOCABULARY, type Interval, type Kline, type Order, ParsedCryptoSymbol, type Position, ROUTER_PROVIDER, TRADING_CRYPTO_MARKET_DATA_KEY, TRADING_CRYPTO_TRADE_KEY, type Ticker, TradingServiceError, apply, bybitAssetClassOf, bybitCategory, name, normalizeCryptoSymbol, parseCryptoSymbol, parseIntervalMs, routeAllows, toBybitInterval };
package/lib/index.js CHANGED
@@ -1,8 +1,13 @@
1
- import { BybitRestClient, INTERVAL_VOCABULARY, TradingServiceError, normalizeCryptoSymbol, parseIntervalMs, toBybitInterval } from "./rest.js";
1
+ import { BybitRestClient, INTERVAL_VOCABULARY, TradingServiceError, bybitAssetClassOf, bybitCategory, normalizeCryptoSymbol, parseCryptoSymbol, parseIntervalMs, toBybitInterval } 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
  //#region src/index.ts
7
+ /**
8
+ * @dshtrading/connector-bybit
9
+ * Bybit 加密货币连接器插件(提供 tradingCryptoMarketData 与 crypto_* 工具)。
10
+ */
6
11
  const name = "dsh-trading-crypto-connector-bybit";
7
12
  const Config = Schema.object({
8
13
  enabled: Schema.boolean().default(true).description("是否激活 Bybit 连接器"),
@@ -26,6 +31,15 @@ var BybitMarketDataService = class extends Service {
26
31
  async getKlines(symbol, interval = "1d", limit = 100) {
27
32
  return this.client.getKlines(symbol, interval, limit);
28
33
  }
34
+ /**
35
+ * 标的名册(P3):现货 ∪ 线性永续;永续输出规范形 `BTCUSDT-SWAP`(form=perp)。
36
+ * assetClass 只由交易所 `symbolType` 字面量裁决(stock/ETF/xstocks→equity、
37
+ * commodity→commodity、innovation/adventure→crypto);交易所没给(空串)或未登记
38
+ * (forex/mstocks)即留空,不本地推断(见 rest.ts bybitAssetClassOf)。
39
+ */
40
+ async listInstruments() {
41
+ return this.client.listInstruments();
42
+ }
29
43
  subscribeTicker(symbol, cb, options) {
30
44
  const ms = Math.max(options?.intervalMs ?? 5e3, 1e3);
31
45
  const tick = () => {
@@ -97,7 +111,7 @@ var BybitMarketDataService = class extends Service {
97
111
  };
98
112
  var BybitTradeService = class extends Service {
99
113
  client;
100
- /** 插件配置(服务缝闸门 P0:dryRun 强制模拟 / liveTrading 总闸门)。 */
114
+ /** 插件配置(服务缝闸门 P0:dryRun 强制模拟 / liveTrading 非权威镜像——实盘授权由 @dshtrading/authority 的人工签署平面裁定)。 */
101
115
  config;
102
116
  constructor(ctx, options, serviceName = TRADING_CRYPTO_TRADE_KEY) {
103
117
  super(ctx, serviceName);
@@ -116,7 +130,7 @@ var BybitTradeService = class extends Service {
116
130
  }
117
131
  async placeOrder(order) {
118
132
  const requestedDryRun = order.dryRun ?? true;
119
- if (!requestedDryRun && !this.config.liveTrading) throw new TradingServiceError("TRADING_LIVE_TRADING_DISABLED", `Bybit TradeService.placeOrder rejected: the request asks for real execution (dryRun=${String(order.dryRun)}) but liveTrading=false — enable liveTrading explicitly or keep dryRun=true for a simulated fill.`);
133
+ if (!requestedDryRun && !liveTradingEnabled(this.config.liveTrading)) throw new TradingServiceError("TRADING_LIVE_TRADING_DISABLED", `Bybit TradeService.placeOrder rejected: the request asks for real execution (dryRun=${String(order.dryRun)}) but the signed live-trading authority does not grant real execution (see @dshtrading/authority) — keep dryRun=true for a simulated fill.`);
120
134
  if (requestedDryRun || this.config.dryRun) return {
121
135
  id: `dry-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
122
136
  symbol: order.symbol,
@@ -131,7 +145,7 @@ var BybitTradeService = class extends Service {
131
145
  return this.client.placeOrder(void 0, order);
132
146
  }
133
147
  async cancelOrder(orderId, _symbol) {
134
- if (!this.config.liveTrading || this.config.dryRun) throw new TradingServiceError("TRADING_LIVE_TRADING_DISABLED", "Bybit TradeService.cancelOrder rejected at the service seam: cancel is a live action and requires liveTrading=true with dryRun=false.");
148
+ if (!liveTradingEnabled(this.config.liveTrading) || this.config.dryRun) throw new TradingServiceError("TRADING_LIVE_TRADING_DISABLED", "Bybit TradeService.cancelOrder rejected at the service seam: cancel is a live action and requires a signed live-trading grant (see @dshtrading/authority) with dryRun=false.");
135
149
  return this.client.cancelOrder(void 0, orderId);
136
150
  }
137
151
  async getOrder(symbol, id) {
@@ -184,11 +198,11 @@ function apply(ctx, config) {
184
198
  };
185
199
  register(defineTool({
186
200
  name: "crypto_get_ticker",
187
- description: "Get the latest trade price and quote for a crypto pair via Bybit API.",
201
+ description: "Get the latest trade price and quote for a crypto pair via Bybit API (spot, or perpetual with the -SWAP suffix).",
188
202
  parameters: { symbol: {
189
203
  type: "string",
190
204
  required: true,
191
- description: "Crypto pair symbol, e.g. BTCUSDT"
205
+ description: "Pair symbol: BTCUSDT for spot, BTCUSDT-SWAP for the USDT perpetual"
192
206
  } },
193
207
  output: {
194
208
  schema: { type: "string" },
@@ -203,12 +217,12 @@ function apply(ctx, config) {
203
217
  }));
204
218
  register(defineTool({
205
219
  name: "crypto_get_klines",
206
- description: "Get recent public klines for a crypto pair via Bybit API. Supports 1m/5m/15m/30m/1h/4h/1d/1w/1M.",
220
+ description: "Get recent public klines for a crypto pair via Bybit API (spot, or perpetual with the -SWAP suffix). Supports 1m/5m/15m/30m/1h/4h/1d/1w/1M.",
207
221
  parameters: {
208
222
  symbol: {
209
223
  type: "string",
210
224
  required: true,
211
- description: "Crypto pair symbol, e.g. BTCUSDT"
225
+ description: "Pair symbol: BTCUSDT for spot, BTCUSDT-SWAP for the USDT perpetual"
212
226
  },
213
227
  interval: {
214
228
  type: "string",
@@ -237,4 +251,4 @@ function apply(ctx, config) {
237
251
  });
238
252
  }
239
253
  //#endregion
240
- export { BybitMarketDataService, BybitRestClient, BybitTradeService, Config, INTERVAL_VOCABULARY, ROUTER_PROVIDER, TRADING_CRYPTO_MARKET_DATA_KEY, TRADING_CRYPTO_TRADE_KEY, TradingServiceError, apply, name, normalizeCryptoSymbol, parseIntervalMs, routeAllows, toBybitInterval };
254
+ export { BybitMarketDataService, BybitRestClient, BybitTradeService, Config, INTERVAL_VOCABULARY, ROUTER_PROVIDER, TRADING_CRYPTO_MARKET_DATA_KEY, TRADING_CRYPTO_TRADE_KEY, TradingServiceError, apply, bybitAssetClassOf, bybitCategory, name, normalizeCryptoSymbol, parseCryptoSymbol, parseIntervalMs, routeAllows, toBybitInterval };
package/lib/rest.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AccountBalance as AccountBalance$1, DerivativesPoint, Interval as Interval$1, Kline as Kline$1, Order as Order$1, OrderRequest, Orderbook, Position as Position$1, Ticker as Ticker$1, TradeTick, TradingErrorCode } from "@dshtrading/api";
1
+ import { AccountBalance as AccountBalance$1, DerivativesPoint, InstrumentAssetClass, InstrumentForm, InstrumentRef, Interval as Interval$1, Kline as Kline$1, Order as Order$1, OrderRequest, Orderbook, Position as Position$1, Ticker as Ticker$1, TradeTick, TradingErrorCode } from "@dshtrading/api";
2
2
  //#region src/rest.d.ts
3
3
  declare class TradingServiceError extends Error {
4
4
  readonly code: TradingErrorCode;
@@ -7,7 +7,32 @@ declare class TradingServiceError extends Error {
7
7
  declare const INTERVAL_VOCABULARY: readonly ["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w", "1M"];
8
8
  declare function toBybitInterval(interval: Interval$1): string;
9
9
  declare function parseIntervalMs(interval: Interval$1): number;
10
+ /** 入参符号的形态裁决结果(交易所侧符号 + 规范形)。 */
11
+ interface ParsedCryptoSymbol {
12
+ /** 交易所侧符号(无 `-SWAP` 后缀):Bybit 现货与线性永续同形,如 BTCUSDT。 */
13
+ readonly base: string;
14
+ readonly form: InstrumentForm;
15
+ /** 市场规范形:spot=BTCUSDT、perp=BTCUSDT-SWAP(输出纪律用)。 */
16
+ readonly canonical: string;
17
+ }
18
+ /**
19
+ * 解析入参符号:形态由规范后缀裁决(复用 @dshtrading/api 的 instrumentFormOf,
20
+ * 连接器不自写 endsWith('-SWAP'))。
21
+ * `-SWAP` 必须整段剥掉再做分隔符清理——否则 BTCUSDT-SWAP 会被揉成 BTCUSDTSWAP,
22
+ * Bybit 查无此标的(issue #54 评审 M2)。
23
+ */
24
+ declare function parseCryptoSymbol(raw: string): ParsedCryptoSymbol;
25
+ /**
26
+ * 交易所侧符号(无形态后缀)。
27
+ * 只做归一化、不表达形态:**凡可能收到 `-SWAP` 的行情路径必须改用
28
+ * parseCryptoSymbol 并按 form 分 category**,否则永续会被静默打到现货端点
29
+ * (P3 硬不变量:任何路径不得把 -SWAP 落到 spot 端点)。
30
+ */
10
31
  declare function normalizeCryptoSymbol(raw: string): string;
32
+ /** Bybit v5 market 的 category:形态的唯一分岔(spot / linear),端点与响应形状一致。 */
33
+ declare function bybitCategory(form: InstrumentForm): 'spot' | 'linear';
34
+ /** symbolType → 资产类别;空串/未登记/缺失 = undefined(不推断)。 */
35
+ declare function bybitAssetClassOf(symbolType: unknown): InstrumentAssetClass | undefined;
11
36
  interface BybitRestOptions {
12
37
  baseUrl?: string;
13
38
  apiKey?: string;
@@ -22,8 +47,18 @@ declare class BybitRestClient {
22
47
  private readonly fetchImpl;
23
48
  constructor(options?: BybitRestOptions);
24
49
  private requestJson;
50
+ /**
51
+ * 公共 ticker:现货 → `category=spot`,永续(`-SWAP`)→ `category=linear`。
52
+ * 形态只由规范后缀裁决;永续请求绝不回落现货端点。输出 symbol 为规范形。
53
+ */
25
54
  getTicker(symbol: string): Promise<Ticker$1>;
55
+ /** 公共 K 线:与 getTicker 同一条形态分岔(spot / linear)。 */
26
56
  getKlines(symbol: string, interval?: Interval$1, limit?: number): Promise<Kline$1[]>;
57
+ /**
58
+ * 标的名册:现货 ∪ 线性永续(只收 status=Trading;线性侧还要求 contractType=LinearPerpetual,
59
+ * instruments-info 会连已交割/预上市行一起返回)。输出规范形 + form + contract 原样透传。
60
+ */
61
+ listInstruments(): Promise<InstrumentRef[]>;
27
62
  /**
28
63
  * 线性合约行情行:GET /v5/market/tickers?category=linear —— 单端点同时携带
29
64
  * fundingRate(当前资金费率,小数)、openInterest(base 币计持仓量)、
@@ -67,4 +102,4 @@ declare class BybitRestClient {
67
102
  }>;
68
103
  }
69
104
  //#endregion
70
- export { type AccountBalance$1 as AccountBalance, BybitRestClient, BybitRestOptions, INTERVAL_VOCABULARY, type Interval$1 as Interval, type Kline$1 as Kline, type Order$1 as Order, type Position$1 as Position, type Ticker$1 as Ticker, TradingServiceError, normalizeCryptoSymbol, parseIntervalMs, toBybitInterval };
105
+ export { type AccountBalance$1 as AccountBalance, BybitRestClient, BybitRestOptions, INTERVAL_VOCABULARY, type Interval$1 as Interval, type Kline$1 as Kline, type Order$1 as Order, ParsedCryptoSymbol, type Position$1 as Position, type Ticker$1 as Ticker, TradingServiceError, bybitAssetClassOf, bybitCategory, normalizeCryptoSymbol, parseCryptoSymbol, parseIntervalMs, toBybitInterval };
package/lib/rest.js CHANGED
@@ -1,4 +1,9 @@
1
+ import { SWAP_SYMBOL_SUFFIX, instrumentFormOf } from "@dshtrading/api";
1
2
  //#region src/rest.ts
3
+ /**
4
+ * @dshtrading/connector-bybit/rest
5
+ * Bybit API v5 REST 客户端(支持公共行情与现货/衍生品交易)。
6
+ */
2
7
  var TradingServiceError = class extends Error {
3
8
  code;
4
9
  constructor(code, message, cause) {
@@ -45,10 +50,34 @@ function parseIntervalMs(interval) {
45
50
  case "1M": return 2592e6;
46
51
  }
47
52
  }
53
+ /**
54
+ * 解析入参符号:形态由规范后缀裁决(复用 @dshtrading/api 的 instrumentFormOf,
55
+ * 连接器不自写 endsWith('-SWAP'))。
56
+ * `-SWAP` 必须整段剥掉再做分隔符清理——否则 BTCUSDT-SWAP 会被揉成 BTCUSDTSWAP,
57
+ * Bybit 查无此标的(issue #54 评审 M2)。
58
+ */
59
+ function parseCryptoSymbol(raw) {
60
+ const form = instrumentFormOf(raw);
61
+ const base = raw.trim().toUpperCase().replace(/-SWAP$/, "").replace(/[-_/]/g, "");
62
+ if (!base) throw new TradingServiceError("TRADING_INVALID_ARGUMENT", "Symbol cannot be empty");
63
+ return {
64
+ base,
65
+ form,
66
+ canonical: form === "perp" ? `${base}${SWAP_SYMBOL_SUFFIX}` : base
67
+ };
68
+ }
69
+ /**
70
+ * 交易所侧符号(无形态后缀)。
71
+ * 只做归一化、不表达形态:**凡可能收到 `-SWAP` 的行情路径必须改用
72
+ * parseCryptoSymbol 并按 form 分 category**,否则永续会被静默打到现货端点
73
+ * (P3 硬不变量:任何路径不得把 -SWAP 落到 spot 端点)。
74
+ */
48
75
  function normalizeCryptoSymbol(raw) {
49
- const clean = raw.trim().toUpperCase().replace(/-SWAP$/, "").replace(/[-_/]/g, "");
50
- if (!clean) throw new TradingServiceError("TRADING_INVALID_ARGUMENT", "Symbol cannot be empty");
51
- return clean;
76
+ return parseCryptoSymbol(raw).base;
77
+ }
78
+ /** Bybit v5 market 的 category:形态的唯一分岔(spot / linear),端点与响应形状一致。 */
79
+ function bybitCategory(form) {
80
+ return form === "perp" ? "linear" : "spot";
52
81
  }
53
82
  /** 宽松转 number(字符串/数字皆收,非有限值返回 undefined)。 */
54
83
  function num(value) {
@@ -56,6 +85,78 @@ function num(value) {
56
85
  const n = typeof value === "string" ? Number(value) : typeof value === "number" ? value : NaN;
57
86
  return Number.isFinite(n) ? n : void 0;
58
87
  }
88
+ function asRecord(value) {
89
+ return typeof value === "object" && value !== null ? value : void 0;
90
+ }
91
+ /** 线性永续 instruments-info 行 → contract 元数据(字段缺失一律缺省,不本地推断)。 */
92
+ function parseContractMeta(row) {
93
+ const lot = asRecord(row.lotSizeFilter);
94
+ const price = asRecord(row.priceFilter);
95
+ const leverage = asRecord(row.leverageFilter);
96
+ const multiplier = num(row.contractSize);
97
+ const tickSize = num(price?.tickSize);
98
+ const lotSize = num(lot?.qtyStep);
99
+ const maxLeverage = num(leverage?.maxLeverage);
100
+ const settleCcy = typeof row.settleCoin === "string" && row.settleCoin !== "" ? row.settleCoin : void 0;
101
+ const contract = {
102
+ ...multiplier !== void 0 ? { multiplier } : {},
103
+ ...tickSize !== void 0 ? { tickSize } : {},
104
+ ...lotSize !== void 0 ? { lotSize } : {},
105
+ ...maxLeverage !== void 0 ? { maxLeverage } : {},
106
+ ...settleCcy !== void 0 ? { settleCcy } : {}
107
+ };
108
+ return Object.keys(contract).length > 0 ? contract : void 0;
109
+ }
110
+ /**
111
+ * Bybit `symbolType` 字面量 → 资产类别标签。
112
+ *
113
+ * `symbolType` 是 instruments-info 行上的**可选**分类字段,官方枚举页列出的取值有
114
+ * innovation/adventure/xstocks/commodity/stock/forex/ETF/mstocks。2026-10-08 可达环境实测
115
+ * (线性 893 行 + 现货 528 行,见 spikes/impl-crypto-perp-tradfi/EVIDENCE-reverify-2026-10-08.md):
116
+ * 线性侧 stock 202 / innovation 128 / ETF 54 / commodity 4 / forex 3 / 空串 502;
117
+ * 现货侧 空串 514 / xstocks 11(NVDAXUSDT、AAPLXUSDT 等代币化股票)/ adventure 3。
118
+ *
119
+ * 只登记**实测过的**字面量:
120
+ * - 空串 = 交易所没给分类(标准加密品种)⇒ **留空而不是推断成 crypto**(不变量:取不到即留空,
121
+ * 漏标而非错标;空串不是分类值)。
122
+ * - `forex`(EURUSD/GBPUSD/USDJPY)与 `mstocks` 未登记:枚举无外汇成员,mstocks 未在实测中出现;
123
+ * 要不要扩枚举属契约层决定,不在本连接器内定。
124
+ */
125
+ const BYBIT_SYMBOL_TYPE_ASSET_CLASS = {
126
+ innovation: "crypto",
127
+ adventure: "crypto",
128
+ stock: "equity",
129
+ ETF: "equity",
130
+ xstocks: "equity",
131
+ commodity: "commodity"
132
+ };
133
+ /** symbolType → 资产类别;空串/未登记/缺失 = undefined(不推断)。 */
134
+ function bybitAssetClassOf(symbolType) {
135
+ if (typeof symbolType !== "string") return void 0;
136
+ return BYBIT_SYMBOL_TYPE_ASSET_CLASS[symbolType.trim()];
137
+ }
138
+ /**
139
+ * instruments-info 行 → InstrumentRef(输出规范形)。
140
+ * assetClass 只由交易所 `symbolType` 字面量裁决(现货与线性同一规则);交易所没给
141
+ * (空串/未登记)即留空,绝不按符号猜(硬不变量 #2:TradFi 归属只信交易所元数据)。
142
+ */
143
+ function toInstrumentRef(row, form) {
144
+ const exchangeSymbol = typeof row.symbol === "string" ? row.symbol.trim().toUpperCase() : "";
145
+ if (exchangeSymbol === "") return void 0;
146
+ const assetClass = bybitAssetClassOf(row.symbolType);
147
+ if (form === "spot") return {
148
+ symbol: exchangeSymbol,
149
+ form,
150
+ ...assetClass !== void 0 ? { assetClass } : {}
151
+ };
152
+ const contract = parseContractMeta(row);
153
+ return {
154
+ symbol: `${exchangeSymbol}${SWAP_SYMBOL_SUFFIX}`,
155
+ form,
156
+ ...assetClass !== void 0 ? { assetClass } : {},
157
+ ...contract !== void 0 ? { contract } : {}
158
+ };
159
+ }
59
160
  var BybitRestClient = class {
60
161
  baseUrl;
61
162
  apiKey;
@@ -78,10 +179,14 @@ var BybitRestClient = class {
78
179
  throw new TradingServiceError("TRADING_NETWORK", `Bybit network error: ${err instanceof Error ? err.message : String(err)}`, err);
79
180
  }
80
181
  }
182
+ /**
183
+ * 公共 ticker:现货 → `category=spot`,永续(`-SWAP`)→ `category=linear`。
184
+ * 形态只由规范后缀裁决;永续请求绝不回落现货端点。输出 symbol 为规范形。
185
+ */
81
186
  async getTicker(symbol) {
82
- const sym = normalizeCryptoSymbol(symbol);
83
- const data = await this.requestJson(`/v5/market/tickers?category=spot&symbol=${sym}`);
84
- if (data.retCode !== 0 || !data.result?.list || data.result.list.length === 0) throw new TradingServiceError("TRADING_SYMBOL_NOT_FOUND", `Bybit ticker not found for ${sym}`);
187
+ const { base: sym, form, canonical } = parseCryptoSymbol(symbol);
188
+ const data = await this.requestJson(`/v5/market/tickers?category=${bybitCategory(form)}&symbol=${sym}`);
189
+ if (data.retCode !== 0 || !data.result?.list || data.result.list.length === 0) throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `Bybit ticker not found for ${canonical} (category=${bybitCategory(form)})`);
85
190
  const row = data.result.list[0];
86
191
  const price = parseFloat(row.lastPrice);
87
192
  const volume = parseFloat(row.volume24h);
@@ -89,7 +194,7 @@ var BybitRestClient = class {
89
194
  const prevClose = row.prevPrice24h ? parseFloat(row.prevPrice24h) : void 0;
90
195
  const changePercent = row.price24hPcnt ? parseFloat(row.price24hPcnt) * 100 : void 0;
91
196
  return {
92
- symbol: sym,
197
+ symbol: canonical,
93
198
  price,
94
199
  volume,
95
200
  timestamp,
@@ -97,11 +202,12 @@ var BybitRestClient = class {
97
202
  ...changePercent !== void 0 && Number.isFinite(changePercent) ? { changePercent } : {}
98
203
  };
99
204
  }
205
+ /** 公共 K 线:与 getTicker 同一条形态分岔(spot / linear)。 */
100
206
  async getKlines(symbol, interval = "1d", limit = 100) {
101
- const sym = normalizeCryptoSymbol(symbol);
207
+ const { base: sym, form } = parseCryptoSymbol(symbol);
102
208
  const bybitInt = toBybitInterval(interval);
103
209
  const stepMs = parseIntervalMs(interval);
104
- const data = await this.requestJson(`/v5/market/kline?category=spot&symbol=${sym}&interval=${bybitInt}&limit=${limit}`);
210
+ const data = await this.requestJson(`/v5/market/kline?category=${bybitCategory(form)}&symbol=${sym}&interval=${bybitInt}&limit=${limit}`);
105
211
  if (data.retCode !== 0 || !data.result?.list) return [];
106
212
  return [...data.result.list].reverse().map((r) => {
107
213
  const openTime = parseInt(r[0], 10);
@@ -116,6 +222,49 @@ var BybitRestClient = class {
116
222
  };
117
223
  });
118
224
  }
225
+ /** instruments-info 单页(limit=1000;游标翻页由 #collectInstrumentRows 驱动)。 */
226
+ async #fetchInstrumentPage(category, cursor) {
227
+ const query = `/v5/market/instruments-info?category=${category}&limit=1000` + (cursor !== void 0 && cursor !== "" ? `&cursor=${encodeURIComponent(cursor)}` : "");
228
+ const data = await this.requestJson(query);
229
+ if (data.retCode !== 0 || !Array.isArray(data.result?.list)) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Bybit instruments-info (${category}): ${data.retMsg}`);
230
+ const next = data.result?.nextPageCursor;
231
+ return {
232
+ list: data.result.list,
233
+ ...typeof next === "string" && next !== "" ? { next } : {}
234
+ };
235
+ }
236
+ async #collectInstrumentRows(category) {
237
+ const rows = [];
238
+ const seen = /* @__PURE__ */ new Set();
239
+ let cursor;
240
+ for (;;) {
241
+ const page = await this.#fetchInstrumentPage(category, cursor);
242
+ rows.push(...page.list);
243
+ if (page.next === void 0) return rows;
244
+ if (seen.has(page.next)) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Bybit instruments-info (${category}): upstream repeated page cursor ${page.next}`);
245
+ seen.add(page.next);
246
+ cursor = page.next;
247
+ }
248
+ }
249
+ /**
250
+ * 标的名册:现货 ∪ 线性永续(只收 status=Trading;线性侧还要求 contractType=LinearPerpetual,
251
+ * instruments-info 会连已交割/预上市行一起返回)。输出规范形 + form + contract 原样透传。
252
+ */
253
+ async listInstruments() {
254
+ const [spotRows, linearRows] = await Promise.all([this.#collectInstrumentRows("spot"), this.#collectInstrumentRows("linear")]);
255
+ const out = [];
256
+ for (const row of spotRows) {
257
+ if (row.status !== "Trading") continue;
258
+ const ref = toInstrumentRef(row, "spot");
259
+ if (ref !== void 0) out.push(ref);
260
+ }
261
+ for (const row of linearRows) {
262
+ if (row.contractType !== "LinearPerpetual" || row.status !== "Trading") continue;
263
+ const ref = toInstrumentRef(row, "perp");
264
+ if (ref !== void 0) out.push(ref);
265
+ }
266
+ return out;
267
+ }
119
268
  /**
120
269
  * 线性合约行情行:GET /v5/market/tickers?category=linear —— 单端点同时携带
121
270
  * fundingRate(当前资金费率,小数)、openInterest(base 币计持仓量)、
@@ -211,11 +360,11 @@ var BybitRestClient = class {
211
360
  * bids/asks 全称(2026-09-02 真实响应实证,spikes/impl-orderbook-ticks/bybit-orderbook-raw.json)。
212
361
  */
213
362
  async getOrderbook(symbol) {
214
- const sym = normalizeCryptoSymbol(symbol);
215
- const data = await this.requestJson(`/v5/market/orderbook?category=spot&symbol=${sym}&limit=25`);
216
- if (data.retCode !== 0 || !Array.isArray(data.result?.b) || !Array.isArray(data.result?.a)) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Bybit orderbook for ${sym}: unexpected response shape`);
363
+ const { base: sym, form, canonical } = parseCryptoSymbol(symbol);
364
+ const data = await this.requestJson(`/v5/market/orderbook?category=${bybitCategory(form)}&symbol=${sym}&limit=25`);
365
+ if (data.retCode !== 0 || !Array.isArray(data.result?.b) || !Array.isArray(data.result?.a)) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Bybit orderbook for ${canonical}: unexpected response shape`);
217
366
  return {
218
- symbol: sym,
367
+ symbol: canonical,
219
368
  bids: data.result.b.map((row) => this.#parseBookRow(row)).filter((l) => l !== void 0),
220
369
  asks: data.result.a.map((row) => this.#parseBookRow(row)).filter((l) => l !== void 0),
221
370
  timestamp: num(data.result.ts) ?? Date.now()
@@ -239,12 +388,11 @@ var BybitRestClient = class {
239
388
  }
240
389
  /** 最近逐笔成交:GET /v5/market/recent-trade?category=spot(响应新→旧 → 反转升序)。 */
241
390
  async getRecentTrades(symbol, limit = 50) {
242
- const sym = normalizeCryptoSymbol(symbol);
391
+ const { base: sym, form, canonical } = parseCryptoSymbol(symbol);
243
392
  const capped = Math.max(1, Math.min(Math.floor(limit) || 50, 60));
244
- const data = await this.requestJson(`/v5/market/recent-trade?category=spot&symbol=${sym}&limit=${capped}`);
245
- if (data.retCode !== 0 || !data.result?.list) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Bybit trades for ${sym}: unexpected response shape`);
246
- const symbolOut = sym;
247
- return data.result.list.map((row) => this.#parseTradeRow(row, symbolOut)).reverse();
393
+ const data = await this.requestJson(`/v5/market/recent-trade?category=${bybitCategory(form)}&symbol=${sym}&limit=${capped}`);
394
+ if (data.retCode !== 0 || !data.result?.list) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Bybit trades for ${canonical}: unexpected response shape`);
395
+ return data.result.list.map((row) => this.#parseTradeRow(row, canonical)).reverse();
248
396
  }
249
397
  async getBalance() {
250
398
  return {
@@ -275,4 +423,4 @@ var BybitRestClient = class {
275
423
  }
276
424
  };
277
425
  //#endregion
278
- export { BybitRestClient, INTERVAL_VOCABULARY, TradingServiceError, normalizeCryptoSymbol, parseIntervalMs, toBybitInterval };
426
+ export { BybitRestClient, INTERVAL_VOCABULARY, TradingServiceError, bybitAssetClassOf, bybitCategory, normalizeCryptoSymbol, parseCryptoSymbol, parseIntervalMs, toBybitInterval };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dshtrading/connector-bybit",
3
3
  "description": "【模板】交易所连接器插件脚手架:Bybit(bybit)v2 插件骨架 —— 市场数据服务 + 交易服务 + 三态闸门 + BYOK 凭证 ref。生成器:node scripts/new-connector.mjs --slug <exchange>",
4
- "version": "0.5.0",
4
+ "version": "0.6.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -21,7 +21,8 @@
21
21
  ],
22
22
  "license": "PolyForm-Noncommercial-1.0.0",
23
23
  "dependencies": {
24
- "@dshtrading/api": "^0.5.0"
24
+ "@dshtrading/authority": "^0.6.0",
25
+ "@dshtrading/api": "^0.6.0"
25
26
  },
26
27
  "peerDependencies": {
27
28
  "@deepseek-ai/cordis": ">=4.0.0",