backtest-kit 1.5.16 → 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$4 = 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$4) {
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$4) {
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$4) {
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$4) {
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$3 = 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,8 @@ 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$3) {
7034
- this._eventList.shift();
7098
+ if (this._eventList.length > MAX_EVENTS$4) {
7099
+ this._eventList.pop();
7035
7100
  }
7036
7101
  }
7037
7102
  /**
@@ -7055,10 +7120,10 @@ let ReportStorage$2 = class ReportStorage {
7055
7120
  stopLoss: data.signal.priceStopLoss,
7056
7121
  duration: durationMin,
7057
7122
  };
7058
- this._eventList.push(newEvent);
7123
+ this._eventList.unshift(newEvent);
7059
7124
  // Trim queue if exceeded MAX_EVENTS
7060
- if (this._eventList.length > MAX_EVENTS$3) {
7061
- this._eventList.shift();
7125
+ if (this._eventList.length > MAX_EVENTS$4) {
7126
+ this._eventList.pop();
7062
7127
  }
7063
7128
  }
7064
7129
  /**
@@ -7083,10 +7148,10 @@ let ReportStorage$2 = class ReportStorage {
7083
7148
  closeTimestamp: data.closeTimestamp,
7084
7149
  duration: durationMin,
7085
7150
  };
7086
- this._eventList.push(newEvent);
7151
+ this._eventList.unshift(newEvent);
7087
7152
  // Trim queue if exceeded MAX_EVENTS
7088
- if (this._eventList.length > MAX_EVENTS$3) {
7089
- this._eventList.shift();
7153
+ if (this._eventList.length > MAX_EVENTS$4) {
7154
+ this._eventList.pop();
7090
7155
  }
7091
7156
  }
7092
7157
  /**
@@ -7155,9 +7220,9 @@ let ReportStorage$2 = class ReportStorage {
7155
7220
  "No scheduled signals recorded yet."
7156
7221
  ].join("\n");
7157
7222
  }
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)));
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)));
7161
7226
  const tableData = [header, separator, ...rows];
7162
7227
  const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7163
7228
  return [
@@ -7225,7 +7290,7 @@ class ScheduleMarkdownService {
7225
7290
  * Memoized function to get or create ReportStorage for a symbol-strategy pair.
7226
7291
  * Each symbol-strategy combination gets its own isolated storage instance.
7227
7292
  */
7228
- this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$2());
7293
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$3());
7229
7294
  /**
7230
7295
  * Processes tick events and accumulates scheduled/opened/cancelled events.
7231
7296
  * Should be called from signalEmitter subscription.
@@ -7392,7 +7457,7 @@ function percentile(sortedArray, p) {
7392
7457
  return sortedArray[Math.max(0, index)];
7393
7458
  }
7394
7459
  /** Maximum number of performance events to store per strategy */
7395
- const MAX_EVENTS$2 = 10000;
7460
+ const MAX_EVENTS$3 = 10000;
7396
7461
  /**
7397
7462
  * Storage class for accumulating performance metrics per strategy.
7398
7463
  * Maintains a list of all performance events and provides aggregated statistics.
@@ -7408,10 +7473,10 @@ class PerformanceStorage {
7408
7473
  * @param event - Performance event with timing data
7409
7474
  */
