@imbingox/acex 0.2.0 → 0.3.0-beta.1
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 +25 -1
- package/package.json +7 -3
- package/src/adapters/binance/mark-price.ts +3 -3
- package/src/adapters/binance/private-adapter.ts +6 -1
- package/src/adapters/types.ts +1 -0
- package/src/client/runtime.ts +1 -0
- package/src/managers/market-manager.ts +109 -1
- package/src/types/market.ts +31 -1
- package/src/types/order.ts +1 -0
package/README.md
CHANGED
|
@@ -29,7 +29,9 @@ const book = client.market.getL1Book({
|
|
|
29
29
|
exchange: "binance",
|
|
30
30
|
symbol: "BTC/USDT:USDT",
|
|
31
31
|
});
|
|
32
|
+
const books = client.market.getL1Books("BTC/USDT:USDT");
|
|
32
33
|
console.log(`bid=${book?.bidPrice.toFixed()} ask=${book?.askPrice.toFixed()}`);
|
|
34
|
+
console.log(`venues=${books.length}`);
|
|
33
35
|
console.log(`book freshness=${book?.status.freshness}`);
|
|
34
36
|
|
|
35
37
|
await client.market.subscribeFundingRate({
|
|
@@ -41,7 +43,9 @@ const funding = client.market.getFundingRate({
|
|
|
41
43
|
exchange: "binance",
|
|
42
44
|
symbol: "BTC/USDT:USDT",
|
|
43
45
|
});
|
|
46
|
+
const fundingRates = client.market.getFundingRates("BTC/USDT:USDT");
|
|
44
47
|
console.log(`funding=${funding?.fundingRate.toFixed()}`);
|
|
48
|
+
console.log(`funding venues=${fundingRates.length}`);
|
|
45
49
|
|
|
46
50
|
for await (const event of client.market.events.l1BookUpdates({
|
|
47
51
|
exchange: "binance",
|
|
@@ -121,6 +125,26 @@ bun run type-check
|
|
|
121
125
|
bun run test
|
|
122
126
|
```
|
|
123
127
|
|
|
128
|
+
### 测试分层
|
|
129
|
+
|
|
130
|
+
默认 `bun run test` 只运行快速、确定性的本地测试,不访问真实交易所:
|
|
131
|
+
|
|
132
|
+
| 命令 | 覆盖范围 | 是否进入默认 CI |
|
|
133
|
+
|------|----------|----------------|
|
|
134
|
+
| `bun run test:unit` | `tests/unit/`,底层工具和无全局副作用的单元测试 | 是 |
|
|
135
|
+
| `bun run test:integration` | `tests/integration/`,fake REST + fake WebSocket 的 SDK 跨层集成测试 | 是 |
|
|
136
|
+
| `bun run test` | `test:unit` + `test:integration` | 是 |
|
|
137
|
+
| `bun run test:soak` | `tests/soak/`,60 秒级稳定性/连续更新测试 | 否 |
|
|
138
|
+
| `bun run test:all` | 默认快速测试 + soak 测试 | 否 |
|
|
139
|
+
|
|
140
|
+
测试 support 结构:
|
|
141
|
+
|
|
142
|
+
- `tests/support/test-utils.ts`:通用 fake WebSocket、事件等待、Response helper 和全局清理。
|
|
143
|
+
- `tests/support/exchanges/binance.ts`:Binance 专用 REST/WS fixtures 与 installer。
|
|
144
|
+
- 新增交易所时,优先新增 `tests/support/exchanges/<exchange>.ts`,复用通用 helper,避免把交易所 payload 写进通用测试工具。
|
|
145
|
+
|
|
146
|
+
GitHub Actions 的 `CI` workflow 会在 PR 和 `main` push 时运行 lint、type-check、unit、integration;release workflow 继续复用 `bun run test`,不会执行 soak/live。
|
|
147
|
+
|
|
124
148
|
### 真实环境 smoke / soak 脚本
|
|
125
149
|
|
|
126
150
|
不进默认 `bun run test`,单独执行:
|
|
@@ -141,7 +165,7 @@ bun run test:live:order:soak
|
|
|
141
165
|
|
|
142
166
|
覆盖内容:
|
|
143
167
|
|
|
144
|
-
- `market`:`loadMarkets()`、`subscribeL1Book()`、`subscribeFundingRate()`、`getL1Book()` / `getFundingRate()`、对应事件流和可选断线重连(`--disconnect-target funding` 可单独验证资金费率重连)
|
|
168
|
+
- `market`:`loadMarkets()`、`subscribeL1Book()`、`subscribeFundingRate()`、`getL1Book()` / `getL1Books()`、`getFundingRate()` / `getFundingRates()`、对应事件流和可选断线重连(`--disconnect-target funding` 可单独验证资金费率重连)
|
|
145
169
|
- `account`:Binance PAPI UM 账户 bootstrap、余额/仓位/风险投影、private stream 更新和可选重连
|
|
146
170
|
- `order`:open orders bootstrap、`subscribeOrders()`、订单事件投影和可选重连
|
|
147
171
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imbingox/acex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-beta.1",
|
|
4
4
|
"description": "Multi-exchange trading SDK for market data, account, and order management",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,7 +26,7 @@
|
|
|
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",
|
|
@@ -36,7 +36,11 @@
|
|
|
36
36
|
"test:live:order": "bun run scripts/live-order-smoke.ts",
|
|
37
37
|
"test:live:order:smoke": "bun run scripts/live-order-smoke.ts --duration 10",
|
|
38
38
|
"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"
|
|
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",
|
|
40
|
+
"test:unit": "bun test tests/unit",
|
|
41
|
+
"test:integration": "bun test --max-concurrency=1 tests/integration",
|
|
42
|
+
"test:soak": "bun test --max-concurrency=1 tests/soak",
|
|
43
|
+
"test:all": "bun run test && bun run test:soak"
|
|
40
44
|
},
|
|
41
45
|
"devDependencies": {
|
|
42
46
|
"@biomejs/biome": "^2.4.10",
|
|
@@ -43,13 +43,13 @@ interface BinanceMarkPriceMessage {
|
|
|
43
43
|
T?: number;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const
|
|
46
|
+
const BINANCE_USDM_MARKET_WS_BASE_URL = "wss://fstream.binance.com/market/ws";
|
|
47
47
|
const BINANCE_COINM_WS_BASE_URL = "wss://dstream.binance.com/ws";
|
|
48
48
|
|
|
49
49
|
function getWsBaseUrl(market: BinanceMarketDefinition): string {
|
|
50
50
|
switch (market.family) {
|
|
51
51
|
case "usdm":
|
|
52
|
-
return
|
|
52
|
+
return BINANCE_USDM_MARKET_WS_BASE_URL;
|
|
53
53
|
case "coinm":
|
|
54
54
|
return BINANCE_COINM_WS_BASE_URL;
|
|
55
55
|
case "spot":
|
|
@@ -60,7 +60,7 @@ function getWsBaseUrl(market: BinanceMarketDefinition): string {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function buildMarkPriceUrl(market: BinanceMarketDefinition): string {
|
|
63
|
-
return `${getWsBaseUrl(market)}/${market.id.toLowerCase()}@markPrice
|
|
63
|
+
return `${getWsBaseUrl(market)}/${market.id.toLowerCase()}@markPrice`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function parseMarkPriceMessage(
|
|
@@ -551,7 +551,12 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
551
551
|
type: encodeOrderType(request.type),
|
|
552
552
|
quantity: request.amount,
|
|
553
553
|
price: request.price,
|
|
554
|
-
timeInForce:
|
|
554
|
+
timeInForce:
|
|
555
|
+
request.type === "limit"
|
|
556
|
+
? request.postOnly === true
|
|
557
|
+
? "GTX"
|
|
558
|
+
: "GTC"
|
|
559
|
+
: undefined,
|
|
555
560
|
newClientOrderId: request.clientOrderId,
|
|
556
561
|
reduceOnly:
|
|
557
562
|
request.reduceOnly === undefined
|
package/src/adapters/types.ts
CHANGED
package/src/client/runtime.ts
CHANGED
|
@@ -297,6 +297,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
297
297
|
type: input.type,
|
|
298
298
|
amount: input.amount,
|
|
299
299
|
price: input.type === "limit" ? input.price : undefined,
|
|
300
|
+
postOnly: input.type === "limit" ? input.postOnly : undefined,
|
|
300
301
|
clientOrderId: input.clientOrderId,
|
|
301
302
|
reduceOnly: input.reduceOnly,
|
|
302
303
|
positionSide: input.positionSide,
|
|
@@ -31,6 +31,8 @@ import type {
|
|
|
31
31
|
MarketKeyInput,
|
|
32
32
|
MarketManager,
|
|
33
33
|
MarketStatusChangedEvent,
|
|
34
|
+
NormalizedOrderInput,
|
|
35
|
+
NormalizeOrderInputInput,
|
|
34
36
|
SubscribeFundingRateInput,
|
|
35
37
|
SubscribeL1BookInput,
|
|
36
38
|
SubscriptionActivity,
|
|
@@ -91,6 +93,17 @@ function cloneMarketDefinition(definition: MarketDefinition): MarketDefinition {
|
|
|
91
93
|
return { ...definition, raw: { ...definition.raw } };
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
function floorToStep(value: BigNumber, step: BigNumber): BigNumber {
|
|
97
|
+
if (step.isLessThanOrEqualTo(0)) {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
return value.dividedToIntegerBy(step).multipliedBy(step);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeDecimalInput(value: BigNumber): string {
|
|
104
|
+
return value.isFinite() ? value.toFixed() : value.toString();
|
|
105
|
+
}
|
|
106
|
+
|
|
94
107
|
export class MarketManagerImpl
|
|
95
108
|
implements MarketManager, ManagerLifecycle, HealthReporter<MarketDataStatus>
|
|
96
109
|
{
|
|
@@ -224,9 +237,10 @@ export class MarketManagerImpl
|
|
|
224
237
|
return market ? cloneMarketDefinition(market) : undefined;
|
|
225
238
|
}
|
|
226
239
|
|
|
227
|
-
|
|
240
|
+
getMarkets(symbol: string): MarketDefinition[] {
|
|
228
241
|
return [...this.definitions.values()]
|
|
229
242
|
.filter((market) => market.symbol === symbol)
|
|
243
|
+
.sort((left, right) => left.exchange.localeCompare(right.exchange))
|
|
230
244
|
.map((market) => cloneMarketDefinition(market));
|
|
231
245
|
}
|
|
232
246
|
|
|
@@ -240,16 +254,96 @@ export class MarketManagerImpl
|
|
|
240
254
|
.map((market) => cloneMarketDefinition(market));
|
|
241
255
|
}
|
|
242
256
|
|
|
257
|
+
normalizeOrderInput(input: NormalizeOrderInputInput): NormalizedOrderInput {
|
|
258
|
+
const market = this.resolveLoadedMarket(input);
|
|
259
|
+
const rawPrice = new BigNumber(input.price);
|
|
260
|
+
const rawAmount = new BigNumber(input.amount);
|
|
261
|
+
const price = floorToStep(rawPrice, market.priceStep);
|
|
262
|
+
const amount = floorToStep(rawAmount, market.amountStep);
|
|
263
|
+
|
|
264
|
+
const normalized: NormalizedOrderInput = {
|
|
265
|
+
price: normalizeDecimalInput(price),
|
|
266
|
+
amount: normalizeDecimalInput(amount),
|
|
267
|
+
rawPrice: normalizeDecimalInput(rawPrice),
|
|
268
|
+
rawAmount: normalizeDecimalInput(rawAmount),
|
|
269
|
+
adjusted: !price.isEqualTo(rawPrice) || !amount.isEqualTo(rawAmount),
|
|
270
|
+
accepted: true,
|
|
271
|
+
priceStep: market.priceStep.toFixed(),
|
|
272
|
+
amountStep: market.amountStep.toFixed(),
|
|
273
|
+
minAmount: market.minAmount?.toFixed(),
|
|
274
|
+
minNotional: market.minNotional?.toFixed(),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (!price.isFinite() || price.isLessThanOrEqualTo(0)) {
|
|
278
|
+
return {
|
|
279
|
+
...normalized,
|
|
280
|
+
accepted: false,
|
|
281
|
+
rejectReason: "price_not_positive",
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!amount.isFinite() || amount.isLessThanOrEqualTo(0)) {
|
|
286
|
+
return {
|
|
287
|
+
...normalized,
|
|
288
|
+
accepted: false,
|
|
289
|
+
rejectReason: "amount_not_positive",
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (market.minAmount && amount.isLessThan(market.minAmount)) {
|
|
294
|
+
return {
|
|
295
|
+
...normalized,
|
|
296
|
+
accepted: false,
|
|
297
|
+
rejectReason: "amount_below_min",
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (market.minNotional) {
|
|
302
|
+
const notional = amount.multipliedBy(price);
|
|
303
|
+
if (notional.isLessThan(market.minNotional)) {
|
|
304
|
+
return {
|
|
305
|
+
...normalized,
|
|
306
|
+
accepted: false,
|
|
307
|
+
rejectReason: "notional_below_min",
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return normalized;
|
|
313
|
+
}
|
|
314
|
+
|
|
243
315
|
getL1Book(key: MarketKeyInput): L1Book | undefined {
|
|
244
316
|
const book = this.records.get(marketKey(key))?.l1Book;
|
|
245
317
|
return book ? cloneL1Book(book) : undefined;
|
|
246
318
|
}
|
|
247
319
|
|
|
320
|
+
getL1Books(symbol: string): L1Book[] {
|
|
321
|
+
return [...this.records.values()]
|
|
322
|
+
.filter(
|
|
323
|
+
(record): record is MarketRecord & { l1Book: L1Book } =>
|
|
324
|
+
record.symbol === symbol && Boolean(record.l1Book),
|
|
325
|
+
)
|
|
326
|
+
.map((record) => cloneL1Book(record.l1Book))
|
|
327
|
+
.sort((left, right) => left.exchange.localeCompare(right.exchange));
|
|
328
|
+
}
|
|
329
|
+
|
|
248
330
|
getFundingRate(key: MarketKeyInput): FundingRateSnapshot | undefined {
|
|
249
331
|
const fundingRate = this.records.get(marketKey(key))?.fundingRate;
|
|
250
332
|
return fundingRate ? cloneFundingRate(fundingRate) : undefined;
|
|
251
333
|
}
|
|
252
334
|
|
|
335
|
+
getFundingRates(symbol: string): FundingRateSnapshot[] {
|
|
336
|
+
return [...this.records.values()]
|
|
337
|
+
.filter(
|
|
338
|
+
(
|
|
339
|
+
record,
|
|
340
|
+
): record is MarketRecord & { fundingRate: FundingRateSnapshot } =>
|
|
341
|
+
record.symbol === symbol && Boolean(record.fundingRate),
|
|
342
|
+
)
|
|
343
|
+
.map((record) => cloneFundingRate(record.fundingRate))
|
|
344
|
+
.sort((left, right) => left.exchange.localeCompare(right.exchange));
|
|
345
|
+
}
|
|
346
|
+
|
|
253
347
|
getMarketStatus(key: MarketKeyInput): MarketDataStatus | undefined {
|
|
254
348
|
const status = this.records.get(marketKey(key))?.status;
|
|
255
349
|
return status ? cloneMarketStatus(status) : undefined;
|
|
@@ -391,6 +485,20 @@ export class MarketManagerImpl
|
|
|
391
485
|
return market;
|
|
392
486
|
}
|
|
393
487
|
|
|
488
|
+
private resolveLoadedMarket(input: MarketKeyInput): MarketDefinition {
|
|
489
|
+
const market = this.definitions.get(marketKey(input));
|
|
490
|
+
if (!market) {
|
|
491
|
+
throw this.createError(
|
|
492
|
+
"MARKET_NOT_FOUND",
|
|
493
|
+
`Unknown market symbol: ${input.symbol}`,
|
|
494
|
+
{ exchange: input.exchange, symbol: input.symbol },
|
|
495
|
+
"market",
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return market;
|
|
500
|
+
}
|
|
501
|
+
|
|
394
502
|
private assertSupportedExchange(exchange: Exchange): void {
|
|
395
503
|
if (exchange === this.adapter.exchange) {
|
|
396
504
|
return;
|
package/src/types/market.ts
CHANGED
|
@@ -57,6 +57,33 @@ export interface MarketKeyInput {
|
|
|
57
57
|
symbol: string;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
export type DecimalInput = string | number | BigNumber;
|
|
61
|
+
|
|
62
|
+
export type NormalizeOrderInputRejectReason =
|
|
63
|
+
| "price_not_positive"
|
|
64
|
+
| "amount_not_positive"
|
|
65
|
+
| "amount_below_min"
|
|
66
|
+
| "notional_below_min";
|
|
67
|
+
|
|
68
|
+
export interface NormalizeOrderInputInput extends MarketKeyInput {
|
|
69
|
+
price: DecimalInput;
|
|
70
|
+
amount: DecimalInput;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface NormalizedOrderInput {
|
|
74
|
+
price: string;
|
|
75
|
+
amount: string;
|
|
76
|
+
rawPrice: string;
|
|
77
|
+
rawAmount: string;
|
|
78
|
+
adjusted: boolean;
|
|
79
|
+
accepted: boolean;
|
|
80
|
+
rejectReason?: NormalizeOrderInputRejectReason;
|
|
81
|
+
priceStep: string;
|
|
82
|
+
amountStep: string;
|
|
83
|
+
minAmount?: string;
|
|
84
|
+
minNotional?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
60
87
|
export interface SubscribeL1BookInput extends MarketKeyInput {}
|
|
61
88
|
|
|
62
89
|
export interface SubscribeFundingRateInput extends MarketKeyInput {}
|
|
@@ -142,9 +169,12 @@ export interface MarketManager {
|
|
|
142
169
|
unsubscribeFundingRate(input: SubscribeFundingRateInput): Promise<void>;
|
|
143
170
|
|
|
144
171
|
getMarket(exchange: Exchange, symbol: string): MarketDefinition | undefined;
|
|
145
|
-
|
|
172
|
+
getMarkets(symbol: string): MarketDefinition[];
|
|
146
173
|
listMarkets(exchange?: Exchange): MarketDefinition[];
|
|
174
|
+
normalizeOrderInput(input: NormalizeOrderInputInput): NormalizedOrderInput;
|
|
147
175
|
getL1Book(key: MarketKeyInput): L1Book | undefined;
|
|
176
|
+
getL1Books(symbol: string): L1Book[];
|
|
148
177
|
getFundingRate(key: MarketKeyInput): FundingRateSnapshot | undefined;
|
|
178
|
+
getFundingRates(symbol: string): FundingRateSnapshot[];
|
|
149
179
|
getMarketStatus(key: MarketKeyInput): MarketDataStatus | undefined;
|
|
150
180
|
}
|
package/src/types/order.ts
CHANGED