backtest-kit 13.5.0 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.mjs CHANGED
@@ -629,6 +629,15 @@ const DEFAULT_CONFIG = Object.freeze({ ...GLOBAL_CONFIG });
629
629
  * Consumers should implement retry logic in their listeners to handle transient synchronization failures.
630
630
  */
631
631
  const syncSubject = new Subject();
632
+ /**
633
+ * Pending-order synchronization emitter.
634
+ * Emitted on every live tick while a pending signal is monitored, BEFORE TP/SL/time evaluation.
635
+ * Asks the exchange whether the order is STILL pending (open).
636
+ *
637
+ * If a listener returns false OR throws, the order is treated as no longer open on the exchange
638
+ * and the framework closes the pending signal with closeReason "closed". Never emitted in backtest.
639
+ */
640
+ const syncPendingSubject = new Subject();
632
641
  /**
633
642
  * Global signal emitter for all trading events (live + backtest).
634
643
  * Emits all signal events regardless of execution mode.
@@ -836,6 +845,7 @@ var emitters = /*#__PURE__*/Object.freeze({
836
845
  signalLiveEmitter: signalLiveEmitter,
837
846
  signalNotifySubject: signalNotifySubject,
838
847
  strategyCommitSubject: strategyCommitSubject,
848
+ syncPendingSubject: syncPendingSubject,
839
849
  syncSubject: syncSubject,
840
850
  validationSubject: validationSubject,
841
851
  walkerCompleteSubject: walkerCompleteSubject,
@@ -7055,6 +7065,64 @@ const CALL_SIGNAL_SYNC_CLOSE_FN = trycatch(async (timestamp, currentPrice, close
7055
7065
  errorEmitter.next(error);
7056
7066
  }
7057
7067
  });
7068
+ /**
7069
+ * Calls onSignalPing callback for the pending-order synchronization event.
7070
+ *
7071
+ * Invoked at the start of the pending-signal monitoring block on every LIVE tick, BEFORE the
7072
+ * framework evaluates TP/SL/time completion. It asks the external order management system
7073
+ * whether the order backing this position is STILL pending (open) on the exchange.
7074
+ *
7075
+ * The Subject `await .next()` propagates listener exceptions passthrough, so the trycatch wrapper
7076
+ * collapses both a thrown listener and an explicit `false` into `false` (defaultValue). A `false`
7077
+ * result means the order is no longer open on the exchange and the caller closes the position with
7078
+ * closeReason "closed". A missing/true result keeps the position under normal TP/SL monitoring.
7079
+ */
7080
+ const CALL_SIGNAL_PING_FN = trycatch(async (timestamp, currentPrice, signal, self) => {
7081
+ const publicSignal = TO_PUBLIC_SIGNAL("pending", signal, currentPrice);
7082
+ return await self.params.onSignalPing({
7083
+ action: "signal-ping",
7084
+ symbol: self.params.execution.context.symbol,
7085
+ strategyName: self.params.strategyName,
7086
+ exchangeName: self.params.exchangeName,
7087
+ frameName: self.params.frameName,
7088
+ backtest: self.params.execution.context.backtest,
7089
+ signalId: signal.id,
7090
+ timestamp,
7091
+ signal: publicSignal,
7092
+ currentPrice,
7093
+ pnl: publicSignal.pnl,
7094
+ peakProfit: publicSignal.peakProfit,
7095
+ maxDrawdown: publicSignal.maxDrawdown,
7096
+ position: publicSignal.position,
7097
+ priceOpen: publicSignal.priceOpen,
7098
+ priceTakeProfit: publicSignal.priceTakeProfit,
7099
+ priceStopLoss: publicSignal.priceStopLoss,
7100
+ originalPriceTakeProfit: publicSignal.originalPriceTakeProfit,
7101
+ originalPriceStopLoss: publicSignal.originalPriceStopLoss,
7102
+ originalPriceOpen: publicSignal.originalPriceOpen,
7103
+ scheduledAt: publicSignal.scheduledAt,
7104
+ pendingAt: publicSignal.pendingAt,
7105
+ totalEntries: publicSignal.totalEntries,
7106
+ totalPartials: publicSignal.totalPartials,
7107
+ });
7108
+ }, {
7109
+ defaultValue: false,
7110
+ fallback: (error, timestamp, currentPrice, signal, self) => {
7111
+ const message = "ClientStrategy CALL_SIGNAL_PING_FN thrown";
7112
+ const payload = {
7113
+ error: errorData(error),
7114
+ message: getErrorMessage(error),
7115
+ data: {
7116
+ timestamp,
7117
+ currentPrice,
7118
+ signalId: signal.id,
7119
+ }
7120
+ };
7121
+ self.params.logger.warn(message, payload);
7122
+ console.warn(message, payload);
7123
+ errorEmitter.next(error);
7124
+ }
7125
+ });
7058
7126
  /**
7059
7127
  * Calls onCommit callback with strategy commit event.
7060
7128
  *
@@ -8976,6 +9044,47 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
8976
9044
  await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
8977
9045
  return result;
8978
9046
  };
9047
+ /**
9048
+ * Closes the pending signal with closeReason "closed" after the pending-order ping reported the
9049
+ * order is no longer open on the exchange (CALL_SIGNAL_PING_FN returned false / threw).
9050
+ *
9051
+ * Unlike CLOSE_PENDING_SIGNAL_FN this does NOT re-confirm via onSignalSync — the ping already
9052
+ * established the order is gone, so re-asking the broker would be redundant. Runs the same teardown
9053
+ * (close callback, partial/breakeven clear, risk remove, setPendingSignal(null)). Live-only path.
9054
+ */
9055
+ const CLOSE_PENDING_SIGNAL_AS_CLOSED_FN = async (self, signal, currentPrice) => {
9056
+ const currentTime = self.params.execution.context.when.getTime();
9057
+ const publicSignal = TO_PUBLIC_SIGNAL("pending", signal, currentPrice);
9058
+ self.params.logger.info("ClientStrategy signal closed by pending-order ping (order no longer open on exchange)", {
9059
+ symbol: self.params.execution.context.symbol,
9060
+ signalId: signal.id,
9061
+ priceClose: currentPrice,
9062
+ pnlPercentage: publicSignal.pnl.pnlPercentage,
9063
+ });
9064
+ await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
9065
+ // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
9066
+ await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
9067
+ // КРИТИЧНО: Очищаем состояние ClientBreakeven при закрытии позиции
9068
+ await CALL_BREAKEVEN_CLEAR_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
9069
+ await CALL_RISK_REMOVE_SIGNAL_FN(self, self.params.execution.context.symbol, currentTime, self.params.execution.context.backtest);
9070
+ await self.setPendingSignal(null, currentPrice);
9071
+ const result = {
9072
+ action: "closed",
9073
+ signal: publicSignal,
9074
+ currentPrice: currentPrice,
9075
+ closeReason: "closed",
9076
+ closeTimestamp: currentTime,
9077
+ pnl: publicSignal.pnl,
9078
+ strategyName: self.params.method.context.strategyName,
9079
+ exchangeName: self.params.method.context.exchangeName,
9080
+ frameName: self.params.method.context.frameName,
9081
+ symbol: self.params.execution.context.symbol,
9082
+ backtest: self.params.execution.context.backtest,
9083
+ createdAt: currentTime,
9084
+ };
9085
+ await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
9086
+ return result;
9087
+ };
8979
9088
  const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice, backtest) => {
8980
9089
  let percentTp = 0;
8981
9090
  let percentSl = 0;
@@ -11013,6 +11122,16 @@ class ClientStrategy {
11013
11122
  }
11014
11123
  // Monitor pending signal
11015
11124
  const averagePrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
11125
+ // Pending-order ping: before evaluating TP/SL/time, confirm the order is STILL open on the
11126
+ // exchange. CALL_SIGNAL_PING_FN returns false when the listener returns false OR throws
11127
+ // (Subject .next passthrough), meaning the order is no longer pending — close with "closed".
11128
+ // Skipped in backtest: there is no live exchange to query.
11129
+ if (!this.params.execution.context.backtest) {
11130
+ const stillPending = await CALL_SIGNAL_PING_FN(currentTime, averagePrice, this._pendingSignal, this);
11131
+ if (!stillPending) {
11132
+ return await CLOSE_PENDING_SIGNAL_AS_CLOSED_FN(this, this._pendingSignal, averagePrice);
11133
+ }
11134
+ }
11016
11135
  const closedResult = await CHECK_PENDING_SIGNAL_COMPLETION_FN(this, this._pendingSignal, averagePrice);
11017
11136
  if (closedResult) {
11018
11137
  return closedResult;
@@ -12861,6 +12980,32 @@ const CREATE_SYNC_FN = (self, strategyName, exchangeName, frameName, backtest) =
12861
12980
  },
12862
12981
  defaultValue: false,
12863
12982
  });
