backtest-kit 13.6.0 → 14.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/README.md +524 -1723
  2. package/build/index.cjs +1640 -54
  3. package/build/index.mjs +1635 -55
  4. package/package.json +3 -3
  5. package/types.d.ts +1516 -40
package/build/index.cjs CHANGED
@@ -768,6 +768,22 @@ const riskSubject = new functoolsKit.Subject();
768
768
  * Allows users to track scheduled signal lifecycle and implement custom cancellation logic.
769
769
  */
770
770
  const schedulePingSubject = new functoolsKit.Subject();
771
+ /**
772
+ * Scheduled signal lifecycle emitter (creation and cancellation).
773
+ * Emits when a scheduled signal is created (action "scheduled") or cancelled before
774
+ * activation (action "cancelled": timeout / price_reject / user) during tick()/backtest().
775
+ *
776
+ * The scheduled -> active transition (activation) is intentionally NOT emitted here — that
777
+ * produces an "opened" signal on the regular signal emitters instead.
778
+ */
779
+ const scheduleEventSubject = new functoolsKit.Subject();
780
+ /**
781
+ * Pending signal lifecycle emitter (open and close).
782
+ * Emits when a pending position is opened (action "opened": new signal / immediate / scheduled
783
+ * or user activation) or closed (action "closed" with closeReason take_profit / stop_loss /
784
+ * time_expired / closed) during tick()/backtest().
785
+ */
786
+ const signalEventSubject = new functoolsKit.Subject();
771
787
  /**
772
788
  * Active ping emitter for active pending signal monitoring events.
773
789
  * Emits every minute when an active pending signal is being monitored.
@@ -858,10 +874,12 @@ var emitters = /*#__PURE__*/Object.freeze({
858
874
  progressBacktestEmitter: progressBacktestEmitter,
859
875
  progressWalkerEmitter: progressWalkerEmitter,
860
876
  riskSubject: riskSubject,
877
+ scheduleEventSubject: scheduleEventSubject,
861
878
  schedulePingSubject: schedulePingSubject,
862
879
  shutdownEmitter: shutdownEmitter,
863
880
  signalBacktestEmitter: signalBacktestEmitter,
864
881
  signalEmitter: signalEmitter,
882
+ signalEventSubject: signalEventSubject,
865
883
  signalLiveEmitter: signalLiveEmitter,
866
884
  signalNotifySubject: signalNotifySubject,
867
885
  strategyCommitSubject: strategyCommitSubject,
@@ -1929,7 +1947,7 @@ class PersistStrategyInstance {
1929
1947
  const strategyData = await this._storage.readValue(PersistStrategyInstance.STORAGE_KEY);
1930
1948
  // JSON serializes Infinity as null, so an eternal-hold signal
1931
1949
  // (minuteEstimatedTime: Infinity) reads back as null — restore it.
1932
- for (const signal of [strategyData.closedSignal, strategyData.cancelledSignal, strategyData.activatedSignal]) {
1950
+ for (const signal of [strategyData.closedSignal, strategyData.cancelledSignal, strategyData.activatedSignal, strategyData.takeProfitSignal, strategyData.stopLossSignal]) {
1933
1951
  if (signal && signal.minuteEstimatedTime == null) {
1934
1952
  signal.minuteEstimatedTime = Infinity;
1935
1953
  }
@@ -7673,6 +7691,10 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
7673
7691
  self._closedSignal = strategyData.closedSignal;
7674
7692
  self._cancelledSignal = strategyData.cancelledSignal;
7675
7693
  self._activatedSignal = strategyData.activatedSignal;
7694
+ // Deferred broker-confirmed TP/SL fills snapshot the pending signal and clear it (like
7695
+ // _closedSignal), so they stand on their own and are restored unconditionally too.
7696
+ self._takeProfitSignal = strategyData.takeProfitSignal;
7697
+ self._stopLossSignal = strategyData.stopLossSignal;
7676
7698
  }
7677
7699
  // Restore pending signal
7678
7700
  const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
@@ -7753,6 +7775,8 @@ const PERSIST_STRATEGY_FN = async (self) => {
7753
7775
  closedSignal: self._closedSignal,
7754
7776
  cancelledSignal: self._cancelledSignal,
7755
7777
  activatedSignal: self._activatedSignal,
7778
+ takeProfitSignal: self._takeProfitSignal,
7779
+ stopLossSignal: self._stopLossSignal,
7756
7780
  }, self.params.symbol, self.params.strategyName, self.params.exchangeName);
7757
7781
  };
7758
7782
  const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice, timestamp) => {
@@ -8286,6 +8310,7 @@ const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice)
8286
8310
  maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
8287
8311
  });
8288
8312
  await self.setScheduledSignal(null);
8313
+ await CALL_SCHEDULE_EVENT_FN$1(self, "cancelled", scheduled, currentPrice, currentTime, "timeout");
8289
8314
  await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentPrice, currentTime, self.params.execution.context.backtest);
8290
8315
  const result = {
8291
8316
  action: "cancelled",
@@ -8342,6 +8367,7 @@ const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPr
8342
8367
  });
8343
8368
  await self.setScheduledSignal(null);
8344
8369
  const currentTime = self.params.execution.context.when.getTime();
8370
+ await CALL_SCHEDULE_EVENT_FN$1(self, "cancelled", scheduled, currentPrice, currentTime, "price_reject");
8345
8371
  await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentPrice, currentTime, self.params.execution.context.backtest);
8346
8372
  const result = {
8347
8373
  action: "cancelled",
@@ -8435,6 +8461,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
8435
8461
  await self.setScheduledSignal(null);
8436
8462
  await self.setPendingSignal(activatedSignal, activatedSignal.priceOpen);
8437
8463
  await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activatedSignal, activationTime, self.params.execution.context.backtest);
8464
+ await CALL_SIGNAL_EVENT_FN(self, "opened", self._pendingSignal, self._pendingSignal.priceOpen, activationTime);
8438
8465
  await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, self._pendingSignal, self._pendingSignal.priceOpen, activationTime, self.params.execution.context.backtest);
8439
8466
  const result = {
8440
8467
  action: "opened",
@@ -8569,6 +8596,53 @@ const CALL_SCHEDULE_CALLBACKS_FN = functoolsKit.trycatch(beginTime(async (self,
8569
8596
  errorEmitter.next(error);
8570
8597
  },
8571
8598
  });
8599
+ /**
8600
+ * Emits a scheduled signal lifecycle event (creation / cancellation) to onScheduleEvent.
8601
+ *
8602
+ * Called when a scheduled signal is created (action "scheduled") or cancelled before activation
8603
+ * (action "cancelled" with reason timeout / price_reject / user). The scheduled -> active
8604
+ * transition is intentionally NOT emitted through this path.
8605
+ *
8606
+ * Unlike the CALL_*_CALLBACKS_FN helpers this does not run inside beginTime/runInContext: the
8607
+ * timestamp is passed explicitly by the caller (tick when / candle timestamp) and forwarded to
8608
+ * the connection-level emitter, mirroring how onSchedulePing carries its own timestamp.
8609
+ */
8610
+ const CALL_SCHEDULE_EVENT_FN$1 = functoolsKit.trycatch(async (self, action, signal, currentPrice, timestamp, reason) => {
8611
+ await self.params.onScheduleEvent(action, self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, TO_PUBLIC_SIGNAL("scheduled", signal, currentPrice), currentPrice, self.params.execution.context.backtest, timestamp, reason);
8612
+ }, {
8613
+ fallback: (error, self) => {
8614
+ const message = "ClientStrategy CALL_SCHEDULE_EVENT_FN thrown";
8615
+ const payload = {
8616
+ error: functoolsKit.errorData(error),
8617
+ message: functoolsKit.getErrorMessage(error),
8618
+ };
8619
+ self.params.logger.warn(message, payload);
8620
+ console.warn(message, payload);
8621
+ errorEmitter.next(error);
8622
+ },
8623
+ });
8624
+ /**
8625
+ * Emits a pending signal lifecycle event (open / close) to onSignalEvent.
8626
+ *
8627
+ * Called when a pending position is opened (action "opened") or closed (action "closed" with a
8628
+ * closeReason). Like CALL_SCHEDULE_EVENT_FN it does not run inside beginTime/runInContext: the
8629
+ * timestamp is passed explicitly by the caller (tick when / candle timestamp) and forwarded to
8630
+ * the connection-level emitter.
8631
+ */
8632
+ const CALL_SIGNAL_EVENT_FN = functoolsKit.trycatch(async (self, action, signal, currentPrice, timestamp, closeReason) => {
8633
+ await self.params.onSignalEvent(action, self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, TO_PUBLIC_SIGNAL("pending", signal, currentPrice), currentPrice, self.params.execution.context.backtest, timestamp, closeReason);
8634
+ }, {
8635
+ fallback: (error, self) => {
8636
+ const message = "ClientStrategy CALL_SIGNAL_EVENT_FN thrown";
8637
+ const payload = {
8638
+ error: functoolsKit.errorData(error),
8639
+ message: functoolsKit.getErrorMessage(error),
8640
+ };
8641
+ self.params.logger.warn(message, payload);
8642
+ console.warn(message, payload);
8643
+ errorEmitter.next(error);
8644
+ },
8645
+ });
8572
8646
  const CALL_CANCEL_CALLBACKS_FN = functoolsKit.trycatch(beginTime(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
8573
8647
  await ExecutionContextService.runInContext(async () => {
8574
8648
  if (self.params.callbacks?.onCancel) {
@@ -8944,6 +9018,7 @@ const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
8944
9018
  priceOpen: signal.priceOpen,
8945
9019
  currentPrice: currentPrice,
8946
9020
  });
9021
+ await CALL_SCHEDULE_EVENT_FN$1(self, "scheduled", signal, currentPrice, currentTime);
8947
9022
  await CALL_SCHEDULE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
8948
9023
  const result = {
8949
9024
  action: "scheduled",
@@ -8974,6 +9049,7 @@ const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
8974
9049
  return null;
8975
9050
  }
8976
9051
  await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, signal, currentTime, self.params.execution.context.backtest);
9052
+ await CALL_SIGNAL_EVENT_FN(self, "opened", signal, signal.priceOpen, currentTime);
8977
9053
  await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, signal.priceOpen, currentTime, self.params.execution.context.backtest);
8978
9054
  const result = {
8979
9055
  action: "opened",
@@ -9040,6 +9116,7 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
9040
9116
  priceClose: currentPrice,
9041
9117
  pnlPercentage: publicSignal.pnl.pnlPercentage,
9042
9118
  });
9119
+ await CALL_SIGNAL_EVENT_FN(self, "closed", signal, currentPrice, currentTime, closeReason);
9043
9120
  await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
9044
9121
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
9045
9122
  await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
@@ -9081,6 +9158,7 @@ const CLOSE_PENDING_SIGNAL_AS_CLOSED_FN = async (self, signal, currentPrice) =>
9081
9158
  priceClose: currentPrice,
9082
9159
  pnlPercentage: publicSignal.pnl.pnlPercentage,
9083
9160
  });
9161
+ await CALL_SIGNAL_EVENT_FN(self, "closed", signal, currentPrice, currentTime, "closed");
9084
9162
  await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
9085
9163
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
9086
9164
  await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
@@ -9251,6 +9329,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
9251
9329
  });
9252
9330
  await self.setScheduledSignal(null);
9253
9331
  const publicSignal = TO_PUBLIC_SIGNAL("scheduled", scheduled, averagePrice);
9332
+ await CALL_SCHEDULE_EVENT_FN$1(self, "cancelled", scheduled, averagePrice, closeTimestamp, reason);
9254
9333
  if (reason === "user") {
9255
9334
  await CALL_COMMIT_FN(self, {
9256
9335
  action: "cancel-scheduled",
@@ -9363,6 +9442,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
9363
9442
  await self.setScheduledSignal(null);
9364
9443
  await self.setPendingSignal(activatedSignal, activatedSignal.priceOpen);
9365
9444
  await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activatedSignal, activationTime, self.params.execution.context.backtest);
9445
+ await CALL_SIGNAL_EVENT_FN(self, "opened", activatedSignal, activatedSignal.priceOpen, activationTime);
9366
9446
  await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, activatedSignal, activatedSignal.priceOpen, activationTime, self.params.execution.context.backtest);
9367
9447
  await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, activatedSignal, activationTime, self.params.execution.context.backtest);
9368
9448
  return true;
