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.mjs CHANGED
@@ -200,6 +200,7 @@ const markdownServices$1 = {
200
200
  heatMarkdownService: Symbol('heatMarkdownService'),
201
201
  partialMarkdownService: Symbol('partialMarkdownService'),
202
202
  outlineMarkdownService: Symbol('outlineMarkdownService'),
203
+ riskMarkdownService: Symbol('riskMarkdownService'),
203
204
  };
204
205
  const validationServices$1 = {
205
206
  exchangeValidationService: Symbol('exchangeValidationService'),
@@ -440,11 +441,12 @@ const GET_CANDLES_FN = async (dto, since, self) => {
440
441
  }
441
442
  catch (err) {
442
443
  const message = `ClientExchange GET_CANDLES_FN: attempt ${i + 1} failed for symbol=${dto.symbol}, interval=${dto.interval}, since=${since.toISOString()}, limit=${dto.limit}}`;
443
- self.params.logger.warn(message, {
444
+ const payload = {
444
445
  error: errorData(err),
445
446
  message: getErrorMessage(err),
446
- });
447
- console.warn(message);
447
+ };
448
+ self.params.logger.warn(message, payload);
449
+ console.warn(message, payload);
448
450
  lastError = err;
449
451
  await sleep(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS);
450
452
  }
@@ -1723,6 +1725,12 @@ const partialProfitSubject = new Subject();
1723
1725
  * Emits when a signal reaches a loss level (10%, 20%, 30%, etc).
1724
1726
  */
1725
1727
  const partialLossSubject = new Subject();
1728
+ /**
1729
+ * Risk rejection emitter for risk management violations.
1730
+ * Emits ONLY when a signal is rejected due to risk validation failure.
1731
+ * Does not emit for allowed signals (prevents spam).
1732
+ */
1733
+ const riskSubject = new Subject();
1726
1734
 
1727
1735
  var emitters = /*#__PURE__*/Object.freeze({
1728
1736
  __proto__: null,
@@ -1737,6 +1745,7 @@ var emitters = /*#__PURE__*/Object.freeze({
1737
1745
  progressBacktestEmitter: progressBacktestEmitter,
1738
1746
  progressOptimizerEmitter: progressOptimizerEmitter,
1739
1747
  progressWalkerEmitter: progressWalkerEmitter,
1748
+ riskSubject: riskSubject,
1740
1749
  signalBacktestEmitter: signalBacktestEmitter,
1741
1750
  signalEmitter: signalEmitter,
1742
1751
  signalLiveEmitter: signalLiveEmitter,
@@ -1823,6 +1832,9 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1823
1832
  if (signal.position === undefined || signal.position === null) {
1824
1833
  errors.push('position is required and must be "long" or "short"');
1825
1834
  }
1835
+ if (signal.position !== "long" && signal.position !== "short") {
1836
+ errors.push(`position must be "long" or "short", got "${signal.position}"`);
1837
+ }
1826
1838
  // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
1827
1839
  if (!isFinite(currentPrice)) {
1828
1840
  errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
@@ -2109,10 +2121,13 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
2109
2121
  }, {
2110
2122
  defaultValue: null,
2111
2123
  fallback: (error) => {
2112
- backtest$1.loggerService.warn("ClientStrategy exception thrown", {
2124
+ const message = "ClientStrategy exception thrown";
2125
+ const payload = {
2113
2126
  error: errorData(error),
2114
2127
  message: getErrorMessage(error),
2115
- });
2128
+ };
2129
+ backtest$1.loggerService.warn(message, payload);
2130
+ console.warn(message, payload);
2116
2131
  errorEmitter.next(error);
2117
2132
  },
2118
2133
  });
@@ -2803,7 +2818,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2803
2818
  // Moving towards TP
2804
2819
  const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2805
2820
  const progressPercent = (currentDistance / tpDistance) * 100;
2806
- 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);
2821
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2807
2822
  if (self.params.callbacks?.onPartialProfit) {
2808
2823
  self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2809
2824
  }
@@ -2812,7 +2827,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2812
2827
  // Moving towards SL
2813
2828
  const slDistance = signal.priceOpen - signal.priceStopLoss;
2814
2829
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2815
- 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);
2830
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2816
2831
  if (self.params.callbacks?.onPartialLoss) {
2817
2832
  self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2818
2833
  }
@@ -2825,7 +2840,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2825
2840
  // Moving towards TP
2826
2841
  const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2827
2842
  const progressPercent = (currentDistance / tpDistance) * 100;
2828
- 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);
2843
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2829
2844
  if (self.params.callbacks?.onPartialProfit) {
2830
2845
  self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2831
2846
  }
@@ -2834,7 +2849,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2834
2849
  // Moving towards SL
2835
2850
  const slDistance = signal.priceStopLoss - signal.priceOpen;
2836
2851
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2837
- 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);
2852
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
2838
2853
  if (self.params.callbacks?.onPartialLoss) {
2839
2854
  self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2840
2855
  }
@@ -3789,10 +3804,13 @@ const DO_VALIDATION_FN = trycatch(async (validation, params) => {
3789
3804
  }, {
3790
3805
  defaultValue: false,
3791
3806
  fallback: (error) => {
3792
- backtest$1.loggerService.warn("ClientRisk exception thrown", {
3807
+ const message = "ClientRisk exception thrown";
3808
+ const payload = {
3793
3809
  error: errorData(error),
3794
3810
  message: getErrorMessage(error),
3795
- });
3811
+ };
3812
+ backtest$1.loggerService.warn(message, payload);
3813
+ console.warn(message, payload);
3796
3814
  validationSubject.next(error);
3797
3815
  },
3798
3816
  });
@@ -3862,17 +3880,25 @@ class ClientRisk {
3862
3880
  };
3863
3881
  // Execute custom validations
3864
3882
  let isValid = true;
3883
+ let rejectionNote = "N/A";
3865
3884
  if (this.params.validations) {
3866
3885
  for (const validation of this.params.validations) {
3867
3886
  if (not(await DO_VALIDATION_FN(typeof validation === "function"
3868
3887
  ? validation
3869
3888
  : validation.validate, payload))) {
3870
3889
  isValid = false;
3890
+ // Capture note from validation if available
3891
+ if (typeof validation !== "function" && validation.note) {
3892
+ rejectionNote = validation.note;
3893
+ }
3871
3894
  break;
3872
3895
  }
3873
3896
  }
3874
3897
  }
3875
3898
  if (!isValid) {
3899
+ // Call params.onRejected for riskSubject emission
3900
+ await this.params.onRejected(params.symbol, params, riskMap.size, rejectionNote, Date.now());
3901
+ // Call schema callbacks.onRejected if defined
3876
3902
  if (this.params.callbacks?.onRejected) {
3877
3903
  this.params.callbacks.onRejected(params.symbol, params);
3878
3904
  }
@@ -3935,6 +3961,28 @@ class ClientRisk {
3935
3961
  }
3936
3962
  }
3937
3963
 
