backtest-kit 6.15.0 → 6.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -734,6 +734,11 @@ const schedulePingSubject = new functoolsKit.Subject();
734
734
  * Allows users to track active signal lifecycle and implement custom dynamic management logic.
735
735
  */
736
736
  const activePingSubject = new functoolsKit.Subject();
737
+ /**
738
+ * Idle ping emitter for strategy idle state events.
739
+ * Emits every tick when there is no pending or scheduled signal being monitored.
740
+ */
741
+ const idlePingSubject = new functoolsKit.Subject();
737
742
  /**
738
743
  * Strategy management signal emitter.
739
744
  * Emits when strategy management actions are executed:
@@ -785,6 +790,7 @@ var emitters = /*#__PURE__*/Object.freeze({
785
790
  errorEmitter: errorEmitter,
786
791
  exitEmitter: exitEmitter,
787
792
  highestProfitSubject: highestProfitSubject,
793
+ idlePingSubject: idlePingSubject,
788
794
  maxDrawdownSubject: maxDrawdownSubject,
789
795
  partialLossSubject: partialLossSubject,
790
796
  partialProfitSubject: partialProfitSubject,
@@ -6048,6 +6054,27 @@ const CALL_ACTIVE_PING_CALLBACKS_FN = functoolsKit.trycatch(beginTime(async (sel
6048
6054
  errorEmitter.next(error);
6049
6055
  },
6050
6056
  });
6057
+ const CALL_IDLE_PING_CALLBACKS_FN = functoolsKit.trycatch(beginTime(async (self, symbol, timestamp, backtest, currentPrice) => {
6058
+ await ExecutionContextService.runInContext(async () => {
6059
+ // Call system onIdlePing callback (emits to idlePingSubject)
6060
+ await self.params.onIdlePing(self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, currentPrice, self.params.execution.context.backtest, timestamp);
6061
+ }, {
6062
+ when: new Date(timestamp),
6063
+ symbol: symbol,
6064
+ backtest: backtest,
6065
+ });
6066
+ }), {
6067
+ fallback: (error, self) => {
6068
+ const message = "ClientStrategy CALL_IDLE_PING_CALLBACKS_FN thrown";
6069
+ const payload = {
6070
+ error: functoolsKit.errorData(error),
6071
+ message: functoolsKit.getErrorMessage(error),
6072
+ };
6073
+ self.params.logger.warn(message, payload);
6074
+ console.warn(message, payload);
6075
+ errorEmitter.next(error);
6076
+ },
6077
+ });
6051
6078
  const CALL_ACTIVE_CALLBACKS_FN = functoolsKit.trycatch(beginTime(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
6052
6079
  await ExecutionContextService.runInContext(async () => {
6053
6080
  if (self.params.callbacks?.onActive) {
@@ -6703,6 +6730,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice, backt
6703
6730
  };
6704
6731
  const RETURN_IDLE_FN = async (self, currentPrice) => {
6705
6732
  const currentTime = self.params.execution.context.when.getTime();
6733
+ await CALL_IDLE_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, currentTime, self.params.execution.context.backtest, currentPrice);
6706
6734
  await CALL_IDLE_CALLBACKS_FN(self, self.params.execution.context.symbol, currentPrice, currentTime, self.params.execution.context.backtest);
6707
6735
  const result = {
6708
6736
  action: "idle",
@@ -10375,11 +10403,44 @@ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => functoolsKit.trycatch(async (sy
10375
10403
  },
10376
10404
  defaultValue: null,
10377
10405
  });
10406
+ /**
10407
+ * Creates a callback function for emitting idle ping events.
10408
+ *
10409
+ * Called by ClientStrategy when no active or scheduled signals are present.
10410
+ *
10411
+ * @param self - Reference to StrategyConnectionService instance
10412
+ * @returns Callback function for idle ping events
10413
+ */
10414
+ const CREATE_COMMIT_IDLE_PING_FN = (self) => functoolsKit.trycatch(async (symbol, strategyName, exchangeName, currentPrice, backtest, timestamp) => {
10415
+ const frameName = self.methodContextService.context.frameName;
10416
+ const event = {
10417
+ symbol,
10418
+ strategyName,
10419
+ exchangeName,
10420
+ frameName,
10421
+ currentPrice,
10422
+ backtest,
10423
+ timestamp,
10424
+ };
10425
+ await idlePingSubject.next(event);
10426
+ await self.actionCoreService.pingIdle(backtest, event, { strategyName, exchangeName, frameName });
10427
+ }, {
10428
+ fallback: (error) => {
10429
+ const message = "StrategyConnectionService CREATE_COMMIT_IDLE_PING_FN thrown";
10430
+ const payload = {
10431
+ error: functoolsKit.errorData(error),
10432
+ message: functoolsKit.getErrorMessage(error),
10433
+ };
10434
+ self.loggerService.warn(message, payload);
10435
+ console.warn(message, payload);
10436
+ errorEmitter.next(error);
10437
+ },
10438
+ defaultValue: null,
10439
+ });
10378
10440
  /**
10379
10441
  * Creates a callback function for emitting active ping events.
10380
10442
  *
10381
10443
  * Called by ClientStrategy when an active pending signal is being monitored every minute.
10382
- * Placeholder for future activePingSubject implementation.
10383
10444
  *
10384
10445
  * @param self - Reference to StrategyConnectionService instance
10385
10446
  * @returns Callback function for active ping events
@@ -10626,6 +10687,7 @@ class StrategyConnectionService {
10626
10687
  onInit: CREATE_COMMIT_INIT_FN(this),
10627
10688
  onSchedulePing: CREATE_COMMIT_SCHEDULE_PING_FN(this),
10628
10689
  onActivePing: CREATE_COMMIT_ACTIVE_PING_FN(this),
10690
+ onIdlePing: CREATE_COMMIT_IDLE_PING_FN(this),
10629
10691
  onDispose: CREATE_COMMIT_DISPOSE_FN(this),
10630
10692
  onCommit: CREATE_COMMIT_FN(this),
10631
10693
  onSignalSync: CREATE_SYNC_FN(this, strategyName, exchangeName, frameName, backtest),
@@ -13104,6 +13166,34 @@ const CALL_PING_SCHEDULED_FN = functoolsKit.trycatch(async (event, self) => {
13104
13166
  },
13105
13167
  defaultValue: null,
13106
13168
  });
13169
+ /**
13170
+ * Wrapper to call pingIdle method with error capture.
13171
+ */
13172
+ const CALL_PING_IDLE_FN = functoolsKit.trycatch(async (event, self) => {
13173
+ if (!self._target.pingIdle) {
13174
+ return;
13175
+ }
13176
+ if (await self.params.strategy.hasPendingSignal(event.backtest, event.symbol, {
13177
+ strategyName: event.strategyName,
13178
+ exchangeName: event.exchangeName,
13179
+ frameName: event.frameName,
13180
+ })) {
13181
+ return;
13182
+ }
13183
+ return await self._target.pingIdle(event);
13184
+ }, {
13185
+ fallback: (error) => {
13186
+ const message = "ActionProxy.pingIdle thrown";
13187
+ const payload = {
13188
+ error: functoolsKit.errorData(error),
13189
+ message: functoolsKit.getErrorMessage(error),
13190
+ };
13191
+ LOGGER_SERVICE$4.warn(message, payload);
13192
+ console.warn(message, payload);
13193
+ errorEmitter.next(error);
13194
+ },
13195
+ defaultValue: null,
13196
+ });
13107
13197
  /**
13108
13198
  * Wrapper to call pingActive method with error capture.
13109
13199
  */
@@ -13342,6 +13432,18 @@ class ActionProxy {
13342
13432
  async pingActive(event) {
13343
13433
  return await CALL_PING_ACTIVE_FN(event, this);
13344
13434
  }
13435
+ /**
13436
+ * Handles idle ping events with error capture.
13437
+ *
13438
+ * Wraps the user's pingIdle() method to catch and log any errors.
13439
+ * Called every tick while no signal is pending or scheduled.
13440
+ *
13441
+ * @param event - Idle ping data with symbol, strategy info, current price, timestamp
13442
+ * @returns Promise resolving to user's pingIdle() result or null on error
13443
+ */
13444
+ async pingIdle(event) {
13445
+ return await CALL_PING_IDLE_FN(event, this);
13446
+ }
13345
13447
  /**
13346
13448
  * Handles risk rejection events with error capture.
13347
13449
  *
@@ -13529,6 +13631,23 @@ const CALL_PING_SCHEDULED_CALLBACK_FN = functoolsKit.trycatch(async (self, event
13529
13631
  errorEmitter.next(error);
13530
13632
  },
13531
13633
  });
13634
+ /** Wrapper to call idle ping callback with error handling */
13635
+ const CALL_PING_IDLE_CALLBACK_FN = functoolsKit.trycatch(async (self, event, strategyName, frameName, backtest) => {
13636
+ if (self.params.callbacks?.onPingIdle) {
13637
+ await self.params.callbacks.onPingIdle(event, self.params.actionName, strategyName, frameName, backtest);
13638
+ }
13639
+ }, {
13640
+ fallback: (error, self) => {
13641
+ const message = "ClientAction CALL_PING_IDLE_CALLBACK_FN thrown";
13642
+ const payload = {
13643
+ error: functoolsKit.errorData(error),
13644
+ message: functoolsKit.getErrorMessage(error),
13645
+ };
13646
+ self.params.logger.warn(message, payload);
13647
+ console.warn(message, payload);
13648
+ errorEmitter.next(error);
13649
+ },
13650
+ });
13532
13651
  /** Wrapper to call active ping callback with error handling */
13533
13652
  const CALL_PING_ACTIVE_CALLBACK_FN = functoolsKit.trycatch(async (self, event, strategyName, frameName, backtest) => {
13534
13653
  if (self.params.callbacks?.onPingActive) {
@@ -13895,6 +14014,24 @@ class ClientAction {
13895
14014
  await CALL_PING_ACTIVE_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
13896
14015
  }
13897
14016
  ;
14017
+ /**
14018
+ * Handles idle ping events when no signal is active.
14019
+ */
14020
+ async pingIdle(event) {
14021
+ this.params.logger.debug("ClientAction pingIdle", {
14022
+ actionName: this.params.actionName,
14023
+ strategyName: this.params.strategyName,
14024
+ frameName: this.params.frameName,
14025
+ });
14026
+ if (!this._handlerInstance) {
14027
+ await this.waitForInit();
14028
+ }
14029
+ // Call handler method if defined
14030
+ await this._handlerInstance?.pingIdle(event);
14031
+ // Call callback if defined
14032
+ await CALL_PING_IDLE_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
14033
+ }
14034
+ ;
13898
14035
  /**
13899
14036
  * Handles risk rejection events when signals fail risk validation.
13900
14037
  */
@@ -14147,6 +14284,21 @@ class ActionConnectionService {
14147
14284
  const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
14148
14285
  await action.pingActive(event);
14149
14286
  };
14287
+ /**
14288
+ * Routes idle ping event to appropriate ClientAction instance.
14289
+ *
14290
+ * @param event - Idle ping event data
14291
+ * @param backtest - Whether running in backtest mode
14292
+ * @param context - Execution context with action name, strategy name, exchange name, frame name
14293
+ */
14294
+ this.pingIdle = async (event, backtest, context) => {
14295
+ this.loggerService.log("actionConnectionService pingIdle", {
14296
+ backtest,
14297
+ context,
14298
+ });
14299
+ const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
14300
+ await action.pingIdle(event);
14301
+ };
14150
14302
  /**
14151
14303
  * Routes riskRejection event to appropriate ClientAction instance.
14152
14304
  *
@@ -16250,6 +16402,27 @@ class ActionCoreService {
16250
16402
  await this.actionConnectionService.pingActive(event, backtest, { actionName, ...context });
16251
16403
  }
16252
16404
  };
16405
+ /**
16406
+ * Routes idle ping event to all registered actions for the strategy.
16407
+ *
16408
+ * Retrieves action list from strategy schema (IStrategySchema.actions)
16409
+ * and invokes the pingIdle handler on each ClientAction instance sequentially.
16410
+ * Called every tick when there is no pending or scheduled signal being monitored.
16411
+ *
16412
+ * @param backtest - Whether running in backtest mode (true) or live mode (false)
16413
+ * @param event - Idle state monitoring data
16414
+ * @param context - Strategy execution context with strategyName, exchangeName, frameName
16415
+ */
16416
+ this.pingIdle = async (backtest, event, context) => {
16417
+ this.loggerService.log("actionCoreService pingIdle", {
16418
+ context,
16419
+ });
16420
+ await this.validate(context);
16421
+ const { actions = [] } = this.strategySchemaService.get(context.strategyName);
16422
+ for (const actionName of actions) {
16423
+ await this.actionConnectionService.pingIdle(event, backtest, { actionName, ...context });
16424
+ }
16425
+ };
16253
16426
  /**
16254
16427
  * Routes risk rejection event to all registered actions for the strategy.
16255
16428
  *
@@ -37820,6 +37993,8 @@ const LISTEN_SCHEDULE_PING_METHOD_NAME = "event.listenSchedulePing";
37820
37993
  const LISTEN_SCHEDULE_PING_ONCE_METHOD_NAME = "event.listenSchedulePingOnce";
37821
37994
  const LISTEN_ACTIVE_PING_METHOD_NAME = "event.listenActivePing";
37822
37995
  const LISTEN_ACTIVE_PING_ONCE_METHOD_NAME = "event.listenActivePingOnce";
37996
+ const LISTEN_IDLE_PING_METHOD_NAME = "event.listenIdlePing";
37997
+ const LISTEN_IDLE_PING_ONCE_METHOD_NAME = "event.listenIdlePingOnce";
37823
37998
  const LISTEN_STRATEGY_COMMIT_METHOD_NAME = "event.listenStrategyCommit";
37824
37999
  const LISTEN_STRATEGY_COMMIT_ONCE_METHOD_NAME = "event.listenStrategyCommitOnce";
37825
38000
  const LISTEN_SYNC_METHOD_NAME = "event.listenSync";
@@ -38995,6 +39170,45 @@ function listenActivePingOnce(filterFn, fn) {
38995
39170
  };
38996
39171
  return disposeFn = listenActivePing(wrappedFn);
38997
39172
  }
39173
+ /**
39174
+ * Subscribes to idle ping events with queued async processing.
39175
+ *
39176
+ * Emits every tick when there is no pending or scheduled signal being monitored.
39177
+ *
39178
+ * @param fn - Callback function to handle idle ping events
39179
+ * @returns Unsubscribe function to stop listening
39180
+ */
39181
+ function listenIdlePing(fn) {
39182
+ backtest.loggerService.log(LISTEN_IDLE_PING_METHOD_NAME);
39183
+ const wrappedFn = async (event) => {
39184
+ if (await functoolsKit.not(backtest.strategyCoreService.hasPendingSignal(event.backtest, event.symbol, {
39185
+ strategyName: event.strategyName,
39186
+ exchangeName: event.exchangeName,
39187
+ frameName: event.frameName,
39188
+ }))) {
39189
+ await fn(event);
39190
+ }
39191
+ };
39192
+ return idlePingSubject.subscribe(functoolsKit.queued(wrappedFn));
39193
+ }
39194
+ /**
39195
+ * Subscribes to filtered idle ping events with one-time execution.
39196
+ *
39197
+ * @param filterFn - Predicate to filter events
39198
+ * @param fn - Callback function to handle the matching event
39199
+ * @returns Unsubscribe function to cancel the listener before it fires
39200
+ */
39201
+ function listenIdlePingOnce(filterFn, fn) {
39202
+ backtest.loggerService.log(LISTEN_IDLE_PING_ONCE_METHOD_NAME);
39203
+ let disposeFn;
39204
+ const wrappedFn = async (event) => {
39205
+ if (filterFn(event)) {
39206
+ await fn(event);
39207
+ disposeFn && disposeFn();
39208
+ }
39209
+ };
39210
+ return disposeFn = listenIdlePing(wrappedFn);
39211
+ }
38998
39212
  /**
38999
39213
  * Subscribes to strategy management events with queued async processing.
39000
39214
  *
@@ -45952,6 +46166,7 @@ const RECENT_LIVE_ADAPTER_METHOD_NAME_CLEAR = "RecentLiveAdapter.clear";
45952
46166
  const RECENT_ADAPTER_METHOD_NAME_ENABLE = "RecentAdapter.enable";
45953
46167
  const RECENT_ADAPTER_METHOD_NAME_DISABLE = "RecentAdapter.disable";
45954
46168
  const RECENT_ADAPTER_METHOD_NAME_GET_LATEST_SIGNAL = "RecentAdapter.getLatestSignal";
46169
+ const RECENT_ADAPTER_METHOD_NAME_GET_MINUTES_SINCE_LATEST_SIGNAL = "RecentAdapter.getMinutesSinceLatestSignalCreated";
45955
46170
  /**
45956
46171
  * Builds a composite storage key from context parts.
45957
46172
  * Includes backtest flag as the last segment to prevent live/backtest collisions.
@@ -46010,6 +46225,23 @@ class RecentPersistBacktestUtils {
46010
46225
  });
46011
46226
  return await PersistRecentAdapter.readRecentData(symbol, strategyName, exchangeName, frameName, backtest$1);
46012
46227
  };
46228
+ /**
46229
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46230
+ * @param timestamp - Current timestamp in milliseconds
46231
+ * @param symbol - Trading pair symbol
46232
+ * @param strategyName - Strategy identifier
46233
+ * @param exchangeName - Exchange identifier
46234
+ * @param frameName - Frame identifier
46235
+ * @param backtest - Flag indicating if the context is backtest or live
46236
+ * @returns Whole minutes since the latest signal was created, or null if no signal found
46237
+ */
46238
+ this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest) => {
46239
+ const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest);
46240
+ if (!signal) {
46241
+ return null;
46242
+ }
46243
+ return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
46244
+ };
46013
46245
  }
46014
46246
  }
46015
46247
  /**
@@ -46052,6 +46284,23 @@ class RecentMemoryBacktestUtils {
46052
46284
  backtest.loggerService.info(RECENT_MEMORY_BACKTEST_METHOD_NAME_GET_LATEST_SIGNAL, { key });
46053
46285
  return this._signals.get(key) ?? null;
46054
46286
  };
46287
+ /**
46288
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46289
+ * @param timestamp - Current timestamp in milliseconds
46290
+ * @param symbol - Trading pair symbol
46291
+ * @param strategyName - Strategy identifier
46292
+ * @param exchangeName - Exchange identifier
46293
+ * @param frameName - Frame identifier
46294
+ * @param backtest - Flag indicating if the context is backtest or live
46295
+ * @returns Whole minutes since the latest signal was created, or null if no signal found
46296
+ */
46297
+ this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest) => {
46298
+ const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest);
46299
+ if (!signal) {
46300
+ return null;
46301
+ }
46302
+ return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
46303
+ };
46055
46304
  }
46056
46305
  }
46057
46306
  /**
@@ -46095,6 +46344,23 @@ class RecentPersistLiveUtils {
46095
46344
  });
46096
46345
  return await PersistRecentAdapter.readRecentData(symbol, strategyName, exchangeName, frameName, backtest$1);
46097
46346
  };
46347
+ /**
46348
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46349
+ * @param timestamp - Current timestamp in milliseconds
46350
+ * @param symbol - Trading pair symbol
46351
+ * @param strategyName - Strategy identifier
46352
+ * @param exchangeName - Exchange identifier
46353
+ * @param frameName - Frame identifier
46354
+ * @param backtest - Flag indicating if the context is backtest or live
46355
+ * @returns Whole minutes since the latest signal was created, or null if no signal found
46356
+ */
46357
+ this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest) => {
46358
+ const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest);
46359
+ if (!signal) {
46360
+ return null;
46361
+ }
46362
+ return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
46363
+ };
46098
46364
  }
