@usherlabs/cex-broker 0.2.24 → 0.2.25

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/dist/index.js CHANGED
@@ -274461,6 +274461,9 @@ function compactUndefined(record) {
274461
274461
  return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
274462
274462
  }
274463
274463
 
274464
+ // src/helpers/broker-execution-archive/rows.ts
274465
+ import { createHash as createHash2 } from "crypto";
274466
+
274464
274467
  // src/helpers/broker-execution-archive/redact.ts
274465
274468
  import { createHash } from "crypto";
274466
274469
  var SECRET_KEY_PATTERN = /\b(api[_-]?key|api[_-]?secret|secret|signature|passphrase|password|token|credential)\b/i;
@@ -274533,6 +274536,8 @@ function hashMarketMetadata(payload) {
274533
274536
  var BROKER_WRITE_SOURCE = "broker_write";
274534
274537
  var ARCHIVE_SCHEMA_VERSION = "1";
274535
274538
  var FILL_EVENT_KIND = "trade_history_fill";
274539
+ var ACCOUNT_BALANCE_SCOPE = "spot";
274540
+ var ACCOUNT_BALANCE_PRECISION_BASIS = "ccxt_normalized_number";
274536
274541
 
274537
274542
  // src/helpers/broker-execution-archive/rows.ts
274538
274543
  function firstString2(...values2) {
@@ -274566,6 +274571,151 @@ function normalizeTimestamp2(value) {
274566
274571
  const date = new Date(ms);
274567
274572
  return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
274568
274573
  }
274574
+ function decimalString(value) {
274575
+ if (typeof value !== "number" || !Number.isFinite(value)) {
274576
+ return;
274577
+ }
274578
+ if (Object.is(value, -0)) {
274579
+ return "0";
274580
+ }
274581
+ const rendered = String(value).toLowerCase();
274582
+ if (!rendered.includes("e")) {
274583
+ return rendered;
274584
+ }
274585
+ const [coefficient = "", rawExponent = "0"] = rendered.split("e");
274586
+ const exponent = Number.parseInt(rawExponent, 10);
274587
+ const negative = coefficient.startsWith("-");
274588
+ const unsigned = negative ? coefficient.slice(1) : coefficient;
274589
+ const [integer = "0", fraction = ""] = unsigned.split(".");
274590
+ const digits = `${integer}${fraction}`;
274591
+ const decimalIndex = integer.length + exponent;
274592
+ let expanded;
274593
+ if (decimalIndex <= 0) {
274594
+ expanded = `0.${"0".repeat(-decimalIndex)}${digits}`;
274595
+ } else if (decimalIndex >= digits.length) {
274596
+ expanded = `${digits}${"0".repeat(decimalIndex - digits.length)}`;
274597
+ } else {
274598
+ expanded = `${digits.slice(0, decimalIndex)}.${digits.slice(decimalIndex)}`;
274599
+ }
274600
+ return negative ? `-${expanded}` : expanded;
274601
+ }
274602
+ function normalizedBalanceMap(value) {
274603
+ const record = asRecord(value);
274604
+ if (!record) {
274605
+ return {};
274606
+ }
274607
+ const entries = [];
274608
+ for (const [rawAsset, quantity] of Object.entries(record)) {
274609
+ const asset = rawAsset.trim();
274610
+ const normalized = decimalString(quantity);
274611
+ if (asset && normalized !== undefined) {
274612
+ entries.push([asset, normalized]);
274613
+ }
274614
+ }
274615
+ return Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right)));
274616
+ }
274617
+ function sortedBalanceMap(map) {
274618
+ return Object.fromEntries(Object.entries(map).sort(([left], [right]) => left.localeCompare(right)));
274619
+ }
274620
+ var BALANCE_METADATA_KEYS = new Set([
274621
+ "info",
274622
+ "timestamp",
274623
+ "datetime",
274624
+ "free",
274625
+ "used",
274626
+ "total",
274627
+ "debt"
274628
+ ]);
274629
+ function normalizeCcxtBalanceForArchive(balance) {
274630
+ const record = asRecord(balance) ?? {};
274631
+ const aggregateRecords = {
274632
+ free: asRecord(record.free),
274633
+ used: asRecord(record.used),
274634
+ total: asRecord(record.total)
274635
+ };
274636
+ const maps = {
274637
+ free: normalizedBalanceMap(record.free),
274638
+ used: normalizedBalanceMap(record.used),
274639
+ total: normalizedBalanceMap(record.total)
274640
+ };
274641
+ const assetEntryAssets = new Set;
274642
+ for (const [rawAsset, value] of Object.entries(record)) {
274643
+ if (BALANCE_METADATA_KEYS.has(rawAsset)) {
274644
+ continue;
274645
+ }
274646
+ const asset = rawAsset.trim();
274647
+ const assetEntry = asRecord(value);
274648
+ if (!asset || !assetEntry || !(("free" in assetEntry) || ("used" in assetEntry) || ("total" in assetEntry))) {
274649
+ continue;
274650
+ }
274651
+ assetEntryAssets.add(asset);
274652
+ for (const field of ["free", "used", "total"]) {
274653
+ const normalized = decimalString(assetEntry[field]);
274654
+ if (normalized !== undefined && maps[field][asset] === undefined) {
274655
+ maps[field][asset] = normalized;
274656
+ }
274657
+ }
274658
+ }
274659
+ const reportedAssets = new Set(assetEntryAssets);
274660
+ for (const aggregateRecord of Object.values(aggregateRecords)) {
274661
+ for (const rawAsset of Object.keys(aggregateRecord ?? {})) {
274662
+ const asset = rawAsset.trim();
274663
+ if (asset) {
274664
+ reportedAssets.add(asset);
274665
+ }
274666
+ }
274667
+ }
274668
+ for (const map of Object.values(maps)) {
274669
+ for (const asset of Object.keys(map)) {
274670
+ reportedAssets.add(asset);
274671
+ }
274672
+ }
274673
+ return {
274674
+ exchangeTimestamp: normalizeTimestamp2(record.timestamp ?? record.datetime),
274675
+ reportedAssets: [...reportedAssets].sort(),
274676
+ assetEntryAssets: [...assetEntryAssets].sort(),
274677
+ freeBalances: sortedBalanceMap(maps.free),
274678
+ usedBalances: sortedBalanceMap(maps.used),
274679
+ totalBalances: sortedBalanceMap(maps.total),
274680
+ freeMapPresent: aggregateRecords.free !== undefined,
274681
+ usedMapPresent: aggregateRecords.used !== undefined,
274682
+ totalMapPresent: aggregateRecords.total !== undefined
274683
+ };
274684
+ }
274685
+ function buildAccountBalanceSnapshotRow(input) {
274686
+ const { tags, balance } = input;
274687
+ const observationId = createHash2("sha256").update(JSON.stringify({
274688
+ deployment_id: tags.deployment_id,
274689
+ exchange: tags.exchange,
274690
+ account_selector: tags.account_selector,
274691
+ balance_scope: ACCOUNT_BALANCE_SCOPE,
274692
+ broker_observed_timestamp: tags.broker_observed_timestamp,
274693
+ balance
274694
+ })).digest("hex");
274695
+ return {
274696
+ table: "broker_account.balance_snapshots",
274697
+ row: compactUndefined2({
274698
+ broker_observed_timestamp: tags.broker_observed_timestamp,
274699
+ exchange_timestamp: balance.exchangeTimestamp,
274700
+ source: tags.source,
274701
+ deployment_id: tags.deployment_id,
274702
+ schema_version: ARCHIVE_SCHEMA_VERSION,
274703
+ exchange: tags.exchange,
274704
+ account_selector: tags.account_selector,
274705
+ balance_scope: ACCOUNT_BALANCE_SCOPE,
274706
+ observation_id: observationId,
274707
+ reported_assets: balance.reportedAssets,
274708
+ asset_entry_assets: balance.assetEntryAssets,
274709
+ free_balances: balance.freeBalances,
274710
+ used_balances: balance.usedBalances,
274711
+ total_balances: balance.totalBalances,
274712
+ aggregate_free_map_present: Number(balance.freeMapPresent),
274713
+ aggregate_used_map_present: Number(balance.usedMapPresent),
274714
+ aggregate_total_map_present: Number(balance.totalMapPresent),
274715
+ precision_basis: ACCOUNT_BALANCE_PRECISION_BASIS
274716
+ })
274717
+ };
274718
+ }
274569
274719
  function compactUndefined2(record) {
274570
274720
  return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
274571
274721
  }
