backtest-kit 5.0.0 → 5.2.0

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
@@ -89,6 +89,10 @@ const coreServices$1 = {
89
89
  actionCoreService: Symbol('actionCoreService'),
90
90
  frameCoreService: Symbol('frameCoreService'),
91
91
  };
92
+ const metaServices$1 = {
93
+ priceMetaService: Symbol('priceMetaService'),
94
+ timeMetaService: Symbol('timeMetaService'),
95
+ };
92
96
  const globalServices$1 = {
93
97
  sizingGlobalService: Symbol('sizingGlobalService'),
94
98
  riskGlobalService: Symbol('riskGlobalService'),
@@ -154,6 +158,7 @@ const TYPES = {
154
158
  ...connectionServices$1,
155
159
  ...schemaServices$1,
156
160
  ...coreServices$1,
161
+ ...metaServices$1,
157
162
  ...globalServices$1,
158
163
  ...commandServices$1,
159
164
  ...logicPrivateServices$1,
@@ -3558,19 +3563,6 @@ const beginTime = (run) => (...args) => {
3558
3563
  return fn();
3559
3564
  };
3560
3565
 
3561
- /**
3562
- * Retrieves the current timestamp for debugging purposes.
3563
- * If an execution context is active (e.g., during a backtest), it returns the timestamp from the context to ensure consistency with the simulated time.
3564
- * Can be empty (undefined) if not called from strategy async context, as it's intended for debugging and not critical for logic.
3565
- * @return {number | undefined} The current timestamp in milliseconds from the execution context, or undefined if not available.
3566
- */
3567
- const getDebugTimestamp = () => {
3568
- if (ExecutionContextService.hasContext()) {
3569
- return bt.executionContextService.context.when.getTime();
3570
- }
3571
- return undefined;
3572
- };
3573
-
3574
3566
  const INTERVAL_MINUTES$6 = {
3575
3567
  "1m": 1,
3576
3568
  "3m": 3,
@@ -4278,7 +4270,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
4278
4270
  pendingAt: currentTime, // Для immediate signal оба времени одинаковые
4279
4271
  timestamp: currentTime,
4280
4272
  _isScheduled: false,
4281
- _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, debugTimestamp: currentTime }],
4273
+ _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
4282
4274
  };
4283
4275
  // Валидируем сигнал перед возвратом
4284
4276
  VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
@@ -4302,7 +4294,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
4302
4294
  pendingAt: SCHEDULED_SIGNAL_PENDING_MOCK, // Временно, обновится при активации
4303
4295
  timestamp: currentTime,
4304
4296
  _isScheduled: true,
4305
- _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, debugTimestamp: currentTime }],
4297
+ _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
4306
4298
  };
4307
4299
  // Валидируем сигнал перед возвратом
4308
4300
  VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
@@ -4322,7 +4314,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
4322
4314
  pendingAt: currentTime, // Для immediate signal оба времени одинаковые
4323
4315
  timestamp: currentTime,
4324
4316
  _isScheduled: false,
4325
- _entry: [{ price: currentPrice, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, debugTimestamp: currentTime }],
4317
+ _entry: [{ price: currentPrice, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
4326
4318
  };
4327
4319
  // Валидируем сигнал перед возвратом
4328
4320
  VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
@@ -4392,7 +4384,7 @@ const WAIT_FOR_DISPOSE_FN$1 = async (self) => {
4392
4384
  self.params.logger.debug("ClientStrategy dispose");
4393
4385
  await self.params.onDispose(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName, self.params.method.context.frameName, self.params.execution.context.backtest);
4394
4386
  };
4395
- const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
4387
+ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice, timestamp) => {
4396
4388
  // Initialize partial array if not present
4397
4389
  if (!signal._partial)
4398
4390
  signal._partial = [];
@@ -4421,7 +4413,7 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
4421
4413
  entryCountAtClose,
4422
4414
  currentPrice,
4423
4415
  costBasisAtClose: remainingCostBasis,
4424
- debugTimestamp: getDebugTimestamp(),
4416
+ timestamp,
4425
4417
  });
4426
4418
  self.params.logger.info("PARTIAL_PROFIT_FN executed", {
4427
4419
  signalId: signal.id,
@@ -4431,7 +4423,7 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
4431
4423
  });
4432
4424
  return true;
4433
4425
  };
4434
- const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
4426
+ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice, timestamp) => {
4435
4427
  // Initialize partial array if not present
4436
4428
  if (!signal._partial)
4437
4429
  signal._partial = [];
@@ -4459,7 +4451,7 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
4459
4451
  currentPrice,
4460
4452
  entryCountAtClose,
4461
4453
  costBasisAtClose: remainingCostBasis,
4462
- debugTimestamp: getDebugTimestamp(),
4454
+ timestamp,
4463
4455
  });
4464
4456
  self.params.logger.warn("PARTIAL_LOSS_FN executed", {
4465
4457
  signalId: signal.id,
@@ -4856,10 +4848,10 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4856
4848
  });
4857
4849
  return true;
4858
4850
  };
4859
- const AVERAGE_BUY_FN = (self, signal, currentPrice, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_COST) => {
4851
+ const AVERAGE_BUY_FN = (self, signal, currentPrice, timestamp, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_COST) => {
4860
4852
  // Ensure _entry is initialized (handles signals loaded from disk without _entry)
4861
4853
  if (!signal._entry || signal._entry.length === 0) {
4862
- signal._entry = [{ price: signal.priceOpen, cost: GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, debugTimestamp: getDebugTimestamp() }];
4854
+ signal._entry = [{ price: signal.priceOpen, cost: GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp }];
4863
4855
  }
4864
4856
  if (signal.position === "long") {
4865
4857
  // LONG: new entry must beat the all-time low — strictly below every prior entry price
@@ -4889,7 +4881,7 @@ const AVERAGE_BUY_FN = (self, signal, currentPrice, cost = GLOBAL_CONFIG.CC_POSI
4889
4881
  return false;
4890
4882
  }
4891
4883
  }
4892
- signal._entry.push({ price: currentPrice, cost, debugTimestamp: getDebugTimestamp() });
4884
+ signal._entry.push({ price: currentPrice, cost, timestamp });
4893
4885
  self.params.logger.info("AVERAGE_BUY_FN executed", {
4894
4886
  signalId: signal.id,
4895
4887
  position: signal.position,
@@ -5072,7 +5064,7 @@ const CALL_SCHEDULE_PING_CALLBACKS_FN = functoolsKit.trycatch(beginTime(async (s
5072
5064
  await ExecutionContextService.runInContext(async () => {
5073
5065
  const publicSignal = TO_PUBLIC_SIGNAL(scheduled, currentPrice);
5074
5066
  // Call system onSchedulePing callback first (emits to pingSubject)
5075
- await self.params.onSchedulePing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, publicSignal, self.params.execution.context.backtest, timestamp);
5067
+ await self.params.onSchedulePing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, publicSignal, currentPrice, self.params.execution.context.backtest, timestamp);
5076
5068
  // Call user onSchedulePing callback only if signal is still active (not cancelled, not activated)
5077
5069
  if (self.params.callbacks?.onSchedulePing) {
5078
5070
  await self.params.callbacks.onSchedulePing(self.params.execution.context.symbol, publicSignal, new Date(timestamp), self.params.execution.context.backtest);
@@ -5098,7 +5090,7 @@ const CALL_ACTIVE_PING_CALLBACKS_FN = functoolsKit.trycatch(beginTime(async (sel
5098
5090
  await ExecutionContextService.runInContext(async () => {
5099
5091
  const publicSignal = TO_PUBLIC_SIGNAL(pending, currentPrice);
5100
5092
  // Call system onActivePing callback first (emits to activePingSubject)
5101
- await self.params.onActivePing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, publicSignal, self.params.execution.context.backtest, timestamp);
5093
+ await self.params.onActivePing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, publicSignal, currentPrice, self.params.execution.context.backtest, timestamp);
5102
5094
  // Call user onActivePing callback only if signal is still active (not closed)
5103
5095
  if (self.params.callbacks?.onActivePing) {
5104
5096
  await self.params.callbacks.onActivePing(self.params.execution.context.symbol, publicSignal, new Date(timestamp), self.params.execution.context.backtest);
@@ -5896,6 +5888,57 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
5896
5888
  await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
5897
5889
  return result;
5898
5890
  };
5891
+ const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, averagePrice, closeTimestamp) => {
5892
+ const syncCloseAllowed = await CALL_SIGNAL_SYNC_CLOSE_FN(closeTimestamp, averagePrice, "closed", closedSignal, self);
5893
+ if (!syncCloseAllowed) {
5894
+ self.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry", {
5895
+ symbol: self.params.execution.context.symbol,
5896
+ signalId: closedSignal.id,
5897
+ });
5898
+ self._closedSignal = null;
5899
+ self._pendingSignal = closedSignal;
5900
+ throw new Error(`ClientStrategy backtest: signal close rejected by sync (signalId=${closedSignal.id}). ` +
5901
+ `Retry backtest() with new candle data.`);
5902
+ }
5903
+ self._closedSignal = null;
5904
+ await CALL_COMMIT_FN(self, {
5905
+ action: "close-pending",
5906
+ symbol: self.params.execution.context.symbol,
5907
+ strategyName: self.params.strategyName,
5908
+ exchangeName: self.params.exchangeName,
5909
+ frameName: self.params.frameName,
5910
+ signalId: closedSignal.id,
5911
+ backtest: true,
5912
+ closeId: closedSignal.closeId,
5913
+ timestamp: closeTimestamp,
5914
+ totalEntries: closedSignal._entry?.length ?? 1,
5915
+ totalPartials: closedSignal._partial?.length ?? 0,
5916
+ originalPriceOpen: closedSignal.priceOpen,
5917
+ pnl: toProfitLossDto(closedSignal, averagePrice),
5918
+ });
5919
+ await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
5920
+ await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
5921
+ await CALL_BREAKEVEN_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
5922
+ await CALL_RISK_REMOVE_SIGNAL_FN(self, self.params.execution.context.symbol, closeTimestamp, self.params.execution.context.backtest);
5923
+ const pnl = toProfitLossDto(closedSignal, averagePrice);
5924
+ const result = {
5925
+ action: "closed",
5926
+ signal: TO_PUBLIC_SIGNAL(closedSignal, averagePrice),
5927
+ currentPrice: averagePrice,
5928
+ closeReason: "closed",
5929
+ closeTimestamp,
5930
+ pnl,
5931
+ strategyName: self.params.method.context.strategyName,
5932
+ exchangeName: self.params.method.context.exchangeName,
5933
+ frameName: self.params.method.context.frameName,
5934
+ symbol: self.params.execution.context.symbol,
5935
+ backtest: self.params.execution.context.backtest,
5936
+ closeId: closedSignal.closeId,
5937
+ createdAt: closeTimestamp,
5938
+ };
5939
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
5940
+ return result;
5941
+ };
5899
5942
  const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
5900
5943
  const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
5901
5944
  const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
@@ -5913,7 +5956,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
5913
5956
  if (self._cancelledSignal) {
5914
5957
  // Сигнал был отменен через cancel() в onSchedulePing
5915
5958
  const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "user");
5916
- return { activated: false, cancelled: true, activationIndex: i, result };
5959
+ return { outcome: "cancelled", result };
5917
5960
  }
5918
5961
  // КРИТИЧНО: Проверяем был ли сигнал активирован пользователем через activateScheduled()
5919
5962
  // Обрабатываем inline (как в tick()) с риск-проверкой по averagePrice
@@ -5927,7 +5970,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
5927
5970
  signalId: activatedSignal.id,
5928
5971
  });
5929
5972
  await self.setScheduledSignal(null);
5930
- return { activated: false, cancelled: false, activationIndex: i, result: null };
5973
+ return { outcome: "pending" };
5931
5974
  }
5932
5975
  // Риск-проверка по averagePrice (симметрия с LIVE tick())
5933
5976
  if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, activatedSignal, averagePrice, candle.timestamp, self.params.execution.context.backtest))) {
@@ -5936,7 +5979,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
5936
5979
  signalId: activatedSignal.id,
5937
5980
  });
