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.mjs CHANGED
@@ -748,6 +748,22 @@ const riskSubject = new Subject();
748
748
  * Allows users to track scheduled signal lifecycle and implement custom cancellation logic.
749
749
  */
750
750
  const schedulePingSubject = new Subject();
751
+ /**
752
+ * Scheduled signal lifecycle emitter (creation and cancellation).
753
+ * Emits when a scheduled signal is created (action "scheduled") or cancelled before
754
+ * activation (action "cancelled": timeout / price_reject / user) during tick()/backtest().
755
+ *
756
+ * The scheduled -> active transition (activation) is intentionally NOT emitted here — that
757
+ * produces an "opened" signal on the regular signal emitters instead.
758
+ */
759
+ const scheduleEventSubject = new Subject();
760
+ /**
761
+ * Pending signal lifecycle emitter (open and close).
762
+ * Emits when a pending position is opened (action "opened": new signal / immediate / scheduled
763
+ * or user activation) or closed (action "closed" with closeReason take_profit / stop_loss /
764
+ * time_expired / closed) during tick()/backtest().
765
+ */
766
+ const signalEventSubject = new Subject();
751
767
  /**
752
768
  * Active ping emitter for active pending signal monitoring events.
753
769
  * Emits every minute when an active pending signal is being monitored.
@@ -838,10 +854,12 @@ var emitters = /*#__PURE__*/Object.freeze({
838
854
  progressBacktestEmitter: progressBacktestEmitter,
839
855
  progressWalkerEmitter: progressWalkerEmitter,
840
856
  riskSubject: riskSubject,
857
+ scheduleEventSubject: scheduleEventSubject,
841
858
  schedulePingSubject: schedulePingSubject,
842
859
  shutdownEmitter: shutdownEmitter,
843
860
  signalBacktestEmitter: signalBacktestEmitter,
844
861
  signalEmitter: signalEmitter,
862
+ signalEventSubject: signalEventSubject,
845
863
  signalLiveEmitter: signalLiveEmitter,
846
864
  signalNotifySubject: signalNotifySubject,
847
865
  strategyCommitSubject: strategyCommitSubject,
@@ -1909,7 +1927,7 @@ class PersistStrategyInstance {
1909
1927
  const strategyData = await this._storage.readValue(PersistStrategyInstance.STORAGE_KEY);
1910
1928
  // JSON serializes Infinity as null, so an eternal-hold signal
1911
1929
  // (minuteEstimatedTime: Infinity) reads back as null — restore it.
1912
- for (const signal of [strategyData.closedSignal, strategyData.cancelledSignal, strategyData.activatedSignal]) {
1930
+ for (const signal of [strategyData.closedSignal, strategyData.cancelledSignal, strategyData.activatedSignal, strategyData.takeProfitSignal, strategyData.stopLossSignal]) {
1913
1931
  if (signal && signal.minuteEstimatedTime == null) {
1914
1932
  signal.minuteEstimatedTime = Infinity;
1915
1933
  }
@@ -7653,6 +7671,10 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
7653
7671
  self._closedSignal = strategyData.closedSignal;
7654
7672
  self._cancelledSignal = strategyData.cancelledSignal;
7655
7673
  self._activatedSignal = strategyData.activatedSignal;
7674
+ // Deferred broker-confirmed TP/SL fills snapshot the pending signal and clear it (like
7675
+ // _closedSignal), so they stand on their own and are restored unconditionally too.
7676
+ self._takeProfitSignal = strategyData.takeProfitSignal;
7677
+ self._stopLossSignal = strategyData.stopLossSignal;
7656
7678
  }
7657
7679
  // Restore pending signal
7658
7680
  const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
@@ -7733,6 +7755,8 @@ const PERSIST_STRATEGY_FN = async (self) => {
7733
7755
  closedSignal: self._closedSignal,
7734
7756
  cancelledSignal: self._cancelledSignal,
7735
7757
  activatedSignal: self._activatedSignal,
7758
+ takeProfitSignal: self._takeProfitSignal,
7759
+ stopLossSignal: self._stopLossSignal,
7736
7760
  }, self.params.symbol, self.params.strategyName, self.params.exchangeName);
7737
7761
  };
7738
7762
  const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice, timestamp) => {
@@ -8266,6 +8290,7 @@ const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice)
8266
8290
  maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
8267
8291
  });
8268
8292
  await self.setScheduledSignal(null);
8293
+ await CALL_SCHEDULE_EVENT_FN$1(self, "cancelled", scheduled, currentPrice, currentTime, "timeout");
8269
8294
  await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentPrice, currentTime, self.params.execution.context.backtest);
8270
8295
  const result = {
8271
8296
  action: "cancelled",
@@ -8322,6 +8347,7 @@ const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPr
8322
8347
  });
8323
8348
  await self.setScheduledSignal(null);
8324
8349
  const currentTime = self.params.execution.context.when.getTime();
8350
+ await CALL_SCHEDULE_EVENT_FN$1(self, "cancelled", scheduled, currentPrice, currentTime, "price_reject");
8325
8351
  await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentPrice, currentTime, self.params.execution.context.backtest);
8326
8352
  const result = {
8327
8353
  action: "cancelled",
@@ -8415,6 +8441,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
8415
8441
  await self.setScheduledSignal(null);
8416
8442
  await self.setPendingSignal(activatedSignal, activatedSignal.priceOpen);
8417
8443
  await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activatedSignal, activationTime, self.params.execution.context.backtest);
8444
+ await CALL_SIGNAL_EVENT_FN(self, "opened", self._pendingSignal, self._pendingSignal.priceOpen, activationTime);
8418
8445
  await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, self._pendingSignal, self._pendingSignal.priceOpen, activationTime, self.params.execution.context.backtest);
8419
8446
  const result = {
8420
8447
  action: "opened",
@@ -8549,6 +8576,53 @@ const CALL_SCHEDULE_CALLBACKS_FN = trycatch(beginTime(async (self, symbol, signa
8549
8576
  errorEmitter.next(error);
8550
8577
  },
8551
8578
  });
8579
+ /**
8580
+ * Emits a scheduled signal lifecycle event (creation / cancellation) to onScheduleEvent.
8581
+ *
8582
+ * Called when a scheduled signal is created (action "scheduled") or cancelled before activation
8583
+ * (action "cancelled" with reason timeout / price_reject / user). The scheduled -> active
8584
+ * transition is intentionally NOT emitted through this path.
8585
+ *
8586
+ * Unlike the CALL_*_CALLBACKS_FN helpers this does not run inside beginTime/runInContext: the
8587
+ * timestamp is passed explicitly by the caller (tick when / candle timestamp) and forwarded to
8588
+ * the connection-level emitter, mirroring how onSchedulePing carries its own timestamp.
8589
+ */
8590
+ const CALL_SCHEDULE_EVENT_FN$1 = trycatch(async (self, action, signal, currentPrice, timestamp, reason) => {
8591
+ 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);
8592
+ }, {
8593
+ fallback: (error, self) => {
8594
+ const message = "ClientStrategy CALL_SCHEDULE_EVENT_FN thrown";
8595
+ const payload = {
8596
+ error: errorData(error),
8597
+ message: getErrorMessage(error),
8598
+ };
8599
+ self.params.logger.warn(message, payload);
8600
+ console.warn(message, payload);
8601
+ errorEmitter.next(error);
8602
+ },
8603
+ });
8604
+ /**
8605
+ * Emits a pending signal lifecycle event (open / close) to onSignalEvent.
8606
+ *
8607
+ * Called when a pending position is opened (action "opened") or closed (action "closed" with a
8608
+ * closeReason). Like CALL_SCHEDULE_EVENT_FN it does not run inside beginTime/runInContext: the
8609
+ * timestamp is passed explicitly by the caller (tick when / candle timestamp) and forwarded to
8610
+ * the connection-level emitter.
8611
+ */
8612
+ const CALL_SIGNAL_EVENT_FN = trycatch(async (self, action, signal, currentPrice, timestamp, closeReason) => {
8613
+ 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);
8614
+ }, {
8615
+ fallback: (error, self) => {
8616
+ const message = "ClientStrategy CALL_SIGNAL_EVENT_FN thrown";
8617
+ const payload = {
8618
+ error: errorData(error),
8619
+ message: getErrorMessage(error),
8620
+ };
8621
+ self.params.logger.warn(message, payload);
8622
+ console.warn(message, payload);
8623
+ errorEmitter.next(error);
8624
+ },
8625
+ });
8552
8626
  const CALL_CANCEL_CALLBACKS_FN = trycatch(beginTime(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
8553
8627
  await ExecutionContextService.runInContext(async () => {
8554
8628
  if (self.params.callbacks?.onCancel) {
@@ -8924,6 +8998,7 @@ const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
8924
8998
  priceOpen: signal.priceOpen,
8925
8999
  currentPrice: currentPrice,
8926
9000
  });
9001
+ await CALL_SCHEDULE_EVENT_FN$1(self, "scheduled", signal, currentPrice, currentTime);
8927
9002
  await CALL_SCHEDULE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
8928
9003
  const result = {
8929
9004
  action: "scheduled",
@@ -8954,6 +9029,7 @@ const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
8954
9029
  return null;
8955
9030
  }
8956
9031
  await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, signal, currentTime, self.params.execution.context.backtest);
9032
+ await CALL_SIGNAL_EVENT_FN(self, "opened", signal, signal.priceOpen, currentTime);
8957
9033
  await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, signal.priceOpen, currentTime, self.params.execution.context.backtest);
8958
9034
  const result = {
8959
9035
  action: "opened",
@@ -9020,6 +9096,7 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
9020
9096
  priceClose: currentPrice,
9021
9097
  pnlPercentage: publicSignal.pnl.pnlPercentage,
9022
9098
  });
9099
+ await CALL_SIGNAL_EVENT_FN(self, "closed", signal, currentPrice, currentTime, closeReason);
9023
9100
  await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
9024
9101
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
9025
9102
  await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
@@ -9061,6 +9138,7 @@ const CLOSE_PENDING_SIGNAL_AS_CLOSED_FN = async (self, signal, currentPrice) =>
9061
9138
  priceClose: currentPrice,
9062
9139
  pnlPercentage: publicSignal.pnl.pnlPercentage,
9063
9140
  });
9141
+ await CALL_SIGNAL_EVENT_FN(self, "closed", signal, currentPrice, currentTime, "closed");
9064
9142
  await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
9065
9143
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
9066
9144
  await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
@@ -9231,6 +9309,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
9231
9309
  });
9232
9310
  await self.setScheduledSignal(null);
9233
9311
  const publicSignal = TO_PUBLIC_SIGNAL("scheduled", scheduled, averagePrice);
9312
+ await CALL_SCHEDULE_EVENT_FN$1(self, "cancelled", scheduled, averagePrice, closeTimestamp, reason);
9234
9313
  if (reason === "user") {
9235
9314
  await CALL_COMMIT_FN(self, {
9236
9315
  action: "cancel-scheduled",
@@ -9343,6 +9422,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
9343
9422
  await self.setScheduledSignal(null);
9344
9423
  await self.setPendingSignal(activatedSignal, activatedSignal.priceOpen);
9345
9424
  await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activatedSignal, activationTime, self.params.execution.context.backtest);
9425
+ await CALL_SIGNAL_EVENT_FN(self, "opened", activatedSignal, activatedSignal.priceOpen, activationTime);
9346
9426
  await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, activatedSignal, activatedSignal.priceOpen, activationTime, self.params.execution.context.backtest);
9347
9427
  await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, activatedSignal, activationTime, self.params.execution.context.backtest);
9348
9428
  return true;
