backtest-kit 15.1.0 → 15.3.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
@@ -553,6 +553,16 @@ const GLOBAL_CONFIG = {
553
553
  * Default: 500 notifications
554
554
  */
555
555
  CC_MAX_NOTIFICATIONS: 500,
556
+ /**
557
+ * Minimum interval between `order_sync.check` notifications for a single signalId.
558
+ * Order-ping events (syncPendingSubject) fire on every live tick, so the
559
+ * NotificationAdapter throttles them: a signal produces at most one check
560
+ * notification per this interval. The throttle entry is removed when the
561
+ * signal is closed or cancelled.
562
+ *
563
+ * Default: 900000 ms (15 minutes)
564
+ */
565
+ CC_NOTIFICATION_ORDER_CHECK_TTL: 900000,
556
566
  /**
557
567
  * Maximum number of signals to keep in storage.
558
568
  * Older signals are removed when this limit is exceeded.
@@ -5317,6 +5327,24 @@ const validateCandles = (candles) => {
5317
5327
  };
5318
5328
 
5319
5329
  const MS_PER_MINUTE$7 = 60000;
5330
+ /**
5331
+ * Normalizes raw adapter output to the {@link IAggregatedTradeData} contract.
5332
+ *
5333
+ * Implicit-API exchange adapters (e.g. ccxt `publicGetAggTrades`) return every
5334
+ * scalar as a string. A string `timestamp` is type-invisible at runtime — it
5335
+ * survives serialization and comparison — and only blows up deep inside a
5336
+ * downstream consumer (`Number.isFinite("1783601403565") === false`). Coercing
5337
+ * the numeric fields here, at the single point where adapter data enters the
5338
+ * framework, guarantees the contract's `number` types hold regardless of
5339
+ * adapter behaviour. `isBuyerMaker` is deliberately left untouched: `Boolean`
5340
+ * coercion of a string `"false"` would yield `true`.
5341
+ */
5342
+ const NORMALIZE_AGGREGATED_TRADES_FN$1 = (trades) => trades.map((trade) => ({
5343
+ ...trade,
5344
+ price: Number(trade.price),
5345
+ qty: Number(trade.qty),
5346
+ timestamp: Number(trade.timestamp),
5347
+ }));
5320
5348
  const INTERVAL_MINUTES$9 = {
5321
5349
  "1m": 1,
5322
5350
  "3m": 3,
@@ -6012,7 +6040,7 @@ class ClientExchange {
6012
6040
  if (limit === undefined) {
6013
6041
  const to = new Date(alignedTo);
6014
6042
  const from = new Date(alignedTo - windowMs);
6015
- return await this.params.getAggregatedTrades(symbol, from, to, this.params.execution.context.backtest);
6043
+ return NORMALIZE_AGGREGATED_TRADES_FN$1(await this.params.getAggregatedTrades(symbol, from, to, this.params.execution.context.backtest));
6016
6044
  }
6017
6045
  // With limit: paginate backwards until we have enough trades.
6018
6046
  // Consecutive empty windows bound the scan: before the symbol's listing
@@ -6029,7 +6057,7 @@ class ClientExchange {
6029
6057
  }
6030
6058
  const to = new Date(windowEnd);
6031
6059
  const from = new Date(windowStart);
6032
- const chunk = await this.params.getAggregatedTrades(symbol, from, to, this.params.execution.context.backtest);
6060
+ const chunk = NORMALIZE_AGGREGATED_TRADES_FN$1(await this.params.getAggregatedTrades(symbol, from, to, this.params.execution.context.backtest));
6033
6061
  if (chunk.length === 0) {
6034
6062
  emptyWindows += 1;
6035
6063
  if (emptyWindows >= MAX_CONSECUTIVE_EMPTY_WINDOWS) {
@@ -15376,6 +15404,10 @@ class StrategyConnectionService {
15376
15404
  });
15377
15405
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
15378
15406
  await strategy.waitForInit();
15407
+ if (!this.timeMetaService.hasTimestamp(symbol, context, backtest)) {
15408
+ const timestamp = this.executionContextService.context.when.getTime();
15409
+ await this.timeMetaService.next(symbol, timestamp, context, backtest);
15410
+ }
15379
15411
  const tick = await strategy.tick(symbol, context.strategyName);
15380
15412
  {
15381
15413
  await this.priceMetaService.next(symbol, tick.currentPrice, context, backtest);
@@ -42696,6 +42728,24 @@ const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
42696
42728
  const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
42697
42729
  const EXCHANGE_METHOD_NAME_GET_AGGREGATED_TRADES = "ExchangeUtils.getAggregatedTrades";
42698
42730
  const MS_PER_MINUTE$4 = 60000;
42731
+ /**
42732
+ * Normalizes raw adapter output to the {@link IAggregatedTradeData} contract.
42733
+ *
42734
+ * Implicit-API exchange adapters (e.g. ccxt `publicGetAggTrades`) return every
42735
+ * scalar as a string. A string `timestamp` is type-invisible at runtime — it
42736
+ * survives serialization and comparison — and only blows up deep inside a
42737
+ * downstream consumer (`Number.isFinite("1783601403565") === false`). Coercing
42738
+ * the numeric fields here, at the single point where adapter data enters the
42739
+ * framework, guarantees the contract's `number` types hold regardless of
42740
+ * adapter behaviour. `isBuyerMaker` is deliberately left untouched: `Boolean`
42741
+ * coercion of a string `"false"` would yield `true`.
42742
+ */
42743
+ const NORMALIZE_AGGREGATED_TRADES_FN = (trades) => trades.map((trade) => ({
42744
+ ...trade,
42745
+ price: Number(trade.price),
42746
+ qty: Number(trade.qty),
42747
+ timestamp: Number(trade.timestamp),
42748
+ }));
42699
42749
  /**
42700
42750
  * Gets current timestamp from execution context if available.
42701
42751
  * Returns current Date() if no execution context exists (non-trading GUI).
@@ -43192,7 +43242,7 @@ class ExchangeInstance {
43192
43242
  if (limit === undefined) {
43193
43243
  const to = new Date(alignedTo);
43194
43244
  const from = new Date(alignedTo - windowMs);
43195
- return await this._methods.getAggregatedTrades(symbol, from, to, isBacktest);
43245
+ return NORMALIZE_AGGREGATED_TRADES_FN(await this._methods.getAggregatedTrades(symbol, from, to, isBacktest));
43196
43246
  }
43197
43247
  // With limit: paginate backwards until we have enough trades.
43198
43248
  // Consecutive empty windows bound the scan: before the symbol's listing
@@ -43209,7 +43259,7 @@ class ExchangeInstance {
43209
43259
  }
43210
43260
  const to = new Date(windowEnd);
43211
43261
  const from = new Date(windowStart);
43212
- const chunk = await this._methods.getAggregatedTrades(symbol, from, to, isBacktest);
43262
+ const chunk = NORMALIZE_AGGREGATED_TRADES_FN(await this._methods.getAggregatedTrades(symbol, from, to, isBacktest));
43213
43263
  if (chunk.length === 0) {
43214
43264
  emptyWindows += 1;
43215
43265
  if (emptyWindows >= MAX_CONSECUTIVE_EMPTY_WINDOWS) {
@@ -45708,6 +45758,7 @@ class BrokerAdapter {
45708
45758
  exchangeName: event.exchangeName,
45709
45759
  frameName: event.frameName,
45710
45760
  },
45761
+ when: new Date(event.timestamp),
45711
45762
  backtest: event.backtest,
45712
45763
  });
45713
45764
  });
@@ -45734,6 +45785,7 @@ class BrokerAdapter {
45734
45785
  exchangeName: event.exchangeName,
45735
45786
  frameName: event.frameName,
45736
45787
  },
45788
+ when: new Date(event.timestamp),
45737
45789
  backtest: event.backtest,
45738
45790
  });
45739
45791
  });
@@ -45757,6 +45809,7 @@ class BrokerAdapter {
45757
45809
  exchangeName: event.exchangeName,
45758
45810
  frameName: event.frameName,
45759
45811
  },
45812
+ when: new Date(event.timestamp),
45760
45813
  backtest: event.backtest,
45761
45814
  });
45762
45815
  });
@@ -45775,6 +45828,7 @@ class BrokerAdapter {
45775
45828
  exchangeName: event.exchangeName,
45776
45829
  frameName: event.frameName,
45777
45830
  },
45831
+ when: new Date(event.timestamp),
45778
45832
  backtest: event.backtest,
45779
45833
  });
45780
45834
  });
@@ -45792,6 +45846,7 @@ class BrokerAdapter {
45792
45846
  exchangeName: event.exchangeName,
45793
45847
  frameName: event.frameName,
45794
45848
  },
45849
+ when: new Date(event.timestamp),
45795
45850
  backtest: event.backtest,
45796
45851
  });
45797
45852
  });
@@ -45804,6 +45859,7 @@ class BrokerAdapter {
45804
45859
  exchangeName: event.exchangeName,
45805
45860
  frameName: event.frameName,
45806
45861
  },
45862
+ when: new Date(event.timestamp),
45807
45863
  backtest: event.backtest,
45808
45864
  });
45809
45865
  });
@@ -45821,6 +45877,7 @@ class BrokerAdapter {
45821
45877
  exchangeName: event.exchangeName,
45822
45878
  frameName: event.frameName,
45823
45879
  },
45880
+ when: new Date(event.timestamp),
45824
45881
  backtest: event.backtest,
45825
45882
  };
45826
45883
  if (event.action === "scheduled") {
@@ -45843,6 +45900,7 @@ class BrokerAdapter {
45843
45900
  exchangeName: event.exchangeName,
45844
45901
  frameName: event.frameName,
45845
45902
  },
45903
+ when: new Date(event.timestamp),
45846
45904
  backtest: event.backtest,
45847
45905
  };
45848
45906
  if (event.action === "opened") {
@@ -46617,6 +46675,7 @@ async function commitPartialProfit(symbol, percentToClose) {
46617
46675
  priceTakeProfit: signalForProfit.priceTakeProfit,
46618
46676
  priceStopLoss: signalForProfit.priceStopLoss,
46619
46677
  context: { exchangeName, frameName, strategyName },
46678
+ when: backtest.executionContextService.context.when,
46620
46679
  backtest: isBacktest,
46621
46680
  });
46622
46681
  return await backtest.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
@@ -46685,6 +46744,7 @@ async function commitPartialLoss(symbol, percentToClose) {
46685
46744
  priceTakeProfit: signalForLoss.priceTakeProfit,
46686
46745
  priceStopLoss: signalForLoss.priceStopLoss,
46687
46746
  context: { exchangeName, frameName, strategyName },
46747
+ when: backtest.executionContextService.context.when,
46688
46748
  backtest: isBacktest,
46689
46749
  });
46690
46750
  return await backtest.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
@@ -46769,6 +46829,7 @@ async function commitTrailingStop(symbol, percentShift, currentPrice) {
46769
46829
  takeProfitPrice: signal.priceTakeProfit,
46770
46830
  position: signal.position,
46771
46831
  context: { exchangeName, frameName, strategyName },
46832
+ when: backtest.executionContextService.context.when,
46772
46833
  backtest: isBacktest,
46773
46834
  });
46774
46835
  return await backtest.strategyCoreService.trailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
@@ -46853,6 +46914,7 @@ async function commitTrailingTake(symbol, percentShift, currentPrice) {
46853
46914
  takeProfitPrice: signal.priceTakeProfit,
46854
46915
  position: signal.position,
46855
46916
  context: { exchangeName, frameName, strategyName },
46917
+ when: backtest.executionContextService.context.when,
46856
46918
  backtest: isBacktest,
46857
46919
  });
46858
46920
  return await backtest.strategyCoreService.trailingTake(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
@@ -46908,6 +46970,7 @@ async function commitTrailingStopCost(symbol, newStopLossPrice) {
46908
46970
  position: signal.position,
46909
46971
  takeProfitPrice: signal.priceTakeProfit,
46910
46972
  context: { exchangeName, frameName, strategyName },
46973
+ when: backtest.executionContextService.context.when,
46911
46974
  backtest: isBacktest,
46912
46975
  });
46913
46976
  return await backtest.strategyCoreService.trailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
@@ -46963,6 +47026,7 @@ async function commitTrailingTakeCost(symbol, newTakeProfitPrice) {
46963
47026
  takeProfitPrice: signal.priceTakeProfit,
46964
47027
  position: signal.position,
46965
47028
  context: { exchangeName, frameName, strategyName },
47029
+ when: backtest.executionContextService.context.when,
46966
47030
  backtest: isBacktest,
46967
47031
  });
46968
47032
  return await backtest.strategyCoreService.trailingTake(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
@@ -47024,6 +47088,7 @@ async function commitBreakeven(symbol) {
47024
47088
  newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
47025
47089
  position: signal.position,
47026
47090
  context: { exchangeName, frameName, strategyName },
47091
+ when: backtest.executionContextService.context.when,
47027
47092
  backtest: isBacktest,
47028
47093
  });
47029
47094
  return await backtest.strategyCoreService.breakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
@@ -47116,6 +47181,7 @@ async function commitAverageBuy(symbol, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_C
47116
47181
  priceTakeProfit: signalForAvgBuy.priceTakeProfit,
47117
47182
  priceStopLoss: signalForAvgBuy.priceStopLoss,
47118
47183
  context: { exchangeName, frameName, strategyName },
47184
+ when: backtest.executionContextService.context.when,
47119
47185
  backtest: isBacktest,
47120
47186
  });
47121
47187
  return await backtest.strategyCoreService.averageBuy(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName }, cost);
@@ -47552,6 +47618,7 @@ async function commitPartialProfitCost(symbol, dollarAmount) {
47552
47618
  priceTakeProfit: signalForProfitCost.priceTakeProfit,
47553
47619
  priceStopLoss: signalForProfitCost.priceStopLoss,
47554
47620
  context: { exchangeName, frameName, strategyName },
47621
+ when: backtest.executionContextService.context.when,
47555
47622
  backtest: isBacktest,
47556
47623
  });
47557
47624
  return await backtest.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
@@ -47624,6 +47691,7 @@ async function commitPartialLossCost(symbol, dollarAmount) {
47624
47691
  priceTakeProfit: signalForLossCost.priceTakeProfit,
47625
47692
  priceStopLoss: signalForLossCost.priceStopLoss,
47626
47693
  context: { exchangeName, frameName, strategyName },
47694
+ when: backtest.executionContextService.context.when,
47627
47695
  backtest: isBacktest,
47628
47696
  });
47629
47697
  return await backtest.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
@@ -52538,6 +52606,7 @@ class BacktestUtils {
52538
52606
  priceTakeProfit: signalForProfit.priceTakeProfit,
52539
52607
  priceStopLoss: signalForProfit.priceStopLoss,
52540
52608
  context,
52609
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
52541
52610
  backtest: true,
52542
52611
  });
52543
52612
  return await backtest.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
@@ -52612,6 +52681,7 @@ class BacktestUtils {
52612
52681
  priceTakeProfit: signalForLoss.priceTakeProfit,
52613
52682
  priceStopLoss: signalForLoss.priceStopLoss,
52614
52683
  context,
52684
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
52615
52685
  backtest: true,
52616
52686
  });
52617
52687
  return await backtest.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
@@ -52689,6 +52759,7 @@ class BacktestUtils {
52689
52759
  priceTakeProfit: signalForProfitCost.priceTakeProfit,
52690
52760
  priceStopLoss: signalForProfitCost.priceStopLoss,
52691
52761
  context,
52762
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
52692
52763
  backtest: true,
52693
52764
  });
52694
52765
  return await backtest.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
@@ -52766,6 +52837,7 @@ class BacktestUtils {
52766
52837
  priceTakeProfit: signalForLossCost.priceTakeProfit,
52767
52838
  priceStopLoss: signalForLossCost.priceStopLoss,
52768
52839
  context,
52840
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
52769
52841
  backtest: true,
52770
52842
  });
52771
52843
  return await backtest.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
@@ -52855,6 +52927,7 @@ class BacktestUtils {
52855
52927
  takeProfitPrice: signal.priceTakeProfit,
52856
52928
  position: signal.position,
52857
52929
  context,
52930
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
52858
52931
  backtest: true,
52859
52932
  });
52860
52933
  return await backtest.strategyCoreService.trailingStop(true, symbol, percentShift, currentPrice, context);
@@ -52944,6 +53017,7 @@ class BacktestUtils {
52944
53017
  takeProfitPrice: signal.priceTakeProfit,
52945
53018
  position: signal.position,
52946
53019
  context,
53020
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
52947
53021
  backtest: true,
52948
53022
  });
52949
53023
  return await backtest.strategyCoreService.trailingTake(true, symbol, percentShift, currentPrice, context);
@@ -52999,6 +53073,7 @@ class BacktestUtils {
52999
53073
  position: signal.position,
53000
53074
  takeProfitPrice: signal.priceTakeProfit,
53001
53075
  context,
53076
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
53002
53077
  backtest: true,
53003
53078
  });
53004
53079
  return await backtest.strategyCoreService.trailingStop(true, symbol, percentShift, currentPrice, context);
@@ -53054,6 +53129,7 @@ class BacktestUtils {
53054
53129
  position: signal.position,
53055
53130
  takeProfitPrice: signal.priceTakeProfit,
53056
53131
  context,
53132
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
53057
53133
  backtest: true,
53058
53134
  });
53059
53135
  return await backtest.strategyCoreService.trailingTake(true, symbol, percentShift, currentPrice, context);
@@ -53115,6 +53191,7 @@ class BacktestUtils {
53115
53191
  newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
53116
53192
  position: signal.position,
53117
53193
  context,
53194
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
53118
53195
  backtest: true,
53119
53196
  });
53120
53197
  return await backtest.strategyCoreService.breakeven(true, symbol, currentPrice, context);
@@ -53216,6 +53293,7 @@ class BacktestUtils {
53216
53293
  priceTakeProfit: signalForAvgBuy.priceTakeProfit,
53217
53294
  priceStopLoss: signalForAvgBuy.priceStopLoss,
53218
53295
  context,
53296
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
53219
53297
  backtest: true,
53220
53298
  });
53221
53299
  return await backtest.strategyCoreService.averageBuy(true, symbol, currentPrice, context, cost);
@@ -55547,6 +55625,7 @@ class LiveUtils {
55547
55625
  exchangeName: context.exchangeName,
55548
55626
  frameName: "",
55549
55627
  },
55628
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
55550
55629
  backtest: false,
55551
55630
  });
55552
55631
  return await backtest.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
@@ -55642,6 +55721,7 @@ class LiveUtils {
55642
55721
  exchangeName: context.exchangeName,
55643
55722
  frameName: "",
55644
55723
  },
55724
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
55645
55725
  backtest: false,
55646
55726
  });
55647
55727
  return await backtest.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
@@ -55739,6 +55819,7 @@ class LiveUtils {
55739
55819
  exchangeName: context.exchangeName,
55740
55820
  frameName: "",
55741
55821
  },
55822
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
55742
55823
  backtest: false,
55743
55824
  });
55744
55825
  return await backtest.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
@@ -55836,6 +55917,7 @@ class LiveUtils {
55836
55917
  exchangeName: context.exchangeName,
55837
55918
  frameName: "",
55838
55919
  },
55920
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
55839
55921
  backtest: false,
55840
55922
  });
55841
55923
  return await backtest.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
@@ -55940,6 +56022,7 @@ class LiveUtils {
55940
56022
  takeProfitPrice: signal.priceTakeProfit,
55941
56023
  position: signal.position,
55942
56024
  context,
56025
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
55943
56026
  backtest: false,
55944
56027
  });
55945
56028
  return await backtest.strategyCoreService.trailingStop(false, symbol, percentShift, currentPrice, {
@@ -56044,6 +56127,7 @@ class LiveUtils {
56044
56127
  takeProfitPrice: signal.priceTakeProfit,
56045
56128
  position: signal.position,
56046
56129
  context,
56130
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
56047
56131
  backtest: false,
56048
56132
  });
56049
56133
  return await backtest.strategyCoreService.trailingTake(false, symbol, percentShift, currentPrice, {
@@ -56115,6 +56199,7 @@ class LiveUtils {
56115
56199
  takeProfitPrice: signal.priceTakeProfit,
56116
56200
  position: signal.position,
56117
56201
  context,
56202
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
56118
56203
  backtest: false,
56119
56204
  });
56120
56205
  return await backtest.strategyCoreService.trailingStop(false, symbol, percentShift, currentPrice, {
@@ -56186,6 +56271,7 @@ class LiveUtils {
56186
56271
  takeProfitPrice: signal.priceTakeProfit,
56187
56272
  position: signal.position,
56188
56273
  context,
56274
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
56189
56275
  backtest: false,
56190
56276
  });
56191
56277
  return await backtest.strategyCoreService.trailingTake(false, symbol, percentShift, currentPrice, {
@@ -56267,6 +56353,7 @@ class LiveUtils {
56267
56353
  exchangeName: context.exchangeName,
56268
56354
  frameName: "",
56269
56355
  },
56356
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
56270
56357
  backtest: false,
56271
56358
  });
56272
56359
  return await backtest.strategyCoreService.breakeven(false, symbol, currentPrice, {
@@ -56386,6 +56473,7 @@ class LiveUtils {
56386
56473
  exchangeName: context.exchangeName,
56387
56474
  frameName: "",
56388
56475
  },
56476
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
56389
56477
  backtest: false,
56390
56478
  });
56391
56479
  return await backtest.strategyCoreService.averageBuy(false, symbol, currentPrice, {
@@ -66681,7 +66769,8 @@ const WILDCARD_TARGET = {
66681
66769
  partial_loss: true,
66682
66770
  breakeven: true,
66683
66771
  strategy_commit: true,
66684
- signal_sync: true,
66772
+ order_sync: true,
66773
+ order_check: true,
66685
66774
  risk: true,
66686
66775
  info: true,
66687
66776
  common_error: true,
@@ -67410,14 +67499,15 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
67410
67499
  };
67411
67500
  /**
67412
67501
  * Creates a notification model for signal sync events.
67413
- * Handles signal-open (limit order filled) and signal-close (position exited) actions.
67502
+ * Handles signal-open (position order filled with orderType "active", or resting
67503
+ * entry order placed with orderType "schedule") and signal-close (position exited) actions.
67414
67504
  * @param data - The signal sync contract data
67415
67505
  * @returns NotificationModel for signal sync event
67416
67506
  */
67417
67507
  const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
67418
67508
  if (data.action === "signal-open") {
67419
67509
  return {
67420
- type: "signal_sync.open",
67510
+ type: "order_sync.open",
67421
67511
  id: CREATE_KEY_FN$2(),
67422
67512
  timestamp: data.timestamp,
67423
67513
  backtest: data.backtest,
@@ -67425,6 +67515,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
67425
67515
  strategyName: data.strategyName,
67426
67516
  exchangeName: data.exchangeName,
67427
67517
  signalId: data.signalId,
67518
+ orderType: data.type,
67428
67519
  currentPrice: data.currentPrice,
67429
67520
  pnl: data.signal.pnl,
67430
67521
  maxDrawdown: data.signal.maxDrawdown,
@@ -67462,7 +67553,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
67462
67553
  }
67463
67554
  if (data.action === "signal-close") {
67464
67555
  return {
67465
- type: "signal_sync.close",
67556
+ type: "order_sync.close",
67466
67557
  id: CREATE_KEY_FN$2(),
67467
67558
  timestamp: data.timestamp,
67468
67559
  backtest: data.backtest,
@@ -67470,6 +67561,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
67470
67561
  strategyName: data.strategyName,
67471
67562
  exchangeName: data.exchangeName,
67472
67563
  signalId: data.signalId,
67564
+ orderType: data.type,
67473
67565
  currentPrice: data.currentPrice,
67474
67566
  pnl: data.signal.pnl,
67475
67567
  maxDrawdown: data.signal.maxDrawdown,
@@ -67507,6 +67599,54 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
67507
67599
  }
67508
67600
  throw new Error(`Unrecognized signal sync action: ${get(data, "action")}`);
67509
67601
  };
67602
+ /**
67603
+ * Creates a notification model for order-ping check events.
67604
+ * @param data - The order check contract data
67605
+ * @returns NotificationModel for signal sync check event
67606
+ */
67607
+ const CREATE_ORDER_CHECK_NOTIFICATION_FN = (data) => ({
67608
+ type: "order_sync.check",
67609
+ id: CREATE_KEY_FN$2(),
67610
+ timestamp: data.timestamp,
67611
+ backtest: data.backtest,
67612
+ symbol: data.symbol,
67613
+ strategyName: data.strategyName,
67614
+ exchangeName: data.exchangeName,
67615
+ signalId: data.signalId,
67616
+ orderType: data.type,
67617
+ currentPrice: data.currentPrice,
67618
+ position: data.position,
67619
+ priceOpen: data.priceOpen,
67620
+ priceTakeProfit: data.priceTakeProfit,
67621
+ priceStopLoss: data.priceStopLoss,
67622
+ originalPriceTakeProfit: data.originalPriceTakeProfit,
67623
+ originalPriceStopLoss: data.originalPriceStopLoss,
67624
+ originalPriceOpen: data.originalPriceOpen,
67625
+ totalEntries: data.totalEntries,
67626
+ totalPartials: data.totalPartials,
67627
+ pnl: data.pnl,
67628
+ maxDrawdown: data.maxDrawdown,
67629
+ peakProfit: data.peakProfit,
67630
+ pnlPercentage: data.pnl.pnlPercentage,
67631
+ pnlPriceOpen: data.pnl.priceOpen,
67632
+ pnlPriceClose: data.pnl.priceClose,
67633
+ pnlCost: data.pnl.pnlCost,
67634
+ pnlEntries: data.pnl.pnlEntries,
67635
+ peakProfitPriceOpen: data.peakProfit.priceOpen,
67636
+ peakProfitPriceClose: data.peakProfit.priceClose,
67637
+ peakProfitPercentage: data.peakProfit.pnlPercentage,
67638
+ peakProfitCost: data.peakProfit.pnlCost,
67639
+ peakProfitEntries: data.peakProfit.pnlEntries,
67640
+ maxDrawdownPriceOpen: data.maxDrawdown.priceOpen,
67641
+ maxDrawdownPriceClose: data.maxDrawdown.priceClose,
67642
+ maxDrawdownPercentage: data.maxDrawdown.pnlPercentage,
67643
+ maxDrawdownCost: data.maxDrawdown.pnlCost,
67644
+ maxDrawdownEntries: data.maxDrawdown.pnlEntries,
67645
+ scheduledAt: data.scheduledAt,
67646
+ pendingAt: data.pendingAt,
67647
+ note: data.signal.note,
67648
+ createdAt: data.timestamp,
67649
+ });
67510
67650
  /**
67511
67651
  * Creates a notification model for risk rejection events.
67512
67652
  * @param data - The risk contract data
@@ -67624,6 +67764,7 @@ const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_PARTIAL_LOSS = "Notificati
67624
67764
  const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_BREAKEVEN = "NotificationMemoryBacktestUtils.handleBreakeven";
67625
67765
  const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_STRATEGY_COMMIT = "NotificationMemoryBacktestUtils.handleStrategyCommit";
67626
67766
  const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_SYNC = "NotificationMemoryBacktestUtils.handleSync";
67767
+ const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_CHECK = "NotificationMemoryBacktestUtils.handleCheck";
67627
67768
  const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_RISK = "NotificationMemoryBacktestUtils.handleRisk";
67628
67769
  const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_ERROR = "NotificationMemoryBacktestUtils.handleError";
67629
67770
  const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_CRITICAL_ERROR = "NotificationMemoryBacktestUtils.handleCriticalError";
@@ -67637,6 +67778,7 @@ const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_PARTIAL_LOSS = "NotificationMe
67637
67778
  const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_BREAKEVEN = "NotificationMemoryLiveUtils.handleBreakeven";
67638
67779
  const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_STRATEGY_COMMIT = "NotificationMemoryLiveUtils.handleStrategyCommit";
67639
67780
  const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_SYNC = "NotificationMemoryLiveUtils.handleSync";
67781
+ const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_CHECK = "NotificationMemoryLiveUtils.handleCheck";
67640
67782
  const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_RISK = "NotificationMemoryLiveUtils.handleRisk";
67641
67783
  const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_ERROR = "NotificationMemoryLiveUtils.handleError";
67642
67784
  const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_CRITICAL_ERROR = "NotificationMemoryLiveUtils.handleCriticalError";
@@ -67666,6 +67808,7 @@ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_PARTIAL_LOSS = "Notificat
67666
67808
  const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_BREAKEVEN = "NotificationPersistBacktestUtils.handleBreakeven";
67667
67809
  const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_STRATEGY_COMMIT = "NotificationPersistBacktestUtils.handleStrategyCommit";
67668
67810
  const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_SYNC = "NotificationPersistBacktestUtils.handleSync";
67811
+ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_CHECK = "NotificationPersistBacktestUtils.handleCheck";
67669
67812
  const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_RISK = "NotificationPersistBacktestUtils.handleRisk";
67670
67813
  const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_ERROR = "NotificationPersistBacktestUtils.handleError";
67671
67814
  const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_CRITICAL_ERROR = "NotificationPersistBacktestUtils.handleCriticalError";
@@ -67681,6 +67824,7 @@ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_PARTIAL_LOSS = "NotificationP
67681
67824
  const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_BREAKEVEN = "NotificationPersistLiveUtils.handleBreakeven";
67682
67825
  const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_STRATEGY_COMMIT = "NotificationPersistLiveUtils.handleStrategyCommit";
67683
67826
  const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_SYNC = "NotificationPersistLiveUtils.handleSync";
67827
+ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_CHECK = "NotificationPersistLiveUtils.handleCheck";
67684
67828
  const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_RISK = "NotificationPersistLiveUtils.handleRisk";
67685
67829
  const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_ERROR = "NotificationPersistLiveUtils.handleError";
67686
67830
  const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_CRITICAL_ERROR = "NotificationPersistLiveUtils.handleCriticalError";
@@ -67779,15 +67923,23 @@ class NotificationMemoryBacktestUtils {
67779
67923
  signalId: data.signalId,
67780
67924
  action: data.action,
67781
67925
  });
67782
- // A "schedule" open is a resting-order placement, not a fill:
67783
- // signal_sync.open means "limit order confirmed filled"
67784
- if (data.action === "signal-open" && data.type === "schedule") {
67785
- return;
67786
- }
67787
67926
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
67788
67927
  }, {
67789
67928
  defaultValue: null,
67790
67929
  });
67930
+ /**
67931
+ * Handles order-ping check event.
67932
+ * @param data - The order check contract data
67933
+ */
67934
+ this.handleCheck = functoolsKit.trycatch(async (data) => {
67935
+ backtest.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_CHECK, {
67936
+ signalId: data.signalId,
67937
+ type: data.type,
67938
+ });
67939
+ this._addNotification(CREATE_ORDER_CHECK_NOTIFICATION_FN(data));
67940
+ }, {
67941
+ defaultValue: null,
67942
+ });
67791
67943
  /**
67792
67944
  * Handles risk rejection event.
67793
67945
  * @param data - The risk contract data
@@ -67902,6 +68054,13 @@ class NotificationDummyBacktestUtils {
67902
68054
  }, {
67903
68055
  defaultValue: null,
67904
68056
  });
68057
+ /**
68058
+ * No-op handler for order-ping check event.
68059
+ */
68060
+ this.handleCheck = functoolsKit.trycatch(async () => {
68061
+ }, {
68062
+ defaultValue: null,
68063
+ });
67905
68064
  /**
67906
68065
  * No-op handler for risk rejection event.
67907
68066
  */
@@ -68053,17 +68212,27 @@ class NotificationPersistBacktestUtils {
68053
68212
  signalId: data.signalId,
68054
68213
  action: data.action,
68055
68214
  });
68056
- // A "schedule" open is a resting-order placement, not a fill:
68057
- // signal_sync.open means "limit order confirmed filled"
68058
- if (data.action === "signal-open" && data.type === "schedule") {
68059
- return;
68060
- }
68061
68215
  await this.waitForInit();
68062
68216
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
68063
68217
  await this._updateNotifications();
68064
68218
  }, {
68065
68219
  defaultValue: null,
68066
68220
  });
68221
+ /**
68222
+ * Handles order-ping check event.
68223
+ * @param data - The order check contract data
68224
+ */
68225
+ this.handleCheck = functoolsKit.trycatch(async (data) => {
68226
+ backtest.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_CHECK, {
68227
+ signalId: data.signalId,
68228
+ type: data.type,
68229
+ });
68230
+ await this.waitForInit();
68231
+ this._addNotification(CREATE_ORDER_CHECK_NOTIFICATION_FN(data));
68232
+ await this._updateNotifications();
68233
+ }, {
68234
+ defaultValue: null,
68235
+ });
68067
68236
  /**
68068
68237
  * Handles risk rejection event.
68069
68238
  * @param data - The risk contract data
@@ -68257,15 +68426,23 @@ class NotificationMemoryLiveUtils {
68257
68426
  signalId: data.signalId,
68258
68427
  action: data.action,
68259
68428
  });
68260
- // A "schedule" open is a resting-order placement, not a fill:
68261
- // signal_sync.open means "limit order confirmed filled"
68262
- if (data.action === "signal-open" && data.type === "schedule") {
68263
- return;
68264
- }
68265
68429
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
68266
68430
  }, {
68267
68431
  defaultValue: null,
68268
68432
  });
68433
+ /**
68434
+ * Handles order-ping check event.
68435
+ * @param data - The order check contract data
68436
+ */
68437
+ this.handleCheck = functoolsKit.trycatch(async (data) => {
68438
+ backtest.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_CHECK, {
68439
+ signalId: data.signalId,
68440
+ type: data.type,
68441
+ });
68442
+ this._addNotification(CREATE_ORDER_CHECK_NOTIFICATION_FN(data));
68443
+ }, {
68444
+ defaultValue: null,
68445
+ });
68269
68446
  /**
68270
68447
  * Handles risk rejection event.
68271
68448
  * @param data - The risk contract data
@@ -68380,6 +68557,13 @@ class NotificationDummyLiveUtils {
68380
68557
  }, {
68381
68558
  defaultValue: null,
68382
68559
  });
68560
+ /**
68561
+ * No-op handler for order-ping check event.
68562
+ */
68563
+ this.handleCheck = functoolsKit.trycatch(async () => {
68564
+ }, {
68565
+ defaultValue: null,
68566
+ });
68383
68567
  /**
68384
68568
  * No-op handler for risk rejection event.
68385
68569
  */
