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.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createActivator } from 'di-kit';
2
2
  import { scoped } from 'di-scoped';
3
- import { Subject, makeExtendable, singleshot, getErrorMessage, memoize, not, errorData, trycatch, retry, queued, sleep, randomString, str, isObject, ToolRegistry, typo, and, Source, resolveDocuments, timeout, TIMEOUT_SYMBOL as TIMEOUT_SYMBOL$1, compose, singlerun } from 'functools-kit';
3
+ import { Subject, makeExtendable, singleshot, getErrorMessage, memoize, not, errorData, trycatch, retry, queued, sleep, randomString, str, isObject, ToolRegistry, typo, and, Source, resolveDocuments, timeout, TIMEOUT_SYMBOL as TIMEOUT_SYMBOL$1, compose, BehaviorSubject, waitForNext, singlerun } from 'functools-kit';
4
4
  import * as fs from 'fs/promises';
5
5
  import fs__default, { stat, opendir, readFile } from 'fs/promises';
6
6
  import path, { join, dirname } from 'path';
@@ -69,6 +69,10 @@ const coreServices$1 = {
69
69
  actionCoreService: Symbol('actionCoreService'),
70
70
  frameCoreService: Symbol('frameCoreService'),
71
71
  };
72
+ const metaServices$1 = {
73
+ priceMetaService: Symbol('priceMetaService'),
74
+ timeMetaService: Symbol('timeMetaService'),
75
+ };
72
76
  const globalServices$1 = {
73
77
  sizingGlobalService: Symbol('sizingGlobalService'),
74
78
  riskGlobalService: Symbol('riskGlobalService'),
@@ -134,6 +138,7 @@ const TYPES = {
134
138
  ...connectionServices$1,
135
139
  ...schemaServices$1,
136
140
  ...coreServices$1,
141
+ ...metaServices$1,
137
142
  ...globalServices$1,
138
143
  ...commandServices$1,
139
144
  ...logicPrivateServices$1,
@@ -3538,19 +3543,6 @@ const beginTime = (run) => (...args) => {
3538
3543
  return fn();
3539
3544
  };
3540
3545
 
3541
- /**
3542
- * Retrieves the current timestamp for debugging purposes.
3543
- * 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.
3544
- * Can be empty (undefined) if not called from strategy async context, as it's intended for debugging and not critical for logic.
3545
- * @return {number | undefined} The current timestamp in milliseconds from the execution context, or undefined if not available.
3546
- */
3547
- const getDebugTimestamp = () => {
3548
- if (ExecutionContextService.hasContext()) {
3549
- return bt.executionContextService.context.when.getTime();
3550
- }
3551
- return undefined;
3552
- };
3553
-
3554
3546
  const INTERVAL_MINUTES$6 = {
3555
3547
  "1m": 1,
3556
3548
  "3m": 3,
@@ -4258,7 +4250,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
4258
4250
  pendingAt: currentTime, // Для immediate signal оба времени одинаковые
4259
4251
  timestamp: currentTime,
4260
4252
  _isScheduled: false,
4261
- _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, debugTimestamp: currentTime }],
4253
+ _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
4262
4254
  };
4263
4255
  // Валидируем сигнал перед возвратом
4264
4256
  VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
@@ -4282,7 +4274,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
4282
4274
  pendingAt: SCHEDULED_SIGNAL_PENDING_MOCK, // Временно, обновится при активации
4283
4275
  timestamp: currentTime,
4284
4276
  _isScheduled: true,
4285
- _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, debugTimestamp: currentTime }],
4277
+ _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
4286
4278
  };
4287
4279
  // Валидируем сигнал перед возвратом
4288
4280
  VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
@@ -4302,7 +4294,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
4302
4294
  pendingAt: currentTime, // Для immediate signal оба времени одинаковые
4303
4295
  timestamp: currentTime,
4304
4296
  _isScheduled: false,
4305
- _entry: [{ price: currentPrice, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, debugTimestamp: currentTime }],
4297
+ _entry: [{ price: currentPrice, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
4306
4298
  };
4307
4299
  // Валидируем сигнал перед возвратом
4308
4300
  VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
@@ -4372,7 +4364,7 @@ const WAIT_FOR_DISPOSE_FN$1 = async (self) => {
4372
4364
  self.params.logger.debug("ClientStrategy dispose");
4373
4365
  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);
4374
4366
  };
4375
- const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
4367
+ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice, timestamp) => {
4376
4368
  // Initialize partial array if not present
4377
4369
  if (!signal._partial)
4378
4370
  signal._partial = [];
@@ -4401,7 +4393,7 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
4401
4393
  entryCountAtClose,
4402
4394
  currentPrice,
4403
4395
  costBasisAtClose: remainingCostBasis,
4404
- debugTimestamp: getDebugTimestamp(),
4396
+ timestamp,
4405
4397
  });
4406
4398
  self.params.logger.info("PARTIAL_PROFIT_FN executed", {
4407
4399
  signalId: signal.id,
@@ -4411,7 +4403,7 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
4411
4403
  });
4412
4404
  return true;
4413
4405
  };
4414
- const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
4406
+ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice, timestamp) => {
4415
4407
  // Initialize partial array if not present
4416
4408
  if (!signal._partial)
4417
4409
  signal._partial = [];
@@ -4439,7 +4431,7 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
4439
4431
  currentPrice,
4440
4432
  entryCountAtClose,
4441
4433
  costBasisAtClose: remainingCostBasis,
4442
- debugTimestamp: getDebugTimestamp(),
4434
+ timestamp,
4443
4435
  });
4444
4436
  self.params.logger.warn("PARTIAL_LOSS_FN executed", {
4445
4437
  signalId: signal.id,
@@ -4836,10 +4828,10 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4836
4828
  });
4837
4829
  return true;
4838
4830
  };
4839
- const AVERAGE_BUY_FN = (self, signal, currentPrice, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_COST) => {
4831
+ const AVERAGE_BUY_FN = (self, signal, currentPrice, timestamp, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_COST) => {
4840
4832
  // Ensure _entry is initialized (handles signals loaded from disk without _entry)
4841
4833
  if (!signal._entry || signal._entry.length === 0) {
4842
- signal._entry = [{ price: signal.priceOpen, cost: GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, debugTimestamp: getDebugTimestamp() }];
4834
+ signal._entry = [{ price: signal.priceOpen, cost: GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp }];
4843
4835
  }
4844
4836
  if (signal.position === "long") {
4845
4837
  // LONG: new entry must beat the all-time low — strictly below every prior entry price
@@ -4869,7 +4861,7 @@ const AVERAGE_BUY_FN = (self, signal, currentPrice, cost = GLOBAL_CONFIG.CC_POSI
4869
4861
  return false;
4870
4862
  }
4871
4863
  }
4872
- signal._entry.push({ price: currentPrice, cost, debugTimestamp: getDebugTimestamp() });
4864
+ signal._entry.push({ price: currentPrice, cost, timestamp });
4873
4865
  self.params.logger.info("AVERAGE_BUY_FN executed", {
4874
4866
  signalId: signal.id,
4875
4867
  position: signal.position,
@@ -5052,7 +5044,7 @@ const CALL_SCHEDULE_PING_CALLBACKS_FN = trycatch(beginTime(async (self, symbol,
5052
5044
  await ExecutionContextService.runInContext(async () => {
5053
5045
  const publicSignal = TO_PUBLIC_SIGNAL(scheduled, currentPrice);
5054
5046
  // Call system onSchedulePing callback first (emits to pingSubject)
5055
- 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);
5047
+ 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);
5056
5048
  // Call user onSchedulePing callback only if signal is still active (not cancelled, not activated)
5057
5049
  if (self.params.callbacks?.onSchedulePing) {
5058
5050
  await self.params.callbacks.onSchedulePing(self.params.execution.context.symbol, publicSignal, new Date(timestamp), self.params.execution.context.backtest);
@@ -5078,7 +5070,7 @@ const CALL_ACTIVE_PING_CALLBACKS_FN = trycatch(beginTime(async (self, symbol, pe
5078
5070
  await ExecutionContextService.runInContext(async () => {
5079
5071
  const publicSignal = TO_PUBLIC_SIGNAL(pending, currentPrice);
5080
5072
  // Call system onActivePing callback first (emits to activePingSubject)
5081
- 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);
5073
+ 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);
5082
5074
  // Call user onActivePing callback only if signal is still active (not closed)
5083
5075
  if (self.params.callbacks?.onActivePing) {
5084
5076
  await self.params.callbacks.onActivePing(self.params.execution.context.symbol, publicSignal, new Date(timestamp), self.params.execution.context.backtest);
@@ -5876,6 +5868,57 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
5876
5868
  await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
5877
5869
  return result;
5878
5870
  };
5871
+ const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, averagePrice, closeTimestamp) => {
5872
+ const syncCloseAllowed = await CALL_SIGNAL_SYNC_CLOSE_FN(closeTimestamp, averagePrice, "closed", closedSignal, self);
5873
+ if (!syncCloseAllowed) {
5874
+ self.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry", {
5875
+ symbol: self.params.execution.context.symbol,
5876
+ signalId: closedSignal.id,
5877
+ });
5878
+ self._closedSignal = null;
5879
+ self._pendingSignal = closedSignal;
5880
+ throw new Error(`ClientStrategy backtest: signal close rejected by sync (signalId=${closedSignal.id}). ` +
5881
+ `Retry backtest() with new candle data.`);
5882
+ }
5883
+ self._closedSignal = null;
5884
+ await CALL_COMMIT_FN(self, {
5885
+ action: "close-pending",
5886
+ symbol: self.params.execution.context.symbol,
5887
+ strategyName: self.params.strategyName,
5888
+ exchangeName: self.params.exchangeName,
5889
+ frameName: self.params.frameName,
5890
+ signalId: closedSignal.id,
5891
+ backtest: true,
5892
+ closeId: closedSignal.closeId,
5893
+ timestamp: closeTimestamp,
5894
+ totalEntries: closedSignal._entry?.length ?? 1,
5895
+ totalPartials: closedSignal._partial?.length ?? 0,
5896
+ originalPriceOpen: closedSignal.priceOpen,
5897
+ pnl: toProfitLossDto(closedSignal, averagePrice),
5898
+ });
5899
+ await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
5900
+ await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
5901
+ await CALL_BREAKEVEN_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
5902
+ await CALL_RISK_REMOVE_SIGNAL_FN(self, self.params.execution.context.symbol, closeTimestamp, self.params.execution.context.backtest);
5903
+ const pnl = toProfitLossDto(closedSignal, averagePrice);
5904
+ const result = {
5905
+ action: "closed",
5906
+ signal: TO_PUBLIC_SIGNAL(closedSignal, averagePrice),
5907
+ currentPrice: averagePrice,
5908
+ closeReason: "closed",
5909
+ closeTimestamp,
5910
+ pnl,
5911
+ strategyName: self.params.method.context.strategyName,
5912
+ exchangeName: self.params.method.context.exchangeName,
5913
+ frameName: self.params.method.context.frameName,
5914
+ symbol: self.params.execution.context.symbol,
5915
+ backtest: self.params.execution.context.backtest,
5916
+ closeId: closedSignal.closeId,
5917
+ createdAt: closeTimestamp,
5918
+ };
5919
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
5920
+ return result;
5921
+ };
5879
5922
  const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
5880
5923
  const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
5881
5924
  const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
@@ -5893,7 +5936,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
5893
5936
  if (self._cancelledSignal) {
5894
5937
  // Сигнал был отменен через cancel() в onSchedulePing
5895
5938
  const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "user");
5896
- return { activated: false, cancelled: true, activationIndex: i, result };
5939
+ return { outcome: "cancelled", result };
5897
5940
  }
5898
5941
  // КРИТИЧНО: Проверяем был ли сигнал активирован пользователем через activateScheduled()
5899
5942
  // Обрабатываем inline (как в tick()) с риск-проверкой по averagePrice
@@ -5907,7 +5950,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
5907
5950
  signalId: activatedSignal.id,
5908
5951
  });
5909
5952
  await self.setScheduledSignal(null);
5910
- return { activated: false, cancelled: false, activationIndex: i, result: null };
5953
+ return { outcome: "pending" };
5911
5954
  }
5912
5955
  // Риск-проверка по averagePrice (симметрия с LIVE tick())
5913
5956
  if (await not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, activatedSignal, averagePrice, candle.timestamp, self.params.execution.context.backtest))) {
@@ -5916,7 +5959,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
5916
5959
  signalId: activatedSignal.id,
5917
5960
  });
5918
5961
  await self.setScheduledSignal(null);
5919
- return { activated: false, cancelled: false, activationIndex: i, result: null };
5962
+ return { outcome: "pending" };
5920
5963
  }
