@imbingox/acex 0.3.0-beta.0 → 0.3.0-beta.2
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 +73 -23
- package/package.json +9 -3
- package/src/adapters/binance/adapter.ts +1 -1
- package/src/adapters/binance/market-catalog.ts +2 -2
- package/src/adapters/binance/private-adapter.ts +14 -4
- package/src/adapters/juplend/private-adapter.ts +483 -0
- package/src/adapters/types.ts +27 -4
- package/src/client/context.ts +16 -11
- package/src/client/private-subscription-coordinator.ts +101 -47
- package/src/client/runtime.ts +43 -20
- package/src/errors.ts +1 -1
- package/src/internal/filters.ts +9 -9
- package/src/managers/account-manager.ts +95 -58
- package/src/managers/market-manager.ts +129 -44
- package/src/managers/order-manager.ts +49 -56
- package/src/types/account.ts +30 -10
- package/src/types/client.ts +2 -2
- package/src/types/market.ts +40 -16
- package/src/types/order.ts +8 -7
- package/src/types/shared.ts +43 -7
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`acex` 是一个面向交易场景的 **状态型** 多交易所 SDK。调用方持有一个 `AcexClient`,通过统一的 `market` / `account` / `order` manager 读取最新快照、消费增量事件、执行下单撤单命令;SDK 内部负责本地缓存、ready barrier、websocket 生命周期和自动重连,调用方不需要自己处理。
|
|
4
4
|
|
|
5
|
-
当前 MVP
|
|
5
|
+
当前 MVP 落地 Binance(Spot + USDⓈ-M + COIN-M 行情,PAPI UM 私有链路)以及 Juplend(Jupiter Lend 只读借贷账户视图)。
|
|
6
6
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
@@ -21,12 +21,12 @@ const client = createClient();
|
|
|
21
21
|
await client.start();
|
|
22
22
|
|
|
23
23
|
await client.market.subscribeL1Book({
|
|
24
|
-
|
|
24
|
+
venue: "binance",
|
|
25
25
|
symbol: "BTC/USDT:USDT",
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
const book = client.market.getL1Book({
|
|
29
|
-
|
|
29
|
+
venue: "binance",
|
|
30
30
|
symbol: "BTC/USDT:USDT",
|
|
31
31
|
});
|
|
32
32
|
const books = client.market.getL1Books("BTC/USDT:USDT");
|
|
@@ -35,12 +35,12 @@ console.log(`venues=${books.length}`);
|
|
|
35
35
|
console.log(`book freshness=${book?.status.freshness}`);
|
|
36
36
|
|
|
37
37
|
await client.market.subscribeFundingRate({
|
|
38
|
-
|
|
38
|
+
venue: "binance",
|
|
39
39
|
symbol: "BTC/USDT:USDT",
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
const funding = client.market.getFundingRate({
|
|
43
|
-
|
|
43
|
+
venue: "binance",
|
|
44
44
|
symbol: "BTC/USDT:USDT",
|
|
45
45
|
});
|
|
46
46
|
const fundingRates = client.market.getFundingRates("BTC/USDT:USDT");
|
|
@@ -48,7 +48,7 @@ console.log(`funding=${funding?.fundingRate.toFixed()}`);
|
|
|
48
48
|
console.log(`funding venues=${fundingRates.length}`);
|
|
49
49
|
|
|
50
50
|
for await (const event of client.market.events.l1BookUpdates({
|
|
51
|
-
|
|
51
|
+
venue: "binance",
|
|
52
52
|
symbol: "BTC/USDT:USDT",
|
|
53
53
|
})) {
|
|
54
54
|
console.log(event.snapshot.bidPrice.toFixed());
|
|
@@ -58,42 +58,62 @@ for await (const event of client.market.events.l1BookUpdates({
|
|
|
58
58
|
await client.stop();
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
###
|
|
61
|
+
### 同一个 client 同时使用 Binance + Juplend
|
|
62
|
+
|
|
63
|
+
`createClient({ account: { juplend: { pollIntervalMs } } })` 只是配置 Juplend 账户的 polling 间隔,不代表这个 client 只能注册 Juplend。一个 `AcexClient` 可以同时注册 Binance 交易账户和 Juplend 借贷只读账户,用同一个 `AccountManager` 对比风险值。
|
|
62
64
|
|
|
63
65
|
```ts
|
|
64
|
-
const client = createClient(
|
|
66
|
+
const client = createClient({
|
|
67
|
+
account: {
|
|
68
|
+
juplend: {
|
|
69
|
+
pollIntervalMs: 30_000,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
65
73
|
await client.start();
|
|
66
74
|
|
|
67
75
|
await client.registerAccount({
|
|
68
76
|
accountId: "main-binance",
|
|
69
|
-
|
|
77
|
+
venue: "binance",
|
|
70
78
|
credentials: {
|
|
71
79
|
apiKey: process.env.BINANCE_PAPI_API_KEY,
|
|
72
80
|
secret: process.env.BINANCE_PAPI_SECRET,
|
|
73
81
|
},
|
|
74
82
|
});
|
|
75
83
|
|
|
84
|
+
await client.registerAccount({
|
|
85
|
+
accountId: "jup-loop-a",
|
|
86
|
+
venue: "juplend",
|
|
87
|
+
credentials: {
|
|
88
|
+
apiKey: process.env.JUPITER_API_KEY!,
|
|
89
|
+
},
|
|
90
|
+
options: {
|
|
91
|
+
walletAddress: "<solana-wallet-address>",
|
|
92
|
+
positionId: "<optional-nft-position-id>",
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await client.account.subscribeAccount({ accountId: "jup-loop-a" });
|
|
76
97
|
await client.account.subscribeAccount({ accountId: "main-binance" });
|
|
77
98
|
await client.order.subscribeOrders({ accountId: "main-binance" });
|
|
78
99
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
side: "buy",
|
|
83
|
-
type: "limit",
|
|
84
|
-
price: "71830.6",
|
|
85
|
-
amount: "0.001",
|
|
86
|
-
});
|
|
100
|
+
const binanceRisk = client.account.getRiskSnapshot("main-binance");
|
|
101
|
+
const juplendRisk = client.account.getRiskSnapshot("jup-loop-a");
|
|
102
|
+
const juplendBalances = client.account.getBalances("jup-loop-a");
|
|
87
103
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
for (const balance of juplendBalances) {
|
|
105
|
+
console.log(balance.asset, balance.lending?.netAsset.toFixed());
|
|
106
|
+
}
|
|
107
|
+
console.log({
|
|
108
|
+
binanceRiskRatio: binanceRisk?.riskRatio?.toFixed(),
|
|
109
|
+
juplendRiskRatio: juplendRisk?.riskRatio?.toFixed(),
|
|
92
110
|
});
|
|
93
111
|
|
|
94
112
|
await client.stop();
|
|
95
113
|
```
|
|
96
114
|
|
|
115
|
+
Juplend 使用 Jupiter Portfolio API 读取 Solana 钱包的借贷仓位,不需要私钥,也不支持 supply / borrow / repay / withdraw 等写操作。`accountId` 是你自定义的 SDK 账户名;Solana 钱包地址放在 `options.walletAddress`。如果只想观察某个 Juplend NFT position,可传 `options.positionId`。
|
|
116
|
+
|
|
97
117
|
价格、数量等输出字段统一是 `BigNumber`;`createOrder()` 的 `price` / `amount` 输入仍接受 decimal string。详见手册 [§3 核心概念](./docs/api.md#3-核心概念)。
|
|
98
118
|
|
|
99
119
|
## 核心能力
|
|
@@ -109,8 +129,9 @@ await client.stop();
|
|
|
109
129
|
|
|
110
130
|
## 当前限制
|
|
111
131
|
|
|
112
|
-
-
|
|
113
|
-
-
|
|
132
|
+
- 运行时 market/order 能力只支持 `binance`;`okx` / `bybit` / `gate` 仅类型定义
|
|
133
|
+
- 账户视图支持 Binance PAPI UM 与 Juplend 只读借贷账户
|
|
134
|
+
- Juplend 只读,不支持订单和链上写操作;token 数量来自 USD / oracle price 反算
|
|
114
135
|
- Funding Rate 仅支持 Binance 永续合约,来自 mark price websocket;不支持现货和交割合约
|
|
115
136
|
- `createOrder()` 只支持 `limit` / `market`;条件单、改单、账户级全撤不支持
|
|
116
137
|
- 双向持仓账户下单时必须显式传 `positionSide`
|
|
@@ -125,6 +146,26 @@ bun run type-check
|
|
|
125
146
|
bun run test
|
|
126
147
|
```
|
|
127
148
|
|
|
149
|
+
### 测试分层
|
|
150
|
+
|
|
151
|
+
默认 `bun run test` 只运行快速、确定性的本地测试,不访问真实交易所:
|
|
152
|
+
|
|
153
|
+
| 命令 | 覆盖范围 | 是否进入默认 CI |
|
|
154
|
+
|------|----------|----------------|
|
|
155
|
+
| `bun run test:unit` | `tests/unit/`,底层工具和无全局副作用的单元测试 | 是 |
|
|
156
|
+
| `bun run test:integration` | `tests/integration/`,fake REST + fake WebSocket 的 SDK 跨层集成测试 | 是 |
|
|
157
|
+
| `bun run test` | `test:unit` + `test:integration` | 是 |
|
|
158
|
+
| `bun run test:soak` | `tests/soak/`,60 秒级稳定性/连续更新测试 | 否 |
|
|
159
|
+
| `bun run test:all` | 默认快速测试 + soak 测试 | 否 |
|
|
160
|
+
|
|
161
|
+
测试 support 结构:
|
|
162
|
+
|
|
163
|
+
- `tests/support/test-utils.ts`:通用 fake WebSocket、事件等待、Response helper 和全局清理。
|
|
164
|
+
- `tests/support/exchanges/binance.ts`:Binance 专用 REST/WS fixtures 与 installer。
|
|
165
|
+
- 新增交易所时,优先新增 `tests/support/exchanges/<venue>.ts`,复用通用 helper,避免把交易所 payload 写进通用测试工具。
|
|
166
|
+
|
|
167
|
+
GitHub Actions 的 `CI` workflow 会在 PR 和 `main` push 时运行 lint、type-check、unit、integration;release workflow 继续复用 `bun run test`,不会执行 soak/live。
|
|
168
|
+
|
|
128
169
|
### 真实环境 smoke / soak 脚本
|
|
129
170
|
|
|
130
171
|
不进默认 `bun run test`,单独执行:
|
|
@@ -134,6 +175,7 @@ bun run test:live:market:smoke
|
|
|
134
175
|
bun run test:live:market:soak
|
|
135
176
|
bun run test:live:account:smoke
|
|
136
177
|
bun run test:live:account:soak
|
|
178
|
+
bun run test:live:juplend:smoke
|
|
137
179
|
bun run test:live:order:smoke
|
|
138
180
|
bun run test:live:order:soak
|
|
139
181
|
```
|
|
@@ -147,8 +189,16 @@ bun run test:live:order:soak
|
|
|
147
189
|
|
|
148
190
|
- `market`:`loadMarkets()`、`subscribeL1Book()`、`subscribeFundingRate()`、`getL1Book()` / `getL1Books()`、`getFundingRate()` / `getFundingRates()`、对应事件流和可选断线重连(`--disconnect-target funding` 可单独验证资金费率重连)
|
|
149
191
|
- `account`:Binance PAPI UM 账户 bootstrap、余额/仓位/风险投影、private stream 更新和可选重连
|
|
192
|
+
- `juplend`:Jupiter Portfolio API / vault 元数据连通性、lending balance facet、账户级 `riskRatio`、可选 `--position-id` 过滤单个 NFT position
|
|
150
193
|
- `order`:open orders bootstrap、`subscribeOrders()`、订单事件投影和可选重连
|
|
151
194
|
|
|
195
|
+
Juplend live smoke 示例:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
JUPITER_API_KEY=... JUPLEND_WALLET_ADDRESS=<wallet> bun run test:live:juplend -- --show-amounts
|
|
199
|
+
JUPITER_API_KEY=... bun run test:live:juplend -- --account-id jup-loop-a --wallet-address <wallet> --position-id <nftId> --show-amounts
|
|
200
|
+
```
|
|
201
|
+
|
|
152
202
|
### 发布流程
|
|
153
203
|
|
|
154
204
|
仓库使用 **Changesets + GitHub Actions + npm Trusted Publishing**:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imbingox/acex",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.2",
|
|
4
4
|
"description": "Multi-exchange trading SDK for market data, account, and order management",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,17 +26,23 @@
|
|
|
26
26
|
"lint:fix": "biome check --write .",
|
|
27
27
|
"release": "changeset publish",
|
|
28
28
|
"type-check": "tsc --noEmit",
|
|
29
|
-
"test": "bun test --max-concurrency=1",
|
|
29
|
+
"test": "bun test --max-concurrency=1 tests/unit tests/integration",
|
|
30
30
|
"test:live:account": "bun run scripts/live-account-smoke.ts",
|
|
31
31
|
"test:live:account:smoke": "bun run scripts/live-account-smoke.ts --duration 10",
|
|
32
32
|
"test:live:account:soak": "bun run scripts/live-account-smoke.ts --duration 60 --disconnect-after 5",
|
|
33
|
+
"test:live:juplend": "bun run scripts/live-juplend-account-smoke.ts",
|
|
34
|
+
"test:live:juplend:smoke": "bun run scripts/live-juplend-account-smoke.ts --duration 35 --show-amounts",
|
|
33
35
|
"test:live:market": "bun run scripts/live-market-smoke.ts",
|
|
34
36
|
"test:live:market:smoke": "bun run scripts/live-market-smoke.ts --duration 10",
|
|
35
37
|
"test:live:market:soak": "bun run scripts/live-market-smoke.ts --duration 60 --disconnect-after 5 --disconnect-target perp",
|
|
36
38
|
"test:live:order": "bun run scripts/live-order-smoke.ts",
|
|
37
39
|
"test:live:order:smoke": "bun run scripts/live-order-smoke.ts --duration 10",
|
|
38
40
|
"test:live:order:soak": "bun run scripts/live-order-smoke.ts --duration 60 --disconnect-after 5",
|
|
39
|
-
"version-packages": "changeset version && files=\"package.json\"; if [ -f .changeset/pre.json ]; then files=\"$files .changeset/pre.json\"; fi; if [ -f CHANGELOG.md ]; then files=\"$files CHANGELOG.md\"; fi; biome check --write $files"
|
|
41
|
+
"version-packages": "changeset version && files=\"package.json\"; if [ -f .changeset/pre.json ]; then files=\"$files .changeset/pre.json\"; fi; if [ -f CHANGELOG.md ]; then files=\"$files CHANGELOG.md\"; fi; biome check --write $files",
|
|
42
|
+
"test:unit": "bun test tests/unit",
|
|
43
|
+
"test:integration": "bun test --max-concurrency=1 tests/integration",
|
|
44
|
+
"test:soak": "bun test --max-concurrency=1 tests/soak",
|
|
45
|
+
"test:all": "bun run test && bun run test:soak"
|
|
40
46
|
},
|
|
41
47
|
"devDependencies": {
|
|
42
48
|
"@biomejs/biome": "^2.4.10",
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "./market-catalog.ts";
|
|
16
16
|
|
|
17
17
|
export class BinanceMarketAdapter implements MarketAdapter {
|
|
18
|
-
readonly
|
|
18
|
+
readonly venue = "binance" as const;
|
|
19
19
|
|
|
20
20
|
private readonly definitions = new Map<string, BinanceMarketDefinition>();
|
|
21
21
|
|
|
@@ -135,7 +135,7 @@ function normalizeSpotSymbol(
|
|
|
135
135
|
const notionalValue = notionalFilter?.minNotional ?? notionalFilter?.notional;
|
|
136
136
|
|
|
137
137
|
return {
|
|
138
|
-
|
|
138
|
+
venue: "binance",
|
|
139
139
|
family: "spot",
|
|
140
140
|
symbol: `${symbol.baseAsset}/${symbol.quoteAsset}`,
|
|
141
141
|
id: symbol.symbol,
|
|
@@ -180,7 +180,7 @@ function normalizeDerivativesSymbol(
|
|
|
180
180
|
const notionalValue = notionalFilter?.minNotional ?? notionalFilter?.notional;
|
|
181
181
|
|
|
182
182
|
return {
|
|
183
|
-
|
|
183
|
+
venue: "binance",
|
|
184
184
|
family,
|
|
185
185
|
symbol: buildFuturesSymbol(
|
|
186
186
|
symbol.baseAsset,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHmac } from "node:crypto";
|
|
2
|
+
import BigNumber from "bignumber.js";
|
|
2
3
|
import { createManagedWebSocket } from "../../internal/managed-websocket.ts";
|
|
3
4
|
import type { AccountCredentials, PositionSide } from "../../types/index.ts";
|
|
4
5
|
import type {
|
|
@@ -280,9 +281,13 @@ function mapAccountRisk(
|
|
|
280
281
|
input: BinancePapiAccount,
|
|
281
282
|
receivedAt: number,
|
|
282
283
|
): RawRiskUpdate | undefined {
|
|
284
|
+
const uniMmr = firstString(input.uniMMR);
|
|
285
|
+
const riskRatio = uniMmr
|
|
286
|
+
? new BigNumber(1).dividedBy(uniMmr).toString(10)
|
|
287
|
+
: undefined;
|
|
283
288
|
const risk: RawRiskUpdate = {
|
|
284
289
|
equity: firstString(input.accountEquity, input.totalEquity),
|
|
285
|
-
|
|
290
|
+
riskRatio,
|
|
286
291
|
initialMargin: firstString(
|
|
287
292
|
input.accountInitialMargin,
|
|
288
293
|
input.totalInitialMargin,
|
|
@@ -297,7 +302,7 @@ function mapAccountRisk(
|
|
|
297
302
|
|
|
298
303
|
if (
|
|
299
304
|
!risk.equity &&
|
|
300
|
-
!risk.
|
|
305
|
+
!risk.riskRatio &&
|
|
301
306
|
!risk.initialMargin &&
|
|
302
307
|
!risk.maintenanceMargin
|
|
303
308
|
) {
|
|
@@ -473,7 +478,7 @@ async function readJson<T>(response: Response, url: string): Promise<T> {
|
|
|
473
478
|
}
|
|
474
479
|
|
|
475
480
|
export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
476
|
-
readonly
|
|
481
|
+
readonly venue = "binance" as const;
|
|
477
482
|
|
|
478
483
|
async bootstrapAccount(
|
|
479
484
|
credentials: AccountCredentials,
|
|
@@ -551,7 +556,12 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
551
556
|
type: encodeOrderType(request.type),
|
|
552
557
|
quantity: request.amount,
|
|
553
558
|
price: request.price,
|
|
554
|
-
timeInForce:
|
|
559
|
+
timeInForce:
|
|
560
|
+
request.type === "limit"
|
|
561
|
+
? request.postOnly === true
|
|
562
|
+
? "GTX"
|
|
563
|
+
: "GTC"
|
|
564
|
+
: undefined,
|
|
555
565
|
newClientOrderId: request.clientOrderId,
|
|
556
566
|
reduceOnly:
|
|
557
567
|
request.reduceOnly === undefined
|