backtest-kit 6.15.0 → 7.0.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,
@@ -930,7 +936,6 @@ const INTERVAL_MINUTES$9 = {
930
936
  "6h": 360,
931
937
  "8h": 480,
932
938
  "1d": 1440,
933
- "1w": 10080,
934
939
  };
935
940
  const MS_PER_MINUTE$7 = 60000;
936
941
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
@@ -3110,7 +3115,6 @@ const INTERVAL_MINUTES$8 = {
3110
3115
  "6h": 360,
3111
3116
  "8h": 480,
3112
3117
  "1d": 1440,
3113
- "1w": 10080,
3114
3118
  };
3115
3119
  /**
3116
3120
  * Aligns timestamp down to the nearest interval boundary.
@@ -6048,6 +6052,27 @@ const CALL_ACTIVE_PING_CALLBACKS_FN = functoolsKit.trycatch(beginTime(async (sel
6048
6052
  errorEmitter.next(error);
6049
6053
  },
6050
6054
  });
6055
+ const CALL_IDLE_PING_CALLBACKS_FN = functoolsKit.trycatch(beginTime(async (self, symbol, timestamp, backtest, currentPrice) => {
6056
+ await ExecutionContextService.runInContext(async () => {
6057
+ // Call system onIdlePing callback (emits to idlePingSubject)
6058
+ 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);
6059
+ }, {
6060
+ when: new Date(timestamp),
6061
+ symbol: symbol,
6062
+ backtest: backtest,
6063
+ });
6064
+ }), {
6065
+ fallback: (error, self) => {
6066
+ const message = "ClientStrategy CALL_IDLE_PING_CALLBACKS_FN thrown";
6067
+ const payload = {
6068
+ error: functoolsKit.errorData(error),
6069
+ message: functoolsKit.getErrorMessage(error),
6070
+ };
6071
+ self.params.logger.warn(message, payload);
6072
+ console.warn(message, payload);
6073
+ errorEmitter.next(error);
6074
+ },
6075
+ });
6051
6076
  const CALL_ACTIVE_CALLBACKS_FN = functoolsKit.trycatch(beginTime(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
6052
6077
  await ExecutionContextService.runInContext(async () => {
6053
6078
  if (self.params.callbacks?.onActive) {
@@ -6703,6 +6728,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice, backt
6703
6728
  };
6704
6729
  const RETURN_IDLE_FN = async (self, currentPrice) => {
6705
6730
  const currentTime = self.params.execution.context.when.getTime();
6731
+ await CALL_IDLE_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, currentTime, self.params.execution.context.backtest, currentPrice);
6706
6732
  await CALL_IDLE_CALLBACKS_FN(self, self.params.execution.context.symbol, currentPrice, currentTime, self.params.execution.context.backtest);
6707
6733
  const result = {
6708
6734
  action: "idle",
@@ -10375,11 +10401,44 @@ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => functoolsKit.trycatch(async (sy
10375
10401
  },
10376
10402
  defaultValue: null,
10377
10403
  });
10404
+ /**
10405
+ * Creates a callback function for emitting idle ping events.
10406
+ *
10407
+ * Called by ClientStrategy when no active or scheduled signals are present.
10408
+ *
10409
+ * @param self - Reference to StrategyConnectionService instance
10410
+ * @returns Callback function for idle ping events
10411
+ */
10412
+ const CREATE_COMMIT_IDLE_PING_FN = (self) => functoolsKit.trycatch(async (symbol, strategyName, exchangeName, currentPrice, backtest, timestamp) => {
10413
+ const frameName = self.methodContextService.context.frameName;
10414
+ const event = {
10415
+ symbol,
10416
+ strategyName,
10417
+ exchangeName,
10418
+ frameName,
10419
+ currentPrice,
10420
+ backtest,
10421
+ timestamp,
10422
+ };
10423
+ await idlePingSubject.next(event);
10424
+ await self.actionCoreService.pingIdle(backtest, event, { strategyName, exchangeName, frameName });
10425
+ }, {
10426
+ fallback: (error) => {
10427
+ const message = "StrategyConnectionService CREATE_COMMIT_IDLE_PING_FN thrown";
10428
+ const payload = {
10429
+ error: functoolsKit.errorData(error),
10430
+ message: functoolsKit.getErrorMessage(error),
10431
+ };
10432
+ self.loggerService.warn(message, payload);
10433
+ console.warn(message, payload);
10434
+ errorEmitter.next(error);
10435
+ },
10436
+ defaultValue: null,
10437
+ });
10378
10438
  /**
10379
10439
  * Creates a callback function for emitting active ping events.
10380
10440
  *
10381
10441
  * Called by ClientStrategy when an active pending signal is being monitored every minute.
10382
- * Placeholder for future activePingSubject implementation.
10383
10442
  *
10384
10443
  * @param self - Reference to StrategyConnectionService instance
10385
10444
  * @returns Callback function for active ping events
@@ -10626,6 +10685,7 @@ class StrategyConnectionService {
10626
10685
  onInit: CREATE_COMMIT_INIT_FN(this),
10627
10686
  onSchedulePing: CREATE_COMMIT_SCHEDULE_PING_FN(this),
10628
10687
  onActivePing: CREATE_COMMIT_ACTIVE_PING_FN(this),
10688
+ onIdlePing: CREATE_COMMIT_IDLE_PING_FN(this),
10629
10689
  onDispose: CREATE_COMMIT_DISPOSE_FN(this),
10630
10690
  onCommit: CREATE_COMMIT_FN(this),
10631
10691
  onSignalSync: CREATE_SYNC_FN(this, strategyName, exchangeName, frameName, backtest),
@@ -11977,8 +12037,6 @@ const INTERVAL_MINUTES$6 = {
11977
12037
  "8h": 480,
11978
12038
  "12h": 720,
11979
12039
  "1d": 1440,
11980
- "1w": 10080,
11981
- "1M": 43200,
11982
12040
  };
11983
12041
  /**
11984
12042
  * Wrapper to call onTimeframe callback with error handling.
@@ -12044,7 +12102,7 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
12044
12102
  * Features:
12045
12103
  * - Generates timestamp arrays for backtest iteration
12046
12104
  * - Singleshot caching prevents redundant generation
12047
- * - Configurable interval spacing (1m to 3d)
12105
+ * - Configurable interval spacing (1m to 1d)
12048
12106
  * - Callback support for validation and logging
12049
12107
  *
12050
12108
  * Used by BacktestLogicPrivateService to iterate through historical periods.
@@ -12398,7 +12456,6 @@ const INTERVAL_MINUTES$5 = {
12398
12456
  "6h": 360,
12399
12457
  "8h": 480,
12400
12458
  "1d": 1440,
12401
- "1w": 10080,
12402
12459
  };
12403
12460
  /**
12404
12461
  * Aligns timestamp down to the nearest interval boundary.
@@ -13104,6 +13161,34 @@ const CALL_PING_SCHEDULED_FN = functoolsKit.trycatch(async (event, self) => {
13104
13161
  },
13105
13162
  defaultValue: null,
13106
13163
  });
13164
+ /**
13165
+ * Wrapper to call pingIdle method with error capture.
13166
+ */
13167
+ const CALL_PING_IDLE_FN = functoolsKit.trycatch(async (event, self) => {
13168
+ if (!self._target.pingIdle) {
13169
+ return;
13170
+ }
13171
+ if (await self.params.strategy.hasPendingSignal(event.backtest, event.symbol, {
13172
+ strategyName: event.strategyName,
13173
+ exchangeName: event.exchangeName,
13174
+ frameName: event.frameName,
13175
+ })) {
13176
+ return;
13177
+ }
13178
+ return await self._target.pingIdle(event);
13179
+ }, {
13180
+ fallback: (error) => {
13181
+ const message = "ActionProxy.pingIdle thrown";
13182
+ const payload = {
13183
+ error: functoolsKit.errorData(error),
13184
+ message: functoolsKit.getErrorMessage(error),
13185
+ };
13186
+ LOGGER_SERVICE$4.warn(message, payload);
13187
+ console.warn(message, payload);
13188
+ errorEmitter.next(error);
13189
+ },
13190
+ defaultValue: null,
13191
+ });
13107
13192
  /**
13108
13193
  * Wrapper to call pingActive method with error capture.
13109
13194
  */
