@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.
- package/dist/commands/cli.js +415 -8
- package/dist/handlers/subscribe/broker-lifecycle.d.ts +13 -0
- package/dist/handlers/subscribe/handler.d.ts +2 -0
- package/dist/handlers/subscribe/index.d.ts +1 -0
- package/dist/helpers/account-balance-archive-poller.d.ts +19 -0
- package/dist/helpers/broker-execution-archive/index.d.ts +3 -3
- package/dist/helpers/broker-execution-archive/rows.d.ts +18 -0
- package/dist/helpers/broker-execution-archive/types.d.ts +3 -1
- package/dist/helpers/broker-execution-archive/writer.d.ts +3 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +416 -9
- package/dist/index.js.map +12 -10
- package/dist/server.d.ts +2 -1
- package/package.json +2 -1
package/dist/commands/cli.js
CHANGED
|
@@ -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 (
|
|
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/
|
|
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 = { ...
|
|
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()}-${
|
|
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.
|
|
333204
|
-
|
|
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>;
|
|
@@ -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";
|