@@ -9393,6 +9473,7 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
9393
9473
  if (closeReason === "time_expired" && publicSignal.pnl.pnlPercentage < 0) {
9394
9474
  self.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (time_expired), PNL: ${publicSignal.pnl.pnlPercentage.toFixed(2)}%`);
9395
9475
  }
9476
+ await CALL_SIGNAL_EVENT_FN(self, "closed", signal, averagePrice, closeTimestamp, closeReason);
9396
9477
  await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
9397
9478
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
9398
9479
  await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
@@ -9451,6 +9532,7 @@ const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, aver
9451
9532
  signal: publicSignal,
9452
9533
  note: closedSignal.closeNote ?? closedSignal.note,
9453
9534
  });
9535
+ await CALL_SIGNAL_EVENT_FN(self, "closed", closedSignal, averagePrice, closeTimestamp, "closed");
9454
9536
  await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
9455
9537
  await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
9456
9538
  await CALL_BREAKEVEN_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
@@ -9473,6 +9555,79 @@ const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, aver
9473
9555
  await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
9474
9556
  return result;
9475
9557
  };
9558
+ /**
9559
+ * Closes a deferred broker-confirmed TP/SL fill (createTakeProfit / createStopLoss).
9560
+ *
9561
+ * The exchange and the strategy are parallel states: ClientStrategy evaluates TP/SL against VWAP,
9562
+ * but the real order may fill on candle high/low. When the broker confirms such a fill out of
9563
+ * context, the pending signal is snapshotted into _takeProfitSignal / _stopLossSignal and the next
9564
+ * tick()/backtest() drains it here, closing with the matching closeReason at the effective TP/SL
9565
+ * level (trailing override if set) — bypassing the VWAP completion check.
9566
+ *
9567
+ * Shared by live tick and backtest: the caller passes the close timestamp explicitly (execution
9568
+ * context when in live, candle timestamp in backtest). Mirrors CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN
9569
+ * (commit action "close-pending", carrying closeId/note) but with closeReason take_profit / stop_loss.
9570
+ *
9571
+ * Like the deferred close, the fill snapshot is already established by the broker, so it does NOT
9572
+ * re-confirm via onSignalSync. _takeProfitSignal / _stopLossSignal is cleared and re-persisted by
9573
+ * the caller after draining.
9574
+ */
9575
+ const CLOSE_PENDING_SIGNAL_AS_FILL_FN = async (self, filledSignal, closeReason, closeTimestamp) => {
9576
+ const closePrice = closeReason === "take_profit"
9577
+ ? (filledSignal._trailingPriceTakeProfit ?? filledSignal.priceTakeProfit)
9578
+ : (filledSignal._trailingPriceStopLoss ?? filledSignal.priceStopLoss);
9579
+ const publicSignal = TO_PUBLIC_SIGNAL("pending", filledSignal, closePrice);
9580
+ self.params.logger.info(`ClientStrategy signal ${closeReason} by broker-confirmed fill (createTakeProfit/createStopLoss)`, {
9581
+ symbol: self.params.execution.context.symbol,
9582
+ signalId: filledSignal.id,
9583
+ closeReason,
9584
+ priceClose: closePrice,
9585
+ pnlPercentage: publicSignal.pnl.pnlPercentage,
9586
+ });
9587
+ await CALL_COMMIT_FN(self, {
9588
+ action: "close-pending",
9589
+ symbol: self.params.execution.context.symbol,
9590
+ strategyName: self.params.strategyName,
9591
+ exchangeName: self.params.exchangeName,
9592
+ frameName: self.params.frameName,
9593
+ signalId: filledSignal.id,
9594
+ backtest: self.params.execution.context.backtest,
9595
+ closeId: filledSignal.closeId,
9596
+ timestamp: closeTimestamp,
9597
+ totalEntries: filledSignal._entry?.length ?? 1,
9598
+ totalPartials: filledSignal._partial?.length ?? 0,
9599
+ originalPriceOpen: filledSignal.priceOpen,
9600
+ pnl: publicSignal.pnl,
9601
+ maxDrawdown: publicSignal.maxDrawdown,
9602
+ peakProfit: publicSignal.peakProfit,
9603
+ signal: publicSignal,
9604
+ note: filledSignal.closeNote ?? filledSignal.note,
9605
+ });
9606
+ await CALL_SIGNAL_EVENT_FN(self, "closed", filledSignal, closePrice, closeTimestamp, closeReason);
9607
+ await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, filledSignal, closePrice, closeTimestamp, self.params.execution.context.backtest);
9608
+ // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
9609
+ await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, filledSignal, closePrice, closeTimestamp, self.params.execution.context.backtest);
9610
+ // КРИТИЧНО: Очищаем состояние ClientBreakeven при закрытии позиции
9611
+ await CALL_BREAKEVEN_CLEAR_FN(self, self.params.execution.context.symbol, filledSignal, closePrice, closeTimestamp, self.params.execution.context.backtest);
9612
+ await CALL_RISK_REMOVE_SIGNAL_FN(self, self.params.execution.context.symbol, closeTimestamp, self.params.execution.context.backtest);
9613
+ const result = {
9614
+ action: "closed",
9615
+ signal: publicSignal,
9616
+ currentPrice: closePrice,
9617
+ closeReason,
9618
+ closeTimestamp,
9619
+ pnl: publicSignal.pnl,
9620
+ strategyName: self.params.method.context.strategyName,
9621
+ exchangeName: self.params.method.context.exchangeName,
9622
+ frameName: self.params.method.context.frameName,
9623
+ symbol: self.params.execution.context.symbol,
9624
+ backtest: self.params.execution.context.backtest,
9625
+ closeId: filledSignal.closeId,
9626
+ createdAt: closeTimestamp,
9627
+ };
9628
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
9629
+ return result;
9630
+ };
9476
9631
  const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, frameEndTime) => {
9477
9632
  const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
9478
9633
  const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
@@ -9595,6 +9750,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, fra
9595
9750
  totalPartials: publicSignalForCommit.totalPartials,
9596
9751
  note: activatedSignal.activateNote ?? publicSignalForCommit.note,
9597
9752
  });
9753
+ await CALL_SIGNAL_EVENT_FN(self, "opened", pendingSignal, pendingSignal.priceOpen, candle.timestamp);
9598
9754
  await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, pendingSignal.priceOpen, candle.timestamp, self.params.execution.context.backtest);
9599
9755
  await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, pendingSignal, candle.timestamp, self.params.execution.context.backtest);
9600
9756
  return { outcome: "activated", activationIndex: i };
@@ -9690,6 +9846,20 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
9690
9846
  }
9691
9847
  continue;
9692
9848
  }
9849
+ // КРИТИЧНО: Проверяем broker-confirmed TP fill через createTakeProfit() (напр. из onActivePing).
9850
+ // Закрываем по эффективному уровню TP, минуя VWAP-проверку. Sync не пере-подтверждается.
9851
+ if (self._takeProfitSignal) {
9852
+ const filledSignal = self._takeProfitSignal;
9853
+ self._takeProfitSignal = null;
9854
+ return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(self, filledSignal, "take_profit", currentCandleTimestamp);
9855
+ }
9856
+ // КРИТИЧНО: Проверяем broker-confirmed SL fill через createStopLoss() (напр. из onActivePing).
9857
+ // Закрываем по эффективному уровню SL, минуя VWAP-проверку. Sync не пере-подтверждается.
9858
+ if (self._stopLossSignal) {
9859
+ const filledSignal = self._stopLossSignal;
9860
+ self._stopLossSignal = null;
9861
+ return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(self, filledSignal, "stop_loss", currentCandleTimestamp);
9862
+ }
9693
9863
  let shouldClose = false;
9694
9864
  let closeReason;
9695
9865
  // Check time expiration FIRST (КРИТИЧНО!)
@@ -9925,6 +10095,20 @@ class ClientStrategy {
9925
10095
  this._cancelledSignal = null;
9926
10096
  this._closedSignal = null;
9927
10097
  this._activatedSignal = null;
10098
+ /**
10099
+ * Deferred broker-confirmed take-profit fill (set via createTakeProfit). When non-null, the
10100
+ * exchange reported the TP order was actually filled (e.g. by candle high/low) — the next
10101
+ * tick()/backtest() drains it and closes the pending position with closeReason "take_profit"
10102
+ * at the effective take-profit level, bypassing the VWAP-based TP check.
10103
+ */
10104
+ this._takeProfitSignal = null;
10105
+ /**
10106
+ * Deferred broker-confirmed stop-loss fill (set via createStopLoss). When non-null, the
10107
+ * exchange reported the SL order was actually filled (e.g. by candle high/low) — the next
10108
+ * tick()/backtest() drains it and closes the pending position with closeReason "stop_loss"
10109
+ * at the effective stop-loss level, bypassing the VWAP-based SL check.
10110
+ */
10111
+ this._stopLossSignal = null;
9928
10112
  /**
9929
10113
  * User-supplied signal DTO to be consumed by the next GET_SIGNAL_FN tick instead of
9930
10114
  * params.getSignal. Set via createSignal. When non-null, params.getSignal is NOT called
@@ -9998,6 +10182,10 @@ class ClientStrategy {
9998
10182
  // - при null: сигнал закрыт по TP/SL/timeout, флаг больше не нужен
9999
10183
  // - при новом сигнале: флаг от предыдущего сигнала не должен влиять на новый
10000
10184
  this._closedSignal = null;
10185
+ // КРИТИЧНО: Так же сбрасываем отложенные broker-confirmed TP/SL fills — закрытие позиции
10186
+ // любым путём делает их неактуальными, а новая позиция не должна унаследовать чужой fill.
10187
+ this._takeProfitSignal = null;
10188
+ this._stopLossSignal = null;
10001
10189
  // ЗАЩИТА ИНВАРИАНТА: При установке нового pending сигнала очищаем scheduled
10002
10190
  // Не может быть одновременно pending И scheduled (взаимоисключающие состояния)
10003
10191
  // При null: scheduled может существовать (новый сигнал после закрытия позиции)
@@ -10891,6 +11079,7 @@ class ClientStrategy {
10891
11079
  signal: publicSignal,
10892
11080
  note: cancelledSignal.cancelNote ?? cancelledSignal.note,
10893
11081
  });
11082
+ await CALL_SCHEDULE_EVENT_FN$1(this, "cancelled", cancelledSignal, currentPrice, currentTime, "user");
10894
11083
  // Call onCancel callback
10895
11084
  await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, currentTime, this.params.execution.context.backtest);
10896
11085
  const result = {
@@ -10951,6 +11140,7 @@ class ClientStrategy {
10951
11140
  signal: publicSignal,
10952
11141
  note: closedSignal.closeNote ?? closedSignal.note,
10953
11142
  });
11143
+ await CALL_SIGNAL_EVENT_FN(this, "closed", closedSignal, currentPrice, currentTime, "closed");
10954
11144
  // Call onClose callback
10955
11145
  await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, currentTime, this.params.execution.context.backtest);
10956
11146
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
@@ -10976,6 +11166,32 @@ class ClientStrategy {
10976
11166
  await CALL_TICK_CALLBACKS_FN(this, this.params.execution.context.symbol, result, currentTime, this.params.execution.context.backtest);
10977
11167
  return result;
10978
11168
  }
11169
+ // Check if a broker-confirmed take-profit fill is awaiting (createTakeProfit) - close once.
11170
+ // The exchange filled the TP order (e.g. by high/low); close bypassing the VWAP TP check.
11171
+ if (this._takeProfitSignal) {
11172
+ const filledSignal = this._takeProfitSignal;
11173
+ this._takeProfitSignal = null; // Clear after draining
11174
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
11175
+ await PERSIST_STRATEGY_FN(this);
11176
+ this.params.logger.info("ClientStrategy tick: pending signal closed by broker-confirmed take-profit fill", {
11177
+ symbol: this.params.execution.context.symbol,
11178
+ signalId: filledSignal.id,
11179
+ });
11180
+ return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(this, filledSignal, "take_profit", currentTime);
11181
+ }
11182
+ // Check if a broker-confirmed stop-loss fill is awaiting (createStopLoss) - close once.
11183
+ // The exchange filled the SL order (e.g. by high/low); close bypassing the VWAP SL check.
11184
+ if (this._stopLossSignal) {
11185
+ const filledSignal = this._stopLossSignal;
11186
+ this._stopLossSignal = null; // Clear after draining
11187
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
11188
+ await PERSIST_STRATEGY_FN(this);
11189
+ this.params.logger.info("ClientStrategy tick: pending signal closed by broker-confirmed stop-loss fill", {
11190
+ symbol: this.params.execution.context.symbol,
11191
+ signalId: filledSignal.id,
11192
+ });
11193
+ return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(this, filledSignal, "stop_loss", currentTime);
11194
+ }
10979
11195
  // Check if scheduled signal was activated - emit opened event once
10980
11196
  if (this._activatedSignal) {
10981
11197
  const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
@@ -11076,6 +11292,7 @@ class ClientStrategy {
11076
11292
  totalPartials: publicSignalForCommit.totalPartials,
11077
11293
  note: activatedSignal.activateNote ?? publicSignalForCommit.note,
11078
11294
  });
11295
+ await CALL_SIGNAL_EVENT_FN(this, "opened", pendingSignal, currentPrice, currentTime);
11079
11296
  // Call onOpen callback
11080
11297
  await CALL_OPEN_CALLBACKS_FN(this, this.params.execution.context.symbol, pendingSignal, currentPrice, currentTime, this.params.execution.context.backtest);
11081
11298
  const result = {
@@ -11226,6 +11443,7 @@ class ClientStrategy {
11226
11443
  signal: publicSignal,
11227
11444
  note: cancelledSignal.cancelNote ?? cancelledSignal.note,
11228
11445
  });
11446
+ await CALL_SCHEDULE_EVENT_FN$1(this, "cancelled", cancelledSignal, currentPrice, closeTimestamp, "user");
11229
11447
  await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
11230
11448
  const cancelledResult = {
11231
11449
  action: "cancelled",
@@ -11311,6 +11529,24 @@ class ClientStrategy {
11311
11529
  await CALL_TICK_CALLBACKS_FN(this, this.params.execution.context.symbol, closedResult, closeTimestamp, this.params.execution.context.backtest);
11312
11530
  return closedResult;
11313
11531
  }
11532
+ // If a broker-confirmed take-profit fill is awaiting (createTakeProfit) - close once.
11533
+ // The exchange filled the TP order (e.g. by high/low); close bypassing the VWAP TP check.
11534
+ if (this._takeProfitSignal) {
11535
+ this.params.logger.debug("ClientStrategy backtest: pending signal closed by broker-confirmed take-profit fill");
11536
+ const filledSignal = this._takeProfitSignal;
11537
+ this._takeProfitSignal = null; // Clear after draining
11538
+ const closeTimestamp = this.params.execution.context.when.getTime();
11539
+ return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(this, filledSignal, "take_profit", closeTimestamp);
11540
+ }
11541
+ // If a broker-confirmed stop-loss fill is awaiting (createStopLoss) - close once.
11542
+ // The exchange filled the SL order (e.g. by high/low); close bypassing the VWAP SL check.
11543
+ if (this._stopLossSignal) {
11544
+ this.params.logger.debug("ClientStrategy backtest: pending signal closed by broker-confirmed stop-loss fill");
11545
+ const filledSignal = this._stopLossSignal;
11546
+ this._stopLossSignal = null; // Clear after draining
11547
+ const closeTimestamp = this.params.execution.context.when.getTime();
11548
+ return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(this, filledSignal, "stop_loss", closeTimestamp);
11549
+ }
11314
11550
  if (!this._pendingSignal && !this._scheduledSignal) {
11315
11551
  throw new Error("ClientStrategy backtest: no pending or scheduled signal");
11316
11552
  }
@@ -11428,12 +11664,17 @@ class ClientStrategy {
11428
11664
  hasActivatedSignal: this._activatedSignal !== null,
11429
11665
  hasCancelledSignal: this._cancelledSignal !== null,
11430
11666
  hasClosedSignal: this._closedSignal !== null,
11667
+ hasTakeProfitSignal: this._takeProfitSignal !== null,
11668
+ hasStopLossSignal: this._stopLossSignal !== null,
11431
11669
  });
11432
11670
  this._isStopped = true;
11433
11671
  // Clear pending flags to start from clean state
11434
11672
  // NOTE: _isStopped blocks NEW position opening, but allows:
11435
11673
  // - cancelScheduled() / closePending() for graceful shutdown
11436
11674
  // - Monitoring existing _pendingSignal until TP/SL/timeout
11675
+ // NOTE: _takeProfitSignal / _stopLossSignal are NOT cleared — a broker-confirmed fill
11676
+ // reflects a real exchange close that must still drain to emit the closed event, the
11677
+ // same way an existing _pendingSignal keeps being monitored until its natural close.
11437
11678
  this._activatedSignal = null;
11438
11679
  this._cancelledSignal = null;
11439
11680
  this._closedSignal = null;
@@ -11651,9 +11892,97 @@ class ClientStrategy {
11651
11892
  if (this._cancelledSignal) {
11652
11893
  throw new Error(`ClientStrategy createSignal: a scheduled cancel is awaiting for symbol=${symbol}`);
11653
11894
  }
11895
+ if (this._takeProfitSignal) {
11896
+ throw new Error(`ClientStrategy createSignal: a take-profit fill is awaiting for symbol=${symbol}`);
11897
+ }
11898
+ if (this._stopLossSignal) {
11899
+ throw new Error(`ClientStrategy createSignal: a stop-loss fill is awaiting for symbol=${symbol}`);
11900
+ }
11654
11901
  this._userSignal = dto;
11655
11902
  await PERSIST_STRATEGY_FN(this);
11656
11903
  }
11904
+ /**
11905
+ * Reports that the pending position's take-profit order was actually filled on the exchange
11906
+ * (e.g. by candle high/low), forcing a close that does not wait for the VWAP-based TP check.
11907
+ *
11908
+ * The exchange and the strategy are parallel states: ClientStrategy evaluates TP/SL against
11909
+ * VWAP, but the real order may close on high/low. This bridges that gap — the broker confirms
11910
+ * the fill OUT of the async-hooks execution context, the current pending signal is snapshotted
11911
+ * into _takeProfitSignal and cleared, and the next tick()/backtest() drains it, closing the
11912
+ * position with closeReason "take_profit" at the effective take-profit level.
11913
+ *
11914
+ * No-op if no pending signal exists. Persisted (live mode only) so a crash before the next
11915
+ * tick does not lose the deferred close.
11916
+ *
11917
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
11918
+ * @param backtest - Whether running in backtest mode
11919
+ * @param payload - Optional commit id/note attached to the close
11920
+ * @returns Promise that resolves when the take-profit fill is queued
11921
+ */
11922
+ async createTakeProfit(symbol, backtest, payload) {
11923
+ const closeId = payload.id;
11924
+ this.params.logger.debug("ClientStrategy createTakeProfit", {
11925
+ symbol,
11926
+ hasPendingSignal: this._pendingSignal !== null,
11927
+ closeId,
11928
+ });
11929
+ // Snapshot the pending signal for the next tick/backtest to close with reason "take_profit".
11930
+ if (this._pendingSignal) {
11931
+ this._takeProfitSignal = Object.assign({}, this._pendingSignal, {
11932
+ closeId,
11933
+ closeNote: payload.note,
11934
+ });
11935
+ this._pendingSignal = null;
11936
+ }
11937
+ if (backtest) {
11938
+ // Drained in backtest() with correct candle timestamp; no live persistence.
11939
+ return;
11940
+ }
11941
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, symbol, this.params.strategyName, this.params.exchangeName);
11942
+ // Persist deferred _takeProfitSignal so a crash before the next tick does not lose it
11943
+ await PERSIST_STRATEGY_FN(this);
11944
+ }
11945
+ /**
11946
+ * Reports that the pending position's stop-loss order was actually filled on the exchange
11947
+ * (e.g. by candle high/low), forcing a close that does not wait for the VWAP-based SL check.
11948
+ *
11949
+ * The exchange and the strategy are parallel states: ClientStrategy evaluates TP/SL against
11950
+ * VWAP, but the real order may close on high/low. This bridges that gap — the broker confirms
11951
+ * the fill OUT of the async-hooks execution context, the current pending signal is snapshotted
11952
+ * into _stopLossSignal and cleared, and the next tick()/backtest() drains it, closing the
11953
+ * position with closeReason "stop_loss" at the effective stop-loss level.
11954
+ *
11955
+ * No-op if no pending signal exists. Persisted (live mode only) so a crash before the next
11956
+ * tick does not lose the deferred close.
11957
+ *
11958
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
11959
+ * @param backtest - Whether running in backtest mode
11960
+ * @param payload - Optional commit id/note attached to the close
11961
+ * @returns Promise that resolves when the stop-loss fill is queued
11962
+ */
11963
+ async createStopLoss(symbol, backtest, payload) {
11964
+ const closeId = payload.id;
11965
+ this.params.logger.debug("ClientStrategy createStopLoss", {
11966
+ symbol,
11967
+ hasPendingSignal: this._pendingSignal !== null,
11968
+ closeId,
11969
+ });
11970
+ // Snapshot the pending signal for the next tick/backtest to close with reason "stop_loss".
11971
+ if (this._pendingSignal) {
11972
+ this._stopLossSignal = Object.assign({}, this._pendingSignal, {
11973
+ closeId,
11974
+ closeNote: payload.note,
11975
+ });
11976
+ this._pendingSignal = null;
11977
+ }
11978
+ if (backtest) {
11979
+ // Drained in backtest() with correct candle timestamp; no live persistence.
11980
+ return;
11981
+ }
11982
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, symbol, this.params.strategyName, this.params.exchangeName);
11983
+ // Persist deferred _stopLossSignal so a crash before the next tick does not lose it
11984
+ await PERSIST_STRATEGY_FN(this);
11985
+ }
11657
11986
  /**
11658
11987
  * Returns the deferred strategy-state snapshot exactly as it would be written to persist on
11659
11988
  * this iteration: the in-memory _userSignal, _commitQueue and deferred user-action flags,
@@ -11673,6 +12002,8 @@ class ClientStrategy {
11673
12002
  closedSignal: this._closedSignal,
11674
12003
  cancelledSignal: this._cancelledSignal,
11675
12004
  activatedSignal: this._activatedSignal,
12005
+ takeProfitSignal: this._takeProfitSignal,
12006
+ stopLossSignal: this._stopLossSignal,
11676
12007
  };
11677
12008
  }
11678
12009
  /**
@@ -13011,7 +13342,7 @@ const CREATE_SYNC_PENDING_FN = (self, strategyName, exchangeName, frameName, bac
13011
13342
  return true;
13012
13343
  }
13013
13344
  await syncPendingSubject.next(event);
13014
- await self.actionCoreService.orderPing(backtest, event, { strategyName, exchangeName, frameName });
13345
+ await self.actionCoreService.orderCheck(backtest, event, { strategyName, exchangeName, frameName });
13015
13346
  return true;
13016
13347
  }, {
13017
13348
  fallback: (error) => {
@@ -13165,6 +13496,81 @@ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => functoolsKit.trycatch(async (sy
13165
13496
  },
13166
13497
  defaultValue: null,
13167
13498
  });
13499
+ /**
13500
+ * Creates a callback function for emitting scheduled signal lifecycle events to scheduleEventSubject.
13501
+ *
13502
+ * Called by ClientStrategy when a scheduled signal is created (action "scheduled") or cancelled
13503
+ * before activation (action "cancelled": timeout / price_reject / user). The scheduled -> active
13504
+ * transition is intentionally NOT reported here.
13505
+ *
13506
+ * @param self - Reference to StrategyConnectionService instance
13507
+ * @returns Callback function for scheduled signal lifecycle events
13508
+ */
13509
+ const CREATE_COMMIT_SCHEDULE_EVENT_FN = (self) => functoolsKit.trycatch(async (action, symbol, strategyName, exchangeName, data, currentPrice, backtest, timestamp, reason) => {
13510
+ const event = {
13511
+ action,
13512
+ symbol,
13513
+ strategyName,
13514
+ exchangeName,
13515
+ frameName: data.frameName,
13516
+ data,
13517
+ reason,
13518
+ currentPrice,
13519
+ backtest,
13520
+ timestamp,
13521
+ };
13522
+ await scheduleEventSubject.next(event);
13523
+ await self.actionCoreService.scheduleEvent(backtest, event, { strategyName, exchangeName, frameName: data.frameName });
13524
+ }, {
13525
+ fallback: (error) => {
13526
+ const message = "StrategyConnectionService CREATE_COMMIT_SCHEDULE_EVENT_FN thrown";
13527
+ const payload = {
13528
+ error: functoolsKit.errorData(error),
13529
+ message: functoolsKit.getErrorMessage(error),
13530
+ };
13531
+ self.loggerService.warn(message, payload);
13532
+ console.warn(message, payload);
13533
+ errorEmitter.next(error);
13534
+ },
13535
+ defaultValue: null,
13536
+ });
13537
+ /**
13538
+ * Creates a callback function for emitting pending signal lifecycle events to signalEventSubject.
13539
+ *
13540
+ * Called by ClientStrategy when a pending position is opened (action "opened") or closed
13541
+ * (action "closed" with a closeReason).
13542
+ *
13543
+ * @param self - Reference to StrategyConnectionService instance
13544
+ * @returns Callback function for pending signal lifecycle events
13545
+ */
13546
+ const CREATE_COMMIT_SIGNAL_EVENT_FN = (self) => functoolsKit.trycatch(async (action, symbol, strategyName, exchangeName, data, currentPrice, backtest, timestamp, closeReason) => {
13547
+ const event = {
13548
+ action,
13549
+ symbol,
13550
+ strategyName,
13551
+ exchangeName,
13552
+ frameName: data.frameName,
13553
+ data,
13554
+ closeReason,
13555
+ currentPrice,
13556
+ backtest,
13557
+ timestamp,
13558
+ };
13559
+ await signalEventSubject.next(event);
13560
+ await self.actionCoreService.pendingEvent(backtest, event, { strategyName, exchangeName, frameName: data.frameName });
13561
+ }, {
13562
+ fallback: (error) => {
13563
+ const message = "StrategyConnectionService CREATE_COMMIT_SIGNAL_EVENT_FN thrown";
13564
+ const payload = {
13565
+ error: functoolsKit.errorData(error),
13566
+ message: functoolsKit.getErrorMessage(error),
13567
+ };
13568
+ self.loggerService.warn(message, payload);
13569
+ console.warn(message, payload);
13570
+ errorEmitter.next(error);
13571
+ },
13572
+ defaultValue: null,
13573
+ });
13168
13574
  /**
13169
13575
  * Creates a callback function for emitting idle ping events.
13170
13576
  *
@@ -13457,6 +13863,8 @@ class StrategyConnectionService {
13457
13863
  callbacks,
13458
13864
  onInit: CREATE_COMMIT_INIT_FN(this),
13459
13865
  onSchedulePing: CREATE_COMMIT_SCHEDULE_PING_FN(this),
13866
+ onScheduleEvent: CREATE_COMMIT_SCHEDULE_EVENT_FN(this),
13867
+ onSignalEvent: CREATE_COMMIT_SIGNAL_EVENT_FN(this),
13460
13868
  onActivePing: CREATE_COMMIT_ACTIVE_PING_FN(this),
13461
13869
  onIdlePing: CREATE_COMMIT_IDLE_PING_FN(this),
13462
13870
  onDispose: CREATE_COMMIT_DISPOSE_FN(this),
@@ -14476,6 +14884,50 @@ class StrategyConnectionService {
14476
14884
  const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
14477
14885
  await strategy.createSignal(symbol, currentPrice, dto);
14478
14886
  };
14887
+ /**
14888
+ * Reports that the pending position's take-profit order was actually filled on the exchange
14889
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based TP check.
14890
+ *
14891
+ * Delegates to ClientStrategy.createTakeProfit(). The close is deferred and emitted with
14892
+ * closeReason "take_profit" on the next tick()/backtest(). Works out of the execution context.
14893
+ *
14894
+ * @param backtest - Whether running in backtest mode
14895
+ * @param symbol - Trading pair symbol
14896
+ * @param context - Context with strategyName, exchangeName, frameName
14897
+ * @param payload - Optional commit payload with id and note
14898
+ * @returns Promise that resolves when the take-profit fill is queued
14899
+ */
14900
+ this.createTakeProfit = async (backtest, symbol, context, payload = {}) => {
14901
+ this.loggerService.log("strategyConnectionService createTakeProfit", {
14902
+ symbol,
14903
+ context,
14904
+ payload,
14905
+ });
14906
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
14907
+ await strategy.createTakeProfit(symbol, backtest, payload);
14908
+ };
14909
+ /**
14910
+ * Reports that the pending position's stop-loss order was actually filled on the exchange
14911
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based SL check.
14912
+ *
14913
+ * Delegates to ClientStrategy.createStopLoss(). The close is deferred and emitted with
14914
+ * closeReason "stop_loss" on the next tick()/backtest(). Works out of the execution context.
14915
+ *
14916
+ * @param backtest - Whether running in backtest mode
14917
+ * @param symbol - Trading pair symbol
14918
+ * @param context - Context with strategyName, exchangeName, frameName
14919
+ * @param payload - Optional commit payload with id and note
14920
+ * @returns Promise that resolves when the stop-loss fill is queued
14921
+ */
14922
+ this.createStopLoss = async (backtest, symbol, context, payload = {}) => {
14923
+ this.loggerService.log("strategyConnectionService createStopLoss", {
14924
+ symbol,
14925
+ context,
14926
+ payload,
14927
+ });
14928
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
14929
+ await strategy.createStopLoss(symbol, backtest, payload);
14930
+ };
14479
14931
  /**
14480
14932
  * Returns the in-memory deferred strategy-state snapshot for this iteration.
14481
14933
  *
@@ -16119,6 +16571,54 @@ const CALL_PING_IDLE_FN = functoolsKit.trycatch(async (event, self) => {
16119
16571
  },
16120
16572
  defaultValue: null,
16121
16573
  });
16574
+ /**
16575
+ * Wrapper to call scheduleEvent method with error capture.
16576
+ *
16577
+ * Unlike CALL_PING_SCHEDULED_FN there is no hasScheduledSignal guard: the "cancelled" action fires
16578
+ * exactly while the scheduled signal is being removed, so guarding on its presence would suppress it.
16579
+ */
16580
+ const CALL_SCHEDULE_EVENT_FN = functoolsKit.trycatch(async (event, self) => {
16581
+ if (!self._target.scheduleEvent) {
16582
+ return;
16583
+ }
16584
+ return await self._target.scheduleEvent(event);
16585
+ }, {
16586
+ fallback: (error) => {
16587
+ const message = "ActionProxy.scheduleEvent thrown";
16588
+ const payload = {
16589
+ error: functoolsKit.errorData(error),
16590
+ message: functoolsKit.getErrorMessage(error),
16591
+ };
16592
+ LOGGER_SERVICE$5.warn(message, payload);
16593
+ console.warn(message, payload);
16594
+ errorEmitter.next(error);
16595
+ },
16596
+ defaultValue: null,
16597
+ });
16598
+ /**
16599
+ * Wrapper to call pendingEvent method with error capture.
16600
+ *
16601
+ * Like CALL_SCHEDULE_EVENT_FN there is no hasPendingSignal guard: the "closed" action fires exactly
16602
+ * while the pending signal is being removed, so guarding on its presence would suppress it.
16603
+ */
16604
+ const CALL_PENDING_EVENT_FN = functoolsKit.trycatch(async (event, self) => {
16605
+ if (!self._target.pendingEvent) {
16606
+ return;
16607
+ }
16608
+ return await self._target.pendingEvent(event);
16609
+ }, {
16610
+ fallback: (error) => {
16611
+ const message = "ActionProxy.pendingEvent thrown";
16612
+ const payload = {
16613
+ error: functoolsKit.errorData(error),
16614
+ message: functoolsKit.getErrorMessage(error),
16615
+ };
16616
+ LOGGER_SERVICE$5.warn(message, payload);
16617
+ console.warn(message, payload);
16618
+ errorEmitter.next(error);
16619
+ },
16620
+ defaultValue: null,
16621
+ });
16122
16622
  /**
16123
16623
  * Wrapper to call pingActive method with error capture.
16124
16624
  */
@@ -16345,6 +16845,31 @@ class ActionProxy {
16345
16845
  async pingScheduled(event) {
16346
16846
  return await CALL_PING_SCHEDULED_FN(event, this);
16347
16847
  }
16848
+ /**
16849
+ * Handles scheduled signal lifecycle events with error capture.
16850
+ *
16851
+ * Wraps the user's scheduleEvent() method to catch and log any errors. Called once when a
16852
+ * scheduled signal is created (action "scheduled") and once when it is cancelled before
16853
+ * activation (action "cancelled").
16854
+ *
16855
+ * @param event - Scheduled lifecycle data (action discriminates created vs cancelled)
16856
+ * @returns Promise resolving to user's scheduleEvent() result or null on error
16857
+ */
16858
+ async scheduleEvent(event) {
16859
+ return await CALL_SCHEDULE_EVENT_FN(event, this);
16860
+ }
16861
+ /**
16862
+ * Handles pending signal lifecycle events with error capture.
16863
+ *
16864
+ * Wraps the user's pendingEvent() method to catch and log any errors. Called once when a
16865
+ * pending position is opened (action "opened") and once when it is closed (action "closed").
16866
+ *
16867
+ * @param event - Pending lifecycle data (action discriminates opened vs closed)
16868
+ * @returns Promise resolving to user's pendingEvent() result or null on error
16869
+ */
16870
+ async pendingEvent(event) {
16871
+ return await CALL_PENDING_EVENT_FN(event, this);
16872
+ }
16348
16873
  /**
16349
16874
  * Handles active ping events with error capture.
16350
16875
  *
@@ -16403,14 +16928,14 @@ class ActionProxy {
16403
16928
  *
16404
16929
  * @param event - Pending-ping event with action "signal-ping"
16405
16930
  */
16406
- async orderPing(event) {
16407
- if (this._target.orderPing) {
16408
- console.error("Action::orderPing is unwanted cause exchange integration should be implemented in Broker.useBrokerAdapter as an infrastructure domain layer");
16409
- console.error("If you need to check whether the order is still open on the exchange, please use Broker.useBrokerAdapter with onOrderPing");
16410
- console.error("If Action::orderPing throws the framework will close the position with closeReason \"closed\"!");
16931
+ async orderCheck(event) {
16932
+ if (this._target.orderCheck) {
16933
+ console.error("Action::orderCheck is unwanted cause exchange integration should be implemented in Broker.useBrokerAdapter as an infrastructure domain layer");
16934
+ console.error("If you need to check whether the order is still open on the exchange, please use Broker.useBrokerAdapter with onOrderCheck");
16935
+ console.error("If Action::orderCheck throws the framework will close the position with closeReason \"closed\"!");
16411
16936
  console.error("");
16412
16937
  console.error("You have been warned!");
16413
- await this._target.orderPing(event);
16938
+ await this._target.orderCheck(event);
16414
16939
  }
16415
16940
  }
16416
16941
  /**
@@ -16572,6 +17097,40 @@ const CALL_PING_SCHEDULED_CALLBACK_FN = functoolsKit.trycatch(async (self, event
16572
17097
  errorEmitter.next(error);
16573
17098
  },
16574
17099
  });
17100
+ /** Wrapper to call scheduled lifecycle (creation / cancellation) callback with error handling */
17101
+ const CALL_SCHEDULE_EVENT_CALLBACK_FN = functoolsKit.trycatch(async (self, event, strategyName, frameName, backtest) => {
17102
+ if (self.params.callbacks?.onScheduleEvent) {
17103
+ await self.params.callbacks.onScheduleEvent(event, self.params.actionName, strategyName, frameName, backtest);
17104
+ }
17105
+ }, {
17106
+ fallback: (error, self) => {
17107
+ const message = "ClientAction CALL_SCHEDULE_EVENT_CALLBACK_FN thrown";
17108
+ const payload = {
17109
+ error: functoolsKit.errorData(error),
17110
+ message: functoolsKit.getErrorMessage(error),
17111
+ };
17112
+ self.params.logger.warn(message, payload);
17113
+ console.warn(message, payload);
17114
+ errorEmitter.next(error);
17115
+ },
17116
+ });
17117
+ /** Wrapper to call pending lifecycle (open / close) callback with error handling */
17118
+ const CALL_PENDING_EVENT_CALLBACK_FN = functoolsKit.trycatch(async (self, event, strategyName, frameName, backtest) => {
17119
+ if (self.params.callbacks?.onPendingEvent) {
17120
+ await self.params.callbacks.onPendingEvent(event, self.params.actionName, strategyName, frameName, backtest);
17121
+ }
17122
+ }, {
17123
+ fallback: (error, self) => {
17124
+ const message = "ClientAction CALL_PENDING_EVENT_CALLBACK_FN thrown";
17125
+ const payload = {
17126
+ error: functoolsKit.errorData(error),
17127
+ message: functoolsKit.getErrorMessage(error),
17128
+ };
17129
+ self.params.logger.warn(message, payload);
17130
+ console.warn(message, payload);
17131
+ errorEmitter.next(error);
17132
+ },
17133
+ });
16575
17134
  /** Wrapper to call idle ping callback with error handling */
16576
17135
  const CALL_PING_IDLE_CALLBACK_FN = functoolsKit.trycatch(async (self, event, strategyName, frameName, backtest) => {
16577
17136
  if (self.params.callbacks?.onPingIdle) {
@@ -16633,12 +17192,12 @@ const CALL_SIGNAL_SYNC_CALLBACK_FN = async (self, event, strategyName, frameName
16633
17192
  }
16634
17193
  };
16635
17194
  /**
16636
- * Calls onOrderPing callback WITHOUT trycatch — exceptions must propagate
17195
+ * Calls onOrderCheck callback WITHOUT trycatch — exceptions must propagate
16637
17196
  * up to CREATE_SYNC_PENDING_FN in StrategyConnectionService (which returns false on error).
16638
17197
  */
16639
17198
  const CALL_ORDER_PING_CALLBACK_FN = async (self, event, strategyName, frameName, isBacktest) => {
16640
- if (self.params.callbacks?.onOrderPing) {
16641
- await self.params.callbacks.onOrderPing(event, self.params.actionName, strategyName, frameName, isBacktest);
17199
+ if (self.params.callbacks?.onOrderCheck) {
17200
+ await self.params.callbacks.onOrderCheck(event, self.params.actionName, strategyName, frameName, isBacktest);
16642
17201
  }
16643
17202
  };
16644
17203
  /** Wrapper to call onInit callback with error handling */
@@ -16946,6 +17505,52 @@ class ClientAction {
16946
17505
  await CALL_PING_SCHEDULED_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
16947
17506
  }
16948
17507
  ;
17508
+ /**
17509
+ * Handles scheduled signal lifecycle events (creation / cancellation).
17510
+ *
17511
+ * Manual wiring — EVENT-BASED: users implement {@link IActionCallbacks.onScheduleEvent} (via `addActionSchema`)
17512
+ * to drive the exchange (`commitActivateScheduled` / `commitCancelScheduled`); see that contract
17513
+ * for the full guidance and example. This internal dispatch forwards to the handler/callback.
17514
+ */
17515
+ async scheduleEvent(event) {
17516
+ this.params.logger.debug("ClientAction scheduleEvent", {
17517
+ actionName: this.params.actionName,
17518
+ strategyName: this.params.strategyName,
17519
+ frameName: this.params.frameName,
17520
+ action: event.action,
17521
+ });
17522
+ if (!this._handlerInstance) {
17523
+ await this.waitForInit();
17524
+ }
17525
+ // Call handler method if defined
17526
+ await this._handlerInstance?.scheduleEvent(event);
17527
+ // Call callback if defined
17528
+ await CALL_SCHEDULE_EVENT_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
17529
+ }
17530
+ ;
17531
+ /**
17532
+ * Handles pending signal lifecycle events (open / close).
17533
+ *
17534
+ * Manual wiring — EVENT-BASED: users implement {@link IActionCallbacks.onPendingEvent} (via `addActionSchema`) to
17535
+ * drive the exchange; for per-tick fills use `onPingActive`. See that contract for the full
17536
+ * guidance and example. This internal dispatch forwards to the handler/callback.
17537
+ */
17538
+ async pendingEvent(event) {
17539
+ this.params.logger.debug("ClientAction pendingEvent", {
17540
+ actionName: this.params.actionName,
17541
+ strategyName: this.params.strategyName,
17542
+ frameName: this.params.frameName,
17543
+ action: event.action,
17544
+ });
17545
+ if (!this._handlerInstance) {
17546
+ await this.waitForInit();
17547
+ }
17548
+ // Call handler method if defined
17549
+ await this._handlerInstance?.pendingEvent(event);
17550
+ // Call callback if defined
17551
+ await CALL_PENDING_EVENT_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
17552
+ }
17553
+ ;
16949
17554
  /**
16950
17555
  * Handles active ping events during active pending signal monitoring.
16951
17556
  */
@@ -17023,8 +17628,8 @@ class ClientAction {
17023
17628
  * Gate for the pending-order ping (order still open on exchange?).
17024
17629
  * NOT wrapped in trycatch — exceptions propagate to CREATE_SYNC_PENDING_FN.
17025
17630
  */
17026
- async orderPing(event) {
17027
- this.params.logger.debug("ClientAction orderPing", {
17631
+ async orderCheck(event) {
17632
+ this.params.logger.debug("ClientAction orderCheck", {
17028
17633
  actionName: this.params.actionName,
17029
17634
  strategyName: this.params.strategyName,
17030
17635
  frameName: this.params.frameName,
@@ -17033,7 +17638,7 @@ class ClientAction {
17033
17638
  await this.waitForInit();
17034
17639
  }
17035
17640
  // Call handler method if defined — exceptions propagate
17036
- await this._handlerInstance?.orderPing(event);
17641
+ await this._handlerInstance?.orderCheck(event);
17037
17642
  // Call callback if defined — exceptions propagate
17038
17643
  await CALL_ORDER_PING_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
17039
17644
  }
@@ -17238,6 +17843,36 @@ class ActionConnectionService {
17238
17843
  const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
17239
17844
  await action.pingScheduled(event);
17240
17845
  };
17846
+ /**
17847
+ * Routes a scheduled signal lifecycle event (creation / cancellation) to the ClientAction instance.
17848
+ *
17849
+ * @param event - Scheduled lifecycle event data (action discriminates created vs cancelled)
17850
+ * @param backtest - Whether running in backtest mode
17851
+ * @param context - Execution context with action name, strategy name, exchange name, frame name
17852
+ */
17853
+ this.scheduleEvent = async (event, backtest, context) => {
17854
+ this.loggerService.log("actionConnectionService scheduleEvent", {
17855
+ backtest,
17856
+ context,
17857
+ });
17858
+ const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
17859
+ await action.scheduleEvent(event);
17860
+ };
17861
+ /**
17862
+ * Routes a pending signal lifecycle event (open / close) to the ClientAction instance.
17863
+ *
17864
+ * @param event - Pending lifecycle event data (action discriminates opened vs closed)
17865
+ * @param backtest - Whether running in backtest mode
17866
+ * @param context - Execution context with action name, strategy name, exchange name, frame name
17867
+ */
17868
+ this.pendingEvent = async (event, backtest, context) => {
17869
+ this.loggerService.log("actionConnectionService pendingEvent", {
17870
+ backtest,
17871
+ context,
17872
+ });
17873
+ const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
17874
+ await action.pendingEvent(event);
17875
+ };
17241
17876
  /**
17242
17877
  * Routes active ping event to appropriate ClientAction instance.
17243
17878
  *
@@ -17301,20 +17936,20 @@ class ActionConnectionService {
17301
17936
  await action.signalSync(event);
17302
17937
  };
17303
17938
  /**
17304
- * Routes orderPing event to appropriate ClientAction instance.
17939
+ * Routes orderCheck event to appropriate ClientAction instance.
17305
17940
  * NOT wrapped in trycatch — exceptions propagate to CREATE_SYNC_PENDING_FN.
17306
17941
  *
17307
17942
  * @param event - Pending-ping event with action "signal-ping"
17308
17943
  * @param backtest - Whether running in backtest mode
17309
17944
  * @param context - Execution context
17310
17945
  */
17311
- this.orderPing = async (event, backtest, context) => {
17312
- this.loggerService.log("actionConnectionService orderPing", {
17946
+ this.orderCheck = async (event, backtest, context) => {
17947
+ this.loggerService.log("actionConnectionService orderCheck", {
17313
17948
  backtest,
17314
17949
  context,
17315
17950
  });
17316
17951
  const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
17317
- await action.orderPing(event);
17952
+ await action.orderCheck(event);
17318
17953
  };
17319
17954
  /**
17320
17955
  * Disposes the ClientAction instance for the given action name.
@@ -18182,6 +18817,54 @@ class StrategyCoreService {
18182
18817
  await this.validate(context);
18183
18818
  return await this.strategyConnectionService.createSignal(backtest, symbol, dto, context);
18184
18819
  };
18820
+ /**
18821
+ * Reports that the pending position's take-profit order was actually filled on the exchange
18822
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based TP check.
18823
+ *
18824
+ * Validates the context, then delegates to StrategyConnectionService.createTakeProfit().
18825
+ * The close is deferred and emitted with closeReason "take_profit" on the next tick/backtest.
18826
+ * Does not require execution context.
18827
+ *
18828
+ * @param backtest - Whether running in backtest mode
18829
+ * @param symbol - Trading pair symbol
18830
+ * @param context - Context with strategyName, exchangeName, frameName
18831
+ * @param payload - Optional commit payload with id and note
18832
+ * @returns Promise that resolves when the take-profit fill is queued
18833
+ */
18834
+ this.createTakeProfit = async (backtest, symbol, context, payload = {}) => {
18835
+ this.loggerService.log("strategyCoreService createTakeProfit", {
18836
+ symbol,
18837
+ context,
18838
+ backtest,
18839
+ payload,
18840
+ });
18841
+ await this.validate(context);
18842
+ return await this.strategyConnectionService.createTakeProfit(backtest, symbol, context, payload);
18843
+ };
18844
+ /**
18845
+ * Reports that the pending position's stop-loss order was actually filled on the exchange
18846
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based SL check.
18847
+ *
18848
+ * Validates the context, then delegates to StrategyConnectionService.createStopLoss().
18849
+ * The close is deferred and emitted with closeReason "stop_loss" on the next tick/backtest.
18850
+ * Does not require execution context.
18851
+ *
18852
+ * @param backtest - Whether running in backtest mode
18853
+ * @param symbol - Trading pair symbol
18854
+ * @param context - Context with strategyName, exchangeName, frameName
18855
+ * @param payload - Optional commit payload with id and note
18856
+ * @returns Promise that resolves when the stop-loss fill is queued
18857
+ */
18858
+ this.createStopLoss = async (backtest, symbol, context, payload = {}) => {
18859
+ this.loggerService.log("strategyCoreService createStopLoss", {
18860
+ symbol,
18861
+ context,
18862
+ backtest,
18863
+ payload,
18864
+ });
18865
+ await this.validate(context);
18866
+ return await this.strategyConnectionService.createStopLoss(backtest, symbol, context, payload);
18867
+ };
18185
18868
  /**
18186
18869
  * Returns the in-memory deferred strategy-state snapshot for this iteration.
18187
18870
  *
@@ -19458,6 +20141,48 @@ class ActionCoreService {
19458
20141
  await this.actionConnectionService.pingScheduled(event, backtest, { actionName, ...context });
19459
20142
  }
19460
20143
  };
20144
+ /**
20145
+ * Routes a scheduled signal lifecycle event (creation / cancellation) to all registered actions.
20146
+ *
20147
+ * Retrieves action list from strategy schema (IStrategySchema.actions) and invokes the
20148
+ * scheduleEvent handler on each ClientAction instance sequentially. Called once on creation
20149
+ * (action "scheduled") and once on cancellation before activation (action "cancelled").
20150
+ *
20151
+ * @param backtest - Whether running in backtest mode (true) or live mode (false)
20152
+ * @param event - Scheduled lifecycle event data (action discriminates created vs cancelled)
20153
+ * @param context - Strategy execution context with strategyName, exchangeName, frameName
20154
+ */
20155
+ this.scheduleEvent = async (backtest, event, context) => {
20156
+ this.loggerService.log("actionCoreService scheduleEvent", {
20157
+ context,
20158
+ });
20159
+ await this.validate(context);
20160
+ const { actions = [] } = this.strategySchemaService.get(context.strategyName);
20161
+ for (const actionName of actions) {
20162
+ await this.actionConnectionService.scheduleEvent(event, backtest, { actionName, ...context });
20163
+ }
20164
+ };
20165
+ /**
20166
+ * Routes a pending signal lifecycle event (open / close) to all registered actions.
20167
+ *
20168
+ * Retrieves action list from strategy schema (IStrategySchema.actions) and invokes the
20169
+ * pendingEvent handler on each ClientAction instance sequentially. Called once on open
20170
+ * (action "opened") and once on close (action "closed").
20171
+ *
20172
+ * @param backtest - Whether running in backtest mode (true) or live mode (false)
20173
+ * @param event - Pending lifecycle event data (action discriminates opened vs closed)
20174
+ * @param context - Strategy execution context with strategyName, exchangeName, frameName
20175
+ */
20176
+ this.pendingEvent = async (backtest, event, context) => {
20177
+ this.loggerService.log("actionCoreService pendingEvent", {
20178
+ context,
20179
+ });
20180
+ await this.validate(context);
20181
+ const { actions = [] } = this.strategySchemaService.get(context.strategyName);
20182
+ for (const actionName of actions) {
20183
+ await this.actionConnectionService.pendingEvent(event, backtest, { actionName, ...context });
20184
+ }
20185
+ };
19461
20186
  /**
19462
20187
  * Routes active ping event to all registered actions for the strategy.
19463
20188
  *
@@ -19548,14 +20273,14 @@ class ActionCoreService {
19548
20273
  * @param event - Pending-ping event with action "signal-ping"
19549
20274
  * @param context - Strategy execution context
19550
20275
  */
19551
- this.orderPing = async (backtest, event, context) => {
19552
- this.loggerService.log("actionCoreService orderPing", {
20276
+ this.orderCheck = async (backtest, event, context) => {
20277
+ this.loggerService.log("actionCoreService orderCheck", {
19553
20278
  context,
19554
20279
  });
19555
20280
  await this.validate(context);
19556
20281
  const { actions = [] } = this.strategySchemaService.get(context.strategyName);
19557
20282
  for (const actionName of actions) {
19558
- await this.actionConnectionService.orderPing(event, backtest, { actionName, ...context });
20283
+ await this.actionConnectionService.orderCheck(event, backtest, { actionName, ...context });
19559
20284
  }
19560
20285
  };
19561
20286
  /**
@@ -20023,6 +20748,8 @@ const VALID_METHOD_NAMES = [
20023
20748
  "partialLossAvailable",
20024
20749
  "pingScheduled",
20025
20750
  "pingActive",
20751
+ "scheduleEvent",
20752
+ "pendingEvent",
20026
20753
  "riskRejection",
20027
20754
  "dispose",
20028
20755
  ];
@@ -20033,7 +20760,7 @@ const VALID_METHOD_NAMES = [
20033
20760
  * method one of these produces a dedicated error redirecting to the Broker adapter instead of the
20034
20761
  * generic "invalid method" suggestion.
20035
20762
  */
20036
- const DISCOURAGED_METHOD_NAMES = ["signalSync", "orderPing"];
20763
+ const DISCOURAGED_METHOD_NAMES = ["signalSync", "orderCheck"];
20037
20764
  /**
20038
20765
  * Builds the dedicated error message for a discouraged exchange-integration handler method.
20039
20766
  *
@@ -20045,7 +20772,7 @@ const DISCOURAGED_METHOD_MESSAGE = (actionName, methodName) => functoolsKit.str.
20045
20772
  `ActionSchema ${actionName} contains discouraged method "${methodName}". `,
20046
20773
  `Exchange integration must be implemented in Broker.useBrokerAdapter as the infrastructure domain layer, not in an action handler.`,
20047
20774
  functoolsKit.typo.nbsp,
20048
- `Use Broker.useBrokerAdapter with onOrderPing (order still open?) and onSignalOpenCommit / onSignalCloseCommit (order fill) instead.`,
20775
+ `Use Broker.useBrokerAdapter with onOrderCheck (order still open?) and onSignalOpenCommit / onSignalCloseCommit (order fill) instead.`,
20049
20776
  functoolsKit.typo.nbsp,
20050
20777
  `If you want to keep this property name use one of these patterns: _${methodName} or #${methodName}`,
20051
20778
  ]);
