backtest-kit 9.0.0 → 9.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -938,7 +938,7 @@ const LOGGER_SERVICE$7 = new LoggerService();
938
938
  /** Symbol key for the singleshot waitForInit function on PersistBase instances. */
939
939
  const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
940
940
  // Calculate step in milliseconds for candle close time validation
941
- const INTERVAL_MINUTES$9 = {
941
+ const INTERVAL_MINUTES$a = {
942
942
  "1m": 1,
943
943
  "3m": 3,
944
944
  "5m": 5,
@@ -951,7 +951,7 @@ const INTERVAL_MINUTES$9 = {
951
951
  "8h": 480,
952
952
  "1d": 1440,
953
953
  };
954
- const MS_PER_MINUTE$7 = 60000;
954
+ const MS_PER_MINUTE$8 = 60000;
955
955
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
956
956
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
957
957
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
@@ -2276,7 +2276,7 @@ class PersistCandleInstance {
2276
2276
  * @returns Promise resolving to candles in order, or null on cache miss
2277
2277
  */
2278
2278
  async readCandlesData(limit, sinceTimestamp, _untilTimestamp) {
2279
- const stepMs = INTERVAL_MINUTES$9[this.interval] * MS_PER_MINUTE$7;
2279
+ const stepMs = INTERVAL_MINUTES$a[this.interval] * MS_PER_MINUTE$8;
2280
2280
  const cachedCandles = [];
2281
2281
  for (let i = 0; i < limit; i++) {
2282
2282
  const expectedTimestamp = sinceTimestamp + i * stepMs;
@@ -2311,7 +2311,7 @@ class PersistCandleInstance {
2311
2311
  * @returns Promise that resolves when all writes are complete
2312
2312
  */
2313
2313
  async writeCandlesData(candles) {
2314
- const stepMs = INTERVAL_MINUTES$9[this.interval] * MS_PER_MINUTE$7;
2314
+ const stepMs = INTERVAL_MINUTES$a[this.interval] * MS_PER_MINUTE$8;
2315
2315
  const now = Date.now();
2316
2316
  for (const candle of candles) {
2317
2317
  const candleCloseTime = candle.timestamp + stepMs;
@@ -3197,12 +3197,12 @@ class PersistMeasureUtils {
3197
3197
  * @param key - Cache key within the bucket
3198
3198
  * @returns Promise that resolves when write is complete
3199
3199
  */
3200
- this.writeMeasureData = async (data, bucket, key) => {
3200
+ this.writeMeasureData = async (data, bucket, key, when) => {
3201
3201
  LOGGER_SERVICE$7.info(PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
3202
3202
  const isInitial = !this.getMeasureStorage.has(bucket);
3203
3203
  const instance = this.getMeasureStorage(bucket);
3204
3204
  await instance.waitForInit(isInitial);
3205
- return instance.writeMeasureData(data, key);
3205
+ return instance.writeMeasureData(data, key, when);
3206
3206
  };
3207
3207
  /**
3208
3208
  * Soft-deletes a measure entry in the given bucket by setting `removed: true`.
@@ -3438,12 +3438,12 @@ class PersistIntervalUtils {
3438
3438
  * @param key - Marker key within the bucket
3439
3439
  * @returns Promise that resolves when write is complete
3440
3440
  */
3441
- this.writeIntervalData = async (data, bucket, key) => {
3441
+ this.writeIntervalData = async (data, bucket, key, when) => {
3442
3442
  LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
3443
3443
  const isInitial = !this.getIntervalStorage.has(bucket);
3444
3444
  const instance = this.getIntervalStorage(bucket);
3445
3445
  await instance.waitForInit(isInitial);
3446
- return instance.writeIntervalData(data, key);
3446
+ return instance.writeIntervalData(data, key, when);
3447
3447
  };
3448
3448
  /**
3449
3449
  * Soft-deletes a marker in the given bucket by setting `removed: true`.
@@ -3581,7 +3581,7 @@ class PersistMemoryInstance {
3581
3581
  * @param memoryId - Memory entry identifier
3582
3582
  * @returns Promise that resolves when write is complete
3583
3583
  */
3584
- async writeMemoryData(data, memoryId) {
3584
+ async writeMemoryData(data, memoryId, _when) {
3585
3585
  await this._storage.writeValue(memoryId, data);
3586
3586
  }
3587
3587
  /**
@@ -3646,7 +3646,7 @@ class PersistMemoryDummyInstance {
3646
3646
  * No-op write (discards entry).
3647
3647
  * @returns Promise that resolves immediately
3648
3648
  */
3649
- async writeMemoryData(_data, _memoryId) { }
3649
+ async writeMemoryData(_data, _memoryId, _when) { }
3650
3650
  /**
3651
3651
  * No-op remove.
3652
3652
  * @returns Promise that resolves immediately
@@ -3739,19 +3739,20 @@ class PersistMemoryUtils {
3739
3739
  * Writes a memory entry for the given context.
3740
3740
  * Lazily initializes the instance on first access.
3741
3741
  *
3742
- * @param data - Entry data to persist
3742
+ * @param data - Entry data to persist (already carries `data.when`)
3743
3743
  * @param signalId - Signal identifier
3744
3744
  * @param bucketName - Bucket name
3745
3745
  * @param memoryId - Memory entry identifier
3746
+ * @param when - Logical timestamp this entry belongs to (duplicates `data.when` for API consistency)
3746
3747
  * @returns Promise that resolves when write is complete
3747
3748
  */
3748
- this.writeMemoryData = async (data, signalId, bucketName, memoryId) => {
3749
+ this.writeMemoryData = async (data, signalId, bucketName, memoryId, when) => {
3749
3750
  LOGGER_SERVICE$7.info(PERSIST_MEMORY_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName, memoryId });
3750
3751
  const key = `${signalId}:${bucketName}`;
3751
3752
  const isInitial = !this.getMemoryStorage.has(key);
3752
3753
  const instance = this.getMemoryStorage(signalId, bucketName);
3753
3754
  await instance.waitForInit(isInitial);
3754
- return instance.writeMemoryData(data, memoryId);
3755
+ return instance.writeMemoryData(data, memoryId, when);
3755
3756
  };
3756
3757
  /**
3757
3758
  * Soft-deletes a memory entry for the given context.
@@ -3915,7 +3916,7 @@ class PersistRecentInstance {
3915
3916
  * @param signalRow - Recent signal data to persist
3916
3917
  * @returns Promise that resolves when write is complete
3917
3918
  */
3918
- async writeRecentData(signalRow) {
3919
+ async writeRecentData(signalRow, _when) {
3919
3920
  await this._storage.writeValue(this.symbol, signalRow);
3920
3921
  }
3921
3922
  }
@@ -3943,7 +3944,7 @@ class PersistRecentDummyInstance {
3943
3944
  * No-op write (discards recent signal).
3944
3945
  * @returns Promise that resolves immediately
3945
3946
  */
3946
- async writeRecentData(_signalRow) { }
3947
+ async writeRecentData(_signalRow, _when) { }
3947
3948
  }
3948
3949
  /**
3949
3950
  * Utility class for managing recent signal persistence.
@@ -3990,21 +3991,22 @@ class PersistRecentUtils {
3990
3991
  * Writes the latest recent signal for the given context.
3991
3992
  * Lazily initializes the instance on first access.
3992
3993
  *
3993
- * @param signalRow - Recent signal data to persist
3994
+ * @param signalRow - Recent signal data to persist (already carries `signalRow.timestamp`)
3994
3995
  * @param symbol - Trading pair symbol
3995
3996
  * @param strategyName - Strategy identifier
3996
3997
  * @param exchangeName - Exchange identifier
3997
3998
  * @param frameName - Frame identifier (may be empty)
3998
3999
  * @param backtest - True for backtest mode, false for live mode
4000
+ * @param when - Logical timestamp this signal belongs to (duplicates `signalRow.timestamp` for API consistency)
3999
4001
  * @returns Promise that resolves when write is complete
4000
4002
  */
4001
- this.writeRecentData = async (signalRow, symbol, strategyName, exchangeName, frameName, backtest) => {
4003
+ this.writeRecentData = async (signalRow, symbol, strategyName, exchangeName, frameName, backtest, when) => {
4002
4004
  LOGGER_SERVICE$7.info(PERSIST_RECENT_UTILS_METHOD_NAME_WRITE_DATA);
4003
4005
  const key = this.createKey(symbol, strategyName, exchangeName, frameName, backtest);
4004
4006
  const isInitial = !this.getStorage.has(key);
4005
4007
  const instance = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
4006
4008
  await instance.waitForInit(isInitial);
4007
- return instance.writeRecentData(signalRow);
4009
+ return instance.writeRecentData(signalRow, when);
4008
4010
  };
4009
4011
  }
4010
4012
  /**
@@ -4118,7 +4120,7 @@ class PersistStateInstance {
4118
4120
  * @param data - State data to persist
4119
4121
  * @returns Promise that resolves when write is complete
4120
4122
  */
4121
- async writeStateData(data) {
4123
+ async writeStateData(data, _when) {
4122
4124
  await this._storage.writeValue(this.bucketName, data);
4123
4125
  }
4124
4126
  /**
@@ -4151,7 +4153,7 @@ class PersistStateDummyInstance {
4151
4153
  * No-op write (discards state).
4152
4154
  * @returns Promise that resolves immediately
4153
4155
  */
4154
- async writeStateData(_data) { }
4156
+ async writeStateData(_data, _when) { }
4155
4157
  /**
4156
4158
  * No-op dispose.
4157
4159
  */
@@ -4216,18 +4218,19 @@ class PersistStateUtils {
4216
4218
  * Writes state for the given context.
4217
4219
  * Lazily initializes the instance on first access.
4218
4220
  *
4219
- * @param data - State data to persist
4221
+ * @param data - State data to persist (already carries `data.when`)
4220
4222
  * @param signalId - Signal identifier
4221
4223
  * @param bucketName - Bucket name
4224
+ * @param when - Logical timestamp this value belongs to (duplicates `data.when` for API consistency)
4222
4225
  * @returns Promise that resolves when write is complete
4223
4226
  */
4224
- this.writeStateData = async (data, signalId, bucketName) => {
4227
+ this.writeStateData = async (data, signalId, bucketName, when) => {
4225
4228
  LOGGER_SERVICE$7.info(PERSIST_STATE_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName });
4226
4229
  const key = `${signalId}:${bucketName}`;
4227
4230
  const isInitial = !this.getStateStorage.has(key);
4228
4231
  const instance = this.getStateStorage(signalId, bucketName);
4229
4232
  await instance.waitForInit(isInitial);
4230
- return instance.writeStateData(data);
4233
+ return instance.writeStateData(data, when);
4231
4234
  };
4232
4235
  /**
4233
4236
  * Switches to PersistStateDummyInstance (all operations are no-ops).
@@ -4337,7 +4340,7 @@ class PersistSessionInstance {
4337
4340
  * @param data - Session data to persist
4338
4341
  * @returns Promise that resolves when write is complete
4339
4342
  */
4340
- async writeSessionData(data) {
4343
+ async writeSessionData(data, _when) {
4341
4344
  await this._storage.writeValue(this.frameName, data);
4342
4345
  }
4343
4346
  /**
@@ -4370,7 +4373,7 @@ class PersistSessionDummyInstance {
4370
4373
  * No-op write (discards session data).
4371
4374
  * @returns Promise that resolves immediately
4372
4375
  */
4373
- async writeSessionData(_data) { }
4376
+ async writeSessionData(_data, _when) { }
4374
4377
  /**
4375
4378
  * No-op dispose.
4376
4379
  */
@@ -4438,19 +4441,20 @@ class PersistSessionUtils {
4438
4441
  * Writes session data for the given context.
4439
4442
  * Lazily initializes the instance on first access.
4440
4443
  *
4441
- * @param data - Session data to persist
4444
+ * @param data - Session data to persist (already carries `data.when`)
4442
4445
  * @param strategyName - Strategy identifier
4443
4446
  * @param exchangeName - Exchange identifier
4444
4447
  * @param frameName - Frame identifier
4448
+ * @param when - Logical timestamp this value belongs to (duplicates `data.when` for API consistency)
4445
4449
  * @returns Promise that resolves when write is complete
4446
4450
  */
4447
- this.writeSessionData = async (data, strategyName, exchangeName, frameName) => {
4451
+ this.writeSessionData = async (data, strategyName, exchangeName, frameName, when) => {
4448
4452
  LOGGER_SERVICE$7.info(PERSIST_SESSION_UTILS_METHOD_NAME_WRITE_DATA, { strategyName, exchangeName, frameName });
4449
4453
  const key = `${strategyName}:${exchangeName}:${frameName}`;
4450
4454
  const isInitial = !this.getSessionStorage.has(key);
4451
4455
  const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
4452
4456
  await instance.waitForInit(isInitial);
4453
- return instance.writeSessionData(data);
4457
+ return instance.writeSessionData(data, when);
4454
4458
  };
4455
4459
  /**
4456
4460
  * Switches to PersistSessionDummyInstance (all operations are no-ops).
@@ -4629,8 +4633,8 @@ class CandleUtils {
4629
4633
  }
4630
4634
  const Candle = new CandleUtils();
4631
4635
 
4632
- const MS_PER_MINUTE$6 = 60000;
4633
- const INTERVAL_MINUTES$8 = {
4636
+ const MS_PER_MINUTE$7 = 60000;
4637
+ const INTERVAL_MINUTES$9 = {
4634
4638
  "1m": 1,
4635
4639
  "3m": 3,
4636
4640
  "5m": 5,
@@ -4661,7 +4665,7 @@ const INTERVAL_MINUTES$8 = {
4661
4665
  * @returns Aligned timestamp rounded down to interval boundary
4662
4666
  */
4663
4667
  const ALIGN_TO_INTERVAL_FN$2 = (timestamp, intervalMinutes) => {
4664
- const intervalMs = intervalMinutes * MS_PER_MINUTE$6;
4668
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$7;
4665
4669
  return Math.floor(timestamp / intervalMs) * intervalMs;
4666
4670
  };
4667
4671
  /**
@@ -4815,9 +4819,9 @@ const WRITE_CANDLES_CACHE_FN$1 = functoolsKit.trycatch(functoolsKit.queued(async
4815
4819
  * @returns Promise resolving to array of candle data
4816
4820
  */
4817
4821
  const GET_CANDLES_FN$1 = async (dto, since, self) => {
4818
- const step = INTERVAL_MINUTES$8[dto.interval];
4822
+ const step = INTERVAL_MINUTES$9[dto.interval];
4819
4823
  const sinceTimestamp = since.getTime();
4820
- const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$6;
4824
+ const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$7;
4821
4825
  await Candle.acquireLock(`ClientExchange GET_CANDLES_FN symbol=${dto.symbol} interval=${dto.interval} limit=${dto.limit}`);
4822
4826
  try {
4823
4827
  // Try to read from cache first
@@ -4931,11 +4935,11 @@ class ClientExchange {
4931
4935
  interval,
4932
4936
  limit,
4933
4937
  });
4934
- const step = INTERVAL_MINUTES$8[interval];
4938
+ const step = INTERVAL_MINUTES$9[interval];
4935
4939
  if (!step) {
4936
4940
  throw new Error(`ClientExchange unknown interval=${interval}`);
4937
4941
  }
4938
- const stepMs = step * MS_PER_MINUTE$6;
4942
+ const stepMs = step * MS_PER_MINUTE$7;
4939
4943
  // Align when down to interval boundary
4940
4944
  const whenTimestamp = this.params.execution.context.when.getTime();
4941
4945
  const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
@@ -5012,11 +5016,11 @@ class ClientExchange {
5012
5016
  if (!this.params.execution.context.backtest) {
5013
5017
  throw new Error(`ClientExchange getNextCandles: cannot fetch future candles in live mode`);
5014
5018
  }
5015
- const step = INTERVAL_MINUTES$8[interval];
5019
+ const step = INTERVAL_MINUTES$9[interval];
5016
5020
  if (!step) {
5017
5021
  throw new Error(`ClientExchange getNextCandles: unknown interval=${interval}`);
5018
5022
  }
5019
- const stepMs = step * MS_PER_MINUTE$6;
5023
+ const stepMs = step * MS_PER_MINUTE$7;
5020
5024
  const now = Date.now();
5021
5025
  // Align when down to interval boundary
5022
5026
  const whenTimestamp = this.params.execution.context.when.getTime();
@@ -5201,11 +5205,11 @@ class ClientExchange {
5201
5205
  sDate,
5202
5206
  eDate,
5203
5207
  });
5204
- const step = INTERVAL_MINUTES$8[interval];
5208
+ const step = INTERVAL_MINUTES$9[interval];
5205
5209
  if (!step) {
5206
5210
  throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
5207
5211
  }
5208
- const stepMs = step * MS_PER_MINUTE$6;
5212
+ const stepMs = step * MS_PER_MINUTE$7;
5209
5213
  const whenTimestamp = this.params.execution.context.when.getTime();
5210
5214
  const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
5211
5215
  let sinceTimestamp;
@@ -5334,7 +5338,7 @@ class ClientExchange {
5334
5338
  const alignedTo = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
5335
5339
  const to = new Date(alignedTo);
5336
5340
  const from = new Date(alignedTo -
5337
- GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$6);
5341
+ GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$7);
5338
5342
  return await this.params.getOrderBook(symbol, depth, from, to, this.params.execution.context.backtest);
5339
5343
  }
5340
5344
  /**
@@ -5363,7 +5367,7 @@ class ClientExchange {
5363
5367
  const whenTimestamp = this.params.execution.context.when.getTime();
5364
5368
  // Align to 1-minute boundary to prevent look-ahead bias
5365
5369
  const alignedTo = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, 1);
5366
- const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$6 - MS_PER_MINUTE$6;
5370
+ const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$7 - MS_PER_MINUTE$7;
5367
5371
  // No limit: fetch a single window and return as-is
5368
5372
  if (limit === undefined) {
5369
5373
  const to = new Date(alignedTo);
@@ -6317,7 +6321,7 @@ const validateScheduledSignal = (signal, currentPrice) => {
6317
6321
  }
6318
6322
  };
6319
6323
 
6320
- const INTERVAL_MINUTES$7 = {
6324
+ const INTERVAL_MINUTES$8 = {
6321
6325
  "1m": 1,
6322
6326
  "3m": 3,
6323
6327
  "5m": 5,
@@ -6738,7 +6742,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
6738
6742
  }
6739
6743
  const currentTime = self.params.execution.context.when.getTime();
6740
6744
  {
6741
- const intervalMinutes = INTERVAL_MINUTES$7[self.params.interval];
6745
+ const intervalMinutes = INTERVAL_MINUTES$8[self.params.interval];
6742
6746
  const intervalMs = intervalMinutes * 60 * 1000;
6743
6747
  const alignedTime = Math.floor(currentTime / intervalMs) * intervalMs;
6744
6748
  // Проверяем что наступил новый интервал (по aligned timestamp)
@@ -13733,7 +13737,7 @@ class StrategyConnectionService {
13733
13737
  * Maps FrameInterval to minutes for timestamp calculation.
13734
13738
  * Used to generate timeframe arrays with proper spacing.
13735
13739
  */
13736
- const INTERVAL_MINUTES$6 = {
13740
+ const INTERVAL_MINUTES$7 = {
13737
13741
  "1m": 1,
13738
13742
  "3m": 3,
13739
13743
  "5m": 5,
@@ -13787,7 +13791,7 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
13787
13791
  symbol,
13788
13792
  });
13789
13793
  const { interval, startDate, endDate } = self.params;
13790
- const intervalMinutes = INTERVAL_MINUTES$6[interval];
13794
+ const intervalMinutes = INTERVAL_MINUTES$7[interval];
13791
13795
  if (!intervalMinutes) {
13792
13796
  throw new Error(`ClientFrame unknown interval: ${interval}`);
13793
13797
  }
@@ -14152,8 +14156,8 @@ const get = (object, path) => {
14152
14156
  return pathArrayFlat.reduce((obj, key) => obj && obj[key], object);
14153
14157
  };
14154
14158
 
14155
- const MS_PER_MINUTE$5 = 60000;
14156
- const INTERVAL_MINUTES$5 = {
14159
+ const MS_PER_MINUTE$6 = 60000;
14160
+ const INTERVAL_MINUTES$6 = {
14157
14161
  "1m": 1,
14158
14162
  "3m": 3,
14159
14163
  "5m": 5,
@@ -14184,11 +14188,11 @@ const INTERVAL_MINUTES$5 = {
14184
14188
  * @returns New Date aligned down to interval boundary
14185
14189
  */
14186
14190
  const alignToInterval = (date, interval) => {
14187
- const minutes = INTERVAL_MINUTES$5[interval];
14191
+ const minutes = INTERVAL_MINUTES$6[interval];
14188
14192
  if (minutes === undefined) {
14189
14193
  throw new Error(`alignToInterval: unknown interval=${interval}`);
14190
14194
  }
14191
- const intervalMs = minutes * MS_PER_MINUTE$5;
14195
+ const intervalMs = minutes * MS_PER_MINUTE$6;
14192
14196
  return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
14193
14197
  };
14194
14198
 
@@ -19670,8 +19674,8 @@ class BacktestLogicPrivateService {
19670
19674
  }
19671
19675
 
19672
19676
  const EMITTER_CHECK_INTERVAL = 5000;
19673
- const MS_PER_MINUTE$4 = 60000;
19674
- const INTERVAL_MINUTES$4 = {
19677
+ const MS_PER_MINUTE$5 = 60000;
19678
+ const INTERVAL_MINUTES$5 = {
19675
19679
  "1m": 1,
19676
19680
  "3m": 3,
19677
19681
  "5m": 5,
@@ -19686,7 +19690,7 @@ const INTERVAL_MINUTES$4 = {
19686
19690
  };
19687
19691
  const createEmitter = functoolsKit.memoize(([interval]) => `${interval}`, (interval) => {
19688
19692
  const tickSubject = new functoolsKit.Subject();
19689
- const intervalMs = INTERVAL_MINUTES$4[interval] * MS_PER_MINUTE$4;
19693
+ const intervalMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$5;
19690
19694
  {
19691
19695
  let lastAligned = Math.floor(Date.now() / intervalMs) * intervalMs;
19692
19696
  functoolsKit.Source.fromInterval(EMITTER_CHECK_INTERVAL)
@@ -35192,7 +35196,7 @@ const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
35192
35196
  const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
35193
35197
  const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
35194
35198
  const EXCHANGE_METHOD_NAME_GET_AGGREGATED_TRADES = "ExchangeUtils.getAggregatedTrades";
35195
- const MS_PER_MINUTE$3 = 60000;
35199
+ const MS_PER_MINUTE$4 = 60000;
35196
35200
  /**
35197
35201
  * Gets current timestamp from execution context if available.
35198
35202
  * Returns current Date() if no execution context exists (non-trading GUI).
@@ -35259,7 +35263,7 @@ const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to, _backtest)
35259
35263
  const DEFAULT_GET_AGGREGATED_TRADES_FN = async (_symbol, _from, _to, _backtest) => {
35260
35264
  throw new Error(`getAggregatedTrades is not implemented for this exchange`);
35261
35265
  };
35262
- const INTERVAL_MINUTES$3 = {
35266
+ const INTERVAL_MINUTES$4 = {
35263
35267
  "1m": 1,
35264
35268
  "3m": 3,
35265
35269
  "5m": 5,
@@ -35290,7 +35294,7 @@ const INTERVAL_MINUTES$3 = {
35290
35294
  * @returns Aligned timestamp rounded down to interval boundary
35291
35295
  */
35292
35296
  const ALIGN_TO_INTERVAL_FN$1 = (timestamp, intervalMinutes) => {
35293
- const intervalMs = intervalMinutes * MS_PER_MINUTE$3;
35297
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$4;
35294
35298
  return Math.floor(timestamp / intervalMs) * intervalMs;
35295
35299
  };
35296
35300
  /**
@@ -35430,11 +35434,11 @@ class ExchangeInstance {
35430
35434
  limit,
35431
35435
  });
35432
35436
  const getCandles = this._methods.getCandles;
35433
- const step = INTERVAL_MINUTES$3[interval];
35437
+ const step = INTERVAL_MINUTES$4[interval];
35434
35438
  if (!step) {
35435
35439
  throw new Error(`ExchangeInstance unknown interval=${interval}`);
35436
35440
  }
35437
- const stepMs = step * MS_PER_MINUTE$3;
35441
+ const stepMs = step * MS_PER_MINUTE$4;
35438
35442
  // Align when down to interval boundary
35439
35443
  const when = await GET_TIMESTAMP_FN();
35440
35444
  const whenTimestamp = when.getTime();
@@ -35648,7 +35652,7 @@ class ExchangeInstance {
35648
35652
  const when = await GET_TIMESTAMP_FN();
35649
35653
  const alignedTo = ALIGN_TO_INTERVAL_FN$1(when.getTime(), GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
35650
35654
  const to = new Date(alignedTo);
35651
- const from = new Date(alignedTo - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$3);
35655
+ const from = new Date(alignedTo - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$4);
35652
35656
  const isBacktest = await GET_BACKTEST_FN();
35653
35657
  return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
35654
35658
  };
@@ -35680,7 +35684,7 @@ class ExchangeInstance {
35680
35684
  const when = await GET_TIMESTAMP_FN();
35681
35685
  // Align to 1-minute boundary to prevent look-ahead bias
35682
35686
  const alignedTo = ALIGN_TO_INTERVAL_FN$1(when.getTime(), 1);
35683
- const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$3 - MS_PER_MINUTE$3;
35687
+ const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$4 - MS_PER_MINUTE$4;
35684
35688
  const isBacktest = await GET_BACKTEST_FN();
35685
35689
  // No limit: fetch a single window and return as-is
35686
35690
  if (limit === undefined) {
@@ -35743,11 +35747,11 @@ class ExchangeInstance {
35743
35747
  sDate,
35744
35748
  eDate,
35745
35749
  });
35746
- const step = INTERVAL_MINUTES$3[interval];
35750
+ const step = INTERVAL_MINUTES$4[interval];
35747
35751
  if (!step) {
35748
35752
  throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
35749
35753
  }
35750
- const stepMs = step * MS_PER_MINUTE$3;
35754
+ const stepMs = step * MS_PER_MINUTE$4;
35751
35755
  const when = await GET_TIMESTAMP_FN();
35752
35756
  const nowTimestamp = when.getTime();
35753
35757
  const alignedNow = ALIGN_TO_INTERVAL_FN$1(nowTimestamp, step);
@@ -36059,8 +36063,8 @@ const Exchange = new ExchangeUtils();
36059
36063
 
36060
36064
  const WARM_CANDLES_METHOD_NAME = "cache.warmCandles";
36061
36065
  const CHECK_CANDLES_METHOD_NAME = "cache.checkCandles";
36062
- const MS_PER_MINUTE$2 = 60000;
36063
- const INTERVAL_MINUTES$2 = {
36066
+ const MS_PER_MINUTE$3 = 60000;
36067
+ const INTERVAL_MINUTES$3 = {
36064
36068
  "1m": 1,
36065
36069
  "3m": 3,
36066
36070
  "5m": 5,
@@ -36074,7 +36078,7 @@ const INTERVAL_MINUTES$2 = {
36074
36078
  "1d": 1440,
36075
36079
  };
36076
36080
  const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
36077
- const intervalMs = intervalMinutes * MS_PER_MINUTE$2;
36081
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$3;
36078
36082
  return Math.floor(timestamp / intervalMs) * intervalMs;
36079
36083
  };
36080
36084
  const BAR_LENGTH = 30;
@@ -36099,11 +36103,11 @@ const PRINT_PROGRESS_FN = (fetched, total, symbol, interval) => {
36099
36103
  async function checkCandles(params) {
36100
36104
  const { symbol, exchangeName, interval, from, to, baseDir = "./dump/data/candle" } = params;
36101
36105
  backtest.loggerService.info(CHECK_CANDLES_METHOD_NAME, params);
36102
- const step = INTERVAL_MINUTES$2[interval];
36106
+ const step = INTERVAL_MINUTES$3[interval];
36103
36107
  if (!step) {
36104
36108
  throw new Error(`checkCandles: unsupported interval=${interval}`);
36105
36109
  }
36106
- const stepMs = step * MS_PER_MINUTE$2;
36110
+ const stepMs = step * MS_PER_MINUTE$3;
36107
36111
  const dir = path.join(baseDir, exchangeName, symbol, interval);
36108
36112
  const fromTs = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
36109
36113
  const toTs = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
@@ -36173,11 +36177,11 @@ async function warmCandles(params) {
36173
36177
  from,
36174
36178
  to,
36175
36179
  });
36176
- const step = INTERVAL_MINUTES$2[interval];
36180
+ const step = INTERVAL_MINUTES$3[interval];
36177
36181
  if (!step) {
36178
36182
  throw new Error(`warmCandles: unsupported interval=${interval}`);
36179
36183
  }
36180
- const stepMs = step * MS_PER_MINUTE$2;
36184
+ const stepMs = step * MS_PER_MINUTE$3;
36181
36185
  const instance = new ExchangeInstance(exchangeName);
36182
36186
  const sinceTimestamp = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
36183
36187
  const untilTimestamp = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
@@ -48520,18 +48524,21 @@ class RecentPersistBacktestUtils {
48520
48524
  backtest.loggerService.info(RECENT_PERSIST_BACKTEST_METHOD_NAME_HANDLE_ACTIVE_PING, {
48521
48525
  signalId: event.data.id,
48522
48526
  });
48523
- await PersistRecentAdapter.writeRecentData(event.data, event.symbol, event.strategyName, event.exchangeName, event.data.frameName, event.backtest);
48527
+ await PersistRecentAdapter.writeRecentData(event.data, event.symbol, event.strategyName, event.exchangeName, event.data.frameName, event.backtest, new Date(event.data.timestamp));
48524
48528
  };
48525
48529
  /**
48526
48530
  * Retrieves the latest persisted signal for the given context.
48531
+ * Returns null if the stored signal's `timestamp` is greater than the requested `when`
48532
+ * (look-ahead bias protection).
48527
48533
  * @param symbol - Trading pair symbol
48528
48534
  * @param strategyName - Strategy identifier
48529
48535
  * @param exchangeName - Exchange identifier
48530
48536
  * @param frameName - Frame identifier
48531
48537
  * @param backtest - Flag indicating if the context is backtest or live
48532
- * @returns The latest signal or null if not found
48538
+ * @param when - Logical timestamp at which the read is happening (look-ahead guard)
48539
+ * @returns The latest signal or null if not found / shadowed by look-ahead
48533
48540
  */
48534
- this.getLatestSignal = async (symbol, strategyName, exchangeName, frameName, backtest$1) => {
48541
+ this.getLatestSignal = async (symbol, strategyName, exchangeName, frameName, backtest$1, when) => {
48535
48542
  backtest.loggerService.info(RECENT_PERSIST_BACKTEST_METHOD_NAME_GET_LATEST_SIGNAL, {
48536
48543
  symbol,
48537
48544
  strategyName,
@@ -48539,20 +48546,26 @@ class RecentPersistBacktestUtils {
48539
48546
  frameName,
48540
48547
  backtest: backtest$1,
48541
48548
  });
48542
- return await PersistRecentAdapter.readRecentData(symbol, strategyName, exchangeName, frameName, backtest$1);
48549
+ const signal = await PersistRecentAdapter.readRecentData(symbol, strategyName, exchangeName, frameName, backtest$1);
48550
+ if (!signal || signal.timestamp > when.getTime()) {
48551
+ return null;
48552
+ }
48553
+ return signal;
48543
48554
  };
48544
48555
  /**
48545
48556
  * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
48546
- * @param timestamp - Current timestamp in milliseconds
48557
+ * `timestamp` doubles as the look-ahead cutoff — a signal whose `timestamp` exceeds
48558
+ * the requested one is treated as not yet visible.
48559
+ * @param timestamp - Current timestamp in milliseconds (also serves as look-ahead cutoff)
48547
48560
  * @param symbol - Trading pair symbol
48548
48561
  * @param strategyName - Strategy identifier
48549
48562
  * @param exchangeName - Exchange identifier
48550
48563
  * @param frameName - Frame identifier
48551
48564
  * @param backtest - Flag indicating if the context is backtest or live
48552
- * @returns Whole minutes since the latest signal was created, or null if no signal found
48565
+ * @returns Whole minutes since the latest signal was created, or null if no signal found / shadowed by look-ahead
48553
48566
  */
48554
48567
  this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest) => {
48555
- const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest);
48568
+ const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest, new Date(timestamp));
48556
48569
  if (!signal) {
48557
48570
  return null;
48558
48571
  }
@@ -48588,30 +48601,39 @@ class RecentMemoryBacktestUtils {
48588
48601
  };
48589
48602
  /**
48590
48603
  * Retrieves the latest in-memory signal for the given context.
48604
+ * Returns null if the stored signal's `timestamp` is greater than the requested `when`
48605
+ * (look-ahead bias protection).
48591
48606
  * @param symbol - Trading pair symbol
48592
48607
  * @param strategyName - Strategy identifier
48593
48608
  * @param exchangeName - Exchange identifier
48594
48609
  * @param frameName - Frame identifier
48595
48610
  * @param backtest - Flag indicating if the context is backtest or live
48596
- * @returns The latest signal or null if not found
48611
+ * @param when - Logical timestamp at which the read is happening (look-ahead guard)
48612
+ * @returns The latest signal or null if not found / shadowed by look-ahead
48597
48613
  */
48598
- this.getLatestSignal = async (symbol, strategyName, exchangeName, frameName, backtest$1) => {
48614
+ this.getLatestSignal = async (symbol, strategyName, exchangeName, frameName, backtest$1, when) => {
48599
48615
  const key = CREATE_KEY_FN$7(symbol, strategyName, exchangeName, frameName, backtest$1);
48600
48616
  backtest.loggerService.info(RECENT_MEMORY_BACKTEST_METHOD_NAME_GET_LATEST_SIGNAL, { key });
48601
- return this._signals.get(key) ?? null;
48617
+ const signal = this._signals.get(key) ?? null;
48618
+ if (!signal || signal.timestamp > when.getTime()) {
48619
+ return null;
48620
+ }
48621
+ return signal;
48602
48622
  };
48603
48623
  /**
48604
48624
  * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
48605
- * @param timestamp - Current timestamp in milliseconds
48625
+ * `timestamp` doubles as the look-ahead cutoff — a signal whose `timestamp` exceeds
48626
+ * the requested one is treated as not yet visible.
48627
+ * @param timestamp - Current timestamp in milliseconds (also serves as look-ahead cutoff)
48606
48628
  * @param symbol - Trading pair symbol
48607
48629
  * @param strategyName - Strategy identifier
48608
48630
  * @param exchangeName - Exchange identifier
48609
48631
  * @param frameName - Frame identifier
48610
48632
  * @param backtest - Flag indicating if the context is backtest or live
48611
- * @returns Whole minutes since the latest signal was created, or null if no signal found
48633
+ * @returns Whole minutes since the latest signal was created, or null if no signal found / shadowed by look-ahead
48612
48634
  */
48613
48635
  this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest) => {
48614
- const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest);
48636
+ const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest, new Date(timestamp));
48615
48637
  if (!signal) {
48616
48638
  return null;
48617
48639
  }
@@ -48639,18 +48661,21 @@ class RecentPersistLiveUtils {
48639
48661
  backtest.loggerService.info(RECENT_PERSIST_LIVE_METHOD_NAME_HANDLE_ACTIVE_PING, {
48640
48662
  signalId: event.data.id,
48641
48663
  });
48642
- await PersistRecentAdapter.writeRecentData(event.data, event.symbol, event.strategyName, event.exchangeName, event.data.frameName, event.backtest);
48664
+ await PersistRecentAdapter.writeRecentData(event.data, event.symbol, event.strategyName, event.exchangeName, event.data.frameName, event.backtest, new Date(event.data.timestamp));
48643
48665
  };
48644
48666
  /**
48645
48667
  * Retrieves the latest persisted signal for the given context.
48668
+ * Returns null if the stored signal's `timestamp` is greater than the requested `when`
48669
+ * (look-ahead bias protection).
48646
48670
  * @param symbol - Trading pair symbol
48647
48671
  * @param strategyName - Strategy identifier
48648
48672
  * @param exchangeName - Exchange identifier
48649
48673
  * @param frameName - Frame identifier
48650
48674
  * @param backtest - Flag indicating if the context is backtest or live
48651
- * @returns The latest signal or null if not found
48675
+ * @param when - Logical timestamp at which the read is happening (look-ahead guard)
48676
+ * @returns The latest signal or null if not found / shadowed by look-ahead
48652
48677
  */
48653
- this.getLatestSignal = async (symbol, strategyName, exchangeName, frameName, backtest$1) => {
48678
+ this.getLatestSignal = async (symbol, strategyName, exchangeName, frameName, backtest$1, when) => {
48654
48679
  backtest.loggerService.info(RECENT_PERSIST_LIVE_METHOD_NAME_GET_LATEST_SIGNAL, {
48655
48680
  symbol,
48656
48681
  strategyName,
@@ -48658,20 +48683,26 @@ class RecentPersistLiveUtils {
48658
48683
  frameName,
48659
48684
  backtest: backtest$1,
48660
48685
  });
48661
- return await PersistRecentAdapter.readRecentData(symbol, strategyName, exchangeName, frameName, backtest$1);
48686
+ const signal = await PersistRecentAdapter.readRecentData(symbol, strategyName, exchangeName, frameName, backtest$1);
48687
+ if (!signal || signal.timestamp > when.getTime()) {
48688
+ return null;
48689
+ }
48690
+ return signal;
48662
48691
  };
48663
48692
  /**
48664
48693
  * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
48665
- * @param timestamp - Current timestamp in milliseconds
48694
+ * `timestamp` doubles as the look-ahead cutoff — a signal whose `timestamp` exceeds
48695
+ * the requested one is treated as not yet visible.
48696
+ * @param timestamp - Current timestamp in milliseconds (also serves as look-ahead cutoff)
48666
48697
  * @param symbol - Trading pair symbol
48667
48698
  * @param strategyName - Strategy identifier
48668
48699
  * @param exchangeName - Exchange identifier
48669
48700
  * @param frameName - Frame identifier
48670
48701
  * @param backtest - Flag indicating if the context is backtest or live
48671
- * @returns Whole minutes since the latest signal was created, or null if no signal found
48702
+ * @returns Whole minutes since the latest signal was created, or null if no signal found / shadowed by look-ahead
48672
48703
  */
48673
48704
  this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest) => {
48674
- const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest);
48705
+ const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest, new Date(timestamp));
48675
48706
  if (!signal) {
48676
48707
  return null;
48677
48708
  }
@@ -48707,30 +48738,39 @@ class RecentMemoryLiveUtils {
48707
48738
  };
48708
48739
  /**
48709
48740
  * Retrieves the latest in-memory signal for the given context.
48741
+ * Returns null if the stored signal's `timestamp` is greater than the requested `when`
48742
+ * (look-ahead bias protection).
48710
48743
  * @param symbol - Trading pair symbol
48711
48744
  * @param strategyName - Strategy identifier
48712
48745
  * @param exchangeName - Exchange identifier
48713
48746
  * @param frameName - Frame identifier
48714
48747
  * @param backtest - Flag indicating if the context is backtest or live
48715
- * @returns The latest signal or null if not found
48748
+ * @param when - Logical timestamp at which the read is happening (look-ahead guard)
48749
+ * @returns The latest signal or null if not found / shadowed by look-ahead
48716
48750
  */
48717
- this.getLatestSignal = async (symbol, strategyName, exchangeName, frameName, backtest$1) => {
48751
+ this.getLatestSignal = async (symbol, strategyName, exchangeName, frameName, backtest$1, when) => {
48718
48752
  const key = CREATE_KEY_FN$7(symbol, strategyName, exchangeName, frameName, backtest$1);
48719
48753
  backtest.loggerService.info(RECENT_MEMORY_LIVE_METHOD_NAME_GET_LATEST_SIGNAL, { key });
48720
- return this._signals.get(key) ?? null;
48754
+ const signal = this._signals.get(key) ?? null;
48755
+ if (!signal || signal.timestamp > when.getTime()) {
48756
+ return null;
48757
+ }
48758
+ return signal;
48721
48759
  };
48722
48760
  /**
48723
48761
  * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
48724
- * @param timestamp - Current timestamp in milliseconds
48762
+ * `timestamp` doubles as the look-ahead cutoff — a signal whose `timestamp` exceeds
48763
+ * the requested one is treated as not yet visible.
48764
+ * @param timestamp - Current timestamp in milliseconds (also serves as look-ahead cutoff)
48725
48765
  * @param symbol - Trading pair symbol
48726
48766
  * @param strategyName - Strategy identifier
48727
48767
  * @param exchangeName - Exchange identifier
48728
48768
  * @param frameName - Frame identifier
48729
48769
  * @param backtest - Flag indicating if the context is backtest or live
48730
- * @returns Whole minutes since the latest signal was created, or null if no signal found
48770
+ * @returns Whole minutes since the latest signal was created, or null if no signal found / shadowed by look-ahead
48731
48771
  */
48732
48772
  this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest) => {
48733
- const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest);
48773
+ const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest, new Date(timestamp));
48734
48774
  if (!signal) {
48735
48775
  return null;
48736
48776
  }
@@ -48770,9 +48810,10 @@ class RecentBacktestAdapter {
48770
48810
  * @param exchangeName - Exchange identifier
48771
48811
  * @param frameName - Frame identifier
48772
48812
  * @param backtest - Flag indicating if the context is backtest or live
48773
- * @returns The latest signal or null if not found
48813
+ * @param when - Logical timestamp at which the read is happening (look-ahead guard)
48814
+ * @returns The latest signal or null if not found / shadowed by look-ahead
48774
48815
  */
48775
- this.getLatestSignal = async (symbol, strategyName, exchangeName, frameName, backtest$1) => {
48816
+ this.getLatestSignal = async (symbol, strategyName, exchangeName, frameName, backtest$1, when) => {
48776
48817
  backtest.loggerService.info(RECENT_BACKTEST_ADAPTER_METHOD_NAME_GET_LATEST_SIGNAL, {
48777
48818
  symbol,
48778
48819
  strategyName,
@@ -48780,18 +48821,20 @@ class RecentBacktestAdapter {
48780
48821
  frameName,
48781
48822
  backtest: backtest$1,
48782
48823
  });
48783
- return await this._recentBacktestUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1);
48824
+ return await this._recentBacktestUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1, when);
48784
48825
  };
48785
48826
  /**
48786
48827
  * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
48787
- * Proxies call to the underlying storage adapter.
48788
- * @param timestamp - Current timestamp in milliseconds
48828
+ * Proxies call to the underlying storage adapter. `timestamp` doubles as the
48829
+ * look-ahead cutoff a signal whose `timestamp` exceeds the requested one is
48830
+ * treated as not yet visible.
48831
+ * @param timestamp - Current timestamp in milliseconds (also serves as look-ahead cutoff)
48789
48832
  * @param symbol - Trading pair symbol
48790
48833
  * @param strategyName - Strategy identifier
48791
48834
  * @param exchangeName - Exchange identifier
48792
48835
  * @param frameName - Frame identifier
48793
48836
  * @param backtest - Flag indicating if the context is backtest or live
48794
- * @returns Whole minutes since the latest signal was created, or null if no signal found
48837
+ * @returns Whole minutes since the latest signal was created, or null if no signal found / shadowed by look-ahead
48795
48838
  */
48796
48839
  this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest$1) => {
48797
48840
  backtest.loggerService.info(RECENT_BACKTEST_ADAPTER_METHOD_NAME_GET_LATEST_SIGNAL, {
@@ -48802,7 +48845,7 @@ class RecentBacktestAdapter {
48802
48845
  backtest: backtest$1,
48803
48846
  timestamp,
48804
48847
  });
48805
- const signal = await this._recentBacktestUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1);
48848
+ const signal = await this._recentBacktestUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1, new Date(timestamp));
48806
48849
  if (!signal) {
48807
48850
  return null;
48808
48851
  }
@@ -48874,9 +48917,10 @@ class RecentLiveAdapter {
48874
48917
  * @param exchangeName - Exchange identifier
48875
48918
  * @param frameName - Frame identifier
48876
48919
  * @param backtest - Flag indicating if the context is backtest or live
48877
- * @returns The latest signal or null if not found
48920
+ * @param when - Logical timestamp at which the read is happening (look-ahead guard)
48921
+ * @returns The latest signal or null if not found / shadowed by look-ahead
48878
48922
  */
48879
- this.getLatestSignal = async (symbol, strategyName, exchangeName, frameName, backtest$1) => {
48923
+ this.getLatestSignal = async (symbol, strategyName, exchangeName, frameName, backtest$1, when) => {
48880
48924
  backtest.loggerService.info(RECENT_LIVE_ADAPTER_METHOD_NAME_GET_LATEST_SIGNAL, {
48881
48925
  symbol,
48882
48926
  strategyName,
@@ -48884,18 +48928,20 @@ class RecentLiveAdapter {
48884
48928
  frameName,
48885
48929
  backtest: backtest$1,
48886
48930
  });
48887
- return await this._recentLiveUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1);
48931
+ return await this._recentLiveUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1, when);
48888
48932
  };
48889
48933
  /**
48890
48934
  * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
48891
- * Proxies call to the underlying storage adapter.
48892
- * @param timestamp - Current timestamp in milliseconds
48935
+ * Proxies call to the underlying storage adapter. `timestamp` doubles as the
48936
+ * look-ahead cutoff a signal whose `timestamp` exceeds the requested one is
48937
+ * treated as not yet visible.
48938
+ * @param timestamp - Current timestamp in milliseconds (also serves as look-ahead cutoff)
48893
48939
  * @param symbol - Trading pair symbol
48894
48940
  * @param strategyName - Strategy identifier
48895
48941
  * @param exchangeName - Exchange identifier
48896
48942
  * @param frameName - Frame identifier
48897
48943
  * @param backtest - Flag indicating if the context is backtest or live
48898
- * @returns Whole minutes since the latest signal was created, or null if no signal found
48944
+ * @returns Whole minutes since the latest signal was created, or null if no signal found / shadowed by look-ahead
48899
48945
  */
48900
48946
  this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest$1) => {
48901
48947
  backtest.loggerService.info(RECENT_LIVE_ADAPTER_METHOD_NAME_GET_LATEST_SIGNAL, {
@@ -48906,7 +48952,7 @@ class RecentLiveAdapter {
48906
48952
  backtest: backtest$1,
48907
48953
  timestamp,
48908
48954
  });
48909
- const signal = await this._recentLiveUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1);
48955
+ const signal = await this._recentLiveUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1, new Date(timestamp));
48910
48956
  if (!signal) {
48911
48957
  return null;
48912
48958
  }
@@ -48996,14 +49042,16 @@ class RecentAdapter {
48996
49042
  /**
48997
49043
  * Retrieves the latest active signal for the given symbol and context.
48998
49044
  * Searches backtest storage first, then live storage.
49045
+ * Returns null if the stored signal's `timestamp` is greater than the requested `when`
49046
+ * (look-ahead bias protection).
48999
49047
  *
49000
49048
  * @param symbol - Trading pair symbol
49001
49049
  * @param context - Execution context with strategyName, exchangeName, and frameName
49002
- * @param backtest - Flag indicating if the context is backtest or live
49003
- * @returns The latest signal or null if not found
49050
+ * @param when - Logical timestamp at which the read is happening (look-ahead guard)
49051
+ * @returns The latest signal or null if not found / shadowed by look-ahead
49004
49052
  * @throws Error if RecentAdapter is not enabled
49005
49053
  */
49006
- this.getLatestSignal = async (symbol, context) => {
49054
+ this.getLatestSignal = async (symbol, context, when) => {
49007
49055
  backtest.loggerService.info(RECENT_ADAPTER_METHOD_NAME_GET_LATEST_SIGNAL, {
49008
49056
  symbol,
49009
49057
  context,
@@ -49012,10 +49060,10 @@ class RecentAdapter {
49012
49060
  throw new Error("RecentAdapter is not enabled. Call enable() first.");
49013
49061
  }
49014
49062
  let result = null;
49015
- if (result = await RecentBacktest.getLatestSignal(symbol, context.strategyName, context.exchangeName, context.frameName, true)) {
49063
+ if (result = await RecentBacktest.getLatestSignal(symbol, context.strategyName, context.exchangeName, context.frameName, true, when)) {
49016
49064
  return result;
49017
49065
  }
49018
- if (result = await RecentLive.getLatestSignal(symbol, context.strategyName, context.exchangeName, context.frameName, false)) {
49066
+ if (result = await RecentLive.getLatestSignal(symbol, context.strategyName, context.exchangeName, context.frameName, false, when)) {
49019
49067
  return result;
49020
49068
  }
49021
49069
  return null;
@@ -49023,13 +49071,16 @@ class RecentAdapter {
49023
49071
  /**
49024
49072
  * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
49025
49073
  * Searches backtest storage first, then live storage.
49026
- * @param timestamp - Current timestamp in milliseconds
49074
+ * `when` doubles as the look-ahead cutoff — a signal whose `timestamp` exceeds
49075
+ * `when.getTime()` is treated as not yet visible — and as the "now" against
49076
+ * which elapsed minutes are computed.
49027
49077
  * @param symbol - Trading pair symbol
49028
49078
  * @param context - Execution context with strategyName, exchangeName, and frameName
49029
- * @returns Whole minutes since the latest signal was created, or null if no signal found
49079
+ * @param when - Logical timestamp at which the read is happening (look-ahead cutoff + "now")
49080
+ * @returns Whole minutes since the latest signal was created, or null if no signal found / shadowed by look-ahead
49030
49081
  * @throws Error if RecentAdapter is not enabled
49031
49082
  */
49032
- this.getMinutesSinceLatestSignalCreated = async (symbol, context) => {
49083
+ this.getMinutesSinceLatestSignalCreated = async (symbol, context, when) => {
49033
49084
  backtest.loggerService.info(RECENT_ADAPTER_METHOD_NAME_GET_MINUTES_SINCE_LATEST_SIGNAL, {
49034
49085
  symbol,
49035
49086
  context,
@@ -49038,13 +49089,11 @@ class RecentAdapter {
49038
49089
  throw new Error("RecentAdapter is not enabled. Call enable() first.");
49039
49090
  }
49040
49091
  let signal = null;
49041
- if (signal = await RecentBacktest.getLatestSignal(symbol, context.strategyName, context.exchangeName, context.frameName, true)) {
49042
- const timestamp = await backtest.timeMetaService.getTimestamp(symbol, context, true);
49043
- return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
49092
+ if (signal = await RecentBacktest.getLatestSignal(symbol, context.strategyName, context.exchangeName, context.frameName, true, when)) {
49093
+ return Math.floor((when.getTime() - signal.timestamp) / (1000 * 60));
49044
49094
  }
49045
- if (signal = await RecentLive.getLatestSignal(symbol, context.strategyName, context.exchangeName, context.frameName, false)) {
49046
- const timestamp = await backtest.timeMetaService.getTimestamp(symbol, context, false);
49047
- return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
49095
+ if (signal = await RecentLive.getLatestSignal(symbol, context.strategyName, context.exchangeName, context.frameName, false, when)) {
49096
+ return Math.floor((when.getTime() - signal.timestamp) / (1000 * 60));
49048
49097
  }
49049
49098
  return null;
49050
49099
  };
@@ -49109,41 +49158,54 @@ class StateLocalInstance {
49109
49158
  this.initialValue = initialValue;
49110
49159
  this.signalId = signalId;
49111
49160
  this.bucketName = bucketName;
49161
+ this._when = 0;
49112
49162
  /**
49113
49163
  * Initializes _value from initialValue - local state needs no async setup.
49114
49164
  * @returns Promise that resolves immediately
49115
49165
  */
49116
49166
  this.waitForInit = functoolsKit.singleshot(async (_initial) => {
49117
49167
  this._value = this.initialValue;
49168
+ this._when = 0;
49118
49169
  });
49119
49170
  /**
49120
49171
  * Update the in-memory state value.
49172
+ * Records `when` so future reads with a smaller `when` see `initialValue`.
49173
+ * The dispatch updater receives the look-ahead-guarded current value.
49121
49174
  * @param dispatch - New value or updater function receiving current value
49175
+ * @param when - Logical timestamp this value belongs to
49122
49176
  * @returns Updated state value
49123
49177
  */
49124
- this.setState = functoolsKit.queued(async (dispatch) => {
49178
+ this.setState = functoolsKit.queued(async (dispatch, when) => {
49125
49179
  backtest.loggerService.debug(STATE_LOCAL_INSTANCE_METHOD_NAME_SET, {
49126
49180
  signalId: this.signalId,
49127
49181
  bucketName: this.bucketName,
49128
49182
  });
49129
49183
  if (typeof dispatch === "function") {
49130
- this._value = await dispatch(this._value);
49184
+ const prev = this._when > when.getTime() ? this.initialValue : this._value;
49185
+ this._value = await dispatch(prev);
49131
49186
  }
49132
49187
  else {
49133
49188
  this._value = dispatch;
49134
49189
  }
49190
+ this._when = when.getTime();
49135
49191
  return this._value;
49136
49192
  });
49137
49193
  }
49138
49194
  /**
49139
49195
  * Read the current in-memory state value.
49196
+ * Returns `initialValue` when the stored `when` is greater than the requested `when`
49197
+ * (look-ahead bias protection).
49198
+ * @param when - Logical timestamp at which the read is happening
49140
49199
  * @returns Current state value
49141
49200
  */
49142
- async getState() {
49201
+ async getState(when) {
49143
49202
  backtest.loggerService.debug(STATE_LOCAL_INSTANCE_METHOD_NAME_GET, {
49144
49203
  signalId: this.signalId,
49145
49204
  bucketName: this.bucketName,
49146
49205
  });
49206
+ if (this._when > when.getTime()) {
49207
+ return this.initialValue;
49208
+ }
49147
49209
  return this._value;
49148
49210
  }
49149
49211
  /** Releases resources held by this instance. */
@@ -49177,14 +49239,14 @@ class StateDummyInstance {
49177
49239
  * No-op read - always returns initialValue.
49178
49240
  * @returns initialValue
49179
49241
  */
49180
- async getState() {
49242
+ async getState(_when) {
49181
49243
  return this.initialValue;
49182
49244
  }
49183
49245
  /**
49184
49246
  * No-op write - discards the value and returns initialValue.
49185
49247
  * @returns initialValue
49186
49248
  */
49187
- async setState(_dispatch) {
49249
+ async setState(_dispatch, _when) {
49188
49250
  return this.initialValue;
49189
49251
  }
49190
49252
  /** No-op. */
@@ -49210,6 +49272,7 @@ class StatePersistInstance {
49210
49272
  this.initialValue = initialValue;
49211
49273
  this.signalId = signalId;
49212
49274
  this.bucketName = bucketName;
49275
+ this._when = 0;
49213
49276
  /**
49214
49277
  * Initialize persistence storage and restore state from disk.
49215
49278
  * @param initial - Whether this is the first initialization
@@ -49224,40 +49287,54 @@ class StatePersistInstance {
49224
49287
  const data = await PersistStateAdapter.readStateData(this.signalId, this.bucketName);
49225
49288
  if (data) {
49226
49289
  this._value = data.data;
49290
+ this._when = data.when;
49227
49291
  return;
49228
49292
  }
49229
49293
  this._value = this.initialValue;
49294
+ this._when = 0;
49230
49295
  });
49231
49296
  /**
49232
49297
  * Update state and persist to disk atomically.
49298
+ * A write with a smaller `when` overwrites an existing record — that lets a
49299
+ * restarted backtest reset live-written state without breaking live.
49300
+ * The dispatch updater receives the look-ahead-guarded current value.
49233
49301
  * @param dispatch - New value or updater function receiving current value
49302
+ * @param when - Logical timestamp this value belongs to
49234
49303
  * @returns Updated state value
49235
49304
  */
49236
- this.setState = functoolsKit.queued(async (dispatch) => {
49305
+ this.setState = functoolsKit.queued(async (dispatch, when) => {
49237
49306
  backtest.loggerService.debug(STATE_PERSIST_INSTANCE_METHOD_NAME_SET, {
49238
49307
  signalId: this.signalId,
49239
49308
  bucketName: this.bucketName,
49240
49309
  });
49241
49310
  if (typeof dispatch === "function") {
49242
- this._value = await dispatch(this._value);
49311
+ const prev = this._when > when.getTime() ? this.initialValue : this._value;
49312
+ this._value = await dispatch(prev);
49243
49313
  }
49244
49314
  else {
49245
49315
  this._value = dispatch;
49246
49316
  }
49317
+ this._when = when.getTime();
49247
49318
  const id = CREATE_KEY_FN$6(this.signalId, this.bucketName);
49248
- await PersistStateAdapter.writeStateData({ id, data: this._value }, this.signalId, this.bucketName);
49319
+ await PersistStateAdapter.writeStateData({ id, data: this._value, when: this._when }, this.signalId, this.bucketName, when);
49249
49320
  return this._value;
49250
49321
  });
49251
49322
  }
49252
49323
  /**
49253
49324
  * Read the current persisted state value.
49325
+ * Returns `initialValue` when the stored `when` is greater than the requested `when`
49326
+ * (look-ahead bias protection).
49327
+ * @param when - Logical timestamp at which the read is happening
49254
49328
  * @returns Current state value
49255
49329
  */
49256
- async getState() {
49330
+ async getState(when) {
49257
49331
  backtest.loggerService.debug(STATE_PERSIST_INSTANCE_METHOD_NAME_GET, {
49258
49332
  signalId: this.signalId,
49259
49333
  bucketName: this.bucketName,
49260
49334
  });
49335
+ if (this._when > when.getTime()) {
49336
+ return this.initialValue;
49337
+ }
49261
49338
  return this._value;
49262
49339
  }
49263
49340
  /** Releases resources held by this instance. */
@@ -49310,6 +49387,7 @@ class StateBacktestAdapter {
49310
49387
  * @param dto.signalId - Signal identifier
49311
49388
  * @param dto.bucketName - Bucket name
49312
49389
  * @param dto.initialValue - Default value when no persisted state exists
49390
+ * @param dto.when - Logical timestamp at which the read is happening (look-ahead guard)
49313
49391
  * @returns Current state value
49314
49392
  */
49315
49393
  this.getState = async (dto) => {
@@ -49321,7 +49399,7 @@ class StateBacktestAdapter {
49321
49399
  const isInitial = !this.getInstance.has(key);
49322
49400
  const instance = this.getInstance(dto.signalId, dto.bucketName, dto.initialValue);
49323
49401
  await instance.waitForInit(isInitial);
49324
- return await instance.getState();
49402
+ return await instance.getState(dto.when);
49325
49403
  };
49326
49404
  /**
49327
49405
  * Update the state value for a signal.
@@ -49329,6 +49407,7 @@ class StateBacktestAdapter {
49329
49407
  * @param dto.signalId - Signal identifier
49330
49408
  * @param dto.bucketName - Bucket name
49331
49409
  * @param dto.initialValue - Default value when no persisted state exists
49410
+ * @param dto.when - Logical timestamp this value belongs to
49332
49411
  * @returns Updated state value
49333
49412
  */
49334
49413
  this.setState = async (dispatch, dto) => {
@@ -49340,7 +49419,7 @@ class StateBacktestAdapter {
49340
49419
  const isInitial = !this.getInstance.has(key);
49341
49420
  const instance = this.getInstance(dto.signalId, dto.bucketName, dto.initialValue);
49342
49421
  await instance.waitForInit(isInitial);
49343
- return await instance.setState(dispatch);
49422
+ return await instance.setState(dispatch, dto.when);
49344
49423
  };
49345
49424
  /**
49346
49425
  * Switches to in-memory adapter (default).
@@ -49425,6 +49504,7 @@ class StateLiveAdapter {
49425
49504
  * @param dto.signalId - Signal identifier
49426
49505
  * @param dto.bucketName - Bucket name
49427
49506
  * @param dto.initialValue - Default value when no persisted state exists
49507
+ * @param dto.when - Logical timestamp at which the read is happening (look-ahead guard)
49428
49508
  * @returns Current state value
49429
49509
  */
49430
49510
  this.getState = async (dto) => {
@@ -49436,7 +49516,7 @@ class StateLiveAdapter {
49436
49516
  const isInitial = !this.getInstance.has(key);
49437
49517
  const instance = this.getInstance(dto.signalId, dto.bucketName, dto.initialValue);
49438
49518
  await instance.waitForInit(isInitial);
49439
- return await instance.getState();
49519
+ return await instance.getState(dto.when);
49440
49520
  };
49441
49521
  /**
49442
49522
  * Update the state value for a signal.
@@ -49444,6 +49524,7 @@ class StateLiveAdapter {
49444
49524
  * @param dto.signalId - Signal identifier
49445
49525
  * @param dto.bucketName - Bucket name
49446
49526
  * @param dto.initialValue - Default value when no persisted state exists
49527
+ * @param dto.when - Logical timestamp this value belongs to
49447
49528
  * @returns Updated state value
49448
49529
  */
49449
49530
  this.setState = async (dispatch, dto) => {
@@ -49455,7 +49536,7 @@ class StateLiveAdapter {
49455
49536
  const isInitial = !this.getInstance.has(key);
49456
49537
  const instance = this.getInstance(dto.signalId, dto.bucketName, dto.initialValue);
49457
49538
  await instance.waitForInit(isInitial);
49458
- return await instance.setState(dispatch);
49539
+ return await instance.setState(dispatch, dto.when);
49459
49540
  };
49460
49541
  /**
49461
49542
  * Switches to in-memory adapter.
@@ -49552,6 +49633,7 @@ class StateAdapter {
49552
49633
  * @param dto.bucketName - Bucket name
49553
49634
  * @param dto.initialValue - Default value when no persisted state exists
49554
49635
  * @param dto.backtest - Flag indicating if the context is backtest or live
49636
+ * @param dto.when - Logical timestamp at which the read is happening (look-ahead guard)
49555
49637
  * @returns Current state value
49556
49638
  * @throws Error if adapter is not enabled
49557
49639
  */
@@ -49577,6 +49659,7 @@ class StateAdapter {
49577
49659
  * @param dto.bucketName - Bucket name
49578
49660
  * @param dto.initialValue - Default value when no persisted state exists
49579
49661
  * @param dto.backtest - Flag indicating if the context is backtest or live
49662
+ * @param dto.when - Logical timestamp this value belongs to
49580
49663
  * @returns Updated state value
49581
49664
  * @throws Error if adapter is not enabled
49582
49665
  */
@@ -49650,8 +49733,9 @@ async function getLatestSignal(symbol) {
49650
49733
  if (!MethodContextService.hasContext()) {
49651
49734
  throw new Error("getLatestSignal requires a method context");
49652
49735
  }
49736
+ const { when } = backtest.executionContextService.context;
49653
49737
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
49654
- return await Recent.getLatestSignal(symbol, { exchangeName, frameName, strategyName });
49738
+ return await Recent.getLatestSignal(symbol, { exchangeName, frameName, strategyName }, when);
49655
49739
  }
49656
49740
  /**
49657
49741
  * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
@@ -49686,8 +49770,9 @@ async function getMinutesSinceLatestSignalCreated(symbol) {
49686
49770
  if (!MethodContextService.hasContext()) {
49687
49771
  throw new Error("getMinutesSinceLatestSignalCreated requires a method context");
49688
49772
  }
49773
+ const { when } = backtest.executionContextService.context;
49689
49774
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
49690
- return await Recent.getMinutesSinceLatestSignalCreated(symbol, { exchangeName, frameName, strategyName });
49775
+ return await Recent.getMinutesSinceLatestSignalCreated(symbol, { exchangeName, frameName, strategyName }, when);
49691
49776
  }
49692
49777
  /**
49693
49778
  * Reads the state value scoped to the current active signal.
@@ -49732,7 +49817,7 @@ async function getSignalState(symbol, dto) {
49732
49817
  if (!MethodContextService.hasContext()) {
49733
49818
  throw new Error("getSignalState requires a method context");
49734
49819
  }
49735
- const { backtest: isBacktest } = backtest.executionContextService.context;
49820
+ const { backtest: isBacktest, when } = backtest.executionContextService.context;
49736
49821
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
49737
49822
  const currentPrice = await backtest.exchangeConnectionService.getAveragePrice(symbol);
49738
49823
  let signal;
@@ -49742,6 +49827,7 @@ async function getSignalState(symbol, dto) {
49742
49827
  bucketName,
49743
49828
  initialValue,
49744
49829
  backtest: isBacktest,
49830
+ when,
49745
49831
  });
49746
49832
  }
49747
49833
  if (signal = await backtest.strategyCoreService.getScheduledSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName })) {
@@ -49750,6 +49836,7 @@ async function getSignalState(symbol, dto) {
49750
49836
  bucketName,
49751
49837
  initialValue,
49752
49838
  backtest: isBacktest,
49839
+ when,
49753
49840
  });
49754
49841
  }
49755
49842
  throw new Error(`getSignalState requires a pending or scheduled signal for symbol=${symbol} bucketName=${bucketName}`);
@@ -49801,7 +49888,7 @@ async function setSignalState(symbol, dispatch, dto) {
49801
49888
  if (!MethodContextService.hasContext()) {
49802
49889
  throw new Error("setSignalState requires a method context");
49803
49890
  }
49804
- const { backtest: isBacktest } = backtest.executionContextService.context;
49891
+ const { backtest: isBacktest, when } = backtest.executionContextService.context;
49805
49892
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
49806
49893
  const currentPrice = await backtest.exchangeConnectionService.getAveragePrice(symbol);
49807
49894
  let signal;
@@ -49811,6 +49898,7 @@ async function setSignalState(symbol, dispatch, dto) {
49811
49898
  bucketName,
49812
49899
  initialValue,
49813
49900
  backtest: isBacktest,
49901
+ when,
49814
49902
  });
49815
49903
  }
49816
49904
  if (signal = await backtest.strategyCoreService.getScheduledSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName })) {
@@ -49819,6 +49907,7 @@ async function setSignalState(symbol, dispatch, dto) {
49819
49907
  bucketName,
49820
49908
  initialValue,
49821
49909
  backtest: isBacktest,
49910
+ when,
49822
49911
  });
49823
49912
  }
49824
49913
  throw new Error(`setSignalState requires a pending or scheduled signal for symbol=${symbol} bucketName=${bucketName}`);
@@ -49872,36 +49961,47 @@ class SessionLocalInstance {
49872
49961
  this.frameName = frameName;
49873
49962
  this.backtest = backtest$1;
49874
49963
  this._data = null;
49964
+ this._when = 0;
49875
49965
  /**
49876
49966
  * Initializes _data to null — local session needs no async setup.
49877
49967
  * @returns Promise that resolves immediately
49878
49968
  */
49879
49969
  this.waitForInit = functoolsKit.singleshot(async (_initial) => {
49880
49970
  this._data = null;
49971
+ this._when = 0;
49881
49972
  });
49882
49973
  /**
49883
49974
  * Read the current in-memory session value.
49884
- * @returns Current session value, or null if not set
49975
+ * Returns null if the stored `when` is greater than the requested `when`
49976
+ * (look-ahead bias protection).
49977
+ * @param when - Logical timestamp at which the read is happening
49978
+ * @returns Current session value, or null
49885
49979
  */
49886
- this.getData = async () => {
49980
+ this.getData = async (when) => {
49887
49981
  backtest.loggerService.debug(SESSION_LOCAL_INSTANCE_METHOD_NAME_GET, {
49888
49982
  strategyName: this.strategyName,
49889
49983
  exchangeName: this.exchangeName,
49890
49984
  frameName: this.frameName,
49891
49985
  });
49986
+ if (this._when > when.getTime()) {
49987
+ return null;
49988
+ }
49892
49989
  return this._data;
49893
49990
  };
49894
49991
  /**
49895
49992
  * Update the in-memory session value.
49993
+ * Records `when` so future reads with a smaller `when` see no value.
49896
49994
  * @param value - New value or null to clear
49995
+ * @param when - Logical timestamp this value belongs to
49897
49996
  */
49898
- this.setData = async (value) => {
49997
+ this.setData = async (value, when) => {
49899
49998
  backtest.loggerService.debug(SESSION_LOCAL_INSTANCE_METHOD_NAME_SET, {
49900
49999
  strategyName: this.strategyName,
49901
50000
  exchangeName: this.exchangeName,
49902
50001
  frameName: this.frameName,
49903
50002
  });
49904
50003
  this._data = value;
50004
+ this._when = when.getTime();
49905
50005
  };
49906
50006
  }
49907
50007
  /** Releases resources held by this instance. */
@@ -49937,13 +50037,13 @@ class SessionDummyInstance {
49937
50037
  * No-op read — always returns null.
49938
50038
  * @returns null
49939
50039
  */
49940
- this.getData = async () => {
50040
+ this.getData = async (_when) => {
49941
50041
  return null;
49942
50042
  };
49943
50043
  /**
49944
50044
  * No-op write — discards the value.
49945
50045
  */
49946
- this.setData = async (_value) => {
50046
+ this.setData = async (_value, _when) => {
49947
50047
  };
49948
50048
  }
49949
50049
  /** No-op. */
@@ -49969,6 +50069,7 @@ class SessionPersistInstance {
49969
50069
  this.frameName = frameName;
49970
50070
  this.backtest = backtest$1;
49971
50071
  this._data = null;
50072
+ this._when = 0;
49972
50073
  /**
49973
50074
  * Initialize persistence storage and restore session from disk.
49974
50075
  * @param initial - Whether this is the first initialization
@@ -49984,35 +50085,47 @@ class SessionPersistInstance {
49984
50085
  const data = await PersistSessionAdapter.readSessionData(this.strategyName, this.exchangeName, this.frameName);
49985
50086
  if (data) {
49986
50087
  this._data = data.data;
50088
+ this._when = data.when;
49987
50089
  return;
49988
50090
  }
49989
50091
  this._data = null;
50092
+ this._when = 0;
49990
50093
  });
49991
50094
  /**
49992
50095
  * Read the current persisted session value.
49993
- * @returns Current session value, or null if not set
50096
+ * Returns null if the stored `when` is greater than the requested `when`
50097
+ * (look-ahead bias protection).
50098
+ * @param when - Logical timestamp at which the read is happening
50099
+ * @returns Current session value, or null
49994
50100
  */
49995
- this.getData = async () => {
50101
+ this.getData = async (when) => {
49996
50102
  backtest.loggerService.debug(SESSION_PERSIST_INSTANCE_METHOD_NAME_GET, {
49997
50103
  strategyName: this.strategyName,
49998
50104
  exchangeName: this.exchangeName,
49999
50105
  frameName: this.frameName,
50000
50106
  });
50107
+ if (this._when > when.getTime()) {
50108
+ return null;
50109
+ }
50001
50110
  return this._data;
50002
50111
  };
50003
50112
  /**
50004
50113
  * Update session value and persist to disk atomically.
50114
+ * A write with a smaller `when` overwrites an existing record — that lets
50115
+ * a restarted backtest reset live-written state without breaking live.
50005
50116
  * @param value - New value or null to clear
50117
+ * @param when - Logical timestamp this value belongs to
50006
50118
  */
50007
- this.setData = async (value) => {
50119
+ this.setData = async (value, when) => {
50008
50120
  backtest.loggerService.debug(SESSION_PERSIST_INSTANCE_METHOD_NAME_SET, {
50009
50121
  strategyName: this.strategyName,
50010
50122
  exchangeName: this.exchangeName,
50011
50123
  frameName: this.frameName,
50012
50124
  });
50013
50125
  this._data = value;
50126
+ this._when = when.getTime();
50014
50127
  const id = CREATE_KEY_FN$5(this.symbol, this.strategyName, this.exchangeName, this.frameName, this.backtest);
50015
- await PersistSessionAdapter.writeSessionData({ id, data: value }, this.strategyName, this.exchangeName, this.frameName);
50128
+ await PersistSessionAdapter.writeSessionData({ id, data: value, when: this._when }, this.strategyName, this.exchangeName, this.frameName, when);
50016
50129
  };
50017
50130
  }
50018
50131
  /** Releases resources held by this instance. */
@@ -50045,9 +50158,10 @@ class SessionBacktestAdapter {
50045
50158
  * @param context.strategyName - Strategy identifier
50046
50159
  * @param context.exchangeName - Exchange identifier
50047
50160
  * @param context.frameName - Frame identifier
50048
- * @returns Current session value, or null if not set
50161
+ * @param when - Logical timestamp at which the read is happening (look-ahead guard)
50162
+ * @returns Current session value, or null if not set / look-ahead
50049
50163
  */
50050
- this.getData = async (symbol, context) => {
50164
+ this.getData = async (symbol, context, when) => {
50051
50165
  backtest.loggerService.debug(SESSION_BACKTEST_ADAPTER_METHOD_NAME_GET, {
50052
50166
  strategyName: context.strategyName,
50053
50167
  exchangeName: context.exchangeName,
@@ -50057,7 +50171,7 @@ class SessionBacktestAdapter {
50057
50171
  const isInitial = !this.getInstance.has(key);
50058
50172
  const instance = this.getInstance(symbol, context.strategyName, context.exchangeName, context.frameName, true);
50059
50173
  await instance.waitForInit(isInitial);
50060
- return await instance.getData();
50174
+ return await instance.getData(when);
50061
50175
  };
50062
50176
  /**
50063
50177
  * Update the session value for a backtest run.
@@ -50066,8 +50180,9 @@ class SessionBacktestAdapter {
50066
50180
  * @param context.strategyName - Strategy identifier
50067
50181
  * @param context.exchangeName - Exchange identifier
50068
50182
  * @param context.frameName - Frame identifier
50183
+ * @param when - Logical timestamp this value belongs to
50069
50184
  */
50070
- this.setData = async (symbol, value, context) => {
50185
+ this.setData = async (symbol, value, context, when) => {
50071
50186
  backtest.loggerService.debug(SESSION_BACKTEST_ADAPTER_METHOD_NAME_SET, {
50072
50187
  strategyName: context.strategyName,
50073
50188
  exchangeName: context.exchangeName,
@@ -50077,7 +50192,7 @@ class SessionBacktestAdapter {
50077
50192
  const isInitial = !this.getInstance.has(key);
50078
50193
  const instance = this.getInstance(symbol, context.strategyName, context.exchangeName, context.frameName, true);
50079
50194
  await instance.waitForInit(isInitial);
50080
- return await instance.setData(value);
50195
+ return await instance.setData(value, when);
50081
50196
  };
50082
50197
  /**
50083
50198
  * Switches to in-memory adapter (default).
@@ -50141,9 +50256,10 @@ class SessionLiveAdapter {
50141
50256
  * @param context.strategyName - Strategy identifier
50142
50257
  * @param context.exchangeName - Exchange identifier
50143
50258
  * @param context.frameName - Frame identifier
50144
- * @returns Current session value, or null if not set
50259
+ * @param when - Logical timestamp at which the read is happening (look-ahead guard)
50260
+ * @returns Current session value, or null if not set / look-ahead
50145
50261
  */
50146
- this.getData = async (symbol, context) => {
50262
+ this.getData = async (symbol, context, when) => {
50147
50263
  backtest.loggerService.debug(SESSION_LIVE_ADAPTER_METHOD_NAME_GET, {
50148
50264
  strategyName: context.strategyName,
50149
50265
  exchangeName: context.exchangeName,
@@ -50153,7 +50269,7 @@ class SessionLiveAdapter {
50153
50269
  const isInitial = !this.getInstance.has(key);
50154
50270
  const instance = this.getInstance(symbol, context.strategyName, context.exchangeName, context.frameName, false);
50155
50271
  await instance.waitForInit(isInitial);
50156
- return await instance.getData();
50272
+ return await instance.getData(when);
50157
50273
  };
50158
50274
  /**
50159
50275
  * Update the session value for a live run.
@@ -50162,8 +50278,9 @@ class SessionLiveAdapter {
50162
50278
  * @param context.strategyName - Strategy identifier
50163
50279
  * @param context.exchangeName - Exchange identifier
50164
50280
  * @param context.frameName - Frame identifier
50281
+ * @param when - Logical timestamp this value belongs to
50165
50282
  */
50166
- this.setData = async (symbol, value, context) => {
50283
+ this.setData = async (symbol, value, context, when) => {
50167
50284
  backtest.loggerService.debug(SESSION_LIVE_ADAPTER_METHOD_NAME_SET, {
50168
50285
  strategyName: context.strategyName,
50169
50286
  exchangeName: context.exchangeName,
@@ -50173,7 +50290,7 @@ class SessionLiveAdapter {
50173
50290
  const isInitial = !this.getInstance.has(key);
50174
50291
  const instance = this.getInstance(symbol, context.strategyName, context.exchangeName, context.frameName, false);
50175
50292
  await instance.waitForInit(isInitial);
50176
- return await instance.setData(value);
50293
+ return await instance.setData(value, when);
50177
50294
  };
50178
50295
  /**
50179
50296
  * Switches to in-memory adapter.
@@ -50233,9 +50350,10 @@ class SessionAdapter {
50233
50350
  * @param context.exchangeName - Exchange identifier
50234
50351
  * @param context.frameName - Frame identifier
50235
50352
  * @param backtest - Flag indicating if the context is backtest or live
50236
- * @returns Current session value, or null if not set
50353
+ * @param when - Logical timestamp at which the read is happening (look-ahead guard)
50354
+ * @returns Current session value, or null if not set / look-ahead
50237
50355
  */
50238
- this.getData = async (symbol, context, backtest$1) => {
50356
+ this.getData = async (symbol, context, backtest$1, when) => {
50239
50357
  backtest.loggerService.debug(SESSION_ADAPTER_METHOD_NAME_GET, {
50240
50358
  strategyName: context.strategyName,
50241
50359
  exchangeName: context.exchangeName,
@@ -50243,9 +50361,9 @@ class SessionAdapter {
50243
50361
  backtest: backtest$1,
50244
50362
  });
50245
50363
  if (backtest$1) {
50246
- return await SessionBacktest.getData(symbol, context);
50364
+ return await SessionBacktest.getData(symbol, context, when);
50247
50365
  }
50248
- return await SessionLive.getData(symbol, context);
50366
+ return await SessionLive.getData(symbol, context, when);
50249
50367
  };
50250
50368
  /**
50251
50369
  * Update the session value for a signal.
@@ -50256,8 +50374,9 @@ class SessionAdapter {
50256
50374
  * @param context.exchangeName - Exchange identifier
50257
50375
  * @param context.frameName - Frame identifier
50258
50376
  * @param backtest - Flag indicating if the context is backtest or live
50377
+ * @param when - Logical timestamp this value belongs to
50259
50378
  */
50260
- this.setData = async (symbol, value, context, backtest$1) => {
50379
+ this.setData = async (symbol, value, context, backtest$1, when) => {
50261
50380
  backtest.loggerService.debug(SESSION_ADAPTER_METHOD_NAME_SET, {
50262
50381
  strategyName: context.strategyName,
50263
50382
  exchangeName: context.exchangeName,
@@ -50265,9 +50384,9 @@ class SessionAdapter {
50265
50384
  backtest: backtest$1,
50266
50385
  });
50267
50386
  if (backtest$1) {
50268
- return await SessionBacktest.setData(symbol, value, context);
50387
+ return await SessionBacktest.setData(symbol, value, context, when);
50269
50388
  }
50270
- return await SessionLive.setData(symbol, value, context);
50389
+ return await SessionLive.setData(symbol, value, context, when);
50271
50390
  };
50272
50391
  }
50273
50392
  }
@@ -50319,9 +50438,9 @@ async function getSessionData(symbol) {
50319
50438
  if (!MethodContextService.hasContext()) {
50320
50439
  throw new Error("getSession requires a method context");
50321
50440
  }
50322
- const { backtest: isBacktest } = backtest.executionContextService.context;
50441
+ const { backtest: isBacktest, when } = backtest.executionContextService.context;
50323
50442
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
50324
- return await Session.getData(symbol, { exchangeName, frameName, strategyName }, isBacktest);
50443
+ return await Session.getData(symbol, { exchangeName, frameName, strategyName }, isBacktest, when);
50325
50444
  }
50326
50445
  /**
50327
50446
  * Writes a session value scoped to the current (symbol, strategy, exchange, frame) context.
@@ -50353,9 +50472,9 @@ async function setSessionData(symbol, value) {
50353
50472
  if (!MethodContextService.hasContext()) {
50354
50473
  throw new Error("setSession requires a method context");
50355
50474
  }
50356
- const { backtest: isBacktest } = backtest.executionContextService.context;
50475
+ const { backtest: isBacktest, when } = backtest.executionContextService.context;
50357
50476
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
50358
- await Session.setData(symbol, value, { exchangeName, frameName, strategyName }, isBacktest);
50477
+ await Session.setData(symbol, value, { exchangeName, frameName, strategyName }, isBacktest, when);
50359
50478
  }
50360
50479
 
50361
50480
  const CREATE_SIGNAL_STATE_METHOD_NAME = "state.createSignalState";
@@ -50366,7 +50485,7 @@ const CREATE_SET_STATE_FN = (params) => async (symbol, dispatch) => {
50366
50485
  if (!MethodContextService.hasContext()) {
50367
50486
  throw new Error("createSignalState requires a method context");
50368
50487
  }
50369
- const { backtest: isBacktest } = backtest.executionContextService.context;
50488
+ const { backtest: isBacktest, when } = backtest.executionContextService.context;
50370
50489
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
50371
50490
  const currentPrice = await backtest.exchangeConnectionService.getAveragePrice(symbol);
50372
50491
  let signal;
@@ -50376,6 +50495,7 @@ const CREATE_SET_STATE_FN = (params) => async (symbol, dispatch) => {
50376
50495
  bucketName: params.bucketName,
50377
50496
  initialValue: params.initialValue,
50378
50497
  signalId: signal.id,
50498
+ when,
50379
50499
  });
50380
50500
  }
50381
50501
  if (signal = await backtest.strategyCoreService.getScheduledSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName })) {
@@ -50384,6 +50504,7 @@ const CREATE_SET_STATE_FN = (params) => async (symbol, dispatch) => {
50384
50504
  bucketName: params.bucketName,
50385
50505
  initialValue: params.initialValue,
50386
50506
  signalId: signal.id,
50507
+ when,
50387
50508
  });
50388
50509
  }
50389
50510
  throw new Error(`createSignalState requires a pending or scheduled signal for symbol=${symbol} bucketName=${params.bucketName}`);
@@ -50395,7 +50516,7 @@ const CREATE_GET_STATE_FN = (params) => async (symbol) => {
50395
50516
  if (!MethodContextService.hasContext()) {
50396
50517
  throw new Error("createSignalState requires a method context");
50397
50518
  }
50398
- const { backtest: isBacktest } = backtest.executionContextService.context;
50519
+ const { backtest: isBacktest, when } = backtest.executionContextService.context;
50399
50520
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
50400
50521
  const currentPrice = await backtest.exchangeConnectionService.getAveragePrice(symbol);
50401
50522
  let signal;
@@ -50405,6 +50526,7 @@ const CREATE_GET_STATE_FN = (params) => async (symbol) => {
50405
50526
  bucketName: params.bucketName,
50406
50527
  initialValue: params.initialValue,
50407
50528
  signalId: signal.id,
50529
+ when,
50408
50530
  });
50409
50531
  }
50410
50532
  if (signal = await backtest.strategyCoreService.getScheduledSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName })) {
@@ -50413,6 +50535,7 @@ const CREATE_GET_STATE_FN = (params) => async (symbol) => {
50413
50535
  bucketName: params.bucketName,
50414
50536
  initialValue: params.initialValue,
50415
50537
  signalId: signal.id,
50538
+ when,
50416
50539
  });
50417
50540
  }
50418
50541
  throw new Error(`createSignalState requires a pending or scheduled signal for symbol=${symbol} bucketName=${params.bucketName}`);
@@ -50495,7 +50618,7 @@ const createSearchIndex = () => {
50495
50618
  df.set(term, count);
50496
50619
  });
50497
50620
  };
50498
- const upsert = ({ id, content, index = JSON.stringify(content), priority = Date.now(), }) => {
50621
+ const upsert = ({ id, content, when, index = JSON.stringify(content), priority = Date.now(), }) => {
50499
50622
  const existing = docs.get(id);
50500
50623
  {
50501
50624
  existing && subtractDf(existing.tf);
@@ -50504,12 +50627,21 @@ const createSearchIndex = () => {
50504
50627
  const tf = new Map();
50505
50628
  for (const t of tokens)
50506
50629
  tf.set(t, (tf.get(t) ?? 0) + 1);
50507
- docs.set(id, { tf, len: tokens.length, content, priority });
50630
+ docs.set(id, { tf, len: tokens.length, content, priority, when });
50508
50631
  {
50509
50632
  addDf(tf);
50510
50633
  }
50511
50634
  };
50512
- const read = (id) => docs.get(id)?.content;
50635
+ /**
50636
+ * Read a document by id. Returns undefined when the document was written
50637
+ * with a `when` greater than the requested `when` (look-ahead guard).
50638
+ */
50639
+ const read = (id, when) => {
50640
+ const doc = docs.get(id);
50641
+ if (!doc || doc.when > when)
50642
+ return undefined;
50643
+ return doc.content;
50644
+ };
50513
50645
  const remove = (id) => {
50514
50646
  {
50515
50647
  const existing = docs.get(id);
@@ -50517,16 +50649,30 @@ const createSearchIndex = () => {
50517
50649
  }
50518
50650
  docs.delete(id);
50519
50651
  };
50520
- const list = () => Array.from(docs.entries())
50652
+ /**
50653
+ * List documents whose `when` is less than or equal to the requested `when`
50654
+ * (look-ahead guard), sorted by priority.
50655
+ */
50656
+ const list = (when) => Array.from(docs.entries())
50657
+ .filter(([, doc]) => doc.when <= when)
50521
50658
  .sort(([, a], [, b]) => a.priority - b.priority)
50522
50659
  .map(([id, { content }]) => ({ id, content }));
50523
- const search = (query, settings = DEFAULT_SETTINGS) => {
50660
+ /**
50661
+ * BM25 search over documents whose `when` is less than or equal to the
50662
+ * requested `when` (look-ahead guard).
50663
+ *
50664
+ * Document frequency (df) is computed across the whole index — the time-cut
50665
+ * is applied only to the candidate set, so scores stay comparable across
50666
+ * different `when` values.
50667
+ */
50668
+ const search = (query, when, settings = DEFAULT_SETTINGS) => {
50524
50669
  const terms = tokenize(query);
50525
50670
  if (!terms.length || !docs.size)
50526
50671
  return [];
50527
50672
  const N = docs.size;
50528
50673
  const avgLen = [...docs.values()].reduce((s, d) => s + d.len, 0) / N;
50529
50674
  return [...docs.entries()]
50675
+ .filter(([, doc]) => doc.when <= when)
50530
50676
  .map(([id, doc]) => {
50531
50677
  let score = 0;
50532
50678
  for (const term of terms) {
@@ -50623,8 +50769,9 @@ class MemoryLocalInstance {
50623
50769
  * @param memoryId - Unique entry identifier
50624
50770
  * @param value - Value to store and index
50625
50771
  * @param description - BM25 index string
50772
+ * @param when - Logical timestamp this entry belongs to (look-ahead guard)
50626
50773
  */
50627
- async writeMemory(memoryId, value, description) {
50774
+ async writeMemory(memoryId, value, description, when) {
50628
50775
  backtest.loggerService.debug(MEMORY_LOCAL_INSTANCE_METHOD_NAME_WRITE, {
50629
50776
  signalId: this.signalId,
50630
50777
  bucketName: this.bucketName,
@@ -50635,21 +50782,24 @@ class MemoryLocalInstance {
50635
50782
  content: value,
50636
50783
  index: description,
50637
50784
  priority: Date.now(),
50785
+ when: when.getTime(),
50638
50786
  });
50639
50787
  }
50640
50788
  /**
50641
50789
  * Read a single entry from the in-memory index.
50790
+ * Behaves as not-found if the stored `when` is greater than the requested `when`.
50642
50791
  * @param memoryId - Unique entry identifier
50792
+ * @param when - Logical timestamp at which the read is happening (look-ahead guard)
50643
50793
  * @returns Parsed entry value
50644
- * @throws Error if entry not found
50794
+ * @throws Error if entry not found (or shadowed by look-ahead)
50645
50795
  */
50646
- async readMemory(memoryId) {
50796
+ async readMemory(memoryId, when) {
50647
50797
  backtest.loggerService.debug(MEMORY_LOCAL_INSTANCE_METHOD_NAME_READ, {
50648
50798
  signalId: this.signalId,
50649
50799
  bucketName: this.bucketName,
50650
50800
  memoryId,
50651
50801
  });
50652
- const value = this._index.read(memoryId);
50802
+ const value = this._index.read(memoryId, when.getTime());
50653
50803
  if (!value) {
50654
50804
  throw new Error(`MemoryLocalInstance value not found memoryId=${memoryId}`);
50655
50805
  }
@@ -50657,33 +50807,38 @@ class MemoryLocalInstance {
50657
50807
  }
50658
50808
  /**
50659
50809
  * Search entries using BM25 full-text scoring.
50810
+ * Filters out entries whose `when` is greater than the requested `when`.
50660
50811
  * @param query - Search query string
50812
+ * @param when - Logical timestamp at which the search is happening (look-ahead guard)
50661
50813
  * @returns Matching entries sorted by relevance score
50662
50814
  */
50663
- async searchMemory(query, settings) {
50815
+ async searchMemory(query, when, settings) {
50664
50816
  backtest.loggerService.debug(MEMORY_LOCAL_INSTANCE_METHOD_NAME_SEARCH, {
50665
50817
  signalId: this.signalId,
50666
50818
  bucketName: this.bucketName,
50667
50819
  query,
50668
50820
  });
50669
- return this._index.search(query, settings).map(SEARCH_MEMORY_FN);
50821
+ return this._index.search(query, when.getTime(), settings).map(SEARCH_MEMORY_FN);
50670
50822
  }
50671
50823
  /**
50672
50824
  * List all entries stored in the in-memory index.
50825
+ * Filters out entries whose `when` is greater than the requested `when`.
50826
+ * @param when - Logical timestamp at which the list is happening (look-ahead guard)
50673
50827
  * @returns Array of all stored entries
50674
50828
  */
50675
- async listMemory() {
50829
+ async listMemory(when) {
50676
50830
  backtest.loggerService.debug(MEMORY_LOCAL_INSTANCE_METHOD_NAME_LIST, {
50677
50831
  signalId: this.signalId,
50678
50832
  bucketName: this.bucketName,
50679
50833
  });
50680
- return this._index.list().map(LIST_MEMORY_FN);
50834
+ return this._index.list(when.getTime()).map(LIST_MEMORY_FN);
50681
50835
  }
50682
50836
  /**
50683
50837
  * Remove an entry from the in-memory index.
50684
50838
  * @param memoryId - Unique entry identifier
50839
+ * @param when - Logical timestamp (kept for API consistency; removal is by UUID)
50685
50840
  */
50686
- async removeMemory(memoryId) {
50841
+ async removeMemory(memoryId, _when) {
50687
50842
  backtest.loggerService.debug(MEMORY_LOCAL_INSTANCE_METHOD_NAME_REMOVE, {
50688
50843
  signalId: this.signalId,
50689
50844
  bucketName: this.bucketName,
@@ -50729,12 +50884,13 @@ class MemoryPersistInstance {
50729
50884
  initial,
50730
50885
  });
50731
50886
  await PersistMemoryAdapter.waitForInit(this.signalId, this.bucketName, initial);
50732
- for await (const { memoryId, data: { data, index, priority } } of PersistMemoryAdapter.listMemoryData(this.signalId, this.bucketName)) {
50887
+ for await (const { memoryId, data: { data, index, priority, when } } of PersistMemoryAdapter.listMemoryData(this.signalId, this.bucketName)) {
50733
50888
  this._index.upsert({
50734
50889
  id: memoryId,
50735
50890
  content: data,
50736
50891
  index,
50737
50892
  priority,
50893
+ when,
50738
50894
  });
50739
50895
  }
50740
50896
  }
@@ -50743,69 +50899,79 @@ class MemoryPersistInstance {
50743
50899
  * @param memoryId - Unique entry identifier
50744
50900
  * @param value - Value to persist and index
50745
50901
  * @param index - BM25 index string; defaults to JSON.stringify(value)
50902
+ * @param when - Logical timestamp this entry belongs to (look-ahead guard)
50746
50903
  */
50747
- async writeMemory(memoryId, value, index = JSON.stringify(value)) {
50904
+ async writeMemory(memoryId, value, index, when) {
50748
50905
  backtest.loggerService.debug(MEMORY_PERSIST_INSTANCE_METHOD_NAME_WRITE, {
50749
50906
  signalId: this.signalId,
50750
50907
  bucketName: this.bucketName,
50751
50908
  memoryId,
50752
50909
  });
50753
50910
  const priority = Date.now();
50754
- await PersistMemoryAdapter.writeMemoryData({ data: value, priority, removed: false, index }, this.signalId, this.bucketName, memoryId);
50911
+ const whenMs = when.getTime();
50912
+ await PersistMemoryAdapter.writeMemoryData({ data: value, priority, removed: false, index, when: whenMs }, this.signalId, this.bucketName, memoryId, when);
50755
50913
  this._index.upsert({
50756
50914
  id: memoryId,
50757
50915
  content: value,
50758
50916
  index,
50759
50917
  priority,
50918
+ when: whenMs,
50760
50919
  });
50761
50920
  }
50762
50921
  /**
50763
50922
  * Read a single entry from disk.
50923
+ * Behaves as not-found if the stored `when` is greater than the requested `when`.
50764
50924
  * @param memoryId - Unique entry identifier
50925
+ * @param when - Logical timestamp at which the read is happening (look-ahead guard)
50765
50926
  * @returns Entry value
50766
- * @throws Error if entry not found
50927
+ * @throws Error if entry not found (or shadowed by look-ahead)
50767
50928
  */
50768
- async readMemory(memoryId) {
50929
+ async readMemory(memoryId, when) {
50769
50930
  backtest.loggerService.debug(MEMORY_PERSIST_INSTANCE_METHOD_NAME_READ, {
50770
50931
  signalId: this.signalId,
50771
50932
  bucketName: this.bucketName,
50772
50933
  memoryId,
50773
50934
  });
50774
50935
  const data = await PersistMemoryAdapter.readMemoryData(this.signalId, this.bucketName, memoryId);
50775
- if (!data) {
50936
+ if (!data || data.when > when.getTime()) {
50776
50937
  throw new Error(`MemoryPersistInstance value not found memoryId=${memoryId}`);
50777
50938
  }
50778
50939
  return data.data;
50779
50940
  }
50780
50941
  /**
50781
50942
  * Search entries using BM25 index rebuilt from disk on init.
50943
+ * Filters out entries whose `when` is greater than the requested `when`.
50782
50944
  * @param query - Search query string
50945
+ * @param when - Logical timestamp at which the search is happening (look-ahead guard)
50783
50946
  * @returns Matching entries sorted by relevance score
50784
50947
  */
50785
- async searchMemory(query, settings) {
50948
+ async searchMemory(query, when, settings) {
50786
50949
  backtest.loggerService.debug(MEMORY_PERSIST_INSTANCE_METHOD_NAME_SEARCH, {
50787
50950
  signalId: this.signalId,
50788
50951
  bucketName: this.bucketName,
50789
50952
  query,
50790
50953
  });
50791
- return this._index.search(query, settings).map(SEARCH_MEMORY_FN);
50954
+ return this._index.search(query, when.getTime(), settings).map(SEARCH_MEMORY_FN);
50792
50955
  }
50793
50956
  /**
50794
50957
  * List all entries from the in-memory index (populated from disk on init).
50958
+ * Filters out entries whose `when` is greater than the requested `when`.
50959
+ * @param when - Logical timestamp at which the list is happening (look-ahead guard)
50795
50960
  * @returns Array of all stored entries
50796
50961
  */
50797
- async listMemory() {
50962
+ async listMemory(when) {
50798
50963
  backtest.loggerService.debug(MEMORY_PERSIST_INSTANCE_METHOD_NAME_LIST, {
50799
50964
  signalId: this.signalId,
50800
50965
  bucketName: this.bucketName,
50801
50966
  });
50802
- return this._index.list().map(LIST_MEMORY_FN);
50967
+ return this._index.list(when.getTime()).map(LIST_MEMORY_FN);
50803
50968
  }
50804
50969
  /**
50805
50970
  * Remove an entry from disk and from the BM25 index.
50806
50971
  * @param memoryId - Unique entry identifier
50972
+ * @param when - Logical timestamp (kept for API consistency; removal is by UUID)
50807
50973
  */
50808
- async removeMemory(memoryId) {
50974
+ async removeMemory(memoryId, _when) {
50809
50975
  backtest.loggerService.debug(MEMORY_PERSIST_INSTANCE_METHOD_NAME_REMOVE, {
50810
50976
  signalId: this.signalId,
50811
50977
  bucketName: this.bucketName,
@@ -50842,34 +51008,34 @@ class MemoryDummyInstance {
50842
51008
  * No-op write - discards the value.
50843
51009
  * @returns Promise that resolves immediately
50844
51010
  */
50845
- async writeMemory() {
51011
+ async writeMemory(_memoryId, _value, _description, _when) {
50846
51012
  }
50847
51013
  /**
50848
51014
  * No-op read - always throws.
50849
51015
  * @throws Error always
50850
51016
  */
50851
- async readMemory(_memoryId) {
51017
+ async readMemory(_memoryId, _when) {
50852
51018
  throw new Error("MemoryDummyInstance: readMemory not supported");
50853
51019
  }
50854
51020
  /**
50855
51021
  * No-op search - returns empty array.
50856
51022
  * @returns Empty array
50857
51023
  */
50858
- async searchMemory() {
51024
+ async searchMemory(_query, _when, _settings) {
50859
51025
  return [];
50860
51026
  }
50861
51027
  /**
50862
51028
  * No-op list - returns empty array.
50863
51029
  * @returns Empty array
50864
51030
  */
50865
- async listMemory() {
51031
+ async listMemory(_when) {
50866
51032
  return [];
50867
51033
  }
50868
51034
  /**
50869
51035
  * No-op remove.
50870
51036
  * @returns Promise that resolves immediately
50871
51037
  */
50872
- async removeMemory() {
51038
+ async removeMemory(_memoryId, _when) {
50873
51039
  }
50874
51040
  /** No-op. */
50875
51041
  dispose() {
@@ -50913,6 +51079,7 @@ class MemoryBacktestAdapter {
50913
51079
  * @param dto.signalId - Signal identifier
50914
51080
  * @param dto.bucketName - Bucket name
50915
51081
  * @param dto.description - BM25 index string; defaults to JSON.stringify(value)
51082
+ * @param dto.when - Logical timestamp this entry belongs to (look-ahead guard)
50916
51083
  */
50917
51084
  this.writeMemory = async (dto) => {
50918
51085
  backtest.loggerService.debug(MEMORY_BACKTEST_ADAPTER_METHOD_NAME_WRITE, {
@@ -50924,13 +51091,14 @@ class MemoryBacktestAdapter {
50924
51091
  const isInitial = !this.getInstance.has(key);
50925
51092
  const instance = this.getInstance(dto.signalId, dto.bucketName);
50926
51093
  await instance.waitForInit(isInitial);
50927
- return await instance.writeMemory(dto.memoryId, dto.value, dto.description);
51094
+ return await instance.writeMemory(dto.memoryId, dto.value, dto.description, dto.when);
50928
51095
  };
50929
51096
  /**
50930
51097
  * Search memory using BM25 full-text scoring.
50931
51098
  * @param dto.query - Search query string
50932
51099
  * @param dto.signalId - Signal identifier
50933
51100
  * @param dto.bucketName - Bucket name
51101
+ * @param dto.when - Logical timestamp at which the search is happening (look-ahead guard)
50934
51102
  * @returns Matching entries sorted by relevance score
50935
51103
  */
50936
51104
  this.searchMemory = async (dto) => {
@@ -50943,12 +51111,13 @@ class MemoryBacktestAdapter {
50943
51111
  const isInitial = !this.getInstance.has(key);
50944
51112
  const instance = this.getInstance(dto.signalId, dto.bucketName);
50945
51113
  await instance.waitForInit(isInitial);
50946
- return await instance.searchMemory(dto.query, dto.settings);
51114
+ return await instance.searchMemory(dto.query, dto.when, dto.settings);
50947
51115
  };
50948
51116
  /**
50949
51117
  * List all entries in memory.
50950
51118
  * @param dto.signalId - Signal identifier
50951
51119
  * @param dto.bucketName - Bucket name
51120
+ * @param dto.when - Logical timestamp at which the list is happening (look-ahead guard)
50952
51121
  * @returns Array of all stored entries
50953
51122
  */
50954
51123
  this.listMemory = async (dto) => {
@@ -50960,13 +51129,14 @@ class MemoryBacktestAdapter {
50960
51129
  const isInitial = !this.getInstance.has(key);
50961
51130
  const instance = this.getInstance(dto.signalId, dto.bucketName);
50962
51131
  await instance.waitForInit(isInitial);
50963
- return await instance.listMemory();
51132
+ return await instance.listMemory(dto.when);
50964
51133
  };
50965
51134
  /**
50966
51135
  * Remove an entry from memory.
50967
51136
  * @param dto.memoryId - Unique entry identifier
50968
51137
  * @param dto.signalId - Signal identifier
50969
51138
  * @param dto.bucketName - Bucket name
51139
+ * @param dto.when - Logical timestamp (kept for API consistency; removal is by UUID)
50970
51140
  */
50971
51141
  this.removeMemory = async (dto) => {
50972
51142
  backtest.loggerService.debug(MEMORY_BACKTEST_ADAPTER_METHOD_NAME_REMOVE, {
@@ -50978,13 +51148,14 @@ class MemoryBacktestAdapter {
50978
51148
  const isInitial = !this.getInstance.has(key);
50979
51149
  const instance = this.getInstance(dto.signalId, dto.bucketName);
50980
51150
  await instance.waitForInit(isInitial);
50981
- return await instance.removeMemory(dto.memoryId);
51151
+ return await instance.removeMemory(dto.memoryId, dto.when);
50982
51152
  };
50983
51153
  /**
50984
51154
  * Read a single entry from memory.
50985
51155
  * @param dto.memoryId - Unique entry identifier
50986
51156
  * @param dto.signalId - Signal identifier
50987
51157
  * @param dto.bucketName - Bucket name
51158
+ * @param dto.when - Logical timestamp at which the read is happening (look-ahead guard)
50988
51159
  * @returns Entry value
50989
51160
  * @throws Error if entry not found
50990
51161
  */
@@ -50998,7 +51169,7 @@ class MemoryBacktestAdapter {
50998
51169
  const isInitial = !this.getInstance.has(key);
50999
51170
  const instance = this.getInstance(dto.signalId, dto.bucketName);
51000
51171
  await instance.waitForInit(isInitial);
51001
- return await instance.readMemory(dto.memoryId);
51172
+ return await instance.readMemory(dto.memoryId, dto.when);
51002
51173
  };
51003
51174
  /**
51004
51175
  * Switches to in-memory BM25 adapter (default).
@@ -51080,6 +51251,7 @@ class MemoryLiveAdapter {
51080
51251
  * @param dto.signalId - Signal identifier
51081
51252
  * @param dto.bucketName - Bucket name
51082
51253
  * @param dto.description - BM25 index string; defaults to JSON.stringify(value)
51254
+ * @param dto.when - Logical timestamp this entry belongs to (look-ahead guard)
51083
51255
  */
51084
51256
  this.writeMemory = async (dto) => {
51085
51257
  backtest.loggerService.debug(MEMORY_LIVE_ADAPTER_METHOD_NAME_WRITE, {
@@ -51091,13 +51263,14 @@ class MemoryLiveAdapter {
51091
51263
  const isInitial = !this.getInstance.has(key);
51092
51264
  const instance = this.getInstance(dto.signalId, dto.bucketName);
51093
51265
  await instance.waitForInit(isInitial);
51094
- return await instance.writeMemory(dto.memoryId, dto.value, dto.description);
51266
+ return await instance.writeMemory(dto.memoryId, dto.value, dto.description, dto.when);
51095
51267
  };
51096
51268
  /**
51097
51269
  * Search memory using BM25 full-text scoring.
51098
51270
  * @param dto.query - Search query string
51099
51271
  * @param dto.signalId - Signal identifier
51100
51272
  * @param dto.bucketName - Bucket name
51273
+ * @param dto.when - Logical timestamp at which the search is happening (look-ahead guard)
51101
51274
  * @returns Matching entries sorted by relevance score
51102
51275
  */
51103
51276
  this.searchMemory = async (dto) => {
@@ -51110,12 +51283,13 @@ class MemoryLiveAdapter {
51110
51283
  const isInitial = !this.getInstance.has(key);
51111
51284
  const instance = this.getInstance(dto.signalId, dto.bucketName);
51112
51285
  await instance.waitForInit(isInitial);
51113
- return await instance.searchMemory(dto.query, dto.settings);
51286
+ return await instance.searchMemory(dto.query, dto.when, dto.settings);
51114
51287
  };
51115
51288
  /**
51116
51289
  * List all entries in memory.
51117
51290
  * @param dto.signalId - Signal identifier
51118
51291
  * @param dto.bucketName - Bucket name
51292
+ * @param dto.when - Logical timestamp at which the list is happening (look-ahead guard)
51119
51293
  * @returns Array of all stored entries
51120
51294
  */
51121
51295
  this.listMemory = async (dto) => {
@@ -51127,13 +51301,14 @@ class MemoryLiveAdapter {
51127
51301
  const isInitial = !this.getInstance.has(key);
51128
51302
  const instance = this.getInstance(dto.signalId, dto.bucketName);
51129
51303
  await instance.waitForInit(isInitial);
51130
- return await instance.listMemory();
51304
+ return await instance.listMemory(dto.when);
51131
51305
  };
51132
51306
  /**
51133
51307
  * Remove an entry from memory.
51134
51308
  * @param dto.memoryId - Unique entry identifier
51135
51309
  * @param dto.signalId - Signal identifier
51136
51310
  * @param dto.bucketName - Bucket name
51311
+ * @param dto.when - Logical timestamp (kept for API consistency; removal is by UUID)
51137
51312
  */
51138
51313
  this.removeMemory = async (dto) => {
51139
51314
  backtest.loggerService.debug(MEMORY_LIVE_ADAPTER_METHOD_NAME_REMOVE, {
@@ -51145,13 +51320,14 @@ class MemoryLiveAdapter {
51145
51320
  const isInitial = !this.getInstance.has(key);
51146
51321
  const instance = this.getInstance(dto.signalId, dto.bucketName);
51147
51322
  await instance.waitForInit(isInitial);
51148
- return await instance.removeMemory(dto.memoryId);
51323
+ return await instance.removeMemory(dto.memoryId, dto.when);
51149
51324
  };
51150
51325
  /**
51151
51326
  * Read a single entry from memory.
51152
51327
  * @param dto.memoryId - Unique entry identifier
51153
51328
  * @param dto.signalId - Signal identifier
51154
51329
  * @param dto.bucketName - Bucket name
51330
+ * @param dto.when - Logical timestamp at which the read is happening (look-ahead guard)
51155
51331
  * @returns Entry value
51156
51332
  * @throws Error if entry not found
51157
51333
  */
@@ -51165,7 +51341,7 @@ class MemoryLiveAdapter {
51165
51341
  const isInitial = !this.getInstance.has(key);
51166
51342
  const instance = this.getInstance(dto.signalId, dto.bucketName);
51167
51343
  await instance.waitForInit(isInitial);
51168
- return await instance.readMemory(dto.memoryId);
51344
+ return await instance.readMemory(dto.memoryId, dto.when);
51169
51345
  };
51170
51346
  /**
51171
51347
  * Switches to in-memory BM25 adapter.
@@ -51264,6 +51440,7 @@ class MemoryAdapter {
51264
51440
  * @param dto.bucketName - Bucket name
51265
51441
  * @param dto.description - BM25 index string; defaults to JSON.stringify(value)
51266
51442
  * @param dto.backtest - Flag indicating if the context is backtest or live
51443
+ * @param dto.when - Logical timestamp this entry belongs to (look-ahead guard)
51267
51444
  */
51268
51445
  this.writeMemory = async (dto) => {
51269
51446
  if (!this.enable.hasValue()) {
@@ -51287,6 +51464,7 @@ class MemoryAdapter {
51287
51464
  * @param dto.signalId - Signal identifier
51288
51465
  * @param dto.bucketName - Bucket name
51289
51466
  * @param dto.backtest - Flag indicating if the context is backtest or live
51467
+ * @param dto.when - Logical timestamp at which the search is happening (look-ahead guard)
51290
51468
  * @returns Matching entries sorted by relevance score
51291
51469
  */
51292
51470
  this.searchMemory = async (dto) => {
@@ -51310,6 +51488,7 @@ class MemoryAdapter {
51310
51488
  * @param dto.signalId - Signal identifier
51311
51489
  * @param dto.bucketName - Bucket name
51312
51490
  * @param dto.backtest - Flag indicating if the context is backtest or live
51491
+ * @param dto.when - Logical timestamp at which the list is happening (look-ahead guard)
51313
51492
  * @returns Array of all stored entries
51314
51493
  */
51315
51494
  this.listMemory = async (dto) => {
@@ -51333,6 +51512,7 @@ class MemoryAdapter {
51333
51512
  * @param dto.signalId - Signal identifier
51334
51513
  * @param dto.bucketName - Bucket name
51335
51514
  * @param dto.backtest - Flag indicating if the context is backtest or live
51515
+ * @param dto.when - Logical timestamp (kept for API consistency; removal is by UUID)
51336
51516
  */
51337
51517
  this.removeMemory = async (dto) => {
51338
51518
  if (!this.enable.hasValue()) {
@@ -51356,6 +51536,7 @@ class MemoryAdapter {
51356
51536
  * @param dto.signalId - Signal identifier
51357
51537
  * @param dto.bucketName - Bucket name
51358
51538
  * @param dto.backtest - Flag indicating if the context is backtest or live
51539
+ * @param dto.when - Logical timestamp at which the read is happening (look-ahead guard)
51359
51540
  * @returns Entry value
51360
51541
  * @throws Error if entry not found
51361
51542
  */
@@ -51428,7 +51609,7 @@ async function writeMemory(dto) {
51428
51609
  if (!MethodContextService.hasContext()) {
51429
51610
  throw new Error("writeMemory requires a method context");
51430
51611
  }
51431
- const { backtest: isBacktest, symbol } = backtest.executionContextService.context;
51612
+ const { backtest: isBacktest, symbol, when } = backtest.executionContextService.context;
51432
51613
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
51433
51614
  const currentPrice = await backtest.exchangeConnectionService.getAveragePrice(symbol);
51434
51615
  let signal;
@@ -51440,6 +51621,7 @@ async function writeMemory(dto) {
51440
51621
  bucketName,
51441
51622
  description,
51442
51623
  backtest: isBacktest,
51624
+ when,
51443
51625
  });
51444
51626
  return;
51445
51627
  }
@@ -51451,6 +51633,7 @@ async function writeMemory(dto) {
51451
51633
  bucketName,
51452
51634
  description,
51453
51635
  backtest: isBacktest,
51636
+ when,
51454
51637
  });
51455
51638
  return;
51456
51639
  }
@@ -51486,7 +51669,7 @@ async function readMemory(dto) {
51486
51669
  if (!MethodContextService.hasContext()) {
51487
51670
  throw new Error("readMemory requires a method context");
51488
51671
  }
51489
- const { backtest: isBacktest, symbol } = backtest.executionContextService.context;
51672
+ const { backtest: isBacktest, symbol, when } = backtest.executionContextService.context;
51490
51673
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
51491
51674
  const currentPrice = await backtest.exchangeConnectionService.getAveragePrice(symbol);
51492
51675
  let signal;
@@ -51496,6 +51679,7 @@ async function readMemory(dto) {
51496
51679
  signalId: signal.id,
51497
51680
  bucketName,
51498
51681
  backtest: isBacktest,
51682
+ when,
51499
51683
  });
51500
51684
  }
51501
51685
  if (signal = await backtest.strategyCoreService.getScheduledSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName })) {
@@ -51504,6 +51688,7 @@ async function readMemory(dto) {
51504
51688
  signalId: signal.id,
51505
51689
  bucketName,
51506
51690
  backtest: isBacktest,
51691
+ when,
51507
51692
  });
51508
51693
  }
51509
51694
  throw new Error(`readMemory requires a pending or scheduled signal for symbol=${symbol} memoryId=${memoryId}`);
@@ -51538,7 +51723,7 @@ async function searchMemory(dto) {
51538
51723
  if (!MethodContextService.hasContext()) {
51539
51724
  throw new Error("searchMemory requires a method context");
51540
51725
  }
51541
- const { backtest: isBacktest, symbol } = backtest.executionContextService.context;
51726
+ const { backtest: isBacktest, symbol, when } = backtest.executionContextService.context;
51542
51727
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
51543
51728
  const currentPrice = await backtest.exchangeConnectionService.getAveragePrice(symbol);
51544
51729
  let signal;
@@ -51548,6 +51733,7 @@ async function searchMemory(dto) {
51548
51733
  signalId: signal.id,
51549
51734
  bucketName,
51550
51735
  backtest: isBacktest,
51736
+ when,
51551
51737
  });
51552
51738
  }
51553
51739
  if (signal = await backtest.strategyCoreService.getScheduledSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName })) {
@@ -51556,6 +51742,7 @@ async function searchMemory(dto) {
51556
51742
  signalId: signal.id,
51557
51743
  bucketName,
51558
51744
  backtest: isBacktest,
51745
+ when,
51559
51746
  });
51560
51747
  }
51561
51748
  throw new Error(`searchMemory requires a pending or scheduled signal for symbol=${symbol} query=${query}`);
@@ -51588,7 +51775,7 @@ async function listMemory(dto) {
51588
51775
  if (!MethodContextService.hasContext()) {
51589
51776
  throw new Error("listMemory requires a method context");
51590
51777
  }
51591
- const { backtest: isBacktest, symbol } = backtest.executionContextService.context;
51778
+ const { backtest: isBacktest, symbol, when } = backtest.executionContextService.context;
51592
51779
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
51593
51780
  const currentPrice = await backtest.exchangeConnectionService.getAveragePrice(symbol);
51594
51781
  let signal;
@@ -51597,6 +51784,7 @@ async function listMemory(dto) {
51597
51784
  signalId: signal.id,
51598
51785
  bucketName,
51599
51786
  backtest: isBacktest,
51787
+ when,
51600
51788
  });
51601
51789
  }
51602
51790
  if (signal = await backtest.strategyCoreService.getScheduledSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName })) {
@@ -51604,6 +51792,7 @@ async function listMemory(dto) {
51604
51792
  signalId: signal.id,
51605
51793
  bucketName,
51606
51794
  backtest: isBacktest,
51795
+ when,
51607
51796
  });
51608
51797
  }
51609
51798
  throw new Error(`listMemory requires a pending or scheduled signal for symbol=${symbol} bucketName=${bucketName}`);
@@ -51638,7 +51827,7 @@ async function removeMemory(dto) {
51638
51827
  if (!MethodContextService.hasContext()) {
51639
51828
  throw new Error("removeMemory requires a method context");
51640
51829
  }
51641
- const { backtest: isBacktest, symbol } = backtest.executionContextService.context;
51830
+ const { backtest: isBacktest, symbol, when } = backtest.executionContextService.context;
51642
51831
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
51643
51832
  const currentPrice = await backtest.exchangeConnectionService.getAveragePrice(symbol);
51644
51833
  let signal;
@@ -51648,6 +51837,7 @@ async function removeMemory(dto) {
51648
51837
  signalId: signal.id,
51649
51838
  bucketName,
51650
51839
  backtest: isBacktest,
51840
+ when,
51651
51841
  });
51652
51842
  return;
51653
51843
  }
@@ -51657,6 +51847,7 @@ async function removeMemory(dto) {
51657
51847
  signalId: signal.id,
51658
51848
  bucketName,
51659
51849
  backtest: isBacktest,
51850
+ when,
51660
51851
  });
51661
51852
  return;
51662
51853
  }
@@ -51922,6 +52113,9 @@ class DumpMemoryInstance {
51922
52113
  value: { messages },
51923
52114
  description,
51924
52115
  backtest: this.backtest,
52116
+ // when=0: dumps are UI artifacts (agent transcripts, markdown reports);
52117
+ // they must stay visible regardless of the reader's logical time.
52118
+ when: new Date(0)
51925
52119
  });
51926
52120
  }
51927
52121
  /**
@@ -51944,6 +52138,7 @@ class DumpMemoryInstance {
51944
52138
  value: record,
51945
52139
  description,
51946
52140
  backtest: this.backtest,
52141
+ when: new Date(0)
51947
52142
  });
51948
52143
  }
51949
52144
  /**
@@ -51967,6 +52162,7 @@ class DumpMemoryInstance {
51967
52162
  value: { rows },
51968
52163
  description,
51969
52164
  backtest: this.backtest,
52165
+ when: new Date(0)
51970
52166
  });
51971
52167
  }
51972
52168
  /**
@@ -51989,6 +52185,7 @@ class DumpMemoryInstance {
51989
52185
  value: { content },
51990
52186
  description,
51991
52187
  backtest: this.backtest,
52188
+ when: new Date(0)
51992
52189
  });
51993
52190
  }
51994
52191
  /**
@@ -52011,6 +52208,7 @@ class DumpMemoryInstance {
52011
52208
  value: { content },
52012
52209
  description,
52013
52210
  backtest: this.backtest,
52211
+ when: new Date(0)
52014
52212
  });
52015
52213
  }
52016
52214
  /**
@@ -52034,6 +52232,7 @@ class DumpMemoryInstance {
52034
52232
  value: json,
52035
52233
  description,
52036
52234
  backtest: this.backtest,
52235
+ when: new Date(0)
52037
52236
  });
52038
52237
  }
52039
52238
  /** Releases resources held by this instance. */
@@ -60995,8 +61194,8 @@ const CACHE_METHOD_NAME_CLEAR = "CacheUtils.clear";
60995
61194
  const CACHE_METHOD_NAME_RESET_COUNTER = "CacheUtils.resetCounter";
60996
61195
  const CACHE_FILE_INSTANCE_METHOD_NAME_RUN = "CacheFileInstance.run";
60997
61196
  const CACHE_FILE_INSTANCE_METHOD_NAME_HAS_VALUE = "CacheFileInstance.hasValue";
60998
- const MS_PER_MINUTE$1 = 60000;
60999
- const INTERVAL_MINUTES$1 = {
61197
+ const MS_PER_MINUTE$2 = 60000;
61198
+ const INTERVAL_MINUTES$2 = {
61000
61199
  "1m": 1,
61001
61200
  "3m": 3,
61002
61201
  "5m": 5,
@@ -61030,11 +61229,11 @@ const INTERVAL_MINUTES$1 = {
61030
61229
  * ```
61031
61230
  */
61032
61231
  const align$1 = (timestamp, interval) => {
61033
- const intervalMinutes = INTERVAL_MINUTES$1[interval];
61232
+ const intervalMinutes = INTERVAL_MINUTES$2[interval];
61034
61233
  if (!intervalMinutes) {
61035
61234
  throw new Error(`align: unknown interval=${interval}`);
61036
61235
  }
61037
- const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
61236
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$2;
61038
61237
  return Math.floor(timestamp / intervalMs) * intervalMs;
61039
61238
  };
61040
61239
  /**
@@ -61123,7 +61322,7 @@ class CacheFnInstance {
61123
61322
  */
61124
61323
  this.run = (...args) => {
61125
61324
  backtest.loggerService.debug(CACHE_METHOD_NAME_RUN, { args });
61126
- const step = INTERVAL_MINUTES$1[this.interval];
61325
+ const step = INTERVAL_MINUTES$2[this.interval];
61127
61326
  {
61128
61327
  if (!MethodContextService.hasContext()) {
61129
61328
  throw new Error("CacheFnInstance run requires method context");
@@ -61318,7 +61517,7 @@ class CacheFileInstance {
61318
61517
  */
61319
61518
  this.run = async (...args) => {
61320
61519
  backtest.loggerService.debug(CACHE_FILE_INSTANCE_METHOD_NAME_RUN, { args });
61321
- const step = INTERVAL_MINUTES$1[this.interval];
61520
+ const step = INTERVAL_MINUTES$2[this.interval];
61322
61521
  {
61323
61522
  if (!MethodContextService.hasContext()) {
61324
61523
  throw new Error("CacheFileInstance run requires method context");
@@ -61340,7 +61539,7 @@ class CacheFileInstance {
61340
61539
  return cached.data;
61341
61540
  }
61342
61541
  const result = await this.fn.call(null, ...args);
61343
- await PersistMeasureAdapter.writeMeasureData({ id: entityKey, data: result, removed: false }, bucket, entityKey);
61542
+ await PersistMeasureAdapter.writeMeasureData({ id: entityKey, data: result, removed: false }, bucket, entityKey, when);
61344
61543
  return result;
61345
61544
  };
61346
61545
  /**
@@ -61629,8 +61828,8 @@ const INTERVAL_METHOD_NAME_FILE_HAS_VALUE = "IntervalUtils.file.hasValue";
61629
61828
  const INTERVAL_METHOD_NAME_DISPOSE = "IntervalUtils.dispose";
61630
61829
  const INTERVAL_METHOD_NAME_CLEAR = "IntervalUtils.clear";
61631
61830
  const INTERVAL_METHOD_NAME_RESET_COUNTER = "IntervalUtils.resetCounter";
61632
- const MS_PER_MINUTE = 60000;
61633
- const INTERVAL_MINUTES = {
61831
+ const MS_PER_MINUTE$1 = 60000;
61832
+ const INTERVAL_MINUTES$1 = {
61634
61833
  "1m": 1,
61635
61834
  "3m": 3,
61636
61835
  "5m": 5,
@@ -61660,11 +61859,11 @@ const INTERVAL_MINUTES = {
61660
61859
  * ```
61661
61860
  */
61662
61861
  const align = (timestamp, interval) => {
61663
- const intervalMinutes = INTERVAL_MINUTES[interval];
61862
+ const intervalMinutes = INTERVAL_MINUTES$1[interval];
61664
61863
  if (!intervalMinutes) {
61665
61864
  throw new Error(`align: unknown interval=${interval}`);
61666
61865
  }
61667
- const intervalMs = intervalMinutes * MS_PER_MINUTE;
61866
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
61668
61867
  return Math.floor(timestamp / intervalMs) * intervalMs;
61669
61868
  };
61670
61869
  /**
@@ -61739,7 +61938,7 @@ class IntervalFnInstance {
61739
61938
  */
61740
61939
  this.run = (...args) => {
61741
61940
  backtest.loggerService.debug(INTERVAL_METHOD_NAME_RUN, { args });
61742
- const step = INTERVAL_MINUTES[this.interval];
61941
+ const step = INTERVAL_MINUTES$1[this.interval];
61743
61942
  {
61744
61943
  if (!MethodContextService.hasContext()) {
61745
61944
  throw new Error("IntervalFnInstance run requires method context");
@@ -61906,7 +62105,7 @@ class IntervalFileInstance {
61906
62105
  */
61907
62106
  this.run = async (...args) => {
61908
62107
  backtest.loggerService.debug(INTERVAL_FILE_INSTANCE_METHOD_NAME_RUN, { args });
61909
- const step = INTERVAL_MINUTES[this.interval];
62108
+ const step = INTERVAL_MINUTES$1[this.interval];
61910
62109
  {
61911
62110
  if (!MethodContextService.hasContext()) {
61912
62111
  throw new Error("IntervalFileInstance run requires method context");
@@ -61929,7 +62128,7 @@ class IntervalFileInstance {
61929
62128
  }
61930
62129
  const result = await this.fn.call(null, ...args);
61931
62130
  if (result !== null) {
61932
- await PersistIntervalAdapter.writeIntervalData({ id: entityKey, data: result, removed: false }, bucket, entityKey);
62131
+ await PersistIntervalAdapter.writeIntervalData({ id: entityKey, data: result, when, removed: false }, bucket, entityKey, when);
61933
62132
  }
61934
62133
  return result;
61935
62134
  };
@@ -63053,6 +63252,31 @@ class ActionBase {
63053
63252
  // @ts-ignore
63054
63253
  ActionBase = functoolsKit.makeExtendable(ActionBase);
63055
63254
 
63255
+ const MS_PER_MINUTE = 60000;
63256
+ const INTERVAL_MINUTES = {
63257
+ "1m": 1,
63258
+ "3m": 3,
63259
+ "5m": 5,
63260
+ "15m": 15,
63261
+ "30m": 30,
63262
+ "1h": 60,
63263
+ "2h": 120,
63264
+ "4h": 240,
63265
+ "6h": 360,
63266
+ "8h": 480,
63267
+ "1d": 1440,
63268
+ };
63269
+ /**
63270
+ * Returns the step in milliseconds for a given candle interval.
63271
+ * For example, for "15m" interval, it returns 900000 (15 * 60 * 1000).
63272
+ *
63273
+ * @param interval - Candle interval (e.g., "1m", "15m", "1h")
63274
+ * @returns Step in milliseconds corresponding to the interval
63275
+ */
63276
+ const intervalStepMs = (interval) => {
63277
+ return INTERVAL_MINUTES[interval] * MS_PER_MINUTE;
63278
+ };
63279
+
63056
63280
  /**
63057
63281
  * Rounds a price to the appropriate precision based on the tick size.
63058
63282
  *
@@ -63511,6 +63735,7 @@ exports.getWalkerSchema = getWalkerSchema;
63511
63735
  exports.hasNoPendingSignal = hasNoPendingSignal;
63512
63736
  exports.hasNoScheduledSignal = hasNoScheduledSignal;
63513
63737
  exports.hasTradeContext = hasTradeContext;
63738
+ exports.intervalStepMs = intervalStepMs;
63514
63739
  exports.investedCostToPercent = investedCostToPercent;
63515
63740
  exports.lib = backtest;
63516
63741
  exports.listExchangeSchema = listExchangeSchema;