@imbingox/acex 0.4.0-beta.12 → 0.4.0-beta.14

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.
@@ -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
+ }