@imbingox/acex 0.3.0-beta.0 → 0.3.0-beta.2
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 +73 -23
- package/package.json +9 -3
- package/src/adapters/binance/adapter.ts +1 -1
- package/src/adapters/binance/market-catalog.ts +2 -2
- package/src/adapters/binance/private-adapter.ts +14 -4
- package/src/adapters/juplend/private-adapter.ts +483 -0
- package/src/adapters/types.ts +27 -4
- package/src/client/context.ts +16 -11
- package/src/client/private-subscription-coordinator.ts +101 -47
- package/src/client/runtime.ts +43 -20
- package/src/errors.ts +1 -1
- package/src/internal/filters.ts +9 -9
- package/src/managers/account-manager.ts +95 -58
- package/src/managers/market-manager.ts +129 -44
- package/src/managers/order-manager.ts +49 -56
- package/src/types/account.ts +30 -10
- package/src/types/client.ts +2 -2
- package/src/types/market.ts +40 -16
- package/src/types/order.ts +8 -7
- package/src/types/shared.ts +43 -7
|
@@ -18,7 +18,6 @@ import { AcexError } from "../errors.ts";
|
|
|
18
18
|
import { AsyncEventBus } from "../internal/async-event-bus.ts";
|
|
19
19
|
import { matchesMarketFilter } from "../internal/filters.ts";
|
|
20
20
|
import type {
|
|
21
|
-
Exchange,
|
|
22
21
|
FundingRateSnapshot,
|
|
23
22
|
FundingRateUpdatedEvent,
|
|
24
23
|
L1Book,
|
|
@@ -31,9 +30,12 @@ import type {
|
|
|
31
30
|
MarketKeyInput,
|
|
32
31
|
MarketManager,
|
|
33
32
|
MarketStatusChangedEvent,
|
|
33
|
+
NormalizedOrderInput,
|
|
34
|
+
NormalizeOrderInputInput,
|
|
34
35
|
SubscribeFundingRateInput,
|
|
35
36
|
SubscribeL1BookInput,
|
|
36
37
|
SubscriptionActivity,
|
|
38
|
+
Venue,
|
|
37
39
|
} from "../types/index.ts";
|
|
38
40
|
|
|
39
41
|
export interface MarketManagerOptions {
|
|
@@ -44,7 +46,7 @@ export interface MarketManagerOptions {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
interface MarketRecord {
|
|
47
|
-
|
|
49
|
+
venue: Venue;
|
|
48
50
|
symbol: string;
|
|
49
51
|
market?: MarketDefinition;
|
|
50
52
|
l1Book?: L1Book;
|
|
@@ -66,7 +68,7 @@ const DEFAULT_L1_RECONNECT_DELAY_MS = 1_000;
|
|
|
66
68
|
const DEFAULT_L1_RECONNECT_MAX_DELAY_MS = 10_000;
|
|
67
69
|
|
|
68
70
|
function marketKey(input: MarketKeyInput): string {
|
|
69
|
-
return `${input.
|
|
71
|
+
return `${input.venue}:${input.symbol}`;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
function cloneMarketStatus(status: MarketDataStatus): MarketDataStatus {
|
|
@@ -91,6 +93,17 @@ function cloneMarketDefinition(definition: MarketDefinition): MarketDefinition {
|
|
|
91
93
|
return { ...definition, raw: { ...definition.raw } };
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
function floorToStep(value: BigNumber, step: BigNumber): BigNumber {
|
|
97
|
+
if (step.isLessThanOrEqualTo(0)) {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
return value.dividedToIntegerBy(step).multipliedBy(step);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeDecimalInput(value: BigNumber): string {
|
|
104
|
+
return value.isFinite() ? value.toFixed() : value.toString();
|
|
105
|
+
}
|
|
106
|
+
|
|
94
107
|
export class MarketManagerImpl
|
|
95
108
|
implements MarketManager, ManagerLifecycle, HealthReporter<MarketDataStatus>
|
|
96
109
|
{
|
|
@@ -156,7 +169,7 @@ export class MarketManagerImpl
|
|
|
156
169
|
this.context.assertStarted();
|
|
157
170
|
const market = await this.resolveMarketDefinition(input);
|
|
158
171
|
const record = this.getOrCreateRecord({
|
|
159
|
-
|
|
172
|
+
venue: input.venue,
|
|
160
173
|
symbol: market.symbol,
|
|
161
174
|
});
|
|
162
175
|
|
|
@@ -189,7 +202,7 @@ export class MarketManagerImpl
|
|
|
189
202
|
const market = await this.resolveMarketDefinition(input);
|
|
190
203
|
this.assertFundingRateSupported(market);
|
|
191
204
|
const record = this.getOrCreateRecord({
|
|
192
|
-
|
|
205
|
+
venue: input.venue,
|
|
193
206
|
symbol: market.symbol,
|
|
194
207
|
});
|
|
195
208
|
|
|
@@ -219,28 +232,86 @@ export class MarketManagerImpl
|
|
|
219
232
|
this.recomputeAndPublishStatus(record, this.context.now());
|
|
220
233
|
}
|
|
221
234
|
|
|
222
|
-
getMarket(
|
|
223
|
-
const market = this.definitions.get(marketKey({
|
|
235
|
+
getMarket(venue: Venue, symbol: string): MarketDefinition | undefined {
|
|
236
|
+
const market = this.definitions.get(marketKey({ venue, symbol }));
|
|
224
237
|
return market ? cloneMarketDefinition(market) : undefined;
|
|
225
238
|
}
|
|
226
239
|
|
|
227
240
|
getMarkets(symbol: string): MarketDefinition[] {
|
|
228
241
|
return [...this.definitions.values()]
|
|
229
242
|
.filter((market) => market.symbol === symbol)
|
|
230
|
-
.sort((left, right) => left.
|
|
243
|
+
.sort((left, right) => left.venue.localeCompare(right.venue))
|
|
231
244
|
.map((market) => cloneMarketDefinition(market));
|
|
232
245
|
}
|
|
233
246
|
|
|
234
|
-
listMarkets(
|
|
247
|
+
listMarkets(venue?: Venue): MarketDefinition[] {
|
|
235
248
|
const values = [...this.definitions.values()];
|
|
236
|
-
const filtered =
|
|
237
|
-
? values.filter((market) => market.
|
|
249
|
+
const filtered = venue
|
|
250
|
+
? values.filter((market) => market.venue === venue)
|
|
238
251
|
: values;
|
|
239
252
|
return filtered
|
|
240
253
|
.sort((left, right) => left.symbol.localeCompare(right.symbol))
|
|
241
254
|
.map((market) => cloneMarketDefinition(market));
|
|
242
255
|
}
|
|
243
256
|
|
|
257
|
+
normalizeOrderInput(input: NormalizeOrderInputInput): NormalizedOrderInput {
|
|
258
|
+
const market = this.resolveLoadedMarket(input);
|
|
259
|
+
const rawPrice = new BigNumber(input.price);
|
|
260
|
+
const rawAmount = new BigNumber(input.amount);
|
|
261
|
+
const price = floorToStep(rawPrice, market.priceStep);
|
|
262
|
+
const amount = floorToStep(rawAmount, market.amountStep);
|
|
263
|
+
|
|
264
|
+
const normalized: NormalizedOrderInput = {
|
|
265
|
+
price: normalizeDecimalInput(price),
|
|
266
|
+
amount: normalizeDecimalInput(amount),
|
|
267
|
+
rawPrice: normalizeDecimalInput(rawPrice),
|
|
268
|
+
rawAmount: normalizeDecimalInput(rawAmount),
|
|
269
|
+
adjusted: !price.isEqualTo(rawPrice) || !amount.isEqualTo(rawAmount),
|
|
270
|
+
accepted: true,
|
|
271
|
+
priceStep: market.priceStep.toFixed(),
|
|
272
|
+
amountStep: market.amountStep.toFixed(),
|
|
273
|
+
minAmount: market.minAmount?.toFixed(),
|
|
274
|
+
minNotional: market.minNotional?.toFixed(),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (!price.isFinite() || price.isLessThanOrEqualTo(0)) {
|
|
278
|
+
return {
|
|
279
|
+
...normalized,
|
|
280
|
+
accepted: false,
|
|
281
|
+
rejectReason: "price_not_positive",
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!amount.isFinite() || amount.isLessThanOrEqualTo(0)) {
|
|
286
|
+
return {
|
|
287
|
+
...normalized,
|
|
288
|
+
accepted: false,
|
|
289
|
+
rejectReason: "amount_not_positive",
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (market.minAmount && amount.isLessThan(market.minAmount)) {
|
|
294
|
+
return {
|
|
295
|
+
...normalized,
|
|
296
|
+
accepted: false,
|
|
297
|
+
rejectReason: "amount_below_min",
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (market.minNotional) {
|
|
302
|
+
const notional = amount.multipliedBy(price);
|
|
303
|
+
if (notional.isLessThan(market.minNotional)) {
|
|
304
|
+
return {
|
|
305
|
+
...normalized,
|
|
306
|
+
accepted: false,
|
|
307
|
+
rejectReason: "notional_below_min",
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return normalized;
|
|
313
|
+
}
|
|
314
|
+
|
|
244
315
|
getL1Book(key: MarketKeyInput): L1Book | undefined {
|
|
245
316
|
const book = this.records.get(marketKey(key))?.l1Book;
|
|
246
317
|
return book ? cloneL1Book(book) : undefined;
|
|
@@ -253,7 +324,7 @@ export class MarketManagerImpl
|
|
|
253
324
|
record.symbol === symbol && Boolean(record.l1Book),
|
|
254
325
|
)
|
|
255
326
|
.map((record) => cloneL1Book(record.l1Book))
|
|
256
|
-
.sort((left, right) => left.
|
|
327
|
+
.sort((left, right) => left.venue.localeCompare(right.venue));
|
|
257
328
|
}
|
|
258
329
|
|
|
259
330
|
getFundingRate(key: MarketKeyInput): FundingRateSnapshot | undefined {
|
|
@@ -270,7 +341,7 @@ export class MarketManagerImpl
|
|
|
270
341
|
record.symbol === symbol && Boolean(record.fundingRate),
|
|
271
342
|
)
|
|
272
343
|
.map((record) => cloneFundingRate(record.fundingRate))
|
|
273
|
-
.sort((left, right) => left.
|
|
344
|
+
.sort((left, right) => left.venue.localeCompare(right.venue));
|
|
274
345
|
}
|
|
275
346
|
|
|
276
347
|
getMarketStatus(key: MarketKeyInput): MarketDataStatus | undefined {
|
|
@@ -335,8 +406,8 @@ export class MarketManagerImpl
|
|
|
335
406
|
return [...this.records.values()]
|
|
336
407
|
.map((record) => cloneMarketStatus(record.status))
|
|
337
408
|
.sort((left, right) =>
|
|
338
|
-
`${left.
|
|
339
|
-
`${right.
|
|
409
|
+
`${left.venue}:${left.symbol}`.localeCompare(
|
|
410
|
+
`${right.venue}:${right.symbol}`,
|
|
340
411
|
),
|
|
341
412
|
);
|
|
342
413
|
}
|
|
@@ -379,17 +450,17 @@ export class MarketManagerImpl
|
|
|
379
450
|
error instanceof Error
|
|
380
451
|
? error
|
|
381
452
|
: new Error("Unknown catalog load failure"),
|
|
382
|
-
{
|
|
453
|
+
{ venue: this.adapter.venue },
|
|
383
454
|
);
|
|
384
455
|
throw wrapped;
|
|
385
456
|
}
|
|
386
457
|
}
|
|
387
458
|
|
|
388
459
|
private async resolveMarketDefinition(input: {
|
|
389
|
-
|
|
460
|
+
venue: Venue;
|
|
390
461
|
symbol: string;
|
|
391
462
|
}): Promise<MarketDefinition> {
|
|
392
|
-
this.
|
|
463
|
+
this.assertSupportedVenue(input.venue);
|
|
393
464
|
await this.loadMarketCatalog();
|
|
394
465
|
|
|
395
466
|
const market = this.definitions.get(marketKey(input));
|
|
@@ -397,7 +468,7 @@ export class MarketManagerImpl
|
|
|
397
468
|
throw this.createError(
|
|
398
469
|
"MARKET_NOT_FOUND",
|
|
399
470
|
`Unknown market symbol: ${input.symbol}`,
|
|
400
|
-
{
|
|
471
|
+
{ venue: input.venue, symbol: input.symbol },
|
|
401
472
|
"market",
|
|
402
473
|
);
|
|
403
474
|
}
|
|
@@ -406,7 +477,21 @@ export class MarketManagerImpl
|
|
|
406
477
|
throw this.createError(
|
|
407
478
|
"MARKET_INACTIVE",
|
|
408
479
|
`Inactive market symbol: ${input.symbol}`,
|
|
409
|
-
{
|
|
480
|
+
{ venue: input.venue, symbol: input.symbol },
|
|
481
|
+
"market",
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return market;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private resolveLoadedMarket(input: MarketKeyInput): MarketDefinition {
|
|
489
|
+
const market = this.definitions.get(marketKey(input));
|
|
490
|
+
if (!market) {
|
|
491
|
+
throw this.createError(
|
|
492
|
+
"MARKET_NOT_FOUND",
|
|
493
|
+
`Unknown market symbol: ${input.symbol}`,
|
|
494
|
+
{ venue: input.venue, symbol: input.symbol },
|
|
410
495
|
"market",
|
|
411
496
|
);
|
|
412
497
|
}
|
|
@@ -414,15 +499,15 @@ export class MarketManagerImpl
|
|
|
414
499
|
return market;
|
|
415
500
|
}
|
|
416
501
|
|
|
417
|
-
private
|
|
418
|
-
if (
|
|
502
|
+
private assertSupportedVenue(venue: Venue): void {
|
|
503
|
+
if (venue === this.adapter.venue) {
|
|
419
504
|
return;
|
|
420
505
|
}
|
|
421
506
|
|
|
422
507
|
throw this.createError(
|
|
423
|
-
"
|
|
424
|
-
`
|
|
425
|
-
{
|
|
508
|
+
"VENUE_NOT_SUPPORTED",
|
|
509
|
+
`Venue is not supported yet: ${venue}`,
|
|
510
|
+
{ venue },
|
|
426
511
|
"client",
|
|
427
512
|
);
|
|
428
513
|
}
|
|
@@ -435,13 +520,13 @@ export class MarketManagerImpl
|
|
|
435
520
|
throw this.createError(
|
|
436
521
|
"MARKET_FUNDING_RATE_UNSUPPORTED",
|
|
437
522
|
`Funding rate is not supported for market: ${market.symbol}`,
|
|
438
|
-
{
|
|
523
|
+
{ venue: market.venue, symbol: market.symbol },
|
|
439
524
|
"market",
|
|
440
525
|
);
|
|
441
526
|
}
|
|
442
527
|
|
|
443
528
|
private getOrCreateRecord(input: {
|
|
444
|
-
|
|
529
|
+
venue: Venue;
|
|
445
530
|
symbol: string;
|
|
446
531
|
}): MarketRecord {
|
|
447
532
|
const key = marketKey(input);
|
|
@@ -451,12 +536,12 @@ export class MarketManagerImpl
|
|
|
451
536
|
}
|
|
452
537
|
|
|
453
538
|
const record: MarketRecord = {
|
|
454
|
-
|
|
539
|
+
venue: input.venue,
|
|
455
540
|
symbol: input.symbol,
|
|
456
541
|
l1BookSubscribed: false,
|
|
457
542
|
fundingRateSubscribed: false,
|
|
458
543
|
status: {
|
|
459
|
-
|
|
544
|
+
venue: input.venue,
|
|
460
545
|
symbol: input.symbol,
|
|
461
546
|
activity: "inactive",
|
|
462
547
|
ready: false,
|
|
@@ -487,7 +572,7 @@ export class MarketManagerImpl
|
|
|
487
572
|
`Timed out waiting for market data: ${market.symbol}`,
|
|
488
573
|
);
|
|
489
574
|
this.context.publishRuntimeError("runtime", timeoutError, {
|
|
490
|
-
|
|
575
|
+
venue: market.venue,
|
|
491
576
|
symbol: market.symbol,
|
|
492
577
|
});
|
|
493
578
|
this.updateConnectionState(record, "l1Book", "stale", "ws_disconnected");
|
|
@@ -515,7 +600,7 @@ export class MarketManagerImpl
|
|
|
515
600
|
`Timed out waiting for market data: ${market.symbol}`,
|
|
516
601
|
);
|
|
517
602
|
this.context.publishRuntimeError("runtime", timeoutError, {
|
|
518
|
-
|
|
603
|
+
venue: market.venue,
|
|
519
604
|
symbol: market.symbol,
|
|
520
605
|
});
|
|
521
606
|
this.updateConnectionState(
|
|
@@ -535,7 +620,7 @@ export class MarketManagerImpl
|
|
|
535
620
|
const callbacks: L1BookStreamCallbacks = {
|
|
536
621
|
onUpdate: (update: RawL1BookUpdate) => {
|
|
537
622
|
record.l1Book = this.createL1Book(
|
|
538
|
-
record.
|
|
623
|
+
record.venue,
|
|
539
624
|
record.symbol,
|
|
540
625
|
update,
|
|
541
626
|
record.l1Book,
|
|
@@ -546,7 +631,7 @@ export class MarketManagerImpl
|
|
|
546
631
|
|
|
547
632
|
const event: L1BookUpdatedEvent = {
|
|
548
633
|
type: "l1_book.updated",
|
|
549
|
-
|
|
634
|
+
venue: record.venue,
|
|
550
635
|
symbol: record.symbol,
|
|
551
636
|
snapshot: cloneL1Book(record.l1Book),
|
|
552
637
|
ts: this.context.now(),
|
|
@@ -568,7 +653,7 @@ export class MarketManagerImpl
|
|
|
568
653
|
},
|
|
569
654
|
onError: (error) => {
|
|
570
655
|
this.context.publishRuntimeError("runtime", error, {
|
|
571
|
-
|
|
656
|
+
venue: record.venue,
|
|
572
657
|
symbol: record.symbol,
|
|
573
658
|
});
|
|
574
659
|
},
|
|
@@ -592,7 +677,7 @@ export class MarketManagerImpl
|
|
|
592
677
|
const callbacks: FundingRateStreamCallbacks = {
|
|
593
678
|
onUpdate: (update: RawFundingRateUpdate) => {
|
|
594
679
|
record.fundingRate = this.createFundingRate(
|
|
595
|
-
record.
|
|
680
|
+
record.venue,
|
|
596
681
|
record.symbol,
|
|
597
682
|
update,
|
|
598
683
|
record.fundingRate,
|
|
@@ -603,7 +688,7 @@ export class MarketManagerImpl
|
|
|
603
688
|
|
|
604
689
|
const event: FundingRateUpdatedEvent = {
|
|
605
690
|
type: "funding_rate.updated",
|
|
606
|
-
|
|
691
|
+
venue: record.venue,
|
|
607
692
|
symbol: record.symbol,
|
|
608
693
|
snapshot: cloneFundingRate(record.fundingRate),
|
|
609
694
|
ts: this.context.now(),
|
|
@@ -625,7 +710,7 @@ export class MarketManagerImpl
|
|
|
625
710
|
},
|
|
626
711
|
onError: (error) => {
|
|
627
712
|
this.context.publishRuntimeError("runtime", error, {
|
|
628
|
-
|
|
713
|
+
venue: record.venue,
|
|
629
714
|
symbol: record.symbol,
|
|
630
715
|
});
|
|
631
716
|
},
|
|
@@ -643,13 +728,13 @@ export class MarketManagerImpl
|
|
|
643
728
|
}
|
|
644
729
|
|
|
645
730
|
private createL1Book(
|
|
646
|
-
|
|
731
|
+
venue: Venue,
|
|
647
732
|
symbol: string,
|
|
648
733
|
input: RawL1BookUpdate,
|
|
649
734
|
previous?: L1Book,
|
|
650
735
|
): L1Book {
|
|
651
736
|
return {
|
|
652
|
-
|
|
737
|
+
venue,
|
|
653
738
|
symbol,
|
|
654
739
|
bidPrice: new BigNumber(input.bidPrice),
|
|
655
740
|
bidSize: new BigNumber(input.bidSize),
|
|
@@ -670,13 +755,13 @@ export class MarketManagerImpl
|
|
|
670
755
|
}
|
|
671
756
|
|
|
672
757
|
private createFundingRate(
|
|
673
|
-
|
|
758
|
+
venue: Venue,
|
|
674
759
|
symbol: string,
|
|
675
760
|
input: RawFundingRateUpdate,
|
|
676
761
|
previous?: FundingRateSnapshot,
|
|
677
762
|
): FundingRateSnapshot {
|
|
678
763
|
return {
|
|
679
|
-
|
|
764
|
+
venue,
|
|
680
765
|
symbol,
|
|
681
766
|
fundingRate: new BigNumber(input.fundingRate),
|
|
682
767
|
nextFundingTime: input.nextFundingTime,
|
|
@@ -851,7 +936,7 @@ export class MarketManagerImpl
|
|
|
851
936
|
private publishStatus(record: MarketRecord): void {
|
|
852
937
|
const event: MarketStatusChangedEvent = {
|
|
853
938
|
type: "market.status_changed",
|
|
854
|
-
|
|
939
|
+
venue: record.venue,
|
|
855
940
|
symbol: record.symbol,
|
|
856
941
|
status: cloneMarketStatus(record.status),
|
|
857
942
|
ts: this.context.now(),
|
|
@@ -900,9 +985,9 @@ export class MarketManagerImpl
|
|
|
900
985
|
| "MARKET_NOT_FOUND"
|
|
901
986
|
| "MARKET_INACTIVE"
|
|
902
987
|
| "MARKET_FUNDING_RATE_UNSUPPORTED"
|
|
903
|
-
| "
|
|
988
|
+
| "VENUE_NOT_SUPPORTED",
|
|
904
989
|
message: string,
|
|
905
|
-
metadata?: {
|
|
990
|
+
metadata?: { venue?: Venue; symbol?: string },
|
|
906
991
|
source: "market" | "client" = "market",
|
|
907
992
|
): AcexError {
|
|
908
993
|
const error = new AcexError(code, message);
|