@@ -9373,6 +9453,7 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
9373
9453
  if (closeReason === "time_expired" && publicSignal.pnl.pnlPercentage < 0) {
9374
9454
  self.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (time_expired), PNL: ${publicSignal.pnl.pnlPercentage.toFixed(2)}%`);
9375
9455
  }
9456
+ await CALL_SIGNAL_EVENT_FN(self, "closed", signal, averagePrice, closeTimestamp, closeReason);
9376
9457
  await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
9377
9458
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
9378
9459
  await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
@@ -9431,6 +9512,7 @@ const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, aver
9431
9512
  signal: publicSignal,
9432
9513
  note: closedSignal.closeNote ?? closedSignal.note,
9433
9514
  });
9515
+ await CALL_SIGNAL_EVENT_FN(self, "closed", closedSignal, averagePrice, closeTimestamp, "closed");
9434
9516
  await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
9435
9517
  await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
9436
9518
  await CALL_BREAKEVEN_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
@@ -9453,6 +9535,79 @@ const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, aver
9453
9535
  await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
9454
9536
  return result;
9455
9537
  };
9538
+ /**
9539
+ * Closes a deferred broker-confirmed TP/SL fill (createTakeProfit / createStopLoss).
9540
+ *
9541
+ * The exchange and the strategy are parallel states: ClientStrategy evaluates TP/SL against VWAP,
9542
+ * but the real order may fill on candle high/low. When the broker confirms such a fill out of
9543
+ * context, the pending signal is snapshotted into _takeProfitSignal / _stopLossSignal and the next
9544
+ * tick()/backtest() drains it here, closing with the matching closeReason at the effective TP/SL
9545
+ * level (trailing override if set) — bypassing the VWAP completion check.
9546
+ *
9547
+ * Shared by live tick and backtest: the caller passes the close timestamp explicitly (execution
9548
+ * context when in live, candle timestamp in backtest). Mirrors CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN
9549
+ * (commit action "close-pending", carrying closeId/note) but with closeReason take_profit / stop_loss.
9550
+ *
9551
+ * Like the deferred close, the fill snapshot is already established by the broker, so it does NOT
9552
+ * re-confirm via onSignalSync. _takeProfitSignal / _stopLossSignal is cleared and re-persisted by
9553
+ * the caller after draining.
9554
+ */
9555
+ const CLOSE_PENDING_SIGNAL_AS_FILL_FN = async (self, filledSignal, closeReason, closeTimestamp) => {
9556
+ const closePrice = closeReason === "take_profit"
9557
+ ? (filledSignal._trailingPriceTakeProfit ?? filledSignal.priceTakeProfit)
9558
+ : (filledSignal._trailingPriceStopLoss ?? filledSignal.priceStopLoss);
9559
+ const publicSignal = TO_PUBLIC_SIGNAL("pending", filledSignal, closePrice);
9560
+ self.params.logger.info(`ClientStrategy signal ${closeReason} by broker-confirmed fill (createTakeProfit/createStopLoss)`, {
9561
+ symbol: self.params.execution.context.symbol,
9562
+ signalId: filledSignal.id,
9563
+ closeReason,
9564
+ priceClose: closePrice,
9565
+ pnlPercentage: publicSignal.pnl.pnlPercentage,
9566
+ });
9567
+ await CALL_COMMIT_FN(self, {
9568
+ action: "close-pending",
9569
+ symbol: self.params.execution.context.symbol,
9570
+ strategyName: self.params.strategyName,
9571
+ exchangeName: self.params.exchangeName,
9572
+ frameName: self.params.frameName,
9573
+ signalId: filledSignal.id,
9574
+ backtest: self.params.execution.context.backtest,
9575
+ closeId: filledSignal.closeId,
9576
+ timestamp: closeTimestamp,
9577
+ totalEntries: filledSignal._entry?.length ?? 1,
9578
+ totalPartials: filledSignal._partial?.length ?? 0,
9579
+ originalPriceOpen: filledSignal.priceOpen,
9580
+ pnl: publicSignal.pnl,
9581
+ maxDrawdown: publicSignal.maxDrawdown,
9582
+ peakProfit: publicSignal.peakProfit,
9583
+ signal: publicSignal,
9584
+ note: filledSignal.closeNote ?? filledSignal.note,
9585
+ });
9586
+ await CALL_SIGNAL_EVENT_FN(self, "closed", filledSignal, closePrice, closeTimestamp, closeReason);
9587
+ await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, filledSignal, closePrice, closeTimestamp, self.params.execution.context.backtest);
9588
+ // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
9589
+ await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, filledSignal, closePrice, closeTimestamp, self.params.execution.context.backtest);
9590
+ // КРИТИЧНО: Очищаем состояние ClientBreakeven при закрытии позиции
9591
+ await CALL_BREAKEVEN_CLEAR_FN(self, self.params.execution.context.symbol, filledSignal, closePrice, closeTimestamp, self.params.execution.context.backtest);
9592
+ await CALL_RISK_REMOVE_SIGNAL_FN(self, self.params.execution.context.symbol, closeTimestamp, self.params.execution.context.backtest);
9593
+ const result = {
9594
+ action: "closed",
9595
+ signal: publicSignal,
9596
+ currentPrice: closePrice,
9597
+ closeReason,
9598
+ closeTimestamp,
9599
+ pnl: publicSignal.pnl,
9600
+ strategyName: self.params.method.context.strategyName,
9601
+ exchangeName: self.params.method.context.exchangeName,
9602
+ frameName: self.params.method.context.frameName,
9603
+ symbol: self.params.execution.context.symbol,
9604
+ backtest: self.params.execution.context.backtest,
9605
+ closeId: filledSignal.closeId,
9606
+ createdAt: closeTimestamp,
9607
+ };
9608
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
9609
+ return result;
9610
+ };
9456
9611
  const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, frameEndTime) => {
9457
9612
  const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
9458
9613
  const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
@@ -9575,6 +9730,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, fra
9575
9730
  totalPartials: publicSignalForCommit.totalPartials,
9576
9731
  note: activatedSignal.activateNote ?? publicSignalForCommit.note,
9577
9732
  });
9733
+ await CALL_SIGNAL_EVENT_FN(self, "opened", pendingSignal, pendingSignal.priceOpen, candle.timestamp);
9578
9734
  await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, pendingSignal.priceOpen, candle.timestamp, self.params.execution.context.backtest);
9579
9735
  await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, pendingSignal, candle.timestamp, self.params.execution.context.backtest);
9580
9736
  return { outcome: "activated", activationIndex: i };
@@ -9670,6 +9826,20 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
9670
9826
  }
9671
9827
  continue;
9672
9828
  }
9829
+ // КРИТИЧНО: Проверяем broker-confirmed TP fill через createTakeProfit() (напр. из onActivePing).
9830
+ // Закрываем по эффективному уровню TP, минуя VWAP-проверку. Sync не пере-подтверждается.
9831
+ if (self._takeProfitSignal) {
9832
+ const filledSignal = self._takeProfitSignal;
9833
+ self._takeProfitSignal = null;
9834
+ return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(self, filledSignal, "take_profit", currentCandleTimestamp);
9835
+ }
9836
+ // КРИТИЧНО: Проверяем broker-confirmed SL fill через createStopLoss() (напр. из onActivePing).
9837
+ // Закрываем по эффективному уровню SL, минуя VWAP-проверку. Sync не пере-подтверждается.
9838
+ if (self._stopLossSignal) {
9839
+ const filledSignal = self._stopLossSignal;
9840
+ self._stopLossSignal = null;
9841
+ return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(self, filledSignal, "stop_loss", currentCandleTimestamp);
9842
+ }
9673
9843
  let shouldClose = false;
9674
9844
  let closeReason;
9675
9845
  // Check time expiration FIRST (КРИТИЧНО!)
@@ -9905,6 +10075,20 @@ class ClientStrategy {
9905
10075
  this._cancelledSignal = null;
9906
10076
  this._closedSignal = null;
9907
10077
  this._activatedSignal = null;
10078
+ /**
10079
+ * Deferred broker-confirmed take-profit fill (set via createTakeProfit). When non-null, the
10080
+ * exchange reported the TP order was actually filled (e.g. by candle high/low) — the next
10081
+ * tick()/backtest() drains it and closes the pending position with closeReason "take_profit"
10082
+ * at the effective take-profit level, bypassing the VWAP-based TP check.
10083
+ */
10084
+ this._takeProfitSignal = null;
10085
+ /**
10086
+ * Deferred broker-confirmed stop-loss fill (set via createStopLoss). When non-null, the
10087
+ * exchange reported the SL order was actually filled (e.g. by candle high/low) — the next
10088
+ * tick()/backtest() drains it and closes the pending position with closeReason "stop_loss"
10089
+ * at the effective stop-loss level, bypassing the VWAP-based SL check.
10090
+ */
10091
+ this._stopLossSignal = null;
9908
10092
  /**
9909
10093
  * User-supplied signal DTO to be consumed by the next GET_SIGNAL_FN tick instead of
9910
10094
  * params.getSignal. Set via createSignal. When non-null, params.getSignal is NOT called
@@ -9978,6 +10162,10 @@ class ClientStrategy {
9978
10162
  // - при null: сигнал закрыт по TP/SL/timeout, флаг больше не нужен
9979
10163
  // - при новом сигнале: флаг от предыдущего сигнала не должен влиять на новый
9980
10164
  this._closedSignal = null;
10165
+ // КРИТИЧНО: Так же сбрасываем отложенные broker-confirmed TP/SL fills — закрытие позиции
10166
+ // любым путём делает их неактуальными, а новая позиция не должна унаследовать чужой fill.
10167
+ this._takeProfitSignal = null;
10168
+ this._stopLossSignal = null;
9981
10169
  // ЗАЩИТА ИНВАРИАНТА: При установке нового pending сигнала очищаем scheduled
9982
10170
  // Не может быть одновременно pending И scheduled (взаимоисключающие состояния)
9983
10171
  // При null: scheduled может существовать (новый сигнал после закрытия позиции)
@@ -10871,6 +11059,7 @@ class ClientStrategy {
10871
11059
  signal: publicSignal,
10872
11060
  note: cancelledSignal.cancelNote ?? cancelledSignal.note,
10873
11061
  });
11062
+ await CALL_SCHEDULE_EVENT_FN$1(this, "cancelled", cancelledSignal, currentPrice, currentTime, "user");
10874
11063
  // Call onCancel callback
10875
11064
  await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, currentTime, this.params.execution.context.backtest);
10876
11065
  const result = {
@@ -10931,6 +11120,7 @@ class ClientStrategy {
10931
11120
  signal: publicSignal,
10932
11121
  note: closedSignal.closeNote ?? closedSignal.note,
10933
11122
  });
11123
+ await CALL_SIGNAL_EVENT_FN(this, "closed", closedSignal, currentPrice, currentTime, "closed");
10934
11124
  // Call onClose callback
10935
11125
  await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, currentTime, this.params.execution.context.backtest);
10936
11126
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
@@ -10956,6 +11146,32 @@ class ClientStrategy {
10956
11146
  await CALL_TICK_CALLBACKS_FN(this, this.params.execution.context.symbol, result, currentTime, this.params.execution.context.backtest);
10957
11147
  return result;
10958
11148
  }
11149
+ // Check if a broker-confirmed take-profit fill is awaiting (createTakeProfit) - close once.
11150
+ // The exchange filled the TP order (e.g. by high/low); close bypassing the VWAP TP check.
11151
+ if (this._takeProfitSignal) {
11152
+ const filledSignal = this._takeProfitSignal;
11153
+ this._takeProfitSignal = null; // Clear after draining
11154
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
11155
+ await PERSIST_STRATEGY_FN(this);
11156
+ this.params.logger.info("ClientStrategy tick: pending signal closed by broker-confirmed take-profit fill", {
11157
+ symbol: this.params.execution.context.symbol,
11158
+ signalId: filledSignal.id,
11159
+ });
11160
+ return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(this, filledSignal, "take_profit", currentTime);
11161
+ }
11162
+ // Check if a broker-confirmed stop-loss fill is awaiting (createStopLoss) - close once.
11163
+ // The exchange filled the SL order (e.g. by high/low); close bypassing the VWAP SL check.
11164
+ if (this._stopLossSignal) {
11165
+ const filledSignal = this._stopLossSignal;
11166
+ this._stopLossSignal = null; // Clear after draining
11167
+ // Persist the cleared deferred state so the drained flag is not replayed on restart
11168
+ await PERSIST_STRATEGY_FN(this);
11169
+ this.params.logger.info("ClientStrategy tick: pending signal closed by broker-confirmed stop-loss fill", {
11170
+ symbol: this.params.execution.context.symbol,
11171
+ signalId: filledSignal.id,
11172
+ });
11173
+ return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(this, filledSignal, "stop_loss", currentTime);
11174
+ }
10959
11175
  // Check if scheduled signal was activated - emit opened event once
10960
11176
  if (this._activatedSignal) {
10961
11177
  const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
@@ -11056,6 +11272,7 @@ class ClientStrategy {
11056
11272
  totalPartials: publicSignalForCommit.totalPartials,
11057
11273
  note: activatedSignal.activateNote ?? publicSignalForCommit.note,
11058
11274
  });
11275
+ await CALL_SIGNAL_EVENT_FN(this, "opened", pendingSignal, currentPrice, currentTime);
11059
11276
  // Call onOpen callback
11060
11277
  await CALL_OPEN_CALLBACKS_FN(this, this.params.execution.context.symbol, pendingSignal, currentPrice, currentTime, this.params.execution.context.backtest);
11061
11278
  const result = {
@@ -11206,6 +11423,7 @@ class ClientStrategy {
11206
11423
  signal: publicSignal,
11207
11424
  note: cancelledSignal.cancelNote ?? cancelledSignal.note,
11208
11425
  });
11426
+ await CALL_SCHEDULE_EVENT_FN$1(this, "cancelled", cancelledSignal, currentPrice, closeTimestamp, "user");
11209
11427
  await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
11210
11428
  const cancelledResult = {
11211
11429
  action: "cancelled",
@@ -11291,6 +11509,24 @@ class ClientStrategy {
11291
11509
  await CALL_TICK_CALLBACKS_FN(this, this.params.execution.context.symbol, closedResult, closeTimestamp, this.params.execution.context.backtest);
11292
11510
  return closedResult;
11293
11511
  }
11512
+ // If a broker-confirmed take-profit fill is awaiting (createTakeProfit) - close once.
11513
+ // The exchange filled the TP order (e.g. by high/low); close bypassing the VWAP TP check.
11514
+ if (this._takeProfitSignal) {
11515
+ this.params.logger.debug("ClientStrategy backtest: pending signal closed by broker-confirmed take-profit fill");
11516
+ const filledSignal = this._takeProfitSignal;
11517
+ this._takeProfitSignal = null; // Clear after draining
11518
+ const closeTimestamp = this.params.execution.context.when.getTime();
11519
+ return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(this, filledSignal, "take_profit", closeTimestamp);
11520
+ }
11521
+ // If a broker-confirmed stop-loss fill is awaiting (createStopLoss) - close once.
11522
+ // The exchange filled the SL order (e.g. by high/low); close bypassing the VWAP SL check.
11523
+ if (this._stopLossSignal) {
11524
+ this.params.logger.debug("ClientStrategy backtest: pending signal closed by broker-confirmed stop-loss fill");
11525
+ const filledSignal = this._stopLossSignal;
11526
+ this._stopLossSignal = null; // Clear after draining
11527
+ const closeTimestamp = this.params.execution.context.when.getTime();
11528
+ return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(this, filledSignal, "stop_loss", closeTimestamp);
11529
+ }
11294
11530
  if (!this._pendingSignal && !this._scheduledSignal) {
11295
11531
  throw new Error("ClientStrategy backtest: no pending or scheduled signal");
11296
11532
  }
@@ -11408,12 +11644,17 @@ class ClientStrategy {
11408
11644
  hasActivatedSignal: this._activatedSignal !== null,
11409
11645
  hasCancelledSignal: this._cancelledSignal !== null,
11410
11646
  hasClosedSignal: this._closedSignal !== null,
11647
+ hasTakeProfitSignal: this._takeProfitSignal !== null,
11648
+ hasStopLossSignal: this._stopLossSignal !== null,
11411
11649
  });
11412
11650
  this._isStopped = true;
11413
11651
  // Clear pending flags to start from clean state
11414
11652
  // NOTE: _isStopped blocks NEW position opening, but allows:
11415
11653
  // - cancelScheduled() / closePending() for graceful shutdown
11416
11654
  // - Monitoring existing _pendingSignal until TP/SL/timeout
11655
+ // NOTE: _takeProfitSignal / _stopLossSignal are NOT cleared — a broker-confirmed fill
11656
+ // reflects a real exchange close that must still drain to emit the closed event, the
11657
+ // same way an existing _pendingSignal keeps being monitored until its natural close.
11417
11658
  this._activatedSignal = null;
11418
11659
  this._cancelledSignal = null;
11419
11660
  this._closedSignal = null;
@@ -11631,9 +11872,97 @@ class ClientStrategy {
11631
11872
  if (this._cancelledSignal) {
11632
11873
  throw new Error(`ClientStrategy createSignal: a scheduled cancel is awaiting for symbol=${symbol}`);
11633
11874
  }
11875
+ if (this._takeProfitSignal) {
11876
+ throw new Error(`ClientStrategy createSignal: a take-profit fill is awaiting for symbol=${symbol}`);
11877
+ }
11878
+ if (this._stopLossSignal) {
11879
+ throw new Error(`ClientStrategy createSignal: a stop-loss fill is awaiting for symbol=${symbol}`);
11880
+ }
11634
11881
  this._userSignal = dto;
11635
11882
  await PERSIST_STRATEGY_FN(this);
11636
11883
  }
11884
+ /**
11885
+ * Reports that the pending position's take-profit order was actually filled on the exchange
11886
+ * (e.g. by candle high/low), forcing a close that does not wait for the VWAP-based TP check.
11887
+ *
11888
+ * The exchange and the strategy are parallel states: ClientStrategy evaluates TP/SL against
11889
+ * VWAP, but the real order may close on high/low. This bridges that gap — the broker confirms
11890
+ * the fill OUT of the async-hooks execution context, the current pending signal is snapshotted
11891
+ * into _takeProfitSignal and cleared, and the next tick()/backtest() drains it, closing the
11892
+ * position with closeReason "take_profit" at the effective take-profit level.
11893
+ *
11894
+ * No-op if no pending signal exists. Persisted (live mode only) so a crash before the next
11895
+ * tick does not lose the deferred close.
11896
+ *
11897
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
11898
+ * @param backtest - Whether running in backtest mode
11899
+ * @param payload - Optional commit id/note attached to the close
11900
+ * @returns Promise that resolves when the take-profit fill is queued
11901
+ */
11902
+ async createTakeProfit(symbol, backtest, payload) {
11903
+ const closeId = payload.id;
11904
+ this.params.logger.debug("ClientStrategy createTakeProfit", {
11905
+ symbol,
11906
+ hasPendingSignal: this._pendingSignal !== null,
11907
+ closeId,
11908
+ });
11909
+ // Snapshot the pending signal for the next tick/backtest to close with reason "take_profit".
11910
+ if (this._pendingSignal) {
11911
+ this._takeProfitSignal = Object.assign({}, this._pendingSignal, {
11912
+ closeId,
11913
+ closeNote: payload.note,
11914
+ });
11915
+ this._pendingSignal = null;
11916
+ }
11917
+ if (backtest) {
11918
+ // Drained in backtest() with correct candle timestamp; no live persistence.
11919
+ return;
11920
+ }
11921
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, symbol, this.params.strategyName, this.params.exchangeName);
11922
+ // Persist deferred _takeProfitSignal so a crash before the next tick does not lose it
11923
+ await PERSIST_STRATEGY_FN(this);
11924
+ }
11925
+ /**
11926
+ * Reports that the pending position's stop-loss order was actually filled on the exchange
11927
+ * (e.g. by candle high/low), forcing a close that does not wait for the VWAP-based SL check.
11928
+ *
11929
+ * The exchange and the strategy are parallel states: ClientStrategy evaluates TP/SL against
11930
+ * VWAP, but the real order may close on high/low. This bridges that gap — the broker confirms
11931
+ * the fill OUT of the async-hooks execution context, the current pending signal is snapshotted
11932
+ * into _stopLossSignal and cleared, and the next tick()/backtest() drains it, closing the
11933
+ * position with closeReason "stop_loss" at the effective stop-loss level.
11934
+ *
11935
+ * No-op if no pending signal exists. Persisted (live mode only) so a crash before the next
11936
+ * tick does not lose the deferred close.
11937
+ *
11938
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
11939
+ * @param backtest - Whether running in backtest mode
11940
+ * @param payload - Optional commit id/note attached to the close
11941
+ * @returns Promise that resolves when the stop-loss fill is queued
11942
+ */
11943
+ async createStopLoss(symbol, backtest, payload) {
11944
+ const closeId = payload.id;
11945
+ this.params.logger.debug("ClientStrategy createStopLoss", {
11946
+ symbol,
11947
+ hasPendingSignal: this._pendingSignal !== null,
11948
+ closeId,
11949
+ });
11950
+ // Snapshot the pending signal for the next tick/backtest to close with reason "stop_loss".
11951
+ if (this._pendingSignal) {
11952
+ this._stopLossSignal = Object.assign({}, this._pendingSignal, {
11953
+ closeId,
11954
+ closeNote: payload.note,
11955
+ });
11956
+ this._pendingSignal = null;
11957
+ }
11958
+ if (backtest) {
11959
+ // Drained in backtest() with correct candle timestamp; no live persistence.
11960
+ return;
11961
+ }
11962
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, symbol, this.params.strategyName, this.params.exchangeName);
11963
+ // Persist deferred _stopLossSignal so a crash before the next tick does not lose it
11964
+ await PERSIST_STRATEGY_FN(this);
11965
+ }
11637
11966
  /**
11638
11967
  * Returns the deferred strategy-state snapshot exactly as it would be written to persist on
11639
11968
  * this iteration: the in-memory _userSignal, _commitQueue and deferred user-action flags,
@@ -11653,6 +11982,8 @@ class ClientStrategy {
11653
11982
  closedSignal: this._closedSignal,
11654
11983
  cancelledSignal: this._cancelledSignal,
11655
11984
  activatedSignal: this._activatedSignal,
11985
+ takeProfitSignal: this._takeProfitSignal,
11986
+ stopLossSignal: this._stopLossSignal,
11656
11987
  };
11657
11988
  }
11658
11989
  /**
@@ -12991,7 +13322,7 @@ const CREATE_SYNC_PENDING_FN = (self, strategyName, exchangeName, frameName, bac
12991
13322
  return true;
12992
13323
  }
12993
13324
  await syncPendingSubject.next(event);
12994
- await self.actionCoreService.orderPing(backtest, event, { strategyName, exchangeName, frameName });
13325
+ await self.actionCoreService.orderCheck(backtest, event, { strategyName, exchangeName, frameName });
12995
13326
  return true;
12996
13327
  }, {
12997
13328
  fallback: (error) => {
@@ -13145,6 +13476,81 @@ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => trycatch(async (symbol, strateg
13145
13476
  },
13146
13477
  defaultValue: null,
13147
13478
  });
13479
+ /**
13480
+ * Creates a callback function for emitting scheduled signal lifecycle events to scheduleEventSubject.
13481
+ *
13482
+ * Called by ClientStrategy when a scheduled signal is created (action "scheduled") or cancelled
13483
+ * before activation (action "cancelled": timeout / price_reject / user). The scheduled -> active
13484
+ * transition is intentionally NOT reported here.
13485
+ *
13486
+ * @param self - Reference to StrategyConnectionService instance
13487
+ * @returns Callback function for scheduled signal lifecycle events
13488
+ */
13489
+ const CREATE_COMMIT_SCHEDULE_EVENT_FN = (self) => trycatch(async (action, symbol, strategyName, exchangeName, data, currentPrice, backtest, timestamp, reason) => {
13490
+ const event = {
13491
+ action,
13492
+ symbol,
13493
+ strategyName,
13494
+ exchangeName,
13495
+ frameName: data.frameName,
13496
+ data,
13497
+ reason,
13498
+ currentPrice,
13499
+ backtest,
13500
+ timestamp,
13501
+ };
13502
+ await scheduleEventSubject.next(event);
13503
+ await self.actionCoreService.scheduleEvent(backtest, event, { strategyName, exchangeName, frameName: data.frameName });
13504
+ }, {
13505
+ fallback: (error) => {
13506
+ const message = "StrategyConnectionService CREATE_COMMIT_SCHEDULE_EVENT_FN thrown";
13507
+ const payload = {
13508
+ error: errorData(error),
13509
+ message: getErrorMessage(error),
13510
+ };
13511
+ self.loggerService.warn(message, payload);
13512
+ console.warn(message, payload);
13513
+ errorEmitter.next(error);
13514
+ },
13515
+ defaultValue: null,
13516
+ });
13517
+ /**
13518
+ * Creates a callback function for emitting pending signal lifecycle events to signalEventSubject.
13519
+ *
13520
+ * Called by ClientStrategy when a pending position is opened (action "opened") or closed
13521
+ * (action "closed" with a closeReason).
13522
+ *
13523
+ * @param self - Reference to StrategyConnectionService instance
13524
+ * @returns Callback function for pending signal lifecycle events
13525
+ */
13526
+ const CREATE_COMMIT_SIGNAL_EVENT_FN = (self) => trycatch(async (action, symbol, strategyName, exchangeName, data, currentPrice, backtest, timestamp, closeReason) => {
13527
+ const event = {
13528
+ action,
13529
+ symbol,
13530
+ strategyName,
13531
+ exchangeName,
13532
+ frameName: data.frameName,
13533
+ data,
13534
+ closeReason,
13535
+ currentPrice,
13536
+ backtest,
13537
+ timestamp,
13538
+ };
13539
+ await signalEventSubject.next(event);
13540
+ await self.actionCoreService.pendingEvent(backtest, event, { strategyName, exchangeName, frameName: data.frameName });
13541
+ }, {
13542
+ fallback: (error) => {
13543
+ const message = "StrategyConnectionService CREATE_COMMIT_SIGNAL_EVENT_FN thrown";
13544
+ const payload = {
13545
+ error: errorData(error),
13546
+ message: getErrorMessage(error),
13547
+ };
13548
+ self.loggerService.warn(message, payload);
13549
+ console.warn(message, payload);
13550
+ errorEmitter.next(error);
13551
+ },
13552
+ defaultValue: null,
13553
+ });
13148
13554
  /**
13149
13555
  * Creates a callback function for emitting idle ping events.
13150
13556
  *
@@ -13437,6 +13843,8 @@ class StrategyConnectionService {
13437
13843
  callbacks,
13438
13844
  onInit: CREATE_COMMIT_INIT_FN(this),
13439
13845
  onSchedulePing: CREATE_COMMIT_SCHEDULE_PING_FN(this),
13846
+ onScheduleEvent: CREATE_COMMIT_SCHEDULE_EVENT_FN(this),
13847
+ onSignalEvent: CREATE_COMMIT_SIGNAL_EVENT_FN(this),
13440
13848
  onActivePing: CREATE_COMMIT_ACTIVE_PING_FN(this),
13441
13849
  onIdlePing: CREATE_COMMIT_IDLE_PING_FN(this),
13442
13850
  onDispose: CREATE_COMMIT_DISPOSE_FN(this),
@@ -14456,6 +14864,50 @@ class StrategyConnectionService {
14456
14864
  const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
14457
14865
  await strategy.createSignal(symbol, currentPrice, dto);
14458
14866
  };
14867
+ /**
14868
+ * Reports that the pending position's take-profit order was actually filled on the exchange
14869
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based TP check.
14870
+ *
14871
+ * Delegates to ClientStrategy.createTakeProfit(). The close is deferred and emitted with
14872
+ * closeReason "take_profit" on the next tick()/backtest(). Works out of the execution context.
14873
+ *
14874
+ * @param backtest - Whether running in backtest mode
14875
+ * @param symbol - Trading pair symbol
14876
+ * @param context - Context with strategyName, exchangeName, frameName
14877
+ * @param payload - Optional commit payload with id and note
14878
+ * @returns Promise that resolves when the take-profit fill is queued
14879
+ */
14880
+ this.createTakeProfit = async (backtest, symbol, context, payload = {}) => {
14881
+ this.loggerService.log("strategyConnectionService createTakeProfit", {
14882
+ symbol,
14883
+ context,
14884
+ payload,
14885
+ });
14886
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
14887
+ await strategy.createTakeProfit(symbol, backtest, payload);
14888
+ };
14889
+ /**
14890
+ * Reports that the pending position's stop-loss order was actually filled on the exchange
14891
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based SL check.
14892
+ *
14893
+ * Delegates to ClientStrategy.createStopLoss(). The close is deferred and emitted with
14894
+ * closeReason "stop_loss" on the next tick()/backtest(). Works out of the execution context.
14895
+ *
14896
+ * @param backtest - Whether running in backtest mode
14897
+ * @param symbol - Trading pair symbol
14898
+ * @param context - Context with strategyName, exchangeName, frameName
14899
+ * @param payload - Optional commit payload with id and note
14900
+ * @returns Promise that resolves when the stop-loss fill is queued
14901
+ */
14902
+ this.createStopLoss = async (backtest, symbol, context, payload = {}) => {
14903
+ this.loggerService.log("strategyConnectionService createStopLoss", {
14904
+ symbol,
14905
+ context,
14906
+ payload,
14907
+ });
14908
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
14909
+ await strategy.createStopLoss(symbol, backtest, payload);
14910
+ };
14459
14911
  /**
14460
14912
  * Returns the in-memory deferred strategy-state snapshot for this iteration.
14461
14913
  *
@@ -16099,6 +16551,54 @@ const CALL_PING_IDLE_FN = trycatch(async (event, self) => {
16099
16551
  },
16100
16552
  defaultValue: null,
16101
16553
  });
16554
+ /**
16555
+ * Wrapper to call scheduleEvent method with error capture.
16556
+ *
16557
+ * Unlike CALL_PING_SCHEDULED_FN there is no hasScheduledSignal guard: the "cancelled" action fires
16558
+ * exactly while the scheduled signal is being removed, so guarding on its presence would suppress it.
16559
+ */
16560
+ const CALL_SCHEDULE_EVENT_FN = trycatch(async (event, self) => {
16561
+ if (!self._target.scheduleEvent) {
16562
+ return;
16563
+ }
16564
+ return await self._target.scheduleEvent(event);
16565
+ }, {
16566
+ fallback: (error) => {
16567
+ const message = "ActionProxy.scheduleEvent thrown";
16568
+ const payload = {
16569
+ error: errorData(error),
16570
+ message: getErrorMessage(error),
16571
+ };
16572
+ LOGGER_SERVICE$5.warn(message, payload);
16573
+ console.warn(message, payload);
16574
+ errorEmitter.next(error);
16575
+ },
16576
+ defaultValue: null,
16577
+ });
16578
+ /**
16579
+ * Wrapper to call pendingEvent method with error capture.
16580
+ *
16581
+ * Like CALL_SCHEDULE_EVENT_FN there is no hasPendingSignal guard: the "closed" action fires exactly
16582
+ * while the pending signal is being removed, so guarding on its presence would suppress it.
16583
+ */
16584
+ const CALL_PENDING_EVENT_FN = trycatch(async (event, self) => {
16585
+ if (!self._target.pendingEvent) {
16586
+ return;
16587
+ }
16588
+ return await self._target.pendingEvent(event);
16589
+ }, {
16590
+ fallback: (error) => {
16591
+ const message = "ActionProxy.pendingEvent thrown";
16592
+ const payload = {
16593
+ error: errorData(error),
16594
+ message: getErrorMessage(error),
16595
+ };
16596
+ LOGGER_SERVICE$5.warn(message, payload);
16597
+ console.warn(message, payload);
16598
+ errorEmitter.next(error);
16599
+ },
16600
+ defaultValue: null,
16601
+ });
16102
16602
  /**
16103
16603
  * Wrapper to call pingActive method with error capture.
16104
16604
  */
@@ -16325,6 +16825,31 @@ class ActionProxy {
16325
16825
  async pingScheduled(event) {
16326
16826
  return await CALL_PING_SCHEDULED_FN(event, this);
16327
16827
  }
16828
+ /**
16829
+ * Handles scheduled signal lifecycle events with error capture.
16830
+ *
16831
+ * Wraps the user's scheduleEvent() method to catch and log any errors. Called once when a
16832
+ * scheduled signal is created (action "scheduled") and once when it is cancelled before
16833
+ * activation (action "cancelled").
16834
+ *
16835
+ * @param event - Scheduled lifecycle data (action discriminates created vs cancelled)
16836
+ * @returns Promise resolving to user's scheduleEvent() result or null on error
16837
+ */
16838
+ async scheduleEvent(event) {
16839
+ return await CALL_SCHEDULE_EVENT_FN(event, this);
16840
+ }
16841
+ /**
16842
+ * Handles pending signal lifecycle events with error capture.
16843
+ *
16844
+ * Wraps the user's pendingEvent() method to catch and log any errors. Called once when a
16845
+ * pending position is opened (action "opened") and once when it is closed (action "closed").
16846
+ *
16847
+ * @param event - Pending lifecycle data (action discriminates opened vs closed)
16848
+ * @returns Promise resolving to user's pendingEvent() result or null on error
16849
+ */
16850
+ async pendingEvent(event) {
16851
+ return await CALL_PENDING_EVENT_FN(event, this);
16852
+ }
16328
16853
  /**
16329
16854
  * Handles active ping events with error capture.
16330
16855
  *
@@ -16383,14 +16908,14 @@ class ActionProxy {
16383
16908
  *
16384
16909
  * @param event - Pending-ping event with action "signal-ping"
16385
16910
  */
16386
- async orderPing(event) {
16387
- if (this._target.orderPing) {
16388
- console.error("Action::orderPing is unwanted cause exchange integration should be implemented in Broker.useBrokerAdapter as an infrastructure domain layer");
16389
- console.error("If you need to check whether the order is still open on the exchange, please use Broker.useBrokerAdapter with onOrderPing");
16390
- console.error("If Action::orderPing throws the framework will close the position with closeReason \"closed\"!");
16911
+ async orderCheck(event) {
16912
+ if (this._target.orderCheck) {
16913
+ console.error("Action::orderCheck is unwanted cause exchange integration should be implemented in Broker.useBrokerAdapter as an infrastructure domain layer");
16914
+ console.error("If you need to check whether the order is still open on the exchange, please use Broker.useBrokerAdapter with onOrderCheck");
16915
+ console.error("If Action::orderCheck throws the framework will close the position with closeReason \"closed\"!");
16391
16916
  console.error("");
16392
16917
  console.error("You have been warned!");
16393
- await this._target.orderPing(event);
16918
+ await this._target.orderCheck(event);
16394
16919
  }
16395
16920
  }
16396
16921
  /**
@@ -16552,6 +17077,40 @@ const CALL_PING_SCHEDULED_CALLBACK_FN = trycatch(async (self, event, strategyNam
16552
17077
  errorEmitter.next(error);
16553
17078
  },
16554
17079
  });
17080
+ /** Wrapper to call scheduled lifecycle (creation / cancellation) callback with error handling */
17081
+ const CALL_SCHEDULE_EVENT_CALLBACK_FN = trycatch(async (self, event, strategyName, frameName, backtest) => {
17082
+ if (self.params.callbacks?.onScheduleEvent) {
17083
+ await self.params.callbacks.onScheduleEvent(event, self.params.actionName, strategyName, frameName, backtest);
17084
+ }
17085
+ }, {
17086
+ fallback: (error, self) => {
17087
+ const message = "ClientAction CALL_SCHEDULE_EVENT_CALLBACK_FN thrown";
17088
+ const payload = {
17089
+ error: errorData(error),
17090
+ message: getErrorMessage(error),
17091
+ };
17092
+ self.params.logger.warn(message, payload);
17093
+ console.warn(message, payload);
17094
+ errorEmitter.next(error);
17095
+ },
17096
+ });
17097
+ /** Wrapper to call pending lifecycle (open / close) callback with error handling */
17098
+ const CALL_PENDING_EVENT_CALLBACK_FN = trycatch(async (self, event, strategyName, frameName, backtest) => {
17099
+ if (self.params.callbacks?.onPendingEvent) {
17100
+ await self.params.callbacks.onPendingEvent(event, self.params.actionName, strategyName, frameName, backtest);
17101
+ }
17102
+ }, {
17103
+ fallback: (error, self) => {
17104
+ const message = "ClientAction CALL_PENDING_EVENT_CALLBACK_FN thrown";
17105
+ const payload = {
17106
+ error: errorData(error),
17107
+ message: getErrorMessage(error),
17108
+ };
17109
+ self.params.logger.warn(message, payload);
17110
+ console.warn(message, payload);
17111
+ errorEmitter.next(error);
17112
+ },
17113
+ });
16555
17114
  /** Wrapper to call idle ping callback with error handling */
16556
17115
  const CALL_PING_IDLE_CALLBACK_FN = trycatch(async (self, event, strategyName, frameName, backtest) => {
16557
17116
  if (self.params.callbacks?.onPingIdle) {
@@ -16613,12 +17172,12 @@ const CALL_SIGNAL_SYNC_CALLBACK_FN = async (self, event, strategyName, frameName
16613
17172
  }
16614
17173
  };
16615
17174
  /**
16616
- * Calls onOrderPing callback WITHOUT trycatch — exceptions must propagate
17175
+ * Calls onOrderCheck callback WITHOUT trycatch — exceptions must propagate
16617
17176
  * up to CREATE_SYNC_PENDING_FN in StrategyConnectionService (which returns false on error).
16618
17177
  */
16619
17178
  const CALL_ORDER_PING_CALLBACK_FN = async (self, event, strategyName, frameName, isBacktest) => {
16620
- if (self.params.callbacks?.onOrderPing) {
16621
- await self.params.callbacks.onOrderPing(event, self.params.actionName, strategyName, frameName, isBacktest);
17179
+ if (self.params.callbacks?.onOrderCheck) {
17180
+ await self.params.callbacks.onOrderCheck(event, self.params.actionName, strategyName, frameName, isBacktest);
16622
17181
  }
16623
17182
  };
16624
17183
  /** Wrapper to call onInit callback with error handling */
@@ -16926,6 +17485,52 @@ class ClientAction {
16926
17485
  await CALL_PING_SCHEDULED_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
16927
17486
  }
16928
17487
  ;
17488
+ /**
17489
+ * Handles scheduled signal lifecycle events (creation / cancellation).
17490
+ *
17491
+ * Manual wiring — EVENT-BASED: users implement {@link IActionCallbacks.onScheduleEvent} (via `addActionSchema`)
17492
+ * to drive the exchange (`commitActivateScheduled` / `commitCancelScheduled`); see that contract
17493
+ * for the full guidance and example. This internal dispatch forwards to the handler/callback.
17494
+ */
17495
+ async scheduleEvent(event) {
17496
+ this.params.logger.debug("ClientAction scheduleEvent", {
17497
+ actionName: this.params.actionName,
17498
+ strategyName: this.params.strategyName,
17499
+ frameName: this.params.frameName,
17500
+ action: event.action,
17501
+ });
17502
+ if (!this._handlerInstance) {
17503
+ await this.waitForInit();
17504
+ }
17505
+ // Call handler method if defined
17506
+ await this._handlerInstance?.scheduleEvent(event);
17507
+ // Call callback if defined
17508
+ await CALL_SCHEDULE_EVENT_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
17509
+ }
17510
+ ;
17511
+ /**
17512
+ * Handles pending signal lifecycle events (open / close).
17513
+ *
17514
+ * Manual wiring — EVENT-BASED: users implement {@link IActionCallbacks.onPendingEvent} (via `addActionSchema`) to
17515
+ * drive the exchange; for per-tick fills use `onPingActive`. See that contract for the full
17516
+ * guidance and example. This internal dispatch forwards to the handler/callback.
17517
+ */
17518
+ async pendingEvent(event) {
17519
+ this.params.logger.debug("ClientAction pendingEvent", {
17520
+ actionName: this.params.actionName,
17521
+ strategyName: this.params.strategyName,
17522
+ frameName: this.params.frameName,
17523
+ action: event.action,
17524
+ });
17525
+ if (!this._handlerInstance) {
17526
+ await this.waitForInit();
17527
+ }
17528
+ // Call handler method if defined
17529
+ await this._handlerInstance?.pendingEvent(event);
17530
+ // Call callback if defined
17531
+ await CALL_PENDING_EVENT_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
17532
+ }
17533
+ ;
16929
17534
  /**
16930
17535
  * Handles active ping events during active pending signal monitoring.
16931
17536
  */
@@ -17003,8 +17608,8 @@ class ClientAction {
17003
17608
  * Gate for the pending-order ping (order still open on exchange?).
17004
17609
  * NOT wrapped in trycatch — exceptions propagate to CREATE_SYNC_PENDING_FN.
17005
17610
  */
17006
- async orderPing(event) {
17007
- this.params.logger.debug("ClientAction orderPing", {
17611
+ async orderCheck(event) {
17612
+ this.params.logger.debug("ClientAction orderCheck", {
17008
17613
  actionName: this.params.actionName,
17009
17614
  strategyName: this.params.strategyName,
17010
17615
  frameName: this.params.frameName,
@@ -17013,7 +17618,7 @@ class ClientAction {
17013
17618
  await this.waitForInit();
17014
17619
  }
17015
17620
  // Call handler method if defined — exceptions propagate
17016
- await this._handlerInstance?.orderPing(event);
17621
+ await this._handlerInstance?.orderCheck(event);
17017
17622
  // Call callback if defined — exceptions propagate
17018
17623
  await CALL_ORDER_PING_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
17019
17624
  }
@@ -17218,6 +17823,36 @@ class ActionConnectionService {
17218
17823
  const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
17219
17824
  await action.pingScheduled(event);
17220
17825
  };
17826
+ /**
17827
+ * Routes a scheduled signal lifecycle event (creation / cancellation) to the ClientAction instance.
17828
+ *
17829
+ * @param event - Scheduled lifecycle event data (action discriminates created vs cancelled)
17830
+ * @param backtest - Whether running in backtest mode
17831
+ * @param context - Execution context with action name, strategy name, exchange name, frame name
17832
+ */
17833
+ this.scheduleEvent = async (event, backtest, context) => {
17834
+ this.loggerService.log("actionConnectionService scheduleEvent", {
17835
+ backtest,
17836
+ context,
17837
+ });
17838
+ const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
17839
+ await action.scheduleEvent(event);
17840
+ };
17841
+ /**
17842
+ * Routes a pending signal lifecycle event (open / close) to the ClientAction instance.
17843
+ *
17844
+ * @param event - Pending lifecycle event data (action discriminates opened vs closed)
17845
+ * @param backtest - Whether running in backtest mode
17846
+ * @param context - Execution context with action name, strategy name, exchange name, frame name
17847
+ */
17848
+ this.pendingEvent = async (event, backtest, context) => {
17849
+ this.loggerService.log("actionConnectionService pendingEvent", {
17850
+ backtest,
17851
+ context,
17852
+ });
17853
+ const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
17854
+ await action.pendingEvent(event);
17855
+ };
17221
17856
  /**
17222
17857
  * Routes active ping event to appropriate ClientAction instance.
17223
17858
  *
@@ -17281,20 +17916,20 @@ class ActionConnectionService {
17281
17916
  await action.signalSync(event);
17282
17917
  };
17283
17918
  /**
17284
- * Routes orderPing event to appropriate ClientAction instance.
17919
+ * Routes orderCheck event to appropriate ClientAction instance.
17285
17920
  * NOT wrapped in trycatch — exceptions propagate to CREATE_SYNC_PENDING_FN.
17286
17921
  *
17287
17922
  * @param event - Pending-ping event with action "signal-ping"
17288
17923
  * @param backtest - Whether running in backtest mode
17289
17924
  * @param context - Execution context
17290
17925
  */
17291
- this.orderPing = async (event, backtest, context) => {
17292
- this.loggerService.log("actionConnectionService orderPing", {
17926
+ this.orderCheck = async (event, backtest, context) => {
17927
+ this.loggerService.log("actionConnectionService orderCheck", {
17293
17928
  backtest,
17294
17929
  context,
17295
17930
  });
17296
17931
  const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
17297
- await action.orderPing(event);
17932
+ await action.orderCheck(event);
17298
17933
  };
17299
17934
  /**
17300
17935
  * Disposes the ClientAction instance for the given action name.
@@ -18162,6 +18797,54 @@ class StrategyCoreService {
18162
18797
  await this.validate(context);
18163
18798
  return await this.strategyConnectionService.createSignal(backtest, symbol, dto, context);
18164
18799
  };
18800
+ /**
18801
+ * Reports that the pending position's take-profit order was actually filled on the exchange
18802
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based TP check.
18803
+ *
18804
+ * Validates the context, then delegates to StrategyConnectionService.createTakeProfit().
18805
+ * The close is deferred and emitted with closeReason "take_profit" on the next tick/backtest.
18806
+ * Does not require execution context.
18807
+ *
18808
+ * @param backtest - Whether running in backtest mode
18809
+ * @param symbol - Trading pair symbol
18810
+ * @param context - Context with strategyName, exchangeName, frameName
18811
+ * @param payload - Optional commit payload with id and note
18812
+ * @returns Promise that resolves when the take-profit fill is queued
18813
+ */
18814
+ this.createTakeProfit = async (backtest, symbol, context, payload = {}) => {
18815
+ this.loggerService.log("strategyCoreService createTakeProfit", {
18816
+ symbol,
18817
+ context,
18818
+ backtest,
18819
+ payload,
18820
+ });
18821
+ await this.validate(context);
18822
+ return await this.strategyConnectionService.createTakeProfit(backtest, symbol, context, payload);
18823
+ };
18824
+ /**
18825
+ * Reports that the pending position's stop-loss order was actually filled on the exchange
18826
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based SL check.
18827
+ *
18828
+ * Validates the context, then delegates to StrategyConnectionService.createStopLoss().
18829
+ * The close is deferred and emitted with closeReason "stop_loss" on the next tick/backtest.
18830
+ * Does not require execution context.
18831
+ *
18832
+ * @param backtest - Whether running in backtest mode
18833
+ * @param symbol - Trading pair symbol
18834
+ * @param context - Context with strategyName, exchangeName, frameName
18835
+ * @param payload - Optional commit payload with id and note
18836
+ * @returns Promise that resolves when the stop-loss fill is queued
18837
+ */
18838
+ this.createStopLoss = async (backtest, symbol, context, payload = {}) => {
18839
+ this.loggerService.log("strategyCoreService createStopLoss", {
18840
+ symbol,
18841
+ context,
18842
+ backtest,
18843
+ payload,
18844
+ });
18845
+ await this.validate(context);
18846
+ return await this.strategyConnectionService.createStopLoss(backtest, symbol, context, payload);
18847
+ };
18165
18848
  /**
18166
18849
  * Returns the in-memory deferred strategy-state snapshot for this iteration.
18167
18850
  *
@@ -19438,6 +20121,48 @@ class ActionCoreService {
19438
20121
  await this.actionConnectionService.pingScheduled(event, backtest, { actionName, ...context });
19439
20122
  }
19440
20123
  };
20124
+ /**
20125
+ * Routes a scheduled signal lifecycle event (creation / cancellation) to all registered actions.
20126
+ *
20127
+ * Retrieves action list from strategy schema (IStrategySchema.actions) and invokes the
20128
+ * scheduleEvent handler on each ClientAction instance sequentially. Called once on creation
20129
+ * (action "scheduled") and once on cancellation before activation (action "cancelled").
20130
+ *
20131
+ * @param backtest - Whether running in backtest mode (true) or live mode (false)
20132
+ * @param event - Scheduled lifecycle event data (action discriminates created vs cancelled)
20133
+ * @param context - Strategy execution context with strategyName, exchangeName, frameName
20134
+ */
20135
+ this.scheduleEvent = async (backtest, event, context) => {
20136
+ this.loggerService.log("actionCoreService scheduleEvent", {
20137
+ context,
20138
+ });
20139
+ await this.validate(context);
20140
+ const { actions = [] } = this.strategySchemaService.get(context.strategyName);
20141
+ for (const actionName of actions) {
20142
+ await this.actionConnectionService.scheduleEvent(event, backtest, { actionName, ...context });
20143
+ }
20144
+ };
20145
+ /**
20146
+ * Routes a pending signal lifecycle event (open / close) to all registered actions.
20147
+ *
20148
+ * Retrieves action list from strategy schema (IStrategySchema.actions) and invokes the
20149
+ * pendingEvent handler on each ClientAction instance sequentially. Called once on open
20150
+ * (action "opened") and once on close (action "closed").
20151
+ *
20152
+ * @param backtest - Whether running in backtest mode (true) or live mode (false)
20153
+ * @param event - Pending lifecycle event data (action discriminates opened vs closed)
20154
+ * @param context - Strategy execution context with strategyName, exchangeName, frameName
20155
+ */
20156
+ this.pendingEvent = async (backtest, event, context) => {
20157
+ this.loggerService.log("actionCoreService pendingEvent", {
20158
+ context,
20159
+ });
20160
+ await this.validate(context);
20161
+ const { actions = [] } = this.strategySchemaService.get(context.strategyName);
20162
+ for (const actionName of actions) {
20163
+ await this.actionConnectionService.pendingEvent(event, backtest, { actionName, ...context });
20164
+ }
20165
+ };
19441
20166
  /**
19442
20167
  * Routes active ping event to all registered actions for the strategy.
19443
20168
  *
@@ -19528,14 +20253,14 @@ class ActionCoreService {
19528
20253
  * @param event - Pending-ping event with action "signal-ping"
19529
20254
  * @param context - Strategy execution context
19530
20255
  */
19531
- this.orderPing = async (backtest, event, context) => {
19532
- this.loggerService.log("actionCoreService orderPing", {
20256
+ this.orderCheck = async (backtest, event, context) => {
20257
+ this.loggerService.log("actionCoreService orderCheck", {
19533
20258
  context,
19534
20259
  });
19535
20260
  await this.validate(context);
19536
20261
  const { actions = [] } = this.strategySchemaService.get(context.strategyName);
19537
20262
  for (const actionName of actions) {
19538
- await this.actionConnectionService.orderPing(event, backtest, { actionName, ...context });
20263
+ await this.actionConnectionService.orderCheck(event, backtest, { actionName, ...context });
19539
20264
  }
19540
20265
  };
19541
20266
  /**
@@ -20003,6 +20728,8 @@ const VALID_METHOD_NAMES = [
20003
20728
  "partialLossAvailable",
20004
20729
  "pingScheduled",
20005
20730
  "pingActive",
20731
+ "scheduleEvent",
20732
+ "pendingEvent",
20006
20733
  "riskRejection",
20007
20734
  "dispose",
20008
20735
  ];
@@ -20013,7 +20740,7 @@ const VALID_METHOD_NAMES = [
20013
20740
  * method one of these produces a dedicated error redirecting to the Broker adapter instead of the
20014
20741
  * generic "invalid method" suggestion.
20015
20742
  */
20016
- const DISCOURAGED_METHOD_NAMES = ["signalSync", "orderPing"];
20743
+ const DISCOURAGED_METHOD_NAMES = ["signalSync", "orderCheck"];
20017
20744
  /**
20018
20745
  * Builds the dedicated error message for a discouraged exchange-integration handler method.
20019
20746
  *
@@ -20025,7 +20752,7 @@ const DISCOURAGED_METHOD_MESSAGE = (actionName, methodName) => str.newline([
20025
20752
  `ActionSchema ${actionName} contains discouraged method "${methodName}". `,
20026
20753
  `Exchange integration must be implemented in Broker.useBrokerAdapter as the infrastructure domain layer, not in an action handler.`,
20027
20754
  typo.nbsp,
20028
- `Use Broker.useBrokerAdapter with onOrderPing (order still open?) and onSignalOpenCommit / onSignalCloseCommit (order fill) instead.`,
20755
+ `Use Broker.useBrokerAdapter with onOrderCheck (order still open?) and onSignalOpenCommit / onSignalCloseCommit (order fill) instead.`,
20029
20756
  typo.nbsp,
20030
20757
  `If you want to keep this property name use one of these patterns: _${methodName} or #${methodName}`,
20031
20758
  ]);