46099
46365
  }
46100
46366
  /**
@@ -46137,6 +46403,23 @@ class RecentMemoryLiveUtils {
46137
46403
  backtest.loggerService.info(RECENT_MEMORY_LIVE_METHOD_NAME_GET_LATEST_SIGNAL, { key });
46138
46404
  return this._signals.get(key) ?? null;
46139
46405
  };
46406
+ /**
46407
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46408
+ * @param timestamp - Current timestamp in milliseconds
46409
+ * @param symbol - Trading pair symbol
46410
+ * @param strategyName - Strategy identifier
46411
+ * @param exchangeName - Exchange identifier
46412
+ * @param frameName - Frame identifier
46413
+ * @param backtest - Flag indicating if the context is backtest or live
46414
+ * @returns Whole minutes since the latest signal was created, or null if no signal found
46415
+ */
46416
+ this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest) => {
46417
+ const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest);
46418
+ if (!signal) {
46419
+ return null;
46420
+ }
46421
+ return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
46422
+ };
46140
46423
  }
46141
46424
  }
46142
46425
  /**
@@ -46183,6 +46466,32 @@ class RecentBacktestAdapter {
46183
46466
  });
46184
46467
  return await this._recentBacktestUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1);
46185
46468
  };
46469
+ /**
46470
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46471
+ * Proxies call to the underlying storage adapter.
46472
+ * @param timestamp - Current timestamp in milliseconds
46473
+ * @param symbol - Trading pair symbol
46474
+ * @param strategyName - Strategy identifier
46475
+ * @param exchangeName - Exchange identifier
46476
+ * @param frameName - Frame identifier
46477
+ * @param backtest - Flag indicating if the context is backtest or live
46478
+ * @returns Whole minutes since the latest signal was created, or null if no signal found
46479
+ */
46480
+ this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest$1) => {
46481
+ backtest.loggerService.info(RECENT_BACKTEST_ADAPTER_METHOD_NAME_GET_LATEST_SIGNAL, {
46482
+ symbol,
46483
+ strategyName,
46484
+ exchangeName,
46485
+ frameName,
46486
+ backtest: backtest$1,
46487
+ timestamp,
46488
+ });
46489
+ const signal = await this._recentBacktestUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1);
46490
+ if (!signal) {
46491
+ return null;
46492
+ }
46493
+ return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
46494
+ };
46186
46495
  /**
46187
46496
  * Sets the storage adapter constructor.
46188
46497
  * All future storage operations will use this adapter.
@@ -46261,6 +46570,32 @@ class RecentLiveAdapter {
46261
46570
  });
46262
46571
  return await this._recentLiveUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1);
46263
46572
  };
46573
+ /**
46574
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46575
+ * Proxies call to the underlying storage adapter.
46576
+ * @param timestamp - Current timestamp in milliseconds
46577
+ * @param symbol - Trading pair symbol
46578
+ * @param strategyName - Strategy identifier
46579
+ * @param exchangeName - Exchange identifier
46580
+ * @param frameName - Frame identifier
46581
+ * @param backtest - Flag indicating if the context is backtest or live
46582
+ * @returns Whole minutes since the latest signal was created, or null if no signal found
46583
+ */
46584
+ this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest$1) => {
46585
+ backtest.loggerService.info(RECENT_LIVE_ADAPTER_METHOD_NAME_GET_LATEST_SIGNAL, {
46586
+ symbol,
46587
+ strategyName,
46588
+ exchangeName,
46589
+ frameName,
46590
+ backtest: backtest$1,
46591
+ timestamp,
46592
+ });
46593
+ const signal = await this._recentLiveUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1);
46594
+ if (!signal) {
46595
+ return null;
46596
+ }
46597
+ return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
46598
+ };
46264
46599
  /**
46265
46600
  * Sets the storage adapter constructor.
46266
46601
  * All future storage operations will use this adapter.
@@ -46369,6 +46704,27 @@ class RecentAdapter {
46369
46704
  }
46370
46705
  return null;
46371
46706
  };
46707
+ /**
46708
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46709
+ * Searches backtest storage first, then live storage.
46710
+ * @param timestamp - Current timestamp in milliseconds
46711
+ * @param symbol - Trading pair symbol
46712
+ * @param context - Execution context with strategyName, exchangeName, and frameName
46713
+ * @returns Whole minutes since the latest signal was created, or null if no signal found
46714
+ * @throws Error if RecentAdapter is not enabled
46715
+ */
46716
+ this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, context) => {
46717
+ backtest.loggerService.info(RECENT_ADAPTER_METHOD_NAME_GET_MINUTES_SINCE_LATEST_SIGNAL, {
46718
+ symbol,
46719
+ context,
46720
+ timestamp,
46721
+ });
46722
+ const signal = await this.getLatestSignal(symbol, context);
46723
+ if (!signal) {
46724
+ return null;
46725
+ }
46726
+ return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
46727
+ };
46372
46728
  }
