@tradejs/node 1.0.6 → 1.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.
package/dist/ai.mjs CHANGED
@@ -13,9 +13,9 @@ import {
13
13
  runAiPrompt,
14
14
  runAiPromptLocal,
15
15
  trimSeriesDeep
16
- } from "./chunk-72FKXJ2I.mjs";
17
- import "./chunk-EOZSJKUM.mjs";
18
- import "./chunk-CGJ2UU6H.mjs";
16
+ } from "./chunk-2JKX3DM7.mjs";
17
+ import "./chunk-WGOYR6AB.mjs";
18
+ import "./chunk-JU77QVJ3.mjs";
19
19
  import "./chunk-6DZX6EAA.mjs";
20
20
  export {
21
21
  DEFAULT_AI_MODEL,
package/dist/backtest.js CHANGED
@@ -39,14 +39,14 @@ module.exports = __toCommonJS(backtest_exports);
39
39
  __reExport(backtest_exports, require("@tradejs/core/backtest"), module.exports);
40
40
 
41
41
  // src/testing.ts
42
- var import_indicators2 = require("@tradejs/core/indicators");
43
- var import_constants = require("@tradejs/core/constants");
42
+ var import_indicators3 = require("@tradejs/core/indicators");
44
43
  var import_time = require("@tradejs/core/time");
45
44
  var import_ai2 = require("@tradejs/infra/ai");
46
45
  var import_ml2 = require("@tradejs/infra/ml");
47
- var import_logger4 = require("@tradejs/infra/logger");
46
+ var import_logger5 = require("@tradejs/infra/logger");
48
47
 
49
48
  // src/ai.ts
49
+ var import_aiLanguages = require("@tradejs/infra/aiLanguages");
50
50
  var import_redis = require("@tradejs/infra/redis");
51
51
  var import_userSettings = require("@tradejs/infra/userSettings");
52
52
 
@@ -74,6 +74,109 @@ var trimSeriesDeep = (value) => {
74
74
  return value;
75
75
  };
76
76
 
77
+ // src/aiMarketContext.ts
78
+ var SESSION_WINDOWS = [
79
+ { name: "asia", startMinuteUtc: 0, endMinuteUtc: 8 * 60 },
80
+ { name: "europe", startMinuteUtc: 7 * 60, endMinuteUtc: 16 * 60 },
81
+ { name: "us", startMinuteUtc: 13 * 60, endMinuteUtc: 22 * 60 }
82
+ ];
83
+ var toRecord = (value) => {
84
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
85
+ return null;
86
+ }
87
+ return value;
88
+ };
89
+ var toFiniteNumber = (value) => {
90
+ const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
91
+ return Number.isFinite(numeric) ? numeric : null;
92
+ };
93
+ var getLastFiniteNumber = (value) => {
94
+ const numeric = toFiniteNumber(value);
95
+ if (numeric != null) {
96
+ return numeric;
97
+ }
98
+ if (!Array.isArray(value)) {
99
+ return null;
100
+ }
101
+ for (let i = value.length - 1; i >= 0; i -= 1) {
102
+ const nested = getLastFiniteNumber(value[i]);
103
+ if (nested != null) {
104
+ return nested;
105
+ }
106
+ }
107
+ return null;
108
+ };
109
+ var roundTo = (value, decimals) => {
110
+ const factor = 10 ** decimals;
111
+ return Math.round(value * factor) / factor;
112
+ };
113
+ var isInsideSession = (minuteUtc, startMinuteUtc, endMinuteUtc) => startMinuteUtc <= endMinuteUtc ? minuteUtc >= startMinuteUtc && minuteUtc < endMinuteUtc : minuteUtc >= startMinuteUtc || minuteUtc < endMinuteUtc;
114
+ var buildTradingSessionContext = (timestamp) => {
115
+ const date = new Date(timestamp);
116
+ const utcHour = date.getUTCHours();
117
+ const utcMinute = date.getUTCMinutes();
118
+ const minuteUtc = utcHour * 60 + utcMinute;
119
+ const activeSessions = SESSION_WINDOWS.filter(
120
+ (session) => isInsideSession(minuteUtc, session.startMinuteUtc, session.endMinuteUtc)
121
+ ).map((session) => session.name);
122
+ const primarySession = activeSessions.includes("us") ? "us" : activeSessions.includes("europe") ? "europe" : activeSessions.includes("asia") ? "asia" : "off_hours";
123
+ return {
124
+ timezone: "UTC",
125
+ utcHour,
126
+ utcMinute,
127
+ primarySession,
128
+ activeSessions,
129
+ isOverlap: activeSessions.length > 1,
130
+ overlap: activeSessions.length > 1 ? `${activeSessions.join("_")}_overlap` : null
131
+ };
132
+ };
133
+ var buildMissingSpreadContext = () => ({
134
+ source: "binance_coinbase_btc",
135
+ indicatorKey: "payload.indicators.spread",
136
+ available: false,
137
+ value: null,
138
+ bps: null,
139
+ absBps: null,
140
+ bias: null,
141
+ severity: null
142
+ });
143
+ var buildSpreadContextFromValue = (spread) => {
144
+ const value = roundTo(spread, 8);
145
+ const bps = roundTo(value * 1e4, 2);
146
+ const absBps = Math.abs(bps);
147
+ const bias = Math.abs(bps) < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
148
+ const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
149
+ return {
150
+ source: "binance_coinbase_btc",
151
+ indicatorKey: "payload.indicators.spread",
152
+ available: true,
153
+ value,
154
+ bps,
155
+ absBps,
156
+ bias,
157
+ severity
158
+ };
159
+ };
160
+ var readSpreadFromSignal = (signal) => {
161
+ const indicatorSpread = getLastFiniteNumber(signal.indicators?.spread);
162
+ if (indicatorSpread != null) {
163
+ return indicatorSpread;
164
+ }
165
+ return getLastFiniteNumber(signal.additionalIndicators?.spread);
166
+ };
167
+ var buildAiMarketContext = (signal) => {
168
+ const existingMarketContext = toRecord(
169
+ signal.additionalIndicators?.marketContext
170
+ );
171
+ const existingSpread = toRecord(existingMarketContext?.binanceCoinbaseSpread);
172
+ const spread = readSpreadFromSignal(signal);
173
+ return {
174
+ ...existingMarketContext ?? {},
175
+ tradingSession: buildTradingSessionContext(signal.timestamp),
176
+ binanceCoinbaseSpread: spread != null ? buildSpreadContextFromValue(spread) : existingSpread ?? buildMissingSpreadContext()
177
+ };
178
+ };
179
+
77
180
  // src/strategy/manifests.ts
