backtest-kit 3.8.1 → 4.0.1

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
@@ -7359,6 +7359,52 @@ class ClientStrategy {
7359
7359
  await PersistSignalAdapter.writeSignalData(this._pendingSignal, symbol, this.params.strategyName, this.params.exchangeName);
7360
7360
  // Commit will be emitted in tick() with correct currentTime
7361
7361
  }
7362
+ /**
7363
+ * Validates preconditions for partialProfit without mutating state.
7364
+ *
7365
+ * Returns false (never throws) when any condition would cause partialProfit to fail or skip.
7366
+ * Use this to pre-check before calling partialProfit to avoid needing to handle exceptions.
7367
+ *
7368
+ * @param symbol - Trading pair symbol
7369
+ * @param percentToClose - Percentage of position to close (0-100)
7370
+ * @param currentPrice - Current market price (must be in profit direction)
7371
+ * @returns boolean - true if partialProfit would execute, false otherwise
7372
+ */
7373
+ async validatePartialProfit(symbol, percentToClose, currentPrice) {
7374
+ this.params.logger.debug("ClientStrategy validatePartialProfit", {
7375
+ symbol,
7376
+ percentToClose,
7377
+ currentPrice,
7378
+ hasPendingSignal: this._pendingSignal !== null,
7379
+ });
7380
+ if (!this._pendingSignal)
7381
+ return false;
7382
+ if (typeof percentToClose !== "number" || !isFinite(percentToClose))
7383
+ return false;
7384
+ if (percentToClose <= 0)
7385
+ return false;
7386
+ if (percentToClose > 100)
7387
+ return false;
7388
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
7389
+ return false;
7390
+ const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7391
+ if (this._pendingSignal.position === "long" && currentPrice <= effectivePriceOpen)
7392
+ return false;
7393
+ if (this._pendingSignal.position === "short" && currentPrice >= effectivePriceOpen)
7394
+ return false;
7395
+ const effectiveTakeProfit = this._pendingSignal._trailingPriceTakeProfit ?? this._pendingSignal.priceTakeProfit;
7396
+ if (this._pendingSignal.position === "long" && currentPrice >= effectiveTakeProfit)
7397
+ return false;
7398
+ if (this._pendingSignal.position === "short" && currentPrice <= effectiveTakeProfit)
7399
+ return false;
7400
+ const { totalClosedPercent, remainingCostBasis } = getTotalClosed(this._pendingSignal);
7401
+ const totalInvested = (this._pendingSignal._entry ?? []).reduce((s, e) => s + e.cost, 0) || GLOBAL_CONFIG.CC_POSITION_ENTRY_COST;
7402
+ const newPartialDollar = (percentToClose / 100) * remainingCostBasis;
7403
+ const newTotalClosedDollar = (totalClosedPercent / 100) * totalInvested + newPartialDollar;
7404
+ if (newTotalClosedDollar > totalInvested)
7405
+ return false;
7406
+ return true;
7407
+ }
7362
7408
  /**
7363
7409
  * Executes partial close at profit level (moving toward TP).
7364
7410
  *
@@ -7494,6 +7540,52 @@ class ClientStrategy {
7494
7540
  });
7495
7541
  return true;
7496
7542
  }
7543
+ /**
7544
+ * Validates preconditions for partialLoss without mutating state.
7545
+ *
7546
+ * Returns false (never throws) when any condition would cause partialLoss to fail or skip.
7547
+ * Use this to pre-check before calling partialLoss to avoid needing to handle exceptions.
7548
+ *
7549
+ * @param symbol - Trading pair symbol
7550
+ * @param percentToClose - Percentage of position to close (0-100)
7551
+ * @param currentPrice - Current market price (must be in loss direction)
7552
+ * @returns boolean - true if partialLoss would execute, false otherwise
7553
+ */
7554
+ async validatePartialLoss(symbol, percentToClose, currentPrice) {
7555
+ this.params.logger.debug("ClientStrategy validatePartialLoss", {
7556
+ symbol,
7557
+ percentToClose,
7558
+ currentPrice,
7559
+ hasPendingSignal: this._pendingSignal !== null,
7560
+ });
7561
+ if (!this._pendingSignal)
7562
+ return false;
7563
+ if (typeof percentToClose !== "number" || !isFinite(percentToClose))
7564
+ return false;
7565
+ if (percentToClose <= 0)
7566
+ return false;
7567
+ if (percentToClose > 100)
7568
+ return false;
7569
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
7570
+ return false;
7571
+ const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7572
+ if (this._pendingSignal.position === "long" && currentPrice >= effectivePriceOpen)
7573
+ return false;
7574
+ if (this._pendingSignal.position === "short" && currentPrice <= effectivePriceOpen)
7575
+ return false;
7576
+ const effectiveStopLoss = this._pendingSignal._trailingPriceStopLoss ?? this._pendingSignal.priceStopLoss;
7577
+ if (this._pendingSignal.position === "long" && currentPrice <= effectiveStopLoss)
7578
+ return false;
7579
+ if (this._pendingSignal.position === "short" && currentPrice >= effectiveStopLoss)
7580
+ return false;
7581
+ const { totalClosedPercent, remainingCostBasis } = getTotalClosed(this._pendingSignal);
7582
+ const totalInvested = (this._pendingSignal._entry ?? []).reduce((s, e) => s + e.cost, 0) || GLOBAL_CONFIG.CC_POSITION_ENTRY_COST;
7583
+ const newPartialDollar = (percentToClose / 100) * remainingCostBasis;
7584
+ const newTotalClosedDollar = (totalClosedPercent / 100) * totalInvested + newPartialDollar;
7585
+ if (newTotalClosedDollar > totalInvested)
7586
+ return false;
7587
+ return true;
7588
+ }
7497
7589
  /**
7498
7590
  * Executes partial close at loss level (moving toward SL).
7499
7591
  *
@@ -7629,6 +7721,82 @@ class ClientStrategy {
7629
7721
  });
7630
7722
  return true;
7631
7723
  }
7724
+ /**
7725
+ * Validates preconditions for breakeven without mutating state.
7726
+ *
7727
+ * Returns false (never throws) when any condition would cause breakeven to fail or skip.
7728
+ * Mirrors the full BREAKEVEN_FN condition logic including threshold, trailing state and intrusion checks.
7729
+ *
7730
+ * @param symbol - Trading pair symbol
7731
+ * @param currentPrice - Current market price to check threshold
7732
+ * @returns boolean - true if breakeven would execute, false otherwise
7733
+ */
7734
+ async validateBreakeven(symbol, currentPrice) {
7735
+ this.params.logger.debug("ClientStrategy validateBreakeven", {
7736
+ symbol,
7737
+ currentPrice,
7738
+ hasPendingSignal: this._pendingSignal !== null,
7739
+ });
7740
+ if (!this._pendingSignal)
7741
+ return false;
7742
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
7743
+ return false;
7744
+ const signal = this._pendingSignal;
7745
+ const breakevenPrice = getEffectivePriceOpen(signal);
7746
+ const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
7747
+ if (signal.position === "long" && breakevenPrice >= effectiveTakeProfit)
7748
+ return false;
7749
+ if (signal.position === "short" && breakevenPrice <= effectiveTakeProfit)
7750
+ return false;
7751
+ const breakevenThresholdPercent = (GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE + GLOBAL_CONFIG.CC_PERCENT_FEE) * 2;
7752
+ if (signal._trailingPriceStopLoss !== undefined) {
7753
+ const trailingStopLoss = signal._trailingPriceStopLoss;
7754
+ if (signal.position === "long") {
7755
+ const isPositiveTrailing = trailingStopLoss > breakevenPrice;
7756
+ if (isPositiveTrailing)
7757
+ return true; // already protecting profit
7758
+ const thresholdPrice = breakevenPrice * (1 + breakevenThresholdPercent / 100);
7759
+ const isThresholdReached = currentPrice >= thresholdPrice;
7760
+ if (!isThresholdReached || breakevenPrice <= trailingStopLoss)
7761
+ return false;
7762
+ if (currentPrice < breakevenPrice)
7763
+ return false; // price intrusion
7764
+ return true;
7765
+ }
7766
+ else {
7767
+ const isPositiveTrailing = trailingStopLoss < breakevenPrice;
7768
+ if (isPositiveTrailing)
7769
+ return true; // already protecting profit
7770
+ const thresholdPrice = breakevenPrice * (1 - breakevenThresholdPercent / 100);
7771
+ const isThresholdReached = currentPrice <= thresholdPrice;
7772
+ if (!isThresholdReached || breakevenPrice >= trailingStopLoss)
7773
+ return false;
7774
+ if (currentPrice > breakevenPrice)
7775
+ return false; // price intrusion
7776
+ return true;
7777
+ }
7778
+ }
7779
+ const currentStopLoss = signal.priceStopLoss;
7780
+ if (signal.position === "long") {
7781
+ const thresholdPrice = breakevenPrice * (1 + breakevenThresholdPercent / 100);
7782
+ const isThresholdReached = currentPrice >= thresholdPrice;
7783
+ const canMove = isThresholdReached && currentStopLoss < breakevenPrice;
7784
+ if (!canMove)
7785
+ return false;
7786
+ if (currentPrice < breakevenPrice)
7787
+ return false;
7788
+ }
7789
+ else {
7790
+ const thresholdPrice = breakevenPrice * (1 - breakevenThresholdPercent / 100);
7791
+ const isThresholdReached = currentPrice <= thresholdPrice;
7792
+ const canMove = isThresholdReached && currentStopLoss > breakevenPrice;
7793
+ if (!canMove)
7794
+ return false;
7795
+ if (currentPrice > breakevenPrice)
7796
+ return false;
7797
+ }
7798
+ return true;
7799
+ }
7632
7800
  /**
7633
7801
  * Moves stop-loss to breakeven (entry price) when price reaches threshold.
7634
7802
  *
@@ -7751,6 +7919,65 @@ class ClientStrategy {
7751
7919
  });
7752
7920
  return true;
7753
7921
  }
7922
+ /**
7923
+ * Validates preconditions for trailingStop without mutating state.
7924
+ *
7925
+ * Returns false (never throws) when any condition would cause trailingStop to fail or skip.
7926
+ * Includes absorption check: returns false if new SL would not improve on the current trailing SL.
7927
+ *
7928
+ * @param symbol - Trading pair symbol
7929
+ * @param percentShift - Percentage shift of ORIGINAL SL distance [-100, 100], excluding 0
7930
+ * @param currentPrice - Current market price to check for intrusion
7931
+ * @returns boolean - true if trailingStop would execute, false otherwise
7932
+ */
7933
+ async validateTrailingStop(symbol, percentShift, currentPrice) {
7934
+ this.params.logger.debug("ClientStrategy validateTrailingStop", {
7935
+ symbol,
7936
+ percentShift,
7937
+ currentPrice,
7938
+ hasPendingSignal: this._pendingSignal !== null,
7939
+ });
7940
+ if (!this._pendingSignal)
7941
+ return false;
7942
+ if (typeof percentShift !== "number" || !isFinite(percentShift))
7943
+ return false;
7944
+ if (percentShift < -100 || percentShift > 100)
7945
+ return false;
7946
+ if (percentShift === 0)
7947
+ return false;
7948
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
7949
+ return false;
7950
+ const signal = this._pendingSignal;
7951
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
7952
+ const slDistancePercent = Math.abs((effectivePriceOpen - signal.priceStopLoss) / effectivePriceOpen * 100);
7953
+ const newSlDistancePercent = slDistancePercent + percentShift;
7954
+ let newStopLoss;
7955
+ if (signal.position === "long") {
7956
+ newStopLoss = effectivePriceOpen * (1 - newSlDistancePercent / 100);
7957
+ }
7958
+ else {
7959
+ newStopLoss = effectivePriceOpen * (1 + newSlDistancePercent / 100);
7960
+ }
7961
+ // Intrusion check (mirrors trailingStop method: applied before TRAILING_STOP_LOSS_FN, for all calls)
7962
+ if (signal.position === "long" && currentPrice < newStopLoss)
7963
+ return false;
7964
+ if (signal.position === "short" && currentPrice > newStopLoss)
7965
+ return false;
7966
+ const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
7967
+ if (signal.position === "long" && newStopLoss >= effectiveTakeProfit)
7968
+ return false;
7969
+ if (signal.position === "short" && newStopLoss <= effectiveTakeProfit)
7970
+ return false;
7971
+ // Absorption check (mirrors TRAILING_STOP_LOSS_FN: first call is unconditional)
7972
+ const currentTrailingSL = signal._trailingPriceStopLoss;
7973
+ if (currentTrailingSL !== undefined) {
7974
+ if (signal.position === "long" && newStopLoss <= currentTrailingSL)
7975
+ return false;
7976
+ if (signal.position === "short" && newStopLoss >= currentTrailingSL)
7977
+ return false;
7978
+ }
7979
+ return true;
7980
+ }
7754
7981
  /**
7755
7982
  * Adjusts trailing stop-loss by shifting distance between entry and original SL.
7756
7983
  *
@@ -7939,6 +8166,65 @@ class ClientStrategy {
7939
8166
  });
7940
8167
  return true;
7941
8168
  }
8169
+ /**
8170
+ * Validates preconditions for trailingTake without mutating state.
8171
+ *
8172
+ * Returns false (never throws) when any condition would cause trailingTake to fail or skip.
8173
+ * Includes absorption check: returns false if new TP would not improve on the current trailing TP.
8174
+ *
8175
+ * @param symbol - Trading pair symbol
8176
+ * @param percentShift - Percentage adjustment to ORIGINAL TP distance (-100 to 100)
8177
+ * @param currentPrice - Current market price to check for intrusion
8178
+ * @returns boolean - true if trailingTake would execute, false otherwise
8179
+ */
8180
+ async validateTrailingTake(symbol, percentShift, currentPrice) {
8181
+ this.params.logger.debug("ClientStrategy validateTrailingTake", {
8182
+ symbol,
8183
+ percentShift,
8184
+ currentPrice,
8185
+ hasPendingSignal: this._pendingSignal !== null,
8186
+ });
8187
+ if (!this._pendingSignal)
8188
+ return false;
8189
+ if (typeof percentShift !== "number" || !isFinite(percentShift))
8190
+ return false;
8191
+ if (percentShift < -100 || percentShift > 100)
8192
+ return false;
8193
+ if (percentShift === 0)
8194
+ return false;
8195
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
8196
+ return false;
8197
+ const signal = this._pendingSignal;
8198
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
8199
+ const tpDistancePercent = Math.abs((signal.priceTakeProfit - effectivePriceOpen) / effectivePriceOpen * 100);
8200
+ const newTpDistancePercent = tpDistancePercent + percentShift;
8201
+ let newTakeProfit;
8202
+ if (signal.position === "long") {
8203
+ newTakeProfit = effectivePriceOpen * (1 + newTpDistancePercent / 100);
8204
+ }
8205
+ else {
8206
+ newTakeProfit = effectivePriceOpen * (1 - newTpDistancePercent / 100);
8207
+ }
8208
+ // Intrusion check (mirrors trailingTake method: applied before TRAILING_TAKE_PROFIT_FN, for all calls)
8209
+ if (signal.position === "long" && currentPrice > newTakeProfit)
8210
+ return false;
8211
+ if (signal.position === "short" && currentPrice < newTakeProfit)
8212
+ return false;
8213
+ const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
8214
+ if (signal.position === "long" && newTakeProfit <= effectiveStopLoss)
8215
+ return false;
8216
+ if (signal.position === "short" && newTakeProfit >= effectiveStopLoss)
8217
+ return false;
8218
+ // Absorption check (mirrors TRAILING_TAKE_PROFIT_FN: first call is unconditional)
8219
+ const currentTrailingTP = signal._trailingPriceTakeProfit;
8220
+ if (currentTrailingTP !== undefined) {
8221
+ if (signal.position === "long" && newTakeProfit >= currentTrailingTP)
8222
+ return false;
8223
+ if (signal.position === "short" && newTakeProfit <= currentTrailingTP)
8224
+ return false;
8225
+ }
8226
+ return true;
8227
+ }
7942
8228
  /**
7943
8229
  * Adjusts the trailing take-profit distance for an active pending signal.
7944
8230
  *
@@ -8113,6 +8399,41 @@ class ClientStrategy {
8113
8399
  });
8114
8400
  return true;
8115
8401
  }
8402
+ /**
8403
+ * Validates preconditions for averageBuy without mutating state.
8404
+ *
8405
+ * Returns false (never throws) when any condition would cause averageBuy to fail or skip.
8406
+ *
8407
+ * @param symbol - Trading pair symbol
8408
+ * @param currentPrice - New entry price to add
8409
+ * @returns boolean - true if averageBuy would execute, false otherwise
8410
+ */
8411
+ async validateAverageBuy(symbol, currentPrice) {
8412
+ this.params.logger.debug("ClientStrategy validateAverageBuy", {
8413
+ symbol,
8414
+ currentPrice,
8415
+ hasPendingSignal: this._pendingSignal !== null,
8416
+ });
8417
+ if (!this._pendingSignal)
8418
+ return false;
8419
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
8420
+ return false;
8421
+ const signal = this._pendingSignal;
8422
+ const entries = (!signal._entry || signal._entry.length === 0)
8423
+ ? [{ price: signal.priceOpen, cost: GLOBAL_CONFIG.CC_POSITION_ENTRY_COST }]
8424
+ : signal._entry;
8425
+ if (signal.position === "long") {
8426
+ const minEntryPrice = Math.min(...entries.map((e) => e.price));
8427
+ if (!GLOBAL_CONFIG.CC_ENABLE_DCA_EVERYWHERE && currentPrice >= minEntryPrice)
8428
+ return false;
8429
+ }
8430
+ else {
8431
+ const maxEntryPrice = Math.max(...entries.map((e) => e.price));
8432
+ if (!GLOBAL_CONFIG.CC_ENABLE_DCA_EVERYWHERE && currentPrice <= maxEntryPrice)
8433
+ return false;
8434
+ }
8435
+ return true;
8436
+ }
8116
8437
  /**
8117
8438
  * Adds a new averaging entry to an open position (DCA — Dollar Cost Averaging).
8118
8439
  *
@@ -8498,12 +8819,22 @@ const Risk = new RiskUtils();
8498
8819
  */
8499
8820
  const CREATE_SYNC_FN = (self, strategyName, exchangeName, frameName, backtest) => trycatch(async (event) => {
8500
8821
  if (event.backtest) {
8501
- return;
8822
+ return true;
8502
8823
  }
8503
8824
  await syncSubject.next(event);
8504
8825
  await self.actionCoreService.signalSync(backtest, event, { strategyName, exchangeName, frameName });
8505
8826
  return true;
8506
8827
  }, {
8828
+ fallback: (error) => {
8829
+ const message = "StrategyConnectionService CREATE_SYNC_FN thrown. Broker rejected order request";
8830
+ const payload = {
8831
+ error: errorData(error),
8832
+ message: getErrorMessage(error),
8833
+ };
8834
+ self.loggerService.warn(message, payload);
8835
+ console.error(message, payload);
8836
+ errorEmitter.next(error);
8837
+ },
8507
8838
  defaultValue: false,
8508
8839
  });
