@usherlabs/cex-broker 0.2.24 → 0.2.26

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.
@@ -315208,6 +315208,9 @@ function compactUndefined(record) {
315208
315208
  return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
315209
315209
  }
315210
315210
 
315211
+ // src/helpers/broker-execution-archive/rows.ts
315212
+ import { createHash as createHash2 } from "node:crypto";
315213
+
315211
315214
  // src/helpers/broker-execution-archive/redact.ts
315212
315215
  import { createHash } from "node:crypto";
315213
315216
  var SECRET_KEY_PATTERN = /\b(api[_-]?key|api[_-]?secret|secret|signature|passphrase|password|token|credential)\b/i;
@@ -315280,6 +315283,8 @@ function hashMarketMetadata(payload) {
315280
315283
  var BROKER_WRITE_SOURCE = "broker_write";
315281
315284
  var ARCHIVE_SCHEMA_VERSION = "1";
315282
315285
  var FILL_EVENT_KIND = "trade_history_fill";
315286
+ var ACCOUNT_BALANCE_SCOPE = "spot";
315287
+ var ACCOUNT_BALANCE_PRECISION_BASIS = "ccxt_normalized_number";
315283
315288
 
315284
315289
  // src/helpers/broker-execution-archive/rows.ts
315285
315290
  function firstString2(...values2) {
@@ -315313,6 +315318,151 @@ function normalizeTimestamp2(value) {
315313
315318
  const date = new Date(ms);
315314
315319
  return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
315315
315320
  }
315321
+ function decimalString(value) {
315322
+ if (typeof value !== "number" || !Number.isFinite(value)) {
315323
+ return;
315324
+ }
315325
+ if (Object.is(value, -0)) {
315326
+ return "0";
315327
+ }
315328
+ const rendered = String(value).toLowerCase();
315329
+ if (!rendered.includes("e")) {
315330
+ return rendered;
315331
+ }
315332
+ const [coefficient = "", rawExponent = "0"] = rendered.split("e");
315333
+ const exponent = Number.parseInt(rawExponent, 10);
315334
+ const negative = coefficient.startsWith("-");
315335
+ const unsigned = negative ? coefficient.slice(1) : coefficient;
315336
+ const [integer = "0", fraction = ""] = unsigned.split(".");
315337
+ const digits = `${integer}${fraction}`;
315338
+ const decimalIndex = integer.length + exponent;
315339
+ let expanded;
315340
+ if (decimalIndex <= 0) {
315341
+ expanded = `0.${"0".repeat(-decimalIndex)}${digits}`;
315342
+ } else if (decimalIndex >= digits.length) {
315343
+ expanded = `${digits}${"0".repeat(decimalIndex - digits.length)}`;
315344
+ } else {
315345
+ expanded = `${digits.slice(0, decimalIndex)}.${digits.slice(decimalIndex)}`;
315346
+ }
315347
+ return negative ? `-${expanded}` : expanded;
315348
+ }
315349
+ function normalizedBalanceMap(value) {
315350
+ const record = asRecord(value);
315351
+ if (!record) {
315352
+ return {};
315353
+ }
315354
+ const entries = [];
315355
+ for (const [rawAsset, quantity] of Object.entries(record)) {
315356
+ const asset = rawAsset.trim();
315357
+ const normalized = decimalString(quantity);
315358
+ if (asset && normalized !== undefined) {
315359
+ entries.push([asset, normalized]);
315360
+ }
315361
+ }
315362
+ return Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right)));
315363
+ }
315364
+ function sortedBalanceMap(map) {
315365
+ return Object.fromEntries(Object.entries(map).sort(([left], [right]) => left.localeCompare(right)));
315366
+ }
315367
+ var BALANCE_METADATA_KEYS = new Set([
315368
+ "info",
315369
+ "timestamp",
315370
+ "datetime",
315371
+ "free",
315372
+ "used",
315373
+ "total",
315374
+ "debt"
315375
+ ]);
315376
+ function normalizeCcxtBalanceForArchive(balance) {
315377
+ const record = asRecord(balance) ?? {};
315378
+ const aggregateRecords = {
315379
+ free: asRecord(record.free),
315380
+ used: asRecord(record.used),
315381
+ total: asRecord(record.total)
315382
+ };
315383
+ const maps = {
315384
+ free: normalizedBalanceMap(record.free),
315385
+ used: normalizedBalanceMap(record.used),
315386
+ total: normalizedBalanceMap(record.total)
315387
+ };
315388
+ const assetEntryAssets = new Set;
315389
+ for (const [rawAsset, value] of Object.entries(record)) {
315390
+ if (BALANCE_METADATA_KEYS.has(rawAsset)) {
315391
+ continue;
315392
+ }
315393
+ const asset = rawAsset.trim();
315394
+ const assetEntry = asRecord(value);
315395
+ if (!asset || !assetEntry || !(("free" in assetEntry) || ("used" in assetEntry) || ("total" in assetEntry))) {
315396
+ continue;
315397
+ }
315398
+ assetEntryAssets.add(asset);
315399
+ for (const field of ["free", "used", "total"]) {
315400
+ const normalized = decimalString(assetEntry[field]);
315401
+ if (normalized !== undefined && maps[field][asset] === undefined) {
315402
+ maps[field][asset] = normalized;
315403
+ }
315404
+ }
315405
+ }
315406
+ const reportedAssets = new Set(assetEntryAssets);
315407
+ for (const aggregateRecord of Object.values(aggregateRecords)) {
315408
+ for (const rawAsset of Object.keys(aggregateRecord ?? {})) {
315409
+ const asset = rawAsset.trim();
315410
+ if (asset) {
315411
+ reportedAssets.add(asset);
315412
+ }
315413
+ }
315414
+ }
315415
+ for (const map of Object.values(maps)) {
315416
+ for (const asset of Object.keys(map)) {
315417
+ reportedAssets.add(asset);
315418
+ }
315419
+ }
315420
+ return {
315421
+ exchangeTimestamp: normalizeTimestamp2(record.timestamp ?? record.datetime),
315422
+ reportedAssets: [...reportedAssets].sort(),
315423
+ assetEntryAssets: [...assetEntryAssets].sort(),
315424
+ freeBalances: sortedBalanceMap(maps.free),
315425
+ usedBalances: sortedBalanceMap(maps.used),
315426
+ totalBalances: sortedBalanceMap(maps.total),
315427
+ freeMapPresent: aggregateRecords.free !== undefined,
315428
+ usedMapPresent: aggregateRecords.used !== undefined,
315429
+ totalMapPresent: aggregateRecords.total !== undefined
315430
+ };
315431
+ }
315432
+ function buildAccountBalanceSnapshotRow(input) {
315433
+ const { tags, balance } = input;
315434
+ const observationId = createHash2("sha256").update(JSON.stringify({
315435
+ deployment_id: tags.deployment_id,
315436
+ exchange: tags.exchange,
315437
+ account_selector: tags.account_selector,
315438
+ balance_scope: ACCOUNT_BALANCE_SCOPE,
315439
+ broker_observed_timestamp: tags.broker_observed_timestamp,
315440
+ balance
315441
+ })).digest("hex");
315442
+ return {
315443
+ table: "broker_account.balance_snapshots",
315444
+ row: compactUndefined2({
315445
+ broker_observed_timestamp: tags.broker_observed_timestamp,
315446
+ exchange_timestamp: balance.exchangeTimestamp,
315447
+ source: tags.source,
315448
+ deployment_id: tags.deployment_id,
315449
+ schema_version: ARCHIVE_SCHEMA_VERSION,
315450
+ exchange: tags.exchange,
315451
+ account_selector: tags.account_selector,
315452
+ balance_scope: ACCOUNT_BALANCE_SCOPE,
315453
+ observation_id: observationId,
315454
+ reported_assets: balance.reportedAssets,
315455
+ asset_entry_assets: balance.assetEntryAssets,
315456
+ free_balances: balance.freeBalances,
315457
+ used_balances: balance.usedBalances,
315458
+ total_balances: balance.totalBalances,
315459
+ aggregate_free_map_present: Number(balance.freeMapPresent),
315460
+ aggregate_used_map_present: Number(balance.usedMapPresent),
315461
+ aggregate_total_map_present: Number(balance.totalMapPresent),
315462
+ precision_basis: ACCOUNT_BALANCE_PRECISION_BASIS
315463
+ })
315464
+ };
315465
+ }
315316
315466
  function compactUndefined2(record) {
315317
315467
  return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
315318
315468
  }