@@ -42404,6 +43131,13 @@ const breakevenNewTakeProfitPrice = (priceTakeProfit, trailingPriceTakeProfit) =
42404
43131
  const BROKER_METHOD_NAME_COMMIT_SIGNAL_OPEN = "BrokerAdapter.commitSignalOpen";
42405
43132
  const BROKER_METHOD_NAME_COMMIT_SIGNAL_CLOSE = "BrokerAdapter.commitSignalClose";
42406
43133
  const BROKER_METHOD_NAME_COMMIT_SIGNAL_PENDING = "BrokerAdapter.commitSignalPending";
43134
+ const BROKER_METHOD_NAME_COMMIT_ACTIVE_PING = "BrokerAdapter.commitActivePing";
43135
+ const BROKER_METHOD_NAME_COMMIT_SCHEDULE_PING = "BrokerAdapter.commitSchedulePing";
43136
+ const BROKER_METHOD_NAME_COMMIT_IDLE_PING = "BrokerAdapter.commitIdlePing";
43137
+ const BROKER_METHOD_NAME_COMMIT_SCHEDULE_OPEN = "BrokerAdapter.commitScheduleOpen";
43138
+ const BROKER_METHOD_NAME_COMMIT_SCHEDULE_CANCELLED = "BrokerAdapter.commitScheduleCancelled";
43139
+ const BROKER_METHOD_NAME_COMMIT_PENDING_OPEN = "BrokerAdapter.commitPendingOpen";
43140
+ const BROKER_METHOD_NAME_COMMIT_PENDING_CLOSE = "BrokerAdapter.commitPendingClose";
42407
43141
  const BROKER_METHOD_NAME_COMMIT_PARTIAL_PROFIT = "BrokerAdapter.commitPartialProfit";
