@imbingox/acex 0.4.0-beta.15 → 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 +12 -0
- package/docs/api.md +38 -8
- package/package.json +1 -1
- package/src/client/context.ts +22 -0
- package/src/client/private-subscription-coordinator.ts +113 -5
- package/src/client/runtime.ts +43 -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/data-status.ts +9 -0
- package/src/managers/order/model.ts +3 -0
- package/src/managers/order/snapshot.ts +1 -0
- package/src/managers/order-manager.ts +244 -31
- 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 +11 -3
- package/src/types/shared.ts +15 -0
|
@@ -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;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OrderDataStatus, Venue } from "../../types/index.ts";
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_MAX_CLOSED_ORDERS_PER_SYMBOL = 500;
|
|
4
|
+
export const DEFAULT_MISSING_ORDER_EVICTION_THRESHOLD = 3;
|
|
4
5
|
|
|
5
6
|
export function cloneOrderStatus(status: OrderDataStatus): OrderDataStatus {
|
|
6
7
|
return { ...status };
|
|
@@ -28,6 +29,14 @@ export function normalizeMaxClosedOrdersPerSymbol(
|
|
|
28
29
|
: DEFAULT_MAX_CLOSED_ORDERS_PER_SYMBOL;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
export function normalizeMissingOrderEvictionThreshold(
|
|
33
|
+
value: number | undefined,
|
|
34
|
+
): number {
|
|
35
|
+
return value !== undefined && Number.isInteger(value) && value > 0
|
|
36
|
+
? value
|
|
37
|
+
: DEFAULT_MISSING_ORDER_EVICTION_THRESHOLD;
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
export function successfulStatus(
|
|
32
41
|
status: OrderDataStatus,
|
|
33
42
|
options: {
|
|
@@ -15,6 +15,7 @@ export interface OrderRecord {
|
|
|
15
15
|
orderIdOnlyIndex: Map<string, Set<string>>;
|
|
16
16
|
clientOrderIdIndex: Map<string, Set<string>>;
|
|
17
17
|
pendingClientOrderIdIndex: Map<string, PendingOrderClaim>;
|
|
18
|
+
missingOrderConfirmations: Map<string, number>;
|
|
18
19
|
status: OrderDataStatus;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -29,8 +30,10 @@ export interface OrderLocation {
|
|
|
29
30
|
export interface PendingOrderClaim {
|
|
30
31
|
localOrderId: string;
|
|
31
32
|
symbol: string;
|
|
33
|
+
claimedAt: number;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export interface OrderManagerOptions {
|
|
35
37
|
maxClosedOrdersPerSymbol?: number;
|
|
38
|
+
missingOrderEvictionThreshold?: number;
|
|
36
39
|
}
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
import type {
|
|
6
6
|
AccountAwareManager,
|
|
7
7
|
ClientContext,
|
|
8
|
+
ExpiredPendingOrderClaim,
|
|
8
9
|
HealthReporter,
|
|
9
10
|
ManagerLifecycle,
|
|
10
11
|
PrivateOrderDataConsumer,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
buildAcexErrorDetails,
|
|
17
18
|
formatAcexErrorMessage,
|
|
18
19
|
} from "../errors.ts";
|
|
20
|
+
import type { AsyncEventBusOverflowInfo } from "../internal/async-event-bus.ts";
|
|
19
21
|
import { AsyncEventBus } from "../internal/async-event-bus.ts";
|
|
20
22
|
import { matchesOrderFilter } from "../internal/filters.ts";
|
|
21
23
|
import { isTransportError } from "../internal/http-client.ts";
|
|
@@ -43,6 +45,7 @@ import {
|
|
|
43
45
|
cloneOrderStatus,
|
|
44
46
|
createOrderDataStatus,
|
|
45
47
|
normalizeMaxClosedOrdersPerSymbol,
|
|
48
|
+
normalizeMissingOrderEvictionThreshold,
|
|
46
49
|
successfulStatus,
|
|
47
50
|
} from "./order/data-status.ts";
|
|
48
51
|
import {
|
|
@@ -61,6 +64,7 @@ import { createSnapshot, isOpenOrder } from "./order/snapshot.ts";
|
|
|
61
64
|
import {
|
|
62
65
|
getAllSnapshots,
|
|
63
66
|
getExistingSnapshot,
|
|
67
|
+
getExistingSnapshotLocation,
|
|
64
68
|
getLocalOrderIdForVenueOrderId,
|
|
65
69
|
getLocationByLocalOrderId,
|
|
66
70
|
getOpenOrderSnapshots,
|
|
@@ -96,6 +100,7 @@ export class OrderManagerImpl
|
|
|
96
100
|
|
|
97
101
|
private readonly context: ClientContext;
|
|
98
102
|
private readonly maxClosedOrdersPerSymbol: number;
|
|
103
|
+
private readonly missingOrderEvictionThreshold: number;
|
|
99
104
|
private readonly orderBus = new AsyncEventBus<OrderEvent>();
|
|
100
105
|
private readonly orderStatusBus =
|
|
101
106
|
new AsyncEventBus<OrderStatusChangedEvent>();
|
|
@@ -107,25 +112,38 @@ export class OrderManagerImpl
|
|
|
107
112
|
this.maxClosedOrdersPerSymbol = normalizeMaxClosedOrdersPerSymbol(
|
|
108
113
|
options.maxClosedOrdersPerSymbol,
|
|
109
114
|
);
|
|
115
|
+
this.missingOrderEvictionThreshold = normalizeMissingOrderEvictionThreshold(
|
|
116
|
+
options.missingOrderEvictionThreshold,
|
|
117
|
+
);
|
|
110
118
|
|
|
111
119
|
this.events = {
|
|
112
|
-
status: (filter) =>
|
|
113
|
-
this.orderStatusBus.stream(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
},
|
|
118
131
|
),
|
|
119
|
-
updates: (filter) =>
|
|
120
|
-
this.orderBus.stream(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
},
|
|
129
147
|
),
|
|
130
148
|
};
|
|
131
149
|
}
|
|
@@ -494,6 +512,9 @@ export class OrderManagerImpl
|
|
|
494
512
|
openSetKeys.add(lookupKey);
|
|
495
513
|
}
|
|
496
514
|
const current = getExistingSnapshot(record, update);
|
|
515
|
+
if (current) {
|
|
516
|
+
this.clearMissingOrderConfirmationsForUpdate(record, current);
|
|
517
|
+
}
|
|
497
518
|
const nextSnapshot = this.applyUpdateToRecord(
|
|
498
519
|
record,
|
|
499
520
|
accountId,
|
|
@@ -565,6 +586,36 @@ export class OrderManagerImpl
|
|
|
565
586
|
return this.getOpenOrders(accountId);
|
|
566
587
|
}
|
|
567
588
|
|
|
589
|
+
getExpiredPrivateOrderClaims(
|
|
590
|
+
accountId: string,
|
|
591
|
+
now: number,
|
|
592
|
+
ttlMs: number,
|
|
593
|
+
): ExpiredPendingOrderClaim[] {
|
|
594
|
+
const record = this.records.get(accountId);
|
|
595
|
+
if (!record || ttlMs <= 0) {
|
|
596
|
+
return [];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const expired: ExpiredPendingOrderClaim[] = [];
|
|
600
|
+
for (const [
|
|
601
|
+
venueClientOrderId,
|
|
602
|
+
claim,
|
|
603
|
+
] of record.pendingClientOrderIdIndex) {
|
|
604
|
+
if (now - claim.claimedAt < ttlMs) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
expired.push({
|
|
609
|
+
venueClientOrderId,
|
|
610
|
+
localOrderId: claim.localOrderId,
|
|
611
|
+
symbol: claim.symbol,
|
|
612
|
+
claimedAt: claim.claimedAt,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return expired;
|
|
617
|
+
}
|
|
618
|
+
|
|
568
619
|
onPrivateOrderUpdate(
|
|
569
620
|
accountId: string,
|
|
570
621
|
venue: Venue,
|
|
@@ -590,32 +641,129 @@ export class OrderManagerImpl
|
|
|
590
641
|
return;
|
|
591
642
|
}
|
|
592
643
|
|
|
593
|
-
|
|
594
|
-
snapshot.status === "filled"
|
|
595
|
-
? "order.filled"
|
|
596
|
-
: snapshot.status === "rejected"
|
|
597
|
-
? "order.rejected"
|
|
598
|
-
: snapshot.status === "canceled" || snapshot.status === "expired"
|
|
599
|
-
? "order.canceled"
|
|
600
|
-
: "order.updated";
|
|
644
|
+
this.publishOrderEvent(accountId, venue, snapshot);
|
|
601
645
|
|
|
602
|
-
|
|
603
|
-
|
|
646
|
+
record.status = successfulStatus(record.status, {
|
|
647
|
+
preserveStatus: options.preserveStatus,
|
|
648
|
+
lastReceivedAt: snapshot.receivedAt,
|
|
649
|
+
lastReadyAt: snapshot.updatedAt,
|
|
650
|
+
});
|
|
651
|
+
this.publishStatus(record);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
onPrivateOrderConfirmedMissing(
|
|
655
|
+
accountId: string,
|
|
656
|
+
venue: Venue,
|
|
657
|
+
order: OrderSnapshot,
|
|
658
|
+
): void {
|
|
659
|
+
const record = this.getOrCreateRecord(accountId, venue);
|
|
660
|
+
if (!record.subscribed) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const location = getExistingSnapshotLocation(record, order);
|
|
665
|
+
if (!location || location.table !== "open") {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const current = getSnapshotAtLocation(record, location);
|
|
670
|
+
if (!current || !isOpenOrder(current)) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const confirmations =
|
|
675
|
+
(record.missingOrderConfirmations.get(location.localOrderId) ?? 0) + 1;
|
|
676
|
+
if (confirmations < this.missingOrderEvictionThreshold) {
|
|
677
|
+
record.missingOrderConfirmations.set(
|
|
678
|
+
location.localOrderId,
|
|
679
|
+
confirmations,
|
|
680
|
+
);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const receivedAt = this.context.now();
|
|
685
|
+
const snapshot = createSnapshot(
|
|
604
686
|
accountId,
|
|
605
687
|
venue,
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
688
|
+
{
|
|
689
|
+
orderId: current.orderId,
|
|
690
|
+
clientOrderId: current.clientOrderId,
|
|
691
|
+
symbol: current.symbol,
|
|
692
|
+
side: current.side,
|
|
693
|
+
type: current.type,
|
|
694
|
+
status: "unknown",
|
|
695
|
+
price: current.price,
|
|
696
|
+
triggerPrice: current.triggerPrice,
|
|
697
|
+
amount: current.amount,
|
|
698
|
+
filled: current.filled,
|
|
699
|
+
remaining: current.remaining,
|
|
700
|
+
reduceOnly: current.reduceOnly,
|
|
701
|
+
positionSide: current.positionSide,
|
|
702
|
+
avgFillPrice: current.avgFillPrice,
|
|
703
|
+
receivedAt,
|
|
704
|
+
},
|
|
705
|
+
current,
|
|
706
|
+
);
|
|
610
707
|
|
|
708
|
+
if (
|
|
709
|
+
!this.writeSnapshot(record, location.localOrderId, snapshot, location)
|
|
710
|
+
) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
this.context.publishRuntimeError(
|
|
715
|
+
"order",
|
|
716
|
+
new Error(
|
|
717
|
+
`Evicted ${venue} open order after ${confirmations} confirmed missing checks`,
|
|
718
|
+
),
|
|
719
|
+
{
|
|
720
|
+
accountId,
|
|
721
|
+
venue,
|
|
722
|
+
symbol: current.symbol,
|
|
723
|
+
},
|
|
724
|
+
);
|
|
725
|
+
this.publishOrderEvent(accountId, venue, snapshot);
|
|
611
726
|
record.status = successfulStatus(record.status, {
|
|
612
|
-
preserveStatus: options.preserveStatus,
|
|
613
727
|
lastReceivedAt: snapshot.receivedAt,
|
|
614
728
|
lastReadyAt: snapshot.updatedAt,
|
|
615
729
|
});
|
|
616
730
|
this.publishStatus(record);
|
|
617
731
|
}
|
|
618
732
|
|
|
733
|
+
onPrivateOrderClaimNotFound(
|
|
734
|
+
accountId: string,
|
|
735
|
+
venue: Venue,
|
|
736
|
+
claim: ExpiredPendingOrderClaim,
|
|
737
|
+
): void {
|
|
738
|
+
const record = this.records.get(accountId);
|
|
739
|
+
if (!record) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const pending = record.pendingClientOrderIdIndex.get(
|
|
744
|
+
claim.venueClientOrderId,
|
|
745
|
+
);
|
|
746
|
+
if (
|
|
747
|
+
pending?.localOrderId !== claim.localOrderId ||
|
|
748
|
+
pending.symbol !== claim.symbol
|
|
749
|
+
) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
record.pendingClientOrderIdIndex.delete(claim.venueClientOrderId);
|
|
754
|
+
this.context.publishRuntimeError(
|
|
755
|
+
"order",
|
|
756
|
+
new Error(
|
|
757
|
+
`createOrder timed out and the order was not found on the venue: ${claim.venueClientOrderId}`,
|
|
758
|
+
),
|
|
759
|
+
{
|
|
760
|
+
accountId,
|
|
761
|
+
venue,
|
|
762
|
+
symbol: claim.symbol,
|
|
763
|
+
},
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
619
767
|
onPrivateOrderStreamState(
|
|
620
768
|
accountId: string,
|
|
621
769
|
venue: Venue,
|
|
@@ -670,6 +818,7 @@ export class OrderManagerImpl
|
|
|
670
818
|
orderIdOnlyIndex: new Map(),
|
|
671
819
|
clientOrderIdIndex: new Map(),
|
|
672
820
|
pendingClientOrderIdIndex: new Map(),
|
|
821
|
+
missingOrderConfirmations: new Map(),
|
|
673
822
|
status: createOrderDataStatus(accountId, venue, "inactive"),
|
|
674
823
|
};
|
|
675
824
|
|
|
@@ -718,9 +867,51 @@ export class OrderManagerImpl
|
|
|
718
867
|
|
|
719
868
|
this.warnSystemClientOrderIdOnlyClaim(record, snapshot);
|
|
720
869
|
this.warnProvisionalTerminalOrder(record, snapshot);
|
|
870
|
+
this.clearMissingOrderConfirmations(record, localOrderId);
|
|
721
871
|
return true;
|
|
722
872
|
}
|
|
723
873
|
|
|
874
|
+
private clearMissingOrderConfirmations(
|
|
875
|
+
record: OrderRecord,
|
|
876
|
+
localOrderId: string,
|
|
877
|
+
): void {
|
|
878
|
+
record.missingOrderConfirmations.delete(localOrderId);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
private clearMissingOrderConfirmationsForUpdate(
|
|
882
|
+
record: OrderRecord,
|
|
883
|
+
update: { symbol: string; orderId?: string; clientOrderId?: string },
|
|
884
|
+
): void {
|
|
885
|
+
const location = getExistingSnapshotLocation(record, update);
|
|
886
|
+
if (location) {
|
|
887
|
+
this.clearMissingOrderConfirmations(record, location.localOrderId);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private publishOrderEvent(
|
|
892
|
+
accountId: string,
|
|
893
|
+
venue: Venue,
|
|
894
|
+
snapshot: OrderSnapshot,
|
|
895
|
+
): void {
|
|
896
|
+
const eventType =
|
|
897
|
+
snapshot.status === "filled"
|
|
898
|
+
? "order.filled"
|
|
899
|
+
: snapshot.status === "rejected"
|
|
900
|
+
? "order.rejected"
|
|
901
|
+
: isOpenOrder(snapshot)
|
|
902
|
+
? "order.updated"
|
|
903
|
+
: "order.canceled";
|
|
904
|
+
|
|
905
|
+
this.orderBus.publish({
|
|
906
|
+
type: eventType,
|
|
907
|
+
accountId,
|
|
908
|
+
venue,
|
|
909
|
+
symbol: snapshot.symbol,
|
|
910
|
+
snapshot,
|
|
911
|
+
ts: this.context.now(),
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
|
|
724
915
|
private warnDroppedUnkeyedTerminalOrder(
|
|
725
916
|
record: OrderRecord,
|
|
726
917
|
snapshot: OrderSnapshot,
|
|
@@ -800,6 +991,9 @@ export class OrderManagerImpl
|
|
|
800
991
|
options: { requestStartedAt?: number; preserveStatus?: boolean } = {},
|
|
801
992
|
): OrderSnapshot | undefined {
|
|
802
993
|
const resolution = this.resolveLocalOrderIdForUpdate(record, update);
|
|
994
|
+
if (resolution.localOrderId) {
|
|
995
|
+
this.clearMissingOrderConfirmations(record, resolution.localOrderId);
|
|
996
|
+
}
|
|
803
997
|
const localOrderId = resolution.localOrderId ?? this.generateLocalOrderId();
|
|
804
998
|
const previousLocation = getLocationByLocalOrderId(record, localOrderId);
|
|
805
999
|
const previous = previousLocation
|
|
@@ -874,6 +1068,7 @@ export class OrderManagerImpl
|
|
|
874
1068
|
record.pendingClientOrderIdIndex.set(venueClientOrderId, {
|
|
875
1069
|
localOrderId,
|
|
876
1070
|
symbol,
|
|
1071
|
+
claimedAt: this.context.now(),
|
|
877
1072
|
});
|
|
878
1073
|
}
|
|
879
1074
|
|
|
@@ -955,6 +1150,9 @@ export class OrderManagerImpl
|
|
|
955
1150
|
update,
|
|
956
1151
|
options.localOrderId,
|
|
957
1152
|
);
|
|
1153
|
+
if (resolution.localOrderId) {
|
|
1154
|
+
this.clearMissingOrderConfirmations(record, resolution.localOrderId);
|
|
1155
|
+
}
|
|
958
1156
|
const localOrderId = resolution.localOrderId ?? this.generateLocalOrderId();
|
|
959
1157
|
const previousLocation = getLocationByLocalOrderId(record, localOrderId);
|
|
960
1158
|
const previous = previousLocation
|
|
@@ -1083,6 +1281,21 @@ export class OrderManagerImpl
|
|
|
1083
1281
|
},
|
|
1084
1282
|
};
|
|
1085
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
|
+
}
|
|
1086
1299
|
}
|
|
1087
1300
|
|
|
1088
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 {
|