8509
8840
  /**
@@ -9204,6 +9535,21 @@ class StrategyConnectionService {
9204
9535
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9205
9536
  await strategy.closePending(symbol, backtest, closeId);
9206
9537
  };
9538
+ /**
9539
+ * Checks whether `partialProfit` would succeed without executing it.
9540
+ * Delegates to `ClientStrategy.validatePartialProfit()` — no throws, pure boolean result.
9541
+ *
9542
+ * @param backtest - Whether running in backtest mode
9543
+ * @param symbol - Trading pair symbol
9544
+ * @param percentToClose - Percentage of position to check (0-100]
9545
+ * @param currentPrice - Current market price to validate against
9546
+ * @param context - Execution context with strategyName, exchangeName, frameName
9547
+ * @returns Promise<boolean> - true if `partialProfit` would execute, false otherwise
9548
+ */
9549
+ this.validatePartialProfit = (backtest, symbol, percentToClose, currentPrice, context) => {
9550
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9551
+ return Promise.resolve(strategy.validatePartialProfit(symbol, percentToClose, currentPrice));
9552
+ };
9207
9553
  /**
9208
9554
  * Executes partial close at profit level (moving toward TP).
9209
9555
  *
@@ -9245,6 +9591,21 @@ class StrategyConnectionService {
9245
9591
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9246
9592
  return await strategy.partialProfit(symbol, percentToClose, currentPrice, backtest);
9247
9593
  };
9594
+ /**
9595
+ * Checks whether `partialLoss` would succeed without executing it.
9596
+ * Delegates to `ClientStrategy.validatePartialLoss()` — no throws, pure boolean result.
9597
+ *
9598
+ * @param backtest - Whether running in backtest mode
9599
+ * @param symbol - Trading pair symbol
9600
+ * @param percentToClose - Percentage of position to check (0-100]
9601
+ * @param currentPrice - Current market price to validate against
9602
+ * @param context - Execution context with strategyName, exchangeName, frameName
9603
+ * @returns Promise<boolean> - true if `partialLoss` would execute, false otherwise
9604
+ */
9605
+ this.validatePartialLoss = (backtest, symbol, percentToClose, currentPrice, context) => {
9606
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9607
+ return Promise.resolve(strategy.validatePartialLoss(symbol, percentToClose, currentPrice));
9608
+ };
9248
9609
  /**
9249
9610
  * Executes partial close at loss level (moving toward SL).
9250
9611
  *
@@ -9286,6 +9647,21 @@ class StrategyConnectionService {
9286
9647
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9287
9648
  return await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest);
9288
9649
  };
9650
+ /**
9651
+ * Checks whether `trailingStop` would succeed without executing it.
9652
+ * Delegates to `ClientStrategy.validateTrailingStop()` — no throws, pure boolean result.
9653
+ *
9654
+ * @param backtest - Whether running in backtest mode
9655
+ * @param symbol - Trading pair symbol
9656
+ * @param percentShift - Percentage shift of ORIGINAL SL distance [-100, 100], excluding 0
9657
+ * @param currentPrice - Current market price to validate against
9658
+ * @param context - Execution context with strategyName, exchangeName, frameName
9659
+ * @returns Promise<boolean> - true if `trailingStop` would execute, false otherwise
9660
+ */
9661
+ this.validateTrailingStop = (backtest, symbol, percentShift, currentPrice, context) => {
9662
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9663
+ return Promise.resolve(strategy.validateTrailingStop(symbol, percentShift, currentPrice));
9664
+ };
9289
9665
  /**
9290
9666
  * Adjusts the trailing stop-loss distance for an active pending signal.
9291
9667
  *
@@ -9325,6 +9701,21 @@ class StrategyConnectionService {
9325
9701
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9326
9702
  return await strategy.trailingStop(symbol, percentShift, currentPrice, backtest);
9327
9703
  };
9704
+ /**
9705
+ * Checks whether `trailingTake` would succeed without executing it.
9706
+ * Delegates to `ClientStrategy.validateTrailingTake()` — no throws, pure boolean result.
9707
+ *
9708
+ * @param backtest - Whether running in backtest mode
9709
+ * @param symbol - Trading pair symbol
9710
+ * @param percentShift - Percentage adjustment to ORIGINAL TP distance [-100, 100], excluding 0
9711
+ * @param currentPrice - Current market price to validate against
9712
+ * @param context - Execution context with strategyName, exchangeName, frameName
9713
+ * @returns Promise<boolean> - true if `trailingTake` would execute, false otherwise
9714
+ */
9715
+ this.validateTrailingTake = (backtest, symbol, percentShift, currentPrice, context) => {
9716
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9717
+ return Promise.resolve(strategy.validateTrailingTake(symbol, percentShift, currentPrice));
9718
+ };
9328
9719
  /**
9329
9720
  * Adjusts the trailing take-profit distance for an active pending signal.
9330
9721
  *
@@ -9364,6 +9755,20 @@ class StrategyConnectionService {
9364
9755
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9365
9756
  return await strategy.trailingTake(symbol, percentShift, currentPrice, backtest);
9366
9757
  };
9758
+ /**
9759
+ * Checks whether `breakeven` would succeed without executing it.
9760
+ * Delegates to `ClientStrategy.validateBreakeven()` — no throws, pure boolean result.
9761
+ *
9762
+ * @param backtest - Whether running in backtest mode
9763
+ * @param symbol - Trading pair symbol
9764
+ * @param currentPrice - Current market price to validate against
9765
+ * @param context - Execution context with strategyName, exchangeName, frameName
9766
+ * @returns Promise<boolean> - true if `breakeven` would execute, false otherwise
9767
+ */
9768
+ this.validateBreakeven = (backtest, symbol, currentPrice, context) => {
9769
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9770
+ return Promise.resolve(strategy.validateBreakeven(symbol, currentPrice));
9771
+ };
9367
9772
  /**
9368
9773
  * Delegates to ClientStrategy.breakeven() with current execution context.
9369
9774
  *
@@ -9429,6 +9834,20 @@ class StrategyConnectionService {
9429
9834
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9430
9835
  return await strategy.activateScheduled(symbol, backtest, activateId);
9431
9836
  };
9837
+ /**
9838
+ * Checks whether `averageBuy` would succeed without executing it.
9839
+ * Delegates to `ClientStrategy.validateAverageBuy()` — no throws, pure boolean result.
9840
+ *
9841
+ * @param backtest - Whether running in backtest mode
9842
+ * @param symbol - Trading pair symbol
9843
+ * @param currentPrice - New entry price to validate
9844
+ * @param context - Execution context with strategyName, exchangeName, frameName
9845
+ * @returns Promise<boolean> - true if `averageBuy` would execute, false otherwise
9846
+ */
9847
+ this.validateAverageBuy = (backtest, symbol, currentPrice, context) => {
9848
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9849
+ return Promise.resolve(strategy.validateAverageBuy(symbol, currentPrice));
9850
+ };
9432
9851
  /**
9433
9852
  * Adds a new DCA entry to the active pending signal.
9434
9853
  *
@@ -12787,6 +13206,28 @@ class StrategyCoreService {
12787
13206
  }
12788
13207
  return await this.strategyConnectionService.clear(payload);
12789
13208
  };
13209
+ /**
13210
+ * Checks whether `partialProfit` would succeed without executing it.
13211
+ * Validates context, then delegates to StrategyConnectionService.validatePartialProfit().
13212
+ *
13213
+ * @param backtest - Whether running in backtest mode
13214
+ * @param symbol - Trading pair symbol
13215
+ * @param percentToClose - Percentage of position to check (0-100]
13216
+ * @param currentPrice - Current market price to validate against
13217
+ * @param context - Execution context with strategyName, exchangeName, frameName
13218
+ * @returns Promise<boolean> - true if `partialProfit` would execute, false otherwise
13219
+ */
13220
+ this.validatePartialProfit = async (backtest, symbol, percentToClose, currentPrice, context) => {
13221
+ this.loggerService.log("strategyCoreService validatePartialProfit", {
13222
+ symbol,
13223
+ percentToClose,
13224
+ currentPrice,
13225
+ context,
13226
+ backtest,
13227
+ });
13228
+ await this.validate(context);
13229
+ return await this.strategyConnectionService.validatePartialProfit(backtest, symbol, percentToClose, currentPrice, context);
13230
+ };
12790
13231
  /**
12791
13232
  * Executes partial close at profit level (moving toward TP).
12792
13233
  *
@@ -12828,6 +13269,28 @@ class StrategyCoreService {
12828
13269
  await this.validate(context);
12829
13270
  return await this.strategyConnectionService.partialProfit(backtest, symbol, percentToClose, currentPrice, context);
12830
13271
  };
13272
+ /**
13273
+ * Checks whether `partialLoss` would succeed without executing it.
13274
+ * Validates context, then delegates to StrategyConnectionService.validatePartialLoss().
13275
+ *
13276
+ * @param backtest - Whether running in backtest mode
13277
+ * @param symbol - Trading pair symbol
13278
+ * @param percentToClose - Percentage of position to check (0-100]
13279
+ * @param currentPrice - Current market price to validate against
13280
+ * @param context - Execution context with strategyName, exchangeName, frameName
13281
+ * @returns Promise<boolean> - true if `partialLoss` would execute, false otherwise
13282
+ */
13283
+ this.validatePartialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
13284
+ this.loggerService.log("strategyCoreService validatePartialLoss", {
13285
+ symbol,
13286
+ percentToClose,
13287
+ currentPrice,
13288
+ context,
13289
+ backtest,
13290
+ });
13291
+ await this.validate(context);
13292
+ return await this.strategyConnectionService.validatePartialLoss(backtest, symbol, percentToClose, currentPrice, context);
13293
+ };
12831
13294
  /**
12832
13295
  * Executes partial close at loss level (moving toward SL).
12833
13296
  *
@@ -12897,6 +13360,28 @@ class StrategyCoreService {
12897
13360
  * );
12898
13361
  * ```
12899
13362
  */