42408
43142
  const BROKER_METHOD_NAME_COMMIT_PARTIAL_LOSS = "BrokerAdapter.commitPartialLoss";
42409
43143
  const BROKER_METHOD_NAME_COMMIT_TRAILING_STOP = "BrokerAdapter.commitTrailingStop";
@@ -42417,7 +43151,14 @@ const BROKER_METHOD_NAME_CLEAR = "BrokerAdapter.clear";
42417
43151
  const BROKER_BASE_METHOD_NAME_WAIT_FOR_INIT = "BrokerBase.waitForInit";
42418
43152
  const BROKER_BASE_METHOD_NAME_ON_SIGNAL_OPEN = "BrokerBase.onSignalOpenCommit";
42419
43153
  const BROKER_BASE_METHOD_NAME_ON_SIGNAL_CLOSE = "BrokerBase.onSignalCloseCommit";
42420
- const BROKER_BASE_METHOD_NAME_ON_SIGNAL_PENDING = "BrokerBase.onOrderPing";
43154
+ const BROKER_BASE_METHOD_NAME_ON_SIGNAL_PENDING = "BrokerBase.onOrderCheck";
43155
+ const BROKER_BASE_METHOD_NAME_ON_ACTIVE_PING = "BrokerBase.onSignalActivePing";
43156
+ const BROKER_BASE_METHOD_NAME_ON_SCHEDULE_PING = "BrokerBase.onSignalSchedulePing";
43157
+ const BROKER_BASE_METHOD_NAME_ON_IDLE_PING = "BrokerBase.onSignalIdlePing";
43158
+ const BROKER_BASE_METHOD_NAME_ON_SCHEDULE_OPEN = "BrokerBase.onSignalScheduleOpen";
43159
+ const BROKER_BASE_METHOD_NAME_ON_SCHEDULE_CANCELLED = "BrokerBase.onSignalScheduleCancelled";
43160
+ const BROKER_BASE_METHOD_NAME_ON_PENDING_OPEN = "BrokerBase.onSignalPendingOpen";
43161
+ const BROKER_BASE_METHOD_NAME_ON_PENDING_CLOSE = "BrokerBase.onSignalPendingClose";
42421
43162
  const BROKER_BASE_METHOD_NAME_ON_PARTIAL_PROFIT = "BrokerBase.onPartialProfitCommit";