12983
+ /**
12984
+ * If the syncPendingSubject listener or any registered action throws, it means the order backing the
12985
+ * open position is no longer pending on the exchange (filled/cancelled/liquidated externally).
12986
+ * The trycatch wrapper collapses a throw OR an explicit false into false, and ClientStrategy closes
12987
+ * the pending signal with closeReason "closed". Skipped in backtest — there is no live exchange.
12988
+ */
12989
+ const CREATE_SYNC_PENDING_FN = (self, strategyName, exchangeName, frameName, backtest) => trycatch(async (event) => {
12990
+ if (event.backtest) {
12991
+ return true;
12992
+ }
12993
+ await syncPendingSubject.next(event);
12994
+ await self.actionCoreService.orderPing(backtest, event, { strategyName, exchangeName, frameName });
12995
+ return true;
12996
+ }, {
12997
+ fallback: (error) => {
12998
+ const message = "StrategyConnectionService CREATE_SYNC_PENDING_FN thrown. Order no longer pending on exchange";
12999
+ const payload = {
13000
+ error: errorData(error),
13001
+ message: getErrorMessage(error),
13002
+ };
13003
+ self.loggerService.warn(message, payload);
13004
+ console.error(message, payload);
13005
+ errorEmitter.next(error);
13006
+ },
13007
+ defaultValue: false,
13008
+ });
12864
13009
  /**
12865
13010
  * Emits signal tick results with correct execution context timestamp.
12866
13011
  * Wraps emitter calls in ExecutionContextService.runInContext to preserve
@@ -13297,6 +13442,7 @@ class StrategyConnectionService {
13297
13442
  onDispose: CREATE_COMMIT_DISPOSE_FN(this),
13298
13443
  onCommit: CREATE_COMMIT_FN(this),
13299
13444
  onSignalSync: CREATE_SYNC_FN(this, strategyName, exchangeName, frameName, backtest),
13445
+ onSignalPing: CREATE_SYNC_PENDING_FN(this, strategyName, exchangeName, frameName, backtest),
13300
13446
  onHighestProfit: CREATE_HIGHEST_PROFIT_FN(this, strategyName, exchangeName, frameName, backtest),
13301
13447
  onMaxDrawdown: CREATE_MAX_DRAWDOWN_FN(this, strategyName, exchangeName, frameName, backtest),
13302
13448
  });
@@ -16231,6 +16377,22 @@ class ActionProxy {
16231
16377
  await this._target.signalSync(event);
16232
16378
  }
16233
16379
  }
16380
+ /**
16381
+ * Gate for the pending-order ping (order still open on exchange?).
16382
+ * NOT wrapped in trycatch — exceptions propagate to CREATE_SYNC_PENDING_FN.
16383
+ *
16384
+ * @param event - Pending-ping event with action "signal-ping"
16385
+ */
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\"!");
16391
+ console.error("");
16392
+ console.error("You have been warned!");
16393
+ await this._target.orderPing(event);
16394
+ }
16395
+ }
16234
16396
  /**
16235
16397
  * Cleans up resources with error capture.
16236
16398
  *
@@ -16450,6 +16612,15 @@ const CALL_SIGNAL_SYNC_CALLBACK_FN = async (self, event, strategyName, frameName
16450
16612
  await self.params.callbacks.onSignalSync(event, self.params.actionName, strategyName, frameName, isBacktest);
16451
16613
  }
16452
16614
  };
16615
+ /**
16616
+ * Calls onOrderPing callback WITHOUT trycatch — exceptions must propagate
16617
+ * up to CREATE_SYNC_PENDING_FN in StrategyConnectionService (which returns false on error).
16618
+ */
16619
+ 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);
16622
+ }
16623
+ };
16453
16624
  /** Wrapper to call onInit callback with error handling */
