@dshtrading/connector-binance 0.1.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/LICENSE +75 -0
- package/lib/dataplane.d.ts +7 -0
- package/lib/dataplane.js +22 -0
- package/lib/index.d.ts +135 -0
- package/lib/index.js +359 -0
- package/lib/rest.d.ts +80 -0
- package/lib/rest.js +366 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Copyright (c) 2026 zhu1090093659 (dsh-trading)
|
|
2
|
+
|
|
3
|
+
# PolyForm Noncommercial License 1.0.0
|
|
4
|
+
|
|
5
|
+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
6
|
+
|
|
7
|
+
## Acceptance
|
|
8
|
+
|
|
9
|
+
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
|
|
14
|
+
|
|
15
|
+
## Distribution License
|
|
16
|
+
|
|
17
|
+
The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
|
|
18
|
+
|
|
19
|
+
## Notices
|
|
20
|
+
|
|
21
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
|
|
22
|
+
|
|
23
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
24
|
+
|
|
25
|
+
## Changes and New Works License
|
|
26
|
+
|
|
27
|
+
The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
|
|
28
|
+
|
|
29
|
+
## Patent License
|
|
30
|
+
|
|
31
|
+
The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
|
|
32
|
+
|
|
33
|
+
## Noncommercial Purposes
|
|
34
|
+
|
|
35
|
+
Any noncommercial purpose is a permitted purpose.
|
|
36
|
+
|
|
37
|
+
## Personal Uses
|
|
38
|
+
|
|
39
|
+
Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
|
|
40
|
+
|
|
41
|
+
## Noncommercial Organizations
|
|
42
|
+
|
|
43
|
+
Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
|
|
44
|
+
|
|
45
|
+
## Fair Use
|
|
46
|
+
|
|
47
|
+
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
|
48
|
+
|
|
49
|
+
## No Other Rights
|
|
50
|
+
|
|
51
|
+
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
|
52
|
+
|
|
53
|
+
## Patent Defense
|
|
54
|
+
|
|
55
|
+
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
|
56
|
+
|
|
57
|
+
## Violations
|
|
58
|
+
|
|
59
|
+
The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
|
|
60
|
+
|
|
61
|
+
## No Liability
|
|
62
|
+
|
|
63
|
+
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
|
64
|
+
|
|
65
|
+
## Definitions
|
|
66
|
+
|
|
67
|
+
The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
|
|
68
|
+
|
|
69
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
70
|
+
|
|
71
|
+
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
72
|
+
|
|
73
|
+
**Your licenses** are all the licenses granted to you for the software under these terms.
|
|
74
|
+
|
|
75
|
+
**Use** means anything you do with the software requiring one of your licenses.
|
package/lib/dataplane.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { BinanceMarketDataService, ROUTER_PROVIDER, TRADING_CRYPTO_MARKET_DATA_KEY, routeAllows } from "./index.js";
|
|
2
|
+
//#region src/dataplane.ts
|
|
3
|
+
const inject = [];
|
|
4
|
+
/** 解析注册表服务;老部署(base/router 未升级)返回 undefined → 调用方回退旧的直接 provide 路径。 */
|
|
5
|
+
function resolveMarketDataRegistry(ctx) {
|
|
6
|
+
const candidate = ctx.get?.("tradingMarketDataRegistry", false);
|
|
7
|
+
return candidate !== void 0 ? candidate : void 0;
|
|
8
|
+
}
|
|
9
|
+
function apply(ctx, config) {
|
|
10
|
+
if (!config.enabled) return;
|
|
11
|
+
const registry = resolveMarketDataRegistry(ctx);
|
|
12
|
+
if (registry === void 0) {
|
|
13
|
+
if (!routeAllows(ctx, config, "crypto")) return;
|
|
14
|
+
new BinanceMarketDataService(ctx);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const inner = ctx.isolate(TRADING_CRYPTO_MARKET_DATA_KEY);
|
|
18
|
+
const service = new BinanceMarketDataService(inner);
|
|
19
|
+
ctx.effect(() => registry.register("crypto", ROUTER_PROVIDER, service));
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { apply, inject };
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { BinanceRestClient, BinanceRestOptions, INTERVAL_VOCABULARY, TradingServiceError, normalizeBinanceFuturesSymbol } from "./rest.js";
|
|
2
|
+
import { Context, Service } from "@deepseek-ai/cordis";
|
|
3
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
4
|
+
import { DerivativesData, DerivativesHistory, Disposable, Interval, Kline, MarketDataService, Orderbook, Ticker, TradeTick } from "@dshtrading/api";
|
|
5
|
+
//#region src/index.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Cordis 插件名 = patch 行 id(TEMPLATES §8):`dsh-trading-crypto-*` 市场命名空间,
|
|
8
|
+
* 全仓唯一,绝不使用 `base` 等官方保留 id(insert-only 铁律 #1)。
|
|
9
|
+
*/
|
|
10
|
+
declare const name = "dsh-trading-crypto-connector-binance";
|
|
11
|
+
interface Config {
|
|
12
|
+
/** 互斥激活总开关(默认 true):false 时本插件不注册任何服务/工具(okx 行启用时须关本开关;docs/okx-integration.md §8.2 方案 B)。 */
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
/** 交易安全闸门(铁律 #3):true 时下单类工具强制 dry-run。 */
|
|
15
|
+
dryRun: boolean;
|
|
16
|
+
/** 实盘总闸门:默认 false;false 时无论 dryRun 与否都拒绝实盘下单 [S4]。 */
|
|
17
|
+
liveTrading: boolean;
|
|
18
|
+
}
|
|
19
|
+
declare const Config: Schema<Config>;
|
|
20
|
+
/** 需要宿主提供的 Cordis 服务。 */
|
|
21
|
+
declare const inject: string[];
|
|
22
|
+
/** ctx 服务键:与 @dshtrading/api 的 Context 模块增强一致(市场命名空间)。 */
|
|
23
|
+
declare const TRADING_CRYPTO_MARKET_DATA_KEY = "tradingCryptoMarketData";
|
|
24
|
+
interface SubscribeTickerOptions {
|
|
25
|
+
/** 轮询间隔(ms)。切片阶段 subscribeTicker 以 REST 轮询实现,WS 在后续任务。 */
|
|
26
|
+
readonly intervalMs?: number;
|
|
27
|
+
}
|
|
28
|
+
declare class BinanceMarketDataService extends Service implements MarketDataService {
|
|
29
|
+
private readonly client;
|
|
30
|
+
constructor(ctx: Context, options?: BinanceRestOptions, name?: string);
|
|
31
|
+
getTicker(symbol: string): Promise<Ticker>;
|
|
32
|
+
getKlines(symbol: string, interval: Interval, limit?: number): Promise<Kline[]>;
|
|
33
|
+
listInstruments(): Promise<Array<{
|
|
34
|
+
symbol: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
}>>;
|
|
37
|
+
subscribeTicker(symbol: string, cb: (ticker: Ticker) => void, options?: SubscribeTickerOptions): Disposable;
|
|
38
|
+
/**
|
|
39
|
+
* 盘口快照(api 可选契约 getOrderbook,issue #39):现货 depth 20 档透传,
|
|
40
|
+
* SWAP/原生输入归一到现货词汇(盘口跟图表标的走)。
|
|
41
|
+
*/
|
|
42
|
+
getOrderbook(symbol: string): Promise<Orderbook>;
|
|
43
|
+
/** 最近逐笔成交(api 可选契约 getRecentTrades,issue #39),时间升序。 */
|
|
44
|
+
getRecentTrades(symbol: string, limit?: number): Promise<TradeTick[]>;
|
|
45
|
+
/**
|
|
46
|
+
* 衍生品指标快照(api 可选契约 getDerivatives,issue #38):聚合 Binance USDT-M
|
|
47
|
+
* 合约公共端点(fapi openInterest / fundingRate / futures/data 多空比族)。
|
|
48
|
+
* 任一子查询失败只降级该字段(undefined,面板按缺格隐藏);全部失败才抛
|
|
49
|
+
* 结构化错误(桥层转 ok:false,前端不弹横幅)。输出 symbol 为规范 SWAP 形。
|
|
50
|
+
*/
|
|
51
|
+
getDerivatives(symbol: string): Promise<DerivativesData>;
|
|
52
|
+
/**
|
|
53
|
+
* 衍生品历史序列(api 可选契约 getDerivativesHistory,issue #54):
|
|
54
|
+
* /fapi/v1/fundingRate + /futures/data/openInterestHist 双端点聚合并发拉取,
|
|
55
|
+
* 任一失败只降级该序列(字段缺省 → 对应趋势卡隐藏),全部失败才抛结构化错误。
|
|
56
|
+
*/
|
|
57
|
+
getDerivativesHistory(symbol: string): Promise<DerivativesHistory>;
|
|
58
|
+
}
|
|
59
|
+
/** crypto_place_order 参数契约(本切片词汇:Binance 现货下单参数,dryRun 缺省 true)。 */
|
|
60
|
+
interface PlaceOrderArgs {
|
|
61
|
+
/** 交易对符号,如 `BTCUSDT`(执行前归一化为大写)。 */
|
|
62
|
+
readonly symbol: string;
|
|
63
|
+
/** 方向(交易所词汇大写)。 */
|
|
64
|
+
readonly side: 'BUY' | 'SELL';
|
|
65
|
+
/** 订单类型(交易所词汇大写)。 */
|
|
66
|
+
readonly type: 'MARKET' | 'LIMIT';
|
|
67
|
+
/** base 资产数量,必须 > 0。 */
|
|
68
|
+
readonly quantity: number;
|
|
69
|
+
/** LIMIT 单必填(schema 无法条件必填,execute 内校验),必须 > 0。 */
|
|
70
|
+
readonly price?: number;
|
|
71
|
+
/** 缺省视为 true:仅模拟。显式 false 即实盘意图,进入闸门 ①/③。 */
|
|
72
|
+
readonly dryRun?: boolean;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 闸门判定结果(三条路径,顺序即铁律 #3 修订版的裁决顺序):
|
|
76
|
+
* - `reject` —— ① 请求实盘(dryRun!==true)而 liveTrading=false:工具返回结构化
|
|
77
|
+
* 拒绝(不抛异常,模型可读到明确原因与出路);
|
|
78
|
+
* - `simulate` —— ② dryRun=true(显式、缺省,或被插件 config.dryRun 强制):
|
|
79
|
+
* 返回 DRY-RUN 模拟成交回执;
|
|
80
|
+
* - `live` —— ③ dryRun=false 且 liveTrading=true:本切片无签名下单能力,
|
|
81
|
+
* 工具抛 TRADING_NOT_IMPLEMENTED 结构化错误。
|
|
82
|
+
*
|
|
83
|
+
* 优先级说明:config.dryRun=true 是「强制模拟」开关,只影响 ②/③ 的归类;
|
|
84
|
+
* 显式 dryRun=false 的实盘意图在 liveTrading=false 时仍走 ① 明确拒绝,
|
|
85
|
+
* 不做静默降级(拒绝语义优先于强制模拟,调用方必须知道实盘意图被拒)。
|
|
86
|
+
*/
|
|
87
|
+
type OrderGateVerdict = {
|
|
88
|
+
action: 'reject';
|
|
89
|
+
code: 'TRADING_LIVE_TRADING_DISABLED';
|
|
90
|
+
message: string;
|
|
91
|
+
} | {
|
|
92
|
+
action: 'simulate';
|
|
93
|
+
} | {
|
|
94
|
+
action: 'live';
|
|
95
|
+
};
|
|
96
|
+
declare function evaluateOrderGate(config: Config, args: PlaceOrderArgs): OrderGateVerdict;
|
|
97
|
+
/** DRY-RUN 回执:模拟成交 + 当前市价参照(参照取不到不阻断模拟本身)。 */
|
|
98
|
+
interface DryRunReference {
|
|
99
|
+
source: 'binance-public-ticker';
|
|
100
|
+
price?: number;
|
|
101
|
+
bid?: number;
|
|
102
|
+
ask?: number;
|
|
103
|
+
timestamp?: number;
|
|
104
|
+
unavailable?: string;
|
|
105
|
+
}
|
|
106
|
+
declare function buildDryRunReceipt(args: PlaceOrderArgs, marketData: Pick<MarketDataService, 'getTicker'>): Promise<string>;
|
|
107
|
+
interface PlaceOrderToolDeps {
|
|
108
|
+
/** 行情服务( dry-run 回执的市价参照),按接口取用,不直连 REST。 */
|
|
109
|
+
readonly marketData: Pick<MarketDataService, 'getTicker'>;
|
|
110
|
+
/** 插件配置(dryRun 强制模拟 / liveTrading 总闸门)。 */
|
|
111
|
+
readonly config: Config;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* crypto_place_order 工具工厂(独立导出便于单测三条闸门路径)。
|
|
115
|
+
*
|
|
116
|
+
* 审批不在这里做:dryRun!==true 的调用由 @dshtrading/base 的 gate 插件在
|
|
117
|
+
* `tools/pre-execute` waterfall 统一 ask(S4:headless 下 ask=deny,fail-closed);
|
|
118
|
+
* 工具内不再重复调 ctx.approval。
|
|
119
|
+
*/
|
|
120
|
+
declare function createPlaceOrderTool(deps: PlaceOrderToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
121
|
+
/** 本连接器的路由 provider slug(docs/exchange-routing.md §2.2:路由层词汇,非包名)。 */
|
|
122
|
+
declare const ROUTER_PROVIDER = "binance";
|
|
123
|
+
/** 市场路由服务的最小消费面(api 包 MarketRouterService 同构;不定死接口)。 */
|
|
124
|
+
interface MarketRouterLike {
|
|
125
|
+
activeProvider(market: string): string | undefined;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 路由裁决(2026-08-29 设置驱动重构):router 存在且给本市场选了别的 provider →
|
|
129
|
+
* 本连接器静默退出(设置是权威);router 不存在(老部署)→ 回退 enabled 语义。
|
|
130
|
+
* 返回 true = 应继续 apply。
|
|
131
|
+
*/
|
|
132
|
+
declare function routeAllows(ctx: Context, config: Config, market: string): boolean;
|
|
133
|
+
declare function apply(ctx: Context, config: Config): void;
|
|
134
|
+
//#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 };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { BinanceRestClient, INTERVAL_VOCABULARY, TradingServiceError, normalizeBinanceFuturesSymbol } from "./rest.js";
|
|
2
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
3
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
5
|
+
import { createGetIndicatorsTool } from "@dshtrading/indicators/tool";
|
|
6
|
+
//#region src/index.ts
|
|
7
|
+
/**
|
|
8
|
+
* 互斥激活的注册面(与 connector-okx 的 registerTool 同款,2026-08-31 补齐):
|
|
9
|
+
* dsh-tools 对同名重复注册直接抛错,互斥纪律下冲突降级为「先到先得 + warn」。
|
|
10
|
+
* 没有这层时,inject 回调就绪顺序不定会让同名工具(含 crypto_get_indicators)
|
|
11
|
+
* 在两 connector 同树时随机炸挂载——activation.test 曾因此竞态翻车。
|
|
12
|
+
*/
|
|
13
|
+
function registerTool(ctx, tool, log) {
|
|
14
|
+
const tools = ctx.tools;
|
|
15
|
+
if (tools.get(tool.name) !== void 0) {
|
|
16
|
+
log.warn("[dsh-trading-crypto-connector-binance] tool %s already registered by another provider — skipped (mutual exclusion: at most one crypto connector/toolset may be active)", tool.name);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
tools.register(tool);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Cordis 插件名 = patch 行 id(TEMPLATES §8):`dsh-trading-crypto-*` 市场命名空间,
|
|
23
|
+
* 全仓唯一,绝不使用 `base` 等官方保留 id(insert-only 铁律 #1)。
|
|
24
|
+
*/
|
|
25
|
+
const name = "dsh-trading-crypto-connector-binance";
|
|
26
|
+
const Config = Schema.object({
|
|
27
|
+
enabled: Schema.boolean().default(true),
|
|
28
|
+
dryRun: Schema.boolean().default(true),
|
|
29
|
+
liveTrading: Schema.boolean().default(false)
|
|
30
|
+
});
|
|
31
|
+
/** 需要宿主提供的 Cordis 服务。 */
|
|
32
|
+
const inject = ["tools"];
|
|
33
|
+
/** ctx 服务键:与 @dshtrading/api 的 Context 模块增强一致(市场命名空间)。 */
|
|
34
|
+
const TRADING_CRYPTO_MARKET_DATA_KEY = "tradingCryptoMarketData";
|
|
35
|
+
const SUBSCRIBE_MIN_MS = 250;
|
|
36
|
+
const SUBSCRIBE_DEFAULT_MS = 5e3;
|
|
37
|
+
var BinanceMarketDataService = class extends Service {
|
|
38
|
+
client;
|
|
39
|
+
constructor(ctx, options = {}, name = TRADING_CRYPTO_MARKET_DATA_KEY) {
|
|
40
|
+
super(ctx, name);
|
|
41
|
+
this.client = new BinanceRestClient(options);
|
|
42
|
+
}
|
|
43
|
+
getTicker(symbol) {
|
|
44
|
+
return this.client.getTicker(symbol);
|
|
45
|
+
}
|
|
46
|
+
getKlines(symbol, interval, limit) {
|
|
47
|
+
return this.client.getKlines(symbol, interval, limit);
|
|
48
|
+
}
|
|
49
|
+
listInstruments() {
|
|
50
|
+
return this.client.listInstruments();
|
|
51
|
+
}
|
|
52
|
+
subscribeTicker(symbol, cb, options) {
|
|
53
|
+
const ms = Math.max(options?.intervalMs ?? SUBSCRIBE_DEFAULT_MS, SUBSCRIBE_MIN_MS);
|
|
54
|
+
const tick = () => {
|
|
55
|
+
this.client.getTicker(symbol).then(cb, () => {});
|
|
56
|
+
};
|
|
57
|
+
tick();
|
|
58
|
+
const timer = setInterval(tick, ms);
|
|
59
|
+
return { dispose: () => clearInterval(timer) };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 盘口快照(api 可选契约 getOrderbook,issue #39):现货 depth 20 档透传,
|
|
63
|
+
* SWAP/原生输入归一到现货词汇(盘口跟图表标的走)。
|
|
64
|
+
*/
|
|
65
|
+
async getOrderbook(symbol) {
|
|
66
|
+
return this.client.getOrderbook(symbol);
|
|
67
|
+
}
|
|
68
|
+
/** 最近逐笔成交(api 可选契约 getRecentTrades,issue #39),时间升序。 */
|
|
69
|
+
async getRecentTrades(symbol, limit = 50) {
|
|
70
|
+
return this.client.getRecentTrades(symbol, limit);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 衍生品指标快照(api 可选契约 getDerivatives,issue #38):聚合 Binance USDT-M
|
|
74
|
+
* 合约公共端点(fapi openInterest / fundingRate / futures/data 多空比族)。
|
|
75
|
+
* 任一子查询失败只降级该字段(undefined,面板按缺格隐藏);全部失败才抛
|
|
76
|
+
* 结构化错误(桥层转 ok:false,前端不弹横幅)。输出 symbol 为规范 SWAP 形。
|
|
77
|
+
*/
|
|
78
|
+
async getDerivatives(symbol) {
|
|
79
|
+
const futuresSymbol = normalizeBinanceFuturesSymbol(symbol);
|
|
80
|
+
const unavailable = [];
|
|
81
|
+
const collect = (label, task) => task.catch((error) => {
|
|
82
|
+
unavailable.push(`${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
83
|
+
});
|
|
84
|
+
const [interest, funding, globalRatio, topRatio, takerRatio, premium] = await Promise.all([
|
|
85
|
+
collect("open-interest", this.client.getFuturesOpenInterest(futuresSymbol)),
|
|
86
|
+
collect("funding", this.client.getFuturesFundingRate(futuresSymbol)),
|
|
87
|
+
collect("global-ls", this.client.getFuturesLongShortRatio("global", futuresSymbol)),
|
|
88
|
+
collect("top-ls", this.client.getFuturesLongShortRatio("top", futuresSymbol)),
|
|
89
|
+
collect("taker", this.client.getFuturesTakerRatio(futuresSymbol)),
|
|
90
|
+
collect("premium-index", this.client.getFuturesPremiumIndex(futuresSymbol))
|
|
91
|
+
]);
|
|
92
|
+
const openInterest = interest?.openInterest;
|
|
93
|
+
const fundingRate = funding?.fundingRate ?? premium?.lastFundingRate;
|
|
94
|
+
const longShortRatio = globalRatio;
|
|
95
|
+
const topTraderLongShortRatio = topRatio;
|
|
96
|
+
const takerBuySellRatio = takerRatio;
|
|
97
|
+
const markPrice = premium?.markPrice;
|
|
98
|
+
const indexPrice = premium?.indexPrice;
|
|
99
|
+
const nextFundingTime = premium?.nextFundingTime;
|
|
100
|
+
if (openInterest === void 0 && fundingRate === void 0 && longShortRatio === void 0 && topTraderLongShortRatio === void 0 && takerBuySellRatio === void 0 && markPrice === void 0 && indexPrice === void 0 && nextFundingTime === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance derivatives for ${futuresSymbol}: all sub-queries failed` + (unavailable.length > 0 ? ` (${unavailable.join("; ")})` : ""));
|
|
101
|
+
return {
|
|
102
|
+
symbol: `${futuresSymbol}-SWAP`,
|
|
103
|
+
source: "binance",
|
|
104
|
+
...openInterest !== void 0 ? { openInterest } : {},
|
|
105
|
+
...longShortRatio !== void 0 ? { longShortRatio } : {},
|
|
106
|
+
...topTraderLongShortRatio !== void 0 ? { topTraderLongShortRatio } : {},
|
|
107
|
+
...takerBuySellRatio !== void 0 ? { takerBuySellRatio } : {},
|
|
108
|
+
...fundingRate !== void 0 ? { fundingRate } : {},
|
|
109
|
+
...nextFundingTime !== void 0 ? { nextFundingTime } : {},
|
|
110
|
+
...markPrice !== void 0 ? { markPrice } : {},
|
|
111
|
+
...indexPrice !== void 0 ? { indexPrice } : {},
|
|
112
|
+
timestamp: interest?.time ?? funding?.fundingTime ?? Date.now()
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 衍生品历史序列(api 可选契约 getDerivativesHistory,issue #54):
|
|
117
|
+
* /fapi/v1/fundingRate + /futures/data/openInterestHist 双端点聚合并发拉取,
|
|
118
|
+
* 任一失败只降级该序列(字段缺省 → 对应趋势卡隐藏),全部失败才抛结构化错误。
|
|
119
|
+
*/
|
|
120
|
+
async getDerivativesHistory(symbol) {
|
|
121
|
+
const futuresSymbol = normalizeBinanceFuturesSymbol(symbol);
|
|
122
|
+
const unavailable = [];
|
|
123
|
+
const collect = (label, task) => task.catch((error) => {
|
|
124
|
+
unavailable.push(`${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
125
|
+
});
|
|
126
|
+
const [fundingRates, openInterest] = await Promise.all([collect("funding-history", this.client.getFuturesFundingRateHistory(futuresSymbol, 30)), collect("oi-history", this.client.getFuturesOpenInterestHistory(futuresSymbol, 30))]);
|
|
127
|
+
if (fundingRates === void 0 && openInterest === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance derivatives history for ${futuresSymbol}: all sub-queries failed` + (unavailable.length > 0 ? ` (${unavailable.join("; ")})` : ""));
|
|
128
|
+
return {
|
|
129
|
+
symbol: `${futuresSymbol}-SWAP`,
|
|
130
|
+
source: "binance",
|
|
131
|
+
...fundingRates !== void 0 && fundingRates.length > 0 ? { fundingRates } : {},
|
|
132
|
+
...openInterest !== void 0 && openInterest.length > 0 ? { openInterest } : {}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
function evaluateOrderGate(config, args) {
|
|
137
|
+
const requestedDryRun = args.dryRun ?? true;
|
|
138
|
+
if (!requestedDryRun && !config.liveTrading) return {
|
|
139
|
+
action: "reject",
|
|
140
|
+
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.`
|
|
142
|
+
};
|
|
143
|
+
if (requestedDryRun || config.dryRun) return { action: "simulate" };
|
|
144
|
+
return { action: "live" };
|
|
145
|
+
}
|
|
146
|
+
/** Binance 现货符号形如 BTCUSDT / ETHUSDT:大写字母数字(kit 同款词汇)。 */
|
|
147
|
+
const SPOT_SYMBOL_PATTERN = /^[A-Z0-9]{4,20}$/;
|
|
148
|
+
/** 参数校验(模型调用问题抛普通 Error,与 kit 先例一致;服务故障才用错误词汇)。 */
|
|
149
|
+
function validatePlaceOrderArgs(args) {
|
|
150
|
+
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
|
+
if (args.side !== "BUY" && args.side !== "SELL") throw new Error(`crypto_place_order: invalid side ${JSON.stringify(args.side)} — expected BUY or SELL`);
|
|
152
|
+
if (args.type !== "MARKET" && args.type !== "LIMIT") throw new Error(`crypto_place_order: invalid type ${JSON.stringify(args.type)} — expected MARKET or LIMIT`);
|
|
153
|
+
if (typeof args.quantity !== "number" || !Number.isFinite(args.quantity) || args.quantity <= 0) throw new Error(`crypto_place_order: invalid quantity ${JSON.stringify(args.quantity)} — expected a positive number`);
|
|
154
|
+
if (args.type === "LIMIT" && (typeof args.price !== "number" || !Number.isFinite(args.price) || args.price <= 0)) throw new Error("crypto_place_order: LIMIT orders require a positive price");
|
|
155
|
+
}
|
|
156
|
+
function normalizePlaceOrderArgs(raw) {
|
|
157
|
+
const args = raw ?? {};
|
|
158
|
+
const symbol = typeof args.symbol === "string" ? args.symbol.trim().toUpperCase() : void 0;
|
|
159
|
+
return {
|
|
160
|
+
...args,
|
|
161
|
+
symbol
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function buildDryRunReceipt(args, marketData) {
|
|
165
|
+
let reference;
|
|
166
|
+
try {
|
|
167
|
+
const ticker = await marketData.getTicker(args.symbol);
|
|
168
|
+
reference = {
|
|
169
|
+
source: "binance-public-ticker",
|
|
170
|
+
price: ticker.price,
|
|
171
|
+
bid: ticker.bid,
|
|
172
|
+
ask: ticker.ask,
|
|
173
|
+
timestamp: ticker.timestamp
|
|
174
|
+
};
|
|
175
|
+
} catch (error) {
|
|
176
|
+
reference = {
|
|
177
|
+
source: "binance-public-ticker",
|
|
178
|
+
unavailable: error instanceof Error ? error.message : String(error)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return JSON.stringify({
|
|
182
|
+
status: "filled",
|
|
183
|
+
dryRun: true,
|
|
184
|
+
note: "DRY-RUN — simulated fill; no order was sent to any exchange. The reference price is market data only, not a fill price.",
|
|
185
|
+
id: `dry-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
186
|
+
symbol: args.symbol,
|
|
187
|
+
side: args.side.toLowerCase(),
|
|
188
|
+
type: args.type.toLowerCase(),
|
|
189
|
+
quantity: args.quantity,
|
|
190
|
+
...args.type === "LIMIT" ? { price: args.price } : {},
|
|
191
|
+
reference,
|
|
192
|
+
timestamp: Date.now()
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* crypto_place_order 工具工厂(独立导出便于单测三条闸门路径)。
|
|
197
|
+
*
|
|
198
|
+
* 审批不在这里做:dryRun!==true 的调用由 @dshtrading/base 的 gate 插件在
|
|
199
|
+
* `tools/pre-execute` waterfall 统一 ask(S4:headless 下 ask=deny,fail-closed);
|
|
200
|
+
* 工具内不再重复调 ctx.approval。
|
|
201
|
+
*/
|
|
202
|
+
function createPlaceOrderTool(deps) {
|
|
203
|
+
return defineTool({
|
|
204
|
+
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.",
|
|
206
|
+
parameters: {
|
|
207
|
+
symbol: {
|
|
208
|
+
type: "string",
|
|
209
|
+
required: true,
|
|
210
|
+
description: "Trading pair symbol, e.g. BTCUSDT"
|
|
211
|
+
},
|
|
212
|
+
side: {
|
|
213
|
+
type: "string",
|
|
214
|
+
enum: ["BUY", "SELL"],
|
|
215
|
+
required: true,
|
|
216
|
+
description: "Order side"
|
|
217
|
+
},
|
|
218
|
+
type: {
|
|
219
|
+
type: "string",
|
|
220
|
+
enum: ["MARKET", "LIMIT"],
|
|
221
|
+
required: true,
|
|
222
|
+
description: "Order type"
|
|
223
|
+
},
|
|
224
|
+
quantity: {
|
|
225
|
+
type: "number",
|
|
226
|
+
required: true,
|
|
227
|
+
description: "Base asset quantity, must be > 0"
|
|
228
|
+
},
|
|
229
|
+
price: {
|
|
230
|
+
type: "number",
|
|
231
|
+
description: "Limit price; required when type=LIMIT"
|
|
232
|
+
},
|
|
233
|
+
dryRun: {
|
|
234
|
+
type: "boolean",
|
|
235
|
+
description: "true (default) = simulate only and return a DRY-RUN receipt; false = request real execution (gated by liveTrading and user approval)",
|
|
236
|
+
default: true
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
output: {
|
|
240
|
+
schema: { type: "string" },
|
|
241
|
+
render: (_args, value) => [{
|
|
242
|
+
type: "text",
|
|
243
|
+
text: value
|
|
244
|
+
}]
|
|
245
|
+
},
|
|
246
|
+
async execute(raw) {
|
|
247
|
+
const args = normalizePlaceOrderArgs(raw);
|
|
248
|
+
validatePlaceOrderArgs(args);
|
|
249
|
+
const verdict = evaluateOrderGate(deps.config, args);
|
|
250
|
+
if (verdict.action === "reject") return JSON.stringify({
|
|
251
|
+
status: "rejected",
|
|
252
|
+
code: verdict.code,
|
|
253
|
+
message: verdict.message
|
|
254
|
+
});
|
|
255
|
+
if (verdict.action === "simulate") return buildDryRunReceipt(args, deps.marketData);
|
|
256
|
+
throw new TradingServiceError("TRADING_NOT_IMPLEMENTED", "crypto_place_order: live order execution is not implemented in this slice — signed order placement (credentials + exchange endpoint) is a follow-up task. Keep dryRun=true for simulated fills.");
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
function logger(ctx) {
|
|
261
|
+
const service = ctx.logger;
|
|
262
|
+
return typeof service === "function" ? service(name) : console;
|
|
263
|
+
}
|
|
264
|
+
/** 本连接器的路由 provider slug(docs/exchange-routing.md §2.2:路由层词汇,非包名)。 */
|
|
265
|
+
const ROUTER_PROVIDER = "binance";
|
|
266
|
+
/**
|
|
267
|
+
* 路由裁决(2026-08-29 设置驱动重构):router 存在且给本市场选了别的 provider →
|
|
268
|
+
* 本连接器静默退出(设置是权威);router 不存在(老部署)→ 回退 enabled 语义。
|
|
269
|
+
* 返回 true = 应继续 apply。
|
|
270
|
+
*/
|
|
271
|
+
function routeAllows(ctx, config, market) {
|
|
272
|
+
if (!config.enabled) return false;
|
|
273
|
+
const router = ctx.get?.("tradingMarketRouter", false);
|
|
274
|
+
if (router === void 0) return true;
|
|
275
|
+
return router.activeProvider(market) === ROUTER_PROVIDER;
|
|
276
|
+
}
|
|
277
|
+
function apply(ctx, config) {
|
|
278
|
+
const log = logger(ctx);
|
|
279
|
+
if (!config.enabled) {
|
|
280
|
+
log.info("[dsh-trading-crypto-connector-binance] not activated (enabled=false) — tradingCryptoMarketData and crypto_* tools stay unregistered; market data comes from the other active connector");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const router = ctx.get?.("tradingMarketRouter", false);
|
|
284
|
+
const active = router?.activeProvider("crypto");
|
|
285
|
+
if (router !== void 0 && active !== "binance") {
|
|
286
|
+
log.info("[dsh-trading-crypto-connector-binance] market router selects %s for crypto — not activated; set dshtrading.markets.crypto.provider to binance to use this connector", String(active ?? "(unset)"));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
new BinanceMarketDataService(ctx);
|
|
290
|
+
ctx.inject(["tradingCryptoMarketData"], (ctx) => {
|
|
291
|
+
const marketData = ctx.tradingCryptoMarketData;
|
|
292
|
+
const register = (tool) => registerTool(ctx, tool, log);
|
|
293
|
+
register(createGetIndicatorsTool({
|
|
294
|
+
marketData,
|
|
295
|
+
providerLabel: "binance"
|
|
296
|
+
}));
|
|
297
|
+
register(defineTool({
|
|
298
|
+
name: "crypto_get_ticker",
|
|
299
|
+
description: "Get the latest public ticker (last price, bid/ask, 24h volume) for a crypto symbol via the Binance public REST API. No credentials required.",
|
|
300
|
+
parameters: { symbol: {
|
|
301
|
+
type: "string",
|
|
302
|
+
required: true,
|
|
303
|
+
description: "Trading pair symbol, e.g. BTCUSDT"
|
|
304
|
+
} },
|
|
305
|
+
output: {
|
|
306
|
+
schema: { type: "string" },
|
|
307
|
+
render: (_args, value) => [{
|
|
308
|
+
type: "text",
|
|
309
|
+
text: value
|
|
310
|
+
}]
|
|
311
|
+
},
|
|
312
|
+
async execute(args) {
|
|
313
|
+
const ticker = await marketData.getTicker(args.symbol);
|
|
314
|
+
return JSON.stringify(ticker);
|
|
315
|
+
}
|
|
316
|
+
}));
|
|
317
|
+
register(defineTool({
|
|
318
|
+
name: "crypto_get_klines",
|
|
319
|
+
description: "Get recent public klines (candles: open/high/low/close/volume) for a crypto symbol via the Binance public REST API. No credentials required.",
|
|
320
|
+
parameters: {
|
|
321
|
+
symbol: {
|
|
322
|
+
type: "string",
|
|
323
|
+
required: true,
|
|
324
|
+
description: "Trading pair symbol, e.g. BTCUSDT"
|
|
325
|
+
},
|
|
326
|
+
interval: {
|
|
327
|
+
type: "string",
|
|
328
|
+
enum: INTERVAL_VOCABULARY,
|
|
329
|
+
description: "Kline interval (Binance vocabulary)",
|
|
330
|
+
default: "1h"
|
|
331
|
+
},
|
|
332
|
+
limit: {
|
|
333
|
+
type: "integer",
|
|
334
|
+
description: "Number of candles to return (1-1000)",
|
|
335
|
+
default: 100
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
output: {
|
|
339
|
+
schema: { type: "string" },
|
|
340
|
+
render: (_args, value) => [{
|
|
341
|
+
type: "text",
|
|
342
|
+
text: value
|
|
343
|
+
}]
|
|
344
|
+
},
|
|
345
|
+
async execute(args) {
|
|
346
|
+
const interval = args.interval ?? "1h";
|
|
347
|
+
const limit = args.limit ?? 100;
|
|
348
|
+
const klines = await marketData.getKlines(args.symbol, interval, limit);
|
|
349
|
+
return JSON.stringify(klines);
|
|
350
|
+
}
|
|
351
|
+
}));
|
|
352
|
+
register(createPlaceOrderTool({
|
|
353
|
+
marketData,
|
|
354
|
+
config
|
|
355
|
+
}));
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
//#endregion
|
|
359
|
+
export { BinanceMarketDataService, BinanceRestClient, Config, INTERVAL_VOCABULARY, ROUTER_PROVIDER, TRADING_CRYPTO_MARKET_DATA_KEY, TradingServiceError, apply, buildDryRunReceipt, createPlaceOrderTool, evaluateOrderGate, inject, name, normalizeBinanceFuturesSymbol, routeAllows };
|
package/lib/rest.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { DerivativesPoint, Interval, Kline, Orderbook, Ticker, TradeTick, TradingErrorCode } from "@dshtrading/api";
|
|
2
|
+
//#region src/rest.d.ts
|
|
3
|
+
/** api 包 TradingError 契约的运行时 Error 实现。 */
|
|
4
|
+
declare class TradingServiceError extends Error {
|
|
5
|
+
readonly code: TradingErrorCode;
|
|
6
|
+
constructor(code: TradingErrorCode, message: string, cause?: unknown);
|
|
7
|
+
}
|
|
8
|
+
interface BinanceRestOptions {
|
|
9
|
+
/** 覆盖 API base(测试/反代用),末尾不带斜杠。 */
|
|
10
|
+
readonly baseUrl?: string;
|
|
11
|
+
/** 覆盖 USDT-M 合约(fapi)base(测试/反代用),末尾不带斜杠。 */
|
|
12
|
+
readonly fapiBaseUrl?: string;
|
|
13
|
+
/** 单请求超时(ms),默认 10s。 */
|
|
14
|
+
readonly timeoutMs?: number;
|
|
15
|
+
/** 注入 fetch 实现;缺省用全局 fetch(Node 22+ 内置)。 */
|
|
16
|
+
readonly fetchImpl?: typeof fetch;
|
|
17
|
+
}
|
|
18
|
+
/** 全量 interval 词汇,供工具 parameters 的 enum 使用。 */
|
|
19
|
+
declare const INTERVAL_VOCABULARY: readonly string[];
|
|
20
|
+
/**
|
|
21
|
+
* 衍生品输入归一(issue #38,与 kit-crypto/derivatives 同词汇):规范现货、规范 SWAP、
|
|
22
|
+
* OKX 原生 SWAP 形一律归一到 fapi 词汇——BTCUSDT / BTCUSDT-SWAP / BTC-USDT-SWAP → BTCUSDT。
|
|
23
|
+
*/
|
|
24
|
+
declare function normalizeBinanceFuturesSymbol(raw: string): string;
|
|
25
|
+
declare class BinanceRestClient {
|
|
26
|
+
#private;
|
|
27
|
+
constructor(options?: BinanceRestOptions);
|
|
28
|
+
/**
|
|
29
|
+
* 最新行情:/api/v3/ticker/24hr 提供最新价与 24h 量,/api/v3/ticker/bookTicker 补充 bid/ask。
|
|
30
|
+
*/
|
|
31
|
+
getTicker(symbol: string): Promise<Ticker>;
|
|
32
|
+
getKlines(symbol: string, interval: Interval, limit?: number): Promise<Kline[]>;
|
|
33
|
+
/**
|
|
34
|
+
* 全部可交易现货标的名册(GET /api/v3/exchangeInfo,status=TRADING 过滤,Issue #15)。
|
|
35
|
+
* 输出 symbol 为规范形(BTCUSDT),name 为 baseAsset/quoteAsset(如 BTC/USDT)。
|
|
36
|
+
*/
|
|
37
|
+
listInstruments(): Promise<Array<{
|
|
38
|
+
symbol: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
}>>;
|
|
41
|
+
/** 盘口快照:GET /api/v3/depth(limit=20 档;bids 降序 / asks 升序,Binance 原生序)。 */
|
|
42
|
+
getOrderbook(symbol: string): Promise<Orderbook>;
|
|
43
|
+
/** 最近逐笔成交:GET /api/v3/trades(时间升序;isBuyerMaker=true → 主动卖)。 */
|
|
44
|
+
getRecentTrades(symbol: string, limit?: number): Promise<TradeTick[]>;
|
|
45
|
+
/** 未平仓合约量:GET /fapi/v1/openInterest(openInterest 以 base 币计,time=快照 ms)。 */
|
|
46
|
+
getFuturesOpenInterest(symbol: string): Promise<{
|
|
47
|
+
openInterest: number;
|
|
48
|
+
time: number;
|
|
49
|
+
}>;
|
|
50
|
+
/** 最新资金费率:GET /fapi/v1/fundingRate?limit=1([{ fundingRate, fundingTime }],费率为小数)。 */
|
|
51
|
+
getFuturesFundingRate(symbol: string): Promise<{
|
|
52
|
+
fundingRate: number;
|
|
53
|
+
fundingTime: number;
|
|
54
|
+
}>;
|
|
55
|
+
/** 多空持仓人数比族:GET /futures/data/{globalLongShortAccountRatio|topLongShortPositionRatio}(period=1h,limit=1)。 */
|
|
56
|
+
getFuturesLongShortRatio(kind: 'global' | 'top', symbol: string): Promise<number>;
|
|
57
|
+
/** 主动买卖量比:GET /futures/data/takerlongshortRatio(period=1h,limit=1,buySellRatio=买/卖)。 */
|
|
58
|
+
getFuturesTakerRatio(symbol: string): Promise<number>;
|
|
59
|
+
/**
|
|
60
|
+
* 标记/指数价格与下期结算:GET /fapi/v1/premiumIndex —— 单端点同时携带
|
|
61
|
+
* markPrice / indexPrice / lastFundingRate / nextFundingTime
|
|
62
|
+
* (2026-09-03 真实网络实证,spikes/impl-crypto-derivatives)。
|
|
63
|
+
*/
|
|
64
|
+
getFuturesPremiumIndex(symbol: string): Promise<{
|
|
65
|
+
markPrice?: number;
|
|
66
|
+
indexPrice?: number;
|
|
67
|
+
lastFundingRate?: number;
|
|
68
|
+
nextFundingTime?: number;
|
|
69
|
+
time: number;
|
|
70
|
+
}>;
|
|
71
|
+
/** 资金费率历史:GET /fapi/v1/fundingRate(时间升序;value 为小数费率)。 */
|
|
72
|
+
getFuturesFundingRateHistory(symbol: string, limit?: number): Promise<DerivativesPoint[]>;
|
|
73
|
+
/**
|
|
74
|
+
* OI 历史:GET /futures/data/openInterestHist(period=1d;sumOpenInterest 为 base 币数,
|
|
75
|
+
* 与快照 openInterest 同语义。2026-09-03 真实网络实证)。响应时间升序。
|
|
76
|
+
*/
|
|
77
|
+
getFuturesOpenInterestHistory(symbol: string, limit?: number): Promise<DerivativesPoint[]>;
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { BinanceRestClient, BinanceRestOptions, INTERVAL_VOCABULARY, TradingServiceError, normalizeBinanceFuturesSymbol };
|
package/lib/rest.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
//#region src/rest.ts
|
|
2
|
+
/** api 包 TradingError 契约的运行时 Error 实现。 */
|
|
3
|
+
var TradingServiceError = class extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message, cause) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "TradingServiceError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
if (cause !== void 0) this.cause = cause;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_BASE_URL = "https://api.binance.com";
|
|
13
|
+
const DEFAULT_FAPI_BASE_URL = "https://fapi.binance.com";
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
15
|
+
/** Binance klines 支持的 interval 词汇(与 api 包 Interval 对齐)。 */
|
|
16
|
+
const INTERVALS = [
|
|
17
|
+
"1m",
|
|
18
|
+
"3m",
|
|
19
|
+
"5m",
|
|
20
|
+
"15m",
|
|
21
|
+
"30m",
|
|
22
|
+
"1h",
|
|
23
|
+
"2h",
|
|
24
|
+
"4h",
|
|
25
|
+
"6h",
|
|
26
|
+
"8h",
|
|
27
|
+
"12h",
|
|
28
|
+
"1d",
|
|
29
|
+
"3d",
|
|
30
|
+
"1w",
|
|
31
|
+
"1M"
|
|
32
|
+
];
|
|
33
|
+
/** 全量 interval 词汇,供工具 parameters 的 enum 使用。 */
|
|
34
|
+
const INTERVAL_VOCABULARY = INTERVALS;
|
|
35
|
+
function isInterval(value) {
|
|
36
|
+
return typeof value === "string" && INTERVALS.includes(value);
|
|
37
|
+
}
|
|
38
|
+
/** 校验并规范化 symbol(Binance 现货符号为大写无分隔,如 BTCUSDT)。 */
|
|
39
|
+
function requireSymbol(symbol) {
|
|
40
|
+
if (typeof symbol !== "string" || !symbol.trim()) throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", "Symbol must be a non-empty string, e.g. BTCUSDT");
|
|
41
|
+
return symbol.trim().toUpperCase();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 衍生品输入归一(issue #38,与 kit-crypto/derivatives 同词汇):规范现货、规范 SWAP、
|
|
45
|
+
* OKX 原生 SWAP 形一律归一到 fapi 词汇——BTCUSDT / BTCUSDT-SWAP / BTC-USDT-SWAP → BTCUSDT。
|
|
46
|
+
*/
|
|
47
|
+
function normalizeBinanceFuturesSymbol(raw) {
|
|
48
|
+
const clean = requireSymbol(raw).replace(/[-_]/g, "");
|
|
49
|
+
return clean.endsWith("SWAP") ? clean.slice(0, -4) : clean;
|
|
50
|
+
}
|
|
51
|
+
/** Binance 返回数值均为字符串,宽松转 number(非有限值返回 undefined)。 */
|
|
52
|
+
function num(value) {
|
|
53
|
+
if (value === "") return void 0;
|
|
54
|
+
const n = typeof value === "string" ? Number(value) : typeof value === "number" ? value : NaN;
|
|
55
|
+
return Number.isFinite(n) ? n : void 0;
|
|
56
|
+
}
|
|
57
|
+
/** Binance 错误码 → api 词汇的已知映射(-1121 Invalid symbol / -1120 Invalid period)。 */
|
|
58
|
+
const BINANCE_CODE_MAP = /* @__PURE__ */ new Map([[-1121, "TRADING_UNSUPPORTED_SYMBOL"], [-1120, "TRADING_UNSUPPORTED_INTERVAL"]]);
|
|
59
|
+
async function httpToTradingError(res, path) {
|
|
60
|
+
let body;
|
|
61
|
+
try {
|
|
62
|
+
body = await res.json();
|
|
63
|
+
} catch {
|
|
64
|
+
body = void 0;
|
|
65
|
+
}
|
|
66
|
+
const err = body ?? {};
|
|
67
|
+
const binanceCode = typeof err.code === "number" ? err.code : void 0;
|
|
68
|
+
const binanceMsg = typeof err.msg === "string" ? err.msg : "";
|
|
69
|
+
let code;
|
|
70
|
+
if (res.status === 429 || res.status === 418) code = "TRADING_RATE_LIMITED";
|
|
71
|
+
else if (res.status === 401 || res.status === 403) code = "TRADING_AUTH_FAILED";
|
|
72
|
+
else if (binanceCode !== void 0 && BINANCE_CODE_MAP.has(binanceCode)) code = BINANCE_CODE_MAP.get(binanceCode);
|
|
73
|
+
else code = "TRADING_EXCHANGE_ERROR";
|
|
74
|
+
const detail = [
|
|
75
|
+
res.status,
|
|
76
|
+
binanceCode !== void 0 ? `code=${binanceCode}` : void 0,
|
|
77
|
+
binanceMsg || res.statusText
|
|
78
|
+
].filter((part) => part !== void 0 && part !== "").join(" ");
|
|
79
|
+
return new TradingServiceError(code, `Binance ${path}: ${detail}`);
|
|
80
|
+
}
|
|
81
|
+
/** K 线响应行:[openTime, open, high, low, close, volume, closeTime, ...]。 */
|
|
82
|
+
function parseKlineRow(row, symbol) {
|
|
83
|
+
if (!Array.isArray(row) || row.length < 7) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance klines: malformed row for ${symbol}`);
|
|
84
|
+
const openTime = num(row[0]);
|
|
85
|
+
const open = num(row[1]);
|
|
86
|
+
const high = num(row[2]);
|
|
87
|
+
const low = num(row[3]);
|
|
88
|
+
const close = num(row[4]);
|
|
89
|
+
const volume = num(row[5]);
|
|
90
|
+
const closeTime = num(row[6]);
|
|
91
|
+
if (openTime === void 0 || open === void 0 || high === void 0 || low === void 0 || close === void 0 || volume === void 0 || closeTime === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance klines: malformed row values for ${symbol}`);
|
|
92
|
+
return {
|
|
93
|
+
openTime,
|
|
94
|
+
open,
|
|
95
|
+
high,
|
|
96
|
+
low,
|
|
97
|
+
close,
|
|
98
|
+
volume,
|
|
99
|
+
closeTime
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** depth 档位行 [price, quantity] → OrderbookLevel(字符串数值,宽容解析)。 */
|
|
103
|
+
function parseDepthLevel(row) {
|
|
104
|
+
if (!Array.isArray(row) || row.length < 2) return void 0;
|
|
105
|
+
const price = num(row[0]);
|
|
106
|
+
const amount = num(row[1]);
|
|
107
|
+
if (price === void 0 || amount === void 0 || price <= 0 || amount <= 0) return void 0;
|
|
108
|
+
return {
|
|
109
|
+
price,
|
|
110
|
+
amount
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** depth 响应(bids 降序 / asks 升序,Binance 原生序)→ Orderbook。 */
|
|
114
|
+
function parseDepthBody(body, symbol) {
|
|
115
|
+
const d = body;
|
|
116
|
+
if (!Array.isArray(d?.bids) || !Array.isArray(d?.asks)) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance depth for ${symbol}: unexpected response shape`);
|
|
117
|
+
return {
|
|
118
|
+
symbol,
|
|
119
|
+
bids: d.bids.map(parseDepthLevel).filter((l) => l !== void 0),
|
|
120
|
+
asks: d.asks.map(parseDepthLevel).filter((l) => l !== void 0),
|
|
121
|
+
timestamp: Date.now()
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** 逐笔成交行(/api/v3/trades)→ TradeTick(isBuyerMaker=true → taker 是卖方)。 */
|
|
125
|
+
function parseTradeRow(row, symbol) {
|
|
126
|
+
const d = row;
|
|
127
|
+
const price = num(d.price);
|
|
128
|
+
const amount = num(d.qty);
|
|
129
|
+
if (price === void 0 || amount === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance trades for ${symbol}: malformed trade row`);
|
|
130
|
+
return {
|
|
131
|
+
id: String(d.id ?? ""),
|
|
132
|
+
symbol,
|
|
133
|
+
price,
|
|
134
|
+
amount,
|
|
135
|
+
side: d.isBuyerMaker === true ? "sell" : d.isBuyerMaker === false ? "buy" : "unknown",
|
|
136
|
+
timestamp: num(d.time) ?? Date.now()
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
var BinanceRestClient = class {
|
|
140
|
+
#baseUrl;
|
|
141
|
+
#fapiBaseUrl;
|
|
142
|
+
#timeoutMs;
|
|
143
|
+
#fetchImpl;
|
|
144
|
+
constructor(options = {}) {
|
|
145
|
+
this.#baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
146
|
+
this.#fapiBaseUrl = options.fapiBaseUrl ?? DEFAULT_FAPI_BASE_URL;
|
|
147
|
+
this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
148
|
+
this.#fetchImpl = options.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
|
|
149
|
+
}
|
|
150
|
+
async #request(path, params, base = this.#baseUrl) {
|
|
151
|
+
const query = new URLSearchParams(params).toString();
|
|
152
|
+
const target = query ? `${base}${path}?${query}` : `${base}${path}`;
|
|
153
|
+
const controller = new AbortController();
|
|
154
|
+
const timer = setTimeout(() => controller.abort(new DOMException(`request timed out after ${this.#timeoutMs}ms`, "TimeoutError")), this.#timeoutMs);
|
|
155
|
+
let res;
|
|
156
|
+
try {
|
|
157
|
+
res = await this.#fetchImpl(target, { signal: controller.signal });
|
|
158
|
+
} catch (cause) {
|
|
159
|
+
const timedOut = controller.signal.aborted;
|
|
160
|
+
throw new TradingServiceError("TRADING_NETWORK", timedOut ? `Binance ${path}: request timed out after ${this.#timeoutMs}ms` : `Binance ${path}: network error`, cause);
|
|
161
|
+
} finally {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
}
|
|
164
|
+
if (!res.ok) throw await httpToTradingError(res, path);
|
|
165
|
+
try {
|
|
166
|
+
return await res.json();
|
|
167
|
+
} catch (cause) {
|
|
168
|
+
throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance ${path}: invalid JSON response`, cause);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 最新行情:/api/v3/ticker/24hr 提供最新价与 24h 量,/api/v3/ticker/bookTicker 补充 bid/ask。
|
|
173
|
+
*/
|
|
174
|
+
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 })]);
|
|
177
|
+
const d = day;
|
|
178
|
+
const b = book;
|
|
179
|
+
const price = num(d.lastPrice);
|
|
180
|
+
if (price === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance ticker for ${sym}: missing/invalid lastPrice`);
|
|
181
|
+
const bid = num(b.bidPrice);
|
|
182
|
+
const ask = num(b.askPrice);
|
|
183
|
+
const volume = num(d.volume);
|
|
184
|
+
const prevClose = num(d.prevClosePrice);
|
|
185
|
+
const changePercent = num(d.priceChangePercent);
|
|
186
|
+
return {
|
|
187
|
+
symbol: typeof d.symbol === "string" && d.symbol ? d.symbol : sym,
|
|
188
|
+
price,
|
|
189
|
+
timestamp: Date.now(),
|
|
190
|
+
...bid !== void 0 ? { bid } : {},
|
|
191
|
+
...ask !== void 0 ? { ask } : {},
|
|
192
|
+
...volume !== void 0 ? { volume } : {},
|
|
193
|
+
...prevClose !== void 0 ? { prevClose } : {},
|
|
194
|
+
...changePercent !== void 0 ? { changePercent } : {}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async getKlines(symbol, interval, limit = 100) {
|
|
198
|
+
const sym = requireSymbol(symbol);
|
|
199
|
+
if (!isInterval(interval)) throw new TradingServiceError("TRADING_UNSUPPORTED_INTERVAL", `Binance klines: unsupported interval ${String(interval)}`);
|
|
200
|
+
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,
|
|
203
|
+
interval,
|
|
204
|
+
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));
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 全部可交易现货标的名册(GET /api/v3/exchangeInfo,status=TRADING 过滤,Issue #15)。
|
|
211
|
+
* 输出 symbol 为规范形(BTCUSDT),name 为 baseAsset/quoteAsset(如 BTC/USDT)。
|
|
212
|
+
*/
|
|
213
|
+
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");
|
|
216
|
+
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;
|
|
219
|
+
result.push({
|
|
220
|
+
symbol: item.symbol,
|
|
221
|
+
...name ? { name } : {}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
/** 盘口快照:GET /api/v3/depth(limit=20 档;bids 降序 / asks 升序,Binance 原生序)。 */
|
|
227
|
+
async getOrderbook(symbol) {
|
|
228
|
+
const sym = normalizeBinanceFuturesSymbol(symbol);
|
|
229
|
+
return parseDepthBody(await this.#request("/api/v3/depth", {
|
|
230
|
+
symbol: sym,
|
|
231
|
+
limit: "20"
|
|
232
|
+
}), sym);
|
|
233
|
+
}
|
|
234
|
+
/** 最近逐笔成交:GET /api/v3/trades(时间升序;isBuyerMaker=true → 主动卖)。 */
|
|
235
|
+
async getRecentTrades(symbol, limit = 50) {
|
|
236
|
+
const sym = normalizeBinanceFuturesSymbol(symbol);
|
|
237
|
+
const capped = Math.max(1, Math.min(Math.floor(limit) || 50, 100));
|
|
238
|
+
const body = await this.#request("/api/v3/trades", {
|
|
239
|
+
symbol: sym,
|
|
240
|
+
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));
|
|
244
|
+
}
|
|
245
|
+
/** 未平仓合约量:GET /fapi/v1/openInterest(openInterest 以 base 币计,time=快照 ms)。 */
|
|
246
|
+
async getFuturesOpenInterest(symbol) {
|
|
247
|
+
const sym = normalizeBinanceFuturesSymbol(symbol);
|
|
248
|
+
const body = await this.#request("/fapi/v1/openInterest", { symbol: sym }, this.#fapiBaseUrl);
|
|
249
|
+
const openInterest = num(body.openInterest);
|
|
250
|
+
if (openInterest === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance futures openInterest for ${sym}: missing/invalid openInterest`);
|
|
251
|
+
return {
|
|
252
|
+
openInterest,
|
|
253
|
+
time: num(body.time) ?? Date.now()
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** 最新资金费率:GET /fapi/v1/fundingRate?limit=1([{ fundingRate, fundingTime }],费率为小数)。 */
|
|
257
|
+
async getFuturesFundingRate(symbol) {
|
|
258
|
+
const sym = normalizeBinanceFuturesSymbol(symbol);
|
|
259
|
+
const body = await this.#request("/fapi/v1/fundingRate", {
|
|
260
|
+
symbol: sym,
|
|
261
|
+
limit: "1"
|
|
262
|
+
}, this.#fapiBaseUrl);
|
|
263
|
+
const row = Array.isArray(body) ? body[0] : void 0;
|
|
264
|
+
const fundingRate = num(row?.fundingRate);
|
|
265
|
+
if (fundingRate === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance futures fundingRate for ${sym}: missing/invalid fundingRate`);
|
|
266
|
+
return {
|
|
267
|
+
fundingRate,
|
|
268
|
+
fundingTime: num(row?.fundingTime) ?? Date.now()
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/** 多空持仓人数比族:GET /futures/data/{globalLongShortAccountRatio|topLongShortPositionRatio}(period=1h,limit=1)。 */
|
|
272
|
+
async getFuturesLongShortRatio(kind, symbol) {
|
|
273
|
+
const sym = normalizeBinanceFuturesSymbol(symbol);
|
|
274
|
+
const path = kind === "global" ? "/futures/data/globalLongShortAccountRatio" : "/futures/data/topLongShortPositionRatio";
|
|
275
|
+
const body = await this.#request(path, {
|
|
276
|
+
symbol: sym,
|
|
277
|
+
period: "1h",
|
|
278
|
+
limit: "1"
|
|
279
|
+
}, this.#fapiBaseUrl);
|
|
280
|
+
const ratio = num((Array.isArray(body) ? body[0] : void 0)?.longShortRatio);
|
|
281
|
+
if (ratio === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance futures ${kind} long/short ratio for ${sym}: missing/invalid longShortRatio`);
|
|
282
|
+
return ratio;
|
|
283
|
+
}
|
|
284
|
+
/** 主动买卖量比:GET /futures/data/takerlongshortRatio(period=1h,limit=1,buySellRatio=买/卖)。 */
|
|
285
|
+
async getFuturesTakerRatio(symbol) {
|
|
286
|
+
const sym = normalizeBinanceFuturesSymbol(symbol);
|
|
287
|
+
const body = await this.#request("/futures/data/takerlongshortRatio", {
|
|
288
|
+
symbol: sym,
|
|
289
|
+
period: "1h",
|
|
290
|
+
limit: "1"
|
|
291
|
+
}, this.#fapiBaseUrl);
|
|
292
|
+
const ratio = num((Array.isArray(body) ? body[0] : void 0)?.buySellRatio);
|
|
293
|
+
if (ratio === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance futures taker ratio for ${sym}: missing/invalid buySellRatio`);
|
|
294
|
+
return ratio;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* 标记/指数价格与下期结算:GET /fapi/v1/premiumIndex —— 单端点同时携带
|
|
298
|
+
* markPrice / indexPrice / lastFundingRate / nextFundingTime
|
|
299
|
+
* (2026-09-03 真实网络实证,spikes/impl-crypto-derivatives)。
|
|
300
|
+
*/
|
|
301
|
+
async getFuturesPremiumIndex(symbol) {
|
|
302
|
+
const sym = normalizeBinanceFuturesSymbol(symbol);
|
|
303
|
+
const body = await this.#request("/fapi/v1/premiumIndex", { symbol: sym }, this.#fapiBaseUrl);
|
|
304
|
+
const time = num(body.time);
|
|
305
|
+
if (time === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance premiumIndex for ${sym}: missing/invalid time`);
|
|
306
|
+
const markPrice = num(body.markPrice);
|
|
307
|
+
const indexPrice = num(body.indexPrice);
|
|
308
|
+
const lastFundingRate = num(body.lastFundingRate);
|
|
309
|
+
const nextFundingTime = num(body.nextFundingTime);
|
|
310
|
+
return {
|
|
311
|
+
...markPrice !== void 0 ? { markPrice } : {},
|
|
312
|
+
...indexPrice !== void 0 ? { indexPrice } : {},
|
|
313
|
+
...lastFundingRate !== void 0 ? { lastFundingRate } : {},
|
|
314
|
+
...nextFundingTime !== void 0 ? { nextFundingTime } : {},
|
|
315
|
+
time
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/** 资金费率历史:GET /fapi/v1/fundingRate(时间升序;value 为小数费率)。 */
|
|
319
|
+
async getFuturesFundingRateHistory(symbol, limit = 30) {
|
|
320
|
+
const sym = normalizeBinanceFuturesSymbol(symbol);
|
|
321
|
+
const capped = Math.max(1, Math.min(Math.floor(limit) || 30, 100));
|
|
322
|
+
const body = await this.#request("/fapi/v1/fundingRate", {
|
|
323
|
+
symbol: sym,
|
|
324
|
+
limit: String(capped)
|
|
325
|
+
}, this.#fapiBaseUrl);
|
|
326
|
+
if (!Array.isArray(body)) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance funding history for ${sym}: unexpected response shape`);
|
|
327
|
+
const points = [];
|
|
328
|
+
for (const row of body) {
|
|
329
|
+
const d = row;
|
|
330
|
+
const time = num(d.fundingTime);
|
|
331
|
+
const value = num(d.fundingRate);
|
|
332
|
+
if (time !== void 0 && value !== void 0) points.push({
|
|
333
|
+
time,
|
|
334
|
+
value
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return points;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* OI 历史:GET /futures/data/openInterestHist(period=1d;sumOpenInterest 为 base 币数,
|
|
341
|
+
* 与快照 openInterest 同语义。2026-09-03 真实网络实证)。响应时间升序。
|
|
342
|
+
*/
|
|
343
|
+
async getFuturesOpenInterestHistory(symbol, limit = 30) {
|
|
344
|
+
const sym = normalizeBinanceFuturesSymbol(symbol);
|
|
345
|
+
const capped = Math.max(1, Math.min(Math.floor(limit) || 30, 30));
|
|
346
|
+
const body = await this.#request("/futures/data/openInterestHist", {
|
|
347
|
+
symbol: sym,
|
|
348
|
+
period: "1d",
|
|
349
|
+
limit: String(capped)
|
|
350
|
+
}, this.#fapiBaseUrl);
|
|
351
|
+
if (!Array.isArray(body)) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Binance OI history for ${sym}: unexpected response shape`);
|
|
352
|
+
const points = [];
|
|
353
|
+
for (const row of body) {
|
|
354
|
+
const d = row;
|
|
355
|
+
const time = num(d.timestamp);
|
|
356
|
+
const value = num(d.sumOpenInterest);
|
|
357
|
+
if (time !== void 0 && value !== void 0) points.push({
|
|
358
|
+
time,
|
|
359
|
+
value
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return points;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
//#endregion
|
|
366
|
+
export { BinanceRestClient, INTERVAL_VOCABULARY, TradingServiceError, normalizeBinanceFuturesSymbol };
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dshtrading/connector-binance",
|
|
3
|
+
"description": "Binance market connector plugin for dsh-trading: public market data over REST+WS implementing the @dshtrading/api contracts",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "./lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json",
|
|
14
|
+
"./dataplane": {
|
|
15
|
+
"types": "./lib/dataplane.d.ts",
|
|
16
|
+
"default": "./lib/dataplane.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib"
|
|
21
|
+
],
|
|
22
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@dshtrading/api": "^0.1.0",
|
|
25
|
+
"@dshtrading/indicators": "^0.1.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@deepseek-ai/cordis": ">=4.0.0",
|
|
29
|
+
"@deepseek-ai/dsh-tools": ">=0.1.2-alpha.1",
|
|
30
|
+
"@deepseek-ai/schemastery": ">=3.18.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"tsdown": "^0.22.0",
|
|
34
|
+
"vitest": "^3.0.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsdown",
|
|
38
|
+
"test": "vitest run"
|
|
39
|
+
}
|
|
40
|
+
}
|