backtest-kit 13.4.0 → 13.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -649,6 +649,15 @@ const DEFAULT_CONFIG = Object.freeze({ ...GLOBAL_CONFIG });
649
649
  * Consumers should implement retry logic in their listeners to handle transient synchronization failures.
650
650
  */
651
651
  const syncSubject = new functoolsKit.Subject();
652
+ /**
653
+ * Pending-order synchronization emitter.
654
+ * Emitted on every live tick while a pending signal is monitored, BEFORE TP/SL/time evaluation.
655
+ * Asks the exchange whether the order is STILL pending (open).
656
+ *
657
+ * If a listener returns false OR throws, the order is treated as no longer open on the exchange
658
+ * and the framework closes the pending signal with closeReason "closed". Never emitted in backtest.
659
+ */
660
+ const syncPendingSubject = new functoolsKit.Subject();
652
661
  /**
653
662
  * Global signal emitter for all trading events (live + backtest).
654
663
  * Emits all signal events regardless of execution mode.
@@ -856,6 +865,7 @@ var emitters = /*#__PURE__*/Object.freeze({
856
865
  signalLiveEmitter: signalLiveEmitter,
857
866
  signalNotifySubject: signalNotifySubject,
858
867
  strategyCommitSubject: strategyCommitSubject,
868
+ syncPendingSubject: syncPendingSubject,
859
869
  syncSubject: syncSubject,
860
870
  validationSubject: validationSubject,
861
871
  walkerCompleteSubject: walkerCompleteSubject,
@@ -7075,6 +7085,64 @@ const CALL_SIGNAL_SYNC_CLOSE_FN = functoolsKit.trycatch(async (timestamp, curren
7075
7085
  errorEmitter.next(error);
7076
7086
  }
7077
7087
  });
7088
+ /**
7089
+ * Calls onSignalPing callback for the pending-order synchronization event.
7090
+ *
7091
+ * Invoked at the start of the pending-signal monitoring block on every LIVE tick, BEFORE the
7092
+ * framework evaluates TP/SL/time completion. It asks the external order management system
7093
+ * whether the order backing this position is STILL pending (open) on the exchange.
7094
+ *
7095
+ * The Subject `await .next()` propagates listener exceptions passthrough, so the trycatch wrapper
7096
+ * collapses both a thrown listener and an explicit `false` into `false` (defaultValue). A `false`
7097
+ * result means the order is no longer open on the exchange and the caller closes the position with
7098
+ * closeReason "closed". A missing/true result keeps the position under normal TP/SL monitoring.
7099
+ */
7100
+ const CALL_SIGNAL_PING_FN = functoolsKit.trycatch(async (timestamp, currentPrice, signal, self) => {
7101
+ const publicSignal = TO_PUBLIC_SIGNAL("pending", signal, currentPrice);
7102
+ return await self.params.onSignalPing({
7103
+ action: "signal-ping",
7104
+ symbol: self.params.execution.context.symbol,
7105
+ strategyName: self.params.strategyName,
7106
+ exchangeName: self.params.exchangeName,
7107
+ frameName: self.params.frameName,
7108
+ backtest: self.params.execution.context.backtest,
7109
+ signalId: signal.id,
7110
+ timestamp,
7111
+ signal: publicSignal,
7112
+ currentPrice,
7113
+ pnl: publicSignal.pnl,
7114
+ peakProfit: publicSignal.peakProfit,
7115
+ maxDrawdown: publicSignal.maxDrawdown,
7116
+ position: publicSignal.position,
7117
+ priceOpen: publicSignal.priceOpen,
7118
+ priceTakeProfit: publicSignal.priceTakeProfit,
7119
+ priceStopLoss: publicSignal.priceStopLoss,
7120
+ originalPriceTakeProfit: publicSignal.originalPriceTakeProfit,
7121
+ originalPriceStopLoss: publicSignal.originalPriceStopLoss,
7122
+ originalPriceOpen: publicSignal.originalPriceOpen,
7123
+ scheduledAt: publicSignal.scheduledAt,
7124
+ pendingAt: publicSignal.pendingAt,
7125
+ totalEntries: publicSignal.totalEntries,
7126
+ totalPartials: publicSignal.totalPartials,
7127
+ });
7128
+ }, {
7129
+ defaultValue: false,
7130
+ fallback: (error, timestamp, currentPrice, signal, self) => {
7131
+ const message = "ClientStrategy CALL_SIGNAL_PING_FN thrown";
7132
+ const payload = {
7133
+ error: functoolsKit.errorData(error),
7134
+ message: functoolsKit.getErrorMessage(error),
7135
+ data: {
7136
+ timestamp,
7137
+ currentPrice,
7138
+ signalId: signal.id,
7139
+ }
7140
+ };
7141
+ self.params.logger.warn(message, payload);
7142
+ console.warn(message, payload);
7143
+ errorEmitter.next(error);
7144
+ }
7145
+ });
7078
7146
  /**
7079
7147
  * Calls onCommit callback with strategy commit event.
7080
7148
  *
@@ -8996,6 +9064,47 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
8996
9064
  await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
8997
9065
  return result;
8998
9066
  };
9067
+ /**
9068
+ * Closes the pending signal with closeReason "closed" after the pending-order ping reported the
9069
+ * order is no longer open on the exchange (CALL_SIGNAL_PING_FN returned false / threw).
9070
+ *
9071
+ * Unlike CLOSE_PENDING_SIGNAL_FN this does NOT re-confirm via onSignalSync — the ping already
9072
+ * established the order is gone, so re-asking the broker would be redundant. Runs the same teardown
9073
+ * (close callback, partial/breakeven clear, risk remove, setPendingSignal(null)). Live-only path.
9074
+ */
9075
+ const CLOSE_PENDING_SIGNAL_AS_CLOSED_FN = async (self, signal, currentPrice) => {
9076
+ const currentTime = self.params.execution.context.when.getTime();
9077
+ const publicSignal = TO_PUBLIC_SIGNAL("pending", signal, currentPrice);
9078
+ self.params.logger.info("ClientStrategy signal closed by pending-order ping (order no longer open on exchange)", {
9079
+ symbol: self.params.execution.context.symbol,
9080
+ signalId: signal.id,
9081
+ priceClose: currentPrice,
9082
+ pnlPercentage: publicSignal.pnl.pnlPercentage,
9083
+ });
9084
+ await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
9085
+ // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
9086
+ await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
9087
+ // КРИТИЧНО: Очищаем состояние ClientBreakeven при закрытии позиции
9088
+ await CALL_BREAKEVEN_CLEAR_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
9089
+ await CALL_RISK_REMOVE_SIGNAL_FN(self, self.params.execution.context.symbol, currentTime, self.params.execution.context.backtest);
9090
+ await self.setPendingSignal(null, currentPrice);
9091
+ const result = {
9092
+ action: "closed",
9093
+ signal: publicSignal,
9094
+ currentPrice: currentPrice,
9095
+ closeReason: "closed",
9096
+ closeTimestamp: currentTime,
9097
+ pnl: publicSignal.pnl,
9098
+ strategyName: self.params.method.context.strategyName,
9099
+ exchangeName: self.params.method.context.exchangeName,
9100
+ frameName: self.params.method.context.frameName,
9101
+ symbol: self.params.execution.context.symbol,
9102
+ backtest: self.params.execution.context.backtest,
9103
+ createdAt: currentTime,
9104
+ };
9105
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
9106
+ return result;
9107
+ };
8999
9108
  const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice, backtest) => {
9000
9109
  let percentTp = 0;
9001
9110
  let percentSl = 0;
@@ -11033,6 +11142,16 @@ class ClientStrategy {
11033
11142
  }
11034
11143
  // Monitor pending signal
11035
11144
  const averagePrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
11145
+ // Pending-order ping: before evaluating TP/SL/time, confirm the order is STILL open on the
11146
+ // exchange. CALL_SIGNAL_PING_FN returns false when the listener returns false OR throws
11147
+ // (Subject .next passthrough), meaning the order is no longer pending — close with "closed".
11148
+ // Skipped in backtest: there is no live exchange to query.
11149
+ if (!this.params.execution.context.backtest) {
11150
+ const stillPending = await CALL_SIGNAL_PING_FN(currentTime, averagePrice, this._pendingSignal, this);
11151
+ if (!stillPending) {
11152
+ return await CLOSE_PENDING_SIGNAL_AS_CLOSED_FN(this, this._pendingSignal, averagePrice);
11153
+ }
11154
+ }
11036
11155
  const closedResult = await CHECK_PENDING_SIGNAL_COMPLETION_FN(this, this._pendingSignal, averagePrice);
11037
11156
  if (closedResult) {
11038
11157
  return closedResult;
@@ -12881,6 +13000,32 @@ const CREATE_SYNC_FN = (self, strategyName, exchangeName, frameName, backtest) =
12881
13000
  },
12882
13001
  defaultValue: false,
12883
13002
  });
