@tradejs/cli 2.0.6 → 2.0.8

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.
@@ -1681,6 +1681,7 @@ var import_time3 = require("@tradejs/core/time");
1681
1681
  var import_backtestArtifacts = require("@tradejs/infra/backtestArtifacts");
1682
1682
  var import_redis4 = require("@tradejs/infra/redis");
1683
1683
  var import_tradingAccounts = require("@tradejs/infra/tradingAccounts");
1684
+ var import_types3 = require("@tradejs/types");
1684
1685
 
1685
1686
  // src/lib/runFormatting.ts
1686
1687
  var import_date_fns = require("date-fns");
@@ -1710,6 +1711,7 @@ var import_progress = __toESM(require("progress"));
1710
1711
  var import_chalk = __toESM(require("chalk"));
1711
1712
  var import_connectors = require("@tradejs/connectors");
1712
1713
  var import_connectors2 = require("@tradejs/node/connectors");
1714
+ var import_strategies = require("@tradejs/node/strategies");
1713
1715
  var import_timescale = require("@tradejs/infra/timescale");
1714
1716
 
1715
1717
  // src/lib/binanceMarketData.ts
@@ -1719,14 +1721,6 @@ var MARKET_FEATURE_INTERVAL_MS = {
1719
1721
  "15m": 9e5,
1720
1722
  "1h": 36e5
1721
1723
  };
1722
- var STABLE_QUOTE_SYMBOLS = /* @__PURE__ */ new Set([
1723
- "USDCUSDT",
1724
- "FDUSDUSDT",
1725
- "TUSDUSDT",
1726
- "BUSDUSDT",
1727
- "USDPUSDT",
1728
- "DAIUSDT"
1729
- ]);
1730
1724
  var safeDivide = (num, den) => Number.isFinite(num) && Number.isFinite(den) && den !== 0 ? num / den : null;
1731
1725
  var mean = (values) => values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null;
