backtest-kit 1.5.14 → 1.5.16

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
@@ -590,6 +590,14 @@ class ClientExchange {
590
590
  const vwap = sumPriceVolume / totalVolume;
591
591
  return vwap;
592
592
  }
593
+ /**
594
+ * Formats quantity according to exchange-specific rules for the given symbol.
595
+ * Applies proper decimal precision and rounding based on symbol's lot size filters.
596
+ *
597
+ * @param symbol - Trading pair symbol
598
+ * @param quantity - Raw quantity to format
599
+ * @returns Promise resolving to formatted quantity as string
600
+ */
593
601
  async formatQuantity(symbol, quantity) {
594
602
  this.params.logger.debug("binanceService formatQuantity", {
595
603
  symbol,
@@ -597,6 +605,14 @@ class ClientExchange {
597
605
  });
598
606
  return await this.params.formatQuantity(symbol, quantity);
599
607
  }
608
+ /**
609
+ * Formats price according to exchange-specific rules for the given symbol.
610
+ * Applies proper decimal precision and rounding based on symbol's price filters.
611
+ *
612
+ * @param symbol - Trading pair symbol
613
+ * @param price - Raw price to format
614
+ * @returns Promise resolving to formatted price as string
615
+ */
600
616
  async formatPrice(symbol, price) {
601
617
  this.params.logger.debug("binanceService formatPrice", {
602
618
  symbol,
@@ -6471,7 +6487,7 @@ const columns$3 = [
6471
6487
  },
6472
6488
  ];
6473
6489
  /** Maximum number of events to store in live trading reports */
6474
- const MAX_EVENTS$3 = 250;
6490
+ const MAX_EVENTS$4 = 250;
6475
6491
  /**
6476
6492
  * Storage class for accumulating all tick events per strategy.
6477
6493
  * Maintains a chronological list of all events (idle, opened, active, closed).
@@ -6504,7 +6520,7 @@ let ReportStorage$3 = class ReportStorage {
6504
6520
  }
6505
6521
  {
6506
6522
  this._eventList.push(newEvent);
6507
- if (this._eventList.length > MAX_EVENTS$3) {
6523
+ if (this._eventList.length > MAX_EVENTS$4) {
6508
6524
  this._eventList.shift();
6509
6525
  }
6510
6526
  }
@@ -6528,19 +6544,16 @@ let ReportStorage$3 = class ReportStorage {
6528
6544
  stopLoss: data.signal.priceStopLoss,
6529
6545
  });
6530
6546
  // Trim queue if exceeded MAX_EVENTS
6531
- if (this._eventList.length > MAX_EVENTS$3) {
6547
+ if (this._eventList.length > MAX_EVENTS$4) {
6532
6548
  this._eventList.shift();
6533
6549
  }
6534
6550
  }
6535
6551
  /**
6536
- * Updates or adds an active event to the storage.
6537
- * Replaces the previous event with the same signalId.
6552
+ * Adds an active event to the storage.
6538
6553
  *
6539
6554
  * @param data - Active tick result
6540
6555
  */
6541
6556
  addActiveEvent(data) {
6542
- // Find existing event with the same signalId
6543
- const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
6544
6557
  const newEvent = {
6545
6558
  timestamp: Date.now(),
6546
6559
  action: "active",
@@ -6555,29 +6568,20 @@ let ReportStorage$3 = class ReportStorage {
6555
6568
  percentTp: data.percentTp,
6556
6569
  percentSl: data.percentSl,
6557
6570
  };
6558
- // Replace existing event or add new one
6559
- if (existingIndex !== -1) {
6560
- this._eventList[existingIndex] = newEvent;
6561
- }
6562
- else {
6563
- this._eventList.push(newEvent);
6564
- // Trim queue if exceeded MAX_EVENTS
6565
- if (this._eventList.length > MAX_EVENTS$3) {
6566
- this._eventList.shift();
6567
- }
6571
+ this._eventList.push(newEvent);
6572
+ // Trim queue if exceeded MAX_EVENTS
6573
+ if (this._eventList.length > MAX_EVENTS$4) {
6574
+ this._eventList.shift();
6568
6575
  }
6569
6576
  }
6570
6577
  /**
6571
- * Updates or adds a closed event to the storage.
6572
- * Replaces the previous event with the same signalId.
6578
+ * Adds a closed event to the storage.
6573
6579
  *
6574
6580
  * @param data - Closed tick result
6575
6581
  */
6576
6582
  addClosedEvent(data) {
6577
6583
  const durationMs = data.closeTimestamp - data.signal.pendingAt;
6578
6584
  const durationMin = Math.round(durationMs / 60000);
6579
- // Find existing event with the same signalId
6580
- const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
6581
6585
  const newEvent = {
6582
6586
  timestamp: data.closeTimestamp,
6583
6587
  action: "closed",
@@ -6593,16 +6597,10 @@ let ReportStorage$3 = class ReportStorage {
6593
6597
  closeReason: data.closeReason,
6594
6598
  duration: durationMin,
6595
6599
  };
6596
- // Replace existing event or add new one
6597
- if (existingIndex !== -1) {
6598
- this._eventList[existingIndex] = newEvent;
6599
- }
6600
- else {
6601
- this._eventList.push(newEvent);
6602
- // Trim queue if exceeded MAX_EVENTS
6603
- if (this._eventList.length > MAX_EVENTS$3) {
6604
- this._eventList.shift();
6605
- }
6600
+ this._eventList.push(newEvent);
6601
+ // Trim queue if exceeded MAX_EVENTS
6602
+ if (this._eventList.length > MAX_EVENTS$4) {
6603
+ this._eventList.shift();
6606
6604
  }
6607
6605
  }
6608
6606
  /**
@@ -7003,7 +7001,7 @@ const columns$2 = [
7003
7001
  },
7004
7002
  ];
7005
7003
  /** Maximum number of events to store in schedule reports */
7006
- const MAX_EVENTS$2 = 250;
7004
+ const MAX_EVENTS$3 = 250;
7007
7005
  /**
7008
7006
  * Storage class for accumulating scheduled signal events per strategy.
7009
7007
  * Maintains a chronological list of scheduled and cancelled events.
@@ -7032,21 +7030,45 @@ let ReportStorage$2 = class ReportStorage {
7032
7030
  stopLoss: data.signal.priceStopLoss,
7033
7031
  });
7034
7032
  // Trim queue if exceeded MAX_EVENTS
7035
- if (this._eventList.length > MAX_EVENTS$2) {
7033
+ if (this._eventList.length > MAX_EVENTS$3) {
7034
+ this._eventList.shift();
7035
+ }
7036
+ }
7037
+ /**
7038
+ * Adds an opened event to the storage.
7039
+ *
7040
+ * @param data - Opened tick result
7041
+ */
7042
+ addOpenedEvent(data) {
7043
+ const durationMs = data.signal.pendingAt - data.signal.scheduledAt;
7044
+ const durationMin = Math.round(durationMs / 60000);
7045
+ const newEvent = {
7046
+ timestamp: data.signal.pendingAt,
7047
+ action: "opened",
7048
+ symbol: data.signal.symbol,
7049
+ signalId: data.signal.id,
7050
+ position: data.signal.position,
7051
+ note: data.signal.note,
7052
+ currentPrice: data.currentPrice,
7053
+ priceOpen: data.signal.priceOpen,
7054
+ takeProfit: data.signal.priceTakeProfit,
7055
+ stopLoss: data.signal.priceStopLoss,
7056
+ duration: durationMin,
7057
+ };
7058
+ this._eventList.push(newEvent);
7059
+ // Trim queue if exceeded MAX_EVENTS
7060
+ if (this._eventList.length > MAX_EVENTS$3) {
7036
7061
  this._eventList.shift();
7037
7062
  }
7038
7063
  }
7039
7064
  /**
7040
- * Updates or adds a cancelled event to the storage.
7041
- * Replaces the previous event with the same signalId.
7065
+ * Adds a cancelled event to the storage.
7042
7066
  *
7043
7067
  * @param data - Cancelled tick result
7044
7068
  */
7045
7069
  addCancelledEvent(data) {
7046
7070
  const durationMs = data.closeTimestamp - data.signal.scheduledAt;
7047
7071
  const durationMin = Math.round(durationMs / 60000);
7048
- // Find existing event with the same signalId
7049
- const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
7050
7072
  const newEvent = {
7051
7073
  timestamp: data.closeTimestamp,
7052
7074
  action: "cancelled",
@@ -7061,16 +7083,10 @@ let ReportStorage$2 = class ReportStorage {
7061
7083
  closeTimestamp: data.closeTimestamp,
7062
7084
  duration: durationMin,
7063
7085
  };
7064
- // Replace existing event or add new one
7065
- if (existingIndex !== -1) {
7066
- this._eventList[existingIndex] = newEvent;
7067
- }
7068
- else {
7069
- this._eventList.push(newEvent);
7070
- // Trim queue if exceeded MAX_EVENTS
7071
- if (this._eventList.length > MAX_EVENTS$2) {
7072
- this._eventList.shift();
7073
- }
7086
+ this._eventList.push(newEvent);
7087
+ // Trim queue if exceeded MAX_EVENTS
7088
+ if (this._eventList.length > MAX_EVENTS$3) {
7089
+ this._eventList.shift();
7074
7090
  }
7075
7091
  }
7076
7092
  /**
@@ -7084,29 +7100,44 @@ let ReportStorage$2 = class ReportStorage {
7084
7100
  eventList: [],
7085
7101
  totalEvents: 0,
7086
7102
  totalScheduled: 0,
7103
+ totalOpened: 0,
7087
7104
  totalCancelled: 0,
7088
7105
  cancellationRate: null,
7106
+ activationRate: null,
7089
7107
  avgWaitTime: null,
7108
+ avgActivationTime: null,
7090
7109
  };
7091
7110
  }
7092
7111
  const scheduledEvents = this._eventList.filter((e) => e.action === "scheduled");
7112
+ const openedEvents = this._eventList.filter((e) => e.action === "opened");
7093
7113
  const cancelledEvents = this._eventList.filter((e) => e.action === "cancelled");
7094
7114
  const totalScheduled = scheduledEvents.length;
7115
+ const totalOpened = openedEvents.length;
7095
7116
  const totalCancelled = cancelledEvents.length;
7096
7117
  // Calculate cancellation rate
7097
7118
  const cancellationRate = totalScheduled > 0 ? (totalCancelled / totalScheduled) * 100 : null;
7119
+ // Calculate activation rate
7120
+ const activationRate = totalScheduled > 0 ? (totalOpened / totalScheduled) * 100 : null;
7098
7121
  // Calculate average wait time for cancelled signals
7099
7122
  const avgWaitTime = totalCancelled > 0
7100
7123
  ? cancelledEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
7101
7124
  totalCancelled
7102
7125
  : null;
7126
+ // Calculate average activation time for opened signals
7127
+ const avgActivationTime = totalOpened > 0
7128
+ ? openedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
7129
+ totalOpened
7130
+ : null;
7103
7131
  return {
7104
7132
  eventList: this._eventList,
7105
7133
  totalEvents: this._eventList.length,
7106
7134
  totalScheduled,
7135
+ totalOpened,
7107
7136
  totalCancelled,
7108
7137
  cancellationRate,
7138
+ activationRate,
7109
7139
  avgWaitTime,
7140
+ avgActivationTime,
7110
7141
  };
7111
7142
  }
7112
7143
  /**
@@ -7136,8 +7167,11 @@ let ReportStorage$2 = class ReportStorage {
7136
7167
  "",
7137
7168
  `**Total events:** ${stats.totalEvents}`,
7138
7169
  `**Scheduled signals:** ${stats.totalScheduled}`,
7170
+ `**Opened signals:** ${stats.totalOpened}`,
7139
7171
  `**Cancelled signals:** ${stats.totalCancelled}`,
7172
+ `**Activation rate:** ${stats.activationRate === null ? "N/A" : `${stats.activationRate.toFixed(2)}% (higher is better)`}`,
7140
7173
  `**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`,
7174
+ `**Average activation time:** ${stats.avgActivationTime === null ? "N/A" : `${stats.avgActivationTime.toFixed(2)} minutes`}`,
7141
7175
  `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`
7142
7176
  ].join("\n");
7143
7177
  }
@@ -7193,10 +7227,10 @@ class ScheduleMarkdownService {
7193
7227
  */
7194
7228
  this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$2());
7195
7229
  /**
7196
- * Processes tick events and accumulates scheduled/cancelled events.
7197
- * Should be called from signalLiveEmitter subscription.
7230
+ * Processes tick events and accumulates scheduled/opened/cancelled events.
7231
+ * Should be called from signalEmitter subscription.
7198
7232
  *
7199
- * Processes only scheduled and cancelled event types.
7233
+ * Processes only scheduled, opened and cancelled event types.
7200
7234
  *
7201
7235
  * @param data - Tick result from strategy execution
7202
7236
  *
@@ -7214,6 +7248,13 @@ class ScheduleMarkdownService {
7214
7248
  if (data.action === "scheduled") {
7215
7249
  storage.addScheduledEvent(data);
7216
7250
  }
7251
+ else if (data.action === "opened") {
7252
+ // Check if this opened signal was previously scheduled
7253
+ // by checking if signal has scheduledAt != pendingAt
7254
+ if (data.signal.scheduledAt !== data.signal.pendingAt) {
7255
+ storage.addOpenedEvent(data);
7256
+ }
7257
+ }
7217
7258
  else if (data.action === "cancelled") {
7218
7259
  storage.addCancelledEvent(data);
7219
7260
  }
@@ -7351,7 +7392,7 @@ function percentile(sortedArray, p) {
7351
7392
  return sortedArray[Math.max(0, index)];
7352
7393
  }
7353
7394
  /** Maximum number of performance events to store per strategy */
7354
- const MAX_EVENTS$1 = 10000;
7395
+ const MAX_EVENTS$2 = 10000;
7355
7396
  /**
7356
7397
  * Storage class for accumulating performance metrics per strategy.
7357
7398
  * Maintains a list of all performance events and provides aggregated statistics.
@@ -7369,7 +7410,7 @@ class PerformanceStorage {
7369
7410
  addEvent(event) {
7370
7411
  this._events.push(event);
7371
7412
  // Trim queue if exceeded MAX_EVENTS (keep most recent)
7372
- if (this._events.length > MAX_EVENTS$1) {
7413
+ if (this._events.length > MAX_EVENTS$2) {
7373
7414
  this._events.shift();
7374
7415
  }
7375
7416
  }
@@ -8281,6 +8322,8 @@ const columns$1 = [
8281
8322
  format: (data) => data.totalTrades.toString(),
8282
8323
  },
8283
8324
  ];
8325
+ /** Maximum number of signals to store per symbol in heatmap reports */
8326
+ const MAX_EVENTS$1 = 250;
8284
8327
  /**
8285
8328
  * Storage class for accumulating closed signals per strategy and generating heatmap.
8286
8329
  * Maintains symbol-level statistics and provides portfolio-wide metrics.
@@ -8300,7 +8343,12 @@ class HeatmapStorage {
8300
8343
  if (!this.symbolData.has(symbol)) {
8301
8344
  this.symbolData.set(symbol, []);
8302
8345
  }
8303
- this.symbolData.get(symbol).push(data);
8346
+ const signals = this.symbolData.get(symbol);
8347
+ signals.push(data);
8348
+ // Trim queue if exceeded MAX_EVENTS per symbol
8349
+ if (signals.length > MAX_EVENTS$1) {
8350
+ signals.shift();
8351
+ }
8304
8352
  }
8305
8353
  /**
8306
8354
  * Calculates statistics for a single symbol.
package/build/index.mjs CHANGED
@@ -588,6 +588,14 @@ class ClientExchange {
588
588
  const vwap = sumPriceVolume / totalVolume;
589
589
  return vwap;
590
590
  }
591
+ /**
592
+ * Formats quantity according to exchange-specific rules for the given symbol.
593
+ * Applies proper decimal precision and rounding based on symbol's lot size filters.
594
+ *
595
+ * @param symbol - Trading pair symbol
596
+ * @param quantity - Raw quantity to format
597
+ * @returns Promise resolving to formatted quantity as string
598
+ */
591
599
  async formatQuantity(symbol, quantity) {
592
600
  this.params.logger.debug("binanceService formatQuantity", {
593
601
  symbol,
@@ -595,6 +603,14 @@ class ClientExchange {
595
603
  });
596
604
  return await this.params.formatQuantity(symbol, quantity);
597
605
  }
606
+ /**
607
+ * Formats price according to exchange-specific rules for the given symbol.
608
+ * Applies proper decimal precision and rounding based on symbol's price filters.
609
+ *
610
+ * @param symbol - Trading pair symbol
611
+ * @param price - Raw price to format
612
+ * @returns Promise resolving to formatted price as string
613
+ */
598
614
  async formatPrice(symbol, price) {
599
615
  this.params.logger.debug("binanceService formatPrice", {
600
616
  symbol,
@@ -6469,7 +6485,7 @@ const columns$3 = [
6469
6485
  },
6470
6486
  ];
6471
6487
  /** Maximum number of events to store in live trading reports */
6472
- const MAX_EVENTS$3 = 250;
6488
+ const MAX_EVENTS$4 = 250;
6473
6489
  /**
6474
6490
  * Storage class for accumulating all tick events per strategy.
6475
6491
  * Maintains a chronological list of all events (idle, opened, active, closed).
@@ -6502,7 +6518,7 @@ let ReportStorage$3 = class ReportStorage {
6502
6518
  }
6503
6519
  {
6504
6520
  this._eventList.push(newEvent);
6505
- if (this._eventList.length > MAX_EVENTS$3) {
6521
+ if (this._eventList.length > MAX_EVENTS$4) {
6506
6522
  this._eventList.shift();
6507
6523
  }
6508
6524
  }
@@ -6526,19 +6542,16 @@ let ReportStorage$3 = class ReportStorage {
6526
6542
  stopLoss: data.signal.priceStopLoss,
6527
6543
  });
6528
6544
  // Trim queue if exceeded MAX_EVENTS
6529
- if (this._eventList.length > MAX_EVENTS$3) {
6545
+ if (this._eventList.length > MAX_EVENTS$4) {
6530
6546
  this._eventList.shift();
6531
6547
  }
6532
6548
  }
6533
6549
  /**
6534
- * Updates or adds an active event to the storage.
6535
- * Replaces the previous event with the same signalId.
6550
+ * Adds an active event to the storage.
6536
6551
  *
6537
6552
  * @param data - Active tick result
6538
6553
  */
6539
6554
  addActiveEvent(data) {
6540
- // Find existing event with the same signalId
6541
- const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
6542
6555
  const newEvent = {
6543
6556
  timestamp: Date.now(),
6544
6557
  action: "active",
@@ -6553,29 +6566,20 @@ let ReportStorage$3 = class ReportStorage {
6553
6566
  percentTp: data.percentTp,
6554
6567
  percentSl: data.percentSl,
6555
6568
  };
6556
- // Replace existing event or add new one
6557
- if (existingIndex !== -1) {
6558
- this._eventList[existingIndex] = newEvent;
6559
- }
6560
- else {
6561
- this._eventList.push(newEvent);
6562
- // Trim queue if exceeded MAX_EVENTS
6563
- if (this._eventList.length > MAX_EVENTS$3) {
6564
- this._eventList.shift();
6565
- }
6569
+ this._eventList.push(newEvent);
6570
+ // Trim queue if exceeded MAX_EVENTS
6571
+ if (this._eventList.length > MAX_EVENTS$4) {
6572
+ this._eventList.shift();
6566
6573
  }
6567
6574
  }
6568
6575
  /**
6569
- * Updates or adds a closed event to the storage.
6570
- * Replaces the previous event with the same signalId.
6576
+ * Adds a closed event to the storage.
6571
6577
  *
6572
6578
  * @param data - Closed tick result
6573
6579
  */
6574
6580
  addClosedEvent(data) {
6575
6581
  const durationMs = data.closeTimestamp - data.signal.pendingAt;
6576
6582
  const durationMin = Math.round(durationMs / 60000);
6577
- // Find existing event with the same signalId
6578
- const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
6579
6583
  const newEvent = {
6580
6584
  timestamp: data.closeTimestamp,
6581
6585
  action: "closed",
@@ -6591,16 +6595,10 @@ let ReportStorage$3 = class ReportStorage {
6591
6595
  closeReason: data.closeReason,
6592
6596
  duration: durationMin,
6593
6597
  };
6594
- // Replace existing event or add new one
6595
- if (existingIndex !== -1) {
6596
- this._eventList[existingIndex] = newEvent;
6597
- }
6598
- else {
6599
- this._eventList.push(newEvent);
6600
- // Trim queue if exceeded MAX_EVENTS
6601
- if (this._eventList.length > MAX_EVENTS$3) {
6602
- this._eventList.shift();
6603
- }
6598
+ this._eventList.push(newEvent);
6599
+ // Trim queue if exceeded MAX_EVENTS
6600
+ if (this._eventList.length > MAX_EVENTS$4) {
6601
+ this._eventList.shift();
6604
6602
  }
6605
6603
  }
6606
6604
  /**
@@ -7001,7 +6999,7 @@ const columns$2 = [
7001
6999
  },
7002
7000
  ];
7003
7001
  /** Maximum number of events to store in schedule reports */
7004
- const MAX_EVENTS$2 = 250;
7002
+ const MAX_EVENTS$3 = 250;
7005
7003
  /**
7006
7004
  * Storage class for accumulating scheduled signal events per strategy.
7007
7005
  * Maintains a chronological list of scheduled and cancelled events.
@@ -7030,21 +7028,45 @@ let ReportStorage$2 = class ReportStorage {
7030
7028
  stopLoss: data.signal.priceStopLoss,
7031
7029
  });
7032
7030
  // Trim queue if exceeded MAX_EVENTS
7033
- if (this._eventList.length > MAX_EVENTS$2) {
7031
+ if (this._eventList.length > MAX_EVENTS$3) {
7032
+ this._eventList.shift();
7033
+ }
7034
+ }
7035
+ /**
7036
+ * Adds an opened event to the storage.
7037
+ *
7038
+ * @param data - Opened tick result
7039
+ */
7040
+ addOpenedEvent(data) {
7041
+ const durationMs = data.signal.pendingAt - data.signal.scheduledAt;
7042
+ const durationMin = Math.round(durationMs / 60000);
7043
+ const newEvent = {
7044
+ timestamp: data.signal.pendingAt,
7045
+ action: "opened",
7046
+ symbol: data.signal.symbol,
7047
+ signalId: data.signal.id,
7048
+ position: data.signal.position,
7049
+ note: data.signal.note,
7050
+ currentPrice: data.currentPrice,
7051
+ priceOpen: data.signal.priceOpen,
7052
+ takeProfit: data.signal.priceTakeProfit,
7053
+ stopLoss: data.signal.priceStopLoss,
7054
+ duration: durationMin,
7055
+ };
7056
+ this._eventList.push(newEvent);
7057
+ // Trim queue if exceeded MAX_EVENTS
7058
+ if (this._eventList.length > MAX_EVENTS$3) {
7034
7059
  this._eventList.shift();
7035
7060
  }
7036
7061
  }
7037
7062
  /**
7038
- * Updates or adds a cancelled event to the storage.
7039
- * Replaces the previous event with the same signalId.
7063
+ * Adds a cancelled event to the storage.
7040
7064
  *
7041
7065
  * @param data - Cancelled tick result
7042
7066
  */
7043
7067
  addCancelledEvent(data) {
7044
7068
  const durationMs = data.closeTimestamp - data.signal.scheduledAt;
7045
7069
  const durationMin = Math.round(durationMs / 60000);
7046
- // Find existing event with the same signalId
7047
- const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
7048
7070
  const newEvent = {
7049
7071
  timestamp: data.closeTimestamp,
7050
7072
  action: "cancelled",
@@ -7059,16 +7081,10 @@ let ReportStorage$2 = class ReportStorage {
7059
7081
  closeTimestamp: data.closeTimestamp,
7060
7082
  duration: durationMin,
7061
7083
  };
7062
- // Replace existing event or add new one
7063
- if (existingIndex !== -1) {
7064
- this._eventList[existingIndex] = newEvent;
7065
- }
7066
- else {
7067
- this._eventList.push(newEvent);
7068
- // Trim queue if exceeded MAX_EVENTS
7069
- if (this._eventList.length > MAX_EVENTS$2) {
7070
- this._eventList.shift();
7071
- }
7084
+ this._eventList.push(newEvent);
7085
+ // Trim queue if exceeded MAX_EVENTS
7086
+ if (this._eventList.length > MAX_EVENTS$3) {
7087
+ this._eventList.shift();
7072
7088
  }
7073
7089
  }
7074
7090
  /**
@@ -7082,29 +7098,44 @@ let ReportStorage$2 = class ReportStorage {
7082
7098
  eventList: [],
7083
7099
  totalEvents: 0,
7084
7100
  totalScheduled: 0,
7101
+ totalOpened: 0,
7085
7102
  totalCancelled: 0,
7086
7103
  cancellationRate: null,
7104
+ activationRate: null,
7087
7105
  avgWaitTime: null,
7106
+ avgActivationTime: null,
7088
7107
  };
7089
7108
  }
7090
7109
  const scheduledEvents = this._eventList.filter((e) => e.action === "scheduled");
7110
+ const openedEvents = this._eventList.filter((e) => e.action === "opened");
7091
7111
  const cancelledEvents = this._eventList.filter((e) => e.action === "cancelled");
7092
7112
  const totalScheduled = scheduledEvents.length;
7113
+ const totalOpened = openedEvents.length;
7093
7114
  const totalCancelled = cancelledEvents.length;
7094
7115
  // Calculate cancellation rate
7095
7116
  const cancellationRate = totalScheduled > 0 ? (totalCancelled / totalScheduled) * 100 : null;
7117
+ // Calculate activation rate
7118
+ const activationRate = totalScheduled > 0 ? (totalOpened / totalScheduled) * 100 : null;
7096
7119
  // Calculate average wait time for cancelled signals
7097
7120
  const avgWaitTime = totalCancelled > 0
7098
7121
  ? cancelledEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
7099
7122
  totalCancelled
7100
7123
  : null;
7124
+ // Calculate average activation time for opened signals
7125
+ const avgActivationTime = totalOpened > 0
7126
+ ? openedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
7127
+ totalOpened
7128
+ : null;
7101
7129
  return {
7102
7130
  eventList: this._eventList,
7103
7131
  totalEvents: this._eventList.length,
7104
7132
  totalScheduled,
7133
+ totalOpened,
7105
7134
  totalCancelled,
7106
7135
  cancellationRate,
7136
+ activationRate,
7107
7137
  avgWaitTime,
7138
+ avgActivationTime,
7108
7139
  };
7109
7140
  }
7110
7141
  /**
@@ -7134,8 +7165,11 @@ let ReportStorage$2 = class ReportStorage {
7134
7165
  "",
7135
7166
  `**Total events:** ${stats.totalEvents}`,
7136
7167
  `**Scheduled signals:** ${stats.totalScheduled}`,
7168
+ `**Opened signals:** ${stats.totalOpened}`,
7137
7169
  `**Cancelled signals:** ${stats.totalCancelled}`,
7170
+ `**Activation rate:** ${stats.activationRate === null ? "N/A" : `${stats.activationRate.toFixed(2)}% (higher is better)`}`,
7138
7171
  `**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`,
7172
+ `**Average activation time:** ${stats.avgActivationTime === null ? "N/A" : `${stats.avgActivationTime.toFixed(2)} minutes`}`,
7139
7173
  `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`
7140
7174
  ].join("\n");
7141
7175
  }
@@ -7191,10 +7225,10 @@ class ScheduleMarkdownService {
7191
7225
  */
7192
7226
  this.getStorage = memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$2());
7193
7227
  /**
7194
- * Processes tick events and accumulates scheduled/cancelled events.
7195
- * Should be called from signalLiveEmitter subscription.
7228
+ * Processes tick events and accumulates scheduled/opened/cancelled events.
7229
+ * Should be called from signalEmitter subscription.
7196
7230
  *
7197
- * Processes only scheduled and cancelled event types.
7231
+ * Processes only scheduled, opened and cancelled event types.
7198
7232
  *
7199
7233
  * @param data - Tick result from strategy execution
7200
7234
  *
@@ -7212,6 +7246,13 @@ class ScheduleMarkdownService {
7212
7246
  if (data.action === "scheduled") {
7213
7247
  storage.addScheduledEvent(data);
7214
7248
  }
7249
+ else if (data.action === "opened") {
7250
+ // Check if this opened signal was previously scheduled
7251
+ // by checking if signal has scheduledAt != pendingAt
7252
+ if (data.signal.scheduledAt !== data.signal.pendingAt) {
7253
+ storage.addOpenedEvent(data);
7254
+ }
7255
+ }
7215
7256
  else if (data.action === "cancelled") {
7216
7257
  storage.addCancelledEvent(data);
7217
7258
  }
@@ -7349,7 +7390,7 @@ function percentile(sortedArray, p) {
7349
7390
  return sortedArray[Math.max(0, index)];
7350
7391
  }
7351
7392
  /** Maximum number of performance events to store per strategy */
7352
- const MAX_EVENTS$1 = 10000;
7393
+ const MAX_EVENTS$2 = 10000;
7353
7394
  /**
7354
7395
  * Storage class for accumulating performance metrics per strategy.
7355
7396
  * Maintains a list of all performance events and provides aggregated statistics.
@@ -7367,7 +7408,7 @@ class PerformanceStorage {
7367
7408
  addEvent(event) {
7368
7409
  this._events.push(event);
7369
7410
  // Trim queue if exceeded MAX_EVENTS (keep most recent)
7370
- if (this._events.length > MAX_EVENTS$1) {
7411
+ if (this._events.length > MAX_EVENTS$2) {
7371
7412
  this._events.shift();
7372
7413
  }
7373
7414
  }
@@ -8279,6 +8320,8 @@ const columns$1 = [
8279
8320
  format: (data) => data.totalTrades.toString(),
8280
8321
  },
8281
8322
  ];
8323
+ /** Maximum number of signals to store per symbol in heatmap reports */
8324
+ const MAX_EVENTS$1 = 250;
8282
8325
  /**
8283
8326
  * Storage class for accumulating closed signals per strategy and generating heatmap.
8284
8327
  * Maintains symbol-level statistics and provides portfolio-wide metrics.
@@ -8298,7 +8341,12 @@ class HeatmapStorage {
8298
8341
  if (!this.symbolData.has(symbol)) {
8299
8342
  this.symbolData.set(symbol, []);
8300
8343
  }
8301
- this.symbolData.get(symbol).push(data);
8344
+ const signals = this.symbolData.get(symbol);
8345
+ signals.push(data);
8346
+ // Trim queue if exceeded MAX_EVENTS per symbol
8347
+ if (signals.length > MAX_EVENTS$1) {
8348
+ signals.shift();
8349
+ }
8302
8350
  }
8303
8351
  /**
8304
8352
  * Calculates statistics for a single symbol.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "1.5.14",
3
+ "version": "1.5.16",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",
package/types.d.ts CHANGED
@@ -4079,13 +4079,13 @@ declare class LiveMarkdownService {
4079
4079
 
4080
4080
  /**
4081
4081
  * Unified scheduled signal event data for report generation.
4082
- * Contains all information about scheduled and cancelled events.
4082
+ * Contains all information about scheduled, opened and cancelled events.
4083
4083
  */
4084
4084
  interface ScheduledEvent {
4085
4085
  /** Event timestamp in milliseconds (scheduledAt for scheduled/cancelled events) */
4086
4086
  timestamp: number;
4087
4087
  /** Event action type */
4088
- action: "scheduled" | "cancelled";
4088
+ action: "scheduled" | "opened" | "cancelled";
4089
4089
  /** Trading pair symbol */
4090
4090
  symbol: string;
4091
4091
  /** Signal ID */
@@ -4104,13 +4104,13 @@ interface ScheduledEvent {
4104
4104
  stopLoss: number;
4105
4105
  /** Close timestamp (only for cancelled) */
4106
4106
  closeTimestamp?: number;
4107
- /** Duration in minutes (only for cancelled) */
4107
+ /** Duration in minutes (only for cancelled/opened) */
4108
4108
  duration?: number;
4109
4109
  }
4110
4110
  /**
4111
4111
  * Statistical data calculated from scheduled signals.
4112
4112
  *
4113
- * Provides metrics for scheduled signal tracking and cancellation analysis.
4113
+ * Provides metrics for scheduled signal tracking, activation and cancellation analysis.
4114
4114
  *
4115
4115
  * @example
4116
4116
  * ```typescript
@@ -4118,10 +4118,11 @@ interface ScheduledEvent {
4118
4118
  *
4119
4119
  * console.log(`Total events: ${stats.totalEvents}`);
4120
4120
  * console.log(`Scheduled signals: ${stats.totalScheduled}`);
4121
+ * console.log(`Opened signals: ${stats.totalOpened}`);
4121
4122
  * console.log(`Cancelled signals: ${stats.totalCancelled}`);
4122
4123
  * console.log(`Cancellation rate: ${stats.cancellationRate}%`);
4123
4124
  *
4124
- * // Access raw event data (includes scheduled, cancelled)
4125
+ * // Access raw event data (includes scheduled, opened, cancelled)
4125
4126
  * stats.eventList.forEach(event => {
4126
4127
  * if (event.action === "cancelled") {
4127
4128
  * console.log(`Cancelled signal: ${event.signalId}`);
@@ -4130,18 +4131,24 @@ interface ScheduledEvent {
4130
4131
  * ```
4131
4132
  */
4132
4133
  interface ScheduleStatistics {
4133
- /** Array of all scheduled/cancelled events with full details */
4134
+ /** Array of all scheduled/opened/cancelled events with full details */
4134
4135
  eventList: ScheduledEvent[];
4135
- /** Total number of all events (includes scheduled, cancelled) */
4136
+ /** Total number of all events (includes scheduled, opened, cancelled) */
4136
4137
  totalEvents: number;
4137
4138
  /** Total number of scheduled signals */
4138
4139
  totalScheduled: number;
4140
+ /** Total number of opened signals (activated from scheduled) */
4141
+ totalOpened: number;
4139
4142
  /** Total number of cancelled signals */
4140
4143
  totalCancelled: number;
4141
4144
  /** Cancellation rate as percentage (0-100), null if no scheduled signals. Lower is better. */
4142
4145
  cancellationRate: number | null;
4146
+ /** Activation rate as percentage (0-100), null if no scheduled signals. Higher is better. */
4147
+ activationRate: number | null;
4143
4148
  /** Average waiting time for cancelled signals in minutes, null if no cancelled signals */
4144
4149
  avgWaitTime: number | null;
4150
+ /** Average waiting time for opened signals in minutes, null if no opened signals */
4151
+ avgActivationTime: number | null;
4145
4152
  }
4146
4153
  /**
4147
4154
  * Service for generating and saving scheduled signals markdown reports.
@@ -4173,10 +4180,10 @@ declare class ScheduleMarkdownService {
4173
4180
  */
4174
4181
  private getStorage;
4175
4182
  /**
4176
- * Processes tick events and accumulates scheduled/cancelled events.
4177
- * Should be called from signalLiveEmitter subscription.
4183
+ * Processes tick events and accumulates scheduled/opened/cancelled events.
4184
+ * Should be called from signalEmitter subscription.
4178
4185
  *
4179
- * Processes only scheduled and cancelled event types.
4186
+ * Processes only scheduled, opened and cancelled event types.
4180
4187
  *
4181
4188
  * @param data - Tick result from strategy execution
4182
4189
  *
@@ -6708,7 +6715,23 @@ declare class ClientExchange implements IExchange {
6708
6715
  * @throws Error if no candles available
6709
6716
  */
6710
6717
  getAveragePrice(symbol: string): Promise<number>;
6718
+ /**
6719
+ * Formats quantity according to exchange-specific rules for the given symbol.
6720
+ * Applies proper decimal precision and rounding based on symbol's lot size filters.
6721
+ *
6722
+ * @param symbol - Trading pair symbol
6723
+ * @param quantity - Raw quantity to format
6724
+ * @returns Promise resolving to formatted quantity as string
6725
+ */
6711
6726
  formatQuantity(symbol: string, quantity: number): Promise<string>;
6727
+ /**
6728
+ * Formats price according to exchange-specific rules for the given symbol.
6729
+ * Applies proper decimal precision and rounding based on symbol's price filters.
6730
+ *
6731
+ * @param symbol - Trading pair symbol
6732
+ * @param price - Raw price to format
6733
+ * @returns Promise resolving to formatted price as string
6734
+ */
6712
6735
  formatPrice(symbol: string, price: number): Promise<string>;
6713
6736
  }
6714
6737