backtest-kit 1.5.15 → 1.5.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -202,6 +202,7 @@ const markdownServices$1 = {
202
202
  heatMarkdownService: Symbol('heatMarkdownService'),
203
203
  partialMarkdownService: Symbol('partialMarkdownService'),
204
204
  outlineMarkdownService: Symbol('outlineMarkdownService'),
205
+ riskMarkdownService: Symbol('riskMarkdownService'),
205
206
  };
206
207
  const validationServices$1 = {
207
208
  exchangeValidationService: Symbol('exchangeValidationService'),
@@ -442,11 +443,12 @@ const GET_CANDLES_FN = async (dto, since, self) => {
442
443
  }
443
444
  catch (err) {
444
445
  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, {
446
+ const payload = {
446
447
  error: functoolsKit.errorData(err),
447
448
  message: functoolsKit.getErrorMessage(err),
448
- });
449
- console.warn(message);
449
+ };
450
+ self.params.logger.warn(message, payload);
451
+ console.warn(message, payload);
450
452
  lastError = err;
451
453
  await functoolsKit.sleep(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS);
452
454
  }
@@ -1725,6 +1727,12 @@ const partialProfitSubject = new functoolsKit.Subject();
1725
1727
  * Emits when a signal reaches a loss level (10%, 20%, 30%, etc).
1726
1728
  */
1727
1729
  const partialLossSubject = new functoolsKit.Subject();
1730
+ /**
1731
+ * Risk rejection emitter for risk management violations.
1732
+ * Emits ONLY when a signal is rejected due to risk validation failure.
1733
+ * Does not emit for allowed signals (prevents spam).
1734
+ */
1735
+ const riskSubject = new functoolsKit.Subject();
1728
1736
 
1729
1737
  var emitters = /*#__PURE__*/Object.freeze({
1730
1738
  __proto__: null,
@@ -1739,6 +1747,7 @@ var emitters = /*#__PURE__*/Object.freeze({
1739
1747
  progressBacktestEmitter: progressBacktestEmitter,
1740
1748
  progressOptimizerEmitter: progressOptimizerEmitter,
1741
1749
  progressWalkerEmitter: progressWalkerEmitter,
1750
+ riskSubject: riskSubject,
1742
1751
  signalBacktestEmitter: signalBacktestEmitter,
1743
1752
  signalEmitter: signalEmitter,
1744
1753
  signalLiveEmitter: signalLiveEmitter,
@@ -1825,6 +1834,9 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1825
1834
  if (signal.position === undefined || signal.position === null) {
1826
1835
  errors.push('position is required and must be "long" or "short"');
1827
1836
  }
1837
+ if (signal.position !== "long" && signal.position !== "short") {
1838
+ errors.push(`position must be "long" or "short", got "${signal.position}"`);
1839
+ }
1828
1840
  // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
1829
1841
  if (!isFinite(currentPrice)) {
1830
1842
  errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
@@ -2111,10 +2123,13 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
2111
2123
  }, {
2112
2124
  defaultValue: null,
2113
2125
  fallback: (error) => {
2114
- backtest$1.loggerService.warn("ClientStrategy exception thrown", {
2126
+ const message = "ClientStrategy exception thrown";
2127
+ const payload = {
2115
2128
  error: functoolsKit.errorData(error),
2116
2129
  message: functoolsKit.getErrorMessage(error),
2117
- });
2130
+ };
2131
+ backtest$1.loggerService.warn(message, payload);
2132
+ console.warn(message, payload);
2118
2133
  errorEmitter.next(error);
2119
2134
  },
2120
2135
  });
@@ -2805,7 +2820,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2805
2820
  // Moving towards TP
2806
2821
  const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2807
2822
  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);
2823
+ 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
2824
  if (self.params.callbacks?.onPartialProfit) {
2810
2825
  self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2811
2826
  }
@@ -2814,7 +2829,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2814
2829
  // Moving towards SL
2815
2830
  const slDistance = signal.priceOpen - signal.priceStopLoss;
2816
2831
  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);
2832
+ 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
2833
  if (self.params.callbacks?.onPartialLoss) {
2819
2834
  self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2820
2835
  }
@@ -2827,7 +2842,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2827
2842
  // Moving towards TP
2828
2843
  const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2829
2844
  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);
2845
+ 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
2846
  if (self.params.callbacks?.onPartialProfit) {
2832
2847
  self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2833
2848
  }
@@ -2836,7 +2851,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2836
2851
  // Moving towards SL
2837
2852
  const slDistance = signal.priceStopLoss - signal.priceOpen;
2838
2853
  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);
2854
+ 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
2855
  if (self.params.callbacks?.onPartialLoss) {
2841
2856
  self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2842
2857
  }
@@ -3791,10 +3806,13 @@ const DO_VALIDATION_FN = functoolsKit.trycatch(async (validation, params) => {
3791
3806
  }, {
3792
3807
  defaultValue: false,
3793
3808
  fallback: (error) => {
3794
- backtest$1.loggerService.warn("ClientRisk exception thrown", {
3809
+ const message = "ClientRisk exception thrown";
3810
+ const payload = {
3795
3811
  error: functoolsKit.errorData(error),
3796
3812
  message: functoolsKit.getErrorMessage(error),
3797
- });
3813
+ };
3814
+ backtest$1.loggerService.warn(message, payload);
3815
+ console.warn(message, payload);
3798
3816
  validationSubject.next(error);
3799
3817
  },
3800
3818
  });
@@ -3864,17 +3882,25 @@ class ClientRisk {
3864
3882
  };
3865
3883
  // Execute custom validations
3866
3884
  let isValid = true;
3885
+ let rejectionNote = "N/A";
3867
3886
  if (this.params.validations) {
3868
3887
  for (const validation of this.params.validations) {
3869
3888
  if (functoolsKit.not(await DO_VALIDATION_FN(typeof validation === "function"
3870
3889
  ? validation
3871
3890
  : validation.validate, payload))) {
3872
3891
  isValid = false;
3892
+ // Capture note from validation if available
3893
+ if (typeof validation !== "function" && validation.note) {
3894
+ rejectionNote = validation.note;
3895
+ }
3873
3896
  break;
3874
3897
  }
3875
3898
  }
3876
3899
  }
3877
3900
  if (!isValid) {
3901
+ // Call params.onRejected for riskSubject emission
3902
+ await this.params.onRejected(params.symbol, params, riskMap.size, rejectionNote, Date.now());
3903
+ // Call schema callbacks.onRejected if defined
3878
3904
  if (this.params.callbacks?.onRejected) {
3879
3905
  this.params.callbacks.onRejected(params.symbol, params);
3880
3906
  }
@@ -3937,6 +3963,28 @@ class ClientRisk {
3937
3963
  }
3938
3964
  }
3939
3965
 
3966
+ /**
3967
+ * Callback function for emitting risk rejection events to riskSubject.
3968
+ *
3969
+ * Called by ClientRisk when a signal is rejected due to risk validation failure.
3970
+ * Emits RiskContract event to all subscribers.
3971
+ *
3972
+ * @param symbol - Trading pair symbol
3973
+ * @param params - Risk check arguments
3974
+ * @param activePositionCount - Number of active positions at rejection time
3975
+ * @param comment - Rejection reason from validation note or "N/A"
3976
+ * @param timestamp - Event timestamp in milliseconds
3977
+ */
3978
+ const COMMIT_REJECTION_FN = async (symbol, params, activePositionCount, comment, timestamp) => await riskSubject.next({
3979
+ symbol,
3980
+ pendingSignal: params.pendingSignal,
3981
+ strategyName: params.strategyName,
3982
+ exchangeName: params.exchangeName,
3983
+ currentPrice: params.currentPrice,
3984
+ activePositionCount,
3985
+ comment,
3986
+ timestamp,
3987
+ });
3940
3988
  /**
3941
3989
  * Connection service routing risk operations to correct ClientRisk instance.
3942
3990
  *
@@ -3987,6 +4035,7 @@ class RiskConnectionService {
3987
4035
  return new ClientRisk({
3988
4036
  ...schema,
3989
4037
  logger: this.loggerService,
4038
+ onRejected: COMMIT_REJECTION_FN,
3990
4039
  });
3991
4040
  });
3992
4041
  /**
@@ -3994,6 +4043,7 @@ class RiskConnectionService {
3994
4043
  *
3995
4044
  * Routes to appropriate ClientRisk instance based on provided context.
3996
4045
  * Validates portfolio drawdown, symbol exposure, position count, and daily loss limits.
4046
+ * ClientRisk will emit riskSubject event via onRejected callback when signal is rejected.
3997
4047
  *
3998
4048
  * @param params - Risk check arguments (portfolio state, position details)
3999
4049
  * @param context - Execution context with risk name
@@ -5982,7 +6032,7 @@ function isUnsafe$3(value) {
5982
6032
  }
5983
6033
  return false;
5984
6034
  }
5985
- const columns$4 = [
6035
+ const columns$5 = [
5986
6036
  {
5987
6037
  key: "signalId",
5988
6038
  label: "Signal ID",
@@ -6056,11 +6106,13 @@ const columns$4 = [
6056
6106
  format: (data) => new Date(data.closeTimestamp).toISOString(),
6057
6107
  },
6058
6108
  ];
6109
+ /** Maximum number of signals to store in backtest reports */
6110
+ const MAX_EVENTS$6 = 250;
6059
6111
  /**
6060
6112
  * Storage class for accumulating closed signals per strategy.
6061
6113
  * Maintains a list of all closed signals and provides methods to generate reports.
6062
6114
  */