78
181
  var import_indicators = require("@tradejs/core/indicators");
79
182
  var import_logger2 = require("@tradejs/infra/logger");
@@ -83,6 +186,7 @@ var import_fs = __toESM(require("fs"));
83
186
  var import_path = __toESM(require("path"));
84
187
  var import_module = require("module");
85
188
  var import_url = require("url");
189
+ var import_config = require("@tradejs/core/config");
86
190
  var import_logger = require("@tradejs/infra/logger");
87
191
  var CONFIG_FILE_NAMES = [
88
192
  "tradejs.config.ts",
@@ -115,10 +219,14 @@ var normalizeConfig = (rawConfig) => {
115
219
  const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
116
220
  const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
117
221
  const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
222
+ const hooks = (0, import_config.normalizeTradejsConfigHooks)(
223
+ config.hooks
224
+ );
118
225
  return {
119
226
  strategies: strategies2,
120
227
  indicators,
121
- connectors
228
+ connectors,
229
+ ...hooks ? { hooks } : {}
122
230
  };
123
231
  };
124
232
  var getRequireFn = (cwd = getTradejsProjectCwd()) => (0, import_module.createRequire)(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
@@ -493,24 +601,36 @@ var strategies = new Proxy(
493
601
  );
494
602
 
495
603
  // src/strategyAdapters/ai.ts
496
- var buildBaseAiPayload = (signal) => ({
497
- signal: {
498
- symbol: signal.symbol,
499
- signalId: signal.signalId,
500
- interval: signal.interval,
501
- direction: signal.direction,
502
- timestamp: signal.timestamp,
503
- strategy: signal.strategy,
504
- prices: {
505
- currentPrice: signal.prices.currentPrice,
506
- takeProfitPrice: signal.prices.takeProfitPrice,
507
- stopLossPrice: signal.prices.stopLossPrice
508
- }
509
- },
510
- figures: trimSeriesDeep(signal.figures ?? {}),
511
- indicators: trimSeriesDeep(signal.indicators),
512
- additionalIndicators: trimSeriesDeep(signal.additionalIndicators ?? {})
513
- });
604
+ var toRecord2 = (value) => {
605
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
606
+ return {};
607
+ }
608
+ return value;
609
+ };
610
+ var buildBaseAiPayload = (signal) => {
611
+ const additionalIndicators = {
612
+ ...toRecord2(signal.additionalIndicators),
613
+ marketContext: buildAiMarketContext(signal)
614
+ };
615
+ return {
616
+ signal: {
617
+ symbol: signal.symbol,
618
+ signalId: signal.signalId,
619
+ interval: signal.interval,
620
+ direction: signal.direction,
621
+ timestamp: signal.timestamp,
622
+ strategy: signal.strategy,
623
+ prices: {
624
+ currentPrice: signal.prices.currentPrice,
625
+ takeProfitPrice: signal.prices.takeProfitPrice,
626
+ stopLossPrice: signal.prices.stopLossPrice
627
+ }
628
+ },
629
+ figures: trimSeriesDeep(signal.figures ?? {}),
630
+ indicators: trimSeriesDeep(signal.indicators),
631
+ additionalIndicators: trimSeriesDeep(additionalIndicators)
632
+ };
633
+ };
514
634
  var defaultAiAdapter = {};
515
635
  var getStrategyAiAdapter = (strategy) => getStrategyManifest(strategy)?.aiAdapter ?? defaultAiAdapter;
516
636
  var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy);
