@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.
@@ -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
- exchange: Exchange;
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.exchange}:${input.symbol}`;
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
- exchange: input.exchange,
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
- exchange: input.exchange,
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(exchange: Exchange, symbol: string): MarketDefinition | undefined {
223
- const market = this.definitions.get(marketKey({ exchange, symbol }));
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.exchange.localeCompare(right.exchange))
243
+ .sort((left, right) => left.venue.localeCompare(right.venue))
231
244
  .map((market) => cloneMarketDefinition(market));
232
245
  }
233
246
 
234
- listMarkets(exchange?: Exchange): MarketDefinition[] {
247
+ listMarkets(venue?: Venue): MarketDefinition[] {
235
248
  const values = [...this.definitions.values()];
236
- const filtered = exchange
237
- ? values.filter((market) => market.exchange === exchange)
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.exchange.localeCompare(right.exchange));
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.exchange.localeCompare(right.exchange));
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.exchange}:${left.symbol}`.localeCompare(
339
- `${right.exchange}:${right.symbol}`,
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
- { exchange: this.adapter.exchange },
453
+ { venue: this.adapter.venue },
383
454
  );
384
455
  throw wrapped;
385
456
  }
386
457
  }
387
458
 
388
459
  private async resolveMarketDefinition(input: {
389
- exchange: Exchange;
460
+ venue: Venue;
390
461
  symbol: string;
391
462
  }): Promise<MarketDefinition> {
392
- this.assertSupportedExchange(input.exchange);
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
- { exchange: input.exchange, symbol: input.symbol },
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
- { exchange: input.exchange, symbol: input.symbol },
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 assertSupportedExchange(exchange: Exchange): void {
418
- if (exchange === this.adapter.exchange) {
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
- "EXCHANGE_NOT_SUPPORTED",
424
- `Exchange is not supported yet: ${exchange}`,
425
- { exchange },
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
- { exchange: market.exchange, symbol: market.symbol },
523
+ { venue: market.venue, symbol: market.symbol },
439
524
  "market",
440
525
  );
441
526
  }
442
527
 
443
528
  private getOrCreateRecord(input: {
444
- exchange: Exchange;
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
- exchange: input.exchange,
539
+ venue: input.venue,
455
540
  symbol: input.symbol,
456
541
  l1BookSubscribed: false,
457
542
  fundingRateSubscribed: false,
458
543
  status: {
459
- exchange: input.exchange,
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
- exchange: market.exchange,
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
- exchange: market.exchange,
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.exchange,
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
- exchange: record.exchange,
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
- exchange: record.exchange,
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.exchange,
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
- exchange: record.exchange,
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
- exchange: record.exchange,
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
- exchange: Exchange,
731
+ venue: Venue,
647
732
  symbol: string,
648
733
  input: RawL1BookUpdate,
649
734
  previous?: L1Book,
650
735
  ): L1Book {
651
736
  return {
652
- exchange,
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
- exchange: Exchange,
758
+ venue: Venue,
674
759
  symbol: string,
675
760
  input: RawFundingRateUpdate,
676
761
  previous?: FundingRateSnapshot,
677
762
  ): FundingRateSnapshot {
678
763
  return {
679
- exchange,
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
- exchange: record.exchange,
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
- | "EXCHANGE_NOT_SUPPORTED",
988
+ | "VENUE_NOT_SUPPORTED",
904
989
  message: string,
905
- metadata?: { exchange?: Exchange; symbol?: string },
990
+ metadata?: { venue?: Venue; symbol?: string },
906
991
  source: "market" | "client" = "market",
907
992
  ): AcexError {
908
993
  const error = new AcexError(code, message);