@imbingox/acex 0.4.0-beta.11 → 0.4.0-beta.13
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/docs/api.md +4 -2
- package/package.json +2 -1
- package/src/adapters/binance/private-adapter.ts +253 -43
- package/src/adapters/types.ts +2 -0
- package/src/client/private-subscription-coordinator.ts +31 -0
- package/src/internal/watermark.ts +11 -0
- package/src/managers/order/data-status.ts +61 -0
- package/src/managers/order/identity.ts +77 -0
- package/src/managers/order/model.ts +36 -0
- package/src/managers/order/snapshot.ts +87 -0
- package/src/managers/order/store.ts +486 -0
- package/src/managers/order-manager.ts +168 -720
- package/src/types/shared.ts +1 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { OrderSnapshot } from "../../types/index.ts";
|
|
2
|
+
|
|
3
|
+
export const SDK_CLIENT_ORDER_ID_PREFIX = "acex-";
|
|
4
|
+
export const VENUE_CLIENT_ORDER_ID_PATTERN = /^[.A-Z:/a-z0-9_-]{1,32}$/;
|
|
5
|
+
|
|
6
|
+
const SYSTEM_CLIENT_ORDER_ID_PATTERNS = [
|
|
7
|
+
/^adl_autoclose$/,
|
|
8
|
+
/^autoclose-/,
|
|
9
|
+
/^settlement_autoclose-/,
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export function getOrderLookupKeys(input: {
|
|
13
|
+
symbol: string;
|
|
14
|
+
orderId?: string;
|
|
15
|
+
clientOrderId?: string;
|
|
16
|
+
}): string[] {
|
|
17
|
+
const keys: string[] = [];
|
|
18
|
+
if (input.orderId) {
|
|
19
|
+
keys.push(`symbol:${input.symbol}:order:${input.orderId}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (input.clientOrderId) {
|
|
23
|
+
keys.push(`symbol:${input.symbol}:client:${input.clientOrderId}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return keys;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function shouldMatchOrderQuery(
|
|
30
|
+
candidate: OrderSnapshot,
|
|
31
|
+
input: { symbol?: string; orderId?: string; clientOrderId?: string },
|
|
32
|
+
): boolean {
|
|
33
|
+
if (input.symbol && candidate.symbol !== input.symbol) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (input.orderId && candidate.orderId !== input.orderId) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (input.clientOrderId && candidate.clientOrderId !== input.clientOrderId) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return Boolean(input.orderId || input.clientOrderId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function shouldMatchStoredOrderIdentity(
|
|
49
|
+
candidate: OrderSnapshot,
|
|
50
|
+
input: { symbol: string; orderId?: string; clientOrderId?: string },
|
|
51
|
+
): boolean {
|
|
52
|
+
if (candidate.symbol !== input.symbol) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (candidate.orderId && input.orderId) {
|
|
57
|
+
return candidate.orderId === input.orderId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// clientOrderId is only a temporary identity for an order that does not yet
|
|
61
|
+
// have an orderId. A candidate that already carries an orderId (including an
|
|
62
|
+
// old order sitting in closed that reused this clientOrderId) must not be
|
|
63
|
+
// merged by a cid-only update; otherwise the stale orderId would be carried
|
|
64
|
+
// forward and pollute closed. When the orderId is later filled in, the
|
|
65
|
+
// candidate still lacks an orderId and matches normally.
|
|
66
|
+
return Boolean(
|
|
67
|
+
input.clientOrderId &&
|
|
68
|
+
candidate.clientOrderId === input.clientOrderId &&
|
|
69
|
+
!candidate.orderId,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isSystemClientOrderId(clientOrderId: string): boolean {
|
|
74
|
+
return SYSTEM_CLIENT_ORDER_ID_PATTERNS.some((pattern) =>
|
|
75
|
+
pattern.test(clientOrderId),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
OrderDataStatus,
|
|
3
|
+
OrderSnapshot,
|
|
4
|
+
Venue,
|
|
5
|
+
} from "../../types/index.ts";
|
|
6
|
+
|
|
7
|
+
export interface OrderRecord {
|
|
8
|
+
accountId: string;
|
|
9
|
+
venue: Venue;
|
|
10
|
+
subscribed: boolean;
|
|
11
|
+
openOrders: Map<string, Map<string, OrderSnapshot>>;
|
|
12
|
+
closedOrders: Map<string, Map<string, OrderSnapshot>>;
|
|
13
|
+
localOrderLocations: Map<string, OrderLocation>;
|
|
14
|
+
orderIdIndex: Map<string, Map<string, string>>;
|
|
15
|
+
orderIdOnlyIndex: Map<string, Set<string>>;
|
|
16
|
+
clientOrderIdIndex: Map<string, Set<string>>;
|
|
17
|
+
pendingClientOrderIdIndex: Map<string, PendingOrderClaim>;
|
|
18
|
+
status: OrderDataStatus;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type OrderTable = "open" | "closed";
|
|
22
|
+
|
|
23
|
+
export interface OrderLocation {
|
|
24
|
+
table: OrderTable;
|
|
25
|
+
symbol: string;
|
|
26
|
+
localOrderId: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PendingOrderClaim {
|
|
30
|
+
localOrderId: string;
|
|
31
|
+
symbol: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface OrderManagerOptions {
|
|
35
|
+
maxClosedOrdersPerSymbol?: number;
|
|
36
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import BigNumber from "bignumber.js";
|
|
2
|
+
import type { RawOrderUpdate } from "../../adapters/types.ts";
|
|
3
|
+
import { toCanonical } from "../../internal/decimal.ts";
|
|
4
|
+
import type { OrderSnapshot, Venue } from "../../types/index.ts";
|
|
5
|
+
|
|
6
|
+
export function createSnapshot(
|
|
7
|
+
accountId: string,
|
|
8
|
+
venue: Venue,
|
|
9
|
+
input: RawOrderUpdate,
|
|
10
|
+
previous?: OrderSnapshot,
|
|
11
|
+
): OrderSnapshot {
|
|
12
|
+
const amount = new BigNumber(input.amount);
|
|
13
|
+
const rawFilled = new BigNumber(input.filled);
|
|
14
|
+
const filled = previous
|
|
15
|
+
? BigNumber.maximum(rawFilled, previous.filled)
|
|
16
|
+
: rawFilled;
|
|
17
|
+
const filledWasClamped = !filled.eq(rawFilled);
|
|
18
|
+
const remaining =
|
|
19
|
+
input.remaining === undefined || filledWasClamped
|
|
20
|
+
? amount.minus(filled)
|
|
21
|
+
: new BigNumber(input.remaining);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
accountId,
|
|
25
|
+
venue,
|
|
26
|
+
orderId: input.orderId ?? previous?.orderId,
|
|
27
|
+
clientOrderId: input.clientOrderId ?? previous?.clientOrderId,
|
|
28
|
+
symbol: input.symbol,
|
|
29
|
+
side: input.side,
|
|
30
|
+
type: input.type,
|
|
31
|
+
status: mergeOrderStatus(input, previous),
|
|
32
|
+
price:
|
|
33
|
+
input.price === undefined ? previous?.price : toCanonical(input.price),
|
|
34
|
+
triggerPrice:
|
|
35
|
+
input.triggerPrice === undefined
|
|
36
|
+
? previous?.triggerPrice
|
|
37
|
+
: toCanonical(input.triggerPrice),
|
|
38
|
+
amount: toCanonical(amount),
|
|
39
|
+
filled: toCanonical(filled),
|
|
40
|
+
remaining: toCanonical(remaining),
|
|
41
|
+
reduceOnly: input.reduceOnly ?? previous?.reduceOnly,
|
|
42
|
+
positionSide: input.positionSide ?? previous?.positionSide,
|
|
43
|
+
avgFillPrice:
|
|
44
|
+
input.avgFillPrice === undefined
|
|
45
|
+
? previous?.avgFillPrice
|
|
46
|
+
: toCanonical(input.avgFillPrice),
|
|
47
|
+
exchangeTs: input.exchangeTs,
|
|
48
|
+
receivedAt: input.receivedAt,
|
|
49
|
+
updatedAt: input.receivedAt,
|
|
50
|
+
seq: (previous?.seq ?? 0) + 1,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function mergeOrderStatus(
|
|
55
|
+
input: RawOrderUpdate,
|
|
56
|
+
previous?: OrderSnapshot,
|
|
57
|
+
): OrderSnapshot["status"] {
|
|
58
|
+
if (!previous) {
|
|
59
|
+
return input.status;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (orderPriority(input.status) < orderPriority(previous.status)) {
|
|
63
|
+
return previous.status;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return input.status;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isOpenOrder(snapshot: OrderSnapshot): boolean {
|
|
70
|
+
return snapshot.status === "open" || snapshot.status === "partially_filled";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function orderPriority(status: OrderSnapshot["status"]): number {
|
|
74
|
+
switch (status) {
|
|
75
|
+
case "filled":
|
|
76
|
+
return 5;
|
|
77
|
+
case "canceled":
|
|
78
|
+
case "expired":
|
|
79
|
+
return 4;
|
|
80
|
+
case "rejected":
|
|
81
|
+
return 3;
|
|
82
|
+
case "partially_filled":
|
|
83
|
+
return 2;
|
|
84
|
+
case "open":
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import type { OrderSnapshot } from "../../types/index.ts";
|
|
2
|
+
import {
|
|
3
|
+
isSystemClientOrderId,
|
|
4
|
+
shouldMatchStoredOrderIdentity,
|
|
5
|
+
} from "./identity.ts";
|
|
6
|
+
import type { OrderLocation, OrderRecord, OrderTable } from "./model.ts";
|
|
7
|
+
import { isOpenOrder } from "./snapshot.ts";
|
|
8
|
+
|
|
9
|
+
export interface SetSnapshotResult {
|
|
10
|
+
location?: OrderLocation;
|
|
11
|
+
trimmedSnapshots: OrderSnapshot[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SetSnapshotOptions {
|
|
15
|
+
maxClosedOrdersPerSymbol: number;
|
|
16
|
+
previousLocation?: OrderLocation;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LocalOrderResolution {
|
|
20
|
+
localOrderId?: string;
|
|
21
|
+
source?: "exact" | "pending" | "provisional" | "preferred";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getSnapshotAtLocation(
|
|
25
|
+
record: OrderRecord,
|
|
26
|
+
location: OrderLocation,
|
|
27
|
+
): OrderSnapshot | undefined {
|
|
28
|
+
return getOrderTable(record, location.table)
|
|
29
|
+
.get(location.symbol)
|
|
30
|
+
?.get(location.localOrderId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getSnapshotByLocalOrderId(
|
|
34
|
+
record: OrderRecord,
|
|
35
|
+
localOrderId: string,
|
|
36
|
+
): OrderSnapshot | undefined {
|
|
37
|
+
const location = record.localOrderLocations.get(localOrderId);
|
|
38
|
+
return location ? getSnapshotAtLocation(record, location) : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getOrderTable(
|
|
42
|
+
record: OrderRecord,
|
|
43
|
+
table: OrderTable,
|
|
44
|
+
): Map<string, Map<string, OrderSnapshot>> {
|
|
45
|
+
return table === "open" ? record.openOrders : record.closedOrders;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getOrCreateSymbolOrders(
|
|
49
|
+
table: Map<string, Map<string, OrderSnapshot>>,
|
|
50
|
+
symbol: string,
|
|
51
|
+
): Map<string, OrderSnapshot> {
|
|
52
|
+
const existing = table.get(symbol);
|
|
53
|
+
if (existing) {
|
|
54
|
+
return existing;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const created = new Map<string, OrderSnapshot>();
|
|
58
|
+
table.set(symbol, created);
|
|
59
|
+
return created;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getOrCreateOrderIdSymbolIndex(
|
|
63
|
+
record: OrderRecord,
|
|
64
|
+
symbol: string,
|
|
65
|
+
): Map<string, string> {
|
|
66
|
+
const existing = record.orderIdIndex.get(symbol);
|
|
67
|
+
if (existing) {
|
|
68
|
+
return existing;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const created = new Map<string, string>();
|
|
72
|
+
record.orderIdIndex.set(symbol, created);
|
|
73
|
+
return created;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getLocalOrderIdForVenueOrderId(
|
|
77
|
+
record: OrderRecord,
|
|
78
|
+
symbol: string,
|
|
79
|
+
orderId: string,
|
|
80
|
+
): string | undefined {
|
|
81
|
+
return record.orderIdIndex.get(symbol)?.get(orderId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getLocationByLocalOrderId(
|
|
85
|
+
record: OrderRecord,
|
|
86
|
+
localOrderId: string,
|
|
87
|
+
): OrderLocation | undefined {
|
|
88
|
+
return record.localOrderLocations.get(localOrderId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getExistingSnapshot(
|
|
92
|
+
record: OrderRecord,
|
|
93
|
+
update: { symbol: string; orderId?: string; clientOrderId?: string },
|
|
94
|
+
): OrderSnapshot | undefined {
|
|
95
|
+
const location = getExistingSnapshotLocation(record, update);
|
|
96
|
+
return location ? getSnapshotAtLocation(record, location) : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getExistingSnapshotLocation(
|
|
100
|
+
record: OrderRecord,
|
|
101
|
+
update: { symbol: string; orderId?: string; clientOrderId?: string },
|
|
102
|
+
): OrderLocation | undefined {
|
|
103
|
+
const resolution = resolveLocalOrderIdForUpdate(record, update);
|
|
104
|
+
return resolution.localOrderId
|
|
105
|
+
? record.localOrderLocations.get(resolution.localOrderId)
|
|
106
|
+
: undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function resolveLocalOrderIdForUpdate(
|
|
110
|
+
record: OrderRecord,
|
|
111
|
+
update: { symbol: string; orderId?: string; clientOrderId?: string },
|
|
112
|
+
options: {
|
|
113
|
+
preferredLocalOrderId?: string;
|
|
114
|
+
pendingLocalOrderId?: string;
|
|
115
|
+
} = {},
|
|
116
|
+
): LocalOrderResolution {
|
|
117
|
+
if (update.orderId) {
|
|
118
|
+
const exact = getLocalOrderIdForVenueOrderId(
|
|
119
|
+
record,
|
|
120
|
+
update.symbol,
|
|
121
|
+
update.orderId,
|
|
122
|
+
);
|
|
123
|
+
if (exact) {
|
|
124
|
+
return { localOrderId: exact, source: "exact" };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (options.preferredLocalOrderId) {
|
|
129
|
+
return {
|
|
130
|
+
localOrderId: options.preferredLocalOrderId,
|
|
131
|
+
source: "preferred",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (options.pendingLocalOrderId) {
|
|
136
|
+
return { localOrderId: options.pendingLocalOrderId, source: "pending" };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (update.clientOrderId && !isSystemClientOrderId(update.clientOrderId)) {
|
|
140
|
+
for (const localOrderId of record.clientOrderIdIndex.get(
|
|
141
|
+
update.clientOrderId,
|
|
142
|
+
) ?? []) {
|
|
143
|
+
const snapshot = getSnapshotByLocalOrderId(record, localOrderId);
|
|
144
|
+
if (snapshot && shouldMatchStoredOrderIdentity(snapshot, update)) {
|
|
145
|
+
return { localOrderId, source: "provisional" };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function getSnapshotsForOrderId(
|
|
154
|
+
record: OrderRecord,
|
|
155
|
+
orderId: string,
|
|
156
|
+
): OrderSnapshot[] {
|
|
157
|
+
return getSnapshotsForLocalOrderIds(
|
|
158
|
+
record,
|
|
159
|
+
record.orderIdOnlyIndex.get(orderId),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function getSnapshotsForClientOrderId(
|
|
164
|
+
record: OrderRecord,
|
|
165
|
+
clientOrderId: string,
|
|
166
|
+
): OrderSnapshot[] {
|
|
167
|
+
return getSnapshotsForLocalOrderIds(
|
|
168
|
+
record,
|
|
169
|
+
record.clientOrderIdIndex.get(clientOrderId),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function getSnapshotsForLocalOrderIds(
|
|
174
|
+
record: OrderRecord,
|
|
175
|
+
localOrderIds?: Iterable<string>,
|
|
176
|
+
): OrderSnapshot[] {
|
|
177
|
+
if (!localOrderIds) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const snapshots: OrderSnapshot[] = [];
|
|
182
|
+
for (const localOrderId of localOrderIds) {
|
|
183
|
+
const snapshot = getSnapshotByLocalOrderId(record, localOrderId);
|
|
184
|
+
if (snapshot) {
|
|
185
|
+
snapshots.push(snapshot);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return snapshots;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function getOpenOrderSnapshots(
|
|
193
|
+
record: OrderRecord,
|
|
194
|
+
symbol?: string,
|
|
195
|
+
): OrderSnapshot[] {
|
|
196
|
+
if (symbol) {
|
|
197
|
+
return [...(record.openOrders.get(symbol)?.values() ?? [])];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return getSnapshotsInTable(record.openOrders);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function getAllSnapshots(record: OrderRecord): OrderSnapshot[] {
|
|
204
|
+
return [
|
|
205
|
+
...getSnapshotsInTable(record.openOrders),
|
|
206
|
+
...getSnapshotsInTable(record.closedOrders),
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function getSnapshotsInTable(
|
|
211
|
+
table: Map<string, Map<string, OrderSnapshot>>,
|
|
212
|
+
): OrderSnapshot[] {
|
|
213
|
+
const snapshots: OrderSnapshot[] = [];
|
|
214
|
+
for (const symbolOrders of table.values()) {
|
|
215
|
+
snapshots.push(...symbolOrders.values());
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return snapshots;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function getSnapshotCount(record: OrderRecord): number {
|
|
222
|
+
return (
|
|
223
|
+
getSnapshotCountInTable(record.openOrders) +
|
|
224
|
+
getSnapshotCountInTable(record.closedOrders)
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function getSnapshotCountInTable(
|
|
229
|
+
table: Map<string, Map<string, OrderSnapshot>>,
|
|
230
|
+
): number {
|
|
231
|
+
let size = 0;
|
|
232
|
+
for (const symbolOrders of table.values()) {
|
|
233
|
+
size += symbolOrders.size;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return size;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function addLocalOrderIdToSetIndex(
|
|
240
|
+
index: Map<string, Set<string>>,
|
|
241
|
+
key: string,
|
|
242
|
+
localOrderId: string,
|
|
243
|
+
): void {
|
|
244
|
+
const localOrderIds = index.get(key);
|
|
245
|
+
if (localOrderIds) {
|
|
246
|
+
localOrderIds.add(localOrderId);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
index.set(key, new Set([localOrderId]));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function removeLocalOrderIdFromSetIndex(
|
|
254
|
+
index: Map<string, Set<string>>,
|
|
255
|
+
key: string,
|
|
256
|
+
localOrderId: string,
|
|
257
|
+
): void {
|
|
258
|
+
const localOrderIds = index.get(key);
|
|
259
|
+
if (!localOrderIds) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
localOrderIds.delete(localOrderId);
|
|
264
|
+
|
|
265
|
+
if (localOrderIds.size === 0) {
|
|
266
|
+
index.delete(key);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function selectLatestSnapshot(
|
|
271
|
+
snapshots: OrderSnapshot[],
|
|
272
|
+
): OrderSnapshot | undefined {
|
|
273
|
+
let latest: OrderSnapshot | undefined;
|
|
274
|
+
for (const snapshot of snapshots) {
|
|
275
|
+
if (!latest) {
|
|
276
|
+
latest = snapshot;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const snapshotOpen = isOpenOrder(snapshot);
|
|
281
|
+
const latestOpen = isOpenOrder(latest);
|
|
282
|
+
if (snapshotOpen !== latestOpen) {
|
|
283
|
+
// Open candidate has absolute priority: current active order takes
|
|
284
|
+
// precedence over historical terminal state (when clientOrderId is
|
|
285
|
+
// reused, the old order is already closed).
|
|
286
|
+
if (snapshotOpen) {
|
|
287
|
+
latest = snapshot;
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Both open or both closed: take the latest by updatedAt.
|
|
293
|
+
// seq must not be used -- seq is a per-order version number and is not
|
|
294
|
+
// comparable across orders (e.g. different orders that reuse a cid).
|
|
295
|
+
if (snapshot.updatedAt > latest.updatedAt) {
|
|
296
|
+
latest = snapshot;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return latest;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function isVenueClientOrderIdInUseForOpenOrder(
|
|
304
|
+
record: OrderRecord,
|
|
305
|
+
venueClientOrderId: string,
|
|
306
|
+
): boolean {
|
|
307
|
+
for (const localOrderId of record.clientOrderIdIndex.get(
|
|
308
|
+
venueClientOrderId,
|
|
309
|
+
) ?? []) {
|
|
310
|
+
const location = record.localOrderLocations.get(localOrderId);
|
|
311
|
+
if (location?.table === "open") {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function setSnapshot(
|
|
320
|
+
record: OrderRecord,
|
|
321
|
+
localOrderId: string,
|
|
322
|
+
snapshot: OrderSnapshot,
|
|
323
|
+
options: SetSnapshotOptions,
|
|
324
|
+
): SetSnapshotResult {
|
|
325
|
+
if (!snapshot.orderId && !snapshot.clientOrderId) {
|
|
326
|
+
return { trimmedSnapshots: [] };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const currentLocation =
|
|
330
|
+
options.previousLocation ?? record.localOrderLocations.get(localOrderId);
|
|
331
|
+
if (currentLocation) {
|
|
332
|
+
return moveSnapshot(record, currentLocation, localOrderId, snapshot, {
|
|
333
|
+
maxClosedOrdersPerSymbol: options.maxClosedOrdersPerSymbol,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return insertSnapshot(record, localOrderId, snapshot, {
|
|
338
|
+
maxClosedOrdersPerSymbol: options.maxClosedOrdersPerSymbol,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function insertSnapshot(
|
|
343
|
+
record: OrderRecord,
|
|
344
|
+
localOrderId: string,
|
|
345
|
+
snapshot: OrderSnapshot,
|
|
346
|
+
options: { maxClosedOrdersPerSymbol: number },
|
|
347
|
+
): SetSnapshotResult {
|
|
348
|
+
const existingLocation = record.localOrderLocations.get(localOrderId);
|
|
349
|
+
if (existingLocation) {
|
|
350
|
+
deleteSnapshot(record, existingLocation);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const location: OrderLocation = {
|
|
354
|
+
table: isOpenOrder(snapshot) ? "open" : "closed",
|
|
355
|
+
symbol: snapshot.symbol,
|
|
356
|
+
localOrderId,
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const table = getOrderTable(record, location.table);
|
|
360
|
+
const symbolOrders = getOrCreateSymbolOrders(table, location.symbol);
|
|
361
|
+
symbolOrders.set(localOrderId, snapshot);
|
|
362
|
+
record.localOrderLocations.set(localOrderId, location);
|
|
363
|
+
|
|
364
|
+
if (snapshot.orderId) {
|
|
365
|
+
const symbolIndex = getOrCreateOrderIdSymbolIndex(record, snapshot.symbol);
|
|
366
|
+
symbolIndex.set(snapshot.orderId, localOrderId);
|
|
367
|
+
addLocalOrderIdToSetIndex(
|
|
368
|
+
record.orderIdOnlyIndex,
|
|
369
|
+
snapshot.orderId,
|
|
370
|
+
localOrderId,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (snapshot.clientOrderId) {
|
|
375
|
+
addLocalOrderIdToSetIndex(
|
|
376
|
+
record.clientOrderIdIndex,
|
|
377
|
+
snapshot.clientOrderId,
|
|
378
|
+
localOrderId,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const trimmedSnapshots = trimClosedOrdersForSymbol(record, location, {
|
|
383
|
+
maxClosedOrdersPerSymbol: options.maxClosedOrdersPerSymbol,
|
|
384
|
+
});
|
|
385
|
+
return { location, trimmedSnapshots };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function deleteSnapshot(
|
|
389
|
+
record: OrderRecord,
|
|
390
|
+
location: OrderLocation,
|
|
391
|
+
): OrderSnapshot | undefined {
|
|
392
|
+
const snapshot = getSnapshotAtLocation(record, location);
|
|
393
|
+
if (!snapshot) {
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const table = getOrderTable(record, location.table);
|
|
398
|
+
const symbolOrders = table.get(location.symbol);
|
|
399
|
+
symbolOrders?.delete(location.localOrderId);
|
|
400
|
+
if (symbolOrders?.size === 0) {
|
|
401
|
+
table.delete(location.symbol);
|
|
402
|
+
}
|
|
403
|
+
record.localOrderLocations.delete(location.localOrderId);
|
|
404
|
+
|
|
405
|
+
if (snapshot.orderId) {
|
|
406
|
+
const symbolIndex = record.orderIdIndex.get(location.symbol);
|
|
407
|
+
if (
|
|
408
|
+
symbolIndex?.get(snapshot.orderId) &&
|
|
409
|
+
symbolIndex.get(snapshot.orderId) === location.localOrderId
|
|
410
|
+
) {
|
|
411
|
+
symbolIndex.delete(snapshot.orderId);
|
|
412
|
+
}
|
|
413
|
+
if (symbolIndex?.size === 0) {
|
|
414
|
+
record.orderIdIndex.delete(location.symbol);
|
|
415
|
+
}
|
|
416
|
+
removeLocalOrderIdFromSetIndex(
|
|
417
|
+
record.orderIdOnlyIndex,
|
|
418
|
+
snapshot.orderId,
|
|
419
|
+
location.localOrderId,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (snapshot.clientOrderId) {
|
|
424
|
+
removeLocalOrderIdFromSetIndex(
|
|
425
|
+
record.clientOrderIdIndex,
|
|
426
|
+
snapshot.clientOrderId,
|
|
427
|
+
location.localOrderId,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return snapshot;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function moveSnapshot(
|
|
435
|
+
record: OrderRecord,
|
|
436
|
+
previousLocation: OrderLocation,
|
|
437
|
+
localOrderId: string,
|
|
438
|
+
snapshot: OrderSnapshot,
|
|
439
|
+
options: { maxClosedOrdersPerSymbol: number },
|
|
440
|
+
): SetSnapshotResult {
|
|
441
|
+
deleteSnapshot(record, previousLocation);
|
|
442
|
+
return insertSnapshot(record, localOrderId, snapshot, {
|
|
443
|
+
maxClosedOrdersPerSymbol: options.maxClosedOrdersPerSymbol,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function trimClosedOrdersForSymbol(
|
|
448
|
+
record: OrderRecord,
|
|
449
|
+
location: OrderLocation,
|
|
450
|
+
options: { maxClosedOrdersPerSymbol: number },
|
|
451
|
+
): OrderSnapshot[] {
|
|
452
|
+
if (location.table !== "closed") {
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let symbolOrders = record.closedOrders.get(location.symbol);
|
|
457
|
+
if (!symbolOrders || symbolOrders.size <= options.maxClosedOrdersPerSymbol) {
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const trimmedSnapshots: OrderSnapshot[] = [];
|
|
462
|
+
const trimBatchSize = Math.max(
|
|
463
|
+
1,
|
|
464
|
+
Math.floor(options.maxClosedOrdersPerSymbol / 10),
|
|
465
|
+
);
|
|
466
|
+
while (symbolOrders && symbolOrders.size > options.maxClosedOrdersPerSymbol) {
|
|
467
|
+
const keys = symbolOrders.keys();
|
|
468
|
+
for (let deleted = 0; deleted < trimBatchSize; deleted += 1) {
|
|
469
|
+
const next = keys.next();
|
|
470
|
+
if (next.done) {
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
const deletedSnapshot = deleteSnapshot(record, {
|
|
474
|
+
table: "closed",
|
|
475
|
+
symbol: location.symbol,
|
|
476
|
+
localOrderId: next.value,
|
|
477
|
+
});
|
|
478
|
+
if (deletedSnapshot) {
|
|
479
|
+
trimmedSnapshots.push(deletedSnapshot);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
symbolOrders = record.closedOrders.get(location.symbol);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return trimmedSnapshots;
|
|
486
|
+
}
|