@@ -523,6 +643,107 @@ var buildAiPayloadByStrategy = (signal) => {
523
643
  // src/ai.ts
524
644
  var buildAiPayload = (signal) => buildAiPayloadByStrategy(signal);
525
645
 
646
+ // src/strategyHelpers/derivativesContext.ts
647
+ var import_indicators2 = require("@tradejs/core/indicators");
648
+ var import_constants = require("@tradejs/core/constants");
649
+ var import_timescale = require("@tradejs/infra/timescale");
650
+ var import_logger3 = require("@tradejs/infra/logger");
651
+ var DEFAULT_INTERVALS = ["15m", "1h"];
652
+ var DEFAULT_LOOKBACK_HOURS = 48;
653
+ var derivativesContextUnavailable = false;
654
+ var parseEnabledFlag = (value, env) => {
655
+ const normalized = String(value ?? "").trim().toLowerCase();
656
+ if (!normalized) return false;
657
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
658
+ if (normalized === "backtest") return env === "BACKTEST";
659
+ if (normalized === "live") return env !== "BACKTEST";
660
+ return false;
661
+ };
662
+ var parseLookbackMs = () => {
663
+ const hours = Number(process.env.DERIVATIVES_CONTEXT_LOOKBACK_HOURS);
664
+ const normalizedHours = Number.isFinite(hours) && hours > 0 ? hours : DEFAULT_LOOKBACK_HOURS;
665
+ return normalizedHours * 60 * 60 * 1e3;
666
+ };
667
+ var parseIntervals = () => {
668
+ const fromEnv = (0, import_indicators2.normalizeDerivativesIntervals)(
669
+ process.env.DERIVATIVES_CONTEXT_INTERVALS
670
+ );
671
+ return fromEnv.length ? fromEnv : DEFAULT_INTERVALS;
672
+ };
673
+ var getDerivativesContextReferenceSymbols = () => [
674
+ ...import_constants.DERIVATIVES_CONTEXT_REFERENCE_SYMBOLS
675
+ ];
676
+ var normalizeSymbol = (symbol) => String(symbol || "").trim().toUpperCase();
677
+ var resolvePrimaryReferenceSymbol = (signalSymbol) => {
678
+ const symbol = normalizeSymbol(signalSymbol);
679
+ const referenceSymbols = getDerivativesContextReferenceSymbols();
680
+ return referenceSymbols.some((referenceSymbol) => referenceSymbol === symbol) ? symbol : referenceSymbols[0];
681
+ };
682
+ var buildReferenceDerivativesContext = (params) => {
683
+ const { targetSymbol, primaryReferenceSymbol, referenceContexts } = params;
684
+ const primaryContext = referenceContexts[primaryReferenceSymbol] ?? referenceContexts[getDerivativesContextReferenceSymbols()[0]];
685
+ if (!primaryContext) {
686
+ throw new Error("No derivatives reference contexts built");
687
+ }
688
+ return {
689
+ ...primaryContext,
690
+ targetSymbol,
691
+ primaryReferenceSymbol: primaryContext.symbol,
692
+ referenceSymbols: getDerivativesContextReferenceSymbols(),
693
+ referenceContexts
694
+ };
695
+ };
696
+ var isDerivativesContextEnabled = (env) => parseEnabledFlag(process.env.DERIVATIVES_CONTEXT_ENABLED, env);
697
+ var enrichSignalWithDerivativesContext = async (params) => {
698
+ const { signal, env, enabled = isDerivativesContextEnabled(env) } = params;
699
+ if (!enabled || derivativesContextUnavailable) {
700
+ return false;
701
+ }
702
+ try {
703
+ const intervals = parseIntervals();
704
+ const referenceSymbols = getDerivativesContextReferenceSymbols();
705
+ const lookbackMs = parseLookbackMs();
706
+ const contexts = await Promise.all(
707
+ referenceSymbols.map(async (symbol) => {
708
+ const rowsByInterval = await (0, import_timescale.getDerivativesWindow)({
709
+ symbol,
710
+ intervals,
711
+ endMs: signal.timestamp,
712
+ lookbackMs
713
+ });
714
+ return [
715
+ symbol,
716
+ (0, import_indicators2.buildDerivativesContext)({
717
+ symbol,
718
+ direction: signal.direction,
719
+ timestamp: signal.timestamp,
720
+ rowsByInterval,
721
+ intervals
722
+ })
723
+ ];
724
+ })
725
+ );
726
+ const referenceContexts = Object.fromEntries(contexts);
727
+ const derivativesContext = buildReferenceDerivativesContext({
728
+ targetSymbol: signal.symbol,
729
+ primaryReferenceSymbol: resolvePrimaryReferenceSymbol(signal.symbol),
730
+ referenceContexts
731
+ });
732
+ signal.additionalIndicators = {
733
+ ...signal.additionalIndicators ?? {},
734
+ derivativesContext
735
+ };
736
+ return true;
737
+ } catch (error) {
738
+ derivativesContextUnavailable = true;
739
+ import_logger3.logger.warn(
740
+ "Derivatives context disabled after Timescale read failure: %s",
741
+ String(error)
742
+ );
743
+ return false;
744
+ }
745
+ };
746
+
526
747
  // src/strategyAdapters/ml.ts
527
748
  var defaultMlAdapter = {
528
749
  normalizeStrategyConfig: (strategyConfig) => strategyConfig
@@ -566,7 +787,7 @@ var buildMlPayload = (payload) => {
566
787
  };
567
788
 
568
789
  // src/connectorsRegistry.ts
569
- var import_logger3 = require("@tradejs/infra/logger");
790
+ var import_logger4 = require("@tradejs/infra/logger");
570
791
  var createConnectorRegistryState = () => ({
571
792
  connectorCreators: /* @__PURE__ */ new Map(),
572
793
  providerToConnectorName: /* @__PURE__ */ new Map(),
@@ -618,7 +839,7 @@ var normalizeProviders = (providers, connectorName) => {
618
839
  var registerProvider = (provider, connectorName, source, providerToConnectorName) => {
619
840
  const existing = providerToConnectorName.get(provider);
620
841
  if (existing && existing !== connectorName) {
621
- import_logger3.logger.warn(
842
+ import_logger4.logger.warn(
622
843
  'Skip duplicate connector provider "%s" from %s: already mapped to %s',
623
844
  provider,
624
845
  source,
@@ -631,11 +852,11 @@ var registerProvider = (provider, connectorName, source, providerToConnectorName
631
852
  var registerEntry = (entry, source, state) => {
632
853
  const connectorName = String(entry?.name ?? "").trim();
633
854
  if (!connectorName) {
634
- import_logger3.logger.warn("Skip connector entry without name from %s", source);
855
+ import_logger4.logger.warn("Skip connector entry without name from %s", source);
635
856
  return;
636
857
  }
637
858
  if (typeof entry.creator !== "function") {
638
- import_logger3.logger.warn(
859
+ import_logger4.logger.warn(
639
860
  'Skip connector entry "%s" from %s: creator must be a function',
640
861
  connectorName,
641
862
  source
@@ -647,7 +868,7 @@ var registerEntry = (entry, source, state) => {
647
868
  state.connectorCreators
648
869
  );
649
870
  if (existingByName) {
650
- import_logger3.logger.warn(
871
+ import_logger4.logger.warn(
651
872
  'Skip duplicate connector "%s" from %s: already registered as %s',
652
873
  connectorName,
653
874
  source,
@@ -719,7 +940,7 @@ var ensureConnectorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
719
940
  );
720
941
  const pluginDefinition = extractConnectorPluginDefinition(moduleExport);
721
942
  if (!pluginDefinition) {
722
- import_logger3.logger.warn(
943
+ import_logger4.logger.warn(
723
944
  'Skip connector plugin "%s": export { connectorEntries } is missing',
724
945
  moduleName
725
946
  );
@@ -727,7 +948,7 @@ var ensureConnectorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
727
948
  }
728
949
  registerEntries2(pluginDefinition.connectorEntries, moduleName, state);
729
950
  } catch (error) {
730
- import_logger3.logger.warn(
951
+ import_logger4.logger.warn(
731
952
  'Failed to load connector plugin "%s": %s',
732
953
  moduleName,
733
954
  String(error)
@@ -818,6 +1039,7 @@ var createTestConnector = (connector, context) => {
818
1039
  currentPositionProfit = 0;
819
1040
  };
820
1041
  return {
1042
+ __tradejsTestConnector: true,
821
1043
  getState: async () => state,
822
1044
  setState: async (newState) => {
823
1045
  state = {
@@ -840,6 +1062,24 @@ var createTestConnector = (connector, context) => {
840
1062
  };
841
1063
  },
842
1064
  getPosition: async () => currentPosition || null,
1065
+ getOpenPositionPnl: async () => {
1066
+ if (typeof connector.getOpenPositionPnl === "function") {
1067
+ return connector.getOpenPositionPnl();
1068
+ }
1069
+ if (!currentPosition) {
1070
+ return [];
1071
+ }
1072
+ return [
1073
+ {
1074
+ symbol: currentPosition.symbol,
1075
+ qty: currentPosition.qty,
1076
+ price: currentPosition.price,
1077
+ currentPrice: currentPosition.price,
1078
+ unrealizedPnl: 0,
1079
+ direction: currentPosition.direction
1080
+ }
1081
+ ];
1082
+ },
843
1083
  checkTp: async (candle) => {
844
1084
  if (!candle || !currentPosition || !currentPosition.qty) {
845
1085
  return;
@@ -957,7 +1197,6 @@ var createTestConnector = (connector, context) => {
957
1197
  };
958
1198
 
959
1199
  // src/testing.ts
960
- var preloadStart = (0, import_time.getTimestamp)(import_constants.PRELOAD_DAYS);
961
1200
  var createTestingKlineCacheState = () => ({
962
1201
  coinKlineCache: /* @__PURE__ */ new Map(),
963
1202
  btcKlineCache: /* @__PURE__ */ new Map(),
@@ -980,7 +1219,15 @@ var getTestingKlineCacheState = (cwd = getTradejsProjectCwd()) => {
980
1219
  };
981
1220
  };
982
1221
  var getKlineCacheKey = (params) => {
983
- const { userName, connectorName, symbol, end, interval, cacheOnly } = params;
1222
+ const {
1223
+ userName,
1224
+ connectorName,
1225
+ symbol,
1226
+ preloadStart,
1227
+ end,
1228
+ interval,
1229
+ cacheOnly
1230
+ } = params;
984
1231
  return [
985
1232
  userName,
986
1233
  connectorName,
@@ -996,6 +1243,7 @@ var getPreparedDataCacheKey = (params) => {
996
1243
  userName,
997
1244
  connectorName,
998
1245
  symbol,
1246
+ preloadStart,
999
1247
  start,
1000
1248
  end,
1001
1249
  interval,
@@ -1006,6 +1254,7 @@ var getPreparedDataCacheKey = (params) => {
1006
1254
  userName,
1007
1255
  connectorName,
1008
1256
  symbol,
1257
+ preloadStart,
1009
1258
  start,
1010
1259
  end,
1011
1260
  interval,
@@ -1014,7 +1263,7 @@ var getPreparedDataCacheKey = (params) => {
1014
1263
  ].join(":");
1015
1264
  };
1016
1265
  var getConnectorCacheKey = (params) => [params.userName, params.connectorName].join(":");
1017
- var splitCandlesForTesting = (candles, start) => {
1266
+ var splitCandlesForTesting = (candles, start, preloadStart) => {
1018
1267
  const prevData = [];
1019
1268
  const testData = [];
1020
1269
  for (const candle of candles) {
@@ -1075,6 +1324,7 @@ var testing = async ({
1075
1324
  if (!start) {
1076
1325
  throw new Error("no start");
1077
1326
  }
1327
+ const preloadStart = (0, import_time.getBacktestPreloadStart)(start);
1078
1328
  const startedAt = Date.now();
1079
1329
  const formatTimeoutMessage = (stage) => `Test ${name} (${symbol}) timed out after ${timeoutMs}ms during ${stage}`;
1080
1330
  const getRemainingTimeoutMs = (stage) => {
@@ -1150,7 +1400,7 @@ var testing = async ({
1150
1400
  })
1151
1401
  );
1152
1402
  if (!binanceConnector || !coinbaseConnector) {
1153
- import_logger4.logger.warn(
1403
+ import_logger5.logger.warn(
1154
1404
  "Binance/Coinbase connectors are unavailable. Reusing %s for BTC references.",
1155
1405
  connectorName
1156
1406
  );
@@ -1161,6 +1411,7 @@ var testing = async ({
1161
1411
  userName,
1162
1412
  connectorName,
1163
1413
  symbol,
1414
+ preloadStart,
1164
1415
  end,
1165
1416
  interval,
1166
1417
  cacheOnly
@@ -1169,6 +1420,7 @@ var testing = async ({
1169
1420
  userName,
1170
1421
  connectorName,
1171
1422
  symbol: "BTCUSDT",
1423
+ preloadStart,
1172
1424
  end,
1173
1425
  interval,
1174
1426
  cacheOnly
@@ -1181,6 +1433,7 @@ var testing = async ({
1181
1433
  userName,
1182
1434
  connectorName: btcBinanceConnectorName,
1183
1435
  symbol: "BTCUSDT",
1436
+ preloadStart,
1184
1437
  end,
1185
1438
  interval,
1186
1439
  cacheOnly
@@ -1189,6 +1442,7 @@ var testing = async ({
1189
1442
  userName,
1190
1443
  connectorName: btcCoinbaseConnectorName,
1191
1444
  symbol: "BTCUSDT",
1445
+ preloadStart,
1192
1446
  end,
1193
1447
  interval,
1194
1448
  cacheOnly
@@ -1199,6 +1453,7 @@ var testing = async ({
1199
1453
  userName,
1200
1454
  connectorName,
1201
1455
  symbol,
1456
+ preloadStart,
1202
1457
  start,
1203
1458
  end,
1204
1459
  interval,
@@ -1256,20 +1511,22 @@ var testing = async ({
1256
1511
  if (!cachedBtcCoinbaseData) {
1257
1512
  state.btcCoinbaseKlineCache.set(btcCoinbaseCacheKey, btcCoinbaseData);
1258
1513
  }
1259
- const { prevData: prevDataRaw, testData: testDataRaw } = splitCandlesForTesting(data, start);
1260
- const { prevData: btcPrevDataRaw, testData: btcTestDataRaw } = splitCandlesForTesting(btcData, start);
1514
+ const { prevData: prevDataRaw, testData: testDataRaw } = splitCandlesForTesting(data, start, preloadStart);
1515
+ const { prevData: btcPrevDataRaw, testData: btcTestDataRaw } = splitCandlesForTesting(btcData, start, preloadStart);
1261
1516
  const { prevData: btcBinancePrevDataRaw } = splitCandlesForTesting(
1262
1517
  btcBinanceData,
1263
- start
1518
+ start,
1519
+ preloadStart
1264
1520
  );
1265
1521
  const { prevData: btcCoinbasePrevDataRaw } = splitCandlesForTesting(
1266
1522
  btcCoinbaseData,
1267
- start
1523
+ start,
1524
+ preloadStart
1268
1525
  );
1269
- const { alignedCoinCandles: prevData2, alignedBtcCandles: btcPrevData2 } = (0, import_indicators2.alignSortedCandlesByTimestamp)(prevDataRaw, btcPrevDataRaw);
1270
- const { alignedCoinCandles: testData2, alignedBtcCandles: btcTestData2 } = (0, import_indicators2.alignSortedCandlesByTimestamp)(testDataRaw, btcTestDataRaw);
1271
- const { alignedBtcCandles: btcBinancePrevData2 } = (0, import_indicators2.alignSortedCandlesByTimestamp)(prevDataRaw, btcBinancePrevDataRaw);
1272
- const { alignedBtcCandles: btcCoinbasePrevData2 } = (0, import_indicators2.alignSortedCandlesByTimestamp)(prevDataRaw, btcCoinbasePrevDataRaw);
1526
+ const { alignedCoinCandles: prevData2, alignedBtcCandles: btcPrevData2 } = (0, import_indicators3.alignSortedCandlesByTimestamp)(prevDataRaw, btcPrevDataRaw);
1527
+ const { alignedCoinCandles: testData2, alignedBtcCandles: btcTestData2 } = (0, import_indicators3.alignSortedCandlesByTimestamp)(testDataRaw, btcTestDataRaw);
1528
+ const { alignedBtcCandles: btcBinancePrevData2 } = (0, import_indicators3.alignSortedCandlesByTimestamp)(prevDataRaw, btcBinancePrevDataRaw);
1529
+ const { alignedBtcCandles: btcCoinbasePrevData2 } = (0, import_indicators3.alignSortedCandlesByTimestamp)(prevDataRaw, btcCoinbasePrevDataRaw);
1273
1530
  preparedData = {
1274
1531
  prevData: prevData2,
1275
1532
  btcPrevData: btcPrevData2,
@@ -1311,6 +1568,7 @@ var testing = async ({
1311
1568
  );
1312
1569
  const pendingMlPayloadBySignalId = /* @__PURE__ */ new Map();
1313
1570
  const pendingAiRowBySignalId = /* @__PURE__ */ new Map();
1571
+ const replaySignalEvaluations = [];
1314
1572
  const flushClosedResultsBatch = async () => {
1315
1573
  if (!ml && !ai) return;
1316
1574
  const batch = await testConnector.drainMlResultsBatch();
@@ -1355,6 +1613,47 @@ var testing = async ({
1355
1613
  "strategy signal",
1356
1614
  strategy(candle, btcCandle)
1357
1615
  );
1616
+ if (!signal || typeof signal === "string") {
1617
+ replaySignalEvaluations.push({
1618
+ evaluationId: `${testId}:${strategyName}:${symbol}:${candle.timestamp}`,
1619
+ userName,
1620
+ strategy: strategyName,
1621
+ symbol,
1622
+ interval,
1623
+ timestamp: candle.timestamp,
1624
+ evaluatedAt: candle.timestamp,
1625
+ status: "skip",
1626
+ reason: typeof signal === "string" && signal.trim() ? signal : "NO_SIGNAL"
1627
+ });
1628
+ } else {
1629
+ replaySignalEvaluations.push({
1630
+ evaluationId: `${signal.signalId || testId}:${strategyName}:${symbol}:${signal.timestamp || candle.timestamp}`,
1631
+ userName,
1632
+ strategy: signal.strategy || strategyName,
1633
+ symbol: signal.symbol || symbol,
1634
+ interval: signal.interval || interval,
1635
+ timestamp: typeof signal.timestamp === "number" && Number.isFinite(signal.timestamp) ? signal.timestamp : candle.timestamp,
1636
+ evaluatedAt: candle.timestamp,
1637
+ status: "signal",
1638
+ reason: signal.orderSkipReason || signal.orderStatus,
1639
+ signalId: signal.signalId,
1640
+ direction: signal.direction,
1641
+ orderStatus: signal.orderStatus,
1642
+ orderSkipReason: signal.orderSkipReason,
1643
+ aiAnalysis: signal.aiAnalysis ?? null,
1644
+ ml: signal.ml
1645
+ });
1646
+ }
1647
+ const shouldCapturePayload = signal && typeof signal !== "string" && signal.signalId && (ml || ai);
1648
+ if (shouldCapturePayload) {
1649
+ await withTimeout(
1650
+ "derivatives context",
1651
+ enrichSignalWithDerivativesContext({
1652
+ signal,
1653
+ env: "BACKTEST"
1654
+ })
1655
+ );
1656
+ }
1358
1657
  if (ml && signal && typeof signal !== "string" && signal.signalId) {
1359
1658
  const payload = buildMlPayload({
1360
1659
  signal,
@@ -1387,7 +1686,11 @@ var testing = async ({
1387
1686
  }
1388
1687
  }
1389
1688
  await withTimeout("flush closed results", flushClosedResultsBatch());
1390
- return await withTimeout("collect result", testConnector.getResult());
1689
+ const result = await withTimeout("collect result", testConnector.getResult());
1690
+ return {
1691
+ ...result,
1692
+ inlineReplaySignalEvaluations: replaySignalEvaluations
1693
+ };
1391
1694
  };
1392
1695
  // Annotate the CommonJS export names for ESM import in node:
1393
1696
  0 && (module.exports = {