backtest-kit 1.5.16 → 1.5.18

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/build/index.cjs CHANGED
@@ -111,6 +111,14 @@ const GLOBAL_CONFIG = {
111
111
  * Example: 3 candles = 12 points (use average), 5 candles = 20 points (use median)
112
112
  */
113
113
  CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN: 5,
114
+ /**
115
+ * Controls visibility of signal notes in markdown report tables.
116
+ * When enabled, the "Note" column will be displayed in all markdown reports
117
+ * (backtest, live, schedule, risk, etc.)
118
+ *
119
+ * Default: false (notes are hidden to reduce table width and improve readability)
120
+ */
121
+ CC_REPORT_SHOW_SIGNAL_NOTE: false,
114
122
  };
115
123
  const DEFAULT_CONFIG = Object.freeze({ ...GLOBAL_CONFIG });
116
124
 
@@ -202,6 +210,7 @@ const markdownServices$1 = {
202
210
  heatMarkdownService: Symbol('heatMarkdownService'),
203
211
  partialMarkdownService: Symbol('partialMarkdownService'),
204
212
  outlineMarkdownService: Symbol('outlineMarkdownService'),
213
+ riskMarkdownService: Symbol('riskMarkdownService'),
205
214
  };
206
215
  const validationServices$1 = {
207
216
  exchangeValidationService: Symbol('exchangeValidationService'),
@@ -442,11 +451,12 @@ const GET_CANDLES_FN = async (dto, since, self) => {
442
451
  }
443
452
  catch (err) {
444
453
  const message = `ClientExchange GET_CANDLES_FN: attempt ${i + 1} failed for symbol=${dto.symbol}, interval=${dto.interval}, since=${since.toISOString()}, limit=${dto.limit}}`;
445
- self.params.logger.warn(message, {
454
+ const payload = {
446
455
  error: functoolsKit.errorData(err),
447
456
  message: functoolsKit.getErrorMessage(err),
448
- });
449
- console.warn(message);
457
+ };
458
+ self.params.logger.warn(message, payload);
459
+ console.warn(message, payload);
450
460
  lastError = err;
451
461
  await functoolsKit.sleep(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS);
452
462
  }
@@ -1725,6 +1735,12 @@ const partialProfitSubject = new functoolsKit.Subject();
1725
1735
  * Emits when a signal reaches a loss level (10%, 20%, 30%, etc).
1726
1736
  */
1727
1737
  const partialLossSubject = new functoolsKit.Subject();
1738
+ /**
1739
+ * Risk rejection emitter for risk management violations.
1740
+ * Emits ONLY when a signal is rejected due to risk validation failure.
1741
+ * Does not emit for allowed signals (prevents spam).
1742
+ */
1743
+ const riskSubject = new functoolsKit.Subject();
1728
1744
 
1729
1745
  var emitters = /*#__PURE__*/Object.freeze({
1730
1746
  __proto__: null,
@@ -1739,6 +1755,7 @@ var emitters = /*#__PURE__*/Object.freeze({
1739
1755
  progressBacktestEmitter: progressBacktestEmitter,
1740
1756
  progressOptimizerEmitter: progressOptimizerEmitter,
1741
1757
  progressWalkerEmitter: progressWalkerEmitter,
1758
+ riskSubject: riskSubject,
1742
1759
  signalBacktestEmitter: signalBacktestEmitter,
1743
1760
  signalEmitter: signalEmitter,
1744
1761
  signalLiveEmitter: signalLiveEmitter,
@@ -1825,6 +1842,9 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1825
1842
  if (signal.position === undefined || signal.position === null) {
1826
1843
  errors.push('position is required and must be "long" or "short"');
1827
1844
  }
1845
+ if (signal.position !== "long" && signal.position !== "short") {
1846
+ errors.push(`position must be "long" or "short", got "${signal.position}"`);
1847
+ }
1828
1848
  // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
1829
1849
  if (!isFinite(currentPrice)) {
1830
1850
  errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
@@ -2111,10 +2131,13 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
2111
2131
  }, {
2112
2132
  defaultValue: null,
2113
2133
  fallback: (error) => {
2114
- backtest$1.loggerService.warn("ClientStrategy exception thrown", {
2134
+ const message = "ClientStrategy exception thrown";
2135
+ const payload = {
2115
2136
  error: functoolsKit.errorData(error),
2116
2137
  message: functoolsKit.getErrorMessage(error),
2117
- });
2138
+ };
2139
+ backtest$1.loggerService.warn(message, payload);
2140
+ console.warn(message, payload);
2118
2141
  errorEmitter.next(error);
2119
2142
  },
2120
2143
  });
@@ -2805,7 +2828,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2805
2828
  // Moving towards TP
2806
2829
  const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2807
2830
  const progressPercent = (currentDistance / tpDistance) * 100;
2808
- await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, self.params.execution.context.when);
2831
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2809
2832
  if (self.params.callbacks?.onPartialProfit) {
2810
2833
  self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2811
2834
  }
@@ -2814,7 +2837,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2814
2837
  // Moving towards SL
2815
2838
  const slDistance = signal.priceOpen - signal.priceStopLoss;
2816
2839
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2817
- await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, self.params.execution.context.when);
2840
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2818
2841
  if (self.params.callbacks?.onPartialLoss) {
2819
2842
  self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2820
2843
  }
@@ -2827,7 +2850,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2827
2850
  // Moving towards TP
2828
2851
  const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2829
2852
  const progressPercent = (currentDistance / tpDistance) * 100;
2830
- await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, self.params.execution.context.when);
2853
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2831
2854
  if (self.params.callbacks?.onPartialProfit) {
2832
2855
  self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2833
2856
  }
@@ -2836,7 +2859,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2836
2859
  // Moving towards SL
2837
2860
  const slDistance = signal.priceStopLoss - signal.priceOpen;
2838
2861
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2839
- await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, self.params.execution.context.when);
2862
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2840
2863
  if (self.params.callbacks?.onPartialLoss) {
2841
2864
  self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2842
2865
  }
@@ -3791,10 +3814,13 @@ const DO_VALIDATION_FN = functoolsKit.trycatch(async (validation, params) => {
3791
3814
  }, {
3792
3815
  defaultValue: false,
3793
3816
  fallback: (error) => {
3794
- backtest$1.loggerService.warn("ClientRisk exception thrown", {
3817
+ const message = "ClientRisk exception thrown";
3818
+ const payload = {
3795
3819
  error: functoolsKit.errorData(error),
3796
3820
  message: functoolsKit.getErrorMessage(error),
3797
- });
3821
+ };
3822
+ backtest$1.loggerService.warn(message, payload);
3823
+ console.warn(message, payload);
3798
3824
  validationSubject.next(error);
3799
3825
  },
3800
3826
  });
@@ -3864,17 +3890,25 @@ class ClientRisk {
3864
3890
  };
3865
3891
  // Execute custom validations
3866
3892
  let isValid = true;
3893
+ let rejectionNote = "N/A";
3867
3894
  if (this.params.validations) {
3868
3895
  for (const validation of this.params.validations) {
3869
3896
  if (functoolsKit.not(await DO_VALIDATION_FN(typeof validation === "function"
3870
3897
  ? validation
3871
3898
  : validation.validate, payload))) {
3872
3899
  isValid = false;
3900
+ // Capture note from validation if available
3901
+ if (typeof validation !== "function" && validation.note) {
3902
+ rejectionNote = validation.note;
3903
+ }
3873
3904
  break;
3874
3905
  }
3875
3906
  }
3876
3907
  }
3877
3908
  if (!isValid) {
3909
+ // Call params.onRejected for riskSubject emission
3910
+ await this.params.onRejected(params.symbol, params, riskMap.size, rejectionNote, Date.now());
3911
+ // Call schema callbacks.onRejected if defined
3878
3912
  if (this.params.callbacks?.onRejected) {
3879
3913
  this.params.callbacks.onRejected(params.symbol, params);
3880
3914
  }
@@ -3937,6 +3971,28 @@ class ClientRisk {
3937
3971
  }
3938
3972
  }
3939
3973
 
3974
+ /**
3975
+ * Callback function for emitting risk rejection events to riskSubject.
3976
+ *
3977
+ * Called by ClientRisk when a signal is rejected due to risk validation failure.
3978
+ * Emits RiskContract event to all subscribers.
3979
+ *
3980
+ * @param symbol - Trading pair symbol
3981
+ * @param params - Risk check arguments
3982
+ * @param activePositionCount - Number of active positions at rejection time
3983
+ * @param comment - Rejection reason from validation note or "N/A"
3984
+ * @param timestamp - Event timestamp in milliseconds
3985
+ */
3986
+ const COMMIT_REJECTION_FN = async (symbol, params, activePositionCount, comment, timestamp) => await riskSubject.next({
3987
+ symbol,
3988
+ pendingSignal: params.pendingSignal,
3989
+ strategyName: params.strategyName,
3990
+ exchangeName: params.exchangeName,
3991
+ currentPrice: params.currentPrice,
3992
+ activePositionCount,
3993
+ comment,
3994
+ timestamp,
3995
+ });
3940
3996
  /**
3941
3997
  * Connection service routing risk operations to correct ClientRisk instance.
3942
3998
  *
@@ -3987,6 +4043,7 @@ class RiskConnectionService {
3987
4043
  return new ClientRisk({
3988
4044
  ...schema,
3989
4045
  logger: this.loggerService,
4046
+ onRejected: COMMIT_REJECTION_FN,
3990
4047
  });
3991
4048
  });
3992
4049
  /**
@@ -3994,6 +4051,7 @@ class RiskConnectionService {
3994
4051
  *
3995
4052
  * Routes to appropriate ClientRisk instance based on provided context.
3996
4053
  * Validates portfolio drawdown, symbol exposure, position count, and daily loss limits.
4054
+ * ClientRisk will emit riskSubject event via onRejected callback when signal is rejected.
3997
4055
  *
3998
4056
  * @param params - Risk check arguments (portfolio state, position details)
3999
4057
  * @param context - Execution context with risk name
@@ -5982,46 +6040,54 @@ function isUnsafe$3(value) {
5982
6040
  }
5983
6041
  return false;
5984
6042
  }
5985
- const columns$4 = [
6043
+ const columns$6 = [
5986
6044
  {
5987
6045
  key: "signalId",
5988
6046
  label: "Signal ID",
5989
6047
  format: (data) => data.signal.id,
6048
+ isVisible: () => true,
5990
6049
  },
5991
6050
  {
5992
6051
  key: "symbol",
5993
6052
  label: "Symbol",
5994
6053
  format: (data) => data.signal.symbol,
6054
+ isVisible: () => true,
5995
6055
  },
5996
6056
  {
5997
6057
  key: "position",
5998
6058
  label: "Position",
5999
6059
  format: (data) => data.signal.position.toUpperCase(),
6060
+ isVisible: () => true,
6000
6061
  },
6001
6062
  {
6002
6063
  key: "note",
6003
6064
  label: "Note",
6004
6065
  format: (data) => toPlainString(data.signal.note ?? "N/A"),
6066
+ isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
6005
6067
  },
6006
6068
  {
6007
6069
  key: "openPrice",
6008
6070
  label: "Open Price",
6009
6071
  format: (data) => `${data.signal.priceOpen.toFixed(8)} USD`,
6072
+ isVisible: () => true,
6010
6073
  },
6011
6074
  {
6012
6075
  key: "closePrice",
6013
6076
  label: "Close Price",
6014
6077
  format: (data) => `${data.currentPrice.toFixed(8)} USD`,
6078
+ isVisible: () => true,
6015
6079
  },
6016
6080
  {
6017
6081
  key: "takeProfit",
6018
6082
  label: "Take Profit",
6019
6083
  format: (data) => `${data.signal.priceTakeProfit.toFixed(8)} USD`,
6084
+ isVisible: () => true,
6020
6085
  },
6021
6086
  {
6022
6087
  key: "stopLoss",
6023
6088
  label: "Stop Loss",
6024
6089
  format: (data) => `${data.signal.priceStopLoss.toFixed(8)} USD`,
6090
+ isVisible: () => true,
6025
6091
  },
6026
6092
  {
6027
6093
  key: "pnl",
@@ -6030,11 +6096,13 @@ const columns$4 = [
6030
6096
  const pnlPercentage = data.pnl.pnlPercentage;
6031
6097
  return `${pnlPercentage > 0 ? "+" : ""}${pnlPercentage.toFixed(2)}%`;
6032
6098
  },
6099
+ isVisible: () => true,
6033
6100
  },
6034
6101
  {
6035
6102
  key: "closeReason",
6036
6103
  label: "Close Reason",
6037
6104
  format: (data) => data.closeReason,
6105
+ isVisible: () => true,
6038
6106
  },
6039
6107
  {
6040
6108
  key: "duration",
@@ -6044,23 +6112,28 @@ const columns$4 = [
6044
6112
  const durationMin = Math.round(durationMs / 60000);
6045
6113
  return `${durationMin}`;
6046
6114
  },
6115
+ isVisible: () => true,
6047
6116
  },
6048
6117
  {
6049
6118
  key: "openTimestamp",
6050
6119
  label: "Open Time",
6051
6120
  format: (data) => new Date(data.signal.pendingAt).toISOString(),
6121
+ isVisible: () => true,
6052
6122
  },
6053
6123
  {
6054
6124
  key: "closeTimestamp",
6055
6125
  label: "Close Time",
6056
6126
  format: (data) => new Date(data.closeTimestamp).toISOString(),
6127
+ isVisible: () => true,
6057
6128
  },
6058
6129
  ];
6130
+ /** Maximum number of signals to store in backtest reports */
6131
+ const MAX_EVENTS$6 = 250;
6059
6132
  /**
6060
6133
  * Storage class for accumulating closed signals per strategy.
6061
6134
  * Maintains a list of all closed signals and provides methods to generate reports.
6062
6135
  */