3964
+ /**
3965
+ * Callback function for emitting risk rejection events to riskSubject.
3966
+ *
3967
+ * Called by ClientRisk when a signal is rejected due to risk validation failure.
3968
+ * Emits RiskContract event to all subscribers.
3969
+ *
3970
+ * @param symbol - Trading pair symbol
3971
+ * @param params - Risk check arguments
3972
+ * @param activePositionCount - Number of active positions at rejection time
3973
+ * @param comment - Rejection reason from validation note or "N/A"
3974
+ * @param timestamp - Event timestamp in milliseconds
3975
+ */
3976
+ const COMMIT_REJECTION_FN = async (symbol, params, activePositionCount, comment, timestamp) => await riskSubject.next({
3977
+ symbol,
3978
+ pendingSignal: params.pendingSignal,
3979
+ strategyName: params.strategyName,
3980
+ exchangeName: params.exchangeName,
3981
+ currentPrice: params.currentPrice,
3982
+ activePositionCount,
3983
+ comment,
3984
+ timestamp,
3985
+ });
3938
3986
  /**
3939
3987
  * Connection service routing risk operations to correct ClientRisk instance.
3940
3988
  *
@@ -3985,6 +4033,7 @@ class RiskConnectionService {
3985
4033
  return new ClientRisk({
3986
4034
  ...schema,
3987
4035
  logger: this.loggerService,
4036
+ onRejected: COMMIT_REJECTION_FN,
3988
4037
  });
3989
4038
  });
3990
4039
  /**
@@ -3992,6 +4041,7 @@ class RiskConnectionService {
3992
4041
  *
3993
4042
  * Routes to appropriate ClientRisk instance based on provided context.
3994
4043
  * Validates portfolio drawdown, symbol exposure, position count, and daily loss limits.
4044
+ * ClientRisk will emit riskSubject event via onRejected callback when signal is rejected.
3995
4045
  *
3996
4046
  * @param params - Risk check arguments (portfolio state, position details)
3997
4047
  * @param context - Execution context with risk name
@@ -5980,7 +6030,7 @@ function isUnsafe$3(value) {
5980
6030
  }
5981
6031
  return false;
5982
6032
  }
5983
- const columns$4 = [
6033
+ const columns$5 = [
5984
6034
  {
5985
6035
  key: "signalId",
5986
6036
  label: "Signal ID",
@@ -6054,11 +6104,13 @@ const columns$4 = [
6054
6104
  format: (data) => new Date(data.closeTimestamp).toISOString(),
6055
6105
  },
6056
6106
  ];
6107
+ /** Maximum number of signals to store in backtest reports */
6108
+ const MAX_EVENTS$6 = 250;
6057
6109
  /**
6058
6110
  * Storage class for accumulating closed signals per strategy.
6059
6111
  * Maintains a list of all closed signals and provides methods to generate reports.
6060
6112
  */
