@tradejs/node 2.0.19 → 2.0.21

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/backtest.js CHANGED
@@ -6513,6 +6513,7 @@ var createBacktestProgress = ({
6513
6513
  // src/backtest/session.ts
6514
6514
  var import_types2 = require("@tradejs/types");
6515
6515
  var import_ai3 = require("@tradejs/infra/ai");
6516
+ var import_coreResearch = require("@tradejs/infra/coreResearch");
6516
6517
  var import_ml3 = require("@tradejs/infra/ml");
6517
6518
 
6518
6519
  // src/executionCosts.ts
@@ -7611,6 +7612,37 @@ var createTestConnector = (connector, context) => {
7611
7612
  };
7612
7613
  };
7613
7614
 
7615
+ // src/backtest/researchTrace.ts
7616
+ var asRecord2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
7617
+ var findSetupKey = (value, depth = 0) => {
7618
+ if (depth > 5) return null;
7619
+ const record = asRecord2(value);
7620
+ if (!record) return null;
7621
+ for (const key of ["setupIdentity", "setupId", "patternId", "candidateId"]) {
7622
+ const candidate = record[key];
7623
+ if (typeof candidate === "string" && candidate.trim() || typeof candidate === "number" && Number.isFinite(candidate)) {
7624
+ return `${key}:${String(candidate)}`;
7625
+ }
7626
+ }
7627
+ for (const [key, nested] of Object.entries(record)) {
7628
+ if (/context|signal|setup|pattern|candidate|strategy/i.test(key) && nested != null) {
7629
+ const found = findSetupKey(nested, depth + 1);
7630
+ if (found) return found;
7631
+ }
7632
+ }
7633
+ return null;
7634
+ };
7635
+ var resolveCoreResearchSetupIdentity = (signal) => {
7636
+ const strategyKey = findSetupKey(signal.additionalIndicators);
7637
+ return strategyKey ? {
7638
+ setupIdentity: `${signal.strategy}|${signal.symbol}|${signal.direction}|${strategyKey}`,
7639
+ setupIdentitySource: "strategy-context"
7640
+ } : {
7641
+ setupIdentity: `${signal.strategy}|${signal.symbol}|${signal.direction}|${signal.timestamp}`,
7642
+ setupIdentitySource: "signal-time-fallback"
7643
+ };
7644
+ };
7645
+
7614
7646
  // src/backtest/session.ts
7615
7647
  var createWarningCounts = () => ({
7616
7648
  [import_types2.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY]: 0
@@ -7700,7 +7732,7 @@ var createBacktestSession = async ({
7700
7732
  const testConnector = createTestConnector(connector, {
7701
7733
  userName: test.userName,
7702
7734
  mlEnabled: test.ml,
7703
- aiEnabled: test.ai,
7735
+ aiEnabled: Boolean(test.ai || test.researchTrace),
7704
7736
  fastMode: test.fast,
7705
7737
  instrument,
7706
7738
  executionCostModel,
@@ -7737,11 +7769,18 @@ var createBacktestSession = async ({
7737
7769
  );
7738
7770
  const pendingMlPayloadBySignalId = /* @__PURE__ */ new Map();
7739
7771
  const pendingAiRowBySignalId = /* @__PURE__ */ new Map();
7772
+ const pendingResearchSetupBySignalId = /* @__PURE__ */ new Map();
7773
+ const researchSkipCounts = /* @__PURE__ */ new Map();
7774
+ const researchEventCounts = /* @__PURE__ */ new Map();
7740
7775
  const warningCounts = createWarningCounts();
7741
7776
  const replayEvaluations = test.collectReplaySignalEvaluations ? [] : null;
7742
7777
  const chunkId = test.chunkId ?? "single";
7743
7778
  const processSignal = async (signal, candle) => {
7744
7779
  recordSignalWarning(warningCounts, signal);
7780
+ if (test.researchTrace && typeof signal === "string") {
7781
+ const reason = signal.trim() || "NO_SIGNAL";
7782
+ researchSkipCounts.set(reason, (researchSkipCounts.get(reason) ?? 0) + 1);
7783
+ }
7745
7784
  if (typeof signal === "string" && signal.startsWith("BACKTEST_ENTRY_DELAY_")) {
7746
7785
  return;
7747
7786
  }
@@ -7758,6 +7797,32 @@ var createBacktestSession = async ({
7758
7797
  onStageStart: monitor.contextStage
7759
7798
  });
7760
7799
  }
7800
+ if (test.researchTrace && signal && typeof signal !== "string" && signal.signalId) {
7801
+ const identity = resolveCoreResearchSetupIdentity(signal);
7802
+ pendingResearchSetupBySignalId.set(signal.signalId, identity);
7803
+ await (0, import_coreResearch.appendCoreResearchTraceEvent)({
7804
+ strategyName: test.strategyName,
7805
+ chunkId,
7806
+ event: {
7807
+ schema: "tradejs-core-research-trace/v1",
7808
+ event: signal.orderStatus === "failed" || signal.orderStatus === "skipped" ? "entry_rejected" : "signal_emitted",
7809
+ timestamp: signal.timestamp,
7810
+ strategy: signal.strategy || test.strategyName,
7811
+ symbol: signal.symbol || test.symbol,
7812
+ direction: signal.direction,
7813
+ signalId: signal.signalId,
7814
+ configId: test.configId,
7815
+ backtestRunId: test.backtestRunId,
7816
+ backtestTestKey: test.backtestTestKey,
7817
+ ...identity
7818
+ }
7819
+ });
7820
+ const traceEvent = signal.orderStatus === "failed" || signal.orderStatus === "skipped" ? "entry_rejected" : "signal_emitted";
7821
+ researchEventCounts.set(
7822
+ traceEvent,
7823
+ (researchEventCounts.get(traceEvent) ?? 0) + 1
7824
+ );
7825
+ }
7761
7826
  if (test.ml && signal && typeof signal !== "string" && signal.signalId) {
7762
7827
  pendingMlPayloadBySignalId.set(
7763
7828
  signal.signalId,
@@ -7795,7 +7860,7 @@ var createBacktestSession = async ({
7795
7860
  }
7796
7861
  };
7797
7862
  const flush = async () => {
7798
- if (!test.ml && !test.ai) return;
7863
+ if (!test.ml && !test.ai && !test.researchTrace) return;
7799
7864
  const batch = await testConnector.drainMlResultsBatch();
7800
7865
  for (const resultRecord of batch) {
7801
7866
  const payload = pendingMlPayloadBySignalId.get(resultRecord.signalId);
@@ -7824,9 +7889,63 @@ var createBacktestSession = async ({
7824
7889
  ...rowBase,
7825
7890
  payload: buildAiPayload(aiSignal),
7826
7891
  profit: resultRecord.profit,
7827
- tradeResult: resultRecord.tradeResult
7892
+ tradeResult: resultRecord.tradeResult,
7893
+ research: {
7894
+ schema: "tradejs-core-research-row/v1",
7895
+ ...resolveCoreResearchSetupIdentity(aiSignal)
7896
+ }
7897
+ }
7898
+ });
7899
+ }
7900
+ const researchSetup = pendingResearchSetupBySignalId.get(
7901
+ resultRecord.signalId
7902
+ );
7903
+ if (test.researchTrace && researchSetup && resultRecord.tradeResult) {
7904
+ pendingResearchSetupBySignalId.delete(resultRecord.signalId);
7905
+ await (0, import_coreResearch.appendCoreResearchTraceEvent)({
7906
+ strategyName: test.strategyName,
7907
+ chunkId,
7908
+ event: {
7909
+ schema: "tradejs-core-research-trace/v1",
7910
+ event: "entry_executed",
7911
+ timestamp: resultRecord.tradeResult.entryTimestamp,
7912
+ strategy: test.strategyName,
7913
+ symbol: test.symbol,
7914
+ direction: resultRecord.tradeResult.direction,
7915
+ signalId: resultRecord.signalId,
7916
+ configId: test.configId,
7917
+ backtestRunId: test.backtestRunId,
7918
+ backtestTestKey: test.backtestTestKey,
7919
+ ...researchSetup
7828
7920
  }
7829
7921
  });
7922
+ researchEventCounts.set(
7923
+ "entry_executed",
7924
+ (researchEventCounts.get("entry_executed") ?? 0) + 1
7925
+ );
7926
+ await (0, import_coreResearch.appendCoreResearchTraceEvent)({
7927
+ strategyName: test.strategyName,
7928
+ chunkId,
7929
+ event: {
7930
+ schema: "tradejs-core-research-trace/v1",
7931
+ event: "position_exited",
7932
+ timestamp: resultRecord.tradeResult.exitTimestamp,
7933
+ strategy: test.strategyName,
7934
+ symbol: test.symbol,
7935
+ direction: resultRecord.tradeResult.direction,
7936
+ signalId: resultRecord.signalId,
7937
+ configId: test.configId,
7938
+ backtestRunId: test.backtestRunId,
7939
+ backtestTestKey: test.backtestTestKey,
7940
+ netProfit: resultRecord.tradeResult.netProfit,
7941
+ exitReason: resultRecord.tradeResult.exitReason,
7942
+ ...researchSetup
7943
+ }
7944
+ });
7945
+ researchEventCounts.set(
7946
+ "position_exited",
7947
+ (researchEventCounts.get("position_exited") ?? 0) + 1
7948
+ );
7830
7949
  }
7831
7950
  }
7832
7951
  };
@@ -7860,15 +7979,41 @@ var createBacktestSession = async ({
7860
7979
  flush: () => monitor.run("flush closed results", flush),
7861
7980
  result: async () => {
7862
7981
  await monitor.run("flush closed results", flush);
7982
+ if (test.researchTrace) {
7983
+ await (0, import_coreResearch.appendCoreResearchTraceEvent)({
7984
+ strategyName: test.strategyName,
7985
+ chunkId,
7986
+ event: {
7987
+ schema: "tradejs-core-research-trace/v1",
7988
+ event: "skip_summary",
7989
+ timestamp: test.options.end ?? test.options.start ?? 0,
7990
+ strategy: test.strategyName,
7991
+ symbol: test.symbol,
7992
+ configId: test.configId,
7993
+ backtestRunId: test.backtestRunId,
7994
+ backtestTestKey: test.backtestTestKey,
7995
+ skipCounts: Object.fromEntries(
7996
+ [...researchSkipCounts.entries()].sort(
7997
+ ([left], [right]) => left < right ? -1 : left > right ? 1 : 0
7998
+ )
7999
+ )
8000
+ }
8001
+ });
8002
+ }
7863
8003
  const result = await monitor.run(
7864
8004
  "collect result",
7865
8005
  () => testConnector.getResult()
7866
8006
  );
8007
+ const researchTraceSummary = test.researchTrace ? {
8008
+ events: Object.fromEntries(researchEventCounts),
8009
+ skipCounts: Object.fromEntries(researchSkipCounts)
8010
+ } : void 0;
7867
8011
  return replayEvaluations ? {
7868
8012
  ...result,
7869
8013
  warningCounts,
7870
- inlineReplaySignalEvaluations: replayEvaluations
7871
- } : { ...result, warningCounts };
8014
+ inlineReplaySignalEvaluations: replayEvaluations,
8015
+ researchTraceSummary
8016
+ } : { ...result, warningCounts, researchTraceSummary };
7872
8017
  }
7873
8018
  };
7874
8019
  };
package/dist/backtest.mjs CHANGED
@@ -142,6 +142,7 @@ import {
142
142
  BACKTEST_WARNING_CODES
143
143
  } from "@tradejs/types";
144
144
  import { appendAiDatasetRow } from "@tradejs/infra/ai";
145
+ import { appendCoreResearchTraceEvent } from "@tradejs/infra/coreResearch";
145
146
  import {
146
147
  appendMlDatasetRow,
147
148
  buildMlTrainingRow,
@@ -1256,6 +1257,37 @@ var createTestConnector = (connector, context) => {
1256
1257
  };
1257
1258
  };
1258
1259
 
1260
+ // src/backtest/researchTrace.ts
1261
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
1262
+ var findSetupKey = (value, depth = 0) => {
1263
+ if (depth > 5) return null;
1264
+ const record = asRecord(value);
1265
+ if (!record) return null;
1266
+ for (const key of ["setupIdentity", "setupId", "patternId", "candidateId"]) {
1267
+ const candidate = record[key];
1268
+ if (typeof candidate === "string" && candidate.trim() || typeof candidate === "number" && Number.isFinite(candidate)) {
1269
+ return `${key}:${String(candidate)}`;
1270
+ }
1271
+ }
1272
+ for (const [key, nested] of Object.entries(record)) {
1273
+ if (/context|signal|setup|pattern|candidate|strategy/i.test(key) && nested != null) {
1274
+ const found = findSetupKey(nested, depth + 1);
1275
+ if (found) return found;
1276
+ }
1277
+ }
1278
+ return null;
1279
+ };
1280
+ var resolveCoreResearchSetupIdentity = (signal) => {
1281
+ const strategyKey = findSetupKey(signal.additionalIndicators);
1282
+ return strategyKey ? {
1283
+ setupIdentity: `${signal.strategy}|${signal.symbol}|${signal.direction}|${strategyKey}`,
1284
+ setupIdentitySource: "strategy-context"
1285
+ } : {
1286
+ setupIdentity: `${signal.strategy}|${signal.symbol}|${signal.direction}|${signal.timestamp}`,
1287
+ setupIdentitySource: "signal-time-fallback"
1288
+ };
1289
+ };
1290
+
1259
1291
  // src/backtest/session.ts
1260
1292
  var createWarningCounts = () => ({
1261
1293
  [BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY]: 0
@@ -1345,7 +1377,7 @@ var createBacktestSession = async ({
1345
1377
  const testConnector = createTestConnector(connector, {
1346
1378
  userName: test.userName,
1347
1379
  mlEnabled: test.ml,
1348
- aiEnabled: test.ai,
1380
+ aiEnabled: Boolean(test.ai || test.researchTrace),
1349
1381
  fastMode: test.fast,
1350
1382
  instrument,
1351
1383
  executionCostModel,
@@ -1382,11 +1414,18 @@ var createBacktestSession = async ({
1382
1414
  );
1383
1415
  const pendingMlPayloadBySignalId = /* @__PURE__ */ new Map();
1384
1416
  const pendingAiRowBySignalId = /* @__PURE__ */ new Map();
1417
+ const pendingResearchSetupBySignalId = /* @__PURE__ */ new Map();
1418
+ const researchSkipCounts = /* @__PURE__ */ new Map();
1419
+ const researchEventCounts = /* @__PURE__ */ new Map();
1385
1420
  const warningCounts = createWarningCounts();
1386
1421
  const replayEvaluations = test.collectReplaySignalEvaluations ? [] : null;
1387
1422
  const chunkId = test.chunkId ?? "single";
1388
1423
  const processSignal = async (signal, candle) => {
1389
1424
  recordSignalWarning(warningCounts, signal);
1425
+ if (test.researchTrace && typeof signal === "string") {
1426
+ const reason = signal.trim() || "NO_SIGNAL";
1427
+ researchSkipCounts.set(reason, (researchSkipCounts.get(reason) ?? 0) + 1);
1428
+ }
1390
1429
  if (typeof signal === "string" && signal.startsWith("BACKTEST_ENTRY_DELAY_")) {
1391
1430
  return;
1392
1431
  }
@@ -1403,6 +1442,32 @@ var createBacktestSession = async ({
1403
1442
  onStageStart: monitor.contextStage
1404
1443
  });
1405
1444
  }
1445
+ if (test.researchTrace && signal && typeof signal !== "string" && signal.signalId) {
1446
+ const identity = resolveCoreResearchSetupIdentity(signal);
1447
+ pendingResearchSetupBySignalId.set(signal.signalId, identity);
1448
+ await appendCoreResearchTraceEvent({
1449
+ strategyName: test.strategyName,
1450
+ chunkId,
1451
+ event: {
1452
+ schema: "tradejs-core-research-trace/v1",
1453
+ event: signal.orderStatus === "failed" || signal.orderStatus === "skipped" ? "entry_rejected" : "signal_emitted",
1454
+ timestamp: signal.timestamp,
1455
+ strategy: signal.strategy || test.strategyName,
1456
+ symbol: signal.symbol || test.symbol,
1457
+ direction: signal.direction,
1458
+ signalId: signal.signalId,
1459
+ configId: test.configId,
1460
+ backtestRunId: test.backtestRunId,
1461
+ backtestTestKey: test.backtestTestKey,
1462
+ ...identity
1463
+ }
1464
+ });
1465
+ const traceEvent = signal.orderStatus === "failed" || signal.orderStatus === "skipped" ? "entry_rejected" : "signal_emitted";
1466
+ researchEventCounts.set(
1467
+ traceEvent,
1468
+ (researchEventCounts.get(traceEvent) ?? 0) + 1
1469
+ );
1470
+ }
1406
1471
  if (test.ml && signal && typeof signal !== "string" && signal.signalId) {
1407
1472
  pendingMlPayloadBySignalId.set(
1408
1473
  signal.signalId,
@@ -1440,7 +1505,7 @@ var createBacktestSession = async ({
1440
1505
  }
1441
1506
  };
1442
1507
  const flush = async () => {
1443
- if (!test.ml && !test.ai) return;
1508
+ if (!test.ml && !test.ai && !test.researchTrace) return;
1444
1509
  const batch = await testConnector.drainMlResultsBatch();
1445
1510
  for (const resultRecord of batch) {
1446
1511
  const payload = pendingMlPayloadBySignalId.get(resultRecord.signalId);
@@ -1469,10 +1534,64 @@ var createBacktestSession = async ({
1469
1534
  ...rowBase,
1470
1535
  payload: buildAiPayload(aiSignal),
1471
1536
  profit: resultRecord.profit,
1472
- tradeResult: resultRecord.tradeResult
1537
+ tradeResult: resultRecord.tradeResult,
1538
+ research: {
1539
+ schema: "tradejs-core-research-row/v1",
1540
+ ...resolveCoreResearchSetupIdentity(aiSignal)
1541
+ }
1473
1542
  }
1474
1543
  });
1475
1544
  }
1545
+ const researchSetup = pendingResearchSetupBySignalId.get(
1546
+ resultRecord.signalId
1547
+ );
1548
+ if (test.researchTrace && researchSetup && resultRecord.tradeResult) {
1549
+ pendingResearchSetupBySignalId.delete(resultRecord.signalId);
1550
+ await appendCoreResearchTraceEvent({
1551
+ strategyName: test.strategyName,
1552
+ chunkId,
1553
+ event: {
1554
+ schema: "tradejs-core-research-trace/v1",
1555
+ event: "entry_executed",
1556
+ timestamp: resultRecord.tradeResult.entryTimestamp,
1557
+ strategy: test.strategyName,
1558
+ symbol: test.symbol,
1559
+ direction: resultRecord.tradeResult.direction,
1560
+ signalId: resultRecord.signalId,
1561
+ configId: test.configId,
1562
+ backtestRunId: test.backtestRunId,
1563
+ backtestTestKey: test.backtestTestKey,
1564
+ ...researchSetup
1565
+ }
1566
+ });
1567
+ researchEventCounts.set(
1568
+ "entry_executed",
1569
+ (researchEventCounts.get("entry_executed") ?? 0) + 1
1570
+ );
1571
+ await appendCoreResearchTraceEvent({
1572
+ strategyName: test.strategyName,
1573
+ chunkId,
1574
+ event: {
1575
+ schema: "tradejs-core-research-trace/v1",
1576
+ event: "position_exited",
1577
+ timestamp: resultRecord.tradeResult.exitTimestamp,
1578
+ strategy: test.strategyName,
1579
+ symbol: test.symbol,
1580
+ direction: resultRecord.tradeResult.direction,
1581
+ signalId: resultRecord.signalId,
1582
+ configId: test.configId,
1583
+ backtestRunId: test.backtestRunId,
1584
+ backtestTestKey: test.backtestTestKey,
1585
+ netProfit: resultRecord.tradeResult.netProfit,
1586
+ exitReason: resultRecord.tradeResult.exitReason,
1587
+ ...researchSetup
1588
+ }
1589
+ });
1590
+ researchEventCounts.set(
1591
+ "position_exited",
1592
+ (researchEventCounts.get("position_exited") ?? 0) + 1
1593
+ );
1594
+ }
1476
1595
  }
1477
1596
  };
1478
1597
  return {
@@ -1505,15 +1624,41 @@ var createBacktestSession = async ({
1505
1624
  flush: () => monitor.run("flush closed results", flush),
1506
1625
  result: async () => {
1507
1626
  await monitor.run("flush closed results", flush);
1627
+ if (test.researchTrace) {
1628
+ await appendCoreResearchTraceEvent({
1629
+ strategyName: test.strategyName,
1630
+ chunkId,
1631
+ event: {
1632
+ schema: "tradejs-core-research-trace/v1",
1633
+ event: "skip_summary",
1634
+ timestamp: test.options.end ?? test.options.start ?? 0,
1635
+ strategy: test.strategyName,
1636
+ symbol: test.symbol,
1637
+ configId: test.configId,
1638
+ backtestRunId: test.backtestRunId,
1639
+ backtestTestKey: test.backtestTestKey,
1640
+ skipCounts: Object.fromEntries(
1641
+ [...researchSkipCounts.entries()].sort(
1642
+ ([left], [right]) => left < right ? -1 : left > right ? 1 : 0
1643
+ )
1644
+ )
1645
+ }
1646
+ });
1647
+ }
1508
1648
  const result = await monitor.run(
1509
1649
  "collect result",
1510
1650
  () => testConnector.getResult()
1511
1651
  );
1652
+ const researchTraceSummary = test.researchTrace ? {
1653
+ events: Object.fromEntries(researchEventCounts),
1654
+ skipCounts: Object.fromEntries(researchSkipCounts)
1655
+ } : void 0;
1512
1656
  return replayEvaluations ? {
1513
1657
  ...result,
1514
1658
  warningCounts,
1515
- inlineReplaySignalEvaluations: replayEvaluations
1516
- } : { ...result, warningCounts };
1659
+ inlineReplaySignalEvaluations: replayEvaluations,
1660
+ researchTraceSummary
1661
+ } : { ...result, warningCounts, researchTraceSummary };
1517
1662
  }
1518
1663
  };
1519
1664
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/node",
3
- "version": "2.0.19",
3
+ "version": "2.0.21",
4
4
  "description": "Node-only runtime for the TradeJS TypeScript framework: strategies, backtests, Pine strategy loading, and plugin registries.",
5
5
  "keywords": [
6
6
  "tradejs",
@@ -67,9 +67,9 @@
67
67
  "dependencies": {
68
68
  "@langchain/core": "^1.2.3",
69
69
  "@langchain/openai": "^1.5.5",
70
- "@tradejs/core": "^2.0.19",
71
- "@tradejs/infra": "^2.0.19",
72
- "@tradejs/types": "^2.0.19",
70
+ "@tradejs/core": "^2.0.21",
71
+ "@tradejs/infra": "^2.0.21",
72
+ "@tradejs/types": "^2.0.21",
73
73
  "chalk": "4.1.2",
74
74
  "ioredis": "5.11.1",
75
75
  "lodash": "^4.18.1",
@@ -80,7 +80,7 @@
80
80
  "tsconfig-paths": "^4.2.0"
81
81
  },
82
82
  "devDependencies": {
83
- "@tradejs/strategies": "^2.0.19",
83
+ "@tradejs/strategies": "^2.0.21",
84
84
  "@types/node": "^24",
85
85
  "tsup": "^8.5.1",
86
86
  "typescript": "^5.9"