@@ -42424,6 +43151,13 @@ const breakevenNewTakeProfitPrice = (priceTakeProfit, trailingPriceTakeProfit) =
42424
43151
  const BROKER_METHOD_NAME_COMMIT_SIGNAL_OPEN = "BrokerAdapter.commitSignalOpen";
42425
43152
  const BROKER_METHOD_NAME_COMMIT_SIGNAL_CLOSE = "BrokerAdapter.commitSignalClose";
42426
43153
  const BROKER_METHOD_NAME_COMMIT_SIGNAL_PENDING = "BrokerAdapter.commitSignalPending";
43154
+ const BROKER_METHOD_NAME_COMMIT_ACTIVE_PING = "BrokerAdapter.commitActivePing";
43155
+ const BROKER_METHOD_NAME_COMMIT_SCHEDULE_PING = "BrokerAdapter.commitSchedulePing";
43156
+ const BROKER_METHOD_NAME_COMMIT_IDLE_PING = "BrokerAdapter.commitIdlePing";
43157
+ const BROKER_METHOD_NAME_COMMIT_SCHEDULE_OPEN = "BrokerAdapter.commitScheduleOpen";
43158
+ const BROKER_METHOD_NAME_COMMIT_SCHEDULE_CANCELLED = "BrokerAdapter.commitScheduleCancelled";
43159
+ const BROKER_METHOD_NAME_COMMIT_PENDING_OPEN = "BrokerAdapter.commitPendingOpen";
43160
+ const BROKER_METHOD_NAME_COMMIT_PENDING_CLOSE = "BrokerAdapter.commitPendingClose";
42427
43161
  const BROKER_METHOD_NAME_COMMIT_PARTIAL_PROFIT = "BrokerAdapter.commitPartialProfit";