13363
+ this.validateTrailingStop = async (backtest, symbol, percentShift, currentPrice, context) => {
13364
+ this.loggerService.log("strategyCoreService validateTrailingStop", {
13365
+ symbol,
13366
+ percentShift,
13367
+ currentPrice,
13368
+ context,
13369
+ backtest,
13370
+ });
13371
+ await this.validate(context);
13372
+ return await this.strategyConnectionService.validateTrailingStop(backtest, symbol, percentShift, currentPrice, context);
13373
+ };
13374
+ /**
13375
+ * Checks whether `trailingStop` would succeed without executing it.
13376
+ * Validates context, then delegates to StrategyConnectionService.validateTrailingStop().
13377
+ *
13378
+ * @param backtest - Whether running in backtest mode
13379
+ * @param symbol - Trading pair symbol
13380
+ * @param percentShift - Percentage shift of ORIGINAL SL distance [-100, 100], excluding 0
13381
+ * @param currentPrice - Current market price to validate against
13382
+ * @param context - Execution context with strategyName, exchangeName, frameName
13383
+ * @returns Promise<boolean> - true if `trailingStop` would execute, false otherwise
13384
+ */
12900
13385
  this.trailingStop = async (backtest, symbol, percentShift, currentPrice, context) => {
12901
13386
  this.loggerService.log("strategyCoreService trailingStop", {
12902
13387
  symbol,
@@ -12932,6 +13417,28 @@ class StrategyCoreService {
12932
13417
  * );
12933
13418
  * ```
12934
13419
  */
13420
+ this.validateTrailingTake = async (backtest, symbol, percentShift, currentPrice, context) => {
13421
+ this.loggerService.log("strategyCoreService validateTrailingTake", {
13422
+ symbol,
13423
+ percentShift,
13424
+ currentPrice,
13425
+ context,
13426
+ backtest,
13427
+ });
13428
+ await this.validate(context);
13429
+ return await this.strategyConnectionService.validateTrailingTake(backtest, symbol, percentShift, currentPrice, context);
13430
+ };
13431
+ /**
13432
+ * Checks whether `trailingTake` would succeed without executing it.
13433
+ * Validates context, then delegates to StrategyConnectionService.validateTrailingTake().
13434
+ *
13435
+ * @param backtest - Whether running in backtest mode
13436
+ * @param symbol - Trading pair symbol
13437
+ * @param percentShift - Percentage adjustment to ORIGINAL TP distance [-100, 100], excluding 0
13438
+ * @param currentPrice - Current market price to validate against
13439
+ * @param context - Execution context with strategyName, exchangeName, frameName
13440
+ * @returns Promise<boolean> - true if `trailingTake` would execute, false otherwise
13441
+ */
12935
13442
  this.trailingTake = async (backtest, symbol, percentShift, currentPrice, context) => {
12936
13443
  this.loggerService.log("strategyCoreService trailingTake", {
12937
13444
  symbol,
@@ -12963,6 +13470,26 @@ class StrategyCoreService {
12963
13470
  * );
12964
13471
  * ```
12965
13472
  */
13473
+ this.validateBreakeven = async (backtest, symbol, currentPrice, context) => {
13474
+ this.loggerService.log("strategyCoreService validateBreakeven", {
13475
+ symbol,
13476
+ currentPrice,
13477
+ context,
13478
+ backtest,
13479
+ });
13480
+ await this.validate(context);
13481
+ return await this.strategyConnectionService.validateBreakeven(backtest, symbol, currentPrice, context);
13482
+ };
13483
+ /**
13484
+ * Checks whether `breakeven` would succeed without executing it.
13485
+ * Validates context, then delegates to StrategyConnectionService.validateBreakeven().
13486
+ *
13487
+ * @param backtest - Whether running in backtest mode
13488
+ * @param symbol - Trading pair symbol
13489
+ * @param currentPrice - Current market price to validate against
13490
+ * @param context - Execution context with strategyName, exchangeName, frameName
13491
+ * @returns Promise<boolean> - true if `breakeven` would execute, false otherwise
13492
+ */
12966
13493
  this.breakeven = async (backtest, symbol, currentPrice, context) => {
12967
13494
  this.loggerService.log("strategyCoreService breakeven", {
12968
13495
  symbol,
@@ -13018,6 +13545,26 @@ class StrategyCoreService {
13018
13545
  * @param context - Execution context with strategyName, exchangeName, frameName
13019
13546
  * @returns Promise<boolean> - true if entry added, false if rejected
13020
13547
  */
13548
+ this.validateAverageBuy = async (backtest, symbol, currentPrice, context) => {
13549
+ this.loggerService.log("strategyCoreService validateAverageBuy", {
13550
+ symbol,
13551
+ currentPrice,
13552
+ context,
13553
+ backtest,
13554
+ });
13555
+ await this.validate(context);
13556
+ return await this.strategyConnectionService.validateAverageBuy(backtest, symbol, currentPrice, context);
13557
+ };
13558
+ /**
13559
+ * Checks whether `averageBuy` would succeed without executing it.
13560
+ * Validates context, then delegates to StrategyConnectionService.validateAverageBuy().
13561
+ *
13562
+ * @param backtest - Whether running in backtest mode
13563
+ * @param symbol - Trading pair symbol
13564
+ * @param currentPrice - New entry price to validate
13565
+ * @param context - Execution context with strategyName, exchangeName, frameName
13566
+ * @returns Promise<boolean> - true if `averageBuy` would execute, false otherwise
13567
+ */
13021
13568
  this.averageBuy = async (backtest, symbol, currentPrice, context, cost) => {
13022
13569
  this.loggerService.log("strategyCoreService averageBuy", {
13023
13570
  symbol,
@@ -30137,6 +30684,1026 @@ const investedCostToPercent = (dollarAmount, investedCost) => {
30137
30684
  return (dollarAmount / investedCost) * 100;
30138
30685
  };
30139
30686
 
30687
+ /**
30688
+ * Convert an absolute stop-loss price to a percentShift for `commitTrailingStop`.
30689
+ *
30690
+ * percentShift = newSlDistancePercent - originalSlDistancePercent
30691
+ * where distance = Math.abs((effectivePriceOpen - slPrice) / effectivePriceOpen * 100)
30692
+ *
30693
+ * @param newStopLossPrice - Desired absolute stop-loss price
30694
+ * @param originalStopLossPrice - Original stop-loss price from the pending signal
30695
+ * @param effectivePriceOpen - Effective entry price (from `getPositionAveragePrice`)
30696
+ * @returns percentShift to pass to `commitTrailingStop`
30697
+ *
30698
+ * @example
30699
+ * // LONG: entry=100, originalSL=90, desired newSL=95
30700
+ * const shift = slPriceToPercentShift(95, 90, 100); // -5
30701
+ * await commitTrailingStop("BTCUSDT", shift, currentPrice);
30702
+ */
30703
+ const slPriceToPercentShift = (newStopLossPrice, originalStopLossPrice, effectivePriceOpen) => {
30704
+ const originalSlDistancePercent = Math.abs((effectivePriceOpen - originalStopLossPrice) / effectivePriceOpen * 100);
30705
+ const newSlDistancePercent = Math.abs((effectivePriceOpen - newStopLossPrice) / effectivePriceOpen * 100);
30706
+ return newSlDistancePercent - originalSlDistancePercent;
30707
+ };
30708
+
30709
+ /**
30710
+ * Convert an absolute take-profit price to a percentShift for `commitTrailingTake`.
30711
+ *
30712
+ * percentShift = newTpDistancePercent - originalTpDistancePercent
30713
+ * where distance = Math.abs((tpPrice - effectivePriceOpen) / effectivePriceOpen * 100)
30714
+ *
30715
+ * @param newTakeProfitPrice - Desired absolute take-profit price
30716
+ * @param originalTakeProfitPrice - Original take-profit price from the pending signal
30717
+ * @param effectivePriceOpen - Effective entry price (from `getPositionAveragePrice`)
30718
+ * @returns percentShift to pass to `commitTrailingTake`
30719
+ *
30720
+ * @example
30721
+ * // LONG: entry=100, originalTP=110, desired newTP=107
30722
+ * const shift = tpPriceToPercentShift(107, 110, 100); // -3
30723
+ * await commitTrailingTake("BTCUSDT", shift, currentPrice);
30724
+ */
30725
+ const tpPriceToPercentShift = (newTakeProfitPrice, originalTakeProfitPrice, effectivePriceOpen) => {
30726
+ const originalTpDistancePercent = Math.abs((originalTakeProfitPrice - effectivePriceOpen) / effectivePriceOpen * 100);
30727
+ const newTpDistancePercent = Math.abs((newTakeProfitPrice - effectivePriceOpen) / effectivePriceOpen * 100);
30728
+ return newTpDistancePercent - originalTpDistancePercent;
30729
+ };
30730
+
30731
+ /**
30732
+ * Convert a percentShift for `commitTrailingStop` back to an absolute stop-loss price.
30733
+ *
30734
+ * Inverse of `slPriceToPercentShift`.
30735
+ *
30736
+ * newSlDistancePercent = originalSlDistancePercent + percentShift
30737
+ * LONG: newStopLossPrice = effectivePriceOpen * (1 - newSlDistancePercent / 100)
30738
+ * SHORT: newStopLossPrice = effectivePriceOpen * (1 + newSlDistancePercent / 100)
30739
+ *
30740
+ * @param percentShift - Value returned by `slPriceToPercentShift` (or passed to `commitTrailingStop`)
30741
+ * @param originalStopLossPrice - Original stop-loss price from the pending signal
30742
+ * @param effectivePriceOpen - Effective entry price (from `getPositionAveragePrice`)
30743
+ * @param position - Position direction: "long" or "short"
30744
+ * @returns Absolute stop-loss price corresponding to the given percentShift
30745
+ *
30746
+ * @example
30747
+ * // LONG: entry=100, originalSL=90, percentShift=-5
30748
+ * const price = slPercentShiftToPrice(-5, 90, 100, "long"); // 95
30749
+ */
30750
+ const slPercentShiftToPrice = (percentShift, originalStopLossPrice, effectivePriceOpen, position) => {
30751
+ const originalSlDistancePercent = Math.abs((effectivePriceOpen - originalStopLossPrice) / effectivePriceOpen * 100);
30752
+ const newSlDistancePercent = originalSlDistancePercent + percentShift;
30753
+ return position === "long"
30754
+ ? effectivePriceOpen * (1 - newSlDistancePercent / 100)
30755
+ : effectivePriceOpen * (1 + newSlDistancePercent / 100);
30756
+ };
30757
+
30758
+ /**
30759
+ * Convert a percentShift for `commitTrailingTake` back to an absolute take-profit price.
30760
+ *
30761
+ * Inverse of `tpPriceToPercentShift`.
30762
+ *
30763
+ * newTpDistancePercent = originalTpDistancePercent + percentShift
30764
+ * LONG: newTakeProfitPrice = effectivePriceOpen * (1 + newTpDistancePercent / 100)
30765
+ * SHORT: newTakeProfitPrice = effectivePriceOpen * (1 - newTpDistancePercent / 100)
30766
+ *
30767
+ * @param percentShift - Value returned by `tpPriceToPercentShift` (or passed to `commitTrailingTake`)
30768
+ * @param originalTakeProfitPrice - Original take-profit price from the pending signal
30769
+ * @param effectivePriceOpen - Effective entry price (from `getPositionAveragePrice`)
30770
+ * @param position - Position direction: "long" or "short"
30771
+ * @returns Absolute take-profit price corresponding to the given percentShift
30772
+ *
30773
+ * @example
30774
+ * // LONG: entry=100, originalTP=110, percentShift=-3
30775
+ * const price = tpPercentShiftToPrice(-3, 110, 100, "long"); // 107
30776
+ */
30777
+ const tpPercentShiftToPrice = (percentShift, originalTakeProfitPrice, effectivePriceOpen, position) => {
30778
+ const originalTpDistancePercent = Math.abs((originalTakeProfitPrice - effectivePriceOpen) / effectivePriceOpen * 100);
30779
+ const newTpDistancePercent = originalTpDistancePercent + percentShift;
30780
+ return position === "long"
30781
+ ? effectivePriceOpen * (1 + newTpDistancePercent / 100)
30782
+ : effectivePriceOpen * (1 - newTpDistancePercent / 100);
30783
+ };
30784
+
30785
+ /**
30786
+ * Compute the dollar cost of a partial close from percentToClose and current invested cost basis.
30787
+ *
30788
+ * cost = (percentToClose / 100) * investedCost
30789
+ *
30790
+ * @param percentToClose - Percentage of position to close (0–100)
30791
+ * @param investedCost - Current invested cost basis (from `getPositionInvestedCost`)
30792
+ * @returns Dollar amount that will be closed
30793
+ *
30794
+ * @example
30795
+ * // Position investedCost=$1000, closing 25%
30796
+ * const cost = percentToCloseCost(25, 1000); // 250
30797
+ */
30798
+ const percentToCloseCost = (percentToClose, investedCost) => {
30799
+ return (percentToClose / 100) * investedCost;
30800
+ };
30801
+
30802
+ /**
30803
+ * Compute the new stop-loss price for a breakeven operation.
30804
+ *
30805
+ * Breakeven moves the SL to the effective entry price (effectivePriceOpen).
30806
+ * The value is the same regardless of position direction.
30807
+ *
30808
+ * @param effectivePriceOpen - Effective entry price (from `getPositionAveragePrice`)
30809
+ * @returns New stop-loss price equal to the effective entry price
30810
+ *
30811
+ * @example
30812
+ * // LONG: entry=100, SL was 90 → breakeven SL = 100
30813
+ * const newSL = breakevenNewStopLossPrice(100); // 100
30814
+ *
30815
+ * // SHORT: entry=100, SL was 110 → breakeven SL = 100
30816
+ * const newSL = breakevenNewStopLossPrice(100); // 100
30817
+ */
30818
+ const breakevenNewStopLossPrice = (effectivePriceOpen) => {
30819
+ return effectivePriceOpen;
30820
+ };
30821
+
30822
+ /**
30823
+ * Compute the effective take-profit price for a breakeven operation.
30824
+ *
30825
+ * Breakeven does not change the take-profit. Returns the currently effective TP:
30826
+ * `_trailingPriceTakeProfit` if set (trailing TP active), otherwise `priceTakeProfit`.
30827
+ *
30828
+ * @param priceTakeProfit - Original take-profit price from the pending signal
30829
+ * @param trailingPriceTakeProfit - Trailing take-profit override, or undefined if not set
30830
+ * @returns Effective take-profit price (unchanged by breakeven)
30831
+ *
30832
+ * @example
30833
+ * // No trailing TP set
30834
+ * const newTP = breakevenNewTakeProfitPrice(110, undefined); // 110
30835
+ *
30836
+ * // Trailing TP active
30837
+ * const newTP = breakevenNewTakeProfitPrice(110, 107); // 107
30838
+ */
30839
+ const breakevenNewTakeProfitPrice = (priceTakeProfit, trailingPriceTakeProfit) => {
30840
+ return trailingPriceTakeProfit ?? priceTakeProfit;
30841
+ };
30842
+
30843
+ const BROKER_METHOD_NAME_COMMIT_SIGNAL_OPEN = "BrokerAdapter.commitSignalOpen";
30844
+ const BROKER_METHOD_NAME_COMMIT_SIGNAL_CLOSE = "BrokerAdapter.commitSignalClose";
30845
+ const BROKER_METHOD_NAME_COMMIT_PARTIAL_PROFIT = "BrokerAdapter.commitPartialProfit";
30846
+ const BROKER_METHOD_NAME_COMMIT_PARTIAL_LOSS = "BrokerAdapter.commitPartialLoss";
30847
+ const BROKER_METHOD_NAME_COMMIT_TRAILING_STOP = "BrokerAdapter.commitTrailingStop";
30848
+ const BROKER_METHOD_NAME_COMMIT_TRAILING_TAKE = "BrokerAdapter.commitTrailingTake";
30849
+ const BROKER_METHOD_NAME_COMMIT_BREAKEVEN = "BrokerAdapter.commitBreakeven";
30850
+ const BROKER_METHOD_NAME_COMMIT_AVERAGE_BUY = "BrokerAdapter.commitAverageBuy";
30851
+ const BROKER_METHOD_NAME_USE_BROKER_ADAPTER = "BrokerAdapter.useBrokerAdapter";
30852
+ const BROKER_METHOD_NAME_ENABLE = "BrokerAdapter.enable";
30853
+ const BROKER_METHOD_NAME_DISABLE = "BrokerAdapter.disable";
30854
+ const BROKER_BASE_METHOD_NAME_WAIT_FOR_INIT = "BrokerBase.waitForInit";
30855
+ const BROKER_BASE_METHOD_NAME_ON_SIGNAL_OPEN = "BrokerBase.onSignalOpenCommit";
30856
+ const BROKER_BASE_METHOD_NAME_ON_SIGNAL_CLOSE = "BrokerBase.onSignalCloseCommit";
30857
+ const BROKER_BASE_METHOD_NAME_ON_PARTIAL_PROFIT = "BrokerBase.onPartialProfitCommit";
30858
+ const BROKER_BASE_METHOD_NAME_ON_PARTIAL_LOSS = "BrokerBase.onPartialLossCommit";
30859
+ const BROKER_BASE_METHOD_NAME_ON_TRAILING_STOP = "BrokerBase.onTrailingStopCommit";
30860
+ const BROKER_BASE_METHOD_NAME_ON_TRAILING_TAKE = "BrokerBase.onTrailingTakeCommit";
30861
+ const BROKER_BASE_METHOD_NAME_ON_BREAKEVEN = "BrokerBase.onBreakevenCommit";
30862
+ const BROKER_BASE_METHOD_NAME_ON_AVERAGE_BUY = "BrokerBase.onAverageBuyCommit";
30863
+ class BrokerProxy {
30864
+ constructor(_instance) {
30865
+ this._instance = _instance;
30866
+ this.waitForInit = singleshot(async () => {
30867
+ if (this._instance.waitForInit) {
30868
+ await this._instance.waitForInit();
30869
+ }
30870
+ });
30871
+ }
30872
+ async onSignalOpenCommit(payload) {
30873
+ if (this._instance.onSignalOpenCommit) {
30874
+ await this._instance.onSignalOpenCommit(payload);
30875
+ }
30876
+ }
30877
+ async onSignalCloseCommit(payload) {
30878
+ if (this._instance.onSignalCloseCommit) {
30879
+ await this._instance.onSignalCloseCommit(payload);
30880
+ }
30881
+ }
30882
+ async onPartialProfitCommit(payload) {
30883
+ if (this._instance.onPartialProfitCommit) {
30884
+ await this._instance.onPartialProfitCommit(payload);
30885
+ }
30886
+ }
30887
+ async onPartialLossCommit(payload) {
30888
+ if (this._instance.onPartialLossCommit) {
30889
+ await this._instance.onPartialLossCommit(payload);
30890
+ }
30891
+ }
30892
+ async onTrailingStopCommit(payload) {
30893
+ if (this._instance.onTrailingStopCommit) {
30894
+ await this._instance.onTrailingStopCommit(payload);
30895
+ }
30896
+ }
30897
+ async onTrailingTakeCommit(payload) {
30898
+ if (this._instance.onTrailingTakeCommit) {
30899
+ await this._instance.onTrailingTakeCommit(payload);
30900
+ }
30901
+ }
30902
+ async onBreakevenCommit(payload) {
30903
+ if (this._instance.onBreakevenCommit) {
30904
+ await this._instance.onBreakevenCommit(payload);
30905
+ }
30906
+ }
30907
+ async onAverageBuyCommit(payload) {
30908
+ if (this._instance.onAverageBuyCommit) {
30909
+ await this._instance.onAverageBuyCommit(payload);
30910
+ }
30911
+ }
30912
+ }
30913
+ /**
30914
+ * Facade for broker integration — intercepts all commit* operations before DI-core mutations.
30915
+ *
30916
+ * Acts as a transaction control point: if any commit* method throws, the DI-core mutation
30917
+ * is never reached and the state remains unchanged.
30918
+ *
30919
+ * In backtest mode all commit* calls are silently skipped (payload.backtest === true).
30920
+ * In live mode the call is forwarded to the registered IBroker adapter via BrokerProxy.
30921
+ *
30922
+ * signal-open and signal-close events are routed automatically via syncSubject subscription
30923
+ * (activated on `enable()`). All other commit* methods are called explicitly from
30924
+ * Live.ts / Backtest.ts / strategy.ts before the corresponding strategyCoreService call.
30925
+ *
30926
+ * @example
30927
+ * ```typescript
30928
+ * import { Broker } from "backtest-kit";
30929
+ *
30930
+ * // Register a custom broker adapter
30931
+ * Broker.useBrokerAdapter(MyBrokerAdapter);
30932
+ *
30933
+ * // Activate syncSubject subscription (signal-open / signal-close routing)
30934
+ * const dispose = Broker.enable();
30935
+ *
30936
+ * // ... run strategy ...
30937
+ *
30938
+ * // Deactivate when done
30939
+ * Broker.disable();
30940
+ * ```
30941
+ */
30942
+ class BrokerAdapter {
30943
+ constructor() {
30944
+ this._brokerInstance = null;
30945
+ /**
30946
+ * Forwards a signal-open event to the registered broker adapter.
30947
+ *
30948
+ * Called automatically via syncSubject when `enable()` is active.
30949
+ * Skipped silently in backtest mode or when no adapter is registered.
30950
+ *
30951
+ * @param payload - Signal open details: symbol, cost, position, prices, context, backtest flag
30952
+ *
30953
+ * @example
30954
+ * ```typescript
30955
+ * await Broker.commitSignalOpen({
30956
+ * symbol: "BTCUSDT",
30957
+ * cost: 100,
30958
+ * position: "long",
30959
+ * priceOpen: 50000,
30960
+ * priceTakeProfit: 55000,
30961
+ * priceStopLoss: 48000,
30962
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
30963
+ * backtest: false,
30964
+ * });
30965
+ * ```
30966
+ */
30967
+ this.commitSignalOpen = async (payload) => {
30968
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_SIGNAL_OPEN, {
30969
+ symbol: payload.symbol,
30970
+ context: payload.context,
30971
+ });
30972
+ if (!this.enable.hasValue()) {
30973
+ return;
30974
+ }
30975
+ if (payload.backtest) {
30976
+ return;
30977
+ }
30978
+ await this._brokerInstance?.onSignalOpenCommit(payload);
30979
+ };
30980
+ /**
30981
+ * Forwards a signal-close event to the registered broker adapter.
30982
+ *
30983
+ * Called automatically via syncSubject when `enable()` is active.
30984
+ * Skipped silently in backtest mode or when no adapter is registered.
30985
+ *
30986
+ * @param payload - Signal close details: symbol, cost, position, currentPrice, pnl, context, backtest flag
30987
+ *
30988
+ * @example
30989
+ * ```typescript
30990
+ * await Broker.commitSignalClose({
30991
+ * symbol: "BTCUSDT",
30992
+ * cost: 100,
30993
+ * position: "long",
30994
+ * currentPrice: 54000,
30995
+ * priceTakeProfit: 55000,
30996
+ * priceStopLoss: 48000,
30997
+ * totalEntries: 2,
30998
+ * totalPartials: 1,
30999
+ * pnl: { profit: 80, loss: 0, volume: 100 },
31000
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
31001
+ * backtest: false,
31002
+ * });
31003
+ * ```
31004
+ */
31005
+ this.commitSignalClose = async (payload) => {
31006
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_SIGNAL_CLOSE, {
31007
+ symbol: payload.symbol,
31008
+ context: payload.context,
31009
+ });
31010
+ if (!this.enable.hasValue()) {
31011
+ return;
31012
+ }
31013
+ if (payload.backtest) {
31014
+ return;
31015
+ }
31016
+ await this._brokerInstance?.onSignalCloseCommit(payload);
31017
+ };
31018
+ /**
31019
+ * Intercepts a partial-profit close before DI-core mutation.
31020
+ *
31021
+ * Called explicitly from Live.ts / Backtest.ts / strategy.ts after all validations pass,
31022
+ * but before `strategyCoreService.partialProfit()`. If this method throws, the DI mutation
31023
+ * is skipped and state remains unchanged.
31024
+ *
31025
+ * Skipped silently in backtest mode or when no adapter is registered.
31026
+ *
31027
+ * @param payload - Partial profit details: symbol, percentToClose, cost (dollar value), currentPrice, context, backtest flag
31028
+ *
31029
+ * @example
31030
+ * ```typescript
31031
+ * await Broker.commitPartialProfit({
31032
+ * symbol: "BTCUSDT",
31033
+ * percentToClose: 30,
31034
+ * cost: 30,
31035
+ * currentPrice: 52000,
31036
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
31037
+ * backtest: false,
31038
+ * });
31039
+ * ```
31040
+ */
31041
+ this.commitPartialProfit = async (payload) => {
31042
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_PARTIAL_PROFIT, {
31043
+ symbol: payload.symbol,
31044
+ context: payload.context,
31045
+ });
31046
+ if (!this.enable.hasValue()) {
31047
+ return;
31048
+ }
31049
+ if (payload.backtest) {
31050
+ return;
31051
+ }
31052
+ await this._brokerInstance?.onPartialProfitCommit(payload);
31053
+ };
31054
+ /**
31055
+ * Intercepts a partial-loss close before DI-core mutation.
31056
+ *
31057
+ * Called explicitly from Live.ts / Backtest.ts / strategy.ts after all validations pass,
31058
+ * but before `strategyCoreService.partialLoss()`. If this method throws, the DI mutation
31059
+ * is skipped and state remains unchanged.
31060
+ *
31061
+ * Skipped silently in backtest mode or when no adapter is registered.
31062
+ *
31063
+ * @param payload - Partial loss details: symbol, percentToClose, cost (dollar value), currentPrice, context, backtest flag
31064
+ *
31065
+ * @example
31066
+ * ```typescript
31067
+ * await Broker.commitPartialLoss({
31068
+ * symbol: "BTCUSDT",
31069
+ * percentToClose: 40,
31070
+ * cost: 40,
31071
+ * currentPrice: 48500,
31072
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
31073
+ * backtest: false,
31074
+ * });
31075
+ * ```
31076
+ */
31077
+ this.commitPartialLoss = async (payload) => {
31078
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_PARTIAL_LOSS, {
31079
+ symbol: payload.symbol,
31080
+ context: payload.context,
31081
+ });
31082
+ if (!this.enable.hasValue()) {
31083
+ return;
31084
+ }
31085
+ if (payload.backtest) {
31086
+ return;
31087
+ }
31088
+ await this._brokerInstance?.onPartialLossCommit(payload);
31089
+ };
31090
+ /**
31091
+ * Intercepts a trailing stop-loss update before DI-core mutation.
31092
+ *
31093
+ * Called explicitly after all validations pass, but before `strategyCoreService.trailingStop()`.
31094
+ * `newStopLossPrice` is the absolute price computed from percentShift + original SL + effectivePriceOpen.
31095
+ *
31096
+ * Skipped silently in backtest mode or when no adapter is registered.
31097
+ *
31098
+ * @param payload - Trailing stop details: symbol, percentShift, currentPrice, newStopLossPrice, context, backtest flag
31099
+ *
31100
+ * @example
31101
+ * ```typescript
31102
+ * // LONG: entry=100, originalSL=90, percentShift=-5 → newSL=95
31103
+ * await Broker.commitTrailingStop({
31104
+ * symbol: "BTCUSDT",
31105
+ * percentShift: -5,
31106
+ * currentPrice: 102,
31107
+ * newStopLossPrice: 95,
31108
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
31109
+ * backtest: false,
31110
+ * });
31111
+ * ```
31112
+ */
31113
+ this.commitTrailingStop = async (payload) => {
31114
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_TRAILING_STOP, {
31115
+ symbol: payload.symbol,
31116
+ context: payload.context,
31117
+ });
31118
+ if (!this.enable.hasValue()) {
31119
+ return;
31120
+ }
31121
+ if (payload.backtest) {
31122
+ return;
31123
+ }
31124
+ await this._brokerInstance?.onTrailingStopCommit(payload);
31125
+ };
31126
+ /**
31127
+ * Intercepts a trailing take-profit update before DI-core mutation.
31128
+ *
31129
+ * Called explicitly after all validations pass, but before `strategyCoreService.trailingTake()`.
31130
+ * `newTakeProfitPrice` is the absolute price computed from percentShift + original TP + effectivePriceOpen.
31131
+ *
31132
+ * Skipped silently in backtest mode or when no adapter is registered.
31133
+ *
31134
+ * @param payload - Trailing take details: symbol, percentShift, currentPrice, newTakeProfitPrice, context, backtest flag
31135
+ *
31136
+ * @example
31137
+ * ```typescript
31138
+ * // LONG: entry=100, originalTP=110, percentShift=-3 → newTP=107
31139
+ * await Broker.commitTrailingTake({
31140
+ * symbol: "BTCUSDT",
31141
+ * percentShift: -3,
31142
+ * currentPrice: 102,
31143
+ * newTakeProfitPrice: 107,
31144
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
31145
+ * backtest: false,
31146
+ * });
31147
+ * ```
31148
+ */
31149
+ this.commitTrailingTake = async (payload) => {
31150
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_TRAILING_TAKE, {
31151
+ symbol: payload.symbol,
31152
+ context: payload.context,
31153
+ });
31154
+ if (!this.enable.hasValue()) {
31155
+ return;
31156
+ }
31157
+ if (payload.backtest) {
31158
+ return;
31159
+ }
31160
+ await this._brokerInstance?.onTrailingTakeCommit(payload);
31161
+ };
31162
+ /**
31163
+ * Intercepts a breakeven operation before DI-core mutation.
31164
+ *
31165
+ * Called explicitly after all validations pass, but before `strategyCoreService.breakeven()`.
31166
+ * `newStopLossPrice` equals effectivePriceOpen (entry price).
31167
+ * `newTakeProfitPrice` equals `_trailingPriceTakeProfit ?? priceTakeProfit` (TP is unchanged by breakeven).
31168
+ *
31169
+ * Skipped silently in backtest mode or when no adapter is registered.
31170
+ *
31171
+ * @param payload - Breakeven details: symbol, currentPrice, newStopLossPrice, newTakeProfitPrice, context, backtest flag
31172
+ *
31173
+ * @example
31174
+ * ```typescript
31175
+ * // LONG: entry=100, currentPrice=100.5, newSL=100 (entry), newTP=110 (unchanged)
31176
+ * await Broker.commitBreakeven({
31177
+ * symbol: "BTCUSDT",
31178
+ * currentPrice: 100.5,
31179
+ * newStopLossPrice: 100,
31180
+ * newTakeProfitPrice: 110,
31181
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
31182
+ * backtest: false,
31183
+ * });
31184
+ * ```
31185
+ */
31186
+ this.commitBreakeven = async (payload) => {
31187
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_BREAKEVEN, {
31188
+ symbol: payload.symbol,
31189
+ context: payload.context,
31190
+ });
31191
+ if (!this.enable.hasValue()) {
31192
+ return;
31193
+ }
31194
+ if (payload.backtest) {
31195
+ return;
31196
+ }
31197
+ await this._brokerInstance?.onBreakevenCommit(payload);
31198
+ };
31199
+ /**
31200
+ * Intercepts a DCA average-buy entry before DI-core mutation.
31201
+ *
31202
+ * Called explicitly after all validations pass, but before `strategyCoreService.averageBuy()`.
31203
+ * `currentPrice` is the market price at which the new DCA entry is added.
31204
+ * `cost` is the dollar amount of the new entry (default: CC_POSITION_ENTRY_COST).
31205
+ *
31206
+ * Skipped silently in backtest mode or when no adapter is registered.
31207
+ *
31208
+ * @param payload - Average buy details: symbol, currentPrice, cost, context, backtest flag
31209
+ *
31210
+ * @example
31211
+ * ```typescript
31212
+ * // Add DCA entry at current market price
31213
+ * await Broker.commitAverageBuy({
31214
+ * symbol: "BTCUSDT",
31215
+ * currentPrice: 42000,
31216
+ * cost: 100,
31217
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
31218
+ * backtest: false,
31219
+ * });
31220
+ * ```
31221
+ */
31222
+ this.commitAverageBuy = async (payload) => {
31223
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_AVERAGE_BUY, {
31224
+ symbol: payload.symbol,
31225
+ context: payload.context,
31226
+ });
31227
+ if (!this.enable.hasValue()) {
31228
+ return;
31229
+ }
31230
+ if (payload.backtest) {
31231
+ return;
31232
+ }
31233
+ await this._brokerInstance?.onAverageBuyCommit(payload);
31234
+ };
31235
+ /**
31236
+ * Registers a broker adapter instance or constructor to receive commit* callbacks.
31237
+ *
31238
+ * Must be called before `enable()`. Accepts either a class constructor (called with `new`)
31239
+ * or an already-instantiated object implementing `Partial<IBroker>`.
31240
+ *
31241
+ * @param broker - IBroker constructor or instance
31242
+ *
31243
+ * @example
31244
+ * ```typescript
31245
+ * import { Broker } from "backtest-kit";
31246
+ *
31247
+ * // Register via constructor
31248
+ * Broker.useBrokerAdapter(MyBrokerAdapter);
31249
+ *
31250
+ * // Register via instance
31251
+ * Broker.useBrokerAdapter(new MyBrokerAdapter());
31252
+ * ```
31253
+ */
31254
+ this.useBrokerAdapter = (broker) => {
31255
+ bt.loggerService.info(BROKER_METHOD_NAME_USE_BROKER_ADAPTER, {});
31256
+ if (typeof broker === "function") {
31257
+ const instance = Reflect.construct(broker, []);
31258
+ this._brokerInstance = new BrokerProxy(instance);
31259
+ return;
31260
+ }
31261
+ this._brokerInstance = new BrokerProxy(broker);
31262
+ };
31263
+ /**
31264
+ * Activates the broker: subscribes to syncSubject for signal-open / signal-close routing.
31265
+ *
31266
+ * Must be called after `useBrokerAdapter()`. Returns a dispose function that unsubscribes
31267
+ * from syncSubject (equivalent to calling `disable()`).
31268
+ *
31269
+ * Calling `enable()` without a registered adapter throws immediately.
31270
+ * Calling `enable()` more than once is idempotent (singleshot guard).
31271
+ *
31272
+ * @returns Dispose function — call it to deactivate the broker subscription
31273
+ *
31274
+ * @example
31275
+ * ```typescript
31276
+ * import { Broker } from "backtest-kit";
31277
+ *
31278
+ * Broker.useBrokerAdapter(MyBrokerAdapter);
31279
+ * const dispose = Broker.enable();
31280
+ *
31281
+ * // ... run backtest or live session ...
31282
+ *
31283
+ * dispose(); // or Broker.disable()
31284
+ * ```
31285
+ */
31286
+ this.enable = singleshot(() => {
31287
+ bt.loggerService.info(BROKER_METHOD_NAME_ENABLE, {});
31288
+ if (!this._brokerInstance) {
31289
+ this.enable.clear();
31290
+ throw new Error("No broker instance provided. Call Broker.useBrokerAdapter first.");
31291
+ }
31292
+ const unSignalOpen = syncSubject.subscribe(async (event) => {
31293
+ if (event.action !== "signal-open") {
31294
+ return;
31295
+ }
31296
+ await this.commitSignalOpen({
31297
+ position: event.signal.position,
31298
+ cost: event.signal.cost,
31299
+ symbol: event.symbol,
31300
+ priceTakeProfit: event.signal.priceTakeProfit,
31301
+ priceStopLoss: event.signal.priceStopLoss,
31302
+ priceOpen: event.signal.priceOpen,
31303
+ context: {
31304
+ strategyName: event.strategyName,
31305
+ exchangeName: event.exchangeName,
31306
+ frameName: event.frameName,
31307
+ },
31308
+ backtest: event.backtest,
31309
+ });
31310
+ });
31311
+ const unSignalClose = syncSubject.subscribe(async (event) => {
31312
+ if (event.action !== "signal-close") {
31313
+ return;
31314
+ }
31315
+ await this.commitSignalClose({
31316
+ position: event.signal.position,
31317
+ currentPrice: event.currentPrice,
31318
+ cost: event.signal.cost,
31319
+ symbol: event.symbol,
31320
+ pnl: event.pnl,
31321
+ totalEntries: event.totalEntries,
31322
+ totalPartials: event.totalPartials,
31323
+ priceStopLoss: event.signal.priceStopLoss,
31324
+ priceTakeProfit: event.signal.priceTakeProfit,
31325
+ context: {
31326
+ strategyName: event.strategyName,
31327
+ exchangeName: event.exchangeName,
31328
+ frameName: event.frameName,
31329
+ },
31330
+ backtest: event.backtest,
31331
+ });
31332
+ });
31333
+ const disposeFn = compose(() => unSignalOpen(), () => unSignalClose());
31334
+ return () => {
31335
+ this.enable.clear();
31336
+ disposeFn();
31337
+ };
31338
+ });
31339
+ /**
31340
+ * Deactivates the broker: unsubscribes from syncSubject and resets the singleshot guard.
31341
+ *
31342
+ * Idempotent — safe to call even if `enable()` was never called.
31343
+ * After `disable()`, `enable()` can be called again to reactivate.
31344
+ *
31345
+ * @example
31346
+ * ```typescript
31347
+ * import { Broker } from "backtest-kit";
31348
+ *
31349
+ * Broker.useBrokerAdapter(MyBrokerAdapter);
31350
+ * Broker.enable();
31351
+ *
31352
+ * // Stop receiving events
31353
+ * Broker.disable();
31354
+ * ```
31355
+ */
31356
+ this.disable = () => {
31357
+ bt.loggerService.info(BROKER_METHOD_NAME_DISABLE, {});
31358
+ if (this.enable.hasValue()) {
31359
+ const lastSubscription = this.enable();
31360
+ lastSubscription();
31361
+ }
31362
+ };
31363
+ }
31364
+ }
31365
+ /**
31366
+ * Base class for custom broker adapter implementations.
31367
+ *
31368
+ * Provides default no-op implementations for all IBroker methods that log events.
31369
+ * Extend this class to implement a real exchange adapter for:
31370
+ * - Placing and canceling limit/market orders
31371
+ * - Updating stop-loss and take-profit levels on exchange
31372
+ * - Tracking position state in an external system
31373
+ * - Sending trade notifications (Telegram, Discord, Email)
31374
+ * - Recording trades to a database or analytics service
31375
+ *
31376
+ * Key features:
31377
+ * - All methods have default implementations (no need to override unused methods)
31378
+ * - Automatic logging of all events via bt.loggerService
31379
+ * - Implements the full IBroker interface
31380
+ * - `makeExtendable` applied for correct subclass instantiation
31381
+ *
31382
+ * Lifecycle:
31383
+ * 1. Constructor called (no arguments)
31384
+ * 2. `waitForInit()` called once for async initialization (e.g. exchange login)
31385
+ * 3. Event methods called as strategy executes
31386
+ * 4. No explicit dispose — clean up in `waitForInit` teardown or externally
31387
+ *
31388
+ * Event flow (called only in live mode, skipped in backtest):
31389
+ * - `onSignalOpenCommit` — new position opened
31390
+ * - `onSignalCloseCommit` — position closed (SL/TP hit or manual close)
31391
+ * - `onPartialProfitCommit` — partial close at profit executed
31392
+ * - `onPartialLossCommit` — partial close at loss executed
31393
+ * - `onTrailingStopCommit` — trailing stop-loss updated
31394
+ * - `onTrailingTakeCommit` — trailing take-profit updated
31395
+ * - `onBreakevenCommit` — stop-loss moved to entry price
31396
+ * - `onAverageBuyCommit` — new DCA entry added to position
31397
+ *
31398
+ * @example
31399
+ * ```typescript
31400
+ * import { BrokerBase, Broker } from "backtest-kit";
31401
+ *
31402
+ * // Extend BrokerBase and override only needed methods
31403
+ * class BinanceBroker extends BrokerBase {
31404
+ * private client: BinanceClient | null = null;
31405
+ *
31406
+ * async waitForInit() {
31407
+ * super.waitForInit(); // Call parent for logging
31408
+ * this.client = new BinanceClient(process.env.API_KEY, process.env.SECRET);
31409
+ * await this.client.connect();
31410
+ * }
31411
+ *
31412
+ * async onSignalOpenCommit(payload: BrokerSignalOpenPayload) {
31413
+ * super.onSignalOpenCommit(payload); // Call parent for logging
31414
+ * await this.client!.placeOrder({
31415
+ * symbol: payload.symbol,
31416
+ * side: payload.position === "long" ? "BUY" : "SELL",
31417
+ * quantity: payload.cost / payload.priceOpen,
31418
+ * });
31419
+ * }
31420
+ *
31421
+ * async onSignalCloseCommit(payload: BrokerSignalClosePayload) {
31422
+ * super.onSignalCloseCommit(payload); // Call parent for logging
31423
+ * await this.client!.closePosition(payload.symbol);
31424
+ * }
31425
+ * }
31426
+ *
31427
+ * // Register the adapter
31428
+ * Broker.useBrokerAdapter(BinanceBroker);
31429
+ * Broker.enable();
31430
+ * ```
31431
+ *
31432
+ * @example
31433
+ * ```typescript
31434
+ * // Minimal implementation — only handle opens and closes
31435
+ * class NotifyBroker extends BrokerBase {
31436
+ * async onSignalOpenCommit(payload: BrokerSignalOpenPayload) {
31437
+ * await sendTelegram(`Opened ${payload.position} on ${payload.symbol}`);
31438
+ * }
31439
+ *
31440
+ * async onSignalCloseCommit(payload: BrokerSignalClosePayload) {
31441
+ * const pnl = payload.pnl.profit - payload.pnl.loss;
31442
+ * await sendTelegram(`Closed ${payload.symbol}: PnL $${pnl.toFixed(2)}`);
31443
+ * }
31444
+ * }
31445
+ * ```
31446
+ */
31447
+ class BrokerBase {
31448
+ /**
31449
+ * Performs async initialization before the broker starts receiving events.
31450
+ *
31451
+ * Called once by BrokerProxy via `waitForInit()` (singleshot) before the first event.
31452
+ * Override to establish exchange connections, authenticate API clients, load configuration.
31453
+ *
31454
+ * Default implementation: Logs initialization event.
31455
+ *
31456
+ * @example
31457
+ * ```typescript
31458
+ * async waitForInit() {
31459
+ * super.waitForInit(); // Keep parent logging
31460
+ * this.exchange = new ExchangeClient(process.env.API_KEY);
31461
+ * await this.exchange.authenticate();
31462
+ * }
31463
+ * ```
31464
+ */
31465
+ async waitForInit() {
31466
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_WAIT_FOR_INIT, {});
31467
+ }
31468
+ /**
31469
+ * Called when a new position is opened (signal activated).
31470
+ *
31471
+ * Triggered automatically via syncSubject when a scheduled signal's priceOpen is hit.
31472
+ * Use to place the actual entry order on the exchange.
31473
+ *
31474
+ * Default implementation: Logs signal-open event.
31475
+ *
31476
+ * @param payload - Signal open details: symbol, cost, position, priceOpen, priceTakeProfit, priceStopLoss, context, backtest
31477
+ *
31478
+ * @example
31479
+ * ```typescript
31480
+ * async onSignalOpenCommit(payload: BrokerSignalOpenPayload) {
31481
+ * super.onSignalOpenCommit(payload); // Keep parent logging
31482
+ * await this.exchange.placeMarketOrder({
31483
+ * symbol: payload.symbol,
31484
+ * side: payload.position === "long" ? "BUY" : "SELL",
31485
+ * quantity: payload.cost / payload.priceOpen,
31486
+ * });
31487
+ * }
31488
+ * ```
31489
+ */
31490
+ async onSignalOpenCommit(payload) {
31491
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SIGNAL_OPEN, {
31492
+ symbol: payload.symbol,
31493
+ context: payload.context,
31494
+ });
31495
+ }
31496
+ /**
31497
+ * Called when a position is fully closed (SL/TP hit or manual close).
31498
+ *
31499
+ * Triggered automatically via syncSubject when a pending signal is closed.
31500
+ * Use to place the exit order and record final PnL.
31501
+ *
31502
+ * Default implementation: Logs signal-close event.
31503
+ *
31504
+ * @param payload - Signal close details: symbol, cost, position, currentPrice, pnl, totalEntries, totalPartials, context, backtest
31505
+ *
31506
+ * @example
31507
+ * ```typescript
31508
+ * async onSignalCloseCommit(payload: BrokerSignalClosePayload) {
31509
+ * super.onSignalCloseCommit(payload); // Keep parent logging
31510
+ * await this.exchange.closePosition(payload.symbol);
31511
+ * await this.db.recordTrade({ symbol: payload.symbol, pnl: payload.pnl });
31512
+ * }
31513
+ * ```
31514
+ */
31515
+ async onSignalCloseCommit(payload) {
31516
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SIGNAL_CLOSE, {
31517
+ symbol: payload.symbol,
31518
+ context: payload.context,
31519
+ });
31520
+ }
31521
+ /**
31522
+ * Called when a partial close at profit is executed.
31523
+ *
31524
+ * Triggered explicitly from strategy.ts / Live.ts / Backtest.ts after all validations pass,
31525
+ * before `strategyCoreService.partialProfit()`. If this method throws, the DI mutation is skipped.
31526
+ * Use to partially close the position on the exchange at the profit level.
31527
+ *
31528
+ * Default implementation: Logs partial profit event.
31529
+ *
31530
+ * @param payload - Partial profit details: symbol, percentToClose, cost (dollar value), currentPrice, context, backtest
31531
+ *
31532
+ * @example
31533
+ * ```typescript
31534
+ * async onPartialProfitCommit(payload: BrokerPartialProfitPayload) {
31535
+ * super.onPartialProfitCommit(payload); // Keep parent logging
31536
+ * await this.exchange.reducePosition({
31537
+ * symbol: payload.symbol,
31538
+ * dollarAmount: payload.cost,
31539
+ * price: payload.currentPrice,
31540
+ * });
31541
+ * }
31542
+ * ```
31543
+ */
31544
+ async onPartialProfitCommit(payload) {
31545
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_PARTIAL_PROFIT, {
31546
+ symbol: payload.symbol,
31547
+ context: payload.context,
31548
+ });
31549
+ }
31550
+ /**
31551
+ * Called when a partial close at loss is executed.
31552
+ *
31553
+ * Triggered explicitly from strategy.ts / Live.ts / Backtest.ts after all validations pass,
31554
+ * before `strategyCoreService.partialLoss()`. If this method throws, the DI mutation is skipped.
31555
+ * Use to partially close the position on the exchange at the loss level.
31556
+ *
31557
+ * Default implementation: Logs partial loss event.
31558
+ *
31559
+ * @param payload - Partial loss details: symbol, percentToClose, cost (dollar value), currentPrice, context, backtest
31560
+ *
31561
+ * @example
31562
+ * ```typescript
31563
+ * async onPartialLossCommit(payload: BrokerPartialLossPayload) {
31564
+ * super.onPartialLossCommit(payload); // Keep parent logging
31565
+ * await this.exchange.reducePosition({
31566
+ * symbol: payload.symbol,
31567
+ * dollarAmount: payload.cost,
31568
+ * price: payload.currentPrice,
31569
+ * });
31570
+ * }
31571
+ * ```
31572
+ */
31573
+ async onPartialLossCommit(payload) {
31574
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_PARTIAL_LOSS, {
31575
+ symbol: payload.symbol,
31576
+ context: payload.context,
31577
+ });
31578
+ }
31579
+ /**
31580
+ * Called when the trailing stop-loss level is updated.
31581
+ *
31582
+ * Triggered explicitly after all validations pass, before `strategyCoreService.trailingStop()`.
31583
+ * `newStopLossPrice` is the absolute SL price — use it to update the exchange order directly.
31584
+ *
31585
+ * Default implementation: Logs trailing stop event.
31586
+ *
31587
+ * @param payload - Trailing stop details: symbol, percentShift, currentPrice, newStopLossPrice, context, backtest
31588
+ *
31589
+ * @example
31590
+ * ```typescript
31591
+ * async onTrailingStopCommit(payload: BrokerTrailingStopPayload) {
31592
+ * super.onTrailingStopCommit(payload); // Keep parent logging
31593
+ * await this.exchange.updateStopLoss({
31594
+ * symbol: payload.symbol,
31595
+ * price: payload.newStopLossPrice,
31596
+ * });
31597
+ * }
31598
+ * ```
31599
+ */
31600
+ async onTrailingStopCommit(payload) {
31601
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_TRAILING_STOP, {
31602
+ symbol: payload.symbol,
31603
+ context: payload.context,
31604
+ });
31605
+ }
31606
+ /**
31607
+ * Called when the trailing take-profit level is updated.
31608
+ *
31609
+ * Triggered explicitly after all validations pass, before `strategyCoreService.trailingTake()`.
31610
+ * `newTakeProfitPrice` is the absolute TP price — use it to update the exchange order directly.
31611
+ *
31612
+ * Default implementation: Logs trailing take event.
31613
+ *
31614
+ * @param payload - Trailing take details: symbol, percentShift, currentPrice, newTakeProfitPrice, context, backtest
31615
+ *
31616
+ * @example
31617
+ * ```typescript
31618
+ * async onTrailingTakeCommit(payload: BrokerTrailingTakePayload) {
31619
+ * super.onTrailingTakeCommit(payload); // Keep parent logging
31620
+ * await this.exchange.updateTakeProfit({
31621
+ * symbol: payload.symbol,
31622
+ * price: payload.newTakeProfitPrice,
31623
+ * });
31624
+ * }
31625
+ * ```
31626
+ */
31627
+ async onTrailingTakeCommit(payload) {
31628
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_TRAILING_TAKE, {
31629
+ symbol: payload.symbol,
31630
+ context: payload.context,
31631
+ });
31632
+ }
31633
+ /**
31634
+ * Called when the stop-loss is moved to breakeven (entry price).
31635
+ *
31636
+ * Triggered explicitly after all validations pass, before `strategyCoreService.breakeven()`.
31637
+ * `newStopLossPrice` equals `effectivePriceOpen` — the position's effective entry price.
31638
+ * `newTakeProfitPrice` is unchanged by breakeven.
31639
+ *
31640
+ * Default implementation: Logs breakeven event.
31641
+ *
31642
+ * @param payload - Breakeven details: symbol, currentPrice, newStopLossPrice, newTakeProfitPrice, context, backtest
31643
+ *
31644
+ * @example
31645
+ * ```typescript
31646
+ * async onBreakevenCommit(payload: BrokerBreakevenPayload) {
31647
+ * super.onBreakevenCommit(payload); // Keep parent logging
31648
+ * await this.exchange.updateStopLoss({
31649
+ * symbol: payload.symbol,
31650
+ * price: payload.newStopLossPrice, // = entry price
31651
+ * });
31652
+ * }
31653
+ * ```
31654
+ */
31655
+ async onBreakevenCommit(payload) {
31656
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_BREAKEVEN, {
31657
+ symbol: payload.symbol,
31658
+ context: payload.context,
31659
+ });
31660
+ }
31661
+ /**
31662
+ * Called when a new DCA entry is added to the active position.
31663
+ *
31664
+ * Triggered explicitly after all validations pass, before `strategyCoreService.averageBuy()`.
31665
+ * `currentPrice` is the market price at which the new averaging entry is placed.
31666
+ * `cost` is the dollar amount of the new DCA entry.
31667
+ *
31668
+ * Default implementation: Logs average buy event.
31669
+ *
31670
+ * @param payload - Average buy details: symbol, currentPrice, cost, context, backtest
31671
+ *
31672
+ * @example
31673
+ * ```typescript
31674
+ * async onAverageBuyCommit(payload: BrokerAverageBuyPayload) {
31675
+ * super.onAverageBuyCommit(payload); // Keep parent logging
31676
+ * await this.exchange.placeMarketOrder({
31677
+ * symbol: payload.symbol,
31678
+ * side: "BUY",
31679
+ * quantity: payload.cost / payload.currentPrice,
31680
+ * });
31681
+ * }
31682
+ * ```
31683
+ */
31684
+ async onAverageBuyCommit(payload) {
31685
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_AVERAGE_BUY, {
31686
+ symbol: payload.symbol,
31687
+ context: payload.context,
31688
+ });
31689
+ }
31690
+ }
31691
+ // @ts-ignore
31692
+ BrokerBase = makeExtendable(BrokerBase);
31693
+ /**
31694
+ * Global singleton instance of BrokerAdapter.
31695
+ * Provides static-like access to all broker commit methods and lifecycle controls.
31696
+ *
31697
+ * @example
31698
+ * ```typescript
31699
+ * import { Broker } from "backtest-kit";
31700
+ *
31701
+ * Broker.useBrokerAdapter(MyBrokerAdapter);
31702
+ * const dispose = Broker.enable();
31703
+ * ```
31704
+ */
31705
+ const Broker = new BrokerAdapter();
31706
+
30140
31707
  const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