5921
5964
  const pendingSignal = {
5922
5965
  ...activatedSignal,
@@ -5945,7 +5988,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
5945
5988
  originalPriceOpen: activatedSignal.priceOpen,
5946
5989
  pnl: toProfitLossDto(activatedSignal, averagePrice),
5947
5990
  });
5948
- return { activated: false, cancelled: true, activationIndex: i, result: null };
5991
+ return { outcome: "pending" };
5949
5992
  }
5950
5993
  await self.setScheduledSignal(null);
5951
5994
  await self.setPendingSignal(pendingSignal);
@@ -5978,18 +6021,13 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
5978
6021
  });
5979
6022
  await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, pendingSignal.priceOpen, candle.timestamp, self.params.execution.context.backtest);
5980
6023
  await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, pendingSignal, candle.timestamp, self.params.execution.context.backtest);
5981
- return {
5982
- activated: true,
5983
- cancelled: false,
5984
- activationIndex: i,
5985
- result: null,
5986
- };
6024
+ return { outcome: "activated", activationIndex: i };
5987
6025
  }
5988
6026
  // КРИТИЧНО: Проверяем timeout ПЕРЕД проверкой цены
5989
6027
  const elapsedTime = candle.timestamp - scheduled.scheduledAt;
5990
6028
  if (elapsedTime >= maxTimeToWait) {
5991
6029
  const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "timeout");
5992
- return { activated: false, cancelled: true, activationIndex: i, result };
6030
+ return { outcome: "cancelled", result };
5993
6031
  }
5994
6032
  let shouldActivate = false;
5995
6033
  let shouldCancel = false;
@@ -6027,27 +6065,17 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
6027
6065
  }
6028
6066
  if (shouldCancel) {
6029
6067
  const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "price_reject");
6030
- return { activated: false, cancelled: true, activationIndex: i, result };
6068
+ return { outcome: "cancelled", result };
6031
6069
  }
6032
6070
  if (shouldActivate) {
6033
6071
  await ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, candle.timestamp);
6034
- return {
6035
- activated: true,
6036
- cancelled: false,
6037
- activationIndex: i,
6038
- result: null,
6039
- };
6072
+ return { outcome: "activated", activationIndex: i };
6040
6073
  }
6041
6074
  await CALL_SCHEDULE_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, candle.timestamp, true, averagePrice);
6042
6075
  // Process queued commit events with candle timestamp
6043
6076
  await PROCESS_COMMIT_QUEUE_FN(self, averagePrice, candle.timestamp);
6044
6077
  }
6045
- return {
6046
- activated: false,
6047
- cancelled: false,
6048
- activationIndex: -1,
6049
- result: null,
6050
- };
6078
+ return { outcome: "pending" };
6051
6079
  };
6052
6080
  const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
6053
6081
  const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
@@ -6066,6 +6094,10 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
6066
6094
  const startIndex = Math.max(0, i - (candlesCount - 1));
6067
6095
  const recentCandles = candles.slice(startIndex, i + 1);
6068
6096
  const averagePrice = GET_AVG_PRICE_FN(recentCandles);
6097
+ // КРИТИЧНО: Проверяем был ли сигнал закрыт пользователем через closePending()
6098
+ if (self._closedSignal) {
6099
+ return await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
6100
+ }
6069
6101
  await CALL_ACTIVE_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentCandleTimestamp, true, averagePrice);
6070
6102
  let shouldClose = false;
6071
6103
  let closeReason;
@@ -6170,7 +6202,32 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
6170
6202
  // Process queued commit events with candle timestamp
6171
6203
  await PROCESS_COMMIT_QUEUE_FN(self, averagePrice, currentCandleTimestamp);
6172
6204
  }
6173
- return null;
6205
+ // Loop exhausted without closing — check if we have enough data
6206
+ const lastCandles = candles.slice(-GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
6207
+ const lastPrice = GET_AVG_PRICE_FN(lastCandles);
6208
+ const closeTimestamp = lastCandles[lastCandles.length - 1].timestamp;
6209
+ const signalTime = signal.pendingAt;
6210
+ const maxTimeToWait = signal.minuteEstimatedTime * 60 * 1000;
6211
+ const elapsedTime = closeTimestamp - signalTime;
6212
+ if (elapsedTime < maxTimeToWait) {
6213
+ const bufferCandlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
6214
+ const requiredCandlesCount = signal.minuteEstimatedTime + bufferCandlesCount + 1;
6215
+ throw new Error(str.newline(`ClientStrategy backtest: Insufficient candle data for pending signal. ` +
6216
+ `Signal opened at ${new Date(signal.pendingAt).toISOString()}, ` +
6217
+ `last candle at ${new Date(closeTimestamp).toISOString()}. ` +
6218
+ `Elapsed: ${Math.floor(elapsedTime / 60000)}min of ${signal.minuteEstimatedTime}min required. ` +
6219
+ `Provided ${candles.length} candles, but need at least ${requiredCandlesCount} candles. ` +
6220
+ `\nBreakdown: ${signal.minuteEstimatedTime} candles for signal lifetime + ${bufferCandlesCount} buffer candles. ` +
6221
+ `\nBuffer explanation: VWAP calculation requires ${GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT} candles, ` +
6222
+ `so first ${bufferCandlesCount} candles are skipped to ensure accurate price averaging. ` +
6223
+ `Provide complete candle range: [pendingAt - ${bufferCandlesCount}min, pendingAt + ${signal.minuteEstimatedTime}min].`));
6224
+ }
6225
+ const timeExpiredResult = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, lastPrice, "time_expired", closeTimestamp);
6226
+ if (!timeExpiredResult) {
6227
+ throw new Error(`ClientStrategy backtest: time_expired close rejected by sync (signalId=${signal.id}). ` +
6228
+ `Retry backtest() with new candle data.`);
6229
+ }
6230
+ return timeExpiredResult;
6174
6231
  };
6175
6232
  /**
6176
6233
  * Client implementation for trading strategy lifecycle management.
@@ -6654,16 +6711,16 @@ class ClientStrategy {
6654
6711
  * // No DCA: [{ price: 43000, cost: 100 }]
6655
6712
  * // One DCA: [{ price: 43000, cost: 100 }, { price: 42000, cost: 100 }]
6656
6713
  */
