@imbingox/acex 0.3.0-beta.3 → 0.3.0-beta.5
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 -1
- package/docs/api.md +16 -6
- package/package.json +2 -2
- package/src/adapters/binance/private-adapter.ts +80 -3
- package/src/adapters/juplend/private-adapter.ts +2 -1
- package/src/adapters/types.ts +7 -1
- package/src/client/context.ts +1 -0
- package/src/client/private-subscription-coordinator.ts +173 -0
- package/src/managers/account-manager.ts +31 -14
- package/src/types/account.ts +3 -1
- package/src/types/shared.ts +3 -0
package/README.md
CHANGED
|
@@ -60,11 +60,14 @@ await client.stop();
|
|
|
60
60
|
|
|
61
61
|
### 同一个 client 同时使用 Binance + Juplend
|
|
62
62
|
|
|
63
|
-
`createClient({ account: { juplend: { pollIntervalMs } } })`
|
|
63
|
+
`createClient({ account: { binance: { riskPollIntervalMs }, juplend: { pollIntervalMs } } })` 只是分别配置 Binance 风险/仓位校准间隔和 Juplend 账户 polling 间隔,不代表这个 client 只能注册某个 venue。一个 `AcexClient` 可以同时注册 Binance 交易账户和 Juplend 借贷只读账户,用同一个 `AccountManager` 对比风险值。
|
|
64
64
|
|
|
65
65
|
```ts
|
|
66
66
|
const client = createClient({
|
|
67
67
|
account: {
|
|
68
|
+
binance: {
|
|
69
|
+
riskPollIntervalMs: 5_000,
|
|
70
|
+
},
|
|
68
71
|
juplend: {
|
|
69
72
|
pollIntervalMs: 30_000,
|
|
70
73
|
},
|
package/docs/api.md
CHANGED
|
@@ -64,11 +64,14 @@ for await (const event of client.market.events.l1BookUpdates({
|
|
|
64
64
|
await client.stop();
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
同一个 client 可以同时注册 Binance 交易账户和 Juplend 借贷只读账户。`account.juplend.pollIntervalMs`
|
|
67
|
+
同一个 client 可以同时注册 Binance 交易账户和 Juplend 借贷只读账户。`account.binance.riskPollIntervalMs` 只配置 Binance 风险/仓位校准间隔;`account.juplend.pollIntervalMs` 只配置 Juplend polling 间隔,不会把 client 限定为某个 venue:
|
|
68
68
|
|
|
69
69
|
```ts
|
|
70
70
|
const client = createClient({
|
|
71
71
|
account: {
|
|
72
|
+
binance: {
|
|
73
|
+
riskPollIntervalMs: 5_000,
|
|
74
|
+
},
|
|
72
75
|
juplend: {
|
|
73
76
|
pollIntervalMs: 30_000,
|
|
74
77
|
},
|
|
@@ -208,6 +211,9 @@ const client = createClient({
|
|
|
208
211
|
streamReconnectDelayMs: 1_000,
|
|
209
212
|
streamReconnectMaxDelayMs: 10_000,
|
|
210
213
|
listenKeyKeepAliveMs: 30 * 60_000,
|
|
214
|
+
binance: {
|
|
215
|
+
riskPollIntervalMs: 5_000,
|
|
216
|
+
},
|
|
211
217
|
juplend: {
|
|
212
218
|
pollIntervalMs: 30_000,
|
|
213
219
|
},
|
|
@@ -612,7 +618,9 @@ const btcPosition = client.account.getPosition({
|
|
|
612
618
|
const risk = client.account.getRiskSnapshot("main-binance");
|
|
613
619
|
```
|
|
614
620
|
|
|
615
|
-
所有数量字段(`free` / `used` / `total` / `size` / `entryPrice` / `
|
|
621
|
+
所有数量字段(`free` / `used` / `total` / `size` / `entryPrice` / `netEquity` / `riskEquity` / ...)都是 `BigNumber`。
|
|
622
|
+
|
|
623
|
+
`RiskSnapshot.netEquity` 表示不含风控折算的净资产价值;`riskEquity` 表示抵押系数或清算阈值折算后的风控净权益。Binance 使用 `actualEquity` / `accountEquity` 映射这两个字段;Juplend 使用 `totalCollateralUsd - totalDebtUsd` / `Σ(suppliedValue × liquidationThreshold) - totalDebtUsd`。
|
|
616
624
|
|
|
617
625
|
> **注意**:`AccountSnapshot.balances` 是 `Record<string, BalanceSnapshot>`,不是数组;需要数组视图用 `getBalances()`。
|
|
618
626
|
|
|
@@ -902,9 +910,6 @@ type PrivateRuntimeStatus =
|
|
|
902
910
|
type PrivateRuntimeReason =
|
|
903
911
|
| "credentials_missing" | "auth_failed" | "http_failed" | "rate_limited"
|
|
904
912
|
| "ws_disconnected" | "heartbeat_timeout" | "reconciling";
|
|
905
|
-
type PrivateRuntimeReason =
|
|
906
|
-
| "credentials_missing" | "auth_failed"
|
|
907
|
-
| "ws_disconnected" | "heartbeat_timeout" | "reconciling";
|
|
908
913
|
|
|
909
914
|
type OrderSide = "buy" | "sell";
|
|
910
915
|
type OrderStatus =
|
|
@@ -930,6 +935,9 @@ interface AccountRuntimeOptions {
|
|
|
930
935
|
streamReconnectDelayMs?: number;
|
|
931
936
|
streamReconnectMaxDelayMs?: number;
|
|
932
937
|
listenKeyKeepAliveMs?: number;
|
|
938
|
+
binance?: {
|
|
939
|
+
riskPollIntervalMs?: number; // 默认 5_000
|
|
940
|
+
};
|
|
933
941
|
juplend?: {
|
|
934
942
|
pollIntervalMs?: number;
|
|
935
943
|
};
|
|
@@ -1216,8 +1224,10 @@ interface PositionSnapshot {
|
|
|
1216
1224
|
interface RiskSnapshot {
|
|
1217
1225
|
accountId: string;
|
|
1218
1226
|
venue: Venue;
|
|
1219
|
-
|
|
1227
|
+
netEquity?: BigNumber;
|
|
1228
|
+
riskEquity?: BigNumber;
|
|
1220
1229
|
riskRatio?: BigNumber;
|
|
1230
|
+
riskLeverage?: BigNumber;
|
|
1221
1231
|
initialMargin?: BigNumber;
|
|
1222
1232
|
maintenanceMargin?: BigNumber;
|
|
1223
1233
|
exchangeTs?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imbingox/acex",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.5",
|
|
4
4
|
"description": "Multi-exchange trading SDK for market data, account, and order management",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@biomejs/biome": "^2.4.10",
|
|
50
50
|
"@changesets/cli": "^2.31.0",
|
|
51
|
+
"@mindfoldhq/trellis": "^0.4.0",
|
|
51
52
|
"@types/bun": "latest",
|
|
52
53
|
"typescript": "^6.0.2"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"@mindfoldhq/trellis": "^0.4.0",
|
|
56
56
|
"bignumber.js": "^11.0.0"
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -38,6 +38,7 @@ interface BinancePapiBalance {
|
|
|
38
38
|
|
|
39
39
|
interface BinancePapiAccount {
|
|
40
40
|
accountEquity?: string;
|
|
41
|
+
actualEquity?: string;
|
|
41
42
|
totalEquity?: string;
|
|
42
43
|
accountInitialMargin?: string;
|
|
43
44
|
totalInitialMargin?: string;
|
|
@@ -56,6 +57,7 @@ interface BinancePapiUmPosition {
|
|
|
56
57
|
unrealizedProfit?: string;
|
|
57
58
|
liquidationPrice?: string;
|
|
58
59
|
leverage?: string;
|
|
60
|
+
notional?: string;
|
|
59
61
|
positionSide?: string;
|
|
60
62
|
updateTime?: number;
|
|
61
63
|
}
|
|
@@ -285,14 +287,20 @@ function mapBalance(
|
|
|
285
287
|
function mapAccountRisk(
|
|
286
288
|
input: BinancePapiAccount,
|
|
287
289
|
receivedAt: number,
|
|
290
|
+
positions: BinancePapiUmPosition[] = [],
|
|
288
291
|
): RawRiskUpdate | undefined {
|
|
289
292
|
const uniMmr = firstString(input.uniMMR);
|
|
290
293
|
const riskRatio = uniMmr
|
|
291
294
|
? new BigNumber(1).dividedBy(uniMmr).toString(10)
|
|
292
295
|
: undefined;
|
|
296
|
+
const netEquity = firstString(input.actualEquity);
|
|
297
|
+
const riskEquity = firstString(input.accountEquity, input.totalEquity);
|
|
298
|
+
const riskLeverage = calculateRiskLeverage(riskEquity, positions);
|
|
293
299
|
const risk: RawRiskUpdate = {
|
|
294
|
-
|
|
300
|
+
netEquity,
|
|
301
|
+
riskEquity,
|
|
295
302
|
riskRatio,
|
|
303
|
+
riskLeverage,
|
|
296
304
|
initialMargin: firstString(
|
|
297
305
|
input.accountInitialMargin,
|
|
298
306
|
input.totalInitialMargin,
|
|
@@ -306,8 +314,10 @@ function mapAccountRisk(
|
|
|
306
314
|
};
|
|
307
315
|
|
|
308
316
|
if (
|
|
309
|
-
!risk.
|
|
317
|
+
!risk.netEquity &&
|
|
318
|
+
!risk.riskEquity &&
|
|
310
319
|
!risk.riskRatio &&
|
|
320
|
+
!risk.riskLeverage &&
|
|
311
321
|
!risk.initialMargin &&
|
|
312
322
|
!risk.maintenanceMargin
|
|
313
323
|
) {
|
|
@@ -317,6 +327,34 @@ function mapAccountRisk(
|
|
|
317
327
|
return risk;
|
|
318
328
|
}
|
|
319
329
|
|
|
330
|
+
function calculateRiskLeverage(
|
|
331
|
+
riskEquity: string | undefined,
|
|
332
|
+
positions: BinancePapiUmPosition[],
|
|
333
|
+
): string | undefined {
|
|
334
|
+
if (!riskEquity) {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const riskEquityValue = new BigNumber(riskEquity);
|
|
339
|
+
if (!riskEquityValue.isFinite() || riskEquityValue.isZero()) {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const grossExposure = positions.reduce((total, position) => {
|
|
344
|
+
const notional = firstString(position.notional);
|
|
345
|
+
if (!notional) {
|
|
346
|
+
return total;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const value = new BigNumber(notional);
|
|
350
|
+
return value.isFinite() ? total.plus(value.absoluteValue()) : total;
|
|
351
|
+
}, new BigNumber(0));
|
|
352
|
+
|
|
353
|
+
return grossExposure.isZero()
|
|
354
|
+
? undefined
|
|
355
|
+
: grossExposure.dividedBy(riskEquityValue).toString(10);
|
|
356
|
+
}
|
|
357
|
+
|
|
320
358
|
function mapUmPosition(
|
|
321
359
|
input: BinancePapiUmPosition,
|
|
322
360
|
receivedAt: number,
|
|
@@ -339,6 +377,22 @@ function mapUmPosition(
|
|
|
339
377
|
};
|
|
340
378
|
}
|
|
341
379
|
|
|
380
|
+
function mapAccountRefresh(
|
|
381
|
+
account: BinancePapiAccount,
|
|
382
|
+
positions: BinancePapiUmPosition[],
|
|
383
|
+
receivedAt: number,
|
|
384
|
+
): RawAccountUpdate {
|
|
385
|
+
return {
|
|
386
|
+
positions: positions.flatMap((position) => {
|
|
387
|
+
const mapped = mapUmPosition(position, receivedAt);
|
|
388
|
+
return mapped ? [mapped] : [];
|
|
389
|
+
}),
|
|
390
|
+
risk: mapAccountRisk(account, receivedAt, positions),
|
|
391
|
+
exchangeTs: account.updateTime,
|
|
392
|
+
receivedAt,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
342
396
|
function mapOpenOrder(
|
|
343
397
|
input: BinancePapiOpenOrder,
|
|
344
398
|
receivedAt: number,
|
|
@@ -550,12 +604,35 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
550
604
|
const mapped = mapUmPosition(position, receivedAt);
|
|
551
605
|
return mapped ? [mapped] : [];
|
|
552
606
|
}),
|
|
553
|
-
risk: mapAccountRisk(account, receivedAt),
|
|
607
|
+
risk: mapAccountRisk(account, receivedAt, positions),
|
|
554
608
|
exchangeTs: account.updateTime,
|
|
555
609
|
receivedAt,
|
|
556
610
|
};
|
|
557
611
|
}
|
|
558
612
|
|
|
613
|
+
async refreshAccount(
|
|
614
|
+
credentials: AccountCredentials,
|
|
615
|
+
accountOptions?: Record<string, unknown>,
|
|
616
|
+
): Promise<RawAccountUpdate> {
|
|
617
|
+
const receivedAt = Date.now();
|
|
618
|
+
const [account, positions] = await Promise.all([
|
|
619
|
+
this.signedRequest<BinancePapiAccount>(
|
|
620
|
+
"GET",
|
|
621
|
+
"/papi/v1/account",
|
|
622
|
+
credentials,
|
|
623
|
+
accountOptions,
|
|
624
|
+
),
|
|
625
|
+
this.signedRequest<BinancePapiUmPosition[]>(
|
|
626
|
+
"GET",
|
|
627
|
+
"/papi/v1/um/positionRisk",
|
|
628
|
+
credentials,
|
|
629
|
+
accountOptions,
|
|
630
|
+
),
|
|
631
|
+
]);
|
|
632
|
+
|
|
633
|
+
return mapAccountRefresh(account, positions, receivedAt);
|
|
634
|
+
}
|
|
635
|
+
|
|
559
636
|
async bootstrapOpenOrders(
|
|
560
637
|
credentials: AccountCredentials,
|
|
561
638
|
accountOptions?: Record<string, unknown>,
|
|
@@ -222,7 +222,8 @@ function buildRisk(input: {
|
|
|
222
222
|
: undefined;
|
|
223
223
|
|
|
224
224
|
return {
|
|
225
|
-
|
|
225
|
+
netEquity: totalCollateralUsd.minus(totalDebtUsd).toString(10),
|
|
226
|
+
riskEquity: weightedLiquidationValueUsd.minus(totalDebtUsd).toString(10),
|
|
226
227
|
riskRatio,
|
|
227
228
|
receivedAt: input.receivedAt,
|
|
228
229
|
lending: {
|
package/src/adapters/types.ts
CHANGED
|
@@ -119,8 +119,10 @@ export interface RawPositionUpdate {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
export interface RawRiskUpdate {
|
|
122
|
-
|
|
122
|
+
netEquity?: string;
|
|
123
|
+
riskEquity?: string;
|
|
123
124
|
riskRatio?: string;
|
|
125
|
+
riskLeverage?: string;
|
|
124
126
|
initialMargin?: string;
|
|
125
127
|
maintenanceMargin?: string;
|
|
126
128
|
exchangeTs?: number;
|
|
@@ -222,6 +224,10 @@ export interface PrivateUserDataAdapter {
|
|
|
222
224
|
credentials: AccountCredentials,
|
|
223
225
|
accountOptions?: Record<string, unknown>,
|
|
224
226
|
): Promise<RawAccountBootstrap>;
|
|
227
|
+
refreshAccount?(
|
|
228
|
+
credentials: AccountCredentials,
|
|
229
|
+
accountOptions?: Record<string, unknown>,
|
|
230
|
+
): Promise<RawAccountUpdate>;
|
|
225
231
|
bootstrapOpenOrders(
|
|
226
232
|
credentials: AccountCredentials,
|
|
227
233
|
accountOptions?: Record<string, unknown>,
|
package/src/client/context.ts
CHANGED
|
@@ -19,6 +19,9 @@ interface PrivateSubscriptionRecord {
|
|
|
19
19
|
accountReady: boolean;
|
|
20
20
|
orderReady: boolean;
|
|
21
21
|
stream?: StreamHandle;
|
|
22
|
+
accountRefreshTimer?: ReturnType<typeof setTimeout>;
|
|
23
|
+
accountRefreshInFlight?: Promise<void>;
|
|
24
|
+
accountRefreshGeneration: number;
|
|
22
25
|
startPromise?: Promise<void>;
|
|
23
26
|
reconcilePromise?: Promise<void>;
|
|
24
27
|
}
|
|
@@ -27,6 +30,16 @@ const DEFAULT_STREAM_OPEN_TIMEOUT_MS = 15_000;
|
|
|
27
30
|
const DEFAULT_STREAM_RECONNECT_DELAY_MS = 1_000;
|
|
28
31
|
const DEFAULT_STREAM_RECONNECT_MAX_DELAY_MS = 10_000;
|
|
29
32
|
const DEFAULT_LISTEN_KEY_KEEPALIVE_MS = 30 * 60 * 1_000;
|
|
33
|
+
const DEFAULT_BINANCE_RISK_POLL_INTERVAL_MS = 5_000;
|
|
34
|
+
|
|
35
|
+
function normalizePositiveInterval(
|
|
36
|
+
value: number | undefined,
|
|
37
|
+
fallback: number,
|
|
38
|
+
): number {
|
|
39
|
+
return value !== undefined && Number.isFinite(value) && value > 0
|
|
40
|
+
? value
|
|
41
|
+
: fallback;
|
|
42
|
+
}
|
|
30
43
|
|
|
31
44
|
export class PrivateSubscriptionCoordinator {
|
|
32
45
|
private readonly context: ClientContext;
|
|
@@ -37,6 +50,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
37
50
|
private readonly streamReconnectDelayMs: number;
|
|
38
51
|
private readonly streamReconnectMaxDelayMs: number;
|
|
39
52
|
private readonly listenKeyKeepAliveMs: number;
|
|
53
|
+
private readonly binanceRiskPollIntervalMs: number;
|
|
40
54
|
private readonly juplendPollIntervalMs?: number;
|
|
41
55
|
private readonly records = new Map<string, PrivateSubscriptionRecord>();
|
|
42
56
|
|
|
@@ -62,6 +76,10 @@ export class PrivateSubscriptionCoordinator {
|
|
|
62
76
|
DEFAULT_STREAM_RECONNECT_MAX_DELAY_MS;
|
|
63
77
|
this.listenKeyKeepAliveMs =
|
|
64
78
|
options.listenKeyKeepAliveMs ?? DEFAULT_LISTEN_KEY_KEEPALIVE_MS;
|
|
79
|
+
this.binanceRiskPollIntervalMs = normalizePositiveInterval(
|
|
80
|
+
options.binance?.riskPollIntervalMs,
|
|
81
|
+
DEFAULT_BINANCE_RISK_POLL_INTERVAL_MS,
|
|
82
|
+
);
|
|
65
83
|
this.juplendPollIntervalMs = options.juplend?.pollIntervalMs;
|
|
66
84
|
}
|
|
67
85
|
|
|
@@ -81,6 +99,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
81
99
|
} else {
|
|
82
100
|
await this.ensureStream(record, account);
|
|
83
101
|
await this.bootstrapAccount(record, account);
|
|
102
|
+
this.ensureAccountRefreshPolling(record);
|
|
84
103
|
}
|
|
85
104
|
} catch (error) {
|
|
86
105
|
record.accountSubscribed = false;
|
|
@@ -96,6 +115,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
96
115
|
}
|
|
97
116
|
|
|
98
117
|
record.accountSubscribed = false;
|
|
118
|
+
this.stopAccountRefreshPolling(record);
|
|
99
119
|
this.closeIfUnused(record);
|
|
100
120
|
}
|
|
101
121
|
|
|
@@ -153,6 +173,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
153
173
|
|
|
154
174
|
onClientStopping(): void {
|
|
155
175
|
for (const record of this.records.values()) {
|
|
176
|
+
this.stopAccountRefreshPolling(record);
|
|
156
177
|
this.closeStream(record);
|
|
157
178
|
}
|
|
158
179
|
}
|
|
@@ -164,6 +185,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
164
185
|
}
|
|
165
186
|
|
|
166
187
|
this.closeStream(record);
|
|
188
|
+
this.stopAccountRefreshPolling(record);
|
|
167
189
|
this.records.delete(accountId);
|
|
168
190
|
}
|
|
169
191
|
|
|
@@ -186,6 +208,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
186
208
|
private async resumeRecord(record: PrivateSubscriptionRecord): Promise<void> {
|
|
187
209
|
const account = this.getAccount(record.accountId);
|
|
188
210
|
this.closeStream(record);
|
|
211
|
+
this.stopAccountRefreshPolling(record);
|
|
189
212
|
|
|
190
213
|
try {
|
|
191
214
|
if (record.venue === "juplend" && record.accountSubscribed) {
|
|
@@ -195,6 +218,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
195
218
|
await this.ensureStream(record, account);
|
|
196
219
|
if (record.accountSubscribed) {
|
|
197
220
|
await this.bootstrapAccount(record, account);
|
|
221
|
+
this.ensureAccountRefreshPolling(record);
|
|
198
222
|
}
|
|
199
223
|
}
|
|
200
224
|
if (record.ordersSubscribed) {
|
|
@@ -244,6 +268,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
244
268
|
ordersSubscribed: false,
|
|
245
269
|
accountReady: false,
|
|
246
270
|
orderReady: false,
|
|
271
|
+
accountRefreshGeneration: 0,
|
|
247
272
|
};
|
|
248
273
|
|
|
249
274
|
this.records.set(account.accountId, record);
|
|
@@ -259,6 +284,7 @@ export class PrivateSubscriptionCoordinator {
|
|
|
259
284
|
return;
|
|
260
285
|
}
|
|
261
286
|
|
|
287
|
+
this.stopAccountRefreshPolling(record);
|
|
262
288
|
this.closeStream(record);
|
|
263
289
|
this.records.delete(record.accountId);
|
|
264
290
|
}
|
|
@@ -268,6 +294,153 @@ export class PrivateSubscriptionCoordinator {
|
|
|
268
294
|
record.stream = undefined;
|
|
269
295
|
}
|
|
270
296
|
|
|
297
|
+
private ensureAccountRefreshPolling(record: PrivateSubscriptionRecord): void {
|
|
298
|
+
if (
|
|
299
|
+
record.venue !== "binance" ||
|
|
300
|
+
!record.accountSubscribed ||
|
|
301
|
+
record.accountRefreshTimer ||
|
|
302
|
+
record.accountRefreshInFlight
|
|
303
|
+
) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.scheduleAccountRefreshPoll(record);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private stopAccountRefreshPolling(record: PrivateSubscriptionRecord): void {
|
|
311
|
+
record.accountRefreshGeneration += 1;
|
|
312
|
+
if (record.accountRefreshTimer) {
|
|
313
|
+
clearTimeout(record.accountRefreshTimer);
|
|
314
|
+
record.accountRefreshTimer = undefined;
|
|
315
|
+
}
|
|
316
|
+
record.accountRefreshInFlight = undefined;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private scheduleAccountRefreshPoll(record: PrivateSubscriptionRecord): void {
|
|
320
|
+
if (record.venue !== "binance" || !record.accountSubscribed) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const generation = record.accountRefreshGeneration;
|
|
325
|
+
record.accountRefreshTimer = setTimeout(() => {
|
|
326
|
+
record.accountRefreshTimer = undefined;
|
|
327
|
+
if (
|
|
328
|
+
generation !== record.accountRefreshGeneration ||
|
|
329
|
+
record.venue !== "binance" ||
|
|
330
|
+
!record.accountSubscribed
|
|
331
|
+
) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let latestAccount: RegisteredAccountRecord;
|
|
336
|
+
try {
|
|
337
|
+
latestAccount = this.getAccount(record.accountId);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
this.handleAccountRefreshLookupError(record, error);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
record.accountRefreshInFlight = this.refreshAccount(
|
|
344
|
+
record,
|
|
345
|
+
latestAccount,
|
|
346
|
+
generation,
|
|
347
|
+
)
|
|
348
|
+
.catch(() => {})
|
|
349
|
+
.finally(() => {
|
|
350
|
+
if (generation !== record.accountRefreshGeneration) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
record.accountRefreshInFlight = undefined;
|
|
355
|
+
if (record.accountSubscribed && record.venue === "binance") {
|
|
356
|
+
this.scheduleAccountRefreshPoll(record);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}, this.binanceRiskPollIntervalMs);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private handleAccountRefreshLookupError(
|
|
363
|
+
record: PrivateSubscriptionRecord,
|
|
364
|
+
error: unknown,
|
|
365
|
+
): void {
|
|
366
|
+
this.stopAccountRefreshPolling(record);
|
|
367
|
+
if (error instanceof AcexError && error.code === "ACCOUNT_NOT_FOUND") {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.context.publishRuntimeError(
|
|
372
|
+
"adapter",
|
|
373
|
+
error instanceof Error
|
|
374
|
+
? error
|
|
375
|
+
: new Error(`Failed to load ${record.venue} account for risk refresh`),
|
|
376
|
+
{
|
|
377
|
+
accountId: record.accountId,
|
|
378
|
+
venue: record.venue,
|
|
379
|
+
},
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private async refreshAccount(
|
|
384
|
+
record: PrivateSubscriptionRecord,
|
|
385
|
+
account: RegisteredAccountRecord,
|
|
386
|
+
generation: number,
|
|
387
|
+
): Promise<void> {
|
|
388
|
+
const adapter = this.getAdapter(record.venue);
|
|
389
|
+
if (!adapter.refreshAccount) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const update = await adapter.refreshAccount(account.credentials ?? {}, {
|
|
395
|
+
...account.options,
|
|
396
|
+
accountId: account.accountId,
|
|
397
|
+
});
|
|
398
|
+
if (
|
|
399
|
+
!record.accountSubscribed ||
|
|
400
|
+
generation !== record.accountRefreshGeneration
|
|
401
|
+
) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
record.accountReady = true;
|
|
406
|
+
this.accountConsumer.onPrivateAccountUpdate(
|
|
407
|
+
record.accountId,
|
|
408
|
+
record.venue,
|
|
409
|
+
update,
|
|
410
|
+
{ preserveStatus: true },
|
|
411
|
+
);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (
|
|
414
|
+
!record.accountSubscribed ||
|
|
415
|
+
generation !== record.accountRefreshGeneration
|
|
416
|
+
) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
this.context.publishRuntimeError(
|
|
421
|
+
"adapter",
|
|
422
|
+
error instanceof Error
|
|
423
|
+
? error
|
|
424
|
+
: new Error(
|
|
425
|
+
`Failed to refresh ${record.venue} private account state`,
|
|
426
|
+
),
|
|
427
|
+
{
|
|
428
|
+
accountId: record.accountId,
|
|
429
|
+
venue: record.venue,
|
|
430
|
+
},
|
|
431
|
+
);
|
|
432
|
+
this.accountConsumer.onPrivateAccountStreamState(
|
|
433
|
+
record.accountId,
|
|
434
|
+
record.venue,
|
|
435
|
+
{
|
|
436
|
+
runtimeStatus: "degraded",
|
|
437
|
+
ready: record.accountReady,
|
|
438
|
+
reason: "http_failed",
|
|
439
|
+
},
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
271
444
|
private async ensureStream(
|
|
272
445
|
record: PrivateSubscriptionRecord,
|
|
273
446
|
account: RegisteredAccountRecord,
|
|
@@ -277,6 +277,7 @@ export class AccountManagerImpl
|
|
|
277
277
|
accountId: string,
|
|
278
278
|
venue: Venue,
|
|
279
279
|
update: RawAccountUpdate,
|
|
280
|
+
options: { preserveStatus?: boolean } = {},
|
|
280
281
|
): void {
|
|
281
282
|
const record = this.getOrCreateRecord(accountId, venue);
|
|
282
283
|
if (!record.subscribed) {
|
|
@@ -358,16 +359,24 @@ export class AccountManagerImpl
|
|
|
358
359
|
receivedAt: update.receivedAt,
|
|
359
360
|
updatedAt: update.receivedAt,
|
|
360
361
|
};
|
|
361
|
-
record.status =
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
362
|
+
record.status = options.preserveStatus
|
|
363
|
+
? {
|
|
364
|
+
...record.status,
|
|
365
|
+
activity: "active",
|
|
366
|
+
lastReceivedAt: update.receivedAt,
|
|
367
|
+
lastReadyAt: record.status.lastReadyAt ?? update.receivedAt,
|
|
368
|
+
inactiveSince: undefined,
|
|
369
|
+
}
|
|
370
|
+
: {
|
|
371
|
+
...record.status,
|
|
372
|
+
activity: "active",
|
|
373
|
+
ready: true,
|
|
374
|
+
runtimeStatus: "healthy",
|
|
375
|
+
reason: undefined,
|
|
376
|
+
lastReceivedAt: update.receivedAt,
|
|
377
|
+
lastReadyAt: update.receivedAt,
|
|
378
|
+
inactiveSince: undefined,
|
|
379
|
+
};
|
|
371
380
|
this.publishStatus(record);
|
|
372
381
|
}
|
|
373
382
|
|
|
@@ -580,14 +589,22 @@ export class AccountManagerImpl
|
|
|
580
589
|
return {
|
|
581
590
|
accountId,
|
|
582
591
|
venue,
|
|
583
|
-
|
|
584
|
-
input.
|
|
585
|
-
? previous?.
|
|
586
|
-
: new BigNumber(input.
|
|
592
|
+
netEquity:
|
|
593
|
+
input.netEquity === undefined
|
|
594
|
+
? previous?.netEquity
|
|
595
|
+
: new BigNumber(input.netEquity),
|
|
596
|
+
riskEquity:
|
|
597
|
+
input.riskEquity === undefined
|
|
598
|
+
? previous?.riskEquity
|
|
599
|
+
: new BigNumber(input.riskEquity),
|
|
587
600
|
riskRatio:
|
|
588
601
|
input.riskRatio === undefined
|
|
589
602
|
? previous?.riskRatio
|
|
590
603
|
: new BigNumber(input.riskRatio),
|
|
604
|
+
riskLeverage:
|
|
605
|
+
input.riskLeverage === undefined
|
|
606
|
+
? previous?.riskLeverage
|
|
607
|
+
: new BigNumber(input.riskLeverage),
|
|
591
608
|
initialMargin:
|
|
592
609
|
input.initialMargin === undefined
|
|
593
610
|
? previous?.initialMargin
|
package/src/types/account.ts
CHANGED
|
@@ -91,8 +91,10 @@ export interface PositionSnapshot {
|
|
|
91
91
|
export interface RiskSnapshot {
|
|
92
92
|
accountId: string;
|
|
93
93
|
venue: Venue;
|
|
94
|
-
|
|
94
|
+
netEquity?: BigNumber;
|
|
95
|
+
riskEquity?: BigNumber;
|
|
95
96
|
riskRatio?: BigNumber;
|
|
97
|
+
riskLeverage?: BigNumber;
|
|
96
98
|
initialMargin?: BigNumber;
|
|
97
99
|
maintenanceMargin?: BigNumber;
|
|
98
100
|
exchangeTs?: number;
|
package/src/types/shared.ts
CHANGED