42428
43162
  const BROKER_METHOD_NAME_COMMIT_PARTIAL_LOSS = "BrokerAdapter.commitPartialLoss";
42429
43163
  const BROKER_METHOD_NAME_COMMIT_TRAILING_STOP = "BrokerAdapter.commitTrailingStop";
@@ -42437,7 +43171,14 @@ const BROKER_METHOD_NAME_CLEAR = "BrokerAdapter.clear";
42437
43171
  const BROKER_BASE_METHOD_NAME_WAIT_FOR_INIT = "BrokerBase.waitForInit";
42438
43172
  const BROKER_BASE_METHOD_NAME_ON_SIGNAL_OPEN = "BrokerBase.onSignalOpenCommit";
42439
43173
  const BROKER_BASE_METHOD_NAME_ON_SIGNAL_CLOSE = "BrokerBase.onSignalCloseCommit";
42440
- const BROKER_BASE_METHOD_NAME_ON_SIGNAL_PENDING = "BrokerBase.onOrderPing";
43174
+ const BROKER_BASE_METHOD_NAME_ON_SIGNAL_PENDING = "BrokerBase.onOrderCheck";
43175
+ const BROKER_BASE_METHOD_NAME_ON_ACTIVE_PING = "BrokerBase.onSignalActivePing";
43176
+ const BROKER_BASE_METHOD_NAME_ON_SCHEDULE_PING = "BrokerBase.onSignalSchedulePing";
43177
+ const BROKER_BASE_METHOD_NAME_ON_IDLE_PING = "BrokerBase.onSignalIdlePing";
43178
+ const BROKER_BASE_METHOD_NAME_ON_SCHEDULE_OPEN = "BrokerBase.onSignalScheduleOpen";
43179
+ const BROKER_BASE_METHOD_NAME_ON_SCHEDULE_CANCELLED = "BrokerBase.onSignalScheduleCancelled";
43180
+ const BROKER_BASE_METHOD_NAME_ON_PENDING_OPEN = "BrokerBase.onSignalPendingOpen";
43181
+ const BROKER_BASE_METHOD_NAME_ON_PENDING_CLOSE = "BrokerBase.onSignalPendingClose";
42441
43182
  const BROKER_BASE_METHOD_NAME_ON_PARTIAL_PROFIT = "BrokerBase.onPartialProfitCommit";