13003
+ /**
13004
+ * If the syncPendingSubject listener or any registered action throws, it means the order backing the
13005
+ * open position is no longer pending on the exchange (filled/cancelled/liquidated externally).
13006
+ * The trycatch wrapper collapses a throw OR an explicit false into false, and ClientStrategy closes
13007
+ * the pending signal with closeReason "closed". Skipped in backtest — there is no live exchange.
13008
+ */
13009
+ const CREATE_SYNC_PENDING_FN = (self, strategyName, exchangeName, frameName, backtest) => functoolsKit.trycatch(async (event) => {
13010
+ if (event.backtest) {
13011
+ return true;
13012
+ }
13013
+ await syncPendingSubject.next(event);
13014
+ await self.actionCoreService.orderPing(backtest, event, { strategyName, exchangeName, frameName });
13015
+ return true;
13016
+ }, {
13017
+ fallback: (error) => {
13018
+ const message = "StrategyConnectionService CREATE_SYNC_PENDING_FN thrown. Order no longer pending on exchange";
13019
+ const payload = {
13020
+ error: functoolsKit.errorData(error),
13021
+ message: functoolsKit.getErrorMessage(error),
13022
+ };
13023
+ self.loggerService.warn(message, payload);
13024
+ console.error(message, payload);
13025
+ errorEmitter.next(error);
13026
+ },
13027
+ defaultValue: false,
13028
+ });
12884
13029
  /**
12885
13030
  * Emits signal tick results with correct execution context timestamp.
12886
13031
  * Wraps emitter calls in ExecutionContextService.runInContext to preserve
@@ -13317,6 +13462,7 @@ class StrategyConnectionService {
13317
13462
  onDispose: CREATE_COMMIT_DISPOSE_FN(this),
13318
13463
  onCommit: CREATE_COMMIT_FN(this),
13319
13464
  onSignalSync: CREATE_SYNC_FN(this, strategyName, exchangeName, frameName, backtest),
13465
+ onSignalPing: CREATE_SYNC_PENDING_FN(this, strategyName, exchangeName, frameName, backtest),
13320
13466
  onHighestProfit: CREATE_HIGHEST_PROFIT_FN(this, strategyName, exchangeName, frameName, backtest),
13321
13467
  onMaxDrawdown: CREATE_MAX_DRAWDOWN_FN(this, strategyName, exchangeName, frameName, backtest),
13322
13468
  });
@@ -16251,6 +16397,22 @@ class ActionProxy {
16251
16397
  await this._target.signalSync(event);
16252
16398
  }
16253
16399
  }
16400
+ /**
16401
+ * Gate for the pending-order ping (order still open on exchange?).
16402
+ * NOT wrapped in trycatch — exceptions propagate to CREATE_SYNC_PENDING_FN.
16403
+ *
16404
+ * @param event - Pending-ping event with action "signal-ping"
16405
+ */
16406
+ async orderPing(event) {
16407
+ if (this._target.orderPing) {
16408
+ console.error("Action::orderPing is unwanted cause exchange integration should be implemented in Broker.useBrokerAdapter as an infrastructure domain layer");
16409
+ console.error("If you need to check whether the order is still open on the exchange, please use Broker.useBrokerAdapter with onOrderPing");
16410
+ console.error("If Action::orderPing throws the framework will close the position with closeReason \"closed\"!");
16411
+ console.error("");
16412
+ console.error("You have been warned!");
16413
+ await this._target.orderPing(event);
16414
+ }
16415
+ }
16254
16416
  /**
16255
16417
  * Cleans up resources with error capture.
16256
16418
  *
@@ -16470,6 +16632,15 @@ const CALL_SIGNAL_SYNC_CALLBACK_FN = async (self, event, strategyName, frameName
16470
16632
  await self.params.callbacks.onSignalSync(event, self.params.actionName, strategyName, frameName, isBacktest);
16471
16633
  }
16472
16634
  };
16635
+ /**
16636
+ * Calls onOrderPing callback WITHOUT trycatch — exceptions must propagate
16637
+ * up to CREATE_SYNC_PENDING_FN in StrategyConnectionService (which returns false on error).
16638
+ */
16639
+ const CALL_ORDER_PING_CALLBACK_FN = async (self, event, strategyName, frameName, isBacktest) => {
16640
+ if (self.params.callbacks?.onOrderPing) {
16641
+ await self.params.callbacks.onOrderPing(event, self.params.actionName, strategyName, frameName, isBacktest);
16642
+ }
16643
+ };
16473
16644
  /** Wrapper to call onInit callback with error handling */