@@ -68532,17 +68716,27 @@ class NotificationPersistLiveUtils {
68532
68716
  signalId: data.signalId,
68533
68717
  action: data.action,
68534
68718
  });
68535
- // A "schedule" open is a resting-order placement, not a fill:
68536
- // signal_sync.open means "limit order confirmed filled"
68537
- if (data.action === "signal-open" && data.type === "schedule") {
68538
- return;
68539
- }
68540
68719
  await this.waitForInit();
68541
68720
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
68542
68721
  await this._updateNotifications();
68543
68722
  }, {
68544
68723
  defaultValue: null,
68545
68724
  });
68725
+ /**
68726
+ * Handles order-ping check event.
68727
+ * @param data - The order check contract data
68728
+ */
68729
+ this.handleCheck = functoolsKit.trycatch(async (data) => {
68730
+ backtest.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_CHECK, {
68731
+ signalId: data.signalId,
68732
+ type: data.type,
68733
+ });
68734
+ await this.waitForInit();
68735
+ this._addNotification(CREATE_ORDER_CHECK_NOTIFICATION_FN(data));
68736
+ await this._updateNotifications();
68737
+ }, {
68738
+ defaultValue: null,
68739
+ });
68546
68740
  /**
68547
68741
  * Handles risk rejection event.
68548
68742
  * @param data - The risk contract data
@@ -68720,6 +68914,16 @@ class NotificationBacktestAdapter {
68720
68914
  }, {
68721
68915
  defaultValue: null,
68722
68916
  });
68917
+ /**
68918
+ * Handles order-ping check event.
68919
+ * Proxies call to the underlying notification adapter.
68920
+ * @param data - The order check contract data
68921
+ */
68922
+ this.handleCheck = functoolsKit.trycatch(async (data) => {
68923
+ return await this.getInstance().handleCheck(data);
68924
+ }, {
68925
+ defaultValue: null,
68926
+ });
68723
68927
  /**
68724
68928
  * Handles risk rejection event.
68725
68929
  * Proxies call to the underlying notification adapter.
@@ -68890,6 +69094,16 @@ class NotificationLiveAdapter {
68890
69094
  }, {
68891
69095
  defaultValue: null,
68892
69096
  });
69097
+ /**
69098
+ * Handles order-ping check event.
69099
+ * Proxies call to the underlying notification adapter.
69100
+ * @param data - The order check contract data
69101
+ */
69102
+ this.handleCheck = functoolsKit.trycatch(async (data) => {
69103
+ return await this.getInstance().handleCheck(data);
69104
+ }, {
69105
+ defaultValue: null,
69106
+ });
68893
69107
  /**
68894
69108
  * Handles risk rejection event.
68895
69109
  * Proxies call to the underlying notification adapter.
@@ -69003,16 +69217,40 @@ class NotificationAdapter {
69003
69217
  *
69004
69218
  * @returns Cleanup function that unsubscribes from all emitters
69005
69219
  */