42442
43183
  const BROKER_BASE_METHOD_NAME_ON_PARTIAL_LOSS = "BrokerBase.onPartialLossCommit";
42443
43184
  const BROKER_BASE_METHOD_NAME_ON_TRAILING_STOP = "BrokerBase.onTrailingStopCommit";
@@ -42489,7 +43230,7 @@ class BrokerProxy {
42489
43230
  /**
42490
43231
  * Forwards a pending-order ping to the underlying adapter.
42491
43232
  *
42492
- * If the adapter does not implement `onOrderPing`, the call is silently skipped
43233
+ * If the adapter does not implement `onOrderCheck`, the call is silently skipped
42493
43234
  * (the order is assumed still open). When implemented, exceptions propagate — a throw means
42494
43235
  * the order was NOT FOUND by `payload.signalId` and the framework closes the position with
42495
43236
  * closeReason "closed". The adapter must throw ONLY on a confirmed "order not found by id"
@@ -42498,10 +43239,101 @@ class BrokerProxy {
42498
43239
  *
42499
43240
  * @param payload - Pending ping details: symbol, signalId, position, prices, pnl, context, backtest flag.
42500
43241
  */
42501
- async onOrderPing(payload) {
42502
- if (this._instance.onOrderPing) {
43242
+ async onOrderCheck(payload) {
43243
+ if (this._instance.onOrderCheck) {
43244
+ await this.waitForInit();
43245
+ await this._instance.onOrderCheck(payload);
43246
+ return;
43247
+ }
43248
+ }
43249
+ /**
43250
+ * Forwards an active-ping event to the underlying adapter.
43251
+ * Silently skipped when the adapter does not implement `onSignalActivePing`.
43252
+ *
43253
+ * @param payload - Active ping details: symbol, signalId, position, prices, pnl, context, backtest.
43254
+ */
43255
+ async onSignalActivePing(payload) {
43256
+ if (this._instance.onSignalActivePing) {
43257
+ await this.waitForInit();
43258
+ await this._instance.onSignalActivePing(payload);
43259
+ return;
43260
+ }
43261
+ }
43262
+ /**
43263
+ * Forwards a schedule-ping event to the underlying adapter.
43264
+ * Silently skipped when the adapter does not implement `onSignalSchedulePing`.
43265
+ *
43266
+ * @param payload - Schedule ping details: symbol, signalId, position, prices, context, backtest.
43267
+ */
43268
+ async onSignalSchedulePing(payload) {
43269
+ if (this._instance.onSignalSchedulePing) {
43270
+ await this.waitForInit();
43271
+ await this._instance.onSignalSchedulePing(payload);
43272
+ return;
43273
+ }
43274
+ }
43275
+ /**
43276
+ * Forwards an idle-ping event to the underlying adapter.
43277
+ * Silently skipped when the adapter does not implement `onSignalIdlePing`.
43278
+ *
43279
+ * @param payload - Idle ping details: symbol, currentPrice, context, backtest.
43280
+ */
43281
+ async onSignalIdlePing(payload) {
43282
+ if (this._instance.onSignalIdlePing) {
43283
+ await this.waitForInit();
43284
+ await this._instance.onSignalIdlePing(payload);
43285
+ return;
43286
+ }
43287
+ }
43288
+ /**
43289
+ * Forwards a scheduled-signal-open event to the underlying adapter.
43290
+ * Silently skipped when the adapter does not implement `onSignalScheduleOpen`.
43291
+ *
43292
+ * @param payload - Scheduled open details: symbol, signalId, position, prices, context, backtest.
43293
+ */
43294
+ async onSignalScheduleOpen(payload) {
43295
+ if (this._instance.onSignalScheduleOpen) {
43296
+ await this.waitForInit();
43297
+ await this._instance.onSignalScheduleOpen(payload);
43298
+ return;
43299
+ }
43300
+ }
43301
+ /**
43302
+ * Forwards a scheduled-signal-cancelled event to the underlying adapter.
43303
+ * Silently skipped when the adapter does not implement `onSignalScheduleCancelled`.
43304
+ *
43305
+ * @param payload - Scheduled cancel details: symbol, signalId, position, prices, reason, context, backtest.
43306
+ */
43307
+ async onSignalScheduleCancelled(payload) {
43308
+ if (this._instance.onSignalScheduleCancelled) {
43309
+ await this.waitForInit();
43310
+ await this._instance.onSignalScheduleCancelled(payload);
43311
+ return;
43312
+ }
43313
+ }
43314
+ /**
43315
+ * Forwards a pending-signal-open event to the underlying adapter.
43316
+ * Silently skipped when the adapter does not implement `onSignalPendingOpen`.
43317
+ *
43318
+ * @param payload - Pending open details: symbol, signalId, position, prices, context, backtest.
43319
+ */
43320
+ async onSignalPendingOpen(payload) {
43321
+ if (this._instance.onSignalPendingOpen) {
42503
43322
  await this.waitForInit();
42504
- await this._instance.onOrderPing(payload);
43323
+ await this._instance.onSignalPendingOpen(payload);
43324
+ return;
43325
+ }
43326
+ }
43327
+ /**
43328
+ * Forwards a pending-signal-close event to the underlying adapter.
43329
+ * Silently skipped when the adapter does not implement `onSignalPendingClose`.
43330
+ *
43331
+ * @param payload - Pending close details: symbol, signalId, position, prices, closeReason, context, backtest.
43332
+ */
43333
+ async onSignalPendingClose(payload) {
43334
+ if (this._instance.onSignalPendingClose) {
43335
+ await this.waitForInit();
43336
+ await this._instance.onSignalPendingClose(payload);
42505
43337
  return;
42506
43338
  }
42507
43339
  }
@@ -42757,7 +43589,178 @@ class BrokerAdapter {
42757
43589
  }
42758
43590
  const instance = this.getInstance();
42759
43591
  if (instance) {
42760
- await instance.onOrderPing(payload);
43592
+ await instance.onOrderCheck(payload);
43593
+ }
43594
+ };
43595
+ /**
43596
+ * Forwards an active-ping to the registered broker adapter.
43597
+ *
43598
+ * Called automatically via activePingSubject when `enable()` is active, on every live tick while a
43599
+ * pending signal is monitored. Skipped silently in backtest mode or when no adapter is registered.
43600
+ * Purely informational — a throw does NOT close the position.
43601
+ *
43602
+ * @param payload - Active ping details: symbol, signalId, position, prices, pnl, context, backtest
43603
+ */
43604
+ this.commitActivePing = async (payload) => {
43605
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_ACTIVE_PING, {
43606
+ symbol: payload.symbol,
43607
+ context: payload.context,
43608
+ });
43609
+ if (!this.enable.hasValue()) {
43610
+ return;
43611
+ }
43612
+ if (payload.backtest) {
43613
+ return;
43614
+ }
43615
+ const instance = this.getInstance();
43616
+ if (instance) {
43617
+ await instance.onSignalActivePing(payload);
43618
+ }
43619
+ };
43620
+ /**
43621
+ * Forwards a schedule-ping to the registered broker adapter.
43622
+ *
43623
+ * Called automatically via schedulePingSubject when `enable()` is active, on every live tick while
43624
+ * a scheduled signal is monitored. Skipped silently in backtest mode or when no adapter is
43625
+ * registered. Purely informational.
43626
+ *
43627
+ * @param payload - Schedule ping details: symbol, signalId, position, prices, context, backtest
43628
+ */
43629
+ this.commitSchedulePing = async (payload) => {
43630
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_SCHEDULE_PING, {
43631
+ symbol: payload.symbol,
43632
+ context: payload.context,
43633
+ });
43634
+ if (!this.enable.hasValue()) {
43635
+ return;
43636
+ }
43637
+ if (payload.backtest) {
43638
+ return;
43639
+ }
43640
+ const instance = this.getInstance();
43641
+ if (instance) {
43642
+ await instance.onSignalSchedulePing(payload);
43643
+ }
43644
+ };
43645
+ /**
43646
+ * Forwards an idle-ping to the registered broker adapter.
43647
+ *
43648
+ * Called automatically via idlePingSubject when `enable()` is active, on every live tick while the
43649
+ * strategy has no pending or scheduled signal. Skipped silently in backtest mode or when no adapter
43650
+ * is registered. Purely informational.
43651
+ *
43652
+ * @param payload - Idle ping details: symbol, currentPrice, context, backtest
43653
+ */
43654
+ this.commitIdlePing = async (payload) => {
43655
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_IDLE_PING, {
43656
+ symbol: payload.symbol,
43657
+ context: payload.context,
43658
+ });
43659
+ if (!this.enable.hasValue()) {
43660
+ return;
43661
+ }
43662
+ if (payload.backtest) {
43663
+ return;
43664
+ }
43665
+ const instance = this.getInstance();
43666
+ if (instance) {
43667
+ await instance.onSignalIdlePing(payload);
43668
+ }
43669
+ };
43670
+ /**
43671
+ * Forwards a scheduled-signal-open to the registered broker adapter.
43672
+ *
43673
+ * Called automatically via scheduleEventSubject (action "scheduled") when a scheduled signal is
43674
+ * created. Skipped silently in backtest mode or when no adapter is registered.
43675
+ *
43676
+ * @param payload - Scheduled open details: symbol, signalId, position, prices, context, backtest
43677
+ */
43678
+ this.commitScheduleOpen = async (payload) => {
43679
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_SCHEDULE_OPEN, {
43680
+ symbol: payload.symbol,
43681
+ context: payload.context,
43682
+ });
43683
+ if (!this.enable.hasValue()) {
43684
+ return;
43685
+ }
43686
+ if (payload.backtest) {
43687
+ return;
43688
+ }
43689
+ const instance = this.getInstance();
43690
+ if (instance) {
43691
+ await instance.onSignalScheduleOpen(payload);
43692
+ }
43693
+ };
43694
+ /**
43695
+ * Forwards a scheduled-signal-cancelled to the registered broker adapter.
43696
+ *
43697
+ * Called automatically via scheduleEventSubject (action "cancelled") when a scheduled signal is
43698
+ * removed before activation. Skipped silently in backtest mode or when no adapter is registered.
43699
+ *
43700
+ * @param payload - Scheduled cancel details: symbol, signalId, position, prices, reason, context, backtest
43701
+ */
43702
+ this.commitScheduleCancelled = async (payload) => {
43703
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_SCHEDULE_CANCELLED, {
43704
+ symbol: payload.symbol,
43705
+ context: payload.context,
43706
+ });
43707
+ if (!this.enable.hasValue()) {
43708
+ return;
43709
+ }
43710
+ if (payload.backtest) {
43711
+ return;
43712
+ }
43713
+ const instance = this.getInstance();
43714
+ if (instance) {
43715
+ await instance.onSignalScheduleCancelled(payload);
43716
+ }
43717
+ };
43718
+ /**
43719
+ * Forwards a pending-signal-open to the registered broker adapter.
43720
+ *
43721
+ * Called automatically via signalEventSubject (action "opened") when a pending position is opened.
43722
+ * Skipped silently in backtest mode or when no adapter is registered.
43723
+ *
43724
+ * @param payload - Pending open details: symbol, signalId, position, prices, context, backtest
43725
+ */
43726
+ this.commitPendingOpen = async (payload) => {
43727
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_PENDING_OPEN, {
43728
+ symbol: payload.symbol,
43729
+ context: payload.context,
43730
+ });
43731
+ if (!this.enable.hasValue()) {
43732
+ return;
43733
+ }
43734
+ if (payload.backtest) {
43735
+ return;
43736
+ }
43737
+ const instance = this.getInstance();
43738
+ if (instance) {
43739
+ await instance.onSignalPendingOpen(payload);
43740
+ }
43741
+ };
43742
+ /**
43743
+ * Forwards a pending-signal-close to the registered broker adapter.
43744
+ *
43745
+ * Called automatically via signalEventSubject (action "closed") when a pending position is closed.
43746
+ * Skipped silently in backtest mode or when no adapter is registered.
43747
+ *
43748
+ * @param payload - Pending close details: symbol, signalId, position, prices, closeReason, context, backtest
43749
+ */
43750
+ this.commitPendingClose = async (payload) => {
43751
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_PENDING_CLOSE, {
43752
+ symbol: payload.symbol,
43753
+ context: payload.context,
43754
+ });
43755
+ if (!this.enable.hasValue()) {
43756
+ return;
43757
+ }
43758
+ if (payload.backtest) {
43759
+ return;
43760
+ }
43761
+ const instance = this.getInstance();
43762
+ if (instance) {
43763
+ await instance.onSignalPendingClose(payload);
42761
43764
  }