16454
16625
  const CALL_INIT_CALLBACK_FN = trycatch(async (self, strategyName, frameName, backtest) => {
16455
16626
  if (self.params.callbacks?.onInit) {
@@ -16828,6 +16999,25 @@ class ClientAction {
16828
16999
  await CALL_SIGNAL_SYNC_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
16829
17000
  }
16830
17001
  ;
17002
+ /**
17003
+ * Gate for the pending-order ping (order still open on exchange?).
17004
+ * NOT wrapped in trycatch — exceptions propagate to CREATE_SYNC_PENDING_FN.
17005
+ */
17006
+ async orderPing(event) {
17007
+ this.params.logger.debug("ClientAction orderPing", {
17008
+ actionName: this.params.actionName,
17009
+ strategyName: this.params.strategyName,
17010
+ frameName: this.params.frameName,
17011
+ });
17012
+ if (!this._handlerInstance) {
17013
+ await this.waitForInit();
17014
+ }
17015
+ // Call handler method if defined — exceptions propagate
17016
+ await this._handlerInstance?.orderPing(event);
17017
+ // Call callback if defined — exceptions propagate
17018
+ await CALL_ORDER_PING_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
17019
+ }
17020
+ ;
16831
17021
  }
16832
17022
 
16833
17023
  /**
@@ -17090,6 +17280,22 @@ class ActionConnectionService {
17090
17280
  const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
17091
17281
  await action.signalSync(event);
17092
17282
  };
17283
+ /**
17284
+ * Routes orderPing event to appropriate ClientAction instance.
17285
+ * NOT wrapped in trycatch — exceptions propagate to CREATE_SYNC_PENDING_FN.
17286
+ *
17287
+ * @param event - Pending-ping event with action "signal-ping"
17288
+ * @param backtest - Whether running in backtest mode
17289
+ * @param context - Execution context
17290
+ */
17291
+ this.orderPing = async (event, backtest, context) => {
17292
+ this.loggerService.log("actionConnectionService orderPing", {
17293
+ backtest,
17294
+ context,
17295
+ });
17296
+ const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
17297
+ await action.orderPing(event);
17298
+ };
17093
17299
  /**
17094
17300
  * Disposes the ClientAction instance for the given action name.
17095
17301
  *
@@ -19314,6 +19520,24 @@ class ActionCoreService {
19314
19520
  await this.actionConnectionService.signalSync(event, backtest, { actionName, ...context });
19315
19521
  }
19316
19522
  };
19523
+ /**
19524
+ * Gates the pending-order ping across all registered actions.
19525
+ * NOT wrapped in trycatch — exceptions propagate to CREATE_SYNC_PENDING_FN.
19526
+ *
19527
+ * @param backtest - Whether running in backtest mode
19528
+ * @param event - Pending-ping event with action "signal-ping"
19529
+ * @param context - Strategy execution context
19530
+ */
19531
+ this.orderPing = async (backtest, event, context) => {
19532
+ this.loggerService.log("actionCoreService orderPing", {
19533
+ context,
19534
+ });
19535
+ await this.validate(context);
19536
+ const { actions = [] } = this.strategySchemaService.get(context.strategyName);
19537
+ for (const actionName of actions) {
19538
+ await this.actionConnectionService.orderPing(event, backtest, { actionName, ...context });
19539
+ }
19540
+ };
19317
19541
  /**
19318
19542
  * Disposes all ClientAction instances for the strategy.
19319
19543
  *
@@ -19782,6 +20006,29 @@ const VALID_METHOD_NAMES = [
19782
20006
  "riskRejection",
19783
20007
  "dispose",
19784
20008
  ];
20009
+ /**
20010
+ * Exchange-integration handler methods that exist on IAction but are intentionally NOT in
20011
+ * VALID_METHOD_NAMES. They are discouraged on action handlers: exchange synchronization belongs
20012
+ * in the infrastructure domain layer (Broker.useBrokerAdapter), not in actions. Naming a handler
20013
+ * method one of these produces a dedicated error redirecting to the Broker adapter instead of the
20014
+ * generic "invalid method" suggestion.
20015
+ */
20016
+ const DISCOURAGED_METHOD_NAMES = ["signalSync", "orderPing"];
20017
+ /**
20018
+ * Builds the dedicated error message for a discouraged exchange-integration handler method.
20019
+ *
20020
+ * @param actionName - Name of the action being validated
20021
+ * @param methodName - The discouraged method name found on the handler
20022
+ * @returns Multi-line error message redirecting to the Broker adapter
20023
+ */
20024
+ const DISCOURAGED_METHOD_MESSAGE = (actionName, methodName) => str.newline([
20025
+ `ActionSchema ${actionName} contains discouraged method "${methodName}". `,
20026
+ `Exchange integration must be implemented in Broker.useBrokerAdapter as the infrastructure domain layer, not in an action handler.`,
20027
+ typo.nbsp,
20028
+ `Use Broker.useBrokerAdapter with onOrderPing (order still open?) and onSignalOpenCommit / onSignalCloseCommit (order fill) instead.`,
20029
+ typo.nbsp,
20030
+ `If you want to keep this property name use one of these patterns: _${methodName} or #${methodName}`,
20031
+ ]);
19785
20032
  /**
19786
20033
  * Calculates the Levenshtein distance between two strings.
19787
20034
  *
@@ -19884,6 +20131,13 @@ const VALIDATE_CLASS_METHODS = (actionName, handler, self) => {
19884
20131
  }
19885
20132
  const descriptor = Object.getOwnPropertyDescriptor(handler.prototype, methodName);
19886
20133
  const isMethod = descriptor && typeof descriptor.value === "function";
20134
+ if (isMethod && DISCOURAGED_METHOD_NAMES.includes(methodName)) {
20135
+ const msg = DISCOURAGED_METHOD_MESSAGE(actionName, methodName);
20136
+ self.loggerService.log(`actionValidationService exception thrown`, {
20137
+ msg,
20138
+ });
20139
+ console.log(msg);
20140
+ }
19887
20141
  if (isMethod && !VALID_METHOD_NAMES.includes(methodName)) {
19888
20142
  const suggestions = FIND_SUGGESTIONS(methodName, VALID_METHOD_NAMES);
19889
20143
  const lines = [
@@ -19923,6 +20177,14 @@ const VALIDATE_OBJECT_METHODS = (actionName, handler, self) => {
19923
20177
  if (methodName.startsWith("_")) {
19924
20178
  continue;
19925
20179
  }
20180
+ if (typeof handler[methodName] === "function" &&
20181
+ DISCOURAGED_METHOD_NAMES.includes(methodName)) {
20182
+ const msg = DISCOURAGED_METHOD_MESSAGE(actionName, methodName);
20183
+ self.loggerService.log(`actionValidationService exception thrown`, {
20184
+ msg,
20185
+ });
20186
+ console.log(msg);
20187
+ }
19926
20188
  if (typeof handler[methodName] === "function" &&
19927
20189
  !VALID_METHOD_NAMES.includes(methodName)) {
19928
20190
  const suggestions = FIND_SUGGESTIONS(methodName, VALID_METHOD_NAMES);
@@ -42141,6 +42403,7 @@ const breakevenNewTakeProfitPrice = (priceTakeProfit, trailingPriceTakeProfit) =
42141
42403
 
42142
42404
  const BROKER_METHOD_NAME_COMMIT_SIGNAL_OPEN = "BrokerAdapter.commitSignalOpen";
42143
42405
  const BROKER_METHOD_NAME_COMMIT_SIGNAL_CLOSE = "BrokerAdapter.commitSignalClose";
42406
+ const BROKER_METHOD_NAME_COMMIT_SIGNAL_PENDING = "BrokerAdapter.commitSignalPending";
42144
42407
  const BROKER_METHOD_NAME_COMMIT_PARTIAL_PROFIT = "BrokerAdapter.commitPartialProfit";
42145
42408
  const BROKER_METHOD_NAME_COMMIT_PARTIAL_LOSS = "BrokerAdapter.commitPartialLoss";
42146
42409
  const BROKER_METHOD_NAME_COMMIT_TRAILING_STOP = "BrokerAdapter.commitTrailingStop";
@@ -42154,6 +42417,7 @@ const BROKER_METHOD_NAME_CLEAR = "BrokerAdapter.clear";
42154
42417
  const BROKER_BASE_METHOD_NAME_WAIT_FOR_INIT = "BrokerBase.waitForInit";
42155
42418
  const BROKER_BASE_METHOD_NAME_ON_SIGNAL_OPEN = "BrokerBase.onSignalOpenCommit";
42156
42419
  const BROKER_BASE_METHOD_NAME_ON_SIGNAL_CLOSE = "BrokerBase.onSignalCloseCommit";
42420
+ const BROKER_BASE_METHOD_NAME_ON_SIGNAL_PENDING = "BrokerBase.onOrderPing";
42157
42421
  const BROKER_BASE_METHOD_NAME_ON_PARTIAL_PROFIT = "BrokerBase.onPartialProfitCommit";
42158
42422
  const BROKER_BASE_METHOD_NAME_ON_PARTIAL_LOSS = "BrokerBase.onPartialLossCommit";
42159
42423
  const BROKER_BASE_METHOD_NAME_ON_TRAILING_STOP = "BrokerBase.onTrailingStopCommit";
@@ -42202,6 +42466,25 @@ class BrokerProxy {
42202
42466
  }
42203
42467
  throw new Error("BrokerProxy onSignalOpenCommit is not implemented");
42204
42468
  }
42469
+ /**
42470
+ * Forwards a pending-order ping to the underlying adapter.
42471
+ *
42472
+ * If the adapter does not implement `onOrderPing`, the call is silently skipped
42473
+ * (the order is assumed still open). When implemented, exceptions propagate — a throw means
42474
+ * the order was NOT FOUND by `payload.signalId` and the framework closes the position with
42475
+ * closeReason "closed". The adapter must throw ONLY on a confirmed "order not found by id"
42476
+ * result and SWALLOW transient/network errors (return normally), otherwise a connectivity blip
42477
+ * would wrongly close an open position.
42478
+ *
42479
+ * @param payload - Pending ping details: symbol, signalId, position, prices, pnl, context, backtest flag.
42480
+ */
42481
+ async onOrderPing(payload) {
42482
+ if (this._instance.onOrderPing) {
42483
+ await this.waitForInit();
42484
+ await this._instance.onOrderPing(payload);
42485
+ return;
42486
+ }
42487
+ }
42205
42488
  /**
42206
42489
  * Forwards a signal-close event to the underlying adapter.
42207
42490
  * Throws if the adapter does not implement `onSignalCloseCommit`.
@@ -42431,6 +42714,32 @@ class BrokerAdapter {
42431
42714
  await instance.onSignalCloseCommit(payload);
42432
42715
  }
42433
42716
  };
42717
+ /**
42718
+ * Forwards a pending-order ping to the registered broker adapter.
42719
+ *
42720
+ * Called automatically via syncPendingSubject when `enable()` is active, on every live tick
42721
+ * while a pending signal is monitored. Skipped silently in backtest mode or when no adapter is
42722
+ * registered. Exceptions are NOT swallowed: a throw from the adapter propagates up to
42723
+ * syncPendingSubject.next() → CREATE_SYNC_PENDING_FN, which closes the position with "closed".
42724
+ *
42725
+ * @param payload - Pending ping details: symbol, position, prices, pnl, context, backtest flag
42726
+ */
42727
+ this.commitSignalPending = async (payload) => {
42728
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_SIGNAL_PENDING, {
42729
+ symbol: payload.symbol,
42730
+ context: payload.context,
42731
+ });
42732
+ if (!this.enable.hasValue()) {
42733
+ return;
42734
+ }
42735
+ if (payload.backtest) {
42736
+ return;
42737
+ }
42738
+ const instance = this.getInstance();
42739
+ if (instance) {
42740
+ await instance.onOrderPing(payload);
42741
+ }
42742
+ };
42434
42743
  /**
42435
42744
  * Intercepts a partial-profit close before DI-core mutation.
42436
42745
  *
@@ -42733,6 +43042,7 @@ class BrokerAdapter {
42733
43042
  position: event.signal.position,
42734
43043
  cost: event.signal.cost,
42735
43044
  symbol: event.symbol,
43045
+ signalId: event.signalId,
42736
43046
  priceTakeProfit: event.signal.priceTakeProfit,
42737
43047
  priceStopLoss: event.signal.priceStopLoss,
42738
43048
  priceOpen: event.signal.priceOpen,
@@ -42756,6 +43066,7 @@ class BrokerAdapter {
42756
43066
  currentPrice: event.currentPrice,
42757
43067
  cost: event.signal.cost,
42758
43068
  symbol: event.symbol,
43069
+ signalId: event.signalId,
42759
43070
  pnl: event.signal.pnl,
42760
43071
  priceOpen: event.signal.priceOpen,
42761
43072
  peakProfit: event.signal.peakProfit,
@@ -42772,7 +43083,29 @@ class BrokerAdapter {
42772
43083
  backtest: event.backtest,
42773
43084
  });
42774
43085
  });
42775
- const disposeFn = compose(() => unSignalOpen(), () => unSignalClose());
43086
+ const unSignalPending = syncPendingSubject.subscribe(async (event) => {
43087
+ await this.commitSignalPending({
43088
+ position: event.position,
43089
+ currentPrice: event.currentPrice,
43090
+ symbol: event.symbol,
43091
+ signalId: event.signalId,
43092
+ priceOpen: event.priceOpen,
43093
+ priceTakeProfit: event.priceTakeProfit,
43094
+ priceStopLoss: event.priceStopLoss,
43095
+ pnl: event.pnl,
43096
+ peakProfit: event.peakProfit,
43097
+ maxDrawdown: event.maxDrawdown,
43098
+ totalEntries: event.totalEntries,
43099
+ totalPartials: event.totalPartials,
43100
+ context: {
43101
+ strategyName: event.strategyName,
43102
+ exchangeName: event.exchangeName,
43103
+ frameName: event.frameName,
43104
+ },
43105
+ backtest: event.backtest,
43106
+ });
43107
+ });
43108
+ const disposeFn = compose(() => unSignalOpen(), () => unSignalClose(), () => unSignalPending());
42776
43109
  return () => {
42777
43110
  this.enable.clear();
42778
43111
  disposeFn();
@@ -42945,6 +43278,44 @@ class BrokerBase {
42945
43278
  context: payload.context,
42946
43279
  });
42947
43280
  }
43281
+ /**
43282
+ * Called on every live tick while a pending signal is monitored, BEFORE TP/SL/time evaluation.
43283
+ *
43284
+ * Override to query the exchange for the order by `payload.signalId` and THROW ONLY when it is
43285
+ * definitively NOT FOUND by that id (filled, cancelled, or liquidated externally) — the framework
43286
+ * then closes the position with closeReason "closed". The default implementation logs and returns
43287
+ * normally, which keeps the position under normal TP/SL monitoring.
43288
+ *
43289
+ * CRITICAL: swallow transient/network errors (timeout, 5xx, rate limit, disconnect) — return
43290
+ * normally instead of throwing. A thrown network error would wrongly close an open position; only
43291
+ * a confirmed "order not found by id" response is a valid reason to throw.
43292
+ *
43293
+ * @param payload - Pending ping details: symbol, signalId, position, prices, pnl, context, backtest
43294
+ *
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
+ * ```
43312
+ */
43313
+ async onOrderPing(payload) {
43314
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SIGNAL_PENDING, {
43315
+ symbol: payload.symbol,
43316
+ context: payload.context,
43317
+ });
43318
+ }
42948
43319
  /**
42949
43320
  * Called when a position is fully closed (SL/TP hit or manual close).
42950
43321
  *
@@ -43343,6 +43714,7 @@ async function commitPartialProfit(symbol, percentToClose) {
43343
43714
  }
43344
43715
  await Broker.commitPartialProfit({
43345
43716
  symbol,
43717
+ signalId: signalForProfit.id,
43346
43718
  percentToClose,
43347
43719
  cost: percentToCloseCost(percentToClose, investedCostForProfit),
43348
43720
  currentPrice,
@@ -43408,6 +43780,7 @@ async function commitPartialLoss(symbol, percentToClose) {
43408
43780
  }
43409
43781
  await Broker.commitPartialLoss({
43410
43782
  symbol,
43783
+ signalId: signalForLoss.id,
43411
43784
  percentToClose,
43412
43785
  cost: percentToCloseCost(percentToClose, investedCostForLoss),
43413
43786
  currentPrice,
@@ -43489,6 +43862,7 @@ async function commitTrailingStop(symbol, percentShift, currentPrice) {
43489
43862
  }
43490
43863
  await Broker.commitTrailingStop({
43491
43864
  symbol,
43865
+ signalId: signal.id,
43492
43866
  percentShift,
43493
43867
  currentPrice,
43494
43868
  newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
@@ -43569,6 +43943,7 @@ async function commitTrailingTake(symbol, percentShift, currentPrice) {
43569
43943
  }
43570
43944
  await Broker.commitTrailingTake({
43571
43945
  symbol,
43946
+ signalId: signal.id,
43572
43947
  percentShift,
43573
43948
  currentPrice,
43574
43949
  newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
@@ -43620,6 +43995,7 @@ async function commitTrailingStopCost(symbol, newStopLossPrice) {
43620
43995
  }
43621
43996
  await Broker.commitTrailingStop({
43622
43997
  symbol,
43998
+ signalId: signal.id,
43623
43999
  percentShift,
43624
44000
  currentPrice,
43625
44001
  newStopLossPrice,
@@ -43671,6 +44047,7 @@ async function commitTrailingTakeCost(symbol, newTakeProfitPrice) {
43671
44047
  }
43672
44048
  await Broker.commitTrailingTake({
43673
44049
  symbol,
44050
+ signalId: signal.id,
43674
44051
  percentShift,
43675
44052
  currentPrice,
43676
44053
  newTakeProfitPrice,
@@ -43732,6 +44109,7 @@ async function commitBreakeven(symbol) {
43732
44109
  }
43733
44110
  await Broker.commitBreakeven({
43734
44111
  symbol,
44112
+ signalId: signal.id,
43735
44113
  currentPrice,
43736
44114
  newStopLossPrice: breakevenNewStopLossPrice(effectivePriceOpen),
43737
44115
  newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
@@ -43822,6 +44200,7 @@ async function commitAverageBuy(symbol, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_C
43822
44200
  }
43823
44201
  await Broker.commitAverageBuy({
43824
44202
  symbol,
44203
+ signalId: signalForAvgBuy.id,
43825
44204
  currentPrice,
43826
44205
  cost,
43827
44206
  position: signalForAvgBuy.position,
@@ -44205,6 +44584,7 @@ async function commitPartialProfitCost(symbol, dollarAmount) {
44205
44584
  }
44206
44585
  await Broker.commitPartialProfit({
44207
44586
  symbol,
44587
+ signalId: signalForProfitCost.id,
44208
44588
  percentToClose,
44209
44589
  cost: dollarAmount,
44210
44590
  currentPrice,
@@ -44273,6 +44653,7 @@ async function commitPartialLossCost(symbol, dollarAmount) {
44273
44653
  }
44274
44654
  await Broker.commitPartialLoss({
44275
44655
  symbol,
44656
+ signalId: signalForLossCost.id,
44276
44657
  percentToClose,
44277
44658
  cost: dollarAmount,
44278
44659
  currentPrice,
@@ -48827,6 +49208,7 @@ class BacktestUtils {
48827
49208
  }
48828
49209
  await Broker.commitPartialProfit({
48829
49210
  symbol,
49211
+ signalId: signalForProfit.id,
48830
49212
  percentToClose,
48831
49213
  cost: percentToCloseCost(percentToClose, investedCostForProfit),
48832
49214
  currentPrice,
@@ -48898,6 +49280,7 @@ class BacktestUtils {
48898
49280
  }
48899
49281
  await Broker.commitPartialLoss({
48900
49282
  symbol,
49283
+ signalId: signalForLoss.id,
48901
49284
  percentToClose,
48902
49285
  cost: percentToCloseCost(percentToClose, investedCostForLoss),
48903
49286
  currentPrice,
@@ -48971,6 +49354,7 @@ class BacktestUtils {
48971
49354
  }
48972
49355
  await Broker.commitPartialProfit({
48973
49356
  symbol,
49357
+ signalId: signalForProfitCost.id,
48974
49358
  percentToClose,
48975
49359
  cost: dollarAmount,
48976
49360
  currentPrice,
@@ -49044,6 +49428,7 @@ class BacktestUtils {
49044
49428
  }
49045
49429
  await Broker.commitPartialLoss({
49046
49430
  symbol,
49431
+ signalId: signalForLossCost.id,
49047
49432
  percentToClose,
49048
49433
  cost: dollarAmount,
49049
49434
  currentPrice,
@@ -49130,6 +49515,7 @@ class BacktestUtils {
49130
49515
  }
49131
49516
  await Broker.commitTrailingStop({
49132
49517
  symbol,
49518
+ signalId: signal.id,
49133
49519
  percentShift,
49134
49520
  currentPrice,
49135
49521
  newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
@@ -49215,6 +49601,7 @@ class BacktestUtils {
49215
49601
  }
49216
49602
  await Broker.commitTrailingTake({
49217
49603
  symbol,
49604
+ signalId: signal.id,
49218
49605
  percentShift,
49219
49606
  currentPrice,
49220
49607
  newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
@@ -49269,6 +49656,7 @@ class BacktestUtils {
49269
49656
  }
49270
49657
  await Broker.commitTrailingStop({
49271
49658
  symbol,
49659
+ signalId: signal.id,
49272
49660
  percentShift,
49273
49661
  currentPrice,
49274
49662
  newStopLossPrice,
@@ -49323,6 +49711,7 @@ class BacktestUtils {
49323
49711
  }
49324
49712
  await Broker.commitTrailingTake({
49325
49713
  symbol,
49714
+ signalId: signal.id,
49326
49715
  percentShift,
49327
49716
  currentPrice,
49328
49717
  newTakeProfitPrice,
@@ -49384,6 +49773,7 @@ class BacktestUtils {
49384
49773
  }
49385
49774
  await Broker.commitBreakeven({
49386
49775
  symbol,
49776
+ signalId: signal.id,
49387
49777
  currentPrice,
49388
49778
  newStopLossPrice: breakevenNewStopLossPrice(effectivePriceOpen),
49389
49779
  newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
@@ -49483,6 +49873,7 @@ class BacktestUtils {
49483
49873
  }
49484
49874
  await Broker.commitAverageBuy({
49485
49875
  symbol,
49876
+ signalId: signalForAvgBuy.id,
49486
49877
  currentPrice,
49487
49878
  cost,
49488
49879
  position: signalForAvgBuy.position,
@@ -51730,6 +52121,7 @@ class LiveUtils {
51730
52121
  }
51731
52122
  await Broker.commitPartialProfit({
51732
52123
  symbol,
52124
+ signalId: signalForProfit.id,
51733
52125
  percentToClose,
51734
52126
  cost: percentToCloseCost(percentToClose, investedCost),
51735
52127
  currentPrice,
@@ -51820,6 +52212,7 @@ class LiveUtils {
51820
52212
  }
51821
52213
  await Broker.commitPartialLoss({
51822
52214
  symbol,
52215
+ signalId: signalForLoss.id,
51823
52216
  percentToClose,
51824
52217
  cost: percentToCloseCost(percentToClose, investedCost),
51825
52218
  currentPrice,
@@ -51912,6 +52305,7 @@ class LiveUtils {
51912
52305
  }
51913
52306
  await Broker.commitPartialProfit({
51914
52307
  symbol,
52308
+ signalId: signalForProfitCost.id,
51915
52309
  percentToClose,
51916
52310
  cost: dollarAmount,
51917
52311
  currentPrice,
@@ -52004,6 +52398,7 @@ class LiveUtils {
52004
52398
  }
52005
52399
  await Broker.commitPartialLoss({
52006
52400
  symbol,
52401
+ signalId: signalForLossCost.id,
52007
52402
  percentToClose,
52008
52403
  cost: dollarAmount,
52009
52404
  currentPrice,
@@ -52109,6 +52504,7 @@ class LiveUtils {
52109
52504
  }
52110
52505
  await Broker.commitTrailingStop({
52111
52506
  symbol,
52507
+ signalId: signal.id,
52112
52508
  percentShift,
52113
52509
  currentPrice,
52114
52510
  newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
@@ -52209,6 +52605,7 @@ class LiveUtils {
52209
52605
  }
52210
52606
  await Broker.commitTrailingTake({
52211
52607
  symbol,
52608
+ signalId: signal.id,
52212
52609
  percentShift,
52213
52610
  currentPrice,
52214
52611
  newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
@@ -52279,6 +52676,7 @@ class LiveUtils {
52279
52676
  }
52280
52677
  await Broker.commitTrailingStop({
52281
52678
  symbol,
52679
+ signalId: signal.id,
52282
52680
  percentShift,
52283
52681
  currentPrice,
52284
52682
  newStopLossPrice,
@@ -52349,6 +52747,7 @@ class LiveUtils {
52349
52747
  }
52350
52748
  await Broker.commitTrailingTake({
52351
52749
  symbol,
52750
+ signalId: signal.id,
52352
52751
  percentShift,
52353
52752
  currentPrice,
52354
52753
  newTakeProfitPrice,
@@ -52426,6 +52825,7 @@ class LiveUtils {
52426
52825
  }
52427
52826
  await Broker.commitBreakeven({
52428
52827
  symbol,
52828
+ signalId: signal.id,
52429
52829
  currentPrice,
52430
52830
  newStopLossPrice: breakevenNewStopLossPrice(effectivePriceOpen),
52431
52831
  newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
@@ -52543,6 +52943,7 @@ class LiveUtils {
52543
52943
  }
52544
52944
  await Broker.commitAverageBuy({
52545
52945
  symbol,
52946
+ signalId: signalForAvgBuy.id,
52546
52947
  currentPrice,
52547
52948
  cost,
52548
52949
  position: signalForAvgBuy.position,
@@ -59964,6 +60365,7 @@ const SUBJECT_ISOLATION_LIST = [
59964
60365
  signalLiveEmitter,
59965
60366
  strategyCommitSubject,
59966
60367
  syncSubject,
60368
+ syncPendingSubject,
59967
60369
  validationSubject,
59968
60370
  signalNotifySubject,
59969
60371
  beforeStartSubject,