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.mjs CHANGED
@@ -533,6 +533,16 @@ const GLOBAL_CONFIG = {
533
533
  * Default: 500 notifications
534
534
  */
535
535
  CC_MAX_NOTIFICATIONS: 500,
536
+ /**
537
+ * Minimum interval between `order_sync.check` notifications for a single signalId.
538
+ * Order-ping events (syncPendingSubject) fire on every live tick, so the
539
+ * NotificationAdapter throttles them: a signal produces at most one check
540
+ * notification per this interval. The throttle entry is removed when the
541
+ * signal is closed or cancelled.
542
+ *
543
+ * Default: 900000 ms (15 minutes)
544
+ */
545
+ CC_NOTIFICATION_ORDER_CHECK_TTL: 900000,
536
546
  /**
537
547
  * Maximum number of signals to keep in storage.
538
548
  * Older signals are removed when this limit is exceeded.
@@ -5297,6 +5307,24 @@ const validateCandles = (candles) => {
5297
5307
  };
5298
5308
 
5299
5309
  const MS_PER_MINUTE$7 = 60000;
5310
+ /**
5311
+ * Normalizes raw adapter output to the {@link IAggregatedTradeData} contract.
5312
+ *
5313
+ * Implicit-API exchange adapters (e.g. ccxt `publicGetAggTrades`) return every
5314
+ * scalar as a string. A string `timestamp` is type-invisible at runtime — it
5315
+ * survives serialization and comparison — and only blows up deep inside a
5316
+ * downstream consumer (`Number.isFinite("1783601403565") === false`). Coercing
5317
+ * the numeric fields here, at the single point where adapter data enters the
5318
+ * framework, guarantees the contract's `number` types hold regardless of
5319
+ * adapter behaviour. `isBuyerMaker` is deliberately left untouched: `Boolean`
5320
+ * coercion of a string `"false"` would yield `true`.
5321
+ */
5322
+ const NORMALIZE_AGGREGATED_TRADES_FN$1 = (trades) => trades.map((trade) => ({
5323
+ ...trade,
5324
+ price: Number(trade.price),
5325
+ qty: Number(trade.qty),
5326
+ timestamp: Number(trade.timestamp),
5327
+ }));
5300
5328
  const INTERVAL_MINUTES$9 = {
5301
5329
  "1m": 1,
5302
5330
  "3m": 3,
@@ -5992,7 +6020,7 @@ class ClientExchange {
5992
6020
  if (limit === undefined) {
5993
6021
  const to = new Date(alignedTo);
5994
6022
  const from = new Date(alignedTo - windowMs);
5995
- return await this.params.getAggregatedTrades(symbol, from, to, this.params.execution.context.backtest);
6023
+ return NORMALIZE_AGGREGATED_TRADES_FN$1(await this.params.getAggregatedTrades(symbol, from, to, this.params.execution.context.backtest));
5996
6024
  }
5997
6025
  // With limit: paginate backwards until we have enough trades.
5998
6026
  // Consecutive empty windows bound the scan: before the symbol's listing
@@ -6009,7 +6037,7 @@ class ClientExchange {
6009
6037
  }
6010
6038
  const to = new Date(windowEnd);
6011
6039
  const from = new Date(windowStart);
6012
- const chunk = await this.params.getAggregatedTrades(symbol, from, to, this.params.execution.context.backtest);
6040
+ const chunk = NORMALIZE_AGGREGATED_TRADES_FN$1(await this.params.getAggregatedTrades(symbol, from, to, this.params.execution.context.backtest));
6013
6041
  if (chunk.length === 0) {
6014
6042
  emptyWindows += 1;
6015
6043
  if (emptyWindows >= MAX_CONSECUTIVE_EMPTY_WINDOWS) {
@@ -15356,6 +15384,10 @@ class StrategyConnectionService {
15356
15384
  });
15357
15385
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
15358
15386
  await strategy.waitForInit();
15387
+ if (!this.timeMetaService.hasTimestamp(symbol, context, backtest)) {
15388
+ const timestamp = this.executionContextService.context.when.getTime();
15389
+ await this.timeMetaService.next(symbol, timestamp, context, backtest);
15390
+ }
15359
15391
  const tick = await strategy.tick(symbol, context.strategyName);
15360
15392
  {
15361
15393
  await this.priceMetaService.next(symbol, tick.currentPrice, context, backtest);
@@ -42676,6 +42708,24 @@ const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
42676
42708
  const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
42677
42709
  const EXCHANGE_METHOD_NAME_GET_AGGREGATED_TRADES = "ExchangeUtils.getAggregatedTrades";
42678
42710
  const MS_PER_MINUTE$4 = 60000;
42711
+ /**
42712
+ * Normalizes raw adapter output to the {@link IAggregatedTradeData} contract.
42713
+ *
42714
+ * Implicit-API exchange adapters (e.g. ccxt `publicGetAggTrades`) return every
42715
+ * scalar as a string. A string `timestamp` is type-invisible at runtime — it
42716
+ * survives serialization and comparison — and only blows up deep inside a
42717
+ * downstream consumer (`Number.isFinite("1783601403565") === false`). Coercing
42718
+ * the numeric fields here, at the single point where adapter data enters the
42719
+ * framework, guarantees the contract's `number` types hold regardless of
42720
+ * adapter behaviour. `isBuyerMaker` is deliberately left untouched: `Boolean`
42721
+ * coercion of a string `"false"` would yield `true`.
42722
+ */
42723
+ const NORMALIZE_AGGREGATED_TRADES_FN = (trades) => trades.map((trade) => ({
42724
+ ...trade,
42725
+ price: Number(trade.price),
42726
+ qty: Number(trade.qty),
42727
+ timestamp: Number(trade.timestamp),
42728
+ }));
42679
42729
  /**
42680
42730
  * Gets current timestamp from execution context if available.
42681
42731
  * Returns current Date() if no execution context exists (non-trading GUI).
@@ -43172,7 +43222,7 @@ class ExchangeInstance {
43172
43222
  if (limit === undefined) {
43173
43223
  const to = new Date(alignedTo);
43174
43224
  const from = new Date(alignedTo - windowMs);
43175
- return await this._methods.getAggregatedTrades(symbol, from, to, isBacktest);
43225
+ return NORMALIZE_AGGREGATED_TRADES_FN(await this._methods.getAggregatedTrades(symbol, from, to, isBacktest));
43176
43226
  }
43177
43227
  // With limit: paginate backwards until we have enough trades.
43178
43228
  // Consecutive empty windows bound the scan: before the symbol's listing
@@ -43189,7 +43239,7 @@ class ExchangeInstance {
43189
43239
  }
43190
43240
  const to = new Date(windowEnd);
43191
43241
  const from = new Date(windowStart);
43192
- const chunk = await this._methods.getAggregatedTrades(symbol, from, to, isBacktest);
43242
+ const chunk = NORMALIZE_AGGREGATED_TRADES_FN(await this._methods.getAggregatedTrades(symbol, from, to, isBacktest));
43193
43243
  if (chunk.length === 0) {
43194
43244
  emptyWindows += 1;
43195
43245
  if (emptyWindows >= MAX_CONSECUTIVE_EMPTY_WINDOWS) {
@@ -45688,6 +45738,7 @@ class BrokerAdapter {
45688
45738
  exchangeName: event.exchangeName,
45689
45739
  frameName: event.frameName,
45690
45740
  },
45741
+ when: new Date(event.timestamp),
45691
45742
  backtest: event.backtest,
45692
45743
  });
45693
45744
  });
@@ -45714,6 +45765,7 @@ class BrokerAdapter {
45714
45765
  exchangeName: event.exchangeName,
45715
45766
  frameName: event.frameName,
45716
45767
  },
45768
+ when: new Date(event.timestamp),
45717
45769
  backtest: event.backtest,
45718
45770
  });
45719
45771
  });
@@ -45737,6 +45789,7 @@ class BrokerAdapter {
45737
45789
  exchangeName: event.exchangeName,
45738
45790
  frameName: event.frameName,
45739
45791
  },
45792
+ when: new Date(event.timestamp),
45740
45793
  backtest: event.backtest,
45741
45794
  });
45742
45795
  });
@@ -45755,6 +45808,7 @@ class BrokerAdapter {
45755
45808
  exchangeName: event.exchangeName,
45756
45809
  frameName: event.frameName,
45757
45810
  },
45811
+ when: new Date(event.timestamp),
45758
45812
  backtest: event.backtest,
45759
45813
  });
45760
45814
  });
@@ -45772,6 +45826,7 @@ class BrokerAdapter {
45772
45826
  exchangeName: event.exchangeName,
45773
45827
  frameName: event.frameName,
45774
45828
  },
45829
+ when: new Date(event.timestamp),
45775
45830
  backtest: event.backtest,
45776
45831
  });
45777
45832
  });
@@ -45784,6 +45839,7 @@ class BrokerAdapter {
45784
45839
  exchangeName: event.exchangeName,
45785
45840
  frameName: event.frameName,
45786
45841
  },
45842
+ when: new Date(event.timestamp),
45787
45843
  backtest: event.backtest,
45788
45844
  });
45789
45845
  });
@@ -45801,6 +45857,7 @@ class BrokerAdapter {
45801
45857
  exchangeName: event.exchangeName,
45802
45858
  frameName: event.frameName,
45803
45859
  },
45860
+ when: new Date(event.timestamp),
45804
45861
  backtest: event.backtest,
45805
45862
  };
45806
45863
  if (event.action === "scheduled") {
@@ -45823,6 +45880,7 @@ class BrokerAdapter {
45823
45880
  exchangeName: event.exchangeName,
45824
45881
  frameName: event.frameName,
45825
45882
  },
45883
+ when: new Date(event.timestamp),
45826
45884
  backtest: event.backtest,
45827
45885
  };
45828
45886
  if (event.action === "opened") {
@@ -46597,6 +46655,7 @@ async function commitPartialProfit(symbol, percentToClose) {
46597
46655
  priceTakeProfit: signalForProfit.priceTakeProfit,
46598
46656
  priceStopLoss: signalForProfit.priceStopLoss,
46599
46657
  context: { exchangeName, frameName, strategyName },
46658
+ when: backtest.executionContextService.context.when,
46600
46659
  backtest: isBacktest,
46601
46660
  });
46602
46661
  return await backtest.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
@@ -46665,6 +46724,7 @@ async function commitPartialLoss(symbol, percentToClose) {
46665
46724
  priceTakeProfit: signalForLoss.priceTakeProfit,
46666
46725
  priceStopLoss: signalForLoss.priceStopLoss,
46667
46726
  context: { exchangeName, frameName, strategyName },
46727
+ when: backtest.executionContextService.context.when,
46668
46728
  backtest: isBacktest,
46669
46729
  });
46670
46730
  return await backtest.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
@@ -46749,6 +46809,7 @@ async function commitTrailingStop(symbol, percentShift, currentPrice) {
46749
46809
  takeProfitPrice: signal.priceTakeProfit,
46750
46810
  position: signal.position,
46751
46811
  context: { exchangeName, frameName, strategyName },
46812
+ when: backtest.executionContextService.context.when,
46752
46813
  backtest: isBacktest,
46753
46814
  });
46754
46815
  return await backtest.strategyCoreService.trailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
@@ -46833,6 +46894,7 @@ async function commitTrailingTake(symbol, percentShift, currentPrice) {
46833
46894
  takeProfitPrice: signal.priceTakeProfit,
46834
46895
  position: signal.position,
46835
46896
  context: { exchangeName, frameName, strategyName },
46897
+ when: backtest.executionContextService.context.when,
46836
46898
  backtest: isBacktest,
46837
46899
  });
46838
46900
  return await backtest.strategyCoreService.trailingTake(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
@@ -46888,6 +46950,7 @@ async function commitTrailingStopCost(symbol, newStopLossPrice) {
46888
46950
  position: signal.position,
46889
46951
  takeProfitPrice: signal.priceTakeProfit,
46890
46952
  context: { exchangeName, frameName, strategyName },
46953
+ when: backtest.executionContextService.context.when,
46891
46954
  backtest: isBacktest,
46892
46955
  });
46893
46956
  return await backtest.strategyCoreService.trailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
@@ -46943,6 +47006,7 @@ async function commitTrailingTakeCost(symbol, newTakeProfitPrice) {
46943
47006
  takeProfitPrice: signal.priceTakeProfit,
46944
47007
  position: signal.position,
46945
47008
  context: { exchangeName, frameName, strategyName },
47009
+ when: backtest.executionContextService.context.when,
46946
47010
  backtest: isBacktest,
46947
47011
  });
46948
47012
  return await backtest.strategyCoreService.trailingTake(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
@@ -47004,6 +47068,7 @@ async function commitBreakeven(symbol) {
47004
47068
  newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
47005
47069
  position: signal.position,
47006
47070
  context: { exchangeName, frameName, strategyName },
47071
+ when: backtest.executionContextService.context.when,
47007
47072
  backtest: isBacktest,
47008
47073
  });
47009
47074
  return await backtest.strategyCoreService.breakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
@@ -47096,6 +47161,7 @@ async function commitAverageBuy(symbol, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_C
47096
47161
  priceTakeProfit: signalForAvgBuy.priceTakeProfit,
47097
47162
  priceStopLoss: signalForAvgBuy.priceStopLoss,
47098
47163
  context: { exchangeName, frameName, strategyName },
47164
+ when: backtest.executionContextService.context.when,
47099
47165
  backtest: isBacktest,
47100
47166
  });
47101
47167
  return await backtest.strategyCoreService.averageBuy(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName }, cost);
@@ -47532,6 +47598,7 @@ async function commitPartialProfitCost(symbol, dollarAmount) {
47532
47598
  priceTakeProfit: signalForProfitCost.priceTakeProfit,
47533
47599
  priceStopLoss: signalForProfitCost.priceStopLoss,
47534
47600
  context: { exchangeName, frameName, strategyName },
47601
+ when: backtest.executionContextService.context.when,
47535
47602
  backtest: isBacktest,
47536
47603
  });
47537
47604
  return await backtest.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
@@ -47604,6 +47671,7 @@ async function commitPartialLossCost(symbol, dollarAmount) {
47604
47671
  priceTakeProfit: signalForLossCost.priceTakeProfit,
47605
47672
  priceStopLoss: signalForLossCost.priceStopLoss,
47606
47673
  context: { exchangeName, frameName, strategyName },
47674
+ when: backtest.executionContextService.context.when,
47607
47675
  backtest: isBacktest,
47608
47676
  });
47609
47677
  return await backtest.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
@@ -52518,6 +52586,7 @@ class BacktestUtils {
52518
52586
  priceTakeProfit: signalForProfit.priceTakeProfit,
52519
52587
  priceStopLoss: signalForProfit.priceStopLoss,
52520
52588
  context,
52589
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
52521
52590
  backtest: true,
52522
52591
  });
52523
52592
  return await backtest.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
@@ -52592,6 +52661,7 @@ class BacktestUtils {
52592
52661
  priceTakeProfit: signalForLoss.priceTakeProfit,
52593
52662
  priceStopLoss: signalForLoss.priceStopLoss,
52594
52663
  context,
52664
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
52595
52665
  backtest: true,
52596
52666
  });
52597
52667
  return await backtest.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
@@ -52669,6 +52739,7 @@ class BacktestUtils {
52669
52739
  priceTakeProfit: signalForProfitCost.priceTakeProfit,
52670
52740
  priceStopLoss: signalForProfitCost.priceStopLoss,
52671
52741
  context,
52742
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
52672
52743
  backtest: true,
52673
52744
  });
52674
52745
  return await backtest.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
@@ -52746,6 +52817,7 @@ class BacktestUtils {
52746
52817
  priceTakeProfit: signalForLossCost.priceTakeProfit,
52747
52818
  priceStopLoss: signalForLossCost.priceStopLoss,
52748
52819
  context,
52820
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
52749
52821
  backtest: true,
52750
52822
  });
52751
52823
  return await backtest.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
@@ -52835,6 +52907,7 @@ class BacktestUtils {
52835
52907
  takeProfitPrice: signal.priceTakeProfit,
52836
52908
  position: signal.position,
52837
52909
  context,
52910
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
52838
52911
  backtest: true,
52839
52912
  });
52840
52913
  return await backtest.strategyCoreService.trailingStop(true, symbol, percentShift, currentPrice, context);
@@ -52924,6 +52997,7 @@ class BacktestUtils {
52924
52997
  takeProfitPrice: signal.priceTakeProfit,
52925
52998
  position: signal.position,
52926
52999
  context,
53000
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
52927
53001
  backtest: true,
52928
53002
  });
52929
53003
  return await backtest.strategyCoreService.trailingTake(true, symbol, percentShift, currentPrice, context);
@@ -52979,6 +53053,7 @@ class BacktestUtils {
52979
53053
  position: signal.position,
52980
53054
  takeProfitPrice: signal.priceTakeProfit,
52981
53055
  context,
53056
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
52982
53057
  backtest: true,
52983
53058
  });
52984
53059
  return await backtest.strategyCoreService.trailingStop(true, symbol, percentShift, currentPrice, context);
@@ -53034,6 +53109,7 @@ class BacktestUtils {
53034
53109
  position: signal.position,
53035
53110
  takeProfitPrice: signal.priceTakeProfit,
53036
53111
  context,
53112
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
53037
53113
  backtest: true,
53038
53114
  });
53039
53115
  return await backtest.strategyCoreService.trailingTake(true, symbol, percentShift, currentPrice, context);
@@ -53095,6 +53171,7 @@ class BacktestUtils {
53095
53171
  newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
53096
53172
  position: signal.position,
53097
53173
  context,
53174
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
53098
53175
  backtest: true,
53099
53176
  });
53100
53177
  return await backtest.strategyCoreService.breakeven(true, symbol, currentPrice, context);
@@ -53196,6 +53273,7 @@ class BacktestUtils {
53196
53273
  priceTakeProfit: signalForAvgBuy.priceTakeProfit,
53197
53274
  priceStopLoss: signalForAvgBuy.priceStopLoss,
53198
53275
  context,
53276
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, context, true)),
53199
53277
  backtest: true,
53200
53278
  });
53201
53279
  return await backtest.strategyCoreService.averageBuy(true, symbol, currentPrice, context, cost);
@@ -55527,6 +55605,7 @@ class LiveUtils {
55527
55605
  exchangeName: context.exchangeName,
55528
55606
  frameName: "",
55529
55607
  },
55608
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
55530
55609
  backtest: false,
55531
55610
  });
55532
55611
  return await backtest.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
@@ -55622,6 +55701,7 @@ class LiveUtils {
55622
55701
  exchangeName: context.exchangeName,
55623
55702
  frameName: "",
55624
55703
  },
55704
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
55625
55705
  backtest: false,
55626
55706
  });
55627
55707
  return await backtest.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
@@ -55719,6 +55799,7 @@ class LiveUtils {
55719
55799
  exchangeName: context.exchangeName,
55720
55800
  frameName: "",
55721
55801
  },
55802
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
55722
55803
  backtest: false,
55723
55804
  });
55724
55805
  return await backtest.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
@@ -55816,6 +55897,7 @@ class LiveUtils {
55816
55897
  exchangeName: context.exchangeName,
55817
55898
  frameName: "",
55818
55899
  },
55900
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
55819
55901
  backtest: false,
55820
55902
  });
55821
55903
  return await backtest.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
@@ -55920,6 +56002,7 @@ class LiveUtils {
55920
56002
  takeProfitPrice: signal.priceTakeProfit,
55921
56003
  position: signal.position,
55922
56004
  context,
56005
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
55923
56006
  backtest: false,
55924
56007
  });
55925
56008
  return await backtest.strategyCoreService.trailingStop(false, symbol, percentShift, currentPrice, {
@@ -56024,6 +56107,7 @@ class LiveUtils {
56024
56107
  takeProfitPrice: signal.priceTakeProfit,
56025
56108
  position: signal.position,
56026
56109
  context,
56110
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
56027
56111
  backtest: false,
56028
56112
  });
56029
56113
  return await backtest.strategyCoreService.trailingTake(false, symbol, percentShift, currentPrice, {
@@ -56095,6 +56179,7 @@ class LiveUtils {
56095
56179
  takeProfitPrice: signal.priceTakeProfit,
56096
56180
  position: signal.position,
56097
56181
  context,
56182
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
56098
56183
  backtest: false,
56099
56184
  });
56100
56185
  return await backtest.strategyCoreService.trailingStop(false, symbol, percentShift, currentPrice, {
@@ -56166,6 +56251,7 @@ class LiveUtils {
56166
56251
  takeProfitPrice: signal.priceTakeProfit,
56167
56252
  position: signal.position,
56168
56253
  context,
56254
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
56169
56255
  backtest: false,
56170
56256
  });
56171
56257
  return await backtest.strategyCoreService.trailingTake(false, symbol, percentShift, currentPrice, {
@@ -56247,6 +56333,7 @@ class LiveUtils {
56247
56333
  exchangeName: context.exchangeName,
56248
56334
  frameName: "",
56249
56335
  },
56336
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
56250
56337
  backtest: false,
56251
56338
  });
56252
56339
  return await backtest.strategyCoreService.breakeven(false, symbol, currentPrice, {
@@ -56366,6 +56453,7 @@ class LiveUtils {
56366
56453
  exchangeName: context.exchangeName,
56367
56454
  frameName: "",
56368
56455
  },
56456
+ when: new Date(await backtest.timeMetaService.getTimestamp(symbol, { strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "" }, false)),
56369
56457
  backtest: false,
56370
56458
  });
56371
56459
  return await backtest.strategyCoreService.averageBuy(false, symbol, currentPrice, {
@@ -66661,7 +66749,8 @@ const WILDCARD_TARGET = {
66661
66749
  partial_loss: true,
66662
66750
  breakeven: true,
66663
66751
  strategy_commit: true,
66664
- signal_sync: true,
66752
+ order_sync: true,
66753
+ order_check: true,
66665
66754
  risk: true,
66666
66755
  info: true,
66667
66756
  common_error: true,
@@ -67390,14 +67479,15 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
67390
67479
  };
67391
67480
  /**
67392
67481
  * Creates a notification model for signal sync events.
67393
- * Handles signal-open (limit order filled) and signal-close (position exited) actions.
67482
+ * Handles signal-open (position order filled with orderType "active", or resting
67483
+ * entry order placed with orderType "schedule") and signal-close (position exited) actions.
67394
67484
  * @param data - The signal sync contract data
67395
67485
  * @returns NotificationModel for signal sync event
67396
67486
  */
67397
67487
  const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
67398
67488
  if (data.action === "signal-open") {
67399
67489
  return {
67400
- type: "signal_sync.open",
67490
+ type: "order_sync.open",
67401
67491
  id: CREATE_KEY_FN$2(),
67402
67492
  timestamp: data.timestamp,
67403
67493
  backtest: data.backtest,
@@ -67405,6 +67495,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
67405
67495
  strategyName: data.strategyName,
67406
67496
  exchangeName: data.exchangeName,
67407
67497
  signalId: data.signalId,
67498
+ orderType: data.type,
67408
67499
  currentPrice: data.currentPrice,
67409
67500
  pnl: data.signal.pnl,
67410
67501
  maxDrawdown: data.signal.maxDrawdown,
@@ -67442,7 +67533,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
67442
67533
  }
67443
67534
  if (data.action === "signal-close") {
67444
67535
  return {
67445
- type: "signal_sync.close",
67536
+ type: "order_sync.close",
67446
67537
  id: CREATE_KEY_FN$2(),
67447
67538
  timestamp: data.timestamp,
67448
67539
  backtest: data.backtest,
@@ -67450,6 +67541,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
67450
67541
  strategyName: data.strategyName,
67451
67542
  exchangeName: data.exchangeName,
67452
67543
  signalId: data.signalId,
67544
+ orderType: data.type,
67453
67545
  currentPrice: data.currentPrice,
67454
67546
  pnl: data.signal.pnl,
67455
67547
  maxDrawdown: data.signal.maxDrawdown,
@@ -67487,6 +67579,54 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
67487
67579
  }
67488
67580
  throw new Error(`Unrecognized signal sync action: ${get(data, "action")}`);
67489
67581
  };
67582
+ /**
67583
+ * Creates a notification model for order-ping check events.
67584
+ * @param data - The order check contract data
67585
+ * @returns NotificationModel for signal sync check event
67586
+ */
67587
+ const CREATE_ORDER_CHECK_NOTIFICATION_FN = (data) => ({
67588
+ type: "order_sync.check",
67589
+ id: CREATE_KEY_FN$2(),
67590
+ timestamp: data.timestamp,
67591
+ backtest: data.backtest,
67592
+ symbol: data.symbol,
67593
+ strategyName: data.strategyName,
67594
+ exchangeName: data.exchangeName,
67595
+ signalId: data.signalId,
67596
+ orderType: data.type,
67597
+ currentPrice: data.currentPrice,
67598
+ position: data.position,
67599
+ priceOpen: data.priceOpen,
67600
+ priceTakeProfit: data.priceTakeProfit,
67601
+ priceStopLoss: data.priceStopLoss,
67602
+ originalPriceTakeProfit: data.originalPriceTakeProfit,
67603
+ originalPriceStopLoss: data.originalPriceStopLoss,
67604
+ originalPriceOpen: data.originalPriceOpen,
67605
+ totalEntries: data.totalEntries,
67606
+ totalPartials: data.totalPartials,
67607
+ pnl: data.pnl,
67608
+ maxDrawdown: data.maxDrawdown,
67609
+ peakProfit: data.peakProfit,
67610
+ pnlPercentage: data.pnl.pnlPercentage,
67611
+ pnlPriceOpen: data.pnl.priceOpen,
67612
+ pnlPriceClose: data.pnl.priceClose,
67613
+ pnlCost: data.pnl.pnlCost,
67614
+ pnlEntries: data.pnl.pnlEntries,
67615
+ peakProfitPriceOpen: data.peakProfit.priceOpen,
67616
+ peakProfitPriceClose: data.peakProfit.priceClose,
67617
+ peakProfitPercentage: data.peakProfit.pnlPercentage,
67618
+ peakProfitCost: data.peakProfit.pnlCost,
67619
+ peakProfitEntries: data.peakProfit.pnlEntries,
67620
+ maxDrawdownPriceOpen: data.maxDrawdown.priceOpen,
67621
+ maxDrawdownPriceClose: data.maxDrawdown.priceClose,
67622
+ maxDrawdownPercentage: data.maxDrawdown.pnlPercentage,
67623
+ maxDrawdownCost: data.maxDrawdown.pnlCost,
67624
+ maxDrawdownEntries: data.maxDrawdown.pnlEntries,
67625
+ scheduledAt: data.scheduledAt,
67626
+ pendingAt: data.pendingAt,
67627
+ note: data.signal.note,
67628
+ createdAt: data.timestamp,
67629
+ });
67490
67630
  /**
67491
67631
  * Creates a notification model for risk rejection events.
67492
67632
  * @param data - The risk contract data
@@ -67604,6 +67744,7 @@ const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_PARTIAL_LOSS = "Notificati
67604
67744
  const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_BREAKEVEN = "NotificationMemoryBacktestUtils.handleBreakeven";
67605
67745
  const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_STRATEGY_COMMIT = "NotificationMemoryBacktestUtils.handleStrategyCommit";
67606
67746
  const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_SYNC = "NotificationMemoryBacktestUtils.handleSync";
67747
+ const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_CHECK = "NotificationMemoryBacktestUtils.handleCheck";
67607
67748
  const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_RISK = "NotificationMemoryBacktestUtils.handleRisk";
67608
67749
  const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_ERROR = "NotificationMemoryBacktestUtils.handleError";
67609
67750
  const NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_CRITICAL_ERROR = "NotificationMemoryBacktestUtils.handleCriticalError";
@@ -67617,6 +67758,7 @@ const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_PARTIAL_LOSS = "NotificationMe
67617
67758
  const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_BREAKEVEN = "NotificationMemoryLiveUtils.handleBreakeven";
67618
67759
  const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_STRATEGY_COMMIT = "NotificationMemoryLiveUtils.handleStrategyCommit";
67619
67760
  const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_SYNC = "NotificationMemoryLiveUtils.handleSync";
67761
+ const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_CHECK = "NotificationMemoryLiveUtils.handleCheck";
67620
67762
  const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_RISK = "NotificationMemoryLiveUtils.handleRisk";
67621
67763
  const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_ERROR = "NotificationMemoryLiveUtils.handleError";
67622
67764
  const NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_CRITICAL_ERROR = "NotificationMemoryLiveUtils.handleCriticalError";
@@ -67646,6 +67788,7 @@ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_PARTIAL_LOSS = "Notificat
67646
67788
  const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_BREAKEVEN = "NotificationPersistBacktestUtils.handleBreakeven";
67647
67789
  const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_STRATEGY_COMMIT = "NotificationPersistBacktestUtils.handleStrategyCommit";
67648
67790
  const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_SYNC = "NotificationPersistBacktestUtils.handleSync";
67791
+ const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_CHECK = "NotificationPersistBacktestUtils.handleCheck";
67649
67792
  const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_RISK = "NotificationPersistBacktestUtils.handleRisk";
67650
67793
  const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_ERROR = "NotificationPersistBacktestUtils.handleError";
67651
67794
  const NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_CRITICAL_ERROR = "NotificationPersistBacktestUtils.handleCriticalError";
@@ -67661,6 +67804,7 @@ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_PARTIAL_LOSS = "NotificationP
67661
67804
  const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_BREAKEVEN = "NotificationPersistLiveUtils.handleBreakeven";
67662
67805
  const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_STRATEGY_COMMIT = "NotificationPersistLiveUtils.handleStrategyCommit";
67663
67806
  const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_SYNC = "NotificationPersistLiveUtils.handleSync";
67807
+ const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_CHECK = "NotificationPersistLiveUtils.handleCheck";
67664
67808
  const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_RISK = "NotificationPersistLiveUtils.handleRisk";
67665
67809
  const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_ERROR = "NotificationPersistLiveUtils.handleError";
67666
67810
  const NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_CRITICAL_ERROR = "NotificationPersistLiveUtils.handleCriticalError";
@@ -67759,15 +67903,23 @@ class NotificationMemoryBacktestUtils {
67759
67903
  signalId: data.signalId,
67760
67904
  action: data.action,
67761
67905
  });
67762
- // A "schedule" open is a resting-order placement, not a fill:
67763
- // signal_sync.open means "limit order confirmed filled"
67764
- if (data.action === "signal-open" && data.type === "schedule") {
67765
- return;
67766
- }
67767
67906
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
67768
67907
  }, {
67769
67908
  defaultValue: null,
67770
67909
  });
67910
+ /**
67911
+ * Handles order-ping check event.
67912
+ * @param data - The order check contract data
67913
+ */
67914
+ this.handleCheck = trycatch(async (data) => {
67915
+ backtest.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_CHECK, {
67916
+ signalId: data.signalId,
67917
+ type: data.type,
67918
+ });
67919
+ this._addNotification(CREATE_ORDER_CHECK_NOTIFICATION_FN(data));
67920
+ }, {
67921
+ defaultValue: null,
67922
+ });
67771
67923
  /**
67772
67924
  * Handles risk rejection event.
67773
67925
  * @param data - The risk contract data
@@ -67882,6 +68034,13 @@ class NotificationDummyBacktestUtils {
67882
68034
  }, {
67883
68035
  defaultValue: null,
67884
68036
  });
68037
+ /**
68038
+ * No-op handler for order-ping check event.
68039
+ */
68040
+ this.handleCheck = trycatch(async () => {
68041
+ }, {
68042
+ defaultValue: null,
68043
+ });
67885
68044
  /**
67886
68045
  * No-op handler for risk rejection event.
67887
68046
  */
@@ -68033,17 +68192,27 @@ class NotificationPersistBacktestUtils {
68033
68192
  signalId: data.signalId,
68034
68193
  action: data.action,
68035
68194
  });
68036
- // A "schedule" open is a resting-order placement, not a fill:
68037
- // signal_sync.open means "limit order confirmed filled"
68038
- if (data.action === "signal-open" && data.type === "schedule") {
68039
- return;
68040
- }
68041
68195
  await this.waitForInit();
68042
68196
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
68043
68197
  await this._updateNotifications();
68044
68198
  }, {
68045
68199
  defaultValue: null,
68046
68200
  });
68201
+ /**
68202
+ * Handles order-ping check event.
68203
+ * @param data - The order check contract data
68204
+ */
68205
+ this.handleCheck = trycatch(async (data) => {
68206
+ backtest.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_CHECK, {
68207
+ signalId: data.signalId,
68208
+ type: data.type,
68209
+ });
68210
+ await this.waitForInit();
68211
+ this._addNotification(CREATE_ORDER_CHECK_NOTIFICATION_FN(data));
68212
+ await this._updateNotifications();
68213
+ }, {
68214
+ defaultValue: null,
68215
+ });
68047
68216
  /**
68048
68217
  * Handles risk rejection event.
68049
68218
  * @param data - The risk contract data
@@ -68237,15 +68406,23 @@ class NotificationMemoryLiveUtils {
68237
68406
  signalId: data.signalId,
68238
68407
  action: data.action,
68239
68408
  });
68240
- // A "schedule" open is a resting-order placement, not a fill:
68241
- // signal_sync.open means "limit order confirmed filled"
68242
- if (data.action === "signal-open" && data.type === "schedule") {
68243
- return;
68244
- }
68245
68409
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
68246
68410
  }, {
68247
68411
  defaultValue: null,
68248
68412
  });
68413
+ /**
68414
+ * Handles order-ping check event.
68415
+ * @param data - The order check contract data
68416
+ */
68417
+ this.handleCheck = trycatch(async (data) => {
68418
+ backtest.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_CHECK, {
68419
+ signalId: data.signalId,
68420
+ type: data.type,
68421
+ });
68422
+ this._addNotification(CREATE_ORDER_CHECK_NOTIFICATION_FN(data));
68423
+ }, {
68424
+ defaultValue: null,
68425
+ });
68249
68426
  /**
68250
68427
  * Handles risk rejection event.
68251
68428
  * @param data - The risk contract data
@@ -68360,6 +68537,13 @@ class NotificationDummyLiveUtils {
68360
68537
  }, {
68361
68538
  defaultValue: null,
68362
68539
  });
68540
+ /**
68541
+ * No-op handler for order-ping check event.
68542
+ */
68543
+ this.handleCheck = trycatch(async () => {
68544
+ }, {
68545
+ defaultValue: null,
68546
+ });
68363
68547
  /**
68364
68548
  * No-op handler for risk rejection event.
68365
68549
  */