5938
5981
  await self.setScheduledSignal(null);
5939
- return { activated: false, cancelled: false, activationIndex: i, result: null };
5982
+ return { outcome: "pending" };
5940
5983
  }
5941
5984
  const pendingSignal = {
5942
5985
  ...activatedSignal,
@@ -5965,7 +6008,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
5965
6008
  originalPriceOpen: activatedSignal.priceOpen,
5966
6009
  pnl: toProfitLossDto(activatedSignal, averagePrice),
5967
6010
  });
5968
- return { activated: false, cancelled: true, activationIndex: i, result: null };
6011
+ return { outcome: "pending" };
5969
6012
  }
5970
6013
  await self.setScheduledSignal(null);
5971
6014
  await self.setPendingSignal(pendingSignal);
@@ -5998,18 +6041,13 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
5998
6041
  });
5999
6042
  await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, pendingSignal.priceOpen, candle.timestamp, self.params.execution.context.backtest);
6000
6043
  await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, pendingSignal, candle.timestamp, self.params.execution.context.backtest);
6001
- return {
6002
- activated: true,
6003
- cancelled: false,
6004
- activationIndex: i,
6005
- result: null,
6006
- };
6044
+ return { outcome: "activated", activationIndex: i };
6007
6045
  }
6008
6046
  // КРИТИЧНО: Проверяем timeout ПЕРЕД проверкой цены
6009
6047
  const elapsedTime = candle.timestamp - scheduled.scheduledAt;
6010
6048
  if (elapsedTime >= maxTimeToWait) {
6011
6049
  const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "timeout");
6012
- return { activated: false, cancelled: true, activationIndex: i, result };
6050
+ return { outcome: "cancelled", result };
6013
6051
  }
6014
6052
  let shouldActivate = false;
6015
6053
  let shouldCancel = false;
@@ -6047,27 +6085,17 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
6047
6085
  }
6048
6086
  if (shouldCancel) {
6049
6087
  const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "price_reject");
6050
- return { activated: false, cancelled: true, activationIndex: i, result };
6088
+ return { outcome: "cancelled", result };
6051
6089
  }
6052
6090
  if (shouldActivate) {
6053
6091
  await ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, candle.timestamp);
6054
- return {
6055
- activated: true,
6056
- cancelled: false,
6057
- activationIndex: i,
6058
- result: null,
6059
- };
6092
+ return { outcome: "activated", activationIndex: i };
6060
6093
  }
6061
6094
  await CALL_SCHEDULE_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, candle.timestamp, true, averagePrice);
6062
6095
  // Process queued commit events with candle timestamp
6063
6096
  await PROCESS_COMMIT_QUEUE_FN(self, averagePrice, candle.timestamp);
6064
6097
  }
6065
- return {
6066
- activated: false,
6067
- cancelled: false,
6068
- activationIndex: -1,
6069
- result: null,
6070
- };
6098
+ return { outcome: "pending" };
6071
6099
  };
6072
6100
  const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
6073
6101
  const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
@@ -6086,6 +6114,10 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
6086
6114
  const startIndex = Math.max(0, i - (candlesCount - 1));
6087
6115
  const recentCandles = candles.slice(startIndex, i + 1);
6088
6116
  const averagePrice = GET_AVG_PRICE_FN(recentCandles);
6117
+ // КРИТИЧНО: Проверяем был ли сигнал закрыт пользователем через closePending()
6118
+ if (self._closedSignal) {
6119
+ return await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
6120
+ }
6089
6121
  await CALL_ACTIVE_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentCandleTimestamp, true, averagePrice);
6090
6122
  let shouldClose = false;
6091
6123
  let closeReason;
@@ -6190,7 +6222,32 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
6190
6222
  // Process queued commit events with candle timestamp
6191
6223
  await PROCESS_COMMIT_QUEUE_FN(self, averagePrice, currentCandleTimestamp);
6192
6224
  }
6193
- return null;
6225
+ // Loop exhausted without closing — check if we have enough data
6226
+ const lastCandles = candles.slice(-GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
6227
+ const lastPrice = GET_AVG_PRICE_FN(lastCandles);
6228
+ const closeTimestamp = lastCandles[lastCandles.length - 1].timestamp;
6229
+ const signalTime = signal.pendingAt;
6230
+ const maxTimeToWait = signal.minuteEstimatedTime * 60 * 1000;
6231
+ const elapsedTime = closeTimestamp - signalTime;
6232
+ if (elapsedTime < maxTimeToWait) {
6233
+ const bufferCandlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
6234
+ const requiredCandlesCount = signal.minuteEstimatedTime + bufferCandlesCount + 1;
6235
+ throw new Error(functoolsKit.str.newline(`ClientStrategy backtest: Insufficient candle data for pending signal. ` +
6236
+ `Signal opened at ${new Date(signal.pendingAt).toISOString()}, ` +
6237
+ `last candle at ${new Date(closeTimestamp).toISOString()}. ` +
6238
+ `Elapsed: ${Math.floor(elapsedTime / 60000)}min of ${signal.minuteEstimatedTime}min required. ` +
6239
+ `Provided ${candles.length} candles, but need at least ${requiredCandlesCount} candles. ` +
6240
+ `\nBreakdown: ${signal.minuteEstimatedTime} candles for signal lifetime + ${bufferCandlesCount} buffer candles. ` +
6241
+ `\nBuffer explanation: VWAP calculation requires ${GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT} candles, ` +
6242
+ `so first ${bufferCandlesCount} candles are skipped to ensure accurate price averaging. ` +
6243
+ `Provide complete candle range: [pendingAt - ${bufferCandlesCount}min, pendingAt + ${signal.minuteEstimatedTime}min].`));
6244
+ }
6245
+ const timeExpiredResult = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, lastPrice, "time_expired", closeTimestamp);
6246
+ if (!timeExpiredResult) {
6247
+ throw new Error(`ClientStrategy backtest: time_expired close rejected by sync (signalId=${signal.id}). ` +
6248
+ `Retry backtest() with new candle data.`);
6249
+ }
6250
+ return timeExpiredResult;
6194
6251
  };
6195
6252
  /**
6196
6253
  * Client implementation for trading strategy lifecycle management.
@@ -6674,16 +6731,16 @@ class ClientStrategy {
6674
6731
  * // No DCA: [{ price: 43000, cost: 100 }]
6675
6732
  * // One DCA: [{ price: 43000, cost: 100 }, { price: 42000, cost: 100 }]
6676
6733
  */