7410
7475
  addEvent(event) {
7411
- this._events.push(event);
7476
+ this._events.unshift(event);
7412
7477
  // Trim queue if exceeded MAX_EVENTS (keep most recent)
7413
- if (this._events.length > MAX_EVENTS$2) {
7414
- this._events.shift();
7478
+ if (this._events.length > MAX_EVENTS$3) {
7479
+ this._events.pop();
7415
7480
  }
7416
7481
  }
7417
7482
  /**
@@ -7877,7 +7942,7 @@ const pnlColumns = [
7877
7942
  * Storage class for accumulating walker results.
7878
7943
  * Maintains a list of all strategy results and provides methods to generate reports.
7879
7944
  */
7880
- let ReportStorage$1 = class ReportStorage {
7945
+ let ReportStorage$2 = class ReportStorage {
7881
7946
  constructor(walkerName) {
7882
7947
  this.walkerName = walkerName;
7883
7948
  /** Walker metadata (set from first addResult call) */
@@ -7895,17 +7960,13 @@ let ReportStorage$1 = class ReportStorage {
7895
7960
  * @param data - Walker contract with strategy result
7896
7961
  */
7897
7962
  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
7963
+ this._totalStrategies = data.totalStrategies;
7964
+ this._bestMetric = data.bestMetric;
7965
+ this._bestStrategy = data.bestStrategy;
7904
7966
  if (data.strategyName === data.bestStrategy) {
7905
7967
  this._bestStats = data.stats;
7906
7968
  }
7907
- // Add strategy result to comparison list
7908
- this._strategyResults.push({
7969
+ this._strategyResults.unshift({
7909
7970
  strategyName: data.strategyName,
7910
7971
  stats: data.stats,
7911
7972
  metricValue: data.metricValue,
@@ -8089,7 +8150,7 @@ class WalkerMarkdownService {
8089
8150
  * Memoized function to get or create ReportStorage for a walker.
8090
8151
  * Each walker gets its own isolated storage instance.
8091
8152
  */
8092
- this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$1(walkerName));
8153
+ this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$2(walkerName));
8093
8154
  /**
8094
8155
  * Processes walker progress events and accumulates strategy results.
8095
8156
  * Should be called from walkerEmitter.
@@ -8260,7 +8321,7 @@ function isUnsafe(value) {
8260
8321
  }
8261
8322
  return false;
8262
8323
  }
8263
- const columns$1 = [
8324
+ const columns$2 = [
8264
8325
  {
8265
8326
  key: "symbol",
8266
8327
  label: "Symbol",
@@ -8323,7 +8384,7 @@ const columns$1 = [
8323
8384
  },
8324
8385
  ];
8325
8386
  /** Maximum number of signals to store per symbol in heatmap reports */
8326
- const MAX_EVENTS$1 = 250;
8387
+ const MAX_EVENTS$2 = 250;
8327
8388
  /**
8328
8389
  * Storage class for accumulating closed signals per strategy and generating heatmap.
8329
8390
  * Maintains symbol-level statistics and provides portfolio-wide metrics.
@@ -8344,10 +8405,10 @@ class HeatmapStorage {
8344
8405
  this.symbolData.set(symbol, []);
8345
8406
  }
8346
8407
  const signals = this.symbolData.get(symbol);
8347
- signals.push(data);
8408
+ signals.unshift(data);
8348
8409
  // Trim queue if exceeded MAX_EVENTS per symbol
8349
- if (signals.length > MAX_EVENTS$1) {
8350
- signals.shift();
8410
+ if (signals.length > MAX_EVENTS$2) {
8411
+ signals.pop();
8351
8412
  }
8352
8413
  }
8353
8414
  /**
@@ -8567,9 +8628,9 @@ class HeatmapStorage {
8567
8628
  "*No data available*"
8568
8629
  ].join("\n");
8569
8630
  }
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)));
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)));
8573
8634
  const tableData = [header, separator, ...rows];
8574
8635
  const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
8575
8636
  return [
@@ -10607,7 +10668,7 @@ const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, back
10607
10668
  revenuePercent,
10608
10669
  backtest,
10609
10670
  });
10610
- 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());
10611
10672
  }
10612
10673
  }
10613
10674
  if (shouldPersist) {
@@ -10654,7 +10715,7 @@ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest,
10654
10715
  lossPercent,
10655
10716
  backtest,
10656
10717
  });
10657
- 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());
10658
10719
  }
10659
10720
  }
10660
10721
  if (shouldPersist) {
@@ -10925,6 +10986,7 @@ class ClientPartial {
10925
10986
  symbol,
10926
10987
  data,
10927
10988
  priceClose,
10989
+ backtest,
10928
10990
  });
10929
10991
  if (this._states === NEED_FETCH) {
10930
10992
  throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
@@ -10941,14 +11003,18 @@ class ClientPartial {
10941
11003
  * Emits PartialProfitContract event to all subscribers.
10942
11004
  *
10943
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
10944
11008
  * @param data - Signal row data
10945
11009
  * @param currentPrice - Current market price
10946
11010
  * @param level - Profit level reached
10947
11011
  * @param backtest - True if backtest mode
10948
11012
  * @param timestamp - Event timestamp in milliseconds
10949
11013
  */
10950
- 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({
10951
11015
  symbol,
11016
+ strategyName,
11017
+ exchangeName,
10952
11018
  data,
10953
11019
  currentPrice,
10954
11020
  level,
@@ -10962,14 +11028,18 @@ const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, tim
10962
11028
  * Emits PartialLossContract event to all subscribers.
10963
11029
  *
10964
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
10965
11033
  * @param data - Signal row data
10966
11034
  * @param currentPrice - Current market price
10967
11035
  * @param level - Loss level reached
10968
11036
  * @param backtest - True if backtest mode
10969
11037
  * @param timestamp - Event timestamp in milliseconds
10970
11038
  */
10971
- 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({
10972
11040
  symbol,
11041
+ strategyName,
11042
+ exchangeName,
10973
11043
  data,
10974
11044
  currentPrice,
10975
11045
  level,
@@ -11115,7 +11185,7 @@ class PartialConnectionService {
11115
11185
  }
11116
11186
  }
11117
11187
 
11118
- const columns = [
11188
+ const columns$1 = [
11119
11189
  {
11120
11190
  key: "action",
11121
11191
  label: "Action",
@@ -11163,12 +11233,12 @@ const columns = [
11163
11233
  },
11164
11234
  ];
11165
11235
  /** Maximum number of events to store in partial reports */
11166
- const MAX_EVENTS = 250;
11236
+ const MAX_EVENTS$1 = 250;
11167
11237
  /**
11168
11238
  * Storage class for accumulating partial profit/loss events per symbol-strategy pair.
11169
11239
  * Maintains a chronological list of profit and loss level events.
11170
11240
  */
11171
- class ReportStorage {
11241
+ let ReportStorage$1 = class ReportStorage {
11172
11242
  constructor() {
11173
11243
  /** Internal list of all partial events for this symbol */
11174
11244
  this._eventList = [];
@@ -11182,7 +11252,7 @@ class ReportStorage {
11182
11252
  * @param backtest - True if backtest mode
11183
11253
  */
11184
11254
  addProfitEvent(data, currentPrice, level, backtest, timestamp) {
11185
- this._eventList.push({
11255
+ this._eventList.unshift({
11186
11256
  timestamp,
11187
11257
  action: "profit",
11188
11258
  symbol: data.symbol,
@@ -11194,8 +11264,8 @@ class ReportStorage {
11194
11264
  backtest,
11195
11265
  });
11196
11266
  // Trim queue if exceeded MAX_EVENTS
11197
- if (this._eventList.length > MAX_EVENTS) {
11198
- this._eventList.shift();
11267
+ if (this._eventList.length > MAX_EVENTS$1) {
11268
+ this._eventList.pop();
11199
11269
  }
11200
11270
  }
11201
11271
  /**
@@ -11207,7 +11277,7 @@ class ReportStorage {
11207
11277
  * @param backtest - True if backtest mode
11208
11278
  */
11209
11279
  addLossEvent(data, currentPrice, level, backtest, timestamp) {
11210
- this._eventList.push({
11280
+ this._eventList.unshift({
11211
11281
  timestamp,
11212
11282
  action: "loss",
11213
11283
  symbol: data.symbol,
@@ -11219,8 +11289,8 @@ class ReportStorage {
11219
11289
  backtest,
11220
11290
  });
11221
11291
  // Trim queue if exceeded MAX_EVENTS
11222
- if (this._eventList.length > MAX_EVENTS) {
11223
- this._eventList.shift();
11292
+ if (this._eventList.length > MAX_EVENTS$1) {
11293
+ this._eventList.pop();
11224
11294
  }
11225
11295
  }
11226
11296
  /**
@@ -11262,9 +11332,9 @@ class ReportStorage {
11262
11332
  "No partial profit/loss events recorded yet."
11263
11333
  ].join("\n");
11264
11334
  }
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)));
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)));
11268
11338
  const tableData = [header, separator, ...rows];
11269
11339
  const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
11270
11340
  return [
@@ -11298,7 +11368,7 @@ class ReportStorage {
11298
11368
  console.error(`Failed to save markdown report:`, error);
11299
11369
  }
11300
11370
  }
11301
- }
11371
+ };
11302
11372
  /**
11303
11373
  * Service for generating and saving partial profit/loss markdown reports.
11304
11374
  *
@@ -11328,7 +11398,7 @@ class PartialMarkdownService {
11328
11398
  * Memoized function to get or create ReportStorage for a symbol-strategy pair.
11329
11399
  * Each symbol-strategy combination gets its own isolated storage instance.
11330
11400
  */
11331
- this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage());
11401
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$1());
11332
11402
  /**
11333
11403
  * Processes profit events and accumulates them.
11334
11404
  * Should be called from partialProfitSubject subscription.
@@ -11638,7 +11708,7 @@ class PartialGlobalService {
11638
11708
  * Warning threshold for message size in kilobytes.
11639
11709
  * Messages exceeding this size trigger console warnings.
11640
11710
  */
11641
- const WARN_KB = 100;
11711
+ const WARN_KB = 20;
11642
11712
  /**
11643
11713
  * Internal function for dumping signal data to markdown files.
11644
11714
  * Creates a directory structure with system prompts, user messages, and LLM output.
@@ -11900,6 +11970,353 @@ class ConfigValidationService {
11900
11970
  }
11901
11971
  }
11902
11972
 
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
+ }
12147
+ }
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
+
11903
12320
  {
11904
12321
  provide(TYPES.loggerService, () => new LoggerService());
11905
12322
  }
@@ -11960,6 +12377,7 @@ class ConfigValidationService {
11960
12377
  provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
11961
12378
  provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
11962
12379
  provide(TYPES.outlineMarkdownService, () => new OutlineMarkdownService());
12380
+ provide(TYPES.riskMarkdownService, () => new RiskMarkdownService());
11963
12381
  }
11964
12382
  {
11965
12383
  provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
@@ -12035,6 +12453,7 @@ const markdownServices = {
12035
12453
  heatMarkdownService: inject(TYPES.heatMarkdownService),
12036
12454
  partialMarkdownService: inject(TYPES.partialMarkdownService),
12037
12455
  outlineMarkdownService: inject(TYPES.outlineMarkdownService),
12456
+ riskMarkdownService: inject(TYPES.riskMarkdownService),
12038
12457
  };
12039
12458
  const validationServices = {
12040
12459
  exchangeValidationService: inject(TYPES.exchangeValidationService),
@@ -12809,6 +13228,8 @@ const LISTEN_PARTIAL_PROFIT_METHOD_NAME = "event.listenPartialProfit";
12809
13228
  const LISTEN_PARTIAL_PROFIT_ONCE_METHOD_NAME = "event.listenPartialProfitOnce";
12810
13229
  const LISTEN_PARTIAL_LOSS_METHOD_NAME = "event.listenPartialLoss";
12811
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";
12812
13233
  /**
12813
13234
  * Subscribes to all signal events with queued async processing.
12814
13235
  *
@@ -13607,6 +14028,75 @@ function listenPartialLossOnce(filterFn, fn) {
13607
14028
  backtest$1.loggerService.log(LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME);
13608
14029
  return partialLossSubject.filter(filterFn).once(fn);
13609
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
+ }
13610
14100
 
13611
14101
  const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
13612
14102
  const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
@@ -16231,6 +16721,186 @@ class ConstantUtils {
16231
16721
  */
16232
16722
  const Constant = new ConstantUtils();
16233
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
+
16234
16904
  exports.Backtest = Backtest;
16235
16905
  exports.Constant = Constant;
16236
16906
  exports.ExecutionContextService = ExecutionContextService;
@@ -16246,6 +16916,7 @@ exports.PersistRiskAdapter = PersistRiskAdapter;
16246
16916
  exports.PersistScheduleAdapter = PersistScheduleAdapter;
16247
16917
  exports.PersistSignalAdapter = PersistSignalAdapter;
16248
16918
  exports.PositionSize = PositionSize;
16919
+ exports.Risk = Risk;
16249
16920
  exports.Schedule = Schedule;
16250
16921
  exports.Walker = Walker;
16251
16922
  exports.addExchange = addExchange;
@@ -16288,6 +16959,8 @@ exports.listenPartialLossOnce = listenPartialLossOnce;
16288
16959
  exports.listenPartialProfit = listenPartialProfit;
16289
16960
  exports.listenPartialProfitOnce = listenPartialProfitOnce;
16290
16961
  exports.listenPerformance = listenPerformance;
16962
+ exports.listenRisk = listenRisk;
16963
+ exports.listenRiskOnce = listenRiskOnce;
16291
16964
  exports.listenSignal = listenSignal;
16292
16965
  exports.listenSignalBacktest = listenSignalBacktest;
16293
16966
  exports.listenSignalBacktestOnce = listenSignalBacktestOnce;