@@ -68512,17 +68696,27 @@ class NotificationPersistLiveUtils {
68512
68696
  signalId: data.signalId,
68513
68697
  action: data.action,
68514
68698
  });
68515
- // A "schedule" open is a resting-order placement, not a fill:
68516
- // signal_sync.open means "limit order confirmed filled"
68517
- if (data.action === "signal-open" && data.type === "schedule") {
68518
- return;
68519
- }
68520
68699
  await this.waitForInit();
68521
68700
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
68522
68701
  await this._updateNotifications();
68523
68702
  }, {
68524
68703
  defaultValue: null,
68525
68704
  });
68705
+ /**
68706
+ * Handles order-ping check event.
68707
+ * @param data - The order check contract data
68708
+ */
68709
+ this.handleCheck = trycatch(async (data) => {
68710
+ backtest.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_CHECK, {
68711
+ signalId: data.signalId,
68712
+ type: data.type,
68713
+ });
68714
+ await this.waitForInit();
68715
+ this._addNotification(CREATE_ORDER_CHECK_NOTIFICATION_FN(data));
68716
+ await this._updateNotifications();
68717
+ }, {
68718
+ defaultValue: null,
68719
+ });
68526
68720
  /**
68527
68721
  * Handles risk rejection event.
68528
68722
  * @param data - The risk contract data
@@ -68700,6 +68894,16 @@ class NotificationBacktestAdapter {
68700
68894
  }, {
68701
68895
  defaultValue: null,
68702
68896
  });
68897
+ /**
68898
+ * Handles order-ping check event.
68899
+ * Proxies call to the underlying notification adapter.
68900
+ * @param data - The order check contract data
68901
+ */
68902
+ this.handleCheck = trycatch(async (data) => {
68903
+ return await this.getInstance().handleCheck(data);
68904
+ }, {
68905
+ defaultValue: null,
68906
+ });
68703
68907
  /**
68704
68908
  * Handles risk rejection event.
68705
68909
  * Proxies call to the underlying notification adapter.
@@ -68870,6 +69074,16 @@ class NotificationLiveAdapter {
68870
69074
  }, {
68871
69075
  defaultValue: null,
68872
69076
  });
69077
+ /**
69078
+ * Handles order-ping check event.
69079
+ * Proxies call to the underlying notification adapter.
69080
+ * @param data - The order check contract data
69081
+ */
69082
+ this.handleCheck = trycatch(async (data) => {
69083
+ return await this.getInstance().handleCheck(data);
69084
+ }, {
69085
+ defaultValue: null,
69086
+ });
68873
69087
  /**
68874
69088
  * Handles risk rejection event.
68875
69089
  * Proxies call to the underlying notification adapter.
@@ -68983,16 +69197,40 @@ class NotificationAdapter {
68983
69197
  *
68984
69198
  * @returns Cleanup function that unsubscribes from all emitters
68985
69199
  */
