@imbingox/acex 0.4.0-beta.1 → 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 +4 -3
- package/docs/api.md +474 -1002
- package/package.json +1 -1
- package/src/adapters/binance/adapter.ts +19 -1
- package/src/adapters/binance/market-catalog.ts +83 -12
- 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/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 +227 -23
- package/src/managers/market-manager.ts +224 -34
- package/src/managers/order-manager.ts +791 -76
- package/src/types/client.ts +1 -0
- package/src/types/market.ts +25 -0
- package/src/types/order.ts +1 -0
- 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
|
+
}
|
|
@@ -17,6 +17,10 @@ import type {
|
|
|
17
17
|
import { AsyncEventBus } from "../internal/async-event-bus.ts";
|
|
18
18
|
import { toCanonical } from "../internal/decimal.ts";
|
|
19
19
|
import { matchesAccountFilter } from "../internal/filters.ts";
|
|
20
|
+
import {
|
|
21
|
+
canDeleteMissingFromSnapshot,
|
|
22
|
+
shouldApplyWatermarkedUpdate,
|
|
23
|
+
} from "../internal/watermark.ts";
|
|
20
24
|
import type {
|
|
21
25
|
AccountDataStatus,
|
|
22
26
|
AccountEvent,
|
|
@@ -61,6 +65,46 @@ function isZeroDecimal(value: string): boolean {
|
|
|
61
65
|
return new BigNumber(value).isZero();
|
|
62
66
|
}
|
|
63
67
|
|
|
68
|
+
function isZeroBalance(balance: BalanceSnapshot): boolean {
|
|
69
|
+
return (
|
|
70
|
+
isZeroDecimal(balance.free) &&
|
|
71
|
+
isZeroDecimal(balance.used) &&
|
|
72
|
+
isZeroDecimal(balance.total)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function successfulStatus(
|
|
77
|
+
status: AccountDataStatus,
|
|
78
|
+
options: {
|
|
79
|
+
ready?: boolean;
|
|
80
|
+
lastReceivedAt?: number;
|
|
81
|
+
lastReadyAt?: number;
|
|
82
|
+
preserveStatus?: boolean;
|
|
83
|
+
},
|
|
84
|
+
): AccountDataStatus {
|
|
85
|
+
const preservesStreamState =
|
|
86
|
+
options.preserveStatus &&
|
|
87
|
+
(status.runtimeStatus === "reconnecting" ||
|
|
88
|
+
status.reason === "ws_disconnected" ||
|
|
89
|
+
status.reason === "heartbeat_timeout");
|
|
90
|
+
const ready = options.ready ?? true;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
...status,
|
|
94
|
+
activity: "active",
|
|
95
|
+
ready,
|
|
96
|
+
runtimeStatus: preservesStreamState ? status.runtimeStatus : "healthy",
|
|
97
|
+
reason: preservesStreamState ? status.reason : undefined,
|
|
98
|
+
lastReceivedAt: options.lastReceivedAt ?? status.lastReceivedAt,
|
|
99
|
+
lastReadyAt: ready
|
|
100
|
+
? (options.lastReadyAt ??
|
|
101
|
+
(options.preserveStatus ? status.lastReadyAt : undefined) ??
|
|
102
|
+
Date.now())
|
|
103
|
+
: status.lastReadyAt,
|
|
104
|
+
inactiveSince: undefined,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
64
108
|
export class AccountManagerImpl
|
|
65
109
|
implements
|
|
66
110
|
AccountManager,
|
|
@@ -282,7 +326,7 @@ export class AccountManagerImpl
|
|
|
282
326
|
accountId: string,
|
|
283
327
|
venue: Venue,
|
|
284
328
|
update: RawAccountUpdate,
|
|
285
|
-
options: { preserveStatus?: boolean } = {},
|
|
329
|
+
options: { preserveStatus?: boolean; requestStartedAt?: number } = {},
|
|
286
330
|
): void {
|
|
287
331
|
const record = this.getOrCreateRecord(accountId, venue);
|
|
288
332
|
if (!record.subscribed) {
|
|
@@ -300,7 +344,17 @@ export class AccountManagerImpl
|
|
|
300
344
|
);
|
|
301
345
|
let risk = previous.risk;
|
|
302
346
|
|
|
347
|
+
let latestAppliedAt = 0;
|
|
303
348
|
for (const balance of update.balances ?? []) {
|
|
349
|
+
if (
|
|
350
|
+
!shouldApplyWatermarkedUpdate(balances[balance.asset], balance, {
|
|
351
|
+
requestStartedAt: options.requestStartedAt,
|
|
352
|
+
source: options.requestStartedAt === undefined ? "stream" : "rest",
|
|
353
|
+
})
|
|
354
|
+
) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
304
358
|
const nextBalance = this.createBalance(
|
|
305
359
|
accountId,
|
|
306
360
|
venue,
|
|
@@ -308,6 +362,7 @@ export class AccountManagerImpl
|
|
|
308
362
|
balances[balance.asset],
|
|
309
363
|
);
|
|
310
364
|
balances[balance.asset] = nextBalance;
|
|
365
|
+
latestAppliedAt = Math.max(latestAppliedAt, nextBalance.receivedAt);
|
|
311
366
|
this.accountBus.publish({
|
|
312
367
|
type: "balance.updated",
|
|
313
368
|
accountId,
|
|
@@ -320,6 +375,15 @@ export class AccountManagerImpl
|
|
|
320
375
|
|
|
321
376
|
for (const position of update.positions ?? []) {
|
|
322
377
|
const key = positionKey(position.symbol, position.side);
|
|
378
|
+
if (
|
|
379
|
+
!shouldApplyWatermarkedUpdate(positions.get(key), position, {
|
|
380
|
+
requestStartedAt: options.requestStartedAt,
|
|
381
|
+
source: options.requestStartedAt === undefined ? "stream" : "rest",
|
|
382
|
+
})
|
|
383
|
+
) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
|
|
323
387
|
const nextPosition = this.createPosition(
|
|
324
388
|
accountId,
|
|
325
389
|
venue,
|
|
@@ -333,6 +397,7 @@ export class AccountManagerImpl
|
|
|
333
397
|
positions.set(key, nextPosition);
|
|
334
398
|
}
|
|
335
399
|
|
|
400
|
+
latestAppliedAt = Math.max(latestAppliedAt, nextPosition.receivedAt);
|
|
336
401
|
this.accountBus.publish({
|
|
337
402
|
type: "position.updated",
|
|
338
403
|
accountId,
|
|
@@ -343,8 +408,15 @@ export class AccountManagerImpl
|
|
|
343
408
|
});
|
|
344
409
|
}
|
|
345
410
|
|
|
346
|
-
if (
|
|
411
|
+
if (
|
|
412
|
+
update.risk &&
|
|
413
|
+
shouldApplyWatermarkedUpdate(previous.risk, update.risk, {
|
|
414
|
+
requestStartedAt: options.requestStartedAt,
|
|
415
|
+
source: options.requestStartedAt === undefined ? "stream" : "rest",
|
|
416
|
+
})
|
|
417
|
+
) {
|
|
347
418
|
risk = this.createRisk(accountId, venue, update.risk, previous.risk);
|
|
419
|
+
latestAppliedAt = Math.max(latestAppliedAt, risk.receivedAt);
|
|
348
420
|
this.accountBus.publish({
|
|
349
421
|
type: "risk.updated",
|
|
350
422
|
accountId,
|
|
@@ -354,34 +426,166 @@ export class AccountManagerImpl
|
|
|
354
426
|
});
|
|
355
427
|
}
|
|
356
428
|
|
|
429
|
+
if (latestAppliedAt === 0) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
357
433
|
record.snapshot = {
|
|
358
434
|
accountId,
|
|
359
435
|
venue,
|
|
360
436
|
balances,
|
|
361
437
|
positions: [...positions.values()],
|
|
362
438
|
risk,
|
|
363
|
-
exchangeTs:
|
|
364
|
-
|
|
365
|
-
|
|
439
|
+
exchangeTs:
|
|
440
|
+
update.exchangeTs === undefined
|
|
441
|
+
? previous.exchangeTs
|
|
442
|
+
: update.exchangeTs,
|
|
443
|
+
receivedAt: latestAppliedAt,
|
|
444
|
+
updatedAt: latestAppliedAt,
|
|
366
445
|
};
|
|
367
|
-
record.status =
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
446
|
+
record.status = successfulStatus(record.status, {
|
|
447
|
+
preserveStatus: options.preserveStatus,
|
|
448
|
+
lastReceivedAt: latestAppliedAt,
|
|
449
|
+
lastReadyAt: latestAppliedAt,
|
|
450
|
+
});
|
|
451
|
+
this.publishStatus(record);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
onPrivateAccountReconcile(
|
|
455
|
+
accountId: string,
|
|
456
|
+
venue: Venue,
|
|
457
|
+
snapshot: RawAccountBootstrap,
|
|
458
|
+
options: { requestStartedAt: number; preserveStatus?: boolean },
|
|
459
|
+
): void {
|
|
460
|
+
const record = this.getOrCreateRecord(accountId, venue);
|
|
461
|
+
if (!record.subscribed) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const previous =
|
|
466
|
+
record.snapshot ?? this.createEmptySnapshot(accountId, venue);
|
|
467
|
+
const balances = { ...previous.balances };
|
|
468
|
+
const positions = new Map(
|
|
469
|
+
previous.positions.map((position) => [
|
|
470
|
+
positionKey(position.symbol, position.side),
|
|
471
|
+
position,
|
|
472
|
+
]),
|
|
473
|
+
);
|
|
474
|
+
let risk = previous.risk;
|
|
475
|
+
|
|
476
|
+
const incomingBalanceAssets = new Set<string>();
|
|
477
|
+
for (const balance of snapshot.balances) {
|
|
478
|
+
incomingBalanceAssets.add(balance.asset);
|
|
479
|
+
if (
|
|
480
|
+
!shouldApplyWatermarkedUpdate(balances[balance.asset], balance, {
|
|
481
|
+
requestStartedAt: options.requestStartedAt,
|
|
482
|
+
source: "rest",
|
|
483
|
+
})
|
|
484
|
+
) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const nextBalance = this.createBalance(
|
|
489
|
+
accountId,
|
|
490
|
+
venue,
|
|
491
|
+
balance,
|
|
492
|
+
balances[balance.asset],
|
|
493
|
+
);
|
|
494
|
+
if (isZeroBalance(nextBalance)) {
|
|
495
|
+
delete balances[balance.asset];
|
|
496
|
+
} else {
|
|
497
|
+
balances[balance.asset] = nextBalance;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
for (const [asset, balance] of Object.entries(balances)) {
|
|
502
|
+
if (
|
|
503
|
+
(!incomingBalanceAssets.has(asset) || isZeroBalance(balance)) &&
|
|
504
|
+
canDeleteMissingFromSnapshot(balance, {
|
|
505
|
+
requestStartedAt: options.requestStartedAt,
|
|
506
|
+
snapshotExchangeTs: snapshot.exchangeTs,
|
|
507
|
+
})
|
|
508
|
+
) {
|
|
509
|
+
delete balances[asset];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const incomingPositionKeys = new Set<string>();
|
|
514
|
+
for (const position of snapshot.positions) {
|
|
515
|
+
const key = positionKey(position.symbol, position.side);
|
|
516
|
+
incomingPositionKeys.add(key);
|
|
517
|
+
if (
|
|
518
|
+
!shouldApplyWatermarkedUpdate(positions.get(key), position, {
|
|
519
|
+
requestStartedAt: options.requestStartedAt,
|
|
520
|
+
source: "rest",
|
|
521
|
+
})
|
|
522
|
+
) {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const nextPosition = this.createPosition(
|
|
527
|
+
accountId,
|
|
528
|
+
venue,
|
|
529
|
+
position,
|
|
530
|
+
positions.get(key),
|
|
531
|
+
);
|
|
532
|
+
if (isZeroDecimal(nextPosition.size)) {
|
|
533
|
+
positions.delete(key);
|
|
534
|
+
} else {
|
|
535
|
+
positions.set(key, nextPosition);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
for (const [key, position] of positions.entries()) {
|
|
540
|
+
if (
|
|
541
|
+
!incomingPositionKeys.has(key) &&
|
|
542
|
+
canDeleteMissingFromSnapshot(position, {
|
|
543
|
+
requestStartedAt: options.requestStartedAt,
|
|
544
|
+
snapshotExchangeTs: snapshot.exchangeTs,
|
|
545
|
+
})
|
|
546
|
+
) {
|
|
547
|
+
positions.delete(key);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (
|
|
552
|
+
snapshot.risk &&
|
|
553
|
+
shouldApplyWatermarkedUpdate(previous.risk, snapshot.risk, {
|
|
554
|
+
requestStartedAt: options.requestStartedAt,
|
|
555
|
+
source: "rest",
|
|
556
|
+
})
|
|
557
|
+
) {
|
|
558
|
+
risk = this.createRisk(accountId, venue, snapshot.risk, previous.risk);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
record.snapshot = {
|
|
562
|
+
accountId,
|
|
563
|
+
venue,
|
|
564
|
+
balances,
|
|
565
|
+
positions: [...positions.values()],
|
|
566
|
+
risk,
|
|
567
|
+
exchangeTs:
|
|
568
|
+
snapshot.exchangeTs === undefined
|
|
569
|
+
? previous.exchangeTs
|
|
570
|
+
: snapshot.exchangeTs,
|
|
571
|
+
receivedAt: snapshot.receivedAt,
|
|
572
|
+
updatedAt: snapshot.receivedAt,
|
|
573
|
+
};
|
|
574
|
+
record.status = successfulStatus(record.status, {
|
|
575
|
+
preserveStatus: options.preserveStatus,
|
|
576
|
+
lastReceivedAt: snapshot.receivedAt,
|
|
577
|
+
lastReadyAt: snapshot.receivedAt,
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
const event: AccountSnapshotReplacedEvent = {
|
|
581
|
+
type: "account.snapshot_replaced",
|
|
582
|
+
accountId,
|
|
583
|
+
venue,
|
|
584
|
+
snapshot: record.snapshot,
|
|
585
|
+
ts: this.context.now(),
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
this.accountBus.publish(event);
|
|
385
589
|
this.publishStatus(record);
|
|
386
590
|
}
|
|
387
591
|
|