backtest-kit 11.7.0 → 11.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.mjs CHANGED
@@ -74,6 +74,7 @@ const metaServices$1 = {
74
74
  contextMetaService: Symbol('contextMetaService'),
75
75
  priceMetaService: Symbol('priceMetaService'),
76
76
  timeMetaService: Symbol('timeMetaService'),
77
+ runtimeMetaService: Symbol('runtimeMetaService'),
77
78
  };
78
79
  const globalServices$1 = {
79
80
  sizingGlobalService: Symbol('sizingGlobalService'),
@@ -4660,7 +4661,7 @@ const LOGGER_SERVICE$8 = new LoggerService();
4660
4661
  * @param backtest - `true` for backtest, `false` for live.
4661
4662
  * @returns Colon-joined composite key.
4662
4663
  */
4663
- const CREATE_KEY_FN$y = (symbol, strategyName, exchangeName, frameName, backtest) => {
4664
+ const CREATE_KEY_FN$B = (symbol, strategyName, exchangeName, frameName, backtest) => {
4664
4665
  const parts = [symbol, strategyName, exchangeName];
4665
4666
  if (frameName)
4666
4667
  parts.push(frameName);
@@ -4703,7 +4704,7 @@ class LookupUtils {
4703
4704
  LOGGER_SERVICE$8.info(METHOD_NAME_ADD_ACTIVITY, {
4704
4705
  activity,
4705
4706
  });
4706
- const key = CREATE_KEY_FN$y(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
4707
+ const key = CREATE_KEY_FN$B(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
4707
4708
  this._lookupMap.set(key, activity);
4708
4709
  };
4709
4710
  /**
@@ -4717,7 +4718,7 @@ class LookupUtils {
4717
4718
  LOGGER_SERVICE$8.info(METHOD_NAME_REMOVE_ACTIVITY, {
4718
4719
  activity,
4719
4720
  });
4720
- const key = CREATE_KEY_FN$y(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
4721
+ const key = CREATE_KEY_FN$B(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
4721
4722
  this._lookupMap.delete(key);
4722
4723
  };
4723
4724
  /**
@@ -12335,7 +12336,7 @@ const GET_RISK_FN = (dto, backtest, exchangeName, frameName, self) => {
12335
12336
  * @param backtest - Whether running in backtest mode
12336
12337
  * @returns Unique string key for memoization
12337
12338
  */
12338
- const CREATE_KEY_FN$x = (symbol, strategyName, exchangeName, frameName, backtest) => {
12339
+ const CREATE_KEY_FN$A = (symbol, strategyName, exchangeName, frameName, backtest) => {
12339
12340
  const parts = [symbol, strategyName, exchangeName];
12340
12341
  if (frameName)
12341
12342
  parts.push(frameName);
@@ -12356,6 +12357,7 @@ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => trycatch(async (symbol, strateg
12356
12357
  symbol,
12357
12358
  strategyName,
12358
12359
  exchangeName,
12360
+ frameName: data.frameName,
12359
12361
  currentPrice,
12360
12362
  data,
12361
12363
  backtest,
@@ -12423,6 +12425,7 @@ const CREATE_COMMIT_ACTIVE_PING_FN = (self) => trycatch(async (symbol, strategyN
12423
12425
  symbol,
12424
12426
  strategyName,
12425
12427
  exchangeName,
12428
+ frameName: data.frameName,
12426
12429
  currentPrice,
12427
12430
  data,
12428
12431
  backtest,
@@ -12635,7 +12638,7 @@ class StrategyConnectionService {
12635
12638
  * @param backtest - Whether running in backtest mode
12636
12639
  * @returns Configured ClientStrategy instance
12637
12640
  */
12638
- this.getStrategy = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$x(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
12641
+ this.getStrategy = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$A(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
12639
12642
  const { riskName = "", riskList = [], getSignal, interval = STRATEGY_DEFAULT_INTERVAL, callbacks, } = this.strategySchemaService.get(strategyName);
12640
12643
  return new ClientStrategy({
12641
12644
  symbol,
@@ -13597,7 +13600,7 @@ class StrategyConnectionService {
13597
13600
  }
13598
13601
  return;
13599
13602
  }
13600
- const key = CREATE_KEY_FN$x(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
13603
+ const key = CREATE_KEY_FN$A(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
13601
13604
  if (!this.getStrategy.has(key)) {
13602
13605
  return;
13603
13606
  }
@@ -13995,11 +13998,51 @@ class StrategyConnectionService {
13995
13998
  }
13996
13999
  }
13997
14000
 
14001
+ const MS_PER_MINUTE$6 = 60000;
14002
+ const INTERVAL_MINUTES$7 = {
14003
+ "1m": 1,
14004
+ "3m": 3,
14005
+ "5m": 5,
14006
+ "15m": 15,
14007
+ "30m": 30,
14008
+ "1h": 60,
14009
+ "2h": 120,
14010
+ "4h": 240,
14011
+ "6h": 360,
14012
+ "8h": 480,
14013
+ "1d": 1440,
14014
+ };
14015
+ /**
14016
+ * Aligns timestamp down to the nearest interval boundary.
14017
+ * For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
14018
+ *
14019
+ * Candle timestamp convention:
14020
+ * - Candle timestamp = openTime (when candle opens)
14021
+ * - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
14022
+ *
14023
+ * Adapter contract:
14024
+ * - Adapter must return candles with timestamp = openTime
14025
+ * - First returned candle.timestamp must equal aligned since
14026
+ * - Adapter must return exactly `limit` candles
14027
+ *
14028
+ * @param date - Date to align
14029
+ * @param interval - Candle interval (e.g., "1m", "15m", "1h")
14030
+ * @returns New Date aligned down to interval boundary
14031
+ */
14032
+ const alignToInterval = (date, interval) => {
14033
+ const minutes = INTERVAL_MINUTES$7[interval];
14034
+ if (minutes === undefined) {
14035
+ throw new Error(`alignToInterval: unknown interval=${interval}`);
14036
+ }
14037
+ const intervalMs = minutes * MS_PER_MINUTE$6;
14038
+ return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
14039
+ };
14040
+
13998
14041
  /**
13999
14042
  * Maps FrameInterval to minutes for timestamp calculation.
14000
14043
  * Used to generate timeframe arrays with proper spacing.
14001
14044
  */
14002
- const INTERVAL_MINUTES$7 = {
14045
+ const INTERVAL_MINUTES$6 = {
14003
14046
  "1m": 1,
14004
14047
  "3m": 3,
14005
14048
  "5m": 5,
@@ -14053,7 +14096,7 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
14053
14096
  symbol,
14054
14097
  });
14055
14098
  const { interval, startDate, endDate } = self.params;
14056
- const intervalMinutes = INTERVAL_MINUTES$7[interval];
14099
+ const intervalMinutes = INTERVAL_MINUTES$6[interval];
14057
14100
  if (!intervalMinutes) {
14058
14101
  throw new Error(`ClientFrame unknown interval: ${interval}`);
14059
14102
  }
@@ -14062,8 +14105,14 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
14062
14105
  today.setUTCHours(0, 0, 0, 0);
14063
14106
  // Ensure endDate doesn't go beyond today
14064
14107
  const effectiveEndDate = endDate > today ? today : endDate;
14108
+ // Align the iteration start down to the 1-minute boundary so every generated
14109
+ // timestamp lands on a clean minute, matching live mode
14110
+ // (LiveLogicPrivateService aligns `when` via alignToInterval(new Date(), "1m")).
14111
+ // Without this, a startDate carrying sub-minute (or any non-aligned) offset
14112
+ // would propagate that offset to every tick `when` — and therefore to
14113
+ // IRuntimeInfo.when handed to Cron handlers — diverging from live behaviour.
14065
14114
  const timeframes = [];
14066
- let currentDate = new Date(startDate);
14115
+ let currentDate = alignToInterval(startDate, "1m");
14067
14116
  while (currentDate <= effectiveEndDate) {
14068
14117
  timeframes.push(new Date(currentDate));
14069
14118
  currentDate = new Date(currentDate.getTime() + intervalMinutes * 60 * 1000);
@@ -14097,6 +14146,7 @@ class ClientFrame {
14097
14146
  }
14098
14147
  }
14099
14148
 
14149
+ const DEFAULT_INTERVAL = "1m";
14100
14150
  /**
14101
14151
  * Connection service routing frame operations to correct ClientFrame instance.
14102
14152
  *
@@ -14140,7 +14190,7 @@ class FrameConnectionService {
14140
14190
  logger: this.loggerService,
14141
14191
  startDate,
14142
14192
  endDate,
14143
- interval,
14193
+ interval: interval || DEFAULT_INTERVAL,
14144
14194
  callbacks,
14145
14195
  });
14146
14196
  });
@@ -14799,7 +14849,7 @@ class ClientRisk {
14799
14849
  * @param backtest - Whether running in backtest mode
14800
14850
  * @returns Unique string key for memoization
14801
14851
  */
14802
- const CREATE_KEY_FN$w = (riskName, exchangeName, frameName, backtest) => {
14852
+ const CREATE_KEY_FN$z = (riskName, exchangeName, frameName, backtest) => {
14803
14853
  const parts = [riskName, exchangeName];
14804
14854
  if (frameName)
14805
14855
  parts.push(frameName);
@@ -14899,7 +14949,7 @@ class RiskConnectionService {
14899
14949
  * @param backtest - True if backtest mode, false if live mode
14900
14950
  * @returns Configured ClientRisk instance
14901
14951
  */
14902
- this.getRisk = memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$w(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
14952
+ this.getRisk = memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$z(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
14903
14953
  const schema = this.riskSchemaService.get(riskName);
14904
14954
  return new ClientRisk({
14905
14955
  ...schema,
@@ -14989,7 +15039,7 @@ class RiskConnectionService {
14989
15039
  payload,
14990
15040
  });
14991
15041
  if (payload) {
14992
- const key = CREATE_KEY_FN$w(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
15042
+ const key = CREATE_KEY_FN$z(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
14993
15043
  this.getRisk.clear(key);
14994
15044
  }
14995
15045
  else {
@@ -16108,7 +16158,7 @@ class ClientAction {
16108
16158
  * @param backtest - Whether running in backtest mode
16109
16159
  * @returns Unique string key for memoization
16110
16160
  */
16111
- const CREATE_KEY_FN$v = (actionName, strategyName, exchangeName, frameName, backtest) => {
16161
+ const CREATE_KEY_FN$y = (actionName, strategyName, exchangeName, frameName, backtest) => {
16112
16162
  const parts = [actionName, strategyName, exchangeName];
16113
16163
  if (frameName)
16114
16164
  parts.push(frameName);
@@ -16160,7 +16210,7 @@ class ActionConnectionService {
16160
16210
  * @param backtest - True if backtest mode, false if live mode
16161
16211
  * @returns Configured ClientAction instance
16162
16212
  */
16163
- this.getAction = memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$v(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
16213
+ this.getAction = memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$y(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
16164
16214
  const schema = this.actionSchemaService.get(actionName);
16165
16215
  return new ClientAction({
16166
16216
  ...schema,
@@ -16386,7 +16436,7 @@ class ActionConnectionService {
16386
16436
  await Promise.all(actions.map(async (action) => await action.dispose()));
16387
16437
  return;
16388
16438
  }
16389
- const key = CREATE_KEY_FN$v(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
16439
+ const key = CREATE_KEY_FN$y(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
16390
16440
  if (!this.getAction.has(key)) {
16391
16441
  return;
16392
16442
  }
@@ -16397,14 +16447,14 @@ class ActionConnectionService {
16397
16447
  }
16398
16448
  }
16399
16449
 
16400
- const METHOD_NAME_VALIDATE$3 = "exchangeCoreService validate";
16450
+ const METHOD_NAME_VALIDATE$6 = "exchangeCoreService validate";
16401
16451
  /**
16402
16452
  * Creates a unique key for memoizing validate calls.
16403
16453
  * Key format: "exchangeName"
16404
16454
  * @param exchangeName - Exchange name
16405
16455
  * @returns Unique string key for memoization
16406
16456
  */
16407
- const CREATE_KEY_FN$u = (exchangeName) => {
16457
+ const CREATE_KEY_FN$x = (exchangeName) => {
16408
16458
  return exchangeName;
16409
16459
  };
16410
16460
  /**
@@ -16428,11 +16478,11 @@ class ExchangeCoreService {
16428
16478
  * @param exchangeName - Name of the exchange to validate
16429
16479
  * @returns Promise that resolves when validation is complete
16430
16480
  */
16431
- this.validate = memoize(([exchangeName]) => CREATE_KEY_FN$u(exchangeName), async (exchangeName) => {
16432
- this.loggerService.log(METHOD_NAME_VALIDATE$3, {
16481
+ this.validate = memoize(([exchangeName]) => CREATE_KEY_FN$x(exchangeName), async (exchangeName) => {
16482
+ this.loggerService.log(METHOD_NAME_VALIDATE$6, {
16433
16483
  exchangeName,
16434
16484
  });
16435
- this.exchangeValidationService.validate(exchangeName, METHOD_NAME_VALIDATE$3);
16485
+ this.exchangeValidationService.validate(exchangeName, METHOD_NAME_VALIDATE$6);
16436
16486
  });
16437
16487
  /**
16438
16488
  * Fetches historical candles with execution context.
@@ -16701,14 +16751,14 @@ class ExchangeCoreService {
16701
16751
  }
16702
16752
  }
16703
16753
 
16704
- const METHOD_NAME_VALIDATE$2 = "strategyCoreService validate";
16754
+ const METHOD_NAME_VALIDATE$5 = "strategyCoreService validate";
16705
16755
  /**
16706
16756
  * Creates a unique key for memoizing validate calls.
16707
16757
  * Key format: "strategyName:exchangeName:frameName"
16708
16758
  * @param context - Execution context with strategyName, exchangeName, frameName
16709
16759
  * @returns Unique string key for memoization
16710
16760
  */
16711
- const CREATE_KEY_FN$t = (context) => {
16761
+ const CREATE_KEY_FN$w = (context) => {
16712
16762
  const parts = [context.strategyName, context.exchangeName];
16713
16763
  if (context.frameName)
16714
16764
  parts.push(context.frameName);
@@ -16731,6 +16781,7 @@ class StrategyCoreService {
16731
16781
  this.strategyValidationService = inject(TYPES.strategyValidationService);
16732
16782
  this.exchangeValidationService = inject(TYPES.exchangeValidationService);
16733
16783
  this.frameValidationService = inject(TYPES.frameValidationService);
16784
+ this.actionValidationService = inject(TYPES.actionValidationService);
16734
16785
  /**
16735
16786
  * Validates strategy and associated risk configuration.
16736
16787
  *
@@ -16740,16 +16791,17 @@ class StrategyCoreService {
16740
16791
  * @param context - Execution context with strategyName, exchangeName, frameName
16741
16792
  * @returns Promise that resolves when validation is complete
16742
16793
  */
16743
- this.validate = memoize(([context]) => CREATE_KEY_FN$t(context), async (context) => {
16744
- this.loggerService.log(METHOD_NAME_VALIDATE$2, {
16794
+ this.validate = memoize(([context]) => CREATE_KEY_FN$w(context), async (context) => {
16795
+ this.loggerService.log(METHOD_NAME_VALIDATE$5, {
16745
16796
  context,
16746
16797
  });
16747
- const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
16748
- this.strategyValidationService.validate(context.strategyName, METHOD_NAME_VALIDATE$2);
16749
- this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_VALIDATE$2);
16750
- context.frameName && this.frameValidationService.validate(context.frameName, METHOD_NAME_VALIDATE$2);
16751
- riskName && this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$2);
16752
- riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$2));
16798
+ const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
16799
+ this.strategyValidationService.validate(context.strategyName, METHOD_NAME_VALIDATE$5);
16800
+ this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_VALIDATE$5);
16801
+ context.frameName && this.frameValidationService.validate(context.frameName, METHOD_NAME_VALIDATE$5);
16802
+ riskName && this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$5);
16803
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$5));
16804
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_VALIDATE$5));
16753
16805
  });
16754
16806
  /**
16755
16807
  * Retrieves the currently active pending signal for the symbol.
@@ -18110,7 +18162,7 @@ class SizingGlobalService {
18110
18162
  * @param context - Context with riskName, exchangeName, frameName
18111
18163
  * @returns Unique string key for memoization
18112
18164
  */
18113
- const CREATE_KEY_FN$s = (context) => {
18165
+ const CREATE_KEY_FN$v = (context) => {
18114
18166
  const parts = [context.riskName, context.exchangeName];
18115
18167
  if (context.frameName)
18116
18168
  parts.push(context.frameName);
@@ -18136,7 +18188,7 @@ class RiskGlobalService {
18136
18188
  * @param payload - Payload with riskName, exchangeName and frameName
18137
18189
  * @returns Promise that resolves when validation is complete
18138
18190
  */
18139
- this.validate = memoize(([context]) => CREATE_KEY_FN$s(context), async (context) => {
18191
+ this.validate = memoize(([context]) => CREATE_KEY_FN$v(context), async (context) => {
18140
18192
  this.loggerService.log("riskGlobalService validate", {
18141
18193
  context,
18142
18194
  });
@@ -18228,14 +18280,14 @@ class RiskGlobalService {
18228
18280
  }
18229
18281
  }
18230
18282
 
18231
- const METHOD_NAME_VALIDATE$1 = "actionCoreService validate";
18283
+ const METHOD_NAME_VALIDATE$4 = "actionCoreService validate";
18232
18284
  /**
18233
18285
  * Creates a unique key for memoizing validate calls.
18234
18286
  * Key format: "strategyName:exchangeName:frameName"
18235
18287
  * @param context - Execution context with strategyName, exchangeName, frameName
18236
18288
  * @returns Unique string key for memoization
18237
18289
  */
18238
- const CREATE_KEY_FN$r = (context) => {
18290
+ const CREATE_KEY_FN$u = (context) => {
18239
18291
  const parts = [context.strategyName, context.exchangeName];
18240
18292
  if (context.frameName)
18241
18293
  parts.push(context.frameName);
@@ -18279,17 +18331,17 @@ class ActionCoreService {
18279
18331
  * @param context - Strategy execution context with strategyName, exchangeName and frameName
18280
18332
  * @returns Promise that resolves when all validations complete
18281
18333
  */
18282
- this.validate = memoize(([context]) => CREATE_KEY_FN$r(context), async (context) => {
18283
- this.loggerService.log(METHOD_NAME_VALIDATE$1, {
18334
+ this.validate = memoize(([context]) => CREATE_KEY_FN$u(context), async (context) => {
18335
+ this.loggerService.log(METHOD_NAME_VALIDATE$4, {
18284
18336
  context,
18285
18337
  });
18286
18338
  const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
18287
- this.strategyValidationService.validate(context.strategyName, METHOD_NAME_VALIDATE$1);
18288
- this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_VALIDATE$1);
18289
- context.frameName && this.frameValidationService.validate(context.frameName, METHOD_NAME_VALIDATE$1);
18290
- riskName && this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$1);
18291
- riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$1));
18292
- actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_VALIDATE$1));
18339
+ this.strategyValidationService.validate(context.strategyName, METHOD_NAME_VALIDATE$4);
18340
+ this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_VALIDATE$4);
18341
+ context.frameName && this.frameValidationService.validate(context.frameName, METHOD_NAME_VALIDATE$4);
18342
+ riskName && this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$4);
18343
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$4));
18344
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_VALIDATE$4));
18293
18345
  });
18294
18346
  /**
18295
18347
  * Initializes all ClientAction instances for the strategy.
@@ -18785,8 +18837,8 @@ class FrameSchemaService {
18785
18837
  if (typeof frameSchema.frameName !== "string") {
18786
18838
  throw new Error(`frame schema validation failed: missing frameName`);
18787
18839
  }
18788
- if (typeof frameSchema.interval !== "string") {
18789
- throw new Error(`frame schema validation failed: missing interval for frameName=${frameSchema.frameName}`);
18840
+ if (frameSchema.interval && typeof frameSchema.interval !== "string") {
18841
+ throw new Error(`frame schema validation failed: invalid interval for frameName=${frameSchema.frameName}`);
18790
18842
  }
18791
18843
  if (!(frameSchema.startDate instanceof Date)) {
18792
18844
  throw new Error(`frame schema validation failed: missing startDate for frameName=${frameSchema.frameName}`);
@@ -19914,8 +19966,8 @@ class BacktestLogicPrivateService {
19914
19966
  }
19915
19967
 
19916
19968
  const EMITTER_CHECK_INTERVAL = 5000;
19917
- const MS_PER_MINUTE$6 = 60000;
19918
- const INTERVAL_MINUTES$6 = {
19969
+ const MS_PER_MINUTE$5 = 60000;
19970
+ const INTERVAL_MINUTES$5 = {
19919
19971
  "1m": 1,
19920
19972
  "3m": 3,
19921
19973
  "5m": 5,
@@ -19930,7 +19982,7 @@ const INTERVAL_MINUTES$6 = {
19930
19982
  };
19931
19983
  const createEmitter = memoize(([interval]) => `${interval}`, (interval) => {
19932
19984
  const tickSubject = new Subject();
19933
- const intervalMs = INTERVAL_MINUTES$6[interval] * MS_PER_MINUTE$6;
19985
+ const intervalMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$5;
19934
19986
  {
19935
19987
  let lastAligned = Math.floor(Date.now() / intervalMs) * intervalMs;
19936
19988
  Source.fromInterval(EMITTER_CHECK_INTERVAL)
@@ -19957,46 +20009,6 @@ const waitForCandle = async (interval) => {
19957
20009
  return emitter.toPromise();
19958
20010
  };
19959
20011
 
19960
- const MS_PER_MINUTE$5 = 60000;
19961
- const INTERVAL_MINUTES$5 = {
19962
- "1m": 1,
19963
- "3m": 3,
19964
- "5m": 5,
19965
- "15m": 15,
19966
- "30m": 30,
19967
- "1h": 60,
19968
- "2h": 120,
19969
- "4h": 240,
19970
- "6h": 360,
19971
- "8h": 480,
19972
- "1d": 1440,
19973
- };
19974
- /**
19975
- * Aligns timestamp down to the nearest interval boundary.
19976
- * For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
19977
- *
19978
- * Candle timestamp convention:
19979
- * - Candle timestamp = openTime (when candle opens)
19980
- * - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
19981
- *
19982
- * Adapter contract:
19983
- * - Adapter must return candles with timestamp = openTime
19984
- * - First returned candle.timestamp must equal aligned since
19985
- * - Adapter must return exactly `limit` candles
19986
- *
19987
- * @param date - Date to align
19988
- * @param interval - Candle interval (e.g., "1m", "15m", "1h")
19989
- * @returns New Date aligned down to interval boundary
19990
- */
19991
- const alignToInterval = (date, interval) => {
19992
- const minutes = INTERVAL_MINUTES$5[interval];
19993
- if (minutes === undefined) {
19994
- throw new Error(`alignToInterval: unknown interval=${interval}`);
19995
- }
19996
- const intervalMs = minutes * MS_PER_MINUTE$5;
19997
- return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
19998
- };
19999
-
20000
20012
  /**
20001
20013
  * Private service for live trading orchestration using async generators.
20002
20014
  *
@@ -20819,6 +20831,17 @@ class WalkerLogicPublicService {
20819
20831
  }
20820
20832
 
20821
20833
  const METHOD_NAME_RUN$2 = "liveCommandService run";
20834
+ const METHOD_NAME_VALIDATE$3 = "liveCommandService validate";
20835
+ /**
20836
+ * Creates a unique key for memoizing validate calls.
20837
+ * Key format: "strategyName:exchangeName:frameName"
20838
+ * @param context - Context with strategyName, exchangeName, frameName
20839
+ * @returns Unique string key for memoization
20840
+ */
20841
+ const CREATE_KEY_FN$t = (context) => {
20842
+ const parts = [context.strategyName, context.exchangeName];
20843
+ return parts.join(":");
20844
+ };
20822
20845
  /**
20823
20846
  * Global service providing access to live trading functionality.
20824
20847
  *
@@ -20834,6 +20857,25 @@ class LiveCommandService {
20834
20857
  this.strategySchemaService = inject(TYPES.strategySchemaService);
20835
20858
  this.riskValidationService = inject(TYPES.riskValidationService);
20836
20859
  this.actionValidationService = inject(TYPES.actionValidationService);
20860
+ /**
20861
+ * Validates strategy and associated risk configuration.
20862
+ * Memoized to avoid redundant validations for the same strategy-exchange combination.
20863
+ *
20864
+ * @param context - Context with strategyName, exchangeName
20865
+ * @param methodName - Name of the calling method for error tracking
20866
+ */
20867
+ this.validate = memoize(([context]) => CREATE_KEY_FN$t(context), (context, methodName) => {
20868
+ this.loggerService.log(METHOD_NAME_VALIDATE$3, {
20869
+ context,
20870
+ methodName,
20871
+ });
20872
+ this.strategyValidationService.validate(context.strategyName, methodName);
20873
+ this.exchangeValidationService.validate(context.exchangeName, methodName);
20874
+ const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
20875
+ riskName && this.riskValidationService.validate(riskName, methodName);
20876
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
20877
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, methodName));
20878
+ });
20837
20879
  /**
20838
20880
  * Runs live trading for a symbol with context propagation.
20839
20881
  *
@@ -20848,22 +20890,26 @@ class LiveCommandService {
20848
20890
  symbol,
20849
20891
  context,
20850
20892
  });
20851
- {
20852
- this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$2);
20853
- this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$2);
20854
- }
20855
- {
20856
- const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
20857
- riskName && this.riskValidationService.validate(riskName, METHOD_NAME_RUN$2);
20858
- riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_RUN$2));
20859
- actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_RUN$2));
20860
- }
20893
+ this.validate(context, METHOD_NAME_RUN$2);
20861
20894
  return this.liveLogicPublicService.run(symbol, context);
20862
20895
  };
20863
20896
  }
20864
20897
  }
20865
20898
 
20866
20899
  const METHOD_NAME_RUN$1 = "backtestCommandService run";
20900
+ const METHOD_NAME_VALIDATE$2 = "backtestCommandService validate";
20901
+ /**
20902
+ * Creates a unique key for memoizing validate calls.
20903
+ * Key format: "strategyName:exchangeName:frameName"
20904
+ * @param context - Context with strategyName, exchangeName, frameName
20905
+ * @returns Unique string key for memoization
20906
+ */
20907
+ const CREATE_KEY_FN$s = (context) => {
20908
+ const parts = [context.strategyName, context.exchangeName];
20909
+ if (context.frameName)
20910
+ parts.push(context.frameName);
20911
+ return parts.join(":");
20912
+ };
20867
20913
  /**
20868
20914
  * Global service providing access to backtest functionality.
20869
20915
  *
@@ -20880,6 +20926,26 @@ class BacktestCommandService {
20880
20926
  this.strategyValidationService = inject(TYPES.strategyValidationService);
20881
20927
  this.exchangeValidationService = inject(TYPES.exchangeValidationService);
20882
20928
  this.frameValidationService = inject(TYPES.frameValidationService);
20929
+ /**
20930
+ * Validates strategy and associated risk configuration.
20931
+ * Memoized to avoid redundant validations for the same strategy-exchange-frame combination.
20932
+ *
20933
+ * @param context - Context with strategyName, exchangeName and frameName
20934
+ * @param methodName - Name of the calling method for error tracking
20935
+ */
20936
+ this.validate = memoize(([context]) => CREATE_KEY_FN$s(context), (context, methodName) => {
20937
+ this.loggerService.log(METHOD_NAME_VALIDATE$2, {
20938
+ context,
20939
+ methodName,
20940
+ });
20941
+ this.strategyValidationService.validate(context.strategyName, methodName);
20942
+ this.exchangeValidationService.validate(context.exchangeName, methodName);
20943
+ this.frameValidationService.validate(context.frameName, methodName);
20944
+ const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
20945
+ riskName && this.riskValidationService.validate(riskName, methodName);
20946
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
20947
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, methodName));
20948
+ });
20883
20949
  /**
20884
20950
  * Runs backtest for a symbol with context propagation.
20885
20951
  *
@@ -20892,23 +20958,26 @@ class BacktestCommandService {
20892
20958
  symbol,
20893
20959
  context,
20894
20960
  });
20895
- {
20896
- this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$1);
20897
- this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$1);
20898
- this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN$1);
20899
- }
20900
- {
20901
- const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
20902
- riskName && this.riskValidationService.validate(riskName, METHOD_NAME_RUN$1);
20903
- riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_RUN$1));
20904
- actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_RUN$1));
20905
- }
20961
+ this.validate(context, METHOD_NAME_RUN$1);
20906
20962
  return this.backtestLogicPublicService.run(symbol, context);
20907
20963
  };
20908
20964
  }
20909
20965
  }
20910
20966
 
20911
20967
  const METHOD_NAME_RUN = "walkerCommandService run";
20968
+ const METHOD_NAME_VALIDATE$1 = "walkerCommandService validate";
20969
+ /**
20970
+ * Creates a unique key for memoizing validate calls.
20971
+ * Key format: "walkerName:exchangeName:frameName"
20972
+ * @param context - Context with walkerName, exchangeName, frameName
20973
+ * @returns Unique string key for memoization
20974
+ */
20975
+ const CREATE_KEY_FN$r = (context) => {
20976
+ const parts = [context.walkerName, context.exchangeName];
20977
+ if (context.frameName)
20978
+ parts.push(context.frameName);
20979
+ return parts.join(":");
20980
+ };
20912
20981
  /**
20913
20982
  * Global service providing access to walker functionality.
20914
20983
  *
@@ -20927,6 +20996,34 @@ class WalkerCommandService {
20927
20996
  this.strategySchemaService = inject(TYPES.strategySchemaService);
20928
20997
  this.riskValidationService = inject(TYPES.riskValidationService);
20929
20998
  this.actionValidationService = inject(TYPES.actionValidationService);
20999
+ /**
21000
+ * Validates walker and associated strategy configurations.
21001
+ * Memoized to avoid redundant validations for the same walker-exchange-frame combination.
21002
+ *
21003
+ * Strategy/risk/action validation is performed explicitly here in addition to the
21004
+ * cascade inside WalkerValidationService — this is critical-path code and the
21005
+ * redundant check is intentional defense-in-depth.
21006
+ *
21007
+ * @param context - Context with walkerName, exchangeName and frameName
21008
+ * @param methodName - Name of the calling method for error tracking
21009
+ */
21010
+ this.validate = memoize(([context]) => CREATE_KEY_FN$r(context), (context, methodName) => {
21011
+ this.loggerService.log(METHOD_NAME_VALIDATE$1, {
21012
+ context,
21013
+ methodName,
21014
+ });
21015
+ this.exchangeValidationService.validate(context.exchangeName, methodName);
21016
+ this.frameValidationService.validate(context.frameName, methodName);
21017
+ this.walkerValidationService.validate(context.walkerName, methodName);
21018
+ const walkerSchema = this.walkerSchemaService.get(context.walkerName);
21019
+ for (const strategyName of walkerSchema.strategies) {
21020
+ const { riskName, riskList, actions } = this.strategySchemaService.get(strategyName);
21021
+ this.strategyValidationService.validate(strategyName, methodName);
21022
+ riskName && this.riskValidationService.validate(riskName, methodName);
21023
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
21024
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, methodName));
21025
+ }
21026
+ });
20930
21027
  /**
20931
21028
  * Runs walker comparison for a symbol with context propagation.
20932
21029
  *
@@ -20938,21 +21035,7 @@ class WalkerCommandService {
20938
21035
  symbol,
20939
21036
  context,
20940
21037
  });
20941
- {
20942
- this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN);
20943
- this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN);
20944
- this.walkerValidationService.validate(context.walkerName, METHOD_NAME_RUN);
20945
- }
20946
- {
20947
- const walkerSchema = this.walkerSchemaService.get(context.walkerName);
20948
- for (const strategyName of walkerSchema.strategies) {
20949
- const { riskName, riskList, actions } = this.strategySchemaService.get(strategyName);
20950
- this.strategyValidationService.validate(strategyName, METHOD_NAME_RUN);
20951
- riskName && this.riskValidationService.validate(riskName, METHOD_NAME_RUN);
20952
- riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_RUN));
20953
- actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_RUN));
20954
- }
20955
- }
21038
+ this.validate(context, METHOD_NAME_RUN);
20956
21039
  return this.walkerLogicPublicService.run(symbol, context);
20957
21040
  };
20958
21041
  }
@@ -28675,6 +28758,36 @@ class WalkerValidationService {
28675
28758
  * Injected logger service instance
28676
28759
  */
28677
28760
  this.loggerService = inject(TYPES.loggerService);
28761
+ /**
28762
+ * @private
28763
+ * @readonly
28764
+ * Injected walker schema service instance
28765
+ */
28766
+ this.walkerSchemaService = inject(TYPES.walkerSchemaService);
28767
+ /**
28768
+ * @private
28769
+ * @readonly
28770
+ * Injected strategy validation service instance
28771
+ */
28772
+ this.strategyValidationService = inject(TYPES.strategyValidationService);
28773
+ /**
28774
+ * @private
28775
+ * @readonly
28776
+ * Injected strategy schema service instance
28777
+ */
28778
+ this.strategySchemaService = inject(TYPES.strategySchemaService);
28779
+ /**
28780
+ * @private
28781
+ * @readonly
28782
+ * Injected risk validation service instance
28783
+ */
28784
+ this.riskValidationService = inject(TYPES.riskValidationService);
28785
+ /**
28786
+ * @private
28787
+ * @readonly
28788
+ * Injected action validation service instance
28789
+ */
28790
+ this.actionValidationService = inject(TYPES.actionValidationService);
28678
28791
  /**
28679
28792
  * @private
28680
28793
  * Map storing walker schemas by walker name
@@ -28696,9 +28809,12 @@ class WalkerValidationService {
28696
28809
  this._walkerMap.set(walkerName, walkerSchema);
28697
28810
  };
28698
28811
  /**
28699
- * Validates the existence of a walker
28812
+ * Validates the existence of a walker and its associated strategy configurations.
28813
+ * Each strategy referenced by the walker is validated via StrategyValidationService,
28814
+ * which in turn validates the strategy's risk profiles and actions.
28700
28815
  * @public
28701
28816
  * @throws {Error} If walkerName is not found
28817
+ * @throws {Error} If any referenced strategy (or its risk/actions) is invalid
28702
28818
  * Memoized function to cache validation results
28703
28819
  */
28704
28820
  this.validate = memoize(([walkerName]) => walkerName, (walkerName, source) => {
@@ -28710,6 +28826,14 @@ class WalkerValidationService {
28710
28826
  if (!walker) {
28711
28827
  throw new Error(`walker ${walkerName} not found source=${source}`);
28712
28828
  }
28829
+ const walkerSchema = this.walkerSchemaService.get(walkerName);
28830
+ for (const strategyName of walkerSchema.strategies) {
28831
+ const { riskName, riskList, actions } = this.strategySchemaService.get(strategyName);
28832
+ this.strategyValidationService.validate(strategyName, source);
28833
+ riskName && this.riskValidationService.validate(riskName, source);
28834
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, source));
28835
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, source));
28836
+ }
28713
28837
  return true;
28714
28838
  });
28715
28839
  /**
@@ -30146,6 +30270,10 @@ class PartialGlobalService {
30146
30270
  * Frame validation service for validating frame existence.
30147
30271
  */
30148
30272
  this.frameValidationService = inject(TYPES.frameValidationService);
30273
+ /**
30274
+ * Action validation service for validating action existence.
30275
+ */
30276
+ this.actionValidationService = inject(TYPES.actionValidationService);
30149
30277
  /**
30150
30278
  * Validates strategy and associated risk configuration.
30151
30279
  * Memoized to avoid redundant validations for the same strategy-exchange-frame combination.
@@ -30161,9 +30289,10 @@ class PartialGlobalService {
30161
30289
  this.strategyValidationService.validate(context.strategyName, methodName);
30162
30290
  this.exchangeValidationService.validate(context.exchangeName, methodName);
30163
30291
  context.frameName && this.frameValidationService.validate(context.frameName, methodName);
30164
- const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
30292
+ const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
30165
30293
  riskName && this.riskValidationService.validate(riskName, methodName);
30166
30294
  riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
30295
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, methodName));
30167
30296
  });
30168
30297
  /**
30169
30298
  * Processes profit state and emits events for newly reached profit levels.
@@ -31230,6 +31359,10 @@ class BreakevenGlobalService {
31230
31359
  * Frame validation service for validating frame existence.
31231
31360
  */
31232
31361
  this.frameValidationService = inject(TYPES.frameValidationService);
31362
+ /**
31363
+ * Action validation service for validating frame existence.
31364
+ */
31365
+ this.actionValidationService = inject(TYPES.actionValidationService);
31233
31366
  /**
31234
31367
  * Validates strategy and associated risk configuration.
31235
31368
  * Memoized to avoid redundant validations for the same strategy-exchange-frame combination.
@@ -31245,9 +31378,10 @@ class BreakevenGlobalService {
31245
31378
  this.strategyValidationService.validate(context.strategyName, methodName);
31246
31379
  this.exchangeValidationService.validate(context.exchangeName, methodName);
31247
31380
  context.frameName && this.frameValidationService.validate(context.frameName, methodName);
31248
- const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
31381
+ const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
31249
31382
  riskName && this.riskValidationService.validate(riskName, methodName);
31250
31383
  riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
31384
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, methodName));
31251
31385
  });
31252
31386
  /**
31253
31387
  * Checks if breakeven should be triggered and emits event if conditions met.
@@ -36135,6 +36269,21 @@ class PriceMetaService {
36135
36269
  * Instances are cached until clear() is called.
36136
36270
  */
36137
36271
  this.getSource = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$b(symbol, strategyName, exchangeName, frameName, backtest), () => new BehaviorSubject());
36272
+ /**
36273
+ * Checks if a price exists for the given key and has emitted at least one value.
36274
+ *
36275
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
36276
+ * @param context - Strategy, exchange, and frame identifiers
36277
+ * @param backtest - True if backtest mode, false if live mode
36278
+ * @returns True if a price exists and has emitted a value, false otherwise
36279
+ */
36280
+ this.hasPrice = (symbol, context, backtest) => {
36281
+ const key = CREATE_KEY_FN$b(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
36282
+ if (!this.getSource.has(key)) {
36283
+ return false;
36284
+ }
36285
+ return !!this.getSource.get(key)?.data;
36286
+ };
36138
36287
  /**
36139
36288
  * Returns the current market price for the given symbol and context.
36140
36289
  *
@@ -36797,6 +36946,129 @@ class NotificationHelperService {
36797
36946
  }
36798
36947
  }
36799
36948
 
36949
+ const GET_RANGE_FN = trycatch((self, context, backtest) => {
36950
+ if (!backtest) {
36951
+ return null;
36952
+ }
36953
+ const { startDate, endDate } = self.frameSchemaService.get(context.frameName);
36954
+ return {
36955
+ from: startDate,
36956
+ to: endDate,
36957
+ };
36958
+ }, {
36959
+ fallback: (error, self) => {
36960
+ const message = "RuntimeMetaService GET_RANGE_FN thrown";
36961
+ const payload = {
36962
+ error: errorData(error),
36963
+ message: getErrorMessage(error),
36964
+ };
36965
+ self.loggerService.warn(message, payload);
36966
+ console.error(message, payload);
36967
+ errorEmitter.next(error);
36968
+ },
36969
+ defaultValue: null,
36970
+ });
36971
+ const GET_INFO_FN = trycatch((self, context) => {
36972
+ const { info } = self.strategySchemaService.get(context.strategyName);
36973
+ return info || null;
36974
+ }, {
36975
+ fallback: (error, self) => {
36976
+ const message = "RuntimeMetaService GET_INFO_FN thrown";
36977
+ const payload = {
36978
+ error: errorData(error),
36979
+ message: getErrorMessage(error),
36980
+ };
36981
+ self.loggerService.warn(message, payload);
36982
+ console.error(message, payload);
36983
+ errorEmitter.next(error);
36984
+ },
36985
+ defaultValue: null,
36986
+ });
36987
+ const GET_PRICE_FN = trycatch(async (self, symbol, context, backtest) => {
36988
+ return await self.priceMetaService.getCurrentPrice(symbol, context, backtest);
36989
+ }, {
36990
+ fallback: (error, self) => {
36991
+ const message = "RuntimeMetaService GET_PRICE_FN thrown";
36992
+ const payload = {
36993
+ error: errorData(error),
36994
+ message: getErrorMessage(error),
36995
+ };
36996
+ self.loggerService.warn(message, payload);
36997
+ console.error(message, payload);
36998
+ errorEmitter.next(error);
36999
+ },
37000
+ defaultValue: null,
37001
+ });
37002
+ const RuntimeMetaService = singleton(class {
37003
+ constructor() {
37004
+ this.loggerService = inject(TYPES.loggerService);
37005
+ this.timeMetaService = inject(TYPES.timeMetaService);
37006
+ this.priceMetaService = inject(TYPES.priceMetaService);
37007
+ this.frameSchemaService = inject(TYPES.frameSchemaService);
37008
+ this.strategySchemaService = inject(TYPES.strategySchemaService);
37009
+ /**
37010
+ * Fetches the time range for the current strategy execution context.
37011
+ *
37012
+ * For backtest mode, it retrieves the start and end dates from the frame schema.
37013
+ * For live mode, it returns null since there is no predefined time range.
37014
+ *
37015
+ * This method is memoized to optimize performance, as the time range for a given context will not change during execution.
37016
+ *
37017
+ * @param context - Strategy, exchange, and frame identifiers
37018
+ * @param backtest - True if backtest mode, false if live mode
37019
+ * @returns An object containing 'from' and 'to' Date objects for backtest mode, or null for live mode
37020
+ */
37021
+ this._getRange = memoize(([context, backtest]) => `${context.frameName}:${backtest ? "backtest" : "live"}`, (context, backtest) => {
37022
+ return GET_RANGE_FN(this, context, backtest);
37023
+ });
37024
+ /**
37025
+ * Fetches strategy-defined runtime information for the current execution context.
37026
+ *
37027
+ * This method retrieves the 'info' object defined in the strategy schema, which can contain any custom data the strategy wants to track at runtime.
37028
+ * The content of this object is not defined by the system and can be used freely by strategy implementations for monitoring, reporting, or external logic.
37029
+ *
37030
+ * This method is memoized to optimize performance, as the strategy info for a given context will not change during execution.
37031
+ *
37032
+ * @param context - Strategy, exchange, and frame identifiers
37033
+ * @returns The 'info' object defined in the strategy schema for the given strategy, or null if not defined
37034
+ */
37035
+ this._getInfo = memoize(([context]) => context.strategyName, (context) => {
37036
+ return GET_INFO_FN(this, context);
37037
+ });
37038
+ /**
37039
+ * Fetches comprehensive runtime information for a given symbol and strategy context, including current price, timestamp, and strategy-specific info.
37040
+ *
37041
+ * This method aggregates data from multiple sources (time, price, frame schema, strategy schema) to provide a complete picture of the current runtime state for a strategy tick.
37042
+ *
37043
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
37044
+ * @param context - Strategy, exchange, and frame identifiers
37045
+ * @param backtest - True if backtest mode, false if live mode
37046
+ * @returns An object containing symbol, time range, strategy-defined info, context, timestamp, current price, and backtest flag
37047
+ */
37048
+ this.getRuntimeInfo = async (symbol, context, backtest) => {
37049
+ this.loggerService.log("runtimeMetaService getRuntimeInfo", {
37050
+ symbol,
37051
+ context,
37052
+ backtest,
37053
+ });
37054
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
37055
+ const when = new Date(timestamp);
37056
+ const currentPrice = await GET_PRICE_FN(this, symbol, context, backtest);
37057
+ const range = this._getRange(context, backtest);
37058
+ const info = this._getInfo(context);
37059
+ return {
37060
+ symbol,
37061
+ range,
37062
+ info,
37063
+ context,
37064
+ backtest,
37065
+ when,
37066
+ currentPrice,
37067
+ };
37068
+ };
37069
+ }
37070
+ });
37071
+
36800
37072
  {
36801
37073
  provide(TYPES.loggerService, () => new LoggerService());
36802
37074
  }
@@ -36833,6 +37105,7 @@ class NotificationHelperService {
36833
37105
  provide(TYPES.contextMetaService, () => new ContextMetaService());
36834
37106
  provide(TYPES.priceMetaService, () => new PriceMetaService());
36835
37107
  provide(TYPES.timeMetaService, () => new TimeMetaService());
37108
+ provide(TYPES.runtimeMetaService, () => new RuntimeMetaService());
36836
37109
  }
36837
37110
  {
36838
37111
  provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
@@ -36936,6 +37209,7 @@ const metaServices = {
36936
37209
  timeMetaService: inject(TYPES.timeMetaService),
36937
37210
  priceMetaService: inject(TYPES.priceMetaService),
36938
37211
  contextMetaService: inject(TYPES.contextMetaService),
37212
+ runtimeMetaService: inject(TYPES.runtimeMetaService),
36939
37213
  };
36940
37214
  const globalServices = {
36941
37215
  sizingGlobalService: inject(TYPES.sizingGlobalService),
@@ -50449,6 +50723,7 @@ const GET_TIMESTAMP_METHOD_NAME = "meta.getTimestamp";
50449
50723
  const GET_MODE_METHOD_NAME = "meta.getMode";
50450
50724
  const GET_SYMBOL_METHOD_NAME = "meta.getSymbol";
50451
50725
  const GET_CONTEXT_METHOD_NAME = "meta.getContext";
50726
+ const GET_RUNTIME_INFO_METHOD_NAME = "meta.getRuntimeInfo";
50452
50727
  /**
50453
50728
  * Gets the current date from execution context.
50454
50729
  *
@@ -50565,6 +50840,45 @@ async function getContext() {
50565
50840
  }
50566
50841
  return backtest.methodContextService.context;
50567
50842
  }
50843
+ /**
50844
+ * Gets runtime information about the current execution environment.
50845
+ *
50846
+ * This includes details such as the current symbol, exchange, timeframe, strategy, and whether it's a backtest or live run.
50847
+ *
50848
+ * @returns Promise resolving to an object containing runtime information
50849
+ * @throws Error if method context or execution context is not active
50850
+ *
50851
+ * @example
50852
+ * ```typescript
50853
+ * const runtimeInfo = await getRuntimeInfo();
50854
+ * console.log(runtimeInfo);
50855
+ * // {
50856
+ * // symbol: "BTCUSDT",
50857
+ * // context: {,
50858
+ * // exchangeName: "Binance",
50859
+ * // frameName: "1m",
50860
+ * // strategyName: "MyStrategy",
50861
+ * // },
50862
+ * // backtest: false
50863
+ * // }
50864
+ * ```
50865
+ */
50866
+ async function getRuntimeInfo() {
50867
+ backtest.loggerService.info(GET_RUNTIME_INFO_METHOD_NAME);
50868
+ if (!MethodContextService.hasContext()) {
50869
+ throw new Error("getRuntimeInfo requires a method context");
50870
+ }
50871
+ if (!ExecutionContextService.hasContext()) {
50872
+ throw new Error("getRuntimeInfo requires an execution context");
50873
+ }
50874
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
50875
+ const { symbol, backtest: isBacktest } = backtest.executionContextService.context;
50876
+ return await backtest.runtimeMetaService.getRuntimeInfo(symbol, {
50877
+ exchangeName,
50878
+ frameName,
50879
+ strategyName,
50880
+ }, isBacktest);
50881
+ }
50568
50882
 
50569
50883
  const RECENT_PERSIST_BACKTEST_METHOD_NAME_HANDLE_ACTIVE_PING = "RecentPersistBacktestUtils.handleActivePing";
50570
50884
  const RECENT_PERSIST_BACKTEST_METHOD_NAME_GET_LATEST_SIGNAL = "RecentPersistBacktestUtils.getLatestSignal";
@@ -64591,6 +64905,20 @@ const CRON_METHOD_NAME_TICK = "CronUtils._tick";
64591
64905
  const CRON_METHOD_NAME_ENABLE = "CronUtils.enable";
64592
64906
  const CRON_METHOD_NAME_DISABLE = "CronUtils.disable";
64593
64907
  const CRON_METHOD_NAME_DISPOSE = "CronUtils.dispose";
64908
+ /**
64909
+ * Watchdog timeout (ms) for a single cron handler invocation.
64910
+ *
64911
+ * A handler that does not settle within this window is treated as failed:
64912
+ * `_runEntry` races `entry.handler(info)` against this `sleep` and, when the
64913
+ * timeout wins, throws into the same `catch` as any other handler error —
64914
+ * surfacing `failed = true`, logging a warning, and (for periodic entries)
64915
+ * rolling back the watermark so the boundary is retried on the next tick.
64916
+ *
64917
+ * This guards the `singlerun`-serialised tick pipeline against a handler that
64918
+ * never resolves (a lost `resolve`, a hung promise with no timeout of its
64919
+ * own): without it such a handler would stall every subsequent tick forever.
64920
+ */
64921
+ const CRON_HANDLER_TIMEOUT = 120000;
64594
64922
  /**
64595
64923
  * Local logger instance.
64596
64924
  *
@@ -64599,6 +64927,20 @@ const CRON_METHOD_NAME_DISPOSE = "CronUtils.dispose";
64599
64927
  * being bootstrapped — `Cron` can be imported and used in isolation.
64600
64928
  */
64601
64929
  const LOGGER_SERVICE$1 = new LoggerService();
64930
+ /**
64931
+ * Local runtime-meta-service instance.
64932
+ *
64933
+ * Like {@link LOGGER_SERVICE}, instantiated directly via `new` rather than
64934
+ * resolved from the DI container so `CronUtils` carries no compile-time
64935
+ * dependency on a bootstrapped framework. `RuntimeMetaService` is built with
64936
+ * the `singleton` HOF from `di-singleton`, so `new RuntimeMetaService()`
64937
+ * returns the one shared singleton proxy — the same instance the rest of the
64938
+ * framework injects — and resolves its own dependencies lazily on first use.
64939
+ *
64940
+ * Used by {@link CronUtils._runEntry} to assemble the {@link IRuntimeInfo}
64941
+ * snapshot handed to each cron handler.
64942
+ */
64943
+ const RUNTIME_META_SERVICE = new RuntimeMetaService();
64602
64944
  /**
64603
64945
  * Utility class for registering periodic tasks that fire on candle-interval
64604
64946
  * boundaries of the virtual time produced by parallel backtests.
@@ -64621,8 +64963,8 @@ const LOGGER_SERVICE$1 = new LoggerService();
64621
64963
  * Cron.register({
64622
64964
  * name: "tg-signal-parser",
64623
64965
  * interval: "1h",
64624
- * handler: async (symbol, when, backtest) => {
64625
- * await parseTelegramSignalsToMongo(when);
64966
+ * handler: async (info) => {
64967
+ * await parseTelegramSignalsToMongo(info.when);
64626
64968
  * },
64627
64969
  * });
64628
64970
  *
@@ -64664,12 +65006,15 @@ class CronUtils {
64664
65006
  * - Fire-once global: `${name}:once:g${generation}`.
64665
65007
  * - Fire-once fan-out: `${name}:once:${symbol}:g${generation}`.
64666
65008
  *
64667
- * Value is the shared in-flight handler promise. Every parallel `tick` for
64668
- * the same slot key awaits this exact promise (mutex semantics) and is
64669
- * released together when it settles. `_inFlight` is owned exclusively by
64670
- * `_runEntry` `clear()` does **not** touch it, so the singleshot promise
64671
- * survives concurrent `clear` calls and continues to coordinate parallel
64672
- * ticks until it settles.
65009
+ * Value is the shared in-flight handler promise. It resolves to a `boolean`
65010
+ * "failed" flag (`true` when the handler or the runtime-info assembly
65011
+ * threw), which `_tick` uses to roll back the periodic watermark of the slot
65012
+ * it opened so a failed boundary is retried. Every parallel `tick` for the
65013
+ * same slot key awaits this exact promise (mutex semantics) and is released
65014
+ * together when it settles. `_inFlight` is owned exclusively by `_runEntry` —
65015
+ * `clear()` does **not** touch it, so the singleshot promise survives
65016
+ * concurrent `clear` calls and continues to coordinate parallel ticks until
65017
+ * it settles.
64673
65018
  */
64674
65019
  this._inFlight = new Map();
64675
65020
  /**
@@ -64713,9 +65058,12 @@ class CronUtils {
64713
65058
  *
64714
65059
  * Written synchronously in `_tick` at slot-open time (before the `await`),
64715
65060
  * so a still-in-flight handler does not let a later tick re-open the same
64716
- * (or an already-passed) boundary. Fire-once entries never touch this map —
64717
- * they use `_firedOnce`. Pruned by `_clearBoundaryFor` on
64718
- * `register`/`unregister` and wiped by `dispose`.
65061
+ * (or an already-passed) boundary. If that handler then **fails**, the
65062
+ * advance is rolled back after the slot settles — the prior value is restored
65063
+ * (or the key deleted if there was none) — so the failed boundary is retried
65064
+ * on the next tick, mirroring catch-up of a skipped boundary. Fire-once
65065
+ * entries never touch this map — they use `_firedOnce`. Pruned by
65066
+ * `_clearBoundaryFor` on `register`/`unregister` and wiped by `dispose`.
64719
65067
  */
64720
65068
  this._lastBoundary = new Map();
64721
65069
  /**
@@ -64735,7 +65083,7 @@ class CronUtils {
64735
65083
  * name: "fetch-funding",
64736
65084
  * interval: "8h",
64737
65085
  * symbols: ["BTCUSDT", "ETHUSDT"],
64738
- * handler: async (symbol, when, backtest) => { ... },
65086
+ * handler: async (info) => { ... },
64739
65087
  * });
64740
65088
  * // Later:
64741
65089
  * dispose();
@@ -64855,7 +65203,7 @@ class CronUtils {
64855
65203
  * 4. **Fire-once** (`entry.interval === undefined`):
64856
65204
  * - If the entry's fired-once key is already in `_firedOnce`, skip.
64857
65205
  * - Slot key: `${name}:once` (+ scope) (+ gen).
64858
- * - `aligned` = the 1-minute-aligned `when` from step 0.
65206
+ * - `alignedMs` = the 1-minute-aligned `when` from step 0 (`ts`).
64859
65207
  * 5. **Periodic** (`entry.interval` set):
64860
65208
  * - Align `when` to the entry's interval via {@link alignToInterval} to
64861
65209
  * get `alignedMs`, the boundary this tick belongs to.
@@ -64877,32 +65225,44 @@ class CronUtils {
64877
65225
  * handler is still in flight.
64878
65226
  * - Slot key: `${name}:${alignedMs}` (+ scope) (+ gen).
64879
65227
  * 6. Singleshot per slot key: look up the slot in `_inFlight`. If a promise
64880
- * already exists, `await` the same promise. Otherwise invoke
64881
- * `entry.handler`, store the promise, and `await` it. The slot is
64882
- * removed in `.finally()` so the next boundary creates a fresh promise;
64883
- * for fire-once entries the fired-once key is also added to
64884
- * `_firedOnce` on success so subsequent ticks skip it.
65228
+ * already exists, `await` the same promise. Otherwise open the slot via
65229
+ * {@link _runEntry} which assembles the {@link IRuntimeInfo} snapshot
65230
+ * (from `symbol`, `context`, `backtest`) and invokes `entry.handler(info)`
65231
+ * store the promise, and `await` it. The slot is removed in `.finally()`
65232
+ * so the next boundary creates a fresh promise; for fire-once entries the
65233
+ * fired-once key is also added to `_firedOnce` on success so subsequent
65234
+ * ticks skip it.
65235
+ * 7. After `await Promise.all`, roll back the watermark for every **periodic**
65236
+ * slot this tick *opened* (not the ones whose in-flight promise it reused)
65237
+ * whose handler reported failure, so the next tick re-opens and re-runs
65238
+ * that boundary.
64885
65239
  *
64886
65240
  * Errors thrown by `handler` are caught, logged via `console.error`, and
64887
65241
  * **not** rethrown — a failing handler must not break the per-symbol
64888
65242
  * tick loop or unblock other parallel backtests with an unhandled
64889
65243
  * rejection. A failed fire-once handler is **not** marked as fired and
64890
- * will retry on the next tick.
65244
+ * will retry on the next tick. A failed **periodic** handler likewise
65245
+ * retries: the boundary watermark advanced at slot-open time is rolled back
65246
+ * after the slot settles (step 7), so the next tick re-opens that boundary.
64891
65247
  *
64892
65248
  * Requires active method context and execution context.
64893
65249
  *
64894
65250
  * @param symbol - Trading symbol from the current tick.
64895
65251
  * @param when - Virtual time of the current tick.
64896
65252
  * @param backtest - `true` for backtest ticks, `false` for live ticks.
64897
- * Forwarded as the third argument to `entry.handler`. Only the value
64898
- * from the tick that **opens** a given slot is observed by all parallel
64899
- * awaiters of that slot.
65253
+ * Forwarded to {@link _runEntry} and surfaced as `info.backtest`. Only the
65254
+ * value from the tick that **opens** a given slot is observed by all
65255
+ * parallel awaiters of that slot.
65256
+ * @param context - Strategy/exchange/frame identifiers from the originating
65257
+ * lifecycle event, forwarded to `RuntimeMetaService.getRuntimeInfo` to
65258
+ * build the {@link IRuntimeInfo} snapshot passed to the handler.
64900
65259
  * @throws Error if method or execution context is missing.
64901
65260
  */
64902
- this._tick = async (symbol, when, backtest) => {
65261
+ this._tick = async (symbol, when, backtest, context) => {
64903
65262
  LOGGER_SERVICE$1.debug(CRON_METHOD_NAME_TICK, {
64904
65263
  symbol,
64905
65264
  when,
65265
+ context,
64906
65266
  });
64907
65267
  if (!MethodContextService.hasContext()) {
64908
65268
  throw new Error("CronUtils _tick requires method context");
@@ -64912,6 +65272,10 @@ class CronUtils {
64912
65272
  }
64913
65273
  const ts = alignToInterval(when, "1m").getTime();
64914
65274
  const taskList = [];
65275
+ // Periodic slots THIS tick actually opened (the `!pending` branch), tracked
65276
+ // for watermark rollback on failure. See {@link IOpenedSlot} for what is and
65277
+ // is not recorded here and why.
65278
+ const openedList = [];
64915
65279
  for (const { entry, generation } of this._entries.values()) {
64916
65280
  if (entry.symbols?.length && !entry.symbols.includes(symbol)) {
64917
65281
  continue;
@@ -64919,7 +65283,6 @@ class CronUtils {
64919
65283
  const perSymbol = !!entry.symbols?.length;
64920
65284
  const scope = perSymbol ? `:${symbol}` : "";
64921
65285
  const genSuffix = `:g${generation}`;
64922
- let aligned;
64923
65286
  let alignedMs;
64924
65287
  let slotKey;
64925
65288
  let firedKey;
@@ -64931,15 +65294,13 @@ class CronUtils {
64931
65294
  if (this._firedOnce.has(onceKey)) {
64932
65295
  continue;
64933
65296
  }
64934
- aligned = alignToInterval(when, "1m");
64935
65297
  alignedMs = ts;
64936
65298
  slotKey = `${entry.name}:once${scope}${genSuffix}`;
64937
65299
  firedKey = onceKey;
64938
65300
  boundaryKey = null;
64939
65301
  }
64940
65302
  else {
64941
- aligned = alignToInterval(when, entry.interval);
64942
- alignedMs = aligned.getTime();
65303
+ alignedMs = alignToInterval(when, entry.interval).getTime();
64943
65304
  boundaryKey = `${entry.name}${scope}${genSuffix}`;
64944
65305
  const lastBoundary = this._lastBoundary.get(boundaryKey);
64945
65306
  // Fire when the tick's aligned boundary has advanced past the last one
@@ -64957,16 +65318,70 @@ class CronUtils {
64957
65318
  // Advance the watermark synchronously at slot-open time, before the
64958
65319
  // await below. Otherwise a later tick on the same (or an already
64959
65320
  // crossed) boundary, arriving while this handler is still in flight,
64960
- // would see the stale watermark and open a duplicate slot.
65321
+ // would see the stale watermark and open a duplicate slot. The advance
65322
+ // is rolled back after the slot settles if the handler failed (see the
65323
+ // post-await loop below), so a failed boundary is retried next tick.
64961
65324
  if (boundaryKey !== null) {
65325
+ // Capture the pre-advance value so it can be restored verbatim on
65326
+ // failure (undefined => the boundary had never opened => delete the
65327
+ // key on rollback). Read fresh here rather than reusing `lastBoundary`
65328
+ // above to keep the value↔slot binding local and obvious; there is no
65329
+ // `await` between the two reads, so they are identical.
65330
+ const prevBoundary = this._lastBoundary.get(boundaryKey);
64962
65331
  this._lastBoundary.set(boundaryKey, alignedMs);
65332
+ pending = this._runEntry(entry, symbol, alignedMs, slotKey, firedKey, backtest, context);
65333
+ this._inFlight.set(slotKey, pending);
65334
+ openedList.push({ boundaryKey, prevBoundary, pending });
65335
+ }
65336
+ else {
65337
+ pending = this._runEntry(entry, symbol, alignedMs, slotKey, firedKey, backtest, context);
65338
+ this._inFlight.set(slotKey, pending);
64963
65339
  }
64964
- pending = this._runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest);
64965
- this._inFlight.set(slotKey, pending);
64966
65340
  }
64967
65341
  taskList.push(pending);
64968
65342
  }
64969
- await Promise.all(taskList);
65343
+ {
65344
+ // Watchdog: warn (do not interrupt) if the slots this tick is awaiting
65345
+ // have not settled within CRON_HANDLER_TIMEOUT. We deliberately keep
65346
+ // awaiting Promise.all so the singlerun pipeline stays serialised and no
65347
+ // duplicate/zombie slots are spawned — the timer only surfaces the stall.
65348
+ // Use a real setTimeout/clearTimeout (not sleep) so the alarm is cancelled
65349
+ // the instant Promise.all resolves, rather than lingering for the full
65350
+ // timeout on every fast tick.
65351
+ const timer = setTimeout(() => {
65352
+ const message = `${CRON_METHOD_NAME_TICK} timed out after ${CRON_HANDLER_TIMEOUT}ms`;
65353
+ const payload = { symbol, when, context };
65354
+ LOGGER_SERVICE$1.warn(message, payload);
65355
+ console.error(message, payload);
65356
+ errorEmitter.next(new Error(message));
65357
+ }, CRON_HANDLER_TIMEOUT);
65358
+ try {
65359
+ await Promise.all(taskList);
65360
+ }
65361
+ finally {
65362
+ clearTimeout(timer);
65363
+ }
65364
+ }
65365
+ // Roll back the watermark for any periodic slot THIS tick opened whose
65366
+ // handler failed, so the next tick re-opens the same boundary and retries
65367
+ // it — mirroring how a skipped boundary is later caught up. Restoring
65368
+ // `prevBoundary` (or deleting the key when it was `undefined`) re-arms the
65369
+ // strict-`>` gate without disturbing any earlier already-fired boundary.
65370
+ // `await pending` is cheap — every promise already settled in `Promise.all`
65371
+ // above; we re-await via `openedList` because its entries (opened slots
65372
+ // only) do not line up with `taskList` indices.
65373
+ for (const { boundaryKey, prevBoundary, pending } of openedList) {
65374
+ const failed = await pending;
65375
+ if (!failed) {
65376
+ continue;
65377
+ }
65378
+ if (prevBoundary === undefined) {
65379
+ this._lastBoundary.delete(boundaryKey);
65380
+ }
65381
+ else {
65382
+ this._lastBoundary.set(boundaryKey, prevBoundary);
65383
+ }
65384
+ }
64970
65385
  };
64971
65386
  /**
64972
65387
  * Subscribe `Cron` to the engine's strategy lifecycle subjects so registered
@@ -64981,7 +65396,11 @@ class CronUtils {
64981
65396
  *
64982
65397
  * All four subjects are subscribed to a single `singlerun`-wrapped
64983
65398
  * handler that builds `_tick(event.symbol, new Date(event.timestamp),
64984
- * event.backtest)`. `singlerun` merges the four streams into one serial
65399
+ * event.backtest, { strategyName, exchangeName, frameName })`. The context
65400
+ * object is read uniformly from the event — every contract carries
65401
+ * `strategyName`, `exchangeName` and `frameName` at the top level (Active /
65402
+ * Schedule contracts gained `frameName` for exactly this reason), so no
65403
+ * per-event branching is needed. `singlerun` merges the four streams into one serial
64985
65404
  * queue: at most one `_tick` runs at a time, the next waits. This matters
64986
65405
  * because the engine can emit `beforeStart` and an immediate `idlePing`
64987
65406
  * on the very same minute, and concurrent `_tick`s on the same
@@ -65017,7 +65436,11 @@ class CronUtils {
65017
65436
  this.enable = singleshot(() => {
65018
65437
  LOGGER_SERVICE$1.info(CRON_METHOD_NAME_ENABLE);
65019
65438
  const handleTick = singlerun(async (event) => {
65020
- return await this._tick(event.symbol, new Date(event.timestamp), event.backtest);
65439
+ return await this._tick(event.symbol, new Date(event.timestamp), event.backtest, {
65440
+ strategyName: event.strategyName,
65441
+ exchangeName: event.exchangeName,
65442
+ frameName: event.frameName,
65443
+ });
65021
65444
  });
65022
65445
  const unBeforeStart = beforeStartSubject.subscribe(handleTick);
65023
65446
  const unIdlePing = idlePingSubject.subscribe(handleTick);
@@ -65120,25 +65543,51 @@ class CronUtils {
65120
65543
  /**
65121
65544
  * Build the singleshot promise for a single in-flight slot.
65122
65545
  *
65123
- * Invokes `entry.handler(symbol, aligned, backtest)`, swallows and logs
65124
- * any error via `console.error`, and clears the `_inFlight` slot
65125
- * in `.finally()` so the next boundary produces a fresh promise. For
65126
- * fire-once entries `firedKey` is added to `_firedOnce` on success so
65127
- * subsequent ticks skip it.
65128
- *
65546
+ * Assembles the {@link IRuntimeInfo} snapshot via
65547
+ * `RuntimeMetaService.getRuntimeInfo(symbol, context, backtest)` and invokes
65548
+ * `entry.handler(info)`. Logs any error via `console.error` and **returns** a
65549
+ * `failed` boolean (`true` when the handler or the runtime-info assembly —
65550
+ * threw) so the caller (`_tick`) can roll back the periodic watermark of the
65551
+ * slot it opened and retry that boundary. The error is **not** rethrown, so a
65552
+ * failing handler never produces an unhandled rejection. Clears the
65553
+ * `_inFlight` slot in `.finally()` so the next boundary produces a fresh
65554
+ * promise. For fire-once entries `firedKey` is added to `_firedOnce` on
65555
+ * success so subsequent ticks skip it.
65556
+ *
65557
+ * `getRuntimeInfo` is the user-facing aggregator: its sub-fetches (range,
65558
+ * info, price) are individually wrapped in `trycatch` with `null` fallbacks,
65559
+ * so it almost never throws for missing data. Whatever does throw — the
65560
+ * handler, or in rare cases `getRuntimeInfo` — is caught here and reported via
65561
+ * the returned `failed` flag; the watermark rollback treats both identically.
65562
+ *
65563
+ * @param context - Strategy/exchange/frame identifiers from the originating
65564
+ * lifecycle event, forwarded to `getRuntimeInfo` to resolve `range`/`info`.
65129
65565
  * @param firedKey - Key to add to `_firedOnce` on success, or `null` for
65130
65566
  * periodic entries (which never populate `_firedOnce`).
65131
- * @param backtest - Value forwarded as the third handler argument; the
65132
- * "winner" tick's flag is what all parallel awaiters of this slot see.
65567
+ * @param backtest - Forwarded to `getRuntimeInfo` and surfaced as
65568
+ * `info.backtest`; the "winner" tick's flag is what all parallel awaiters
65569
+ * of this slot see.
65570
+ * @returns `true` if the handler (or `getRuntimeInfo`) threw, `false` on
65571
+ * success. `_tick` uses this to decide whether to roll back the watermark.
65133
65572
  */
65134
- async _runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest) {
65573
+ async _runEntry(entry, symbol, alignedMs, slotKey, firedKey, backtest, context) {
65135
65574
  let failed = false;
65136
65575
  try {
65137
- await entry.handler(symbol, aligned, backtest);
65576
+ const info = await RUNTIME_META_SERVICE.getRuntimeInfo(symbol, context, backtest);
65577
+ await entry.handler(info);
65138
65578
  }
65139
- catch (err) {
65579
+ catch (error) {
65140
65580
  failed = true;
65141
- console.error(`${CRON_METHOD_NAME_TICK} entry "${entry.name}" failed`, { symbol, alignedMs, err });
65581
+ const message = `${CRON_METHOD_NAME_TICK} entry "${entry.name}" failed`;
65582
+ const payload = {
65583
+ symbol,
65584
+ alignedMs,
65585
+ error: errorData(error),
65586
+ message: getErrorMessage(error),
65587
+ };
65588
+ LOGGER_SERVICE$1.warn(message, payload);
65589
+ console.error(message, payload);
65590
+ errorEmitter.next(error);
65142
65591
  }
65143
65592
  finally {
65144
65593
  this._inFlight.delete(slotKey);
@@ -65146,6 +65595,7 @@ class CronUtils {
65146
65595
  this._firedOnce.add(firedKey);
65147
65596
  }
65148
65597
  }
65598
+ return failed;
65149
65599
  }
65150
65600
  }
65151
65601
  /**
@@ -65159,7 +65609,7 @@ class CronUtils {
65159
65609
  * Cron.register({
65160
65610
  * name: "tg-parser",
65161
65611
  * interval: "1h",
65162
- * handler: async (symbol, when, backtest) => { ... },
65612
+ * handler: async (info) => { ... },
65163
65613
  * });
65164
65614
  * ```
65165
65615
  */
@@ -66325,4 +66775,4 @@ const validateSignal = (signal, currentPrice) => {
66325
66775
  return !errors.length;
66326
66776
  };
66327
66777
 
66328
- export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Cron, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Interval, Live, Log, Lookup, Markdown, MarkdownFileBase, MarkdownFolderBase, MarkdownWriter, MaxDrawdown, Memory, MemoryBacktest, MemoryBacktestAdapter, MemoryLive, MemoryLiveAdapter, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistBreakevenInstance, PersistCandleAdapter, PersistCandleInstance, PersistIntervalAdapter, PersistIntervalInstance, PersistLogAdapter, PersistLogInstance, PersistMeasureAdapter, PersistMeasureInstance, PersistMemoryAdapter, PersistMemoryInstance, PersistNotificationAdapter, PersistNotificationInstance, PersistPartialAdapter, PersistPartialInstance, PersistRecentAdapter, PersistRecentInstance, PersistRiskAdapter, PersistRiskInstance, PersistScheduleAdapter, PersistScheduleInstance, PersistSessionAdapter, PersistSessionInstance, PersistSignalAdapter, PersistSignalInstance, PersistStateAdapter, PersistStateInstance, PersistStorageAdapter, PersistStorageInstance, Position, PositionSize, Recent, RecentBacktest, RecentLive, Reflect$1 as Reflect, Report, ReportBase, ReportWriter, Risk, Schedule, Session, SessionBacktest, SessionLive, State, StateBacktest, StateBacktestAdapter, StateLive, StateLiveAdapter, Storage, StorageBacktest, StorageLive, Strategy, Sync, System, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, beginContext, beginTime, cacheCandles, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitSignalNotify, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, createSignalState, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getClosePrice, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getLatestSignal, getMaxDrawdownDistancePnlCost, getMaxDrawdownDistancePnlPercentage, getMinutesSinceLatestSignalCreated, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionActiveMinutes, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestMaxDrawdownPnlCost, getPositionHighestMaxDrawdownPnlPercentage, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitDistancePnlCost, getPositionHighestProfitDistancePnlPercentage, getPositionHighestProfitMinutes, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionMaxDrawdownMinutes, getPositionMaxDrawdownPnlCost, getPositionMaxDrawdownPnlPercentage, getPositionMaxDrawdownPrice, getPositionMaxDrawdownTimestamp, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getPositionWaitingMinutes, getRawCandles, getRiskSchema, getScheduledSignal, getSessionData, getSignalState, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, intervalStepMs, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenAfterEnd, listenAfterEndOnce, listenBacktestProgress, listenBeforeStart, listenBeforeStartOnce, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenIdlePing, listenIdlePingOnce, listenMaxDrawdown, listenMaxDrawdownOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalNotify, listenSignalNotifyOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, setSessionData, setSignalState, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toPlainString, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, waitForReady, warmCandles, writeMemory };
66778
+ export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Cron, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Interval, Live, Log, Lookup, Markdown, MarkdownFileBase, MarkdownFolderBase, MarkdownWriter, MaxDrawdown, Memory, MemoryBacktest, MemoryBacktestAdapter, MemoryLive, MemoryLiveAdapter, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistBreakevenInstance, PersistCandleAdapter, PersistCandleInstance, PersistIntervalAdapter, PersistIntervalInstance, PersistLogAdapter, PersistLogInstance, PersistMeasureAdapter, PersistMeasureInstance, PersistMemoryAdapter, PersistMemoryInstance, PersistNotificationAdapter, PersistNotificationInstance, PersistPartialAdapter, PersistPartialInstance, PersistRecentAdapter, PersistRecentInstance, PersistRiskAdapter, PersistRiskInstance, PersistScheduleAdapter, PersistScheduleInstance, PersistSessionAdapter, PersistSessionInstance, PersistSignalAdapter, PersistSignalInstance, PersistStateAdapter, PersistStateInstance, PersistStorageAdapter, PersistStorageInstance, Position, PositionSize, Recent, RecentBacktest, RecentLive, Reflect$1 as Reflect, Report, ReportBase, ReportWriter, Risk, Schedule, Session, SessionBacktest, SessionLive, State, StateBacktest, StateBacktestAdapter, StateLive, StateLiveAdapter, Storage, StorageBacktest, StorageLive, Strategy, Sync, System, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, beginContext, beginTime, cacheCandles, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitSignalNotify, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, createSignalState, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getClosePrice, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getLatestSignal, getMaxDrawdownDistancePnlCost, getMaxDrawdownDistancePnlPercentage, getMinutesSinceLatestSignalCreated, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionActiveMinutes, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestMaxDrawdownPnlCost, getPositionHighestMaxDrawdownPnlPercentage, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitDistancePnlCost, getPositionHighestProfitDistancePnlPercentage, getPositionHighestProfitMinutes, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionMaxDrawdownMinutes, getPositionMaxDrawdownPnlCost, getPositionMaxDrawdownPnlPercentage, getPositionMaxDrawdownPrice, getPositionMaxDrawdownTimestamp, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getPositionWaitingMinutes, getRawCandles, getRiskSchema, getRuntimeInfo, getScheduledSignal, getSessionData, getSignalState, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, intervalStepMs, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenAfterEnd, listenAfterEndOnce, listenBacktestProgress, listenBeforeStart, listenBeforeStartOnce, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenIdlePing, listenIdlePingOnce, listenMaxDrawdown, listenMaxDrawdownOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalNotify, listenSignalNotifyOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, setSessionData, setSignalState, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toPlainString, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, waitForReady, warmCandles, writeMemory };