69006
- this.enable = functoolsKit.singleshot(({ signal = false, info = false, partial_profit = false, partial_loss = false, breakeven = false, strategy_commit = false, signal_sync = false, risk = false, common_error = false, critical_error = false, validation_error = false, } = WILDCARD_TARGET) => {
69220
+ this.enable = functoolsKit.singleshot(({ signal = false, info = false, partial_profit = false, partial_loss = false, breakeven = false, strategy_commit = false, order_sync = false, order_check = false, risk = false, common_error = false, critical_error = false, validation_error = false, } = WILDCARD_TARGET) => {
69007
69221
  backtest.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_ENABLE);
69008
69222
  let unLive;
69009
69223
  let unBacktest;
69010
69224
  {
69225
+ // Throttle state for order_sync.check: signalId -> timestamp of the last
69226
+ // emitted check notification. Entries are dropped on signal close/cancel
69227
+ // so the map cannot grow unbounded.
69228
+ const checkThrottleMap = new Map();
69011
69229
  const unBacktestSignal = signalBacktestEmitter.subscribe(async (data) => {
69012
69230
  if (signal) {
69013
69231
  await NotificationBacktest.handleSignal(data);
69014
69232
  }
69015
69233
  });
69234
+ // Cleanup for checkThrottleMap. Tick-result emitters are NOT sufficient here:
69235
+ // out-of-band closes (commitClosePending, failed order ping, scheduled cancel
69236
+ // via commit) never reach signalBacktestEmitter/signalLiveEmitter. The
69237
+ // lifecycle channels cover every path: signalEventSubject "closed" fires for
69238
+ // all pending-signal closes (TP/SL/time_expired/user/ping), scheduleEventSubject
69239
+ // "cancelled" fires for scheduled signals removed before activation.
69240
+ const unBacktestSignalEvent = signalEventSubject
69241
+ .filter(({ backtest }) => backtest)
69242
+ .connect(async (event) => {
69243
+ if (event.action === "closed") {
69244
+ checkThrottleMap.delete(event.data.id);
69245
+ }
69246
+ });
69247
+ const unBacktestScheduleEvent = scheduleEventSubject
69248
+ .filter(({ backtest }) => backtest)
69249
+ .connect(async (event) => {
69250
+ if (event.action === "cancelled") {
69251
+ checkThrottleMap.delete(event.data.id);
69252
+ }
69253
+ });
69016
69254
  const unBacktestPartialProfit = partialProfitSubject
69017
69255
  .filter(({ backtest }) => backtest)
69018
69256
  .connect(async (data) => {
@@ -69044,10 +69282,22 @@ class NotificationAdapter {
69044
69282
  const unBacktestSync = syncSubject
69045
69283
  .filter(({ backtest }) => backtest)
69046
69284
  .connect(async (data) => {
69047
- if (signal_sync) {
69285
+ if (order_sync) {
69048
69286
  await NotificationBacktest.handleSync(data);
69049
69287
  }
69050
69288
  });
69289
+ const unBacktestCheck = syncPendingSubject
69290
+ .filter(({ backtest }) => backtest)
69291
+ .connect(async (data) => {
69292
+ if (order_check) {
69293
+ const lastTimestamp = checkThrottleMap.get(data.signalId);
69294
+ if (lastTimestamp !== undefined && data.timestamp - lastTimestamp < GLOBAL_CONFIG.CC_NOTIFICATION_ORDER_CHECK_TTL) {
69295
+ return;
69296
+ }
69297
+ checkThrottleMap.set(data.signalId, data.timestamp);
69298
+ await NotificationBacktest.handleCheck(data);
69299
+ }
69300
+ });
69051
69301
  const unBacktestRisk = riskSubject
69052
69302
  .filter(({ backtest }) => backtest)
69053
69303
  .connect(async (data) => {
@@ -69077,14 +69327,38 @@ class NotificationAdapter {
69077
69327
  await NotificationBacktest.handleSignalNotify(data);
69078
69328
  }
69079
69329
  });
69080
- unBacktest = functoolsKit.compose(() => unBacktestSignal(), () => unBacktestPartialProfit(), () => unBacktestPartialLoss(), () => unBacktestBreakeven(), () => unBacktestStrategyCommit(), () => unBacktestSync(), () => unBacktestRisk(), () => unBacktestError(), () => unBacktestExit(), () => unBacktestValidation(), () => unBacktestSignalNotify());
69330
+ unBacktest = functoolsKit.compose(() => unBacktestSignal(), () => unBacktestSignalEvent(), () => unBacktestScheduleEvent(), () => unBacktestPartialProfit(), () => unBacktestPartialLoss(), () => unBacktestBreakeven(), () => unBacktestStrategyCommit(), () => unBacktestSync(), () => unBacktestCheck(), () => unBacktestRisk(), () => unBacktestError(), () => unBacktestExit(), () => unBacktestValidation(), () => unBacktestSignalNotify(), () => checkThrottleMap.clear());
69081
69331
  }
69082
69332
  {
69333
+ // Throttle state for order_sync.check: signalId -> timestamp of the last
69334
+ // emitted check notification. Entries are dropped on signal close/cancel
69335
+ // so the map cannot grow unbounded.
69336
+ const checkThrottleMap = new Map();
69083
69337
  const unLiveSignal = signalLiveEmitter.subscribe(async (data) => {
69084
69338
  if (signal) {
69085
69339
  await NotificationLive.handleSignal(data);
69086
69340
  }
69087
69341
  });
69342
+ // Cleanup for checkThrottleMap. Tick-result emitters are NOT sufficient here:
69343
+ // out-of-band closes (commitClosePending, failed order ping, scheduled cancel
69344
+ // via commit) never reach signalBacktestEmitter/signalLiveEmitter. The
69345
+ // lifecycle channels cover every path: signalEventSubject "closed" fires for
69346
+ // all pending-signal closes (TP/SL/time_expired/user/ping), scheduleEventSubject
69347
+ // "cancelled" fires for scheduled signals removed before activation.
69348
+ const unLiveSignalEvent = signalEventSubject
69349
+ .filter(({ backtest }) => !backtest)
69350
+ .connect(async (event) => {
69351
+ if (event.action === "closed") {
69352
+ checkThrottleMap.delete(event.data.id);
69353
+ }
69354
+ });
69355
+ const unLiveScheduleEvent = scheduleEventSubject
69356
+ .filter(({ backtest }) => !backtest)
69357
+ .connect(async (event) => {
69358
+ if (event.action === "cancelled") {
69359
+ checkThrottleMap.delete(event.data.id);
69360
+ }
69361
+ });
69088
69362
  const unLivePartialProfit = partialProfitSubject
69089
69363
  .filter(({ backtest }) => !backtest)
69090
69364
  .connect(async (data) => {
@@ -69116,10 +69390,22 @@ class NotificationAdapter {
69116
69390
  const unLiveSync = syncSubject
69117
69391
  .filter(({ backtest }) => !backtest)
69118
69392
  .connect(async (data) => {
69119
- if (signal_sync) {
69393
+ if (order_sync) {
69120
69394
  await NotificationLive.handleSync(data);
69121
69395
  }
69122
69396
  });
69397
+ const unLiveCheck = syncPendingSubject
69398
+ .filter(({ backtest }) => !backtest)
69399
+ .connect(async (data) => {
69400
+ if (order_check) {
69401
+ const lastTimestamp = checkThrottleMap.get(data.signalId);
69402
+ if (lastTimestamp !== undefined && data.timestamp - lastTimestamp < GLOBAL_CONFIG.CC_NOTIFICATION_ORDER_CHECK_TTL) {
69403
+ return;
69404
+ }
69405
+ checkThrottleMap.set(data.signalId, data.timestamp);
69406
+ await NotificationLive.handleCheck(data);
69407
+ }
69408
+ });
69123
69409
  const unLiveRisk = riskSubject
69124
69410
  .filter(({ backtest }) => !backtest)
69125
69411
  .connect(async (data) => {
@@ -69149,7 +69435,7 @@ class NotificationAdapter {
69149
69435
  await NotificationLive.handleSignalNotify(data);
69150
69436
  }
69151
69437
  });
69152
- unLive = functoolsKit.compose(() => unLiveSignal(), () => unLivePartialProfit(), () => unLivePartialLoss(), () => unLiveBreakeven(), () => unLiveStrategyCommit(), () => unLiveSync(), () => unLiveRisk(), () => unLiveError(), () => unLiveExit(), () => unLiveValidation(), () => unLiveSignalNotify());
69438
+ unLive = functoolsKit.compose(() => unLiveSignal(), () => unLiveSignalEvent(), () => unLiveScheduleEvent(), () => unLivePartialProfit(), () => unLivePartialLoss(), () => unLiveBreakeven(), () => unLiveStrategyCommit(), () => unLiveSync(), () => unLiveCheck(), () => unLiveRisk(), () => unLiveError(), () => unLiveExit(), () => unLiveValidation(), () => unLiveSignalNotify(), () => checkThrottleMap.clear());
69153
69439
  }
69154
69440
  return () => {
69155
69441
  unLive();
@@ -70444,17 +70730,31 @@ const CRON_METHOD_NAME_DISPOSE = "CronUtils.dispose";
70444
70730
  /**
70445
70731
  * Watchdog timeout (ms) for a single cron handler invocation.
70446
70732
  *
70447
- * A handler that does not settle within this window is treated as failed:
70448
- * `_runEntry` races `entry.handler(info)` against this `sleep` and, when the
70449
- * timeout wins, throws into the same `catch` as any other handler error
70450
- * surfacing `failed = true`, logging a warning, and (for periodic entries)
70451
- * rolling back the watermark so the boundary is retried on the next tick.
70733
+ * A slot that does not settle within this window is treated as failed:
70734
+ * `_runEntry` races the runtime-info assembly plus `entry.handler(info)`
70735
+ * against a timer of this duration and, when the timer wins, rejects into the
70736
+ * same `catch` as any other handler error — surfacing `failed = true`, logging
70737
+ * a warning, and (for periodic entries) rolling back the watermark so the
70738
+ * boundary is retried on the next tick.
70452
70739
  *
70453
70740
  * This guards the `singlerun`-serialised tick pipeline against a handler that
70454
- * never resolves (a lost `resolve`, a hung promise with no timeout of its
70455
- * own): without it such a handler would stall every subsequent tick forever.
70741
+ * never resolves (a lost `resolve`, a hung network call with no timeout of its
70742
+ * own): without it such a handler would hold its `_inFlight` slot forever and
70743
+ * `_tick`'s `Promise.all` would never settle, silently stalling every
70744
+ * subsequent lifecycle tick while the process stays alive and outwardly
70745
+ * healthy.
70746
+ */
70747
+ const CRON_HANDLER_TIMEOUT = 900000;
70748
+ /**
70749
+ * Early-warning threshold (ms) for a single cron handler invocation.
70750
+ *
70751
+ * Purely observational: when a slot is still running this long after it was
70752
+ * opened, `_runEntry` logs a warning naming the entry — so a slow handler is
70753
+ * visible in the logs long before the {@link CRON_HANDLER_TIMEOUT} watchdog
70754
+ * forcibly fails it. Nothing is interrupted and no rollback happens at this
70755
+ * mark; the slot keeps running and may still succeed.
70456
70756
  */
70457
- const CRON_HANDLER_TIMEOUT = 120000;
70757
+ const CRON_HANDLER_WARN_TIMEOUT = 120000;
70458
70758
  /**
70459
70759
  * Local logger instance.
70460
70760
  *
@@ -70876,28 +71176,9 @@ class CronUtils {
70876
71176
  }
70877
71177
  taskList.push(pending);
70878
71178
  }
70879
- {
70880
- // Watchdog: warn (do not interrupt) if the slots this tick is awaiting
70881
- // have not settled within CRON_HANDLER_TIMEOUT. We deliberately keep
70882
- // awaiting Promise.all so the singlerun pipeline stays serialised and no
70883
- // duplicate/zombie slots are spawned — the timer only surfaces the stall.
70884
- // Use a real setTimeout/clearTimeout (not sleep) so the alarm is cancelled
70885
- // the instant Promise.all resolves, rather than lingering for the full
70886
- // timeout on every fast tick.
70887
- const timer = setTimeout(() => {
70888
- const message = `${CRON_METHOD_NAME_TICK} timed out after ${CRON_HANDLER_TIMEOUT}ms`;
70889
- const payload = { symbol, when, context };
70890
- LOGGER_SERVICE$1.warn(message, payload);
70891
- console.error(message, payload);
70892
- errorEmitter.next(new Error(message));
70893
- }, CRON_HANDLER_TIMEOUT);
70894
- try {
70895
- await Promise.all(taskList);
70896
- }
70897
- finally {
70898
- clearTimeout(timer);
70899
- }
70900
- }
71179
+ // Every slot self-terminates via the `_runEntry` watchdog, so this settles
71180
+ // within CRON_HANDLER_TIMEOUT plus epsilon even when a handler hangs.
71181
+ await Promise.all(taskList);
70901
71182
  // Roll back the watermark for any periodic slot THIS tick opened whose
70902
71183
  // handler failed, so the next tick re-opens the same boundary and retries
70903
71184
  // it — mirroring how a skipped boundary is later caught up. Restoring
@@ -71081,9 +71362,17 @@ class CronUtils {
71081
71362
  *
71082
71363
  * Assembles the {@link IRuntimeInfo} snapshot via
71083
71364
  * `RuntimeMetaService.getRuntimeInfo(symbol, context, backtest)` and invokes
71084
- * `entry.handler(info)`. Logs any error via `console.error` and **returns** a
71085
- * `failed` boolean (`true` when the handler or the runtime-info assembly
71086
- * threw) so the caller (`_tick`) can roll back the periodic watermark of the
71365
+ * `entry.handler(info)`, racing both against the
71366
+ * {@link CRON_HANDLER_TIMEOUT} watchdog a slot that does not settle in time
71367
+ * rejects into the same `catch` as any other error, so a hung handler (or a
71368
+ * hung price fetch inside `getRuntimeInfo`) can never hold the `_inFlight`
71369
+ * slot forever and stall the serialised tick pipeline. A slot still running
71370
+ * at {@link CRON_HANDLER_WARN_TIMEOUT} logs an observational warning first,
71371
+ * so a slow handler is visible in the logs well before the watchdog forcibly
71372
+ * fails it. Logs any error via
71373
+ * `console.error` and **returns** a `failed` boolean (`true` when the
71374
+ * handler — or the runtime-info assembly — threw or timed out) so the caller
71375
+ * (`_tick`) can roll back the periodic watermark of the
71087
71376
  * slot it opened and retry that boundary. The error is **not** rethrown, so a
71088
71377
  * failing handler never produces an unhandled rejection. Clears the
71089
71378
  * `_inFlight` slot in `.finally()` so the next boundary produces a fresh
@@ -71108,9 +71397,37 @@ class CronUtils {
71108
71397
  */
71109
71398
  async _runEntry(entry, symbol, alignedMs, slotKey, firedKey, backtest, context) {
71110
71399
  let failed = false;
71400
+ let watchdog;
71401
+ // Observational early warning: fires while the slot is still running, long
71402
+ // before the watchdog below forcibly fails it. Cancelled in `finally`, so
71403
+ // it never fires for slots that settle in time.
71404
+ const slowAlarm = setTimeout(() => {
71405
+ const message = `${CRON_METHOD_NAME_TICK} entry "${entry.name}" still running after ${CRON_HANDLER_WARN_TIMEOUT}ms (watchdog at ${CRON_HANDLER_TIMEOUT}ms)`;
71406
+ const payload = { symbol, alignedMs };
71407
+ LOGGER_SERVICE$1.warn(message, payload);
71408
+ console.error(message, payload);
71409
+ }, CRON_HANDLER_WARN_TIMEOUT);
71111
71410
  try {
71112
- const info = await RUNTIME_META_SERVICE.getRuntimeInfo(symbol, context, backtest);
71113
- await entry.handler(info);
71411
+ // The runtime-info assembly is raced alongside the handler: in live mode
71412
+ // getRuntimeInfo reaches out to the exchange for the current price, so it
71413
+ // can hang on a dead network exactly like a user handler can.
71414
+ const work = (async () => {
71415
+ const info = await RUNTIME_META_SERVICE.getRuntimeInfo(symbol, context, backtest);
71416
+ await entry.handler(info);
71417
+ })();
71418
+ // A timed-out slot abandons `work`; swallow its eventual rejection so a
71419
+ // zombie handler that fails later never surfaces as an unhandled
71420
+ // rejection. Its late success is equally unobserved: `failed` was already
71421
+ // reported and (for fire-once entries) the fired mark deliberately not set.
71422
+ work.catch(() => void 0);
71423
+ await Promise.race([
71424
+ work,
71425
+ new Promise((_, reject) => {
71426
+ watchdog = setTimeout(() => {
71427
+ reject(new Error(`Cron entry "${entry.name}" timed out after ${CRON_HANDLER_TIMEOUT}ms`));
71428
+ }, CRON_HANDLER_TIMEOUT);
71429
+ }),
71430
+ ]);
71114
71431
  }
71115
71432
  catch (error) {
71116
71433
  failed = true;
@@ -71126,6 +71443,8 @@ class CronUtils {
71126
71443
  errorEmitter.next(error);
71127
71444
  }
71128
71445
  finally {
71446
+ clearTimeout(slowAlarm);
71447
+ clearTimeout(watchdog);
71129
71448
  this._inFlight.delete(slotKey);
71130
71449
  if (!failed && firedKey !== null) {
71131
71450
  this._firedOnce.add(firedKey);