@@ -13342,6 +13427,18 @@ class ActionProxy {
13342
13427
  async pingActive(event) {
13343
13428
  return await CALL_PING_ACTIVE_FN(event, this);
13344
13429
  }
13430
+ /**
13431
+ * Handles idle ping events with error capture.
13432
+ *
13433
+ * Wraps the user's pingIdle() method to catch and log any errors.
13434
+ * Called every tick while no signal is pending or scheduled.
13435
+ *
13436
+ * @param event - Idle ping data with symbol, strategy info, current price, timestamp
13437
+ * @returns Promise resolving to user's pingIdle() result or null on error
13438
+ */
13439
+ async pingIdle(event) {
13440
+ return await CALL_PING_IDLE_FN(event, this);
13441
+ }
13345
13442
  /**
13346
13443
  * Handles risk rejection events with error capture.
13347
13444
  *
@@ -13529,6 +13626,23 @@ const CALL_PING_SCHEDULED_CALLBACK_FN = functoolsKit.trycatch(async (self, event
13529
13626
  errorEmitter.next(error);
13530
13627
  },
13531
13628
  });
13629
+ /** Wrapper to call idle ping callback with error handling */
13630
+ const CALL_PING_IDLE_CALLBACK_FN = functoolsKit.trycatch(async (self, event, strategyName, frameName, backtest) => {
13631
+ if (self.params.callbacks?.onPingIdle) {
13632
+ await self.params.callbacks.onPingIdle(event, self.params.actionName, strategyName, frameName, backtest);
13633
+ }
13634
+ }, {
13635
+ fallback: (error, self) => {
13636
+ const message = "ClientAction CALL_PING_IDLE_CALLBACK_FN thrown";
13637
+ const payload = {
13638
+ error: functoolsKit.errorData(error),
13639
+ message: functoolsKit.getErrorMessage(error),
13640
+ };
13641
+ self.params.logger.warn(message, payload);
13642
+ console.warn(message, payload);
13643
+ errorEmitter.next(error);
13644
+ },
13645
+ });
13532
13646
  /** Wrapper to call active ping callback with error handling */
13533
13647
  const CALL_PING_ACTIVE_CALLBACK_FN = functoolsKit.trycatch(async (self, event, strategyName, frameName, backtest) => {
13534
13648
  if (self.params.callbacks?.onPingActive) {
@@ -13895,6 +14009,24 @@ class ClientAction {
13895
14009
  await CALL_PING_ACTIVE_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
13896
14010
  }
13897
14011
  ;
14012
+ /**
14013
+ * Handles idle ping events when no signal is active.
14014
+ */
14015
+ async pingIdle(event) {
14016
+ this.params.logger.debug("ClientAction pingIdle", {
14017
+ actionName: this.params.actionName,
14018
+ strategyName: this.params.strategyName,
14019
+ frameName: this.params.frameName,
14020
+ });
14021
+ if (!this._handlerInstance) {
14022
+ await this.waitForInit();
14023
+ }
14024
+ // Call handler method if defined
14025
+ await this._handlerInstance?.pingIdle(event);
14026
+ // Call callback if defined
14027
+ await CALL_PING_IDLE_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
14028
+ }
14029
+ ;
13898
14030
  /**
13899
14031
  * Handles risk rejection events when signals fail risk validation.
13900
14032
  */
@@ -14147,6 +14279,21 @@ class ActionConnectionService {
14147
14279
  const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
14148
14280
  await action.pingActive(event);
14149
14281
  };
14282
+ /**
14283
+ * Routes idle ping event to appropriate ClientAction instance.
14284
+ *
14285
+ * @param event - Idle ping event data
14286
+ * @param backtest - Whether running in backtest mode
14287
+ * @param context - Execution context with action name, strategy name, exchange name, frame name
14288
+ */
14289
+ this.pingIdle = async (event, backtest, context) => {
14290
+ this.loggerService.log("actionConnectionService pingIdle", {
14291
+ backtest,
14292
+ context,
14293
+ });
14294
+ const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
14295
+ await action.pingIdle(event);
14296
+ };
14150
14297
  /**
14151
14298
  * Routes riskRejection event to appropriate ClientAction instance.
14152
14299
  *
@@ -16250,6 +16397,27 @@ class ActionCoreService {
16250
16397
  await this.actionConnectionService.pingActive(event, backtest, { actionName, ...context });
16251
16398
  }
16252
16399
  };
16400
+ /**
16401
+ * Routes idle ping event to all registered actions for the strategy.
16402
+ *
16403
+ * Retrieves action list from strategy schema (IStrategySchema.actions)
16404
+ * and invokes the pingIdle handler on each ClientAction instance sequentially.
16405
+ * Called every tick when there is no pending or scheduled signal being monitored.
16406
+ *
16407
+ * @param backtest - Whether running in backtest mode (true) or live mode (false)
16408
+ * @param event - Idle state monitoring data
16409
+ * @param context - Strategy execution context with strategyName, exchangeName, frameName
16410
+ */
16411
+ this.pingIdle = async (backtest, event, context) => {
16412
+ this.loggerService.log("actionCoreService pingIdle", {
16413
+ context,
16414
+ });
16415
+ await this.validate(context);
16416
+ const { actions = [] } = this.strategySchemaService.get(context.strategyName);
16417
+ for (const actionName of actions) {
16418
+ await this.actionConnectionService.pingIdle(event, backtest, { actionName, ...context });
16419
+ }
16420
+ };
16253
16421
  /**
16254
16422
  * Routes risk rejection event to all registered actions for the strategy.
16255
16423
  *
@@ -17659,7 +17827,6 @@ const INTERVAL_MINUTES$4 = {
17659
17827
  "6h": 360,
17660
17828
  "8h": 480,
17661
17829
  "1d": 1440,
17662
- "1w": 10080,
17663
17830
  };
17664
17831
  const createEmitter = functoolsKit.memoize(([interval]) => `${interval}`, (interval) => {
17665
17832
  const tickSubject = new functoolsKit.Subject();
@@ -32872,7 +33039,6 @@ const INTERVAL_MINUTES$3 = {
32872
33039
  "6h": 360,
32873
33040
  "8h": 480,
32874
33041
  "1d": 1440,
32875
- "1w": 10080,
32876
33042
  };
32877
33043
  /**
32878
33044
  * Aligns timestamp down to the nearest interval boundary.
@@ -33623,7 +33789,6 @@ const INTERVAL_MINUTES$2 = {
33623
33789
  "6h": 360,
33624
33790
  "8h": 480,
33625
33791
  "1d": 1440,
33626
- "1w": 10080,
33627
33792
  };
33628
33793
  const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
33629
33794
  const intervalMs = intervalMinutes * MS_PER_MINUTE$2;
@@ -37820,6 +37985,8 @@ const LISTEN_SCHEDULE_PING_METHOD_NAME = "event.listenSchedulePing";
37820
37985
  const LISTEN_SCHEDULE_PING_ONCE_METHOD_NAME = "event.listenSchedulePingOnce";
37821
37986
  const LISTEN_ACTIVE_PING_METHOD_NAME = "event.listenActivePing";
37822
37987
  const LISTEN_ACTIVE_PING_ONCE_METHOD_NAME = "event.listenActivePingOnce";
37988
+ const LISTEN_IDLE_PING_METHOD_NAME = "event.listenIdlePing";
37989
+ const LISTEN_IDLE_PING_ONCE_METHOD_NAME = "event.listenIdlePingOnce";
37823
37990
  const LISTEN_STRATEGY_COMMIT_METHOD_NAME = "event.listenStrategyCommit";
37824
37991
  const LISTEN_STRATEGY_COMMIT_ONCE_METHOD_NAME = "event.listenStrategyCommitOnce";
37825
37992
  const LISTEN_SYNC_METHOD_NAME = "event.listenSync";
@@ -38995,6 +39162,45 @@ function listenActivePingOnce(filterFn, fn) {
38995
39162
  };
38996
39163
  return disposeFn = listenActivePing(wrappedFn);
38997
39164
  }
39165
+ /**
39166
+ * Subscribes to idle ping events with queued async processing.
39167
+ *
39168
+ * Emits every tick when there is no pending or scheduled signal being monitored.
39169
+ *
39170
+ * @param fn - Callback function to handle idle ping events
39171
+ * @returns Unsubscribe function to stop listening
39172
+ */
39173
+ function listenIdlePing(fn) {
39174
+ backtest.loggerService.log(LISTEN_IDLE_PING_METHOD_NAME);
39175
+ const wrappedFn = async (event) => {
39176
+ if (await functoolsKit.not(backtest.strategyCoreService.hasPendingSignal(event.backtest, event.symbol, {
39177
+ strategyName: event.strategyName,
39178
+ exchangeName: event.exchangeName,
39179
+ frameName: event.frameName,
39180
+ }))) {
39181
+ await fn(event);
39182
+ }
39183
+ };
39184
+ return idlePingSubject.subscribe(functoolsKit.queued(wrappedFn));
39185
+ }
39186
+ /**
39187
+ * Subscribes to filtered idle ping events with one-time execution.
39188
+ *
39189
+ * @param filterFn - Predicate to filter events
39190
+ * @param fn - Callback function to handle the matching event
39191
+ * @returns Unsubscribe function to cancel the listener before it fires
39192
+ */
39193
+ function listenIdlePingOnce(filterFn, fn) {
39194
+ backtest.loggerService.log(LISTEN_IDLE_PING_ONCE_METHOD_NAME);
39195
+ let disposeFn;
39196
+ const wrappedFn = async (event) => {
39197
+ if (filterFn(event)) {
39198
+ await fn(event);
39199
+ disposeFn && disposeFn();
39200
+ }
39201
+ };
39202
+ return disposeFn = listenIdlePing(wrappedFn);
39203
+ }
38998
39204
  /**
38999
39205
  * Subscribes to strategy management events with queued async processing.
39000
39206
  *
@@ -45205,7 +45411,7 @@ function addExchangeSchema(exchangeSchema) {
45205
45411
  *
45206
45412
  * @param frameSchema - Frame configuration object
45207
45413
  * @param frameSchema.frameName - Unique frame identifier
45208
- * @param frameSchema.interval - Timeframe interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h" | "12h" | "1d" | "3d")
45414
+ * @param frameSchema.interval - Timeframe interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h" | "12h" | "1d")
45209
45415
  * @param frameSchema.startDate - Start date for timeframe generation
45210
45416
  * @param frameSchema.endDate - End date for timeframe generation
45211
45417
  * @param frameSchema.callbacks - Optional callback for timeframe events
@@ -45952,6 +46158,7 @@ const RECENT_LIVE_ADAPTER_METHOD_NAME_CLEAR = "RecentLiveAdapter.clear";
45952
46158
  const RECENT_ADAPTER_METHOD_NAME_ENABLE = "RecentAdapter.enable";
45953
46159
  const RECENT_ADAPTER_METHOD_NAME_DISABLE = "RecentAdapter.disable";
45954
46160
  const RECENT_ADAPTER_METHOD_NAME_GET_LATEST_SIGNAL = "RecentAdapter.getLatestSignal";
46161
+ const RECENT_ADAPTER_METHOD_NAME_GET_MINUTES_SINCE_LATEST_SIGNAL = "RecentAdapter.getMinutesSinceLatestSignalCreated";
45955
46162
  /**
45956
46163
  * Builds a composite storage key from context parts.
45957
46164
  * Includes backtest flag as the last segment to prevent live/backtest collisions.
@@ -46010,6 +46217,23 @@ class RecentPersistBacktestUtils {
46010
46217
  });
46011
46218
  return await PersistRecentAdapter.readRecentData(symbol, strategyName, exchangeName, frameName, backtest$1);
46012
46219
  };
46220
+ /**
46221
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46222
+ * @param timestamp - Current timestamp in milliseconds
46223
+ * @param symbol - Trading pair symbol
46224
+ * @param strategyName - Strategy identifier
46225
+ * @param exchangeName - Exchange identifier
46226
+ * @param frameName - Frame identifier
46227
+ * @param backtest - Flag indicating if the context is backtest or live
46228
+ * @returns Whole minutes since the latest signal was created, or null if no signal found
46229
+ */
46230
+ this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest) => {
46231
+ const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest);
46232
+ if (!signal) {
46233
+ return null;
46234
+ }
46235
+ return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
46236
+ };
46013
46237
  }
46014
46238
  }