6657
- async getPositionEntries(symbol) {
6714
+ async getPositionEntries(symbol, timestamp) {
6658
6715
  this.params.logger.debug("ClientStrategy getPositionEntries", { symbol });
6659
6716
  if (!this._pendingSignal) {
6660
6717
  return null;
6661
6718
  }
6662
6719
  const entries = this._pendingSignal._entry;
6663
6720
  if (!entries || entries.length === 0) {
6664
- return [{ price: this._pendingSignal.priceOpen, cost: GLOBAL_CONFIG.CC_POSITION_ENTRY_COST }];
6721
+ return [{ price: this._pendingSignal.priceOpen, cost: GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp }];
6665
6722
  }
6666
- return entries.map(({ price, cost }) => ({ price, cost }));
6723
+ return entries.map(({ price, cost, timestamp }) => ({ price, cost, timestamp }));
6667
6724
  }
6668
6725
  /**
6669
6726
  * Performs a single tick of strategy execution.
@@ -7117,11 +7174,12 @@ class ClientStrategy {
7117
7174
  priceOpen: scheduled.priceOpen,
7118
7175
  position: scheduled.position,
7119
7176
  });
7120
- const { activated, cancelled, activationIndex, result } = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles);
7121
- if (cancelled && result) {
7122
- return result;
7177
+ const scheduledResult = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles);
7178
+ if (scheduledResult.outcome === "cancelled") {
7179
+ return scheduledResult.result;
7123
7180
  }
7124
- if (activated) {
7181
+ if (scheduledResult.outcome === "activated") {
7182
+ const { activationIndex } = scheduledResult;
7125
7183
  // КРИТИЧНО: activationIndex - индекс свечи активации в массиве candles
7126
7184
  // BacktestLogicPrivateService включил буфер в начало массива, поэтому перед activationIndex достаточно свечей
7127
7185
  // PROCESS_PENDING_SIGNAL_CANDLES_FN пропустит первые bufferCandlesCount свечей для VWAP
@@ -7193,40 +7251,7 @@ class ClientStrategy {
7193
7251
  if (candles.length < candlesCount) {
7194
7252
  this.params.logger.warn(`ClientStrategy backtest: Expected at least ${candlesCount} candles for VWAP, got ${candles.length}`);
7195
7253
  }
7196
- const closedResult = await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles);
7197
- if (closedResult) {
7198
- return closedResult;
7199
- }
7200
- // Signal didn't close during candle processing - check if we have enough data
7201
- const lastCandles = candles.slice(-GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
7202
- const lastPrice = GET_AVG_PRICE_FN(lastCandles);
7203
- const closeTimestamp = lastCandles[lastCandles.length - 1].timestamp;
7204
- const signalTime = signal.pendingAt;
7205
- const maxTimeToWait = signal.minuteEstimatedTime * 60 * 1000;
7206
- const elapsedTime = closeTimestamp - signalTime;
7207
- // Check if we actually reached time expiration or just ran out of candles
7208
- if (elapsedTime < maxTimeToWait) {
7209
- // EDGE CASE: backtest() called with insufficient candle data
7210
- const bufferCandlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
7211
- const requiredCandlesCount = signal.minuteEstimatedTime + bufferCandlesCount + 1;
7212
- throw new Error(str.newline(`ClientStrategy backtest: Insufficient candle data for pending signal. ` +
7213
- `Signal opened at ${new Date(signal.pendingAt).toISOString()}, ` +
7214
- `last candle at ${new Date(closeTimestamp).toISOString()}. ` +
7215
- `Elapsed: ${Math.floor(elapsedTime / 60000)}min of ${signal.minuteEstimatedTime}min required. ` +
7216
- `Provided ${candles.length} candles, but need at least ${requiredCandlesCount} candles. ` +
7217
- `\nBreakdown: ${signal.minuteEstimatedTime} candles for signal lifetime + ${bufferCandlesCount} buffer candles. ` +
7218
- `\nBuffer explanation: VWAP calculation requires ${GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT} candles, ` +
7219
- `so first ${bufferCandlesCount} candles are skipped to ensure accurate price averaging. ` +
7220
- `Provide complete candle range: [pendingAt - ${bufferCandlesCount}min, pendingAt + ${signal.minuteEstimatedTime}min].`));
7221
- }
7222
- // Time actually expired - close with time_expired
7223
- const timeExpiredResult = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(this, signal, lastPrice, "time_expired", closeTimestamp);
7224
- if (!timeExpiredResult) {
7225
- // Sync rejected the close — signal remains in _pendingSignal, caller must retry
7226
- throw new Error(`ClientStrategy backtest: time_expired close rejected by sync (signalId=${signal.id}). ` +
7227
- `Retry backtest() with new candle data.`);
7228
- }
7229
- return timeExpiredResult;
7254
+ return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles);
7230
7255
  }
7231
7256
  /**
7232
7257
  * Stops the strategy from generating new signals.
@@ -7499,7 +7524,7 @@ class ClientStrategy {
7499
7524
  * // success3 = false (skipped, would exceed 100%)
7500
7525
  * ```
7501
7526
  */
7502
- async partialProfit(symbol, percentToClose, currentPrice, backtest) {
7527
+ async partialProfit(symbol, percentToClose, currentPrice, backtest, timestamp) {
7503
7528
  this.params.logger.debug("ClientStrategy partialProfit", {
7504
7529
  symbol,
7505
7530
  percentToClose,
@@ -7563,7 +7588,7 @@ class ClientStrategy {
7563
7588
  return false;
7564
7589
  }
7565
7590
  // Execute partial close logic
7566
- const wasExecuted = PARTIAL_PROFIT_FN(this, this._pendingSignal, percentToClose, currentPrice);
7591
+ const wasExecuted = PARTIAL_PROFIT_FN(this, this._pendingSignal, percentToClose, currentPrice, timestamp);
7567
7592
  // If partial was not executed (exceeded 100%), return false without persistence
7568
7593
  if (!wasExecuted) {
7569
7594
  return false;
@@ -7682,7 +7707,7 @@ class ClientStrategy {
7682
7707
  * // success3 = false (skipped, would exceed 100%)
7683
7708
  * ```
7684
7709
  */
7685
- async partialLoss(symbol, percentToClose, currentPrice, backtest) {
7710
+ async partialLoss(symbol, percentToClose, currentPrice, backtest, timestamp) {
7686
7711
  this.params.logger.debug("ClientStrategy partialLoss", {
7687
7712
  symbol,
7688
7713
  percentToClose,
@@ -7746,7 +7771,7 @@ class ClientStrategy {
7746
7771
  return false;
7747
7772
  }
7748
7773
  // Execute partial close logic
7749
- const wasExecuted = PARTIAL_LOSS_FN(this, this._pendingSignal, percentToClose, currentPrice);
7774
+ const wasExecuted = PARTIAL_LOSS_FN(this, this._pendingSignal, percentToClose, currentPrice, timestamp);
7750
7775
  // If partial was not executed (exceeded 100%), return false without persistence
7751
7776
  if (!wasExecuted) {
7752
7777
  return false;
@@ -8502,7 +8527,7 @@ class ClientStrategy {
8502
8527
  * @param backtest - Whether running in backtest mode
8503
8528
  * @returns Promise<boolean> - true if entry added, false if rejected by direction check
8504
8529
  */
8505
- async averageBuy(symbol, currentPrice, backtest, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_COST) {
8530
+ async averageBuy(symbol, currentPrice, backtest, timestamp, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_COST) {
8506
8531
  this.params.logger.debug("ClientStrategy averageBuy", {
8507
8532
  symbol,
8508
8533
  currentPrice,
@@ -8517,7 +8542,7 @@ class ClientStrategy {
8517
8542
  throw new Error(`ClientStrategy averageBuy: currentPrice must be a positive finite number, got ${currentPrice}`);
8518
8543
  }
8519
8544
  // Execute averaging logic
8520
- const result = AVERAGE_BUY_FN(this, this._pendingSignal, currentPrice, cost);
8545
+ const result = AVERAGE_BUY_FN(this, this._pendingSignal, currentPrice, timestamp, cost);
8521
8546
  if (!result) {
8522
8547
  return false;
8523
8548
  }
@@ -8981,7 +9006,7 @@ const GET_RISK_FN = (dto, backtest, exchangeName, frameName, self) => {
8981
9006
  * @param backtest - Whether running in backtest mode
8982
9007
  * @returns Unique string key for memoization
8983
9008
  */
8984
- const CREATE_KEY_FN$m = (symbol, strategyName, exchangeName, frameName, backtest) => {
9009
+ const CREATE_KEY_FN$o = (symbol, strategyName, exchangeName, frameName, backtest) => {
8985
9010
  const parts = [symbol, strategyName, exchangeName];
8986
9011
  if (frameName)
8987
9012
  parts.push(frameName);
@@ -8997,11 +9022,12 @@ const CREATE_KEY_FN$m = (symbol, strategyName, exchangeName, frameName, backtest
8997
9022
  * @param self - Reference to StrategyConnectionService instance
8998
9023
  * @returns Callback function for schedule ping events
8999
9024
  */
9000
- const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => trycatch(async (symbol, strategyName, exchangeName, data, backtest, timestamp) => {
9025
+ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => trycatch(async (symbol, strategyName, exchangeName, data, currentPrice, backtest, timestamp) => {
9001
9026
  const event = {
9002
9027
  symbol,
9003
9028
  strategyName,
9004
9029
  exchangeName,
9030
+ currentPrice,
9005
9031
  data,
9006
9032
  backtest,
9007
9033
  timestamp,
@@ -9030,11 +9056,12 @@ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => trycatch(async (symbol, strateg
9030
9056
  * @param self - Reference to StrategyConnectionService instance
9031
9057
  * @returns Callback function for active ping events
9032
9058
  */
9033
- const CREATE_COMMIT_ACTIVE_PING_FN = (self) => trycatch(async (symbol, strategyName, exchangeName, data, backtest, timestamp) => {
9059
+ const CREATE_COMMIT_ACTIVE_PING_FN = (self) => trycatch(async (symbol, strategyName, exchangeName, data, currentPrice, backtest, timestamp) => {
9034
9060
  const event = {
9035
9061
  symbol,
9036
9062
  strategyName,
9037
9063
  exchangeName,
9064
+ currentPrice,
9038
9065
  data,
9039
9066
  backtest,
9040
9067
  timestamp,
@@ -9158,6 +9185,8 @@ class StrategyConnectionService {
9158
9185
  this.partialConnectionService = inject(TYPES.partialConnectionService);
9159
9186
  this.breakevenConnectionService = inject(TYPES.breakevenConnectionService);
9160
9187
  this.actionCoreService = inject(TYPES.actionCoreService);
9188
+ this.timeMetaService = inject(TYPES.timeMetaService);
9189
+ this.priceMetaService = inject(TYPES.priceMetaService);
9161
9190
  /**
9162
9191
  * Retrieves memoized ClientStrategy instance for given symbol-strategy pair with exchange and frame isolation.
9163
9192
  *
@@ -9171,7 +9200,7 @@ class StrategyConnectionService {
9171
9200
  * @param backtest - Whether running in backtest mode
9172
9201
  * @returns Configured ClientStrategy instance
9173
9202
  */
9174
- this.getStrategy = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$m(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
9203
+ this.getStrategy = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$o(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
9175
9204
  const { riskName = "", riskList = [], getSignal, interval, callbacks, } = this.strategySchemaService.get(strategyName);
9176
9205
  return new ClientStrategy({
9177
9206
  symbol,
@@ -9258,6 +9287,20 @@ class StrategyConnectionService {
9258
9287
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9259
9288
  return await strategy.getTotalCostClosed(symbol);
9260
9289
  };
9290
+ /**
9291
+ * Returns the effective (DCA-averaged) entry price for the current pending signal.
9292
+ *
9293
+ * This is the harmonic mean of all _entry prices, which is the correct
9294
+ * cost-basis price used in all PNL calculations.
9295
+ * With no DCA entries, equals the original priceOpen.
9296
+ *
9297
+ * Returns null if no pending signal exists.
9298
+ *
9299
+ * @param backtest - Whether running in backtest mode
9300
+ * @param symbol - Trading pair symbol
9301
+ * @param context - Execution context with strategyName, exchangeName, frameName
9302
+ * @returns Promise resolving to effective entry price or null
9303
+ */
9261
9304
  this.getPositionAveragePrice = async (backtest, symbol, context) => {
9262
9305
  this.loggerService.log("strategyConnectionService getPositionAveragePrice", {
9263
9306
  symbol,
@@ -9267,6 +9310,19 @@ class StrategyConnectionService {
9267
9310
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9268
9311
  return await strategy.getPositionAveragePrice(symbol);
9269
9312
  };
9313
+ /**
9314
+ * Returns the number of DCA entries made for the current pending signal.
9315
+ *
9316
+ * 1 = original entry only (no DCA).
9317
+ * Increases by 1 with each successful commitAverageBuy().
9318
+ *
9319
+ * Returns null if no pending signal exists.
9320
+ *
9321
+ * @param backtest - Whether running in backtest mode
9322
+ * @param symbol - Trading pair symbol
9323
+ * @param context - Execution context with strategyName, exchangeName, frameName
9324
+ * @returns Promise resolving to entry count or null
9325
+ */
9270
9326
  this.getPositionInvestedCount = async (backtest, symbol, context) => {
9271
9327
  this.loggerService.log("strategyConnectionService getPositionInvestedCount", {
9272
9328
  symbol,
@@ -9276,6 +9332,19 @@ class StrategyConnectionService {
9276
9332
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9277
9333
  return await strategy.getPositionInvestedCount(symbol);
9278
9334
  };
9335
+ /**
9336
+ * Returns the total invested cost basis in dollars for the current pending signal.
9337
+ *
9338
+ * Equal to entryCount × $100 (COST_BASIS_PER_ENTRY).
9339
+ * 1 entry = $100, 2 entries = $200, etc.
9340
+ *
9341
+ * Returns null if no pending signal exists.
9342
+ *
9343
+ * @param backtest - Whether running in backtest mode
9344
+ * @param symbol - Trading pair symbol
9345
+ * @param context - Execution context with strategyName, exchangeName, frameName
9346
+ * @returns Promise resolving to total invested cost in dollars or null
9347
+ */
9279
9348
  this.getPositionInvestedCost = async (backtest, symbol, context) => {
9280
9349
  this.loggerService.log("strategyConnectionService getPositionInvestedCost", {
9281
9350
  symbol,
@@ -9285,6 +9354,20 @@ class StrategyConnectionService {
9285
9354
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9286
9355
  return await strategy.getPositionInvestedCost(symbol);
9287
9356
  };
9357
+ /**
9358
+ * Returns the unrealized PNL percentage for the current pending signal at currentPrice.
9359
+ *
9360
+ * Accounts for partial closes, DCA entries, slippage and fees
9361
+ * (delegates to toProfitLossDto).
9362
+ *
9363
+ * Returns null if no pending signal exists.
9364
+ *
9365
+ * @param backtest - Whether running in backtest mode
9366
+ * @param symbol - Trading pair symbol
9367
+ * @param currentPrice - Current market price
9368
+ * @param context - Execution context with strategyName, exchangeName, frameName
9369
+ * @returns Promise resolving to pnlPercentage or null
9370
+ */
9288
9371
  this.getPositionPnlPercent = async (backtest, symbol, currentPrice, context) => {
9289
9372
  this.loggerService.log("strategyConnectionService getPositionPnlPercent", {
9290
9373
  symbol,
@@ -9295,6 +9378,20 @@ class StrategyConnectionService {
9295
9378
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9296
9379
  return await strategy.getPositionPnlPercent(symbol, currentPrice);
9297
9380
  };
9381
+ /**
9382
+ * Returns the unrealized PNL in dollars for the current pending signal at currentPrice.
9383
+ *
9384
+ * Calculated as: pnlPercentage / 100 × totalInvestedCost
9385
+ * Accounts for partial closes, DCA entries, slippage and fees.
9386
+ *
9387
+ * Returns null if no pending signal exists.
9388
+ *
9389
+ * @param backtest - Whether running in backtest mode
9390
+ * @param symbol - Trading pair symbol
9391
+ * @param currentPrice - Current market price
9392
+ * @param context - Execution context with strategyName, exchangeName, frameName
9393
+ * @returns Promise resolving to pnl in dollars or null
9394
+ */
9298
9395
  this.getPositionPnlCost = async (backtest, symbol, currentPrice, context) => {
9299
9396
  this.loggerService.log("strategyConnectionService getPositionPnlCost", {
9300
9397
  symbol,
@@ -9305,6 +9402,27 @@ class StrategyConnectionService {
9305
9402
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9306
9403
  return await strategy.getPositionPnlCost(symbol, currentPrice);
9307
9404
  };
9405
+ /**
9406
+ * Returns the list of DCA entry prices for the current pending signal.
9407
+ *
9408
+ * The first element is always the original priceOpen (initial entry).
9409
+ * Each subsequent element is a price added by commitAverageBuy().
9410
+ *
9411
+ * Returns null if no pending signal exists.
9412
+ * Returns a single-element array [priceOpen] if no DCA entries were made.
9413
+ *
9414
+ * @param backtest - Whether running in backtest mode
9415
+ * @param symbol - Trading pair symbol
9416
+ * @param context - Execution context with strategyName, exchangeName, frameName
9417
+ * @returns Promise resolving to array of entry prices or null
9418
+ *
9419
+ * @example
9420
+ * ```typescript
9421
+ * // No DCA: [43000]
9422
+ * // One DCA: [43000, 42000]
9423
+ * // Two DCA: [43000, 42000, 41500]
9424
+ * ```
9425
+ */
9308
9426
  this.getPositionLevels = async (backtest, symbol, context) => {
9309
9427
  this.loggerService.log("strategyConnectionService getPositionLevels", {
9310
9428
  symbol,
@@ -9314,6 +9432,20 @@ class StrategyConnectionService {
9314
9432
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9315
9433
  return await strategy.getPositionLevels(symbol);
9316
9434
  };
9435
+ /**
9436
+ * Returns the list of partial closes for the current pending signal.
9437
+ *
9438
+ * Each entry records a partial profit or loss close event with its type,
9439
+ * percent closed, price at close, cost basis snapshot, and entry count at close.
9440
+ *
9441
+ * Returns null if no pending signal exists.
9442
+ * Returns an empty array if no partial closes have been executed.
9443
+ *
9444
+ * @param backtest - Whether running in backtest mode
9445
+ * @param symbol - Trading pair symbol
9446
+ * @param context - Execution context with strategyName, exchangeName, frameName
9447
+ * @returns Promise resolving to array of partial close records or null
9448
+ */
9317
9449
  this.getPositionPartials = async (backtest, symbol, context) => {
9318
9450
  this.loggerService.log("strategyConnectionService getPositionPartials", {
9319
9451
  symbol,
@@ -9323,6 +9455,27 @@ class StrategyConnectionService {
9323
9455
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9324
9456
  return await strategy.getPositionPartials(symbol);
9325
9457
  };
9458
+ /**
9459
+ * Returns the list of DCA entry prices and costs for the current pending signal.
9460
+ *
9461
+ * Each entry records the price and cost of a single position entry.
9462
+ * The first element is always the original priceOpen (initial entry).
9463
+ * Each subsequent element is an entry added by averageBuy().
9464
+ *
9465
+ * Returns null if no pending signal exists.
9466
+ * Returns a single-element array [{ price: priceOpen, cost }] if no DCA entries were made.
9467
+ *
9468
+ * @param backtest - Whether running in backtest mode
9469
+ * @param symbol - Trading pair symbol
9470
+ * @param context - Execution context with strategyName, exchangeName, frameName
9471
+ * @returns Promise resolving to array of entry records or null
9472
+ *
9473
+ * @example
9474
+ * ```typescript
9475
+ * // No DCA: [{ price: 43000, cost: 100 }]
9476
+ * // One DCA: [{ price: 43000, cost: 100 }, { price: 42000, cost: 100 }]
9477
+ * ```
9478
+ */
9326
9479
  this.getPositionEntries = async (backtest, symbol, context) => {
9327
9480
  this.loggerService.log("strategyConnectionService getPositionEntries", {
9328
9481
  symbol,
@@ -9330,7 +9483,8 @@ class StrategyConnectionService {
9330
9483
  backtest,
9331
9484
  });
9332
9485
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9333
- return await strategy.getPositionEntries(symbol);
9486
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
9487
+ return await strategy.getPositionEntries(symbol, timestamp);
9334
9488
  };
9335
9489
  /**
9336
9490
  * Retrieves the currently active scheduled signal for the strategy.
@@ -9432,6 +9586,10 @@ class StrategyConnectionService {
9432
9586
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9433
9587
  await strategy.waitForInit();
9434
9588
  const tick = await strategy.tick(symbol, context.strategyName);
9589
+ {
9590
+ this.priceMetaService.next(symbol, tick.currentPrice, context, backtest);
9591
+ this.timeMetaService.next(symbol, tick.createdAt, context, backtest);
9592
+ }
9435
9593
  {
9436
9594
  await CALL_SIGNAL_EMIT_FN(this, tick, context, backtest, symbol);
9437
9595
  }
@@ -9538,7 +9696,7 @@ class StrategyConnectionService {
9538
9696
  }
9539
9697
  return;
9540
9698
  }
9541
- const key = CREATE_KEY_FN$m(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
9699
+ const key = CREATE_KEY_FN$o(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
9542
9700
  if (!this.getStrategy.has(key)) {
9543
9701
  return;
9544
9702
  }
@@ -9607,9 +9765,9 @@ class StrategyConnectionService {
9607
9765
  * @param context - Execution context with strategyName, exchangeName, frameName
9608
9766
  * @returns Promise<boolean> - true if `partialProfit` would execute, false otherwise
9609
9767
  */
9610
- this.validatePartialProfit = (backtest, symbol, percentToClose, currentPrice, context) => {
9768
+ this.validatePartialProfit = async (backtest, symbol, percentToClose, currentPrice, context) => {
9611
9769
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9612
- return Promise.resolve(strategy.validatePartialProfit(symbol, percentToClose, currentPrice));
9770
+ return await strategy.validatePartialProfit(symbol, percentToClose, currentPrice);
9613
9771
  };
9614
9772
  /**
9615
9773
  * Executes partial close at profit level (moving toward TP).
@@ -9650,7 +9808,8 @@ class StrategyConnectionService {
9650
9808
  backtest,
9651
9809
  });
9652
9810
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9653
- return await strategy.partialProfit(symbol, percentToClose, currentPrice, backtest);
9811
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
9812
+ return await strategy.partialProfit(symbol, percentToClose, currentPrice, backtest, timestamp);
9654
9813
  };
9655
9814
  /**
9656
9815
  * Checks whether `partialLoss` would succeed without executing it.
@@ -9663,9 +9822,9 @@ class StrategyConnectionService {
9663
9822
  * @param context - Execution context with strategyName, exchangeName, frameName
9664
9823
  * @returns Promise<boolean> - true if `partialLoss` would execute, false otherwise
9665
9824
  */
9666
- this.validatePartialLoss = (backtest, symbol, percentToClose, currentPrice, context) => {
9825
+ this.validatePartialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
9667
9826
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9668
- return Promise.resolve(strategy.validatePartialLoss(symbol, percentToClose, currentPrice));
9827
+ return await strategy.validatePartialLoss(symbol, percentToClose, currentPrice);
9669
9828
  };
9670
9829
  /**
9671
9830
  * Executes partial close at loss level (moving toward SL).
@@ -9706,7 +9865,8 @@ class StrategyConnectionService {
9706
9865
  backtest,
9707
9866
  });
9708
9867
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9709
- return await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest);
9868
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
9869
+ return await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest, timestamp);
9710
9870
  };
9711
9871
  /**
9712
9872
  * Checks whether `trailingStop` would succeed without executing it.
@@ -9719,9 +9879,9 @@ class StrategyConnectionService {
9719
9879
  * @param context - Execution context with strategyName, exchangeName, frameName
9720
9880
  * @returns Promise<boolean> - true if `trailingStop` would execute, false otherwise
9721
9881
  */
9722
- this.validateTrailingStop = (backtest, symbol, percentShift, currentPrice, context) => {
9882
+ this.validateTrailingStop = async (backtest, symbol, percentShift, currentPrice, context) => {
9723
9883
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9724
- return Promise.resolve(strategy.validateTrailingStop(symbol, percentShift, currentPrice));
9884
+ return await strategy.validateTrailingStop(symbol, percentShift, currentPrice);
9725
9885
  };
9726
9886
  /**
9727
9887
  * Adjusts the trailing stop-loss distance for an active pending signal.
@@ -9773,9 +9933,9 @@ class StrategyConnectionService {
9773
9933
  * @param context - Execution context with strategyName, exchangeName, frameName
9774
9934
  * @returns Promise<boolean> - true if `trailingTake` would execute, false otherwise
9775
9935
  */
9776
- this.validateTrailingTake = (backtest, symbol, percentShift, currentPrice, context) => {
9936
+ this.validateTrailingTake = async (backtest, symbol, percentShift, currentPrice, context) => {
9777
9937
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9778
- return Promise.resolve(strategy.validateTrailingTake(symbol, percentShift, currentPrice));
9938
+ return await strategy.validateTrailingTake(symbol, percentShift, currentPrice);
9779
9939
  };
9780
9940
  /**
9781
9941
  * Adjusts the trailing take-profit distance for an active pending signal.
@@ -9826,9 +9986,9 @@ class StrategyConnectionService {
9826
9986
  * @param context - Execution context with strategyName, exchangeName, frameName
9827
9987
  * @returns Promise<boolean> - true if `breakeven` would execute, false otherwise
9828
9988
  */
9829
- this.validateBreakeven = (backtest, symbol, currentPrice, context) => {
9989
+ this.validateBreakeven = async (backtest, symbol, currentPrice, context) => {
9830
9990
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9831
- return Promise.resolve(strategy.validateBreakeven(symbol, currentPrice));
9991
+ return await strategy.validateBreakeven(symbol, currentPrice);
9832
9992
  };
9833
9993
  /**
9834
9994
  * Delegates to ClientStrategy.breakeven() with current execution context.
@@ -9905,9 +10065,9 @@ class StrategyConnectionService {
9905
10065
  * @param context - Execution context with strategyName, exchangeName, frameName
9906
10066
  * @returns Promise<boolean> - true if `averageBuy` would execute, false otherwise
9907
10067
  */
9908
- this.validateAverageBuy = (backtest, symbol, currentPrice, context) => {
10068
+ this.validateAverageBuy = async (backtest, symbol, currentPrice, context) => {
9909
10069
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9910
- return Promise.resolve(strategy.validateAverageBuy(symbol, currentPrice));
10070
+ return await strategy.validateAverageBuy(symbol, currentPrice);
9911
10071
  };
9912
10072
  /**
9913
10073
  * Adds a new DCA entry to the active pending signal.
@@ -9928,7 +10088,8 @@ class StrategyConnectionService {
9928
10088
  backtest,
9929
10089
  });
9930
10090
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9931
- return await strategy.averageBuy(symbol, currentPrice, backtest, cost);
10091
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
10092
+ return await strategy.averageBuy(symbol, currentPrice, backtest, timestamp, cost);
9932
10093
  };
9933
10094
  }
9934
10095
  }
@@ -10669,7 +10830,7 @@ class ClientRisk {
10669
10830
  * @param backtest - Whether running in backtest mode
10670
10831
  * @returns Unique string key for memoization
10671
10832
  */
10672
- const CREATE_KEY_FN$l = (riskName, exchangeName, frameName, backtest) => {
10833
+ const CREATE_KEY_FN$n = (riskName, exchangeName, frameName, backtest) => {
10673
10834
  const parts = [riskName, exchangeName];
10674
10835
  if (frameName)
10675
10836
  parts.push(frameName);
@@ -10768,7 +10929,7 @@ class RiskConnectionService {
10768
10929
  * @param backtest - True if backtest mode, false if live mode
10769
10930
  * @returns Configured ClientRisk instance
10770
10931
  */
10771
- this.getRisk = memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$l(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
10932
+ this.getRisk = memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$n(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
10772
10933
  const schema = this.riskSchemaService.get(riskName);
10773
10934
  return new ClientRisk({
10774
10935
  ...schema,
@@ -10836,7 +10997,7 @@ class RiskConnectionService {
10836
10997
  payload,
10837
10998
  });
10838
10999
  if (payload) {
10839
- const key = CREATE_KEY_FN$l(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
11000
+ const key = CREATE_KEY_FN$n(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
10840
11001
  this.getRisk.clear(key);
10841
11002
  }
10842
11003
  else {
@@ -12303,7 +12464,7 @@ class ClientAction {
12303
12464
  * @param backtest - Whether running in backtest mode
12304
12465
  * @returns Unique string key for memoization
12305
12466
  */
12306
- const CREATE_KEY_FN$k = (actionName, strategyName, exchangeName, frameName, backtest) => {
12467
+ const CREATE_KEY_FN$m = (actionName, strategyName, exchangeName, frameName, backtest) => {
12307
12468
  const parts = [actionName, strategyName, exchangeName];
12308
12469
  if (frameName)
12309
12470
  parts.push(frameName);
@@ -12354,7 +12515,7 @@ class ActionConnectionService {
12354
12515
  * @param backtest - True if backtest mode, false if live mode
12355
12516
  * @returns Configured ClientAction instance
12356
12517
  */
12357
- this.getAction = memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$k(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
12518
+ this.getAction = memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$m(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
12358
12519
  const schema = this.actionSchemaService.get(actionName);
12359
12520
  return new ClientAction({
12360
12521
  ...schema,
@@ -12564,7 +12725,7 @@ class ActionConnectionService {
12564
12725
  await Promise.all(actions.map(async (action) => await action.dispose()));
12565
12726
  return;
12566
12727
  }
12567
- const key = CREATE_KEY_FN$k(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
12728
+ const key = CREATE_KEY_FN$m(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
12568
12729
  if (!this.getAction.has(key)) {
12569
12730
  return;
12570
12731
  }
@@ -12582,7 +12743,7 @@ const METHOD_NAME_VALIDATE$2 = "exchangeCoreService validate";
12582
12743
  * @param exchangeName - Exchange name
12583
12744
  * @returns Unique string key for memoization
12584
12745
  */
12585
- const CREATE_KEY_FN$j = (exchangeName) => {
12746
+ const CREATE_KEY_FN$l = (exchangeName) => {
12586
12747
  return exchangeName;
12587
12748
  };
12588
12749
  /**
@@ -12606,7 +12767,7 @@ class ExchangeCoreService {
12606
12767
  * @param exchangeName - Name of the exchange to validate
12607
12768
  * @returns Promise that resolves when validation is complete
12608
12769
  */
12609
- this.validate = memoize(([exchangeName]) => CREATE_KEY_FN$j(exchangeName), async (exchangeName) => {
12770
+ this.validate = memoize(([exchangeName]) => CREATE_KEY_FN$l(exchangeName), async (exchangeName) => {
12610
12771
  this.loggerService.log(METHOD_NAME_VALIDATE$2, {
12611
12772
  exchangeName,
12612
12773
  });
@@ -12858,7 +13019,7 @@ const METHOD_NAME_VALIDATE$1 = "strategyCoreService validate";
12858
13019
  * @param context - Execution context with strategyName, exchangeName, frameName
12859
13020
  * @returns Unique string key for memoization
12860
13021
  */
12861
- const CREATE_KEY_FN$i = (context) => {
13022
+ const CREATE_KEY_FN$k = (context) => {
12862
13023
  const parts = [context.strategyName, context.exchangeName];
12863
13024
  if (context.frameName)
12864
13025
  parts.push(context.frameName);
@@ -12890,7 +13051,7 @@ class StrategyCoreService {
12890
13051
  * @param context - Execution context with strategyName, exchangeName, frameName
12891
13052
  * @returns Promise that resolves when validation is complete
12892
13053
  */
12893
- this.validate = memoize(([context]) => CREATE_KEY_FN$i(context), async (context) => {
13054
+ this.validate = memoize(([context]) => CREATE_KEY_FN$k(context), async (context) => {
12894
13055
  this.loggerService.log(METHOD_NAME_VALIDATE$1, {
12895
13056
  context,
12896
13057
  });
@@ -13725,7 +13886,7 @@ class SizingGlobalService {
13725
13886
  * @param context - Context with riskName, exchangeName, frameName
13726
13887
  * @returns Unique string key for memoization
13727
13888
  */
13728
- const CREATE_KEY_FN$h = (context) => {
13889
+ const CREATE_KEY_FN$j = (context) => {
13729
13890
  const parts = [context.riskName, context.exchangeName];
13730
13891
  if (context.frameName)
13731
13892
  parts.push(context.frameName);
@@ -13751,7 +13912,7 @@ class RiskGlobalService {
13751
13912
  * @param payload - Payload with riskName, exchangeName and frameName
13752
13913
  * @returns Promise that resolves when validation is complete
13753
13914
  */
13754
- this.validate = memoize(([context]) => CREATE_KEY_FN$h(context), async (context) => {
13915
+ this.validate = memoize(([context]) => CREATE_KEY_FN$j(context), async (context) => {
13755
13916
  this.loggerService.log("riskGlobalService validate", {
13756
13917
  context,
13757
13918
  });
@@ -13829,7 +13990,7 @@ const METHOD_NAME_VALIDATE = "actionCoreService validate";
13829
13990
  * @param context - Execution context with strategyName, exchangeName, frameName
13830
13991
  * @returns Unique string key for memoization
13831
13992
  */
13832
- const CREATE_KEY_FN$g = (context) => {
13993
+ const CREATE_KEY_FN$i = (context) => {
13833
13994
  const parts = [context.strategyName, context.exchangeName];
13834
13995
  if (context.frameName)
13835
13996
  parts.push(context.frameName);
@@ -13873,7 +14034,7 @@ class ActionCoreService {
13873
14034
  * @param context - Strategy execution context with strategyName, exchangeName and frameName
13874
14035
  * @returns Promise that resolves when all validations complete
13875
14036
  */
13876
- this.validate = memoize(([context]) => CREATE_KEY_FN$g(context), async (context) => {
14037
+ this.validate = memoize(([context]) => CREATE_KEY_FN$i(context), async (context) => {
13877
14038
  this.loggerService.log(METHOD_NAME_VALIDATE, {
13878
14039
  context,
13879
14040
  });
@@ -18408,7 +18569,7 @@ const Markdown = new MarkdownAdapter();
18408
18569
  * @param backtest - Whether running in backtest mode
18409
18570
  * @returns Unique string key for memoization
18410
18571
  */
18411
- const CREATE_KEY_FN$f = (symbol, strategyName, exchangeName, frameName, backtest) => {
18572
+ const CREATE_KEY_FN$h = (symbol, strategyName, exchangeName, frameName, backtest) => {
18412
18573
  const parts = [symbol, strategyName, exchangeName];
18413
18574
  if (frameName)
18414
18575
  parts.push(frameName);
@@ -18647,7 +18808,7 @@ class BacktestMarkdownService {
18647
18808
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
18648
18809
  * Each combination gets its own isolated storage instance.
18649
18810
  */
18650
- this.getStorage = 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));
18811
+ this.getStorage = 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));
18651
18812
  /**
18652
18813
  * Processes tick events and accumulates closed signals.
18653
18814
  * Should be called from IStrategyCallbacks.onTick.
@@ -18804,7 +18965,7 @@ class BacktestMarkdownService {
18804
18965
  payload,
18805
18966
  });
18806
18967
  if (payload) {
18807
- const key = CREATE_KEY_FN$f(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
18968
+ const key = CREATE_KEY_FN$h(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
18808
18969
  this.getStorage.clear(key);
18809
18970
  }
18810
18971
  else {
@@ -18866,7 +19027,7 @@ class BacktestMarkdownService {
18866
19027
  * @param backtest - Whether running in backtest mode
18867
19028
  * @returns Unique string key for memoization
18868
19029
  */
18869
- const CREATE_KEY_FN$e = (symbol, strategyName, exchangeName, frameName, backtest) => {
19030
+ const CREATE_KEY_FN$g = (symbol, strategyName, exchangeName, frameName, backtest) => {
18870
19031
  const parts = [symbol, strategyName, exchangeName];
18871
19032
  if (frameName)
18872
19033
  parts.push(frameName);
@@ -19349,7 +19510,7 @@ class LiveMarkdownService {
19349
19510
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
19350
19511
  * Each combination gets its own isolated storage instance.
19351
19512
  */
19352
- this.getStorage = 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));
19513
+ this.getStorage = 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));
19353
19514
  /**
19354
19515
  * Subscribes to live signal emitter to receive tick events.
19355
19516
  * Protected against multiple subscriptions.
@@ -19567,7 +19728,7 @@ class LiveMarkdownService {
19567
19728
  payload,
19568
19729
  });
19569
19730
  if (payload) {
19570
- const key = CREATE_KEY_FN$e(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
19731
+ const key = CREATE_KEY_FN$g(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
19571
19732
  this.getStorage.clear(key);
19572
19733
  }
19573
19734
  else {
@@ -19587,7 +19748,7 @@ class LiveMarkdownService {
19587
19748
  * @param backtest - Whether running in backtest mode
19588
19749
  * @returns Unique string key for memoization
19589
19750
  */
19590
- const CREATE_KEY_FN$d = (symbol, strategyName, exchangeName, frameName, backtest) => {
19751
+ const CREATE_KEY_FN$f = (symbol, strategyName, exchangeName, frameName, backtest) => {
19591
19752
  const parts = [symbol, strategyName, exchangeName];
19592
19753
  if (frameName)
19593
19754
  parts.push(frameName);
@@ -19878,7 +20039,7 @@ class ScheduleMarkdownService {
19878
20039
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
19879
20040
  * Each combination gets its own isolated storage instance.
19880
20041
  */
19881
- this.getStorage = 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));
20042
+ this.getStorage = 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));
19882
20043
  /**
19883
20044
  * Subscribes to signal emitter to receive scheduled signal events.
19884
20045
  * Protected against multiple subscriptions.
@@ -20081,7 +20242,7 @@ class ScheduleMarkdownService {
20081
20242
  payload,
20082
20243
  });
20083
20244
  if (payload) {
20084
- const key = CREATE_KEY_FN$d(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20245
+ const key = CREATE_KEY_FN$f(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20085
20246
  this.getStorage.clear(key);
20086
20247
  }
20087
20248
  else {
@@ -20101,7 +20262,7 @@ class ScheduleMarkdownService {
20101
20262
  * @param backtest - Whether running in backtest mode
20102
20263
  * @returns Unique string key for memoization
20103
20264
  */
20104
- const CREATE_KEY_FN$c = (symbol, strategyName, exchangeName, frameName, backtest) => {
20265
+ const CREATE_KEY_FN$e = (symbol, strategyName, exchangeName, frameName, backtest) => {
20105
20266
  const parts = [symbol, strategyName, exchangeName];
20106
20267
  if (frameName)
20107
20268
  parts.push(frameName);
@@ -20349,7 +20510,7 @@ class PerformanceMarkdownService {
20349
20510
  * Memoized function to get or create PerformanceStorage for a symbol-strategy-exchange-frame-backtest combination.
20350
20511
  * Each combination gets its own isolated storage instance.
20351
20512
  */
20352
- this.getStorage = 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));
20513
+ this.getStorage = 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));
20353
20514
  /**
20354
20515
  * Subscribes to performance emitter to receive performance events.
20355
20516
  * Protected against multiple subscriptions.
@@ -20516,7 +20677,7 @@ class PerformanceMarkdownService {
20516
20677
  payload,
20517
20678
  });
20518
20679
  if (payload) {
20519
- const key = CREATE_KEY_FN$c(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20680
+ const key = CREATE_KEY_FN$e(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20520
20681
  this.getStorage.clear(key);
20521
20682
  }
20522
20683
  else {
@@ -20986,7 +21147,7 @@ class WalkerMarkdownService {
20986
21147
  * @param backtest - Whether running in backtest mode
20987
21148
  * @returns Unique string key for memoization
20988
21149
  */
20989
- const CREATE_KEY_FN$b = (exchangeName, frameName, backtest) => {
21150
+ const CREATE_KEY_FN$d = (exchangeName, frameName, backtest) => {
20990
21151
  const parts = [exchangeName];
20991
21152
  if (frameName)
20992
21153
  parts.push(frameName);
@@ -21353,7 +21514,7 @@ class HeatMarkdownService {
21353
21514
  * Memoized function to get or create HeatmapStorage for exchange, frame and backtest mode.
21354
21515
  * Each exchangeName + frameName + backtest mode combination gets its own isolated heatmap storage instance.
21355
21516
  */
21356
- this.getStorage = memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$b(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
21517
+ this.getStorage = memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$d(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
21357
21518
  /**
21358
21519
  * Subscribes to signal emitter to receive tick events.
21359
21520
  * Protected against multiple subscriptions.
@@ -21548,7 +21709,7 @@ class HeatMarkdownService {
21548
21709
  payload,
21549
21710
  });
21550
21711
  if (payload) {
21551
- const key = CREATE_KEY_FN$b(payload.exchangeName, payload.frameName, payload.backtest);
21712
+ const key = CREATE_KEY_FN$d(payload.exchangeName, payload.frameName, payload.backtest);
21552
21713
  this.getStorage.clear(key);
21553
21714
  }
21554
21715
  else {
@@ -22579,7 +22740,7 @@ class ClientPartial {
22579
22740
  * @param backtest - Whether running in backtest mode
22580
22741
  * @returns Unique string key for memoization
22581
22742
  */
22582
- const CREATE_KEY_FN$a = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
22743
+ const CREATE_KEY_FN$c = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
22583
22744
  /**
22584
22745
  * Creates a callback function for emitting profit events to partialProfitSubject.
22585
22746
  *
@@ -22701,7 +22862,7 @@ class PartialConnectionService {
22701
22862
  * Key format: "signalId:backtest" or "signalId:live"
22702
22863
  * Value: ClientPartial instance with logger and event emitters
22703
22864
  */
22704
- this.getPartial = memoize(([signalId, backtest]) => CREATE_KEY_FN$a(signalId, backtest), (signalId, backtest) => {
22865
+ this.getPartial = memoize(([signalId, backtest]) => CREATE_KEY_FN$c(signalId, backtest), (signalId, backtest) => {
22705
22866
  return new ClientPartial({
22706
22867
  signalId,
22707
22868
  logger: this.loggerService,
@@ -22791,7 +22952,7 @@ class PartialConnectionService {
22791
22952
  const partial = this.getPartial(data.id, backtest);
22792
22953
  await partial.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
22793
22954
  await partial.clear(symbol, data, priceClose, backtest);
22794
- const key = CREATE_KEY_FN$a(data.id, backtest);
22955
+ const key = CREATE_KEY_FN$c(data.id, backtest);
22795
22956
  this.getPartial.clear(key);
22796
22957
  };
22797
22958
  }
@@ -22807,7 +22968,7 @@ class PartialConnectionService {
22807
22968
  * @param backtest - Whether running in backtest mode
22808
22969
  * @returns Unique string key for memoization
22809
22970
  */
22810
- const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest) => {
22971
+ const CREATE_KEY_FN$b = (symbol, strategyName, exchangeName, frameName, backtest) => {
22811
22972
  const parts = [symbol, strategyName, exchangeName];
22812
22973
  if (frameName)
22813
22974
  parts.push(frameName);
@@ -23032,7 +23193,7 @@ class PartialMarkdownService {
23032
23193
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
23033
23194
  * Each combination gets its own isolated storage instance.
23034
23195
  */
23035
- this.getStorage = 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));
23196
+ this.getStorage = 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));
23036
23197
  /**
23037
23198
  * Subscribes to partial profit/loss signal emitters to receive events.
23038
23199
  * Protected against multiple subscriptions.
@@ -23242,7 +23403,7 @@ class PartialMarkdownService {
23242
23403
  payload,
23243
23404
  });
23244
23405
  if (payload) {
23245
- const key = CREATE_KEY_FN$9(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
23406
+ const key = CREATE_KEY_FN$b(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
23246
23407
  this.getStorage.clear(key);
23247
23408
  }
23248
23409
  else {
@@ -23258,7 +23419,7 @@ class PartialMarkdownService {
23258
23419
  * @param context - Context with strategyName, exchangeName, frameName
23259
23420
  * @returns Unique string key for memoization
23260
23421
  */
23261
- const CREATE_KEY_FN$8 = (context) => {
23422
+ const CREATE_KEY_FN$a = (context) => {
23262
23423
  const parts = [context.strategyName, context.exchangeName];
23263
23424
  if (context.frameName)
23264
23425
  parts.push(context.frameName);
@@ -23332,7 +23493,7 @@ class PartialGlobalService {
23332
23493
  * @param context - Context with strategyName, exchangeName and frameName
23333
23494
  * @param methodName - Name of the calling method for error tracking
23334
23495
  */
23335
- this.validate = memoize(([context]) => CREATE_KEY_FN$8(context), (context, methodName) => {
23496
+ this.validate = memoize(([context]) => CREATE_KEY_FN$a(context), (context, methodName) => {
23336
23497
  this.loggerService.log("partialGlobalService validate", {
23337
23498
  context,
23338
23499
  methodName,
@@ -23787,7 +23948,7 @@ class ClientBreakeven {
23787
23948
  * @param backtest - Whether running in backtest mode
23788
23949
  * @returns Unique string key for memoization
23789
23950
  */
23790
- const CREATE_KEY_FN$7 = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
23951
+ const CREATE_KEY_FN$9 = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
23791
23952
  /**
23792
23953
  * Creates a callback function for emitting breakeven events to breakevenSubject.
23793
23954
  *
@@ -23873,7 +24034,7 @@ class BreakevenConnectionService {
23873
24034
  * Key format: "signalId:backtest" or "signalId:live"
23874
24035
  * Value: ClientBreakeven instance with logger and event emitter
23875
24036
  */
23876
- this.getBreakeven = memoize(([signalId, backtest]) => CREATE_KEY_FN$7(signalId, backtest), (signalId, backtest) => {
24037
+ this.getBreakeven = memoize(([signalId, backtest]) => CREATE_KEY_FN$9(signalId, backtest), (signalId, backtest) => {
23877
24038
  return new ClientBreakeven({
23878
24039
  signalId,
23879
24040
  logger: this.loggerService,
@@ -23934,7 +24095,7 @@ class BreakevenConnectionService {
23934
24095
  const breakeven = this.getBreakeven(data.id, backtest);
23935
24096
  await breakeven.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
23936
24097
  await breakeven.clear(symbol, data, priceClose, backtest);
23937
- const key = CREATE_KEY_FN$7(data.id, backtest);
24098
+ const key = CREATE_KEY_FN$9(data.id, backtest);
23938
24099
  this.getBreakeven.clear(key);
23939
24100
  };
23940
24101
  }
@@ -23950,7 +24111,7 @@ class BreakevenConnectionService {
23950
24111
  * @param backtest - Whether running in backtest mode
23951
24112
  * @returns Unique string key for memoization
23952
24113
  */
23953
- const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest) => {
24114
+ const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest) => {
23954
24115
  const parts = [symbol, strategyName, exchangeName];
23955
24116
  if (frameName)
23956
24117
  parts.push(frameName);
@@ -24127,7 +24288,7 @@ class BreakevenMarkdownService {
24127
24288
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
24128
24289
  * Each combination gets its own isolated storage instance.
24129
24290
  */
24130
- this.getStorage = 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));
24291
+ this.getStorage = 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));
24131
24292
  /**
24132
24293
  * Subscribes to breakeven signal emitter to receive events.
24133
24294
  * Protected against multiple subscriptions.
@@ -24316,7 +24477,7 @@ class BreakevenMarkdownService {
24316
24477
  payload,
24317
24478
  });
24318
24479
  if (payload) {
24319
- const key = CREATE_KEY_FN$6(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
24480
+ const key = CREATE_KEY_FN$8(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
24320
24481
  this.getStorage.clear(key);
24321
24482
  }
24322
24483
  else {
@@ -24332,7 +24493,7 @@ class BreakevenMarkdownService {
24332
24493
  * @param context - Context with strategyName, exchangeName, frameName
24333
24494
  * @returns Unique string key for memoization
24334
24495
  */
24335
- const CREATE_KEY_FN$5 = (context) => {
24496
+ const CREATE_KEY_FN$7 = (context) => {
24336
24497
  const parts = [context.strategyName, context.exchangeName];
24337
24498
  if (context.frameName)
24338
24499
  parts.push(context.frameName);
@@ -24406,7 +24567,7 @@ class BreakevenGlobalService {
24406
24567
  * @param context - Context with strategyName, exchangeName and frameName
24407
24568
  * @param methodName - Name of the calling method for error tracking
24408
24569
  */
24409
- this.validate = memoize(([context]) => CREATE_KEY_FN$5(context), (context, methodName) => {
24570
+ this.validate = memoize(([context]) => CREATE_KEY_FN$7(context), (context, methodName) => {
24410
24571
  this.loggerService.log("breakevenGlobalService validate", {
24411
24572
  context,
24412
24573
  methodName,
@@ -24626,7 +24787,7 @@ class ConfigValidationService {
24626
24787
  * @param backtest - Whether running in backtest mode
24627
24788
  * @returns Unique string key for memoization
24628
24789
  */
24629
- const CREATE_KEY_FN$4 = (symbol, strategyName, exchangeName, frameName, backtest) => {
24790
+ const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest) => {
24630
24791
  const parts = [symbol, strategyName, exchangeName];
24631
24792
  if (frameName)
24632
24793
  parts.push(frameName);
@@ -24795,7 +24956,7 @@ class RiskMarkdownService {
24795
24956
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
24796
24957
  * Each combination gets its own isolated storage instance.
24797
24958
  */
24798
- this.getStorage = 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));
24959
+ this.getStorage = 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));
24799
24960
  /**
24800
24961
  * Subscribes to risk rejection emitter to receive rejection events.
24801
24962
  * Protected against multiple subscriptions.
@@ -24984,7 +25145,7 @@ class RiskMarkdownService {
24984
25145
  payload,
24985
25146
  });
24986
25147
  if (payload) {
24987
- const key = CREATE_KEY_FN$4(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
25148
+ const key = CREATE_KEY_FN$6(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
24988
25149
  this.getStorage.clear(key);
24989
25150
  }
24990
25151
  else {
@@ -27672,7 +27833,7 @@ class SyncReportService {
27672
27833
  * @returns Colon-separated key string for memoization
27673
27834
  * @internal
27674
27835
  */
27675
- const CREATE_KEY_FN$3 = (symbol, strategyName, exchangeName, frameName, backtest) => {
27836
+ const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest) => {
27676
27837
  const parts = [symbol, strategyName, exchangeName];
27677
27838
  if (frameName)
27678
27839
  parts.push(frameName);
@@ -27920,7 +28081,7 @@ class StrategyMarkdownService {
27920
28081
  *
27921
28082
  * @internal
27922
28083
  */
27923
- this.getStorage = 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));
28084
+ this.getStorage = 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));
27924
28085
  /**
27925
28086
  * Records a cancel-scheduled event when a scheduled signal is cancelled.
27926
28087
  *
@@ -28488,7 +28649,7 @@ class StrategyMarkdownService {
28488
28649
  this.clear = async (payload) => {
28489
28650
  this.loggerService.log("strategyMarkdownService clear", { payload });
28490
28651
  if (payload) {
28491
- const key = CREATE_KEY_FN$3(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
28652
+ const key = CREATE_KEY_FN$5(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
28492
28653
  this.getStorage.clear(key);
28493
28654
  }
28494
28655
  else {
@@ -28596,7 +28757,7 @@ class StrategyMarkdownService {
28596
28757
  * Creates a unique key for memoizing ReportStorage instances.
28597
28758
  * Key format: "symbol:strategyName:exchangeName[:frameName]:backtest|live"
28598
28759
  */
28599
- const CREATE_KEY_FN$2 = (symbol, strategyName, exchangeName, frameName, backtest) => {
28760
+ const CREATE_KEY_FN$4 = (symbol, strategyName, exchangeName, frameName, backtest) => {
28600
28761
  const parts = [symbol, strategyName, exchangeName];
28601
28762
  if (frameName)
28602
28763
  parts.push(frameName);
@@ -28728,7 +28889,7 @@ class ReportStorage {
28728
28889
  class SyncMarkdownService {
28729
28890
  constructor() {
28730
28891
  this.loggerService = inject(TYPES.loggerService);
28731
- this.getStorage = 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));
28892
+ this.getStorage = 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));
28732
28893
  this.subscribe = singleshot(() => {
28733
28894
  this.loggerService.log("syncMarkdownService init");
28734
28895
  const unsubscribe = syncSubject.subscribe(this.tick);
@@ -28803,7 +28964,7 @@ class SyncMarkdownService {
28803
28964
  this.clear = async (payload) => {
28804
28965
  this.loggerService.log("syncMarkdownService clear", { payload });
28805
28966
  if (payload) {
28806
- const key = CREATE_KEY_FN$2(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
28967
+ const key = CREATE_KEY_FN$4(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
28807
28968
  this.getStorage.clear(key);
28808
28969
  }
28809
28970
  else {
@@ -28813,6 +28974,275 @@ class SyncMarkdownService {
28813
28974
  }
28814
28975
  }
28815
28976
 
28977
+ const LISTEN_TIMEOUT$1 = 120000;
28978
+ /**
28979
+ * Creates a unique memoization key for a price stream.
28980
+ * Key format: "symbol:strategyName:exchangeName[:frameName]:backtest|live"
28981
+ *
28982
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
28983
+ * @param strategyName - Strategy identifier
28984
+ * @param exchangeName - Exchange identifier
28985
+ * @param frameName - Frame identifier (omitted when empty)
28986
+ * @param backtest - Whether running in backtest mode
28987
+ * @returns Unique string key for memoization
28988
+ */
28989
+ const CREATE_KEY_FN$3 = (symbol, strategyName, exchangeName, frameName, backtest) => {
28990
+ const parts = [symbol, strategyName, exchangeName];
28991
+ if (frameName)
28992
+ parts.push(frameName);
28993
+ parts.push(backtest ? "backtest" : "live");
28994
+ return parts.join(":");
28995
+ };
28996
+ /**
28997
+ * Service for tracking the latest market price per symbol-strategy-exchange-frame combination.
28998
+ *
28999
+ * Maintains a memoized BehaviorSubject per unique key that is updated on every strategy tick
29000
+ * by StrategyConnectionService. Consumers can synchronously read the last known price or
29001
+ * await the first value if none has arrived yet.
29002
+ *
29003
+ * Primary use case: providing the current price outside of a tick execution context,
29004
+ * e.g., when a command is triggered between ticks.
29005
+ *
29006
+ * Features:
29007
+ * - One BehaviorSubject per (symbol, strategyName, exchangeName, frameName, backtest) key
29008
+ * - Falls back to ExchangeConnectionService.getAveragePrice when called inside an execution context
29009
+ * - Waits up to LISTEN_TIMEOUT ms for the first price if none is cached yet
29010
+ * - clear() disposes the BehaviorSubject for a single key or all keys
29011
+ *
29012
+ * Architecture:
29013
+ * - Registered as singleton in DI container
29014
+ * - Updated by StrategyConnectionService after each tick
29015
+ * - Cleared by Backtest/Live/Walker at strategy start to prevent stale data
29016
+ *
29017
+ * @example
29018
+ * ```typescript
29019
+ * const price = await backtest.priceMetaService.getCurrentPrice("BTCUSDT", context, false);
29020
+ * ```
29021
+ */
29022
+ class PriceMetaService {
29023
+ constructor() {
29024
+ this.loggerService = inject(TYPES.loggerService);
29025
+ this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
29026
+ /**
29027
+ * Memoized factory for BehaviorSubject streams keyed by (symbol, strategyName, exchangeName, frameName, backtest).
29028
+ *
29029
+ * Each subject holds the latest currentPrice emitted by the strategy iterator for that key.
29030
+ * Instances are cached until clear() is called.
29031
+ */
29032
+ this.getSource = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$3(symbol, strategyName, exchangeName, frameName, backtest), () => new BehaviorSubject());
29033
+ /**
29034
+ * Returns the current market price for the given symbol and context.
29035
+ *
29036
+ * When called inside an execution context (i.e., during a signal handler or action),
29037
+ * delegates to ExchangeConnectionService.getAveragePrice for the live exchange price.
29038
+ * Otherwise, reads the last value from the cached BehaviorSubject. If no value has
29039
+ * been emitted yet, waits up to LISTEN_TIMEOUT ms for the first tick before throwing.
29040
+ *
29041
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29042
+ * @param context - Strategy, exchange, and frame identifiers
29043
+ * @param backtest - True if backtest mode, false if live mode
29044
+ * @returns Current market price in quote currency
29045
+ * @throws When no price arrives within LISTEN_TIMEOUT ms
29046
+ */
29047
+ this.getCurrentPrice = async (symbol, context, backtest) => {
29048
+ this.loggerService.log("priceMetaService getCurrentPrice", {
29049
+ symbol,
29050
+ context,
29051
+ backtest,
29052
+ });
29053
+ if (ExecutionContextService.hasContext() &&
29054
+ MethodContextService.hasContext()) {
29055
+ return await this.exchangeConnectionService.getAveragePrice(symbol);
29056
+ }
29057
+ const source = this.getSource(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
29058
+ if (source.data) {
29059
+ return source.data;
29060
+ }
29061
+ 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...`);
29062
+ const currentPrice = await waitForNext(source, (data) => !!data, LISTEN_TIMEOUT$1);
29063
+ if (typeof currentPrice === "symbol") {
29064
+ throw new Error(`PriceMetaService: Timeout while waiting for currentPrice for ${CREATE_KEY_FN$3(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
29065
+ }
29066
+ return currentPrice;
29067
+ };
29068
+ /**
29069
+ * Pushes a new price value into the BehaviorSubject for the given key.
29070
+ *
29071
+ * Called by StrategyConnectionService after each strategy tick to keep
29072
+ * the cached price up to date.
29073
+ *
29074
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29075
+ * @param currentPrice - The latest price from the tick
29076
+ * @param context - Strategy, exchange, and frame identifiers
29077
+ * @param backtest - True if backtest mode, false if live mode
29078
+ */
29079
+ this.next = async (symbol, currentPrice, context, backtest) => {
29080
+ this.loggerService.log("priceMetaService next", {
29081
+ symbol,
29082
+ currentPrice,
29083
+ context,
29084
+ backtest,
29085
+ });
29086
+ const source = this.getSource(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
29087
+ source.next(currentPrice);
29088
+ };
29089
+ /**
29090
+ * Disposes cached BehaviorSubject(s) to free memory and prevent stale data.
29091
+ *
29092
+ * When called without arguments, clears all memoized price streams.
29093
+ * When called with a payload, clears only the stream for the specified key.
29094
+ * Should be called at strategy start (Backtest/Live/Walker) to reset state.
29095
+ *
29096
+ * @param payload - Optional key to clear a single stream; omit to clear all
29097
+ */
29098
+ this.clear = (payload) => {
29099
+ this.loggerService.log("priceMetaService clear", {
29100
+ payload
29101
+ });
29102
+ if (!payload) {
29103
+ this.getSource.clear();
29104
+ return;
29105
+ }
29106
+ const key = CREATE_KEY_FN$3(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
29107
+ this.getSource.clear(key);
29108
+ };
29109
+ }
29110
+ }
29111
+
29112
+ const LISTEN_TIMEOUT = 120000;
29113
+ /**
29114
+ * Creates a unique memoization key for a timestamp stream.
29115
+ * Key format: "symbol:strategyName:exchangeName[:frameName]:backtest|live"
29116
+ *
29117
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29118
+ * @param strategyName - Strategy identifier
29119
+ * @param exchangeName - Exchange identifier
29120
+ * @param frameName - Frame identifier (omitted when empty)
29121
+ * @param backtest - Whether running in backtest mode
29122
+ * @returns Unique string key for memoization
29123
+ */
29124
+ const CREATE_KEY_FN$2 = (symbol, strategyName, exchangeName, frameName, backtest) => {
29125
+ const parts = [symbol, strategyName, exchangeName];
29126
+ if (frameName)
29127
+ parts.push(frameName);
29128
+ parts.push(backtest ? "backtest" : "live");
29129
+ return parts.join(":");
29130
+ };
29131
+ /**
29132
+ * Service for tracking the latest candle timestamp per symbol-strategy-exchange-frame combination.
29133
+ *
29134
+ * Maintains a memoized BehaviorSubject per unique key that is updated on every strategy tick
29135
+ * by StrategyConnectionService. Consumers can synchronously read the last known timestamp or
29136
+ * await the first value if none has arrived yet.
29137
+ *
29138
+ * Primary use case: providing the current candle time outside of a tick execution context,
29139
+ * e.g., when a command is triggered between ticks.
29140
+ *
29141
+ * Features:
29142
+ * - One BehaviorSubject per (symbol, strategyName, exchangeName, frameName, backtest) key
29143
+ * - Falls back to ExecutionContextService.context.when when called inside an execution context
29144
+ * - Waits up to LISTEN_TIMEOUT ms for the first timestamp if none is cached yet
29145
+ * - clear() disposes the BehaviorSubject for a single key or all keys
29146
+ *
29147
+ * Architecture:
29148
+ * - Registered as singleton in DI container
29149
+ * - Updated by StrategyConnectionService after each tick
29150
+ * - Cleared by Backtest/Live/Walker at strategy start to prevent stale data
29151
+ *
29152
+ * @example
29153
+ * ```typescript
29154
+ * const ts = await backtest.timeMetaService.getTimestamp("BTCUSDT", context, false);
29155
+ * ```
29156
+ */
29157
+ class TimeMetaService {
29158
+ constructor() {
29159
+ this.loggerService = inject(TYPES.loggerService);
29160
+ this.executionContextService = inject(TYPES.executionContextService);
29161
+ /**
29162
+ * Memoized factory for BehaviorSubject streams keyed by (symbol, strategyName, exchangeName, frameName, backtest).
29163
+ *
29164
+ * Each subject holds the latest createdAt timestamp emitted by the strategy iterator for that key.
29165
+ * Instances are cached until clear() is called.
29166
+ */
29167
+ this.getSource = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$2(symbol, strategyName, exchangeName, frameName, backtest), () => new BehaviorSubject());
29168
+ /**
29169
+ * Returns the current candle timestamp (in milliseconds) for the given symbol and context.
29170
+ *
29171
+ * When called inside an execution context (i.e., during a signal handler or action),
29172
+ * reads the timestamp directly from ExecutionContextService.context.when.
29173
+ * Otherwise, reads the last value from the cached BehaviorSubject. If no value has
29174
+ * been emitted yet, waits up to LISTEN_TIMEOUT ms for the first tick before throwing.
29175
+ *
29176
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29177
+ * @param context - Strategy, exchange, and frame identifiers
29178
+ * @param backtest - True if backtest mode, false if live mode
29179
+ * @returns Unix timestamp in milliseconds of the latest processed candle
29180
+ * @throws When no timestamp arrives within LISTEN_TIMEOUT ms
29181
+ */
29182
+ this.getTimestamp = async (symbol, context, backtest) => {
29183
+ this.loggerService.log("timeMetaService getTimestamp", {
29184
+ symbol,
29185
+ context,
29186
+ backtest,
29187
+ });
29188
+ if (ExecutionContextService.hasContext()) {
29189
+ return this.executionContextService.context.when.getTime();
29190
+ }
29191
+ const source = this.getSource(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
29192
+ if (source.data) {
29193
+ return source.data;
29194
+ }
29195
+ 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...`);
29196
+ const timestamp = await waitForNext(source, (data) => !!data, LISTEN_TIMEOUT);
29197
+ if (typeof timestamp === "symbol") {
29198
+ throw new Error(`TimeMetaService: Timeout while waiting for timestamp for ${CREATE_KEY_FN$2(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
29199
+ }
29200
+ return timestamp;
29201
+ };
29202
+ /**
29203
+ * Pushes a new timestamp value into the BehaviorSubject for the given key.
29204
+ *
29205
+ * Called by StrategyConnectionService after each strategy tick to keep
29206
+ * the cached timestamp up to date.
29207
+ *
29208
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29209
+ * @param timestamp - The createdAt timestamp from the tick (milliseconds)
29210
+ * @param context - Strategy, exchange, and frame identifiers
29211
+ * @param backtest - True if backtest mode, false if live mode
29212
+ */
29213
+ this.next = async (symbol, timestamp, context, backtest) => {
29214
+ this.loggerService.log("timeMetaService next", {
29215
+ symbol,
29216
+ timestamp,
29217
+ context,
29218
+ backtest,
29219
+ });
29220
+ const source = this.getSource(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
29221
+ source.next(timestamp);
29222
+ };
29223
+ /**
29224
+ * Disposes cached BehaviorSubject(s) to free memory and prevent stale data.
29225
+ *
29226
+ * When called without arguments, clears all memoized timestamp streams.
29227
+ * When called with a payload, clears only the stream for the specified key.
29228
+ * Should be called at strategy start (Backtest/Live/Walker) to reset state.
29229
+ *
29230
+ * @param payload - Optional key to clear a single stream; omit to clear all
29231
+ */
29232
+ this.clear = (payload) => {
29233
+ this.loggerService.log("timeMetaService clear", {
29234
+ payload,
29235
+ });
29236
+ if (!payload) {
29237
+ this.getSource.clear();
29238
+ return;
29239
+ }
29240
+ const key = CREATE_KEY_FN$2(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
29241
+ this.getSource.clear(key);
29242
+ };
29243
+ }
29244
+ }
29245
+
28816
29246
  {
28817
29247
  provide(TYPES.loggerService, () => new LoggerService());
28818
29248
  }
@@ -28845,6 +29275,10 @@ class SyncMarkdownService {
28845
29275
  provide(TYPES.actionCoreService, () => new ActionCoreService());
28846
29276
  provide(TYPES.frameCoreService, () => new FrameCoreService());
28847
29277
  }
29278
+ {
29279
+ provide(TYPES.priceMetaService, () => new PriceMetaService());
29280
+ provide(TYPES.timeMetaService, () => new TimeMetaService());
29281
+ }
28848
29282
  {
28849
29283
  provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
28850
29284
  provide(TYPES.riskGlobalService, () => new RiskGlobalService());
@@ -28936,6 +29370,10 @@ const coreServices = {
28936
29370
  actionCoreService: inject(TYPES.actionCoreService),
28937
29371
  frameCoreService: inject(TYPES.frameCoreService),
28938
29372
  };
29373
+ const metaServices = {
29374
+ timeMetaService: inject(TYPES.timeMetaService),
29375
+ priceMetaService: inject(TYPES.priceMetaService),
29376
+ };
28939
29377
  const globalServices = {
28940
29378
  sizingGlobalService: inject(TYPES.sizingGlobalService),
28941
29379
  riskGlobalService: inject(TYPES.riskGlobalService),
@@ -29000,6 +29438,7 @@ const backtest = {
29000
29438
  ...connectionServices,
29001
29439
  ...schemaServices,
29002
29440
  ...coreServices,
29441
+ ...metaServices,
29003
29442
  ...globalServices,
29004
29443
  ...commandServices,
29005
29444
  ...logicPrivateServices,
@@ -34486,6 +34925,20 @@ class BacktestInstance {
34486
34925
  frameName: context.frameName,
34487
34926
  backtest: true,
34488
34927
  });
34928
+ bt.timeMetaService.clear({
34929
+ symbol,
34930
+ strategyName: context.strategyName,
34931
+ exchangeName: context.exchangeName,
34932
+ frameName: context.frameName,
34933
+ backtest: true,
34934
+ });
34935
+ bt.priceMetaService.clear({
34936
+ symbol,
34937
+ strategyName: context.strategyName,
34938
+ exchangeName: context.exchangeName,
34939
+ frameName: context.frameName,
34940
+ backtest: true,
34941
+ });
34489
34942
  }
34490
34943
  {
34491
34944
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
@@ -36390,6 +36843,20 @@ class LiveInstance {
36390
36843
  frameName: "",
36391
36844
  backtest: false,
36392
36845
  });
36846
+ bt.timeMetaService.clear({
36847
+ symbol,
36848
+ strategyName: context.strategyName,
36849
+ exchangeName: context.exchangeName,
36850
+ frameName: "",
36851
+ backtest: false,
36852
+ });
36853
+ bt.priceMetaService.clear({
36854
+ symbol,
36855
+ strategyName: context.strategyName,
36856
+ exchangeName: context.exchangeName,
36857
+ frameName: "",
36858
+ backtest: false,
36859
+ });
36393
36860
  }
36394
36861
  {
36395
36862
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
@@ -40538,6 +41005,20 @@ class WalkerInstance {
40538
41005
  frameName: walkerSchema.frameName,
40539
41006
  backtest: true,
40540
41007
  });
41008
+ bt.timeMetaService.clear({
41009
+ symbol,
41010
+ strategyName,
41011
+ exchangeName: walkerSchema.exchangeName,
41012
+ frameName: walkerSchema.frameName,
41013
+ backtest: true,
41014
+ });
41015
+ bt.priceMetaService.clear({
41016
+ symbol,
41017
+ strategyName,
41018
+ exchangeName: walkerSchema.exchangeName,
41019
+ frameName: walkerSchema.frameName,
41020
+ backtest: true,
41021
+ });
40541
41022
  }
40542
41023
  {
40543
41024
  const { riskName, riskList, actions } = bt.strategySchemaService.get(strategyName);
@@ -43505,13 +43986,15 @@ class NotificationMemoryBacktestUtils {
43505
43986
  * Handles signal sync events (signal-open, signal-close).
43506
43987
  * @param data - The signal sync contract data
43507
43988
  */
43508
- this.handleSync = async (data) => {
43989
+ this.handleSync = trycatch(async (data) => {
43509
43990
  bt.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_SYNC, {
43510
43991
  signalId: data.signalId,
43511
43992
  action: data.action,
43512
43993
  });
43513
43994
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
43514
- };
43995
+ }, {
43996
+ defaultValue: null,
43997
+ });
43515
43998
  /**
43516
43999
  * Handles risk rejection event.
43517
44000
  * @param data - The risk contract data
@@ -43620,8 +44103,10 @@ class NotificationDummyBacktestUtils {
43620
44103
  /**
43621
44104
  * No-op handler for signal sync event.
43622
44105
  */
43623
- this.handleSync = async () => {
43624
- };
44106
+ this.handleSync = trycatch(async () => {
44107
+ }, {
44108
+ defaultValue: null,
44109
+ });
43625
44110
  /**
43626
44111
  * No-op handler for risk rejection event.
43627
44112
  */
@@ -43760,7 +44245,7 @@ class NotificationPersistBacktestUtils {
43760
44245
  * Handles signal sync events (signal-open, signal-close).
43761
44246
  * @param data - The signal sync contract data
43762
44247
  */
43763
- this.handleSync = async (data) => {
44248
+ this.handleSync = trycatch(async (data) => {
43764
44249
  bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_SYNC, {
43765
44250
  signalId: data.signalId,
43766
44251
  action: data.action,
@@ -43768,7 +44253,9 @@ class NotificationPersistBacktestUtils {
43768
44253
  await this.waitForInit();
43769
44254
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
43770
44255
  await this._updateNotifications();
43771
- };
44256
+ }, {
44257
+ defaultValue: null,
44258
+ });
43772
44259
  /**
43773
44260
  * Handles risk rejection event.
43774
44261
  * @param data - The risk contract data
@@ -43951,13 +44438,15 @@ class NotificationMemoryLiveUtils {
43951
44438
  * Handles signal sync events (signal-open, signal-close).
43952
44439
  * @param data - The signal sync contract data
43953
44440
  */
43954
- this.handleSync = async (data) => {
44441
+ this.handleSync = trycatch(async (data) => {
43955
44442
  bt.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_SYNC, {
43956
44443
  signalId: data.signalId,
43957
44444
  action: data.action,
43958
44445
  });
43959
44446
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
43960
- };
44447
+ }, {
44448
+ defaultValue: null,
44449
+ });
43961
44450
  /**
43962
44451
  * Handles risk rejection event.
43963
44452
  * @param data - The risk contract data
@@ -44066,8 +44555,10 @@ class NotificationDummyLiveUtils {
44066
44555
  /**
44067
44556
  * No-op handler for signal sync event.
44068
44557
  */
44069
- this.handleSync = async () => {
44070
- };
44558
+ this.handleSync = trycatch(async () => {
44559
+ }, {
44560
+ defaultValue: null,
44561
+ });
44071
44562
  /**
44072
44563
  * No-op handler for risk rejection event.
44073
44564
  */
@@ -44207,7 +44698,7 @@ class NotificationPersistLiveUtils {
44207
44698
  * Handles signal sync events (signal-open, signal-close).
44208
44699
  * @param data - The signal sync contract data
44209
44700
  */
44210
- this.handleSync = async (data) => {
44701
+ this.handleSync = trycatch(async (data) => {
44211
44702
  bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_SYNC, {
44212
44703
  signalId: data.signalId,
44213
44704
  action: data.action,
@@ -44215,7 +44706,9 @@ class NotificationPersistLiveUtils {
44215
44706
  await this.waitForInit();
44216
44707
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
44217
44708
  await this._updateNotifications();
44218
- };
44709
+ }, {
44710
+ defaultValue: null,
44711
+ });
44219
44712
  /**
44220
44713
  * Handles risk rejection event.
44221
44714
  * @param data - The risk contract data
@@ -44377,9 +44870,11 @@ class NotificationBacktestAdapter {
44377
44870
  * Proxies call to the underlying notification adapter.
44378
44871
  * @param data - The signal sync contract data
44379
44872
  */
44380
- this.handleSync = async (data) => {
44873
+ this.handleSync = trycatch(async (data) => {
44381
44874
  return await this._notificationBacktestUtils.handleSync(data);
44382
- };
44875
+ }, {
44876
+ defaultValue: null,
44877
+ });
44383
44878
  /**
44384
44879
  * Handles risk rejection event.
44385
44880
  * Proxies call to the underlying notification adapter.
@@ -44521,9 +45016,11 @@ class NotificationLiveAdapter {
44521
45016
  * Proxies call to the underlying notification adapter.
44522
45017
  * @param data - The signal sync contract data
44523
45018
  */
44524
- this.handleSync = async (data) => {
45019
+ this.handleSync = trycatch(async (data) => {
44525
45020
  return await this._notificationLiveUtils.handleSync(data);
44526
- };
45021
+ }, {
45022
+ defaultValue: null,
45023
+ });
44527
45024
  /**
44528
45025
  * Handles risk rejection event.
44529
45026
  * Proxies call to the underlying notification adapter.