@imbingox/acex 0.4.0-beta.16 → 0.4.0-beta.17
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/CHANGELOG.md +6 -0
- package/docs/api.md +32 -5
- package/package.json +1 -1
- package/src/client/runtime.ts +42 -5
- package/src/errors.ts +1 -0
- package/src/internal/async-event-bus.ts +75 -3
- package/src/managers/account-manager.ts +43 -16
- package/src/managers/market-manager.ts +111 -7
- package/src/managers/order-manager.ts +42 -16
- package/src/types/account.ts +9 -2
- package/src/types/client.ts +8 -2
- package/src/types/market.ts +19 -4
- package/src/types/order.ts +9 -2
- package/src/types/shared.ts +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @imbingox/acex
|
|
2
2
|
|
|
3
|
+
## 0.4.0-beta.17
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 35b8163: 事件流新增 `conflate` / `buffer` 与 `maxBuffer` 订阅选项:L1 Book 与 Funding Rate 默认改为 latest-wins,慢消费者只保留同一 `venue:symbol` 的最新事件;market status 事件按 activity/ready/freshness/reason 去重发布;buffer 溢出会丢弃最旧事件并通过 `EVENT_BUFFER_OVERFLOW` runtime error 告警。
|
|
8
|
+
|
|
3
9
|
## 0.4.0-beta.16
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/docs/api.md
CHANGED
|
@@ -372,6 +372,26 @@ console.log(time.serverTime, time.roundTripMs, time.estimatedOffsetMs);
|
|
|
372
372
|
|
|
373
373
|
Funding Rate 当前通过 Binance mark price websocket 更新,仅支持永续合约(`MarketDefinition.type === "swap"`,包括 Binance TradFi Perps)。spot 或 future 订阅会抛 `MARKET_FUNDING_RATE_UNSUPPORTED`。
|
|
374
374
|
|
|
375
|
+
### 5.5 事件流 options
|
|
376
|
+
|
|
377
|
+
Market 事件流支持可选第二参:
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
type EventStreamOptions = {
|
|
381
|
+
mode?: "conflate" | "buffer";
|
|
382
|
+
maxBuffer?: number;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
client.market.events.l1BookUpdates(
|
|
386
|
+
{ venue: "binance", symbol: "BTC/USDT:USDT" },
|
|
387
|
+
{ mode: "buffer", maxBuffer: 50_000 },
|
|
388
|
+
);
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
`l1BookUpdates()` 与 `fundingRateUpdates()` 默认使用 `conflate`,同一 `venue:symbol` 慢消费者只保留最新事件,适合策略热路径。需要录制每个 tick 时显式传 `{ mode: "buffer" }`。`market.events.all()` 与 `market.events.status()` 默认使用 `buffer`;显式传 `{ mode: "conflate" }` 时,`all()` 按 `type:venue:symbol` 合并,`status()` 按 `venue:symbol` 合并。
|
|
392
|
+
|
|
393
|
+
`buffer` 模式默认每个订阅者最多积压 `10_000` 条事件,超过后丢弃最旧事件。每次积压 episode 只会向 `client.events.errors()` 发布一次 `EVENT_BUFFER_OVERFLOW` runtime error,事件 metadata 包含 `stream` 与 `maxBuffer`;队列排空后再次溢出会再次告警。`conflate` 模式天然有界,不使用 `maxBuffer`。
|
|
394
|
+
|
|
375
395
|
## 6. AccountManager
|
|
376
396
|
|
|
377
397
|
```ts
|
|
@@ -400,7 +420,7 @@ Account 事件用于消费余额、仓位、风险或全量快照替换:
|
|
|
400
420
|
```ts
|
|
401
421
|
for await (const event of client.account.events.updates({
|
|
402
422
|
accountId: "main-binance",
|
|
403
|
-
})) {
|
|
423
|
+
}, { maxBuffer: 20_000 })) {
|
|
404
424
|
if (event.type === "risk.updated") {
|
|
405
425
|
console.log(event.snapshot.riskRatio);
|
|
406
426
|
}
|
|
@@ -408,6 +428,8 @@ for await (const event of client.account.events.updates({
|
|
|
408
428
|
}
|
|
409
429
|
```
|
|
410
430
|
|
|
431
|
+
Account 事件流只支持 `{ maxBuffer?: number }`,不提供 conflate;余额、仓位、风险和状态事件默认按 buffer 语义保留顺序。
|
|
432
|
+
|
|
411
433
|
## 7. OrderManager
|
|
412
434
|
|
|
413
435
|
```ts
|
|
@@ -456,7 +478,7 @@ Order 事件用于消费订单状态变化和 open orders 快照校准。Binance
|
|
|
456
478
|
for await (const event of client.order.events.updates({
|
|
457
479
|
accountId: "main-binance",
|
|
458
480
|
symbol: "BTC/USDT:USDT",
|
|
459
|
-
})) {
|
|
481
|
+
}, { maxBuffer: 20_000 })) {
|
|
460
482
|
if (event.type === "order.filled") {
|
|
461
483
|
console.log(event.snapshot.filled);
|
|
462
484
|
}
|
|
@@ -464,23 +486,28 @@ for await (const event of client.order.events.updates({
|
|
|
464
486
|
}
|
|
465
487
|
```
|
|
466
488
|
|
|
489
|
+
Order 事件流只支持 `{ maxBuffer?: number }`,不提供 conflate;订单中间状态和错误恢复信号默认按 buffer 语义保留顺序。
|
|
490
|
+
|
|
467
491
|
## 8. 健康与错误事件
|
|
468
492
|
|
|
469
493
|
```ts
|
|
470
494
|
const health = client.getHealth();
|
|
471
495
|
|
|
472
|
-
for await (const event of client.events.health(
|
|
496
|
+
for await (const event of client.events.health(
|
|
497
|
+
{ venue: "binance" },
|
|
498
|
+
{ maxBuffer: 20_000 },
|
|
499
|
+
)) {
|
|
473
500
|
console.log(event.type);
|
|
474
501
|
break;
|
|
475
502
|
}
|
|
476
503
|
|
|
477
|
-
for await (const error of client.events.errors()) {
|
|
504
|
+
for await (const error of client.events.errors({ maxBuffer: 20_000 })) {
|
|
478
505
|
console.error(error.source, error.error);
|
|
479
506
|
break;
|
|
480
507
|
}
|
|
481
508
|
```
|
|
482
509
|
|
|
483
|
-
`getHealth()` 聚合 client、market、account、order 的当前状态。`events.health(filter)` 只返回满足 filter 的事件;如果事件没有 filter
|
|
510
|
+
`getHealth()` 聚合 client、market、account、order 的当前状态。`events.health(filter, options?)` 只返回满足 filter 的事件;如果事件没有 filter 请求的字段,会被过滤掉。`events.health()` 与 `events.errors()` 只支持 `{ maxBuffer?: number }`,默认 buffer 上限同样是 `10_000`;`errors()` 自身溢出时只丢弃最旧错误事件,不再发布新的 overflow 错误,避免递归。
|
|
484
511
|
|
|
485
512
|
## 9. 数据类型速查
|
|
486
513
|
|
package/package.json
CHANGED
package/src/client/runtime.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
buildAcexErrorDetails,
|
|
16
16
|
type VenueErrorReason,
|
|
17
17
|
} from "../errors.ts";
|
|
18
|
+
import type { AsyncEventBusOverflowInfo } from "../internal/async-event-bus.ts";
|
|
18
19
|
import { AsyncEventBus } from "../internal/async-event-bus.ts";
|
|
19
20
|
import { matchesHealthFilter } from "../internal/filters.ts";
|
|
20
21
|
import { ReactiveRateLimiter } from "../internal/rate-limiter.ts";
|
|
@@ -26,6 +27,7 @@ import type {
|
|
|
26
27
|
AccountManager,
|
|
27
28
|
AcexClient,
|
|
28
29
|
AcexInternalError,
|
|
30
|
+
BufferedEventStreamOptions,
|
|
29
31
|
CancelAllOrdersInput,
|
|
30
32
|
CancelOrderInput,
|
|
31
33
|
ClientEventStreams,
|
|
@@ -75,14 +77,30 @@ class ClientEventStreamsImpl implements ClientEventStreams {
|
|
|
75
77
|
constructor(
|
|
76
78
|
private readonly healthBus: AsyncEventBus<HealthEvent>,
|
|
77
79
|
private readonly errorBus: AsyncEventBus<AcexInternalError>,
|
|
80
|
+
private readonly onHealthOverflow: (
|
|
81
|
+
info: AsyncEventBusOverflowInfo,
|
|
82
|
+
) => void,
|
|
78
83
|
) {}
|
|
79
84
|
|
|
80
|
-
errors(
|
|
81
|
-
|
|
85
|
+
errors(
|
|
86
|
+
options?: BufferedEventStreamOptions,
|
|
87
|
+
): AsyncIterable<AcexInternalError> {
|
|
88
|
+
return this.errorBus.stream(() => true, {
|
|
89
|
+
maxBuffer: options?.maxBuffer,
|
|
90
|
+
});
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
health(
|
|
85
|
-
|
|
93
|
+
health(
|
|
94
|
+
filter?: HealthEventFilter,
|
|
95
|
+
options?: BufferedEventStreamOptions,
|
|
96
|
+
): AsyncIterable<HealthEvent> {
|
|
97
|
+
return this.healthBus.stream(
|
|
98
|
+
(event) => matchesHealthFilter(event, filter),
|
|
99
|
+
{
|
|
100
|
+
maxBuffer: options?.maxBuffer,
|
|
101
|
+
onOverflow: this.onHealthOverflow,
|
|
102
|
+
},
|
|
103
|
+
);
|
|
86
104
|
}
|
|
87
105
|
}
|
|
88
106
|
|
|
@@ -149,7 +167,11 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
149
167
|
this.market = this.marketManager;
|
|
150
168
|
this.account = this.accountManager;
|
|
151
169
|
this.order = this.orderManager;
|
|
152
|
-
this.events = new ClientEventStreamsImpl(
|
|
170
|
+
this.events = new ClientEventStreamsImpl(
|
|
171
|
+
this.healthBus,
|
|
172
|
+
this.errorBus,
|
|
173
|
+
this.createOverflowHandler("client.health"),
|
|
174
|
+
);
|
|
153
175
|
}
|
|
154
176
|
|
|
155
177
|
// --- AcexClient public API ---
|
|
@@ -421,6 +443,21 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
421
443
|
this.healthBus.publish(event);
|
|
422
444
|
}
|
|
423
445
|
|
|
446
|
+
private createOverflowHandler(
|
|
447
|
+
stream: string,
|
|
448
|
+
): (info: AsyncEventBusOverflowInfo) => void {
|
|
449
|
+
return ({ maxBuffer }) => {
|
|
450
|
+
const error = new AcexError(
|
|
451
|
+
"EVENT_BUFFER_OVERFLOW",
|
|
452
|
+
`Event stream buffer overflow: ${stream}`,
|
|
453
|
+
);
|
|
454
|
+
this.publishRuntimeError("runtime", error, {
|
|
455
|
+
stream,
|
|
456
|
+
maxBuffer,
|
|
457
|
+
});
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
424
461
|
// --- Private ---
|
|
425
462
|
|
|
426
463
|
private setClientStatus(status: ClientStatus): void {
|
package/src/errors.ts
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
type EventPredicate<T> = (event: T) => boolean;
|
|
2
2
|
|
|
3
|
+
export type AsyncEventBusStreamMode = "buffer" | "conflate";
|
|
4
|
+
|
|
5
|
+
export interface AsyncEventBusOverflowInfo {
|
|
6
|
+
maxBuffer: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AsyncEventBusStreamOptions<T> {
|
|
10
|
+
mode?: AsyncEventBusStreamMode;
|
|
11
|
+
maxBuffer?: number;
|
|
12
|
+
conflateKey?: (event: T) => string;
|
|
13
|
+
onOverflow?: (info: AsyncEventBusOverflowInfo) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
3
16
|
interface BusListener<T> {
|
|
4
17
|
close(): void;
|
|
5
18
|
dispatch(event: T): void;
|
|
6
19
|
}
|
|
7
20
|
|
|
21
|
+
const DEFAULT_MAX_BUFFER = 10_000;
|
|
22
|
+
|
|
8
23
|
function doneResult<T>(): IteratorResult<T> {
|
|
9
24
|
return { done: true, value: undefined as T };
|
|
10
25
|
}
|
|
@@ -20,11 +35,67 @@ export class AsyncEventBus<T> {
|
|
|
20
35
|
|
|
21
36
|
stream<U extends T = T>(
|
|
22
37
|
filter: ((event: T) => event is U) | EventPredicate<T> = () => true,
|
|
38
|
+
options: AsyncEventBusStreamOptions<U> = {},
|
|
23
39
|
): AsyncIterable<U> {
|
|
24
40
|
let closed = false;
|
|
25
|
-
const
|
|
41
|
+
const mode = options.mode ?? "buffer";
|
|
42
|
+
const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
|
|
43
|
+
const bufferQueue: U[] = [];
|
|
44
|
+
const conflateQueue =
|
|
45
|
+
mode === "conflate" ? new Map<string, U>() : undefined;
|
|
46
|
+
let overflowNotified = false;
|
|
26
47
|
let pendingResolve: ((result: IteratorResult<U>) => void) | undefined;
|
|
27
48
|
|
|
49
|
+
if (mode === "conflate" && !options.conflateKey) {
|
|
50
|
+
throw new Error("AsyncEventBus conflate mode requires conflateKey");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const resetOverflowIfDrained = () => {
|
|
54
|
+
if (bufferQueue.length === 0) {
|
|
55
|
+
overflowNotified = false;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const enqueue = (event: U) => {
|
|
60
|
+
if (conflateQueue) {
|
|
61
|
+
const key = options.conflateKey?.(event);
|
|
62
|
+
if (key === undefined) {
|
|
63
|
+
throw new Error("AsyncEventBus conflate mode requires conflateKey");
|
|
64
|
+
}
|
|
65
|
+
conflateQueue.set(key, event);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
bufferQueue.push(event);
|
|
70
|
+
|
|
71
|
+
if (bufferQueue.length <= maxBuffer) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
bufferQueue.shift();
|
|
76
|
+
if (!overflowNotified) {
|
|
77
|
+
overflowNotified = true;
|
|
78
|
+
options.onOverflow?.({ maxBuffer });
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const dequeue = (): U | undefined => {
|
|
83
|
+
if (conflateQueue) {
|
|
84
|
+
const first = conflateQueue.entries().next();
|
|
85
|
+
if (first.done) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const [key, event] = first.value;
|
|
90
|
+
conflateQueue.delete(key);
|
|
91
|
+
return event;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const event = bufferQueue.shift();
|
|
95
|
+
resetOverflowIfDrained();
|
|
96
|
+
return event;
|
|
97
|
+
};
|
|
98
|
+
|
|
28
99
|
const close = () => {
|
|
29
100
|
if (closed) {
|
|
30
101
|
return;
|
|
@@ -55,7 +126,7 @@ export class AsyncEventBus<T> {
|
|
|
55
126
|
return;
|
|
56
127
|
}
|
|
57
128
|
|
|
58
|
-
|
|
129
|
+
enqueue(typedEvent);
|
|
59
130
|
},
|
|
60
131
|
};
|
|
61
132
|
|
|
@@ -70,11 +141,12 @@ export class AsyncEventBus<T> {
|
|
|
70
141
|
return doneResult<U>();
|
|
71
142
|
}
|
|
72
143
|
|
|
73
|
-
const queued =
|
|
144
|
+
const queued = dequeue();
|
|
74
145
|
if (queued !== undefined) {
|
|
75
146
|
return { done: false, value: queued };
|
|
76
147
|
}
|
|
77
148
|
|
|
149
|
+
resetOverflowIfDrained();
|
|
78
150
|
return await new Promise<IteratorResult<U>>((resolve) => {
|
|
79
151
|
pendingResolve = resolve;
|
|
80
152
|
});
|
|
@@ -14,6 +14,8 @@ import type {
|
|
|
14
14
|
PrivateAccountDataConsumer,
|
|
15
15
|
PrivateSubscriptionState,
|
|
16
16
|
} from "../client/context.ts";
|
|
17
|
+
import { AcexError } from "../errors.ts";
|
|
18
|
+
import type { AsyncEventBusOverflowInfo } from "../internal/async-event-bus.ts";
|
|
17
19
|
import { AsyncEventBus } from "../internal/async-event-bus.ts";
|
|
18
20
|
import { toCanonical } from "../internal/decimal.ts";
|
|
19
21
|
import { matchesAccountFilter } from "../internal/filters.ts";
|
|
@@ -125,23 +127,33 @@ export class AccountManagerImpl
|
|
|
125
127
|
this.context = context;
|
|
126
128
|
|
|
127
129
|
this.events = {
|
|
128
|
-
status: (filter) =>
|
|
129
|
-
this.accountStatusBus.stream(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
status: (filter, options) =>
|
|
131
|
+
this.accountStatusBus.stream(
|
|
132
|
+
(event) =>
|
|
133
|
+
matchesAccountFilter(
|
|
134
|
+
{ accountId: event.accountId, venue: event.venue },
|
|
135
|
+
filter,
|
|
136
|
+
),
|
|
137
|
+
{
|
|
138
|
+
maxBuffer: options?.maxBuffer,
|
|
139
|
+
onOverflow: this.createOverflowHandler("account.status"),
|
|
140
|
+
},
|
|
134
141
|
),
|
|
135
|
-
updates: (filter) =>
|
|
136
|
-
this.accountBus.stream(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
updates: (filter, options) =>
|
|
143
|
+
this.accountBus.stream(
|
|
144
|
+
(event) =>
|
|
145
|
+
matchesAccountFilter(
|
|
146
|
+
{
|
|
147
|
+
accountId: event.accountId,
|
|
148
|
+
venue: event.venue,
|
|
149
|
+
symbol: "symbol" in event ? event.symbol : undefined,
|
|
150
|
+
},
|
|
151
|
+
filter,
|
|
152
|
+
),
|
|
153
|
+
{
|
|
154
|
+
maxBuffer: options?.maxBuffer,
|
|
155
|
+
onOverflow: this.createOverflowHandler("account.updates"),
|
|
156
|
+
},
|
|
145
157
|
),
|
|
146
158
|
};
|
|
147
159
|
}
|
|
@@ -872,4 +884,19 @@ export class AccountManagerImpl
|
|
|
872
884
|
this.accountStatusBus.publish(event);
|
|
873
885
|
this.context.publishHealthEvent(event);
|
|
874
886
|
}
|
|
887
|
+
|
|
888
|
+
private createOverflowHandler(
|
|
889
|
+
stream: string,
|
|
890
|
+
): (info: AsyncEventBusOverflowInfo) => void {
|
|
891
|
+
return ({ maxBuffer }) => {
|
|
892
|
+
const error = new AcexError(
|
|
893
|
+
"EVENT_BUFFER_OVERFLOW",
|
|
894
|
+
`Event stream buffer overflow: ${stream}`,
|
|
895
|
+
);
|
|
896
|
+
this.context.publishRuntimeError("account", error, {
|
|
897
|
+
stream,
|
|
898
|
+
maxBuffer,
|
|
899
|
+
});
|
|
900
|
+
};
|
|
901
|
+
}
|
|
875
902
|
}
|
|
@@ -19,10 +19,15 @@ import {
|
|
|
19
19
|
buildAcexErrorDetails,
|
|
20
20
|
formatAcexErrorMessage,
|
|
21
21
|
} from "../errors.ts";
|
|
22
|
+
import type {
|
|
23
|
+
AsyncEventBusOverflowInfo,
|
|
24
|
+
AsyncEventBusStreamOptions,
|
|
25
|
+
} from "../internal/async-event-bus.ts";
|
|
22
26
|
import { AsyncEventBus } from "../internal/async-event-bus.ts";
|
|
23
27
|
import { toCanonical } from "../internal/decimal.ts";
|
|
24
28
|
import { matchesMarketFilter } from "../internal/filters.ts";
|
|
25
29
|
import type {
|
|
30
|
+
EventStreamOptions,
|
|
26
31
|
FundingRateSnapshot,
|
|
27
32
|
FundingRateUpdatedEvent,
|
|
28
33
|
L1Book,
|
|
@@ -67,6 +72,14 @@ interface MarketRecord {
|
|
|
67
72
|
status: MarketDataStatus;
|
|
68
73
|
l1BookStream?: StreamHandle;
|
|
69
74
|
fundingRateStream?: StreamHandle;
|
|
75
|
+
lastPublishedStatusKey?: MarketStatusPublicationKey;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface MarketStatusPublicationKey {
|
|
79
|
+
activity: MarketDataStatus["activity"];
|
|
80
|
+
ready: MarketDataStatus["ready"];
|
|
81
|
+
freshness: MarketDataStatus["freshness"];
|
|
82
|
+
reason: MarketDataStatus["reason"];
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
interface CatalogFetchResult {
|
|
@@ -85,10 +98,38 @@ function marketKey(input: MarketKeyInput): string {
|
|
|
85
98
|
return `${input.venue}:${input.symbol}`;
|
|
86
99
|
}
|
|
87
100
|
|
|
101
|
+
function marketEventConflateKey(event: MarketEvent): string {
|
|
102
|
+
return `${event.type}:${marketKey(event)}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
88
105
|
function cloneMarketStatus(status: MarketDataStatus): MarketDataStatus {
|
|
89
106
|
return { ...status };
|
|
90
107
|
}
|
|
91
108
|
|
|
109
|
+
function statusPublicationKey(
|
|
110
|
+
status: MarketDataStatus,
|
|
111
|
+
): MarketStatusPublicationKey {
|
|
112
|
+
return {
|
|
113
|
+
activity: status.activity,
|
|
114
|
+
ready: status.ready,
|
|
115
|
+
freshness: status.freshness,
|
|
116
|
+
reason: status.reason,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function sameStatusPublicationKey(
|
|
121
|
+
current: MarketStatusPublicationKey,
|
|
122
|
+
previous: MarketStatusPublicationKey | undefined,
|
|
123
|
+
): boolean {
|
|
124
|
+
return (
|
|
125
|
+
previous !== undefined &&
|
|
126
|
+
current.activity === previous.activity &&
|
|
127
|
+
current.ready === previous.ready &&
|
|
128
|
+
current.freshness === previous.freshness &&
|
|
129
|
+
current.reason === previous.reason
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
92
133
|
function cloneStreamStatus(
|
|
93
134
|
status: MarketDataStreamStatus,
|
|
94
135
|
): MarketDataStreamStatus {
|
|
@@ -153,23 +194,49 @@ export class MarketManagerImpl
|
|
|
153
194
|
options.l1ReconnectMaxDelayMs ?? DEFAULT_L1_RECONNECT_MAX_DELAY_MS;
|
|
154
195
|
|
|
155
196
|
this.events = {
|
|
156
|
-
all: (filter) =>
|
|
157
|
-
this.marketBus.stream(
|
|
158
|
-
|
|
197
|
+
all: (filter, options) =>
|
|
198
|
+
this.marketBus.stream(
|
|
199
|
+
(event) => matchesMarketFilter(event, filter),
|
|
200
|
+
this.createStreamOptions(
|
|
201
|
+
"market.all",
|
|
202
|
+
options,
|
|
203
|
+
"buffer",
|
|
204
|
+
marketEventConflateKey,
|
|
205
|
+
),
|
|
206
|
+
),
|
|
207
|
+
fundingRateUpdates: (filter, options) =>
|
|
159
208
|
this.marketBus.stream(
|
|
160
209
|
(event): event is FundingRateUpdatedEvent =>
|
|
161
210
|
event.type === "funding_rate.updated" &&
|
|
162
211
|
matchesMarketFilter(event, filter),
|
|
212
|
+
this.createStreamOptions(
|
|
213
|
+
"market.fundingRateUpdates",
|
|
214
|
+
options,
|
|
215
|
+
"conflate",
|
|
216
|
+
marketKey,
|
|
217
|
+
),
|
|
163
218
|
),
|
|
164
|
-
l1BookUpdates: (filter) =>
|
|
219
|
+
l1BookUpdates: (filter, options) =>
|
|
165
220
|
this.marketBus.stream(
|
|
166
221
|
(event): event is L1BookUpdatedEvent =>
|
|
167
222
|
event.type === "l1_book.updated" &&
|
|
168
223
|
matchesMarketFilter(event, filter),
|
|
224
|
+
this.createStreamOptions(
|
|
225
|
+
"market.l1BookUpdates",
|
|
226
|
+
options,
|
|
227
|
+
"conflate",
|
|
228
|
+
marketKey,
|
|
229
|
+
),
|
|
169
230
|
),
|
|
170
|
-
status: (filter) =>
|
|
171
|
-
this.marketStatusBus.stream(
|
|
172
|
-
matchesMarketFilter(event, filter),
|
|
231
|
+
status: (filter, options) =>
|
|
232
|
+
this.marketStatusBus.stream(
|
|
233
|
+
(event) => matchesMarketFilter(event, filter),
|
|
234
|
+
this.createStreamOptions(
|
|
235
|
+
"market.status",
|
|
236
|
+
options,
|
|
237
|
+
"buffer",
|
|
238
|
+
marketKey,
|
|
239
|
+
),
|
|
173
240
|
),
|
|
174
241
|
};
|
|
175
242
|
}
|
|
@@ -1064,6 +1131,14 @@ export class MarketManagerImpl
|
|
|
1064
1131
|
record.status.lastReadyAt = this.resolveLastReadyAt(record);
|
|
1065
1132
|
}
|
|
1066
1133
|
|
|
1134
|
+
const publicationKey = statusPublicationKey(record.status);
|
|
1135
|
+
if (
|
|
1136
|
+
sameStatusPublicationKey(publicationKey, record.lastPublishedStatusKey)
|
|
1137
|
+
) {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
record.lastPublishedStatusKey = publicationKey;
|
|
1067
1142
|
this.publishStatus(record);
|
|
1068
1143
|
}
|
|
1069
1144
|
|
|
@@ -1184,6 +1259,35 @@ export class MarketManagerImpl
|
|
|
1184
1259
|
this.context.publishHealthEvent(event);
|
|
1185
1260
|
}
|
|
1186
1261
|
|
|
1262
|
+
private createStreamOptions<U extends { venue: Venue; symbol: string }>(
|
|
1263
|
+
stream: string,
|
|
1264
|
+
options: EventStreamOptions | undefined,
|
|
1265
|
+
defaultMode: "buffer" | "conflate",
|
|
1266
|
+
conflateKey: (event: U) => string,
|
|
1267
|
+
): AsyncEventBusStreamOptions<U> {
|
|
1268
|
+
return {
|
|
1269
|
+
mode: options?.mode ?? defaultMode,
|
|
1270
|
+
maxBuffer: options?.maxBuffer,
|
|
1271
|
+
conflateKey,
|
|
1272
|
+
onOverflow: this.createOverflowHandler(stream),
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private createOverflowHandler(
|
|
1277
|
+
stream: string,
|
|
1278
|
+
): (info: AsyncEventBusOverflowInfo) => void {
|
|
1279
|
+
return ({ maxBuffer }) => {
|
|
1280
|
+
const error = new AcexError(
|
|
1281
|
+
"EVENT_BUFFER_OVERFLOW",
|
|
1282
|
+
`Event stream buffer overflow: ${stream}`,
|
|
1283
|
+
);
|
|
1284
|
+
this.context.publishRuntimeError("market", error, {
|
|
1285
|
+
stream,
|
|
1286
|
+
maxBuffer,
|
|
1287
|
+
});
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1187
1291
|
private async resumeStreams(): Promise<void> {
|
|
1188
1292
|
for (const record of this.records.values()) {
|
|
1189
1293
|
const market = record.market;
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
buildAcexErrorDetails,
|
|
18
18
|
formatAcexErrorMessage,
|
|
19
19
|
} from "../errors.ts";
|
|
20
|
+
import type { AsyncEventBusOverflowInfo } from "../internal/async-event-bus.ts";
|
|
20
21
|
import { AsyncEventBus } from "../internal/async-event-bus.ts";
|
|
21
22
|
import { matchesOrderFilter } from "../internal/filters.ts";
|
|
22
23
|
import { isTransportError } from "../internal/http-client.ts";
|
|
@@ -116,23 +117,33 @@ export class OrderManagerImpl
|
|
|
116
117
|
);
|
|
117
118
|
|
|
118
119
|
this.events = {
|
|
119
|
-
status: (filter) =>
|
|
120
|
-
this.orderStatusBus.stream(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
status: (filter, options) =>
|
|
121
|
+
this.orderStatusBus.stream(
|
|
122
|
+
(event) =>
|
|
123
|
+
matchesOrderFilter(
|
|
124
|
+
{ accountId: event.accountId, venue: event.venue },
|
|
125
|
+
filter,
|
|
126
|
+
),
|
|
127
|
+
{
|
|
128
|
+
maxBuffer: options?.maxBuffer,
|
|
129
|
+
onOverflow: this.createOverflowHandler("order.status"),
|
|
130
|
+
},
|
|
125
131
|
),
|
|
126
|
-
updates: (filter) =>
|
|
127
|
-
this.orderBus.stream(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
updates: (filter, options) =>
|
|
133
|
+
this.orderBus.stream(
|
|
134
|
+
(event) =>
|
|
135
|
+
matchesOrderFilter(
|
|
136
|
+
{
|
|
137
|
+
accountId: event.accountId,
|
|
138
|
+
venue: event.venue,
|
|
139
|
+
symbol: "symbol" in event ? event.symbol : undefined,
|
|
140
|
+
},
|
|
141
|
+
filter,
|
|
142
|
+
),
|
|
143
|
+
{
|
|
144
|
+
maxBuffer: options?.maxBuffer,
|
|
145
|
+
onOverflow: this.createOverflowHandler("order.updates"),
|
|
146
|
+
},
|
|
136
147
|
),
|
|
137
148
|
};
|
|
138
149
|
}
|
|
@@ -1270,6 +1281,21 @@ export class OrderManagerImpl
|
|
|
1270
1281
|
},
|
|
1271
1282
|
};
|
|
1272
1283
|
}
|
|
1284
|
+
|
|
1285
|
+
private createOverflowHandler(
|
|
1286
|
+
stream: string,
|
|
1287
|
+
): (info: AsyncEventBusOverflowInfo) => void {
|
|
1288
|
+
return ({ maxBuffer }) => {
|
|
1289
|
+
const error = new AcexError(
|
|
1290
|
+
"EVENT_BUFFER_OVERFLOW",
|
|
1291
|
+
`Event stream buffer overflow: ${stream}`,
|
|
1292
|
+
);
|
|
1293
|
+
this.context.publishRuntimeError("order", error, {
|
|
1294
|
+
stream,
|
|
1295
|
+
maxBuffer,
|
|
1296
|
+
});
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1273
1299
|
}
|
|
1274
1300
|
|
|
1275
1301
|
function isOrderErrorCode(
|
package/src/types/account.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
BufferedEventStreamOptions,
|
|
2
3
|
PrivateRuntimeReason,
|
|
3
4
|
PrivateRuntimeStatus,
|
|
4
5
|
SubscriptionActivity,
|
|
@@ -158,8 +159,14 @@ export type AccountEvent =
|
|
|
158
159
|
| AccountSnapshotReplacedEvent;
|
|
159
160
|
|
|
160
161
|
export interface AccountEventStreams {
|
|
161
|
-
updates(
|
|
162
|
-
|
|
162
|
+
updates(
|
|
163
|
+
filter?: AccountEventFilter,
|
|
164
|
+
options?: BufferedEventStreamOptions,
|
|
165
|
+
): AsyncIterable<AccountEvent>;
|
|
166
|
+
status(
|
|
167
|
+
filter?: AccountEventFilter,
|
|
168
|
+
options?: BufferedEventStreamOptions,
|
|
169
|
+
): AsyncIterable<AccountStatusChangedEvent>;
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
export interface AccountManager {
|
package/src/types/client.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
import type {
|
|
19
19
|
AccountCredentials,
|
|
20
20
|
AcexInternalError,
|
|
21
|
+
BufferedEventStreamOptions,
|
|
21
22
|
ClientStatus,
|
|
22
23
|
CreateClientOptions,
|
|
23
24
|
RegisterAccountInput,
|
|
@@ -54,8 +55,13 @@ export interface HealthEventFilter {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
export interface ClientEventStreams {
|
|
57
|
-
health(
|
|
58
|
-
|
|
58
|
+
health(
|
|
59
|
+
filter?: HealthEventFilter,
|
|
60
|
+
options?: BufferedEventStreamOptions,
|
|
61
|
+
): AsyncIterable<HealthEvent>;
|
|
62
|
+
errors(
|
|
63
|
+
options?: BufferedEventStreamOptions,
|
|
64
|
+
): AsyncIterable<AcexInternalError>;
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
export type VenueRuntimeStatus = "available" | "type_only" | "reserved";
|
package/src/types/market.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type BigNumber from "bignumber.js";
|
|
2
2
|
import type { AcexError } from "../errors.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
EventStreamOptions,
|
|
5
|
+
MarketFreshness,
|
|
6
|
+
SubscriptionActivity,
|
|
7
|
+
Venue,
|
|
8
|
+
} from "./shared.ts";
|
|
4
9
|
|
|
5
10
|
export type MarketType = "spot" | "swap" | "future";
|
|
6
11
|
|
|
@@ -170,12 +175,22 @@ export type MarketEvent =
|
|
|
170
175
|
| MarketStatusChangedEvent;
|
|
171
176
|
|
|
172
177
|
export interface MarketEventStreams {
|
|
173
|
-
l1BookUpdates(
|
|
178
|
+
l1BookUpdates(
|
|
179
|
+
filter?: MarketEventFilter,
|
|
180
|
+
options?: EventStreamOptions,
|
|
181
|
+
): AsyncIterable<L1BookUpdatedEvent>;
|
|
174
182
|
fundingRateUpdates(
|
|
175
183
|
filter?: MarketEventFilter,
|
|
184
|
+
options?: EventStreamOptions,
|
|
176
185
|
): AsyncIterable<FundingRateUpdatedEvent>;
|
|
177
|
-
status(
|
|
178
|
-
|
|
186
|
+
status(
|
|
187
|
+
filter?: MarketEventFilter,
|
|
188
|
+
options?: EventStreamOptions,
|
|
189
|
+
): AsyncIterable<MarketStatusChangedEvent>;
|
|
190
|
+
all(
|
|
191
|
+
filter?: MarketEventFilter,
|
|
192
|
+
options?: EventStreamOptions,
|
|
193
|
+
): AsyncIterable<MarketEvent>;
|
|
179
194
|
}
|
|
180
195
|
|
|
181
196
|
export interface MarketManager {
|
package/src/types/order.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PositionSide } from "./account.ts";
|
|
2
2
|
import type {
|
|
3
|
+
BufferedEventStreamOptions,
|
|
3
4
|
PrivateRuntimeReason,
|
|
4
5
|
PrivateRuntimeStatus,
|
|
5
6
|
SubscriptionActivity,
|
|
@@ -160,8 +161,14 @@ export type OrderEvent =
|
|
|
160
161
|
| OrderSnapshotReplacedEvent;
|
|
161
162
|
|
|
162
163
|
export interface OrderEventStreams {
|
|
163
|
-
updates(
|
|
164
|
-
|
|
164
|
+
updates(
|
|
165
|
+
filter?: OrderEventFilter,
|
|
166
|
+
options?: BufferedEventStreamOptions,
|
|
167
|
+
): AsyncIterable<OrderEvent>;
|
|
168
|
+
status(
|
|
169
|
+
filter?: OrderEventFilter,
|
|
170
|
+
options?: BufferedEventStreamOptions,
|
|
171
|
+
): AsyncIterable<OrderStatusChangedEvent>;
|
|
165
172
|
}
|
|
166
173
|
|
|
167
174
|
export interface OrderManager {
|
package/src/types/shared.ts
CHANGED
|
@@ -175,11 +175,24 @@ export interface StopOptions {
|
|
|
175
175
|
timeoutMs?: number;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
export type EventStreamMode = "buffer" | "conflate";
|
|
179
|
+
|
|
180
|
+
export interface EventStreamOptions {
|
|
181
|
+
mode?: EventStreamMode;
|
|
182
|
+
maxBuffer?: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface BufferedEventStreamOptions {
|
|
186
|
+
maxBuffer?: number;
|
|
187
|
+
}
|
|
188
|
+
|
|
178
189
|
export interface AcexInternalError {
|
|
179
190
|
source: "client" | "market" | "account" | "order" | "adapter" | "runtime";
|
|
180
191
|
venue?: Venue;
|
|
181
192
|
accountId?: string;
|
|
182
193
|
symbol?: string;
|
|
194
|
+
stream?: string;
|
|
195
|
+
maxBuffer?: number;
|
|
183
196
|
error: Error;
|
|
184
197
|
ts: number;
|
|
185
198
|
}
|