1732
1726
  var standardDeviation = (values) => {
@@ -1770,9 +1764,6 @@ var classifyBtcAltRegime = ({
1770
1764
  if (btcVsAltReturn24h < -5e-3) return "alt_lead";
1771
1765
  return "neutral";
1772
1766
  };
1773
- var selectBreadthUniverseFromTickers = (tickers, limit) => tickers.filter(
1774
- (ticker) => ticker.symbol.endsWith("USDT") && ticker.symbol !== "BTCUSDT" && !STABLE_QUOTE_SYMBOLS.has(ticker.symbol) && Number.isFinite(ticker.turnover24h) && ticker.turnover24h > 0
1775
- ).sort((a, b) => b.turnover24h - a.turnover24h).slice(0, Math.max(0, limit)).map((ticker) => ticker.symbol);
1776
1767
  var aggregateAggTradesToRows = ({
1777
1768
  symbol,
1778
1769
  interval: interval2,
@@ -2254,10 +2245,6 @@ var backfillBinanceMarketContext = async (params, enabled) => {
2254
2245
  process.env.BINANCE_MARKET_CONTEXT_TRADE_FLOW_SYMBOL_LIMIT,
2255
2246
  2
2256
2247
  );
2257
- const breadthLimit = asInt(
2258
- process.env.BINANCE_MARKET_CONTEXT_BREADTH_LIMIT,
2259
- 30
2260
- );
2261
2248
  const breadthChunkDays = asFloat(
2262
2249
  process.env.BINANCE_MARKET_CONTEXT_BREADTH_CHUNK_DAYS,
2263
2250
  30
@@ -2380,88 +2367,113 @@ var backfillBinanceMarketContext = async (params, enabled) => {
2380
2367
  }
2381
2368
  }
2382
2369
  if (includeBreadth) {
2383
- const tickers = await connector.getTickers();
2384
- const breadthSymbols = selectBreadthUniverseFromTickers(
2385
- tickers,
2386
- breadthLimit
2387
- );
2388
- const universe = `binance_top${breadthSymbols.length}_usdt`;
2389
- const coverage = await (0, import_timescale.getMarketBreadthCoverage)({
2390
- universe,
2391
- interval: interval2,
2392
- startMs: breadthStartMs,
2393
- endMs
2394
- });
2395
- const hasBtcAltMetrics = (coverage?.btcAltMetricsRows ?? 0) >= Math.floor(Math.max(1, coverage?.rows ?? 0) * 0.9);
2396
- if (!hasCoverage({
2397
- coverage,
2370
+ const breadthUniverses = (0, import_strategies.getBinanceBreadthUniverses)();
2371
+ const breadthSymbols = breadthUniverses.at(-1)?.symbols ?? [];
2372
+ const connectorInterval = marketIntervalToConnectorInterval(interval2);
2373
+ const chunks = buildBreadthBackfillChunks({
2398
2374
  startMs: breadthStartMs,
2399
2375
  endMs,
2400
- intervalMs
2401
- }) || !hasBtcAltMetrics) {
2402
- const connectorInterval = marketIntervalToConnectorInterval(interval2);
2403
- const chunks = buildBreadthBackfillChunks({
2404
- startMs: breadthStartMs,
2405
- endMs,
2406
- intervalMs,
2407
- chunkDays: breadthChunkDays
2408
- });
2409
- const missingChunks = filterMissingBreadthBackfillChunks({
2410
- chunks,
2411
- coverage,
2412
- intervalMs
2413
- });
2414
- const bar = new import_progress.default(
2415
- "breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
2416
- {
2417
- total: Math.max(1, missingChunks.length * breadthSymbols.length),
2418
- width: 24
2419
- }
2420
- );
2421
- let candlesRead = 0;
2422
- let skippedBreadthChunks = chunks.length - missingChunks.length;
2423
- if (!missingChunks.length) {
2424
- bar.tick(1, {
2425
- candles: 0,
2426
- skip: skippedBreadthChunks,
2427
- chunk: "cached",
2428
- symbol: universe
2376
+ intervalMs,
2377
+ chunkDays: breadthChunkDays
2378
+ });
2379
+ const plans = await Promise.all(
2380
+ breadthUniverses.map(async (definition) => {
2381
+ const coverage = await (0, import_timescale.getMarketBreadthCoverage)({
2382
+ universe: definition.universe,
2383
+ interval: interval2,
2384
+ startMs: breadthStartMs,
2385
+ endMs
2429
2386
  });
2430
- }
2431
- for (let chunkIndex = 0; chunkIndex < missingChunks.length; chunkIndex += 1) {
2432
- const chunk = missingChunks[chunkIndex];
2433
- const originalChunkIndex = chunks.findIndex(
2434
- (item) => item.startMs === chunk.startMs && item.endMs === chunk.endMs
2435
- );
2436
- const candlesBySymbol = {};
2437
- for (const symbol of breadthSymbols) {
2438
- const candles = await connector.kline({
2439
- symbol,
2440
- interval: connectorInterval,
2441
- start: chunk.fetchStartMs,
2442
- end: chunk.endMs,
2443
- silent: true
2444
- });
2445
- candlesBySymbol[symbol] = candles;
2446
- candlesRead += candles.length;
2447
- bar.tick(1, {
2448
- candles: candlesRead,
2449
- skip: skippedBreadthChunks,
2450
- chunk: `${originalChunkIndex + 1}/${chunks.length}`,
2451
- symbol
2452
- });
2387
+ const hasBtcAltMetrics = (coverage?.btcAltMetricsRows ?? 0) >= Math.floor(Math.max(1, coverage?.rows ?? 0) * 0.9);
2388
+ const missingChunks2 = !hasCoverage({
2389
+ coverage,
2390
+ startMs: breadthStartMs,
2391
+ endMs,
2392
+ intervalMs
2393
+ }) || !hasBtcAltMetrics ? !hasBtcAltMetrics ? chunks : filterMissingBreadthBackfillChunks({
2394
+ chunks,
2395
+ coverage,
2396
+ intervalMs
2397
+ }) : [];
2398
+ if (!missingChunks2.length) {
2399
+ console.log(
2400
+ import_chalk.default.gray(`breadth cached: universe=${definition.universe}`)
2401
+ );
2453
2402
  }
2454
- const btcCandles = await connector.kline({
2455
- symbol: "BTCUSDT",
2403
+ return {
2404
+ definition,
2405
+ missingChunkKeys: new Set(
2406
+ missingChunks2.map((chunk) => `${chunk.startMs}:${chunk.endMs}`)
2407
+ )
2408
+ };
2409
+ })
2410
+ );
2411
+ const missingChunks = chunks.filter((chunk) => {
2412
+ const key = `${chunk.startMs}:${chunk.endMs}`;
2413
+ return plans.some((plan) => plan.missingChunkKeys.has(key));
2414
+ });
2415
+ const bar = new import_progress.default(
2416
+ "breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
2417
+ {
2418
+ total: Math.max(1, missingChunks.length * breadthSymbols.length),
2419
+ width: 24
2420
+ }
2421
+ );
2422
+ let candlesRead = 0;
2423
+ const skippedBreadthChunks = chunks.length - missingChunks.length;
2424
+ if (!missingChunks.length) {
2425
+ bar.tick(1, {
2426
+ candles: 0,
2427
+ skip: skippedBreadthChunks,
2428
+ chunk: "cached",
2429
+ symbol: "top5/top10/top30/top50/top100"
2430
+ });
2431
+ }
2432
+ for (let chunkIndex = 0; chunkIndex < missingChunks.length; chunkIndex += 1) {
2433
+ const chunk = missingChunks[chunkIndex];
2434
+ const originalChunkIndex = chunks.findIndex(
2435
+ (item) => item.startMs === chunk.startMs && item.endMs === chunk.endMs
2436
+ );
2437
+ const candlesBySymbol = {};
2438
+ for (const symbol of breadthSymbols) {
2439
+ const candles = await connector.kline({
2440
+ symbol,
2456
2441
  interval: connectorInterval,
2457
2442
  start: chunk.fetchStartMs,
2458
2443
  end: chunk.endMs,
2459
2444
  silent: true
2460
2445
  });
2446
+ candlesBySymbol[symbol] = candles;
2447
+ candlesRead += candles.length;
2448
+ bar.tick(1, {
2449
+ candles: candlesRead,
2450
+ skip: skippedBreadthChunks,
2451
+ chunk: `${originalChunkIndex + 1}/${chunks.length}`,
2452
+ symbol
2453
+ });
2454
+ }
2455
+ const btcCandles = await connector.kline({
2456
+ symbol: "BTCUSDT",
2457
+ interval: connectorInterval,
2458
+ start: chunk.fetchStartMs,
2459
+ end: chunk.endMs,
2460
+ silent: true
2461
+ });
2462
+ const chunkKey = `${chunk.startMs}:${chunk.endMs}`;
2463
+ for (const plan of plans) {
2464
+ if (!plan.missingChunkKeys.has(chunkKey)) {
2465
+ continue;
2466
+ }
2467
+ const symbolSet = new Set(plan.definition.symbols);
2468
+ const universeCandles = Object.fromEntries(
2469
+ Object.entries(candlesBySymbol).filter(
2470
+ ([symbol]) => symbolSet.has(symbol)
2471
+ )
2472
+ );
2461
2473
  const rows = buildMarketBreadthRows({
2462
- universe,
2474
+ universe: plan.definition.universe,
2463
2475
  interval: interval2,
2464
- candlesBySymbol,
2476
+ candlesBySymbol: universeCandles,
2465
2477
  btcCandles
2466
2478
  }).filter((row) => {
2467
2479
  const ts = row.ts.getTime();
@@ -2470,8 +2482,6 @@ var backfillBinanceMarketContext = async (params, enabled) => {
2470
2482
  await (0, import_timescale.upsertMarketBreadthRows)(rows);
2471
2483
  breadthRows += rows.length;
2472
2484
  }
2473
- } else {
2474
- console.log(import_chalk.default.gray(`breadth cached: universe=${universe}`));
2475
2485
  }
2476
2486
  }
2477
2487
  console.log(
@@ -4666,11 +4676,30 @@ var resolveTimeWindow = ({
4666
4676
  endTime,
4667
4677
  nowMs = Date.now(),
4668
4678
  defaultStartMs,
4669
- defaultEndMs
4679
+ defaultEndMs,
4680
+ closedIntervalMs
4670
4681
  }) => {
4671
4682
  const explicitStart = toEpochMs(startTime);
4672
4683
  const explicitEnd = toEpochMs(endTime);
4673
4684
  const resolvedDays = toPositiveNumber(days);
4685
+ const alignToLastClosedInterval = (window2, preserveDuration) => {
4686
+ if (typeof closedIntervalMs !== "number" || !Number.isFinite(closedIntervalMs) || closedIntervalMs <= 0) {
4687
+ return window2;
4688
+ }
4689
+ const alignedEnd = Math.floor(window2.end / closedIntervalMs) * closedIntervalMs - 1;
4690
+ const delta = window2.end - alignedEnd;
4691
+ const alignedStart = preserveDuration ? window2.start - delta : window2.start;
4692
+ if (alignedStart >= alignedEnd) {
4693
+ throw new Error(
4694
+ `Invalid closed-candle window: start (${alignedStart}) must be less than end (${alignedEnd})`
4695
+ );
4696
+ }
4697
+ return {
4698
+ ...window2,
4699
+ start: Math.trunc(alignedStart),
4700
+ end: Math.trunc(alignedEnd)
4701
+ };
4702
+ };
4674
4703
  if (explicitStart != null || explicitEnd != null) {
4675
4704
  const resolvedStart = explicitStart ?? (resolvedDays != null && explicitEnd != null ? explicitEnd - resolvedDays * ONE_DAY_MS : defaultStartMs);
4676
4705
  const resolvedEnd2 = explicitEnd ?? (resolvedDays != null && explicitStart != null ? explicitStart + resolvedDays * ONE_DAY_MS : defaultEndMs ?? nowMs);
@@ -4679,11 +4708,14 @@ var resolveTimeWindow = ({
4679
4708
  `Invalid time window: startTime (${resolvedStart}) must be less than endTime (${resolvedEnd2})`
4680
4709
  );
4681
4710
  }
4682
- return {
4683
- start: resolvedStart,
4684
- end: resolvedEnd2,
4685
- source: "explicit"
4686
- };
4711
+ return alignToLastClosedInterval(
4712
+ {
4713
+ start: resolvedStart,
4714
+ end: resolvedEnd2,
4715
+ source: "explicit"
4716
+ },
4717
+ explicitStart == null && resolvedDays != null
4718
+ );
4687
4719
  }
4688
4720
  if (resolvedDays != null) {
4689
4721
  const resolvedEnd2 = defaultEndMs ?? nowMs;
@@ -4693,11 +4725,14 @@ var resolveTimeWindow = ({
4693
4725
  `Invalid time window: computed start (${resolvedStart}) must be less than end (${resolvedEnd2})`
4694
4726
  );
4695
4727
  }
4696
- return {
4697
- start: Math.trunc(resolvedStart),
4698
- end: Math.trunc(resolvedEnd2),
4699
- source: "days"
4700
- };
4728
+ return alignToLastClosedInterval(
4729
+ {
4730
+ start: Math.trunc(resolvedStart),
4731
+ end: Math.trunc(resolvedEnd2),
4732
+ source: "days"
4733
+ },
4734
+ true
4735
+ );
4701
4736
  }
4702
4737
  const resolvedEnd = defaultEndMs ?? nowMs;
4703
4738
  if (defaultStartMs >= resolvedEnd) {
@@ -4705,11 +4740,14 @@ var resolveTimeWindow = ({
4705
4740
  `Invalid default time window: start (${defaultStartMs}) must be less than end (${resolvedEnd})`
4706
4741
  );
4707
4742
  }
4708
- return {
4709
- start: Math.trunc(defaultStartMs),
4710
- end: Math.trunc(resolvedEnd),
4711
- source: "default"
4712
- };
4743
+ return alignToLastClosedInterval(
4744
+ {
4745
+ start: Math.trunc(defaultStartMs),
4746
+ end: Math.trunc(resolvedEnd),
4747
+ source: "default"
4748
+ },
4749
+ true
4750
+ );
4713
4751
  };
4714
4752
 
4715
4753
  // src/lib/runtimeStrategyBacktest.ts
@@ -5095,7 +5133,8 @@ var prepareRunEnvironment = async ({
5095
5133
  accountId,
5096
5134
  deploymentId,
5097
5135
  assetClasses,
5098
- deployment
5136
+ deployment,
5137
+ closedIntervalMs
5099
5138
  }) => {
5100
5139
  const connectorName = await resolveRunConnectorName({
5101
5140
  value: connector,
@@ -5158,7 +5197,8 @@ var prepareRunEnvironment = async ({
5158
5197
  startTime,
5159
5198
  endTime,
5160
5199
  defaultStartMs: (0, import_time2.getTimestamp)(import_constants2.BACKTEST_DEFAULT_DAYS),
5161
- defaultEndMs: (0, import_time2.getTimestamp)()
5200
+ defaultEndMs: (0, import_time2.getTimestamp)(),
5201
+ closedIntervalMs
5162
5202
  });
5163
5203
  const preloadStart = (0, import_time2.getBacktestPreloadStart)(
5164
5204
  window2.start,
@@ -5553,8 +5593,12 @@ var markBacktestRunStatus = async ({
5553
5593
  };
5554
5594
 
5555
5595
  // src/lib/backtest/runState.ts
5596
+ var import_types2 = require("@tradejs/types");
5556
5597
  var successTests = 0;
5557
5598
  var errorTests = 0;
5599
+ var warningCounts = {
5600
+ [import_types2.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY]: 0
5601
+ };
5558
5602
  var errorMessages = [];
5559
5603
  var topResults = [];
5560
5604
  var bestTickerResults = /* @__PURE__ */ new Map();
@@ -5601,6 +5645,14 @@ var addResultToAggregate = (aggregate, result) => {
5601
5645
  aggregate.winRateCount += 1;
5602
5646
  }
5603
5647
  };
5648
+ var addResultWarnings = (result) => {
5649
+ for (const code of Object.values(import_types2.BACKTEST_WARNING_CODES)) {
5650
+ const count = Number(result.warningCounts?.[code] ?? 0);
5651
+ if (Number.isFinite(count) && count > 0) {
5652
+ warningCounts[code] = (warningCounts[code] ?? 0) + count;
5653
+ }
5654
+ }
5655
+ };
5604
5656
  var getAggregateAverageProfit = (aggregate) => aggregate.count > 0 ? aggregate.netProfitSum / aggregate.count : 0;
5605
5657
  var getAggregateWinRate = (aggregate) => {
5606
5658
  const closedTrades = aggregate.wins + aggregate.losses;
@@ -5626,6 +5678,7 @@ var markTestsStarted = () => {
5626
5678
  var getRunCounters = () => ({
5627
5679
  successTests,
5628
5680
  errorTests,
5681
+ warningCounts: { ...warningCounts },
5629
5682
  errors: [...errorMessages]
5630
5683
  });
5631
5684
  var getTopResults = () => topResults;
@@ -5639,6 +5692,7 @@ var setBestTickerResultForSymbol = (symbol, result) => {
5639
5692
  };
5640
5693
  var recordResultAggregates = (result) => {
5641
5694
  addResultToAggregate(progressStats, result);
5695
+ addResultWarnings(result);
5642
5696
  const configId = result.test.configId || result.test.name;
5643
5697
  const existing = configResultBuckets.get(configId);
5644
5698
  const bucket = existing ?? {
@@ -5688,6 +5742,9 @@ var setRuntimeCompareContext = ({
5688
5742
  var resetRunState = () => {
5689
5743
  successTests = 0;
5690
5744
  errorTests = 0;
5745
+ for (const code of Object.values(import_types2.BACKTEST_WARNING_CODES)) {
5746
+ warningCounts[code] = 0;
5747
+ }
5691
5748
  errorMessages.length = 0;
5692
5749
  topResults = [];
5693
5750
  bestTickerResults.clear();
@@ -6284,9 +6341,15 @@ var printRunOutro = () => {
6284
6341
  import_chalk9.default.gray(`backtest total: done in ${formatDuration(getRunStartedAt())}`)
6285
6342
  );
6286
6343
  console.log("");
6287
- const { successTests: successTests2, errorTests: errorTests2 } = getRunCounters();
6344
+ const { successTests: successTests2, errorTests: errorTests2, warningCounts: warningCounts2 } = getRunCounters();
6288
6345
  console.log(`${import_chalk9.default.green("SUCCESS TESTS")}: ${successTests2}`);
6289
6346
  console.log(`${import_chalk9.default.red("ERRORS")}: ${errorTests2}`);
6347
+ const takeProfitCrossedWarnings = warningCounts2[import_types3.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY] ?? 0;
6348
+ console.log(
6349
+ import_chalk9.default.yellow(
6350
+ `WARNINGS (${import_types3.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY}): ${takeProfitCrossedWarnings}`
6351
+ )
6352
+ );
6290
6353
  console.log("");
6291
6354
  };
6292
6355
 
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/scripts/binanceBreadthUniversesUpdate.ts
31
+ var binanceBreadthUniversesUpdate_exports = {};
32
+ __export(binanceBreadthUniversesUpdate_exports, {
33
+ BINANCE_BREADTH_UNIVERSES_RELATIVE_PATH: () => BINANCE_BREADTH_UNIVERSES_RELATIVE_PATH,
34
+ main: () => main
35
+ });
36
+ module.exports = __toCommonJS(binanceBreadthUniversesUpdate_exports);
37
+ var import_config = require("dotenv/config");
38
+ var import_args = __toESM(require("args"));
39
+ var import_promises = __toESM(require("fs/promises"));
40
+ var import_node_path = __toESM(require("path"));
41
+ var import_chalk = __toESM(require("chalk"));
42
+ var import_connectors = require("@tradejs/connectors");
43
+ var import_connectors2 = require("@tradejs/node/connectors");
44
+ var import_strategies = require("@tradejs/node/strategies");
45
+
46
+ // src/lib/binanceMarketData.ts
47
+ var STABLE_QUOTE_SYMBOLS = /* @__PURE__ */ new Set([
48
+ "USDCUSDT",
49
+ "FDUSDUSDT",
50
+ "TUSDUSDT",
51
+ "BUSDUSDT",
52
+ "USDPUSDT",
53
+ "DAIUSDT"
54
+ ]);
55
+ var selectBreadthUniverseFromTickers = (tickers, limit) => tickers.filter(
56
+ (ticker) => ticker.symbol.endsWith("USDT") && ticker.symbol !== "BTCUSDT" && !STABLE_QUOTE_SYMBOLS.has(ticker.symbol) && Number.isFinite(ticker.turnover24h) && ticker.turnover24h > 0
57
+ ).sort((a, b) => b.turnover24h - a.turnover24h).slice(0, Math.max(0, limit)).map((ticker) => ticker.symbol);
58
+
59
+ // src/scripts/binanceBreadthUniversesUpdate.ts
60
+ import_args.default.example(
61
+ "yarn breadth:universes:update --write",
62
+ "Refresh the versioned Binance top5/top10/top30/top50/top100 breadth snapshot"
63
+ );
64
+ import_args.default.option(
65
+ ["w", "write"],
66
+ "Write the snapshot; without this flag only print the proposed update",
67
+ false
68
+ );
69
+ import_args.default.option(["u", "user"], "Connector user name", "root");
70
+ var flags = import_args.default.parse(process.argv);
71
+ var BINANCE_BREADTH_UNIVERSES_RELATIVE_PATH = "packages/node/src/config/binanceBreadthUniverses.json";
72
+ var main = async () => {
73
+ const projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
74
+ const connectorCreator = await (0, import_connectors2.getConnectorCreatorByName)(
75
+ import_connectors.ConnectorNames.Binance,
76
+ projectRoot
77
+ );
78
+ if (!connectorCreator) {
79
+ throw new Error("Binance connector is not registered");
80
+ }
81
+ const connector = await connectorCreator({
82
+ userName: String(flags.user || "root"),
83
+ universe: "crypto"
84
+ });
85
+ const rankedSymbols = selectBreadthUniverseFromTickers(
86
+ await connector.getTickers(),
87
+ 100
88
+ );
89
+ const snapshot = (0, import_strategies.buildBinanceBreadthUniverseSnapshot)({ rankedSymbols });
90
+ const serialized = `${JSON.stringify(snapshot, null, 2)}
91
+ `;
92
+ console.log(
93
+ import_chalk.default.cyan(
94
+ `Binance breadth snapshot ${snapshot.fingerprint}: ${Object.entries(
95
+ snapshot.universes
96
+ ).map(
97
+ ([key, definition]) => `${key}=${definition.size}:${definition.fingerprint}`
98
+ ).join(" ")}`
99
+ )
100
+ );
101
+ if (!flags.write) {
102
+ console.log(serialized);
103
+ console.log(
104
+ import_chalk.default.yellow("Dry run only. Pass --write to update the snapshot file.")
105
+ );
106
+ return snapshot;
107
+ }
108
+ const outputPath = import_node_path.default.join(
109
+ projectRoot,
110
+ BINANCE_BREADTH_UNIVERSES_RELATIVE_PATH
111
+ );
112
+ await import_promises.default.writeFile(outputPath, serialized, "utf8");
113
+ console.log(import_chalk.default.green(`Updated ${outputPath}`));
114
+ return snapshot;
115
+ };
116
+ // Annotate the CommonJS export names for ESM import in node:
117
+ 0 && (module.exports = {
118
+ BINANCE_BREADTH_UNIVERSES_RELATIVE_PATH,
119
+ main
120
+ });
@@ -37,6 +37,7 @@ var import_config = require("dotenv/config");
37
37
  var import_args = __toESM(require("args"));
38
38
  var import_chalk = __toESM(require("chalk"));
39
39
  var import_connectors = require("@tradejs/connectors");
40
+ var import_strategies = require("@tradejs/node/strategies");
40
41
  var import_timescale = require("@tradejs/infra/timescale");
41
42
 
42
43
  // src/lib/binanceMarketData.ts
@@ -46,14 +47,6 @@ var MARKET_FEATURE_INTERVAL_MS = {
46
47
  "15m": 9e5,
47
48
  "1h": 36e5
48
49
  };
49
- var STABLE_QUOTE_SYMBOLS = /* @__PURE__ */ new Set([
50
- "USDCUSDT",
51
- "FDUSDUSDT",
52
- "TUSDUSDT",
53
- "BUSDUSDT",
54
- "USDPUSDT",
55
- "DAIUSDT"
56
- ]);
57
50
  var safeDivide = (num, den) => Number.isFinite(num) && Number.isFinite(den) && den !== 0 ? num / den : null;
58
51
  var mean = (values) => values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null;
59
52
  var standardDeviation = (values) => {
@@ -106,23 +99,23 @@ var normalizeMarketFeatureInterval = (value) => {
106
99
  return "15m";
107
100
  };
108
101
  var normalizeBinanceSymbols = (value) => String(value || "").split(",").map((item) => item.trim().toUpperCase()).filter(Boolean);
109
- var selectBreadthUniverseFromTickers = (tickers, limit) => tickers.filter(
110
- (ticker) => ticker.symbol.endsWith("USDT") && ticker.symbol !== "BTCUSDT" && !STABLE_QUOTE_SYMBOLS.has(ticker.symbol) && Number.isFinite(ticker.turnover24h) && ticker.turnover24h > 0
111
- ).sort((a, b) => b.turnover24h - a.turnover24h).slice(0, Math.max(0, limit)).map((ticker) => ticker.symbol);
112
102
  var estimateBinanceMarketDataVolume = ({
113
103
  symbols,
114
104
  days,
115
105
  interval,
116
106
  includeAggTrades,
117
107
  includeBreadth,
118
- breadthLimit
108
+ breadthSizes
119
109
  }) => {
120
110
  const intervalMs = MARKET_FEATURE_INTERVAL_MS[interval];
121
111
  const bucketRowsPerSymbol = Math.ceil(days * 864e5 / intervalMs);
122
112
  const aggTradeBucketRows = includeAggTrades ? symbols.length * bucketRowsPerSymbol : 0;
123
- const breadthSymbols = includeBreadth ? Math.max(0, breadthLimit) : 0;
113
+ const normalizedBreadthSizes = breadthSizes.filter(
114
+ (size) => Number.isFinite(size) && size > 0
115
+ );
116
+ const breadthSymbols = includeBreadth ? Math.max(0, ...normalizedBreadthSizes) : 0;
124
117
  const breadthCandleRows = breadthSymbols * bucketRowsPerSymbol;
125
- const breadthRows = includeBreadth ? bucketRowsPerSymbol : 0;
118
+ const breadthRows = includeBreadth ? bucketRowsPerSymbol * normalizedBreadthSizes.length : 0;
126
119
  return {
127
120
  interval,
128
121
  days,
@@ -379,11 +372,6 @@ import_args.default.option(
379
372
  ["w", "write"],
380
373
  "Write rows to Timescale; without this flag only estimate"
381
374
  );
382
- import_args.default.option(
383
- ["B", "breadthLimit"],
384
- "Top USDT symbols used for breadth universe",
385
- 30
386
- );
387
375
  import_args.default.option(
388
376
  ["M", "batchMinutes"],
389
377
  "aggTrades request window size in minutes",
@@ -463,7 +451,7 @@ var main = async () => {
463
451
  const interval = normalizeMarketFeatureInterval(flags.interval);
464
452
  const hours = flags.hours == null ? null : asFloat(flags.hours, 0);
465
453
  const days = hours != null && hours > 0 ? hours / 24 : asFloat(flags.days, 1);
466
- const breadthLimit = asInt(flags.breadthLimit, 30);
454
+ const breadthUniverses = (0, import_strategies.getBinanceBreadthUniverses)();
467
455
  const batchMinutes = asInt(flags.batchMinutes, 15);
468
456
  const requestDelayMs = asInt(flags.requestDelayMs, 75);
469
457
  const includeAll = Boolean(flags.all);
@@ -481,7 +469,7 @@ var main = async () => {
481
469
  interval,
482
470
  includeAggTrades: modes.includeAggTrades,
483
471
  includeBreadth: modes.includeBreadth,
484
- breadthLimit
472
+ breadthSizes: breadthUniverses.map(({ size }) => size)
485
473
  });
486
474
  printEstimate(estimate);
487
475
  if (!flags.write) {
@@ -520,11 +508,7 @@ var main = async () => {
520
508
  }
521
509
  }
522
510
  if (modes.includeBreadth) {
523
- const tickers = await connector.getTickers();
524
- const breadthSymbols = selectBreadthUniverseFromTickers(
525
- tickers,
526
- breadthLimit
527
- );
511
+ const breadthSymbols = breadthUniverses.at(-1)?.symbols ?? [];
528
512
  const connectorInterval = intervalToConnectorInterval(interval);
529
513
  const candlesBySymbol = {};
530
514
  for (const symbol of breadthSymbols) {
@@ -541,14 +525,29 @@ var main = async () => {
541
525
  process.stdout.write(`${candles.length}
542
526
  `);
543
527
  }
544
- const universe = `binance_top${breadthSymbols.length}_usdt`;
545
- const rows = buildMarketBreadthRows({
546
- universe,
547
- interval,
548
- candlesBySymbol
528
+ const btcCandles = await connector.kline({
529
+ symbol: "BTCUSDT",
530
+ interval: connectorInterval,
531
+ start: fromMs,
532
+ end: toMs,
533
+ silent: true
549
534
  });
550
- await (0, import_timescale.upsertMarketBreadthRows)(rows);
551
- breadthRows += rows.length;
535
+ for (const definition of breadthUniverses) {
536
+ const symbolSet = new Set(definition.symbols);
537
+ const universeCandles = Object.fromEntries(
538
+ Object.entries(candlesBySymbol).filter(
539
+ ([symbol]) => symbolSet.has(symbol)
540
+ )
541
+ );
542
+ const rows = buildMarketBreadthRows({
543
+ universe: definition.universe,
544
+ interval,
545
+ candlesBySymbol: universeCandles,
546
+ btcCandles
547
+ });
548
+ await (0, import_timescale.upsertMarketBreadthRows)(rows);
549
+ breadthRows += rows.length;
550
+ }
552
551
  }
553
552
  console.log(import_chalk.default.green("Binance market ingest done"));
554
553
  console.log(`aggTrades raw rows: ${aggTradesRaw}`);