16474
16645
  const CALL_INIT_CALLBACK_FN = functoolsKit.trycatch(async (self, strategyName, frameName, backtest) => {
16475
16646
  if (self.params.callbacks?.onInit) {
@@ -16848,6 +17019,25 @@ class ClientAction {
16848
17019
  await CALL_SIGNAL_SYNC_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
16849
17020
  }
16850
17021
  ;
17022
+ /**
17023
+ * Gate for the pending-order ping (order still open on exchange?).
17024
+ * NOT wrapped in trycatch — exceptions propagate to CREATE_SYNC_PENDING_FN.
17025
+ */
17026
+ async orderPing(event) {
17027
+ this.params.logger.debug("ClientAction orderPing", {
17028
+ actionName: this.params.actionName,
17029
+ strategyName: this.params.strategyName,
17030
+ frameName: this.params.frameName,
17031
+ });
17032
+ if (!this._handlerInstance) {
17033
+ await this.waitForInit();
17034
+ }
17035
+ // Call handler method if defined — exceptions propagate
17036
+ await this._handlerInstance?.orderPing(event);
17037
+ // Call callback if defined — exceptions propagate
17038
+ await CALL_ORDER_PING_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
17039
+ }
17040
+ ;
16851
17041
  }
16852
17042
 
16853
17043
  /**
@@ -17110,6 +17300,22 @@ class ActionConnectionService {
17110
17300
  const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
17111
17301
  await action.signalSync(event);
17112
17302
  };
17303
+ /**
17304
+ * Routes orderPing event to appropriate ClientAction instance.
17305
+ * NOT wrapped in trycatch — exceptions propagate to CREATE_SYNC_PENDING_FN.
17306
+ *
17307
+ * @param event - Pending-ping event with action "signal-ping"
17308
+ * @param backtest - Whether running in backtest mode
17309
+ * @param context - Execution context
17310
+ */
17311
+ this.orderPing = async (event, backtest, context) => {
17312
+ this.loggerService.log("actionConnectionService orderPing", {
17313
+ backtest,
17314
+ context,
17315
+ });
17316
+ const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
17317
+ await action.orderPing(event);
17318
+ };
17113
17319
  /**
17114
17320
  * Disposes the ClientAction instance for the given action name.
17115
17321
  *
@@ -19334,6 +19540,24 @@ class ActionCoreService {
19334
19540
  await this.actionConnectionService.signalSync(event, backtest, { actionName, ...context });
19335
19541
  }
19336
19542
  };
19543
+ /**
19544
+ * Gates the pending-order ping across all registered actions.
19545
+ * NOT wrapped in trycatch — exceptions propagate to CREATE_SYNC_PENDING_FN.
19546
+ *
19547
+ * @param backtest - Whether running in backtest mode
19548
+ * @param event - Pending-ping event with action "signal-ping"
19549
+ * @param context - Strategy execution context
19550
+ */
19551
+ this.orderPing = async (backtest, event, context) => {
19552
+ this.loggerService.log("actionCoreService orderPing", {
19553
+ context,
19554
+ });
19555
+ await this.validate(context);
19556
+ const { actions = [] } = this.strategySchemaService.get(context.strategyName);
19557
+ for (const actionName of actions) {
19558
+ await this.actionConnectionService.orderPing(event, backtest, { actionName, ...context });
19559
+ }
19560
+ };
19337
19561
  /**
19338
19562
  * Disposes all ClientAction instances for the strategy.
19339
19563
  *
@@ -19802,6 +20026,29 @@ const VALID_METHOD_NAMES = [
19802
20026
  "riskRejection",
19803
20027
  "dispose",
19804
20028
  ];
20029
+ /**
20030
+ * Exchange-integration handler methods that exist on IAction but are intentionally NOT in
20031
+ * VALID_METHOD_NAMES. They are discouraged on action handlers: exchange synchronization belongs
20032
+ * in the infrastructure domain layer (Broker.useBrokerAdapter), not in actions. Naming a handler
20033
+ * method one of these produces a dedicated error redirecting to the Broker adapter instead of the
20034
+ * generic "invalid method" suggestion.
20035
+ */
20036
+ const DISCOURAGED_METHOD_NAMES = ["signalSync", "orderPing"];
20037
+ /**
20038
+ * Builds the dedicated error message for a discouraged exchange-integration handler method.
20039
+ *
20040
+ * @param actionName - Name of the action being validated
20041
+ * @param methodName - The discouraged method name found on the handler
20042
+ * @returns Multi-line error message redirecting to the Broker adapter
20043
+ */
20044
+ const DISCOURAGED_METHOD_MESSAGE = (actionName, methodName) => functoolsKit.str.newline([
20045
+ `ActionSchema ${actionName} contains discouraged method "${methodName}". `,
20046
+ `Exchange integration must be implemented in Broker.useBrokerAdapter as the infrastructure domain layer, not in an action handler.`,
20047
+ functoolsKit.typo.nbsp,
20048
+ `Use Broker.useBrokerAdapter with onOrderPing (order still open?) and onSignalOpenCommit / onSignalCloseCommit (order fill) instead.`,
20049
+ functoolsKit.typo.nbsp,
20050
+ `If you want to keep this property name use one of these patterns: _${methodName} or #${methodName}`,
20051
+ ]);
19805
20052
  /**
19806
20053
  * Calculates the Levenshtein distance between two strings.
19807
20054
  *
@@ -19904,6 +20151,13 @@ const VALIDATE_CLASS_METHODS = (actionName, handler, self) => {
19904
20151
  }
19905
20152
  const descriptor = Object.getOwnPropertyDescriptor(handler.prototype, methodName);
19906
20153
  const isMethod = descriptor && typeof descriptor.value === "function";
20154
+ if (isMethod && DISCOURAGED_METHOD_NAMES.includes(methodName)) {
20155
+ const msg = DISCOURAGED_METHOD_MESSAGE(actionName, methodName);
20156
+ self.loggerService.log(`actionValidationService exception thrown`, {
20157
+ msg,
20158
+ });
20159
+ console.log(msg);
20160
+ }
19907
20161
  if (isMethod && !VALID_METHOD_NAMES.includes(methodName)) {
19908
20162
  const suggestions = FIND_SUGGESTIONS(methodName, VALID_METHOD_NAMES);
19909
20163
  const lines = [
@@ -19943,6 +20197,14 @@ const VALIDATE_OBJECT_METHODS = (actionName, handler, self) => {
19943
20197
  if (methodName.startsWith("_")) {
19944
20198
  continue;
19945
20199
  }
20200
+ if (typeof handler[methodName] === "function" &&
20201
+ DISCOURAGED_METHOD_NAMES.includes(methodName)) {
20202
+ const msg = DISCOURAGED_METHOD_MESSAGE(actionName, methodName);
20203
+ self.loggerService.log(`actionValidationService exception thrown`, {
20204
+ msg,
20205
+ });
20206
+ console.log(msg);
20207
+ }
19946
20208
  if (typeof handler[methodName] === "function" &&
19947
20209
  !VALID_METHOD_NAMES.includes(methodName)) {
19948
20210
  const suggestions = FIND_SUGGESTIONS(methodName, VALID_METHOD_NAMES);
@@ -42161,6 +42423,7 @@ const breakevenNewTakeProfitPrice = (priceTakeProfit, trailingPriceTakeProfit) =
42161
42423
 
42162
42424
  const BROKER_METHOD_NAME_COMMIT_SIGNAL_OPEN = "BrokerAdapter.commitSignalOpen";
42163
42425
  const BROKER_METHOD_NAME_COMMIT_SIGNAL_CLOSE = "BrokerAdapter.commitSignalClose";
42426
+ const BROKER_METHOD_NAME_COMMIT_SIGNAL_PENDING = "BrokerAdapter.commitSignalPending";
42164
42427
  const BROKER_METHOD_NAME_COMMIT_PARTIAL_PROFIT = "BrokerAdapter.commitPartialProfit";
42165
42428
  const BROKER_METHOD_NAME_COMMIT_PARTIAL_LOSS = "BrokerAdapter.commitPartialLoss";
42166
42429
  const BROKER_METHOD_NAME_COMMIT_TRAILING_STOP = "BrokerAdapter.commitTrailingStop";
@@ -42174,6 +42437,7 @@ const BROKER_METHOD_NAME_CLEAR = "BrokerAdapter.clear";
42174
42437
  const BROKER_BASE_METHOD_NAME_WAIT_FOR_INIT = "BrokerBase.waitForInit";
42175
42438
  const BROKER_BASE_METHOD_NAME_ON_SIGNAL_OPEN = "BrokerBase.onSignalOpenCommit";
42176
42439
  const BROKER_BASE_METHOD_NAME_ON_SIGNAL_CLOSE = "BrokerBase.onSignalCloseCommit";
42440
+ const BROKER_BASE_METHOD_NAME_ON_SIGNAL_PENDING = "BrokerBase.onOrderPing";
42177
42441
  const BROKER_BASE_METHOD_NAME_ON_PARTIAL_PROFIT = "BrokerBase.onPartialProfitCommit";
42178
42442
  const BROKER_BASE_METHOD_NAME_ON_PARTIAL_LOSS = "BrokerBase.onPartialLossCommit";
42179
42443
  const BROKER_BASE_METHOD_NAME_ON_TRAILING_STOP = "BrokerBase.onTrailingStopCommit";
@@ -42222,6 +42486,25 @@ class BrokerProxy {
42222
42486
  }
42223
42487
  throw new Error("BrokerProxy onSignalOpenCommit is not implemented");
42224
42488
  }
42489
+ /**
42490
+ * Forwards a pending-order ping to the underlying adapter.
42491
+ *
42492
+ * If the adapter does not implement `onOrderPing`, the call is silently skipped
42493
+ * (the order is assumed still open). When implemented, exceptions propagate — a throw means
42494
+ * the order was NOT FOUND by `payload.signalId` and the framework closes the position with
42495
+ * closeReason "closed". The adapter must throw ONLY on a confirmed "order not found by id"
42496
+ * result and SWALLOW transient/network errors (return normally), otherwise a connectivity blip
42497
+ * would wrongly close an open position.
42498
+ *
42499
+ * @param payload - Pending ping details: symbol, signalId, position, prices, pnl, context, backtest flag.
42500
+ */
42501
+ async onOrderPing(payload) {
42502
+ if (this._instance.onOrderPing) {
42503
+ await this.waitForInit();
42504
+ await this._instance.onOrderPing(payload);
42505
+ return;
42506
+ }
42507
+ }
42225
42508
  /**
42226
42509
  * Forwards a signal-close event to the underlying adapter.
42227
42510
  * Throws if the adapter does not implement `onSignalCloseCommit`.
@@ -42451,6 +42734,32 @@ class BrokerAdapter {
42451
42734
  await instance.onSignalCloseCommit(payload);
42452
42735
  }
42453
42736
  };
42737
+ /**
42738
+ * Forwards a pending-order ping to the registered broker adapter.
42739
+ *
42740
+ * Called automatically via syncPendingSubject when `enable()` is active, on every live tick
42741
+ * while a pending signal is monitored. Skipped silently in backtest mode or when no adapter is
42742
+ * registered. Exceptions are NOT swallowed: a throw from the adapter propagates up to
42743
+ * syncPendingSubject.next() → CREATE_SYNC_PENDING_FN, which closes the position with "closed".
42744
+ *
42745
+ * @param payload - Pending ping details: symbol, position, prices, pnl, context, backtest flag
42746
+ */
42747
+ this.commitSignalPending = async (payload) => {
42748
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_SIGNAL_PENDING, {
42749
+ symbol: payload.symbol,
42750
+ context: payload.context,
42751
+ });
42752
+ if (!this.enable.hasValue()) {
42753
+ return;
42754
+ }
42755
+ if (payload.backtest) {
42756
+ return;
42757
+ }
42758
+ const instance = this.getInstance();
42759
+ if (instance) {
42760
+ await instance.onOrderPing(payload);
42761
+ }
42762
+ };
42454
42763
  /**
42455
42764
  * Intercepts a partial-profit close before DI-core mutation.
42456
42765
  *
@@ -42753,6 +43062,7 @@ class BrokerAdapter {
42753
43062
  position: event.signal.position,
42754
43063
  cost: event.signal.cost,
42755
43064
  symbol: event.symbol,
43065
+ signalId: event.signalId,
42756
43066
  priceTakeProfit: event.signal.priceTakeProfit,
42757
43067
  priceStopLoss: event.signal.priceStopLoss,
42758
43068
  priceOpen: event.signal.priceOpen,
@@ -42776,6 +43086,7 @@ class BrokerAdapter {
42776
43086
  currentPrice: event.currentPrice,
42777
43087
  cost: event.signal.cost,
42778
43088
  symbol: event.symbol,
43089
+ signalId: event.signalId,
42779
43090
  pnl: event.signal.pnl,
42780
43091
  priceOpen: event.signal.priceOpen,
42781
43092
  peakProfit: event.signal.peakProfit,
@@ -42792,7 +43103,29 @@ class BrokerAdapter {
42792
43103
  backtest: event.backtest,
42793
43104
  });
42794
43105
  });
42795
- const disposeFn = functoolsKit.compose(() => unSignalOpen(), () => unSignalClose());
43106
+ const unSignalPending = syncPendingSubject.subscribe(async (event) => {
43107
+ await this.commitSignalPending({
43108
+ position: event.position,
43109
+ currentPrice: event.currentPrice,
43110
+ symbol: event.symbol,
43111
+ signalId: event.signalId,
43112
+ priceOpen: event.priceOpen,
43113
+ priceTakeProfit: event.priceTakeProfit,
43114
+ priceStopLoss: event.priceStopLoss,
43115
+ pnl: event.pnl,
43116
+ peakProfit: event.peakProfit,
43117
+ maxDrawdown: event.maxDrawdown,
43118
+ totalEntries: event.totalEntries,
43119
+ totalPartials: event.totalPartials,
43120
+ context: {
43121
+ strategyName: event.strategyName,
43122
+ exchangeName: event.exchangeName,
43123
+ frameName: event.frameName,
43124
+ },
43125
+ backtest: event.backtest,
43126
+ });
43127
+ });
43128
+ const disposeFn = functoolsKit.compose(() => unSignalOpen(), () => unSignalClose(), () => unSignalPending());
42796
43129
  return () => {
42797
43130
  this.enable.clear();
42798
43131
  disposeFn();
@@ -42965,6 +43298,44 @@ class BrokerBase {
42965
43298
  context: payload.context,
42966
43299
  });
42967
43300
  }
43301
+ /**
43302
+ * Called on every live tick while a pending signal is monitored, BEFORE TP/SL/time evaluation.
43303
+ *
43304
+ * Override to query the exchange for the order by `payload.signalId` and THROW ONLY when it is
43305
+ * definitively NOT FOUND by that id (filled, cancelled, or liquidated externally) — the framework
43306
+ * then closes the position with closeReason "closed". The default implementation logs and returns
43307
+ * normally, which keeps the position under normal TP/SL monitoring.
43308
+ *
43309
+ * CRITICAL: swallow transient/network errors (timeout, 5xx, rate limit, disconnect) — return
43310
+ * normally instead of throwing. A thrown network error would wrongly close an open position; only
43311
+ * a confirmed "order not found by id" response is a valid reason to throw.
43312
+ *
43313
+ * @param payload - Pending ping details: symbol, signalId, position, prices, pnl, context, backtest
43314
+ *
43315
+ * @example
43316
+ * ```typescript
43317
+ * async onOrderPing(payload: BrokerSignalPendingPayload) {
43318
+ * super.onOrderPing(payload); // Keep parent logging
43319
+ * let order: Order | null;
43320
+ * try {
43321
+ * order = await this.exchange.getOrderById(payload.signalId);
43322
+ * } catch (networkError) {
43323
+ * // Transient/network error — swallow and keep the position open, retry next tick
43324
+ * return;
43325
+ * }
43326
+ * if (!order) {
43327
+ * // Confirmed not found by id — close the position
43328
+ * throw new Error(`Order ${payload.signalId} not found on the exchange`);
43329
+ * }
43330
+ * }
43331
+ * ```
43332
+ */
43333
+ async onOrderPing(payload) {
43334
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SIGNAL_PENDING, {
43335
+ symbol: payload.symbol,
43336
+ context: payload.context,
43337
+ });
43338
+ }
42968
43339
  /**
42969
43340
  * Called when a position is fully closed (SL/TP hit or manual close).
42970
43341
  *
@@ -43363,6 +43734,7 @@ async function commitPartialProfit(symbol, percentToClose) {
43363
43734
  }
43364
43735
  await Broker.commitPartialProfit({
43365
43736
  symbol,
43737
+ signalId: signalForProfit.id,
43366
43738
  percentToClose,
43367
43739
  cost: percentToCloseCost(percentToClose, investedCostForProfit),
43368
43740
  currentPrice,
@@ -43428,6 +43800,7 @@ async function commitPartialLoss(symbol, percentToClose) {
43428
43800
  }
43429
43801
  await Broker.commitPartialLoss({
43430
43802
  symbol,
43803
+ signalId: signalForLoss.id,
43431
43804
  percentToClose,
43432
43805
  cost: percentToCloseCost(percentToClose, investedCostForLoss),
43433
43806
  currentPrice,
@@ -43509,6 +43882,7 @@ async function commitTrailingStop(symbol, percentShift, currentPrice) {
43509
43882
  }
43510
43883
  await Broker.commitTrailingStop({
43511
43884
  symbol,
43885
+ signalId: signal.id,
43512
43886
  percentShift,
43513
43887
  currentPrice,
43514
43888
  newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
@@ -43589,6 +43963,7 @@ async function commitTrailingTake(symbol, percentShift, currentPrice) {
43589
43963
  }
43590
43964
  await Broker.commitTrailingTake({
43591
43965
  symbol,
43966
+ signalId: signal.id,
43592
43967
  percentShift,
43593
43968
  currentPrice,
43594
43969
  newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
@@ -43640,6 +44015,7 @@ async function commitTrailingStopCost(symbol, newStopLossPrice) {
43640
44015
  }
43641
44016
  await Broker.commitTrailingStop({
43642
44017
  symbol,
44018
+ signalId: signal.id,
43643
44019
  percentShift,
43644
44020
  currentPrice,
43645
44021
  newStopLossPrice,
@@ -43691,6 +44067,7 @@ async function commitTrailingTakeCost(symbol, newTakeProfitPrice) {
43691
44067
  }
43692
44068
  await Broker.commitTrailingTake({
43693
44069
  symbol,
44070
+ signalId: signal.id,
43694
44071
  percentShift,
43695
44072
  currentPrice,
43696
44073
  newTakeProfitPrice,
@@ -43752,6 +44129,7 @@ async function commitBreakeven(symbol) {
43752
44129
  }
43753
44130
  await Broker.commitBreakeven({
43754
44131
  symbol,
44132
+ signalId: signal.id,
43755
44133
  currentPrice,
43756
44134
  newStopLossPrice: breakevenNewStopLossPrice(effectivePriceOpen),
43757
44135
  newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
@@ -43842,6 +44220,7 @@ async function commitAverageBuy(symbol, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_C
43842
44220
  }
43843
44221
  await Broker.commitAverageBuy({
43844
44222
  symbol,
44223
+ signalId: signalForAvgBuy.id,
43845
44224
  currentPrice,
43846
44225
  cost,
43847
44226
  position: signalForAvgBuy.position,
@@ -44225,6 +44604,7 @@ async function commitPartialProfitCost(symbol, dollarAmount) {
44225
44604
  }
44226
44605
  await Broker.commitPartialProfit({
44227
44606
  symbol,
44607
+ signalId: signalForProfitCost.id,
44228
44608
  percentToClose,
44229
44609
  cost: dollarAmount,
44230
44610
  currentPrice,
@@ -44293,6 +44673,7 @@ async function commitPartialLossCost(symbol, dollarAmount) {
44293
44673
  }
44294
44674
  await Broker.commitPartialLoss({
44295
44675
  symbol,
44676
+ signalId: signalForLossCost.id,
44296
44677
  percentToClose,
44297
44678
  cost: dollarAmount,
44298
44679
  currentPrice,
@@ -48847,6 +49228,7 @@ class BacktestUtils {
48847
49228
  }
48848
49229
  await Broker.commitPartialProfit({
48849
49230
  symbol,
49231
+ signalId: signalForProfit.id,
48850
49232
  percentToClose,
48851
49233
  cost: percentToCloseCost(percentToClose, investedCostForProfit),
48852
49234
  currentPrice,
@@ -48918,6 +49300,7 @@ class BacktestUtils {
48918
49300
  }
48919
49301
  await Broker.commitPartialLoss({
48920
49302
  symbol,
49303
+ signalId: signalForLoss.id,
48921
49304
  percentToClose,
48922
49305
  cost: percentToCloseCost(percentToClose, investedCostForLoss),
48923
49306
  currentPrice,
@@ -48991,6 +49374,7 @@ class BacktestUtils {
48991
49374
  }
48992
49375
  await Broker.commitPartialProfit({
48993
49376
  symbol,
49377
+ signalId: signalForProfitCost.id,
48994
49378
  percentToClose,
48995
49379
  cost: dollarAmount,
48996
49380
  currentPrice,
@@ -49064,6 +49448,7 @@ class BacktestUtils {
49064
49448
  }
49065
49449
  await Broker.commitPartialLoss({
49066
49450
  symbol,
49451
+ signalId: signalForLossCost.id,
49067
49452
  percentToClose,
49068
49453
  cost: dollarAmount,
49069
49454
  currentPrice,
@@ -49150,6 +49535,7 @@ class BacktestUtils {
49150
49535
  }
49151
49536
  await Broker.commitTrailingStop({
49152
49537
  symbol,
49538
+ signalId: signal.id,
49153
49539
  percentShift,
49154
49540
  currentPrice,
49155
49541
  newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
@@ -49235,6 +49621,7 @@ class BacktestUtils {
49235
49621
  }
49236
49622
  await Broker.commitTrailingTake({
49237
49623
  symbol,
49624
+ signalId: signal.id,
49238
49625
  percentShift,
49239
49626
  currentPrice,
49240
49627
  newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
@@ -49289,6 +49676,7 @@ class BacktestUtils {
49289
49676
  }
49290
49677
  await Broker.commitTrailingStop({
49291
49678
  symbol,
49679
+ signalId: signal.id,
49292
49680
  percentShift,
49293
49681
  currentPrice,
49294
49682
  newStopLossPrice,
@@ -49343,6 +49731,7 @@ class BacktestUtils {
49343
49731
  }
49344
49732
  await Broker.commitTrailingTake({
49345
49733
  symbol,
49734
+ signalId: signal.id,
49346
49735
  percentShift,
49347
49736
  currentPrice,
49348
49737
  newTakeProfitPrice,
@@ -49404,6 +49793,7 @@ class BacktestUtils {
49404
49793
  }
49405
49794
  await Broker.commitBreakeven({
49406
49795
  symbol,
49796
+ signalId: signal.id,
49407
49797
  currentPrice,
49408
49798
  newStopLossPrice: breakevenNewStopLossPrice(effectivePriceOpen),
49409
49799
  newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
@@ -49503,6 +49893,7 @@ class BacktestUtils {
49503
49893
  }
49504
49894
  await Broker.commitAverageBuy({
49505
49895
  symbol,
49896
+ signalId: signalForAvgBuy.id,
49506
49897
  currentPrice,
49507
49898
  cost,
49508
49899
  position: signalForAvgBuy.position,
@@ -51750,6 +52141,7 @@ class LiveUtils {
51750
52141
  }
51751
52142
  await Broker.commitPartialProfit({
51752
52143
  symbol,
52144
+ signalId: signalForProfit.id,
51753
52145
  percentToClose,
51754
52146
  cost: percentToCloseCost(percentToClose, investedCost),
51755
52147
  currentPrice,
@@ -51840,6 +52232,7 @@ class LiveUtils {
51840
52232
  }
51841
52233
  await Broker.commitPartialLoss({
51842
52234
  symbol,
52235
+ signalId: signalForLoss.id,
51843
52236
  percentToClose,
51844
52237
  cost: percentToCloseCost(percentToClose, investedCost),
51845
52238
  currentPrice,
@@ -51932,6 +52325,7 @@ class LiveUtils {
51932
52325
  }
51933
52326
  await Broker.commitPartialProfit({
51934
52327
  symbol,
52328
+ signalId: signalForProfitCost.id,
51935
52329
  percentToClose,
51936
52330
  cost: dollarAmount,
51937
52331
  currentPrice,
@@ -52024,6 +52418,7 @@ class LiveUtils {
52024
52418
  }
52025
52419
  await Broker.commitPartialLoss({
52026
52420
  symbol,
52421
+ signalId: signalForLossCost.id,
52027
52422
  percentToClose,
52028
52423
  cost: dollarAmount,
52029
52424
  currentPrice,
@@ -52129,6 +52524,7 @@ class LiveUtils {
52129
52524
  }
52130
52525
  await Broker.commitTrailingStop({
52131
52526
  symbol,
52527
+ signalId: signal.id,
52132
52528
  percentShift,
52133
52529
  currentPrice,
52134
52530
  newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
@@ -52229,6 +52625,7 @@ class LiveUtils {
52229
52625
  }
52230
52626
  await Broker.commitTrailingTake({
52231
52627
  symbol,
52628
+ signalId: signal.id,
52232
52629
  percentShift,
52233
52630
  currentPrice,
52234
52631
  newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
@@ -52299,6 +52696,7 @@ class LiveUtils {
52299
52696
  }
52300
52697
  await Broker.commitTrailingStop({
52301
52698
  symbol,
52699
+ signalId: signal.id,
52302
52700
  percentShift,
52303
52701
  currentPrice,
52304
52702
  newStopLossPrice,
@@ -52369,6 +52767,7 @@ class LiveUtils {
52369
52767
  }
52370
52768
  await Broker.commitTrailingTake({
52371
52769
  symbol,
52770
+ signalId: signal.id,
52372
52771
  percentShift,
52373
52772
  currentPrice,
52374
52773
  newTakeProfitPrice,
@@ -52446,6 +52845,7 @@ class LiveUtils {
52446
52845
  }
52447
52846
  await Broker.commitBreakeven({
52448
52847
  symbol,
52848
+ signalId: signal.id,
52449
52849
  currentPrice,
52450
52850
  newStopLossPrice: breakevenNewStopLossPrice(effectivePriceOpen),
52451
52851
  newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
@@ -52563,6 +52963,7 @@ class LiveUtils {
52563
52963
  }
52564
52964
  await Broker.commitAverageBuy({
52565
52965
  symbol,
52966
+ signalId: signalForAvgBuy.id,
52566
52967
  currentPrice,
52567
52968
  cost,
52568
52969
  position: signalForAvgBuy.position,
@@ -59984,6 +60385,7 @@ const SUBJECT_ISOLATION_LIST = [
59984
60385
  signalLiveEmitter,
59985
60386
  strategyCommitSubject,
59986
60387
  syncSubject,
60388
+ syncPendingSubject,
59987
60389
  validationSubject,
59988
60390
  signalNotifySubject,
59989
60391
  beforeStartSubject,