6063
- let ReportStorage$4 = class ReportStorage {
6136
+ let ReportStorage$5 = class ReportStorage {
6064
6137
  constructor() {
6065
6138
  /** Internal list of all closed signals for this strategy */
6066
6139
  this._signalList = [];
@@ -6071,7 +6144,11 @@ let ReportStorage$4 = class ReportStorage {
6071
6144
  * @param data - Closed signal data with PNL and close reason
6072
6145
  */
6073
6146
  addSignal(data) {
6074
- this._signalList.push(data);
6147
+ this._signalList.unshift(data);
6148
+ // Trim queue if exceeded MAX_EVENTS
6149
+ if (this._signalList.length > MAX_EVENTS$6) {
6150
+ this._signalList.pop();
6151
+ }
6075
6152
  }
6076
6153
  /**
6077
6154
  * Calculates statistical data from closed signals (Controller).
@@ -6154,9 +6231,10 @@ let ReportStorage$4 = class ReportStorage {
6154
6231
  "No signals closed yet."
6155
6232
  ].join("\n");
6156
6233
  }
6157
- const header = columns$4.map((col) => col.label);
6158
- const separator = columns$4.map(() => "---");
6159
- const rows = this._signalList.map((closedSignal) => columns$4.map((col) => col.format(closedSignal)));
6234
+ const visibleColumns = columns$6.filter((col) => col.isVisible());
6235
+ const header = visibleColumns.map((col) => col.label);
6236
+ const separator = visibleColumns.map(() => "---");
6237
+ const rows = this._signalList.map((closedSignal) => visibleColumns.map((col) => col.format(closedSignal)));
6160
6238
  const tableData = [header, separator, ...rows];
6161
6239
  const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
6162
6240
  return [
@@ -6232,7 +6310,7 @@ class BacktestMarkdownService {
6232
6310
  * Memoized function to get or create ReportStorage for a symbol-strategy pair.
6233
6311
  * Each symbol-strategy combination gets its own isolated storage instance.
6234
6312
  */
6235
- this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$4());
6313
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$5());
6236
6314
  /**
6237
6315
  * Processes tick events and accumulates closed signals.
6238
6316
  * Should be called from IStrategyCallbacks.onTick.
@@ -6403,46 +6481,54 @@ function isUnsafe$2(value) {
6403
6481
  }
6404
6482
  return false;
6405
6483
  }
6406
- const columns$3 = [
6484
+ const columns$5 = [
6407
6485
  {
6408
6486
  key: "timestamp",
6409
6487
  label: "Timestamp",
6410
6488
  format: (data) => new Date(data.timestamp).toISOString(),
6489
+ isVisible: () => true,
6411
6490
  },
6412
6491
  {
6413
6492
  key: "action",
6414
6493
  label: "Action",
6415
6494
  format: (data) => data.action.toUpperCase(),
6495
+ isVisible: () => true,
6416
6496
  },
6417
6497
  {
6418
6498
  key: "symbol",
6419
6499
  label: "Symbol",
6420
6500
  format: (data) => data.symbol ?? "N/A",
6501
+ isVisible: () => true,
6421
6502
  },
6422
6503
  {
6423
6504
  key: "signalId",
6424
6505
  label: "Signal ID",
6425
6506
  format: (data) => data.signalId ?? "N/A",
6507
+ isVisible: () => true,
6426
6508
  },
6427
6509
  {
6428
6510
  key: "position",
6429
6511
  label: "Position",
6430
6512
  format: (data) => data.position?.toUpperCase() ?? "N/A",
6513
+ isVisible: () => true,
6431
6514
  },
6432
6515
  {
6433
6516
  key: "note",
6434
6517
  label: "Note",
6435
6518
  format: (data) => toPlainString(data.note ?? "N/A"),
6519
+ isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
6436
6520
  },
6437
6521
  {
6438
6522
  key: "currentPrice",
6439
6523
  label: "Current Price",
6440
6524
  format: (data) => `${data.currentPrice.toFixed(8)} USD`,
6525
+ isVisible: () => true,
6441
6526
  },
6442
6527
  {
6443
6528
  key: "openPrice",
6444
6529
  label: "Open Price",
6445
6530
  format: (data) => data.openPrice !== undefined ? `${data.openPrice.toFixed(8)} USD` : "N/A",
6531
+ isVisible: () => true,
6446
6532
  },
6447
6533
  {
6448
6534
  key: "takeProfit",
@@ -6450,21 +6536,25 @@ const columns$3 = [
6450
6536
  format: (data) => data.takeProfit !== undefined
6451
6537
  ? `${data.takeProfit.toFixed(8)} USD`
6452
6538
  : "N/A",
6539
+ isVisible: () => true,
6453
6540
  },
6454
6541
  {
6455
6542
  key: "stopLoss",
6456
6543
  label: "Stop Loss",
6457
6544
  format: (data) => data.stopLoss !== undefined ? `${data.stopLoss.toFixed(8)} USD` : "N/A",
6545
+ isVisible: () => true,
6458
6546
  },
6459
6547
  {
6460
6548
  key: "percentTp",
6461
6549
  label: "% to TP",
6462
6550
  format: (data) => data.percentTp !== undefined ? `${data.percentTp.toFixed(2)}%` : "N/A",
6551
+ isVisible: () => true,
6463
6552
  },
6464
6553
  {
6465
6554
  key: "percentSl",
6466
6555
  label: "% to SL",
6467
6556
  format: (data) => data.percentSl !== undefined ? `${data.percentSl.toFixed(2)}%` : "N/A",
6557
+ isVisible: () => true,
6468
6558
  },
6469
6559
  {
6470
6560
  key: "pnl",
@@ -6474,25 +6564,28 @@ const columns$3 = [
6474
6564
  return "N/A";
6475
6565
  return `${data.pnl > 0 ? "+" : ""}${data.pnl.toFixed(2)}%`;
6476
6566
  },
6567
+ isVisible: () => true,
6477
6568
  },
6478
6569
  {
6479
6570
  key: "closeReason",
6480
6571
  label: "Close Reason",
6481
6572
  format: (data) => data.closeReason ?? "N/A",
6573
+ isVisible: () => true,
6482
6574
  },
6483
6575
  {
6484
6576
  key: "duration",
6485
6577
  label: "Duration (min)",
6486
6578
  format: (data) => data.duration !== undefined ? `${data.duration}` : "N/A",
6579
+ isVisible: () => true,
6487
6580
  },
6488
6581
  ];
6489
6582
  /** Maximum number of events to store in live trading reports */
6490
- const MAX_EVENTS$4 = 250;
6583
+ const MAX_EVENTS$5 = 250;
6491
6584
  /**
6492
6585
  * Storage class for accumulating all tick events per strategy.
6493
6586
  * Maintains a chronological list of all events (idle, opened, active, closed).
6494
6587
  */
6495
- let ReportStorage$3 = class ReportStorage {
6588
+ let ReportStorage$4 = class ReportStorage {
6496
6589
  constructor() {
6497
6590
  /** Internal list of all tick events for this strategy */
6498
6591
  this._eventList = [];
@@ -6519,9 +6612,9 @@ let ReportStorage$3 = class ReportStorage {
6519
6612
  return;
6520
6613
  }
6521
6614
  {
6522
- this._eventList.push(newEvent);
6523
- if (this._eventList.length > MAX_EVENTS$4) {
6524
- this._eventList.shift();
6615
+ this._eventList.unshift(newEvent);
6616
+ if (this._eventList.length > MAX_EVENTS$5) {
6617
+ this._eventList.pop();
6525
6618
  }
6526
6619
  }
6527
6620
  }
@@ -6531,7 +6624,7 @@ let ReportStorage$3 = class ReportStorage {
6531
6624
  * @param data - Opened tick result
6532
6625
  */
6533
6626
  addOpenedEvent(data) {
6534
- this._eventList.push({
6627
+ this._eventList.unshift({
6535
6628
  timestamp: data.signal.pendingAt,
6536
6629
  action: "opened",
6537
6630
  symbol: data.signal.symbol,
@@ -6544,12 +6637,13 @@ let ReportStorage$3 = class ReportStorage {
6544
6637
  stopLoss: data.signal.priceStopLoss,
6545
6638
  });
6546
6639
  // Trim queue if exceeded MAX_EVENTS
6547
- if (this._eventList.length > MAX_EVENTS$4) {
6548
- this._eventList.shift();
6640
+ if (this._eventList.length > MAX_EVENTS$5) {
6641
+ this._eventList.pop();
6549
6642
  }
6550
6643
  }
6551
6644
  /**
6552
6645
  * Adds an active event to the storage.
6646
+ * Replaces the last active event with the same signalId.
6553
6647
  *
6554
6648
  * @param data - Active tick result
6555
6649
  */
@@ -6568,10 +6662,18 @@ let ReportStorage$3 = class ReportStorage {
6568
6662
  percentTp: data.percentTp,
6569
6663
  percentSl: data.percentSl,
6570
6664
  };
6571
- this._eventList.push(newEvent);
6665
+ // Find the last active event with the same signalId
6666
+ const lastActiveIndex = this._eventList.findLastIndex((event) => event.action === "active" && event.signalId === data.signal.id);
6667
+ // Replace the last active event with the same signalId
6668
+ if (lastActiveIndex !== -1) {
6669
+ this._eventList[lastActiveIndex] = newEvent;
6670
+ return;
6671
+ }
6672
+ // If no previous active event found, add new event
6673
+ this._eventList.unshift(newEvent);
6572
6674
  // Trim queue if exceeded MAX_EVENTS
6573
- if (this._eventList.length > MAX_EVENTS$4) {
6574
- this._eventList.shift();
6675
+ if (this._eventList.length > MAX_EVENTS$5) {
6676
+ this._eventList.pop();
6575
6677
  }
6576
6678
  }
6577
6679
  /**
@@ -6597,10 +6699,10 @@ let ReportStorage$3 = class ReportStorage {
6597
6699
  closeReason: data.closeReason,
6598
6700
  duration: durationMin,
6599
6701
  };
6600
- this._eventList.push(newEvent);
6702
+ this._eventList.unshift(newEvent);
6601
6703
  // Trim queue if exceeded MAX_EVENTS
6602
- if (this._eventList.length > MAX_EVENTS$4) {
6603
- this._eventList.shift();
6704
+ if (this._eventList.length > MAX_EVENTS$5) {
6705
+ this._eventList.pop();
6604
6706
  }
6605
6707
  }
6606
6708
  /**
@@ -6699,9 +6801,10 @@ let ReportStorage$3 = class ReportStorage {
6699
6801
  "No events recorded yet."
6700
6802
  ].join("\n");
6701
6803
  }
6702
- const header = columns$3.map((col) => col.label);
6703
- const separator = columns$3.map(() => "---");
6704
- const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
6804
+ const visibleColumns = columns$5.filter((col) => col.isVisible());
6805
+ const header = visibleColumns.map((col) => col.label);
6806
+ const separator = visibleColumns.map(() => "---");
6807
+ const rows = this._eventList.map((event) => visibleColumns.map((col) => col.format(event)));
6705
6808
  const tableData = [header, separator, ...rows];
6706
6809
  const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
6707
6810
  return [
@@ -6780,7 +6883,7 @@ class LiveMarkdownService {
6780
6883
  * Memoized function to get or create ReportStorage for a symbol-strategy pair.
6781
6884
  * Each symbol-strategy combination gets its own isolated storage instance.
6782
6885
  */
6783
- this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$3());
6886
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$4());
6784
6887
  /**
6785
6888
  * Processes tick events and accumulates all event types.
6786
6889
  * Should be called from IStrategyCallbacks.onTick.
@@ -6943,70 +7046,81 @@ class LiveMarkdownService {
6943
7046
  }
6944
7047
  }
6945
7048
 
6946
- const columns$2 = [
7049
+ const columns$4 = [
6947
7050
  {
6948
7051
  key: "timestamp",
6949
7052
  label: "Timestamp",
6950
7053
  format: (data) => new Date(data.timestamp).toISOString(),
7054
+ isVisible: () => true,
6951
7055
  },
6952
7056
  {
6953
7057
  key: "action",
6954
7058
  label: "Action",
6955
7059
  format: (data) => data.action.toUpperCase(),
7060
+ isVisible: () => true,
6956
7061
  },
6957
7062
  {
6958
7063
  key: "symbol",
6959
7064
  label: "Symbol",
6960
7065
  format: (data) => data.symbol,
7066
+ isVisible: () => true,
6961
7067
  },
6962
7068
  {
6963
7069
  key: "signalId",
6964
7070
  label: "Signal ID",
6965
7071
  format: (data) => data.signalId,
7072
+ isVisible: () => true,
6966
7073
  },
6967
7074
  {
6968
7075
  key: "position",
6969
7076
  label: "Position",
6970
7077
  format: (data) => data.position.toUpperCase(),
7078
+ isVisible: () => true,
6971
7079
  },
6972
7080
  {
6973
7081
  key: "note",
6974
7082
  label: "Note",
6975
7083
  format: (data) => toPlainString(data.note ?? "N/A"),
7084
+ isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
6976
7085
  },
6977
7086
  {
6978
7087
  key: "currentPrice",
6979
7088
  label: "Current Price",
6980
7089
  format: (data) => `${data.currentPrice.toFixed(8)} USD`,
7090
+ isVisible: () => true,
6981
7091
  },
6982
7092
  {
6983
7093
  key: "priceOpen",
6984
7094
  label: "Entry Price",
6985
7095
  format: (data) => `${data.priceOpen.toFixed(8)} USD`,
7096
+ isVisible: () => true,
6986
7097
  },
6987
7098
  {
6988
7099
  key: "takeProfit",
6989
7100
  label: "Take Profit",
6990
7101
  format: (data) => `${data.takeProfit.toFixed(8)} USD`,
7102
+ isVisible: () => true,
6991
7103
  },
6992
7104
  {
6993
7105
  key: "stopLoss",
6994
7106
  label: "Stop Loss",
6995
7107
  format: (data) => `${data.stopLoss.toFixed(8)} USD`,
7108
+ isVisible: () => true,
6996
7109
  },
6997
7110
  {
6998
7111
  key: "duration",
6999
7112
  label: "Wait Time (min)",
7000
7113
  format: (data) => data.duration !== undefined ? `${data.duration}` : "N/A",
7114
+ isVisible: () => true,
7001
7115
  },
7002
7116
  ];
7003
7117
  /** Maximum number of events to store in schedule reports */
7004
- const MAX_EVENTS$3 = 250;
7118
+ const MAX_EVENTS$4 = 250;
7005
7119
  /**
7006
7120
  * Storage class for accumulating scheduled signal events per strategy.
7007
7121
  * Maintains a chronological list of scheduled and cancelled events.
7008
7122
  */
7009
- let ReportStorage$2 = class ReportStorage {
7123
+ let ReportStorage$3 = class ReportStorage {
7010
7124
  constructor() {
7011
7125
  /** Internal list of all scheduled events for this strategy */
7012
7126
  this._eventList = [];
@@ -7017,7 +7131,7 @@ let ReportStorage$2 = class ReportStorage {
7017
7131
  * @param data - Scheduled tick result
7018
7132
  */
7019
7133
  addScheduledEvent(data) {
7020
- this._eventList.push({
7134
+ this._eventList.unshift({
7021
7135
  timestamp: data.signal.scheduledAt,
7022
7136
  action: "scheduled",
7023
7137
  symbol: data.signal.symbol,
@@ -7030,8 +7144,8 @@ let ReportStorage$2 = class ReportStorage {
7030
7144
  stopLoss: data.signal.priceStopLoss,
7031
7145
  });
7032
7146
  // Trim queue if exceeded MAX_EVENTS
7033
- if (this._eventList.length > MAX_EVENTS$3) {
7034
- this._eventList.shift();
7147
+ if (this._eventList.length > MAX_EVENTS$4) {
7148
+ this._eventList.pop();
7035
7149
  }
7036
7150
  }
7037
7151
  /**
@@ -7055,10 +7169,10 @@ let ReportStorage$2 = class ReportStorage {
7055
7169
  stopLoss: data.signal.priceStopLoss,
7056
7170
  duration: durationMin,
7057
7171
  };
7058
- this._eventList.push(newEvent);
7172
+ this._eventList.unshift(newEvent);
7059
7173
  // Trim queue if exceeded MAX_EVENTS
7060
- if (this._eventList.length > MAX_EVENTS$3) {
7061
- this._eventList.shift();
7174
+ if (this._eventList.length > MAX_EVENTS$4) {
7175
+ this._eventList.pop();
7062
7176
  }
7063
7177
  }
7064
7178
  /**
@@ -7083,10 +7197,10 @@ let ReportStorage$2 = class ReportStorage {
7083
7197
  closeTimestamp: data.closeTimestamp,
7084
7198
  duration: durationMin,
7085
7199
  };
7086
- this._eventList.push(newEvent);
7200
+ this._eventList.unshift(newEvent);
7087
7201
  // Trim queue if exceeded MAX_EVENTS
7088
- if (this._eventList.length > MAX_EVENTS$3) {
7089
- this._eventList.shift();
7202
+ if (this._eventList.length > MAX_EVENTS$4) {
7203
+ this._eventList.pop();
7090
7204
  }
7091
7205
  }
7092
7206
  /**
@@ -7155,9 +7269,10 @@ let ReportStorage$2 = class ReportStorage {
7155
7269
  "No scheduled signals recorded yet."
7156
7270
  ].join("\n");
7157
7271
  }
7158
- const header = columns$2.map((col) => col.label);
7159
- const separator = columns$2.map(() => "---");
7160
- const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
7272
+ const visibleColumns = columns$4.filter((col) => col.isVisible());
7273
+ const header = visibleColumns.map((col) => col.label);
7274
+ const separator = visibleColumns.map(() => "---");
7275
+ const rows = this._eventList.map((event) => visibleColumns.map((col) => col.format(event)));
7161
7276
  const tableData = [header, separator, ...rows];
7162
7277
  const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7163
7278
  return [
@@ -7225,7 +7340,7 @@ class ScheduleMarkdownService {
7225
7340
  * Memoized function to get or create ReportStorage for a symbol-strategy pair.
7226
7341
  * Each symbol-strategy combination gets its own isolated storage instance.
7227
7342
  */
7228
- this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$2());
7343
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$3());
7229
7344
  /**
7230
7345
  * Processes tick events and accumulates scheduled/opened/cancelled events.
7231
7346
  * Should be called from signalEmitter subscription.
@@ -7391,8 +7506,88 @@ function percentile(sortedArray, p) {
7391
7506
  const index = Math.ceil((sortedArray.length * p) / 100) - 1;
7392
7507
  return sortedArray[Math.max(0, index)];
7393
7508
  }
7509
+ const columns$3 = [
7510
+ {
7511
+ key: "metricType",
7512
+ label: "Metric Type",
7513
+ format: (data) => data.metricType,
7514
+ isVisible: () => true,
7515
+ },
7516
+ {
7517
+ key: "count",
7518
+ label: "Count",
7519
+ format: (data) => data.count.toString(),
7520
+ isVisible: () => true,
7521
+ },
7522
+ {
7523
+ key: "totalDuration",
7524
+ label: "Total (ms)",
7525
+ format: (data) => data.totalDuration.toFixed(2),
7526
+ isVisible: () => true,
7527
+ },
7528
+ {
7529
+ key: "avgDuration",
7530
+ label: "Avg (ms)",
7531
+ format: (data) => data.avgDuration.toFixed(2),
7532
+ isVisible: () => true,
7533
+ },
7534
+ {
7535
+ key: "minDuration",
7536
+ label: "Min (ms)",
7537
+ format: (data) => data.minDuration.toFixed(2),
7538
+ isVisible: () => true,
7539
+ },
7540
+ {
7541
+ key: "maxDuration",
7542
+ label: "Max (ms)",
7543
+ format: (data) => data.maxDuration.toFixed(2),
7544
+ isVisible: () => true,
7545
+ },
7546
+ {
7547
+ key: "stdDev",
7548
+ label: "Std Dev (ms)",
7549
+ format: (data) => data.stdDev.toFixed(2),
7550
+ isVisible: () => true,
7551
+ },
7552
+ {
7553
+ key: "median",
7554
+ label: "Median (ms)",
7555
+ format: (data) => data.median.toFixed(2),
7556
+ isVisible: () => true,
7557
+ },
7558
+ {
7559
+ key: "p95",
7560
+ label: "P95 (ms)",
7561
+ format: (data) => data.p95.toFixed(2),
7562
+ isVisible: () => true,
7563
+ },
7564
+ {
7565
+ key: "p99",
7566
+ label: "P99 (ms)",
7567
+ format: (data) => data.p99.toFixed(2),
7568
+ isVisible: () => true,
7569
+ },
7570
+ {
7571
+ key: "avgWaitTime",
7572
+ label: "Avg Wait (ms)",
7573
+ format: (data) => data.avgWaitTime.toFixed(2),
7574
+ isVisible: () => true,
7575
+ },
7576
+ {
7577
+ key: "minWaitTime",
7578
+ label: "Min Wait (ms)",
7579
+ format: (data) => data.minWaitTime.toFixed(2),
7580
+ isVisible: () => true,
7581
+ },
7582
+ {
7583
+ key: "maxWaitTime",
7584
+ label: "Max Wait (ms)",
7585
+ format: (data) => data.maxWaitTime.toFixed(2),
7586
+ isVisible: () => true,
7587
+ },
7588
+ ];
7394
7589
  /** Maximum number of performance events to store per strategy */
7395
- const MAX_EVENTS$2 = 10000;
7590
+ const MAX_EVENTS$3 = 10000;
7396
7591
  /**
7397
7592
  * Storage class for accumulating performance metrics per strategy.
7398
7593
  * Maintains a list of all performance events and provides aggregated statistics.
@@ -7408,10 +7603,10 @@ class PerformanceStorage {
7408
7603
  * @param event - Performance event with timing data
7409
7604
  */
7410
7605
  addEvent(event) {
7411
- this._events.push(event);
7606
+ this._events.unshift(event);
7412
7607
  // Trim queue if exceeded MAX_EVENTS (keep most recent)
7413
- if (this._events.length > MAX_EVENTS$2) {
7414
- this._events.shift();
7608
+ if (this._events.length > MAX_EVENTS$3) {
7609
+ this._events.pop();
7415
7610
  }
7416
7611
  }
7417
7612
  /**
@@ -7506,40 +7701,13 @@ class PerformanceStorage {
7506
7701
  }
7507
7702
  // Sort metrics by total duration (descending) to show bottlenecks first
7508
7703
  const sortedMetrics = Object.values(stats.metricStats).sort((a, b) => b.totalDuration - a.totalDuration);
7509
- // Generate summary table
7510
- const summaryHeader = [
7511
- "Metric Type",
7512
- "Count",
7513
- "Total (ms)",
7514
- "Avg (ms)",
7515
- "Min (ms)",
7516
- "Max (ms)",
7517
- "Std Dev (ms)",
7518
- "Median (ms)",
7519
- "P95 (ms)",
7520
- "P99 (ms)",
7521
- "Avg Wait (ms)",
7522
- "Min Wait (ms)",
7523
- "Max Wait (ms)",
7524
- ];
7525
- const summarySeparator = summaryHeader.map(() => "---");
7526
- const summaryRows = sortedMetrics.map((metric) => [
7527
- metric.metricType,
7528
- metric.count.toString(),
7529
- metric.totalDuration.toFixed(2),
7530
- metric.avgDuration.toFixed(2),
7531
- metric.minDuration.toFixed(2),
7532
- metric.maxDuration.toFixed(2),
7533
- metric.stdDev.toFixed(2),
7534
- metric.median.toFixed(2),
7535
- metric.p95.toFixed(2),
7536
- metric.p99.toFixed(2),
7537
- metric.avgWaitTime.toFixed(2),
7538
- metric.minWaitTime.toFixed(2),
7539
- metric.maxWaitTime.toFixed(2),
7540
- ]);
7541
- const summaryTableData = [summaryHeader, summarySeparator, ...summaryRows];
7542
- const summaryTable = summaryTableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7704
+ // Generate summary table using Column interface
7705
+ const visibleColumns = columns$3.filter((col) => col.isVisible());
7706
+ const header = visibleColumns.map((col) => col.label);
7707
+ const separator = visibleColumns.map(() => "---");
7708
+ const rows = sortedMetrics.map((metric) => visibleColumns.map((col) => col.format(metric)));
7709
+ const tableData = [header, separator, ...rows];
7710
+ const summaryTable = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7543
7711
  // Calculate percentage of total time for each metric
7544
7712
  const percentages = sortedMetrics.map((metric) => {
7545
7713
  const pct = (metric.totalDuration / stats.totalDuration) * 100;
@@ -7774,21 +7942,25 @@ function createStrategyColumns(metric) {
7774
7942
  key: "rank",
7775
7943
  label: "Rank",
7776
7944
  format: (data, index) => `${index + 1}`,
7945
+ isVisible: () => true,
7777
7946
  },
7778
7947
  {
7779
7948
  key: "strategy",
7780
7949
  label: "Strategy",
7781
7950
  format: (data) => data.strategyName,
7951
+ isVisible: () => true,
7782
7952
  },
7783
7953
  {
7784
7954
  key: "metric",
7785
7955
  label: metric,
7786
7956
  format: (data) => formatMetric(data.metricValue),
7957
+ isVisible: () => true,
7787
7958
  },
7788
7959
  {
7789
7960
  key: "totalSignals",
7790
7961
  label: "Total Signals",
7791
7962
  format: (data) => `${data.stats.totalSignals}`,
7963
+ isVisible: () => true,
7792
7964
  },
7793
7965
  {
7794
7966
  key: "winRate",
@@ -7796,6 +7968,7 @@ function createStrategyColumns(metric) {
7796
7968
  format: (data) => data.stats.winRate !== null
7797
7969
  ? `${data.stats.winRate.toFixed(2)}%`
7798
7970
  : "N/A",
7971
+ isVisible: () => true,
7799
7972
  },
7800
7973
  {
7801
7974
  key: "avgPnl",
@@ -7803,6 +7976,7 @@ function createStrategyColumns(metric) {
7803
7976
  format: (data) => data.stats.avgPnl !== null
7804
7977
  ? `${data.stats.avgPnl > 0 ? "+" : ""}${data.stats.avgPnl.toFixed(2)}%`
7805
7978
  : "N/A",
7979
+ isVisible: () => true,
7806
7980
  },
7807
7981
  {
7808
7982
  key: "totalPnl",
@@ -7810,6 +7984,7 @@ function createStrategyColumns(metric) {
7810
7984
  format: (data) => data.stats.totalPnl !== null
7811
7985
  ? `${data.stats.totalPnl > 0 ? "+" : ""}${data.stats.totalPnl.toFixed(2)}%`
7812
7986
  : "N/A",
7987
+ isVisible: () => true,
7813
7988
  },
7814
7989
  {
7815
7990
  key: "sharpeRatio",
@@ -7817,6 +7992,7 @@ function createStrategyColumns(metric) {
7817
7992
  format: (data) => data.stats.sharpeRatio !== null
7818
7993
  ? `${data.stats.sharpeRatio.toFixed(3)}`
7819
7994
  : "N/A",
7995
+ isVisible: () => true,
7820
7996
  },
7821
7997
  {
7822
7998
  key: "stdDev",
@@ -7824,6 +8000,7 @@ function createStrategyColumns(metric) {
7824
8000
  format: (data) => data.stats.stdDev !== null
7825
8001
  ? `${data.stats.stdDev.toFixed(3)}%`
7826
8002
  : "N/A",
8003
+ isVisible: () => true,
7827
8004
  },
7828
8005
  ];
7829
8006
  }
@@ -7836,48 +8013,56 @@ const pnlColumns = [
7836
8013
  key: "strategy",
7837
8014
  label: "Strategy",
7838
8015
  format: (data) => data.strategyName,
8016
+ isVisible: () => true,
7839
8017
  },
7840
8018
  {
7841
8019
  key: "signalId",
7842
8020
  label: "Signal ID",
7843
8021
  format: (data) => data.signalId,
8022
+ isVisible: () => true,
7844
8023
  },
7845
8024
  {
7846
8025
  key: "symbol",
7847
8026
  label: "Symbol",
7848
8027
  format: (data) => data.symbol,
8028
+ isVisible: () => true,
7849
8029
  },
7850
8030
  {
7851
8031
  key: "position",
7852
8032
  label: "Position",
7853
8033
  format: (data) => data.position.toUpperCase(),
8034
+ isVisible: () => true,
7854
8035
  },
7855
8036
  {
7856
8037
  key: "pnl",
7857
8038
  label: "PNL (net)",
7858
8039
  format: (data) => `${data.pnl > 0 ? "+" : ""}${data.pnl.toFixed(2)}%`,
8040
+ isVisible: () => true,
7859
8041
  },
7860
8042
  {
7861
8043
  key: "closeReason",
7862
8044
  label: "Close Reason",
7863
8045
  format: (data) => data.closeReason,
8046
+ isVisible: () => true,
7864
8047
  },
7865
8048
  {
7866
8049
  key: "openTime",
7867
8050
  label: "Open Time",
7868
8051
  format: (data) => new Date(data.openTime).toISOString(),
8052
+ isVisible: () => true,
7869
8053
  },
7870
8054
  {
7871
8055
  key: "closeTime",
7872
8056
  label: "Close Time",
7873
8057
  format: (data) => new Date(data.closeTime).toISOString(),
8058
+ isVisible: () => true,
7874
8059
  },
7875
8060
  ];
7876
8061
  /**
7877
8062
  * Storage class for accumulating walker results.
7878
8063
  * Maintains a list of all strategy results and provides methods to generate reports.
7879
8064
  */
7880
- let ReportStorage$1 = class ReportStorage {
8065
+ let ReportStorage$2 = class ReportStorage {
7881
8066
  constructor(walkerName) {
7882
8067
  this.walkerName = walkerName;
7883
8068
  /** Walker metadata (set from first addResult call) */
@@ -7895,17 +8080,13 @@ let ReportStorage$1 = class ReportStorage {
7895
8080
  * @param data - Walker contract with strategy result
7896
8081
  */
7897
8082
  addResult(data) {
7898
- {
7899
- this._bestMetric = data.bestMetric;
7900
- this._bestStrategy = data.bestStrategy;
7901
- this._totalStrategies = data.totalStrategies;
7902
- }
7903
- // Update best stats only if this strategy is the current best
8083
+ this._totalStrategies = data.totalStrategies;
8084
+ this._bestMetric = data.bestMetric;
8085
+ this._bestStrategy = data.bestStrategy;
7904
8086
  if (data.strategyName === data.bestStrategy) {
7905
8087
  this._bestStats = data.stats;
7906
8088
  }
7907
- // Add strategy result to comparison list
7908
- this._strategyResults.push({
8089
+ this._strategyResults.unshift({
7909
8090
  strategyName: data.strategyName,
7910
8091
  stats: data.stats,
7911
8092
  metricValue: data.metricValue,
@@ -7959,11 +8140,12 @@ let ReportStorage$1 = class ReportStorage {
7959
8140
  const topStrategies = sortedResults.slice(0, topN);
7960
8141
  // Get columns configuration
7961
8142
  const columns = createStrategyColumns(metric);
8143
+ const visibleColumns = columns.filter((col) => col.isVisible());
7962
8144
  // Build table header
7963
- const header = columns.map((col) => col.label);
7964
- const separator = columns.map(() => "---");
8145
+ const header = visibleColumns.map((col) => col.label);
8146
+ const separator = visibleColumns.map(() => "---");
7965
8147
  // Build table rows
7966
- const rows = topStrategies.map((result, index) => columns.map((col) => col.format(result, index)));
8148
+ const rows = topStrategies.map((result, index) => visibleColumns.map((col) => col.format(result, index)));
7967
8149
  const tableData = [header, separator, ...rows];
7968
8150
  return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7969
8151
  }
@@ -7997,10 +8179,11 @@ let ReportStorage$1 = class ReportStorage {
7997
8179
  return "No closed signals available.";
7998
8180
  }
7999
8181
  // Build table header
8000
- const header = pnlColumns.map((col) => col.label);
8001
- const separator = pnlColumns.map(() => "---");
8182
+ const visibleColumns = pnlColumns.filter((col) => col.isVisible());
8183
+ const header = visibleColumns.map((col) => col.label);
8184
+ const separator = visibleColumns.map(() => "---");
8002
8185
  // Build table rows
8003
- const rows = allSignals.map((signal) => pnlColumns.map((col) => col.format(signal)));
8186
+ const rows = allSignals.map((signal) => visibleColumns.map((col) => col.format(signal)));
8004
8187
  const tableData = [header, separator, ...rows];
8005
8188
  return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
8006
8189
  }
@@ -8089,7 +8272,7 @@ class WalkerMarkdownService {
8089
8272
  * Memoized function to get or create ReportStorage for a walker.
8090
8273
  * Each walker gets its own isolated storage instance.
8091
8274
  */
8092
- this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$1(walkerName));
8275
+ this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$2(walkerName));
8093
8276
  /**
8094
8277
  * Processes walker progress events and accumulates strategy results.
8095
8278
  * Should be called from walkerEmitter.
@@ -8260,70 +8443,82 @@ function isUnsafe(value) {
8260
8443
  }
8261
8444
  return false;
8262
8445
  }
8263
- const columns$1 = [
8446
+ const columns$2 = [
8264
8447
  {
8265
8448
  key: "symbol",
8266
8449
  label: "Symbol",
8267
8450
  format: (data) => data.symbol,
8451
+ isVisible: () => true,
8268
8452
  },
8269
8453
  {
8270
8454
  key: "totalPnl",
8271
8455
  label: "Total PNL",
8272
8456
  format: (data) => data.totalPnl !== null ? functoolsKit.str(data.totalPnl, "%+.2f%%") : "N/A",
8457
+ isVisible: () => true,
8273
8458
  },
8274
8459
  {
8275
8460
  key: "sharpeRatio",
8276
8461
  label: "Sharpe",
8277
8462
  format: (data) => data.sharpeRatio !== null ? functoolsKit.str(data.sharpeRatio, "%.2f") : "N/A",
8463
+ isVisible: () => true,
8278
8464
  },
8279
8465
  {
8280
8466
  key: "profitFactor",
8281
8467
  label: "PF",
8282
8468
  format: (data) => data.profitFactor !== null ? functoolsKit.str(data.profitFactor, "%.2f") : "N/A",
8469
+ isVisible: () => true,
8283
8470
  },
8284
8471
  {
8285
8472
  key: "expectancy",
8286
8473
  label: "Expect",
8287
8474
  format: (data) => data.expectancy !== null ? functoolsKit.str(data.expectancy, "%+.2f%%") : "N/A",
8475
+ isVisible: () => true,
8288
8476
  },
8289
8477
  {
8290
8478
  key: "winRate",
8291
8479
  label: "WR",
8292
8480
  format: (data) => data.winRate !== null ? functoolsKit.str(data.winRate, "%.1f%%") : "N/A",
8481
+ isVisible: () => true,
8293
8482
  },
8294
8483
  {
8295
8484
  key: "avgWin",
8296
8485
  label: "Avg Win",
8297
8486
  format: (data) => data.avgWin !== null ? functoolsKit.str(data.avgWin, "%+.2f%%") : "N/A",
8487
+ isVisible: () => true,
8298
8488
  },
8299
8489
  {
8300
8490
  key: "avgLoss",
8301
8491
  label: "Avg Loss",
8302
8492
  format: (data) => data.avgLoss !== null ? functoolsKit.str(data.avgLoss, "%+.2f%%") : "N/A",
8493
+ isVisible: () => true,
8303
8494
  },
8304
8495
  {
8305
8496
  key: "maxDrawdown",
8306
8497
  label: "Max DD",
8307
8498
  format: (data) => data.maxDrawdown !== null ? functoolsKit.str(-data.maxDrawdown, "%.2f%%") : "N/A",
8499
+ isVisible: () => true,
8308
8500
  },
8309
8501
  {
8310
8502
  key: "maxWinStreak",
8311
8503
  label: "W Streak",
8312
8504
  format: (data) => data.maxWinStreak.toString(),
8505
+ isVisible: () => true,
8313
8506
  },
8314
8507
  {
8315
8508
  key: "maxLossStreak",
8316
8509
  label: "L Streak",
8317
8510
  format: (data) => data.maxLossStreak.toString(),
8511
+ isVisible: () => true,
8318
8512
  },
8319
8513
  {
8320
8514
  key: "totalTrades",
8321
8515
  label: "Trades",
8322
8516
  format: (data) => data.totalTrades.toString(),
8517
+ isVisible: () => true,
8323
8518
  },
8324
8519
  ];
8325
8520
  /** Maximum number of signals to store per symbol in heatmap reports */
8326
- const MAX_EVENTS$1 = 250;
8521
+ const MAX_EVENTS$2 = 250;
8327
8522
  /**
8328
8523
  * Storage class for accumulating closed signals per strategy and generating heatmap.
8329
8524
  * Maintains symbol-level statistics and provides portfolio-wide metrics.
@@ -8344,10 +8539,10 @@ class HeatmapStorage {
8344
8539
  this.symbolData.set(symbol, []);
8345
8540
  }
8346
8541
  const signals = this.symbolData.get(symbol);
8347
- signals.push(data);
8542
+ signals.unshift(data);
8348
8543
  // Trim queue if exceeded MAX_EVENTS per symbol
8349
- if (signals.length > MAX_EVENTS$1) {
8350
- signals.shift();
8544
+ if (signals.length > MAX_EVENTS$2) {
8545
+ signals.pop();
8351
8546
  }
8352
8547
  }
8353
8548
  /**
@@ -8567,9 +8762,10 @@ class HeatmapStorage {
8567
8762
  "*No data available*"
8568
8763
  ].join("\n");
8569
8764
  }
8570
- const header = columns$1.map((col) => col.label);
8571
- const separator = columns$1.map(() => "---");
8572
- const rows = data.symbols.map((row) => columns$1.map((col) => col.format(row)));
8765
+ const visibleColumns = columns$2.filter((col) => col.isVisible());
8766
+ const header = visibleColumns.map((col) => col.label);
8767
+ const separator = visibleColumns.map(() => "---");
8768
+ const rows = data.symbols.map((row) => visibleColumns.map((col) => col.format(row)));
8573
8769
  const tableData = [header, separator, ...rows];
8574
8770
  const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
8575
8771
  return [
@@ -10607,7 +10803,7 @@ const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, back
10607
10803
  revenuePercent,
10608
10804
  backtest,
10609
10805
  });
10610
- await self.params.onProfit(symbol, data, currentPrice, level, backtest, when.getTime());
10806
+ await self.params.onProfit(symbol, data.strategyName, data.exchangeName, data, currentPrice, level, backtest, when.getTime());
10611
10807
  }
10612
10808
  }
10613
10809
  if (shouldPersist) {
@@ -10654,7 +10850,7 @@ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest,
10654
10850
  lossPercent,
10655
10851
  backtest,
10656
10852
  });
10657
- await self.params.onLoss(symbol, data, currentPrice, level, backtest, when.getTime());
10853
+ await self.params.onLoss(symbol, data.strategyName, data.exchangeName, data, currentPrice, level, backtest, when.getTime());
10658
10854
  }
10659
10855
  }
10660
10856
  if (shouldPersist) {
@@ -10925,6 +11121,7 @@ class ClientPartial {
10925
11121
  symbol,
10926
11122
  data,
10927
11123
  priceClose,
11124
+ backtest,
10928
11125
  });
10929
11126
  if (this._states === NEED_FETCH) {
10930
11127
  throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
@@ -10941,14 +11138,18 @@ class ClientPartial {
10941
11138
  * Emits PartialProfitContract event to all subscribers.
10942
11139
  *
10943
11140
  * @param symbol - Trading pair symbol
11141
+ * @param strategyName - Strategy name that generated this signal
11142
+ * @param exchangeName - Exchange name where this signal is being executed
10944
11143
  * @param data - Signal row data
10945
11144
  * @param currentPrice - Current market price
10946
11145
  * @param level - Profit level reached
10947
11146
  * @param backtest - True if backtest mode
10948
11147
  * @param timestamp - Event timestamp in milliseconds
10949
11148
  */
10950
- const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialProfitSubject.next({
11149
+ const COMMIT_PROFIT_FN = async (symbol, strategyName, exchangeName, data, currentPrice, level, backtest, timestamp) => await partialProfitSubject.next({
10951
11150
  symbol,
11151
+ strategyName,
11152
+ exchangeName,
10952
11153
  data,
10953
11154
  currentPrice,
10954
11155
  level,
@@ -10962,14 +11163,18 @@ const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, tim
10962
11163
  * Emits PartialLossContract event to all subscribers.
10963
11164
  *
10964
11165
  * @param symbol - Trading pair symbol
11166
+ * @param strategyName - Strategy name that generated this signal
11167
+ * @param exchangeName - Exchange name where this signal is being executed
10965
11168
  * @param data - Signal row data
10966
11169
  * @param currentPrice - Current market price
10967
11170
  * @param level - Loss level reached
10968
11171
  * @param backtest - True if backtest mode
10969
11172
  * @param timestamp - Event timestamp in milliseconds
10970
11173
  */
10971
- const COMMIT_LOSS_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialLossSubject.next({
11174
+ const COMMIT_LOSS_FN = async (symbol, strategyName, exchangeName, data, currentPrice, level, backtest, timestamp) => await partialLossSubject.next({
10972
11175
  symbol,
11176
+ strategyName,
11177
+ exchangeName,
10973
11178
  data,
10974
11179
  currentPrice,
10975
11180
  level,
@@ -11115,60 +11320,69 @@ class PartialConnectionService {
11115
11320
  }
11116
11321
  }
11117
11322
 
11118
- const columns = [
11323
+ const columns$1 = [
11119
11324
  {
11120
11325
  key: "action",
11121
11326
  label: "Action",
11122
11327
  format: (data) => data.action.toUpperCase(),
11328
+ isVisible: () => true,
11123
11329
  },
11124
11330
  {
11125
11331
  key: "symbol",
11126
11332
  label: "Symbol",
11127
11333
  format: (data) => data.symbol,
11334
+ isVisible: () => true,
11128
11335
  },
11129
11336
  {
11130
11337
  key: "strategyName",
11131
11338
  label: "Strategy",
11132
11339
  format: (data) => data.strategyName,
11340
+ isVisible: () => true,
11133
11341
  },
11134
11342
  {
11135
11343
  key: "signalId",
11136
11344
  label: "Signal ID",
11137
11345
  format: (data) => data.signalId,
11346
+ isVisible: () => true,
11138
11347
  },
11139
11348
  {
11140
11349
  key: "position",
11141
11350
  label: "Position",
11142
11351
  format: (data) => data.position.toUpperCase(),
11352
+ isVisible: () => true,
11143
11353
  },
11144
11354
  {
11145
11355
  key: "level",
11146
11356
  label: "Level %",
11147
11357
  format: (data) => data.action === "profit" ? `+${data.level}%` : `-${data.level}%`,
11358
+ isVisible: () => true,
11148
11359
  },
11149
11360
  {
11150
11361
  key: "currentPrice",
11151
11362
  label: "Current Price",
11152
11363
  format: (data) => `${data.currentPrice.toFixed(8)} USD`,
11364
+ isVisible: () => true,
11153
11365
  },
11154
11366
  {
11155
11367
  key: "timestamp",
11156
11368
  label: "Timestamp",
11157
11369
  format: (data) => new Date(data.timestamp).toISOString(),
11370
+ isVisible: () => true,
11158
11371
  },
11159
11372
  {
11160
11373
  key: "mode",
11161
11374
  label: "Mode",
11162
11375
  format: (data) => (data.backtest ? "Backtest" : "Live"),
11376
+ isVisible: () => true,
11163
11377
  },
11164
11378
  ];
11165
11379
  /** Maximum number of events to store in partial reports */
11166
- const MAX_EVENTS = 250;
11380
+ const MAX_EVENTS$1 = 250;
11167
11381
  /**
11168
11382
  * Storage class for accumulating partial profit/loss events per symbol-strategy pair.
11169
11383
  * Maintains a chronological list of profit and loss level events.
11170
11384
  */
11171
- class ReportStorage {
11385
+ let ReportStorage$1 = class ReportStorage {
11172
11386
  constructor() {
11173
11387
  /** Internal list of all partial events for this symbol */
11174
11388
  this._eventList = [];
@@ -11182,7 +11396,7 @@ class ReportStorage {
11182
11396
  * @param backtest - True if backtest mode
11183
11397
  */
11184
11398
  addProfitEvent(data, currentPrice, level, backtest, timestamp) {
11185
- this._eventList.push({
11399
+ this._eventList.unshift({
11186
11400
  timestamp,
11187
11401
  action: "profit",
11188
11402
  symbol: data.symbol,
@@ -11194,8 +11408,8 @@ class ReportStorage {
11194
11408
  backtest,
11195
11409
  });
11196
11410
  // Trim queue if exceeded MAX_EVENTS
11197
- if (this._eventList.length > MAX_EVENTS) {
11198
- this._eventList.shift();
11411
+ if (this._eventList.length > MAX_EVENTS$1) {
11412
+ this._eventList.pop();
11199
11413
  }
11200
11414
  }
11201
11415
  /**
@@ -11207,7 +11421,7 @@ class ReportStorage {
11207
11421
  * @param backtest - True if backtest mode
11208
11422
  */
11209
11423
  addLossEvent(data, currentPrice, level, backtest, timestamp) {
11210
- this._eventList.push({
11424
+ this._eventList.unshift({
11211
11425
  timestamp,
11212
11426
  action: "loss",
11213
11427
  symbol: data.symbol,
@@ -11219,8 +11433,8 @@ class ReportStorage {
11219
11433
  backtest,
11220
11434
  });
11221
11435
  // Trim queue if exceeded MAX_EVENTS
11222
- if (this._eventList.length > MAX_EVENTS) {
11223
- this._eventList.shift();
11436
+ if (this._eventList.length > MAX_EVENTS$1) {
11437
+ this._eventList.pop();
11224
11438
  }
11225
11439
  }
11226
11440
  /**
@@ -11262,9 +11476,10 @@ class ReportStorage {
11262
11476
  "No partial profit/loss events recorded yet."
11263
11477
  ].join("\n");
11264
11478
  }
11265
- const header = columns.map((col) => col.label);
11266
- const separator = columns.map(() => "---");
11267
- const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
11479
+ const visibleColumns = columns$1.filter((col) => col.isVisible());
11480
+ const header = visibleColumns.map((col) => col.label);
11481
+ const separator = visibleColumns.map(() => "---");
11482
+ const rows = this._eventList.map((event) => visibleColumns.map((col) => col.format(event)));
11268
11483
  const tableData = [header, separator, ...rows];
11269
11484
  const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
11270
11485
  return [
@@ -11298,7 +11513,7 @@ class ReportStorage {
11298
11513
  console.error(`Failed to save markdown report:`, error);
11299
11514
  }
11300
11515
  }
11301
- }
11516
+ };
11302
11517
  /**
11303
11518
  * Service for generating and saving partial profit/loss markdown reports.
11304
11519
  *
@@ -11328,7 +11543,7 @@ class PartialMarkdownService {
11328
11543
  * Memoized function to get or create ReportStorage for a symbol-strategy pair.
11329
11544
  * Each symbol-strategy combination gets its own isolated storage instance.
11330
11545
  */
11331
- this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage());
11546
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$1());
11332
11547
  /**
11333
11548
  * Processes profit events and accumulates them.
11334
11549
  * Should be called from partialProfitSubject subscription.
@@ -11638,7 +11853,7 @@ class PartialGlobalService {
11638
11853
  * Warning threshold for message size in kilobytes.
11639
11854
  * Messages exceeding this size trigger console warnings.
11640
11855
  */
11641
- const WARN_KB = 100;
11856
+ const WARN_KB = 20;
11642
11857
  /**
11643
11858
  * Internal function for dumping signal data to markdown files.
11644
11859
  * Creates a directory structure with system prompts, user messages, and LLM output.
@@ -11900,6 +12115,372 @@ class ConfigValidationService {
11900
12115
  }
11901
12116
  }
11902
12117
 
12118
+ const columns = [
12119
+ {
12120
+ key: "symbol",
12121
+ label: "Symbol",
12122
+ format: (data) => data.symbol,
12123
+ isVisible: () => true,
12124
+ },
12125
+ {
12126
+ key: "strategyName",
12127
+ label: "Strategy",
12128
+ format: (data) => data.strategyName,
12129
+ isVisible: () => true,
12130
+ },
12131
+ {
12132
+ key: "signalId",
12133
+ label: "Signal ID",
12134
+ format: (data) => data.pendingSignal.id || "N/A",
12135
+ isVisible: () => true,
12136
+ },
12137
+ {
12138
+ key: "position",
12139
+ label: "Position",
12140
+ format: (data) => data.pendingSignal.position.toUpperCase(),
12141
+ isVisible: () => true,
12142
+ },
12143
+ {
12144
+ key: "note",
12145
+ label: "Note",
12146
+ format: (data) => toPlainString(data.pendingSignal.note ?? "N/A"),
12147
+ isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
12148
+ },
12149
+ {
12150
+ key: "exchangeName",
12151
+ label: "Exchange",
12152
+ format: (data) => data.exchangeName,
12153
+ isVisible: () => true,
12154
+ },
12155
+ {
12156
+ key: "openPrice",
12157
+ label: "Open Price",
12158
+ format: (data) => data.pendingSignal.priceOpen !== undefined
12159
+ ? `${data.pendingSignal.priceOpen.toFixed(8)} USD`
12160
+ : "N/A",
12161
+ isVisible: () => true,
12162
+ },
12163
+ {
12164
+ key: "takeProfit",
12165
+ label: "Take Profit",
12166
+ format: (data) => data.pendingSignal.priceTakeProfit !== undefined
12167
+ ? `${data.pendingSignal.priceTakeProfit.toFixed(8)} USD`
12168
+ : "N/A",
12169
+ isVisible: () => true,
12170
+ },
12171
+ {
12172
+ key: "stopLoss",
12173
+ label: "Stop Loss",
12174
+ format: (data) => data.pendingSignal.priceStopLoss !== undefined
12175
+ ? `${data.pendingSignal.priceStopLoss.toFixed(8)} USD`
12176
+ : "N/A",
12177
+ isVisible: () => true,
12178
+ },
12179
+ {
12180
+ key: "currentPrice",
12181
+ label: "Current Price",
12182
+ format: (data) => `${data.currentPrice.toFixed(8)} USD`,
12183
+ isVisible: () => true,
12184
+ },
12185
+ {
12186
+ key: "activePositionCount",
12187
+ label: "Active Positions",
12188
+ format: (data) => data.activePositionCount.toString(),
12189
+ isVisible: () => true,
12190
+ },
12191
+ {
12192
+ key: "comment",
12193
+ label: "Reason",
12194
+ format: (data) => data.comment,
12195
+ isVisible: () => true,
12196
+ },
12197
+ {
12198
+ key: "timestamp",
12199
+ label: "Timestamp",
12200
+ format: (data) => new Date(data.timestamp).toISOString(),
12201
+ isVisible: () => true,
12202
+ },
12203
+ ];
12204
+ /** Maximum number of events to store in risk reports */
12205
+ const MAX_EVENTS = 250;
12206
+ /**
12207
+ * Storage class for accumulating risk rejection events per symbol-strategy pair.
12208
+ * Maintains a chronological list of rejected signals due to risk limits.
12209
+ */
12210
+ class ReportStorage {
12211
+ constructor() {
12212
+ /** Internal list of all risk rejection events for this symbol */
12213
+ this._eventList = [];
12214
+ }
12215
+ /**
12216
+ * Adds a risk rejection event to the storage.
12217
+ *
12218
+ * @param event - Risk rejection event data
12219
+ */
12220
+ addRejectionEvent(event) {
12221
+ this._eventList.unshift(event);
12222
+ // Trim queue if exceeded MAX_EVENTS
12223
+ if (this._eventList.length > MAX_EVENTS) {
12224
+ this._eventList.pop();
12225
+ }
12226
+ }
12227
+ /**
12228
+ * Calculates statistical data from risk rejection events (Controller).
12229
+ *
12230
+ * @returns Statistical data (empty object if no events)
12231
+ */
12232
+ async getData() {
12233
+ if (this._eventList.length === 0) {
12234
+ return {
12235
+ eventList: [],
12236
+ totalRejections: 0,
12237
+ bySymbol: {},
12238
+ byStrategy: {},
12239
+ };
12240
+ }
12241
+ const bySymbol = {};
12242
+ const byStrategy = {};
12243
+ for (const event of this._eventList) {
12244
+ bySymbol[event.symbol] = (bySymbol[event.symbol] || 0) + 1;
12245
+ byStrategy[event.strategyName] = (byStrategy[event.strategyName] || 0) + 1;
12246
+ }
12247
+ return {
12248
+ eventList: this._eventList,
12249
+ totalRejections: this._eventList.length,
12250
+ bySymbol,
12251
+ byStrategy,
12252
+ };
12253
+ }
12254
+ /**
12255
+ * Generates markdown report with all risk rejection events for a symbol-strategy pair (View).
12256
+ *
12257
+ * @param symbol - Trading pair symbol
12258
+ * @param strategyName - Strategy name
12259
+ * @returns Markdown formatted report with all events
12260
+ */
12261
+ async getReport(symbol, strategyName) {
12262
+ const stats = await this.getData();
12263
+ if (stats.totalRejections === 0) {
12264
+ return [
12265
+ `# Risk Rejection Report: ${symbol}:${strategyName}`,
12266
+ "",
12267
+ "No risk rejections recorded yet.",
12268
+ ].join("\n");
12269
+ }
12270
+ const visibleColumns = columns.filter((col) => col.isVisible());
12271
+ const header = visibleColumns.map((col) => col.label);
12272
+ const separator = visibleColumns.map(() => "---");
12273
+ const rows = this._eventList.map((event) => visibleColumns.map((col) => col.format(event)));
12274
+ const tableData = [header, separator, ...rows];
12275
+ const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
12276
+ return [
12277
+ `# Risk Rejection Report: ${symbol}:${strategyName}`,
12278
+ "",
12279
+ table,
12280
+ "",
12281
+ `**Total rejections:** ${stats.totalRejections}`,
12282
+ "",
12283
+ "## Rejections by Symbol",
12284
+ ...Object.entries(stats.bySymbol).map(([sym, count]) => `- ${sym}: ${count}`),
12285
+ "",
12286
+ "## Rejections by Strategy",
12287
+ ...Object.entries(stats.byStrategy).map(([strat, count]) => `- ${strat}: ${count}`),
12288
+ ].join("\n");
12289
+ }
12290
+ /**
12291
+ * Saves symbol-strategy report to disk.
12292
+ *
12293
+ * @param symbol - Trading pair symbol
12294
+ * @param strategyName - Strategy name
12295
+ * @param path - Directory path to save report (default: "./dump/risk")
12296
+ */
12297
+ async dump(symbol, strategyName, path$1 = "./dump/risk") {
12298
+ const markdown = await this.getReport(symbol, strategyName);
12299
+ try {
12300
+ const dir = path.join(process.cwd(), path$1);
12301
+ await fs.mkdir(dir, { recursive: true });
12302
+ const filename = `${symbol}_${strategyName}.md`;
12303
+ const filepath = path.join(dir, filename);
12304
+ await fs.writeFile(filepath, markdown, "utf-8");
12305
+ console.log(`Risk rejection report saved: ${filepath}`);
12306
+ }
12307
+ catch (error) {
12308
+ console.error(`Failed to save markdown report:`, error);
12309
+ }
12310
+ }
12311
+ }
12312
+ /**
12313
+ * Service for generating and saving risk rejection markdown reports.
12314
+ *
12315
+ * Features:
12316
+ * - Listens to risk rejection events via riskSubject
12317
+ * - Accumulates all rejection events per symbol-strategy pair
12318
+ * - Generates markdown tables with detailed rejection information
12319
+ * - Provides statistics (total rejections, by symbol, by strategy)
12320
+ * - Saves reports to disk in dump/risk/{symbol}_{strategyName}.md
12321
+ *
12322
+ * @example
12323
+ * ```typescript
12324
+ * const service = new RiskMarkdownService();
12325
+ *
12326
+ * // Service automatically subscribes to subjects on init
12327
+ * // No manual callback setup needed
12328
+ *
12329
+ * // Later: generate and save report
12330
+ * await service.dump("BTCUSDT", "my-strategy");
12331
+ * ```
12332
+ */
12333
+ class RiskMarkdownService {
12334
+ constructor() {
12335
+ /** Logger service for debug output */
12336
+ this.loggerService = inject(TYPES.loggerService);
12337
+ /**
12338
+ * Memoized function to get or create ReportStorage for a symbol-strategy pair.
12339
+ * Each symbol-strategy combination gets its own isolated storage instance.
12340
+ */
12341
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage());
12342
+ /**
12343
+ * Processes risk rejection events and accumulates them.
12344
+ * Should be called from riskSubject subscription.
12345
+ *
12346
+ * @param data - Risk rejection event data
12347
+ *
12348
+ * @example
12349
+ * ```typescript
12350
+ * const service = new RiskMarkdownService();
12351
+ * // Service automatically subscribes in init()
12352
+ * ```
12353
+ */
12354
+ this.tickRejection = async (data) => {
12355
+ this.loggerService.log("riskMarkdownService tickRejection", {
12356
+ data,
12357
+ });
12358
+ const storage = this.getStorage(data.symbol, data.strategyName);
12359
+ storage.addRejectionEvent(data);
12360
+ };
12361
+ /**
12362
+ * Gets statistical data from all risk rejection events for a symbol-strategy pair.
12363
+ * Delegates to ReportStorage.getData().
12364
+ *
12365
+ * @param symbol - Trading pair symbol to get data for
12366
+ * @param strategyName - Strategy name to get data for
12367
+ * @returns Statistical data object with all metrics
12368
+ *
12369
+ * @example
12370
+ * ```typescript
12371
+ * const service = new RiskMarkdownService();
12372
+ * const stats = await service.getData("BTCUSDT", "my-strategy");
12373
+ * console.log(stats.totalRejections, stats.bySymbol);
12374
+ * ```
12375
+ */
12376
+ this.getData = async (symbol, strategyName) => {
12377
+ this.loggerService.log("riskMarkdownService getData", {
12378
+ symbol,
12379
+ strategyName,
12380
+ });
12381
+ const storage = this.getStorage(symbol, strategyName);
12382
+ return storage.getData();
12383
+ };
12384
+ /**
12385
+ * Generates markdown report with all risk rejection events for a symbol-strategy pair.
12386
+ * Delegates to ReportStorage.getReport().
12387
+ *
12388
+ * @param symbol - Trading pair symbol to generate report for
12389
+ * @param strategyName - Strategy name to generate report for
12390
+ * @returns Markdown formatted report string with table of all events
12391
+ *
12392
+ * @example
12393
+ * ```typescript
12394
+ * const service = new RiskMarkdownService();
12395
+ * const markdown = await service.getReport("BTCUSDT", "my-strategy");
12396
+ * console.log(markdown);
12397
+ * ```
12398
+ */
12399
+ this.getReport = async (symbol, strategyName) => {
12400
+ this.loggerService.log("riskMarkdownService getReport", {
12401
+ symbol,
12402
+ strategyName,
12403
+ });
12404
+ const storage = this.getStorage(symbol, strategyName);
12405
+ return storage.getReport(symbol, strategyName);
12406
+ };
12407
+ /**
12408
+ * Saves symbol-strategy report to disk.
12409
+ * Creates directory if it doesn't exist.
12410
+ * Delegates to ReportStorage.dump().
12411
+ *
12412
+ * @param symbol - Trading pair symbol to save report for
12413
+ * @param strategyName - Strategy name to save report for
12414
+ * @param path - Directory path to save report (default: "./dump/risk")
12415
+ *
12416
+ * @example
12417
+ * ```typescript
12418
+ * const service = new RiskMarkdownService();
12419
+ *
12420
+ * // Save to default path: ./dump/risk/BTCUSDT_my-strategy.md
12421
+ * await service.dump("BTCUSDT", "my-strategy");
12422
+ *
12423
+ * // Save to custom path: ./custom/path/BTCUSDT_my-strategy.md
12424
+ * await service.dump("BTCUSDT", "my-strategy", "./custom/path");
12425
+ * ```
12426
+ */
12427
+ this.dump = async (symbol, strategyName, path = "./dump/risk") => {
12428
+ this.loggerService.log("riskMarkdownService dump", {
12429
+ symbol,
12430
+ strategyName,
12431
+ path,
12432
+ });
12433
+ const storage = this.getStorage(symbol, strategyName);
12434
+ await storage.dump(symbol, strategyName, path);
12435
+ };
12436
+ /**
12437
+ * Clears accumulated event data from storage.
12438
+ * If ctx is provided, clears only that specific symbol-strategy pair's data.
12439
+ * If nothing is provided, clears all data.
12440
+ *
12441
+ * @param ctx - Optional context with symbol and strategyName
12442
+ *
12443
+ * @example
12444
+ * ```typescript
12445
+ * const service = new RiskMarkdownService();
12446
+ *
12447
+ * // Clear specific symbol-strategy pair
12448
+ * await service.clear({ symbol: "BTCUSDT", strategyName: "my-strategy" });
12449
+ *
12450
+ * // Clear all data
12451
+ * await service.clear();
12452
+ * ```
12453
+ */
12454
+ this.clear = async (ctx) => {
12455
+ this.loggerService.log("riskMarkdownService clear", {
12456
+ ctx,
12457
+ });
12458
+ if (ctx) {
12459
+ const key = `${ctx.symbol}:${ctx.strategyName}`;
12460
+ this.getStorage.clear(key);
12461
+ }
12462
+ else {
12463
+ this.getStorage.clear();
12464
+ }
12465
+ };
12466
+ /**
12467
+ * Initializes the service by subscribing to risk rejection events.
12468
+ * Uses singleshot to ensure initialization happens only once.
12469
+ * Automatically called on first use.
12470
+ *
12471
+ * @example
12472
+ * ```typescript
12473
+ * const service = new RiskMarkdownService();
12474
+ * await service.init(); // Subscribe to rejection events
12475
+ * ```
12476
+ */
12477
+ this.init = functoolsKit.singleshot(async () => {
12478
+ this.loggerService.log("riskMarkdownService init");
12479
+ riskSubject.subscribe(this.tickRejection);
12480
+ });
12481
+ }
12482
+ }
12483
+
11903
12484
  {
11904
12485
  provide(TYPES.loggerService, () => new LoggerService());
11905
12486
  }
@@ -11960,6 +12541,7 @@ class ConfigValidationService {
11960
12541
  provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
11961
12542
  provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
11962
12543
  provide(TYPES.outlineMarkdownService, () => new OutlineMarkdownService());
12544
+ provide(TYPES.riskMarkdownService, () => new RiskMarkdownService());
11963
12545
  }
11964
12546
  {
11965
12547
  provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
@@ -12035,6 +12617,7 @@ const markdownServices = {
12035
12617
  heatMarkdownService: inject(TYPES.heatMarkdownService),
12036
12618
  partialMarkdownService: inject(TYPES.partialMarkdownService),
12037
12619
  outlineMarkdownService: inject(TYPES.outlineMarkdownService),
12620
+ riskMarkdownService: inject(TYPES.riskMarkdownService),
12038
12621
  };
12039
12622
  const validationServices = {
12040
12623
  exchangeValidationService: inject(TYPES.exchangeValidationService),
@@ -12809,6 +13392,8 @@ const LISTEN_PARTIAL_PROFIT_METHOD_NAME = "event.listenPartialProfit";
12809
13392
  const LISTEN_PARTIAL_PROFIT_ONCE_METHOD_NAME = "event.listenPartialProfitOnce";
12810
13393
  const LISTEN_PARTIAL_LOSS_METHOD_NAME = "event.listenPartialLoss";
12811
13394
  const LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME = "event.listenPartialLossOnce";
13395
+ const LISTEN_RISK_METHOD_NAME = "event.listenRisk";
13396
+ const LISTEN_RISK_ONCE_METHOD_NAME = "event.listenRiskOnce";
12812
13397
  /**
12813
13398
  * Subscribes to all signal events with queued async processing.
12814
13399
  *
@@ -13607,6 +14192,75 @@ function listenPartialLossOnce(filterFn, fn) {
13607
14192
  backtest$1.loggerService.log(LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME);
13608
14193
  return partialLossSubject.filter(filterFn).once(fn);
13609
14194
  }
14195
+ /**
14196
+ * Subscribes to risk rejection events with queued async processing.
14197
+ *
14198
+ * Emits ONLY when a signal is rejected due to risk validation failure.
14199
+ * Does not emit for allowed signals (prevents spam).
14200
+ * Events are processed sequentially in order received, even if callback is async.
14201
+ * Uses queued wrapper to prevent concurrent execution of the callback.
14202
+ *
14203
+ * @param fn - Callback function to handle risk rejection events
14204
+ * @returns Unsubscribe function to stop listening to events
14205
+ *
14206
+ * @example
14207
+ * ```typescript
14208
+ * import { listenRisk } from "./function/event";
14209
+ *
14210
+ * const unsubscribe = listenRisk((event) => {
14211
+ * console.log(`[RISK REJECTED] Signal for ${event.symbol}`);
14212
+ * console.log(`Strategy: ${event.strategyName}`);
14213
+ * console.log(`Position: ${event.pendingSignal.position}`);
14214
+ * console.log(`Active positions: ${event.activePositionCount}`);
14215
+ * console.log(`Reason: ${event.comment}`);
14216
+ * console.log(`Price: ${event.currentPrice}`);
14217
+ * });
14218
+ *
14219
+ * // Later: stop listening
14220
+ * unsubscribe();
14221
+ * ```
14222
+ */
14223
+ function listenRisk(fn) {
14224
+ backtest$1.loggerService.log(LISTEN_RISK_METHOD_NAME);
14225
+ return riskSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
14226
+ }
14227
+ /**
14228
+ * Subscribes to filtered risk rejection events with one-time execution.
14229
+ *
14230
+ * Listens for events matching the filter predicate, then executes callback once
14231
+ * and automatically unsubscribes. Useful for waiting for specific risk rejection conditions.
14232
+ *
14233
+ * @param filterFn - Predicate to filter which events trigger the callback
14234
+ * @param fn - Callback function to handle the filtered event (called only once)
14235
+ * @returns Unsubscribe function to cancel the listener before it fires
14236
+ *
14237
+ * @example
14238
+ * ```typescript
14239
+ * import { listenRiskOnce } from "./function/event";
14240
+ *
14241
+ * // Wait for first risk rejection on BTCUSDT
14242
+ * listenRiskOnce(
14243
+ * (event) => event.symbol === "BTCUSDT",
14244
+ * (event) => {
14245
+ * console.log("BTCUSDT signal rejected!");
14246
+ * console.log("Reason:", event.comment);
14247
+ * }
14248
+ * );
14249
+ *
14250
+ * // Wait for rejection due to position limit
14251
+ * const cancel = listenRiskOnce(
14252
+ * (event) => event.comment.includes("Max") && event.activePositionCount >= 3,
14253
+ * (event) => console.log("Position limit reached:", event.activePositionCount)
14254
+ * );
14255
+ *
14256
+ * // Cancel if needed before event fires
14257
+ * cancel();
14258
+ * ```
14259
+ */
14260
+ function listenRiskOnce(filterFn, fn) {
14261
+ backtest$1.loggerService.log(LISTEN_RISK_ONCE_METHOD_NAME);
14262
+ return riskSubject.filter(filterFn).once(fn);
14263
+ }
13610
14264
 
13611
14265
  const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
13612
14266
  const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
@@ -16231,6 +16885,186 @@ class ConstantUtils {
16231
16885
  */
16232
16886
  const Constant = new ConstantUtils();
16233
16887
 
16888
+ const RISK_METHOD_NAME_GET_DATA = "RiskUtils.getData";
16889
+ const RISK_METHOD_NAME_GET_REPORT = "RiskUtils.getReport";
16890
+ const RISK_METHOD_NAME_DUMP = "RiskUtils.dump";
16891
+ /**
16892
+ * Utility class for accessing risk rejection reports and statistics.
16893
+ *
16894
+ * Provides static-like methods (via singleton instance) to retrieve data
16895
+ * accumulated by RiskMarkdownService from risk rejection events.
16896
+ *
16897
+ * Features:
16898
+ * - Statistical data extraction (total rejections count, by symbol, by strategy)
16899
+ * - Markdown report generation with event tables
16900
+ * - File export to disk
16901
+ *
16902
+ * Data source:
16903
+ * - RiskMarkdownService listens to riskSubject
16904
+ * - Accumulates rejection events in ReportStorage (max 250 events per symbol-strategy pair)
16905
+ * - Events include: timestamp, symbol, strategyName, position, exchangeName, price, activePositionCount, comment
16906
+ *
16907
+ * @example
16908
+ * ```typescript
16909
+ * import { Risk } from "./classes/Risk";
16910
+ *
16911
+ * // Get statistical data for BTCUSDT:my-strategy
16912
+ * const stats = await Risk.getData("BTCUSDT", "my-strategy");
16913
+ * console.log(`Total rejections: ${stats.totalRejections}`);
16914
+ * console.log(`By symbol:`, stats.bySymbol);
16915
+ * console.log(`By strategy:`, stats.byStrategy);
16916
+ *
16917
+ * // Generate markdown report
16918
+ * const markdown = await Risk.getReport("BTCUSDT", "my-strategy");
16919
+ * console.log(markdown); // Formatted table with all rejection events
16920
+ *
16921
+ * // Export report to file
16922
+ * await Risk.dump("BTCUSDT", "my-strategy"); // Saves to ./dump/risk/BTCUSDT_my-strategy.md
16923
+ * await Risk.dump("BTCUSDT", "my-strategy", "./custom/path"); // Custom directory
16924
+ * ```
16925
+ */
16926
+ class RiskUtils {
16927
+ constructor() {
16928
+ /**
16929
+ * Retrieves statistical data from accumulated risk rejection events.
16930
+ *
16931
+ * Delegates to RiskMarkdownService.getData() which reads from ReportStorage.
16932
+ * Returns aggregated metrics calculated from all rejection events.
16933
+ *
16934
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16935
+ * @param strategyName - Strategy name (e.g., "my-strategy")
16936
+ * @returns Promise resolving to RiskStatistics object with counts and event list
16937
+ *
16938
+ * @example
16939
+ * ```typescript
16940
+ * const stats = await Risk.getData("BTCUSDT", "my-strategy");
16941
+ *
16942
+ * console.log(`Total rejections: ${stats.totalRejections}`);
16943
+ * console.log(`Rejections by symbol:`, stats.bySymbol);
16944
+ * console.log(`Rejections by strategy:`, stats.byStrategy);
16945
+ *
16946
+ * // Iterate through all rejection events
16947
+ * for (const event of stats.eventList) {
16948
+ * console.log(`REJECTED: ${event.symbol} - ${event.comment} (${event.activePositionCount} active)`);
16949
+ * }
16950
+ * ```
16951
+ */
16952
+ this.getData = async (symbol, strategyName) => {
16953
+ backtest$1.loggerService.info(RISK_METHOD_NAME_GET_DATA, { symbol, strategyName });
16954
+ backtest$1.strategyValidationService.validate(strategyName, RISK_METHOD_NAME_GET_DATA);
16955
+ {
16956
+ const { riskName } = backtest$1.strategySchemaService.get(strategyName);
16957
+ riskName && backtest$1.riskValidationService.validate(riskName, RISK_METHOD_NAME_GET_DATA);
16958
+ }
16959
+ return await backtest$1.riskMarkdownService.getData(symbol, strategyName);
16960
+ };
16961
+ /**
16962
+ * Generates markdown report with all risk rejection events for a symbol-strategy pair.
16963
+ *
16964
+ * Creates formatted table containing:
16965
+ * - Symbol
16966
+ * - Strategy
16967
+ * - Position (LONG/SHORT)
16968
+ * - Exchange
16969
+ * - Price
16970
+ * - Active Positions (at rejection time)
16971
+ * - Reason (from validation note)
16972
+ * - Timestamp (ISO 8601)
16973
+ *
16974
+ * Also includes summary statistics at the end (total rejections, by symbol, by strategy).
16975
+ *
16976
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16977
+ * @param strategyName - Strategy name (e.g., "my-strategy")
16978
+ * @returns Promise resolving to markdown formatted report string
16979
+ *
16980
+ * @example
16981
+ * ```typescript
16982
+ * const markdown = await Risk.getReport("BTCUSDT", "my-strategy");
16983
+ * console.log(markdown);
16984
+ *
16985
+ * // Output:
16986
+ * // # Risk Rejection Report: BTCUSDT:my-strategy
16987
+ * //
16988
+ * // | Symbol | Strategy | Position | Exchange | Price | Active Positions | Reason | Timestamp |
16989
+ * // | --- | --- | --- | --- | --- | --- | --- | --- |
16990
+ * // | BTCUSDT | my-strategy | LONG | binance | 50000.00000000 USD | 3 | Max 3 positions allowed | 2024-01-15T10:30:00.000Z |
16991
+ * //
16992
+ * // **Total rejections:** 1
16993
+ * //
16994
+ * // ## Rejections by Symbol
16995
+ * // - BTCUSDT: 1
16996
+ * //
16997
+ * // ## Rejections by Strategy
16998
+ * // - my-strategy: 1
16999
+ * ```
17000
+ */
17001
+ this.getReport = async (symbol, strategyName) => {
17002
+ backtest$1.loggerService.info(RISK_METHOD_NAME_GET_REPORT, { symbol, strategyName });
17003
+ backtest$1.strategyValidationService.validate(strategyName, RISK_METHOD_NAME_GET_REPORT);
17004
+ {
17005
+ const { riskName } = backtest$1.strategySchemaService.get(strategyName);
17006
+ riskName && backtest$1.riskValidationService.validate(riskName, RISK_METHOD_NAME_GET_REPORT);
17007
+ }
17008
+ return await backtest$1.riskMarkdownService.getReport(symbol, strategyName);
17009
+ };
17010
+ /**
17011
+ * Generates and saves markdown report to file.
17012
+ *
17013
+ * Creates directory if it doesn't exist.
17014
+ * Filename format: {symbol}_{strategyName}.md (e.g., "BTCUSDT_my-strategy.md")
17015
+ *
17016
+ * Delegates to RiskMarkdownService.dump() which:
17017
+ * 1. Generates markdown report via getReport()
17018
+ * 2. Creates output directory (recursive mkdir)
17019
+ * 3. Writes file with UTF-8 encoding
17020
+ * 4. Logs success/failure to console
17021
+ *
17022
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
17023
+ * @param strategyName - Strategy name (e.g., "my-strategy")
17024
+ * @param path - Output directory path (default: "./dump/risk")
17025
+ * @returns Promise that resolves when file is written
17026
+ *
17027
+ * @example
17028
+ * ```typescript
17029
+ * // Save to default path: ./dump/risk/BTCUSDT_my-strategy.md
17030
+ * await Risk.dump("BTCUSDT", "my-strategy");
17031
+ *
17032
+ * // Save to custom path: ./reports/risk/BTCUSDT_my-strategy.md
17033
+ * await Risk.dump("BTCUSDT", "my-strategy", "./reports/risk");
17034
+ *
17035
+ * // After multiple symbols backtested, export all risk reports
17036
+ * for (const symbol of ["BTCUSDT", "ETHUSDT", "BNBUSDT"]) {
17037
+ * await Risk.dump(symbol, "my-strategy", "./backtest-results");
17038
+ * }
17039
+ * ```
17040
+ */
17041
+ this.dump = async (symbol, strategyName, path) => {
17042
+ backtest$1.loggerService.info(RISK_METHOD_NAME_DUMP, { symbol, strategyName, path });
17043
+ backtest$1.strategyValidationService.validate(strategyName, RISK_METHOD_NAME_DUMP);
17044
+ {
17045
+ const { riskName } = backtest$1.strategySchemaService.get(strategyName);
17046
+ riskName && backtest$1.riskValidationService.validate(riskName, RISK_METHOD_NAME_DUMP);
17047
+ }
17048
+ await backtest$1.riskMarkdownService.dump(symbol, strategyName, path);
17049
+ };
17050
+ }
17051
+ }
17052
+ /**
17053
+ * Global singleton instance of RiskUtils.
17054
+ * Provides static-like access to risk rejection reporting methods.
17055
+ *
17056
+ * @example
17057
+ * ```typescript
17058
+ * import { Risk } from "backtest-kit";
17059
+ *
17060
+ * // Usage same as RiskUtils methods
17061
+ * const stats = await Risk.getData("BTCUSDT", "my-strategy");
17062
+ * const report = await Risk.getReport("BTCUSDT", "my-strategy");
17063
+ * await Risk.dump("BTCUSDT", "my-strategy");
17064
+ * ```
17065
+ */
17066
+ const Risk = new RiskUtils();
17067
+
16234
17068
  exports.Backtest = Backtest;
16235
17069
  exports.Constant = Constant;
16236
17070
  exports.ExecutionContextService = ExecutionContextService;
@@ -16246,6 +17080,7 @@ exports.PersistRiskAdapter = PersistRiskAdapter;
16246
17080
  exports.PersistScheduleAdapter = PersistScheduleAdapter;
16247
17081
  exports.PersistSignalAdapter = PersistSignalAdapter;
16248
17082
  exports.PositionSize = PositionSize;
17083
+ exports.Risk = Risk;
16249
17084
  exports.Schedule = Schedule;
16250
17085
  exports.Walker = Walker;
16251
17086
  exports.addExchange = addExchange;
@@ -16288,6 +17123,8 @@ exports.listenPartialLossOnce = listenPartialLossOnce;
16288
17123
  exports.listenPartialProfit = listenPartialProfit;
16289
17124
  exports.listenPartialProfitOnce = listenPartialProfitOnce;
16290
17125
  exports.listenPerformance = listenPerformance;
17126
+ exports.listenRisk = listenRisk;
17127
+ exports.listenRiskOnce = listenRiskOnce;
16291
17128
  exports.listenSignal = listenSignal;
16292
17129
  exports.listenSignalBacktest = listenSignalBacktest;
16293
17130
  exports.listenSignalBacktestOnce = listenSignalBacktestOnce;