46015
46239
  /**
@@ -46052,6 +46276,23 @@ class RecentMemoryBacktestUtils {
46052
46276
  backtest.loggerService.info(RECENT_MEMORY_BACKTEST_METHOD_NAME_GET_LATEST_SIGNAL, { key });
46053
46277
  return this._signals.get(key) ?? null;
46054
46278
  };
46279
+ /**
46280
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46281
+ * @param timestamp - Current timestamp in milliseconds
46282
+ * @param symbol - Trading pair symbol
46283
+ * @param strategyName - Strategy identifier
46284
+ * @param exchangeName - Exchange identifier
46285
+ * @param frameName - Frame identifier
46286
+ * @param backtest - Flag indicating if the context is backtest or live
46287
+ * @returns Whole minutes since the latest signal was created, or null if no signal found
46288
+ */
46289
+ this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest) => {
46290
+ const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest);
46291
+ if (!signal) {
46292
+ return null;
46293
+ }
46294
+ return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
46295
+ };
46055
46296
  }
46056
46297
  }
46057
46298
  /**
@@ -46095,6 +46336,23 @@ class RecentPersistLiveUtils {
46095
46336
  });
46096
46337
  return await PersistRecentAdapter.readRecentData(symbol, strategyName, exchangeName, frameName, backtest$1);
46097
46338
  };
46339
+ /**
46340
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46341
+ * @param timestamp - Current timestamp in milliseconds
46342
+ * @param symbol - Trading pair symbol
46343
+ * @param strategyName - Strategy identifier
46344
+ * @param exchangeName - Exchange identifier
46345
+ * @param frameName - Frame identifier
46346
+ * @param backtest - Flag indicating if the context is backtest or live
46347
+ * @returns Whole minutes since the latest signal was created, or null if no signal found
46348
+ */
46349
+ this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest) => {
46350
+ const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest);
46351
+ if (!signal) {
46352
+ return null;
46353
+ }
46354
+ return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
46355
+ };
46098
46356
  }