68986
- this.enable = 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) => {
69200
+ this.enable = 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) => {
68987
69201
  backtest.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_ENABLE);
68988
69202
  let unLive;
68989
69203
  let unBacktest;
68990
69204
  {
69205
+ // Throttle state for order_sync.check: signalId -> timestamp of the last
69206
+ // emitted check notification. Entries are dropped on signal close/cancel
69207
+ // so the map cannot grow unbounded.
69208
+ const checkThrottleMap = new Map();
68991
69209
  const unBacktestSignal = signalBacktestEmitter.subscribe(async (data) => {
68992
69210
  if (signal) {
68993
69211
  await NotificationBacktest.handleSignal(data);
68994
69212
  }
68995
69213
  });
69214
+ // Cleanup for checkThrottleMap. Tick-result emitters are NOT sufficient here:
69215
+ // out-of-band closes (commitClosePending, failed order ping, scheduled cancel
69216
+ // via commit) never reach signalBacktestEmitter/signalLiveEmitter. The
69217
+ // lifecycle channels cover every path: signalEventSubject "closed" fires for
69218
+ // all pending-signal closes (TP/SL/time_expired/user/ping), scheduleEventSubject
69219
+ // "cancelled" fires for scheduled signals removed before activation.
69220
+ const unBacktestSignalEvent = signalEventSubject
69221
+ .filter(({ backtest }) => backtest)
69222
+ .connect(async (event) => {
69223
+ if (event.action === "closed") {
69224
+ checkThrottleMap.delete(event.data.id);
69225
+ }
69226
+ });
69227
+ const unBacktestScheduleEvent = scheduleEventSubject
69228
+ .filter(({ backtest }) => backtest)
69229
+ .connect(async (event) => {
69230
+ if (event.action === "cancelled") {
69231
+ checkThrottleMap.delete(event.data.id);
69232
+ }
69233
+ });
68996
69234
  const unBacktestPartialProfit = partialProfitSubject
68997
69235
  .filter(({ backtest }) => backtest)
68998
69236
  .connect(async (data) => {
@@ -69024,10 +69262,22 @@ class NotificationAdapter {
69024
69262
  const unBacktestSync = syncSubject
69025
69263
  .filter(({ backtest }) => backtest)
69026
69264
  .connect(async (data) => {
69027
- if (signal_sync) {
69265
+ if (order_sync) {
69028
69266
  await NotificationBacktest.handleSync(data);
69029
69267
  }
69030
69268
  });
69269
+ const unBacktestCheck = syncPendingSubject
69270
+ .filter(({ backtest }) => backtest)
69271
+ .connect(async (data) => {
69272
+ if (order_check) {
69273
+ const lastTimestamp = checkThrottleMap.get(data.signalId);
69274
+ if (lastTimestamp !== undefined && data.timestamp - lastTimestamp < GLOBAL_CONFIG.CC_NOTIFICATION_ORDER_CHECK_TTL) {
69275
+ return;
69276
+ }
69277
+ checkThrottleMap.set(data.signalId, data.timestamp);
69278
+ await NotificationBacktest.handleCheck(data);
69279
+ }
69280
+ });
69031
69281
  const unBacktestRisk = riskSubject
69032
69282
  .filter(({ backtest }) => backtest)
69033
69283
  .connect(async (data) => {
@@ -69057,14 +69307,38 @@ class NotificationAdapter {
69057
69307
  await NotificationBacktest.handleSignalNotify(data);
69058
69308
  }
69059
69309
  });
69060
- unBacktest = compose(() => unBacktestSignal(), () => unBacktestPartialProfit(), () => unBacktestPartialLoss(), () => unBacktestBreakeven(), () => unBacktestStrategyCommit(), () => unBacktestSync(), () => unBacktestRisk(), () => unBacktestError(), () => unBacktestExit(), () => unBacktestValidation(), () => unBacktestSignalNotify());
69310
+ unBacktest = compose(() => unBacktestSignal(), () => unBacktestSignalEvent(), () => unBacktestScheduleEvent(), () => unBacktestPartialProfit(), () => unBacktestPartialLoss(), () => unBacktestBreakeven(), () => unBacktestStrategyCommit(), () => unBacktestSync(), () => unBacktestCheck(), () => unBacktestRisk(), () => unBacktestError(), () => unBacktestExit(), () => unBacktestValidation(), () => unBacktestSignalNotify(), () => checkThrottleMap.clear());
69061
69311
  }
69062
69312
  {
69313
+ // Throttle state for order_sync.check: signalId -> timestamp of the last
69314
+ // emitted check notification. Entries are dropped on signal close/cancel
69315
+ // so the map cannot grow unbounded.
69316
+ const checkThrottleMap = new Map();
69063
69317
  const unLiveSignal = signalLiveEmitter.subscribe(async (data) => {
69064
69318
  if (signal) {
69065
69319
  await NotificationLive.handleSignal(data);
69066
69320
  }
69067
69321
  });
69322
+ // Cleanup for checkThrottleMap. Tick-result emitters are NOT sufficient here:
69323
+ // out-of-band closes (commitClosePending, failed order ping, scheduled cancel
69324
+ // via commit) never reach signalBacktestEmitter/signalLiveEmitter. The
69325
+ // lifecycle channels cover every path: signalEventSubject "closed" fires for
69326
+ // all pending-signal closes (TP/SL/time_expired/user/ping), scheduleEventSubject
69327
+ // "cancelled" fires for scheduled signals removed before activation.
69328
+ const unLiveSignalEvent = signalEventSubject
69329
+ .filter(({ backtest }) => !backtest)
69330
+ .connect(async (event) => {
69331
+ if (event.action === "closed") {
69332
+ checkThrottleMap.delete(event.data.id);
69333
+ }
69334
+ });
69335
+ const unLiveScheduleEvent = scheduleEventSubject
69336
+ .filter(({ backtest }) => !backtest)
69337
+ .connect(async (event) => {
69338
+ if (event.action === "cancelled") {
69339
+ checkThrottleMap.delete(event.data.id);
69340
+ }
69341
+ });
69068
69342
  const unLivePartialProfit = partialProfitSubject
69069
69343
  .filter(({ backtest }) => !backtest)
69070
69344
  .connect(async (data) => {
@@ -69096,10 +69370,22 @@ class NotificationAdapter {
69096
69370
  const unLiveSync = syncSubject
69097
69371
  .filter(({ backtest }) => !backtest)
69098
69372
  .connect(async (data) => {
69099
- if (signal_sync) {
69373
+ if (order_sync) {
69100
69374
  await NotificationLive.handleSync(data);
69101
69375
  }
69102
69376
  });
69377
+ const unLiveCheck = syncPendingSubject
69378
+ .filter(({ backtest }) => !backtest)
69379
+ .connect(async (data) => {
69380
+ if (order_check) {
69381
+ const lastTimestamp = checkThrottleMap.get(data.signalId);
69382
+ if (lastTimestamp !== undefined && data.timestamp - lastTimestamp < GLOBAL_CONFIG.CC_NOTIFICATION_ORDER_CHECK_TTL) {
69383
+ return;
69384
+ }
69385
+ checkThrottleMap.set(data.signalId, data.timestamp);
69386
+ await NotificationLive.handleCheck(data);
69387
+ }
69388
+ });
69103
69389
  const unLiveRisk = riskSubject
69104
69390
  .filter(({ backtest }) => !backtest)
69105
69391
  .connect(async (data) => {
@@ -69129,7 +69415,7 @@ class NotificationAdapter {
69129
69415
  await NotificationLive.handleSignalNotify(data);
69130
69416
  }
69131
69417
  });
69132
- unLive = compose(() => unLiveSignal(), () => unLivePartialProfit(), () => unLivePartialLoss(), () => unLiveBreakeven(), () => unLiveStrategyCommit(), () => unLiveSync(), () => unLiveRisk(), () => unLiveError(), () => unLiveExit(), () => unLiveValidation(), () => unLiveSignalNotify());
69418
+ unLive = compose(() => unLiveSignal(), () => unLiveSignalEvent(), () => unLiveScheduleEvent(), () => unLivePartialProfit(), () => unLivePartialLoss(), () => unLiveBreakeven(), () => unLiveStrategyCommit(), () => unLiveSync(), () => unLiveCheck(), () => unLiveRisk(), () => unLiveError(), () => unLiveExit(), () => unLiveValidation(), () => unLiveSignalNotify(), () => checkThrottleMap.clear());
69133
69419
  }
69134
69420
  return () => {
69135
69421
  unLive();
@@ -70424,17 +70710,31 @@ const CRON_METHOD_NAME_DISPOSE = "CronUtils.dispose";
70424
70710
  /**
70425
70711
  * Watchdog timeout (ms) for a single cron handler invocation.
70426
70712
  *
70427
- * A handler that does not settle within this window is treated as failed:
70428
- * `_runEntry` races `entry.handler(info)` against this `sleep` and, when the
70429
- * timeout wins, throws into the same `catch` as any other handler error
70430
- * surfacing `failed = true`, logging a warning, and (for periodic entries)
70431
- * rolling back the watermark so the boundary is retried on the next tick.
70713
+ * A slot that does not settle within this window is treated as failed:
70714
+ * `_runEntry` races the runtime-info assembly plus `entry.handler(info)`
70715
+ * against a timer of this duration and, when the timer wins, rejects into the
70716
+ * same `catch` as any other handler error — surfacing `failed = true`, logging
70717
+ * a warning, and (for periodic entries) rolling back the watermark so the
70718
+ * boundary is retried on the next tick.
70432
70719
  *
70433
70720
  * This guards the `singlerun`-serialised tick pipeline against a handler that
70434
- * never resolves (a lost `resolve`, a hung promise with no timeout of its
70435
- * own): without it such a handler would stall every subsequent tick forever.
70721
+ * never resolves (a lost `resolve`, a hung network call with no timeout of its
70722
+ * own): without it such a handler would hold its `_inFlight` slot forever and
70723
+ * `_tick`'s `Promise.all` would never settle, silently stalling every
70724
+ * subsequent lifecycle tick while the process stays alive and outwardly
70725
+ * healthy.
70726
+ */
70727
+ const CRON_HANDLER_TIMEOUT = 900000;
70728
+ /**
70729
+ * Early-warning threshold (ms) for a single cron handler invocation.
70730
+ *
70731
+ * Purely observational: when a slot is still running this long after it was
70732
+ * opened, `_runEntry` logs a warning naming the entry — so a slow handler is
70733
+ * visible in the logs long before the {@link CRON_HANDLER_TIMEOUT} watchdog
70734
+ * forcibly fails it. Nothing is interrupted and no rollback happens at this
70735
+ * mark; the slot keeps running and may still succeed.
70436
70736
  */
70437
- const CRON_HANDLER_TIMEOUT = 120000;
70737
+ const CRON_HANDLER_WARN_TIMEOUT = 120000;
70438
70738
  /**
70439
70739
  * Local logger instance.
70440
70740
  *
@@ -70856,28 +71156,9 @@ class CronUtils {
70856
71156
  }
70857
71157
  taskList.push(pending);
70858
71158
  }
70859
- {
70860
- // Watchdog: warn (do not interrupt) if the slots this tick is awaiting
70861
- // have not settled within CRON_HANDLER_TIMEOUT. We deliberately keep
70862
- // awaiting Promise.all so the singlerun pipeline stays serialised and no
70863
- // duplicate/zombie slots are spawned — the timer only surfaces the stall.
70864
- // Use a real setTimeout/clearTimeout (not sleep) so the alarm is cancelled
70865
- // the instant Promise.all resolves, rather than lingering for the full
70866
- // timeout on every fast tick.
70867
- const timer = setTimeout(() => {
70868
- const message = `${CRON_METHOD_NAME_TICK} timed out after ${CRON_HANDLER_TIMEOUT}ms`;
70869
- const payload = { symbol, when, context };
70870
- LOGGER_SERVICE$1.warn(message, payload);
70871
- console.error(message, payload);
70872
- errorEmitter.next(new Error(message));
70873
- }, CRON_HANDLER_TIMEOUT);
70874
- try {
70875
- await Promise.all(taskList);
70876
- }
70877
- finally {
70878
- clearTimeout(timer);
70879
- }
70880
- }
71159
+ // Every slot self-terminates via the `_runEntry` watchdog, so this settles
71160
+ // within CRON_HANDLER_TIMEOUT plus epsilon even when a handler hangs.
71161
+ await Promise.all(taskList);
70881
71162
  // Roll back the watermark for any periodic slot THIS tick opened whose
70882
71163
  // handler failed, so the next tick re-opens the same boundary and retries
70883
71164
  // it — mirroring how a skipped boundary is later caught up. Restoring
@@ -71061,9 +71342,17 @@ class CronUtils {
71061
71342
  *
71062
71343
  * Assembles the {@link IRuntimeInfo} snapshot via
71063
71344
  * `RuntimeMetaService.getRuntimeInfo(symbol, context, backtest)` and invokes
71064
- * `entry.handler(info)`. Logs any error via `console.error` and **returns** a
71065
- * `failed` boolean (`true` when the handler or the runtime-info assembly
71066
- * threw) so the caller (`_tick`) can roll back the periodic watermark of the
71345
+ * `entry.handler(info)`, racing both against the
71346
+ * {@link CRON_HANDLER_TIMEOUT} watchdog a slot that does not settle in time
71347
+ * rejects into the same `catch` as any other error, so a hung handler (or a
71348
+ * hung price fetch inside `getRuntimeInfo`) can never hold the `_inFlight`
71349
+ * slot forever and stall the serialised tick pipeline. A slot still running
71350
+ * at {@link CRON_HANDLER_WARN_TIMEOUT} logs an observational warning first,
71351
+ * so a slow handler is visible in the logs well before the watchdog forcibly
71352
+ * fails it. Logs any error via
71353
+ * `console.error` and **returns** a `failed` boolean (`true` when the
71354
+ * handler — or the runtime-info assembly — threw or timed out) so the caller
71355
+ * (`_tick`) can roll back the periodic watermark of the
71067
71356
  * slot it opened and retry that boundary. The error is **not** rethrown, so a
71068
71357
  * failing handler never produces an unhandled rejection. Clears the
71069
71358
  * `_inFlight` slot in `.finally()` so the next boundary produces a fresh
@@ -71088,9 +71377,37 @@ class CronUtils {
71088
71377
  */
71089
71378
  async _runEntry(entry, symbol, alignedMs, slotKey, firedKey, backtest, context) {
71090
71379
  let failed = false;
71380
+ let watchdog;
71381
+ // Observational early warning: fires while the slot is still running, long
71382
+ // before the watchdog below forcibly fails it. Cancelled in `finally`, so
71383
+ // it never fires for slots that settle in time.
71384
+ const slowAlarm = setTimeout(() => {
71385
+ const message = `${CRON_METHOD_NAME_TICK} entry "${entry.name}" still running after ${CRON_HANDLER_WARN_TIMEOUT}ms (watchdog at ${CRON_HANDLER_TIMEOUT}ms)`;
71386
+ const payload = { symbol, alignedMs };
71387
+ LOGGER_SERVICE$1.warn(message, payload);
71388
+ console.error(message, payload);
71389
+ }, CRON_HANDLER_WARN_TIMEOUT);
71091
71390
  try {
71092
- const info = await RUNTIME_META_SERVICE.getRuntimeInfo(symbol, context, backtest);
71093
- await entry.handler(info);
71391
+ // The runtime-info assembly is raced alongside the handler: in live mode
71392
+ // getRuntimeInfo reaches out to the exchange for the current price, so it
71393
+ // can hang on a dead network exactly like a user handler can.
71394
+ const work = (async () => {
71395
+ const info = await RUNTIME_META_SERVICE.getRuntimeInfo(symbol, context, backtest);
71396
+ await entry.handler(info);
71397
+ })();
71398
+ // A timed-out slot abandons `work`; swallow its eventual rejection so a
71399
+ // zombie handler that fails later never surfaces as an unhandled
71400
+ // rejection. Its late success is equally unobserved: `failed` was already
71401
+ // reported and (for fire-once entries) the fired mark deliberately not set.
71402
+ work.catch(() => void 0);
71403
+ await Promise.race([
71404
+ work,
71405
+ new Promise((_, reject) => {
71406
+ watchdog = setTimeout(() => {
71407
+ reject(new Error(`Cron entry "${entry.name}" timed out after ${CRON_HANDLER_TIMEOUT}ms`));
71408
+ }, CRON_HANDLER_TIMEOUT);
71409
+ }),
71410
+ ]);
71094
71411
  }
71095
71412
  catch (error) {
71096
71413
  failed = true;
@@ -71106,6 +71423,8 @@ class CronUtils {
71106
71423
  errorEmitter.next(error);
71107
71424
  }
71108
71425
  finally {
71426
+ clearTimeout(slowAlarm);
71427
+ clearTimeout(watchdog);
71109
71428
  this._inFlight.delete(slotKey);
71110
71429
  if (!failed && firedKey !== null) {
71111
71430
  this._firedOnce.add(firedKey);