30141
31708
  const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
30142
31709
  const PARTIAL_PROFIT_METHOD_NAME = "strategy.commitPartialProfit";
@@ -30145,6 +31712,8 @@ const PARTIAL_PROFIT_COST_METHOD_NAME = "strategy.commitPartialProfitCost";
30145
31712
  const PARTIAL_LOSS_COST_METHOD_NAME = "strategy.commitPartialLossCost";
30146
31713
  const TRAILING_STOP_METHOD_NAME = "strategy.commitTrailingStop";
30147
31714
  const TRAILING_PROFIT_METHOD_NAME = "strategy.commitTrailingTake";
31715
+ const TRAILING_STOP_COST_METHOD_NAME = "strategy.commitTrailingStopCost";
31716
+ const TRAILING_PROFIT_COST_METHOD_NAME = "strategy.commitTrailingTakeCost";
30148
31717
  const BREAKEVEN_METHOD_NAME = "strategy.commitBreakeven";
30149
31718
  const ACTIVATE_SCHEDULED_METHOD_NAME = "strategy.commitActivateScheduled";
30150
31719
  const AVERAGE_BUY_METHOD_NAME = "strategy.commitAverageBuy";
@@ -30274,6 +31843,28 @@ async function commitPartialProfit(symbol, percentToClose) {
30274
31843
  const currentPrice = await getAveragePrice(symbol);
30275
31844
  const { backtest: isBacktest } = bt.executionContextService.context;
30276
31845
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
31846
+ const investedCostForProfit = await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
31847
+ if (investedCostForProfit === null) {
31848
+ return false;
31849
+ }
31850
+ const signalForProfit = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
31851
+ if (!signalForProfit) {
31852
+ return false;
31853
+ }
31854
+ if (await not(bt.strategyCoreService.validatePartialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName }))) {
31855
+ return false;
31856
+ }
31857
+ await Broker.commitPartialProfit({
31858
+ symbol,
31859
+ percentToClose,
31860
+ cost: percentToCloseCost(percentToClose, investedCostForProfit),
31861
+ currentPrice,
31862
+ position: signalForProfit.position,
31863
+ priceTakeProfit: signalForProfit.priceTakeProfit,
31864
+ priceStopLoss: signalForProfit.priceStopLoss,
31865
+ context: { exchangeName, frameName, strategyName },
31866
+ backtest: isBacktest,
31867
+ });
30277
31868
  return await bt.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