6063
- let ReportStorage$4 = class ReportStorage {
6115
+ let ReportStorage$5 = class ReportStorage {
6064
6116
  constructor() {
6065
6117
  /** Internal list of all closed signals for this strategy */
6066
6118
  this._signalList = [];
@@ -6071,7 +6123,11 @@ let ReportStorage$4 = class ReportStorage {
6071
6123
  * @param data - Closed signal data with PNL and close reason
6072
6124
  */
6073
6125
  addSignal(data) {
6074
- this._signalList.push(data);
6126
+ this._signalList.unshift(data);
6127
+ // Trim queue if exceeded MAX_EVENTS
6128
+ if (this._signalList.length > MAX_EVENTS$6) {
6129
+ this._signalList.pop();
6130
+ }
6075
6131
  }
6076
6132
  /**
6077
6133
  * Calculates statistical data from closed signals (Controller).
@@ -6154,9 +6210,9 @@ let ReportStorage$4 = class ReportStorage {
6154
6210
  "No signals closed yet."
6155
6211
  ].join("\n");
6156
6212
  }
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)));
6213
+ const header = columns$5.map((col) => col.label);
6214
+ const separator = columns$5.map(() => "---");
6215
+ const rows = this._signalList.map((closedSignal) => columns$5.map((col) => col.format(closedSignal)));
6160
6216
  const tableData = [header, separator, ...rows];
6161
6217
  const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
6162
6218
  return [
@@ -6232,7 +6288,7 @@ class BacktestMarkdownService {
6232
6288
  * Memoized function to get or create ReportStorage for a symbol-strategy pair.
6233
6289
  * Each symbol-strategy combination gets its own isolated storage instance.
6234
6290
  */
6235
- this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$4());
6291
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$5());
6236
6292
  /**
6237
6293
  * Processes tick events and accumulates closed signals.
6238
6294
  * Should be called from IStrategyCallbacks.onTick.
@@ -6403,7 +6459,7 @@ function isUnsafe$2(value) {
6403
6459
  }
6404
6460
  return false;
6405
6461
  }
6406
- const columns$3 = [
6462
+ const columns$4 = [
6407
6463
  {
6408
6464
  key: "timestamp",
6409
6465
  label: "Timestamp",
@@ -6487,12 +6543,12 @@ const columns$3 = [
6487
6543
  },
6488
6544
  ];
6489
6545
  /** Maximum number of events to store in live trading reports */
6490
- const MAX_EVENTS$3 = 250;
6546
+ const MAX_EVENTS$5 = 250;
6491
6547
  /**
6492
6548
  * Storage class for accumulating all tick events per strategy.
6493
6549
  * Maintains a chronological list of all events (idle, opened, active, closed).
6494
6550
  */
6495
- let ReportStorage$3 = class ReportStorage {
6551
+ let ReportStorage$4 = class ReportStorage {
6496
6552
  constructor() {
6497
6553
  /** Internal list of all tick events for this strategy */
6498
6554
  this._eventList = [];
@@ -6519,9 +6575,9 @@ let ReportStorage$3 = class ReportStorage {
6519
6575
  return;
6520
6576
  }
6521
6577
  {
6522
- this._eventList.push(newEvent);
6523
- if (this._eventList.length > MAX_EVENTS$3) {
6524
- this._eventList.shift();
6578
+ this._eventList.unshift(newEvent);
6579
+ if (this._eventList.length > MAX_EVENTS$5) {
6580
+ this._eventList.pop();
6525
6581
  }
6526
6582
  }
6527
6583
  }
@@ -6531,7 +6587,7 @@ let ReportStorage$3 = class ReportStorage {
6531
6587
  * @param data - Opened tick result
6532
6588
  */
6533
6589
  addOpenedEvent(data) {
6534
- this._eventList.push({
6590
+ this._eventList.unshift({
6535
6591
  timestamp: data.signal.pendingAt,
6536
6592
  action: "opened",
6537
6593
  symbol: data.signal.symbol,
@@ -6544,12 +6600,13 @@ let ReportStorage$3 = class ReportStorage {
6544
6600
  stopLoss: data.signal.priceStopLoss,
6545
6601
  });
6546
6602
  // Trim queue if exceeded MAX_EVENTS
6547
- if (this._eventList.length > MAX_EVENTS$3) {
6548
- this._eventList.shift();
6603
+ if (this._eventList.length > MAX_EVENTS$5) {
6604
+ this._eventList.pop();
6549
6605
  }
6550
6606
  }
6551
6607
  /**
6552
6608
  * Adds an active event to the storage.
6609
+ * Replaces the last active event with the same signalId.
6553
6610
  *
6554
6611
  * @param data - Active tick result
6555
6612
  */
@@ -6568,10 +6625,18 @@ let ReportStorage$3 = class ReportStorage {
6568
6625
  percentTp: data.percentTp,
6569
6626
  percentSl: data.percentSl,
6570
6627
  };
6571
- this._eventList.push(newEvent);
6628
+ // Find the last active event with the same signalId
6629
+ const lastActiveIndex = this._eventList.findLastIndex((event) => event.action === "active" && event.signalId === data.signal.id);
6630
+ // Replace the last active event with the same signalId
6631
+ if (lastActiveIndex !== -1) {
6632
+ this._eventList[lastActiveIndex] = newEvent;
6633
+ return;
6634
+ }
6635
+ // If no previous active event found, add new event
6636
+ this._eventList.unshift(newEvent);
6572
6637
  // Trim queue if exceeded MAX_EVENTS
6573
- if (this._eventList.length > MAX_EVENTS$3) {
6574
- this._eventList.shift();
6638
+ if (this._eventList.length > MAX_EVENTS$5) {
6639
+ this._eventList.pop();
6575
6640
  }
6576
6641
  }
6577
6642
  /**
@@ -6597,10 +6662,10 @@ let ReportStorage$3 = class ReportStorage {
6597
6662
  closeReason: data.closeReason,
6598
6663
  duration: durationMin,
6599
6664
  };
6600
- this._eventList.push(newEvent);
6665
+ this._eventList.unshift(newEvent);
6601
6666
  // Trim queue if exceeded MAX_EVENTS
6602
- if (this._eventList.length > MAX_EVENTS$3) {
6603
- this._eventList.shift();
6667
+ if (this._eventList.length > MAX_EVENTS$5) {
6668
+ this._eventList.pop();
6604
6669
  }
6605
6670
  }
6606
6671
  /**
@@ -6699,9 +6764,9 @@ let ReportStorage$3 = class ReportStorage {
6699
6764
  "No events recorded yet."
6700
6765
  ].join("\n");
6701
6766
  }
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)));
6767
+ const header = columns$4.map((col) => col.label);
6768
+ const separator = columns$4.map(() => "---");
6769
+ const rows = this._eventList.map((event) => columns$4.map((col) => col.format(event)));
6705
6770
  const tableData = [header, separator, ...rows];
6706
6771
  const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
6707
6772
  return [
@@ -6780,7 +6845,7 @@ class LiveMarkdownService {
6780
6845
  * Memoized function to get or create ReportStorage for a symbol-strategy pair.
6781
6846
  * Each symbol-strategy combination gets its own isolated storage instance.
6782
6847
  */
6783
- this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$3());
6848
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$4());
6784
6849
  /**
6785
6850
  * Processes tick events and accumulates all event types.
6786
6851
  * Should be called from IStrategyCallbacks.onTick.
@@ -6943,7 +7008,7 @@ class LiveMarkdownService {
6943
7008
  }
6944
7009
  }
6945
7010
 
6946
- const columns$2 = [
7011
+ const columns$3 = [
6947
7012
  {
6948
7013
  key: "timestamp",
6949
7014
  label: "Timestamp",
@@ -7001,12 +7066,12 @@ const columns$2 = [
7001
7066
  },
7002
7067
  ];
7003
7068
  /** Maximum number of events to store in schedule reports */
7004
- const MAX_EVENTS$2 = 250;
7069
+ const MAX_EVENTS$4 = 250;
7005
7070
  /**
7006
7071
  * Storage class for accumulating scheduled signal events per strategy.
7007
7072
  * Maintains a chronological list of scheduled and cancelled events.
7008
7073
  */
7009
- let ReportStorage$2 = class ReportStorage {
7074
+ let ReportStorage$3 = class ReportStorage {
7010
7075
  constructor() {
7011
7076
  /** Internal list of all scheduled events for this strategy */
7012
7077
  this._eventList = [];
@@ -7017,7 +7082,7 @@ let ReportStorage$2 = class ReportStorage {
7017
7082
  * @param data - Scheduled tick result
7018
7083
  */
7019
7084
  addScheduledEvent(data) {
7020
- this._eventList.push({
7085
+ this._eventList.unshift({
7021
7086
  timestamp: data.signal.scheduledAt,
7022
7087
  action: "scheduled",
7023
7088
  symbol: data.signal.symbol,
@@ -7030,8 +7095,35 @@ let ReportStorage$2 = class ReportStorage {
7030
7095
  stopLoss: data.signal.priceStopLoss,
7031
7096
  });
7032
7097
  // Trim queue if exceeded MAX_EVENTS
7033
- if (this._eventList.length > MAX_EVENTS$2) {
7034
- this._eventList.shift();
7098
+ if (this._eventList.length > MAX_EVENTS$4) {
7099
+ this._eventList.pop();
7100
+ }
7101
+ }
7102
+ /**
7103
+ * Adds an opened event to the storage.
7104
+ *
7105
+ * @param data - Opened tick result
7106
+ */
7107
+ addOpenedEvent(data) {
7108
+ const durationMs = data.signal.pendingAt - data.signal.scheduledAt;
7109
+ const durationMin = Math.round(durationMs / 60000);
7110
+ const newEvent = {
7111
+ timestamp: data.signal.pendingAt,
7112
+ action: "opened",
7113
+ symbol: data.signal.symbol,
7114
+ signalId: data.signal.id,
7115
+ position: data.signal.position,
7116
+ note: data.signal.note,
7117
+ currentPrice: data.currentPrice,
7118
+ priceOpen: data.signal.priceOpen,
7119
+ takeProfit: data.signal.priceTakeProfit,
7120
+ stopLoss: data.signal.priceStopLoss,
7121
+ duration: durationMin,
7122
+ };
7123
+ this._eventList.unshift(newEvent);
7124
+ // Trim queue if exceeded MAX_EVENTS
7125
+ if (this._eventList.length > MAX_EVENTS$4) {
7126
+ this._eventList.pop();
7035
7127
  }
7036
7128
  }
7037
7129
  /**
@@ -7056,10 +7148,10 @@ let ReportStorage$2 = class ReportStorage {
7056
7148
  closeTimestamp: data.closeTimestamp,
7057
7149
  duration: durationMin,
7058
7150
  };
7059
- this._eventList.push(newEvent);
7151
+ this._eventList.unshift(newEvent);
7060
7152
  // Trim queue if exceeded MAX_EVENTS
7061
- if (this._eventList.length > MAX_EVENTS$2) {
7062
- this._eventList.shift();
7153
+ if (this._eventList.length > MAX_EVENTS$4) {
7154
+ this._eventList.pop();
7063
7155
  }
7064
7156
  }
7065
7157
  /**
@@ -7073,29 +7165,44 @@ let ReportStorage$2 = class ReportStorage {
7073
7165
  eventList: [],
7074
7166
  totalEvents: 0,
7075
7167
  totalScheduled: 0,
7168
+ totalOpened: 0,
7076
7169
  totalCancelled: 0,
7077
7170
  cancellationRate: null,
7171
+ activationRate: null,
7078
7172
  avgWaitTime: null,
7173
+ avgActivationTime: null,
7079
7174
  };
7080
7175
  }
7081
7176
  const scheduledEvents = this._eventList.filter((e) => e.action === "scheduled");
7177
+ const openedEvents = this._eventList.filter((e) => e.action === "opened");
7082
7178
  const cancelledEvents = this._eventList.filter((e) => e.action === "cancelled");
7083
7179
  const totalScheduled = scheduledEvents.length;
7180
+ const totalOpened = openedEvents.length;
7084
7181
  const totalCancelled = cancelledEvents.length;
7085
7182
  // Calculate cancellation rate
7086
7183
  const cancellationRate = totalScheduled > 0 ? (totalCancelled / totalScheduled) * 100 : null;
7184
+ // Calculate activation rate
7185
+ const activationRate = totalScheduled > 0 ? (totalOpened / totalScheduled) * 100 : null;
7087
7186
  // Calculate average wait time for cancelled signals
7088
7187
  const avgWaitTime = totalCancelled > 0
7089
7188
  ? cancelledEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
7090
7189
  totalCancelled
7091
7190
  : null;
7191
+ // Calculate average activation time for opened signals
7192
+ const avgActivationTime = totalOpened > 0
7193
+ ? openedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
7194
+ totalOpened
7195
+ : null;
7092
7196
  return {
7093
7197
  eventList: this._eventList,
7094
7198
  totalEvents: this._eventList.length,
7095
7199
  totalScheduled,
7200
+ totalOpened,
7096
7201
  totalCancelled,
7097
7202
  cancellationRate,
7203
+ activationRate,
7098
7204
  avgWaitTime,
7205
+ avgActivationTime,
7099
7206
  };
7100
7207
  }
7101
7208
  /**
@@ -7113,9 +7220,9 @@ let ReportStorage$2 = class ReportStorage {
7113
7220
  "No scheduled signals recorded yet."
7114
7221
  ].join("\n");
7115
7222
  }
7116
- const header = columns$2.map((col) => col.label);
7117
- const separator = columns$2.map(() => "---");
7118
- const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
7223
+ const header = columns$3.map((col) => col.label);
7224
+ const separator = columns$3.map(() => "---");
7225
+ const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
7119
7226
  const tableData = [header, separator, ...rows];
7120
7227
  const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7121
7228
  return [
@@ -7125,8 +7232,11 @@ let ReportStorage$2 = class ReportStorage {
7125
7232
  "",
7126
7233
  `**Total events:** ${stats.totalEvents}`,
7127
7234
  `**Scheduled signals:** ${stats.totalScheduled}`,
7235
+ `**Opened signals:** ${stats.totalOpened}`,
7128
7236
  `**Cancelled signals:** ${stats.totalCancelled}`,
7237
+ `**Activation rate:** ${stats.activationRate === null ? "N/A" : `${stats.activationRate.toFixed(2)}% (higher is better)`}`,
7129
7238
  `**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`,
7239
+ `**Average activation time:** ${stats.avgActivationTime === null ? "N/A" : `${stats.avgActivationTime.toFixed(2)} minutes`}`,
7130
7240
  `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`
7131
7241
  ].join("\n");
7132
7242
  }
@@ -7180,12 +7290,12 @@ class ScheduleMarkdownService {
7180
7290
  * Memoized function to get or create ReportStorage for a symbol-strategy pair.
7181
7291
  * Each symbol-strategy combination gets its own isolated storage instance.
7182
7292
  */
7183
- this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$2());
7293
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$3());
7184
7294
  /**
7185
- * Processes tick events and accumulates scheduled/cancelled events.
7186
- * Should be called from signalLiveEmitter subscription.
7295
+ * Processes tick events and accumulates scheduled/opened/cancelled events.
7296
+ * Should be called from signalEmitter subscription.
7187
7297
  *
7188
- * Processes only scheduled and cancelled event types.
7298
+ * Processes only scheduled, opened and cancelled event types.
7189
7299
  *
7190
7300
  * @param data - Tick result from strategy execution
7191
7301
  *
@@ -7203,6 +7313,13 @@ class ScheduleMarkdownService {
7203
7313
  if (data.action === "scheduled") {
7204
7314
  storage.addScheduledEvent(data);
7205
7315
  }
7316
+ else if (data.action === "opened") {
7317
+ // Check if this opened signal was previously scheduled
7318
+ // by checking if signal has scheduledAt != pendingAt
7319
+ if (data.signal.scheduledAt !== data.signal.pendingAt) {
7320
+ storage.addOpenedEvent(data);
7321
+ }
7322
+ }
7206
7323
  else if (data.action === "cancelled") {
7207
7324
  storage.addCancelledEvent(data);
7208
7325
  }
@@ -7340,7 +7457,7 @@ function percentile(sortedArray, p) {
7340
7457
  return sortedArray[Math.max(0, index)];
7341
7458
  }
7342
7459
  /** Maximum number of performance events to store per strategy */
7343
- const MAX_EVENTS$1 = 10000;
7460
+ const MAX_EVENTS$3 = 10000;
7344
7461
  /**
7345
7462
  * Storage class for accumulating performance metrics per strategy.
7346
7463
  * Maintains a list of all performance events and provides aggregated statistics.
@@ -7356,10 +7473,10 @@ class PerformanceStorage {
7356
7473
  * @param event - Performance event with timing data
7357
7474
  */
7358
7475
  addEvent(event) {
7359
- this._events.push(event);
7476
+ this._events.unshift(event);
7360
7477
  // Trim queue if exceeded MAX_EVENTS (keep most recent)
7361
- if (this._events.length > MAX_EVENTS$1) {
7362
- this._events.shift();
7478
+ if (this._events.length > MAX_EVENTS$3) {
7479
+ this._events.pop();
7363
7480
  }
7364
7481
  }
7365
7482
  /**
@@ -7825,7 +7942,7 @@ const pnlColumns = [
7825
7942
  * Storage class for accumulating walker results.
7826
7943
  * Maintains a list of all strategy results and provides methods to generate reports.
7827
7944
  */
7828
- let ReportStorage$1 = class ReportStorage {
7945
+ let ReportStorage$2 = class ReportStorage {
7829
7946
  constructor(walkerName) {
7830
7947
  this.walkerName = walkerName;
7831
7948
  /** Walker metadata (set from first addResult call) */
@@ -7843,17 +7960,13 @@ let ReportStorage$1 = class ReportStorage {
7843
7960
  * @param data - Walker contract with strategy result
7844
7961
  */
7845
7962
  addResult(data) {
7846
- {
7847
- this._bestMetric = data.bestMetric;
7848
- this._bestStrategy = data.bestStrategy;
7849
- this._totalStrategies = data.totalStrategies;
7850
- }
7851
- // Update best stats only if this strategy is the current best
7963
+ this._totalStrategies = data.totalStrategies;
7964
+ this._bestMetric = data.bestMetric;
7965
+ this._bestStrategy = data.bestStrategy;
7852
7966
  if (data.strategyName === data.bestStrategy) {
7853
7967
  this._bestStats = data.stats;
7854
7968
  }
7855
- // Add strategy result to comparison list
7856
- this._strategyResults.push({
7969
+ this._strategyResults.unshift({
7857
7970
  strategyName: data.strategyName,
7858
7971
  stats: data.stats,
7859
7972
  metricValue: data.metricValue,
@@ -8037,7 +8150,7 @@ class WalkerMarkdownService {
8037
8150
  * Memoized function to get or create ReportStorage for a walker.
8038
8151
  * Each walker gets its own isolated storage instance.
8039
8152
  */
8040
- this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$1(walkerName));
8153
+ this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$2(walkerName));
8041
8154
  /**
8042
8155
  * Processes walker progress events and accumulates strategy results.
8043
8156
  * Should be called from walkerEmitter.
@@ -8208,7 +8321,7 @@ function isUnsafe(value) {
8208
8321
  }
8209
8322
  return false;
8210
8323
  }
8211
- const columns$1 = [
8324
+ const columns$2 = [
8212
8325
  {
8213
8326
  key: "symbol",
8214
8327
  label: "Symbol",
@@ -8270,6 +8383,8 @@ const columns$1 = [
8270
8383
  format: (data) => data.totalTrades.toString(),
8271
8384
  },
8272
8385
  ];
8386
+ /** Maximum number of signals to store per symbol in heatmap reports */
8387
+ const MAX_EVENTS$2 = 250;
8273
8388
  /**
8274
8389
  * Storage class for accumulating closed signals per strategy and generating heatmap.
8275
8390
  * Maintains symbol-level statistics and provides portfolio-wide metrics.
@@ -8289,7 +8404,12 @@ class HeatmapStorage {
8289
8404
  if (!this.symbolData.has(symbol)) {
8290
8405
  this.symbolData.set(symbol, []);
8291
8406
  }
8292
- this.symbolData.get(symbol).push(data);
8407
+ const signals = this.symbolData.get(symbol);
8408
+ signals.unshift(data);
8409
+ // Trim queue if exceeded MAX_EVENTS per symbol
8410
+ if (signals.length > MAX_EVENTS$2) {
8411
+ signals.pop();
8412
+ }
8293
8413
  }
8294
8414
  /**
8295
8415
  * Calculates statistics for a single symbol.
@@ -8508,9 +8628,9 @@ class HeatmapStorage {
8508
8628
  "*No data available*"
8509
8629
  ].join("\n");
8510
8630
  }
8511
- const header = columns$1.map((col) => col.label);
8512
- const separator = columns$1.map(() => "---");
8513
- const rows = data.symbols.map((row) => columns$1.map((col) => col.format(row)));
8631
+ const header = columns$2.map((col) => col.label);
8632
+ const separator = columns$2.map(() => "---");
8633
+ const rows = data.symbols.map((row) => columns$2.map((col) => col.format(row)));
8514
8634
  const tableData = [header, separator, ...rows];
8515
8635
  const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
8516
8636
  return [
@@ -10548,7 +10668,7 @@ const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, back
10548
10668
  revenuePercent,
10549
10669
  backtest,
10550
10670
  });
10551
- await self.params.onProfit(symbol, data, currentPrice, level, backtest, when.getTime());
10671
+ await self.params.onProfit(symbol, data.strategyName, data.exchangeName, data, currentPrice, level, backtest, when.getTime());
10552
10672
  }
10553
10673
  }
10554
10674
  if (shouldPersist) {
@@ -10595,7 +10715,7 @@ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest,
10595
10715
  lossPercent,
10596
10716
  backtest,
10597
10717
  });
10598
- await self.params.onLoss(symbol, data, currentPrice, level, backtest, when.getTime());
10718
+ await self.params.onLoss(symbol, data.strategyName, data.exchangeName, data, currentPrice, level, backtest, when.getTime());
10599
10719
  }
10600
10720
  }
10601
10721
  if (shouldPersist) {
@@ -10866,6 +10986,7 @@ class ClientPartial {
10866
10986
  symbol,
10867
10987
  data,
10868
10988
  priceClose,
10989
+ backtest,
10869
10990
  });
10870
10991
  if (this._states === NEED_FETCH) {
10871
10992
  throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
@@ -10882,14 +11003,18 @@ class ClientPartial {
10882
11003
  * Emits PartialProfitContract event to all subscribers.
10883
11004
  *
10884
11005
  * @param symbol - Trading pair symbol
11006
+ * @param strategyName - Strategy name that generated this signal
11007
+ * @param exchangeName - Exchange name where this signal is being executed
10885
11008
  * @param data - Signal row data
10886
11009
  * @param currentPrice - Current market price
10887
11010
  * @param level - Profit level reached
10888
11011
  * @param backtest - True if backtest mode
10889
11012
  * @param timestamp - Event timestamp in milliseconds
10890
11013
  */
10891
- const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialProfitSubject.next({
11014
+ const COMMIT_PROFIT_FN = async (symbol, strategyName, exchangeName, data, currentPrice, level, backtest, timestamp) => await partialProfitSubject.next({
10892
11015
  symbol,
11016
+ strategyName,
11017
+ exchangeName,
10893
11018
  data,
10894
11019
  currentPrice,
10895
11020
  level,
@@ -10903,14 +11028,18 @@ const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, tim
10903
11028
  * Emits PartialLossContract event to all subscribers.
10904
11029
  *
10905
11030
  * @param symbol - Trading pair symbol
11031
+ * @param strategyName - Strategy name that generated this signal
11032
+ * @param exchangeName - Exchange name where this signal is being executed
10906
11033
  * @param data - Signal row data
10907
11034
  * @param currentPrice - Current market price
10908
11035
  * @param level - Loss level reached
10909
11036
  * @param backtest - True if backtest mode
10910
11037
  * @param timestamp - Event timestamp in milliseconds
10911
11038
  */
10912
- const COMMIT_LOSS_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialLossSubject.next({
11039
+ const COMMIT_LOSS_FN = async (symbol, strategyName, exchangeName, data, currentPrice, level, backtest, timestamp) => await partialLossSubject.next({
10913
11040
  symbol,
11041
+ strategyName,
11042
+ exchangeName,
10914
11043
  data,
10915
11044
  currentPrice,
10916
11045
  level,
@@ -11056,7 +11185,7 @@ class PartialConnectionService {
11056
11185
  }
11057
11186
  }
11058
11187
 
11059
- const columns = [
11188
+ const columns$1 = [
11060
11189
  {
11061
11190
  key: "action",
11062
11191
  label: "Action",
@@ -11104,12 +11233,12 @@ const columns = [
11104
11233
  },
11105
11234
  ];
11106
11235
  /** Maximum number of events to store in partial reports */
11107
- const MAX_EVENTS = 250;
11236
+ const MAX_EVENTS$1 = 250;
11108
11237
  /**
11109
11238
  * Storage class for accumulating partial profit/loss events per symbol-strategy pair.
11110
11239
  * Maintains a chronological list of profit and loss level events.
11111
11240
  */
11112
- class ReportStorage {
11241
+ let ReportStorage$1 = class ReportStorage {
11113
11242
  constructor() {
11114
11243
  /** Internal list of all partial events for this symbol */
11115
11244
  this._eventList = [];
@@ -11123,7 +11252,7 @@ class ReportStorage {
11123
11252
  * @param backtest - True if backtest mode
11124
11253
  */
11125
11254
  addProfitEvent(data, currentPrice, level, backtest, timestamp) {
11126
- this._eventList.push({
11255
+ this._eventList.unshift({
11127
11256
  timestamp,
11128
11257
  action: "profit",
11129
11258
  symbol: data.symbol,
@@ -11135,8 +11264,8 @@ class ReportStorage {
11135
11264
  backtest,
11136
11265
  });
11137
11266
  // Trim queue if exceeded MAX_EVENTS
11138
- if (this._eventList.length > MAX_EVENTS) {
11139
- this._eventList.shift();
11267
+ if (this._eventList.length > MAX_EVENTS$1) {
11268
+ this._eventList.pop();
11140
11269
  }
11141
11270
  }
11142
11271
  /**
@@ -11148,7 +11277,7 @@ class ReportStorage {
11148
11277
  * @param backtest - True if backtest mode
11149
11278
  */
11150
11279
  addLossEvent(data, currentPrice, level, backtest, timestamp) {
11151
- this._eventList.push({
11280
+ this._eventList.unshift({
11152
11281
  timestamp,
11153
11282
  action: "loss",
11154
11283
  symbol: data.symbol,
@@ -11160,8 +11289,8 @@ class ReportStorage {
11160
11289
  backtest,
11161
11290
  });
11162
11291
  // Trim queue if exceeded MAX_EVENTS
11163
- if (this._eventList.length > MAX_EVENTS) {
11164
- this._eventList.shift();
11292
+ if (this._eventList.length > MAX_EVENTS$1) {
11293
+ this._eventList.pop();
11165
11294
  }
11166
11295
  }
11167
11296
  /**
@@ -11203,9 +11332,9 @@ class ReportStorage {
11203
11332
  "No partial profit/loss events recorded yet."
11204
11333
  ].join("\n");
11205
11334
  }
11206
- const header = columns.map((col) => col.label);
11207
- const separator = columns.map(() => "---");
11208
- const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
11335
+ const header = columns$1.map((col) => col.label);
11336
+ const separator = columns$1.map(() => "---");
11337
+ const rows = this._eventList.map((event) => columns$1.map((col) => col.format(event)));
11209
11338
  const tableData = [header, separator, ...rows];
11210
11339
  const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
11211
11340
  return [
@@ -11239,7 +11368,7 @@ class ReportStorage {
11239
11368
  console.error(`Failed to save markdown report:`, error);
11240
11369
  }
11241
11370
  }
11242
- }
11371
+ };
11243
11372
  /**
11244
11373
  * Service for generating and saving partial profit/loss markdown reports.
11245
11374
  *
@@ -11269,7 +11398,7 @@ class PartialMarkdownService {
11269
11398
  * Memoized function to get or create ReportStorage for a symbol-strategy pair.
11270
11399
  * Each symbol-strategy combination gets its own isolated storage instance.
11271
11400
  */
11272
- this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage());
11401
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$1());
11273
11402
  /**
11274
11403
  * Processes profit events and accumulates them.
11275
11404
  * Should be called from partialProfitSubject subscription.
@@ -11579,7 +11708,7 @@ class PartialGlobalService {
11579
11708
  * Warning threshold for message size in kilobytes.
11580
11709
  * Messages exceeding this size trigger console warnings.
11581
11710
  */
11582
- const WARN_KB = 100;
11711
+ const WARN_KB = 20;
11583
11712
  /**
11584
11713
  * Internal function for dumping signal data to markdown files.
11585
11714
  * Creates a directory structure with system prompts, user messages, and LLM output.
@@ -11841,35 +11970,382 @@ class ConfigValidationService {
11841
11970
  }
11842
11971
  }
11843
11972
 
11844
- {
11845
- provide(TYPES.loggerService, () => new LoggerService());
11846
- }
11847
- {
11848
- provide(TYPES.executionContextService, () => new ExecutionContextService());
11849
- provide(TYPES.methodContextService, () => new MethodContextService());
11850
- }
11851
- {
11852
- provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
11853
- provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
11854
- provide(TYPES.frameConnectionService, () => new FrameConnectionService());
11855
- provide(TYPES.sizingConnectionService, () => new SizingConnectionService());
11856
- provide(TYPES.riskConnectionService, () => new RiskConnectionService());
11857
- provide(TYPES.optimizerConnectionService, () => new OptimizerConnectionService());
11858
- provide(TYPES.partialConnectionService, () => new PartialConnectionService());
11859
- }
11860
- {
11861
- provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
11862
- provide(TYPES.strategySchemaService, () => new StrategySchemaService());
11863
- provide(TYPES.frameSchemaService, () => new FrameSchemaService());
11864
- provide(TYPES.walkerSchemaService, () => new WalkerSchemaService());
11865
- provide(TYPES.sizingSchemaService, () => new SizingSchemaService());
11866
- provide(TYPES.riskSchemaService, () => new RiskSchemaService());
11867
- provide(TYPES.optimizerSchemaService, () => new OptimizerSchemaService());
11973
+ const columns = [
11974
+ {
11975
+ key: "symbol",
11976
+ label: "Symbol",
11977
+ format: (data) => data.symbol,
11978
+ },
11979
+ {
11980
+ key: "strategyName",
11981
+ label: "Strategy",
11982
+ format: (data) => data.strategyName,
11983
+ },
11984
+ {
11985
+ key: "signalId",
11986
+ label: "Signal ID",
11987
+ format: (data) => data.pendingSignal.id || "N/A",
11988
+ },
11989
+ {
11990
+ key: "position",
11991
+ label: "Position",
11992
+ format: (data) => data.pendingSignal.position.toUpperCase(),
11993
+ },
11994
+ {
11995
+ key: "exchangeName",
11996
+ label: "Exchange",
11997
+ format: (data) => data.exchangeName,
11998
+ },
11999
+ {
12000
+ key: "openPrice",
12001
+ label: "Open Price",
12002
+ format: (data) => data.pendingSignal.priceOpen !== undefined
12003
+ ? `${data.pendingSignal.priceOpen.toFixed(8)} USD`
12004
+ : "N/A",
12005
+ },
12006
+ {
12007
+ key: "takeProfit",
12008
+ label: "Take Profit",
12009
+ format: (data) => data.pendingSignal.priceTakeProfit !== undefined
12010
+ ? `${data.pendingSignal.priceTakeProfit.toFixed(8)} USD`
12011
+ : "N/A",
12012
+ },
12013
+ {
12014
+ key: "stopLoss",
12015
+ label: "Stop Loss",
12016
+ format: (data) => data.pendingSignal.priceStopLoss !== undefined
12017
+ ? `${data.pendingSignal.priceStopLoss.toFixed(8)} USD`
12018
+ : "N/A",
12019
+ },
12020
+ {
12021
+ key: "currentPrice",
12022
+ label: "Current Price",
12023
+ format: (data) => `${data.currentPrice.toFixed(8)} USD`,
12024
+ },
12025
+ {
12026
+ key: "activePositionCount",
12027
+ label: "Active Positions",
12028
+ format: (data) => data.activePositionCount.toString(),
12029
+ },
12030
+ {
12031
+ key: "comment",
12032
+ label: "Reason",
12033
+ format: (data) => data.comment,
12034
+ },
12035
+ {
12036
+ key: "timestamp",
12037
+ label: "Timestamp",
12038
+ format: (data) => new Date(data.timestamp).toISOString(),
12039
+ },
12040
+ ];
12041
+ /** Maximum number of events to store in risk reports */
12042
+ const MAX_EVENTS = 250;
12043
+ /**
12044
+ * Storage class for accumulating risk rejection events per symbol-strategy pair.
12045
+ * Maintains a chronological list of rejected signals due to risk limits.
12046
+ */
12047
+ class ReportStorage {
12048
+ constructor() {
12049
+ /** Internal list of all risk rejection events for this symbol */
12050
+ this._eventList = [];
12051
+ }
12052
+ /**
12053
+ * Adds a risk rejection event to the storage.
12054
+ *
12055
+ * @param event - Risk rejection event data
12056
+ */
12057
+ addRejectionEvent(event) {
12058
+ this._eventList.unshift(event);
12059
+ // Trim queue if exceeded MAX_EVENTS
12060
+ if (this._eventList.length > MAX_EVENTS) {
12061
+ this._eventList.pop();
12062
+ }
12063
+ }
12064
+ /**
12065
+ * Calculates statistical data from risk rejection events (Controller).
12066
+ *
12067
+ * @returns Statistical data (empty object if no events)
12068
+ */
12069
+ async getData() {
12070
+ if (this._eventList.length === 0) {
12071
+ return {
12072
+ eventList: [],
12073
+ totalRejections: 0,
12074
+ bySymbol: {},
12075
+ byStrategy: {},
12076
+ };
12077
+ }
12078
+ const bySymbol = {};
12079
+ const byStrategy = {};
12080
+ for (const event of this._eventList) {
12081
+ bySymbol[event.symbol] = (bySymbol[event.symbol] || 0) + 1;
12082
+ byStrategy[event.strategyName] = (byStrategy[event.strategyName] || 0) + 1;
12083
+ }
12084
+ return {
12085
+ eventList: this._eventList,
12086
+ totalRejections: this._eventList.length,
12087
+ bySymbol,
12088
+ byStrategy,
12089
+ };
12090
+ }
12091
+ /**
12092
+ * Generates markdown report with all risk rejection events for a symbol-strategy pair (View).
12093
+ *
12094
+ * @param symbol - Trading pair symbol
12095
+ * @param strategyName - Strategy name
12096
+ * @returns Markdown formatted report with all events
12097
+ */
12098
+ async getReport(symbol, strategyName) {
12099
+ const stats = await this.getData();
12100
+ if (stats.totalRejections === 0) {
12101
+ return [
12102
+ `# Risk Rejection Report: ${symbol}:${strategyName}`,
12103
+ "",
12104
+ "No risk rejections recorded yet.",
12105
+ ].join("\n");
12106
+ }
12107
+ const header = columns.map((col) => col.label);
12108
+ const separator = columns.map(() => "---");
12109
+ const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
12110
+ const tableData = [header, separator, ...rows];
12111
+ const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
12112
+ return [
12113
+ `# Risk Rejection Report: ${symbol}:${strategyName}`,
12114
+ "",
12115
+ table,
12116
+ "",
12117
+ `**Total rejections:** ${stats.totalRejections}`,
12118
+ "",
12119
+ "## Rejections by Symbol",
12120
+ ...Object.entries(stats.bySymbol).map(([sym, count]) => `- ${sym}: ${count}`),
12121
+ "",
12122
+ "## Rejections by Strategy",
12123
+ ...Object.entries(stats.byStrategy).map(([strat, count]) => `- ${strat}: ${count}`),
12124
+ ].join("\n");
12125
+ }
12126
+ /**
12127
+ * Saves symbol-strategy report to disk.
12128
+ *
12129
+ * @param symbol - Trading pair symbol
12130
+ * @param strategyName - Strategy name
12131
+ * @param path - Directory path to save report (default: "./dump/risk")
12132
+ */
12133
+ async dump(symbol, strategyName, path$1 = "./dump/risk") {
12134
+ const markdown = await this.getReport(symbol, strategyName);
12135
+ try {
12136
+ const dir = path.join(process.cwd(), path$1);
12137
+ await fs.mkdir(dir, { recursive: true });
12138
+ const filename = `${symbol}_${strategyName}.md`;
12139
+ const filepath = path.join(dir, filename);
12140
+ await fs.writeFile(filepath, markdown, "utf-8");
12141
+ console.log(`Risk rejection report saved: ${filepath}`);
12142
+ }
12143
+ catch (error) {
12144
+ console.error(`Failed to save markdown report:`, error);
12145
+ }
12146
+ }
11868
12147
  }
11869
- {
11870
- provide(TYPES.exchangeCoreService, () => new ExchangeCoreService());
11871
- provide(TYPES.strategyCoreService, () => new StrategyCoreService());
11872
- provide(TYPES.frameCoreService, () => new FrameCoreService());
12148
+ /**
12149
+ * Service for generating and saving risk rejection markdown reports.
12150
+ *
12151
+ * Features:
12152
+ * - Listens to risk rejection events via riskSubject
12153
+ * - Accumulates all rejection events per symbol-strategy pair
12154
+ * - Generates markdown tables with detailed rejection information
12155
+ * - Provides statistics (total rejections, by symbol, by strategy)
12156
+ * - Saves reports to disk in dump/risk/{symbol}_{strategyName}.md
12157
+ *
12158
+ * @example
12159
+ * ```typescript
12160
+ * const service = new RiskMarkdownService();
12161
+ *
12162
+ * // Service automatically subscribes to subjects on init
12163
+ * // No manual callback setup needed
12164
+ *
12165
+ * // Later: generate and save report
12166
+ * await service.dump("BTCUSDT", "my-strategy");
12167
+ * ```
12168
+ */
12169
+ class RiskMarkdownService {
12170
+ constructor() {
12171
+ /** Logger service for debug output */
12172
+ this.loggerService = inject(TYPES.loggerService);
12173
+ /**
12174
+ * Memoized function to get or create ReportStorage for a symbol-strategy pair.
12175
+ * Each symbol-strategy combination gets its own isolated storage instance.
12176
+ */
12177
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage());
12178
+ /**
12179
+ * Processes risk rejection events and accumulates them.
12180
+ * Should be called from riskSubject subscription.
12181
+ *
12182
+ * @param data - Risk rejection event data
12183
+ *
12184
+ * @example
12185
+ * ```typescript
12186
+ * const service = new RiskMarkdownService();
12187
+ * // Service automatically subscribes in init()
12188
+ * ```
12189
+ */
12190
+ this.tickRejection = async (data) => {
12191
+ this.loggerService.log("riskMarkdownService tickRejection", {
12192
+ data,
12193
+ });
12194
+ const storage = this.getStorage(data.symbol, data.strategyName);
12195
+ storage.addRejectionEvent(data);
12196
+ };
12197
+ /**
12198
+ * Gets statistical data from all risk rejection events for a symbol-strategy pair.
12199
+ * Delegates to ReportStorage.getData().
12200
+ *
12201
+ * @param symbol - Trading pair symbol to get data for
12202
+ * @param strategyName - Strategy name to get data for
12203
+ * @returns Statistical data object with all metrics
12204
+ *
12205
+ * @example
12206
+ * ```typescript
12207
+ * const service = new RiskMarkdownService();
12208
+ * const stats = await service.getData("BTCUSDT", "my-strategy");
12209
+ * console.log(stats.totalRejections, stats.bySymbol);
12210
+ * ```
12211
+ */
12212
+ this.getData = async (symbol, strategyName) => {
12213
+ this.loggerService.log("riskMarkdownService getData", {
12214
+ symbol,
12215
+ strategyName,
12216
+ });
12217
+ const storage = this.getStorage(symbol, strategyName);
12218
+ return storage.getData();
12219
+ };
12220
+ /**
12221
+ * Generates markdown report with all risk rejection events for a symbol-strategy pair.
12222
+ * Delegates to ReportStorage.getReport().
12223
+ *
12224
+ * @param symbol - Trading pair symbol to generate report for
12225
+ * @param strategyName - Strategy name to generate report for
12226
+ * @returns Markdown formatted report string with table of all events
12227
+ *
12228
+ * @example
12229
+ * ```typescript
12230
+ * const service = new RiskMarkdownService();
12231
+ * const markdown = await service.getReport("BTCUSDT", "my-strategy");
12232
+ * console.log(markdown);
12233
+ * ```
12234
+ */
12235
+ this.getReport = async (symbol, strategyName) => {
12236
+ this.loggerService.log("riskMarkdownService getReport", {
12237
+ symbol,
12238
+ strategyName,
12239
+ });
12240
+ const storage = this.getStorage(symbol, strategyName);
12241
+ return storage.getReport(symbol, strategyName);
12242
+ };
12243
+ /**
12244
+ * Saves symbol-strategy report to disk.
12245
+ * Creates directory if it doesn't exist.
12246
+ * Delegates to ReportStorage.dump().
12247
+ *
12248
+ * @param symbol - Trading pair symbol to save report for
12249
+ * @param strategyName - Strategy name to save report for
12250
+ * @param path - Directory path to save report (default: "./dump/risk")
12251
+ *
12252
+ * @example
12253
+ * ```typescript
12254
+ * const service = new RiskMarkdownService();
12255
+ *
12256
+ * // Save to default path: ./dump/risk/BTCUSDT_my-strategy.md
12257
+ * await service.dump("BTCUSDT", "my-strategy");
12258
+ *
12259
+ * // Save to custom path: ./custom/path/BTCUSDT_my-strategy.md
12260
+ * await service.dump("BTCUSDT", "my-strategy", "./custom/path");
12261
+ * ```
12262
+ */
12263
+ this.dump = async (symbol, strategyName, path = "./dump/risk") => {
12264
+ this.loggerService.log("riskMarkdownService dump", {
12265
+ symbol,
12266
+ strategyName,
12267
+ path,
12268
+ });
12269
+ const storage = this.getStorage(symbol, strategyName);
12270
+ await storage.dump(symbol, strategyName, path);
12271
+ };
12272
+ /**
12273
+ * Clears accumulated event data from storage.
12274
+ * If ctx is provided, clears only that specific symbol-strategy pair's data.
12275
+ * If nothing is provided, clears all data.
12276
+ *
12277
+ * @param ctx - Optional context with symbol and strategyName
12278
+ *
12279
+ * @example
12280
+ * ```typescript
12281
+ * const service = new RiskMarkdownService();
12282
+ *
12283
+ * // Clear specific symbol-strategy pair
12284
+ * await service.clear({ symbol: "BTCUSDT", strategyName: "my-strategy" });
12285
+ *
12286
+ * // Clear all data
12287
+ * await service.clear();
12288
+ * ```
12289
+ */
12290
+ this.clear = async (ctx) => {
12291
+ this.loggerService.log("riskMarkdownService clear", {
12292
+ ctx,
12293
+ });
12294
+ if (ctx) {
12295
+ const key = `${ctx.symbol}:${ctx.strategyName}`;
12296
+ this.getStorage.clear(key);
12297
+ }
12298
+ else {
12299
+ this.getStorage.clear();
12300
+ }
12301
+ };
12302
+ /**
12303
+ * Initializes the service by subscribing to risk rejection events.
12304
+ * Uses singleshot to ensure initialization happens only once.
12305
+ * Automatically called on first use.
12306
+ *
12307
+ * @example
12308
+ * ```typescript
12309
+ * const service = new RiskMarkdownService();
12310
+ * await service.init(); // Subscribe to rejection events
12311
+ * ```
12312
+ */
12313
+ this.init = functoolsKit.singleshot(async () => {
12314
+ this.loggerService.log("riskMarkdownService init");
12315
+ riskSubject.subscribe(this.tickRejection);
12316
+ });
12317
+ }
12318
+ }
12319
+
12320
+ {
12321
+ provide(TYPES.loggerService, () => new LoggerService());
12322
+ }
12323
+ {
12324
+ provide(TYPES.executionContextService, () => new ExecutionContextService());
12325
+ provide(TYPES.methodContextService, () => new MethodContextService());
12326
+ }
12327
+ {
12328
+ provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
12329
+ provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
12330
+ provide(TYPES.frameConnectionService, () => new FrameConnectionService());
12331
+ provide(TYPES.sizingConnectionService, () => new SizingConnectionService());
12332
+ provide(TYPES.riskConnectionService, () => new RiskConnectionService());
12333
+ provide(TYPES.optimizerConnectionService, () => new OptimizerConnectionService());
12334
+ provide(TYPES.partialConnectionService, () => new PartialConnectionService());
12335
+ }
12336
+ {
12337
+ provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
12338
+ provide(TYPES.strategySchemaService, () => new StrategySchemaService());
12339
+ provide(TYPES.frameSchemaService, () => new FrameSchemaService());
12340
+ provide(TYPES.walkerSchemaService, () => new WalkerSchemaService());
12341
+ provide(TYPES.sizingSchemaService, () => new SizingSchemaService());
12342
+ provide(TYPES.riskSchemaService, () => new RiskSchemaService());
12343
+ provide(TYPES.optimizerSchemaService, () => new OptimizerSchemaService());
12344
+ }
12345
+ {
12346
+ provide(TYPES.exchangeCoreService, () => new ExchangeCoreService());
12347
+ provide(TYPES.strategyCoreService, () => new StrategyCoreService());
12348
+ provide(TYPES.frameCoreService, () => new FrameCoreService());
11873
12349
  }
11874
12350
  {
11875
12351
  provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
@@ -11901,6 +12377,7 @@ class ConfigValidationService {
11901
12377
  provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
11902
12378
  provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
11903
12379
  provide(TYPES.outlineMarkdownService, () => new OutlineMarkdownService());
12380
+ provide(TYPES.riskMarkdownService, () => new RiskMarkdownService());
11904
12381
  }
11905
12382
  {
11906
12383
  provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
@@ -11976,6 +12453,7 @@ const markdownServices = {
11976
12453
  heatMarkdownService: inject(TYPES.heatMarkdownService),
11977
12454
  partialMarkdownService: inject(TYPES.partialMarkdownService),
11978
12455
  outlineMarkdownService: inject(TYPES.outlineMarkdownService),
12456
+ riskMarkdownService: inject(TYPES.riskMarkdownService),
11979
12457
  };
11980
12458
  const validationServices = {
11981
12459
  exchangeValidationService: inject(TYPES.exchangeValidationService),
@@ -12750,6 +13228,8 @@ const LISTEN_PARTIAL_PROFIT_METHOD_NAME = "event.listenPartialProfit";
12750
13228
  const LISTEN_PARTIAL_PROFIT_ONCE_METHOD_NAME = "event.listenPartialProfitOnce";
12751
13229
  const LISTEN_PARTIAL_LOSS_METHOD_NAME = "event.listenPartialLoss";
12752
13230
  const LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME = "event.listenPartialLossOnce";
13231
+ const LISTEN_RISK_METHOD_NAME = "event.listenRisk";
13232
+ const LISTEN_RISK_ONCE_METHOD_NAME = "event.listenRiskOnce";
12753
13233
  /**
12754
13234
  * Subscribes to all signal events with queued async processing.
12755
13235
  *
@@ -13548,6 +14028,75 @@ function listenPartialLossOnce(filterFn, fn) {
13548
14028
  backtest$1.loggerService.log(LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME);
13549
14029
  return partialLossSubject.filter(filterFn).once(fn);
13550
14030
  }
14031
+ /**
14032
+ * Subscribes to risk rejection events with queued async processing.
14033
+ *
14034
+ * Emits ONLY when a signal is rejected due to risk validation failure.
14035
+ * Does not emit for allowed signals (prevents spam).
14036
+ * Events are processed sequentially in order received, even if callback is async.
14037
+ * Uses queued wrapper to prevent concurrent execution of the callback.
14038
+ *
14039
+ * @param fn - Callback function to handle risk rejection events
14040
+ * @returns Unsubscribe function to stop listening to events
14041
+ *
14042
+ * @example
14043
+ * ```typescript
14044
+ * import { listenRisk } from "./function/event";
14045
+ *
14046
+ * const unsubscribe = listenRisk((event) => {
14047
+ * console.log(`[RISK REJECTED] Signal for ${event.symbol}`);
14048
+ * console.log(`Strategy: ${event.strategyName}`);
14049
+ * console.log(`Position: ${event.pendingSignal.position}`);
14050
+ * console.log(`Active positions: ${event.activePositionCount}`);
14051
+ * console.log(`Reason: ${event.comment}`);
14052
+ * console.log(`Price: ${event.currentPrice}`);
14053
+ * });
14054
+ *
14055
+ * // Later: stop listening
14056
+ * unsubscribe();
14057
+ * ```
14058
+ */
14059
+ function listenRisk(fn) {
14060
+ backtest$1.loggerService.log(LISTEN_RISK_METHOD_NAME);
14061
+ return riskSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
14062
+ }
14063
+ /**
14064
+ * Subscribes to filtered risk rejection events with one-time execution.
14065
+ *
14066
+ * Listens for events matching the filter predicate, then executes callback once
14067
+ * and automatically unsubscribes. Useful for waiting for specific risk rejection conditions.
14068
+ *
14069
+ * @param filterFn - Predicate to filter which events trigger the callback
14070
+ * @param fn - Callback function to handle the filtered event (called only once)
14071
+ * @returns Unsubscribe function to cancel the listener before it fires
14072
+ *
14073
+ * @example
14074
+ * ```typescript
14075
+ * import { listenRiskOnce } from "./function/event";
14076
+ *
14077
+ * // Wait for first risk rejection on BTCUSDT
14078
+ * listenRiskOnce(
14079
+ * (event) => event.symbol === "BTCUSDT",
14080
+ * (event) => {
14081
+ * console.log("BTCUSDT signal rejected!");
14082
+ * console.log("Reason:", event.comment);
14083
+ * }
14084
+ * );
14085
+ *
14086
+ * // Wait for rejection due to position limit
14087
+ * const cancel = listenRiskOnce(
14088
+ * (event) => event.comment.includes("Max") && event.activePositionCount >= 3,
14089
+ * (event) => console.log("Position limit reached:", event.activePositionCount)
14090
+ * );
14091
+ *
14092
+ * // Cancel if needed before event fires
14093
+ * cancel();
14094
+ * ```
14095
+ */
14096
+ function listenRiskOnce(filterFn, fn) {
14097
+ backtest$1.loggerService.log(LISTEN_RISK_ONCE_METHOD_NAME);
14098
+ return riskSubject.filter(filterFn).once(fn);
14099
+ }
13551
14100
 
13552
14101
  const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
13553
14102
  const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
@@ -16172,6 +16721,186 @@ class ConstantUtils {
16172
16721
  */
16173
16722
  const Constant = new ConstantUtils();
16174
16723
 
16724
+ const RISK_METHOD_NAME_GET_DATA = "RiskUtils.getData";
16725
+ const RISK_METHOD_NAME_GET_REPORT = "RiskUtils.getReport";
16726
+ const RISK_METHOD_NAME_DUMP = "RiskUtils.dump";
16727
+ /**
16728
+ * Utility class for accessing risk rejection reports and statistics.
16729
+ *
16730
+ * Provides static-like methods (via singleton instance) to retrieve data
16731
+ * accumulated by RiskMarkdownService from risk rejection events.
16732
+ *
16733
+ * Features:
16734
+ * - Statistical data extraction (total rejections count, by symbol, by strategy)
16735
+ * - Markdown report generation with event tables
16736
+ * - File export to disk
16737
+ *
16738
+ * Data source:
16739
+ * - RiskMarkdownService listens to riskSubject
16740
+ * - Accumulates rejection events in ReportStorage (max 250 events per symbol-strategy pair)
16741
+ * - Events include: timestamp, symbol, strategyName, position, exchangeName, price, activePositionCount, comment
16742
+ *
16743
+ * @example
16744
+ * ```typescript
16745
+ * import { Risk } from "./classes/Risk";
16746
+ *
16747
+ * // Get statistical data for BTCUSDT:my-strategy
16748
+ * const stats = await Risk.getData("BTCUSDT", "my-strategy");
16749
+ * console.log(`Total rejections: ${stats.totalRejections}`);
16750
+ * console.log(`By symbol:`, stats.bySymbol);
16751
+ * console.log(`By strategy:`, stats.byStrategy);
16752
+ *
16753
+ * // Generate markdown report
16754
+ * const markdown = await Risk.getReport("BTCUSDT", "my-strategy");
16755
+ * console.log(markdown); // Formatted table with all rejection events
16756
+ *
16757
+ * // Export report to file
16758
+ * await Risk.dump("BTCUSDT", "my-strategy"); // Saves to ./dump/risk/BTCUSDT_my-strategy.md
16759
+ * await Risk.dump("BTCUSDT", "my-strategy", "./custom/path"); // Custom directory
16760
+ * ```
16761
+ */
16762
+ class RiskUtils {
16763
+ constructor() {
16764
+ /**
16765
+ * Retrieves statistical data from accumulated risk rejection events.
16766
+ *
16767
+ * Delegates to RiskMarkdownService.getData() which reads from ReportStorage.
16768
+ * Returns aggregated metrics calculated from all rejection events.
16769
+ *
16770
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16771
+ * @param strategyName - Strategy name (e.g., "my-strategy")
16772
+ * @returns Promise resolving to RiskStatistics object with counts and event list
16773
+ *
16774
+ * @example
16775
+ * ```typescript
16776
+ * const stats = await Risk.getData("BTCUSDT", "my-strategy");
16777
+ *
16778
+ * console.log(`Total rejections: ${stats.totalRejections}`);
16779
+ * console.log(`Rejections by symbol:`, stats.bySymbol);
16780
+ * console.log(`Rejections by strategy:`, stats.byStrategy);
16781
+ *
16782
+ * // Iterate through all rejection events
16783
+ * for (const event of stats.eventList) {
16784
+ * console.log(`REJECTED: ${event.symbol} - ${event.comment} (${event.activePositionCount} active)`);
16785
+ * }
16786
+ * ```
16787
+ */
16788
+ this.getData = async (symbol, strategyName) => {
16789
+ backtest$1.loggerService.info(RISK_METHOD_NAME_GET_DATA, { symbol, strategyName });
16790
+ backtest$1.strategyValidationService.validate(strategyName, RISK_METHOD_NAME_GET_DATA);
16791
+ {
16792
+ const { riskName } = backtest$1.strategySchemaService.get(strategyName);
16793
+ riskName && backtest$1.riskValidationService.validate(riskName, RISK_METHOD_NAME_GET_DATA);
16794
+ }
16795
+ return await backtest$1.riskMarkdownService.getData(symbol, strategyName);
16796
+ };
16797
+ /**
16798
+ * Generates markdown report with all risk rejection events for a symbol-strategy pair.
16799
+ *
16800
+ * Creates formatted table containing:
16801
+ * - Symbol
16802
+ * - Strategy
16803
+ * - Position (LONG/SHORT)
16804
+ * - Exchange
16805
+ * - Price
16806
+ * - Active Positions (at rejection time)
16807
+ * - Reason (from validation note)
16808
+ * - Timestamp (ISO 8601)
16809
+ *
16810
+ * Also includes summary statistics at the end (total rejections, by symbol, by strategy).
16811
+ *
16812
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16813
+ * @param strategyName - Strategy name (e.g., "my-strategy")
16814
+ * @returns Promise resolving to markdown formatted report string
16815
+ *
16816
+ * @example
16817
+ * ```typescript
16818
+ * const markdown = await Risk.getReport("BTCUSDT", "my-strategy");
16819
+ * console.log(markdown);
16820
+ *
16821
+ * // Output:
16822
+ * // # Risk Rejection Report: BTCUSDT:my-strategy
16823
+ * //
16824
+ * // | Symbol | Strategy | Position | Exchange | Price | Active Positions | Reason | Timestamp |
16825
+ * // | --- | --- | --- | --- | --- | --- | --- | --- |
16826
+ * // | BTCUSDT | my-strategy | LONG | binance | 50000.00000000 USD | 3 | Max 3 positions allowed | 2024-01-15T10:30:00.000Z |
16827
+ * //
16828
+ * // **Total rejections:** 1
16829
+ * //
16830
+ * // ## Rejections by Symbol
16831
+ * // - BTCUSDT: 1
16832
+ * //
16833
+ * // ## Rejections by Strategy
16834
+ * // - my-strategy: 1
16835
+ * ```
16836
+ */
16837
+ this.getReport = async (symbol, strategyName) => {
16838
+ backtest$1.loggerService.info(RISK_METHOD_NAME_GET_REPORT, { symbol, strategyName });
16839
+ backtest$1.strategyValidationService.validate(strategyName, RISK_METHOD_NAME_GET_REPORT);
16840
+ {
16841
+ const { riskName } = backtest$1.strategySchemaService.get(strategyName);
16842
+ riskName && backtest$1.riskValidationService.validate(riskName, RISK_METHOD_NAME_GET_REPORT);
16843
+ }
16844
+ return await backtest$1.riskMarkdownService.getReport(symbol, strategyName);
16845
+ };
16846
+ /**
16847
+ * Generates and saves markdown report to file.
16848
+ *
16849
+ * Creates directory if it doesn't exist.
16850
+ * Filename format: {symbol}_{strategyName}.md (e.g., "BTCUSDT_my-strategy.md")
16851
+ *
16852
+ * Delegates to RiskMarkdownService.dump() which:
16853
+ * 1. Generates markdown report via getReport()
16854
+ * 2. Creates output directory (recursive mkdir)
16855
+ * 3. Writes file with UTF-8 encoding
16856
+ * 4. Logs success/failure to console
16857
+ *
16858
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16859
+ * @param strategyName - Strategy name (e.g., "my-strategy")
16860
+ * @param path - Output directory path (default: "./dump/risk")
16861
+ * @returns Promise that resolves when file is written
16862
+ *
16863
+ * @example
16864
+ * ```typescript
16865
+ * // Save to default path: ./dump/risk/BTCUSDT_my-strategy.md
16866
+ * await Risk.dump("BTCUSDT", "my-strategy");
16867
+ *
16868
+ * // Save to custom path: ./reports/risk/BTCUSDT_my-strategy.md
16869
+ * await Risk.dump("BTCUSDT", "my-strategy", "./reports/risk");
16870
+ *
16871
+ * // After multiple symbols backtested, export all risk reports
16872
+ * for (const symbol of ["BTCUSDT", "ETHUSDT", "BNBUSDT"]) {
16873
+ * await Risk.dump(symbol, "my-strategy", "./backtest-results");
16874
+ * }
16875
+ * ```
16876
+ */
16877
+ this.dump = async (symbol, strategyName, path) => {
16878
+ backtest$1.loggerService.info(RISK_METHOD_NAME_DUMP, { symbol, strategyName, path });
16879
+ backtest$1.strategyValidationService.validate(strategyName, RISK_METHOD_NAME_DUMP);
16880
+ {
16881
+ const { riskName } = backtest$1.strategySchemaService.get(strategyName);
16882
+ riskName && backtest$1.riskValidationService.validate(riskName, RISK_METHOD_NAME_DUMP);
16883
+ }
16884
+ await backtest$1.riskMarkdownService.dump(symbol, strategyName, path);
16885
+ };
16886
+ }
16887
+ }
16888
+ /**
16889
+ * Global singleton instance of RiskUtils.
16890
+ * Provides static-like access to risk rejection reporting methods.
16891
+ *
16892
+ * @example
16893
+ * ```typescript
16894
+ * import { Risk } from "backtest-kit";
16895
+ *
16896
+ * // Usage same as RiskUtils methods
16897
+ * const stats = await Risk.getData("BTCUSDT", "my-strategy");
16898
+ * const report = await Risk.getReport("BTCUSDT", "my-strategy");
16899
+ * await Risk.dump("BTCUSDT", "my-strategy");
16900
+ * ```
16901
+ */
16902
+ const Risk = new RiskUtils();
16903
+
16175
16904
  exports.Backtest = Backtest;
16176
16905
  exports.Constant = Constant;
16177
16906
  exports.ExecutionContextService = ExecutionContextService;
@@ -16187,6 +16916,7 @@ exports.PersistRiskAdapter = PersistRiskAdapter;
16187
16916
  exports.PersistScheduleAdapter = PersistScheduleAdapter;
16188
16917
  exports.PersistSignalAdapter = PersistSignalAdapter;
16189
16918
  exports.PositionSize = PositionSize;
16919
+ exports.Risk = Risk;
16190
16920
  exports.Schedule = Schedule;
16191
16921
  exports.Walker = Walker;
16192
16922
  exports.addExchange = addExchange;
@@ -16229,6 +16959,8 @@ exports.listenPartialLossOnce = listenPartialLossOnce;
16229
16959
  exports.listenPartialProfit = listenPartialProfit;
16230
16960
  exports.listenPartialProfitOnce = listenPartialProfitOnce;
16231
16961
  exports.listenPerformance = listenPerformance;
16962
+ exports.listenRisk = listenRisk;
16963
+ exports.listenRiskOnce = listenRiskOnce;
16232
16964
  exports.listenSignal = listenSignal;
16233
16965
  exports.listenSignalBacktest = listenSignalBacktest;
16234
16966
  exports.listenSignalBacktestOnce = listenSignalBacktestOnce;