@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.
- package/CHANGELOG.md +176 -0
- package/docs/api.md +4 -2
- package/package.json +7 -2
- package/src/adapters/binance/private-adapter.ts +210 -37
- 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
|
@@ -28,6 +28,17 @@ export function shouldApplyWatermarkedUpdate(
|
|
|
28
28
|
const graceMs = options.graceMs ?? CROSS_CLOCK_WATERMARK_GRACE_MS;
|
|
29
29
|
const requestStartedAt = options.requestStartedAt;
|
|
30
30
|
|
|
31
|
+
if (options.source === "command" && requestStartedAt !== undefined) {
|
|
32
|
+
const hasMissingExchangeTs =
|
|
33
|
+
current.exchangeTs === undefined || incoming.exchangeTs === undefined;
|
|
34
|
+
if (hasMissingExchangeTs) {
|
|
35
|
+
return (
|
|
36
|
+
current.receivedAt <= requestStartedAt &&
|
|
37
|
+
incoming.receivedAt >= current.receivedAt
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
if (
|
|
32
43
|
options.source === "rest" &&
|
|
33
44
|
requestStartedAt !== undefined &&
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { OrderDataStatus, Venue } from "../../types/index.ts";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_MAX_CLOSED_ORDERS_PER_SYMBOL = 500;
|
|
4
|
+
|
|
5
|
+
export function cloneOrderStatus(status: OrderDataStatus): OrderDataStatus {
|
|
6
|
+
return { ...status };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createOrderDataStatus(
|
|
10
|
+
accountId: string,
|
|
11
|
+
venue: Venue,
|
|
12
|
+
activity: "active" | "inactive",
|
|
13
|
+
): OrderDataStatus {
|
|
14
|
+
return {
|
|
15
|
+
accountId,
|
|
16
|
+
venue,
|
|
17
|
+
activity,
|
|
18
|
+
ready: false,
|
|
19
|
+
runtimeStatus: activity === "active" ? "bootstrap_pending" : "stopped",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizeMaxClosedOrdersPerSymbol(
|
|
24
|
+
value: number | undefined,
|
|
25
|
+
): number {
|
|
26
|
+
return value !== undefined && Number.isInteger(value) && value > 0
|
|
27
|
+
? value
|
|
28
|
+
: DEFAULT_MAX_CLOSED_ORDERS_PER_SYMBOL;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function successfulStatus(
|
|
32
|
+
status: OrderDataStatus,
|
|
33
|
+
options: {
|
|
34
|
+
ready?: boolean;
|
|
35
|
+
lastReceivedAt?: number;
|
|
36
|
+
lastReadyAt?: number;
|
|
37
|
+
preserveStatus?: boolean;
|
|
38
|
+
},
|
|
39
|
+
): OrderDataStatus {
|
|
40
|
+
const preservesStreamState =
|
|
41
|
+
options.preserveStatus &&
|
|
42
|
+
(status.runtimeStatus === "reconnecting" ||
|
|
43
|
+
status.reason === "ws_disconnected" ||
|
|
44
|
+
status.reason === "heartbeat_timeout");
|
|
45
|
+
const ready = options.ready ?? true;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
...status,
|
|
49
|
+
activity: "active",
|
|
50
|
+
ready,
|
|
51
|
+
runtimeStatus: preservesStreamState ? status.runtimeStatus : "healthy",
|
|
52
|
+
reason: preservesStreamState ? status.reason : undefined,
|
|
53
|
+
lastReceivedAt: options.lastReceivedAt ?? status.lastReceivedAt,
|
|
54
|
+
lastReadyAt: ready
|
|
55
|
+
? (options.lastReadyAt ??
|
|
56
|
+
(options.preserveStatus ? status.lastReadyAt : undefined) ??
|
|
57
|
+
Date.now())
|
|
58
|
+
: status.lastReadyAt,
|
|
59
|
+
inactiveSince: undefined,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -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
|
+
}
|