46373
46729
  }
46374
46730
  /**
@@ -46388,6 +46744,7 @@ const RecentLive = new RecentLiveAdapter();
46388
46744
  const RecentBacktest = new RecentBacktestAdapter();
46389
46745
 
46390
46746
  const GET_LATEST_SIGNAL_METHOD_NAME = "signal.getLatestSignal";
46747
+ const GET_MINUTES_SINCE_LATEST_SIGNAL_CREATED_METHOD_NAME = "signal.getMinutesSinceLatestSignalCreated";
46391
46748
  /**
46392
46749
  * Returns the latest signal (pending or closed) for the current strategy context.
46393
46750
  *
@@ -46425,6 +46782,42 @@ async function getLatestSignal(symbol) {
46425
46782
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
46426
46783
  return await Recent.getLatestSignal(symbol, { exchangeName, frameName, strategyName });
46427
46784
  }
46785
+ /**
46786
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46787
+ *
46788
+ * Does not distinguish between active and closed signals — measures time since
46789
+ * whichever signal was recorded last. Useful for cooldown logic after a stop-loss.
46790
+ *
46791
+ * Searches backtest storage first, then live storage.
46792
+ * Returns null if no signal exists at all.
46793
+ *
46794
+ * Automatically detects backtest/live mode from execution context.
46795
+ *
46796
+ * @param symbol - Trading pair symbol
46797
+ * @param timestamp - Current timestamp in milliseconds
46798
+ * @returns Promise resolving to whole minutes since the latest signal was created, or null
46799
+ *
46800
+ * @example
46801
+ * ```typescript
46802
+ * import { getMinutesSinceLatestSignalCreated } from "backtest-kit";
46803
+ *
46804
+ * const minutes = await getMinutesSinceLatestSignalCreated("BTCUSDT", Date.now());
46805
+ * if (minutes !== null && minutes < 24 * 60) {
46806
+ * return; // cooldown — skip new signal for 24 hours after last signal
46807
+ * }
46808
+ * ```
46809
+ */
46810
+ async function getMinutesSinceLatestSignalCreated(symbol, timestamp) {
46811
+ backtest.loggerService.info(GET_MINUTES_SINCE_LATEST_SIGNAL_CREATED_METHOD_NAME, { symbol, timestamp });
46812
+ if (!ExecutionContextService.hasContext()) {
46813
+ throw new Error("getMinutesSinceLatestSignalCreated requires an execution context");
46814
+ }
46815
+ if (!MethodContextService.hasContext()) {
46816
+ throw new Error("getMinutesSinceLatestSignalCreated requires a method context");
46817
+ }
46818
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
46819
+ return await Recent.getMinutesSinceLatestSignalCreated(timestamp, symbol, { exchangeName, frameName, strategyName });
46820
+ }
46428
46821
 