42762
43765
  };
42763
43766
  /**
@@ -43125,7 +44128,98 @@ class BrokerAdapter {
43125
44128
  backtest: event.backtest,
43126
44129
  });
43127
44130
  });
43128
- const disposeFn = functoolsKit.compose(() => unSignalOpen(), () => unSignalClose(), () => unSignalPending());
44131
+ const unActivePing = activePingSubject.subscribe(async (event) => {
44132
+ await this.commitActivePing({
44133
+ symbol: event.symbol,
44134
+ signalId: event.data.id,
44135
+ position: event.data.position,
44136
+ currentPrice: event.currentPrice,
44137
+ priceOpen: event.data.priceOpen,
44138
+ priceTakeProfit: event.data.priceTakeProfit,
44139
+ priceStopLoss: event.data.priceStopLoss,
44140
+ pnl: event.data.pnl,
44141
+ context: {
44142
+ strategyName: event.strategyName,
44143
+ exchangeName: event.exchangeName,
44144
+ frameName: event.frameName,
44145
+ },
44146
+ backtest: event.backtest,
44147
+ });
44148
+ });
44149
+ const unSchedulePing = schedulePingSubject.subscribe(async (event) => {
44150
+ await this.commitSchedulePing({
44151
+ symbol: event.symbol,
44152
+ signalId: event.data.id,
44153
+ position: event.data.position,
44154
+ currentPrice: event.currentPrice,
44155
+ priceOpen: event.data.priceOpen,
44156
+ priceTakeProfit: event.data.priceTakeProfit,
44157
+ priceStopLoss: event.data.priceStopLoss,
44158
+ context: {
44159
+ strategyName: event.strategyName,
44160
+ exchangeName: event.exchangeName,
44161
+ frameName: event.frameName,
44162
+ },
44163
+ backtest: event.backtest,
44164
+ });
44165
+ });
44166
+ const unIdlePing = idlePingSubject.subscribe(async (event) => {
44167
+ await this.commitIdlePing({
44168
+ symbol: event.symbol,
44169
+ currentPrice: event.currentPrice,
44170
+ context: {
44171
+ strategyName: event.strategyName,
44172
+ exchangeName: event.exchangeName,
44173
+ frameName: event.frameName,
44174
+ },
44175
+ backtest: event.backtest,
44176
+ });
44177
+ });
44178
+ const unScheduleEvent = scheduleEventSubject.subscribe(async (event) => {
44179
+ const payload = {
44180
+ symbol: event.symbol,
44181
+ signalId: event.data.id,
44182
+ position: event.data.position,
44183
+ currentPrice: event.currentPrice,
44184
+ priceOpen: event.data.priceOpen,
44185
+ priceTakeProfit: event.data.priceTakeProfit,
44186
+ priceStopLoss: event.data.priceStopLoss,
44187
+ context: {
44188
+ strategyName: event.strategyName,
44189
+ exchangeName: event.exchangeName,
44190
+ frameName: event.frameName,
44191
+ },
44192
+ backtest: event.backtest,
44193
+ };
44194
+ if (event.action === "scheduled") {
44195
+ await this.commitScheduleOpen(payload);
44196
+ return;
44197
+ }
44198
+ await this.commitScheduleCancelled({ ...payload, reason: event.reason });
44199
+ });
44200
+ const unSignalEvent = signalEventSubject.subscribe(async (event) => {
44201
+ const payload = {
44202
+ symbol: event.symbol,
44203
+ signalId: event.data.id,
44204
+ position: event.data.position,
44205
+ currentPrice: event.currentPrice,
44206
+ priceOpen: event.data.priceOpen,
44207
+ priceTakeProfit: event.data.priceTakeProfit,
44208
+ priceStopLoss: event.data.priceStopLoss,
44209
+ context: {
44210
+ strategyName: event.strategyName,
44211
+ exchangeName: event.exchangeName,
44212
+ frameName: event.frameName,
44213
+ },
44214
+ backtest: event.backtest,
44215
+ };
44216
+ if (event.action === "opened") {
44217
+ await this.commitPendingOpen(payload);
44218
+ return;
44219
+ }
44220
+ await this.commitPendingClose({ ...payload, closeReason: event.closeReason });
44221
+ });
44222
+ const disposeFn = functoolsKit.compose(() => unSignalOpen(), () => unSignalClose(), () => unSignalPending(), () => unActivePing(), () => unSchedulePing(), () => unIdlePing(), () => unScheduleEvent(), () => unSignalEvent());
43129
44223
  return () => {
43130
44224
  this.enable.clear();
43131
44225
  disposeFn();
@@ -43271,24 +44365,32 @@ class BrokerBase {
43271
44365
  bt.loggerService.info(BROKER_BASE_METHOD_NAME_WAIT_FOR_INIT, {});
43272
44366
  }
43273
44367
  /**
43274
- * Called when a new position is opened (signal activated).
44368
+ * Called when a position is being opened (signal activated).
43275
44369
  *
43276
44370
  * Triggered automatically via syncSubject when a scheduled signal's priceOpen is hit.
43277
44371
  * Use to place the actual entry order on the exchange.
43278
44372
  *
43279
44373
  * Default implementation: Logs signal-open event.
43280
44374
  *
44375
+ * Manual wiring — EXCEPTION-BASED GATE: emitted BEFORE the framework mutates state, so a THROW here
44376
+ * (e.g. limit order rejected) rolls back the open — the pending signal returns to idle and retries
44377
+ * next tick; return normally to let it open. Live-only (backtest short-circuits). See
44378
+ * {@link IBroker.onSignalOpenCommit} for the full semantics.
44379
+ *
43281
44380
  * @param payload - Signal open details: symbol, cost, position, priceOpen, priceTakeProfit, priceStopLoss, context, backtest
43282
44381
  *
43283
44382
  * @example
43284
44383
  * ```typescript
43285
44384
  * async onSignalOpenCommit(payload: BrokerSignalOpenPayload) {
43286
44385
  * super.onSignalOpenCommit(payload); // Keep parent logging
43287
- * await this.exchange.placeMarketOrder({
44386
+ * const order = await this.exchange.placeMarketOrder({
43288
44387
  * symbol: payload.symbol,
43289
44388
  * side: payload.position === "long" ? "BUY" : "SELL",
43290
44389
  * quantity: payload.cost / payload.priceOpen,
43291
44390
  * });
44391
+ * if (!order.filled) {
44392
+ * throw new Error(`Entry not filled for ${payload.symbol}`); // -> roll back the open, retry next tick
44393
+ * }
43292
44394
  * }
43293
44395
  * ```
43294
44396
  */