@@ -275146,6 +275296,15 @@ function isMarketArchiveTable(table) {
275146
275296
  }
275147
275297
 
275148
275298
  // src/helpers/broker-execution-archive/writer.ts
275299
+ var BROKER_EXECUTION_ARCHIVE_TABLES = new Set([
275300
+ "broker_execution.order_events",
275301
+ "broker_execution.market_metadata_snapshots",
275302
+ "broker_execution.transfer_events",
275303
+ "broker_execution.fill_events"
275304
+ ]);
275305
+ function isBrokerExecutionArchiveTable(table) {
275306
+ return BROKER_EXECUTION_ARCHIVE_TABLES.has(table);
275307
+ }
275149
275308
  var DEFAULT_MAX_QUEUE_SIZE = 1e4;
275150
275309
  var DEFAULT_BATCH_SIZE = 10;
275151
275310
  var DEFAULT_FLUSH_INTERVAL_MS = 1000;
@@ -275230,6 +275389,9 @@ class BrokerExecutionArchiver {
275230
275389
  canPersistMarketMetadataSnapshot() {
275231
275390
  return this.isEnabled() && (Boolean(this.otelLogs?.isOtelEnabled()) || Boolean(this.forwarderUrl));
275232
275391
  }
275392
+ canPersistAccountBalanceSnapshots() {
275393
+ return this.isEnabled() && Boolean(this.forwarderUrl);
275394
+ }
275233
275395
  enqueue(row) {
275234
275396
  if (!this.enabled || this.closed || !this.otelLogs && !this.forwarderUrl) {
275235
275397
  return;
@@ -275241,6 +275403,9 @@ class BrokerExecutionArchiver {
275241
275403
  }
275242
275404
  return;
275243
275405
  }
275406
+ if (row.table === "broker_account.balance_snapshots" && !this.forwarderUrl) {
275407
+ return;
275408
+ }
275244
275409
  if (this.queue.length >= this.maxQueueSize) {
275245
275410
  this.queue.shift();
275246
275411
  this.stats.shed += 1;
@@ -275319,7 +275484,7 @@ class BrokerExecutionArchiver {
275319
275484
  return true;
275320
275485
  }
275321
275486
  for (const entry of batch) {
275322
- if (!isMarketArchiveTable(entry.table)) {
275487
+ if (isBrokerExecutionArchiveTable(entry.table)) {
275323
275488
  this.emitOtelLog(entry);
275324
275489
  }
275325
275490
  }
@@ -275455,8 +275620,138 @@ function parsePositiveInt(value, fallback) {
275455
275620
  const parsed = Number.parseInt(value, 10);
275456
275621
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
275457
275622
  }
275458
- // src/helpers/fill-archive-poller.ts
275623
+ // src/helpers/account-balance-archive-poller.ts
275459
275624
  var DEFAULT_CONFIG = {
275625
+ pollIntervalMs: 60000
275626
+ };
275627
+ var metricLabels = (target) => ({
275628
+ exchange: target.exchangeId,
275629
+ account_selector: target.account.label,
275630
+ balance_scope: ACCOUNT_BALANCE_SCOPE
275631
+ });
275632
+
275633
+ class AccountBalanceArchivePoller {
275634
+ params;
275635
+ #timer = null;
275636
+ #stopped = false;
275637
+ #running = null;
275638
+ #lastSuccessMs = new Map;
275639
+ #config;
275640
+ constructor(params) {
275641
+ this.params = params;
275642
+ this.#config = { ...DEFAULT_CONFIG, ...params.config };
275643
+ }
275644
+ start() {
275645
+ if (this.#timer || this.#stopped || !this.params.archiver.canPersistAccountBalanceSnapshots()) {
275646
+ return;
275647
+ }
275648
+ log.info("\uD83D\uDCB0 Account balance archive poller started", {
275649
+ balanceScope: ACCOUNT_BALANCE_SCOPE
275650
+ });
275651
+ this.#schedule(0);
275652
+ }
275653
+ async stop() {
275654
+ this.#stopped = true;
275655
+ if (this.#timer) {
275656
+ clearTimeout(this.#timer);
275657
+ this.#timer = null;
275658
+ }
275659
+ await this.#running;
275660
+ }
275661
+ async pollAllOnce() {
275662
+ if (this.#stopped || this.#running || !this.params.archiver.canPersistAccountBalanceSnapshots()) {
275663
+ return false;
275664
+ }
275665
+ this.#running = this.#pollAllSequentially();
275666
+ try {
275667
+ return await this.#running;
275668
+ } finally {
275669
+ this.#running = null;
275670
+ }
275671
+ }
275672
+ #targets() {
275673
+ const targets = [];
275674
+ for (const [exchangeId, pool] of Object.entries(this.params.brokers)) {
275675
+ for (const account of [pool.primary, ...pool.secondaryBrokers]) {
275676
+ targets.push({ exchangeId, account });
275677
+ }
275678
+ }
275679
+ return targets;
275680
+ }
275681
+ async#pollAllSequentially() {
275682
+ for (const target of this.#targets()) {
275683
+ if (this.#stopped) {
275684
+ break;
275685
+ }
275686
+ await this.#pollOne(target);
275687
+ }
275688
+ return true;
275689
+ }
275690
+ async#pollOne(target) {
275691
+ const labels = metricLabels(target);
275692
+ this.params.metrics?.recordCounter("cex_account_balance_poll_attempts_total", 1, labels);
275693
+ try {
275694
+ const exchange = target.account.exchange;
275695
+ const balance = await exchange.fetchBalance({
275696
+ type: ACCOUNT_BALANCE_SCOPE
275697
+ });
275698
+ const observedAt = new Date;
275699
+ const normalized = normalizeCcxtBalanceForArchive(balance);
275700
+ this.params.archiver.enqueue(buildAccountBalanceSnapshotRow({
275701
+ tags: buildCommonArchiveTags({
275702
+ deploymentId: this.params.archiver.getDeploymentId(),
275703
+ accountSelector: target.account.label,
275704
+ exchange: target.exchangeId,
275705
+ brokerObservedTimestamp: observedAt.toISOString()
275706
+ }),
275707
+ balance: normalized
275708
+ }));
275709
+ const successMs = Date.now();
275710
+ this.#lastSuccessMs.set(this.#targetKey(target), successMs);
275711
+ this.params.metrics?.recordCounter("cex_account_balance_poll_successes_total", 1, labels);
275712
+ this.params.metrics?.recordGauge("cex_account_balance_poll_last_success_timestamp_seconds", Math.floor(successMs / 1000), labels);
275713
+ this.#recordFreshness(labels, successMs, successMs);
275714
+ } catch (error) {
275715
+ this.params.metrics?.recordCounter("cex_account_balance_poll_failures_total", 1, labels);
275716
+ const now3 = Date.now();
275717
+ const lastSuccess = this.#lastSuccessMs.get(this.#targetKey(target));
275718
+ if (lastSuccess !== undefined) {
275719
+ this.#recordFreshness(labels, lastSuccess, now3);
275720
+ }
275721
+ log.warn("Account balance archive poll failed", {
275722
+ exchange: target.exchangeId,
275723
+ account: target.account.label,
275724
+ balanceScope: ACCOUNT_BALANCE_SCOPE,
275725
+ errorType: error instanceof Error ? error.name : "unknown"
275726
+ });
275727
+ }
275728
+ }
275729
+ #recordFreshness(labels, lastSuccessMs, nowMs) {
275730
+ this.params.metrics?.recordGauge("cex_account_balance_poll_freshness_seconds", Math.max(0, (nowMs - lastSuccessMs) / 1000), labels);
275731
+ }
275732
+ #targetKey(target) {
275733
+ return `${target.exchangeId}|${target.account.label}|${ACCOUNT_BALANCE_SCOPE}`;
275734
+ }
275735
+ #schedule(delayMs) {
275736
+ this.#timer = setTimeout(() => void this.#tick(), delayMs);
275737
+ this.#timer.unref?.();
275738
+ }
275739
+ async#tick() {
275740
+ this.#timer = null;
275741
+ try {
275742
+ await this.pollAllOnce();
275743
+ } catch (error) {
275744
+ log.error("Account balance archive poller tick failed", error);
275745
+ } finally {
275746
+ if (!this.#stopped) {
275747
+ this.#schedule(this.#config.pollIntervalMs);
275748
+ }
275749
+ }
275750
+ }
275751
+ }
275752
+
275753
+ // src/helpers/fill-archive-poller.ts
275754
+ var DEFAULT_CONFIG2 = {
275460
275755
  pollIntervalMs: 60000,
275461
275756
  lookbackMs: 24 * 60 * 60 * 1000,
275462
275757
  tradesLimit: 500
@@ -275481,7 +275776,7 @@ class FillArchivePoller {
275481
275776
  #config;
275482
275777
  constructor(params) {
275483
275778
  this.params = params;
275484
- this.#config = { ...DEFAULT_CONFIG, ...params.config };
275779
+ this.#config = { ...DEFAULT_CONFIG2, ...params.config };
275485
275780
  }
275486
275781
  start() {
275487
275782
  if (this.#timer || this.#stopped) {
@@ -295087,6 +295382,7 @@ import WebSocket2 from "ws";
295087
295382
  var BINANCE_SPOT_WS_API_URL = "wss://ws-api.binance.com:443/ws-api/v3";
295088
295383
  var DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS = 16;
295089
295384
  var createWebSocket = (url3) => new WebSocket2(url3);
295385
+ var userDataRequestCounter = 0;
295090
295386
  function getExchangeString(exchange, key) {
295091
295387
  const value = exchange[key];
295092
295388
  if (typeof value !== "string" || value.length === 0) {
@@ -295193,7 +295489,7 @@ class BinanceSpotUserDataStream {
295193
295489
  exchange;
295194
295490
  ws;
295195
295491
  secretValues;
295196
- requestId = `user-data-${Date.now()}-${Math.random()}`;
295492
+ requestId = `user-data-${Date.now()}-${userDataRequestCounter++}`;
295197
295493
  maxBufferedEvents;
295198
295494
  queue = [];
295199
295495
  waiters = [];
@@ -295271,6 +295567,12 @@ class BinanceSpotUserDataStream {
295271
295567
  this.subscriptionId = message.result?.subscriptionId ?? null;
295272
295568
  return;
295273
295569
  }
295570
+ if ("status" in message && typeof message.status === "number" && message.status !== 200) {
295571
+ const errorMessage = message.error?.msg ?? message.error?.message ?? `Binance user-data request failed with status ${message.status}`;
295572
+ const errorCode = message.error?.code;
295573
+ this.fail(new Error(typeof errorCode === "number" ? `${errorMessage} (code ${errorCode})` : errorMessage));
295574
+ return;
295575
+ }
295274
295576
  if (!("event" in message) || !message.event) {
295275
295577
  return;
295276
295578
  }
@@ -296801,6 +297103,7 @@ class CEXBroker {
296801
297103
  orderActivityTracker = new OrderActivityTracker;
296802
297104
  withdrawalObservationTracker = new WithdrawalObservationTracker;
296803
297105
  fillArchivePoller;
297106
+ accountBalanceArchivePoller;
296804
297107
  loadEnvConfig() {
296805
297108
  log.info("\uD83D\uDD27 Loading CEX_BROKER_ environment variables:");
296806
297109
  const configMap = {};
@@ -296945,6 +297248,10 @@ class CEXBroker {
296945
297248
  this.fillArchivePoller.stop();
296946
297249
  this.fillArchivePoller = undefined;
296947
297250
  }
297251
+ if (this.accountBalanceArchivePoller) {
297252
+ await this.accountBalanceArchivePoller.stop();
297253
+ this.accountBalanceArchivePoller = undefined;
297254
+ }
296948
297255
  if (this.server) {
296949
297256
  await this.server.forceShutdown();
296950
297257
  }
@@ -296970,6 +297277,10 @@ class CEXBroker {
296970
297277
  this.fillArchivePoller.stop();
296971
297278
  this.fillArchivePoller = undefined;
296972
297279
  }
297280
+ if (this.accountBalanceArchivePoller) {
297281
+ await this.accountBalanceArchivePoller.stop();
297282
+ this.accountBalanceArchivePoller = undefined;
297283
+ }
296973
297284
  log.info(`Running CEXBroker at ${new Date().toISOString()}`);
296974
297285
  if (this.otelMetrics?.isOtelEnabled()) {
296975
297286
  await this.otelMetrics.initialize();
@@ -296998,6 +297309,14 @@ class CEXBroker {
296998
297309
  });
296999
297310
  this.fillArchivePoller.start();
297000
297311
  }
297312
+ if (this.brokerArchiver?.canPersistAccountBalanceSnapshots()) {
297313
+ this.accountBalanceArchivePoller = new AccountBalanceArchivePoller({
297314
+ brokers: this.brokers,
297315
+ archiver: this.brokerArchiver,
297316
+ metrics: this.otelMetrics
297317
+ });
297318
+ this.accountBalanceArchivePoller.start();
297319
+ }
297001
297320
  return this;
297002
297321
  }
297003
297322
  }
@@ -297005,4 +297324,4 @@ export {
297005
297324
  CEXBroker as default
297006
297325
  };
297007
297326
 
297008
- //# debugId=61BAB4069A0158B064756E2164756E21
297327
+ //# debugId=186957481DC5AB2064756E2164756E21