6677
- async getPositionEntries(symbol) {
6734
+ async getPositionEntries(symbol, timestamp) {
6678
6735
  this.params.logger.debug("ClientStrategy getPositionEntries", { symbol });
6679
6736
  if (!this._pendingSignal) {
6680
6737
  return null;
6681
6738
  }
6682
6739
  const entries = this._pendingSignal._entry;
6683
6740
  if (!entries || entries.length === 0) {
6684
- return [{ price: this._pendingSignal.priceOpen, cost: GLOBAL_CONFIG.CC_POSITION_ENTRY_COST }];
6741
+ return [{ price: this._pendingSignal.priceOpen, cost: GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp }];
6685
6742
  }
6686
- return entries.map(({ price, cost }) => ({ price, cost }));
6743
+ return entries.map(({ price, cost, timestamp }) => ({ price, cost, timestamp }));
6687
6744
  }
6688
6745
  /**
6689
6746
  * Performs a single tick of strategy execution.
@@ -7137,11 +7194,12 @@ class ClientStrategy {
7137
7194
  priceOpen: scheduled.priceOpen,
7138
7195
  position: scheduled.position,
7139
7196
  });
7140
- const { activated, cancelled, activationIndex, result } = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles);
7141
- if (cancelled && result) {
7142
- return result;
7197
+ const scheduledResult = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles);
7198
+ if (scheduledResult.outcome === "cancelled") {
7199
+ return scheduledResult.result;
7143
7200
  }
7144
- if (activated) {
7201
+ if (scheduledResult.outcome === "activated") {
7202
+ const { activationIndex } = scheduledResult;
7145
7203
  // КРИТИЧНО: activationIndex - индекс свечи активации в массиве candles
7146
7204
  // BacktestLogicPrivateService включил буфер в начало массива, поэтому перед activationIndex достаточно свечей
7147
7205
  // PROCESS_PENDING_SIGNAL_CANDLES_FN пропустит первые bufferCandlesCount свечей для VWAP
@@ -7213,40 +7271,7 @@ class ClientStrategy {
7213
7271
  if (candles.length < candlesCount) {
7214
7272
  this.params.logger.warn(`ClientStrategy backtest: Expected at least ${candlesCount} candles for VWAP, got ${candles.length}`);
7215
7273
  }
7216
- const closedResult = await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles);
7217
- if (closedResult) {
7218
- return closedResult;
7219
- }
7220
- // Signal didn't close during candle processing - check if we have enough data
7221
- const lastCandles = candles.slice(-GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
7222
- const lastPrice = GET_AVG_PRICE_FN(lastCandles);
7223
- const closeTimestamp = lastCandles[lastCandles.length - 1].timestamp;
7224
- const signalTime = signal.pendingAt;
7225
- const maxTimeToWait = signal.minuteEstimatedTime * 60 * 1000;
7226
- const elapsedTime = closeTimestamp - signalTime;
7227
- // Check if we actually reached time expiration or just ran out of candles
7228
- if (elapsedTime < maxTimeToWait) {
7229
- // EDGE CASE: backtest() called with insufficient candle data
7230
- const bufferCandlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
7231
- const requiredCandlesCount = signal.minuteEstimatedTime + bufferCandlesCount + 1;
7232
- throw new Error(functoolsKit.str.newline(`ClientStrategy backtest: Insufficient candle data for pending signal. ` +
7233
- `Signal opened at ${new Date(signal.pendingAt).toISOString()}, ` +
7234
- `last candle at ${new Date(closeTimestamp).toISOString()}. ` +
7235
- `Elapsed: ${Math.floor(elapsedTime / 60000)}min of ${signal.minuteEstimatedTime}min required. ` +
7236
- `Provided ${candles.length} candles, but need at least ${requiredCandlesCount} candles. ` +
7237
- `\nBreakdown: ${signal.minuteEstimatedTime} candles for signal lifetime + ${bufferCandlesCount} buffer candles. ` +
7238
- `\nBuffer explanation: VWAP calculation requires ${GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT} candles, ` +
7239
- `so first ${bufferCandlesCount} candles are skipped to ensure accurate price averaging. ` +
7240
- `Provide complete candle range: [pendingAt - ${bufferCandlesCount}min, pendingAt + ${signal.minuteEstimatedTime}min].`));
7241
- }
7242
- // Time actually expired - close with time_expired
7243
- const timeExpiredResult = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(this, signal, lastPrice, "time_expired", closeTimestamp);
7244
- if (!timeExpiredResult) {
7245
- // Sync rejected the close — signal remains in _pendingSignal, caller must retry
7246
- throw new Error(`ClientStrategy backtest: time_expired close rejected by sync (signalId=${signal.id}). ` +
7247
- `Retry backtest() with new candle data.`);
7248
- }
7249
- return timeExpiredResult;
7274
+ return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles);
7250
7275
  }
7251
7276
  /**
7252
7277
  * Stops the strategy from generating new signals.
@@ -7519,7 +7544,7 @@ class ClientStrategy {
7519
7544
  * // success3 = false (skipped, would exceed 100%)
7520
7545
  * ```
7521
7546
  */
7522
- async partialProfit(symbol, percentToClose, currentPrice, backtest) {
7547
+ async partialProfit(symbol, percentToClose, currentPrice, backtest, timestamp) {
7523
7548
  this.params.logger.debug("ClientStrategy partialProfit", {
7524
7549
  symbol,
7525
7550
  percentToClose,
@@ -7583,7 +7608,7 @@ class ClientStrategy {
7583
7608
  return false;
7584
7609
  }
7585
7610
  // Execute partial close logic
7586
- const wasExecuted = PARTIAL_PROFIT_FN(this, this._pendingSignal, percentToClose, currentPrice);
7611
+ const wasExecuted = PARTIAL_PROFIT_FN(this, this._pendingSignal, percentToClose, currentPrice, timestamp);
7587
7612
  // If partial was not executed (exceeded 100%), return false without persistence
7588
7613
  if (!wasExecuted) {
7589
7614
  return false;
@@ -7702,7 +7727,7 @@ class ClientStrategy {
7702
7727
  * // success3 = false (skipped, would exceed 100%)
7703
7728
  * ```
7704
7729
  */
7705
- async partialLoss(symbol, percentToClose, currentPrice, backtest) {
7730
+ async partialLoss(symbol, percentToClose, currentPrice, backtest, timestamp) {
7706
7731
  this.params.logger.debug("ClientStrategy partialLoss", {
7707
7732
  symbol,
7708
7733
  percentToClose,
@@ -7766,7 +7791,7 @@ class ClientStrategy {
7766
7791
  return false;
7767
7792
  }
7768
7793
  // Execute partial close logic
7769
- const wasExecuted = PARTIAL_LOSS_FN(this, this._pendingSignal, percentToClose, currentPrice);
7794
+ const wasExecuted = PARTIAL_LOSS_FN(this, this._pendingSignal, percentToClose, currentPrice, timestamp);
7770
7795
  // If partial was not executed (exceeded 100%), return false without persistence
7771
7796
  if (!wasExecuted) {
7772
7797
  return false;
@@ -8522,7 +8547,7 @@ class ClientStrategy {
8522
8547
  * @param backtest - Whether running in backtest mode
8523
8548
  * @returns Promise<boolean> - true if entry added, false if rejected by direction check
8524
8549
  */
8525
- async averageBuy(symbol, currentPrice, backtest, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_COST) {
8550
+ async averageBuy(symbol, currentPrice, backtest, timestamp, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_COST) {
8526
8551
  this.params.logger.debug("ClientStrategy averageBuy", {
8527
8552
  symbol,
8528
8553
  currentPrice,
@@ -8537,7 +8562,7 @@ class ClientStrategy {
8537
8562
  throw new Error(`ClientStrategy averageBuy: currentPrice must be a positive finite number, got ${currentPrice}`);
8538
8563
  }
8539
8564
  // Execute averaging logic
8540
- const result = AVERAGE_BUY_FN(this, this._pendingSignal, currentPrice, cost);
8565
+ const result = AVERAGE_BUY_FN(this, this._pendingSignal, currentPrice, timestamp, cost);
8541
8566
  if (!result) {
8542
8567
  return false;
8543
8568
  }
@@ -9001,7 +9026,7 @@ const GET_RISK_FN = (dto, backtest, exchangeName, frameName, self) => {
9001
9026
  * @param backtest - Whether running in backtest mode
9002
9027
  * @returns Unique string key for memoization
9003
9028
  */
9004
- const CREATE_KEY_FN$m = (symbol, strategyName, exchangeName, frameName, backtest) => {
9029
+ const CREATE_KEY_FN$o = (symbol, strategyName, exchangeName, frameName, backtest) => {
9005
9030
  const parts = [symbol, strategyName, exchangeName];
9006
9031
  if (frameName)
9007
9032
  parts.push(frameName);
@@ -9017,11 +9042,12 @@ const CREATE_KEY_FN$m = (symbol, strategyName, exchangeName, frameName, backtest
9017
9042
  * @param self - Reference to StrategyConnectionService instance
9018
9043
  * @returns Callback function for schedule ping events
9019
9044
  */
9020
- const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => functoolsKit.trycatch(async (symbol, strategyName, exchangeName, data, backtest, timestamp) => {
9045
+ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => functoolsKit.trycatch(async (symbol, strategyName, exchangeName, data, currentPrice, backtest, timestamp) => {
9021
9046
  const event = {
9022
9047
  symbol,
9023
9048
  strategyName,
9024
9049
  exchangeName,
9050
+ currentPrice,
9025
9051
  data,
9026
9052
  backtest,
9027
9053
  timestamp,
@@ -9050,11 +9076,12 @@ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => functoolsKit.trycatch(async (sy
9050
9076
  * @param self - Reference to StrategyConnectionService instance
9051
9077
  * @returns Callback function for active ping events
9052
9078
  */
9053
- const CREATE_COMMIT_ACTIVE_PING_FN = (self) => functoolsKit.trycatch(async (symbol, strategyName, exchangeName, data, backtest, timestamp) => {
9079
+ const CREATE_COMMIT_ACTIVE_PING_FN = (self) => functoolsKit.trycatch(async (symbol, strategyName, exchangeName, data, currentPrice, backtest, timestamp) => {
9054
9080
  const event = {
9055
9081
  symbol,
9056
9082
  strategyName,
9057
9083
  exchangeName,
9084
+ currentPrice,
9058
9085
  data,
9059
9086
  backtest,
9060
9087
  timestamp,
@@ -9178,6 +9205,8 @@ class StrategyConnectionService {
9178
9205
  this.partialConnectionService = inject(TYPES.partialConnectionService);
9179
9206
  this.breakevenConnectionService = inject(TYPES.breakevenConnectionService);
9180
9207
  this.actionCoreService = inject(TYPES.actionCoreService);
9208
+ this.timeMetaService = inject(TYPES.timeMetaService);
9209
+ this.priceMetaService = inject(TYPES.priceMetaService);
9181
9210
  /**
9182
9211
  * Retrieves memoized ClientStrategy instance for given symbol-strategy pair with exchange and frame isolation.
9183
9212
  *
@@ -9191,7 +9220,7 @@ class StrategyConnectionService {
9191
9220
  * @param backtest - Whether running in backtest mode
9192
9221
  * @returns Configured ClientStrategy instance
9193
9222
  */
9194
- this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$m(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
9223
+ this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$o(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
9195
9224
  const { riskName = "", riskList = [], getSignal, interval, callbacks, } = this.strategySchemaService.get(strategyName);
9196
9225
  return new ClientStrategy({
9197
9226
  symbol,
@@ -9278,6 +9307,20 @@ class StrategyConnectionService {
9278
9307
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9279
9308
  return await strategy.getTotalCostClosed(symbol);
9280
9309
  };
9310
+ /**
9311
+ * Returns the effective (DCA-averaged) entry price for the current pending signal.
9312
+ *
9313
+ * This is the harmonic mean of all _entry prices, which is the correct
9314
+ * cost-basis price used in all PNL calculations.
9315
+ * With no DCA entries, equals the original priceOpen.
9316
+ *
9317
+ * Returns null if no pending signal exists.
9318
+ *
9319
+ * @param backtest - Whether running in backtest mode
9320
+ * @param symbol - Trading pair symbol
9321
+ * @param context - Execution context with strategyName, exchangeName, frameName
9322
+ * @returns Promise resolving to effective entry price or null
9323
+ */
9281
9324
  this.getPositionAveragePrice = async (backtest, symbol, context) => {
9282
9325
  this.loggerService.log("strategyConnectionService getPositionAveragePrice", {
9283
9326
  symbol,
@@ -9287,6 +9330,19 @@ class StrategyConnectionService {
9287
9330
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9288
9331
  return await strategy.getPositionAveragePrice(symbol);
9289
9332
  };
9333
+ /**
9334
+ * Returns the number of DCA entries made for the current pending signal.
9335
+ *
9336
+ * 1 = original entry only (no DCA).
9337
+ * Increases by 1 with each successful commitAverageBuy().
9338
+ *
9339
+ * Returns null if no pending signal exists.
9340
+ *
9341
+ * @param backtest - Whether running in backtest mode
9342
+ * @param symbol - Trading pair symbol
9343
+ * @param context - Execution context with strategyName, exchangeName, frameName
9344
+ * @returns Promise resolving to entry count or null
9345
+ */
9290
9346
  this.getPositionInvestedCount = async (backtest, symbol, context) => {
9291
9347
  this.loggerService.log("strategyConnectionService getPositionInvestedCount", {
9292
9348
  symbol,
@@ -9296,6 +9352,19 @@ class StrategyConnectionService {
9296
9352
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9297
9353
  return await strategy.getPositionInvestedCount(symbol);
9298
9354
  };
9355
+ /**
9356
+ * Returns the total invested cost basis in dollars for the current pending signal.
9357
+ *
9358
+ * Equal to entryCount × $100 (COST_BASIS_PER_ENTRY).
9359
+ * 1 entry = $100, 2 entries = $200, etc.
9360
+ *
9361
+ * Returns null if no pending signal exists.
9362
+ *
9363
+ * @param backtest - Whether running in backtest mode
9364
+ * @param symbol - Trading pair symbol
9365
+ * @param context - Execution context with strategyName, exchangeName, frameName
9366
+ * @returns Promise resolving to total invested cost in dollars or null
9367
+ */
9299
9368
  this.getPositionInvestedCost = async (backtest, symbol, context) => {
9300
9369
  this.loggerService.log("strategyConnectionService getPositionInvestedCost", {
9301
9370
  symbol,
@@ -9305,6 +9374,20 @@ class StrategyConnectionService {
9305
9374
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9306
9375
  return await strategy.getPositionInvestedCost(symbol);
9307
9376
  };
9377
+ /**
9378
+ * Returns the unrealized PNL percentage for the current pending signal at currentPrice.
9379
+ *
9380
+ * Accounts for partial closes, DCA entries, slippage and fees
9381
+ * (delegates to toProfitLossDto).
9382
+ *
9383
+ * Returns null if no pending signal exists.
9384
+ *
9385
+ * @param backtest - Whether running in backtest mode
9386
+ * @param symbol - Trading pair symbol
9387
+ * @param currentPrice - Current market price
9388
+ * @param context - Execution context with strategyName, exchangeName, frameName
9389
+ * @returns Promise resolving to pnlPercentage or null
9390
+ */
9308
9391
  this.getPositionPnlPercent = async (backtest, symbol, currentPrice, context) => {
9309
9392
  this.loggerService.log("strategyConnectionService getPositionPnlPercent", {
9310
9393
  symbol,
@@ -9315,6 +9398,20 @@ class StrategyConnectionService {
9315
9398
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9316
9399
  return await strategy.getPositionPnlPercent(symbol, currentPrice);
9317
9400
  };
9401
+ /**
9402
+ * Returns the unrealized PNL in dollars for the current pending signal at currentPrice.
9403
+ *
9404
+ * Calculated as: pnlPercentage / 100 × totalInvestedCost
9405
+ * Accounts for partial closes, DCA entries, slippage and fees.
9406
+ *
9407
+ * Returns null if no pending signal exists.
9408
+ *
9409
+ * @param backtest - Whether running in backtest mode
9410
+ * @param symbol - Trading pair symbol
9411
+ * @param currentPrice - Current market price
9412
+ * @param context - Execution context with strategyName, exchangeName, frameName
9413
+ * @returns Promise resolving to pnl in dollars or null
9414
+ */
9318
9415
  this.getPositionPnlCost = async (backtest, symbol, currentPrice, context) => {
9319
9416
  this.loggerService.log("strategyConnectionService getPositionPnlCost", {
9320
9417
  symbol,
@@ -9325,6 +9422,27 @@ class StrategyConnectionService {
9325
9422
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9326
9423
  return await strategy.getPositionPnlCost(symbol, currentPrice);
9327
9424
  };
9425
+ /**
9426
+ * Returns the list of DCA entry prices for the current pending signal.
9427
+ *
9428
+ * The first element is always the original priceOpen (initial entry).
9429
+ * Each subsequent element is a price added by commitAverageBuy().
9430
+ *
9431
+ * Returns null if no pending signal exists.
9432
+ * Returns a single-element array [priceOpen] if no DCA entries were made.
9433
+ *
9434
+ * @param backtest - Whether running in backtest mode
9435
+ * @param symbol - Trading pair symbol
9436
+ * @param context - Execution context with strategyName, exchangeName, frameName
9437
+ * @returns Promise resolving to array of entry prices or null
9438
+ *
9439
+ * @example
9440
+ * ```typescript
9441
+ * // No DCA: [43000]
9442
+ * // One DCA: [43000, 42000]
9443
+ * // Two DCA: [43000, 42000, 41500]
9444
+ * ```
9445
+ */
9328
9446
  this.getPositionLevels = async (backtest, symbol, context) => {
9329
9447
  this.loggerService.log("strategyConnectionService getPositionLevels", {
9330
9448
  symbol,
@@ -9334,6 +9452,20 @@ class StrategyConnectionService {
9334
9452
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9335
9453
  return await strategy.getPositionLevels(symbol);
9336
9454
  };
9455
+ /**
9456
+ * Returns the list of partial closes for the current pending signal.
9457
+ *
9458
+ * Each entry records a partial profit or loss close event with its type,
9459
+ * percent closed, price at close, cost basis snapshot, and entry count at close.
9460
+ *
9461
+ * Returns null if no pending signal exists.
9462
+ * Returns an empty array if no partial closes have been executed.
9463
+ *
9464
+ * @param backtest - Whether running in backtest mode
9465
+ * @param symbol - Trading pair symbol
9466
+ * @param context - Execution context with strategyName, exchangeName, frameName
9467
+ * @returns Promise resolving to array of partial close records or null
9468
+ */
9337
9469
  this.getPositionPartials = async (backtest, symbol, context) => {
9338
9470
  this.loggerService.log("strategyConnectionService getPositionPartials", {
9339
9471
  symbol,
@@ -9343,6 +9475,27 @@ class StrategyConnectionService {
9343
9475
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9344
9476
  return await strategy.getPositionPartials(symbol);
9345
9477
  };
9478
+ /**
9479
+ * Returns the list of DCA entry prices and costs for the current pending signal.
9480
+ *
9481
+ * Each entry records the price and cost of a single position entry.
9482
+ * The first element is always the original priceOpen (initial entry).
9483
+ * Each subsequent element is an entry added by averageBuy().
9484
+ *
9485
+ * Returns null if no pending signal exists.
9486
+ * Returns a single-element array [{ price: priceOpen, cost }] if no DCA entries were made.
9487
+ *
9488
+ * @param backtest - Whether running in backtest mode
9489
+ * @param symbol - Trading pair symbol
9490
+ * @param context - Execution context with strategyName, exchangeName, frameName
9491
+ * @returns Promise resolving to array of entry records or null
9492
+ *
9493
+ * @example
9494
+ * ```typescript
9495
+ * // No DCA: [{ price: 43000, cost: 100 }]
9496
+ * // One DCA: [{ price: 43000, cost: 100 }, { price: 42000, cost: 100 }]
9497
+ * ```
9498
+ */
9346
9499
  this.getPositionEntries = async (backtest, symbol, context) => {
9347
9500
  this.loggerService.log("strategyConnectionService getPositionEntries", {
9348
9501
  symbol,
@@ -9350,7 +9503,8 @@ class StrategyConnectionService {
9350
9503
  backtest,
9351
9504
  });
9352
9505
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9353
- return await strategy.getPositionEntries(symbol);
9506
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
9507
+ return await strategy.getPositionEntries(symbol, timestamp);
9354
9508
  };
9355
9509
  /**
9356
9510
  * Retrieves the currently active scheduled signal for the strategy.
@@ -9452,6 +9606,10 @@ class StrategyConnectionService {
9452
9606
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9453
9607
  await strategy.waitForInit();
9454
9608
  const tick = await strategy.tick(symbol, context.strategyName);
9609
+ {
9610
+ this.priceMetaService.next(symbol, tick.currentPrice, context, backtest);
9611
+ this.timeMetaService.next(symbol, tick.createdAt, context, backtest);
9612
+ }
9455
9613
  {
9456
9614
  await CALL_SIGNAL_EMIT_FN(this, tick, context, backtest, symbol);
9457
9615
  }
@@ -9558,7 +9716,7 @@ class StrategyConnectionService {
9558
9716
  }
9559
9717
  return;
9560
9718
  }
9561
- const key = CREATE_KEY_FN$m(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
9719
+ const key = CREATE_KEY_FN$o(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
9562
9720
  if (!this.getStrategy.has(key)) {
9563
9721
  return;
9564
9722
  }
@@ -9627,9 +9785,9 @@ class StrategyConnectionService {
9627
9785
  * @param context - Execution context with strategyName, exchangeName, frameName
9628
9786
  * @returns Promise<boolean> - true if `partialProfit` would execute, false otherwise
9629
9787
  */
9630
- this.validatePartialProfit = (backtest, symbol, percentToClose, currentPrice, context) => {
9788
+ this.validatePartialProfit = async (backtest, symbol, percentToClose, currentPrice, context) => {
9631
9789
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9632
- return Promise.resolve(strategy.validatePartialProfit(symbol, percentToClose, currentPrice));
9790
+ return await strategy.validatePartialProfit(symbol, percentToClose, currentPrice);
9633
9791
  };
9634
9792
  /**
9635
9793
  * Executes partial close at profit level (moving toward TP).
@@ -9670,7 +9828,8 @@ class StrategyConnectionService {
9670
9828
  backtest,
9671
9829
  });
9672
9830
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9673
- return await strategy.partialProfit(symbol, percentToClose, currentPrice, backtest);
9831
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
9832
+ return await strategy.partialProfit(symbol, percentToClose, currentPrice, backtest, timestamp);
9674
9833
  };
9675
9834
  /**
9676
9835
  * Checks whether `partialLoss` would succeed without executing it.
@@ -9683,9 +9842,9 @@ class StrategyConnectionService {
9683
9842
  * @param context - Execution context with strategyName, exchangeName, frameName
9684
9843
  * @returns Promise<boolean> - true if `partialLoss` would execute, false otherwise
9685
9844
  */
9686
- this.validatePartialLoss = (backtest, symbol, percentToClose, currentPrice, context) => {
9845
+ this.validatePartialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
9687
9846
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9688
- return Promise.resolve(strategy.validatePartialLoss(symbol, percentToClose, currentPrice));
9847
+ return await strategy.validatePartialLoss(symbol, percentToClose, currentPrice);
9689
9848
  };
9690
9849
  /**
9691
9850
  * Executes partial close at loss level (moving toward SL).
@@ -9726,7 +9885,8 @@ class StrategyConnectionService {
9726
9885
  backtest,
9727
9886
  });
9728
9887
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9729
- return await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest);
9888
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
9889
+ return await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest, timestamp);
9730
9890
  };
9731
9891
  /**
9732
9892
  * Checks whether `trailingStop` would succeed without executing it.
@@ -9739,9 +9899,9 @@ class StrategyConnectionService {
9739
9899
  * @param context - Execution context with strategyName, exchangeName, frameName
9740
9900
  * @returns Promise<boolean> - true if `trailingStop` would execute, false otherwise
9741
9901
  */
9742
- this.validateTrailingStop = (backtest, symbol, percentShift, currentPrice, context) => {
9902
+ this.validateTrailingStop = async (backtest, symbol, percentShift, currentPrice, context) => {
9743
9903
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9744
- return Promise.resolve(strategy.validateTrailingStop(symbol, percentShift, currentPrice));
9904
+ return await strategy.validateTrailingStop(symbol, percentShift, currentPrice);
9745
9905
  };
9746
9906
  /**
9747
9907
  * Adjusts the trailing stop-loss distance for an active pending signal.
@@ -9793,9 +9953,9 @@ class StrategyConnectionService {
9793
9953
  * @param context - Execution context with strategyName, exchangeName, frameName
9794
9954
  * @returns Promise<boolean> - true if `trailingTake` would execute, false otherwise
9795
9955
  */
9796
- this.validateTrailingTake = (backtest, symbol, percentShift, currentPrice, context) => {
9956
+ this.validateTrailingTake = async (backtest, symbol, percentShift, currentPrice, context) => {
9797
9957
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9798
- return Promise.resolve(strategy.validateTrailingTake(symbol, percentShift, currentPrice));
9958
+ return await strategy.validateTrailingTake(symbol, percentShift, currentPrice);
9799
9959
  };
9800
9960
  /**
9801
9961
  * Adjusts the trailing take-profit distance for an active pending signal.
@@ -9846,9 +10006,9 @@ class StrategyConnectionService {
9846
10006
  * @param context - Execution context with strategyName, exchangeName, frameName
9847
10007
  * @returns Promise<boolean> - true if `breakeven` would execute, false otherwise
9848
10008
  */
9849
- this.validateBreakeven = (backtest, symbol, currentPrice, context) => {
10009
+ this.validateBreakeven = async (backtest, symbol, currentPrice, context) => {
9850
10010
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9851
- return Promise.resolve(strategy.validateBreakeven(symbol, currentPrice));
10011
+ return await strategy.validateBreakeven(symbol, currentPrice);
9852
10012
  };
9853
10013
  /**
9854
10014
  * Delegates to ClientStrategy.breakeven() with current execution context.
@@ -9925,9 +10085,9 @@ class StrategyConnectionService {
9925
10085
  * @param context - Execution context with strategyName, exchangeName, frameName
9926
10086
  * @returns Promise<boolean> - true if `averageBuy` would execute, false otherwise
9927
10087
  */
9928
- this.validateAverageBuy = (backtest, symbol, currentPrice, context) => {
10088
+ this.validateAverageBuy = async (backtest, symbol, currentPrice, context) => {
9929
10089
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9930
- return Promise.resolve(strategy.validateAverageBuy(symbol, currentPrice));
10090
+ return await strategy.validateAverageBuy(symbol, currentPrice);
9931
10091
  };
9932
10092
  /**
9933
10093
  * Adds a new DCA entry to the active pending signal.
@@ -9948,7 +10108,8 @@ class StrategyConnectionService {
9948
10108
  backtest,
9949
10109
  });
9950
10110
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9951
- return await strategy.averageBuy(symbol, currentPrice, backtest, cost);
10111
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
10112
+ return await strategy.averageBuy(symbol, currentPrice, backtest, timestamp, cost);
9952
10113
  };
9953
10114
  }
9954
10115
  }
@@ -10689,7 +10850,7 @@ class ClientRisk {
10689
10850
  * @param backtest - Whether running in backtest mode
10690
10851
  * @returns Unique string key for memoization
10691
10852
  */
10692
- const CREATE_KEY_FN$l = (riskName, exchangeName, frameName, backtest) => {
10853
+ const CREATE_KEY_FN$n = (riskName, exchangeName, frameName, backtest) => {
10693
10854
  const parts = [riskName, exchangeName];
10694
10855
  if (frameName)
10695
10856
  parts.push(frameName);
@@ -10788,7 +10949,7 @@ class RiskConnectionService {
10788
10949
  * @param backtest - True if backtest mode, false if live mode
10789
10950
  * @returns Configured ClientRisk instance
10790
10951
  */
10791
- this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$l(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
10952
+ this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$n(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
10792
10953
  const schema = this.riskSchemaService.get(riskName);
10793
10954
  return new ClientRisk({
10794
10955
  ...schema,
@@ -10856,7 +11017,7 @@ class RiskConnectionService {
10856
11017
  payload,
10857
11018
  });
10858
11019
  if (payload) {
10859
- const key = CREATE_KEY_FN$l(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
11020
+ const key = CREATE_KEY_FN$n(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
10860
11021
  this.getRisk.clear(key);
10861
11022
  }
10862
11023
  else {
@@ -12323,7 +12484,7 @@ class ClientAction {
12323
12484
  * @param backtest - Whether running in backtest mode
12324
12485
  * @returns Unique string key for memoization
12325
12486
  */
12326
- const CREATE_KEY_FN$k = (actionName, strategyName, exchangeName, frameName, backtest) => {
12487
+ const CREATE_KEY_FN$m = (actionName, strategyName, exchangeName, frameName, backtest) => {
12327
12488
  const parts = [actionName, strategyName, exchangeName];
12328
12489
  if (frameName)
12329
12490
  parts.push(frameName);
@@ -12374,7 +12535,7 @@ class ActionConnectionService {
12374
12535
  * @param backtest - True if backtest mode, false if live mode
12375
12536
  * @returns Configured ClientAction instance
12376
12537
  */
12377
- this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$k(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
12538
+ this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$m(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
12378
12539
  const schema = this.actionSchemaService.get(actionName);
12379
12540
  return new ClientAction({
12380
12541
  ...schema,
@@ -12584,7 +12745,7 @@ class ActionConnectionService {
12584
12745
  await Promise.all(actions.map(async (action) => await action.dispose()));
12585
12746
  return;
12586
12747
  }
12587
- const key = CREATE_KEY_FN$k(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
12748
+ const key = CREATE_KEY_FN$m(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
12588
12749
  if (!this.getAction.has(key)) {
12589
12750
  return;
12590
12751
  }
@@ -12602,7 +12763,7 @@ const METHOD_NAME_VALIDATE$2 = "exchangeCoreService validate";
12602
12763
  * @param exchangeName - Exchange name
12603
12764
  * @returns Unique string key for memoization
12604
12765
  */
12605
- const CREATE_KEY_FN$j = (exchangeName) => {
12766
+ const CREATE_KEY_FN$l = (exchangeName) => {
12606
12767
  return exchangeName;
12607
12768
  };
12608
12769
  /**
@@ -12626,7 +12787,7 @@ class ExchangeCoreService {
12626
12787
  * @param exchangeName - Name of the exchange to validate
12627
12788
  * @returns Promise that resolves when validation is complete
12628
12789
  */
12629
- this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$j(exchangeName), async (exchangeName) => {
12790
+ this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$l(exchangeName), async (exchangeName) => {
12630
12791
  this.loggerService.log(METHOD_NAME_VALIDATE$2, {
12631
12792
  exchangeName,
12632
12793
  });
@@ -12878,7 +13039,7 @@ const METHOD_NAME_VALIDATE$1 = "strategyCoreService validate";
12878
13039
  * @param context - Execution context with strategyName, exchangeName, frameName
12879
13040
  * @returns Unique string key for memoization
12880
13041
  */
12881
- const CREATE_KEY_FN$i = (context) => {
13042
+ const CREATE_KEY_FN$k = (context) => {
12882
13043
  const parts = [context.strategyName, context.exchangeName];
12883
13044
  if (context.frameName)
12884
13045
  parts.push(context.frameName);
@@ -12910,7 +13071,7 @@ class StrategyCoreService {
12910
13071
  * @param context - Execution context with strategyName, exchangeName, frameName
12911
13072
  * @returns Promise that resolves when validation is complete
12912
13073
  */
12913
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$i(context), async (context) => {
13074
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$k(context), async (context) => {
12914
13075
  this.loggerService.log(METHOD_NAME_VALIDATE$1, {
12915
13076
  context,
12916
13077
  });
@@ -13745,7 +13906,7 @@ class SizingGlobalService {
13745
13906
  * @param context - Context with riskName, exchangeName, frameName
13746
13907
  * @returns Unique string key for memoization
13747
13908
  */
13748
- const CREATE_KEY_FN$h = (context) => {
13909
+ const CREATE_KEY_FN$j = (context) => {
13749
13910
  const parts = [context.riskName, context.exchangeName];
13750
13911
  if (context.frameName)
13751
13912
  parts.push(context.frameName);
@@ -13771,7 +13932,7 @@ class RiskGlobalService {
13771
13932
  * @param payload - Payload with riskName, exchangeName and frameName
13772
13933
  * @returns Promise that resolves when validation is complete
13773
13934
  */
13774
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$h(context), async (context) => {
13935
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$j(context), async (context) => {
13775
13936
  this.loggerService.log("riskGlobalService validate", {
13776
13937
  context,
13777
13938
  });
@@ -13849,7 +14010,7 @@ const METHOD_NAME_VALIDATE = "actionCoreService validate";
13849
14010
  * @param context - Execution context with strategyName, exchangeName, frameName
13850
14011
  * @returns Unique string key for memoization
13851
14012
  */
13852
- const CREATE_KEY_FN$g = (context) => {
14013
+ const CREATE_KEY_FN$i = (context) => {
13853
14014
  const parts = [context.strategyName, context.exchangeName];
13854
14015
  if (context.frameName)
13855
14016
  parts.push(context.frameName);
@@ -13893,7 +14054,7 @@ class ActionCoreService {
13893
14054
  * @param context - Strategy execution context with strategyName, exchangeName and frameName
13894
14055
  * @returns Promise that resolves when all validations complete
13895
14056
  */
13896
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$g(context), async (context) => {
14057
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$i(context), async (context) => {
13897
14058
  this.loggerService.log(METHOD_NAME_VALIDATE, {
13898
14059
  context,
13899
14060
  });
@@ -18428,7 +18589,7 @@ const Markdown = new MarkdownAdapter();
18428
18589
  * @param backtest - Whether running in backtest mode
18429
18590
  * @returns Unique string key for memoization
18430
18591
  */
18431
- const CREATE_KEY_FN$f = (symbol, strategyName, exchangeName, frameName, backtest) => {
18592
+ const CREATE_KEY_FN$h = (symbol, strategyName, exchangeName, frameName, backtest) => {
18432
18593
  const parts = [symbol, strategyName, exchangeName];
18433
18594
  if (frameName)
18434
18595
  parts.push(frameName);
@@ -18667,7 +18828,7 @@ class BacktestMarkdownService {
18667
18828
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
18668
18829
  * Each combination gets its own isolated storage instance.
18669
18830
  */
18670
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$f(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$8(symbol, strategyName, exchangeName, frameName));
18831
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$h(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$8(symbol, strategyName, exchangeName, frameName));
18671
18832
  /**
18672
18833
  * Processes tick events and accumulates closed signals.
18673
18834
  * Should be called from IStrategyCallbacks.onTick.
@@ -18824,7 +18985,7 @@ class BacktestMarkdownService {
18824
18985
  payload,
18825
18986
  });
18826
18987
  if (payload) {
18827
- const key = CREATE_KEY_FN$f(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
18988
+ const key = CREATE_KEY_FN$h(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
18828
18989
  this.getStorage.clear(key);
18829
18990
  }
18830
18991
  else {
@@ -18886,7 +19047,7 @@ class BacktestMarkdownService {
18886
19047
  * @param backtest - Whether running in backtest mode
18887
19048
  * @returns Unique string key for memoization
18888
19049
  */
18889
- const CREATE_KEY_FN$e = (symbol, strategyName, exchangeName, frameName, backtest) => {
19050
+ const CREATE_KEY_FN$g = (symbol, strategyName, exchangeName, frameName, backtest) => {
18890
19051
  const parts = [symbol, strategyName, exchangeName];
18891
19052
  if (frameName)
18892
19053
  parts.push(frameName);
@@ -19369,7 +19530,7 @@ class LiveMarkdownService {
19369
19530
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
19370
19531
  * Each combination gets its own isolated storage instance.
19371
19532
  */
19372
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$e(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$7(symbol, strategyName, exchangeName, frameName));
19533
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$g(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$7(symbol, strategyName, exchangeName, frameName));
19373
19534
  /**
19374
19535
  * Subscribes to live signal emitter to receive tick events.
19375
19536
  * Protected against multiple subscriptions.
@@ -19587,7 +19748,7 @@ class LiveMarkdownService {
19587
19748
  payload,
19588
19749
  });
19589
19750
  if (payload) {
19590
- const key = CREATE_KEY_FN$e(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
19751
+ const key = CREATE_KEY_FN$g(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
19591
19752
  this.getStorage.clear(key);
19592
19753
  }
19593
19754
  else {
@@ -19607,7 +19768,7 @@ class LiveMarkdownService {
19607
19768
  * @param backtest - Whether running in backtest mode
19608
19769
  * @returns Unique string key for memoization
19609
19770
  */
19610
- const CREATE_KEY_FN$d = (symbol, strategyName, exchangeName, frameName, backtest) => {
19771
+ const CREATE_KEY_FN$f = (symbol, strategyName, exchangeName, frameName, backtest) => {
19611
19772
  const parts = [symbol, strategyName, exchangeName];
19612
19773
  if (frameName)
19613
19774
  parts.push(frameName);
@@ -19898,7 +20059,7 @@ class ScheduleMarkdownService {
19898
20059
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
19899
20060
  * Each combination gets its own isolated storage instance.
19900
20061
  */
19901
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$d(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$6(symbol, strategyName, exchangeName, frameName));
20062
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$f(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$6(symbol, strategyName, exchangeName, frameName));
19902
20063
  /**
19903
20064
  * Subscribes to signal emitter to receive scheduled signal events.
19904
20065
  * Protected against multiple subscriptions.
@@ -20101,7 +20262,7 @@ class ScheduleMarkdownService {
20101
20262
  payload,
20102
20263
  });
20103
20264
  if (payload) {
20104
- const key = CREATE_KEY_FN$d(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20265
+ const key = CREATE_KEY_FN$f(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20105
20266
  this.getStorage.clear(key);
20106
20267
  }
20107
20268
  else {
@@ -20121,7 +20282,7 @@ class ScheduleMarkdownService {
20121
20282
  * @param backtest - Whether running in backtest mode
20122
20283
  * @returns Unique string key for memoization
20123
20284
  */
20124
- const CREATE_KEY_FN$c = (symbol, strategyName, exchangeName, frameName, backtest) => {
20285
+ const CREATE_KEY_FN$e = (symbol, strategyName, exchangeName, frameName, backtest) => {
20125
20286
  const parts = [symbol, strategyName, exchangeName];
20126
20287
  if (frameName)
20127
20288
  parts.push(frameName);
@@ -20369,7 +20530,7 @@ class PerformanceMarkdownService {
20369
20530
  * Memoized function to get or create PerformanceStorage for a symbol-strategy-exchange-frame-backtest combination.
20370
20531
  * Each combination gets its own isolated storage instance.
20371
20532
  */
20372
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$c(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new PerformanceStorage(symbol, strategyName, exchangeName, frameName));
20533
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$e(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new PerformanceStorage(symbol, strategyName, exchangeName, frameName));
20373
20534
  /**
20374
20535
  * Subscribes to performance emitter to receive performance events.
20375
20536
  * Protected against multiple subscriptions.
@@ -20536,7 +20697,7 @@ class PerformanceMarkdownService {
20536
20697
  payload,
20537
20698
  });
20538
20699
  if (payload) {
20539
- const key = CREATE_KEY_FN$c(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20700
+ const key = CREATE_KEY_FN$e(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20540
20701
  this.getStorage.clear(key);
20541
20702
  }
20542
20703
  else {
@@ -21006,7 +21167,7 @@ class WalkerMarkdownService {
21006
21167
  * @param backtest - Whether running in backtest mode
21007
21168
  * @returns Unique string key for memoization
21008
21169
  */
21009
- const CREATE_KEY_FN$b = (exchangeName, frameName, backtest) => {
21170
+ const CREATE_KEY_FN$d = (exchangeName, frameName, backtest) => {
21010
21171
  const parts = [exchangeName];
21011
21172
  if (frameName)
21012
21173
  parts.push(frameName);
@@ -21373,7 +21534,7 @@ class HeatMarkdownService {
21373
21534
  * Memoized function to get or create HeatmapStorage for exchange, frame and backtest mode.
21374
21535
  * Each exchangeName + frameName + backtest mode combination gets its own isolated heatmap storage instance.
21375
21536
  */
21376
- this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$b(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
21537
+ this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$d(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
21377
21538
  /**
21378
21539
  * Subscribes to signal emitter to receive tick events.
21379
21540
  * Protected against multiple subscriptions.
@@ -21568,7 +21729,7 @@ class HeatMarkdownService {
21568
21729
  payload,
21569
21730
  });
21570
21731
  if (payload) {
21571
- const key = CREATE_KEY_FN$b(payload.exchangeName, payload.frameName, payload.backtest);
21732
+ const key = CREATE_KEY_FN$d(payload.exchangeName, payload.frameName, payload.backtest);
21572
21733
  this.getStorage.clear(key);
21573
21734
  }
21574
21735
  else {
@@ -22599,7 +22760,7 @@ class ClientPartial {
22599
22760
  * @param backtest - Whether running in backtest mode
22600
22761
  * @returns Unique string key for memoization
22601
22762
  */
22602
- const CREATE_KEY_FN$a = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
22763
+ const CREATE_KEY_FN$c = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
22603
22764
  /**
22604
22765
  * Creates a callback function for emitting profit events to partialProfitSubject.
22605
22766
  *
@@ -22721,7 +22882,7 @@ class PartialConnectionService {
22721
22882
  * Key format: "signalId:backtest" or "signalId:live"
22722
22883
  * Value: ClientPartial instance with logger and event emitters
22723
22884
  */
22724
- this.getPartial = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$a(signalId, backtest), (signalId, backtest) => {
22885
+ this.getPartial = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$c(signalId, backtest), (signalId, backtest) => {
22725
22886
  return new ClientPartial({
22726
22887
  signalId,
22727
22888
  logger: this.loggerService,
@@ -22811,7 +22972,7 @@ class PartialConnectionService {
22811
22972
  const partial = this.getPartial(data.id, backtest);
22812
22973
  await partial.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
22813
22974
  await partial.clear(symbol, data, priceClose, backtest);
22814
- const key = CREATE_KEY_FN$a(data.id, backtest);
22975
+ const key = CREATE_KEY_FN$c(data.id, backtest);
22815
22976
  this.getPartial.clear(key);
22816
22977
  };
22817
22978
  }
@@ -22827,7 +22988,7 @@ class PartialConnectionService {
22827
22988
  * @param backtest - Whether running in backtest mode
22828
22989
  * @returns Unique string key for memoization
22829
22990
  */
22830
- const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest) => {
22991
+ const CREATE_KEY_FN$b = (symbol, strategyName, exchangeName, frameName, backtest) => {
22831
22992
  const parts = [symbol, strategyName, exchangeName];
22832
22993
  if (frameName)
22833
22994
  parts.push(frameName);
@@ -23052,7 +23213,7 @@ class PartialMarkdownService {
23052
23213
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
23053
23214
  * Each combination gets its own isolated storage instance.
23054
23215
  */
23055
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$9(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$4(symbol, strategyName, exchangeName, frameName));
23216
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$b(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$4(symbol, strategyName, exchangeName, frameName));
23056
23217
  /**
23057
23218
  * Subscribes to partial profit/loss signal emitters to receive events.
23058
23219
  * Protected against multiple subscriptions.
@@ -23262,7 +23423,7 @@ class PartialMarkdownService {
23262
23423
  payload,
23263
23424
  });
23264
23425
  if (payload) {
23265
- const key = CREATE_KEY_FN$9(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
23426
+ const key = CREATE_KEY_FN$b(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
23266
23427
  this.getStorage.clear(key);
23267
23428
  }
23268
23429
  else {
@@ -23278,7 +23439,7 @@ class PartialMarkdownService {
23278
23439
  * @param context - Context with strategyName, exchangeName, frameName
23279
23440
  * @returns Unique string key for memoization
23280
23441
  */
23281
- const CREATE_KEY_FN$8 = (context) => {
23442
+ const CREATE_KEY_FN$a = (context) => {
23282
23443
  const parts = [context.strategyName, context.exchangeName];
23283
23444
  if (context.frameName)
23284
23445
  parts.push(context.frameName);
@@ -23352,7 +23513,7 @@ class PartialGlobalService {
23352
23513
  * @param context - Context with strategyName, exchangeName and frameName
23353
23514
  * @param methodName - Name of the calling method for error tracking
23354
23515
  */
23355
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$8(context), (context, methodName) => {
23516
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$a(context), (context, methodName) => {
23356
23517
  this.loggerService.log("partialGlobalService validate", {
23357
23518
  context,
23358
23519
  methodName,
@@ -23807,7 +23968,7 @@ class ClientBreakeven {
23807
23968
  * @param backtest - Whether running in backtest mode
23808
23969
  * @returns Unique string key for memoization
23809
23970
  */
23810
- const CREATE_KEY_FN$7 = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
23971
+ const CREATE_KEY_FN$9 = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
23811
23972
  /**
23812
23973
  * Creates a callback function for emitting breakeven events to breakevenSubject.
23813
23974
  *
@@ -23893,7 +24054,7 @@ class BreakevenConnectionService {
23893
24054
  * Key format: "signalId:backtest" or "signalId:live"
23894
24055
  * Value: ClientBreakeven instance with logger and event emitter
23895
24056
  */
23896
- this.getBreakeven = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$7(signalId, backtest), (signalId, backtest) => {
24057
+ this.getBreakeven = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$9(signalId, backtest), (signalId, backtest) => {
23897
24058
  return new ClientBreakeven({
23898
24059
  signalId,
23899
24060
  logger: this.loggerService,
@@ -23954,7 +24115,7 @@ class BreakevenConnectionService {
23954
24115
  const breakeven = this.getBreakeven(data.id, backtest);
23955
24116
  await breakeven.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
23956
24117
  await breakeven.clear(symbol, data, priceClose, backtest);
23957
- const key = CREATE_KEY_FN$7(data.id, backtest);
24118
+ const key = CREATE_KEY_FN$9(data.id, backtest);
23958
24119
  this.getBreakeven.clear(key);
23959
24120
  };
23960
24121
  }
@@ -23970,7 +24131,7 @@ class BreakevenConnectionService {
23970
24131
  * @param backtest - Whether running in backtest mode
23971
24132
  * @returns Unique string key for memoization
23972
24133
  */
23973
- const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest) => {
24134
+ const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest) => {
23974
24135
  const parts = [symbol, strategyName, exchangeName];
23975
24136
  if (frameName)
23976
24137
  parts.push(frameName);
@@ -24147,7 +24308,7 @@ class BreakevenMarkdownService {
24147
24308
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
24148
24309
  * Each combination gets its own isolated storage instance.
24149
24310
  */
24150
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$6(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$3(symbol, strategyName, exchangeName, frameName));
24311
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$8(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$3(symbol, strategyName, exchangeName, frameName));
24151
24312
  /**
24152
24313
  * Subscribes to breakeven signal emitter to receive events.
24153
24314
  * Protected against multiple subscriptions.
@@ -24336,7 +24497,7 @@ class BreakevenMarkdownService {
24336
24497
  payload,
24337
24498
  });
24338
24499
  if (payload) {
24339
- const key = CREATE_KEY_FN$6(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
24500
+ const key = CREATE_KEY_FN$8(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
24340
24501
  this.getStorage.clear(key);
24341
24502
  }
24342
24503
  else {
@@ -24352,7 +24513,7 @@ class BreakevenMarkdownService {
24352
24513
  * @param context - Context with strategyName, exchangeName, frameName
24353
24514
  * @returns Unique string key for memoization
24354
24515
  */
24355
- const CREATE_KEY_FN$5 = (context) => {
24516
+ const CREATE_KEY_FN$7 = (context) => {
24356
24517
  const parts = [context.strategyName, context.exchangeName];
24357
24518
  if (context.frameName)
24358
24519
  parts.push(context.frameName);
@@ -24426,7 +24587,7 @@ class BreakevenGlobalService {
24426
24587
  * @param context - Context with strategyName, exchangeName and frameName
24427
24588
  * @param methodName - Name of the calling method for error tracking
24428
24589
  */
24429
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$5(context), (context, methodName) => {
24590
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$7(context), (context, methodName) => {
24430
24591
  this.loggerService.log("breakevenGlobalService validate", {
24431
24592
  context,
24432
24593
  methodName,
@@ -24646,7 +24807,7 @@ class ConfigValidationService {
24646
24807
  * @param backtest - Whether running in backtest mode
24647
24808
  * @returns Unique string key for memoization
24648
24809
  */
24649
- const CREATE_KEY_FN$4 = (symbol, strategyName, exchangeName, frameName, backtest) => {
24810
+ const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest) => {
24650
24811
  const parts = [symbol, strategyName, exchangeName];
24651
24812
  if (frameName)
24652
24813
  parts.push(frameName);
@@ -24815,7 +24976,7 @@ class RiskMarkdownService {
24815
24976
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
24816
24977
  * Each combination gets its own isolated storage instance.
24817
24978
  */
24818
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$4(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$2(symbol, strategyName, exchangeName, frameName));
24979
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$6(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$2(symbol, strategyName, exchangeName, frameName));
24819
24980
  /**
24820
24981
  * Subscribes to risk rejection emitter to receive rejection events.
24821
24982
  * Protected against multiple subscriptions.
@@ -25004,7 +25165,7 @@ class RiskMarkdownService {
25004
25165
  payload,
25005
25166
  });
25006
25167
  if (payload) {
25007
- const key = CREATE_KEY_FN$4(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
25168
+ const key = CREATE_KEY_FN$6(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
25008
25169
  this.getStorage.clear(key);
25009
25170
  }
25010
25171
  else {
@@ -27692,7 +27853,7 @@ class SyncReportService {
27692
27853
  * @returns Colon-separated key string for memoization
27693
27854
  * @internal
27694
27855
  */
27695
- const CREATE_KEY_FN$3 = (symbol, strategyName, exchangeName, frameName, backtest) => {
27856
+ const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest) => {
27696
27857
  const parts = [symbol, strategyName, exchangeName];
27697
27858
  if (frameName)
27698
27859
  parts.push(frameName);
@@ -27940,7 +28101,7 @@ class StrategyMarkdownService {
27940
28101
  *
27941
28102
  * @internal
27942
28103
  */
27943
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$3(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$1(symbol, strategyName, exchangeName, frameName));
28104
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$5(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$1(symbol, strategyName, exchangeName, frameName));
27944
28105
  /**
27945
28106
  * Records a cancel-scheduled event when a scheduled signal is cancelled.
27946
28107
  *
@@ -28508,7 +28669,7 @@ class StrategyMarkdownService {
28508
28669
  this.clear = async (payload) => {
28509
28670
  this.loggerService.log("strategyMarkdownService clear", { payload });
28510
28671
  if (payload) {
28511
- const key = CREATE_KEY_FN$3(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
28672
+ const key = CREATE_KEY_FN$5(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
28512
28673
  this.getStorage.clear(key);
28513
28674
  }
28514
28675
  else {
@@ -28616,7 +28777,7 @@ class StrategyMarkdownService {
28616
28777
  * Creates a unique key for memoizing ReportStorage instances.
28617
28778
  * Key format: "symbol:strategyName:exchangeName[:frameName]:backtest|live"
28618
28779
  */
28619
- const CREATE_KEY_FN$2 = (symbol, strategyName, exchangeName, frameName, backtest) => {
28780
+ const CREATE_KEY_FN$4 = (symbol, strategyName, exchangeName, frameName, backtest) => {
28620
28781
  const parts = [symbol, strategyName, exchangeName];
28621
28782
  if (frameName)
28622
28783
  parts.push(frameName);
@@ -28748,7 +28909,7 @@ class ReportStorage {
28748
28909
  class SyncMarkdownService {
28749
28910
  constructor() {
28750
28911
  this.loggerService = inject(TYPES.loggerService);
28751
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$2(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage(symbol, strategyName, exchangeName, frameName));
28912
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$4(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage(symbol, strategyName, exchangeName, frameName));
28752
28913
  this.subscribe = functoolsKit.singleshot(() => {
28753
28914
  this.loggerService.log("syncMarkdownService init");
28754
28915
  const unsubscribe = syncSubject.subscribe(this.tick);
@@ -28823,7 +28984,7 @@ class SyncMarkdownService {
28823
28984
  this.clear = async (payload) => {
28824
28985
  this.loggerService.log("syncMarkdownService clear", { payload });
28825
28986
  if (payload) {
28826
- const key = CREATE_KEY_FN$2(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
28987
+ const key = CREATE_KEY_FN$4(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
28827
28988
  this.getStorage.clear(key);
28828
28989
  }
28829
28990
  else {
@@ -28833,6 +28994,275 @@ class SyncMarkdownService {
28833
28994
  }
28834
28995
  }
28835
28996
 
28997
+ const LISTEN_TIMEOUT$1 = 120000;
28998
+ /**
28999
+ * Creates a unique memoization key for a price stream.
29000
+ * Key format: "symbol:strategyName:exchangeName[:frameName]:backtest|live"
29001
+ *
29002
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29003
+ * @param strategyName - Strategy identifier
29004
+ * @param exchangeName - Exchange identifier
29005
+ * @param frameName - Frame identifier (omitted when empty)
29006
+ * @param backtest - Whether running in backtest mode
29007
+ * @returns Unique string key for memoization
29008
+ */
29009
+ const CREATE_KEY_FN$3 = (symbol, strategyName, exchangeName, frameName, backtest) => {
29010
+ const parts = [symbol, strategyName, exchangeName];
29011
+ if (frameName)
29012
+ parts.push(frameName);
29013
+ parts.push(backtest ? "backtest" : "live");
29014
+ return parts.join(":");
29015
+ };
29016
+ /**
29017
+ * Service for tracking the latest market price per symbol-strategy-exchange-frame combination.
29018
+ *
29019
+ * Maintains a memoized BehaviorSubject per unique key that is updated on every strategy tick
29020
+ * by StrategyConnectionService. Consumers can synchronously read the last known price or
29021
+ * await the first value if none has arrived yet.
29022
+ *
29023
+ * Primary use case: providing the current price outside of a tick execution context,
29024
+ * e.g., when a command is triggered between ticks.
29025
+ *
29026
+ * Features:
29027
+ * - One BehaviorSubject per (symbol, strategyName, exchangeName, frameName, backtest) key
29028
+ * - Falls back to ExchangeConnectionService.getAveragePrice when called inside an execution context
29029
+ * - Waits up to LISTEN_TIMEOUT ms for the first price if none is cached yet
29030
+ * - clear() disposes the BehaviorSubject for a single key or all keys
29031
+ *
29032
+ * Architecture:
29033
+ * - Registered as singleton in DI container
29034
+ * - Updated by StrategyConnectionService after each tick
29035
+ * - Cleared by Backtest/Live/Walker at strategy start to prevent stale data
29036
+ *
29037
+ * @example
29038
+ * ```typescript
29039
+ * const price = await backtest.priceMetaService.getCurrentPrice("BTCUSDT", context, false);
29040
+ * ```
29041
+ */
29042
+ class PriceMetaService {
29043
+ constructor() {
29044
+ this.loggerService = inject(TYPES.loggerService);
29045
+ this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
29046
+ /**
29047
+ * Memoized factory for BehaviorSubject streams keyed by (symbol, strategyName, exchangeName, frameName, backtest).
29048
+ *
29049
+ * Each subject holds the latest currentPrice emitted by the strategy iterator for that key.
29050
+ * Instances are cached until clear() is called.
29051
+ */
29052
+ this.getSource = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$3(symbol, strategyName, exchangeName, frameName, backtest), () => new functoolsKit.BehaviorSubject());
29053
+ /**
29054
+ * Returns the current market price for the given symbol and context.
29055
+ *
29056
+ * When called inside an execution context (i.e., during a signal handler or action),
29057
+ * delegates to ExchangeConnectionService.getAveragePrice for the live exchange price.
29058
+ * Otherwise, reads the last value from the cached BehaviorSubject. If no value has
29059
+ * been emitted yet, waits up to LISTEN_TIMEOUT ms for the first tick before throwing.
29060
+ *
29061
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29062
+ * @param context - Strategy, exchange, and frame identifiers
29063
+ * @param backtest - True if backtest mode, false if live mode
29064
+ * @returns Current market price in quote currency
29065
+ * @throws When no price arrives within LISTEN_TIMEOUT ms
29066
+ */
29067
+ this.getCurrentPrice = async (symbol, context, backtest) => {
29068
+ this.loggerService.log("priceMetaService getCurrentPrice", {
29069
+ symbol,
29070
+ context,
29071
+ backtest,
29072
+ });
29073
+ if (ExecutionContextService.hasContext() &&
29074
+ MethodContextService.hasContext()) {
29075
+ return await this.exchangeConnectionService.getAveragePrice(symbol);
29076
+ }
29077
+ const source = this.getSource(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
29078
+ if (source.data) {
29079
+ return source.data;
29080
+ }
29081
+ console.warn(`PriceMetaService: No currentPrice available for ${CREATE_KEY_FN$3(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}. Trying to fetch from strategy iterator as a fallback...`);
29082
+ const currentPrice = await functoolsKit.waitForNext(source, (data) => !!data, LISTEN_TIMEOUT$1);
29083
+ if (typeof currentPrice === "symbol") {
29084
+ throw new Error(`PriceMetaService: Timeout while waiting for currentPrice for ${CREATE_KEY_FN$3(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
29085
+ }
29086
+ return currentPrice;
29087
+ };
29088
+ /**
29089
+ * Pushes a new price value into the BehaviorSubject for the given key.
29090
+ *
29091
+ * Called by StrategyConnectionService after each strategy tick to keep
29092
+ * the cached price up to date.
29093
+ *
29094
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29095
+ * @param currentPrice - The latest price from the tick
29096
+ * @param context - Strategy, exchange, and frame identifiers
29097
+ * @param backtest - True if backtest mode, false if live mode
29098
+ */
29099
+ this.next = async (symbol, currentPrice, context, backtest) => {
29100
+ this.loggerService.log("priceMetaService next", {
29101
+ symbol,
29102
+ currentPrice,
29103
+ context,
29104
+ backtest,
29105
+ });
29106
+ const source = this.getSource(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
29107
+ source.next(currentPrice);
29108
+ };
29109
+ /**
29110
+ * Disposes cached BehaviorSubject(s) to free memory and prevent stale data.
29111
+ *
29112
+ * When called without arguments, clears all memoized price streams.
29113
+ * When called with a payload, clears only the stream for the specified key.
29114
+ * Should be called at strategy start (Backtest/Live/Walker) to reset state.
29115
+ *
29116
+ * @param payload - Optional key to clear a single stream; omit to clear all
29117
+ */
29118
+ this.clear = (payload) => {
29119
+ this.loggerService.log("priceMetaService clear", {
29120
+ payload
29121
+ });
29122
+ if (!payload) {
29123
+ this.getSource.clear();
29124
+ return;
29125
+ }
29126
+ const key = CREATE_KEY_FN$3(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
29127
+ this.getSource.clear(key);
29128
+ };
29129
+ }
29130
+ }
29131
+
29132
+ const LISTEN_TIMEOUT = 120000;
29133
+ /**
29134
+ * Creates a unique memoization key for a timestamp stream.
29135
+ * Key format: "symbol:strategyName:exchangeName[:frameName]:backtest|live"
29136
+ *
29137
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29138
+ * @param strategyName - Strategy identifier
29139
+ * @param exchangeName - Exchange identifier
29140
+ * @param frameName - Frame identifier (omitted when empty)
29141
+ * @param backtest - Whether running in backtest mode
29142
+ * @returns Unique string key for memoization
29143
+ */
29144
+ const CREATE_KEY_FN$2 = (symbol, strategyName, exchangeName, frameName, backtest) => {
29145
+ const parts = [symbol, strategyName, exchangeName];
29146
+ if (frameName)
29147
+ parts.push(frameName);
29148
+ parts.push(backtest ? "backtest" : "live");
29149
+ return parts.join(":");
29150
+ };
29151
+ /**
29152
+ * Service for tracking the latest candle timestamp per symbol-strategy-exchange-frame combination.
29153
+ *
29154
+ * Maintains a memoized BehaviorSubject per unique key that is updated on every strategy tick
29155
+ * by StrategyConnectionService. Consumers can synchronously read the last known timestamp or
29156
+ * await the first value if none has arrived yet.
29157
+ *
29158
+ * Primary use case: providing the current candle time outside of a tick execution context,
29159
+ * e.g., when a command is triggered between ticks.
29160
+ *
29161
+ * Features:
29162
+ * - One BehaviorSubject per (symbol, strategyName, exchangeName, frameName, backtest) key
29163
+ * - Falls back to ExecutionContextService.context.when when called inside an execution context
29164
+ * - Waits up to LISTEN_TIMEOUT ms for the first timestamp if none is cached yet
29165
+ * - clear() disposes the BehaviorSubject for a single key or all keys
29166
+ *
29167
+ * Architecture:
29168
+ * - Registered as singleton in DI container
29169
+ * - Updated by StrategyConnectionService after each tick
29170
+ * - Cleared by Backtest/Live/Walker at strategy start to prevent stale data
29171
+ *
29172
+ * @example
29173
+ * ```typescript
29174
+ * const ts = await backtest.timeMetaService.getTimestamp("BTCUSDT", context, false);
29175
+ * ```
29176
+ */
29177
+ class TimeMetaService {
29178
+ constructor() {
29179
+ this.loggerService = inject(TYPES.loggerService);
29180
+ this.executionContextService = inject(TYPES.executionContextService);
29181
+ /**
29182
+ * Memoized factory for BehaviorSubject streams keyed by (symbol, strategyName, exchangeName, frameName, backtest).
29183
+ *
29184
+ * Each subject holds the latest createdAt timestamp emitted by the strategy iterator for that key.
29185
+ * Instances are cached until clear() is called.
29186
+ */
29187
+ this.getSource = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$2(symbol, strategyName, exchangeName, frameName, backtest), () => new functoolsKit.BehaviorSubject());
29188
+ /**
29189
+ * Returns the current candle timestamp (in milliseconds) for the given symbol and context.
29190
+ *
29191
+ * When called inside an execution context (i.e., during a signal handler or action),
29192
+ * reads the timestamp directly from ExecutionContextService.context.when.
29193
+ * Otherwise, reads the last value from the cached BehaviorSubject. If no value has
29194
+ * been emitted yet, waits up to LISTEN_TIMEOUT ms for the first tick before throwing.
29195
+ *
29196
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29197
+ * @param context - Strategy, exchange, and frame identifiers
29198
+ * @param backtest - True if backtest mode, false if live mode
29199
+ * @returns Unix timestamp in milliseconds of the latest processed candle
29200
+ * @throws When no timestamp arrives within LISTEN_TIMEOUT ms
29201
+ */
29202
+ this.getTimestamp = async (symbol, context, backtest) => {
29203
+ this.loggerService.log("timeMetaService getTimestamp", {
29204
+ symbol,
29205
+ context,
29206
+ backtest,
29207
+ });
29208
+ if (ExecutionContextService.hasContext()) {
29209
+ return this.executionContextService.context.when.getTime();
29210
+ }
29211
+ const source = this.getSource(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
29212
+ if (source.data) {
29213
+ return source.data;
29214
+ }
29215
+ console.warn(`TimeMetaService: No timestamp available for ${CREATE_KEY_FN$2(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}. Trying to fetch from strategy iterator as a fallback...`);
29216
+ const timestamp = await functoolsKit.waitForNext(source, (data) => !!data, LISTEN_TIMEOUT);
29217
+ if (typeof timestamp === "symbol") {
29218
+ throw new Error(`TimeMetaService: Timeout while waiting for timestamp for ${CREATE_KEY_FN$2(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
29219
+ }
29220
+ return timestamp;
29221
+ };
29222
+ /**
29223
+ * Pushes a new timestamp value into the BehaviorSubject for the given key.
29224
+ *
29225
+ * Called by StrategyConnectionService after each strategy tick to keep
29226
+ * the cached timestamp up to date.
29227
+ *
29228
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29229
+ * @param timestamp - The createdAt timestamp from the tick (milliseconds)
29230
+ * @param context - Strategy, exchange, and frame identifiers
29231
+ * @param backtest - True if backtest mode, false if live mode
29232
+ */
29233
+ this.next = async (symbol, timestamp, context, backtest) => {
29234
+ this.loggerService.log("timeMetaService next", {
29235
+ symbol,
29236
+ timestamp,
29237
+ context,
29238
+ backtest,
29239
+ });
29240
+ const source = this.getSource(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
29241
+ source.next(timestamp);
29242
+ };
29243
+ /**
29244
+ * Disposes cached BehaviorSubject(s) to free memory and prevent stale data.
29245
+ *
29246
+ * When called without arguments, clears all memoized timestamp streams.
29247
+ * When called with a payload, clears only the stream for the specified key.
29248
+ * Should be called at strategy start (Backtest/Live/Walker) to reset state.
29249
+ *
29250
+ * @param payload - Optional key to clear a single stream; omit to clear all
29251
+ */
29252
+ this.clear = (payload) => {
29253
+ this.loggerService.log("timeMetaService clear", {
29254
+ payload,
29255
+ });
29256
+ if (!payload) {
29257
+ this.getSource.clear();
29258
+ return;
29259
+ }
29260
+ const key = CREATE_KEY_FN$2(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
29261
+ this.getSource.clear(key);
29262
+ };
29263
+ }
29264
+ }
29265
+
28836
29266
  {
28837
29267
  provide(TYPES.loggerService, () => new LoggerService());
28838
29268
  }
@@ -28865,6 +29295,10 @@ class SyncMarkdownService {
28865
29295
  provide(TYPES.actionCoreService, () => new ActionCoreService());
28866
29296
  provide(TYPES.frameCoreService, () => new FrameCoreService());
28867
29297
  }
29298
+ {
29299
+ provide(TYPES.priceMetaService, () => new PriceMetaService());
29300
+ provide(TYPES.timeMetaService, () => new TimeMetaService());
29301
+ }
28868
29302
  {
28869
29303
  provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
28870
29304
  provide(TYPES.riskGlobalService, () => new RiskGlobalService());
@@ -28956,6 +29390,10 @@ const coreServices = {
28956
29390
  actionCoreService: inject(TYPES.actionCoreService),
28957
29391
  frameCoreService: inject(TYPES.frameCoreService),
28958
29392
  };
29393
+ const metaServices = {
29394
+ timeMetaService: inject(TYPES.timeMetaService),
29395
+ priceMetaService: inject(TYPES.priceMetaService),
29396
+ };
28959
29397
  const globalServices = {
28960
29398
  sizingGlobalService: inject(TYPES.sizingGlobalService),
28961
29399
  riskGlobalService: inject(TYPES.riskGlobalService),
@@ -29020,6 +29458,7 @@ const backtest = {
29020
29458
  ...connectionServices,
29021
29459
  ...schemaServices,
29022
29460
  ...coreServices,
29461
+ ...metaServices,
29023
29462
  ...globalServices,
29024
29463
  ...commandServices,
29025
29464
  ...logicPrivateServices,
@@ -34506,6 +34945,20 @@ class BacktestInstance {
34506
34945
  frameName: context.frameName,
34507
34946
  backtest: true,
34508
34947
  });
34948
+ bt.timeMetaService.clear({
34949
+ symbol,
34950
+ strategyName: context.strategyName,
34951
+ exchangeName: context.exchangeName,
34952
+ frameName: context.frameName,
34953
+ backtest: true,
34954
+ });
34955
+ bt.priceMetaService.clear({
34956
+ symbol,
34957
+ strategyName: context.strategyName,
34958
+ exchangeName: context.exchangeName,
34959
+ frameName: context.frameName,
34960
+ backtest: true,
34961
+ });
34509
34962
  }
34510
34963
  {
34511
34964
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
@@ -36410,6 +36863,20 @@ class LiveInstance {
36410
36863
  frameName: "",
36411
36864
  backtest: false,
36412
36865
  });
36866
+ bt.timeMetaService.clear({
36867
+ symbol,
36868
+ strategyName: context.strategyName,
36869
+ exchangeName: context.exchangeName,
36870
+ frameName: "",
36871
+ backtest: false,
36872
+ });
36873
+ bt.priceMetaService.clear({
36874
+ symbol,
36875
+ strategyName: context.strategyName,
36876
+ exchangeName: context.exchangeName,
36877
+ frameName: "",
36878
+ backtest: false,
36879
+ });
36413
36880
  }
36414
36881
  {
36415
36882
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
@@ -40558,6 +41025,20 @@ class WalkerInstance {
40558
41025
  frameName: walkerSchema.frameName,
40559
41026
  backtest: true,
40560
41027
  });
41028
+ bt.timeMetaService.clear({
41029
+ symbol,
41030
+ strategyName,
41031
+ exchangeName: walkerSchema.exchangeName,
41032
+ frameName: walkerSchema.frameName,
41033
+ backtest: true,
41034
+ });
41035
+ bt.priceMetaService.clear({
41036
+ symbol,
41037
+ strategyName,
41038
+ exchangeName: walkerSchema.exchangeName,
41039
+ frameName: walkerSchema.frameName,
41040
+ backtest: true,
41041
+ });
40561
41042
  }
40562
41043
  {
40563
41044
  const { riskName, riskList, actions } = bt.strategySchemaService.get(strategyName);
@@ -43525,13 +44006,15 @@ class NotificationMemoryBacktestUtils {
43525
44006
  * Handles signal sync events (signal-open, signal-close).
43526
44007
  * @param data - The signal sync contract data
43527
44008
  */
43528
- this.handleSync = async (data) => {
44009
+ this.handleSync = functoolsKit.trycatch(async (data) => {
43529
44010
  bt.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_SYNC, {
43530
44011
  signalId: data.signalId,
43531
44012
  action: data.action,
43532
44013
  });
43533
44014
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
43534
- };
44015
+ }, {
44016
+ defaultValue: null,
44017
+ });
43535
44018
  /**
43536
44019
  * Handles risk rejection event.
43537
44020
  * @param data - The risk contract data
@@ -43640,8 +44123,10 @@ class NotificationDummyBacktestUtils {
43640
44123
  /**
43641
44124
  * No-op handler for signal sync event.
43642
44125
  */
43643
- this.handleSync = async () => {
43644
- };
44126
+ this.handleSync = functoolsKit.trycatch(async () => {
44127
+ }, {
44128
+ defaultValue: null,
44129
+ });
43645
44130
  /**
43646
44131
  * No-op handler for risk rejection event.
43647
44132
  */
@@ -43780,7 +44265,7 @@ class NotificationPersistBacktestUtils {
43780
44265
  * Handles signal sync events (signal-open, signal-close).
43781
44266
  * @param data - The signal sync contract data
43782
44267
  */
43783
- this.handleSync = async (data) => {
44268
+ this.handleSync = functoolsKit.trycatch(async (data) => {
43784
44269
  bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_SYNC, {
43785
44270
  signalId: data.signalId,
43786
44271
  action: data.action,
@@ -43788,7 +44273,9 @@ class NotificationPersistBacktestUtils {
43788
44273
  await this.waitForInit();
43789
44274
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
43790
44275
  await this._updateNotifications();
43791
- };
44276
+ }, {
44277
+ defaultValue: null,
44278
+ });
43792
44279
  /**
43793
44280
  * Handles risk rejection event.
43794
44281
  * @param data - The risk contract data
@@ -43971,13 +44458,15 @@ class NotificationMemoryLiveUtils {
43971
44458
  * Handles signal sync events (signal-open, signal-close).
43972
44459
  * @param data - The signal sync contract data
43973
44460
  */
43974
- this.handleSync = async (data) => {
44461
+ this.handleSync = functoolsKit.trycatch(async (data) => {
43975
44462
  bt.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_SYNC, {
43976
44463
  signalId: data.signalId,
43977
44464
  action: data.action,
43978
44465
  });
43979
44466
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
43980
- };
44467
+ }, {
44468
+ defaultValue: null,
44469
+ });
43981
44470
  /**
43982
44471
  * Handles risk rejection event.
43983
44472
  * @param data - The risk contract data
@@ -44086,8 +44575,10 @@ class NotificationDummyLiveUtils {
44086
44575
  /**
44087
44576
  * No-op handler for signal sync event.
44088
44577
  */
44089
- this.handleSync = async () => {
44090
- };
44578
+ this.handleSync = functoolsKit.trycatch(async () => {
44579
+ }, {
44580
+ defaultValue: null,
44581
+ });
44091
44582
  /**
44092
44583
  * No-op handler for risk rejection event.
44093
44584
  */
@@ -44227,7 +44718,7 @@ class NotificationPersistLiveUtils {
44227
44718
  * Handles signal sync events (signal-open, signal-close).
44228
44719
  * @param data - The signal sync contract data
44229
44720
  */
44230
- this.handleSync = async (data) => {
44721
+ this.handleSync = functoolsKit.trycatch(async (data) => {
44231
44722
  bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_SYNC, {
44232
44723
  signalId: data.signalId,
44233
44724
  action: data.action,
@@ -44235,7 +44726,9 @@ class NotificationPersistLiveUtils {
44235
44726
  await this.waitForInit();
44236
44727
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
44237
44728
  await this._updateNotifications();
44238
- };
44729
+ }, {
44730
+ defaultValue: null,
44731
+ });
44239
44732
  /**
44240
44733
  * Handles risk rejection event.
44241
44734
  * @param data - The risk contract data
@@ -44397,9 +44890,11 @@ class NotificationBacktestAdapter {
44397
44890
  * Proxies call to the underlying notification adapter.
44398
44891
  * @param data - The signal sync contract data
44399
44892
  */
44400
- this.handleSync = async (data) => {
44893
+ this.handleSync = functoolsKit.trycatch(async (data) => {
44401
44894
  return await this._notificationBacktestUtils.handleSync(data);
44402
- };
44895
+ }, {
44896
+ defaultValue: null,
44897
+ });
44403
44898
  /**
44404
44899
  * Handles risk rejection event.
44405
44900
  * Proxies call to the underlying notification adapter.
@@ -44541,9 +45036,11 @@ class NotificationLiveAdapter {
44541
45036
  * Proxies call to the underlying notification adapter.
44542
45037
  * @param data - The signal sync contract data
44543
45038
  */
44544
- this.handleSync = async (data) => {
45039
+ this.handleSync = functoolsKit.trycatch(async (data) => {
44545
45040
  return await this._notificationLiveUtils.handleSync(data);
44546
- };
45041
+ }, {
45042
+ defaultValue: null,
45043
+ });
44547
45044
  /**
44548
45045
  * Handles risk rejection event.
44549
45046
  * Proxies call to the underlying notification adapter.