6061
- let ReportStorage$4 = class ReportStorage {
6113
+ let ReportStorage$5 = class ReportStorage {
6062
6114
  constructor() {
6063
6115
  /** Internal list of all closed signals for this strategy */
6064
6116
  this._signalList = [];
@@ -6069,7 +6121,11 @@ let ReportStorage$4 = class ReportStorage {
6069
6121
  * @param data - Closed signal data with PNL and close reason
6070
6122
  */
6071
6123
  addSignal(data) {
6072
- this._signalList.push(data);
6124
+ this._signalList.unshift(data);
6125
+ // Trim queue if exceeded MAX_EVENTS
6126
+ if (this._signalList.length > MAX_EVENTS$6) {
6127
+ this._signalList.pop();
6128
+ }
6073
6129
  }
6074
6130
  /**
6075
6131
  * Calculates statistical data from closed signals (Controller).
@@ -6152,9 +6208,9 @@ let ReportStorage$4 = class ReportStorage {
6152
6208
  "No signals closed yet."
6153
6209
  ].join("\n");
6154
6210
  }
6155
- const header = columns$4.map((col) => col.label);
6156
- const separator = columns$4.map(() => "---");
6157
- const rows = this._signalList.map((closedSignal) => columns$4.map((col) => col.format(closedSignal)));
6211
+ const header = columns$5.map((col) => col.label);
6212
+ const separator = columns$5.map(() => "---");
6213
+ const rows = this._signalList.map((closedSignal) => columns$5.map((col) => col.format(closedSignal)));
6158
6214
  const tableData = [header, separator, ...rows];
6159
6215
  const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
6160
6216
  return [
@@ -6230,7 +6286,7 @@ class BacktestMarkdownService {
6230
6286
  * Memoized function to get or create ReportStorage for a symbol-strategy pair.
6231
6287
  * Each symbol-strategy combination gets its own isolated storage instance.
6232
6288
  */
6233
- this.getStorage = memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$4());
6289
+ this.getStorage = memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$5());
6234
6290
  /**
6235
6291
  * Processes tick events and accumulates closed signals.
6236
6292
  * Should be called from IStrategyCallbacks.onTick.
@@ -6401,7 +6457,7 @@ function isUnsafe$2(value) {
6401
6457
  }
6402
6458
  return false;
6403
6459
  }
6404
- const columns$3 = [
6460
+ const columns$4 = [
6405
6461
  {
6406
6462
  key: "timestamp",
6407
6463
  label: "Timestamp",
@@ -6485,12 +6541,12 @@ const columns$3 = [
6485
6541
  },
6486
6542
  ];
6487
6543
  /** Maximum number of events to store in live trading reports */
6488
- const MAX_EVENTS$4 = 250;
6544
+ const MAX_EVENTS$5 = 250;
6489
6545
  /**
6490
6546
  * Storage class for accumulating all tick events per strategy.
6491
6547
  * Maintains a chronological list of all events (idle, opened, active, closed).
6492
6548
  */
6493
- let ReportStorage$3 = class ReportStorage {
6549
+ let ReportStorage$4 = class ReportStorage {
6494
6550
  constructor() {
6495
6551
  /** Internal list of all tick events for this strategy */
6496
6552
  this._eventList = [];
@@ -6517,9 +6573,9 @@ let ReportStorage$3 = class ReportStorage {
6517
6573
  return;
6518
6574
  }
6519
6575
  {
6520
- this._eventList.push(newEvent);
6521
- if (this._eventList.length > MAX_EVENTS$4) {
6522
- this._eventList.shift();
6576
+ this._eventList.unshift(newEvent);
6577
+ if (this._eventList.length > MAX_EVENTS$5) {
6578
+ this._eventList.pop();
6523
6579
  }
6524
6580
  }
6525
6581
  }
@@ -6529,7 +6585,7 @@ let ReportStorage$3 = class ReportStorage {
6529
6585
  * @param data - Opened tick result
6530
6586
  */
6531
6587
  addOpenedEvent(data) {
6532
- this._eventList.push({
6588
+ this._eventList.unshift({
6533
6589
  timestamp: data.signal.pendingAt,
6534
6590
  action: "opened",
6535
6591
  symbol: data.signal.symbol,
@@ -6542,12 +6598,13 @@ let ReportStorage$3 = class ReportStorage {
6542
6598
  stopLoss: data.signal.priceStopLoss,
6543
6599
  });
6544
6600
  // Trim queue if exceeded MAX_EVENTS
6545
- if (this._eventList.length > MAX_EVENTS$4) {
6546
- this._eventList.shift();
6601
+ if (this._eventList.length > MAX_EVENTS$5) {
6602
+ this._eventList.pop();
6547
6603
  }
6548
6604
  }
6549
6605
  /**
6550
6606
  * Adds an active event to the storage.
6607
+ * Replaces the last active event with the same signalId.
6551
6608
  *
6552
6609
  * @param data - Active tick result
6553
6610
  */
@@ -6566,10 +6623,18 @@ let ReportStorage$3 = class ReportStorage {
6566
6623
  percentTp: data.percentTp,
6567
6624
  percentSl: data.percentSl,
6568
6625
  };
6569
- this._eventList.push(newEvent);
6626
+ // Find the last active event with the same signalId
6627
+ const lastActiveIndex = this._eventList.findLastIndex((event) => event.action === "active" && event.signalId === data.signal.id);
6628
+ // Replace the last active event with the same signalId
6629
+ if (lastActiveIndex !== -1) {
6630
+ this._eventList[lastActiveIndex] = newEvent;
6631
+ return;
6632
+ }
6633
+ // If no previous active event found, add new event
6634
+ this._eventList.unshift(newEvent);
6570
6635
  // Trim queue if exceeded MAX_EVENTS
6571
- if (this._eventList.length > MAX_EVENTS$4) {
6572
- this._eventList.shift();
6636
+ if (this._eventList.length > MAX_EVENTS$5) {
6637
+ this._eventList.pop();
6573
6638
  }
6574
6639
  }
6575
6640
  /**
@@ -6595,10 +6660,10 @@ let ReportStorage$3 = class ReportStorage {
6595
6660
  closeReason: data.closeReason,
6596
6661
  duration: durationMin,
6597
6662
  };
6598
- this._eventList.push(newEvent);
6663
+ this._eventList.unshift(newEvent);
6599
6664
  // Trim queue if exceeded MAX_EVENTS
6600
- if (this._eventList.length > MAX_EVENTS$4) {
6601
- this._eventList.shift();
6665
+ if (this._eventList.length > MAX_EVENTS$5) {
6666
+ this._eventList.pop();
6602
6667
  }
6603
6668
  }
6604
6669
  /**
@@ -6697,9 +6762,9 @@ let ReportStorage$3 = class ReportStorage {
6697
6762
  "No events recorded yet."
6698
6763
  ].join("\n");
6699
6764
  }
6700
- const header = columns$3.map((col) => col.label);
6701
- const separator = columns$3.map(() => "---");
6702
- const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
6765
+ const header = columns$4.map((col) => col.label);
6766
+ const separator = columns$4.map(() => "---");
6767
+ const rows = this._eventList.map((event) => columns$4.map((col) => col.format(event)));
6703
6768
  const tableData = [header, separator, ...rows];
6704
6769
  const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
6705
6770
  return [
@@ -6778,7 +6843,7 @@ class LiveMarkdownService {
6778
6843
  * Memoized function to get or create ReportStorage for a symbol-strategy pair.
6779
6844
  * Each symbol-strategy combination gets its own isolated storage instance.
6780
6845
  */
6781
- this.getStorage = memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$3());
6846
+ this.getStorage = memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$4());
6782
6847
  /**
6783
6848
  * Processes tick events and accumulates all event types.
6784
6849
  * Should be called from IStrategyCallbacks.onTick.
@@ -6941,7 +7006,7 @@ class LiveMarkdownService {
6941
7006
  }
6942
7007
  }
6943
7008
 
6944
- const columns$2 = [
7009
+ const columns$3 = [
6945
7010
  {
6946
7011
  key: "timestamp",
6947
7012
  label: "Timestamp",
@@ -6999,12 +7064,12 @@ const columns$2 = [
6999
7064
  },
7000
7065
  ];
7001
7066
  /** Maximum number of events to store in schedule reports */
7002
- const MAX_EVENTS$3 = 250;
7067
+ const MAX_EVENTS$4 = 250;
7003
7068
  /**
7004
7069
  * Storage class for accumulating scheduled signal events per strategy.
7005
7070
  * Maintains a chronological list of scheduled and cancelled events.
7006
7071
  */
7007
- let ReportStorage$2 = class ReportStorage {
7072
+ let ReportStorage$3 = class ReportStorage {
7008
7073
  constructor() {
7009
7074
  /** Internal list of all scheduled events for this strategy */
7010
7075
  this._eventList = [];
@@ -7015,7 +7080,7 @@ let ReportStorage$2 = class ReportStorage {
7015
7080
  * @param data - Scheduled tick result
7016
7081
  */
7017
7082
  addScheduledEvent(data) {
7018
- this._eventList.push({
7083
+ this._eventList.unshift({
7019
7084
  timestamp: data.signal.scheduledAt,
7020
7085
  action: "scheduled",
7021
7086
  symbol: data.signal.symbol,
@@ -7028,8 +7093,8 @@ let ReportStorage$2 = class ReportStorage {
7028
7093
  stopLoss: data.signal.priceStopLoss,
7029
7094
  });
7030
7095
  // Trim queue if exceeded MAX_EVENTS
7031
- if (this._eventList.length > MAX_EVENTS$3) {
7032
- this._eventList.shift();
7096
+ if (this._eventList.length > MAX_EVENTS$4) {
7097
+ this._eventList.pop();
7033
7098
  }
7034
7099
  }
7035
7100
  /**
@@ -7053,10 +7118,10 @@ let ReportStorage$2 = class ReportStorage {
7053
7118
  stopLoss: data.signal.priceStopLoss,
7054
7119
  duration: durationMin,
7055
7120
  };
7056
- this._eventList.push(newEvent);
7121
+ this._eventList.unshift(newEvent);
7057
7122
  // Trim queue if exceeded MAX_EVENTS
7058
- if (this._eventList.length > MAX_EVENTS$3) {
7059
- this._eventList.shift();
7123
+ if (this._eventList.length > MAX_EVENTS$4) {
7124
+ this._eventList.pop();
7060
7125
  }
7061
7126
  }
7062
7127
  /**
@@ -7081,10 +7146,10 @@ let ReportStorage$2 = class ReportStorage {
7081
7146
  closeTimestamp: data.closeTimestamp,
7082
7147
  duration: durationMin,
7083
7148
  };
7084
- this._eventList.push(newEvent);
7149
+ this._eventList.unshift(newEvent);
7085
7150
  // Trim queue if exceeded MAX_EVENTS
7086
- if (this._eventList.length > MAX_EVENTS$3) {
7087
- this._eventList.shift();
7151
+ if (this._eventList.length > MAX_EVENTS$4) {
7152
+ this._eventList.pop();
7088
7153
  }
7089
7154
  }
7090
7155
  /**
@@ -7153,9 +7218,9 @@ let ReportStorage$2 = class ReportStorage {
7153
7218
  "No scheduled signals recorded yet."
7154
7219
  ].join("\n");
7155
7220
  }
7156
- const header = columns$2.map((col) => col.label);
7157
- const separator = columns$2.map(() => "---");
7158
- const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
7221
+ const header = columns$3.map((col) => col.label);
7222
+ const separator = columns$3.map(() => "---");
7223
+ const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
7159
7224
  const tableData = [header, separator, ...rows];
7160
7225
  const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7161
7226
  return [
@@ -7223,7 +7288,7 @@ class ScheduleMarkdownService {
7223
7288
  * Memoized function to get or create ReportStorage for a symbol-strategy pair.
7224
7289
  * Each symbol-strategy combination gets its own isolated storage instance.
7225
7290
  */
7226
- this.getStorage = memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$2());
7291
+ this.getStorage = memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$3());
7227
7292
  /**
7228
7293
  * Processes tick events and accumulates scheduled/opened/cancelled events.
7229
7294
  * Should be called from signalEmitter subscription.
@@ -7390,7 +7455,7 @@ function percentile(sortedArray, p) {
7390
7455
  return sortedArray[Math.max(0, index)];
7391
7456
  }
7392
7457
  /** Maximum number of performance events to store per strategy */
7393
- const MAX_EVENTS$2 = 10000;
7458
+ const MAX_EVENTS$3 = 10000;
7394
7459
  /**
7395
7460
  * Storage class for accumulating performance metrics per strategy.
7396
7461
  * Maintains a list of all performance events and provides aggregated statistics.
@@ -7406,10 +7471,10 @@ class PerformanceStorage {
7406
7471
  * @param event - Performance event with timing data
7407
7472
  */
7408
7473
  addEvent(event) {
7409
- this._events.push(event);
7474
+ this._events.unshift(event);
7410
7475
  // Trim queue if exceeded MAX_EVENTS (keep most recent)
7411
- if (this._events.length > MAX_EVENTS$2) {
7412
- this._events.shift();
7476
+ if (this._events.length > MAX_EVENTS$3) {
7477
+ this._events.pop();
7413
7478
  }
7414
7479
  }
7415
7480
  /**
@@ -7875,7 +7940,7 @@ const pnlColumns = [
7875
7940
  * Storage class for accumulating walker results.
7876
7941
  * Maintains a list of all strategy results and provides methods to generate reports.
7877
7942
  */
7878
- let ReportStorage$1 = class ReportStorage {
7943
+ let ReportStorage$2 = class ReportStorage {
7879
7944
  constructor(walkerName) {
7880
7945
  this.walkerName = walkerName;
7881
7946
  /** Walker metadata (set from first addResult call) */
@@ -7893,17 +7958,13 @@ let ReportStorage$1 = class ReportStorage {
7893
7958
  * @param data - Walker contract with strategy result
7894
7959
  */
7895
7960
  addResult(data) {
7896
- {
7897
- this._bestMetric = data.bestMetric;
7898
- this._bestStrategy = data.bestStrategy;
7899
- this._totalStrategies = data.totalStrategies;
7900
- }
7901
- // Update best stats only if this strategy is the current best
7961
+ this._totalStrategies = data.totalStrategies;
7962
+ this._bestMetric = data.bestMetric;
7963
+ this._bestStrategy = data.bestStrategy;
7902
7964
  if (data.strategyName === data.bestStrategy) {
7903
7965
  this._bestStats = data.stats;
7904
7966
  }
7905
- // Add strategy result to comparison list
7906
- this._strategyResults.push({
7967
+ this._strategyResults.unshift({
7907
7968
  strategyName: data.strategyName,
7908
7969
  stats: data.stats,
7909
7970
  metricValue: data.metricValue,
@@ -8087,7 +8148,7 @@ class WalkerMarkdownService {
8087
8148
  * Memoized function to get or create ReportStorage for a walker.
8088
8149
  * Each walker gets its own isolated storage instance.
8089
8150
  */
8090
- this.getStorage = memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$1(walkerName));
8151
+ this.getStorage = memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$2(walkerName));
8091
8152
  /**
8092
8153
  * Processes walker progress events and accumulates strategy results.
8093
8154
  * Should be called from walkerEmitter.
@@ -8258,7 +8319,7 @@ function isUnsafe(value) {
8258
8319
  }
8259
8320
  return false;
8260
8321
  }
8261
- const columns$1 = [
8322
+ const columns$2 = [
8262
8323
  {
8263
8324
  key: "symbol",
8264
8325
  label: "Symbol",
@@ -8321,7 +8382,7 @@ const columns$1 = [
8321
8382
  },
8322
8383
  ];
8323
8384
  /** Maximum number of signals to store per symbol in heatmap reports */
8324
- const MAX_EVENTS$1 = 250;
8385
+ const MAX_EVENTS$2 = 250;
8325
8386
  /**
8326
8387
  * Storage class for accumulating closed signals per strategy and generating heatmap.
8327
8388
  * Maintains symbol-level statistics and provides portfolio-wide metrics.
@@ -8342,10 +8403,10 @@ class HeatmapStorage {
8342
8403
  this.symbolData.set(symbol, []);
8343
8404
  }
8344
8405
  const signals = this.symbolData.get(symbol);
8345
- signals.push(data);
8406
+ signals.unshift(data);
8346
8407
  // Trim queue if exceeded MAX_EVENTS per symbol
8347
- if (signals.length > MAX_EVENTS$1) {
8348
- signals.shift();
8408
+ if (signals.length > MAX_EVENTS$2) {
8409
+ signals.pop();
8349
8410
  }
8350
8411
  }
8351
8412
  /**
@@ -8565,9 +8626,9 @@ class HeatmapStorage {
8565
8626
  "*No data available*"
8566
8627
  ].join("\n");
8567
8628
  }
8568
- const header = columns$1.map((col) => col.label);
8569
- const separator = columns$1.map(() => "---");
8570
- const rows = data.symbols.map((row) => columns$1.map((col) => col.format(row)));
8629
+ const header = columns$2.map((col) => col.label);
8630
+ const separator = columns$2.map(() => "---");
8631
+ const rows = data.symbols.map((row) => columns$2.map((col) => col.format(row)));
8571
8632
  const tableData = [header, separator, ...rows];
8572
8633
  const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
8573
8634
  return [
@@ -10605,7 +10666,7 @@ const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, back
10605
10666
  revenuePercent,
10606
10667
  backtest,
10607
10668
  });
10608
- await self.params.onProfit(symbol, data, currentPrice, level, backtest, when.getTime());
10669
+ await self.params.onProfit(symbol, data.strategyName, data.exchangeName, data, currentPrice, level, backtest, when.getTime());
10609
10670
  }
10610
10671
  }
10611
10672
  if (shouldPersist) {
@@ -10652,7 +10713,7 @@ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest,
10652
10713
  lossPercent,
10653
10714
  backtest,
10654
10715
  });
10655
- await self.params.onLoss(symbol, data, currentPrice, level, backtest, when.getTime());
10716
+ await self.params.onLoss(symbol, data.strategyName, data.exchangeName, data, currentPrice, level, backtest, when.getTime());
10656
10717
  }
10657
10718
  }
10658
10719
  if (shouldPersist) {
@@ -10923,6 +10984,7 @@ class ClientPartial {
10923
10984
  symbol,
10924
10985
  data,
10925
10986
  priceClose,
10987
+ backtest,
10926
10988
  });
10927
10989
  if (this._states === NEED_FETCH) {
10928
10990
  throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
@@ -10939,14 +11001,18 @@ class ClientPartial {
10939
11001
  * Emits PartialProfitContract event to all subscribers.
10940
11002
  *
10941
11003
  * @param symbol - Trading pair symbol
11004
+ * @param strategyName - Strategy name that generated this signal
11005
+ * @param exchangeName - Exchange name where this signal is being executed
10942
11006
  * @param data - Signal row data
10943
11007
  * @param currentPrice - Current market price
10944
11008
  * @param level - Profit level reached
10945
11009
  * @param backtest - True if backtest mode
10946
11010
  * @param timestamp - Event timestamp in milliseconds
10947
11011
  */
10948
- const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialProfitSubject.next({
11012
+ const COMMIT_PROFIT_FN = async (symbol, strategyName, exchangeName, data, currentPrice, level, backtest, timestamp) => await partialProfitSubject.next({
10949
11013
  symbol,
11014
+ strategyName,
11015
+ exchangeName,
10950
11016
  data,
10951
11017
  currentPrice,
10952
11018
  level,
@@ -10960,14 +11026,18 @@ const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, tim
10960
11026
  * Emits PartialLossContract event to all subscribers.
10961
11027
  *
10962
11028
  * @param symbol - Trading pair symbol
11029
+ * @param strategyName - Strategy name that generated this signal
11030
+ * @param exchangeName - Exchange name where this signal is being executed
10963
11031
  * @param data - Signal row data
10964
11032
  * @param currentPrice - Current market price
10965
11033
  * @param level - Loss level reached
10966
11034
  * @param backtest - True if backtest mode
10967
11035
  * @param timestamp - Event timestamp in milliseconds
10968
11036
  */
10969
- const COMMIT_LOSS_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialLossSubject.next({
11037
+ const COMMIT_LOSS_FN = async (symbol, strategyName, exchangeName, data, currentPrice, level, backtest, timestamp) => await partialLossSubject.next({
10970
11038
  symbol,
11039
+ strategyName,
11040
+ exchangeName,
10971
11041
  data,
10972
11042
  currentPrice,
10973
11043
  level,
@@ -11113,7 +11183,7 @@ class PartialConnectionService {
11113
11183
  }
11114
11184
  }
11115
11185
 
11116
- const columns = [
11186
+ const columns$1 = [
11117
11187
  {
11118
11188
  key: "action",
11119
11189
  label: "Action",
@@ -11161,12 +11231,12 @@ const columns = [
11161
11231
  },
11162
11232
  ];
11163
11233
  /** Maximum number of events to store in partial reports */
11164
- const MAX_EVENTS = 250;
11234
+ const MAX_EVENTS$1 = 250;
11165
11235
  /**
11166
11236
  * Storage class for accumulating partial profit/loss events per symbol-strategy pair.
11167
11237
  * Maintains a chronological list of profit and loss level events.
11168
11238
  */
11169
- class ReportStorage {
11239
+ let ReportStorage$1 = class ReportStorage {
11170
11240
  constructor() {
11171
11241
  /** Internal list of all partial events for this symbol */
11172
11242
  this._eventList = [];
@@ -11180,7 +11250,7 @@ class ReportStorage {
11180
11250
  * @param backtest - True if backtest mode
11181
11251
  */
11182
11252
  addProfitEvent(data, currentPrice, level, backtest, timestamp) {
11183
- this._eventList.push({
11253
+ this._eventList.unshift({
11184
11254
  timestamp,
11185
11255
  action: "profit",
11186
11256
  symbol: data.symbol,
@@ -11192,8 +11262,8 @@ class ReportStorage {
11192
11262
  backtest,
11193
11263
  });
11194
11264
  // Trim queue if exceeded MAX_EVENTS
11195
- if (this._eventList.length > MAX_EVENTS) {
11196
- this._eventList.shift();
11265
+ if (this._eventList.length > MAX_EVENTS$1) {
11266
+ this._eventList.pop();
11197
11267
  }
11198
11268
  }
11199
11269
  /**
@@ -11205,7 +11275,7 @@ class ReportStorage {
11205
11275
  * @param backtest - True if backtest mode
11206
11276
  */
11207
11277
  addLossEvent(data, currentPrice, level, backtest, timestamp) {
11208
- this._eventList.push({
11278
+ this._eventList.unshift({
11209
11279
  timestamp,
11210
11280
  action: "loss",
11211
11281
  symbol: data.symbol,
@@ -11217,8 +11287,8 @@ class ReportStorage {
11217
11287
  backtest,
11218
11288
  });
11219
11289
  // Trim queue if exceeded MAX_EVENTS
11220
- if (this._eventList.length > MAX_EVENTS) {
11221
- this._eventList.shift();
11290
+ if (this._eventList.length > MAX_EVENTS$1) {
11291
+ this._eventList.pop();
11222
11292
  }
11223
11293
  }
11224
11294
  /**
@@ -11260,9 +11330,9 @@ class ReportStorage {
11260
11330
  "No partial profit/loss events recorded yet."
11261
11331
  ].join("\n");
11262
11332
  }
11263
- const header = columns.map((col) => col.label);
11264
- const separator = columns.map(() => "---");
11265
- const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
11333
+ const header = columns$1.map((col) => col.label);
11334
+ const separator = columns$1.map(() => "---");
11335
+ const rows = this._eventList.map((event) => columns$1.map((col) => col.format(event)));
11266
11336
  const tableData = [header, separator, ...rows];
11267
11337
  const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
11268
11338
  return [
@@ -11296,7 +11366,7 @@ class ReportStorage {
11296
11366
  console.error(`Failed to save markdown report:`, error);
11297
11367
  }
11298
11368
  }
11299
- }
11369
+ };
11300
11370
  /**
11301
11371
  * Service for generating and saving partial profit/loss markdown reports.
11302
11372
  *
@@ -11326,7 +11396,7 @@ class PartialMarkdownService {
11326
11396
  * Memoized function to get or create ReportStorage for a symbol-strategy pair.
11327
11397
  * Each symbol-strategy combination gets its own isolated storage instance.
11328
11398
  */
11329
- this.getStorage = memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage());
11399
+ this.getStorage = memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$1());
11330
11400
  /**
11331
11401
  * Processes profit events and accumulates them.
11332
11402
  * Should be called from partialProfitSubject subscription.
@@ -11636,7 +11706,7 @@ class PartialGlobalService {
11636
11706
  * Warning threshold for message size in kilobytes.
11637
11707
  * Messages exceeding this size trigger console warnings.
11638
11708
  */
11639
- const WARN_KB = 100;
11709
+ const WARN_KB = 20;
11640
11710
  /**
11641
11711
  * Internal function for dumping signal data to markdown files.
11642
11712
  * Creates a directory structure with system prompts, user messages, and LLM output.
@@ -11898,6 +11968,353 @@ class ConfigValidationService {
11898
11968
  }
11899
11969
  }
11900
11970
 
11971
+ const columns = [
11972
+ {
11973
+ key: "symbol",
11974
+ label: "Symbol",
11975
+ format: (data) => data.symbol,
11976
+ },
11977
+ {
11978
+ key: "strategyName",
11979
+ label: "Strategy",
11980
+ format: (data) => data.strategyName,
11981
+ },
11982
+ {
11983
+ key: "signalId",
11984
+ label: "Signal ID",
11985
+ format: (data) => data.pendingSignal.id || "N/A",
11986
+ },
11987
+ {
11988
+ key: "position",
11989
+ label: "Position",
11990
+ format: (data) => data.pendingSignal.position.toUpperCase(),
11991
+ },
11992
+ {
11993
+ key: "exchangeName",
11994
+ label: "Exchange",
11995
+ format: (data) => data.exchangeName,
11996
+ },
11997
+ {
11998
+ key: "openPrice",
11999
+ label: "Open Price",
12000
+ format: (data) => data.pendingSignal.priceOpen !== undefined
12001
+ ? `${data.pendingSignal.priceOpen.toFixed(8)} USD`
12002
+ : "N/A",
12003
+ },
12004
+ {
12005
+ key: "takeProfit",
12006
+ label: "Take Profit",
12007
+ format: (data) => data.pendingSignal.priceTakeProfit !== undefined
12008
+ ? `${data.pendingSignal.priceTakeProfit.toFixed(8)} USD`
12009
+ : "N/A",
12010
+ },
12011
+ {
12012
+ key: "stopLoss",
12013
+ label: "Stop Loss",
12014
+ format: (data) => data.pendingSignal.priceStopLoss !== undefined
12015
+ ? `${data.pendingSignal.priceStopLoss.toFixed(8)} USD`
12016
+ : "N/A",
12017
+ },
12018
+ {
12019
+ key: "currentPrice",
12020
+ label: "Current Price",
12021
+ format: (data) => `${data.currentPrice.toFixed(8)} USD`,
12022
+ },
12023
+ {
12024
+ key: "activePositionCount",
12025
+ label: "Active Positions",
12026
+ format: (data) => data.activePositionCount.toString(),
12027
+ },
12028
+ {
12029
+ key: "comment",
12030
+ label: "Reason",
12031
+ format: (data) => data.comment,
12032
+ },
12033
+ {
12034
+ key: "timestamp",
12035
+ label: "Timestamp",
12036
+ format: (data) => new Date(data.timestamp).toISOString(),
12037
+ },
12038
+ ];
12039
+ /** Maximum number of events to store in risk reports */
12040
+ const MAX_EVENTS = 250;
12041
+ /**
12042
+ * Storage class for accumulating risk rejection events per symbol-strategy pair.
12043
+ * Maintains a chronological list of rejected signals due to risk limits.
12044
+ */
12045
+ class ReportStorage {
12046
+ constructor() {
12047
+ /** Internal list of all risk rejection events for this symbol */
12048
+ this._eventList = [];
12049
+ }
12050
+ /**
12051
+ * Adds a risk rejection event to the storage.
12052
+ *
12053
+ * @param event - Risk rejection event data
12054
+ */
12055
+ addRejectionEvent(event) {
12056
+ this._eventList.unshift(event);
12057
+ // Trim queue if exceeded MAX_EVENTS
12058
+ if (this._eventList.length > MAX_EVENTS) {
12059
+ this._eventList.pop();
12060
+ }
12061
+ }
12062
+ /**
12063
+ * Calculates statistical data from risk rejection events (Controller).
12064
+ *
12065
+ * @returns Statistical data (empty object if no events)
12066
+ */
12067
+ async getData() {
12068
+ if (this._eventList.length === 0) {
12069
+ return {
12070
+ eventList: [],
12071
+ totalRejections: 0,
12072
+ bySymbol: {},
12073
+ byStrategy: {},
12074
+ };
12075
+ }
12076
+ const bySymbol = {};
12077
+ const byStrategy = {};
12078
+ for (const event of this._eventList) {
12079
+ bySymbol[event.symbol] = (bySymbol[event.symbol] || 0) + 1;
12080
+ byStrategy[event.strategyName] = (byStrategy[event.strategyName] || 0) + 1;
12081
+ }
12082
+ return {
12083
+ eventList: this._eventList,
12084
+ totalRejections: this._eventList.length,
12085
+ bySymbol,
12086
+ byStrategy,
12087
+ };
12088
+ }
12089
+ /**
12090
+ * Generates markdown report with all risk rejection events for a symbol-strategy pair (View).
12091
+ *
12092
+ * @param symbol - Trading pair symbol
12093
+ * @param strategyName - Strategy name
12094
+ * @returns Markdown formatted report with all events
12095
+ */
12096
+ async getReport(symbol, strategyName) {
12097
+ const stats = await this.getData();
12098
+ if (stats.totalRejections === 0) {
12099
+ return [
12100
+ `# Risk Rejection Report: ${symbol}:${strategyName}`,
12101
+ "",
12102
+ "No risk rejections recorded yet.",
12103
+ ].join("\n");
12104
+ }
12105
+ const header = columns.map((col) => col.label);
12106
+ const separator = columns.map(() => "---");
12107
+ const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
12108
+ const tableData = [header, separator, ...rows];
12109
+ const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
12110
+ return [
12111
+ `# Risk Rejection Report: ${symbol}:${strategyName}`,
12112
+ "",
12113
+ table,
12114
+ "",
12115
+ `**Total rejections:** ${stats.totalRejections}`,
12116
+ "",
12117
+ "## Rejections by Symbol",
12118
+ ...Object.entries(stats.bySymbol).map(([sym, count]) => `- ${sym}: ${count}`),
12119
+ "",
12120
+ "## Rejections by Strategy",
12121
+ ...Object.entries(stats.byStrategy).map(([strat, count]) => `- ${strat}: ${count}`),
12122
+ ].join("\n");
12123
+ }
12124
+ /**
12125
+ * Saves symbol-strategy report to disk.
12126
+ *
12127
+ * @param symbol - Trading pair symbol
12128
+ * @param strategyName - Strategy name
12129
+ * @param path - Directory path to save report (default: "./dump/risk")
12130
+ */
12131
+ async dump(symbol, strategyName, path = "./dump/risk") {
12132
+ const markdown = await this.getReport(symbol, strategyName);
12133
+ try {
12134
+ const dir = join(process.cwd(), path);
12135
+ await mkdir(dir, { recursive: true });
12136
+ const filename = `${symbol}_${strategyName}.md`;
12137
+ const filepath = join(dir, filename);
12138
+ await writeFile(filepath, markdown, "utf-8");
12139
+ console.log(`Risk rejection report saved: ${filepath}`);
12140
+ }
12141
+ catch (error) {
12142
+ console.error(`Failed to save markdown report:`, error);
12143
+ }
12144
+ }
12145
+ }
12146
+ /**
12147
+ * Service for generating and saving risk rejection markdown reports.
12148
+ *
12149
+ * Features:
12150
+ * - Listens to risk rejection events via riskSubject
12151
+ * - Accumulates all rejection events per symbol-strategy pair
12152
+ * - Generates markdown tables with detailed rejection information
12153
+ * - Provides statistics (total rejections, by symbol, by strategy)
12154
+ * - Saves reports to disk in dump/risk/{symbol}_{strategyName}.md
12155
+ *
12156
+ * @example
12157
+ * ```typescript
12158
+ * const service = new RiskMarkdownService();
12159
+ *
12160
+ * // Service automatically subscribes to subjects on init
12161
+ * // No manual callback setup needed
12162
+ *
12163
+ * // Later: generate and save report
12164
+ * await service.dump("BTCUSDT", "my-strategy");
12165
+ * ```
12166
+ */
12167
+ class RiskMarkdownService {
12168
+ constructor() {
12169
+ /** Logger service for debug output */
12170
+ this.loggerService = inject(TYPES.loggerService);
12171
+ /**
12172
+ * Memoized function to get or create ReportStorage for a symbol-strategy pair.
12173
+ * Each symbol-strategy combination gets its own isolated storage instance.
12174
+ */
12175
+ this.getStorage = memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage());
12176
+ /**
12177
+ * Processes risk rejection events and accumulates them.
12178
+ * Should be called from riskSubject subscription.
12179
+ *
12180
+ * @param data - Risk rejection event data
12181
+ *
12182
+ * @example
12183
+ * ```typescript
12184
+ * const service = new RiskMarkdownService();
12185
+ * // Service automatically subscribes in init()
12186
+ * ```
12187
+ */
12188
+ this.tickRejection = async (data) => {
12189
+ this.loggerService.log("riskMarkdownService tickRejection", {
12190
+ data,
12191
+ });
12192
+ const storage = this.getStorage(data.symbol, data.strategyName);
12193
+ storage.addRejectionEvent(data);
12194
+ };
12195
+ /**
12196
+ * Gets statistical data from all risk rejection events for a symbol-strategy pair.
12197
+ * Delegates to ReportStorage.getData().
12198
+ *
12199
+ * @param symbol - Trading pair symbol to get data for
12200
+ * @param strategyName - Strategy name to get data for
12201
+ * @returns Statistical data object with all metrics
12202
+ *
12203
+ * @example
12204
+ * ```typescript
12205
+ * const service = new RiskMarkdownService();
12206
+ * const stats = await service.getData("BTCUSDT", "my-strategy");
12207
+ * console.log(stats.totalRejections, stats.bySymbol);
12208
+ * ```
12209
+ */
12210
+ this.getData = async (symbol, strategyName) => {
12211
+ this.loggerService.log("riskMarkdownService getData", {
12212
+ symbol,
12213
+ strategyName,
12214
+ });
12215
+ const storage = this.getStorage(symbol, strategyName);
12216
+ return storage.getData();
12217
+ };
12218
+ /**
12219
+ * Generates markdown report with all risk rejection events for a symbol-strategy pair.
12220
+ * Delegates to ReportStorage.getReport().
12221
+ *
12222
+ * @param symbol - Trading pair symbol to generate report for
12223
+ * @param strategyName - Strategy name to generate report for
12224
+ * @returns Markdown formatted report string with table of all events
12225
+ *
12226
+ * @example
12227
+ * ```typescript
12228
+ * const service = new RiskMarkdownService();
12229
+ * const markdown = await service.getReport("BTCUSDT", "my-strategy");
12230
+ * console.log(markdown);
12231
+ * ```
12232
+ */
12233
+ this.getReport = async (symbol, strategyName) => {
12234
+ this.loggerService.log("riskMarkdownService getReport", {
12235
+ symbol,
12236
+ strategyName,
12237
+ });
12238
+ const storage = this.getStorage(symbol, strategyName);
12239
+ return storage.getReport(symbol, strategyName);
12240
+ };
12241
+ /**
12242
+ * Saves symbol-strategy report to disk.
12243
+ * Creates directory if it doesn't exist.
12244
+ * Delegates to ReportStorage.dump().
12245
+ *
12246
+ * @param symbol - Trading pair symbol to save report for
12247
+ * @param strategyName - Strategy name to save report for
12248
+ * @param path - Directory path to save report (default: "./dump/risk")
12249
+ *
12250
+ * @example
12251
+ * ```typescript
12252
+ * const service = new RiskMarkdownService();
12253
+ *
12254
+ * // Save to default path: ./dump/risk/BTCUSDT_my-strategy.md
12255
+ * await service.dump("BTCUSDT", "my-strategy");
12256
+ *
12257
+ * // Save to custom path: ./custom/path/BTCUSDT_my-strategy.md
12258
+ * await service.dump("BTCUSDT", "my-strategy", "./custom/path");
12259
+ * ```
12260
+ */
12261
+ this.dump = async (symbol, strategyName, path = "./dump/risk") => {
12262
+ this.loggerService.log("riskMarkdownService dump", {
12263
+ symbol,
12264
+ strategyName,
12265
+ path,
12266
+ });
12267
+ const storage = this.getStorage(symbol, strategyName);
12268
+ await storage.dump(symbol, strategyName, path);
12269
+ };
12270
+ /**
12271
+ * Clears accumulated event data from storage.
12272
+ * If ctx is provided, clears only that specific symbol-strategy pair's data.
12273
+ * If nothing is provided, clears all data.
12274
+ *
12275
+ * @param ctx - Optional context with symbol and strategyName
12276
+ *
12277
+ * @example
12278
+ * ```typescript
12279
+ * const service = new RiskMarkdownService();
12280
+ *
12281
+ * // Clear specific symbol-strategy pair
12282
+ * await service.clear({ symbol: "BTCUSDT", strategyName: "my-strategy" });
12283
+ *
12284
+ * // Clear all data
12285
+ * await service.clear();
12286
+ * ```
12287
+ */
12288
+ this.clear = async (ctx) => {
12289
+ this.loggerService.log("riskMarkdownService clear", {
12290
+ ctx,
12291
+ });
12292
+ if (ctx) {
12293
+ const key = `${ctx.symbol}:${ctx.strategyName}`;
12294
+ this.getStorage.clear(key);
12295
+ }
12296
+ else {
12297
+ this.getStorage.clear();
12298
+ }
12299
+ };
12300
+ /**
12301
+ * Initializes the service by subscribing to risk rejection events.
12302
+ * Uses singleshot to ensure initialization happens only once.
12303
+ * Automatically called on first use.
12304
+ *
12305
+ * @example
12306
+ * ```typescript
12307
+ * const service = new RiskMarkdownService();
12308
+ * await service.init(); // Subscribe to rejection events
12309
+ * ```
12310
+ */
12311
+ this.init = singleshot(async () => {
12312
+ this.loggerService.log("riskMarkdownService init");
12313
+ riskSubject.subscribe(this.tickRejection);
12314
+ });
12315
+ }
12316
+ }
12317
+
11901
12318
  {
11902
12319
  provide(TYPES.loggerService, () => new LoggerService());
11903
12320
  }
@@ -11958,6 +12375,7 @@ class ConfigValidationService {
11958
12375
  provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
11959
12376
  provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
11960
12377
  provide(TYPES.outlineMarkdownService, () => new OutlineMarkdownService());
12378
+ provide(TYPES.riskMarkdownService, () => new RiskMarkdownService());
11961
12379
  }
11962
12380
  {
11963
12381
  provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
@@ -12033,6 +12451,7 @@ const markdownServices = {
12033
12451
  heatMarkdownService: inject(TYPES.heatMarkdownService),
12034
12452
  partialMarkdownService: inject(TYPES.partialMarkdownService),
12035
12453
  outlineMarkdownService: inject(TYPES.outlineMarkdownService),
12454
+ riskMarkdownService: inject(TYPES.riskMarkdownService),
12036
12455
  };
12037
12456
  const validationServices = {
12038
12457
  exchangeValidationService: inject(TYPES.exchangeValidationService),
@@ -12807,6 +13226,8 @@ const LISTEN_PARTIAL_PROFIT_METHOD_NAME = "event.listenPartialProfit";
12807
13226
  const LISTEN_PARTIAL_PROFIT_ONCE_METHOD_NAME = "event.listenPartialProfitOnce";
12808
13227
  const LISTEN_PARTIAL_LOSS_METHOD_NAME = "event.listenPartialLoss";
12809
13228
  const LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME = "event.listenPartialLossOnce";
13229
+ const LISTEN_RISK_METHOD_NAME = "event.listenRisk";
13230
+ const LISTEN_RISK_ONCE_METHOD_NAME = "event.listenRiskOnce";
12810
13231
  /**
12811
13232
  * Subscribes to all signal events with queued async processing.
12812
13233
  *
@@ -13605,6 +14026,75 @@ function listenPartialLossOnce(filterFn, fn) {
13605
14026
  backtest$1.loggerService.log(LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME);
13606
14027
  return partialLossSubject.filter(filterFn).once(fn);
13607
14028
  }
14029
+ /**
14030
+ * Subscribes to risk rejection events with queued async processing.
14031
+ *
14032
+ * Emits ONLY when a signal is rejected due to risk validation failure.
14033
+ * Does not emit for allowed signals (prevents spam).
14034
+ * Events are processed sequentially in order received, even if callback is async.
14035
+ * Uses queued wrapper to prevent concurrent execution of the callback.
14036
+ *
14037
+ * @param fn - Callback function to handle risk rejection events
14038
+ * @returns Unsubscribe function to stop listening to events
14039
+ *
14040
+ * @example
14041
+ * ```typescript
14042
+ * import { listenRisk } from "./function/event";
14043
+ *
14044
+ * const unsubscribe = listenRisk((event) => {
14045
+ * console.log(`[RISK REJECTED] Signal for ${event.symbol}`);
14046
+ * console.log(`Strategy: ${event.strategyName}`);
14047
+ * console.log(`Position: ${event.pendingSignal.position}`);
14048
+ * console.log(`Active positions: ${event.activePositionCount}`);
14049
+ * console.log(`Reason: ${event.comment}`);
14050
+ * console.log(`Price: ${event.currentPrice}`);
14051
+ * });
14052
+ *
14053
+ * // Later: stop listening
14054
+ * unsubscribe();
14055
+ * ```
14056
+ */
14057
+ function listenRisk(fn) {
14058
+ backtest$1.loggerService.log(LISTEN_RISK_METHOD_NAME);
14059
+ return riskSubject.subscribe(queued(async (event) => fn(event)));
14060
+ }
14061
+ /**
14062
+ * Subscribes to filtered risk rejection events with one-time execution.
14063
+ *
14064
+ * Listens for events matching the filter predicate, then executes callback once
14065
+ * and automatically unsubscribes. Useful for waiting for specific risk rejection conditions.
14066
+ *
14067
+ * @param filterFn - Predicate to filter which events trigger the callback
14068
+ * @param fn - Callback function to handle the filtered event (called only once)
14069
+ * @returns Unsubscribe function to cancel the listener before it fires
14070
+ *
14071
+ * @example
14072
+ * ```typescript
14073
+ * import { listenRiskOnce } from "./function/event";
14074
+ *
14075
+ * // Wait for first risk rejection on BTCUSDT
14076
+ * listenRiskOnce(
14077
+ * (event) => event.symbol === "BTCUSDT",
14078
+ * (event) => {
14079
+ * console.log("BTCUSDT signal rejected!");
14080
+ * console.log("Reason:", event.comment);
14081
+ * }
14082
+ * );
14083
+ *
14084
+ * // Wait for rejection due to position limit
14085
+ * const cancel = listenRiskOnce(
14086
+ * (event) => event.comment.includes("Max") && event.activePositionCount >= 3,
14087
+ * (event) => console.log("Position limit reached:", event.activePositionCount)
14088
+ * );
14089
+ *
14090
+ * // Cancel if needed before event fires
14091
+ * cancel();
14092
+ * ```
14093
+ */
14094
+ function listenRiskOnce(filterFn, fn) {
14095
+ backtest$1.loggerService.log(LISTEN_RISK_ONCE_METHOD_NAME);
14096
+ return riskSubject.filter(filterFn).once(fn);
14097
+ }
13608
14098
 
13609
14099
  const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
13610
14100
  const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
@@ -16229,4 +16719,184 @@ class ConstantUtils {
16229
16719
  */
16230
16720
  const Constant = new ConstantUtils();
16231
16721
 
16232
- export { Backtest, Constant, ExecutionContextService, Heat, Live, MethodContextService, Optimizer, Partial, Performance, PersistBase, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PositionSize, Schedule, Walker, addExchange, addFrame, addOptimizer, addRisk, addSizing, addStrategy, addWalker, dumpSignal, emitters, formatPrice, formatQuantity, getAveragePrice, getCandles, getConfig, getDate, getDefaultConfig, getMode, backtest as lib, listExchanges, listFrames, listOptimizers, listRisks, listSizings, listStrategies, listWalkers, listenBacktestProgress, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenOptimizerProgress, listenPartialLoss, listenPartialLossOnce, listenPartialProfit, listenPartialProfitOnce, listenPerformance, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, setConfig, setLogger };
16722
+ const RISK_METHOD_NAME_GET_DATA = "RiskUtils.getData";
16723
+ const RISK_METHOD_NAME_GET_REPORT = "RiskUtils.getReport";
16724
+ const RISK_METHOD_NAME_DUMP = "RiskUtils.dump";
16725
+ /**
16726
+ * Utility class for accessing risk rejection reports and statistics.
16727
+ *
16728
+ * Provides static-like methods (via singleton instance) to retrieve data
16729
+ * accumulated by RiskMarkdownService from risk rejection events.
16730
+ *
16731
+ * Features:
16732
+ * - Statistical data extraction (total rejections count, by symbol, by strategy)
16733
+ * - Markdown report generation with event tables
16734
+ * - File export to disk
16735
+ *
16736
+ * Data source:
16737
+ * - RiskMarkdownService listens to riskSubject
16738
+ * - Accumulates rejection events in ReportStorage (max 250 events per symbol-strategy pair)
16739
+ * - Events include: timestamp, symbol, strategyName, position, exchangeName, price, activePositionCount, comment
16740
+ *
16741
+ * @example
16742
+ * ```typescript
16743
+ * import { Risk } from "./classes/Risk";
16744
+ *
16745
+ * // Get statistical data for BTCUSDT:my-strategy
16746
+ * const stats = await Risk.getData("BTCUSDT", "my-strategy");
16747
+ * console.log(`Total rejections: ${stats.totalRejections}`);
16748
+ * console.log(`By symbol:`, stats.bySymbol);
16749
+ * console.log(`By strategy:`, stats.byStrategy);
16750
+ *
16751
+ * // Generate markdown report
16752
+ * const markdown = await Risk.getReport("BTCUSDT", "my-strategy");
16753
+ * console.log(markdown); // Formatted table with all rejection events
16754
+ *
16755
+ * // Export report to file
16756
+ * await Risk.dump("BTCUSDT", "my-strategy"); // Saves to ./dump/risk/BTCUSDT_my-strategy.md
16757
+ * await Risk.dump("BTCUSDT", "my-strategy", "./custom/path"); // Custom directory
16758
+ * ```
16759
+ */
16760
+ class RiskUtils {
16761
+ constructor() {
16762
+ /**
16763
+ * Retrieves statistical data from accumulated risk rejection events.
16764
+ *
16765
+ * Delegates to RiskMarkdownService.getData() which reads from ReportStorage.
16766
+ * Returns aggregated metrics calculated from all rejection events.
16767
+ *
16768
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16769
+ * @param strategyName - Strategy name (e.g., "my-strategy")
16770
+ * @returns Promise resolving to RiskStatistics object with counts and event list
16771
+ *
16772
+ * @example
16773
+ * ```typescript
16774
+ * const stats = await Risk.getData("BTCUSDT", "my-strategy");
16775
+ *
16776
+ * console.log(`Total rejections: ${stats.totalRejections}`);
16777
+ * console.log(`Rejections by symbol:`, stats.bySymbol);
16778
+ * console.log(`Rejections by strategy:`, stats.byStrategy);
16779
+ *
16780
+ * // Iterate through all rejection events
16781
+ * for (const event of stats.eventList) {
16782
+ * console.log(`REJECTED: ${event.symbol} - ${event.comment} (${event.activePositionCount} active)`);
16783
+ * }
16784
+ * ```
16785
+ */
16786
+ this.getData = async (symbol, strategyName) => {
16787
+ backtest$1.loggerService.info(RISK_METHOD_NAME_GET_DATA, { symbol, strategyName });
16788
+ backtest$1.strategyValidationService.validate(strategyName, RISK_METHOD_NAME_GET_DATA);
16789
+ {
16790
+ const { riskName } = backtest$1.strategySchemaService.get(strategyName);
16791
+ riskName && backtest$1.riskValidationService.validate(riskName, RISK_METHOD_NAME_GET_DATA);
16792
+ }
16793
+ return await backtest$1.riskMarkdownService.getData(symbol, strategyName);
16794
+ };
16795
+ /**
16796
+ * Generates markdown report with all risk rejection events for a symbol-strategy pair.
16797
+ *
16798
+ * Creates formatted table containing:
16799
+ * - Symbol
16800
+ * - Strategy
16801
+ * - Position (LONG/SHORT)
16802
+ * - Exchange
16803
+ * - Price
16804
+ * - Active Positions (at rejection time)
16805
+ * - Reason (from validation note)
16806
+ * - Timestamp (ISO 8601)
16807
+ *
16808
+ * Also includes summary statistics at the end (total rejections, by symbol, by strategy).
16809
+ *
16810
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16811
+ * @param strategyName - Strategy name (e.g., "my-strategy")
16812
+ * @returns Promise resolving to markdown formatted report string
16813
+ *
16814
+ * @example
16815
+ * ```typescript
16816
+ * const markdown = await Risk.getReport("BTCUSDT", "my-strategy");
16817
+ * console.log(markdown);
16818
+ *
16819
+ * // Output:
16820
+ * // # Risk Rejection Report: BTCUSDT:my-strategy
16821
+ * //
16822
+ * // | Symbol | Strategy | Position | Exchange | Price | Active Positions | Reason | Timestamp |
16823
+ * // | --- | --- | --- | --- | --- | --- | --- | --- |
16824
+ * // | BTCUSDT | my-strategy | LONG | binance | 50000.00000000 USD | 3 | Max 3 positions allowed | 2024-01-15T10:30:00.000Z |
16825
+ * //
16826
+ * // **Total rejections:** 1
16827
+ * //
16828
+ * // ## Rejections by Symbol
16829
+ * // - BTCUSDT: 1
16830
+ * //
16831
+ * // ## Rejections by Strategy
16832
+ * // - my-strategy: 1
16833
+ * ```
16834
+ */
16835
+ this.getReport = async (symbol, strategyName) => {
16836
+ backtest$1.loggerService.info(RISK_METHOD_NAME_GET_REPORT, { symbol, strategyName });
16837
+ backtest$1.strategyValidationService.validate(strategyName, RISK_METHOD_NAME_GET_REPORT);
16838
+ {
16839
+ const { riskName } = backtest$1.strategySchemaService.get(strategyName);
16840
+ riskName && backtest$1.riskValidationService.validate(riskName, RISK_METHOD_NAME_GET_REPORT);
16841
+ }
16842
+ return await backtest$1.riskMarkdownService.getReport(symbol, strategyName);
16843
+ };
16844
+ /**
16845
+ * Generates and saves markdown report to file.
16846
+ *
16847
+ * Creates directory if it doesn't exist.
16848
+ * Filename format: {symbol}_{strategyName}.md (e.g., "BTCUSDT_my-strategy.md")
16849
+ *
16850
+ * Delegates to RiskMarkdownService.dump() which:
16851
+ * 1. Generates markdown report via getReport()
16852
+ * 2. Creates output directory (recursive mkdir)
16853
+ * 3. Writes file with UTF-8 encoding
16854
+ * 4. Logs success/failure to console
16855
+ *
16856
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
16857
+ * @param strategyName - Strategy name (e.g., "my-strategy")
16858
+ * @param path - Output directory path (default: "./dump/risk")
16859
+ * @returns Promise that resolves when file is written
16860
+ *
16861
+ * @example
16862
+ * ```typescript
16863
+ * // Save to default path: ./dump/risk/BTCUSDT_my-strategy.md
16864
+ * await Risk.dump("BTCUSDT", "my-strategy");
16865
+ *
16866
+ * // Save to custom path: ./reports/risk/BTCUSDT_my-strategy.md
16867
+ * await Risk.dump("BTCUSDT", "my-strategy", "./reports/risk");
16868
+ *
16869
+ * // After multiple symbols backtested, export all risk reports
16870
+ * for (const symbol of ["BTCUSDT", "ETHUSDT", "BNBUSDT"]) {
16871
+ * await Risk.dump(symbol, "my-strategy", "./backtest-results");
16872
+ * }
16873
+ * ```
16874
+ */
16875
+ this.dump = async (symbol, strategyName, path) => {
16876
+ backtest$1.loggerService.info(RISK_METHOD_NAME_DUMP, { symbol, strategyName, path });
16877
+ backtest$1.strategyValidationService.validate(strategyName, RISK_METHOD_NAME_DUMP);
16878
+ {
16879
+ const { riskName } = backtest$1.strategySchemaService.get(strategyName);
16880
+ riskName && backtest$1.riskValidationService.validate(riskName, RISK_METHOD_NAME_DUMP);
16881
+ }
16882
+ await backtest$1.riskMarkdownService.dump(symbol, strategyName, path);
16883
+ };
16884
+ }
16885
+ }
16886
+ /**
16887
+ * Global singleton instance of RiskUtils.
16888
+ * Provides static-like access to risk rejection reporting methods.
16889
+ *
16890
+ * @example
16891
+ * ```typescript
16892
+ * import { Risk } from "backtest-kit";
16893
+ *
16894
+ * // Usage same as RiskUtils methods
16895
+ * const stats = await Risk.getData("BTCUSDT", "my-strategy");
16896
+ * const report = await Risk.getReport("BTCUSDT", "my-strategy");
16897
+ * await Risk.dump("BTCUSDT", "my-strategy");
16898
+ * ```
16899
+ */
16900
+ const Risk = new RiskUtils();
16901
+
16902
+ export { Backtest, Constant, ExecutionContextService, Heat, Live, MethodContextService, Optimizer, Partial, Performance, PersistBase, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PositionSize, Risk, Schedule, Walker, addExchange, addFrame, addOptimizer, addRisk, addSizing, addStrategy, addWalker, dumpSignal, emitters, formatPrice, formatQuantity, getAveragePrice, getCandles, getConfig, getDate, getDefaultConfig, getMode, backtest as lib, listExchanges, listFrames, listOptimizers, listRisks, listSizings, listStrategies, listWalkers, listenBacktestProgress, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenOptimizerProgress, listenPartialLoss, listenPartialLossOnce, listenPartialProfit, listenPartialProfitOnce, listenPerformance, listenRisk, listenRiskOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, setConfig, setLogger };