42422
43163
  const BROKER_BASE_METHOD_NAME_ON_PARTIAL_LOSS = "BrokerBase.onPartialLossCommit";
42423
43164
  const BROKER_BASE_METHOD_NAME_ON_TRAILING_STOP = "BrokerBase.onTrailingStopCommit";
@@ -42469,7 +43210,7 @@ class BrokerProxy {
42469
43210
  /**
42470
43211
  * Forwards a pending-order ping to the underlying adapter.
42471
43212
  *
42472
- * If the adapter does not implement `onOrderPing`, the call is silently skipped
43213
+ * If the adapter does not implement `onOrderCheck`, the call is silently skipped
42473
43214
  * (the order is assumed still open). When implemented, exceptions propagate — a throw means
42474
43215
  * the order was NOT FOUND by `payload.signalId` and the framework closes the position with
42475
43216
  * closeReason "closed". The adapter must throw ONLY on a confirmed "order not found by id"
@@ -42478,10 +43219,101 @@ class BrokerProxy {
42478
43219
  *
42479
43220
  * @param payload - Pending ping details: symbol, signalId, position, prices, pnl, context, backtest flag.
42480
43221
  */
42481
- async onOrderPing(payload) {
42482
- if (this._instance.onOrderPing) {
43222
+ async onOrderCheck(payload) {
43223
+ if (this._instance.onOrderCheck) {
43224
+ await this.waitForInit();
43225
+ await this._instance.onOrderCheck(payload);
43226
+ return;
43227
+ }
43228
+ }
43229
+ /**
43230
+ * Forwards an active-ping event to the underlying adapter.
43231
+ * Silently skipped when the adapter does not implement `onSignalActivePing`.
43232
+ *
43233
+ * @param payload - Active ping details: symbol, signalId, position, prices, pnl, context, backtest.
43234
+ */
43235
+ async onSignalActivePing(payload) {
43236
+ if (this._instance.onSignalActivePing) {
43237
+ await this.waitForInit();
43238
+ await this._instance.onSignalActivePing(payload);
43239
+ return;
43240
+ }
43241
+ }
43242
+ /**
43243
+ * Forwards a schedule-ping event to the underlying adapter.
43244
+ * Silently skipped when the adapter does not implement `onSignalSchedulePing`.
43245
+ *
43246
+ * @param payload - Schedule ping details: symbol, signalId, position, prices, context, backtest.
43247
+ */
43248
+ async onSignalSchedulePing(payload) {
43249
+ if (this._instance.onSignalSchedulePing) {
43250
+ await this.waitForInit();
43251
+ await this._instance.onSignalSchedulePing(payload);
43252
+ return;
43253
+ }
43254
+ }
43255
+ /**
43256
+ * Forwards an idle-ping event to the underlying adapter.
43257
+ * Silently skipped when the adapter does not implement `onSignalIdlePing`.
43258
+ *
43259
+ * @param payload - Idle ping details: symbol, currentPrice, context, backtest.
43260
+ */
43261
+ async onSignalIdlePing(payload) {
43262
+ if (this._instance.onSignalIdlePing) {
43263
+ await this.waitForInit();
43264
+ await this._instance.onSignalIdlePing(payload);
43265
+ return;
43266
+ }
43267
+ }
43268
+ /**
43269
+ * Forwards a scheduled-signal-open event to the underlying adapter.
43270
+ * Silently skipped when the adapter does not implement `onSignalScheduleOpen`.
43271
+ *
43272
+ * @param payload - Scheduled open details: symbol, signalId, position, prices, context, backtest.
43273
+ */
43274
+ async onSignalScheduleOpen(payload) {
43275
+ if (this._instance.onSignalScheduleOpen) {
43276
+ await this.waitForInit();
43277
+ await this._instance.onSignalScheduleOpen(payload);
43278
+ return;
43279
+ }
43280
+ }
43281
+ /**
43282
+ * Forwards a scheduled-signal-cancelled event to the underlying adapter.
43283
+ * Silently skipped when the adapter does not implement `onSignalScheduleCancelled`.
43284
+ *
43285
+ * @param payload - Scheduled cancel details: symbol, signalId, position, prices, reason, context, backtest.
43286
+ */
43287
+ async onSignalScheduleCancelled(payload) {
43288
+ if (this._instance.onSignalScheduleCancelled) {
43289
+ await this.waitForInit();
43290
+ await this._instance.onSignalScheduleCancelled(payload);
43291
+ return;
43292
+ }
43293
+ }
43294
+ /**
43295
+ * Forwards a pending-signal-open event to the underlying adapter.
43296
+ * Silently skipped when the adapter does not implement `onSignalPendingOpen`.
43297
+ *
43298
+ * @param payload - Pending open details: symbol, signalId, position, prices, context, backtest.
43299
+ */
43300
+ async onSignalPendingOpen(payload) {
43301
+ if (this._instance.onSignalPendingOpen) {
42483
43302
  await this.waitForInit();
42484
- await this._instance.onOrderPing(payload);
43303
+ await this._instance.onSignalPendingOpen(payload);
43304
+ return;
43305
+ }
43306
+ }
43307
+ /**
43308
+ * Forwards a pending-signal-close event to the underlying adapter.
43309
+ * Silently skipped when the adapter does not implement `onSignalPendingClose`.
43310
+ *
43311
+ * @param payload - Pending close details: symbol, signalId, position, prices, closeReason, context, backtest.
43312
+ */
43313
+ async onSignalPendingClose(payload) {
43314
+ if (this._instance.onSignalPendingClose) {
43315
+ await this.waitForInit();
43316
+ await this._instance.onSignalPendingClose(payload);
42485
43317
  return;
42486
43318
  }
42487
43319
  }
@@ -42737,7 +43569,178 @@ class BrokerAdapter {
42737
43569
  }
42738
43570
  const instance = this.getInstance();
42739
43571
  if (instance) {
42740
- await instance.onOrderPing(payload);
43572
+ await instance.onOrderCheck(payload);
43573
+ }
43574
+ };
43575
+ /**
43576
+ * Forwards an active-ping to the registered broker adapter.
43577
+ *
43578
+ * Called automatically via activePingSubject when `enable()` is active, on every live tick while a
43579
+ * pending signal is monitored. Skipped silently in backtest mode or when no adapter is registered.
43580
+ * Purely informational — a throw does NOT close the position.
43581
+ *
43582
+ * @param payload - Active ping details: symbol, signalId, position, prices, pnl, context, backtest
43583
+ */
43584
+ this.commitActivePing = async (payload) => {
43585
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_ACTIVE_PING, {
43586
+ symbol: payload.symbol,
43587
+ context: payload.context,
43588
+ });
43589
+ if (!this.enable.hasValue()) {
43590
+ return;
43591
+ }
43592
+ if (payload.backtest) {
43593
+ return;
43594
+ }
43595
+ const instance = this.getInstance();
43596
+ if (instance) {
43597
+ await instance.onSignalActivePing(payload);
43598
+ }
43599
+ };
43600
+ /**
43601
+ * Forwards a schedule-ping to the registered broker adapter.
43602
+ *
43603
+ * Called automatically via schedulePingSubject when `enable()` is active, on every live tick while
43604
+ * a scheduled signal is monitored. Skipped silently in backtest mode or when no adapter is
43605
+ * registered. Purely informational.
43606
+ *
43607
+ * @param payload - Schedule ping details: symbol, signalId, position, prices, context, backtest
43608
+ */
43609
+ this.commitSchedulePing = async (payload) => {
43610
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_SCHEDULE_PING, {
43611
+ symbol: payload.symbol,
43612
+ context: payload.context,
43613
+ });
43614
+ if (!this.enable.hasValue()) {
43615
+ return;
43616
+ }
43617
+ if (payload.backtest) {
43618
+ return;
43619
+ }
43620
+ const instance = this.getInstance();
43621
+ if (instance) {
43622
+ await instance.onSignalSchedulePing(payload);
43623
+ }
43624
+ };
43625
+ /**
43626
+ * Forwards an idle-ping to the registered broker adapter.
43627
+ *
43628
+ * Called automatically via idlePingSubject when `enable()` is active, on every live tick while the
43629
+ * strategy has no pending or scheduled signal. Skipped silently in backtest mode or when no adapter
43630
+ * is registered. Purely informational.
43631
+ *
43632
+ * @param payload - Idle ping details: symbol, currentPrice, context, backtest
43633
+ */
43634
+ this.commitIdlePing = async (payload) => {
43635
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_IDLE_PING, {
43636
+ symbol: payload.symbol,
43637
+ context: payload.context,
43638
+ });
43639
+ if (!this.enable.hasValue()) {
43640
+ return;
43641
+ }
43642
+ if (payload.backtest) {
43643
+ return;
43644
+ }
43645
+ const instance = this.getInstance();
43646
+ if (instance) {
43647
+ await instance.onSignalIdlePing(payload);
43648
+ }
43649
+ };
43650
+ /**
43651
+ * Forwards a scheduled-signal-open to the registered broker adapter.
43652
+ *
43653
+ * Called automatically via scheduleEventSubject (action "scheduled") when a scheduled signal is
43654
+ * created. Skipped silently in backtest mode or when no adapter is registered.
43655
+ *
43656
+ * @param payload - Scheduled open details: symbol, signalId, position, prices, context, backtest
43657
+ */
43658
+ this.commitScheduleOpen = async (payload) => {
43659
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_SCHEDULE_OPEN, {
43660
+ symbol: payload.symbol,
43661
+ context: payload.context,
43662
+ });
43663
+ if (!this.enable.hasValue()) {
43664
+ return;
43665
+ }
43666
+ if (payload.backtest) {
43667
+ return;
43668
+ }
43669
+ const instance = this.getInstance();
43670
+ if (instance) {
43671
+ await instance.onSignalScheduleOpen(payload);
43672
+ }
43673
+ };
43674
+ /**
43675
+ * Forwards a scheduled-signal-cancelled to the registered broker adapter.
43676
+ *
43677
+ * Called automatically via scheduleEventSubject (action "cancelled") when a scheduled signal is
43678
+ * removed before activation. Skipped silently in backtest mode or when no adapter is registered.
43679
+ *
43680
+ * @param payload - Scheduled cancel details: symbol, signalId, position, prices, reason, context, backtest
43681
+ */
43682
+ this.commitScheduleCancelled = async (payload) => {
43683
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_SCHEDULE_CANCELLED, {
43684
+ symbol: payload.symbol,
43685
+ context: payload.context,
43686
+ });
43687
+ if (!this.enable.hasValue()) {
43688
+ return;
43689
+ }
43690
+ if (payload.backtest) {
43691
+ return;
43692
+ }
43693
+ const instance = this.getInstance();
43694
+ if (instance) {
43695
+ await instance.onSignalScheduleCancelled(payload);
43696
+ }
43697
+ };
43698
+ /**
43699
+ * Forwards a pending-signal-open to the registered broker adapter.
43700
+ *
43701
+ * Called automatically via signalEventSubject (action "opened") when a pending position is opened.
43702
+ * Skipped silently in backtest mode or when no adapter is registered.
43703
+ *
43704
+ * @param payload - Pending open details: symbol, signalId, position, prices, context, backtest
43705
+ */
43706
+ this.commitPendingOpen = async (payload) => {
43707
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_PENDING_OPEN, {
43708
+ symbol: payload.symbol,
43709
+ context: payload.context,
43710
+ });
43711
+ if (!this.enable.hasValue()) {
43712
+ return;
43713
+ }
43714
+ if (payload.backtest) {
43715
+ return;
43716
+ }
43717
+ const instance = this.getInstance();
43718
+ if (instance) {
43719
+ await instance.onSignalPendingOpen(payload);
43720
+ }
43721
+ };
43722
+ /**
43723
+ * Forwards a pending-signal-close to the registered broker adapter.
43724
+ *
43725
+ * Called automatically via signalEventSubject (action "closed") when a pending position is closed.
43726
+ * Skipped silently in backtest mode or when no adapter is registered.
43727
+ *
43728
+ * @param payload - Pending close details: symbol, signalId, position, prices, closeReason, context, backtest
43729
+ */
43730
+ this.commitPendingClose = async (payload) => {
43731
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_PENDING_CLOSE, {
43732
+ symbol: payload.symbol,
43733
+ context: payload.context,
43734
+ });
43735
+ if (!this.enable.hasValue()) {
43736
+ return;
43737
+ }
43738
+ if (payload.backtest) {
43739
+ return;
43740
+ }
43741
+ const instance = this.getInstance();
43742
+ if (instance) {
43743
+ await instance.onSignalPendingClose(payload);
42741
43744
  }
