backtest-kit 11.6.0 → 11.8.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.
Files changed (6) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1996 -1996
  3. package/build/index.cjs +422 -113
  4. package/build/index.mjs +422 -113
  5. package/package.json +86 -86
  6. package/types.d.ts +205 -48
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'),
@@ -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,
@@ -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);
@@ -19914,8 +19963,8 @@ class BacktestLogicPrivateService {
19914
19963
  }
19915
19964
 
19916
19965
  const EMITTER_CHECK_INTERVAL = 5000;
19917
- const MS_PER_MINUTE$6 = 60000;
19918
- const INTERVAL_MINUTES$6 = {
19966
+ const MS_PER_MINUTE$5 = 60000;
19967
+ const INTERVAL_MINUTES$5 = {
19919
19968
  "1m": 1,
19920
19969
  "3m": 3,
19921
19970
  "5m": 5,
@@ -19930,7 +19979,7 @@ const INTERVAL_MINUTES$6 = {
19930
19979
  };
19931
19980
  const createEmitter = memoize(([interval]) => `${interval}`, (interval) => {
19932
19981
  const tickSubject = new Subject();
19933
- const intervalMs = INTERVAL_MINUTES$6[interval] * MS_PER_MINUTE$6;
19982
+ const intervalMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$5;
19934
19983
  {
19935
19984
  let lastAligned = Math.floor(Date.now() / intervalMs) * intervalMs;
19936
19985
  Source.fromInterval(EMITTER_CHECK_INTERVAL)
@@ -19957,46 +20006,6 @@ const waitForCandle = async (interval) => {
19957
20006
  return emitter.toPromise();
19958
20007
  };
19959
20008
 
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
20009
  /**
20001
20010
  * Private service for live trading orchestration using async generators.
20002
20011
  *
@@ -23958,13 +23967,19 @@ let ReportStorage$a = class ReportStorage {
23958
23967
  // mark-to-market low); equity then moves to the realized close.
23959
23968
  // If equity (at trough or close) goes ≤ 0 (e.g. leveraged loss < -100%) — account
23960
23969
  // blown, fix DD at 100% and stop walking the curve.
23970
+ // Walk the equity curve in chronological close order. Storage is
23971
+ // newest-first (unshift on addSignal); reverse-storage iteration normally
23972
+ // gives chronological order, but explicitly sorting by closeTimestamp
23973
+ // removes the dependency on insertion-order matching close-order (which
23974
+ // can break under crash recovery, signal backfill, or disk replays).
23975
+ const orderedSignals = [...validSignals].sort((a, b) => a.closeTimestamp - b.closeTimestamp);
23961
23976
  let equity = 1;
23962
23977
  let peak = 1;
23963
23978
  let equityMaxDrawdown = 0;
23964
23979
  let blown = false;
23965
- for (let i = validSignals.length - 1; i >= 0; i--) {
23980
+ for (const s of orderedSignals) {
23966
23981
  // Intra-trade trough — mark-to-market low while the position was open.
23967
- const fallPct = validSignals[i].signal.maxDrawdown?.pnlPercentage;
23982
+ const fallPct = s.signal.maxDrawdown?.pnlPercentage;
23968
23983
  if (typeof fallPct === "number" && fallPct < 0) {
23969
23984
  const trough = equity * (1 + fallPct / 100);
23970
23985
  if (trough <= 0) {
@@ -23977,7 +23992,7 @@ let ReportStorage$a = class ReportStorage {
23977
23992
  equityMaxDrawdown = troughDd;
23978
23993
  }
23979
23994
  // Realized close — book the final per-trade result.
23980
- equity *= 1 + validSignals[i].pnl.pnlPercentage / 100;
23995
+ equity *= 1 + s.pnl.pnlPercentage / 100;
23981
23996
  if (equity <= 0) {
23982
23997
  equityMaxDrawdown = 100;
23983
23998
  blown = true;
@@ -25122,14 +25137,20 @@ let ReportStorage$9 = class ReportStorage {
25122
25137
  // snapshot, ≤ 0) is applied as a trough BEFORE booking the realized close. Without it
25123
25138
  // the curve only steps at close, so a trade that dipped to -18% and recovered to +2%
25124
25139
  // would register zero drawdown — understating DD and inflating Calmar/Recovery.
25125
- const chronological = [];
25126
- for (let i = validClosed.length - 1; i >= 0; i--) {
25127
- const fall = validClosed[i].fallPnl;
25128
- chronological.push({
25129
- r: validClosed[i].pnl,
25130
- fall: typeof fall === "number" ? fall : null,
25131
- });
25132
- }
25140
+ // Walk the equity curve in chronological close order. Reverse-storage
25141
+ // iteration (newest-first storage reverse) normally yields chronological
25142
+ // order for live ingest, but explicitly sorting by event.timestamp removes
25143
+ // the dependency on insertion-order matching close-order. This matters
25144
+ // under crash recovery (events reloaded from disk in arbitrary order) and
25145
+ // when ingest latency reorders closed events relative to wall-clock time.
25146
+ const chronological = validClosed
25147
+ .map((e) => ({
25148
+ r: e.pnl,
25149
+ fall: typeof e.fallPnl === "number" ? e.fallPnl : null,
25150
+ ts: e.timestamp,
25151
+ }))
25152
+ .sort((a, b) => a.ts - b.ts)
25153
+ .map(({ r, fall }) => ({ r, fall }));
25133
25154
  let equity = 1;
25134
25155
  let peak = 1;
25135
25156
  let equityMaxDrawdown = 0;
@@ -27203,11 +27224,18 @@ class HeatmapStorage {
27203
27224
  let equityFinal = 1;
27204
27225
  let blown = false;
27205
27226
  if (signals.length > 0) {
27227
+ // Walk the per-symbol equity curve in chronological close order.
27228
+ // Storage is newest-first (unshift on addSignal), but if signals were
27229
+ // ingested out-of-order (e.g. Live + crash recovery loading from disk in
27230
+ // arbitrary order, or a backfill replay), reverse-storage iteration
27231
+ // would misplace peak/trough and silently distort maxDrawdown. Sorting
27232
+ // by closeTimestamp explicitly removes that dependency.
27233
+ const ordered = [...signals].sort((a, b) => a.closeTimestamp - b.closeTimestamp);
27206
27234
  let equity = 1;
27207
27235
  let peak = 1;
27208
27236
  let maxDD = 0;
27209
- for (let i = signals.length - 1; i >= 0; i--) {
27210
- const fallPct = signals[i].signal.maxDrawdown?.pnlPercentage;
27237
+ for (const s of ordered) {
27238
+ const fallPct = s.signal.maxDrawdown?.pnlPercentage;
27211
27239
  if (typeof fallPct === "number" && fallPct < 0) {
27212
27240
  const trough = equity * (1 + fallPct / 100);
27213
27241
  if (trough <= 0) {
@@ -27219,7 +27247,7 @@ class HeatmapStorage {
27219
27247
  if (troughDd > maxDD)
27220
27248
  maxDD = troughDd;
27221
27249
  }
27222
- equity *= 1 + signals[i].pnl.pnlPercentage / 100;
27250
+ equity *= 1 + s.pnl.pnlPercentage / 100;
27223
27251
  if (equity <= 0) {
27224
27252
  maxDD = 100;
27225
27253
  blown = true;
@@ -27656,23 +27684,28 @@ class HeatmapStorage {
27656
27684
  let portfolioCertaintyRatio = null;
27657
27685
  let portfolioExpectedYearlyReturns = null;
27658
27686
  let portfolioTradesPerYear = null;
27659
- const allReturns = [];
27660
- // Parallel array of intra-trade troughs (≤ 0), aligned 1:1 with allReturns,
27661
- // used for mark-to-market DD in the pooled equity curve below.
27662
- const allFalls = [];
27687
+ const pooledTrades = [];
27663
27688
  let poolFirstPendingAt = Infinity;
27664
27689
  let poolLastCloseAt = -Infinity;
27665
27690
  for (const signals of this.symbolData.values()) {
27666
27691
  for (const s of signals) {
27667
- allReturns.push(s.pnl.pnlPercentage);
27668
27692
  const fall = s.signal.maxDrawdown?.pnlPercentage;
27669
- allFalls.push(typeof fall === "number" ? fall : null);
27693
+ pooledTrades.push({
27694
+ r: s.pnl.pnlPercentage,
27695
+ fall: typeof fall === "number" ? fall : null,
27696
+ closeAt: s.closeTimestamp,
27697
+ });
27670
27698
  if (s.signal.pendingAt < poolFirstPendingAt)
27671
27699
  poolFirstPendingAt = s.signal.pendingAt;
27672
27700
  if (s.closeTimestamp > poolLastCloseAt)
27673
27701
  poolLastCloseAt = s.closeTimestamp;
27674
27702
  }
27675
27703
  }
27704
+ pooledTrades.sort((a, b) => a.closeAt - b.closeAt);
27705
+ const allReturns = pooledTrades.map((t) => t.r);
27706
+ // Parallel array of intra-trade troughs (≤ 0), aligned 1:1 with allReturns,
27707
+ // used for mark-to-market DD in the pooled equity curve below.
27708
+ const allFalls = pooledTrades.map((t) => t.fall);
27676
27709
  if (allReturns.length >= MIN_SIGNALS_FOR_RATIOS) {
27677
27710
  const portfolioAvg = allReturns.reduce((acc, r) => acc + r, 0) / allReturns.length;
27678
27711
  const portfolioVariance = allReturns.reduce((acc, r) => acc + Math.pow(r - portfolioAvg, 2), 0) /
@@ -28046,7 +28079,7 @@ class HeatmapStorage {
28046
28079
  `*Peak Profit PNL / Max Drawdown PNL: extremes — the best best-case and worst worst-case observed across all trades. Tail behaviour the averages hide.*`,
28047
28080
  `*Avg Duration / Avg Win Duration / Avg Loss Duration: mean hold time in minutes (closeTimestamp - pendingAt). Winner-shorter-than-loser is a red flag ("cut winners short, let losers run").*`,
28048
28081
  `*Avg Consecutive Win/Loss PNL: average sum of pnlPercentage across consecutive streaks. Pairs with max streak length to show the typical (not worst-case) streak magnitude. Portfolio uses trade-count-weighted mean of per-symbol streak averages — concatenating streaks across symbols would be meaningless (different markets, different timeframes).*`,
28049
- `*Max Drawdown: mark-to-market — both the per-symbol and pooled equity curves apply each trade's worst intra-trade excursion (the lowest unrealized point while the position was open) before booking its realized close, so deep round-trip dips count. It is NOT realized-only (close-to-close); a realized-only curve would understate drawdown and inflate Calmar/Recovery. NOTE: the pooled curve orders trades by storage sequence, not wall-clock time, so simultaneous cross-symbol drawdowns are not modelled.*`,
28082
+ `*Max Drawdown: mark-to-market — both the per-symbol and pooled equity curves apply each trade's worst intra-trade excursion (the lowest unrealized point while the position was open) before booking its realized close, so deep round-trip dips count. It is NOT realized-only (close-to-close); a realized-only curve would understate drawdown and inflate Calmar/Recovery. The pooled curve walks trades chronologically by closeTimestamp; simultaneous cross-symbol drawdowns within the same minute are still serialised (one trade applied at a time), so genuine same-instant tail correlation is not modelled.*`,
28050
28083
  `*All metrics require 100+ signals per symbol to be statistically reliable. Annualized metrics assume the observed trading frequency persists year-round.*`,
28051
28084
  `*IMPORTANT: Per-symbol equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per position** (no portfolio fraction). These metrics ignore the position-sizing subsystem (PositionSize / Kelly / ATR): pnlPercentage is a return on the position's own invested capital, never scaled by account balance. With DCA (commitAverageBuy) the cost basis is the sum of all entries and the entry price is dollar-cost-weighted, so per-trade % is measured against the averaged position, not a fixed stake. If your strategy risks X% of capital per trade, the realized return / drawdown will be roughly X/100 of the reported figures — these metrics represent a theoretical upper bound under full allocation.*`,
28052
28085
  `*Negative values for Sharpe / Annualized Sharpe / Sortino / Calmar / Recovery / Expectancy / Expected Yearly Returns indicate a losing symbol (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
@@ -36111,6 +36144,21 @@ class PriceMetaService {
36111
36144
  * Instances are cached until clear() is called.
36112
36145
  */
36113
36146
  this.getSource = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$b(symbol, strategyName, exchangeName, frameName, backtest), () => new BehaviorSubject());
36147
+ /**
36148
+ * Checks if a price exists for the given key and has emitted at least one value.
36149
+ *
36150
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
36151
+ * @param context - Strategy, exchange, and frame identifiers
36152
+ * @param backtest - True if backtest mode, false if live mode
36153
+ * @returns True if a price exists and has emitted a value, false otherwise
36154
+ */
36155
+ this.hasPrice = (symbol, context, backtest) => {
36156
+ const key = CREATE_KEY_FN$b(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
36157
+ if (!this.getSource.has(key)) {
36158
+ return false;
36159
+ }
36160
+ return !!this.getSource.get(key)?.data;
36161
+ };
36114
36162
  /**
36115
36163
  * Returns the current market price for the given symbol and context.
36116
36164
  *
@@ -36773,6 +36821,129 @@ class NotificationHelperService {
36773
36821
  }
36774
36822
  }
36775
36823
 
36824
+ const GET_RANGE_FN = trycatch((self, context, backtest) => {
36825
+ if (!backtest) {
36826
+ return null;
36827
+ }
36828
+ const { startDate, endDate } = self.frameSchemaService.get(context.frameName);
36829
+ return {
36830
+ from: startDate,
36831
+ to: endDate,
36832
+ };
36833
+ }, {
36834
+ fallback: (error, self) => {
36835
+ const message = "RuntimeMetaService GET_RANGE_FN thrown";
36836
+ const payload = {
36837
+ error: errorData(error),
36838
+ message: getErrorMessage(error),
36839
+ };
36840
+ self.loggerService.warn(message, payload);
36841
+ console.error(message, payload);
36842
+ errorEmitter.next(error);
36843
+ },
36844
+ defaultValue: null,
36845
+ });
36846
+ const GET_INFO_FN = trycatch((self, context) => {
36847
+ const { info } = self.strategySchemaService.get(context.strategyName);
36848
+ return info || null;
36849
+ }, {
36850
+ fallback: (error, self) => {
36851
+ const message = "RuntimeMetaService GET_INFO_FN thrown";
36852
+ const payload = {
36853
+ error: errorData(error),
36854
+ message: getErrorMessage(error),
36855
+ };
36856
+ self.loggerService.warn(message, payload);
36857
+ console.error(message, payload);
36858
+ errorEmitter.next(error);
36859
+ },
36860
+ defaultValue: null,
36861
+ });
36862
+ const GET_PRICE_FN = trycatch(async (self, symbol, context, backtest) => {
36863
+ return await self.priceMetaService.getCurrentPrice(symbol, context, backtest);
36864
+ }, {
36865
+ fallback: (error, self) => {
36866
+ const message = "RuntimeMetaService GET_PRICE_FN thrown";
36867
+ const payload = {
36868
+ error: errorData(error),
36869
+ message: getErrorMessage(error),
36870
+ };
36871
+ self.loggerService.warn(message, payload);
36872
+ console.error(message, payload);
36873
+ errorEmitter.next(error);
36874
+ },
36875
+ defaultValue: null,
36876
+ });
36877
+ const RuntimeMetaService = singleton(class {
36878
+ constructor() {
36879
+ this.loggerService = inject(TYPES.loggerService);
36880
+ this.timeMetaService = inject(TYPES.timeMetaService);
36881
+ this.priceMetaService = inject(TYPES.priceMetaService);
36882
+ this.frameSchemaService = inject(TYPES.frameSchemaService);
36883
+ this.strategySchemaService = inject(TYPES.strategySchemaService);
36884
+ /**
36885
+ * Fetches the time range for the current strategy execution context.
36886
+ *
36887
+ * For backtest mode, it retrieves the start and end dates from the frame schema.
36888
+ * For live mode, it returns null since there is no predefined time range.
36889
+ *
36890
+ * This method is memoized to optimize performance, as the time range for a given context will not change during execution.
36891
+ *
36892
+ * @param context - Strategy, exchange, and frame identifiers
36893
+ * @param backtest - True if backtest mode, false if live mode
36894
+ * @returns An object containing 'from' and 'to' Date objects for backtest mode, or null for live mode
36895
+ */
36896
+ this._getRange = memoize(([context, backtest]) => `${context.frameName}:${backtest ? "backtest" : "live"}`, (context, backtest) => {
36897
+ return GET_RANGE_FN(this, context, backtest);
36898
+ });
36899
+ /**
36900
+ * Fetches strategy-defined runtime information for the current execution context.
36901
+ *
36902
+ * This method retrieves the 'info' object defined in the strategy schema, which can contain any custom data the strategy wants to track at runtime.
36903
+ * 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.
36904
+ *
36905
+ * This method is memoized to optimize performance, as the strategy info for a given context will not change during execution.
36906
+ *
36907
+ * @param context - Strategy, exchange, and frame identifiers
36908
+ * @returns The 'info' object defined in the strategy schema for the given strategy, or null if not defined
36909
+ */
36910
+ this._getInfo = memoize(([context]) => context.strategyName, (context) => {
36911
+ return GET_INFO_FN(this, context);
36912
+ });
36913
+ /**
36914
+ * Fetches comprehensive runtime information for a given symbol and strategy context, including current price, timestamp, and strategy-specific info.
36915
+ *
36916
+ * 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.
36917
+ *
36918
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
36919
+ * @param context - Strategy, exchange, and frame identifiers
36920
+ * @param backtest - True if backtest mode, false if live mode
36921
+ * @returns An object containing symbol, time range, strategy-defined info, context, timestamp, current price, and backtest flag
36922
+ */
36923
+ this.getRuntimeInfo = async (symbol, context, backtest) => {
36924
+ this.loggerService.log("runtimeMetaService getRuntimeInfo", {
36925
+ symbol,
36926
+ context,
36927
+ backtest,
36928
+ });
36929
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
36930
+ const when = new Date(timestamp);
36931
+ const currentPrice = await GET_PRICE_FN(this, symbol, context, backtest);
36932
+ const range = this._getRange(context, backtest);
36933
+ const info = this._getInfo(context);
36934
+ return {
36935
+ symbol,
36936
+ range,
36937
+ info,
36938
+ context,
36939
+ backtest,
36940
+ when,
36941
+ currentPrice,
36942
+ };
36943
+ };
36944
+ }
36945
+ });
36946
+
36776
36947
  {
36777
36948
  provide(TYPES.loggerService, () => new LoggerService());
36778
36949
  }
@@ -36809,6 +36980,7 @@ class NotificationHelperService {
36809
36980
  provide(TYPES.contextMetaService, () => new ContextMetaService());
36810
36981
  provide(TYPES.priceMetaService, () => new PriceMetaService());
36811
36982
  provide(TYPES.timeMetaService, () => new TimeMetaService());
36983
+ provide(TYPES.runtimeMetaService, () => new RuntimeMetaService());
36812
36984
  }
36813
36985
  {
36814
36986
  provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
@@ -36912,6 +37084,7 @@ const metaServices = {
36912
37084
  timeMetaService: inject(TYPES.timeMetaService),
36913
37085
  priceMetaService: inject(TYPES.priceMetaService),
36914
37086
  contextMetaService: inject(TYPES.contextMetaService),
37087
+ runtimeMetaService: inject(TYPES.runtimeMetaService),
36915
37088
  };
36916
37089
  const globalServices = {
36917
37090
  sizingGlobalService: inject(TYPES.sizingGlobalService),
@@ -64567,6 +64740,20 @@ const CRON_METHOD_NAME_TICK = "CronUtils._tick";
64567
64740
  const CRON_METHOD_NAME_ENABLE = "CronUtils.enable";
64568
64741
  const CRON_METHOD_NAME_DISABLE = "CronUtils.disable";
64569
64742
  const CRON_METHOD_NAME_DISPOSE = "CronUtils.dispose";
64743
+ /**
64744
+ * Watchdog timeout (ms) for a single cron handler invocation.
64745
+ *
64746
+ * A handler that does not settle within this window is treated as failed:
64747
+ * `_runEntry` races `entry.handler(info)` against this `sleep` and, when the
64748
+ * timeout wins, throws into the same `catch` as any other handler error —
64749
+ * surfacing `failed = true`, logging a warning, and (for periodic entries)
64750
+ * rolling back the watermark so the boundary is retried on the next tick.
64751
+ *
64752
+ * This guards the `singlerun`-serialised tick pipeline against a handler that
64753
+ * never resolves (a lost `resolve`, a hung promise with no timeout of its
64754
+ * own): without it such a handler would stall every subsequent tick forever.
64755
+ */
64756
+ const CRON_HANDLER_TIMEOUT = 120000;
64570
64757
  /**
64571
64758
  * Local logger instance.
64572
64759
  *
@@ -64575,6 +64762,20 @@ const CRON_METHOD_NAME_DISPOSE = "CronUtils.dispose";
64575
64762
  * being bootstrapped — `Cron` can be imported and used in isolation.
64576
64763
  */
64577
64764
  const LOGGER_SERVICE$1 = new LoggerService();
64765
+ /**
64766
+ * Local runtime-meta-service instance.
64767
+ *
64768
+ * Like {@link LOGGER_SERVICE}, instantiated directly via `new` rather than
64769
+ * resolved from the DI container so `CronUtils` carries no compile-time
64770
+ * dependency on a bootstrapped framework. `RuntimeMetaService` is built with
64771
+ * the `singleton` HOF from `di-singleton`, so `new RuntimeMetaService()`
64772
+ * returns the one shared singleton proxy — the same instance the rest of the
64773
+ * framework injects — and resolves its own dependencies lazily on first use.
64774
+ *
64775
+ * Used by {@link CronUtils._runEntry} to assemble the {@link IRuntimeInfo}
64776
+ * snapshot handed to each cron handler.
64777
+ */
64778
+ const RUNTIME_META_SERVICE = new RuntimeMetaService();
64578
64779
  /**
64579
64780
  * Utility class for registering periodic tasks that fire on candle-interval
64580
64781
  * boundaries of the virtual time produced by parallel backtests.
@@ -64597,8 +64798,8 @@ const LOGGER_SERVICE$1 = new LoggerService();
64597
64798
  * Cron.register({
64598
64799
  * name: "tg-signal-parser",
64599
64800
  * interval: "1h",
64600
- * handler: async (symbol, when, backtest) => {
64601
- * await parseTelegramSignalsToMongo(when);
64801
+ * handler: async (info) => {
64802
+ * await parseTelegramSignalsToMongo(info.when);
64602
64803
  * },
64603
64804
  * });
64604
64805
  *
@@ -64640,12 +64841,15 @@ class CronUtils {
64640
64841
  * - Fire-once global: `${name}:once:g${generation}`.
64641
64842
  * - Fire-once fan-out: `${name}:once:${symbol}:g${generation}`.
64642
64843
  *
64643
- * Value is the shared in-flight handler promise. Every parallel `tick` for
64644
- * the same slot key awaits this exact promise (mutex semantics) and is
64645
- * released together when it settles. `_inFlight` is owned exclusively by
64646
- * `_runEntry` `clear()` does **not** touch it, so the singleshot promise
64647
- * survives concurrent `clear` calls and continues to coordinate parallel
64648
- * ticks until it settles.
64844
+ * Value is the shared in-flight handler promise. It resolves to a `boolean`
64845
+ * "failed" flag (`true` when the handler or the runtime-info assembly
64846
+ * threw), which `_tick` uses to roll back the periodic watermark of the slot
64847
+ * it opened so a failed boundary is retried. Every parallel `tick` for the
64848
+ * same slot key awaits this exact promise (mutex semantics) and is released
64849
+ * together when it settles. `_inFlight` is owned exclusively by `_runEntry` —
64850
+ * `clear()` does **not** touch it, so the singleshot promise survives
64851
+ * concurrent `clear` calls and continues to coordinate parallel ticks until
64852
+ * it settles.
64649
64853
  */
64650
64854
  this._inFlight = new Map();
64651
64855
  /**
@@ -64689,9 +64893,12 @@ class CronUtils {
64689
64893
  *
64690
64894
  * Written synchronously in `_tick` at slot-open time (before the `await`),
64691
64895
  * so a still-in-flight handler does not let a later tick re-open the same
64692
- * (or an already-passed) boundary. Fire-once entries never touch this map —
64693
- * they use `_firedOnce`. Pruned by `_clearBoundaryFor` on
64694
- * `register`/`unregister` and wiped by `dispose`.
64896
+ * (or an already-passed) boundary. If that handler then **fails**, the
64897
+ * advance is rolled back after the slot settles — the prior value is restored
64898
+ * (or the key deleted if there was none) — so the failed boundary is retried
64899
+ * on the next tick, mirroring catch-up of a skipped boundary. Fire-once
64900
+ * entries never touch this map — they use `_firedOnce`. Pruned by
64901
+ * `_clearBoundaryFor` on `register`/`unregister` and wiped by `dispose`.
64695
64902
  */
64696
64903
  this._lastBoundary = new Map();
64697
64904
  /**
@@ -64711,7 +64918,7 @@ class CronUtils {
64711
64918
  * name: "fetch-funding",
64712
64919
  * interval: "8h",
64713
64920
  * symbols: ["BTCUSDT", "ETHUSDT"],
64714
- * handler: async (symbol, when, backtest) => { ... },
64921
+ * handler: async (info) => { ... },
64715
64922
  * });
64716
64923
  * // Later:
64717
64924
  * dispose();
@@ -64831,7 +65038,7 @@ class CronUtils {
64831
65038
  * 4. **Fire-once** (`entry.interval === undefined`):
64832
65039
  * - If the entry's fired-once key is already in `_firedOnce`, skip.
64833
65040
  * - Slot key: `${name}:once` (+ scope) (+ gen).
64834
- * - `aligned` = the 1-minute-aligned `when` from step 0.
65041
+ * - `alignedMs` = the 1-minute-aligned `when` from step 0 (`ts`).
64835
65042
  * 5. **Periodic** (`entry.interval` set):
64836
65043
  * - Align `when` to the entry's interval via {@link alignToInterval} to
64837
65044
  * get `alignedMs`, the boundary this tick belongs to.
@@ -64853,32 +65060,44 @@ class CronUtils {
64853
65060
  * handler is still in flight.
64854
65061
  * - Slot key: `${name}:${alignedMs}` (+ scope) (+ gen).
64855
65062
  * 6. Singleshot per slot key: look up the slot in `_inFlight`. If a promise
64856
- * already exists, `await` the same promise. Otherwise invoke
64857
- * `entry.handler`, store the promise, and `await` it. The slot is
64858
- * removed in `.finally()` so the next boundary creates a fresh promise;
64859
- * for fire-once entries the fired-once key is also added to
64860
- * `_firedOnce` on success so subsequent ticks skip it.
65063
+ * already exists, `await` the same promise. Otherwise open the slot via
65064
+ * {@link _runEntry} which assembles the {@link IRuntimeInfo} snapshot
65065
+ * (from `symbol`, `context`, `backtest`) and invokes `entry.handler(info)`
65066
+ * store the promise, and `await` it. The slot is removed in `.finally()`
65067
+ * so the next boundary creates a fresh promise; for fire-once entries the
65068
+ * fired-once key is also added to `_firedOnce` on success so subsequent
65069
+ * ticks skip it.
65070
+ * 7. After `await Promise.all`, roll back the watermark for every **periodic**
65071
+ * slot this tick *opened* (not the ones whose in-flight promise it reused)
65072
+ * whose handler reported failure, so the next tick re-opens and re-runs
65073
+ * that boundary.
64861
65074
  *
64862
65075
  * Errors thrown by `handler` are caught, logged via `console.error`, and
64863
65076
  * **not** rethrown — a failing handler must not break the per-symbol
64864
65077
  * tick loop or unblock other parallel backtests with an unhandled
64865
65078
  * rejection. A failed fire-once handler is **not** marked as fired and
64866
- * will retry on the next tick.
65079
+ * will retry on the next tick. A failed **periodic** handler likewise
65080
+ * retries: the boundary watermark advanced at slot-open time is rolled back
65081
+ * after the slot settles (step 7), so the next tick re-opens that boundary.
64867
65082
  *
64868
65083
  * Requires active method context and execution context.
64869
65084
  *
64870
65085
  * @param symbol - Trading symbol from the current tick.
64871
65086
  * @param when - Virtual time of the current tick.
64872
65087
  * @param backtest - `true` for backtest ticks, `false` for live ticks.
64873
- * Forwarded as the third argument to `entry.handler`. Only the value
64874
- * from the tick that **opens** a given slot is observed by all parallel
64875
- * awaiters of that slot.
65088
+ * Forwarded to {@link _runEntry} and surfaced as `info.backtest`. Only the
65089
+ * value from the tick that **opens** a given slot is observed by all
65090
+ * parallel awaiters of that slot.
65091
+ * @param context - Strategy/exchange/frame identifiers from the originating
65092
+ * lifecycle event, forwarded to `RuntimeMetaService.getRuntimeInfo` to
65093
+ * build the {@link IRuntimeInfo} snapshot passed to the handler.
64876
65094
  * @throws Error if method or execution context is missing.
64877
65095
  */
64878
- this._tick = async (symbol, when, backtest) => {
65096
+ this._tick = async (symbol, when, backtest, context) => {
64879
65097
  LOGGER_SERVICE$1.debug(CRON_METHOD_NAME_TICK, {
64880
65098
  symbol,
64881
65099
  when,
65100
+ context,
64882
65101
  });
64883
65102
  if (!MethodContextService.hasContext()) {
64884
65103
  throw new Error("CronUtils _tick requires method context");
@@ -64888,6 +65107,10 @@ class CronUtils {
64888
65107
  }
64889
65108
  const ts = alignToInterval(when, "1m").getTime();
64890
65109
  const taskList = [];
65110
+ // Periodic slots THIS tick actually opened (the `!pending` branch), tracked
65111
+ // for watermark rollback on failure. See {@link IOpenedSlot} for what is and
65112
+ // is not recorded here and why.
65113
+ const openedList = [];
64891
65114
  for (const { entry, generation } of this._entries.values()) {
64892
65115
  if (entry.symbols?.length && !entry.symbols.includes(symbol)) {
64893
65116
  continue;
@@ -64895,7 +65118,6 @@ class CronUtils {
64895
65118
  const perSymbol = !!entry.symbols?.length;
64896
65119
  const scope = perSymbol ? `:${symbol}` : "";
64897
65120
  const genSuffix = `:g${generation}`;
64898
- let aligned;
64899
65121
  let alignedMs;
64900
65122
  let slotKey;
64901
65123
  let firedKey;
@@ -64907,15 +65129,13 @@ class CronUtils {
64907
65129
  if (this._firedOnce.has(onceKey)) {
64908
65130
  continue;
64909
65131
  }
64910
- aligned = alignToInterval(when, "1m");
64911
65132
  alignedMs = ts;
64912
65133
  slotKey = `${entry.name}:once${scope}${genSuffix}`;
64913
65134
  firedKey = onceKey;
64914
65135
  boundaryKey = null;
64915
65136
  }
64916
65137
  else {
64917
- aligned = alignToInterval(when, entry.interval);
64918
- alignedMs = aligned.getTime();
65138
+ alignedMs = alignToInterval(when, entry.interval).getTime();
64919
65139
  boundaryKey = `${entry.name}${scope}${genSuffix}`;
64920
65140
  const lastBoundary = this._lastBoundary.get(boundaryKey);
64921
65141
  // Fire when the tick's aligned boundary has advanced past the last one
@@ -64933,16 +65153,70 @@ class CronUtils {
64933
65153
  // Advance the watermark synchronously at slot-open time, before the
64934
65154
  // await below. Otherwise a later tick on the same (or an already
64935
65155
  // crossed) boundary, arriving while this handler is still in flight,
64936
- // would see the stale watermark and open a duplicate slot.
65156
+ // would see the stale watermark and open a duplicate slot. The advance
65157
+ // is rolled back after the slot settles if the handler failed (see the
65158
+ // post-await loop below), so a failed boundary is retried next tick.
64937
65159
  if (boundaryKey !== null) {
65160
+ // Capture the pre-advance value so it can be restored verbatim on
65161
+ // failure (undefined => the boundary had never opened => delete the
65162
+ // key on rollback). Read fresh here rather than reusing `lastBoundary`
65163
+ // above to keep the value↔slot binding local and obvious; there is no
65164
+ // `await` between the two reads, so they are identical.
65165
+ const prevBoundary = this._lastBoundary.get(boundaryKey);
64938
65166
  this._lastBoundary.set(boundaryKey, alignedMs);
65167
+ pending = this._runEntry(entry, symbol, alignedMs, slotKey, firedKey, backtest, context);
65168
+ this._inFlight.set(slotKey, pending);
65169
+ openedList.push({ boundaryKey, prevBoundary, pending });
65170
+ }
65171
+ else {
65172
+ pending = this._runEntry(entry, symbol, alignedMs, slotKey, firedKey, backtest, context);
65173
+ this._inFlight.set(slotKey, pending);
64939
65174
  }
64940
- pending = this._runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest);
64941
- this._inFlight.set(slotKey, pending);
64942
65175
  }
64943
65176
  taskList.push(pending);
64944
65177
  }
64945
- await Promise.all(taskList);
65178
+ {
65179
+ // Watchdog: warn (do not interrupt) if the slots this tick is awaiting
65180
+ // have not settled within CRON_HANDLER_TIMEOUT. We deliberately keep
65181
+ // awaiting Promise.all so the singlerun pipeline stays serialised and no
65182
+ // duplicate/zombie slots are spawned — the timer only surfaces the stall.
65183
+ // Use a real setTimeout/clearTimeout (not sleep) so the alarm is cancelled
65184
+ // the instant Promise.all resolves, rather than lingering for the full
65185
+ // timeout on every fast tick.
65186
+ const timer = setTimeout(() => {
65187
+ const message = `${CRON_METHOD_NAME_TICK} timed out after ${CRON_HANDLER_TIMEOUT}ms`;
65188
+ const payload = { symbol, when, context };
65189
+ LOGGER_SERVICE$1.warn(message, payload);
65190
+ console.error(message, payload);
65191
+ errorEmitter.next(new Error(message));
65192
+ }, CRON_HANDLER_TIMEOUT);
65193
+ try {
65194
+ await Promise.all(taskList);
65195
+ }
65196
+ finally {
65197
+ clearTimeout(timer);
65198
+ }
65199
+ }
65200
+ // Roll back the watermark for any periodic slot THIS tick opened whose
65201
+ // handler failed, so the next tick re-opens the same boundary and retries
65202
+ // it — mirroring how a skipped boundary is later caught up. Restoring
65203
+ // `prevBoundary` (or deleting the key when it was `undefined`) re-arms the
65204
+ // strict-`>` gate without disturbing any earlier already-fired boundary.
65205
+ // `await pending` is cheap — every promise already settled in `Promise.all`
65206
+ // above; we re-await via `openedList` because its entries (opened slots
65207
+ // only) do not line up with `taskList` indices.
65208
+ for (const { boundaryKey, prevBoundary, pending } of openedList) {
65209
+ const failed = await pending;
65210
+ if (!failed) {
65211
+ continue;
65212
+ }
65213
+ if (prevBoundary === undefined) {
65214
+ this._lastBoundary.delete(boundaryKey);
65215
+ }
65216
+ else {
65217
+ this._lastBoundary.set(boundaryKey, prevBoundary);
65218
+ }
65219
+ }
64946
65220
  };
64947
65221
  /**
64948
65222
  * Subscribe `Cron` to the engine's strategy lifecycle subjects so registered
@@ -64957,7 +65231,11 @@ class CronUtils {
64957
65231
  *
64958
65232
  * All four subjects are subscribed to a single `singlerun`-wrapped
64959
65233
  * handler that builds `_tick(event.symbol, new Date(event.timestamp),
64960
- * event.backtest)`. `singlerun` merges the four streams into one serial
65234
+ * event.backtest, { strategyName, exchangeName, frameName })`. The context
65235
+ * object is read uniformly from the event — every contract carries
65236
+ * `strategyName`, `exchangeName` and `frameName` at the top level (Active /
65237
+ * Schedule contracts gained `frameName` for exactly this reason), so no
65238
+ * per-event branching is needed. `singlerun` merges the four streams into one serial
64961
65239
  * queue: at most one `_tick` runs at a time, the next waits. This matters
64962
65240
  * because the engine can emit `beforeStart` and an immediate `idlePing`
64963
65241
  * on the very same minute, and concurrent `_tick`s on the same
@@ -64993,7 +65271,11 @@ class CronUtils {
64993
65271
  this.enable = singleshot(() => {
64994
65272
  LOGGER_SERVICE$1.info(CRON_METHOD_NAME_ENABLE);
64995
65273
  const handleTick = singlerun(async (event) => {
64996
- return await this._tick(event.symbol, new Date(event.timestamp), event.backtest);
65274
+ return await this._tick(event.symbol, new Date(event.timestamp), event.backtest, {
65275
+ strategyName: event.strategyName,
65276
+ exchangeName: event.exchangeName,
65277
+ frameName: event.frameName,
65278
+ });
64997
65279
  });
64998
65280
  const unBeforeStart = beforeStartSubject.subscribe(handleTick);
64999
65281
  const unIdlePing = idlePingSubject.subscribe(handleTick);
@@ -65096,25 +65378,51 @@ class CronUtils {
65096
65378
  /**
65097
65379
  * Build the singleshot promise for a single in-flight slot.
65098
65380
  *
65099
- * Invokes `entry.handler(symbol, aligned, backtest)`, swallows and logs
65100
- * any error via `console.error`, and clears the `_inFlight` slot
65101
- * in `.finally()` so the next boundary produces a fresh promise. For
65102
- * fire-once entries `firedKey` is added to `_firedOnce` on success so
65103
- * subsequent ticks skip it.
65104
- *
65381
+ * Assembles the {@link IRuntimeInfo} snapshot via
65382
+ * `RuntimeMetaService.getRuntimeInfo(symbol, context, backtest)` and invokes
65383
+ * `entry.handler(info)`. Logs any error via `console.error` and **returns** a
65384
+ * `failed` boolean (`true` when the handler or the runtime-info assembly —
65385
+ * threw) so the caller (`_tick`) can roll back the periodic watermark of the
65386
+ * slot it opened and retry that boundary. The error is **not** rethrown, so a
65387
+ * failing handler never produces an unhandled rejection. Clears the
65388
+ * `_inFlight` slot in `.finally()` so the next boundary produces a fresh
65389
+ * promise. For fire-once entries `firedKey` is added to `_firedOnce` on
65390
+ * success so subsequent ticks skip it.
65391
+ *
65392
+ * `getRuntimeInfo` is the user-facing aggregator: its sub-fetches (range,
65393
+ * info, price) are individually wrapped in `trycatch` with `null` fallbacks,
65394
+ * so it almost never throws for missing data. Whatever does throw — the
65395
+ * handler, or in rare cases `getRuntimeInfo` — is caught here and reported via
65396
+ * the returned `failed` flag; the watermark rollback treats both identically.
65397
+ *
65398
+ * @param context - Strategy/exchange/frame identifiers from the originating
65399
+ * lifecycle event, forwarded to `getRuntimeInfo` to resolve `range`/`info`.
65105
65400
  * @param firedKey - Key to add to `_firedOnce` on success, or `null` for
65106
65401
  * periodic entries (which never populate `_firedOnce`).
65107
- * @param backtest - Value forwarded as the third handler argument; the
65108
- * "winner" tick's flag is what all parallel awaiters of this slot see.
65402
+ * @param backtest - Forwarded to `getRuntimeInfo` and surfaced as
65403
+ * `info.backtest`; the "winner" tick's flag is what all parallel awaiters
65404
+ * of this slot see.
65405
+ * @returns `true` if the handler (or `getRuntimeInfo`) threw, `false` on
65406
+ * success. `_tick` uses this to decide whether to roll back the watermark.
65109
65407
  */
65110
- async _runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest) {
65408
+ async _runEntry(entry, symbol, alignedMs, slotKey, firedKey, backtest, context) {
65111
65409
  let failed = false;
65112
65410
  try {
65113
- await entry.handler(symbol, aligned, backtest);
65411
+ const info = await RUNTIME_META_SERVICE.getRuntimeInfo(symbol, context, backtest);
65412
+ await entry.handler(info);
65114
65413
  }
65115
- catch (err) {
65414
+ catch (error) {
65116
65415
  failed = true;
65117
- console.error(`${CRON_METHOD_NAME_TICK} entry "${entry.name}" failed`, { symbol, alignedMs, err });
65416
+ const message = `${CRON_METHOD_NAME_TICK} entry "${entry.name}" failed`;
65417
+ const payload = {
65418
+ symbol,
65419
+ alignedMs,
65420
+ error: errorData(error),
65421
+ message: getErrorMessage(error),
65422
+ };
65423
+ LOGGER_SERVICE$1.warn(message, payload);
65424
+ console.error(message, payload);
65425
+ errorEmitter.next(error);
65118
65426
  }
65119
65427
  finally {
65120
65428
  this._inFlight.delete(slotKey);
@@ -65122,6 +65430,7 @@ class CronUtils {
65122
65430
  this._firedOnce.add(firedKey);
65123
65431
  }
65124
65432
  }
65433
+ return failed;
65125
65434
  }
65126
65435
  }
65127
65436
  /**
@@ -65135,7 +65444,7 @@ class CronUtils {
65135
65444
  * Cron.register({
65136
65445
  * name: "tg-parser",
65137
65446
  * interval: "1h",
65138
- * handler: async (symbol, when, backtest) => { ... },
65447
+ * handler: async (info) => { ... },
65139
65448
  * });
65140
65449
  * ```
65141
65450
  */