46429
46822
  const DEFAULT_BM25_K1 = 1.5;
46430
46823
  const DEFAULT_BM25_B = 0.75;
@@ -49752,6 +50145,7 @@ const METHOD_NAME_CREATE_SNAPSHOT = "SessionUtils.createSnapshot";
49752
50145
  /** List of all global subjects whose listeners should be snapshotted for session isolation */
49753
50146
  const SUBJECT_ISOLATION_LIST = [
49754
50147
  activePingSubject,
50148
+ idlePingSubject,
49755
50149
  backtestScheduleOpenSubject,
49756
50150
  breakevenSubject,
49757
50151
  doneBacktestSubject,
@@ -57872,6 +58266,7 @@ const METHOD_NAME_PARTIAL_PROFIT_AVAILABLE = "ActionBase.partialProfitAvailable"
57872
58266
  const METHOD_NAME_PARTIAL_LOSS_AVAILABLE = "ActionBase.partialLossAvailable";
57873
58267
  const METHOD_NAME_PING_SCHEDULED = "ActionBase.pingScheduled";
57874
58268
  const METHOD_NAME_PING_ACTIVE = "ActionBase.pingActive";
58269
+ const METHOD_NAME_PING_IDLE = "ActionBase.pingIdle";
57875
58270
  const METHOD_NAME_RISK_REJECTION = "ActionBase.riskRejection";
57876
58271
  const METHOD_NAME_DISPOSE = "ActionBase.dispose";
57877
58272
  const DEFAULT_SOURCE = "default";
@@ -58265,6 +58660,26 @@ class ActionBase {
58265
58660
  source,
58266
58661
  });