46099
46357
  }
46100
46358
  /**
@@ -46137,6 +46395,23 @@ class RecentMemoryLiveUtils {
46137
46395
  backtest.loggerService.info(RECENT_MEMORY_LIVE_METHOD_NAME_GET_LATEST_SIGNAL, { key });
46138
46396
  return this._signals.get(key) ?? null;
46139
46397
  };
46398
+ /**
46399
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46400
+ * @param timestamp - Current timestamp in milliseconds
46401
+ * @param symbol - Trading pair symbol
46402
+ * @param strategyName - Strategy identifier
46403
+ * @param exchangeName - Exchange identifier
46404
+ * @param frameName - Frame identifier
46405
+ * @param backtest - Flag indicating if the context is backtest or live
46406
+ * @returns Whole minutes since the latest signal was created, or null if no signal found
46407
+ */
46408
+ this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest) => {
46409
+ const signal = await this.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest);
46410
+ if (!signal) {
46411
+ return null;
46412
+ }
46413
+ return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
46414
+ };
46140
46415
  }
46141
46416
  }
46142
46417
  /**
@@ -46183,6 +46458,32 @@ class RecentBacktestAdapter {
46183
46458
  });
46184
46459
  return await this._recentBacktestUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1);
46185
46460
  };
46461
+ /**
46462
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46463
+ * Proxies call to the underlying storage adapter.
46464
+ * @param timestamp - Current timestamp in milliseconds
46465
+ * @param symbol - Trading pair symbol
46466
+ * @param strategyName - Strategy identifier
46467
+ * @param exchangeName - Exchange identifier
46468
+ * @param frameName - Frame identifier
46469
+ * @param backtest - Flag indicating if the context is backtest or live
46470
+ * @returns Whole minutes since the latest signal was created, or null if no signal found
46471
+ */
46472
+ this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest$1) => {
46473
+ backtest.loggerService.info(RECENT_BACKTEST_ADAPTER_METHOD_NAME_GET_LATEST_SIGNAL, {
46474
+ symbol,
46475
+ strategyName,
46476
+ exchangeName,
46477
+ frameName,
46478
+ backtest: backtest$1,
46479
+ timestamp,
46480
+ });
46481
+ const signal = await this._recentBacktestUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1);
46482
+ if (!signal) {
46483
+ return null;
46484
+ }
46485
+ return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
46486
+ };
46186
46487
  /**
46187
46488
  * Sets the storage adapter constructor.
46188
46489
  * All future storage operations will use this adapter.
@@ -46261,6 +46562,32 @@ class RecentLiveAdapter {
46261
46562
  });
46262
46563
  return await this._recentLiveUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1);
46263
46564
  };
46565
+ /**
46566
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46567
+ * Proxies call to the underlying storage adapter.
46568
+ * @param timestamp - Current timestamp in milliseconds
46569
+ * @param symbol - Trading pair symbol
46570
+ * @param strategyName - Strategy identifier
46571
+ * @param exchangeName - Exchange identifier
46572
+ * @param frameName - Frame identifier
46573
+ * @param backtest - Flag indicating if the context is backtest or live
46574
+ * @returns Whole minutes since the latest signal was created, or null if no signal found
46575
+ */
46576
+ this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, strategyName, exchangeName, frameName, backtest$1) => {
46577
+ backtest.loggerService.info(RECENT_LIVE_ADAPTER_METHOD_NAME_GET_LATEST_SIGNAL, {
46578
+ symbol,
46579
+ strategyName,
46580
+ exchangeName,
46581
+ frameName,
46582
+ backtest: backtest$1,
46583
+ timestamp,
46584
+ });
46585
+ const signal = await this._recentLiveUtils.getLatestSignal(symbol, strategyName, exchangeName, frameName, backtest$1);
46586
+ if (!signal) {
46587
+ return null;
46588
+ }
46589
+ return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
46590
+ };
46264
46591
  /**
46265
46592
  * Sets the storage adapter constructor.
46266
46593
  * All future storage operations will use this adapter.
@@ -46369,6 +46696,27 @@ class RecentAdapter {
46369
46696
  }
46370
46697
  return null;
46371
46698
  };
46699
+ /**
46700
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46701
+ * Searches backtest storage first, then live storage.
46702
+ * @param timestamp - Current timestamp in milliseconds
46703
+ * @param symbol - Trading pair symbol
46704
+ * @param context - Execution context with strategyName, exchangeName, and frameName
46705
+ * @returns Whole minutes since the latest signal was created, or null if no signal found
46706
+ * @throws Error if RecentAdapter is not enabled
46707
+ */
46708
+ this.getMinutesSinceLatestSignalCreated = async (timestamp, symbol, context) => {
46709
+ backtest.loggerService.info(RECENT_ADAPTER_METHOD_NAME_GET_MINUTES_SINCE_LATEST_SIGNAL, {
46710
+ symbol,
46711
+ context,
46712
+ timestamp,
46713
+ });
46714
+ const signal = await this.getLatestSignal(symbol, context);
46715
+ if (!signal) {
46716
+ return null;
46717
+ }
46718
+ return Math.floor((timestamp - signal.timestamp) / (1000 * 60));
46719
+ };
46372
46720
  }