42742
43745
  };
42743
43746
  /**
@@ -43105,7 +44108,98 @@ class BrokerAdapter {
43105
44108
  backtest: event.backtest,
43106
44109
  });
43107
44110
  });
43108
- const disposeFn = compose(() => unSignalOpen(), () => unSignalClose(), () => unSignalPending());
44111
+ const unActivePing = activePingSubject.subscribe(async (event) => {
44112
+ await this.commitActivePing({
44113
+ symbol: event.symbol,
44114
+ signalId: event.data.id,
44115
+ position: event.data.position,
44116
+ currentPrice: event.currentPrice,
44117
+ priceOpen: event.data.priceOpen,
44118
+ priceTakeProfit: event.data.priceTakeProfit,
44119
+ priceStopLoss: event.data.priceStopLoss,
44120
+ pnl: event.data.pnl,
44121
+ context: {
44122
+ strategyName: event.strategyName,
44123
+ exchangeName: event.exchangeName,
44124
+ frameName: event.frameName,
44125
+ },
44126
+ backtest: event.backtest,
44127
+ });
44128
+ });
44129
+ const unSchedulePing = schedulePingSubject.subscribe(async (event) => {
44130
+ await this.commitSchedulePing({
44131
+ symbol: event.symbol,
44132
+ signalId: event.data.id,
44133
+ position: event.data.position,
44134
+ currentPrice: event.currentPrice,
44135
+ priceOpen: event.data.priceOpen,
44136
+ priceTakeProfit: event.data.priceTakeProfit,
44137
+ priceStopLoss: event.data.priceStopLoss,
44138
+ context: {
44139
+ strategyName: event.strategyName,
44140
+ exchangeName: event.exchangeName,
44141
+ frameName: event.frameName,
44142
+ },
44143
+ backtest: event.backtest,
44144
+ });
44145
+ });
44146
+ const unIdlePing = idlePingSubject.subscribe(async (event) => {
44147
+ await this.commitIdlePing({
44148
+ symbol: event.symbol,
44149
+ currentPrice: event.currentPrice,
44150
+ context: {
44151
+ strategyName: event.strategyName,
44152
+ exchangeName: event.exchangeName,
44153
+ frameName: event.frameName,
44154
+ },
44155
+ backtest: event.backtest,
44156
+ });
44157
+ });
44158
+ const unScheduleEvent = scheduleEventSubject.subscribe(async (event) => {
44159
+ const payload = {
44160
+ symbol: event.symbol,
44161
+ signalId: event.data.id,
44162
+ position: event.data.position,
44163
+ currentPrice: event.currentPrice,
44164
+ priceOpen: event.data.priceOpen,
44165
+ priceTakeProfit: event.data.priceTakeProfit,
44166
+ priceStopLoss: event.data.priceStopLoss,
44167
+ context: {
44168
+ strategyName: event.strategyName,
44169
+ exchangeName: event.exchangeName,
44170
+ frameName: event.frameName,
44171
+ },
44172
+ backtest: event.backtest,
44173
+ };
44174
+ if (event.action === "scheduled") {
44175
+ await this.commitScheduleOpen(payload);
44176
+ return;
44177
+ }
44178
+ await this.commitScheduleCancelled({ ...payload, reason: event.reason });
44179
+ });
44180
+ const unSignalEvent = signalEventSubject.subscribe(async (event) => {
44181
+ const payload = {
44182
+ symbol: event.symbol,
44183
+ signalId: event.data.id,
44184
+ position: event.data.position,
44185
+ currentPrice: event.currentPrice,
44186
+ priceOpen: event.data.priceOpen,
44187
+ priceTakeProfit: event.data.priceTakeProfit,
44188
+ priceStopLoss: event.data.priceStopLoss,
44189
+ context: {
44190
+ strategyName: event.strategyName,
44191
+ exchangeName: event.exchangeName,
44192
+ frameName: event.frameName,
44193
+ },
44194
+ backtest: event.backtest,
44195
+ };
44196
+ if (event.action === "opened") {
44197
+ await this.commitPendingOpen(payload);
44198
+ return;
44199
+ }
44200
+ await this.commitPendingClose({ ...payload, closeReason: event.closeReason });
44201
+ });
44202
+ const disposeFn = compose(() => unSignalOpen(), () => unSignalClose(), () => unSignalPending(), () => unActivePing(), () => unSchedulePing(), () => unIdlePing(), () => unScheduleEvent(), () => unSignalEvent());
43109
44203
  return () => {
43110
44204
  this.enable.clear();
43111
44205
  disposeFn();
@@ -43251,24 +44345,32 @@ class BrokerBase {
43251
44345
  bt.loggerService.info(BROKER_BASE_METHOD_NAME_WAIT_FOR_INIT, {});
43252
44346
  }
43253
44347
  /**
43254
- * Called when a new position is opened (signal activated).
44348
+ * Called when a position is being opened (signal activated).
43255
44349
  *
43256
44350
  * Triggered automatically via syncSubject when a scheduled signal's priceOpen is hit.
43257
44351
  * Use to place the actual entry order on the exchange.
43258
44352
  *
43259
44353
  * Default implementation: Logs signal-open event.
43260
44354
  *
44355
+ * Manual wiring — EXCEPTION-BASED GATE: emitted BEFORE the framework mutates state, so a THROW here
44356
+ * (e.g. limit order rejected) rolls back the open — the pending signal returns to idle and retries
44357
+ * next tick; return normally to let it open. Live-only (backtest short-circuits). See
44358
+ * {@link IBroker.onSignalOpenCommit} for the full semantics.
44359
+ *
43261
44360
  * @param payload - Signal open details: symbol, cost, position, priceOpen, priceTakeProfit, priceStopLoss, context, backtest
43262
44361
  *
43263
44362
  * @example
43264
44363
  * ```typescript
43265
44364
  * async onSignalOpenCommit(payload: BrokerSignalOpenPayload) {
43266
44365
  * super.onSignalOpenCommit(payload); // Keep parent logging
43267
- * await this.exchange.placeMarketOrder({
44366
+ * const order = await this.exchange.placeMarketOrder({
43268
44367
  * symbol: payload.symbol,
43269
44368
  * side: payload.position === "long" ? "BUY" : "SELL",
43270
44369
  * quantity: payload.cost / payload.priceOpen,
43271
44370
  * });
44371
+ * if (!order.filled) {
44372
+ * throw new Error(`Entry not filled for ${payload.symbol}`); // -> roll back the open, retry next tick
44373
+ * }
43272
44374
  * }
43273
44375
  * ```
43274
44376
  */
