@imbingox/acex 0.3.0-beta.2 → 0.3.0-beta.4
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 +21 -1
- package/docs/api.md +1464 -0
- package/package.json +4 -3
- package/src/adapters/binance/adapter.ts +10 -1
- package/src/adapters/binance/private-adapter.ts +111 -3
- package/src/adapters/juplend/private-adapter.ts +35 -1
- package/src/adapters/types.ts +13 -0
- package/src/client/context.ts +1 -0
- package/src/client/private-subscription-coordinator.ts +173 -0
- package/src/client/runtime.ts +23 -1
- package/src/client/venue-capabilities.ts +109 -0
- package/src/managers/account-manager.ts +23 -10
- package/src/types/account.ts +1 -0
- package/src/types/client.ts +71 -0
- package/src/types/shared.ts +3 -0
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.4",
|
|
4
4
|
"description": "Multi-exchange trading SDK for market data, account, and order management",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"index.ts",
|
|
19
|
-
"src/"
|
|
19
|
+
"src/",
|
|
20
|
+
"docs/api.md"
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
|
22
23
|
"changeset": "changeset",
|
|
@@ -47,11 +48,11 @@
|
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@biomejs/biome": "^2.4.10",
|
|
49
50
|
"@changesets/cli": "^2.31.0",
|
|
51
|
+
"@mindfoldhq/trellis": "^0.4.0",
|
|
50
52
|
"@types/bun": "latest",
|
|
51
53
|
"typescript": "^6.0.2"
|
|
52
54
|
},
|
|
53
55
|
"dependencies": {
|
|
54
|
-
"@mindfoldhq/trellis": "^0.4.0",
|
|
55
56
|
"bignumber.js": "^11.0.0"
|
|
56
57
|
}
|
|
57
58
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
MarketDefinition,
|
|
3
|
+
VenueMarketCapabilities,
|
|
4
|
+
} from "../../types/index.ts";
|
|
2
5
|
import type {
|
|
3
6
|
FundingRateStreamCallbacks,
|
|
4
7
|
FundingRateStreamOptions,
|
|
@@ -16,6 +19,12 @@ import {
|
|
|
16
19
|
|
|
17
20
|
export class BinanceMarketAdapter implements MarketAdapter {
|
|
18
21
|
readonly venue = "binance" as const;
|
|
22
|
+
readonly marketCapabilities: VenueMarketCapabilities = {
|
|
23
|
+
catalog: "supported",
|
|
24
|
+
l1Book: "supported",
|
|
25
|
+
fundingRate: "market_dependent",
|
|
26
|
+
marketTypes: ["spot", "swap", "future"],
|
|
27
|
+
};
|
|
19
28
|
|
|
20
29
|
private readonly definitions = new Map<string, BinanceMarketDefinition>();
|
|
21
30
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { createHmac } from "node:crypto";
|
|
2
2
|
import BigNumber from "bignumber.js";
|
|
3
3
|
import { createManagedWebSocket } from "../../internal/managed-websocket.ts";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
AccountCredentials,
|
|
6
|
+
PositionSide,
|
|
7
|
+
VenueAccountCapabilities,
|
|
8
|
+
VenueOrderCapabilities,
|
|
9
|
+
} from "../../types/index.ts";
|
|
5
10
|
import type {
|
|
6
11
|
CancelAllOrdersRequest,
|
|
7
12
|
CancelOrderRequest,
|
|
@@ -51,6 +56,7 @@ interface BinancePapiUmPosition {
|
|
|
51
56
|
unrealizedProfit?: string;
|
|
52
57
|
liquidationPrice?: string;
|
|
53
58
|
leverage?: string;
|
|
59
|
+
notional?: string;
|
|
54
60
|
positionSide?: string;
|
|
55
61
|
updateTime?: number;
|
|
56
62
|
}
|
|
@@ -280,14 +286,18 @@ function mapBalance(
|
|
|
280
286
|
function mapAccountRisk(
|
|
281
287
|
input: BinancePapiAccount,
|
|
282
288
|
receivedAt: number,
|
|
289
|
+
positions: BinancePapiUmPosition[] = [],
|
|
283
290
|
): RawRiskUpdate | undefined {
|
|
284
291
|
const uniMmr = firstString(input.uniMMR);
|
|
285
292
|
const riskRatio = uniMmr
|
|
286
293
|
? new BigNumber(1).dividedBy(uniMmr).toString(10)
|
|
287
294
|
: undefined;
|
|
295
|
+
const equity = firstString(input.accountEquity, input.totalEquity);
|
|
296
|
+
const actualLeverage = calculateActualLeverage(equity, positions);
|
|
288
297
|
const risk: RawRiskUpdate = {
|
|
289
|
-
equity
|
|
298
|
+
equity,
|
|
290
299
|
riskRatio,
|
|
300
|
+
actualLeverage,
|
|
291
301
|
initialMargin: firstString(
|
|
292
302
|
input.accountInitialMargin,
|
|
293
303
|
input.totalInitialMargin,
|
|
@@ -303,6 +313,7 @@ function mapAccountRisk(
|
|
|
303
313
|
if (
|
|
304
314
|
!risk.equity &&
|
|
305
315
|
!risk.riskRatio &&
|
|
316
|
+
!risk.actualLeverage &&
|
|
306
317
|
!risk.initialMargin &&
|
|
307
318
|
!risk.maintenanceMargin
|
|
308
319
|
) {
|
|
@@ -312,6 +323,34 @@ function mapAccountRisk(
|
|
|
312
323
|
return risk;
|
|
313
324
|
}
|
|
314
325
|
|
|
326
|
+
function calculateActualLeverage(
|
|
327
|
+
equity: string | undefined,
|
|
328
|
+
positions: BinancePapiUmPosition[],
|
|
329
|
+
): string | undefined {
|
|
330
|
+
if (!equity) {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const equityValue = new BigNumber(equity);
|
|
335
|
+
if (!equityValue.isFinite() || equityValue.isZero()) {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const grossExposure = positions.reduce((total, position) => {
|
|
340
|
+
const notional = firstString(position.notional);
|
|
341
|
+
if (!notional) {
|
|
342
|
+
return total;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const value = new BigNumber(notional);
|
|
346
|
+
return value.isFinite() ? total.plus(value.absoluteValue()) : total;
|
|
347
|
+
}, new BigNumber(0));
|
|
348
|
+
|
|
349
|
+
return grossExposure.isZero()
|
|
350
|
+
? undefined
|
|
351
|
+
: grossExposure.dividedBy(equityValue).toString(10);
|
|
352
|
+
}
|
|
353
|
+
|
|
315
354
|
function mapUmPosition(
|
|
316
355
|
input: BinancePapiUmPosition,
|
|
317
356
|
receivedAt: number,
|
|
@@ -334,6 +373,22 @@ function mapUmPosition(
|
|
|
334
373
|
};
|
|
335
374
|
}
|
|
336
375
|
|
|
376
|
+
function mapAccountRefresh(
|
|
377
|
+
account: BinancePapiAccount,
|
|
378
|
+
positions: BinancePapiUmPosition[],
|
|
379
|
+
receivedAt: number,
|
|
380
|
+
): RawAccountUpdate {
|
|
381
|
+
return {
|
|
382
|
+
positions: positions.flatMap((position) => {
|
|
383
|
+
const mapped = mapUmPosition(position, receivedAt);
|
|
384
|
+
return mapped ? [mapped] : [];
|
|
385
|
+
}),
|
|
386
|
+
risk: mapAccountRisk(account, receivedAt, positions),
|
|
387
|
+
exchangeTs: account.updateTime,
|
|
388
|
+
receivedAt,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
337
392
|
function mapOpenOrder(
|
|
338
393
|
input: BinancePapiOpenOrder,
|
|
339
394
|
receivedAt: number,
|
|
@@ -479,6 +534,36 @@ async function readJson<T>(response: Response, url: string): Promise<T> {
|
|
|
479
534
|
|
|
480
535
|
export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
481
536
|
readonly venue = "binance" as const;
|
|
537
|
+
readonly readOnly = false;
|
|
538
|
+
readonly notes = [
|
|
539
|
+
"Capabilities describe the current SDK runtime, not Binance's full exchange API surface.",
|
|
540
|
+
"Funding rate support depends on the market type.",
|
|
541
|
+
"Order commands currently target Binance PAPI UM USD-M symbols; venue-level order.supported does not mean every Binance market type is orderable.",
|
|
542
|
+
];
|
|
543
|
+
readonly accountCapabilities: VenueAccountCapabilities = {
|
|
544
|
+
register: "supported",
|
|
545
|
+
snapshot: "supported",
|
|
546
|
+
updates: "websocket",
|
|
547
|
+
balances: "supported",
|
|
548
|
+
positions: "supported",
|
|
549
|
+
risk: "supported",
|
|
550
|
+
lending: "unsupported",
|
|
551
|
+
credentialsRequired: true,
|
|
552
|
+
};
|
|
553
|
+
readonly orderCapabilities: VenueOrderCapabilities = {
|
|
554
|
+
supported: true,
|
|
555
|
+
openOrders: "supported",
|
|
556
|
+
updates: "websocket",
|
|
557
|
+
create: "supported",
|
|
558
|
+
cancel: "supported",
|
|
559
|
+
cancelAll: "symbol",
|
|
560
|
+
orderTypes: ["limit", "market"],
|
|
561
|
+
timeInForce: ["gtc", "post_only"],
|
|
562
|
+
postOnly: true,
|
|
563
|
+
reduceOnly: true,
|
|
564
|
+
positionSide: "required_for_hedge",
|
|
565
|
+
clientOrderId: true,
|
|
566
|
+
};
|
|
482
567
|
|
|
483
568
|
async bootstrapAccount(
|
|
484
569
|
credentials: AccountCredentials,
|
|
@@ -515,12 +600,35 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
|
|
|
515
600
|
const mapped = mapUmPosition(position, receivedAt);
|
|
516
601
|
return mapped ? [mapped] : [];
|
|
517
602
|
}),
|
|
518
|
-
risk: mapAccountRisk(account, receivedAt),
|
|
603
|
+
risk: mapAccountRisk(account, receivedAt, positions),
|
|
519
604
|
exchangeTs: account.updateTime,
|
|
520
605
|
receivedAt,
|
|
521
606
|
};
|
|
522
607
|
}
|
|
523
608
|
|
|
609
|
+
async refreshAccount(
|
|
610
|
+
credentials: AccountCredentials,
|
|
611
|
+
accountOptions?: Record<string, unknown>,
|
|
612
|
+
): Promise<RawAccountUpdate> {
|
|
613
|
+
const receivedAt = Date.now();
|
|
614
|
+
const [account, positions] = await Promise.all([
|
|
615
|
+
this.signedRequest<BinancePapiAccount>(
|
|
616
|
+
"GET",
|
|
617
|
+
"/papi/v1/account",
|
|
618
|
+
credentials,
|
|
619
|
+
accountOptions,
|
|
620
|
+
),
|
|
621
|
+
this.signedRequest<BinancePapiUmPosition[]>(
|
|
622
|
+
"GET",
|
|
623
|
+
"/papi/v1/um/positionRisk",
|
|
624
|
+
credentials,
|
|
625
|
+
accountOptions,
|
|
626
|
+
),
|
|
627
|
+
]);
|
|
628
|
+
|
|
629
|
+
return mapAccountRefresh(account, positions, receivedAt);
|
|
630
|
+
}
|
|
631
|
+
|
|
524
632
|
async bootstrapOpenOrders(
|
|
525
633
|
credentials: AccountCredentials,
|
|
526
634
|
accountOptions?: Record<string, unknown>,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import BigNumber from "bignumber.js";
|
|
2
2
|
import { AcexError } from "../../errors.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
AccountCredentials,
|
|
5
|
+
VenueAccountCapabilities,
|
|
6
|
+
VenueOrderCapabilities,
|
|
7
|
+
} from "../../types/index.ts";
|
|
4
8
|
import type {
|
|
5
9
|
CancelAllOrdersRequest,
|
|
6
10
|
CancelOrderRequest,
|
|
@@ -361,6 +365,36 @@ function mapAccount(
|
|
|
361
365
|
|
|
362
366
|
export class JuplendPrivateAdapter implements PrivateUserDataAdapter {
|
|
363
367
|
readonly venue = "juplend" as const;
|
|
368
|
+
readonly readOnly = true;
|
|
369
|
+
readonly notes = [
|
|
370
|
+
"Juplend support is limited to read-only lending account views.",
|
|
371
|
+
"Order and market data managers are not supported for this venue.",
|
|
372
|
+
];
|
|
373
|
+
readonly accountCapabilities: VenueAccountCapabilities = {
|
|
374
|
+
register: "supported",
|
|
375
|
+
snapshot: "supported",
|
|
376
|
+
updates: "polling",
|
|
377
|
+
balances: "supported",
|
|
378
|
+
positions: "unsupported",
|
|
379
|
+
risk: "supported",
|
|
380
|
+
lending: "supported",
|
|
381
|
+
credentialsRequired: true,
|
|
382
|
+
};
|
|
383
|
+
readonly orderCapabilities: VenueOrderCapabilities = {
|
|
384
|
+
supported: false,
|
|
385
|
+
openOrders: "unsupported",
|
|
386
|
+
updates: "unsupported",
|
|
387
|
+
create: "unsupported",
|
|
388
|
+
cancel: "unsupported",
|
|
389
|
+
cancelAll: "unsupported",
|
|
390
|
+
orderTypes: [],
|
|
391
|
+
timeInForce: [],
|
|
392
|
+
postOnly: false,
|
|
393
|
+
reduceOnly: false,
|
|
394
|
+
positionSide: "unsupported",
|
|
395
|
+
clientOrderId: false,
|
|
396
|
+
reason: "read_only",
|
|
397
|
+
};
|
|
364
398
|
|
|
365
399
|
async bootstrapAccount(
|
|
366
400
|
credentials: AccountCredentials,
|
package/src/adapters/types.ts
CHANGED
|
@@ -6,6 +6,9 @@ import type {
|
|
|
6
6
|
OrderStatus,
|
|
7
7
|
PositionSide,
|
|
8
8
|
Venue,
|
|
9
|
+
VenueAccountCapabilities,
|
|
10
|
+
VenueMarketCapabilities,
|
|
11
|
+
VenueOrderCapabilities,
|
|
9
12
|
} from "../types/index.ts";
|
|
10
13
|
|
|
11
14
|
export interface StreamHandle {
|
|
@@ -69,6 +72,7 @@ export interface FundingRateStreamOptions {
|
|
|
69
72
|
|
|
70
73
|
export interface MarketAdapter {
|
|
71
74
|
readonly venue: Venue;
|
|
75
|
+
readonly marketCapabilities: VenueMarketCapabilities;
|
|
72
76
|
loadMarkets(): Promise<MarketDefinition[]>;
|
|
73
77
|
createL1BookStream(
|
|
74
78
|
market: MarketDefinition,
|
|
@@ -117,6 +121,7 @@ export interface RawPositionUpdate {
|
|
|
117
121
|
export interface RawRiskUpdate {
|
|
118
122
|
equity?: string;
|
|
119
123
|
riskRatio?: string;
|
|
124
|
+
actualLeverage?: string;
|
|
120
125
|
initialMargin?: string;
|
|
121
126
|
maintenanceMargin?: string;
|
|
122
127
|
exchangeTs?: number;
|
|
@@ -210,10 +215,18 @@ export interface PrivateStreamOptions {
|
|
|
210
215
|
|
|
211
216
|
export interface PrivateUserDataAdapter {
|
|
212
217
|
readonly venue: Venue;
|
|
218
|
+
readonly readOnly: boolean;
|
|
219
|
+
readonly notes: string[];
|
|
220
|
+
readonly accountCapabilities: VenueAccountCapabilities;
|
|
221
|
+
readonly orderCapabilities: VenueOrderCapabilities;
|
|
213
222
|
bootstrapAccount(
|
|
214
223
|
credentials: AccountCredentials,
|
|
215
224
|
accountOptions?: Record<string, unknown>,
|
|
216
225
|
): Promise<RawAccountBootstrap>;
|
|
226
|
+
refreshAccount?(
|
|
227
|
+
credentials: AccountCredentials,
|
|
228
|
+
accountOptions?: Record<string, unknown>,
|
|
229
|
+
): Promise<RawAccountUpdate>;
|
|
217
230
|
bootstrapOpenOrders(
|
|
218
231
|
credentials: AccountCredentials,
|
|
219
232
|
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,
|
package/src/client/runtime.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
CancelAllOrdersRequest,
|
|
6
6
|
CancelOrderRequest,
|
|
7
7
|
CreateOrderRequest,
|
|
8
|
+
MarketAdapter,
|
|
8
9
|
PrivateUserDataAdapter,
|
|
9
10
|
RawOrderUpdate,
|
|
10
11
|
} from "../adapters/types.ts";
|
|
@@ -35,6 +36,7 @@ import type {
|
|
|
35
36
|
RegisterAccountResult,
|
|
36
37
|
StopOptions,
|
|
37
38
|
Venue,
|
|
39
|
+
VenueCapabilities,
|
|
38
40
|
} from "../types/index.ts";
|
|
39
41
|
import {
|
|
40
42
|
type ClientContext,
|
|
@@ -45,6 +47,10 @@ import {
|
|
|
45
47
|
type RegisteredAccountRecord,
|
|
46
48
|
} from "./context.ts";
|
|
47
49
|
import { PrivateSubscriptionCoordinator } from "./private-subscription-coordinator.ts";
|
|
50
|
+
import {
|
|
51
|
+
getVenueCapabilitiesSnapshot,
|
|
52
|
+
listVenueCapabilitiesSnapshots,
|
|
53
|
+
} from "./venue-capabilities.ts";
|
|
48
54
|
|
|
49
55
|
const activeClients = new Set<AcexClientImpl>();
|
|
50
56
|
|
|
@@ -89,13 +95,15 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
89
95
|
private readonly marketManager: MarketManagerImpl;
|
|
90
96
|
private readonly accountManager: AccountManagerImpl;
|
|
91
97
|
private readonly orderManager: OrderManagerImpl;
|
|
92
|
-
private readonly
|
|
98
|
+
private readonly marketAdapters: Map<Venue, MarketAdapter>;
|
|
99
|
+
private readonly privateAdapters: Map<Venue, PrivateUserDataAdapter>;
|
|
93
100
|
private readonly privateCoordinator: PrivateSubscriptionCoordinator;
|
|
94
101
|
|
|
95
102
|
constructor(options: CreateClientOptions = {}) {
|
|
96
103
|
activeClients.add(this);
|
|
97
104
|
|
|
98
105
|
const marketAdapter = new BinanceMarketAdapter();
|
|
106
|
+
this.marketAdapters = new Map([[marketAdapter.venue, marketAdapter]]);
|
|
99
107
|
const privateAdapters = [
|
|
100
108
|
new BinancePrivateAdapter(),
|
|
101
109
|
new JuplendPrivateAdapter(),
|
|
@@ -142,6 +150,20 @@ export class AcexClientImpl implements AcexClient, ClientContext {
|
|
|
142
150
|
};
|
|
143
151
|
}
|
|
144
152
|
|
|
153
|
+
getVenueCapabilities(venue: Venue): VenueCapabilities {
|
|
154
|
+
return getVenueCapabilitiesSnapshot(venue, {
|
|
155
|
+
marketAdapters: this.marketAdapters,
|
|
156
|
+
privateAdapters: this.privateAdapters,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
listVenueCapabilities(): VenueCapabilities[] {
|
|
161
|
+
return listVenueCapabilitiesSnapshots({
|
|
162
|
+
marketAdapters: this.marketAdapters,
|
|
163
|
+
privateAdapters: this.privateAdapters,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
145
167
|
async registerAccount(
|
|
146
168
|
input: RegisterAccountInput,
|
|
147
169
|
): Promise<RegisterAccountResult> {
|