30278
31869
  }
30279
31870
  /**
@@ -30317,6 +31908,28 @@ async function commitPartialLoss(symbol, percentToClose) {
30317
31908
  const currentPrice = await getAveragePrice(symbol);
30318
31909
  const { backtest: isBacktest } = bt.executionContextService.context;
30319
31910
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
31911
+ const investedCostForLoss = await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
31912
+ if (investedCostForLoss === null) {
31913
+ return false;
31914
+ }
31915
+ const signalForLoss = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
31916
+ if (!signalForLoss) {
31917
+ return false;
31918
+ }
31919
+ if (await not(bt.strategyCoreService.validatePartialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName }))) {
31920
+ return false;
31921
+ }
31922
+ await Broker.commitPartialLoss({
31923
+ symbol,
31924
+ percentToClose,
31925
+ cost: percentToCloseCost(percentToClose, investedCostForLoss),
31926
+ currentPrice,
31927
+ position: signalForLoss.position,
31928
+ priceTakeProfit: signalForLoss.priceTakeProfit,
31929
+ priceStopLoss: signalForLoss.priceStopLoss,
31930
+ context: { exchangeName, frameName, strategyName },
31931
+ backtest: isBacktest,
31932
+ });
30320
31933
  return await bt.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
30321
31934
  }
30322
31935
  /**
@@ -30376,6 +31989,26 @@ async function commitTrailingStop(symbol, percentShift, currentPrice) {
30376
31989
  }
30377
31990
  const { backtest: isBacktest } = bt.executionContextService.context;
30378
31991
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
31992
+ const signal = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
31993
+ if (!signal) {
31994
+ return false;
31995
+ }
31996
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
31997
+ if (effectivePriceOpen === null) {
31998
+ return false;
31999
+ }
32000
+ if (await not(bt.strategyCoreService.validateTrailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName }))) {
32001
+ return false;
32002
+ }
32003
+ await Broker.commitTrailingStop({
32004
+ symbol,
32005
+ percentShift,
32006
+ currentPrice,
32007
+ newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
32008
+ position: signal.position,
32009
+ context: { exchangeName, frameName, strategyName },
32010
+ backtest: isBacktest,
32011
+ });
30379
32012
  return await bt.strategyCoreService.trailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
30380
32013
  }
30381
32014
  /**
@@ -30435,6 +32068,126 @@ async function commitTrailingTake(symbol, percentShift, currentPrice) {
30435
32068
  }
30436
32069
  const { backtest: isBacktest } = bt.executionContextService.context;
30437
32070
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32071
+ const signal = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32072
+ if (!signal) {
32073
+ return false;
32074
+ }
32075
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
32076
+ if (effectivePriceOpen === null) {
32077
+ return false;
32078
+ }
32079
+ if (await not(bt.strategyCoreService.validateTrailingTake(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName }))) {
32080
+ return false;
32081
+ }
32082
+ await Broker.commitTrailingTake({
32083
+ symbol,
32084
+ percentShift,
32085
+ currentPrice,
32086
+ newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
32087
+ position: signal.position,
32088
+ context: { exchangeName, frameName, strategyName },
32089
+ backtest: isBacktest,
32090
+ });
32091
+ return await bt.strategyCoreService.trailingTake(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
32092
+ }
32093
+ /**
32094
+ * Adjusts the trailing stop-loss to an absolute price level.
32095
+ *
32096
+ * Convenience wrapper around commitTrailingStop that converts an absolute
32097
+ * stop-loss price to a percentShift relative to the ORIGINAL SL distance.
32098
+ *
32099
+ * Automatically detects backtest/live mode from execution context.
32100
+ * Automatically fetches current price via getAveragePrice.
32101
+ *
32102
+ * @param symbol - Trading pair symbol
32103
+ * @param newStopLossPrice - Desired absolute stop-loss price
32104
+ * @returns Promise<boolean> - true if trailing SL was set/updated, false if rejected
32105
+ */
32106
+ async function commitTrailingStopCost(symbol, newStopLossPrice) {
32107
+ bt.loggerService.info(TRAILING_STOP_COST_METHOD_NAME, {
32108
+ symbol,
32109
+ newStopLossPrice,
32110
+ });
32111
+ if (!ExecutionContextService.hasContext()) {
32112
+ throw new Error("commitTrailingStopCost requires an execution context");
32113
+ }
32114
+ if (!MethodContextService.hasContext()) {
32115
+ throw new Error("commitTrailingStopCost requires a method context");
32116
+ }
32117
+ const currentPrice = await getAveragePrice(symbol);
32118
+ const { backtest: isBacktest } = bt.executionContextService.context;
32119
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32120
+ const signal = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32121
+ if (!signal) {
32122
+ return false;
32123
+ }
32124
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
32125
+ if (effectivePriceOpen === null) {
32126
+ return false;
32127
+ }
32128
+ const percentShift = slPriceToPercentShift(newStopLossPrice, signal.priceStopLoss, effectivePriceOpen);
32129
+ if (await not(bt.strategyCoreService.validateTrailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName }))) {
32130
+ return false;
32131
+ }
32132
+ await Broker.commitTrailingStop({
32133
+ symbol,
32134
+ percentShift,
32135
+ currentPrice,
32136
+ newStopLossPrice,
32137
+ position: signal.position,
32138
+ context: { exchangeName, frameName, strategyName },
32139
+ backtest: isBacktest,
32140
+ });
32141
+ return await bt.strategyCoreService.trailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
32142
+ }
32143
+ /**
32144
+ * Adjusts the trailing take-profit to an absolute price level.
32145
+ *
32146
+ * Convenience wrapper around commitTrailingTake that converts an absolute
32147
+ * take-profit price to a percentShift relative to the ORIGINAL TP distance.
32148
+ *
32149
+ * Automatically detects backtest/live mode from execution context.
32150
+ * Automatically fetches current price via getAveragePrice.
32151
+ *
32152
+ * @param symbol - Trading pair symbol
32153
+ * @param newTakeProfitPrice - Desired absolute take-profit price
32154
+ * @returns Promise<boolean> - true if trailing TP was set/updated, false if rejected
32155
+ */
32156
+ async function commitTrailingTakeCost(symbol, newTakeProfitPrice) {
32157
+ bt.loggerService.info(TRAILING_PROFIT_COST_METHOD_NAME, {
32158
+ symbol,
32159
+ newTakeProfitPrice,
32160
+ });
32161
+ if (!ExecutionContextService.hasContext()) {
32162
+ throw new Error("commitTrailingTakeCost requires an execution context");
32163
+ }
32164
+ if (!MethodContextService.hasContext()) {
32165
+ throw new Error("commitTrailingTakeCost requires a method context");
32166
+ }
32167
+ const currentPrice = await getAveragePrice(symbol);
32168
+ const { backtest: isBacktest } = bt.executionContextService.context;
32169
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32170
+ const signal = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32171
+ if (!signal) {
32172
+ return false;
32173
+ }
32174
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
32175
+ if (effectivePriceOpen === null) {
32176
+ return false;
32177
+ }
32178
+ const percentShift = tpPriceToPercentShift(newTakeProfitPrice, signal.priceTakeProfit, effectivePriceOpen);
32179
+ if (await not(bt.strategyCoreService.validateTrailingTake(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName }))) {
32180
+ return false;
32181
+ }
32182
+ await Broker.commitTrailingTake({
32183
+ symbol,
32184
+ percentShift,
32185
+ currentPrice,
32186
+ newTakeProfitPrice,
32187
+ position: signal.position,
32188
+ context: { exchangeName, frameName, strategyName },
32189
+ backtest: isBacktest,
32190
+ });
30438
32191
  return await bt.strategyCoreService.trailingTake(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
30439
32192
  }
30440
32193
  /**
@@ -30475,6 +32228,26 @@ async function commitBreakeven(symbol) {
30475
32228
  const currentPrice = await getAveragePrice(symbol);
30476
32229
  const { backtest: isBacktest } = bt.executionContextService.context;
30477
32230
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32231
+ const signal = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32232
+ if (!signal) {
32233
+ return false;
32234
+ }
32235
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
32236
+ if (effectivePriceOpen === null) {
32237
+ return false;
32238
+ }
32239
+ if (await not(bt.strategyCoreService.validateBreakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName }))) {
32240
+ return false;
32241
+ }
32242
+ await Broker.commitBreakeven({
32243
+ symbol,
32244
+ currentPrice,
32245
+ newStopLossPrice: breakevenNewStopLossPrice(effectivePriceOpen),
32246
+ newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
32247
+ position: signal.position,
32248
+ context: { exchangeName, frameName, strategyName },
32249
+ backtest: isBacktest,
32250
+ });
30478
32251
  return await bt.strategyCoreService.breakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
30479
32252
  }
30480
32253
  /**
@@ -30549,6 +32322,23 @@ async function commitAverageBuy(symbol, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_C
30549
32322
  const currentPrice = await getAveragePrice(symbol);
30550
32323
  const { backtest: isBacktest } = bt.executionContextService.context;
30551
32324
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32325
+ if (await not(bt.strategyCoreService.validateAverageBuy(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName }))) {
32326
+ return false;
32327
+ }
32328
+ const signalForAvgBuy = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32329
+ if (!signalForAvgBuy) {
32330
+ return false;
32331
+ }
32332
+ await Broker.commitAverageBuy({
32333
+ symbol,
32334
+ currentPrice,
32335
+ cost,
32336
+ position: signalForAvgBuy.position,
32337
+ priceTakeProfit: signalForAvgBuy.priceTakeProfit,
32338
+ priceStopLoss: signalForAvgBuy.priceStopLoss,
32339
+ context: { exchangeName, frameName, strategyName },
32340
+ backtest: isBacktest,
32341
+ });
30552
32342
  return await bt.strategyCoreService.averageBuy(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName }, cost);
30553
32343
  }
30554
32344
  /**
@@ -30721,7 +32511,9 @@ async function getBreakeven(symbol, currentPrice) {
30721
32511
  return await bt.strategyCoreService.getBreakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
30722
32512
  }
30723
32513
  async function getPositionAveragePrice(symbol) {
30724
- bt.loggerService.info(GET_POSITION_AVERAGE_PRICE_METHOD_NAME, { symbol });
32514
+ bt.loggerService.info(GET_POSITION_AVERAGE_PRICE_METHOD_NAME, {
32515
+ symbol,
32516
+ });
30725
32517
  if (!ExecutionContextService.hasContext()) {
30726
32518
  throw new Error("getPositionAveragePrice requires an execution context");
30727
32519
  }
@@ -30733,7 +32525,9 @@ async function getPositionAveragePrice(symbol) {
30733
32525
  return await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
30734
32526
  }
30735
32527
  async function getPositionInvestedCount(symbol) {
30736
- bt.loggerService.info(GET_POSITION_INVESTED_COUNT_METHOD_NAME, { symbol });
32528
+ bt.loggerService.info(GET_POSITION_INVESTED_COUNT_METHOD_NAME, {
32529
+ symbol,
32530
+ });
30737
32531
  if (!ExecutionContextService.hasContext()) {
30738
32532
  throw new Error("getPositionInvestedCount requires an execution context");
30739
32533
  }
@@ -30745,7 +32539,9 @@ async function getPositionInvestedCount(symbol) {
30745
32539
  return await bt.strategyCoreService.getPositionInvestedCount(isBacktest, symbol, { exchangeName, frameName, strategyName });
30746
32540
  }
30747
32541
  async function getPositionInvestedCost(symbol) {
30748
- bt.loggerService.info(GET_POSITION_INVESTED_COST_METHOD_NAME, { symbol });
32542
+ bt.loggerService.info(GET_POSITION_INVESTED_COST_METHOD_NAME, {
32543
+ symbol,
32544
+ });
30749
32545
  if (!ExecutionContextService.hasContext()) {
30750
32546
  throw new Error("getPositionInvestedCost requires an execution context");
30751
32547
  }
@@ -30799,7 +32595,10 @@ async function getPositionPnlPercent(symbol) {
30799
32595
  * ```
30800
32596
  */
30801
32597
  async function commitPartialProfitCost(symbol, dollarAmount) {
30802
- bt.loggerService.info(PARTIAL_PROFIT_COST_METHOD_NAME, { symbol, dollarAmount });
32598
+ bt.loggerService.info(PARTIAL_PROFIT_COST_METHOD_NAME, {
32599
+ symbol,
32600
+ dollarAmount,
32601
+ });
30803
32602
  if (!ExecutionContextService.hasContext()) {
30804
32603
  throw new Error("commitPartialProfitCost requires an execution context");
30805
32604
  }
@@ -30810,9 +32609,28 @@ async function commitPartialProfitCost(symbol, dollarAmount) {
30810
32609
  const { backtest: isBacktest } = bt.executionContextService.context;
30811
32610
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
30812
32611
  const investedCost = await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
30813
- if (investedCost === null)
32612
+ if (investedCost === null) {
32613
+ return false;
32614
+ }
32615
+ const signalForProfitCost = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32616
+ if (!signalForProfitCost) {
30814
32617
  return false;
32618
+ }
30815
32619
  const percentToClose = investedCostToPercent(dollarAmount, investedCost);
32620
+ if (await not(bt.strategyCoreService.validatePartialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName }))) {
32621
+ return false;
32622
+ }
32623
+ await Broker.commitPartialProfit({
32624
+ symbol,
32625
+ percentToClose,
32626
+ cost: dollarAmount,
32627
+ currentPrice,
32628
+ position: signalForProfitCost.position,
32629
+ priceTakeProfit: signalForProfitCost.priceTakeProfit,
32630
+ priceStopLoss: signalForProfitCost.priceStopLoss,
32631
+ context: { exchangeName, frameName, strategyName },
32632
+ backtest: isBacktest,
32633
+ });
30816
32634
  return await bt.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
30817
32635
  }
30818
32636
  /**
@@ -30845,7 +32663,10 @@ async function commitPartialProfitCost(symbol, dollarAmount) {
30845
32663
  * ```
30846
32664
  */
30847
32665
  async function commitPartialLossCost(symbol, dollarAmount) {
30848
- bt.loggerService.info(PARTIAL_LOSS_COST_METHOD_NAME, { symbol, dollarAmount });
32666
+ bt.loggerService.info(PARTIAL_LOSS_COST_METHOD_NAME, {
32667
+ symbol,
32668
+ dollarAmount,
32669
+ });
30849
32670
  if (!ExecutionContextService.hasContext()) {
30850
32671
  throw new Error("commitPartialLossCost requires an execution context");
30851
32672
  }
@@ -30856,9 +32677,28 @@ async function commitPartialLossCost(symbol, dollarAmount) {
30856
32677
  const { backtest: isBacktest } = bt.executionContextService.context;
30857
32678
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
30858
32679
  const investedCost = await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
30859
- if (investedCost === null)
32680
+ if (investedCost === null) {
30860
32681
  return false;
32682
+ }
32683
+ const signalForLossCost = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32684
+ if (!signalForLossCost) {
32685
+ return false;
32686
+ }
30861
32687
  const percentToClose = investedCostToPercent(dollarAmount, investedCost);
32688
+ if (await not(bt.strategyCoreService.validatePartialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName }))) {
32689
+ return false;
32690
+ }
32691
+ await Broker.commitPartialLoss({
32692
+ symbol,
32693
+ percentToClose,
32694
+ cost: dollarAmount,
32695
+ currentPrice,
32696
+ position: signalForLossCost.position,
32697
+ priceTakeProfit: signalForLossCost.priceTakeProfit,
32698
+ priceStopLoss: signalForLossCost.priceStopLoss,
32699
+ context: { exchangeName, frameName, strategyName },
32700
+ backtest: isBacktest,
32701
+ });
30862
32702
  return await bt.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
30863
32703
  }