58267
58662
  }
58663
+ /**
58664
+ * Handles idle ping events when no signal is active.
58665
+ *
58666
+ * Called every tick while no signal is pending or scheduled.
58667
+ * Use to monitor idle strategy state and implement entry condition logic.
58668
+ *
58669
+ * Triggered by: ActionCoreService.pingIdle() via StrategyConnectionService
58670
+ * Source: idlePingSubject.next() in CREATE_COMMIT_IDLE_PING_FN callback
58671
+ * Frequency: Every tick while no signal is pending or scheduled
58672
+ *
58673
+ * Default implementation: Logs idle ping event.
58674
+ *
58675
+ * @param event - Idle ping data with symbol, strategy info, current price, timestamp
58676
+ */
58677
+ pingIdle(event, source = DEFAULT_SOURCE) {
58678
+ LOGGER_SERVICE.info(METHOD_NAME_PING_IDLE, {
58679
+ event,
58680
+ source,
58681
+ });
58682
+ }
58268
58683
  /**
58269
58684
  * Handles risk rejection events when signals fail risk validation.
58270
58685
  *
@@ -58708,6 +59123,7 @@ exports.getFrameSchema = getFrameSchema;
58708
59123
  exports.getLatestSignal = getLatestSignal;
58709
59124
  exports.getMaxDrawdownDistancePnlCost = getMaxDrawdownDistancePnlCost;
58710
59125
  exports.getMaxDrawdownDistancePnlPercentage = getMaxDrawdownDistancePnlPercentage;
59126
+ exports.getMinutesSinceLatestSignalCreated = getMinutesSinceLatestSignalCreated;
58711
59127
  exports.getMode = getMode;
58712
59128
  exports.getNextCandles = getNextCandles;
58713
59129
  exports.getOrderBook = getOrderBook;
@@ -58780,6 +59196,8 @@ exports.listenError = listenError;
58780
59196
  exports.listenExit = listenExit;
58781
59197
  exports.listenHighestProfit = listenHighestProfit;
58782
59198
  exports.listenHighestProfitOnce = listenHighestProfitOnce;
59199
+ exports.listenIdlePing = listenIdlePing;
59200
+ exports.listenIdlePingOnce = listenIdlePingOnce;
58783
59201
  exports.listenMaxDrawdown = listenMaxDrawdown;
58784
59202
  exports.listenMaxDrawdownOnce = listenMaxDrawdownOnce;
58785
59203
  exports.listenPartialLossAvailable = listenPartialLossAvailable;