@@ -43310,47 +44412,158 @@ class BrokerBase {
43310
44412
  * normally instead of throwing. A thrown network error would wrongly close an open position; only
43311
44413
  * a confirmed "order not found by id" response is a valid reason to throw.
43312
44414
  *
43313
- * @param payload - Pending ping details: symbol, signalId, position, prices, pnl, context, backtest
44415
+ * Manual wiring — EXCEPTION-BASED VARIANT: the throw-driven alternative to the imperative
44416
+ * commit-function wiring in `onSignalActivePing`. See {@link IBroker.onOrderCheck} for the full
44417
+ * comparison and example.
43314
44418
  *
43315
- * @example
43316
- * ```typescript
43317
- * async onOrderPing(payload: BrokerSignalPendingPayload) {
43318
- * super.onOrderPing(payload); // Keep parent logging
43319
- * let order: Order | null;
43320
- * try {
43321
- * order = await this.exchange.getOrderById(payload.signalId);
43322
- * } catch (networkError) {
43323
- * // Transient/network error — swallow and keep the position open, retry next tick
43324
- * return;
43325
- * }
43326
- * if (!order) {
43327
- * // Confirmed not found by id — close the position
43328
- * throw new Error(`Order ${payload.signalId} not found on the exchange`);
43329
- * }
43330
- * }
43331
- * ```
44419
+ * @param payload - Pending ping details: symbol, signalId, position, prices, pnl, context, backtest
43332
44420
  */
43333
- async onOrderPing(payload) {
44421
+ async onOrderCheck(payload) {
43334
44422
  bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SIGNAL_PENDING, {
43335
44423
  symbol: payload.symbol,
43336
44424
  context: payload.context,
43337
44425
  });
43338
44426
  }
43339
44427
  /**
43340
- * Called when a position is fully closed (SL/TP hit or manual close).
44428
+ * Called on every live tick while a pending (open) signal is monitored.
44429
+ *
44430
+ * Purely informational mirror of the active-ping lifecycle — unlike `onOrderCheck`, a throw here
44431
+ * does NOT close the position. Override to mirror live monitoring state into your own systems.
44432
+ * The default implementation logs.
44433
+ *
44434
+ * Manual wiring — EVENT-BASED: this is the primary per-tick hook to drive an open position from real exchange
44435
+ * state (`commitCreateTakeProfit` / `commitCreateStopLoss` / `commitClosePending`). See the
44436
+ * {@link IBroker.onSignalActivePing} contract docs for the full guidance and example.
44437
+ *
44438
+ * @param payload - Active ping details: symbol, signalId, position, prices, pnl, context, backtest
44439
+ */
44440
+ async onSignalActivePing(payload) {
44441
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_ACTIVE_PING, {
44442
+ symbol: payload.symbol,
44443
+ context: payload.context,
44444
+ });
44445
+ }
44446
+ /**
44447
+ * Called on every live tick while a scheduled signal is monitored (waiting for priceOpen).
44448
+ *
44449
+ * Purely informational. Override to mirror scheduled-monitoring state. The default logs.
44450
+ *
44451
+ * Manual wiring — EVENT-BASED: per-tick hook to drive a scheduled (resting) order from real exchange state
44452
+ * (`commitActivateScheduled` / `commitCancelScheduled`). See {@link IBroker.onSignalSchedulePing}
44453
+ * for full guidance and example.
44454
+ *
44455
+ * @param payload - Schedule ping details: symbol, signalId, position, prices, context, backtest
44456
+ */
44457
+ async onSignalSchedulePing(payload) {
44458
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SCHEDULE_PING, {
44459
+ symbol: payload.symbol,
44460
+ context: payload.context,
44461
+ });
44462
+ }
44463
+ /**
44464
+ * Called on every live tick while the strategy is idle (no pending or scheduled signal).
44465
+ *
44466
+ * Purely informational. Override to track idle heartbeats. The default logs.
44467
+ *
44468
+ * @param payload - Idle ping details: symbol, currentPrice, context, backtest
44469
+ */
44470
+ async onSignalIdlePing(payload) {
44471
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_IDLE_PING, {
44472
+ symbol: payload.symbol,
44473
+ context: payload.context,
44474
+ });
44475
+ }
44476
+ /**
44477
+ * Called when a new scheduled signal is created and starts waiting for priceOpen activation.
44478
+ *
44479
+ * The scheduled -> active transition is reported via `onSignalOpenCommit`, not here. Override to
44480
+ * place a resting/limit order on the exchange. The default logs.
44481
+ *
44482
+ * Manual wiring — EVENT-BASED: fires ONCE at creation — place the real resting order (tag it with
44483
+ * `payload.signalId`) and optionally `commitActivateScheduled` / `commitCancelScheduled`. See
44484
+ * {@link IBroker.onSignalScheduleOpen} for full guidance and example.
44485
+ *
44486
+ * @param payload - Scheduled open details: symbol, signalId, position, prices, context, backtest
44487
+ */
44488
+ async onSignalScheduleOpen(payload) {
44489
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SCHEDULE_OPEN, {
44490
+ symbol: payload.symbol,
44491
+ context: payload.context,
44492
+ });
44493
+ }
44494
+ /**
44495
+ * Called when a scheduled signal is cancelled before activation (timeout / price_reject / user).
44496
+ *
44497
+ * Override to cancel the resting/limit order on the exchange. The default logs.
44498
+ *
44499
+ * Manual wiring — EVENT-BASED (outbound): the strategy already dropped the scheduled signal — cancel the matching
44500
+ * exchange order by `payload.signalId`. See {@link IBroker.onSignalScheduleCancelled}.
44501
+ *
44502
+ * @param payload - Scheduled cancel details: symbol, signalId, position, prices, reason, context, backtest
44503
+ */
44504
+ async onSignalScheduleCancelled(payload) {
44505
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SCHEDULE_CANCELLED, {
44506
+ symbol: payload.symbol,
44507
+ context: payload.context,
44508
+ });
44509
+ }
44510
+ /**
44511
+ * Called when a pending position is opened (new signal / immediate / scheduled or user activation).
44512
+ *
44513
+ * Informational lifecycle hook. Override to mirror the open into your own systems. The default logs.
44514
+ *
44515
+ * Manual wiring — EVENT-BASED: fires ONCE at open — place entry + protective TP/SL orders (tag with
44516
+ * `payload.signalId`), then drive per-tick from `onSignalActivePing`. See
44517
+ * {@link IBroker.onSignalPendingOpen}.
44518
+ *
44519
+ * @param payload - Pending open details: symbol, signalId, position, prices, context, backtest
44520
+ */
44521
+ async onSignalPendingOpen(payload) {
44522
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_PENDING_OPEN, {
44523
+ symbol: payload.symbol,
44524
+ context: payload.context,
44525
+ });
44526
+ }
44527
+ /**
44528
+ * Called when a pending position is closed (take_profit / stop_loss / time_expired / closed).
44529
+ *
44530
+ * Informational lifecycle hook. Override to mirror the close into your own systems. The default logs.
44531
+ *
44532
+ * Manual wiring — EVENT-BASED (outbound): the strategy already removed the pending signal — flatten the real
44533
+ * position and cancel leftover TP/SL orders by `payload.signalId`. See
44534
+ * {@link IBroker.onSignalPendingClose}.
44535
+ *
44536
+ * @param payload - Pending close details: symbol, signalId, position, prices, closeReason, context, backtest
44537
+ */
44538
+ async onSignalPendingClose(payload) {
44539
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_PENDING_CLOSE, {
44540
+ symbol: payload.symbol,
44541
+ context: payload.context,
44542
+ });
44543
+ }
44544
+ /**
44545
+ * Called when a position is being closed (SL/TP hit or manual close).
43341
44546
  *
43342
44547
  * Triggered automatically via syncSubject when a pending signal is closed.
43343
44548
  * Use to place the exit order and record final PnL.
43344
44549
  *
43345
44550
  * Default implementation: Logs signal-close event.
43346
44551
  *
44552
+ * Manual wiring — EXCEPTION-BASED GATE: emitted BEFORE the framework mutates state, so a THROW here
44553
+ * (e.g. exit order failed) SKIPS the close — the position stays open and the close retries next
44554
+ * tick; return normally to let it close. Live-only (backtest short-circuits). See
44555
+ * {@link IBroker.onSignalCloseCommit} for the full semantics.
44556
+ *
43347
44557
  * @param payload - Signal close details: symbol, cost, position, currentPrice, pnl, totalEntries, totalPartials, context, backtest
43348
44558
  *
43349
44559
  * @example
43350
44560
  * ```typescript
43351
44561
  * async onSignalCloseCommit(payload: BrokerSignalClosePayload) {
43352
44562
  * super.onSignalCloseCommit(payload); // Keep parent logging
43353
- * await this.exchange.closePosition(payload.symbol);
44563
+ * const ok = await this.exchange.closePosition(payload.symbol);
44564
+ * if (!ok) {
44565
+ * throw new Error(`Exit not filled for ${payload.symbol}`); // -> keep position open, retry next tick
44566
+ * }
43354
44567
  * await this.db.recordTrade({ symbol: payload.symbol, pnl: payload.pnl });
43355
44568
  * }
43356
44569
  * ```
@@ -43606,6 +44819,8 @@ const HAS_NO_PENDING_SIGNAL_METHOD_NAME = "strategy.hasNoPendingSignal";
43606
44819
  const HAS_NO_SCHEDULED_SIGNAL_METHOD_NAME = "strategy.hasNoScheduledSignal";
43607
44820
  const COMMIT_SIGNAL_NOTIFY_METHOD_NAME = "strategy.commitSignalNotify";
43608
44821
  const COMMIT_CREATE_SIGNAL_METHOD_NAME = "strategy.commitCreateSignal";
44822
+ const COMMIT_CREATE_TAKE_PROFIT_METHOD_NAME = "strategy.commitCreateTakeProfit";
44823
+ const COMMIT_CREATE_STOP_LOSS_METHOD_NAME = "strategy.commitCreateStopLoss";
43609
44824
  const GET_STRATEGY_STATUS_METHOD_NAME = "strategy.getStrategyStatus";
43610
44825
  /**
43611
44826
  * Cancels the scheduled signal without stopping the strategy.
@@ -45715,6 +46930,74 @@ async function commitCreateSignal(symbol, dto) {
45715
46930
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
45716
46931
  await bt.strategyCoreService.createSignal(isBacktest, symbol, dto, { exchangeName, frameName, strategyName });
45717
46932
  }
46933
+ /**
46934
+ * Reports that the pending position's take-profit order was actually filled on the exchange
46935
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based TP check.
46936
+ *
46937
+ * The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
46938
+ * but the real order may fill on high/low. The close is deferred and emitted with closeReason
46939
+ * "take_profit" on the next tick. No-op if no pending signal exists.
46940
+ *
46941
+ * Automatically detects backtest/live mode from execution context.
46942
+ *
46943
+ * @param symbol - Trading pair symbol
46944
+ * @param payload - Optional commit payload with id and note
46945
+ * @returns Promise that resolves when the take-profit fill is queued
46946
+ *
46947
+ * @example
46948
+ * ```typescript
46949
+ * import { commitCreateTakeProfit } from "backtest-kit";
46950
+ *
46951
+ * // Report TP fill confirmed on the exchange
46952
+ * await commitCreateTakeProfit("BTCUSDT", { id: "tp-fill-001" });
46953
+ * ```
46954
+ */
46955
+ async function commitCreateTakeProfit(symbol, payload = {}) {
46956
+ bt.loggerService.info(COMMIT_CREATE_TAKE_PROFIT_METHOD_NAME, { symbol, payload });
46957
+ if (!ExecutionContextService.hasContext()) {
46958
+ throw new Error("commitCreateTakeProfit requires an execution context");
46959
+ }
46960
+ if (!MethodContextService.hasContext()) {
46961
+ throw new Error("commitCreateTakeProfit requires a method context");
46962
+ }
46963
+ const { backtest: isBacktest } = bt.executionContextService.context;
46964
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
46965
+ await bt.strategyCoreService.createTakeProfit(isBacktest, symbol, { exchangeName, frameName, strategyName }, payload);
46966
+ }
46967
+ /**
46968
+ * Reports that the pending position's stop-loss order was actually filled on the exchange
46969
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based SL check.
46970
+ *
46971
+ * The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
46972
+ * but the real order may fill on high/low. The close is deferred and emitted with closeReason
46973
+ * "stop_loss" on the next tick. No-op if no pending signal exists.
46974
+ *
46975
+ * Automatically detects backtest/live mode from execution context.
46976
+ *
46977
+ * @param symbol - Trading pair symbol
46978
+ * @param payload - Optional commit payload with id and note
46979
+ * @returns Promise that resolves when the stop-loss fill is queued
46980
+ *
46981
+ * @example
46982
+ * ```typescript
46983
+ * import { commitCreateStopLoss } from "backtest-kit";
46984
+ *
46985
+ * // Report SL fill confirmed on the exchange
46986
+ * await commitCreateStopLoss("BTCUSDT", { id: "sl-fill-001" });
46987
+ * ```
46988
+ */
46989
+ async function commitCreateStopLoss(symbol, payload = {}) {
46990
+ bt.loggerService.info(COMMIT_CREATE_STOP_LOSS_METHOD_NAME, { symbol, payload });
46991
+ if (!ExecutionContextService.hasContext()) {
46992
+ throw new Error("commitCreateStopLoss requires an execution context");
46993
+ }
46994
+ if (!MethodContextService.hasContext()) {
46995
+ throw new Error("commitCreateStopLoss requires a method context");
46996
+ }
46997
+ const { backtest: isBacktest } = bt.executionContextService.context;
46998
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
46999
+ await bt.strategyCoreService.createStopLoss(isBacktest, symbol, { exchangeName, frameName, strategyName }, payload);
47000
+ }
45718
47001
  /**
45719
47002
  * Returns the in-memory deferred strategy-state snapshot for the current iteration: the queued
45720
47003
  * createSignal, commit queue and deferred user-action flags, plus the current pending signal id.
@@ -45817,6 +47100,10 @@ const LISTEN_RISK_METHOD_NAME = "event.listenRisk";
45817
47100
  const LISTEN_RISK_ONCE_METHOD_NAME = "event.listenRiskOnce";
45818
47101
  const LISTEN_SCHEDULE_PING_METHOD_NAME = "event.listenSchedulePing";
45819
47102
  const LISTEN_SCHEDULE_PING_ONCE_METHOD_NAME = "event.listenSchedulePingOnce";
47103
+ const LISTEN_SCHEDULE_EVENT_METHOD_NAME = "event.listenScheduleEvent";
47104
+ const LISTEN_SCHEDULE_EVENT_ONCE_METHOD_NAME = "event.listenScheduleEventOnce";
47105
+ const LISTEN_SIGNAL_EVENT_METHOD_NAME = "event.listenSignalEvent";
47106
+ const LISTEN_SIGNAL_EVENT_ONCE_METHOD_NAME = "event.listenSignalEventOnce";
45820
47107
  const LISTEN_ACTIVE_PING_METHOD_NAME = "event.listenActivePing";
45821
47108
  const LISTEN_ACTIVE_PING_ONCE_METHOD_NAME = "event.listenActivePingOnce";
45822
47109
  const LISTEN_IDLE_PING_METHOD_NAME = "event.listenIdlePing";
@@ -46919,6 +48206,137 @@ function listenSchedulePingOnce(filterFn, fn) {
46919
48206
  };
46920
48207
  return disposeFn = listenSchedulePing(wrappedFn);
46921
48208
  }
48209
+ /**
48210
+ * Subscribes to scheduled signal lifecycle events (creation and cancellation) with queued async processing.
48211
+ *
48212
+ * Emitted when a scheduled signal is created (action "scheduled") or cancelled before activation
48213
+ * (action "cancelled" with reason "timeout" / "price_reject" / "user"), in both live and backtest.
48214
+ *
48215
+ * IMPORTANT: The scheduled -> active transition (activation) is NOT reported here. Activation
48216
+ * produces an "opened" event on the regular signal emitters (listenSignal) instead.
48217
+ *
48218
+ * Events are processed sequentially in order received, even if callback is async.
48219
+ *
48220
+ * @param fn - Callback function to handle scheduled lifecycle events
48221
+ * @returns Unsubscribe function to stop listening
48222
+ *
48223
+ * @example
48224
+ * ```typescript
48225
+ * import { listenScheduleEvent } from "./function/event";
48226
+ *
48227
+ * const unsubscribe = listenScheduleEvent((event) => {
48228
+ * if (event.action === "scheduled") {
48229
+ * console.log(`Scheduled ${event.symbol} @ ${event.data.priceOpen}`);
48230
+ * } else {
48231
+ * console.log(`Cancelled ${event.symbol} (reason: ${event.reason})`);
48232
+ * }
48233
+ * });
48234
+ *
48235
+ * // Later: stop listening
48236
+ * unsubscribe();
48237
+ * ```
48238
+ */
48239
+ function listenScheduleEvent(fn) {
48240
+ bt.loggerService.log(LISTEN_SCHEDULE_EVENT_METHOD_NAME);
48241
+ return scheduleEventSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
48242
+ }
48243
+ /**
48244
+ * Subscribes to filtered scheduled lifecycle events with one-time execution.
48245
+ *
48246
+ * Listens for events matching the filter predicate, then executes callback once
48247
+ * and automatically unsubscribes. Useful for waiting for a specific scheduled creation
48248
+ * or cancellation.
48249
+ *
48250
+ * @param filterFn - Predicate to filter which events trigger the callback
48251
+ * @param fn - Callback function to handle the filtered event (called only once)
48252
+ * @returns Unsubscribe function to cancel the listener before it fires
48253
+ *
48254
+ * @example
48255
+ * ```typescript
48256
+ * import { listenScheduleEventOnce } from "./function/event";
48257
+ *
48258
+ * // Wait for the first cancellation on BTCUSDT
48259
+ * listenScheduleEventOnce(
48260
+ * (event) => event.symbol === "BTCUSDT" && event.action === "cancelled",
48261
+ * (event) => console.log("BTCUSDT scheduled cancelled:", event.reason)
48262
+ * );
48263
+ * ```
48264
+ */
48265
+ function listenScheduleEventOnce(filterFn, fn) {
48266
+ bt.loggerService.log(LISTEN_SCHEDULE_EVENT_ONCE_METHOD_NAME);
48267
+ let disposeFn;
48268
+ const wrappedFn = async (event) => {
48269
+ if (filterFn(event)) {
48270
+ await fn(event);
48271
+ disposeFn && disposeFn();
48272
+ }
48273
+ };
48274
+ return disposeFn = listenScheduleEvent(wrappedFn);
48275
+ }
48276
+ /**
48277
+ * Subscribes to pending signal lifecycle events (open and close) with queued async processing.
48278
+ *
48279
+ * Emitted when a pending position is opened (action "opened": new signal / immediate / scheduled
48280
+ * or user activation) or closed (action "closed" with closeReason "take_profit" / "stop_loss" /
48281
+ * "time_expired" / "closed"), in both live and backtest.
48282
+ *
48283
+ * Events are processed sequentially in order received, even if callback is async.
48284
+ *
48285
+ * @param fn - Callback function to handle pending lifecycle events
48286
+ * @returns Unsubscribe function to stop listening
48287
+ *
48288
+ * @example
48289
+ * ```typescript
48290
+ * import { listenSignalEvent } from "./function/event";
48291
+ *
48292
+ * const unsubscribe = listenSignalEvent((event) => {
48293
+ * if (event.action === "opened") {
48294
+ * console.log(`Opened ${event.symbol} @ ${event.data.priceOpen}`);
48295
+ * } else {
48296
+ * console.log(`Closed ${event.symbol} (reason: ${event.closeReason})`);
48297
+ * }
48298
+ * });
48299
+ *
48300
+ * // Later: stop listening
48301
+ * unsubscribe();
48302
+ * ```
48303
+ */
48304
+ function listenSignalEvent(fn) {
48305
+ bt.loggerService.log(LISTEN_SIGNAL_EVENT_METHOD_NAME);
48306
+ return signalEventSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
48307
+ }
48308
+ /**
48309
+ * Subscribes to filtered pending lifecycle events with one-time execution.
48310
+ *
48311
+ * Listens for events matching the filter predicate, then executes callback once
48312
+ * and automatically unsubscribes. Useful for waiting for a specific open or close.
48313
+ *
48314
+ * @param filterFn - Predicate to filter which events trigger the callback
48315
+ * @param fn - Callback function to handle the filtered event (called only once)
48316
+ * @returns Unsubscribe function to cancel the listener before it fires
48317
+ *
48318
+ * @example
48319
+ * ```typescript
48320
+ * import { listenSignalEventOnce } from "./function/event";
48321
+ *
48322
+ * // Wait for the first close on BTCUSDT
48323
+ * listenSignalEventOnce(
48324
+ * (event) => event.symbol === "BTCUSDT" && event.action === "closed",
48325
+ * (event) => console.log("BTCUSDT closed:", event.closeReason)
48326
+ * );
48327
+ * ```
48328
+ */
48329
+ function listenSignalEventOnce(filterFn, fn) {
48330
+ bt.loggerService.log(LISTEN_SIGNAL_EVENT_ONCE_METHOD_NAME);
48331
+ let disposeFn;
48332
+ const wrappedFn = async (event) => {
48333
+ if (filterFn(event)) {
48334
+ await fn(event);
48335
+ disposeFn && disposeFn();
48336
+ }
48337
+ };
48338
+ return disposeFn = listenSignalEvent(wrappedFn);
48339
+ }
46922
48340
  /**
46923
48341
  * Subscribes to active ping events with queued async processing.
46924
48342
  *
@@ -47416,6 +48834,8 @@ const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
47416
48834
  const BACKTEST_METHOD_NAME_CANCEL_SCHEDULED = "Backtest.commitCancelScheduled";
47417
48835
  const BACKTEST_METHOD_NAME_CLOSE_PENDING = "Backtest.commitClosePending";
47418
48836
  const BACKTEST_METHOD_NAME_COMMIT_CREATE_SIGNAL = "Backtest.commitCreateSignal";
48837
+ const BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT = "Backtest.commitCreateTakeProfit";
48838
+ const BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS = "Backtest.commitCreateStopLoss";
47419
48839
  const BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS = "Backtest.getStrategyStatus";
47420
48840
  const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.commitPartialProfit";
47421
48841
  const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.commitPartialLoss";
@@ -49152,6 +50572,90 @@ class BacktestUtils {
49152
50572
  }
49153
50573
  await bt.strategyCoreService.createSignal(true, symbol, dto, context);
49154
50574
  };
50575
+ /**
50576
+ * Reports that the pending position's take-profit order was actually filled on the exchange
50577
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based TP check.
50578
+ *
50579
+ * The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
50580
+ * but the real order may fill on high/low. The close is deferred and emitted with closeReason
50581
+ * "take_profit" on the next backtest tick. No-op if no pending signal exists.
50582
+ *
50583
+ * @param symbol - Trading pair symbol
50584
+ * @param context - Execution context with strategyName, exchangeName, and frameName
50585
+ * @param payload - Optional commit payload with id and note
50586
+ * @returns Promise that resolves when the take-profit fill is queued
50587
+ *
50588
+ * @example
50589
+ * ```typescript
50590
+ * // Report TP fill confirmed on the exchange
50591
+ * await Backtest.commitCreateTakeProfit("BTCUSDT", {
50592
+ * exchangeName: "binance",
50593
+ * strategyName: "my-strategy",
50594
+ * frameName: "1m"
50595
+ * }, { id: "tp-fill-001" });
50596
+ * ```
50597
+ */
50598
+ this.commitCreateTakeProfit = async (symbol, context, payload = {}) => {
50599
+ bt.loggerService.info(BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT, {
50600
+ symbol,
50601
+ context,
50602
+ payload,
50603
+ });
50604
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
50605
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
50606
+ {
50607
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
50608
+ riskName &&
50609
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
50610
+ riskList &&
50611
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT));
50612
+ actions &&
50613
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT));
50614
+ }
50615
+ await bt.strategyCoreService.createTakeProfit(true, symbol, context, payload);
50616
+ };
50617
+ /**
50618
+ * Reports that the pending position's stop-loss order was actually filled on the exchange
50619
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based SL check.
50620
+ *
50621
+ * The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
50622
+ * but the real order may fill on high/low. The close is deferred and emitted with closeReason
50623
+ * "stop_loss" on the next backtest tick. No-op if no pending signal exists.
50624
+ *
50625
+ * @param symbol - Trading pair symbol
50626
+ * @param context - Execution context with strategyName, exchangeName, and frameName
50627
+ * @param payload - Optional commit payload with id and note
50628
+ * @returns Promise that resolves when the stop-loss fill is queued
50629
+ *
50630
+ * @example
50631
+ * ```typescript
50632
+ * // Report SL fill confirmed on the exchange
50633
+ * await Backtest.commitCreateStopLoss("BTCUSDT", {
50634
+ * exchangeName: "binance",
50635
+ * strategyName: "my-strategy",
50636
+ * frameName: "1m"
50637
+ * }, { id: "sl-fill-001" });
50638
+ * ```
50639
+ */
50640
+ this.commitCreateStopLoss = async (symbol, context, payload = {}) => {
50641
+ bt.loggerService.info(BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS, {
50642
+ symbol,
50643
+ context,
50644
+ payload,
50645
+ });
50646
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
50647
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
50648
+ {
50649
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
50650
+ riskName &&
50651
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
50652
+ riskList &&
50653
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS));
50654
+ actions &&
50655
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS));
50656
+ }
50657
+ await bt.strategyCoreService.createStopLoss(true, symbol, context, payload);
50658
+ };
49155
50659
  /**
49156
50660
  * Returns the in-memory deferred strategy-state snapshot for the current backtest iteration.
49157
50661
  *
@@ -50137,6 +51641,8 @@ const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
50137
51641
  const LIVE_METHOD_NAME_CANCEL_SCHEDULED = "Live.cancelScheduled";
50138
51642
  const LIVE_METHOD_NAME_CLOSE_PENDING = "Live.closePending";
50139
51643
  const LIVE_METHOD_NAME_COMMIT_CREATE_SIGNAL = "Live.commitCreateSignal";
51644
+ const LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT = "Live.commitCreateTakeProfit";
51645
+ const LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS = "Live.commitCreateStopLoss";
50140
51646
  const LIVE_METHOD_NAME_GET_STRATEGY_STATUS = "Live.getStrategyStatus";
50141
51647
  const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.commitPartialProfit";
50142
51648
  const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.commitPartialLoss";
@@ -52050,6 +53556,78 @@ class LiveUtils {
52050
53556
  frameName: "",
52051
53557
  });
52052
53558
  };
53559
+ /**
53560
+ * Reports that the pending position's take-profit order was actually filled on the exchange
53561
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based TP check.
53562
+ *
53563
+ * The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
53564
+ * but the real order may fill on high/low. The close is deferred and emitted with closeReason
53565
+ * "take_profit" on the next live tick. No-op if no pending signal exists.
53566
+ *
53567
+ * @param symbol - Trading pair symbol
53568
+ * @param context - Execution context with strategyName and exchangeName
53569
+ * @param payload - Optional commit payload with id and note
53570
+ * @returns Promise that resolves when the take-profit fill is queued
53571
+ */
53572
+ this.commitCreateTakeProfit = async (symbol, context, payload = {}) => {
53573
+ bt.loggerService.info(LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT, {
53574
+ symbol,
53575
+ context,
53576
+ payload,
53577
+ });
53578
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
53579
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
53580
+ {
53581
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
53582
+ riskName &&
53583
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
53584
+ riskList &&
53585
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT));
53586
+ actions &&
53587
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT));
53588
+ }
53589
+ await bt.strategyCoreService.createTakeProfit(false, symbol, {
53590
+ strategyName: context.strategyName,
53591
+ exchangeName: context.exchangeName,
53592
+ frameName: "",
53593
+ }, payload);
53594
+ };
53595
+ /**
53596
+ * Reports that the pending position's stop-loss order was actually filled on the exchange
53597
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based SL check.
53598
+ *
53599
+ * The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
53600
+ * but the real order may fill on high/low. The close is deferred and emitted with closeReason
53601
+ * "stop_loss" on the next live tick. No-op if no pending signal exists.
53602
+ *
53603
+ * @param symbol - Trading pair symbol
53604
+ * @param context - Execution context with strategyName and exchangeName
53605
+ * @param payload - Optional commit payload with id and note
53606
+ * @returns Promise that resolves when the stop-loss fill is queued
53607
+ */
53608
+ this.commitCreateStopLoss = async (symbol, context, payload = {}) => {
53609
+ bt.loggerService.info(LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS, {
53610
+ symbol,
53611
+ context,
53612
+ payload,
53613
+ });
53614
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
53615
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
53616
+ {
53617
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
53618
+ riskName &&
53619
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
53620
+ riskList &&
53621
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS));
53622
+ actions &&
53623
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS));
53624
+ }
53625
+ await bt.strategyCoreService.createStopLoss(false, symbol, {
53626
+ strategyName: context.strategyName,
53627
+ exchangeName: context.exchangeName,
53628
+ frameName: "",
53629
+ }, payload);
53630
+ };
52053
53631
  /**
52054
53632
  * Returns the in-memory deferred strategy-state snapshot for the current live iteration.
52055
53633
  *
@@ -60389,7 +61967,9 @@ const SUBJECT_ISOLATION_LIST = [
60389
61967
  validationSubject,
60390
61968
  signalNotifySubject,
60391
61969
  beforeStartSubject,
60392
- afterEndSubject
61970
+ afterEndSubject,
61971
+ scheduleEventSubject,
61972
+ signalEventSubject,
60393
61973
  ];
60394
61974
  /**
60395
61975
  * Creates a snapshot function for a given subject by clearing its internal
@@ -68837,6 +70417,8 @@ exports.commitBreakeven = commitBreakeven;
68837
70417
  exports.commitCancelScheduled = commitCancelScheduled;
68838
70418
  exports.commitClosePending = commitClosePending;
68839
70419
  exports.commitCreateSignal = commitCreateSignal;
70420
+ exports.commitCreateStopLoss = commitCreateStopLoss;
70421
+ exports.commitCreateTakeProfit = commitCreateTakeProfit;
68840
70422
  exports.commitPartialLoss = commitPartialLoss;
68841
70423
  exports.commitPartialLossCost = commitPartialLossCost;
68842
70424
  exports.commitPartialProfit = commitPartialProfit;
@@ -68970,11 +70552,15 @@ exports.listenPartialProfitAvailableOnce = listenPartialProfitAvailableOnce;
68970
70552
  exports.listenPerformance = listenPerformance;
68971
70553
  exports.listenRisk = listenRisk;
68972
70554
  exports.listenRiskOnce = listenRiskOnce;
70555
+ exports.listenScheduleEvent = listenScheduleEvent;
70556
+ exports.listenScheduleEventOnce = listenScheduleEventOnce;
68973
70557
  exports.listenSchedulePing = listenSchedulePing;
68974
70558
  exports.listenSchedulePingOnce = listenSchedulePingOnce;
68975
70559
  exports.listenSignal = listenSignal;
68976
70560
  exports.listenSignalBacktest = listenSignalBacktest;
68977
70561
  exports.listenSignalBacktestOnce = listenSignalBacktestOnce;
70562
+ exports.listenSignalEvent = listenSignalEvent;
70563
+ exports.listenSignalEventOnce = listenSignalEventOnce;
68978
70564
  exports.listenSignalLive = listenSignalLive;
68979
70565
  exports.listenSignalLiveOnce = listenSignalLiveOnce;
68980
70566
  exports.listenSignalNotify = listenSignalNotify;