@@ -43290,47 +44392,158 @@ class BrokerBase {
43290
44392
  * normally instead of throwing. A thrown network error would wrongly close an open position; only
43291
44393
  * a confirmed "order not found by id" response is a valid reason to throw.
43292
44394
  *
43293
- * @param payload - Pending ping details: symbol, signalId, position, prices, pnl, context, backtest
44395
+ * Manual wiring — EXCEPTION-BASED VARIANT: the throw-driven alternative to the imperative
44396
+ * commit-function wiring in `onSignalActivePing`. See {@link IBroker.onOrderCheck} for the full
44397
+ * comparison and example.
43294
44398
  *
43295
- * @example
43296
- * ```typescript
43297
- * async onOrderPing(payload: BrokerSignalPendingPayload) {
43298
- * super.onOrderPing(payload); // Keep parent logging
43299
- * let order: Order | null;
43300
- * try {
43301
- * order = await this.exchange.getOrderById(payload.signalId);
43302
- * } catch (networkError) {
43303
- * // Transient/network error — swallow and keep the position open, retry next tick
43304
- * return;
43305
- * }
43306
- * if (!order) {
43307
- * // Confirmed not found by id — close the position
43308
- * throw new Error(`Order ${payload.signalId} not found on the exchange`);
43309
- * }
43310
- * }
43311
- * ```
44399
+ * @param payload - Pending ping details: symbol, signalId, position, prices, pnl, context, backtest
43312
44400
  */
43313
- async onOrderPing(payload) {
44401
+ async onOrderCheck(payload) {
43314
44402
  bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SIGNAL_PENDING, {
43315
44403
  symbol: payload.symbol,
43316
44404
  context: payload.context,
43317
44405
  });
43318
44406
  }
43319
44407
  /**
43320
- * Called when a position is fully closed (SL/TP hit or manual close).
44408
+ * Called on every live tick while a pending (open) signal is monitored.
44409
+ *
44410
+ * Purely informational mirror of the active-ping lifecycle — unlike `onOrderCheck`, a throw here
44411
+ * does NOT close the position. Override to mirror live monitoring state into your own systems.
44412
+ * The default implementation logs.
44413
+ *
44414
+ * Manual wiring — EVENT-BASED: this is the primary per-tick hook to drive an open position from real exchange
44415
+ * state (`commitCreateTakeProfit` / `commitCreateStopLoss` / `commitClosePending`). See the
44416
+ * {@link IBroker.onSignalActivePing} contract docs for the full guidance and example.
44417
+ *
44418
+ * @param payload - Active ping details: symbol, signalId, position, prices, pnl, context, backtest
44419
+ */
44420
+ async onSignalActivePing(payload) {
44421
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_ACTIVE_PING, {
44422
+ symbol: payload.symbol,
44423
+ context: payload.context,
44424
+ });
44425
+ }
44426
+ /**
44427
+ * Called on every live tick while a scheduled signal is monitored (waiting for priceOpen).
44428
+ *
44429
+ * Purely informational. Override to mirror scheduled-monitoring state. The default logs.
44430
+ *
44431
+ * Manual wiring — EVENT-BASED: per-tick hook to drive a scheduled (resting) order from real exchange state
44432
+ * (`commitActivateScheduled` / `commitCancelScheduled`). See {@link IBroker.onSignalSchedulePing}
44433
+ * for full guidance and example.
44434
+ *
44435
+ * @param payload - Schedule ping details: symbol, signalId, position, prices, context, backtest
44436
+ */
44437
+ async onSignalSchedulePing(payload) {
44438
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SCHEDULE_PING, {
44439
+ symbol: payload.symbol,
44440
+ context: payload.context,
44441
+ });
44442
+ }
44443
+ /**
44444
+ * Called on every live tick while the strategy is idle (no pending or scheduled signal).
44445
+ *
44446
+ * Purely informational. Override to track idle heartbeats. The default logs.
44447
+ *
44448
+ * @param payload - Idle ping details: symbol, currentPrice, context, backtest
44449
+ */
44450
+ async onSignalIdlePing(payload) {
44451
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_IDLE_PING, {
44452
+ symbol: payload.symbol,
44453
+ context: payload.context,
44454
+ });
44455
+ }
44456
+ /**
44457
+ * Called when a new scheduled signal is created and starts waiting for priceOpen activation.
44458
+ *
44459
+ * The scheduled -> active transition is reported via `onSignalOpenCommit`, not here. Override to
44460
+ * place a resting/limit order on the exchange. The default logs.
44461
+ *
44462
+ * Manual wiring — EVENT-BASED: fires ONCE at creation — place the real resting order (tag it with
44463
+ * `payload.signalId`) and optionally `commitActivateScheduled` / `commitCancelScheduled`. See
44464
+ * {@link IBroker.onSignalScheduleOpen} for full guidance and example.
44465
+ *
44466
+ * @param payload - Scheduled open details: symbol, signalId, position, prices, context, backtest
44467
+ */
44468
+ async onSignalScheduleOpen(payload) {
44469
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SCHEDULE_OPEN, {
44470
+ symbol: payload.symbol,
44471
+ context: payload.context,
44472
+ });
44473
+ }
44474
+ /**
44475
+ * Called when a scheduled signal is cancelled before activation (timeout / price_reject / user).
44476
+ *
44477
+ * Override to cancel the resting/limit order on the exchange. The default logs.
44478
+ *
44479
+ * Manual wiring — EVENT-BASED (outbound): the strategy already dropped the scheduled signal — cancel the matching
44480
+ * exchange order by `payload.signalId`. See {@link IBroker.onSignalScheduleCancelled}.
44481
+ *
44482
+ * @param payload - Scheduled cancel details: symbol, signalId, position, prices, reason, context, backtest
44483
+ */
44484
+ async onSignalScheduleCancelled(payload) {
44485
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SCHEDULE_CANCELLED, {
44486
+ symbol: payload.symbol,
44487
+ context: payload.context,
44488
+ });
44489
+ }
44490
+ /**
44491
+ * Called when a pending position is opened (new signal / immediate / scheduled or user activation).
44492
+ *
44493
+ * Informational lifecycle hook. Override to mirror the open into your own systems. The default logs.
44494
+ *
44495
+ * Manual wiring — EVENT-BASED: fires ONCE at open — place entry + protective TP/SL orders (tag with
44496
+ * `payload.signalId`), then drive per-tick from `onSignalActivePing`. See
44497
+ * {@link IBroker.onSignalPendingOpen}.
44498
+ *
44499
+ * @param payload - Pending open details: symbol, signalId, position, prices, context, backtest
44500
+ */
44501
+ async onSignalPendingOpen(payload) {
44502
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_PENDING_OPEN, {
44503
+ symbol: payload.symbol,
44504
+ context: payload.context,
44505
+ });
44506
+ }
44507
+ /**
44508
+ * Called when a pending position is closed (take_profit / stop_loss / time_expired / closed).
44509
+ *
44510
+ * Informational lifecycle hook. Override to mirror the close into your own systems. The default logs.
44511
+ *
44512
+ * Manual wiring — EVENT-BASED (outbound): the strategy already removed the pending signal — flatten the real
44513
+ * position and cancel leftover TP/SL orders by `payload.signalId`. See
44514
+ * {@link IBroker.onSignalPendingClose}.
44515
+ *
44516
+ * @param payload - Pending close details: symbol, signalId, position, prices, closeReason, context, backtest
44517
+ */
44518
+ async onSignalPendingClose(payload) {
44519
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_PENDING_CLOSE, {
44520
+ symbol: payload.symbol,
44521
+ context: payload.context,
44522
+ });
44523
+ }
44524
+ /**
44525
+ * Called when a position is being closed (SL/TP hit or manual close).
43321
44526
  *
43322
44527
  * Triggered automatically via syncSubject when a pending signal is closed.
43323
44528
  * Use to place the exit order and record final PnL.
43324
44529
  *
43325
44530
  * Default implementation: Logs signal-close event.
43326
44531
  *
44532
+ * Manual wiring — EXCEPTION-BASED GATE: emitted BEFORE the framework mutates state, so a THROW here
44533
+ * (e.g. exit order failed) SKIPS the close — the position stays open and the close retries next
44534
+ * tick; return normally to let it close. Live-only (backtest short-circuits). See
44535
+ * {@link IBroker.onSignalCloseCommit} for the full semantics.
44536
+ *
43327
44537
  * @param payload - Signal close details: symbol, cost, position, currentPrice, pnl, totalEntries, totalPartials, context, backtest
43328
44538
  *
43329
44539
  * @example
43330
44540
  * ```typescript
43331
44541
  * async onSignalCloseCommit(payload: BrokerSignalClosePayload) {
43332
44542
  * super.onSignalCloseCommit(payload); // Keep parent logging
43333
- * await this.exchange.closePosition(payload.symbol);
44543
+ * const ok = await this.exchange.closePosition(payload.symbol);
44544
+ * if (!ok) {
44545
+ * throw new Error(`Exit not filled for ${payload.symbol}`); // -> keep position open, retry next tick
44546
+ * }
43334
44547
  * await this.db.recordTrade({ symbol: payload.symbol, pnl: payload.pnl });
43335
44548
  * }
43336
44549
  * ```
@@ -43586,6 +44799,8 @@ const HAS_NO_PENDING_SIGNAL_METHOD_NAME = "strategy.hasNoPendingSignal";
43586
44799
  const HAS_NO_SCHEDULED_SIGNAL_METHOD_NAME = "strategy.hasNoScheduledSignal";
43587
44800
  const COMMIT_SIGNAL_NOTIFY_METHOD_NAME = "strategy.commitSignalNotify";
43588
44801
  const COMMIT_CREATE_SIGNAL_METHOD_NAME = "strategy.commitCreateSignal";
44802
+ const COMMIT_CREATE_TAKE_PROFIT_METHOD_NAME = "strategy.commitCreateTakeProfit";
44803
+ const COMMIT_CREATE_STOP_LOSS_METHOD_NAME = "strategy.commitCreateStopLoss";
43589
44804
  const GET_STRATEGY_STATUS_METHOD_NAME = "strategy.getStrategyStatus";
43590
44805
  /**
43591
44806
  * Cancels the scheduled signal without stopping the strategy.
@@ -45695,6 +46910,74 @@ async function commitCreateSignal(symbol, dto) {
45695
46910
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
45696
46911
  await bt.strategyCoreService.createSignal(isBacktest, symbol, dto, { exchangeName, frameName, strategyName });
45697
46912
  }
46913
+ /**
46914
+ * Reports that the pending position's take-profit order was actually filled on the exchange
46915
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based TP check.
46916
+ *
46917
+ * The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
46918
+ * but the real order may fill on high/low. The close is deferred and emitted with closeReason
46919
+ * "take_profit" on the next tick. No-op if no pending signal exists.
46920
+ *
46921
+ * Automatically detects backtest/live mode from execution context.
46922
+ *
46923
+ * @param symbol - Trading pair symbol
46924
+ * @param payload - Optional commit payload with id and note
46925
+ * @returns Promise that resolves when the take-profit fill is queued
46926
+ *
46927
+ * @example
46928
+ * ```typescript
46929
+ * import { commitCreateTakeProfit } from "backtest-kit";
46930
+ *
46931
+ * // Report TP fill confirmed on the exchange
46932
+ * await commitCreateTakeProfit("BTCUSDT", { id: "tp-fill-001" });
46933
+ * ```
46934
+ */
46935
+ async function commitCreateTakeProfit(symbol, payload = {}) {
46936
+ bt.loggerService.info(COMMIT_CREATE_TAKE_PROFIT_METHOD_NAME, { symbol, payload });
46937
+ if (!ExecutionContextService.hasContext()) {
46938
+ throw new Error("commitCreateTakeProfit requires an execution context");
46939
+ }
46940
+ if (!MethodContextService.hasContext()) {
46941
+ throw new Error("commitCreateTakeProfit requires a method context");
46942
+ }
46943
+ const { backtest: isBacktest } = bt.executionContextService.context;
46944
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
46945
+ await bt.strategyCoreService.createTakeProfit(isBacktest, symbol, { exchangeName, frameName, strategyName }, payload);
46946
+ }
46947
+ /**
46948
+ * Reports that the pending position's stop-loss order was actually filled on the exchange
46949
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based SL check.
46950
+ *
46951
+ * The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
46952
+ * but the real order may fill on high/low. The close is deferred and emitted with closeReason
46953
+ * "stop_loss" on the next tick. No-op if no pending signal exists.
46954
+ *
46955
+ * Automatically detects backtest/live mode from execution context.
46956
+ *
46957
+ * @param symbol - Trading pair symbol
46958
+ * @param payload - Optional commit payload with id and note
46959
+ * @returns Promise that resolves when the stop-loss fill is queued
46960
+ *
46961
+ * @example
46962
+ * ```typescript
46963
+ * import { commitCreateStopLoss } from "backtest-kit";
46964
+ *
46965
+ * // Report SL fill confirmed on the exchange
46966
+ * await commitCreateStopLoss("BTCUSDT", { id: "sl-fill-001" });
46967
+ * ```
46968
+ */
46969
+ async function commitCreateStopLoss(symbol, payload = {}) {
46970
+ bt.loggerService.info(COMMIT_CREATE_STOP_LOSS_METHOD_NAME, { symbol, payload });
46971
+ if (!ExecutionContextService.hasContext()) {
46972
+ throw new Error("commitCreateStopLoss requires an execution context");
46973
+ }
46974
+ if (!MethodContextService.hasContext()) {
46975
+ throw new Error("commitCreateStopLoss requires a method context");
46976
+ }
46977
+ const { backtest: isBacktest } = bt.executionContextService.context;
46978
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
46979
+ await bt.strategyCoreService.createStopLoss(isBacktest, symbol, { exchangeName, frameName, strategyName }, payload);
46980
+ }
45698
46981
  /**
45699
46982
  * Returns the in-memory deferred strategy-state snapshot for the current iteration: the queued
45700
46983
  * createSignal, commit queue and deferred user-action flags, plus the current pending signal id.
@@ -45797,6 +47080,10 @@ const LISTEN_RISK_METHOD_NAME = "event.listenRisk";
45797
47080
  const LISTEN_RISK_ONCE_METHOD_NAME = "event.listenRiskOnce";
45798
47081
  const LISTEN_SCHEDULE_PING_METHOD_NAME = "event.listenSchedulePing";
45799
47082
  const LISTEN_SCHEDULE_PING_ONCE_METHOD_NAME = "event.listenSchedulePingOnce";
47083
+ const LISTEN_SCHEDULE_EVENT_METHOD_NAME = "event.listenScheduleEvent";
47084
+ const LISTEN_SCHEDULE_EVENT_ONCE_METHOD_NAME = "event.listenScheduleEventOnce";
47085
+ const LISTEN_SIGNAL_EVENT_METHOD_NAME = "event.listenSignalEvent";
47086
+ const LISTEN_SIGNAL_EVENT_ONCE_METHOD_NAME = "event.listenSignalEventOnce";
45800
47087
  const LISTEN_ACTIVE_PING_METHOD_NAME = "event.listenActivePing";
45801
47088
  const LISTEN_ACTIVE_PING_ONCE_METHOD_NAME = "event.listenActivePingOnce";
45802
47089
  const LISTEN_IDLE_PING_METHOD_NAME = "event.listenIdlePing";
@@ -46899,6 +48186,137 @@ function listenSchedulePingOnce(filterFn, fn) {
46899
48186
  };
46900
48187
  return disposeFn = listenSchedulePing(wrappedFn);
46901
48188
  }
48189
+ /**
48190
+ * Subscribes to scheduled signal lifecycle events (creation and cancellation) with queued async processing.
48191
+ *
48192
+ * Emitted when a scheduled signal is created (action "scheduled") or cancelled before activation
48193
+ * (action "cancelled" with reason "timeout" / "price_reject" / "user"), in both live and backtest.
48194
+ *
48195
+ * IMPORTANT: The scheduled -> active transition (activation) is NOT reported here. Activation
48196
+ * produces an "opened" event on the regular signal emitters (listenSignal) instead.
48197
+ *
48198
+ * Events are processed sequentially in order received, even if callback is async.
48199
+ *
48200
+ * @param fn - Callback function to handle scheduled lifecycle events
48201
+ * @returns Unsubscribe function to stop listening
48202
+ *
48203
+ * @example
48204
+ * ```typescript
48205
+ * import { listenScheduleEvent } from "./function/event";
48206
+ *
48207
+ * const unsubscribe = listenScheduleEvent((event) => {
48208
+ * if (event.action === "scheduled") {
48209
+ * console.log(`Scheduled ${event.symbol} @ ${event.data.priceOpen}`);
48210
+ * } else {
48211
+ * console.log(`Cancelled ${event.symbol} (reason: ${event.reason})`);
48212
+ * }
48213
+ * });
48214
+ *
48215
+ * // Later: stop listening
48216
+ * unsubscribe();
48217
+ * ```
48218
+ */
48219
+ function listenScheduleEvent(fn) {
48220
+ bt.loggerService.log(LISTEN_SCHEDULE_EVENT_METHOD_NAME);
48221
+ return scheduleEventSubject.subscribe(queued(async (event) => fn(event)));
48222
+ }
48223
+ /**
48224
+ * Subscribes to filtered scheduled lifecycle events with one-time execution.
48225
+ *
48226
+ * Listens for events matching the filter predicate, then executes callback once
48227
+ * and automatically unsubscribes. Useful for waiting for a specific scheduled creation
48228
+ * or cancellation.
48229
+ *
48230
+ * @param filterFn - Predicate to filter which events trigger the callback
48231
+ * @param fn - Callback function to handle the filtered event (called only once)
48232
+ * @returns Unsubscribe function to cancel the listener before it fires
48233
+ *
48234
+ * @example
48235
+ * ```typescript
48236
+ * import { listenScheduleEventOnce } from "./function/event";
48237
+ *
48238
+ * // Wait for the first cancellation on BTCUSDT
48239
+ * listenScheduleEventOnce(
48240
+ * (event) => event.symbol === "BTCUSDT" && event.action === "cancelled",
48241
+ * (event) => console.log("BTCUSDT scheduled cancelled:", event.reason)
48242
+ * );
48243
+ * ```
48244
+ */
48245
+ function listenScheduleEventOnce(filterFn, fn) {
48246
+ bt.loggerService.log(LISTEN_SCHEDULE_EVENT_ONCE_METHOD_NAME);
48247
+ let disposeFn;
48248
+ const wrappedFn = async (event) => {
48249
+ if (filterFn(event)) {
48250
+ await fn(event);
48251
+ disposeFn && disposeFn();
48252
+ }
48253
+ };
48254
+ return disposeFn = listenScheduleEvent(wrappedFn);
48255
+ }
48256
+ /**
48257
+ * Subscribes to pending signal lifecycle events (open and close) with queued async processing.
48258
+ *
48259
+ * Emitted when a pending position is opened (action "opened": new signal / immediate / scheduled
48260
+ * or user activation) or closed (action "closed" with closeReason "take_profit" / "stop_loss" /
48261
+ * "time_expired" / "closed"), in both live and backtest.
48262
+ *
48263
+ * Events are processed sequentially in order received, even if callback is async.
48264
+ *
48265
+ * @param fn - Callback function to handle pending lifecycle events
48266
+ * @returns Unsubscribe function to stop listening
48267
+ *
48268
+ * @example
48269
+ * ```typescript
48270
+ * import { listenSignalEvent } from "./function/event";
48271
+ *
48272
+ * const unsubscribe = listenSignalEvent((event) => {
48273
+ * if (event.action === "opened") {
48274
+ * console.log(`Opened ${event.symbol} @ ${event.data.priceOpen}`);
48275
+ * } else {
48276
+ * console.log(`Closed ${event.symbol} (reason: ${event.closeReason})`);
48277
+ * }
48278
+ * });
48279
+ *
48280
+ * // Later: stop listening
48281
+ * unsubscribe();
48282
+ * ```
48283
+ */
48284
+ function listenSignalEvent(fn) {
48285
+ bt.loggerService.log(LISTEN_SIGNAL_EVENT_METHOD_NAME);
48286
+ return signalEventSubject.subscribe(queued(async (event) => fn(event)));
48287
+ }
48288
+ /**
48289
+ * Subscribes to filtered pending lifecycle events with one-time execution.
48290
+ *
48291
+ * Listens for events matching the filter predicate, then executes callback once
48292
+ * and automatically unsubscribes. Useful for waiting for a specific open or close.
48293
+ *
48294
+ * @param filterFn - Predicate to filter which events trigger the callback
48295
+ * @param fn - Callback function to handle the filtered event (called only once)
48296
+ * @returns Unsubscribe function to cancel the listener before it fires
48297
+ *
48298
+ * @example
48299
+ * ```typescript
48300
+ * import { listenSignalEventOnce } from "./function/event";
48301
+ *
48302
+ * // Wait for the first close on BTCUSDT
48303
+ * listenSignalEventOnce(
48304
+ * (event) => event.symbol === "BTCUSDT" && event.action === "closed",
48305
+ * (event) => console.log("BTCUSDT closed:", event.closeReason)
48306
+ * );
48307
+ * ```
48308
+ */
48309
+ function listenSignalEventOnce(filterFn, fn) {
48310
+ bt.loggerService.log(LISTEN_SIGNAL_EVENT_ONCE_METHOD_NAME);
48311
+ let disposeFn;
48312
+ const wrappedFn = async (event) => {
48313
+ if (filterFn(event)) {
48314
+ await fn(event);
48315
+ disposeFn && disposeFn();
48316
+ }
48317
+ };
48318
+ return disposeFn = listenSignalEvent(wrappedFn);
48319
+ }
46902
48320
  /**
46903
48321
  * Subscribes to active ping events with queued async processing.
46904
48322
  *
@@ -47396,6 +48814,8 @@ const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
47396
48814
  const BACKTEST_METHOD_NAME_CANCEL_SCHEDULED = "Backtest.commitCancelScheduled";
47397
48815
  const BACKTEST_METHOD_NAME_CLOSE_PENDING = "Backtest.commitClosePending";
47398
48816
  const BACKTEST_METHOD_NAME_COMMIT_CREATE_SIGNAL = "Backtest.commitCreateSignal";
48817
+ const BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT = "Backtest.commitCreateTakeProfit";
48818
+ const BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS = "Backtest.commitCreateStopLoss";
47399
48819
  const BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS = "Backtest.getStrategyStatus";
47400
48820
  const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.commitPartialProfit";
47401
48821
  const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.commitPartialLoss";
@@ -49132,6 +50552,90 @@ class BacktestUtils {
49132
50552
  }
49133
50553
  await bt.strategyCoreService.createSignal(true, symbol, dto, context);
49134
50554
  };
50555
+ /**
50556
+ * Reports that the pending position's take-profit order was actually filled on the exchange
50557
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based TP check.
50558
+ *
50559
+ * The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
50560
+ * but the real order may fill on high/low. The close is deferred and emitted with closeReason
50561
+ * "take_profit" on the next backtest tick. No-op if no pending signal exists.
50562
+ *
50563
+ * @param symbol - Trading pair symbol
50564
+ * @param context - Execution context with strategyName, exchangeName, and frameName
50565
+ * @param payload - Optional commit payload with id and note
50566
+ * @returns Promise that resolves when the take-profit fill is queued
50567
+ *
50568
+ * @example
50569
+ * ```typescript
50570
+ * // Report TP fill confirmed on the exchange
50571
+ * await Backtest.commitCreateTakeProfit("BTCUSDT", {
50572
+ * exchangeName: "binance",
50573
+ * strategyName: "my-strategy",
50574
+ * frameName: "1m"
50575
+ * }, { id: "tp-fill-001" });
50576
+ * ```
50577
+ */
50578
+ this.commitCreateTakeProfit = async (symbol, context, payload = {}) => {
50579
+ bt.loggerService.info(BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT, {
50580
+ symbol,
50581
+ context,
50582
+ payload,
50583
+ });
50584
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
50585
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
50586
+ {
50587
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
50588
+ riskName &&
50589
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
50590
+ riskList &&
50591
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT));
50592
+ actions &&
50593
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT));
50594
+ }
50595
+ await bt.strategyCoreService.createTakeProfit(true, symbol, context, payload);
50596
+ };
50597
+ /**
50598
+ * Reports that the pending position's stop-loss order was actually filled on the exchange
50599
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based SL check.
50600
+ *
50601
+ * The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
50602
+ * but the real order may fill on high/low. The close is deferred and emitted with closeReason
50603
+ * "stop_loss" on the next backtest tick. No-op if no pending signal exists.
50604
+ *
50605
+ * @param symbol - Trading pair symbol
50606
+ * @param context - Execution context with strategyName, exchangeName, and frameName
50607
+ * @param payload - Optional commit payload with id and note
50608
+ * @returns Promise that resolves when the stop-loss fill is queued
50609
+ *
50610
+ * @example
50611
+ * ```typescript
50612
+ * // Report SL fill confirmed on the exchange
50613
+ * await Backtest.commitCreateStopLoss("BTCUSDT", {
50614
+ * exchangeName: "binance",
50615
+ * strategyName: "my-strategy",
50616
+ * frameName: "1m"
50617
+ * }, { id: "sl-fill-001" });
50618
+ * ```
50619
+ */
50620
+ this.commitCreateStopLoss = async (symbol, context, payload = {}) => {
50621
+ bt.loggerService.info(BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS, {
50622
+ symbol,
50623
+ context,
50624
+ payload,
50625
+ });
50626
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
50627
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
50628
+ {
50629
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
50630
+ riskName &&
50631
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
50632
+ riskList &&
50633
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS));
50634
+ actions &&
50635
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS));
50636
+ }
50637
+ await bt.strategyCoreService.createStopLoss(true, symbol, context, payload);
50638
+ };
49135
50639
  /**
49136
50640
  * Returns the in-memory deferred strategy-state snapshot for the current backtest iteration.
49137
50641
  *
@@ -50117,6 +51621,8 @@ const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
50117
51621
  const LIVE_METHOD_NAME_CANCEL_SCHEDULED = "Live.cancelScheduled";
50118
51622
  const LIVE_METHOD_NAME_CLOSE_PENDING = "Live.closePending";
50119
51623
  const LIVE_METHOD_NAME_COMMIT_CREATE_SIGNAL = "Live.commitCreateSignal";
51624
+ const LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT = "Live.commitCreateTakeProfit";
51625
+ const LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS = "Live.commitCreateStopLoss";
50120
51626
  const LIVE_METHOD_NAME_GET_STRATEGY_STATUS = "Live.getStrategyStatus";
50121
51627
  const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.commitPartialProfit";
50122
51628
  const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.commitPartialLoss";
@@ -52030,6 +53536,78 @@ class LiveUtils {
52030
53536
  frameName: "",
52031
53537
  });
52032
53538
  };
53539
+ /**
53540
+ * Reports that the pending position's take-profit order was actually filled on the exchange
53541
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based TP check.
53542
+ *
53543
+ * The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
53544
+ * but the real order may fill on high/low. The close is deferred and emitted with closeReason
53545
+ * "take_profit" on the next live tick. No-op if no pending signal exists.
53546
+ *
53547
+ * @param symbol - Trading pair symbol
53548
+ * @param context - Execution context with strategyName and exchangeName
53549
+ * @param payload - Optional commit payload with id and note
53550
+ * @returns Promise that resolves when the take-profit fill is queued
53551
+ */
53552
+ this.commitCreateTakeProfit = async (symbol, context, payload = {}) => {
53553
+ bt.loggerService.info(LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT, {
53554
+ symbol,
53555
+ context,
53556
+ payload,
53557
+ });
53558
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
53559
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
53560
+ {
53561
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
53562
+ riskName &&
53563
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
53564
+ riskList &&
53565
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT));
53566
+ actions &&
53567
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT));
53568
+ }
53569
+ await bt.strategyCoreService.createTakeProfit(false, symbol, {
53570
+ strategyName: context.strategyName,
53571
+ exchangeName: context.exchangeName,
53572
+ frameName: "",
53573
+ }, payload);
53574
+ };
53575
+ /**
53576
+ * Reports that the pending position's stop-loss order was actually filled on the exchange
53577
+ * (e.g. by candle high/low), forcing a close that bypasses the VWAP-based SL check.
53578
+ *
53579
+ * The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
53580
+ * but the real order may fill on high/low. The close is deferred and emitted with closeReason
53581
+ * "stop_loss" on the next live tick. No-op if no pending signal exists.
53582
+ *
53583
+ * @param symbol - Trading pair symbol
53584
+ * @param context - Execution context with strategyName and exchangeName
53585
+ * @param payload - Optional commit payload with id and note
53586
+ * @returns Promise that resolves when the stop-loss fill is queued
53587
+ */
53588
+ this.commitCreateStopLoss = async (symbol, context, payload = {}) => {
53589
+ bt.loggerService.info(LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS, {
53590
+ symbol,
53591
+ context,
53592
+ payload,
53593
+ });
53594
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
53595
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
53596
+ {
53597
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
53598
+ riskName &&
53599
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
53600
+ riskList &&
53601
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS));
53602
+ actions &&
53603
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS));
53604
+ }
53605
+ await bt.strategyCoreService.createStopLoss(false, symbol, {
53606
+ strategyName: context.strategyName,
53607
+ exchangeName: context.exchangeName,
53608
+ frameName: "",
53609
+ }, payload);
53610
+ };
52033
53611
  /**
52034
53612
  * Returns the in-memory deferred strategy-state snapshot for the current live iteration.
52035
53613
  *
@@ -60369,7 +61947,9 @@ const SUBJECT_ISOLATION_LIST = [
60369
61947
  validationSubject,
60370
61948
  signalNotifySubject,
60371
61949
  beforeStartSubject,
60372
- afterEndSubject
61950
+ afterEndSubject,
61951
+ scheduleEventSubject,
61952
+ signalEventSubject,
60373
61953
  ];
60374
61954
  /**
60375
61955
  * Creates a snapshot function for a given subject by clearing its internal
@@ -68707,4 +70287,4 @@ const percentValue = (yesterdayValue, todayValue) => {
68707
70287
  return yesterdayValue / todayValue - 1;
68708
70288
  };
68709
70289
 
68710
- export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Cron, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Interval, Live, Log, Lookup, Markdown, MarkdownFileBase, MarkdownFolderBase, MarkdownWriter, MaxDrawdown, Memory, MemoryBacktest, MemoryBacktestAdapter, MemoryLive, MemoryLiveAdapter, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistBreakevenInstance, PersistCandleAdapter, PersistCandleInstance, PersistIntervalAdapter, PersistIntervalInstance, PersistLogAdapter, PersistLogInstance, PersistMeasureAdapter, PersistMeasureInstance, PersistMemoryAdapter, PersistMemoryInstance, PersistNotificationAdapter, PersistNotificationInstance, PersistPartialAdapter, PersistPartialInstance, PersistRecentAdapter, PersistRecentInstance, PersistRiskAdapter, PersistRiskInstance, PersistScheduleAdapter, PersistScheduleInstance, PersistSessionAdapter, PersistSessionInstance, PersistSignalAdapter, PersistSignalInstance, PersistStateAdapter, PersistStateInstance, PersistStorageAdapter, PersistStorageInstance, PersistStrategyAdapter, PersistStrategyInstance, Position, PositionSize, Recent, RecentBacktest, RecentLive, Reflect$1 as Reflect, Report, ReportBase, ReportWriter, Risk, Schedule, Session, SessionBacktest, SessionLive, State, StateBacktest, StateBacktestAdapter, StateLive, StateLiveAdapter, Storage, StorageBacktest, StorageLive, Strategy, Sync, System, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, beginContext, beginTime, cacheCandles, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitCreateSignal, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitSignalNotify, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, createSignalState, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getClosePrice, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getLatestSignal, getMaxDrawdownDistancePnlCost, getMaxDrawdownDistancePnlPercentage, getMinutesSinceLatestSignalCreated, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionActiveMinutes, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestMaxDrawdownPnlCost, getPositionHighestMaxDrawdownPnlPercentage, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitDistancePnlCost, getPositionHighestProfitDistancePnlPercentage, getPositionHighestProfitMinutes, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionMaxDrawdownMinutes, getPositionMaxDrawdownPnlCost, getPositionMaxDrawdownPnlPercentage, getPositionMaxDrawdownPrice, getPositionMaxDrawdownTimestamp, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getPositionWaitingMinutes, getPriceScale, getRawCandles, getRiskSchema, getRuntimeInfo, getScheduledSignal, getSessionData, getSignalState, getSizingSchema, getStrategySchema, getStrategyStatus, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, intervalStepMs, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenAfterEnd, listenAfterEndOnce, listenBacktestProgress, listenBeforeStart, listenBeforeStartOnce, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenIdlePing, listenIdlePingOnce, listenMaxDrawdown, listenMaxDrawdownOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalNotify, listenSignalNotifyOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, setSessionData, setSignalState, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toPlainString, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCandles, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, waitForReady, warmCandles, writeMemory };
70290
+ export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Cron, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Interval, Live, Log, Lookup, Markdown, MarkdownFileBase, MarkdownFolderBase, MarkdownWriter, MaxDrawdown, Memory, MemoryBacktest, MemoryBacktestAdapter, MemoryLive, MemoryLiveAdapter, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistBreakevenInstance, PersistCandleAdapter, PersistCandleInstance, PersistIntervalAdapter, PersistIntervalInstance, PersistLogAdapter, PersistLogInstance, PersistMeasureAdapter, PersistMeasureInstance, PersistMemoryAdapter, PersistMemoryInstance, PersistNotificationAdapter, PersistNotificationInstance, PersistPartialAdapter, PersistPartialInstance, PersistRecentAdapter, PersistRecentInstance, PersistRiskAdapter, PersistRiskInstance, PersistScheduleAdapter, PersistScheduleInstance, PersistSessionAdapter, PersistSessionInstance, PersistSignalAdapter, PersistSignalInstance, PersistStateAdapter, PersistStateInstance, PersistStorageAdapter, PersistStorageInstance, PersistStrategyAdapter, PersistStrategyInstance, Position, PositionSize, Recent, RecentBacktest, RecentLive, Reflect$1 as Reflect, Report, ReportBase, ReportWriter, Risk, Schedule, Session, SessionBacktest, SessionLive, State, StateBacktest, StateBacktestAdapter, StateLive, StateLiveAdapter, Storage, StorageBacktest, StorageLive, Strategy, Sync, System, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, beginContext, beginTime, cacheCandles, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitCreateSignal, commitCreateStopLoss, commitCreateTakeProfit, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitSignalNotify, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, createSignalState, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getClosePrice, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getLatestSignal, getMaxDrawdownDistancePnlCost, getMaxDrawdownDistancePnlPercentage, getMinutesSinceLatestSignalCreated, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionActiveMinutes, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestMaxDrawdownPnlCost, getPositionHighestMaxDrawdownPnlPercentage, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitDistancePnlCost, getPositionHighestProfitDistancePnlPercentage, getPositionHighestProfitMinutes, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionMaxDrawdownMinutes, getPositionMaxDrawdownPnlCost, getPositionMaxDrawdownPnlPercentage, getPositionMaxDrawdownPrice, getPositionMaxDrawdownTimestamp, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getPositionWaitingMinutes, getPriceScale, getRawCandles, getRiskSchema, getRuntimeInfo, getScheduledSignal, getSessionData, getSignalState, getSizingSchema, getStrategySchema, getStrategyStatus, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, intervalStepMs, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenAfterEnd, listenAfterEndOnce, listenBacktestProgress, listenBeforeStart, listenBeforeStartOnce, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenIdlePing, listenIdlePingOnce, listenMaxDrawdown, listenMaxDrawdownOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenScheduleEvent, listenScheduleEventOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalEvent, listenSignalEventOnce, listenSignalLive, listenSignalLiveOnce, listenSignalNotify, listenSignalNotifyOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, setSessionData, setSignalState, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toPlainString, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCandles, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, waitForReady, warmCandles, writeMemory };