@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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `acex` 是一个面向交易场景的 **状态型** 多交易所 SDK。调用方持有一个 `AcexClient`,通过统一的 `market` / `account` / `order` manager 读取最新快照、消费增量事件、执行下单撤单命令;SDK 内部负责本地缓存、ready barrier、websocket 生命周期和自动重连,调用方不需要自己处理。
4
4
 
5
- 当前 MVP 只落地 Binance(Spot + USDⓈ-M + COIN-M 行情,PAPI UM 私有链路)。
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
- exchange: "binance",
24
+ venue: "binance",
25
25
  symbol: "BTC/USDT:USDT",
26
26
  });
27
27
 
28
28
  const book = client.market.getL1Book({
29
- exchange: "binance",
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
- exchange: "binance",
38
+ venue: "binance",
39
39
  symbol: "BTC/USDT:USDT",
40
40
  });
41
41
 
42
42
  const funding = client.market.getFundingRate({
43
- exchange: "binance",
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
- exchange: "binance",
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
- exchange: "binance",
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 created = await client.order.createOrder({
80
- accountId: "main-binance",
81
- symbol: "BTC/USDT:USDT",
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
- await client.order.cancelOrder({
89
- accountId: "main-binance",
90
- symbol: "BTC/USDT:USDT",
91
- orderId: created.orderId,
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
- - 运行时只支持 `binance`;`okx` / `bybit` / `gate` 仅类型定义
113
- - 私有链路仅 Binance PAPI UM(统一账户 / Portfolio Margin)
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.0",
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 exchange = "binance" as const;
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
- exchange: "binance",
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
- exchange: "binance",
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
- marginRatio: input.uniMMR,
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.marginRatio &&
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 exchange = "binance" as const;
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: request.type === "limit" ? "GTC" : undefined,
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