30864
32704
  async function getPositionPnlCost(symbol) {
@@ -30907,6 +32747,34 @@ async function getPositionLevels(symbol) {
30907
32747
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
30908
32748
  return await bt.strategyCoreService.getPositionLevels(isBacktest, symbol, { exchangeName, frameName, strategyName });
30909
32749
  }
32750
+ /**
32751
+ * Returns the list of partial close events for the current pending signal.
32752
+ *
32753
+ * Each element represents a partial profit or loss close executed via
32754
+ * commitPartialProfit / commitPartialLoss (or their Cost variants).
32755
+ *
32756
+ * Returns null if no pending signal exists.
32757
+ * Returns an empty array if no partials were executed yet.
32758
+ *
32759
+ * Each entry contains:
32760
+ * - `type` — "profit" or "loss"
32761
+ * - `percent` — percentage of position closed at this partial
32762
+ * - `currentPrice` — execution price of the partial close
32763
+ * - `costBasisAtClose` — accounting cost basis at the moment of this partial
32764
+ * - `entryCountAtClose` — number of DCA entries accumulated at this partial
32765
+ *
32766
+ * @param symbol - Trading pair symbol
32767
+ * @returns Promise resolving to array of partial close records or null
32768
+ *
32769
+ * @example
32770
+ * ```typescript
32771
+ * import { getPositionPartials } from "backtest-kit";
32772
+ *
32773
+ * const partials = await getPositionPartials("BTCUSDT");
32774
+ * // No partials yet: []
32775
+ * // After one partial profit: [{ type: "profit", percent: 50, currentPrice: 45000, ... }]
32776
+ * ```
32777
+ */
30910
32778
  async function getPositionPartials(symbol) {
30911
32779
  bt.loggerService.info(GET_POSITION_PARTIALS_METHOD_NAME, { symbol });
30912
32780
  if (!ExecutionContextService.hasContext()) {
@@ -32153,6 +34021,8 @@ const BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST = "BacktestUtils.commitPartialPro
32153
34021
  const BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST = "BacktestUtils.commitPartialLossCost";
32154
34022
  const BACKTEST_METHOD_NAME_TRAILING_STOP = "BacktestUtils.commitTrailingStop";
32155
34023
  const BACKTEST_METHOD_NAME_TRAILING_PROFIT = "BacktestUtils.commitTrailingTake";
34024
+ const BACKTEST_METHOD_NAME_TRAILING_STOP_COST = "BacktestUtils.commitTrailingStopCost";
34025
+ const BACKTEST_METHOD_NAME_TRAILING_PROFIT_COST = "BacktestUtils.commitTrailingTakeCost";
32156
34026
  const BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED = "Backtest.commitActivateScheduled";
32157
34027
  const BACKTEST_METHOD_NAME_AVERAGE_BUY = "Backtest.commitAverageBuy";
32158
34028
  const BACKTEST_METHOD_NAME_GET_DATA = "BacktestUtils.getData";
@@ -32332,12 +34202,13 @@ class BacktestInstance {
32332
34202
  }
32333
34203
  {
32334
34204
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
32335
- riskName && bt.riskGlobalService.clear({
32336
- riskName,
32337
- exchangeName: context.exchangeName,
32338
- frameName: context.frameName,
32339
- backtest: true,
32340
- });
34205
+ riskName &&
34206
+ bt.riskGlobalService.clear({
34207
+ riskName,
34208
+ exchangeName: context.exchangeName,
34209
+ frameName: context.frameName,
34210
+ backtest: true,
34211
+ });
32341
34212
  riskList &&
32342
34213
  riskList.forEach((riskName) => bt.riskGlobalService.clear({
32343
34214
  riskName,
@@ -32687,6 +34558,16 @@ class BacktestUtils {
32687
34558
  }
32688
34559
  return await bt.strategyCoreService.getBreakeven(true, symbol, currentPrice, context);
32689
34560
  };
34561
+ /**
34562
+ * Returns the effective (weighted average) entry price for the current pending signal.
34563
+ *
34564
+ * Accounts for all DCA entries via commitAverageBuy.
34565
+ * Returns null if no pending signal exists.
34566
+ *
34567
+ * @param symbol - Trading pair symbol
34568
+ * @param context - Execution context with strategyName, exchangeName, and frameName
34569
+ * @returns Effective entry price, or null if no active position
34570
+ */
32690
34571
  this.getPositionAveragePrice = async (symbol, context) => {
32691
34572
  bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE, {
32692
34573
  symbol,
@@ -32705,6 +34586,15 @@ class BacktestUtils {
32705
34586
  }
32706
34587
  return await bt.strategyCoreService.getPositionAveragePrice(true, symbol, context);
32707
34588
  };
34589
+ /**
34590
+ * Returns the total number of base-asset units currently held in the position.
34591
+ *
34592
+ * Includes units from all DCA entries. Returns null if no pending signal exists.
34593
+ *
34594
+ * @param symbol - Trading pair symbol
34595
+ * @param context - Execution context with strategyName, exchangeName, and frameName
34596
+ * @returns Total units held, or null if no active position
34597
+ */
32708
34598
  this.getPositionInvestedCount = async (symbol, context) => {
32709
34599
  bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT, {
32710
34600
  symbol,
@@ -32723,6 +34613,15 @@ class BacktestUtils {
32723
34613
  }
32724
34614
  return await bt.strategyCoreService.getPositionInvestedCount(true, symbol, context);
32725
34615
  };
34616
+ /**
34617
+ * Returns the total dollar cost invested in the current position.
34618
+ *
34619
+ * Sum of all entry costs across DCA entries. Returns null if no pending signal exists.
34620
+ *
34621
+ * @param symbol - Trading pair symbol
34622
+ * @param context - Execution context with strategyName, exchangeName, and frameName
34623
+ * @returns Total invested cost in quote currency, or null if no active position
34624
+ */
32726
34625
  this.getPositionInvestedCost = async (symbol, context) => {
32727
34626
  bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST, {
32728
34627
  symbol,
@@ -32741,6 +34640,17 @@ class BacktestUtils {
32741
34640
  }
32742
34641
  return await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
32743
34642
  };
34643
+ /**
34644
+ * Returns the current unrealized PnL as a percentage of the invested cost.
34645
+ *
34646
+ * Calculated relative to the effective (weighted average) entry price.
34647
+ * Positive for profit, negative for loss. Returns null if no pending signal exists.
34648
+ *
34649
+ * @param symbol - Trading pair symbol
34650
+ * @param currentPrice - Current market price
34651
+ * @param context - Execution context with strategyName, exchangeName, and frameName
34652
+ * @returns PnL percentage, or null if no active position
34653
+ */
32744
34654
  this.getPositionPnlPercent = async (symbol, currentPrice, context) => {
32745
34655
  bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT, {
32746
34656
  symbol,
@@ -32760,6 +34670,17 @@ class BacktestUtils {
32760
34670
  }
32761
34671
  return await bt.strategyCoreService.getPositionPnlPercent(true, symbol, currentPrice, context);
32762
34672
  };
34673
+ /**
34674
+ * Returns the current unrealized PnL in quote currency (dollar amount).
34675
+ *
34676
+ * Calculated as (currentPrice - effectiveEntry) * units for LONG,
34677
+ * reversed for SHORT. Returns null if no pending signal exists.
34678
+ *
34679
+ * @param symbol - Trading pair symbol
34680
+ * @param currentPrice - Current market price
34681
+ * @param context - Execution context with strategyName, exchangeName, and frameName
34682
+ * @returns PnL in quote currency, or null if no active position
34683
+ */
32763
34684
  this.getPositionPnlCost = async (symbol, currentPrice, context) => {
32764
34685
  bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST, {
32765
34686
  symbol,
@@ -32779,6 +34700,18 @@ class BacktestUtils {
32779
34700
  }
32780
34701
  return await bt.strategyCoreService.getPositionPnlCost(true, symbol, currentPrice, context);
32781
34702
  };
34703
+ /**
34704
+ * Returns the list of DCA entry prices for the current pending signal.
34705
+ *
34706
+ * The first element is always the original priceOpen (initial entry).
34707
+ * Each subsequent element is a price added by commitAverageBuy().
34708
+ * Returns null if no pending signal exists.
34709
+ * Returns a single-element array [priceOpen] if no DCA entries were made.
34710
+ *
34711
+ * @param symbol - Trading pair symbol
34712
+ * @param context - Execution context with strategyName, exchangeName, and frameName
34713
+ * @returns Array of entry prices, or null if no active position
34714
+ */
32782
34715
  this.getPositionLevels = async (symbol, context) => {
32783
34716
  bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_LEVELS, {
32784
34717
  symbol,
@@ -32797,6 +34730,25 @@ class BacktestUtils {
32797
34730
  }
32798
34731
  return await bt.strategyCoreService.getPositionLevels(true, symbol, context);
32799
34732
  };
34733
+ /**
34734
+ * Returns the list of partial close events for the current pending signal.
34735
+ *
34736
+ * Each element represents a partial profit or loss close executed via
34737
+ * commitPartialProfit / commitPartialLoss (or their Cost variants).
34738
+ * Returns null if no pending signal exists.
34739
+ * Returns an empty array if no partials were executed yet.
34740
+ *
34741
+ * Each entry contains:
34742
+ * - `type` — "profit" or "loss"
34743
+ * - `percent` — percentage of position closed at this partial
34744
+ * - `currentPrice` — execution price of the partial close
34745
+ * - `costBasisAtClose` — accounting cost basis at the moment of this partial
34746
+ * - `entryCountAtClose` — number of DCA entries accumulated at this partial
34747
+ *
34748
+ * @param symbol - Trading pair symbol
34749
+ * @param context - Execution context with strategyName, exchangeName, and frameName
34750
+ * @returns Array of partial close records, or null if no active position
34751
+ */
32800
34752
  this.getPositionPartials = async (symbol, context) => {
32801
34753
  bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS, {
32802
34754
  symbol,
@@ -32985,6 +34937,28 @@ class BacktestUtils {
32985
34937
  actions &&
32986
34938
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT));
32987
34939
  }
34940
+ const investedCostForProfit = await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
34941
+ if (investedCostForProfit === null) {
34942
+ return false;
34943
+ }
34944
+ const signalForProfit = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
34945
+ if (!signalForProfit) {
34946
+ return false;
34947
+ }
34948
+ if (await not(bt.strategyCoreService.validatePartialProfit(true, symbol, percentToClose, currentPrice, context))) {
34949
+ return false;
34950
+ }
34951
+ await Broker.commitPartialProfit({
34952
+ symbol,
34953
+ percentToClose,
34954
+ cost: percentToCloseCost(percentToClose, investedCostForProfit),
34955
+ currentPrice,
34956
+ position: signalForProfit.position,
34957
+ priceTakeProfit: signalForProfit.priceTakeProfit,
34958
+ priceStopLoss: signalForProfit.priceStopLoss,
34959
+ context,
34960
+ backtest: true,
34961
+ });
32988
34962
  return await bt.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
32989
34963
  };
32990
34964
  /**
@@ -33034,6 +35008,28 @@ class BacktestUtils {
33034
35008
  actions &&
33035
35009
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_PARTIAL_LOSS));
33036
35010
  }
35011
+ const investedCostForLoss = await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
35012
+ if (investedCostForLoss === null) {
35013
+ return false;
35014
+ }
35015
+ const signalForLoss = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35016
+ if (!signalForLoss) {
35017
+ return false;
35018
+ }
35019
+ if (await not(bt.strategyCoreService.validatePartialLoss(true, symbol, percentToClose, currentPrice, context))) {
35020
+ return false;
35021
+ }
35022
+ await Broker.commitPartialLoss({
35023
+ symbol,
35024
+ percentToClose,
35025
+ cost: percentToCloseCost(percentToClose, investedCostForLoss),
35026
+ currentPrice,
35027
+ position: signalForLoss.position,
35028
+ priceTakeProfit: signalForLoss.priceTakeProfit,
35029
+ priceStopLoss: signalForLoss.priceStopLoss,
35030
+ context,
35031
+ backtest: true,
35032
+ });
33037
35033
  return await bt.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
33038
35034
  };
33039
35035
  /**
@@ -33085,9 +35081,28 @@ class BacktestUtils {
33085
35081
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST));
33086
35082
  }
33087
35083
  const investedCost = await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
33088
- if (investedCost === null)
35084
+ if (investedCost === null) {
35085
+ return false;
35086
+ }
35087
+ const signalForProfitCost = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35088
+ if (!signalForProfitCost) {
33089
35089
  return false;
35090
+ }
33090
35091
  const percentToClose = (dollarAmount / investedCost) * 100;
35092
+ if (await not(bt.strategyCoreService.validatePartialProfit(true, symbol, percentToClose, currentPrice, context))) {
35093
+ return false;
35094
+ }
35095
+ await Broker.commitPartialProfit({
35096
+ symbol,
35097
+ percentToClose,
35098
+ cost: dollarAmount,
35099
+ currentPrice,
35100
+ position: signalForProfitCost.position,
35101
+ priceTakeProfit: signalForProfitCost.priceTakeProfit,
35102
+ priceStopLoss: signalForProfitCost.priceStopLoss,
35103
+ context,
35104
+ backtest: true,
35105
+ });
33091
35106
  return await bt.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
33092
35107
  };
33093
35108
  /**
@@ -33139,9 +35154,28 @@ class BacktestUtils {
33139
35154
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST));
33140
35155
  }
33141
35156
  const investedCost = await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
33142
- if (investedCost === null)
35157
+ if (investedCost === null) {
35158
+ return false;
35159
+ }
35160
+ const signalForLossCost = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35161
+ if (!signalForLossCost) {
33143
35162
  return false;
35163
+ }
33144
35164
  const percentToClose = (dollarAmount / investedCost) * 100;
35165
+ if (await not(bt.strategyCoreService.validatePartialLoss(true, symbol, percentToClose, currentPrice, context))) {
35166
+ return false;
35167
+ }
35168
+ await Broker.commitPartialLoss({
35169
+ symbol,
35170
+ percentToClose,
35171
+ cost: dollarAmount,
35172
+ currentPrice,
35173
+ position: signalForLossCost.position,
35174
+ priceTakeProfit: signalForLossCost.priceTakeProfit,
35175
+ priceStopLoss: signalForLossCost.priceStopLoss,
35176
+ context,
35177
+ backtest: true,
35178
+ });
33145
35179
  return await bt.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
33146
35180
  };
33147
35181
  /**
@@ -33206,6 +35240,26 @@ class BacktestUtils {
33206
35240
  actions &&
33207
35241
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_TRAILING_STOP));
33208
35242
  }
35243
+ const signal = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35244
+ if (!signal) {
35245
+ return false;
35246
+ }
35247
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(true, symbol, context);
35248
+ if (effectivePriceOpen === null) {
35249
+ return false;
35250
+ }
35251
+ if (await not(bt.strategyCoreService.validateTrailingStop(true, symbol, percentShift, currentPrice, context))) {
35252
+ return false;
35253
+ }
35254
+ await Broker.commitTrailingStop({
35255
+ symbol,
35256
+ percentShift,
35257
+ currentPrice,
35258
+ newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
35259
+ position: signal.position,
35260
+ context,
35261
+ backtest: true,
35262
+ });
33209
35263
  return await bt.strategyCoreService.trailingStop(true, symbol, percentShift, currentPrice, context);
33210
35264
  };
33211
35265
  /**
@@ -33270,6 +35324,132 @@ class BacktestUtils {
33270
35324
  actions &&
33271
35325
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_TRAILING_PROFIT));
33272
35326
  }
35327
+ const signal = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35328
+ if (!signal) {
35329
+ return false;
35330
+ }
35331
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(true, symbol, context);
35332
+ if (effectivePriceOpen === null) {
35333
+ return false;
35334
+ }
35335
+ if (await not(bt.strategyCoreService.validateTrailingTake(true, symbol, percentShift, currentPrice, context))) {
35336
+ return false;
35337
+ }
35338
+ await Broker.commitTrailingTake({
35339
+ symbol,
35340
+ percentShift,
35341
+ currentPrice,
35342
+ newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
35343
+ position: signal.position,
35344
+ context,
35345
+ backtest: true,
35346
+ });
35347
+ return await bt.strategyCoreService.trailingTake(true, symbol, percentShift, currentPrice, context);
35348
+ };
35349
+ /**
35350
+ * Adjusts the trailing stop-loss to an absolute price level.
35351
+ *
35352
+ * Convenience wrapper around commitTrailingStop that converts an absolute
35353
+ * stop-loss price to a percentShift relative to the ORIGINAL SL distance.
35354
+ *
35355
+ * @param symbol - Trading pair symbol
35356
+ * @param newStopLossPrice - Desired absolute stop-loss price
35357
+ * @param currentPrice - Current market price to check for intrusion
35358
+ * @param context - Execution context with strategyName, exchangeName, and frameName
35359
+ * @returns Promise<boolean> - true if trailing SL was set/updated, false if rejected
35360
+ */
35361
+ this.commitTrailingStopCost = async (symbol, newStopLossPrice, currentPrice, context) => {
35362
+ bt.loggerService.info(BACKTEST_METHOD_NAME_TRAILING_STOP_COST, {
35363
+ symbol,
35364
+ newStopLossPrice,
35365
+ currentPrice,
35366
+ context,
35367
+ });
35368
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_TRAILING_STOP_COST);
35369
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_TRAILING_STOP_COST);
35370
+ {
35371
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
35372
+ riskName &&
35373
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_STOP_COST);
35374
+ riskList &&
35375
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_STOP_COST));
35376
+ actions &&
35377
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_TRAILING_STOP_COST));
35378
+ }
35379
+ const signal = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35380
+ if (!signal) {
35381
+ return false;
35382
+ }
35383
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(true, symbol, context);
35384
+ if (effectivePriceOpen === null) {
35385
+ return false;
35386
+ }
35387
+ const percentShift = slPriceToPercentShift(newStopLossPrice, signal.priceStopLoss, effectivePriceOpen);
35388
+ if (await not(bt.strategyCoreService.validateTrailingStop(true, symbol, percentShift, currentPrice, context))) {
35389
+ return false;
35390
+ }
35391
+ await Broker.commitTrailingStop({
35392
+ symbol,
35393
+ percentShift,
35394
+ currentPrice,
35395
+ newStopLossPrice,
35396
+ position: signal.position,
35397
+ context,
35398
+ backtest: true,
35399
+ });
35400
+ return await bt.strategyCoreService.trailingStop(true, symbol, percentShift, currentPrice, context);
35401
+ };
35402
+ /**
35403
+ * Adjusts the trailing take-profit to an absolute price level.
35404
+ *
35405
+ * Convenience wrapper around commitTrailingTake that converts an absolute
35406
+ * take-profit price to a percentShift relative to the ORIGINAL TP distance.
35407
+ *
35408
+ * @param symbol - Trading pair symbol
35409
+ * @param newTakeProfitPrice - Desired absolute take-profit price
35410
+ * @param currentPrice - Current market price to check for intrusion
35411
+ * @param context - Execution context with strategyName, exchangeName, and frameName
35412
+ * @returns Promise<boolean> - true if trailing TP was set/updated, false if rejected
35413
+ */
35414
+ this.commitTrailingTakeCost = async (symbol, newTakeProfitPrice, currentPrice, context) => {
35415
+ bt.loggerService.info(BACKTEST_METHOD_NAME_TRAILING_PROFIT_COST, {
35416
+ symbol,
35417
+ newTakeProfitPrice,
35418
+ currentPrice,
35419
+ context,
35420
+ });
35421
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_TRAILING_PROFIT_COST);
35422
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_TRAILING_PROFIT_COST);
35423
+ {
35424
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
35425
+ riskName &&
35426
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_PROFIT_COST);
35427
+ riskList &&
35428
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_PROFIT_COST));
35429
+ actions &&
35430
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_TRAILING_PROFIT_COST));
35431
+ }
35432
+ const signal = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35433
+ if (!signal) {
35434
+ return false;
35435
+ }
35436
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(true, symbol, context);
35437
+ if (effectivePriceOpen === null) {
35438
+ return false;
35439
+ }
35440
+ const percentShift = tpPriceToPercentShift(newTakeProfitPrice, signal.priceTakeProfit, effectivePriceOpen);
35441
+ if (await not(bt.strategyCoreService.validateTrailingTake(true, symbol, percentShift, currentPrice, context))) {
35442
+ return false;
35443
+ }
35444
+ await Broker.commitTrailingTake({
35445
+ symbol,
35446
+ percentShift,
35447
+ currentPrice,
35448
+ newTakeProfitPrice,
35449
+ position: signal.position,
35450
+ context,
35451
+ backtest: true,
35452
+ });
33273
35453
  return await bt.strategyCoreService.trailingTake(true, symbol, percentShift, currentPrice, context);
33274
35454
  };
