@imbingox/acex 0.3.1-beta.0 → 0.4.0-beta.10
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/README.md +11 -10
- package/docs/api.md +502 -1030
- package/package.json +1 -1
- package/src/adapters/binance/adapter.ts +19 -1
- package/src/adapters/binance/market-catalog.ts +93 -22
- package/src/adapters/binance/private-adapter.ts +302 -59
- package/src/adapters/binance/rate-limit.ts +47 -0
- package/src/adapters/binance/server-time.ts +106 -0
- package/src/adapters/juplend/private-adapter.ts +97 -68
- package/src/adapters/types.ts +25 -1
- package/src/client/context.ts +26 -9
- package/src/client/private-subscription-coordinator.ts +898 -63
- package/src/client/runtime.ts +49 -11
- package/src/client/venue-capabilities.ts +1 -0
- package/src/errors.ts +156 -2
- package/src/index.ts +8 -1
- package/src/internal/decimal.ts +19 -0
- package/src/internal/http-client.ts +608 -0
- package/src/internal/rate-limiter.ts +181 -0
- package/src/internal/watermark.ts +83 -0
- package/src/managers/account-manager.ts +267 -55
- package/src/managers/market-manager.ts +261 -60
- package/src/managers/order-manager.ts +798 -84
- package/src/types/account.ts +27 -28
- package/src/types/client.ts +1 -0
- package/src/types/market.ts +37 -12
- package/src/types/order.ts +7 -7
- package/src/types/shared.ts +66 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RateLimiter,
|
|
3
|
+
RateLimitRequestContext,
|
|
4
|
+
RateLimitResponseContext,
|
|
5
|
+
RateLimitScope,
|
|
6
|
+
RateLimitSnapshot,
|
|
7
|
+
RateLimitTransportErrorContext,
|
|
8
|
+
RateLimitUsage,
|
|
9
|
+
} from "../types/index.ts";
|
|
10
|
+
|
|
11
|
+
interface ReactiveRateLimiterOptions {
|
|
12
|
+
readonly now?: () => number;
|
|
13
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
14
|
+
readonly defaultRateLimitMs?: number;
|
|
15
|
+
readonly defaultBanMs?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface RateLimitState {
|
|
19
|
+
usage?: RateLimitUsage;
|
|
20
|
+
blockedUntil?: number;
|
|
21
|
+
retryAfterMs?: number;
|
|
22
|
+
state: RateLimitSnapshot["state"];
|
|
23
|
+
updatedAt?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_RATE_LIMIT_MS = 0;
|
|
27
|
+
const DEFAULT_BAN_MS = 60_000;
|
|
28
|
+
|
|
29
|
+
export class ReactiveRateLimiter implements RateLimiter {
|
|
30
|
+
private readonly now: () => number;
|
|
31
|
+
private readonly sleep: (ms: number) => Promise<void>;
|
|
32
|
+
private readonly defaultRateLimitMs: number;
|
|
33
|
+
private readonly defaultBanMs: number;
|
|
34
|
+
private readonly states = new Map<string, RateLimitState>();
|
|
35
|
+
|
|
36
|
+
constructor(options: ReactiveRateLimiterOptions = {}) {
|
|
37
|
+
this.now = options.now ?? Date.now;
|
|
38
|
+
this.sleep = options.sleep ?? defaultSleep;
|
|
39
|
+
this.defaultRateLimitMs =
|
|
40
|
+
options.defaultRateLimitMs ?? DEFAULT_RATE_LIMIT_MS;
|
|
41
|
+
this.defaultBanMs = options.defaultBanMs ?? DEFAULT_BAN_MS;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async beforeRequest(ctx: RateLimitRequestContext): Promise<void> {
|
|
45
|
+
const snapshot = this.getSnapshot(ctx.scope);
|
|
46
|
+
if (!snapshot?.blockedUntil || snapshot.blockedUntil <= this.now()) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await this.sleep(Math.max(0, snapshot.blockedUntil - this.now()));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
afterResponse(
|
|
54
|
+
ctx: RateLimitRequestContext,
|
|
55
|
+
response: RateLimitResponseContext,
|
|
56
|
+
): void {
|
|
57
|
+
if (response.usage) {
|
|
58
|
+
const existing = this.getState(ctx.scope);
|
|
59
|
+
const hasActiveBlock =
|
|
60
|
+
existing?.blockedUntil !== undefined &&
|
|
61
|
+
existing.blockedUntil > this.now();
|
|
62
|
+
this.updateState(ctx.scope, {
|
|
63
|
+
usage: cloneUsage(response.usage),
|
|
64
|
+
state: hasActiveBlock ? existing.state : "ok",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onTransportError(
|
|
70
|
+
ctx: RateLimitRequestContext,
|
|
71
|
+
error: RateLimitTransportErrorContext,
|
|
72
|
+
): void {
|
|
73
|
+
if (error.usage) {
|
|
74
|
+
this.updateState(ctx.scope, {
|
|
75
|
+
usage: cloneUsage(error.usage),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (error.status !== 429 && error.status !== 418) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const now = this.now();
|
|
84
|
+
const isBan = error.status === 418;
|
|
85
|
+
const retryAfterMs =
|
|
86
|
+
error.retryAfterMs ??
|
|
87
|
+
(isBan ? this.defaultBanMs : this.defaultRateLimitMs);
|
|
88
|
+
const blockedUntil =
|
|
89
|
+
retryAfterMs > 0
|
|
90
|
+
? now + retryAfterMs
|
|
91
|
+
: this.getState(ctx.scope)?.blockedUntil;
|
|
92
|
+
|
|
93
|
+
this.updateState(ctx.scope, {
|
|
94
|
+
blockedUntil,
|
|
95
|
+
retryAfterMs,
|
|
96
|
+
state: isBan ? "banned" : "rate_limited",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getSnapshot(scope: RateLimitScope): RateLimitSnapshot | undefined {
|
|
101
|
+
const state = this.getState(scope);
|
|
102
|
+
if (!state) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const now = this.now();
|
|
107
|
+
const blockedUntil =
|
|
108
|
+
state.blockedUntil !== undefined && state.blockedUntil > now
|
|
109
|
+
? state.blockedUntil
|
|
110
|
+
: undefined;
|
|
111
|
+
const runtimeState =
|
|
112
|
+
blockedUntil === undefined && state.state !== "ok" ? "ok" : state.state;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
scope: { ...scope },
|
|
116
|
+
usage: state.usage ? cloneUsage(state.usage) : undefined,
|
|
117
|
+
blockedUntil,
|
|
118
|
+
retryAfterMs: blockedUntil ? state.retryAfterMs : undefined,
|
|
119
|
+
state: runtimeState,
|
|
120
|
+
updatedAt: state.updatedAt,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private getState(scope: RateLimitScope): RateLimitState | undefined {
|
|
125
|
+
return this.states.get(scopeKey(scope));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private updateState(
|
|
129
|
+
scope: RateLimitScope,
|
|
130
|
+
patch: Partial<RateLimitState>,
|
|
131
|
+
): void {
|
|
132
|
+
const existing = this.getState(scope);
|
|
133
|
+
const nextBlockedUntil = maxOptional(
|
|
134
|
+
existing?.blockedUntil,
|
|
135
|
+
patch.blockedUntil,
|
|
136
|
+
);
|
|
137
|
+
const nextState =
|
|
138
|
+
patch.state ??
|
|
139
|
+
(nextBlockedUntil !== undefined
|
|
140
|
+
? (existing?.state ?? "ok")
|
|
141
|
+
: existing?.state);
|
|
142
|
+
|
|
143
|
+
this.states.set(scopeKey(scope), {
|
|
144
|
+
usage: patch.usage ?? existing?.usage,
|
|
145
|
+
blockedUntil: nextBlockedUntil,
|
|
146
|
+
retryAfterMs: patch.retryAfterMs ?? existing?.retryAfterMs,
|
|
147
|
+
state: nextState ?? "ok",
|
|
148
|
+
updatedAt: this.now(),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function scopeKey(scope: RateLimitScope): string {
|
|
154
|
+
return [scope.venue, scope.accountId ?? "", scope.endpointKey].join("\0");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function cloneUsage(usage: RateLimitUsage): RateLimitUsage {
|
|
158
|
+
return {
|
|
159
|
+
weight: usage.weight ? { ...usage.weight } : undefined,
|
|
160
|
+
orderCount: usage.orderCount ? { ...usage.orderCount } : undefined,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function maxOptional(
|
|
165
|
+
left: number | undefined,
|
|
166
|
+
right: number | undefined,
|
|
167
|
+
): number | undefined {
|
|
168
|
+
if (left === undefined) {
|
|
169
|
+
return right;
|
|
170
|
+
}
|
|
171
|
+
if (right === undefined) {
|
|
172
|
+
return left;
|
|
173
|
+
}
|
|
174
|
+
return Math.max(left, right);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function defaultSleep(ms: number): Promise<void> {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
setTimeout(resolve, ms);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export const CROSS_CLOCK_WATERMARK_GRACE_MS = 10_000;
|
|
2
|
+
|
|
3
|
+
export interface WatermarkedRecord {
|
|
4
|
+
exchangeTs?: number;
|
|
5
|
+
receivedAt: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface WatermarkApplyOptions {
|
|
9
|
+
requestStartedAt?: number;
|
|
10
|
+
source?: "command" | "rest" | "stream";
|
|
11
|
+
graceMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SnapshotDeletionGuard {
|
|
15
|
+
requestStartedAt: number;
|
|
16
|
+
snapshotExchangeTs?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function shouldApplyWatermarkedUpdate(
|
|
20
|
+
current: WatermarkedRecord | undefined,
|
|
21
|
+
incoming: WatermarkedRecord,
|
|
22
|
+
options: WatermarkApplyOptions = {},
|
|
23
|
+
): boolean {
|
|
24
|
+
if (!current) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const graceMs = options.graceMs ?? CROSS_CLOCK_WATERMARK_GRACE_MS;
|
|
29
|
+
const requestStartedAt = options.requestStartedAt;
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
options.source === "rest" &&
|
|
33
|
+
requestStartedAt !== undefined &&
|
|
34
|
+
current.receivedAt > requestStartedAt &&
|
|
35
|
+
(current.exchangeTs === undefined || incoming.exchangeTs === undefined)
|
|
36
|
+
) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (current.exchangeTs !== undefined && incoming.exchangeTs !== undefined) {
|
|
41
|
+
if (incoming.exchangeTs < current.exchangeTs) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (incoming.exchangeTs > current.exchangeTs) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return incoming.receivedAt >= current.receivedAt;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (current.exchangeTs !== undefined) {
|
|
52
|
+
if (incoming.receivedAt < current.exchangeTs + graceMs) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return incoming.receivedAt >= current.receivedAt;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (incoming.exchangeTs !== undefined) {
|
|
60
|
+
if (incoming.exchangeTs < current.receivedAt - graceMs) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return incoming.receivedAt >= current.receivedAt;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return incoming.receivedAt >= current.receivedAt;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function canDeleteMissingFromSnapshot(
|
|
71
|
+
current: WatermarkedRecord,
|
|
72
|
+
guard: SnapshotDeletionGuard,
|
|
73
|
+
): boolean {
|
|
74
|
+
if (current.receivedAt > guard.requestStartedAt) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return !(
|
|
79
|
+
current.exchangeTs !== undefined &&
|
|
80
|
+
guard.snapshotExchangeTs !== undefined &&
|
|
81
|
+
current.exchangeTs > guard.snapshotExchangeTs
|
|
82
|
+
);
|
|
83
|
+
}
|