46373
46721
  }
46374
46722
  /**
@@ -46388,6 +46736,7 @@ const RecentLive = new RecentLiveAdapter();
46388
46736
  const RecentBacktest = new RecentBacktestAdapter();
46389
46737
 
46390
46738
  const GET_LATEST_SIGNAL_METHOD_NAME = "signal.getLatestSignal";
46739
+ const GET_MINUTES_SINCE_LATEST_SIGNAL_CREATED_METHOD_NAME = "signal.getMinutesSinceLatestSignalCreated";
46391
46740
  /**
46392
46741
  * Returns the latest signal (pending or closed) for the current strategy context.
46393
46742
  *
@@ -46425,6 +46774,43 @@ async function getLatestSignal(symbol) {
46425
46774
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
46426
46775
  return await Recent.getLatestSignal(symbol, { exchangeName, frameName, strategyName });
46427
46776
  }
46777
+ /**
46778
+ * Returns the number of whole minutes elapsed since the latest signal's creation timestamp.
46779
+ *
46780
+ * Does not distinguish between active and closed signals — measures time since
46781
+ * whichever signal was recorded last. Useful for cooldown logic after a stop-loss.
46782
+ *
46783
+ * Searches backtest storage first, then live storage.
46784
+ * Returns null if no signal exists at all.
46785
+ *
46786
+ * Automatically detects backtest/live mode from execution context.
46787
+ *
46788
+ * @param symbol - Trading pair symbol
46789
+ * @param timestamp - Current timestamp in milliseconds
46790
+ * @returns Promise resolving to whole minutes since the latest signal was created, or null
46791
+ *
46792
+ * @example
46793
+ * ```typescript
46794
+ * import { getMinutesSinceLatestSignalCreated } from "backtest-kit";
46795
+ *
46796
+ * const minutes = await getMinutesSinceLatestSignalCreated("BTCUSDT");
46797
+ * if (minutes !== null && minutes < 24 * 60) {
46798
+ * return; // cooldown — skip new signal for 24 hours after last signal
46799
+ * }
46800
+ * ```
46801
+ */
46802
+ async function getMinutesSinceLatestSignalCreated(symbol) {
46803
+ backtest.loggerService.info(GET_MINUTES_SINCE_LATEST_SIGNAL_CREATED_METHOD_NAME, { symbol });
46804
+ if (!ExecutionContextService.hasContext()) {
46805
+ throw new Error("getMinutesSinceLatestSignalCreated requires an execution context");
46806
+ }
46807
+ if (!MethodContextService.hasContext()) {
46808
+ throw new Error("getMinutesSinceLatestSignalCreated requires a method context");
46809
+ }
46810
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
46811
+ const { when } = backtest.executionContextService.context;
46812
+ return await Recent.getMinutesSinceLatestSignalCreated(when.getTime(), symbol, { exchangeName, frameName, strategyName });
46813
+ }
46428
46814
 