@@ -315766,6 +315916,15 @@ function isMarketArchiveTable(table) {
315766
315916
  }
315767
315917
 
315768
315918
  // src/helpers/broker-execution-archive/writer.ts
315919
+ var BROKER_EXECUTION_ARCHIVE_TABLES = new Set([
315920
+ "broker_execution.order_events",
315921
+ "broker_execution.market_metadata_snapshots",
315922
+ "broker_execution.transfer_events",
315923
+ "broker_execution.fill_events"
315924
+ ]);
315925
+ function isBrokerExecutionArchiveTable(table) {
315926
+ return BROKER_EXECUTION_ARCHIVE_TABLES.has(table);
315927
+ }
315769
315928
  var DEFAULT_MAX_QUEUE_SIZE = 1e4;
315770
315929
  var DEFAULT_BATCH_SIZE = 10;
315771
315930
  var DEFAULT_FLUSH_INTERVAL_MS = 1000;
@@ -315850,6 +316009,9 @@ class BrokerExecutionArchiver {
315850
316009
  canPersistMarketMetadataSnapshot() {
315851
316010
  return this.isEnabled() && (Boolean(this.otelLogs?.isOtelEnabled()) || Boolean(this.forwarderUrl));
315852
316011
  }
316012
+ canPersistAccountBalanceSnapshots() {
316013
+ return this.isEnabled() && Boolean(this.forwarderUrl);
316014
+ }
315853
316015
  enqueue(row) {
315854
316016
  if (!this.enabled || this.closed || !this.otelLogs && !this.forwarderUrl) {
315855
316017
  return;
@@ -315861,6 +316023,9 @@ class BrokerExecutionArchiver {
315861
316023
  }
315862
316024
  return;
315863
316025
  }
316026
+ if (row.table === "broker_account.balance_snapshots" && !this.forwarderUrl) {
316027
+ return;
316028
+ }
315864
316029
  if (this.queue.length >= this.maxQueueSize) {
315865
316030
  this.queue.shift();
315866
316031
  this.stats.shed += 1;
@@ -315939,7 +316104,7 @@ class BrokerExecutionArchiver {
315939
316104
  return true;
315940
316105
  }
315941
316106
  for (const entry of batch) {
315942
- if (!isMarketArchiveTable(entry.table)) {
316107
+ if (isBrokerExecutionArchiveTable(entry.table)) {
315943
316108
  this.emitOtelLog(entry);
315944
316109
  }
315945
316110
  }
@@ -316075,8 +316240,138 @@ function parsePositiveInt(value, fallback) {
316075
316240
  const parsed = Number.parseInt(value, 10);
316076
316241
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
316077
316242
  }
316078
- // src/helpers/fill-archive-poller.ts
316243
+ // src/helpers/account-balance-archive-poller.ts
316079
316244
  var DEFAULT_CONFIG = {
316245
+ pollIntervalMs: 60000
316246
+ };
316247
+ var metricLabels = (target) => ({
316248
+ exchange: target.exchangeId,
316249
+ account_selector: target.account.label,
316250
+ balance_scope: ACCOUNT_BALANCE_SCOPE
316251
+ });
316252
+
316253
+ class AccountBalanceArchivePoller {
316254
+ params;
316255
+ #timer = null;
316256
+ #stopped = false;
316257
+ #running = null;
316258
+ #lastSuccessMs = new Map;
316259
+ #config;
316260
+ constructor(params) {
316261
+ this.params = params;
316262
+ this.#config = { ...DEFAULT_CONFIG, ...params.config };
316263
+ }
316264
+ start() {
316265
+ if (this.#timer || this.#stopped || !this.params.archiver.canPersistAccountBalanceSnapshots()) {
316266
+ return;
316267
+ }
316268
+ log.info("\uD83D\uDCB0 Account balance archive poller started", {
316269
+ balanceScope: ACCOUNT_BALANCE_SCOPE
316270
+ });
316271
+ this.#schedule(0);
316272
+ }
316273
+ async stop() {
316274
+ this.#stopped = true;
316275
+ if (this.#timer) {
316276
+ clearTimeout(this.#timer);
316277
+ this.#timer = null;
316278
+ }
316279
+ await this.#running;
316280
+ }
316281
+ async pollAllOnce() {
316282
+ if (this.#stopped || this.#running || !this.params.archiver.canPersistAccountBalanceSnapshots()) {
316283
+ return false;
316284
+ }
316285
+ this.#running = this.#pollAllSequentially();
316286
+ try {
316287
+ return await this.#running;
316288
+ } finally {
316289
+ this.#running = null;
316290
+ }
316291
+ }
316292
+ #targets() {
316293
+ const targets = [];
316294
+ for (const [exchangeId, pool] of Object.entries(this.params.brokers)) {
316295
+ for (const account of [pool.primary, ...pool.secondaryBrokers]) {
316296
+ targets.push({ exchangeId, account });
316297
+ }
316298
+ }
316299
+ return targets;
316300
+ }
316301
+ async#pollAllSequentially() {
316302
+ for (const target of this.#targets()) {
316303
+ if (this.#stopped) {
316304
+ break;
316305
+ }
316306
+ await this.#pollOne(target);
316307
+ }
316308
+ return true;
316309
+ }
316310
+ async#pollOne(target) {
316311
+ const labels = metricLabels(target);
316312
+ this.params.metrics?.recordCounter("cex_account_balance_poll_attempts_total", 1, labels);
316313
+ try {
316314
+ const exchange = target.account.exchange;
316315
+ const balance = await exchange.fetchBalance({
316316
+ type: ACCOUNT_BALANCE_SCOPE
316317
+ });
316318
+ const observedAt = new Date;
316319
+ const normalized = normalizeCcxtBalanceForArchive(balance);
316320
+ this.params.archiver.enqueue(buildAccountBalanceSnapshotRow({
316321
+ tags: buildCommonArchiveTags({
316322
+ deploymentId: this.params.archiver.getDeploymentId(),
316323
+ accountSelector: target.account.label,
316324
+ exchange: target.exchangeId,
316325
+ brokerObservedTimestamp: observedAt.toISOString()
316326
+ }),
316327
+ balance: normalized
316328
+ }));
316329
+ const successMs = Date.now();
316330
+ this.#lastSuccessMs.set(this.#targetKey(target), successMs);
316331
+ this.params.metrics?.recordCounter("cex_account_balance_poll_successes_total", 1, labels);
316332
+ this.params.metrics?.recordGauge("cex_account_balance_poll_last_success_timestamp_seconds", Math.floor(successMs / 1000), labels);
316333
+ this.#recordFreshness(labels, successMs, successMs);
316334
+ } catch (error) {
316335
+ this.params.metrics?.recordCounter("cex_account_balance_poll_failures_total", 1, labels);
316336
+ const now3 = Date.now();
316337
+ const lastSuccess = this.#lastSuccessMs.get(this.#targetKey(target));
316338
+ if (lastSuccess !== undefined) {
316339
+ this.#recordFreshness(labels, lastSuccess, now3);
316340
+ }
316341
+ log.warn("Account balance archive poll failed", {
316342
+ exchange: target.exchangeId,
316343
+ account: target.account.label,
316344
+ balanceScope: ACCOUNT_BALANCE_SCOPE,
316345
+ errorType: error instanceof Error ? error.name : "unknown"
316346
+ });
316347
+ }
316348
+ }
316349
+ #recordFreshness(labels, lastSuccessMs, nowMs) {
316350
+ this.params.metrics?.recordGauge("cex_account_balance_poll_freshness_seconds", Math.max(0, (nowMs - lastSuccessMs) / 1000), labels);
316351
+ }
316352
+ #targetKey(target) {
316353
+ return `${target.exchangeId}|${target.account.label}|${ACCOUNT_BALANCE_SCOPE}`;
316354
+ }
316355
+ #schedule(delayMs) {
316356
+ this.#timer = setTimeout(() => void this.#tick(), delayMs);
316357
+ this.#timer.unref?.();
316358
+ }
316359
+ async#tick() {
316360
+ this.#timer = null;
316361
+ try {
316362
+ await this.pollAllOnce();
316363
+ } catch (error) {
316364
+ log.error("Account balance archive poller tick failed", error);
316365
+ } finally {
316366
+ if (!this.#stopped) {
316367
+ this.#schedule(this.#config.pollIntervalMs);
316368
+ }
316369
+ }
316370
+ }
316371
+ }
316372
+
316373
+ // src/helpers/fill-archive-poller.ts
316374
+ var DEFAULT_CONFIG2 = {
316080
316375
  pollIntervalMs: 60000,
316081
316376
  lookbackMs: 24 * 60 * 60 * 1000,
316082
316377
  tradesLimit: 500
@@ -316101,7 +316396,7 @@ class FillArchivePoller {
316101
316396
  #config;
316102
316397
  constructor(params) {
316103
316398
  this.params = params;
316104
- this.#config = { ...DEFAULT_CONFIG, ...params.config };
316399
+ this.#config = { ...DEFAULT_CONFIG2, ...params.config };
316105
316400
  }
316106
316401
  start() {
316107
316402
  if (this.#timer || this.#stopped) {
@@ -332125,6 +332420,59 @@ function createExecuteActionHandler(deps) {
332125
332420
  }
332126
332421
  };
332127
332422
  }
332423
+ // src/handlers/subscribe/broker-lifecycle.ts
332424
+ class SubscribeBrokerLifecycle {
332425
+ #brokers = new Map;
332426
+ #closing = new Map;
332427
+ #shuttingDown = false;
332428
+ register(broker, context2) {
332429
+ this.#brokers.set(broker, context2);
332430
+ if (this.#shuttingDown) {
332431
+ this.close(broker);
332432
+ }
332433
+ }
332434
+ close(broker) {
332435
+ const existing = this.#closing.get(broker);
332436
+ if (existing) {
332437
+ return existing;
332438
+ }
332439
+ const context2 = this.#brokers.get(broker) ?? {
332440
+ cex: "unknown",
332441
+ symbol: "unknown"
332442
+ };
332443
+ this.#brokers.delete(broker);
332444
+ const closing = (async () => {
332445
+ try {
332446
+ await broker.close();
332447
+ log.debug("Request-scoped Subscribe broker closed", context2);
332448
+ return "closed";
332449
+ } catch (error48) {
332450
+ log.warn("Failed to close request-scoped Subscribe broker", {
332451
+ ...context2,
332452
+ error: error48
332453
+ });
332454
+ return "failed";
332455
+ } finally {
332456
+ this.#closing.delete(broker);
332457
+ }
332458
+ })();
332459
+ this.#closing.set(broker, closing);
332460
+ return closing;
332461
+ }
332462
+ async closeAll() {
332463
+ this.#shuttingDown = true;
332464
+ let failed = 0;
332465
+ while (this.#brokers.size > 0 || this.#closing.size > 0) {
332466
+ const inFlight = [...this.#closing.values()];
332467
+ const fresh = [...this.#brokers.keys()].map((broker) => this.close(broker));
332468
+ const outcomes = await Promise.all([...fresh, ...inFlight]);
332469
+ failed += outcomes.filter((outcome) => outcome === "failed").length;
332470
+ }
332471
+ if (failed > 0) {
332472
+ throw new Error(`${failed} request-scoped Subscribe broker(s) failed to close`);
332473
+ }
332474
+ }
332475
+ }
332128
332476
  // src/handlers/subscribe/handler.ts
332129
332477
  var grpc13 = __toESM(require_src3(), 1);
332130
332478
 
@@ -332134,6 +332482,7 @@ import { createHmac } from "node:crypto";
332134
332482
  var BINANCE_SPOT_WS_API_URL = "wss://ws-api.binance.com:443/ws-api/v3";
332135
332483
  var DEFAULT_BINANCE_USER_DATA_MAX_BUFFERED_EVENTS = 16;
332136
332484
  var createWebSocket = (url3) => new wrapper_default(url3);
332485
+ var userDataRequestCounter = 0;
332137
332486
  function getExchangeString(exchange, key) {
332138
332487
  const value = exchange[key];
332139
332488
  if (typeof value !== "string" || value.length === 0) {
@@ -332240,7 +332589,7 @@ class BinanceSpotUserDataStream {
332240
332589
  exchange;
332241
332590
  ws;
332242
332591
  secretValues;
332243
- requestId = `user-data-${Date.now()}-${Math.random()}`;
332592
+ requestId = `user-data-${Date.now()}-${userDataRequestCounter++}`;
332244
332593
  maxBufferedEvents;
332245
332594
  queue = [];
332246
332595
  waiters = [];
@@ -332318,6 +332667,12 @@ class BinanceSpotUserDataStream {
332318
332667
  this.subscriptionId = message.result?.subscriptionId ?? null;
332319
332668
  return;
332320
332669
  }
332670
+ if ("status" in message && typeof message.status === "number" && message.status !== 200) {
332671
+ const errorMessage = message.error?.msg ?? message.error?.message ?? `Binance user-data request failed with status ${message.status}`;
332672
+ const errorCode = message.error?.code;
332673
+ this.fail(new Error(typeof errorCode === "number" ? `${errorMessage} (code ${errorCode})` : errorMessage));
332674
+ return;
332675
+ }
332321
332676
  if (!("event" in message) || !message.event) {
332322
332677
  return;
332323
332678
  }
@@ -333194,15 +333549,34 @@ async function runCcxtSubscribeLoop(call, isClosed, symbol2, subscriptionType, w
333194
333549
  }
333195
333550
  function createSubscribeHandler(deps) {
333196
333551
  const { brokers, whitelistIps, otelMetrics, brokerArchiver } = deps;
333552
+ const brokerLifecycle = deps.brokerLifecycle ?? new SubscribeBrokerLifecycle;
333197
333553
  return async (call) => {
333198
333554
  const subscribeStartTime = Date.now();
333199
333555
  let streamClosed = false;
333556
+ let ownedBroker = null;
333557
+ let ownedBrokerClosePromise;
333200
333558
  const markStreamClosed = () => {
333201
333559
  streamClosed = true;
333202
333560
  };
333203
- const isStreamClosed = () => streamClosed || call.destroyed;
333204
- call.once("close", markStreamClosed);
333561
+ const isStreamClosed = () => streamClosed || call.cancelled || call.writableEnded;
333562
+ const closeOwnedBroker = () => {
333563
+ if (ownedBrokerClosePromise) {
333564
+ return ownedBrokerClosePromise;
333565
+ }
333566
+ if (!ownedBroker) {
333567
+ return Promise.resolve();
333568
+ }
333569
+ const broker = ownedBroker;
333570
+ ownedBroker = null;
333571
+ ownedBrokerClosePromise = brokerLifecycle.close(broker);
333572
+ return ownedBrokerClosePromise;
333573
+ };
333574
+ const closeOwnedBrokerOnCallEnd = () => {
333575
+ closeOwnedBroker();
333576
+ };
333205
333577
  call.once("cancelled", markStreamClosed);
333578
+ call.once("cancelled", closeOwnedBrokerOnCallEnd);
333579
+ call.once("error", closeOwnedBrokerOnCallEnd);
333206
333580
  call.once("end", () => {
333207
333581
  markStreamClosed();
333208
333582
  log.info("Subscribe stream ended");
@@ -333274,6 +333648,17 @@ function createSubscribeHandler(deps) {
333274
333648
  });
333275
333649
  return;
333276
333650
  }
333651
+ if (!selectedBrokerAccount) {
333652
+ ownedBroker = broker;
333653
+ brokerLifecycle.register(broker, {
333654
+ cex: normalizedCex,
333655
+ symbol: symbol2
333656
+ });
333657
+ if (isStreamClosed()) {
333658
+ await closeOwnedBroker();
333659
+ return;
333660
+ }
333661
+ }
333277
333662
  const resolvedSymbol = await resolveSubscriptionSymbol(broker, symbol2, options?.marketType);
333278
333663
  const assetType = parseMarketType(options?.marketType);
333279
333664
  const deploymentId = brokerArchiver?.getDeploymentId() ?? "unknown";
@@ -333538,6 +333923,10 @@ function createSubscribeHandler(deps) {
333538
333923
  symbol: "",
333539
333924
  type: subscriptionType
333540
333925
  });
333926
+ } finally {
333927
+ call.off("cancelled", closeOwnedBrokerOnCallEnd);
333928
+ call.off("error", closeOwnedBrokerOnCallEnd);
333929
+ await closeOwnedBroker();
333541
333930
  }
333542
333931
  };
333543
333932
  }
@@ -333688,7 +334077,7 @@ var CEX_BROKER_PACKAGE_DEFINITION = protoLoader.fromJSON(node_descriptor_default
333688
334077
  // src/server.ts
333689
334078
  var grpcObj = grpc14.loadPackageDefinition(CEX_BROKER_PACKAGE_DEFINITION);
333690
334079
  var cexNode = grpcObj.cex_broker;
333691
- function getServer(policy, brokers, whitelistIps, useVerity, verityProverUrl, otelMetrics, brokerArchiver, orderActivityTracker, withdrawalObservationTracker) {
334080
+ function getServer(policy, brokers, whitelistIps, useVerity, verityProverUrl, otelMetrics, brokerArchiver, orderActivityTracker, withdrawalObservationTracker, subscribeBrokerLifecycle) {
333692
334081
  const server = new grpc14.Server;
333693
334082
  server.addService(cexNode.cex_service.service, {
333694
334083
  ExecuteAction: createExecuteActionHandler({
@@ -333706,7 +334095,8 @@ function getServer(policy, brokers, whitelistIps, useVerity, verityProverUrl, ot
333706
334095
  brokers,
333707
334096
  whitelistIps,
333708
334097
  otelMetrics,
333709
- brokerArchiver
334098
+ brokerArchiver,
334099
+ brokerLifecycle: subscribeBrokerLifecycle
333710
334100
  })
333711
334101
  });
333712
334102
  return server;
@@ -333848,6 +334238,7 @@ class CEXBroker {
333848
334238
  orderActivityTracker = new OrderActivityTracker;
333849
334239
  withdrawalObservationTracker = new WithdrawalObservationTracker;
333850
334240
  fillArchivePoller;
334241
+ accountBalanceArchivePoller;
333851
334242
  loadEnvConfig() {
333852
334243
  log.info("\uD83D\uDD27 Loading CEX_BROKER_ environment variables:");
333853
334244
  const configMap = {};
@@ -333992,6 +334383,10 @@ class CEXBroker {
333992
334383
  this.fillArchivePoller.stop();
333993
334384
  this.fillArchivePoller = undefined;
333994
334385
  }
334386
+ if (this.accountBalanceArchivePoller) {
334387
+ await this.accountBalanceArchivePoller.stop();
334388
+ this.accountBalanceArchivePoller = undefined;
334389
+ }
333995
334390
  if (this.server) {
333996
334391
  await this.server.forceShutdown();
333997
334392
  }
@@ -334017,6 +334412,10 @@ class CEXBroker {
334017
334412
  this.fillArchivePoller.stop();
334018
334413
  this.fillArchivePoller = undefined;
334019
334414
  }
334415
+ if (this.accountBalanceArchivePoller) {
334416
+ await this.accountBalanceArchivePoller.stop();
334417
+ this.accountBalanceArchivePoller = undefined;
334418
+ }
334020
334419
  log.info(`Running CEXBroker at ${new Date().toISOString()}`);
334021
334420
  if (this.otelMetrics?.isOtelEnabled()) {
334022
334421
  await this.otelMetrics.initialize();
@@ -334045,6 +334444,14 @@ class CEXBroker {
334045
334444
  });
334046
334445
  this.fillArchivePoller.start();
334047
334446
  }
334447
+ if (this.brokerArchiver?.canPersistAccountBalanceSnapshots()) {
334448
+ this.accountBalanceArchivePoller = new AccountBalanceArchivePoller({
334449
+ brokers: this.brokers,
334450
+ archiver: this.brokerArchiver,
334451
+ metrics: this.otelMetrics
334452
+ });
334453
+ this.accountBalanceArchivePoller.start();
334454
+ }
334048
334455
  return this;
334049
334456
  }
334050
334457
  }
@@ -0,0 +1,13 @@
1
+ import type { Exchange } from "@usherlabs/ccxt";
2
+ type BrokerContext = {
3
+ cex: string;
4
+ symbol: string;
5
+ };
6
+ export type BrokerCloseOutcome = "closed" | "failed";
7
+ export declare class SubscribeBrokerLifecycle {
8
+ #private;
9
+ register(broker: Exchange, context: BrokerContext): void;
10
+ close(broker: Exchange): Promise<BrokerCloseOutcome>;
11
+ closeAll(): Promise<void>;
12
+ }
13
+ export {};
@@ -3,11 +3,13 @@ import { type BrokerPoolEntry } from "../../helpers/broker";
3
3
  import type { BrokerExecutionArchiver } from "../../helpers/broker-execution-archive";
4
4
  import type { OtelMetrics } from "../../helpers/otel";
5
5
  import type { SubscribeRequest, SubscribeResponse } from "../types";
6
+ import { SubscribeBrokerLifecycle } from "./broker-lifecycle";
6
7
  export type SubscribeDeps = {
7
8
  brokers: Record<string, BrokerPoolEntry>;
8
9
  whitelistIps: string[];
9
10
  otelMetrics?: OtelMetrics;
10
11
  brokerArchiver?: BrokerExecutionArchiver;
12
+ brokerLifecycle?: SubscribeBrokerLifecycle;
11
13
  };
12
14
  type SubscribeCall = grpc.ServerWritableStream<SubscribeRequest, SubscribeResponse>;
13
15
  export declare function createSubscribeHandler(deps: SubscribeDeps): (call: SubscribeCall) => Promise<void>;
@@ -1 +1,2 @@
1
+ export { SubscribeBrokerLifecycle } from "./broker-lifecycle";
1
2
  export { createSubscribeHandler, type SubscribeDeps } from "./handler";
@@ -0,0 +1,19 @@
1
+ import type { BrokerPoolEntry } from "./broker";
2
+ import { type BrokerExecutionArchiver } from "./broker-execution-archive";
3
+ import type { OtelMetrics } from "./otel";
4
+ export type AccountBalanceArchivePollerConfig = {
5
+ pollIntervalMs: number;
6
+ };
7
+ export declare class AccountBalanceArchivePoller {
8
+ #private;
9
+ private readonly params;
10
+ constructor(params: {
11
+ brokers: Record<string, BrokerPoolEntry>;
12
+ archiver: BrokerExecutionArchiver;
13
+ metrics?: OtelMetrics;
14
+ config?: Partial<AccountBalanceArchivePollerConfig>;
15
+ });
16
+ start(): void;
17
+ stop(): Promise<void>;
18
+ pollAllOnce(): Promise<boolean>;
19
+ }
@@ -1,6 +1,6 @@
1
1
  export { archiveOrderExecutionInBackground, archiveSubscribeStreamInBackground, archiveTransferEventInBackground, archiveWithdrawalObservationsInBackground, captureMarketMetadataSnapshot, captureMarketMetadataSnapshotInBackground, } from "./capture";
2
2
  export { hashMarketMetadata, redactErrorForArchive, redactSecretLiterals, redactStreamPayload, } from "./redact";
3
- export { buildCommonArchiveTags, buildFillEventArchiveRow, buildMarketMetadataSnapshotRow, buildOrderEventArchiveRow, buildSubscribeStreamArchiveRow, buildTransferEventArchiveRow, type FillArchiveFields, type NormalizedCcxtTransfer, normalizeCcxtTradeForArchive, normalizeCcxtTransactionForArchive, type TransferArchiveFields, } from "./rows";
4
- export { ARCHIVE_SCHEMA_VERSION, BROKER_WRITE_SOURCE, type BrokerArchiveCommonTags, type BrokerArchiveRow, type BrokerArchiveTable, type OrderArchiveAction, type SubscribeArchiveType, type TransferEventKind, type TransferLifecycleAction, } from "./types";
3
+ export { buildAccountBalanceSnapshotRow, buildCommonArchiveTags, buildFillEventArchiveRow, buildMarketMetadataSnapshotRow, buildOrderEventArchiveRow, buildSubscribeStreamArchiveRow, buildTransferEventArchiveRow, type FillArchiveFields, type NormalizedCcxtBalance, type NormalizedCcxtTransfer, normalizeCcxtBalanceForArchive, normalizeCcxtTradeForArchive, normalizeCcxtTransactionForArchive, type TransferArchiveFields, } from "./rows";
4
+ export { ACCOUNT_BALANCE_PRECISION_BASIS, ACCOUNT_BALANCE_SCOPE, ARCHIVE_SCHEMA_VERSION, BROKER_WRITE_SOURCE, type BrokerArchiveCommonTags, type BrokerArchiveRow, type BrokerArchiveTable, type OrderArchiveAction, type SubscribeArchiveType, type TransferEventKind, type TransferLifecycleAction, } from "./types";
5
5
  export { DEFAULT_WITHDRAWAL_OBSERVATION_TRACKER_MAX_ENTRIES, WithdrawalObservationTracker, } from "./withdrawal-observation-tracker";
6
- export { BrokerExecutionArchiver, type BrokerExecutionArchiverOptions, createBrokerExecutionArchiverFromEnv, isArchiveOtelLogsEnabled, resolveArchiveForwarderUrlFromEnv, } from "./writer";
6
+ export { BrokerExecutionArchiver, type BrokerExecutionArchiverOptions, createBrokerExecutionArchiverFromEnv, isArchiveOtelLogsEnabled, isBrokerExecutionArchiveTable, resolveArchiveForwarderUrlFromEnv, } from "./writer";
@@ -1,5 +1,22 @@
1
1
  import type { OrderExecutionTelemetry } from "../order-telemetry";
2
2
  import { type BrokerArchiveCommonTags, type BrokerArchiveRow, type OrderArchiveAction, type SubscribeArchiveType, type TransferEventKind, type TransferLifecycleAction } from "./types";
3
+ type BalanceQuantityMap = Record<string, string>;
4
+ export type NormalizedCcxtBalance = {
5
+ exchangeTimestamp?: string;
6
+ reportedAssets: string[];
7
+ assetEntryAssets: string[];
8
+ freeBalances: BalanceQuantityMap;
9
+ usedBalances: BalanceQuantityMap;
10
+ totalBalances: BalanceQuantityMap;
11
+ freeMapPresent: boolean;
12
+ usedMapPresent: boolean;
13
+ totalMapPresent: boolean;
14
+ };
15
+ export declare function normalizeCcxtBalanceForArchive(balance: unknown): NormalizedCcxtBalance;
16
+ export declare function buildAccountBalanceSnapshotRow(input: {
17
+ tags: BrokerArchiveCommonTags;
18
+ balance: NormalizedCcxtBalance;
19
+ }): BrokerArchiveRow;
3
20
  export declare function buildCommonArchiveTags(input: {
4
21
  deploymentId: string;
5
22
  accountSelector?: string;
@@ -83,3 +100,4 @@ export declare function buildMarketMetadataSnapshotRow(input: {
83
100
  idempotencyId?: string;
84
101
  marketSnapshot: unknown;
85
102
  }): BrokerArchiveRow;
103
+ export {};
@@ -1,6 +1,6 @@
1
1
  export declare const BROKER_WRITE_SOURCE: "broker_write";
2
2
  export declare const ARCHIVE_SCHEMA_VERSION: "1";
3
- export type BrokerArchiveTable = "broker_execution.order_events" | "broker_execution.market_metadata_snapshots" | "broker_execution.transfer_events" | "broker_execution.fill_events" | "market_data.orderbook_snapshots" | "market_data.candles" | "market_data.cex_stream_events" | "market_data.cex_ticker_events" | "market_data.cex_trades";
3
+ export type BrokerArchiveTable = "broker_execution.order_events" | "broker_execution.market_metadata_snapshots" | "broker_execution.transfer_events" | "broker_execution.fill_events" | "broker_account.balance_snapshots" | "market_data.orderbook_snapshots" | "market_data.candles" | "market_data.cex_stream_events" | "market_data.cex_ticker_events" | "market_data.cex_trades";
4
4
  export type BrokerArchiveRow = {
5
5
  table: BrokerArchiveTable;
6
6
  row: Record<string, unknown>;
@@ -18,3 +18,5 @@ export type SubscribeArchiveType = "ORDERS" | "BALANCE";
18
18
  export type TransferEventKind = "withdrawal" | "deposit" | "internal_transfer";
19
19
  export type TransferLifecycleAction = "submit_withdrawal" | "observe_withdrawal" | "observe_deposit" | "submit_internal_transfer";
20
20
  export declare const FILL_EVENT_KIND: "trade_history_fill";
21
+ export declare const ACCOUNT_BALANCE_SCOPE: "spot";
22
+ export declare const ACCOUNT_BALANCE_PRECISION_BASIS: "ccxt_normalized_number";