33275
35455
  /**
@@ -33310,6 +35490,26 @@ class BacktestUtils {
33310
35490
  actions &&
33311
35491
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_BREAKEVEN));
33312
35492
  }
35493
+ const signal = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35494
+ if (!signal) {
35495
+ return false;
35496
+ }
35497
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(true, symbol, context);
35498
+ if (effectivePriceOpen === null) {
35499
+ return false;
35500
+ }
35501
+ if (await not(bt.strategyCoreService.validateBreakeven(true, symbol, currentPrice, context))) {
35502
+ return false;
35503
+ }
35504
+ await Broker.commitBreakeven({
35505
+ symbol,
35506
+ currentPrice,
35507
+ newStopLossPrice: breakevenNewStopLossPrice(effectivePriceOpen),
35508
+ newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
35509
+ position: signal.position,
35510
+ context,
35511
+ backtest: true,
35512
+ });
33313
35513
  return await bt.strategyCoreService.breakeven(true, symbol, currentPrice, context);
33314
35514
  };
33315
35515
  /**
@@ -33393,6 +35593,23 @@ class BacktestUtils {
33393
35593
  actions &&
33394
35594
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_AVERAGE_BUY));
33395
35595
  }
35596
+ if (await not(bt.strategyCoreService.validateAverageBuy(true, symbol, currentPrice, context))) {
35597
+ return false;
35598
+ }
35599
+ const signalForAvgBuy = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35600
+ if (!signalForAvgBuy) {
35601
+ return false;
35602
+ }
35603
+ await Broker.commitAverageBuy({
35604
+ symbol,
35605
+ currentPrice,
35606
+ cost,
35607
+ position: signalForAvgBuy.position,
35608
+ priceTakeProfit: signalForAvgBuy.priceTakeProfit,
35609
+ priceStopLoss: signalForAvgBuy.priceStopLoss,
35610
+ context,
35611
+ backtest: true,
35612
+ });
33396
35613
  return await bt.strategyCoreService.averageBuy(true, symbol, currentPrice, context, cost);
33397
35614
  };
33398
35615
  /**
@@ -33581,6 +35798,8 @@ const LIVE_METHOD_NAME_PARTIAL_PROFIT_COST = "LiveUtils.commitPartialProfitCost"
33581
35798
  const LIVE_METHOD_NAME_PARTIAL_LOSS_COST = "LiveUtils.commitPartialLossCost";
33582
35799
  const LIVE_METHOD_NAME_TRAILING_STOP = "LiveUtils.commitTrailingStop";
33583
35800
  const LIVE_METHOD_NAME_TRAILING_PROFIT = "LiveUtils.commitTrailingTake";
35801
+ const LIVE_METHOD_NAME_TRAILING_STOP_COST = "LiveUtils.commitTrailingStopCost";
35802
+ const LIVE_METHOD_NAME_TRAILING_PROFIT_COST = "LiveUtils.commitTrailingTakeCost";
33584
35803
  const LIVE_METHOD_NAME_ACTIVATE_SCHEDULED = "Live.commitActivateScheduled";
33585
35804
  const LIVE_METHOD_NAME_AVERAGE_BUY = "Live.commitAverageBuy";
33586
35805
  /**
@@ -33707,43 +35926,82 @@ class LiveInstance {
33707
35926
  context,
33708
35927
  });
33709
35928
  {
33710
- bt.backtestMarkdownService.clear({ symbol, strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "", backtest: false });
33711
- bt.liveMarkdownService.clear({ symbol, strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "", backtest: false });
33712
- bt.scheduleMarkdownService.clear({ symbol, strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "", backtest: false });
33713
- bt.performanceMarkdownService.clear({ symbol, strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "", backtest: false });
33714
- bt.partialMarkdownService.clear({ symbol, strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "", backtest: false });
33715
- bt.riskMarkdownService.clear({ symbol, strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "", backtest: false });
33716
- }
33717
- {
33718
- bt.strategyCoreService.clear({
35929
+ bt.backtestMarkdownService.clear({
33719
35930
  symbol,
33720
35931
  strategyName: context.strategyName,
33721
35932
  exchangeName: context.exchangeName,
33722
35933
  frameName: "",
33723
35934
  backtest: false,
33724
35935
  });
33725
- }
33726
- {
33727
- const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33728
- riskName && bt.riskGlobalService.clear({
33729
- riskName,
35936
+ bt.liveMarkdownService.clear({
35937
+ symbol,
35938
+ strategyName: context.strategyName,
33730
35939
  exchangeName: context.exchangeName,
33731
35940
  frameName: "",
33732
- backtest: false
35941
+ backtest: false,
33733
35942
  });
33734
- riskList && riskList.forEach((riskName) => bt.riskGlobalService.clear({
33735
- riskName,
35943
+ bt.scheduleMarkdownService.clear({
35944
+ symbol,
35945
+ strategyName: context.strategyName,
35946
+ exchangeName: context.exchangeName,
35947
+ frameName: "",
35948
+ backtest: false,
35949
+ });
35950
+ bt.performanceMarkdownService.clear({
35951
+ symbol,
35952
+ strategyName: context.strategyName,
33736
35953
  exchangeName: context.exchangeName,
33737
35954
  frameName: "",
33738
- backtest: false
33739
- }));
33740
- actions && actions.forEach((actionName) => bt.actionCoreService.clear({
33741
- actionName,
35955
+ backtest: false,
35956
+ });
35957
+ bt.partialMarkdownService.clear({
35958
+ symbol,
35959
+ strategyName: context.strategyName,
35960
+ exchangeName: context.exchangeName,
35961
+ frameName: "",
35962
+ backtest: false,
35963
+ });
35964
+ bt.riskMarkdownService.clear({
35965
+ symbol,
33742
35966
  strategyName: context.strategyName,
33743
35967
  exchangeName: context.exchangeName,
33744
35968
  frameName: "",
33745
- backtest: false
33746
- }));
35969
+ backtest: false,
35970
+ });
35971
+ }
35972
+ {
35973
+ bt.strategyCoreService.clear({
35974
+ symbol,
35975
+ strategyName: context.strategyName,
35976
+ exchangeName: context.exchangeName,
35977
+ frameName: "",
35978
+ backtest: false,
35979
+ });
35980
+ }
35981
+ {
35982
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
35983
+ riskName &&
35984
+ bt.riskGlobalService.clear({
35985
+ riskName,
35986
+ exchangeName: context.exchangeName,
35987
+ frameName: "",
35988
+ backtest: false,
35989
+ });
35990
+ riskList &&
35991
+ riskList.forEach((riskName) => bt.riskGlobalService.clear({
35992
+ riskName,
35993
+ exchangeName: context.exchangeName,
35994
+ frameName: "",
35995
+ backtest: false,
35996
+ }));
35997
+ actions &&
35998
+ actions.forEach((actionName) => bt.actionCoreService.clear({
35999
+ actionName,
36000
+ strategyName: context.strategyName,
36001
+ exchangeName: context.exchangeName,
36002
+ frameName: "",
36003
+ backtest: false,
36004
+ }));
33747
36005
  }
33748
36006
  return bt.liveCommandService.run(symbol, context);
33749
36007
  };
@@ -33786,7 +36044,7 @@ class LiveInstance {
33786
36044
  bt.strategyCoreService.stopStrategy(false, symbol, {
33787
36045
  strategyName: context.strategyName,
33788
36046
  exchangeName: context.exchangeName,
33789
- frameName: ""
36047
+ frameName: "",
33790
36048
  });
33791
36049
  bt.strategyCoreService
33792
36050
  .hasPendingSignal(false, symbol, {
@@ -33867,9 +36125,12 @@ class LiveUtils {
33867
36125
  }
33868
36126
  {
33869
36127
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33870
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_RUN);
33871
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_RUN));
33872
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_RUN));
36128
+ riskName &&
36129
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_RUN);
36130
+ riskList &&
36131
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_RUN));
36132
+ actions &&
36133
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_RUN));
33873
36134
  }
33874
36135
  const instance = this._getInstance(symbol, context.strategyName, context.exchangeName);
33875
36136
  return instance.run(symbol, context);
@@ -33900,9 +36161,12 @@ class LiveUtils {
33900
36161
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_BACKGROUND);
33901
36162
  {
33902
36163
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33903
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BACKGROUND);
33904
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BACKGROUND));
33905
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_BACKGROUND));
36164
+ riskName &&
36165
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BACKGROUND);
36166
+ riskList &&
36167
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BACKGROUND));
36168
+ actions &&
36169
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_BACKGROUND));
33906
36170
  }
33907
36171
  const instance = this._getInstance(symbol, context.strategyName, context.exchangeName);
33908
36172
  return instance.background(symbol, context);
@@ -33932,9 +36196,12 @@ class LiveUtils {
33932
36196
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL);
33933
36197
  {
33934
36198
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33935
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL);
33936
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL));
33937
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL));
36199
+ riskName &&
36200
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL);
36201
+ riskList &&
36202
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL));
36203
+ actions &&
36204
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL));
33938
36205
  }
33939
36206
  return await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
33940
36207
  strategyName: context.strategyName,
@@ -33966,9 +36233,12 @@ class LiveUtils {
33966
36233
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
33967
36234
  {
33968
36235
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33969
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
33970
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
33971
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
36236
+ riskName &&
36237
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
36238
+ riskList &&
36239
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
36240
+ actions &&
36241
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
33972
36242
  }
33973
36243
  return await bt.strategyCoreService.getTotalPercentClosed(false, symbol, {
33974
36244
  strategyName: context.strategyName,
@@ -33999,9 +36269,12 @@ class LiveUtils {
33999
36269
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
34000
36270
  {
34001
36271
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34002
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
34003
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
34004
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
36272
+ riskName &&
36273
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
36274
+ riskList &&
36275
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
36276
+ actions &&
36277
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
34005
36278
  }
34006
36279
  return await bt.strategyCoreService.getTotalCostClosed(false, symbol, {
34007
36280
  strategyName: context.strategyName,
@@ -34034,9 +36307,12 @@ class LiveUtils {
34034
36307
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL);
34035
36308
  {
34036
36309
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34037
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL);
34038
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL));
34039
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL));
36310
+ riskName &&
36311
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL);
36312
+ riskList &&
36313
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL));
36314
+ actions &&
36315
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL));
34040
36316
  }
34041
36317
  return await bt.strategyCoreService.getScheduledSignal(false, symbol, currentPrice, {
34042
36318
  strategyName: context.strategyName,
@@ -34077,9 +36353,12 @@ class LiveUtils {
34077
36353
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_BREAKEVEN);
34078
36354
  {
34079
36355
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34080
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_BREAKEVEN);
34081
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_BREAKEVEN));
34082
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_BREAKEVEN));
36356
+ riskName &&
36357
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_BREAKEVEN);
36358
+ riskList &&
36359
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_BREAKEVEN));
36360
+ actions &&
36361
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_BREAKEVEN));
34083
36362
  }
34084
36363
  return await bt.strategyCoreService.getBreakeven(false, symbol, currentPrice, {
34085
36364
  strategyName: context.strategyName,
@@ -34087,15 +36366,31 @@ class LiveUtils {
34087
36366
  frameName: "",
34088
36367
  });
34089
36368
  };
36369
+ /**
36370
+ * Returns the effective (weighted average) entry price for the current pending signal.
36371
+ *
36372
+ * Accounts for all DCA entries via commitAverageBuy.
36373
+ * Returns null if no pending signal exists.
36374
+ *
36375
+ * @param symbol - Trading pair symbol
36376
+ * @param context - Execution context with strategyName and exchangeName
36377
+ * @returns Effective entry price, or null if no active position
36378
+ */
34090
36379
  this.getPositionAveragePrice = async (symbol, context) => {
34091
- bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE, { symbol, context });
36380
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE, {
36381
+ symbol,
36382
+ context,
36383
+ });
34092
36384
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
34093
36385
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
34094
36386
  {
34095
36387
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34096
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
34097
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
34098
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
36388
+ riskName &&
36389
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
36390
+ riskList &&
36391
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
36392
+ actions &&
36393
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
34099
36394
  }
34100
36395
  return await bt.strategyCoreService.getPositionAveragePrice(false, symbol, {
34101
36396
  strategyName: context.strategyName,
@@ -34103,15 +36398,30 @@ class LiveUtils {
34103
36398
  frameName: "",
34104
36399
  });
34105
36400
  };
36401
+ /**
36402
+ * Returns the total number of base-asset units currently held in the position.
36403
+ *
36404
+ * Includes units from all DCA entries. Returns null if no pending signal exists.
36405
+ *
36406
+ * @param symbol - Trading pair symbol
36407
+ * @param context - Execution context with strategyName and exchangeName
36408
+ * @returns Total units held, or null if no active position
36409
+ */
34106
36410
  this.getPositionInvestedCount = async (symbol, context) => {
34107
- bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT, { symbol, context });
36411
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT, {
36412
+ symbol,
36413
+ context,
36414
+ });
34108
36415
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
34109
36416
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
34110
36417
  {
34111
36418
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34112
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
34113
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
34114
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
36419
+ riskName &&
36420
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
36421
+ riskList &&
36422
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
36423
+ actions &&
36424
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
34115
36425
  }
34116
36426
  return await bt.strategyCoreService.getPositionInvestedCount(false, symbol, {
34117
36427
  strategyName: context.strategyName,
@@ -34119,15 +36429,30 @@ class LiveUtils {
34119
36429
  frameName: "",
34120
36430
  });
34121
36431
  };
36432
+ /**
36433
+ * Returns the total dollar cost invested in the current position.
36434
+ *
36435
+ * Sum of all entry costs across DCA entries. Returns null if no pending signal exists.
36436
+ *
36437
+ * @param symbol - Trading pair symbol
36438
+ * @param context - Execution context with strategyName and exchangeName
36439
+ * @returns Total invested cost in quote currency, or null if no active position
36440
+ */
34122
36441
  this.getPositionInvestedCost = async (symbol, context) => {
34123
- bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST, { symbol, context });
36442
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST, {
36443
+ symbol,
36444
+ context,
36445
+ });
34124
36446
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
34125
36447
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
34126
36448
  {
34127
36449
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34128
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
34129
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST));
34130
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST));
36450
+ riskName &&
36451
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
36452
+ riskList &&
36453
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST));
36454
+ actions &&
36455
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST));
34131
36456
  }
34132
36457
  return await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
34133
36458
  strategyName: context.strategyName,
@@ -34135,15 +36460,33 @@ class LiveUtils {
34135
36460
  frameName: "",
34136
36461
  });
34137
36462
  };
36463
+ /**
36464
+ * Returns the current unrealized PnL as a percentage of the invested cost.
36465
+ *
36466
+ * Calculated relative to the effective (weighted average) entry price.
36467
+ * Positive for profit, negative for loss. Returns null if no pending signal exists.
36468
+ *
36469
+ * @param symbol - Trading pair symbol
36470
+ * @param currentPrice - Current market price
36471
+ * @param context - Execution context with strategyName and exchangeName
36472
+ * @returns PnL percentage, or null if no active position
36473
+ */
34138
36474
  this.getPositionPnlPercent = async (symbol, currentPrice, context) => {
34139
- bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT, { symbol, currentPrice, context });
36475
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT, {
36476
+ symbol,
36477
+ currentPrice,
36478
+ context,
36479
+ });
34140
36480
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
34141
36481
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
34142
36482
  {
34143
36483
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34144
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
34145
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT));
34146
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT));
36484
+ riskName &&
36485
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
36486
+ riskList &&
36487
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT));
36488
+ actions &&
36489
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT));
34147
36490
  }
34148
36491
  return await bt.strategyCoreService.getPositionPnlPercent(false, symbol, currentPrice, {
34149
36492
  strategyName: context.strategyName,
@@ -34151,15 +36494,33 @@ class LiveUtils {
34151
36494
  frameName: "",
34152
36495
  });
34153
36496
  };
36497
+ /**
36498
+ * Returns the current unrealized PnL in quote currency (dollar amount).
36499
+ *
36500
+ * Calculated as (currentPrice - effectiveEntry) * units for LONG,
36501
+ * reversed for SHORT. Returns null if no pending signal exists.
36502
+ *
36503
+ * @param symbol - Trading pair symbol
36504
+ * @param currentPrice - Current market price
36505
+ * @param context - Execution context with strategyName and exchangeName
36506
+ * @returns PnL in quote currency, or null if no active position
36507
+ */
34154
36508
  this.getPositionPnlCost = async (symbol, currentPrice, context) => {
34155
- bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PNL_COST, { symbol, currentPrice, context });
36509
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PNL_COST, {
36510
+ symbol,
36511
+ currentPrice,
36512
+ context,
36513
+ });
34156
36514
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
34157
36515
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
34158
36516
  {
34159
36517
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34160
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
34161
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST));
34162
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST));
36518
+ riskName &&
36519
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
36520
+ riskList &&
36521
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST));
36522
+ actions &&
36523
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST));
34163
36524
  }
34164
36525
  return await bt.strategyCoreService.getPositionPnlCost(false, symbol, currentPrice, {
34165
36526
  strategyName: context.strategyName,
@@ -34167,15 +36528,33 @@ class LiveUtils {
34167
36528
  frameName: "",
34168
36529
  });
34169
36530
  };
36531
+ /**
36532
+ * Returns the list of DCA entry prices for the current pending signal.
36533
+ *
36534
+ * The first element is always the original priceOpen (initial entry).
36535
+ * Each subsequent element is a price added by commitAverageBuy().
36536
+ * Returns null if no pending signal exists.
36537
+ * Returns a single-element array [priceOpen] if no DCA entries were made.
36538
+ *
36539
+ * @param symbol - Trading pair symbol
36540
+ * @param context - Execution context with strategyName and exchangeName
36541
+ * @returns Array of entry prices, or null if no active position
36542
+ */
34170
36543
  this.getPositionLevels = async (symbol, context) => {
34171
- bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_LEVELS, { symbol, context });
36544
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_LEVELS, {
36545
+ symbol,
36546
+ context,
36547
+ });
34172
36548
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
34173
36549
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
34174
36550
  {
34175
36551
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34176
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
34177
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_LEVELS));
34178
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_LEVELS));
36552
+ riskName &&
36553
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
36554
+ riskList &&
36555
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_LEVELS));
36556
+ actions &&
36557
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_LEVELS));
34179
36558
  }
34180
36559
  return await bt.strategyCoreService.getPositionLevels(false, symbol, {
34181
36560
  strategyName: context.strategyName,
@@ -34183,15 +36562,40 @@ class LiveUtils {
34183
36562
  frameName: "",
34184
36563
  });
34185
36564
  };
36565
+ /**
36566
+ * Returns the list of partial close events for the current pending signal.
36567
+ *
36568
+ * Each element represents a partial profit or loss close executed via
36569
+ * commitPartialProfit / commitPartialLoss (or their Cost variants).
36570
+ * Returns null if no pending signal exists.
36571
+ * Returns an empty array if no partials were executed yet.
36572
+ *
36573
+ * Each entry contains:
36574
+ * - `type` — "profit" or "loss"
36575
+ * - `percent` — percentage of position closed at this partial
36576
+ * - `currentPrice` — execution price of the partial close
36577
+ * - `costBasisAtClose` — accounting cost basis at the moment of this partial
36578
+ * - `entryCountAtClose` — number of DCA entries accumulated at this partial
36579
+ *
36580
+ * @param symbol - Trading pair symbol
36581
+ * @param context - Execution context with strategyName and exchangeName
36582
+ * @returns Array of partial close records, or null if no active position
36583
+ */
34186
36584
  this.getPositionPartials = async (symbol, context) => {
34187
- bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PARTIALS, { symbol, context });
36585
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PARTIALS, {
36586
+ symbol,
36587
+ context,
36588
+ });
34188
36589
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
34189
36590
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
34190
36591
  {
34191
36592
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34192
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
34193
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS));
34194
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS));
36593
+ riskName &&
36594
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
36595
+ riskList &&
36596
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS));
36597
+ actions &&
36598
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS));
34195
36599
  }
34196
36600
  return await bt.strategyCoreService.getPositionPartials(false, symbol, {
34197
36601
  strategyName: context.strategyName,
@@ -34225,9 +36629,12 @@ class LiveUtils {
34225
36629
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_STOP);
34226
36630
  {
34227
36631
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34228
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_STOP);
34229
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_STOP));
34230
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_STOP));
36632
+ riskName &&
36633
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_STOP);
36634
+ riskList &&
36635
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_STOP));
36636
+ actions &&
36637
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_STOP));
34231
36638
  }
34232
36639
  await bt.strategyCoreService.stopStrategy(false, symbol, {
34233
36640
  strategyName: context.strategyName,
@@ -34268,9 +36675,12 @@ class LiveUtils {
34268
36675
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_CANCEL_SCHEDULED);
34269
36676
  {
34270
36677
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34271
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CANCEL_SCHEDULED);
34272
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CANCEL_SCHEDULED));
34273
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_CANCEL_SCHEDULED));
36678
+ riskName &&
36679
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CANCEL_SCHEDULED);
36680
+ riskList &&
36681
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CANCEL_SCHEDULED));
36682
+ actions &&
36683
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_CANCEL_SCHEDULED));
34274
36684
  }
34275
36685
  await bt.strategyCoreService.cancelScheduled(false, symbol, {
34276
36686
  strategyName: context.strategyName,
@@ -34309,9 +36719,12 @@ class LiveUtils {
34309
36719
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_CLOSE_PENDING);
34310
36720
  {
34311
36721
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34312
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CLOSE_PENDING);
34313
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CLOSE_PENDING));
34314
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_CLOSE_PENDING));
36722
+ riskName &&
36723
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CLOSE_PENDING);
36724
+ riskList &&
36725
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CLOSE_PENDING));
36726
+ actions &&
36727
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_CLOSE_PENDING));
34315
36728
  }
34316
36729
  await bt.strategyCoreService.closePending(false, symbol, {
34317
36730
  strategyName: context.strategyName,
@@ -34358,10 +36771,51 @@ class LiveUtils {
34358
36771
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
34359
36772
  {
34360
36773
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34361
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
34362
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT));
34363
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_PROFIT));
36774
+ riskName &&
36775
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
36776
+ riskList &&
36777
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT));
36778
+ actions &&
36779
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_PROFIT));
36780
+ }
36781
+ const investedCost = await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
36782
+ strategyName: context.strategyName,
36783
+ exchangeName: context.exchangeName,
36784
+ frameName: "",
36785
+ });
36786
+ if (investedCost === null) {
36787
+ return false;
36788
+ }
36789
+ const signalForProfit = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
36790
+ strategyName: context.strategyName,
36791
+ exchangeName: context.exchangeName,
36792
+ frameName: "",
36793
+ });
36794
+ if (!signalForProfit) {
36795
+ return false;
36796
+ }
36797
+ if (await not(bt.strategyCoreService.validatePartialProfit(false, symbol, percentToClose, currentPrice, {
36798
+ strategyName: context.strategyName,
36799
+ exchangeName: context.exchangeName,
36800
+ frameName: "",
36801
+ }))) {
36802
+ return false;
34364
36803
  }
36804
+ await Broker.commitPartialProfit({
36805
+ symbol,
36806
+ percentToClose,
36807
+ cost: percentToCloseCost(percentToClose, investedCost),
36808
+ currentPrice,
36809
+ position: signalForProfit.position,
36810
+ priceTakeProfit: signalForProfit.priceTakeProfit,
36811
+ priceStopLoss: signalForProfit.priceStopLoss,
36812
+ context: {
36813
+ strategyName: context.strategyName,
36814
+ exchangeName: context.exchangeName,
36815
+ frameName: "",
36816
+ },
36817
+ backtest: false,
36818
+ });
34365
36819
  return await bt.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
34366
36820
  strategyName: context.strategyName,
34367
36821
  exchangeName: context.exchangeName,
@@ -34407,10 +36861,51 @@ class LiveUtils {
34407
36861
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_LOSS);
34408
36862
  {
34409
36863
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34410
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS);
34411
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS));
34412
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_LOSS));
36864
+ riskName &&
36865
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS);
36866
+ riskList &&
36867
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS));
36868
+ actions &&
36869
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_LOSS));
36870
+ }
36871
+ const investedCost = await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
36872
+ strategyName: context.strategyName,
36873
+ exchangeName: context.exchangeName,
36874
+ frameName: "",
36875
+ });
36876
+ if (investedCost === null) {
36877
+ return false;
36878
+ }
36879
+ const signalForLoss = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
36880
+ strategyName: context.strategyName,
36881
+ exchangeName: context.exchangeName,
36882
+ frameName: "",
36883
+ });
36884
+ if (!signalForLoss) {
36885
+ return false;
34413
36886
  }
36887
+ if (await not(bt.strategyCoreService.validatePartialLoss(false, symbol, percentToClose, currentPrice, {
36888
+ strategyName: context.strategyName,
36889
+ exchangeName: context.exchangeName,
36890
+ frameName: "",
36891
+ }))) {
36892
+ return false;
36893
+ }
36894
+ await Broker.commitPartialLoss({
36895
+ symbol,
36896
+ percentToClose,
36897
+ cost: percentToCloseCost(percentToClose, investedCost),
36898
+ currentPrice,
36899
+ position: signalForLoss.position,
36900
+ priceTakeProfit: signalForLoss.priceTakeProfit,
36901
+ priceStopLoss: signalForLoss.priceStopLoss,
36902
+ context: {
36903
+ strategyName: context.strategyName,
36904
+ exchangeName: context.exchangeName,
36905
+ frameName: "",
36906
+ },
36907
+ backtest: false,
36908
+ });
34414
36909
  return await bt.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