46429
46815
  const DEFAULT_BM25_K1 = 1.5;
46430
46816
  const DEFAULT_BM25_B = 0.75;
@@ -49752,6 +50138,7 @@ const METHOD_NAME_CREATE_SNAPSHOT = "SessionUtils.createSnapshot";
49752
50138
  /** List of all global subjects whose listeners should be snapshotted for session isolation */
49753
50139
  const SUBJECT_ISOLATION_LIST = [
49754
50140
  activePingSubject,
50141
+ idlePingSubject,
49755
50142
  backtestScheduleOpenSubject,
49756
50143
  breakevenSubject,
49757
50144
  doneBacktestSubject,
@@ -56306,7 +56693,6 @@ const INTERVAL_MINUTES$1 = {
56306
56693
  "6h": 360,
56307
56694
  "8h": 480,
56308
56695
  "1d": 1440,
56309
- "1w": 10080,
56310
56696
  };
56311
56697
  /**
56312
56698
  * Aligns timestamp down to the nearest interval boundary.
@@ -56941,7 +57327,6 @@ const INTERVAL_MINUTES = {
56941
57327
  "6h": 360,
56942
57328
  "8h": 480,
56943
57329
  "1d": 1440,
56944
- "1w": 10080,
56945
57330
  };
56946
57331
  /**
56947
57332
  * Aligns timestamp down to the nearest interval boundary.
@@ -57872,6 +58257,7 @@ const METHOD_NAME_PARTIAL_PROFIT_AVAILABLE = "ActionBase.partialProfitAvailable"
57872
58257
  const METHOD_NAME_PARTIAL_LOSS_AVAILABLE = "ActionBase.partialLossAvailable";
57873
58258
  const METHOD_NAME_PING_SCHEDULED = "ActionBase.pingScheduled";
57874
58259
  const METHOD_NAME_PING_ACTIVE = "ActionBase.pingActive";
58260
+ const METHOD_NAME_PING_IDLE = "ActionBase.pingIdle";
57875
58261
  const METHOD_NAME_RISK_REJECTION = "ActionBase.riskRejection";
57876
58262
  const METHOD_NAME_DISPOSE = "ActionBase.dispose";
57877
58263
  const DEFAULT_SOURCE = "default";
@@ -58265,6 +58651,26 @@ class ActionBase {
58265
58651
  source,
58266
58652
  });
58267
58653
  }
58654
+ /**
58655
+ * Handles idle ping events when no signal is active.
58656
+ *
58657
+ * Called every tick while no signal is pending or scheduled.
58658
+ * Use to monitor idle strategy state and implement entry condition logic.
58659
+ *
58660
+ * Triggered by: ActionCoreService.pingIdle() via StrategyConnectionService
58661
+ * Source: idlePingSubject.next() in CREATE_COMMIT_IDLE_PING_FN callback
58662
+ * Frequency: Every tick while no signal is pending or scheduled
58663
+ *
58664
+ * Default implementation: Logs idle ping event.
58665
+ *
58666
+ * @param event - Idle ping data with symbol, strategy info, current price, timestamp
58667
+ */
58668
+ pingIdle(event, source = DEFAULT_SOURCE) {
58669
+ LOGGER_SERVICE.info(METHOD_NAME_PING_IDLE, {
58670
+ event,
58671
+ source,
58672
+ });
58673
+ }
58268
58674
  /**
58269
58675
  * Handles risk rejection events when signals fail risk validation.
58270
58676
  *
@@ -58708,6 +59114,7 @@ exports.getFrameSchema = getFrameSchema;
58708
59114
  exports.getLatestSignal = getLatestSignal;
58709
59115
  exports.getMaxDrawdownDistancePnlCost = getMaxDrawdownDistancePnlCost;
58710
59116
  exports.getMaxDrawdownDistancePnlPercentage = getMaxDrawdownDistancePnlPercentage;
59117
+ exports.getMinutesSinceLatestSignalCreated = getMinutesSinceLatestSignalCreated;
58711
59118
  exports.getMode = getMode;
58712
59119
  exports.getNextCandles = getNextCandles;
58713
59120
  exports.getOrderBook = getOrderBook;
@@ -58780,6 +59187,8 @@ exports.listenError = listenError;
58780
59187
  exports.listenExit = listenExit;
58781
59188
  exports.listenHighestProfit = listenHighestProfit;
58782
59189
  exports.listenHighestProfitOnce = listenHighestProfitOnce;
59190
+ exports.listenIdlePing = listenIdlePing;
59191
+ exports.listenIdlePingOnce = listenIdlePingOnce;
58783
59192
  exports.listenMaxDrawdown = listenMaxDrawdown;
58784
59193
  exports.listenMaxDrawdownOnce = listenMaxDrawdownOnce;
58785
59194
  exports.listenPartialLossAvailable = listenPartialLossAvailable;