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.cjs CHANGED
@@ -94,6 +94,7 @@ const metaServices$1 = {
94
94
  contextMetaService: Symbol('contextMetaService'),
95
95
  priceMetaService: Symbol('priceMetaService'),
96
96
  timeMetaService: Symbol('timeMetaService'),
97
+ runtimeMetaService: Symbol('runtimeMetaService'),
97
98
  };
98
99
  const globalServices$1 = {
99
100
  sizingGlobalService: Symbol('sizingGlobalService'),
@@ -4680,7 +4681,7 @@ const LOGGER_SERVICE$8 = new LoggerService();
4680
4681
  * @param backtest - `true` for backtest, `false` for live.
4681
4682
  * @returns Colon-joined composite key.
4682
4683
  */
4683
- const CREATE_KEY_FN$y = (symbol, strategyName, exchangeName, frameName, backtest) => {
4684
+ const CREATE_KEY_FN$B = (symbol, strategyName, exchangeName, frameName, backtest) => {
4684
4685
  const parts = [symbol, strategyName, exchangeName];
4685
4686
  if (frameName)
4686
4687
  parts.push(frameName);
@@ -4723,7 +4724,7 @@ class LookupUtils {
4723
4724
  LOGGER_SERVICE$8.info(METHOD_NAME_ADD_ACTIVITY, {
4724
4725
  activity,
4725
4726
  });
4726
- const key = CREATE_KEY_FN$y(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
4727
+ const key = CREATE_KEY_FN$B(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
4727
4728
  this._lookupMap.set(key, activity);
4728
4729
  };
4729
4730
  /**
@@ -4737,7 +4738,7 @@ class LookupUtils {
4737
4738
  LOGGER_SERVICE$8.info(METHOD_NAME_REMOVE_ACTIVITY, {
4738
4739
  activity,
4739
4740
  });
4740
- const key = CREATE_KEY_FN$y(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
4741
+ const key = CREATE_KEY_FN$B(activity.symbol, activity.context.strategyName, activity.context.exchangeName, activity.context.frameName, activity.backtest);
4741
4742
  this._lookupMap.delete(key);
4742
4743
  };
4743
4744
  /**
@@ -12355,7 +12356,7 @@ const GET_RISK_FN = (dto, backtest, exchangeName, frameName, self) => {
12355
12356
  * @param backtest - Whether running in backtest mode
12356
12357
  * @returns Unique string key for memoization
12357
12358
  */
12358
- const CREATE_KEY_FN$x = (symbol, strategyName, exchangeName, frameName, backtest) => {
12359
+ const CREATE_KEY_FN$A = (symbol, strategyName, exchangeName, frameName, backtest) => {
12359
12360
  const parts = [symbol, strategyName, exchangeName];
12360
12361
  if (frameName)
12361
12362
  parts.push(frameName);
@@ -12376,6 +12377,7 @@ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => functoolsKit.trycatch(async (sy
12376
12377
  symbol,
12377
12378
  strategyName,
12378
12379
  exchangeName,
12380
+ frameName: data.frameName,
12379
12381
  currentPrice,
12380
12382
  data,
12381
12383
  backtest,
@@ -12443,6 +12445,7 @@ const CREATE_COMMIT_ACTIVE_PING_FN = (self) => functoolsKit.trycatch(async (symb
12443
12445
  symbol,
12444
12446
  strategyName,
12445
12447
  exchangeName,
12448
+ frameName: data.frameName,
12446
12449
  currentPrice,
12447
12450
  data,
12448
12451
  backtest,
@@ -12655,7 +12658,7 @@ class StrategyConnectionService {
12655
12658
  * @param backtest - Whether running in backtest mode
12656
12659
  * @returns Configured ClientStrategy instance
12657
12660
  */
12658
- this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$x(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
12661
+ this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$A(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
12659
12662
  const { riskName = "", riskList = [], getSignal, interval = STRATEGY_DEFAULT_INTERVAL, callbacks, } = this.strategySchemaService.get(strategyName);
12660
12663
  return new ClientStrategy({
12661
12664
  symbol,
@@ -13617,7 +13620,7 @@ class StrategyConnectionService {
13617
13620
  }
13618
13621
  return;
13619
13622
  }
13620
- const key = CREATE_KEY_FN$x(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
13623
+ const key = CREATE_KEY_FN$A(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
13621
13624
  if (!this.getStrategy.has(key)) {
13622
13625
  return;
13623
13626
  }
@@ -14015,11 +14018,51 @@ class StrategyConnectionService {
14015
14018
  }
14016
14019
  }
14017
14020
 
14021
+ const MS_PER_MINUTE$6 = 60000;
14022
+ const INTERVAL_MINUTES$7 = {
14023
+ "1m": 1,
14024
+ "3m": 3,
14025
+ "5m": 5,
14026
+ "15m": 15,
14027
+ "30m": 30,
14028
+ "1h": 60,
14029
+ "2h": 120,
14030
+ "4h": 240,
14031
+ "6h": 360,
14032
+ "8h": 480,
14033
+ "1d": 1440,
14034
+ };
14035
+ /**
14036
+ * Aligns timestamp down to the nearest interval boundary.
14037
+ * For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
14038
+ *
14039
+ * Candle timestamp convention:
14040
+ * - Candle timestamp = openTime (when candle opens)
14041
+ * - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
14042
+ *
14043
+ * Adapter contract:
14044
+ * - Adapter must return candles with timestamp = openTime
14045
+ * - First returned candle.timestamp must equal aligned since
14046
+ * - Adapter must return exactly `limit` candles
14047
+ *
14048
+ * @param date - Date to align
14049
+ * @param interval - Candle interval (e.g., "1m", "15m", "1h")
14050
+ * @returns New Date aligned down to interval boundary
14051
+ */
14052
+ const alignToInterval = (date, interval) => {
14053
+ const minutes = INTERVAL_MINUTES$7[interval];
14054
+ if (minutes === undefined) {
14055
+ throw new Error(`alignToInterval: unknown interval=${interval}`);
14056
+ }
14057
+ const intervalMs = minutes * MS_PER_MINUTE$6;
14058
+ return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
14059
+ };
14060
+
14018
14061
  /**
14019
14062
  * Maps FrameInterval to minutes for timestamp calculation.
14020
14063
  * Used to generate timeframe arrays with proper spacing.
14021
14064
  */
14022
- const INTERVAL_MINUTES$7 = {
14065
+ const INTERVAL_MINUTES$6 = {
14023
14066
  "1m": 1,
14024
14067
  "3m": 3,
14025
14068
  "5m": 5,
@@ -14073,7 +14116,7 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
14073
14116
  symbol,
14074
14117
  });
14075
14118
  const { interval, startDate, endDate } = self.params;
14076
- const intervalMinutes = INTERVAL_MINUTES$7[interval];
14119
+ const intervalMinutes = INTERVAL_MINUTES$6[interval];
14077
14120
  if (!intervalMinutes) {
14078
14121
  throw new Error(`ClientFrame unknown interval: ${interval}`);
14079
14122
  }
@@ -14082,8 +14125,14 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
14082
14125
  today.setUTCHours(0, 0, 0, 0);
14083
14126
  // Ensure endDate doesn't go beyond today
14084
14127
  const effectiveEndDate = endDate > today ? today : endDate;
14128
+ // Align the iteration start down to the 1-minute boundary so every generated
14129
+ // timestamp lands on a clean minute, matching live mode
14130
+ // (LiveLogicPrivateService aligns `when` via alignToInterval(new Date(), "1m")).
14131
+ // Without this, a startDate carrying sub-minute (or any non-aligned) offset
14132
+ // would propagate that offset to every tick `when` — and therefore to
14133
+ // IRuntimeInfo.when handed to Cron handlers — diverging from live behaviour.
14085
14134
  const timeframes = [];
14086
- let currentDate = new Date(startDate);
14135
+ let currentDate = alignToInterval(startDate, "1m");
14087
14136
  while (currentDate <= effectiveEndDate) {
14088
14137
  timeframes.push(new Date(currentDate));
14089
14138
  currentDate = new Date(currentDate.getTime() + intervalMinutes * 60 * 1000);
@@ -14117,6 +14166,7 @@ class ClientFrame {
14117
14166
  }
14118
14167
  }
14119
14168
 
14169
+ const DEFAULT_INTERVAL = "1m";
14120
14170
  /**
14121
14171
  * Connection service routing frame operations to correct ClientFrame instance.
14122
14172
  *
@@ -14160,7 +14210,7 @@ class FrameConnectionService {
14160
14210
  logger: this.loggerService,
14161
14211
  startDate,
14162
14212
  endDate,
14163
- interval,
14213
+ interval: interval || DEFAULT_INTERVAL,
14164
14214
  callbacks,
14165
14215
  });
14166
14216
  });
@@ -14819,7 +14869,7 @@ class ClientRisk {
14819
14869
  * @param backtest - Whether running in backtest mode
14820
14870
  * @returns Unique string key for memoization
14821
14871
  */
14822
- const CREATE_KEY_FN$w = (riskName, exchangeName, frameName, backtest) => {
14872
+ const CREATE_KEY_FN$z = (riskName, exchangeName, frameName, backtest) => {
14823
14873
  const parts = [riskName, exchangeName];
14824
14874
  if (frameName)
14825
14875
  parts.push(frameName);
@@ -14919,7 +14969,7 @@ class RiskConnectionService {
14919
14969
  * @param backtest - True if backtest mode, false if live mode
14920
14970
  * @returns Configured ClientRisk instance
14921
14971
  */
14922
- this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$w(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
14972
+ this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$z(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
14923
14973
  const schema = this.riskSchemaService.get(riskName);
14924
14974
  return new ClientRisk({
14925
14975
  ...schema,
@@ -15009,7 +15059,7 @@ class RiskConnectionService {
15009
15059
  payload,
15010
15060
  });
15011
15061
  if (payload) {
15012
- const key = CREATE_KEY_FN$w(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
15062
+ const key = CREATE_KEY_FN$z(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
15013
15063
  this.getRisk.clear(key);
15014
15064
  }
15015
15065
  else {
@@ -16128,7 +16178,7 @@ class ClientAction {
16128
16178
  * @param backtest - Whether running in backtest mode
16129
16179
  * @returns Unique string key for memoization
16130
16180
  */
16131
- const CREATE_KEY_FN$v = (actionName, strategyName, exchangeName, frameName, backtest) => {
16181
+ const CREATE_KEY_FN$y = (actionName, strategyName, exchangeName, frameName, backtest) => {
16132
16182
  const parts = [actionName, strategyName, exchangeName];
16133
16183
  if (frameName)
16134
16184
  parts.push(frameName);
@@ -16180,7 +16230,7 @@ class ActionConnectionService {
16180
16230
  * @param backtest - True if backtest mode, false if live mode
16181
16231
  * @returns Configured ClientAction instance
16182
16232
  */
16183
- this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$v(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
16233
+ this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$y(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
16184
16234
  const schema = this.actionSchemaService.get(actionName);
16185
16235
  return new ClientAction({
16186
16236
  ...schema,
@@ -16406,7 +16456,7 @@ class ActionConnectionService {
16406
16456
  await Promise.all(actions.map(async (action) => await action.dispose()));
16407
16457
  return;
16408
16458
  }
16409
- const key = CREATE_KEY_FN$v(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
16459
+ const key = CREATE_KEY_FN$y(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
16410
16460
  if (!this.getAction.has(key)) {
16411
16461
  return;
16412
16462
  }
@@ -16417,14 +16467,14 @@ class ActionConnectionService {
16417
16467
  }
16418
16468
  }
16419
16469
 
16420
- const METHOD_NAME_VALIDATE$3 = "exchangeCoreService validate";
16470
+ const METHOD_NAME_VALIDATE$6 = "exchangeCoreService validate";
16421
16471
  /**
16422
16472
  * Creates a unique key for memoizing validate calls.
16423
16473
  * Key format: "exchangeName"
16424
16474
  * @param exchangeName - Exchange name
16425
16475
  * @returns Unique string key for memoization
16426
16476
  */
16427
- const CREATE_KEY_FN$u = (exchangeName) => {
16477
+ const CREATE_KEY_FN$x = (exchangeName) => {
16428
16478
  return exchangeName;
16429
16479
  };
16430
16480
  /**
@@ -16448,11 +16498,11 @@ class ExchangeCoreService {
16448
16498
  * @param exchangeName - Name of the exchange to validate
16449
16499
  * @returns Promise that resolves when validation is complete
16450
16500
  */
16451
- this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$u(exchangeName), async (exchangeName) => {
16452
- this.loggerService.log(METHOD_NAME_VALIDATE$3, {
16501
+ this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$x(exchangeName), async (exchangeName) => {
16502
+ this.loggerService.log(METHOD_NAME_VALIDATE$6, {
16453
16503
  exchangeName,
16454
16504
  });
16455
- this.exchangeValidationService.validate(exchangeName, METHOD_NAME_VALIDATE$3);
16505
+ this.exchangeValidationService.validate(exchangeName, METHOD_NAME_VALIDATE$6);
16456
16506
  });
16457
16507
  /**
16458
16508
  * Fetches historical candles with execution context.
@@ -16721,14 +16771,14 @@ class ExchangeCoreService {
16721
16771
  }
16722
16772
  }
16723
16773
 
16724
- const METHOD_NAME_VALIDATE$2 = "strategyCoreService validate";
16774
+ const METHOD_NAME_VALIDATE$5 = "strategyCoreService validate";
16725
16775
  /**
16726
16776
  * Creates a unique key for memoizing validate calls.
16727
16777
  * Key format: "strategyName:exchangeName:frameName"
16728
16778
  * @param context - Execution context with strategyName, exchangeName, frameName
16729
16779
  * @returns Unique string key for memoization
16730
16780
  */
16731
- const CREATE_KEY_FN$t = (context) => {
16781
+ const CREATE_KEY_FN$w = (context) => {
16732
16782
  const parts = [context.strategyName, context.exchangeName];
16733
16783
  if (context.frameName)
16734
16784
  parts.push(context.frameName);
@@ -16751,6 +16801,7 @@ class StrategyCoreService {
16751
16801
  this.strategyValidationService = inject(TYPES.strategyValidationService);
16752
16802
  this.exchangeValidationService = inject(TYPES.exchangeValidationService);
16753
16803
  this.frameValidationService = inject(TYPES.frameValidationService);
16804
+ this.actionValidationService = inject(TYPES.actionValidationService);
16754
16805
  /**
16755
16806
  * Validates strategy and associated risk configuration.
16756
16807
  *
@@ -16760,16 +16811,17 @@ class StrategyCoreService {
16760
16811
  * @param context - Execution context with strategyName, exchangeName, frameName
16761
16812
  * @returns Promise that resolves when validation is complete
16762
16813
  */
16763
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$t(context), async (context) => {
16764
- this.loggerService.log(METHOD_NAME_VALIDATE$2, {
16814
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$w(context), async (context) => {
16815
+ this.loggerService.log(METHOD_NAME_VALIDATE$5, {
16765
16816
  context,
16766
16817
  });
16767
- const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
16768
- this.strategyValidationService.validate(context.strategyName, METHOD_NAME_VALIDATE$2);
16769
- this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_VALIDATE$2);
16770
- context.frameName && this.frameValidationService.validate(context.frameName, METHOD_NAME_VALIDATE$2);
16771
- riskName && this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$2);
16772
- riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$2));
16818
+ const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
16819
+ this.strategyValidationService.validate(context.strategyName, METHOD_NAME_VALIDATE$5);
16820
+ this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_VALIDATE$5);
16821
+ context.frameName && this.frameValidationService.validate(context.frameName, METHOD_NAME_VALIDATE$5);
16822
+ riskName && this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$5);
16823
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$5));
16824
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_VALIDATE$5));
16773
16825
  });
16774
16826
  /**
16775
16827
  * Retrieves the currently active pending signal for the symbol.
@@ -18130,7 +18182,7 @@ class SizingGlobalService {
18130
18182
  * @param context - Context with riskName, exchangeName, frameName
18131
18183
  * @returns Unique string key for memoization
18132
18184
  */
18133
- const CREATE_KEY_FN$s = (context) => {
18185
+ const CREATE_KEY_FN$v = (context) => {
18134
18186
  const parts = [context.riskName, context.exchangeName];
18135
18187
  if (context.frameName)
18136
18188
  parts.push(context.frameName);
@@ -18156,7 +18208,7 @@ class RiskGlobalService {
18156
18208
  * @param payload - Payload with riskName, exchangeName and frameName
18157
18209
  * @returns Promise that resolves when validation is complete
18158
18210
  */
18159
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$s(context), async (context) => {
18211
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$v(context), async (context) => {
18160
18212
  this.loggerService.log("riskGlobalService validate", {
18161
18213
  context,
18162
18214
  });
@@ -18248,14 +18300,14 @@ class RiskGlobalService {
18248
18300
  }
18249
18301
  }
18250
18302
 
18251
- const METHOD_NAME_VALIDATE$1 = "actionCoreService validate";
18303
+ const METHOD_NAME_VALIDATE$4 = "actionCoreService validate";
18252
18304
  /**
18253
18305
  * Creates a unique key for memoizing validate calls.
18254
18306
  * Key format: "strategyName:exchangeName:frameName"
18255
18307
  * @param context - Execution context with strategyName, exchangeName, frameName
18256
18308
  * @returns Unique string key for memoization
18257
18309
  */
18258
- const CREATE_KEY_FN$r = (context) => {
18310
+ const CREATE_KEY_FN$u = (context) => {
18259
18311
  const parts = [context.strategyName, context.exchangeName];
18260
18312
  if (context.frameName)
18261
18313
  parts.push(context.frameName);
@@ -18299,17 +18351,17 @@ class ActionCoreService {
18299
18351
  * @param context - Strategy execution context with strategyName, exchangeName and frameName
18300
18352
  * @returns Promise that resolves when all validations complete
18301
18353
  */
18302
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$r(context), async (context) => {
18303
- this.loggerService.log(METHOD_NAME_VALIDATE$1, {
18354
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$u(context), async (context) => {
18355
+ this.loggerService.log(METHOD_NAME_VALIDATE$4, {
18304
18356
  context,
18305
18357
  });
18306
18358
  const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
18307
- this.strategyValidationService.validate(context.strategyName, METHOD_NAME_VALIDATE$1);
18308
- this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_VALIDATE$1);
18309
- context.frameName && this.frameValidationService.validate(context.frameName, METHOD_NAME_VALIDATE$1);
18310
- riskName && this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$1);
18311
- riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$1));
18312
- actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_VALIDATE$1));
18359
+ this.strategyValidationService.validate(context.strategyName, METHOD_NAME_VALIDATE$4);
18360
+ this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_VALIDATE$4);
18361
+ context.frameName && this.frameValidationService.validate(context.frameName, METHOD_NAME_VALIDATE$4);
18362
+ riskName && this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$4);
18363
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE$4));
18364
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_VALIDATE$4));
18313
18365
  });
18314
18366
  /**
18315
18367
  * Initializes all ClientAction instances for the strategy.
@@ -18805,8 +18857,8 @@ class FrameSchemaService {
18805
18857
  if (typeof frameSchema.frameName !== "string") {
18806
18858
  throw new Error(`frame schema validation failed: missing frameName`);
18807
18859
  }
18808
- if (typeof frameSchema.interval !== "string") {
18809
- throw new Error(`frame schema validation failed: missing interval for frameName=${frameSchema.frameName}`);
18860
+ if (frameSchema.interval && typeof frameSchema.interval !== "string") {
18861
+ throw new Error(`frame schema validation failed: invalid interval for frameName=${frameSchema.frameName}`);
18810
18862
  }
18811
18863
  if (!(frameSchema.startDate instanceof Date)) {
18812
18864
  throw new Error(`frame schema validation failed: missing startDate for frameName=${frameSchema.frameName}`);
@@ -19934,8 +19986,8 @@ class BacktestLogicPrivateService {
19934
19986
  }
19935
19987
 
19936
19988
  const EMITTER_CHECK_INTERVAL = 5000;
19937
- const MS_PER_MINUTE$6 = 60000;
19938
- const INTERVAL_MINUTES$6 = {
19989
+ const MS_PER_MINUTE$5 = 60000;
19990
+ const INTERVAL_MINUTES$5 = {
19939
19991
  "1m": 1,
19940
19992
  "3m": 3,
19941
19993
  "5m": 5,
@@ -19950,7 +20002,7 @@ const INTERVAL_MINUTES$6 = {
19950
20002
  };
19951
20003
  const createEmitter = functoolsKit.memoize(([interval]) => `${interval}`, (interval) => {
19952
20004
  const tickSubject = new functoolsKit.Subject();
19953
- const intervalMs = INTERVAL_MINUTES$6[interval] * MS_PER_MINUTE$6;
20005
+ const intervalMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$5;
19954
20006
  {
19955
20007
  let lastAligned = Math.floor(Date.now() / intervalMs) * intervalMs;
19956
20008
  functoolsKit.Source.fromInterval(EMITTER_CHECK_INTERVAL)
@@ -19977,46 +20029,6 @@ const waitForCandle = async (interval) => {
19977
20029
  return emitter.toPromise();
19978
20030
  };
19979
20031
 
19980
- const MS_PER_MINUTE$5 = 60000;
19981
- const INTERVAL_MINUTES$5 = {
19982
- "1m": 1,
19983
- "3m": 3,
19984
- "5m": 5,
19985
- "15m": 15,
19986
- "30m": 30,
19987
- "1h": 60,
19988
- "2h": 120,
19989
- "4h": 240,
19990
- "6h": 360,
19991
- "8h": 480,
19992
- "1d": 1440,
19993
- };
19994
- /**
19995
- * Aligns timestamp down to the nearest interval boundary.
19996
- * For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
19997
- *
19998
- * Candle timestamp convention:
19999
- * - Candle timestamp = openTime (when candle opens)
20000
- * - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
20001
- *
20002
- * Adapter contract:
20003
- * - Adapter must return candles with timestamp = openTime
20004
- * - First returned candle.timestamp must equal aligned since
20005
- * - Adapter must return exactly `limit` candles
20006
- *
20007
- * @param date - Date to align
20008
- * @param interval - Candle interval (e.g., "1m", "15m", "1h")
20009
- * @returns New Date aligned down to interval boundary
20010
- */
20011
- const alignToInterval = (date, interval) => {
20012
- const minutes = INTERVAL_MINUTES$5[interval];
20013
- if (minutes === undefined) {
20014
- throw new Error(`alignToInterval: unknown interval=${interval}`);
20015
- }
20016
- const intervalMs = minutes * MS_PER_MINUTE$5;
20017
- return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
20018
- };
20019
-
20020
20032
  /**
20021
20033
  * Private service for live trading orchestration using async generators.
20022
20034
  *
@@ -20839,6 +20851,17 @@ class WalkerLogicPublicService {
20839
20851
  }
20840
20852
 
20841
20853
  const METHOD_NAME_RUN$2 = "liveCommandService run";
20854
+ const METHOD_NAME_VALIDATE$3 = "liveCommandService validate";
20855
+ /**
20856
+ * Creates a unique key for memoizing validate calls.
20857
+ * Key format: "strategyName:exchangeName:frameName"
20858
+ * @param context - Context with strategyName, exchangeName, frameName
20859
+ * @returns Unique string key for memoization
20860
+ */
20861
+ const CREATE_KEY_FN$t = (context) => {
20862
+ const parts = [context.strategyName, context.exchangeName];
20863
+ return parts.join(":");
20864
+ };
20842
20865
  /**
20843
20866
  * Global service providing access to live trading functionality.
20844
20867
  *
@@ -20854,6 +20877,25 @@ class LiveCommandService {
20854
20877
  this.strategySchemaService = inject(TYPES.strategySchemaService);
20855
20878
  this.riskValidationService = inject(TYPES.riskValidationService);
20856
20879
  this.actionValidationService = inject(TYPES.actionValidationService);
20880
+ /**
20881
+ * Validates strategy and associated risk configuration.
20882
+ * Memoized to avoid redundant validations for the same strategy-exchange combination.
20883
+ *
20884
+ * @param context - Context with strategyName, exchangeName
20885
+ * @param methodName - Name of the calling method for error tracking
20886
+ */
20887
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$t(context), (context, methodName) => {
20888
+ this.loggerService.log(METHOD_NAME_VALIDATE$3, {
20889
+ context,
20890
+ methodName,
20891
+ });
20892
+ this.strategyValidationService.validate(context.strategyName, methodName);
20893
+ this.exchangeValidationService.validate(context.exchangeName, methodName);
20894
+ const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
20895
+ riskName && this.riskValidationService.validate(riskName, methodName);
20896
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
20897
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, methodName));
20898
+ });
20857
20899
  /**
20858
20900
  * Runs live trading for a symbol with context propagation.
20859
20901
  *
@@ -20868,22 +20910,26 @@ class LiveCommandService {
20868
20910
  symbol,
20869
20911
  context,
20870
20912
  });
20871
- {
20872
- this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$2);
20873
- this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$2);
20874
- }
20875
- {
20876
- const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
20877
- riskName && this.riskValidationService.validate(riskName, METHOD_NAME_RUN$2);
20878
- riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_RUN$2));
20879
- actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_RUN$2));
20880
- }
20913
+ this.validate(context, METHOD_NAME_RUN$2);
20881
20914
  return this.liveLogicPublicService.run(symbol, context);
20882
20915
  };
20883
20916
  }
20884
20917
  }
20885
20918
 
20886
20919
  const METHOD_NAME_RUN$1 = "backtestCommandService run";
20920
+ const METHOD_NAME_VALIDATE$2 = "backtestCommandService validate";
20921
+ /**
20922
+ * Creates a unique key for memoizing validate calls.
20923
+ * Key format: "strategyName:exchangeName:frameName"
20924
+ * @param context - Context with strategyName, exchangeName, frameName
20925
+ * @returns Unique string key for memoization
20926
+ */
20927
+ const CREATE_KEY_FN$s = (context) => {
20928
+ const parts = [context.strategyName, context.exchangeName];
20929
+ if (context.frameName)
20930
+ parts.push(context.frameName);
20931
+ return parts.join(":");
20932
+ };
20887
20933
  /**
20888
20934
  * Global service providing access to backtest functionality.
20889
20935
  *
@@ -20900,6 +20946,26 @@ class BacktestCommandService {
20900
20946
  this.strategyValidationService = inject(TYPES.strategyValidationService);
20901
20947
  this.exchangeValidationService = inject(TYPES.exchangeValidationService);
20902
20948
  this.frameValidationService = inject(TYPES.frameValidationService);
20949
+ /**
20950
+ * Validates strategy and associated risk configuration.
20951
+ * Memoized to avoid redundant validations for the same strategy-exchange-frame combination.
20952
+ *
20953
+ * @param context - Context with strategyName, exchangeName and frameName
20954
+ * @param methodName - Name of the calling method for error tracking
20955
+ */
20956
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$s(context), (context, methodName) => {
20957
+ this.loggerService.log(METHOD_NAME_VALIDATE$2, {
20958
+ context,
20959
+ methodName,
20960
+ });
20961
+ this.strategyValidationService.validate(context.strategyName, methodName);
20962
+ this.exchangeValidationService.validate(context.exchangeName, methodName);
20963
+ this.frameValidationService.validate(context.frameName, methodName);
20964
+ const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
20965
+ riskName && this.riskValidationService.validate(riskName, methodName);
20966
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
20967
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, methodName));
20968
+ });
20903
20969
  /**
20904
20970
  * Runs backtest for a symbol with context propagation.
20905
20971
  *
@@ -20912,23 +20978,26 @@ class BacktestCommandService {
20912
20978
  symbol,
20913
20979
  context,
20914
20980
  });
20915
- {
20916
- this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$1);
20917
- this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$1);
20918
- this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN$1);
20919
- }
20920
- {
20921
- const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
20922
- riskName && this.riskValidationService.validate(riskName, METHOD_NAME_RUN$1);
20923
- riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_RUN$1));
20924
- actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_RUN$1));
20925
- }
20981
+ this.validate(context, METHOD_NAME_RUN$1);
20926
20982
  return this.backtestLogicPublicService.run(symbol, context);
20927
20983
  };
20928
20984
  }
20929
20985
  }
20930
20986
 
20931
20987
  const METHOD_NAME_RUN = "walkerCommandService run";
20988
+ const METHOD_NAME_VALIDATE$1 = "walkerCommandService validate";
20989
+ /**
20990
+ * Creates a unique key for memoizing validate calls.
20991
+ * Key format: "walkerName:exchangeName:frameName"
20992
+ * @param context - Context with walkerName, exchangeName, frameName
20993
+ * @returns Unique string key for memoization
20994
+ */
20995
+ const CREATE_KEY_FN$r = (context) => {
20996
+ const parts = [context.walkerName, context.exchangeName];
20997
+ if (context.frameName)
20998
+ parts.push(context.frameName);
20999
+ return parts.join(":");
21000
+ };
20932
21001
  /**
20933
21002
  * Global service providing access to walker functionality.
20934
21003
  *
@@ -20947,6 +21016,34 @@ class WalkerCommandService {
20947
21016
  this.strategySchemaService = inject(TYPES.strategySchemaService);
20948
21017
  this.riskValidationService = inject(TYPES.riskValidationService);
20949
21018
  this.actionValidationService = inject(TYPES.actionValidationService);
21019
+ /**
21020
+ * Validates walker and associated strategy configurations.
21021
+ * Memoized to avoid redundant validations for the same walker-exchange-frame combination.
21022
+ *
21023
+ * Strategy/risk/action validation is performed explicitly here in addition to the
21024
+ * cascade inside WalkerValidationService — this is critical-path code and the
21025
+ * redundant check is intentional defense-in-depth.
21026
+ *
21027
+ * @param context - Context with walkerName, exchangeName and frameName
21028
+ * @param methodName - Name of the calling method for error tracking
21029
+ */
21030
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$r(context), (context, methodName) => {
21031
+ this.loggerService.log(METHOD_NAME_VALIDATE$1, {
21032
+ context,
21033
+ methodName,
21034
+ });
21035
+ this.exchangeValidationService.validate(context.exchangeName, methodName);
21036
+ this.frameValidationService.validate(context.frameName, methodName);
21037
+ this.walkerValidationService.validate(context.walkerName, methodName);
21038
+ const walkerSchema = this.walkerSchemaService.get(context.walkerName);
21039
+ for (const strategyName of walkerSchema.strategies) {
21040
+ const { riskName, riskList, actions } = this.strategySchemaService.get(strategyName);
21041
+ this.strategyValidationService.validate(strategyName, methodName);
21042
+ riskName && this.riskValidationService.validate(riskName, methodName);
21043
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
21044
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, methodName));
21045
+ }
21046
+ });
20950
21047
  /**
20951
21048
  * Runs walker comparison for a symbol with context propagation.
20952
21049
  *
@@ -20958,21 +21055,7 @@ class WalkerCommandService {
20958
21055
  symbol,
20959
21056
  context,
20960
21057
  });
20961
- {
20962
- this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN);
20963
- this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN);
20964
- this.walkerValidationService.validate(context.walkerName, METHOD_NAME_RUN);
20965
- }
20966
- {
20967
- const walkerSchema = this.walkerSchemaService.get(context.walkerName);
20968
- for (const strategyName of walkerSchema.strategies) {
20969
- const { riskName, riskList, actions } = this.strategySchemaService.get(strategyName);
20970
- this.strategyValidationService.validate(strategyName, METHOD_NAME_RUN);
20971
- riskName && this.riskValidationService.validate(riskName, METHOD_NAME_RUN);
20972
- riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_RUN));
20973
- actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, METHOD_NAME_RUN));
20974
- }
20975
- }
21058
+ this.validate(context, METHOD_NAME_RUN);
20976
21059
  return this.walkerLogicPublicService.run(symbol, context);
20977
21060
  };
20978
21061
  }
@@ -28695,6 +28778,36 @@ class WalkerValidationService {
28695
28778
  * Injected logger service instance
28696
28779
  */
28697
28780
  this.loggerService = inject(TYPES.loggerService);
28781
+ /**
28782
+ * @private
28783
+ * @readonly
28784
+ * Injected walker schema service instance
28785
+ */
28786
+ this.walkerSchemaService = inject(TYPES.walkerSchemaService);
28787
+ /**
28788
+ * @private
28789
+ * @readonly
28790
+ * Injected strategy validation service instance
28791
+ */
28792
+ this.strategyValidationService = inject(TYPES.strategyValidationService);
28793
+ /**
28794
+ * @private
28795
+ * @readonly
28796
+ * Injected strategy schema service instance
28797
+ */
28798
+ this.strategySchemaService = inject(TYPES.strategySchemaService);
28799
+ /**
28800
+ * @private
28801
+ * @readonly
28802
+ * Injected risk validation service instance
28803
+ */
28804
+ this.riskValidationService = inject(TYPES.riskValidationService);
28805
+ /**
28806
+ * @private
28807
+ * @readonly
28808
+ * Injected action validation service instance
28809
+ */
28810
+ this.actionValidationService = inject(TYPES.actionValidationService);
28698
28811
  /**
28699
28812
  * @private
28700
28813
  * Map storing walker schemas by walker name
@@ -28716,9 +28829,12 @@ class WalkerValidationService {
28716
28829
  this._walkerMap.set(walkerName, walkerSchema);
28717
28830
  };
28718
28831
  /**
28719
- * Validates the existence of a walker
28832
+ * Validates the existence of a walker and its associated strategy configurations.
28833
+ * Each strategy referenced by the walker is validated via StrategyValidationService,
28834
+ * which in turn validates the strategy's risk profiles and actions.
28720
28835
  * @public
28721
28836
  * @throws {Error} If walkerName is not found
28837
+ * @throws {Error} If any referenced strategy (or its risk/actions) is invalid
28722
28838
  * Memoized function to cache validation results
28723
28839
  */
28724
28840
  this.validate = functoolsKit.memoize(([walkerName]) => walkerName, (walkerName, source) => {
@@ -28730,6 +28846,14 @@ class WalkerValidationService {
28730
28846
  if (!walker) {
28731
28847
  throw new Error(`walker ${walkerName} not found source=${source}`);
28732
28848
  }
28849
+ const walkerSchema = this.walkerSchemaService.get(walkerName);
28850
+ for (const strategyName of walkerSchema.strategies) {
28851
+ const { riskName, riskList, actions } = this.strategySchemaService.get(strategyName);
28852
+ this.strategyValidationService.validate(strategyName, source);
28853
+ riskName && this.riskValidationService.validate(riskName, source);
28854
+ riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, source));
28855
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, source));
28856
+ }
28733
28857
  return true;
28734
28858
  });
28735
28859
  /**
@@ -30166,6 +30290,10 @@ class PartialGlobalService {
30166
30290
  * Frame validation service for validating frame existence.
30167
30291
  */
30168
30292
  this.frameValidationService = inject(TYPES.frameValidationService);
30293
+ /**
30294
+ * Action validation service for validating action existence.
30295
+ */
30296
+ this.actionValidationService = inject(TYPES.actionValidationService);
30169
30297
  /**
30170
30298
  * Validates strategy and associated risk configuration.
30171
30299
  * Memoized to avoid redundant validations for the same strategy-exchange-frame combination.
@@ -30181,9 +30309,10 @@ class PartialGlobalService {
30181
30309
  this.strategyValidationService.validate(context.strategyName, methodName);
30182
30310
  this.exchangeValidationService.validate(context.exchangeName, methodName);
30183
30311
  context.frameName && this.frameValidationService.validate(context.frameName, methodName);
30184
- const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
30312
+ const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
30185
30313
  riskName && this.riskValidationService.validate(riskName, methodName);
30186
30314
  riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
30315
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, methodName));
30187
30316
  });
30188
30317
  /**
30189
30318
  * Processes profit state and emits events for newly reached profit levels.
@@ -31250,6 +31379,10 @@ class BreakevenGlobalService {
31250
31379
  * Frame validation service for validating frame existence.
31251
31380
  */
31252
31381
  this.frameValidationService = inject(TYPES.frameValidationService);
31382
+ /**
31383
+ * Action validation service for validating frame existence.
31384
+ */
31385
+ this.actionValidationService = inject(TYPES.actionValidationService);
31253
31386
  /**
31254
31387
  * Validates strategy and associated risk configuration.
31255
31388
  * Memoized to avoid redundant validations for the same strategy-exchange-frame combination.
@@ -31265,9 +31398,10 @@ class BreakevenGlobalService {
31265
31398
  this.strategyValidationService.validate(context.strategyName, methodName);
31266
31399
  this.exchangeValidationService.validate(context.exchangeName, methodName);
31267
31400
  context.frameName && this.frameValidationService.validate(context.frameName, methodName);
31268
- const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
31401
+ const { riskName, riskList, actions } = this.strategySchemaService.get(context.strategyName);
31269
31402
  riskName && this.riskValidationService.validate(riskName, methodName);
31270
31403
  riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
31404
+ actions && actions.forEach((actionName) => this.actionValidationService.validate(actionName, methodName));
31271
31405
  });
31272
31406
  /**
31273
31407
  * Checks if breakeven should be triggered and emits event if conditions met.
@@ -36155,6 +36289,21 @@ class PriceMetaService {
36155
36289
  * Instances are cached until clear() is called.
36156
36290
  */
36157
36291
  this.getSource = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$b(symbol, strategyName, exchangeName, frameName, backtest), () => new functoolsKit.BehaviorSubject());
36292
+ /**
36293
+ * Checks if a price exists for the given key and has emitted at least one value.
36294
+ *
36295
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
36296
+ * @param context - Strategy, exchange, and frame identifiers
36297
+ * @param backtest - True if backtest mode, false if live mode
36298
+ * @returns True if a price exists and has emitted a value, false otherwise
36299
+ */
36300
+ this.hasPrice = (symbol, context, backtest) => {
36301
+ const key = CREATE_KEY_FN$b(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
36302
+ if (!this.getSource.has(key)) {
36303
+ return false;
36304
+ }
36305
+ return !!this.getSource.get(key)?.data;
36306
+ };
36158
36307
  /**
36159
36308
  * Returns the current market price for the given symbol and context.
36160
36309
  *
@@ -36817,6 +36966,129 @@ class NotificationHelperService {
36817
36966
  }
36818
36967
  }
36819
36968
 
36969
+ const GET_RANGE_FN = functoolsKit.trycatch((self, context, backtest) => {
36970
+ if (!backtest) {
36971
+ return null;
36972
+ }
36973
+ const { startDate, endDate } = self.frameSchemaService.get(context.frameName);
36974
+ return {
36975
+ from: startDate,
36976
+ to: endDate,
36977
+ };
36978
+ }, {
36979
+ fallback: (error, self) => {
36980
+ const message = "RuntimeMetaService GET_RANGE_FN thrown";
36981
+ const payload = {
36982
+ error: functoolsKit.errorData(error),
36983
+ message: functoolsKit.getErrorMessage(error),
36984
+ };
36985
+ self.loggerService.warn(message, payload);
36986
+ console.error(message, payload);
36987
+ errorEmitter.next(error);
36988
+ },
36989
+ defaultValue: null,
36990
+ });
36991
+ const GET_INFO_FN = functoolsKit.trycatch((self, context) => {
36992
+ const { info } = self.strategySchemaService.get(context.strategyName);
36993
+ return info || null;
36994
+ }, {
36995
+ fallback: (error, self) => {
36996
+ const message = "RuntimeMetaService GET_INFO_FN thrown";
36997
+ const payload = {
36998
+ error: functoolsKit.errorData(error),
36999
+ message: functoolsKit.getErrorMessage(error),
37000
+ };
37001
+ self.loggerService.warn(message, payload);
37002
+ console.error(message, payload);
37003
+ errorEmitter.next(error);
37004
+ },
37005
+ defaultValue: null,
37006
+ });
37007
+ const GET_PRICE_FN = functoolsKit.trycatch(async (self, symbol, context, backtest) => {
37008
+ return await self.priceMetaService.getCurrentPrice(symbol, context, backtest);
37009
+ }, {
37010
+ fallback: (error, self) => {
37011
+ const message = "RuntimeMetaService GET_PRICE_FN thrown";
37012
+ const payload = {
37013
+ error: functoolsKit.errorData(error),
37014
+ message: functoolsKit.getErrorMessage(error),
37015
+ };
37016
+ self.loggerService.warn(message, payload);
37017
+ console.error(message, payload);
37018
+ errorEmitter.next(error);
37019
+ },
37020
+ defaultValue: null,
37021
+ });
37022
+ const RuntimeMetaService = diSingleton.singleton(class {
37023
+ constructor() {
37024
+ this.loggerService = inject(TYPES.loggerService);
37025
+ this.timeMetaService = inject(TYPES.timeMetaService);
37026
+ this.priceMetaService = inject(TYPES.priceMetaService);
37027
+ this.frameSchemaService = inject(TYPES.frameSchemaService);
37028
+ this.strategySchemaService = inject(TYPES.strategySchemaService);
37029
+ /**
37030
+ * Fetches the time range for the current strategy execution context.
37031
+ *
37032
+ * For backtest mode, it retrieves the start and end dates from the frame schema.
37033
+ * For live mode, it returns null since there is no predefined time range.
37034
+ *
37035
+ * This method is memoized to optimize performance, as the time range for a given context will not change during execution.
37036
+ *
37037
+ * @param context - Strategy, exchange, and frame identifiers
37038
+ * @param backtest - True if backtest mode, false if live mode
37039
+ * @returns An object containing 'from' and 'to' Date objects for backtest mode, or null for live mode
37040
+ */
37041
+ this._getRange = functoolsKit.memoize(([context, backtest]) => `${context.frameName}:${backtest ? "backtest" : "live"}`, (context, backtest) => {
37042
+ return GET_RANGE_FN(this, context, backtest);
37043
+ });
37044
+ /**
37045
+ * Fetches strategy-defined runtime information for the current execution context.
37046
+ *
37047
+ * This method retrieves the 'info' object defined in the strategy schema, which can contain any custom data the strategy wants to track at runtime.
37048
+ * 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.
37049
+ *
37050
+ * This method is memoized to optimize performance, as the strategy info for a given context will not change during execution.
37051
+ *
37052
+ * @param context - Strategy, exchange, and frame identifiers
37053
+ * @returns The 'info' object defined in the strategy schema for the given strategy, or null if not defined
37054
+ */
37055
+ this._getInfo = functoolsKit.memoize(([context]) => context.strategyName, (context) => {
37056
+ return GET_INFO_FN(this, context);
37057
+ });
37058
+ /**
37059
+ * Fetches comprehensive runtime information for a given symbol and strategy context, including current price, timestamp, and strategy-specific info.
37060
+ *
37061
+ * 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.
37062
+ *
37063
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
37064
+ * @param context - Strategy, exchange, and frame identifiers
37065
+ * @param backtest - True if backtest mode, false if live mode
37066
+ * @returns An object containing symbol, time range, strategy-defined info, context, timestamp, current price, and backtest flag
37067
+ */
37068
+ this.getRuntimeInfo = async (symbol, context, backtest) => {
37069
+ this.loggerService.log("runtimeMetaService getRuntimeInfo", {
37070
+ symbol,
37071
+ context,
37072
+ backtest,
37073
+ });
37074
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
37075
+ const when = new Date(timestamp);
37076
+ const currentPrice = await GET_PRICE_FN(this, symbol, context, backtest);
37077
+ const range = this._getRange(context, backtest);
37078
+ const info = this._getInfo(context);
37079
+ return {
37080
+ symbol,
37081
+ range,
37082
+ info,
37083
+ context,
37084
+ backtest,
37085
+ when,
37086
+ currentPrice,
37087
+ };
37088
+ };
37089
+ }
37090
+ });
37091
+
36820
37092
  {
36821
37093
  provide(TYPES.loggerService, () => new LoggerService());
36822
37094
  }
@@ -36853,6 +37125,7 @@ class NotificationHelperService {
36853
37125
  provide(TYPES.contextMetaService, () => new ContextMetaService());
36854
37126
  provide(TYPES.priceMetaService, () => new PriceMetaService());
36855
37127
  provide(TYPES.timeMetaService, () => new TimeMetaService());
37128
+ provide(TYPES.runtimeMetaService, () => new RuntimeMetaService());
36856
37129
  }
36857
37130
  {
36858
37131
  provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
@@ -36956,6 +37229,7 @@ const metaServices = {
36956
37229
  timeMetaService: inject(TYPES.timeMetaService),
36957
37230
  priceMetaService: inject(TYPES.priceMetaService),
36958
37231
  contextMetaService: inject(TYPES.contextMetaService),
37232
+ runtimeMetaService: inject(TYPES.runtimeMetaService),
36959
37233
  };
36960
37234
  const globalServices = {
36961
37235
  sizingGlobalService: inject(TYPES.sizingGlobalService),
@@ -50469,6 +50743,7 @@ const GET_TIMESTAMP_METHOD_NAME = "meta.getTimestamp";
50469
50743
  const GET_MODE_METHOD_NAME = "meta.getMode";
50470
50744
  const GET_SYMBOL_METHOD_NAME = "meta.getSymbol";
50471
50745
  const GET_CONTEXT_METHOD_NAME = "meta.getContext";
50746
+ const GET_RUNTIME_INFO_METHOD_NAME = "meta.getRuntimeInfo";
50472
50747
  /**
50473
50748
  * Gets the current date from execution context.
50474
50749
  *
@@ -50585,6 +50860,45 @@ async function getContext() {
50585
50860
  }
50586
50861
  return backtest.methodContextService.context;
50587
50862
  }
50863
+ /**
50864
+ * Gets runtime information about the current execution environment.
50865
+ *
50866
+ * This includes details such as the current symbol, exchange, timeframe, strategy, and whether it's a backtest or live run.
50867
+ *
50868
+ * @returns Promise resolving to an object containing runtime information
50869
+ * @throws Error if method context or execution context is not active
50870
+ *
50871
+ * @example
50872
+ * ```typescript
50873
+ * const runtimeInfo = await getRuntimeInfo();
50874
+ * console.log(runtimeInfo);
50875
+ * // {
50876
+ * // symbol: "BTCUSDT",
50877
+ * // context: {,
50878
+ * // exchangeName: "Binance",
50879
+ * // frameName: "1m",
50880
+ * // strategyName: "MyStrategy",
50881
+ * // },
50882
+ * // backtest: false
50883
+ * // }
50884
+ * ```
50885
+ */
50886
+ async function getRuntimeInfo() {
50887
+ backtest.loggerService.info(GET_RUNTIME_INFO_METHOD_NAME);
50888
+ if (!MethodContextService.hasContext()) {
50889
+ throw new Error("getRuntimeInfo requires a method context");
50890
+ }
50891
+ if (!ExecutionContextService.hasContext()) {
50892
+ throw new Error("getRuntimeInfo requires an execution context");
50893
+ }
50894
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
50895
+ const { symbol, backtest: isBacktest } = backtest.executionContextService.context;
50896
+ return await backtest.runtimeMetaService.getRuntimeInfo(symbol, {
50897
+ exchangeName,
50898
+ frameName,
50899
+ strategyName,
50900
+ }, isBacktest);
50901
+ }
50588
50902
 
50589
50903
  const RECENT_PERSIST_BACKTEST_METHOD_NAME_HANDLE_ACTIVE_PING = "RecentPersistBacktestUtils.handleActivePing";
50590
50904
  const RECENT_PERSIST_BACKTEST_METHOD_NAME_GET_LATEST_SIGNAL = "RecentPersistBacktestUtils.getLatestSignal";
@@ -64611,6 +64925,20 @@ const CRON_METHOD_NAME_TICK = "CronUtils._tick";
64611
64925
  const CRON_METHOD_NAME_ENABLE = "CronUtils.enable";
64612
64926
  const CRON_METHOD_NAME_DISABLE = "CronUtils.disable";
64613
64927
  const CRON_METHOD_NAME_DISPOSE = "CronUtils.dispose";
64928
+ /**
64929
+ * Watchdog timeout (ms) for a single cron handler invocation.
64930
+ *
64931
+ * A handler that does not settle within this window is treated as failed:
64932
+ * `_runEntry` races `entry.handler(info)` against this `sleep` and, when the
64933
+ * timeout wins, throws into the same `catch` as any other handler error —
64934
+ * surfacing `failed = true`, logging a warning, and (for periodic entries)
64935
+ * rolling back the watermark so the boundary is retried on the next tick.
64936
+ *
64937
+ * This guards the `singlerun`-serialised tick pipeline against a handler that
64938
+ * never resolves (a lost `resolve`, a hung promise with no timeout of its
64939
+ * own): without it such a handler would stall every subsequent tick forever.
64940
+ */
64941
+ const CRON_HANDLER_TIMEOUT = 120000;
64614
64942
  /**
64615
64943
  * Local logger instance.
64616
64944
  *
@@ -64619,6 +64947,20 @@ const CRON_METHOD_NAME_DISPOSE = "CronUtils.dispose";
64619
64947
  * being bootstrapped — `Cron` can be imported and used in isolation.
64620
64948
  */
64621
64949
  const LOGGER_SERVICE$1 = new LoggerService();
64950
+ /**
64951
+ * Local runtime-meta-service instance.
64952
+ *
64953
+ * Like {@link LOGGER_SERVICE}, instantiated directly via `new` rather than
64954
+ * resolved from the DI container so `CronUtils` carries no compile-time
64955
+ * dependency on a bootstrapped framework. `RuntimeMetaService` is built with
64956
+ * the `singleton` HOF from `di-singleton`, so `new RuntimeMetaService()`
64957
+ * returns the one shared singleton proxy — the same instance the rest of the
64958
+ * framework injects — and resolves its own dependencies lazily on first use.
64959
+ *
64960
+ * Used by {@link CronUtils._runEntry} to assemble the {@link IRuntimeInfo}
64961
+ * snapshot handed to each cron handler.
64962
+ */
64963
+ const RUNTIME_META_SERVICE = new RuntimeMetaService();
64622
64964
  /**
64623
64965
  * Utility class for registering periodic tasks that fire on candle-interval
64624
64966
  * boundaries of the virtual time produced by parallel backtests.
@@ -64641,8 +64983,8 @@ const LOGGER_SERVICE$1 = new LoggerService();
64641
64983
  * Cron.register({
64642
64984
  * name: "tg-signal-parser",
64643
64985
  * interval: "1h",
64644
- * handler: async (symbol, when, backtest) => {
64645
- * await parseTelegramSignalsToMongo(when);
64986
+ * handler: async (info) => {
64987
+ * await parseTelegramSignalsToMongo(info.when);
64646
64988
  * },
64647
64989
  * });
64648
64990
  *
@@ -64684,12 +65026,15 @@ class CronUtils {
64684
65026
  * - Fire-once global: `${name}:once:g${generation}`.
64685
65027
  * - Fire-once fan-out: `${name}:once:${symbol}:g${generation}`.
64686
65028
  *
64687
- * Value is the shared in-flight handler promise. Every parallel `tick` for
64688
- * the same slot key awaits this exact promise (mutex semantics) and is
64689
- * released together when it settles. `_inFlight` is owned exclusively by
64690
- * `_runEntry` `clear()` does **not** touch it, so the singleshot promise
64691
- * survives concurrent `clear` calls and continues to coordinate parallel
64692
- * ticks until it settles.
65029
+ * Value is the shared in-flight handler promise. It resolves to a `boolean`
65030
+ * "failed" flag (`true` when the handler or the runtime-info assembly
65031
+ * threw), which `_tick` uses to roll back the periodic watermark of the slot
65032
+ * it opened so a failed boundary is retried. Every parallel `tick` for the
65033
+ * same slot key awaits this exact promise (mutex semantics) and is released
65034
+ * together when it settles. `_inFlight` is owned exclusively by `_runEntry` —
65035
+ * `clear()` does **not** touch it, so the singleshot promise survives
65036
+ * concurrent `clear` calls and continues to coordinate parallel ticks until
65037
+ * it settles.
64693
65038
  */
64694
65039
  this._inFlight = new Map();
64695
65040
  /**
@@ -64733,9 +65078,12 @@ class CronUtils {
64733
65078
  *
64734
65079
  * Written synchronously in `_tick` at slot-open time (before the `await`),
64735
65080
  * so a still-in-flight handler does not let a later tick re-open the same
64736
- * (or an already-passed) boundary. Fire-once entries never touch this map —
64737
- * they use `_firedOnce`. Pruned by `_clearBoundaryFor` on
64738
- * `register`/`unregister` and wiped by `dispose`.
65081
+ * (or an already-passed) boundary. If that handler then **fails**, the
65082
+ * advance is rolled back after the slot settles — the prior value is restored
65083
+ * (or the key deleted if there was none) — so the failed boundary is retried
65084
+ * on the next tick, mirroring catch-up of a skipped boundary. Fire-once
65085
+ * entries never touch this map — they use `_firedOnce`. Pruned by
65086
+ * `_clearBoundaryFor` on `register`/`unregister` and wiped by `dispose`.
64739
65087
  */
64740
65088
  this._lastBoundary = new Map();
64741
65089
  /**
@@ -64755,7 +65103,7 @@ class CronUtils {
64755
65103
  * name: "fetch-funding",
64756
65104
  * interval: "8h",
64757
65105
  * symbols: ["BTCUSDT", "ETHUSDT"],
64758
- * handler: async (symbol, when, backtest) => { ... },
65106
+ * handler: async (info) => { ... },
64759
65107
  * });
64760
65108
  * // Later:
64761
65109
  * dispose();
@@ -64875,7 +65223,7 @@ class CronUtils {
64875
65223
  * 4. **Fire-once** (`entry.interval === undefined`):
64876
65224
  * - If the entry's fired-once key is already in `_firedOnce`, skip.
64877
65225
  * - Slot key: `${name}:once` (+ scope) (+ gen).
64878
- * - `aligned` = the 1-minute-aligned `when` from step 0.
65226
+ * - `alignedMs` = the 1-minute-aligned `when` from step 0 (`ts`).
64879
65227
  * 5. **Periodic** (`entry.interval` set):
64880
65228
  * - Align `when` to the entry's interval via {@link alignToInterval} to
64881
65229
  * get `alignedMs`, the boundary this tick belongs to.
@@ -64897,32 +65245,44 @@ class CronUtils {
64897
65245
  * handler is still in flight.
64898
65246
  * - Slot key: `${name}:${alignedMs}` (+ scope) (+ gen).
64899
65247
  * 6. Singleshot per slot key: look up the slot in `_inFlight`. If a promise
64900
- * already exists, `await` the same promise. Otherwise invoke
64901
- * `entry.handler`, store the promise, and `await` it. The slot is
64902
- * removed in `.finally()` so the next boundary creates a fresh promise;
64903
- * for fire-once entries the fired-once key is also added to
64904
- * `_firedOnce` on success so subsequent ticks skip it.
65248
+ * already exists, `await` the same promise. Otherwise open the slot via
65249
+ * {@link _runEntry} which assembles the {@link IRuntimeInfo} snapshot
65250
+ * (from `symbol`, `context`, `backtest`) and invokes `entry.handler(info)`
65251
+ * store the promise, and `await` it. The slot is removed in `.finally()`
65252
+ * so the next boundary creates a fresh promise; for fire-once entries the
65253
+ * fired-once key is also added to `_firedOnce` on success so subsequent
65254
+ * ticks skip it.
65255
+ * 7. After `await Promise.all`, roll back the watermark for every **periodic**
65256
+ * slot this tick *opened* (not the ones whose in-flight promise it reused)
65257
+ * whose handler reported failure, so the next tick re-opens and re-runs
65258
+ * that boundary.
64905
65259
  *
64906
65260
  * Errors thrown by `handler` are caught, logged via `console.error`, and
64907
65261
  * **not** rethrown — a failing handler must not break the per-symbol
64908
65262
  * tick loop or unblock other parallel backtests with an unhandled
64909
65263
  * rejection. A failed fire-once handler is **not** marked as fired and
64910
- * will retry on the next tick.
65264
+ * will retry on the next tick. A failed **periodic** handler likewise
65265
+ * retries: the boundary watermark advanced at slot-open time is rolled back
65266
+ * after the slot settles (step 7), so the next tick re-opens that boundary.
64911
65267
  *
64912
65268
  * Requires active method context and execution context.
64913
65269
  *
64914
65270
  * @param symbol - Trading symbol from the current tick.
64915
65271
  * @param when - Virtual time of the current tick.
64916
65272
  * @param backtest - `true` for backtest ticks, `false` for live ticks.
64917
- * Forwarded as the third argument to `entry.handler`. Only the value
64918
- * from the tick that **opens** a given slot is observed by all parallel
64919
- * awaiters of that slot.
65273
+ * Forwarded to {@link _runEntry} and surfaced as `info.backtest`. Only the
65274
+ * value from the tick that **opens** a given slot is observed by all
65275
+ * parallel awaiters of that slot.
65276
+ * @param context - Strategy/exchange/frame identifiers from the originating
65277
+ * lifecycle event, forwarded to `RuntimeMetaService.getRuntimeInfo` to
65278
+ * build the {@link IRuntimeInfo} snapshot passed to the handler.
64920
65279
  * @throws Error if method or execution context is missing.
64921
65280
  */
64922
- this._tick = async (symbol, when, backtest) => {
65281
+ this._tick = async (symbol, when, backtest, context) => {
64923
65282
  LOGGER_SERVICE$1.debug(CRON_METHOD_NAME_TICK, {
64924
65283
  symbol,
64925
65284
  when,
65285
+ context,
64926
65286
  });
64927
65287
  if (!MethodContextService.hasContext()) {
64928
65288
  throw new Error("CronUtils _tick requires method context");
@@ -64932,6 +65292,10 @@ class CronUtils {
64932
65292
  }
64933
65293
  const ts = alignToInterval(when, "1m").getTime();
64934
65294
  const taskList = [];
65295
+ // Periodic slots THIS tick actually opened (the `!pending` branch), tracked
65296
+ // for watermark rollback on failure. See {@link IOpenedSlot} for what is and
65297
+ // is not recorded here and why.
65298
+ const openedList = [];
64935
65299
  for (const { entry, generation } of this._entries.values()) {
64936
65300
  if (entry.symbols?.length && !entry.symbols.includes(symbol)) {
64937
65301
  continue;
@@ -64939,7 +65303,6 @@ class CronUtils {
64939
65303
  const perSymbol = !!entry.symbols?.length;
64940
65304
  const scope = perSymbol ? `:${symbol}` : "";
64941
65305
  const genSuffix = `:g${generation}`;
64942
- let aligned;
64943
65306
  let alignedMs;
64944
65307
  let slotKey;
64945
65308
  let firedKey;
@@ -64951,15 +65314,13 @@ class CronUtils {
64951
65314
  if (this._firedOnce.has(onceKey)) {
64952
65315
  continue;
64953
65316
  }
64954
- aligned = alignToInterval(when, "1m");
64955
65317
  alignedMs = ts;
64956
65318
  slotKey = `${entry.name}:once${scope}${genSuffix}`;
64957
65319
  firedKey = onceKey;
64958
65320
  boundaryKey = null;
64959
65321
  }
64960
65322
  else {
64961
- aligned = alignToInterval(when, entry.interval);
64962
- alignedMs = aligned.getTime();
65323
+ alignedMs = alignToInterval(when, entry.interval).getTime();
64963
65324
  boundaryKey = `${entry.name}${scope}${genSuffix}`;
64964
65325
  const lastBoundary = this._lastBoundary.get(boundaryKey);
64965
65326
  // Fire when the tick's aligned boundary has advanced past the last one
@@ -64977,16 +65338,70 @@ class CronUtils {
64977
65338
  // Advance the watermark synchronously at slot-open time, before the
64978
65339
  // await below. Otherwise a later tick on the same (or an already
64979
65340
  // crossed) boundary, arriving while this handler is still in flight,
64980
- // would see the stale watermark and open a duplicate slot.
65341
+ // would see the stale watermark and open a duplicate slot. The advance
65342
+ // is rolled back after the slot settles if the handler failed (see the
65343
+ // post-await loop below), so a failed boundary is retried next tick.
64981
65344
  if (boundaryKey !== null) {
65345
+ // Capture the pre-advance value so it can be restored verbatim on
65346
+ // failure (undefined => the boundary had never opened => delete the
65347
+ // key on rollback). Read fresh here rather than reusing `lastBoundary`
65348
+ // above to keep the value↔slot binding local and obvious; there is no
65349
+ // `await` between the two reads, so they are identical.
65350
+ const prevBoundary = this._lastBoundary.get(boundaryKey);
64982
65351
  this._lastBoundary.set(boundaryKey, alignedMs);
65352
+ pending = this._runEntry(entry, symbol, alignedMs, slotKey, firedKey, backtest, context);
65353
+ this._inFlight.set(slotKey, pending);
65354
+ openedList.push({ boundaryKey, prevBoundary, pending });
65355
+ }
65356
+ else {
65357
+ pending = this._runEntry(entry, symbol, alignedMs, slotKey, firedKey, backtest, context);
65358
+ this._inFlight.set(slotKey, pending);
64983
65359
  }
64984
- pending = this._runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest);
64985
- this._inFlight.set(slotKey, pending);
64986
65360
  }
64987
65361
  taskList.push(pending);
64988
65362
  }
64989
- await Promise.all(taskList);
65363
+ {
65364
+ // Watchdog: warn (do not interrupt) if the slots this tick is awaiting
65365
+ // have not settled within CRON_HANDLER_TIMEOUT. We deliberately keep
65366
+ // awaiting Promise.all so the singlerun pipeline stays serialised and no
65367
+ // duplicate/zombie slots are spawned — the timer only surfaces the stall.
65368
+ // Use a real setTimeout/clearTimeout (not sleep) so the alarm is cancelled
65369
+ // the instant Promise.all resolves, rather than lingering for the full
65370
+ // timeout on every fast tick.
65371
+ const timer = setTimeout(() => {
65372
+ const message = `${CRON_METHOD_NAME_TICK} timed out after ${CRON_HANDLER_TIMEOUT}ms`;
65373
+ const payload = { symbol, when, context };
65374
+ LOGGER_SERVICE$1.warn(message, payload);
65375
+ console.error(message, payload);
65376
+ errorEmitter.next(new Error(message));
65377
+ }, CRON_HANDLER_TIMEOUT);
65378
+ try {
65379
+ await Promise.all(taskList);
65380
+ }
65381
+ finally {
65382
+ clearTimeout(timer);
65383
+ }
65384
+ }
65385
+ // Roll back the watermark for any periodic slot THIS tick opened whose
65386
+ // handler failed, so the next tick re-opens the same boundary and retries
65387
+ // it — mirroring how a skipped boundary is later caught up. Restoring
65388
+ // `prevBoundary` (or deleting the key when it was `undefined`) re-arms the
65389
+ // strict-`>` gate without disturbing any earlier already-fired boundary.
65390
+ // `await pending` is cheap — every promise already settled in `Promise.all`
65391
+ // above; we re-await via `openedList` because its entries (opened slots
65392
+ // only) do not line up with `taskList` indices.
65393
+ for (const { boundaryKey, prevBoundary, pending } of openedList) {
65394
+ const failed = await pending;
65395
+ if (!failed) {
65396
+ continue;
65397
+ }
65398
+ if (prevBoundary === undefined) {
65399
+ this._lastBoundary.delete(boundaryKey);
65400
+ }
65401
+ else {
65402
+ this._lastBoundary.set(boundaryKey, prevBoundary);
65403
+ }
65404
+ }
64990
65405
  };
64991
65406
  /**
64992
65407
  * Subscribe `Cron` to the engine's strategy lifecycle subjects so registered
@@ -65001,7 +65416,11 @@ class CronUtils {
65001
65416
  *
65002
65417
  * All four subjects are subscribed to a single `singlerun`-wrapped
65003
65418
  * handler that builds `_tick(event.symbol, new Date(event.timestamp),
65004
- * event.backtest)`. `singlerun` merges the four streams into one serial
65419
+ * event.backtest, { strategyName, exchangeName, frameName })`. The context
65420
+ * object is read uniformly from the event — every contract carries
65421
+ * `strategyName`, `exchangeName` and `frameName` at the top level (Active /
65422
+ * Schedule contracts gained `frameName` for exactly this reason), so no
65423
+ * per-event branching is needed. `singlerun` merges the four streams into one serial
65005
65424
  * queue: at most one `_tick` runs at a time, the next waits. This matters
65006
65425
  * because the engine can emit `beforeStart` and an immediate `idlePing`
65007
65426
  * on the very same minute, and concurrent `_tick`s on the same
@@ -65037,7 +65456,11 @@ class CronUtils {
65037
65456
  this.enable = functoolsKit.singleshot(() => {
65038
65457
  LOGGER_SERVICE$1.info(CRON_METHOD_NAME_ENABLE);
65039
65458
  const handleTick = functoolsKit.singlerun(async (event) => {
65040
- return await this._tick(event.symbol, new Date(event.timestamp), event.backtest);
65459
+ return await this._tick(event.symbol, new Date(event.timestamp), event.backtest, {
65460
+ strategyName: event.strategyName,
65461
+ exchangeName: event.exchangeName,
65462
+ frameName: event.frameName,
65463
+ });
65041
65464
  });
65042
65465
  const unBeforeStart = beforeStartSubject.subscribe(handleTick);
65043
65466
  const unIdlePing = idlePingSubject.subscribe(handleTick);
@@ -65140,25 +65563,51 @@ class CronUtils {
65140
65563
  /**
65141
65564
  * Build the singleshot promise for a single in-flight slot.
65142
65565
  *
65143
- * Invokes `entry.handler(symbol, aligned, backtest)`, swallows and logs
65144
- * any error via `console.error`, and clears the `_inFlight` slot
65145
- * in `.finally()` so the next boundary produces a fresh promise. For
65146
- * fire-once entries `firedKey` is added to `_firedOnce` on success so
65147
- * subsequent ticks skip it.
65148
- *
65566
+ * Assembles the {@link IRuntimeInfo} snapshot via
65567
+ * `RuntimeMetaService.getRuntimeInfo(symbol, context, backtest)` and invokes
65568
+ * `entry.handler(info)`. Logs any error via `console.error` and **returns** a
65569
+ * `failed` boolean (`true` when the handler or the runtime-info assembly —
65570
+ * threw) so the caller (`_tick`) can roll back the periodic watermark of the
65571
+ * slot it opened and retry that boundary. The error is **not** rethrown, so a
65572
+ * failing handler never produces an unhandled rejection. Clears the
65573
+ * `_inFlight` slot in `.finally()` so the next boundary produces a fresh
65574
+ * promise. For fire-once entries `firedKey` is added to `_firedOnce` on
65575
+ * success so subsequent ticks skip it.
65576
+ *
65577
+ * `getRuntimeInfo` is the user-facing aggregator: its sub-fetches (range,
65578
+ * info, price) are individually wrapped in `trycatch` with `null` fallbacks,
65579
+ * so it almost never throws for missing data. Whatever does throw — the
65580
+ * handler, or in rare cases `getRuntimeInfo` — is caught here and reported via
65581
+ * the returned `failed` flag; the watermark rollback treats both identically.
65582
+ *
65583
+ * @param context - Strategy/exchange/frame identifiers from the originating
65584
+ * lifecycle event, forwarded to `getRuntimeInfo` to resolve `range`/`info`.
65149
65585
  * @param firedKey - Key to add to `_firedOnce` on success, or `null` for
65150
65586
  * periodic entries (which never populate `_firedOnce`).
65151
- * @param backtest - Value forwarded as the third handler argument; the
65152
- * "winner" tick's flag is what all parallel awaiters of this slot see.
65587
+ * @param backtest - Forwarded to `getRuntimeInfo` and surfaced as
65588
+ * `info.backtest`; the "winner" tick's flag is what all parallel awaiters
65589
+ * of this slot see.
65590
+ * @returns `true` if the handler (or `getRuntimeInfo`) threw, `false` on
65591
+ * success. `_tick` uses this to decide whether to roll back the watermark.
65153
65592
  */
65154
- async _runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest) {
65593
+ async _runEntry(entry, symbol, alignedMs, slotKey, firedKey, backtest, context) {
65155
65594
  let failed = false;
65156
65595
  try {
65157
- await entry.handler(symbol, aligned, backtest);
65596
+ const info = await RUNTIME_META_SERVICE.getRuntimeInfo(symbol, context, backtest);
65597
+ await entry.handler(info);
65158
65598
  }
65159
- catch (err) {
65599
+ catch (error) {
65160
65600
  failed = true;
65161
- console.error(`${CRON_METHOD_NAME_TICK} entry "${entry.name}" failed`, { symbol, alignedMs, err });
65601
+ const message = `${CRON_METHOD_NAME_TICK} entry "${entry.name}" failed`;
65602
+ const payload = {
65603
+ symbol,
65604
+ alignedMs,
65605
+ error: functoolsKit.errorData(error),
65606
+ message: functoolsKit.getErrorMessage(error),
65607
+ };
65608
+ LOGGER_SERVICE$1.warn(message, payload);
65609
+ console.error(message, payload);
65610
+ errorEmitter.next(error);
65162
65611
  }
65163
65612
  finally {
65164
65613
  this._inFlight.delete(slotKey);
@@ -65166,6 +65615,7 @@ class CronUtils {
65166
65615
  this._firedOnce.add(firedKey);
65167
65616
  }
65168
65617
  }
65618
+ return failed;
65169
65619
  }
65170
65620
  }
65171
65621
  /**
@@ -65179,7 +65629,7 @@ class CronUtils {
65179
65629
  * Cron.register({
65180
65630
  * name: "tg-parser",
65181
65631
  * interval: "1h",
65182
- * handler: async (symbol, when, backtest) => { ... },
65632
+ * handler: async (info) => { ... },
65183
65633
  * });
65184
65634
  * ```
65185
65635
  */
@@ -66528,6 +66978,7 @@ exports.getPositionPnlPercent = getPositionPnlPercent;
66528
66978
  exports.getPositionWaitingMinutes = getPositionWaitingMinutes;
66529
66979
  exports.getRawCandles = getRawCandles;
66530
66980
  exports.getRiskSchema = getRiskSchema;
66981
+ exports.getRuntimeInfo = getRuntimeInfo;
66531
66982
  exports.getScheduledSignal = getScheduledSignal;
66532
66983
  exports.getSessionData = getSessionData;
66533
66984
  exports.getSignalState = getSignalState;