@tradejs/node 2.0.15 → 2.0.17

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/ai.js CHANGED
@@ -1147,6 +1147,15 @@ var postProcessAiAnalysisByStrategy = (signal, analysis, payload = buildAiPayloa
1147
1147
  payload,
1148
1148
  analysis
1149
1149
  }) ?? analysis;
1150
+ var postProcessLocalAiAnalysisByStrategy = (signal, analysis, payload = buildAiPayloadByStrategy(signal)) => {
1151
+ const adapter = getSignalAiAdapter(signal);
1152
+ const strategyAnalysis = adapter.postProcessAnalysis?.({ signal, payload, analysis }) ?? analysis;
1153
+ return adapter.postProcessLocalAnalysis?.({
1154
+ signal,
1155
+ payload,
1156
+ analysis: strategyAnalysis
1157
+ }) ?? strategyAnalysis;
1158
+ };
1150
1159
 
1151
1160
  // src/ai.ts
1152
1161
  var parseAIResponse = (input) => {
@@ -1295,7 +1304,7 @@ Input payload structure:
1295
1304
  \u2022 \`baseContext.relative\`: BTC/ETH relative-strength, benchmark MA bias context, Binance alt-basket breadth, and CoinMarketCap historical global/exchange/index context when available.
1296
1305
  \u2022 \`baseContext.derivatives\`: Coinalyze-aligned derivatives summary when available.
1297
1306
  \u2022 \`baseContext.mtf\`: compact multi-timeframe summary plus only the latest few candles for each timeframe.
1298
- \u2022 \`baseContext.gateFeatures\`: direction-aware, normalized fields derived from baseContext; prefer \`setup\`, \`scores\`, \`confirmations\`, \`conflicts\`, \`risk\`, and \`decisionHints\` for quick gate checks before inspecting raw nested context.
1307
+ \u2022 \`baseContext.gateFeatures\`: compact direction-aware fields derived from baseContext; prefer \`setup\`, \`scores\`, \`conflicts\`, \`risk\`, \`decisionHints\`, \`mtf\`, \`volatility\`, \`participation\`, and \`relative\` for quick gate checks before inspecting raw nested context.
1299
1308
  Always inspect \`payload.additionalIndicators.marketContext\` when present:
1300
1309
  \u2022 \`marketContext.execution.binanceCoinbaseSpread\`: AI-friendly BTC spread view projected from \`payload.additionalIndicators.baseContext.relative.execution.venueSpread\`; \`value=(Coinbase-Binance)/Binance\`, \`bps=value*10000\`.
1301
1310
  \u2022 \`marketContext.participation.trueDelta\`: Binance taker buy/sell volume delta from kline payload when \`source=kline_taker_volume\`; otherwise absent/unavailable.
@@ -1577,7 +1586,7 @@ var runAiPromptLocal = async (signal, options = {}) => {
1577
1586
  const signalDirection = getSignalDirection(signal);
1578
1587
  const deterministicQuality = getDeterministicQuality(gateContext);
1579
1588
  const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
1580
- return postProcessAiAnalysisByStrategy(
1589
+ return postProcessLocalAiAnalysisByStrategy(
1581
1590
  signal,
1582
1591
  {
1583
1592
  direction: approvalAllowedNow ? signalDirection : null,
package/dist/ai.mjs CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  runAiPrompt,
15
15
  runAiPromptLocal,
16
16
  trimSeriesDeep
17
- } from "./chunk-DPHKRDD5.mjs";
17
+ } from "./chunk-WK5EUCX5.mjs";
18
18
  import "./chunk-QVSMINLG.mjs";
19
19
  import "./chunk-WS5DYEVZ.mjs";
20
20
  import "./chunk-UV2HVMKZ.mjs";
package/dist/backtest.js CHANGED
@@ -50,7 +50,7 @@ var import_strategies5 = require("@tradejs/core/strategies");
50
50
  var import_time = require("@tradejs/core/time");
51
51
  var import_ai2 = require("@tradejs/infra/ai");
52
52
  var import_ml2 = require("@tradejs/infra/ml");
53
- var import_logger8 = require("@tradejs/infra/logger");
53
+ var import_logger9 = require("@tradejs/infra/logger");
54
54
 
55
55
  // src/ai.ts
56
56
  var import_aiLanguages = require("@tradejs/infra/aiLanguages");
@@ -1153,6 +1153,9 @@ var buildAiPayloadByStrategy = (signal) => {
1153
1153
  // src/ai.ts
1154
1154
  var buildAiPayload = (signal) => buildAiPayloadByStrategy(signal);
1155
1155
 
1156
+ // src/strategyHelpers/marketContextStages.ts
1157
+ var import_logger7 = require("@tradejs/infra/logger");
1158
+
1156
1159
  // src/strategyHelpers/binanceMarketContext.ts
1157
1160
  var import_timescale = require("@tradejs/infra/timescale");
1158
1161
  var import_logger3 = require("@tradejs/infra/logger");
@@ -1461,6 +1464,13 @@ var getBinanceBreadthUniverses = () => BINANCE_BREADTH_UNIVERSE_KEYS.map((key) =
1461
1464
  };
1462
1465
  });
1463
1466
 
1467
+ // src/strategyHelpers/marketContextErrors.ts
1468
+ var MARKET_CONTEXT_CANCELLATION_ERROR_NAMES = /* @__PURE__ */ new Set([
1469
+ "AbortError",
1470
+ "TimescaleQueryTimeoutError"
1471
+ ]);
1472
+ var isMarketContextCancellationError = (error, abortSignal) => abortSignal?.aborted === true || error instanceof Error && MARKET_CONTEXT_CANCELLATION_ERROR_NAMES.has(error.name);
1473
+
1464
1474
  // src/strategyHelpers/binanceMarketContext.ts
1465
1475
  var DEFAULT_MAX_AGE_BY_INTERVAL = {
1466
1476
  "1m": 3 * 6e4,
@@ -1525,7 +1535,8 @@ var getCachedReferenceRows = ({
1525
1535
  referenceSymbols,
1526
1536
  interval,
1527
1537
  timestamp,
1528
- maxAgeMs
1538
+ maxAgeMs,
1539
+ abortSignal
1529
1540
  }) => {
1530
1541
  const key = `${referenceSymbols.join(",")}:${interval}:${timestamp}:${maxAgeMs}`;
1531
1542
  const cached = referenceRowsCache.get(key);
@@ -1536,7 +1547,8 @@ var getCachedReferenceRows = ({
1536
1547
  symbol,
1537
1548
  interval,
1538
1549
  atMs: timestamp,
1539
- maxAgeMs
1550
+ maxAgeMs,
1551
+ ...abortSignal ? { signal: abortSignal } : {}
1540
1552
  });
1541
1553
  return {
1542
1554
  symbol,
@@ -1545,13 +1557,15 @@ var getCachedReferenceRows = ({
1545
1557
  })
1546
1558
  );
1547
1559
  referenceRowsCache.set(key, promise);
1560
+ void promise.catch(() => referenceRowsCache.delete(key));
1548
1561
  return promise;
1549
1562
  };
1550
1563
  var getCachedBreadth = ({
1551
1564
  breadthUniverse,
1552
1565
  interval,
1553
1566
  timestamp,
1554
- maxAgeMs
1567
+ maxAgeMs,
1568
+ abortSignal
1555
1569
  }) => {
1556
1570
  const key = `${breadthUniverse}:${interval}:${timestamp}:${maxAgeMs}`;
1557
1571
  const cached = breadthCache.get(key);
@@ -1560,9 +1574,11 @@ var getCachedBreadth = ({
1560
1574
  universe: breadthUniverse,
1561
1575
  interval,
1562
1576
  atMs: timestamp,
1563
- maxAgeMs
1577
+ maxAgeMs,
1578
+ ...abortSignal ? { signal: abortSignal } : {}
1564
1579
  });
1565
1580
  breadthCache.set(key, promise);
1581
+ void promise.catch(() => breadthCache.delete(key));
1566
1582
  return promise;
1567
1583
  };
1568
1584
  var toMarketBreadthContext = (breadth, interval) => ({
@@ -1608,7 +1624,8 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
1608
1624
  referenceSymbols,
1609
1625
  interval,
1610
1626
  timestamp: signal.timestamp,
1611
- maxAgeMs
1627
+ maxAgeMs,
1628
+ abortSignal: params.abortSignal
1612
1629
  }),
1613
1630
  Promise.all(
1614
1631
  breadthUniverses.map(async ({ key, universe }) => ({
@@ -1617,7 +1634,8 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
1617
1634
  breadthUniverse: universe,
1618
1635
  interval,
1619
1636
  timestamp: signal.timestamp,
1620
- maxAgeMs
1637
+ maxAgeMs,
1638
+ abortSignal: params.abortSignal
1621
1639
  })
1622
1640
  }))
1623
1641
  )
@@ -1732,6 +1750,9 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
1732
1750
  (0, import_strategies.refreshSignalBaseContextGateFeatures)(signal);
1733
1751
  return true;
1734
1752
  } catch (error) {
1753
+ if (isMarketContextCancellationError(error, params.abortSignal)) {
1754
+ throw error;
1755
+ }
1735
1756
  binanceMarketContextUnavailable = true;
1736
1757
  import_logger3.logger.warn(
1737
1758
  "Binance market context disabled after Timescale read failure: %s",
@@ -1852,7 +1873,8 @@ var toIndexRegime = ({
1852
1873
  };
1853
1874
  var getCachedGlobalContext = ({
1854
1875
  timestamp,
1855
- maxAgeMs
1876
+ maxAgeMs,
1877
+ abortSignal
1856
1878
  }) => {
1857
1879
  const key = `${SOURCE_GLOBAL_DAILY}:${timestamp}:${maxAgeMs}`;
1858
1880
  const cached = globalContextCache.get(key);
@@ -1860,14 +1882,17 @@ var getCachedGlobalContext = ({
1860
1882
  const promise = (0, import_timescale2.getLatestMarketGlobalContext)({
1861
1883
  source: SOURCE_GLOBAL_DAILY,
1862
1884
  atMs: timestamp,
1863
- maxAgeMs
1885
+ maxAgeMs,
1886
+ ...abortSignal ? { signal: abortSignal } : {}
1864
1887
  });
1865
1888
  globalContextCache.set(key, promise);
1889
+ void promise.catch(() => globalContextCache.delete(key));
1866
1890
  return promise;
1867
1891
  };
1868
1892
  var getCachedReferenceContexts = ({
1869
1893
  timestamp,
1870
- maxAgeMs
1894
+ maxAgeMs,
1895
+ abortSignal
1871
1896
  }) => {
1872
1897
  const key = `${SOURCE_REFERENCE}:1d:${timestamp}:${maxAgeMs}`;
1873
1898
  const cached = referenceContextCache.get(key);
@@ -1877,14 +1902,17 @@ var getCachedReferenceContexts = ({
1877
1902
  symbols: ["BTCUSDT", "ETHUSDT"],
1878
1903
  interval: "1d",
1879
1904
  atMs: timestamp,
1880
- maxAgeMs
1905
+ maxAgeMs,
1906
+ ...abortSignal ? { signal: abortSignal } : {}
1881
1907
  });
1882
1908
  referenceContextCache.set(key, promise);
1909
+ void promise.catch(() => referenceContextCache.delete(key));
1883
1910
  return promise;
1884
1911
  };
1885
1912
  var getCachedExchangeLiquidityContext = ({
1886
1913
  timestamp,
1887
- maxAgeMs
1914
+ maxAgeMs,
1915
+ abortSignal
1888
1916
  }) => {
1889
1917
  const key = `${SOURCE_EXCHANGE_LIQUIDITY}:1d:${timestamp}:${maxAgeMs}`;
1890
1918
  const cached = exchangeLiquidityContextCache.get(key);
@@ -1893,14 +1921,17 @@ var getCachedExchangeLiquidityContext = ({
1893
1921
  source: SOURCE_EXCHANGE_LIQUIDITY,
1894
1922
  interval: "1d",
1895
1923
  atMs: timestamp,
1896
- maxAgeMs
1924
+ maxAgeMs,
1925
+ ...abortSignal ? { signal: abortSignal } : {}
1897
1926
  });
1898
1927
  exchangeLiquidityContextCache.set(key, promise);
1928
+ void promise.catch(() => exchangeLiquidityContextCache.delete(key));
1899
1929
  return promise;
1900
1930
  };
1901
1931
  var getCachedFearGreedContext = ({
1902
1932
  timestamp,
1903
- maxAgeMs
1933
+ maxAgeMs,
1934
+ abortSignal
1904
1935
  }) => {
1905
1936
  const key = `${SOURCE_FEAR_GREED}:1d:${timestamp}:${maxAgeMs}`;
1906
1937
  const cached = fearGreedContextCache.get(key);
@@ -1909,14 +1940,17 @@ var getCachedFearGreedContext = ({
1909
1940
  source: SOURCE_FEAR_GREED,
1910
1941
  interval: "1d",
1911
1942
  atMs: timestamp,
1912
- maxAgeMs
1943
+ maxAgeMs,
1944
+ ...abortSignal ? { signal: abortSignal } : {}
1913
1945
  });
1914
1946
  fearGreedContextCache.set(key, promise);
1947
+ void promise.catch(() => fearGreedContextCache.delete(key));
1915
1948
  return promise;
1916
1949
  };
1917
1950
  var getCachedIndexContexts = ({
1918
1951
  timestamp,
1919
- maxAgeMs
1952
+ maxAgeMs,
1953
+ abortSignal
1920
1954
  }) => {
1921
1955
  const key = `${SOURCE_INDEX}:1d:${timestamp}:${maxAgeMs}`;
1922
1956
  const cached = indexContextCache.get(key);
@@ -1926,9 +1960,11 @@ var getCachedIndexContexts = ({
1926
1960
  indexSlugs: ["cmc100", "cmc20"],
1927
1961
  interval: "1d",
1928
1962
  atMs: timestamp,
1929
- maxAgeMs
1963
+ maxAgeMs,
1964
+ ...abortSignal ? { signal: abortSignal } : {}
1930
1965
  });
1931
1966
  indexContextCache.set(key, promise);
1967
+ void promise.catch(() => indexContextCache.delete(key));
1932
1968
  return promise;
1933
1969
  };
1934
1970
  var isCoinMarketCapContextEnabled = (env) => parseEnabledFlag2(process.env.COINMARKETCAP_CONTEXT_ENABLED, env);
@@ -1953,27 +1989,33 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1953
1989
  ] = await Promise.all([
1954
1990
  getCachedGlobalContext({
1955
1991
  timestamp: signal.timestamp,
1956
- maxAgeMs
1992
+ maxAgeMs,
1993
+ abortSignal: params.abortSignal
1957
1994
  }),
1958
1995
  getCachedReferenceContexts({
1959
1996
  timestamp: signal.timestamp,
1960
- maxAgeMs
1997
+ maxAgeMs,
1998
+ abortSignal: params.abortSignal
1961
1999
  }),
1962
2000
  getCachedReferenceContexts({
1963
2001
  timestamp: signal.timestamp - DAY_MS,
1964
- maxAgeMs: maxAgeMs + DAY_MS
2002
+ maxAgeMs: maxAgeMs + DAY_MS,
2003
+ abortSignal: params.abortSignal
1965
2004
  }),
1966
2005
  getCachedExchangeLiquidityContext({
1967
2006
  timestamp: signal.timestamp,
1968
- maxAgeMs
2007
+ maxAgeMs,
2008
+ abortSignal: params.abortSignal
1969
2009
  }),
1970
2010
  getCachedFearGreedContext({
1971
2011
  timestamp: signal.timestamp,
1972
- maxAgeMs
2012
+ maxAgeMs,
2013
+ abortSignal: params.abortSignal
1973
2014
  }),
1974
2015
  getCachedIndexContexts({
1975
2016
  timestamp: signal.timestamp,
1976
- maxAgeMs
2017
+ maxAgeMs,
2018
+ abortSignal: params.abortSignal
1977
2019
  })
1978
2020
  ]);
1979
2021
  const globalRow = globalDailyRow;
@@ -2207,6 +2249,9 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
2207
2249
  (0, import_strategies2.refreshSignalBaseContextGateFeatures)(signal);
2208
2250
  return true;
2209
2251
  } catch (error) {
2252
+ if (isMarketContextCancellationError(error, params.abortSignal)) {
2253
+ throw error;
2254
+ }
2210
2255
  coinMarketCapContextUnavailable = true;
2211
2256
  import_logger4.logger.warn(
2212
2257
  "CoinMarketCap context disabled after Timescale read failure: %s",
@@ -2389,7 +2434,8 @@ var enrichSignalWithDerivativesContext = async (params) => {
2389
2434
  symbol,
2390
2435
  intervals: STORED_INTERVALS,
2391
2436
  endMs: derivativesEndMs,
2392
- lookbackMs
2437
+ lookbackMs,
2438
+ ...params.abortSignal ? { signal: params.abortSignal } : {}
2393
2439
  });
2394
2440
  return [
2395
2441
  symbol,
@@ -2417,7 +2463,8 @@ var enrichSignalWithDerivativesContext = async (params) => {
2417
2463
  symbol: targetSymbol,
2418
2464
  intervals: STORED_INTERVALS,
2419
2465
  endMs: derivativesEndMs,
2420
- lookbackMs
2466
+ lookbackMs,
2467
+ ...params.abortSignal ? { signal: params.abortSignal } : {}
2421
2468
  });
2422
2469
  const context = (0, import_indicators2.buildDerivativesContext)({
2423
2470
  symbol: targetSymbol,
@@ -2448,6 +2495,9 @@ var enrichSignalWithDerivativesContext = async (params) => {
2448
2495
  (0, import_strategies3.refreshSignalBaseContextGateFeatures)(signal);
2449
2496
  return true;
2450
2497
  } catch (error) {
2498
+ if (isMarketContextCancellationError(error, params.abortSignal)) {
2499
+ throw error;
2500
+ }
2451
2501
  derivativesContextUnavailable = true;
2452
2502
  import_logger5.logger.warn(
2453
2503
  "Derivatives context disabled after Timescale read failure: %s",
@@ -2681,7 +2731,7 @@ var resolveHyperliquidPerpFromSignalSymbol = (symbol) => {
2681
2731
  var MAX_CACHE_ENTRIES = 2048;
2682
2732
  var MAX_COVERAGE_SERIES_CACHE_ENTRIES = 20;
2683
2733
  var MAX_FLOW_SERIES_CACHE_ENTRIES = 64;
2684
- var SERIES_CHUNK_MS = 30 * 24 * 60 * 6e4;
2734
+ var SERIES_CHUNK_MS = 90 * 24 * 60 * 6e4;
2685
2735
  var MAX_SERIES_LOOKBACK_MS = 60 * 6e4;
2686
2736
  var DEFAULT_MIN_COVERAGE_PCT = 0.8;
2687
2737
  var INTERVAL_MS = {
@@ -2729,6 +2779,7 @@ var setBoundedCache = (key, value) => {
2729
2779
  if (oldestKey != null) contextCache.delete(oldestKey);
2730
2780
  }
2731
2781
  contextCache.set(key, value);
2782
+ void value.catch(() => contextCache.delete(key));
2732
2783
  };
2733
2784
  var setBoundedSeriesCache = (cache, maxEntries, key, value) => {
2734
2785
  if (cache.size >= maxEntries) {
@@ -2736,6 +2787,7 @@ var setBoundedSeriesCache = (cache, maxEntries, key, value) => {
2736
2787
  if (oldestKey != null) cache.delete(oldestKey);
2737
2788
  }
2738
2789
  cache.set(key, value);
2790
+ void value.catch(() => cache.delete(key));
2739
2791
  };
2740
2792
  var uniqueAddressCount = (rows, field) => {
2741
2793
  const addresses = /* @__PURE__ */ new Set();
@@ -2850,7 +2902,8 @@ var loadHyperliquidWhaleFlowAggregateFromSeries = async (params) => {
2850
2902
  fromMs,
2851
2903
  toMs,
2852
2904
  universeFingerprint: params.universeFingerprint,
2853
- whaleRegistryFingerprint: params.whaleRegistryFingerprint
2905
+ whaleRegistryFingerprint: params.whaleRegistryFingerprint,
2906
+ ...params.abortSignal ? { signal: params.abortSignal } : {}
2854
2907
  });
2855
2908
  setBoundedSeriesCache(
2856
2909
  coverageSeriesCache,
@@ -2867,7 +2920,8 @@ var loadHyperliquidWhaleFlowAggregateFromSeries = async (params) => {
2867
2920
  fromMs,
2868
2921
  toMs,
2869
2922
  universeFingerprint: params.universeFingerprint,
2870
- whaleRegistryFingerprint: params.whaleRegistryFingerprint
2923
+ whaleRegistryFingerprint: params.whaleRegistryFingerprint,
2924
+ ...params.abortSignal ? { signal: params.abortSignal } : {}
2871
2925
  });
2872
2926
  setBoundedSeriesCache(
2873
2927
  flowSeriesCache,
@@ -2951,7 +3005,8 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
2951
3005
  decisionTimeMs,
2952
3006
  maxAgeMs,
2953
3007
  universeFingerprint: universe.fingerprint,
2954
- whaleRegistryFingerprint: whales.fingerprint
3008
+ whaleRegistryFingerprint: whales.fingerprint,
3009
+ abortSignal: params.abortSignal
2955
3010
  });
2956
3011
  } else {
2957
3012
  let pending = contextCache.get(cacheKey);
@@ -2962,7 +3017,8 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
2962
3017
  decisionTimeMs,
2963
3018
  maxAgeMs,
2964
3019
  universeFingerprint: universe.fingerprint,
2965
- whaleRegistryFingerprint: whales.fingerprint
3020
+ whaleRegistryFingerprint: whales.fingerprint,
3021
+ ...params.abortSignal ? { signal: params.abortSignal } : {}
2966
3022
  });
2967
3023
  setBoundedCache(cacheKey, pending);
2968
3024
  }
@@ -2971,6 +3027,9 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
2971
3027
  if (!row) return null;
2972
3028
  return toBaseHyperliquidWhaleFlowContext(row, minimumCoveragePct);
2973
3029
  } catch (error) {
3030
+ if (isMarketContextCancellationError(error, params.abortSignal)) {
3031
+ throw error;
3032
+ }
2974
3033
  hyperliquidWhaleContextUnavailable = true;
2975
3034
  import_logger6.logger.warn(
2976
3035
  "Hyperliquid whale context disabled after Timescale read failure: %s",
@@ -2991,7 +3050,8 @@ var enrichSignalWithHyperliquidWhaleContext = async (params) => {
2991
3050
  env,
2992
3051
  enabled: params.enabled,
2993
3052
  marketInterval: params.interval,
2994
- maxAgeMs: params.maxAgeMs
3053
+ maxAgeMs: params.maxAgeMs,
3054
+ abortSignal: params.abortSignal
2995
3055
  });
2996
3056
  if (!hyperliquidWhales) return false;
2997
3057
  const baseContext = signal.additionalIndicators.baseContext;
@@ -3009,6 +3069,125 @@ var enrichSignalWithHyperliquidWhaleContext = async (params) => {
3009
3069
  return true;
3010
3070
  };
3011
3071
 
3072
+ // src/strategyHelpers/marketContextStages.ts
3073
+ var STAGE_ENV_KEYS = {
3074
+ binance: "BINANCE_MARKET_CONTEXT_STAGE_TIMEOUT_MS",
3075
+ coinmarketcap: "COINMARKETCAP_CONTEXT_STAGE_TIMEOUT_MS",
3076
+ derivatives: "DERIVATIVES_CONTEXT_STAGE_TIMEOUT_MS",
3077
+ hyperliquidWhales: "HYPERLIQUID_WHALE_CONTEXT_STAGE_TIMEOUT_MS"
3078
+ };
3079
+ var parsePositiveInt = (value) => {
3080
+ const parsed = Number.parseInt(String(value ?? ""), 10);
3081
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
3082
+ };
3083
+ var resolveMarketContextStageTimeoutMs = (stage) => parsePositiveInt(process.env[STAGE_ENV_KEYS[stage]]) ?? parsePositiveInt(process.env.MARKET_CONTEXT_STAGE_TIMEOUT_MS) ?? 35e3;
3084
+ var runMarketContextStage = async ({
3085
+ stage,
3086
+ parentSignal,
3087
+ operation,
3088
+ onStart,
3089
+ onComplete
3090
+ }) => {
3091
+ const controller = new AbortController();
3092
+ const timeoutMs = resolveMarketContextStageTimeoutMs(stage);
3093
+ const startedAt = Date.now();
3094
+ const onParentAbort = () => controller.abort(parentSignal?.reason);
3095
+ parentSignal?.addEventListener("abort", onParentAbort, { once: true });
3096
+ if (parentSignal?.aborted) onParentAbort();
3097
+ onStart?.(stage);
3098
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3099
+ timer.unref?.();
3100
+ try {
3101
+ let available = false;
3102
+ try {
3103
+ available = await operation(controller.signal);
3104
+ } catch (error) {
3105
+ if (isMarketContextCancellationError(error)) {
3106
+ controller.abort(error);
3107
+ }
3108
+ if (!controller.signal.aborted) throw error;
3109
+ }
3110
+ const status = controller.signal.aborted ? "timed_out" : available ? "available" : "absent";
3111
+ const result = {
3112
+ stage,
3113
+ status,
3114
+ elapsedMs: Date.now() - startedAt
3115
+ };
3116
+ if (status === "timed_out") {
3117
+ import_logger7.logger.warn(
3118
+ "Market context stage timed out: %s after %sms",
3119
+ stage,
3120
+ result.elapsedMs
3121
+ );
3122
+ }
3123
+ onComplete?.(result);
3124
+ return result;
3125
+ } finally {
3126
+ clearTimeout(timer);
3127
+ parentSignal?.removeEventListener("abort", onParentAbort);
3128
+ }
3129
+ };
3130
+ var enrichSignalWithMarketContextStages = async ({
3131
+ signal,
3132
+ env,
3133
+ coinMarketCapEnabled,
3134
+ includeHyperliquidWhales = true,
3135
+ abortSignal,
3136
+ onStageStart,
3137
+ onStageComplete
3138
+ }) => {
3139
+ const stages = [
3140
+ {
3141
+ stage: "binance",
3142
+ operation: (stageSignal) => enrichSignalWithBinanceMarketContext({
3143
+ signal,
3144
+ env,
3145
+ abortSignal: stageSignal
3146
+ })
3147
+ },
3148
+ {
3149
+ stage: "coinmarketcap",
3150
+ operation: (stageSignal) => enrichSignalWithCoinMarketCapContext({
3151
+ signal,
3152
+ env,
3153
+ enabled: coinMarketCapEnabled,
3154
+ abortSignal: stageSignal
3155
+ })
3156
+ },
3157
+ {
3158
+ stage: "derivatives",
3159
+ operation: (stageSignal) => enrichSignalWithDerivativesContext({
3160
+ signal,
3161
+ env,
3162
+ abortSignal: stageSignal
3163
+ })
3164
+ }
3165
+ ];
3166
+ if (includeHyperliquidWhales) {
3167
+ stages.push({
3168
+ stage: "hyperliquidWhales",
3169
+ operation: (stageSignal) => enrichSignalWithHyperliquidWhaleContext({
3170
+ signal,
3171
+ env,
3172
+ abortSignal: stageSignal
3173
+ })
3174
+ });
3175
+ }
3176
+ const results = [];
3177
+ for (const stage of stages) {
3178
+ if (abortSignal?.aborted) break;
3179
+ results.push(
3180
+ await runMarketContextStage({
3181
+ ...stage,
3182
+ parentSignal: abortSignal,
3183
+ onStart: onStageStart,
3184
+ onComplete: onStageComplete
3185
+ })
3186
+ );
3187
+ }
3188
+ return results;
3189
+ };
3190
+
3012
3191
  // src/strategyAdapters/ml.ts
3013
3192
  var defaultMlAdapter = {
3014
3193
  normalizeStrategyConfig: (strategyConfig) => strategyConfig
@@ -3058,7 +3237,7 @@ var buildMlPayload = (payload) => {
3058
3237
  };
3059
3238
 
3060
3239
  // src/connectorsRegistry.ts
3061
- var import_logger7 = require("@tradejs/infra/logger");
3240
+ var import_logger8 = require("@tradejs/infra/logger");
3062
3241
  var createConnectorRegistryState = () => ({
3063
3242
  connectorCreators: /* @__PURE__ */ new Map(),
3064
3243
  providerToConnectorName: /* @__PURE__ */ new Map(),
@@ -3110,7 +3289,7 @@ var normalizeProviders = (providers, connectorName) => {
3110
3289
  var registerProvider = (provider, connectorName, source, providerToConnectorName) => {
3111
3290
  const existing = providerToConnectorName.get(provider);
3112
3291
  if (existing && existing !== connectorName) {
3113
- import_logger7.logger.warn(
3292
+ import_logger8.logger.warn(
3114
3293
  'Skip duplicate connector provider "%s" from %s: already mapped to %s',
3115
3294
  provider,
3116
3295
  source,
@@ -3123,11 +3302,11 @@ var registerProvider = (provider, connectorName, source, providerToConnectorName
3123
3302
  var registerEntry = (entry, source, state) => {
3124
3303
  const connectorName = String(entry?.name ?? "").trim();
3125
3304
  if (!connectorName) {
3126
- import_logger7.logger.warn("Skip connector entry without name from %s", source);
3305
+ import_logger8.logger.warn("Skip connector entry without name from %s", source);
3127
3306
  return;
3128
3307
  }
3129
3308
  if (typeof entry.creator !== "function") {
3130
- import_logger7.logger.warn(
3309
+ import_logger8.logger.warn(
3131
3310
  'Skip connector entry "%s" from %s: creator must be a function',
3132
3311
  connectorName,
3133
3312
  source
@@ -3139,7 +3318,7 @@ var registerEntry = (entry, source, state) => {
3139
3318
  state.connectorCreators
3140
3319
  );
3141
3320
  if (existingByName) {
3142
- import_logger7.logger.warn(
3321
+ import_logger8.logger.warn(
3143
3322
  'Skip duplicate connector "%s" from %s: already registered as %s',
3144
3323
  connectorName,
3145
3324
  source,
@@ -3211,7 +3390,7 @@ var ensureConnectorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
3211
3390
  );
3212
3391
  const pluginDefinition = extractConnectorPluginDefinition(moduleExport);
3213
3392
  if (!pluginDefinition) {
3214
- import_logger7.logger.warn(
3393
+ import_logger8.logger.warn(
3215
3394
  'Skip connector plugin "%s": export { connectorEntries } is missing',
3216
3395
  moduleName
3217
3396
  );
@@ -3219,7 +3398,7 @@ var ensureConnectorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
3219
3398
  }
3220
3399
  registerEntries2(pluginDefinition.connectorEntries, moduleName, state);
3221
3400
  } catch (error) {
3222
- import_logger7.logger.warn(
3401
+ import_logger8.logger.warn(
3223
3402
  'Failed to load connector plugin "%s": %s',
3224
3403
  moduleName,
3225
3404
  String(error)
@@ -4692,7 +4871,7 @@ var prepareTestingData = async (params) => {
4692
4871
  throw new Error(`Unknown connector: ${connectorName}`);
4693
4872
  }
4694
4873
  if (!binanceConnector || !coinbaseConnector) {
4695
- import_logger8.logger.warn(
4874
+ import_logger9.logger.warn(
4696
4875
  "Binance/Coinbase connectors are unavailable. Reusing %s for BTC references.",
4697
4876
  connectorName
4698
4877
  );
@@ -5257,35 +5436,15 @@ var testing = async ({
5257
5436
  }
5258
5437
  const shouldCapturePayload = signal && typeof signal !== "string" && signal.signalId && (ml || ai);
5259
5438
  if (shouldCapturePayload) {
5260
- await withTimeout(
5261
- "binance market context",
5262
- enrichSignalWithBinanceMarketContext({
5263
- signal,
5264
- env: "BACKTEST"
5265
- })
5266
- );
5267
- await withTimeout(
5268
- "coinmarketcap context",
5269
- enrichSignalWithCoinMarketCapContext({
5270
- signal,
5271
- env: "BACKTEST",
5272
- enabled: Boolean(ml || ai)
5273
- })
5274
- );
5275
- await withTimeout(
5276
- "derivatives context",
5277
- enrichSignalWithDerivativesContext({
5278
- signal,
5279
- env: "BACKTEST"
5280
- })
5281
- );
5282
- await withTimeout(
5283
- "hyperliquid whale context",
5284
- enrichSignalWithHyperliquidWhaleContext({
5285
- signal,
5286
- env: "BACKTEST"
5287
- })
5288
- );
5439
+ await enrichSignalWithMarketContextStages({
5440
+ signal,
5441
+ env: "BACKTEST",
5442
+ coinMarketCapEnabled: true,
5443
+ onStageStart: (stage) => {
5444
+ activeStageStartedAt = Date.now();
5445
+ emitProgress(`${stage} context`, { force: true });
5446
+ }
5447
+ });
5289
5448
  }
5290
5449
  if (ml && signal && typeof signal !== "string" && signal.signalId) {
5291
5450
  const payload = buildMlPayload({
@@ -5689,35 +5848,15 @@ var testingGroupInSharedCandleLoop = async (tests) => {
5689
5848
  }
5690
5849
  const shouldCapturePayload = signal && typeof signal !== "string" && signal.signalId && (test.ml || test.ai);
5691
5850
  if (shouldCapturePayload) {
5692
- await withTimeout(
5693
- "binance market context",
5694
- enrichSignalWithBinanceMarketContext({
5695
- signal,
5696
- env: "BACKTEST"
5697
- })
5698
- );
5699
- await withTimeout(
5700
- "coinmarketcap context",
5701
- enrichSignalWithCoinMarketCapContext({
5702
- signal,
5703
- env: "BACKTEST",
5704
- enabled: Boolean(test.ml || test.ai)
5705
- })
5706
- );
5707
- await withTimeout(
5708
- "derivatives context",
5709
- enrichSignalWithDerivativesContext({
5710
- signal,
5711
- env: "BACKTEST"
5712
- })
5713
- );
5714
- await withTimeout(
5715
- "hyperliquid whale context",
5716
- enrichSignalWithHyperliquidWhaleContext({
5717
- signal,
5718
- env: "BACKTEST"
5719
- })
5720
- );
5851
+ await enrichSignalWithMarketContextStages({
5852
+ signal,
5853
+ env: "BACKTEST",
5854
+ coinMarketCapEnabled: true,
5855
+ onStageStart: (stage) => {
5856
+ activeStageStartedAt = Date.now();
5857
+ emitProgress(`${stage} context`, { force: true });
5858
+ }
5859
+ });
5721
5860
  }
5722
5861
  if (test.ml && signal && typeof signal !== "string" && signal.signalId) {
5723
5862
  const payload = buildMlPayload({