34415
36910
  strategyName: context.strategyName,
34416
36911
  exchangeName: context.exchangeName,
@@ -34447,23 +36942,62 @@ class LiveUtils {
34447
36942
  * ```
34448
36943
  */
34449
36944
  this.commitPartialProfitCost = async (symbol, dollarAmount, currentPrice, context) => {
34450
- bt.loggerService.info(LIVE_METHOD_NAME_PARTIAL_PROFIT_COST, { symbol, dollarAmount, currentPrice, context });
36945
+ bt.loggerService.info(LIVE_METHOD_NAME_PARTIAL_PROFIT_COST, {
36946
+ symbol,
36947
+ dollarAmount,
36948
+ currentPrice,
36949
+ context,
36950
+ });
34451
36951
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
34452
36952
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
34453
36953
  {
34454
36954
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34455
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
34456
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST));
34457
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST));
36955
+ riskName &&
36956
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
36957
+ riskList &&
36958
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST));
36959
+ actions &&
36960
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST));
34458
36961
  }
34459
36962
  const investedCost = await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
34460
36963
  strategyName: context.strategyName,
34461
36964
  exchangeName: context.exchangeName,
34462
36965
  frameName: "",
34463
36966
  });
34464
- if (investedCost === null)
36967
+ if (investedCost === null) {
36968
+ return false;
36969
+ }
36970
+ const signalForProfitCost = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
36971
+ strategyName: context.strategyName,
36972
+ exchangeName: context.exchangeName,
36973
+ frameName: "",
36974
+ });
36975
+ if (!signalForProfitCost) {
34465
36976
  return false;
36977
+ }
34466
36978
  const percentToClose = (dollarAmount / investedCost) * 100;
36979
+ if (await not(bt.strategyCoreService.validatePartialProfit(false, symbol, percentToClose, currentPrice, {
36980
+ strategyName: context.strategyName,
36981
+ exchangeName: context.exchangeName,
36982
+ frameName: "",
36983
+ }))) {
36984
+ return false;
36985
+ }
36986
+ await Broker.commitPartialProfit({
36987
+ symbol,
36988
+ percentToClose,
36989
+ cost: dollarAmount,
36990
+ currentPrice,
36991
+ position: signalForProfitCost.position,
36992
+ priceTakeProfit: signalForProfitCost.priceTakeProfit,
36993
+ priceStopLoss: signalForProfitCost.priceStopLoss,
36994
+ context: {
36995
+ strategyName: context.strategyName,
36996
+ exchangeName: context.exchangeName,
36997
+ frameName: "",
36998
+ },
36999
+ backtest: false,
37000
+ });
34467
37001
  return await bt.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
34468
37002
  strategyName: context.strategyName,
34469
37003
  exchangeName: context.exchangeName,
@@ -34500,23 +37034,62 @@ class LiveUtils {
34500
37034
  * ```
34501
37035
  */
34502
37036
  this.commitPartialLossCost = async (symbol, dollarAmount, currentPrice, context) => {
34503
- bt.loggerService.info(LIVE_METHOD_NAME_PARTIAL_LOSS_COST, { symbol, dollarAmount, currentPrice, context });
37037
+ bt.loggerService.info(LIVE_METHOD_NAME_PARTIAL_LOSS_COST, {
37038
+ symbol,
37039
+ dollarAmount,
37040
+ currentPrice,
37041
+ context,
37042
+ });
34504
37043
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
34505
37044
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
34506
37045
  {
34507
37046
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34508
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
34509
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST));
34510
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST));
37047
+ riskName &&
37048
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
37049
+ riskList &&
37050
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST));
37051
+ actions &&
37052
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST));
34511
37053
  }
34512
37054
  const investedCost = await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
34513
37055
  strategyName: context.strategyName,
34514
37056
  exchangeName: context.exchangeName,
34515
37057
  frameName: "",
34516
37058
  });
34517
- if (investedCost === null)
37059
+ if (investedCost === null) {
37060
+ return false;
37061
+ }
37062
+ const signalForLossCost = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
37063
+ strategyName: context.strategyName,
37064
+ exchangeName: context.exchangeName,
37065
+ frameName: "",
37066
+ });
37067
+ if (!signalForLossCost) {
34518
37068
  return false;
37069
+ }
34519
37070
  const percentToClose = (dollarAmount / investedCost) * 100;
37071
+ if (await not(bt.strategyCoreService.validatePartialLoss(false, symbol, percentToClose, currentPrice, {
37072
+ strategyName: context.strategyName,
37073
+ exchangeName: context.exchangeName,
37074
+ frameName: "",
37075
+ }))) {
37076
+ return false;
37077
+ }
37078
+ await Broker.commitPartialLoss({
37079
+ symbol,
37080
+ percentToClose,
37081
+ cost: dollarAmount,
37082
+ currentPrice,
37083
+ position: signalForLossCost.position,
37084
+ priceTakeProfit: signalForLossCost.priceTakeProfit,
37085
+ priceStopLoss: signalForLossCost.priceStopLoss,
37086
+ context: {
37087
+ strategyName: context.strategyName,
37088
+ exchangeName: context.exchangeName,
37089
+ frameName: "",
37090
+ },
37091
+ backtest: false,
37092
+ });
34520
37093
  return await bt.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
34521
37094
  strategyName: context.strategyName,
34522
37095
  exchangeName: context.exchangeName,
@@ -34577,10 +37150,45 @@ class LiveUtils {
34577
37150
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_TRAILING_STOP);
34578
37151
  {
34579
37152
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34580
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP);
34581
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP));
34582
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_TRAILING_STOP));
37153
+ riskName &&
37154
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP);
37155
+ riskList &&
37156
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP));
37157
+ actions &&
37158
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_TRAILING_STOP));
34583
37159
  }
37160
+ const signal = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
37161
+ strategyName: context.strategyName,
37162
+ exchangeName: context.exchangeName,
37163
+ frameName: "",
37164
+ });
37165
+ if (!signal) {
37166
+ return false;
37167
+ }
37168
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(false, symbol, {
37169
+ strategyName: context.strategyName,
37170
+ exchangeName: context.exchangeName,
37171
+ frameName: "",
37172
+ });
37173
+ if (effectivePriceOpen === null) {
37174
+ return false;
37175
+ }
37176
+ if (await not(bt.strategyCoreService.validateTrailingStop(false, symbol, percentShift, currentPrice, {
37177
+ strategyName: context.strategyName,
37178
+ exchangeName: context.exchangeName,
37179
+ frameName: "",
37180
+ }))) {
37181
+ return false;
37182
+ }
37183
+ await Broker.commitTrailingStop({
37184
+ symbol,
37185
+ percentShift,
37186
+ currentPrice,
37187
+ newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
37188
+ position: signal.position,
37189
+ context,
37190
+ backtest: false,
37191
+ });
34584
37192
  return await bt.strategyCoreService.trailingStop(false, symbol, percentShift, currentPrice, {
34585
37193
  strategyName: context.strategyName,
34586
37194
  exchangeName: context.exchangeName,
@@ -34641,10 +37249,183 @@ class LiveUtils {
34641
37249
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_TRAILING_PROFIT);
34642
37250
  {
34643
37251
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34644
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT);
34645
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT));
34646
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_TRAILING_PROFIT));
37252
+ riskName &&
37253
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT);
37254
+ riskList &&
37255
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT));
37256
+ actions &&
37257
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_TRAILING_PROFIT));
37258
+ }
37259
+ const signal = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
37260
+ strategyName: context.strategyName,
37261
+ exchangeName: context.exchangeName,
37262
+ frameName: "",
37263
+ });
37264
+ if (!signal) {
37265
+ return false;
37266
+ }
37267
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(false, symbol, {
37268
+ strategyName: context.strategyName,
37269
+ exchangeName: context.exchangeName,
37270
+ frameName: "",
37271
+ });
37272
+ if (effectivePriceOpen === null) {
37273
+ return false;
34647
37274
  }
37275
+ if (await not(bt.strategyCoreService.validateTrailingTake(false, symbol, percentShift, currentPrice, {
37276
+ strategyName: context.strategyName,
37277
+ exchangeName: context.exchangeName,
37278
+ frameName: "",
37279
+ }))) {
37280
+ return false;
37281
+ }
37282
+ await Broker.commitTrailingTake({
37283
+ symbol,
37284
+ percentShift,
37285
+ currentPrice,
37286
+ newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
37287
+ position: signal.position,
37288
+ context,
37289
+ backtest: false,
37290
+ });
37291
+ return await bt.strategyCoreService.trailingTake(false, symbol, percentShift, currentPrice, {
37292
+ strategyName: context.strategyName,
37293
+ exchangeName: context.exchangeName,
37294
+ frameName: "",
37295
+ });
37296
+ };
37297
+ /**
37298
+ * Adjusts the trailing stop-loss to an absolute price level.
37299
+ *
37300
+ * Convenience wrapper around commitTrailingStop that converts an absolute
37301
+ * stop-loss price to a percentShift relative to the ORIGINAL SL distance.
37302
+ *
37303
+ * @param symbol - Trading pair symbol
37304
+ * @param newStopLossPrice - Desired absolute stop-loss price
37305
+ * @param currentPrice - Current market price to check for intrusion
37306
+ * @param context - Execution context with strategyName and exchangeName
37307
+ * @returns Promise<boolean> - true if trailing SL was set/updated, false if rejected
37308
+ */
37309
+ this.commitTrailingStopCost = async (symbol, newStopLossPrice, currentPrice, context) => {
37310
+ bt.loggerService.info(LIVE_METHOD_NAME_TRAILING_STOP_COST, {
37311
+ symbol,
37312
+ newStopLossPrice,
37313
+ currentPrice,
37314
+ context,
37315
+ });
37316
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_TRAILING_STOP_COST);
37317
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_TRAILING_STOP_COST);
37318
+ {
37319
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
37320
+ riskName &&
37321
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP_COST);
37322
+ riskList &&
37323
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP_COST));
37324
+ actions &&
37325
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_TRAILING_STOP_COST));
37326
+ }
37327
+ const signal = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
37328
+ strategyName: context.strategyName,
37329
+ exchangeName: context.exchangeName,
37330
+ frameName: "",
37331
+ });
37332
+ if (!signal) {
37333
+ return false;
37334
+ }
37335
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(false, symbol, {
37336
+ strategyName: context.strategyName,
37337
+ exchangeName: context.exchangeName,
37338
+ frameName: "",
37339
+ });
37340
+ if (effectivePriceOpen === null) {
37341
+ return false;
37342
+ }
37343
+ const percentShift = slPriceToPercentShift(newStopLossPrice, signal.priceStopLoss, effectivePriceOpen);
37344
+ if (await not(bt.strategyCoreService.validateTrailingStop(false, symbol, percentShift, currentPrice, {
37345
+ strategyName: context.strategyName,
37346
+ exchangeName: context.exchangeName,
37347
+ frameName: "",
37348
+ }))) {
37349
+ return false;
37350
+ }
37351
+ await Broker.commitTrailingStop({
37352
+ symbol,
37353
+ percentShift,
37354
+ currentPrice,
37355
+ newStopLossPrice,
37356
+ position: signal.position,
37357
+ context,
37358
+ backtest: false,
37359
+ });
37360
+ return await bt.strategyCoreService.trailingStop(false, symbol, percentShift, currentPrice, {
37361
+ strategyName: context.strategyName,
37362
+ exchangeName: context.exchangeName,
37363
+ frameName: "",
37364
+ });
37365
+ };
37366
+ /**
37367
+ * Adjusts the trailing take-profit to an absolute price level.
37368
+ *
37369
+ * Convenience wrapper around commitTrailingTake that converts an absolute
37370
+ * take-profit price to a percentShift relative to the ORIGINAL TP distance.
37371
+ *
37372
+ * @param symbol - Trading pair symbol
37373
+ * @param newTakeProfitPrice - Desired absolute take-profit price
37374
+ * @param currentPrice - Current market price to check for intrusion
37375
+ * @param context - Execution context with strategyName and exchangeName
37376
+ * @returns Promise<boolean> - true if trailing TP was set/updated, false if rejected
37377
+ */
37378
+ this.commitTrailingTakeCost = async (symbol, newTakeProfitPrice, currentPrice, context) => {
37379
+ bt.loggerService.info(LIVE_METHOD_NAME_TRAILING_PROFIT_COST, {
37380
+ symbol,
37381
+ newTakeProfitPrice,
37382
+ currentPrice,
37383
+ context,
37384
+ });
37385
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_TRAILING_PROFIT_COST);
37386
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_TRAILING_PROFIT_COST);
37387
+ {
37388
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
37389
+ riskName &&
37390
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT_COST);
37391
+ riskList &&
37392
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT_COST));
37393
+ actions &&
37394
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_TRAILING_PROFIT_COST));
37395
+ }
37396
+ const signal = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
37397
+ strategyName: context.strategyName,
37398
+ exchangeName: context.exchangeName,
37399
+ frameName: "",
37400
+ });
37401
+ if (!signal) {
37402
+ return false;
37403
+ }
37404
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(false, symbol, {
37405
+ strategyName: context.strategyName,
37406
+ exchangeName: context.exchangeName,
37407
+ frameName: "",
37408
+ });
37409
+ if (effectivePriceOpen === null) {
37410
+ return false;
37411
+ }
37412
+ const percentShift = tpPriceToPercentShift(newTakeProfitPrice, signal.priceTakeProfit, effectivePriceOpen);
37413
+ if (await not(bt.strategyCoreService.validateTrailingTake(false, symbol, percentShift, currentPrice, {
37414
+ strategyName: context.strategyName,
37415
+ exchangeName: context.exchangeName,
37416
+ frameName: "",
37417
+ }))) {
37418
+ return false;
37419
+ }
37420
+ await Broker.commitTrailingTake({
37421
+ symbol,
37422
+ percentShift,
37423
+ currentPrice,
37424
+ newTakeProfitPrice,
37425
+ position: signal.position,
37426
+ context,
37427
+ backtest: false,
37428
+ });
34648
37429
  return await bt.strategyCoreService.trailingTake(false, symbol, percentShift, currentPrice, {
34649
37430
  strategyName: context.strategyName,
34650
37431
  exchangeName: context.exchangeName,
@@ -34682,10 +37463,49 @@ class LiveUtils {
34682
37463
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_BREAKEVEN);
34683
37464
  {
34684
37465
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34685
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BREAKEVEN);
34686
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BREAKEVEN));
34687
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_BREAKEVEN));
37466
+ riskName &&
37467
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BREAKEVEN);
37468
+ riskList &&
37469
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BREAKEVEN));
37470
+ actions &&
37471
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_BREAKEVEN));
37472
+ }
37473
+ const signal = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
37474
+ strategyName: context.strategyName,
37475
+ exchangeName: context.exchangeName,
37476
+ frameName: "",
37477
+ });
37478
+ if (!signal) {
37479
+ return false;
37480
+ }
37481
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(false, symbol, {
37482
+ strategyName: context.strategyName,
37483
+ exchangeName: context.exchangeName,
37484
+ frameName: "",
37485
+ });
37486
+ if (effectivePriceOpen === null) {
37487
+ return false;
34688
37488
  }
37489
+ if (await not(bt.strategyCoreService.validateBreakeven(false, symbol, currentPrice, {
37490
+ strategyName: context.strategyName,
37491
+ exchangeName: context.exchangeName,
37492
+ frameName: "",
37493
+ }))) {
37494
+ return false;
37495
+ }
37496
+ await Broker.commitBreakeven({
37497
+ symbol,
37498
+ currentPrice,
37499
+ newStopLossPrice: breakevenNewStopLossPrice(effectivePriceOpen),
37500
+ newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
37501
+ position: signal.position,
37502
+ context: {
37503
+ strategyName: context.strategyName,
37504
+ exchangeName: context.exchangeName,
37505
+ frameName: "",
37506
+ },
37507
+ backtest: false,
37508
+ });
34689
37509
  return await bt.strategyCoreService.breakeven(false, symbol, currentPrice, {
34690
37510
  strategyName: context.strategyName,
34691
37511
  exchangeName: context.exchangeName,
@@ -34722,9 +37542,12 @@ class LiveUtils {
34722
37542
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED);
34723
37543
  {
34724
37544
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34725
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED);
34726
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED));
34727
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED));
37545
+ riskName &&
37546
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED);
37547
+ riskList &&
37548
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED));
37549
+ actions &&
37550
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED));
34728
37551
  }
34729
37552
  await bt.strategyCoreService.activateScheduled(false, symbol, {
34730
37553
  strategyName: context.strategyName,
@@ -34765,10 +37588,42 @@ class LiveUtils {
34765
37588
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_AVERAGE_BUY);
34766
37589
  {
34767
37590
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34768
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_AVERAGE_BUY);
34769
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_AVERAGE_BUY));
34770
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_AVERAGE_BUY));
37591
+ riskName &&
37592
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_AVERAGE_BUY);
37593
+ riskList &&
37594
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_AVERAGE_BUY));
37595
+ actions &&
37596
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_AVERAGE_BUY));
37597
+ }
37598
+ if (await not(bt.strategyCoreService.validateAverageBuy(false, symbol, currentPrice, {
37599
+ strategyName: context.strategyName,
37600
+ exchangeName: context.exchangeName,
37601
+ frameName: "",
37602
+ }))) {
37603
+ return false;
37604
+ }
37605
+ const signalForAvgBuy = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
37606
+ strategyName: context.strategyName,
37607
+ exchangeName: context.exchangeName,
37608
+ frameName: "",
37609
+ });
37610
+ if (!signalForAvgBuy) {
37611
+ return false;
34771
37612
  }
37613
+ await Broker.commitAverageBuy({
37614
+ symbol,
37615
+ currentPrice,
37616
+ cost,
37617
+ position: signalForAvgBuy.position,
37618
+ priceTakeProfit: signalForAvgBuy.priceTakeProfit,
37619
+ priceStopLoss: signalForAvgBuy.priceStopLoss,
37620
+ context: {
37621
+ strategyName: context.strategyName,
37622
+ exchangeName: context.exchangeName,
37623
+ frameName: "",
37624
+ },
37625
+ backtest: false,
37626
+ });
34772
37627
  return await bt.strategyCoreService.averageBuy(false, symbol, currentPrice, {
34773
37628
  strategyName: context.strategyName,
34774
37629
  exchangeName: context.exchangeName,
@@ -34802,9 +37657,12 @@ class LiveUtils {
34802
37657
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_DATA);
34803
37658
  {
34804
37659
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34805
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_DATA);
34806
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_DATA));
34807
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_DATA));
37660
+ riskName &&
37661
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_DATA);
37662
+ riskList &&
37663
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_DATA));
37664
+ actions &&
37665
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_DATA));
34808
37666
  }
34809
37667
  return await bt.liveMarkdownService.getData(symbol, context.strategyName, context.exchangeName, "", false);
34810
37668
  };
@@ -34836,9 +37694,12 @@ class LiveUtils {
34836
37694
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_REPORT);
34837
37695
  {
34838
37696
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34839
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_REPORT);
34840
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_REPORT));
34841
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_REPORT));
37697
+ riskName &&
37698
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_REPORT);
37699
+ riskList &&
37700
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_REPORT));
37701
+ actions &&
37702
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_REPORT));
34842
37703
  }
34843
37704
  return await bt.liveMarkdownService.getReport(symbol, context.strategyName, context.exchangeName, "", false, columns);
34844
37705
  };
@@ -34878,9 +37739,12 @@ class LiveUtils {
34878
37739
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_DUMP);
34879
37740
  {
34880
37741
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34881
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_DUMP);
34882
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_DUMP));
34883
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_DUMP));
37742
+ riskName &&
37743
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_DUMP);
37744
+ riskList &&
37745
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_DUMP));
37746
+ actions &&
37747
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_DUMP));
34884
37748
  }
34885
37749
  await bt.liveMarkdownService.dump(symbol, context.strategyName, context.exchangeName, "", false, path, columns);
34886
37750
  };
@@ -42468,4 +45332,4 @@ const percentValue = (yesterdayValue, todayValue) => {
42468
45332
  return yesterdayValue / todayValue - 1;
42469
45333
  };
42470
45334
 
42471
- export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Log, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Sync, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitTrailingStop, commitTrailingTake, dumpMessages, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionAveragePrice, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getRawCandles, getRiskSchema, getScheduledSignal, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasTradeContext, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentValue, roundTicks, set, setColumns, setConfig, setLogger, shutdown, stopStrategy, toProfitLossDto, validate, waitForCandle, warmCandles };
45335
+ export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Log, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Sync, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, dumpMessages, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionAveragePrice, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getRawCandles, getRiskSchema, getScheduledSignal, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasTradeContext, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, roundTicks, set, setColumns, setConfig, setLogger, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, waitForCandle, warmCandles };