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.cjs CHANGED
@@ -7379,6 +7379,52 @@ class ClientStrategy {
7379
7379
  await PersistSignalAdapter.writeSignalData(this._pendingSignal, symbol, this.params.strategyName, this.params.exchangeName);
7380
7380
  // Commit will be emitted in tick() with correct currentTime
7381
7381
  }
7382
+ /**
7383
+ * Validates preconditions for partialProfit without mutating state.
7384
+ *
7385
+ * Returns false (never throws) when any condition would cause partialProfit to fail or skip.
7386
+ * Use this to pre-check before calling partialProfit to avoid needing to handle exceptions.
7387
+ *
7388
+ * @param symbol - Trading pair symbol
7389
+ * @param percentToClose - Percentage of position to close (0-100)
7390
+ * @param currentPrice - Current market price (must be in profit direction)
7391
+ * @returns boolean - true if partialProfit would execute, false otherwise
7392
+ */
7393
+ async validatePartialProfit(symbol, percentToClose, currentPrice) {
7394
+ this.params.logger.debug("ClientStrategy validatePartialProfit", {
7395
+ symbol,
7396
+ percentToClose,
7397
+ currentPrice,
7398
+ hasPendingSignal: this._pendingSignal !== null,
7399
+ });
7400
+ if (!this._pendingSignal)
7401
+ return false;
7402
+ if (typeof percentToClose !== "number" || !isFinite(percentToClose))
7403
+ return false;
7404
+ if (percentToClose <= 0)
7405
+ return false;
7406
+ if (percentToClose > 100)
7407
+ return false;
7408
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
7409
+ return false;
7410
+ const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7411
+ if (this._pendingSignal.position === "long" && currentPrice <= effectivePriceOpen)
7412
+ return false;
7413
+ if (this._pendingSignal.position === "short" && currentPrice >= effectivePriceOpen)
7414
+ return false;
7415
+ const effectiveTakeProfit = this._pendingSignal._trailingPriceTakeProfit ?? this._pendingSignal.priceTakeProfit;
7416
+ if (this._pendingSignal.position === "long" && currentPrice >= effectiveTakeProfit)
7417
+ return false;
7418
+ if (this._pendingSignal.position === "short" && currentPrice <= effectiveTakeProfit)
7419
+ return false;
7420
+ const { totalClosedPercent, remainingCostBasis } = getTotalClosed(this._pendingSignal);
7421
+ const totalInvested = (this._pendingSignal._entry ?? []).reduce((s, e) => s + e.cost, 0) || GLOBAL_CONFIG.CC_POSITION_ENTRY_COST;
7422
+ const newPartialDollar = (percentToClose / 100) * remainingCostBasis;
7423
+ const newTotalClosedDollar = (totalClosedPercent / 100) * totalInvested + newPartialDollar;
7424
+ if (newTotalClosedDollar > totalInvested)
7425
+ return false;
7426
+ return true;
7427
+ }
7382
7428
  /**
7383
7429
  * Executes partial close at profit level (moving toward TP).
7384
7430
  *
@@ -7514,6 +7560,52 @@ class ClientStrategy {
7514
7560
  });
7515
7561
  return true;
7516
7562
  }
7563
+ /**
7564
+ * Validates preconditions for partialLoss without mutating state.
7565
+ *
7566
+ * Returns false (never throws) when any condition would cause partialLoss to fail or skip.
7567
+ * Use this to pre-check before calling partialLoss to avoid needing to handle exceptions.
7568
+ *
7569
+ * @param symbol - Trading pair symbol
7570
+ * @param percentToClose - Percentage of position to close (0-100)
7571
+ * @param currentPrice - Current market price (must be in loss direction)
7572
+ * @returns boolean - true if partialLoss would execute, false otherwise
7573
+ */
7574
+ async validatePartialLoss(symbol, percentToClose, currentPrice) {
7575
+ this.params.logger.debug("ClientStrategy validatePartialLoss", {
7576
+ symbol,
7577
+ percentToClose,
7578
+ currentPrice,
7579
+ hasPendingSignal: this._pendingSignal !== null,
7580
+ });
7581
+ if (!this._pendingSignal)
7582
+ return false;
7583
+ if (typeof percentToClose !== "number" || !isFinite(percentToClose))
7584
+ return false;
7585
+ if (percentToClose <= 0)
7586
+ return false;
7587
+ if (percentToClose > 100)
7588
+ return false;
7589
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
7590
+ return false;
7591
+ const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7592
+ if (this._pendingSignal.position === "long" && currentPrice >= effectivePriceOpen)
7593
+ return false;
7594
+ if (this._pendingSignal.position === "short" && currentPrice <= effectivePriceOpen)
7595
+ return false;
7596
+ const effectiveStopLoss = this._pendingSignal._trailingPriceStopLoss ?? this._pendingSignal.priceStopLoss;
7597
+ if (this._pendingSignal.position === "long" && currentPrice <= effectiveStopLoss)
7598
+ return false;
7599
+ if (this._pendingSignal.position === "short" && currentPrice >= effectiveStopLoss)
7600
+ return false;
7601
+ const { totalClosedPercent, remainingCostBasis } = getTotalClosed(this._pendingSignal);
7602
+ const totalInvested = (this._pendingSignal._entry ?? []).reduce((s, e) => s + e.cost, 0) || GLOBAL_CONFIG.CC_POSITION_ENTRY_COST;
7603
+ const newPartialDollar = (percentToClose / 100) * remainingCostBasis;
7604
+ const newTotalClosedDollar = (totalClosedPercent / 100) * totalInvested + newPartialDollar;
7605
+ if (newTotalClosedDollar > totalInvested)
7606
+ return false;
7607
+ return true;
7608
+ }
7517
7609
  /**
7518
7610
  * Executes partial close at loss level (moving toward SL).
7519
7611
  *
@@ -7649,6 +7741,82 @@ class ClientStrategy {
7649
7741
  });
7650
7742
  return true;
7651
7743
  }
7744
+ /**
7745
+ * Validates preconditions for breakeven without mutating state.
7746
+ *
7747
+ * Returns false (never throws) when any condition would cause breakeven to fail or skip.
7748
+ * Mirrors the full BREAKEVEN_FN condition logic including threshold, trailing state and intrusion checks.
7749
+ *
7750
+ * @param symbol - Trading pair symbol
7751
+ * @param currentPrice - Current market price to check threshold
7752
+ * @returns boolean - true if breakeven would execute, false otherwise
7753
+ */
7754
+ async validateBreakeven(symbol, currentPrice) {
7755
+ this.params.logger.debug("ClientStrategy validateBreakeven", {
7756
+ symbol,
7757
+ currentPrice,
7758
+ hasPendingSignal: this._pendingSignal !== null,
7759
+ });
7760
+ if (!this._pendingSignal)
7761
+ return false;
7762
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
7763
+ return false;
7764
+ const signal = this._pendingSignal;
7765
+ const breakevenPrice = getEffectivePriceOpen(signal);
7766
+ const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
7767
+ if (signal.position === "long" && breakevenPrice >= effectiveTakeProfit)
7768
+ return false;
7769
+ if (signal.position === "short" && breakevenPrice <= effectiveTakeProfit)
7770
+ return false;
7771
+ const breakevenThresholdPercent = (GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE + GLOBAL_CONFIG.CC_PERCENT_FEE) * 2;
7772
+ if (signal._trailingPriceStopLoss !== undefined) {
7773
+ const trailingStopLoss = signal._trailingPriceStopLoss;
7774
+ if (signal.position === "long") {
7775
+ const isPositiveTrailing = trailingStopLoss > breakevenPrice;
7776
+ if (isPositiveTrailing)
7777
+ return true; // already protecting profit
7778
+ const thresholdPrice = breakevenPrice * (1 + breakevenThresholdPercent / 100);
7779
+ const isThresholdReached = currentPrice >= thresholdPrice;
7780
+ if (!isThresholdReached || breakevenPrice <= trailingStopLoss)
7781
+ return false;
7782
+ if (currentPrice < breakevenPrice)
7783
+ return false; // price intrusion
7784
+ return true;
7785
+ }
7786
+ else {
7787
+ const isPositiveTrailing = trailingStopLoss < breakevenPrice;
7788
+ if (isPositiveTrailing)
7789
+ return true; // already protecting profit
7790
+ const thresholdPrice = breakevenPrice * (1 - breakevenThresholdPercent / 100);
7791
+ const isThresholdReached = currentPrice <= thresholdPrice;
7792
+ if (!isThresholdReached || breakevenPrice >= trailingStopLoss)
7793
+ return false;
7794
+ if (currentPrice > breakevenPrice)
7795
+ return false; // price intrusion
7796
+ return true;
7797
+ }
7798
+ }
7799
+ const currentStopLoss = signal.priceStopLoss;
7800
+ if (signal.position === "long") {
7801
+ const thresholdPrice = breakevenPrice * (1 + breakevenThresholdPercent / 100);
7802
+ const isThresholdReached = currentPrice >= thresholdPrice;
7803
+ const canMove = isThresholdReached && currentStopLoss < breakevenPrice;
7804
+ if (!canMove)
7805
+ return false;
7806
+ if (currentPrice < breakevenPrice)
7807
+ return false;
7808
+ }
7809
+ else {
7810
+ const thresholdPrice = breakevenPrice * (1 - breakevenThresholdPercent / 100);
7811
+ const isThresholdReached = currentPrice <= thresholdPrice;
7812
+ const canMove = isThresholdReached && currentStopLoss > breakevenPrice;
7813
+ if (!canMove)
7814
+ return false;
7815
+ if (currentPrice > breakevenPrice)
7816
+ return false;
7817
+ }
7818
+ return true;
7819
+ }
7652
7820
  /**
7653
7821
  * Moves stop-loss to breakeven (entry price) when price reaches threshold.
7654
7822
  *
@@ -7771,6 +7939,65 @@ class ClientStrategy {
7771
7939
  });
7772
7940
  return true;
7773
7941
  }
7942
+ /**
7943
+ * Validates preconditions for trailingStop without mutating state.
7944
+ *
7945
+ * Returns false (never throws) when any condition would cause trailingStop to fail or skip.
7946
+ * Includes absorption check: returns false if new SL would not improve on the current trailing SL.
7947
+ *
7948
+ * @param symbol - Trading pair symbol
7949
+ * @param percentShift - Percentage shift of ORIGINAL SL distance [-100, 100], excluding 0
7950
+ * @param currentPrice - Current market price to check for intrusion
7951
+ * @returns boolean - true if trailingStop would execute, false otherwise
7952
+ */
7953
+ async validateTrailingStop(symbol, percentShift, currentPrice) {
7954
+ this.params.logger.debug("ClientStrategy validateTrailingStop", {
7955
+ symbol,
7956
+ percentShift,
7957
+ currentPrice,
7958
+ hasPendingSignal: this._pendingSignal !== null,
7959
+ });
7960
+ if (!this._pendingSignal)
7961
+ return false;
7962
+ if (typeof percentShift !== "number" || !isFinite(percentShift))
7963
+ return false;
7964
+ if (percentShift < -100 || percentShift > 100)
7965
+ return false;
7966
+ if (percentShift === 0)
7967
+ return false;
7968
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
7969
+ return false;
7970
+ const signal = this._pendingSignal;
7971
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
7972
+ const slDistancePercent = Math.abs((effectivePriceOpen - signal.priceStopLoss) / effectivePriceOpen * 100);
7973
+ const newSlDistancePercent = slDistancePercent + percentShift;
7974
+ let newStopLoss;
7975
+ if (signal.position === "long") {
7976
+ newStopLoss = effectivePriceOpen * (1 - newSlDistancePercent / 100);
7977
+ }
7978
+ else {
7979
+ newStopLoss = effectivePriceOpen * (1 + newSlDistancePercent / 100);
7980
+ }
7981
+ // Intrusion check (mirrors trailingStop method: applied before TRAILING_STOP_LOSS_FN, for all calls)
7982
+ if (signal.position === "long" && currentPrice < newStopLoss)
7983
+ return false;
7984
+ if (signal.position === "short" && currentPrice > newStopLoss)
7985
+ return false;
7986
+ const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
7987
+ if (signal.position === "long" && newStopLoss >= effectiveTakeProfit)
7988
+ return false;
7989
+ if (signal.position === "short" && newStopLoss <= effectiveTakeProfit)
7990
+ return false;
7991
+ // Absorption check (mirrors TRAILING_STOP_LOSS_FN: first call is unconditional)
7992
+ const currentTrailingSL = signal._trailingPriceStopLoss;
7993
+ if (currentTrailingSL !== undefined) {
7994
+ if (signal.position === "long" && newStopLoss <= currentTrailingSL)
7995
+ return false;
7996
+ if (signal.position === "short" && newStopLoss >= currentTrailingSL)
7997
+ return false;
7998
+ }
7999
+ return true;
8000
+ }
7774
8001
  /**
7775
8002
  * Adjusts trailing stop-loss by shifting distance between entry and original SL.
7776
8003
  *
@@ -7959,6 +8186,65 @@ class ClientStrategy {
7959
8186
  });
7960
8187
  return true;
7961
8188
  }
8189
+ /**
8190
+ * Validates preconditions for trailingTake without mutating state.
8191
+ *
8192
+ * Returns false (never throws) when any condition would cause trailingTake to fail or skip.
8193
+ * Includes absorption check: returns false if new TP would not improve on the current trailing TP.
8194
+ *
8195
+ * @param symbol - Trading pair symbol
8196
+ * @param percentShift - Percentage adjustment to ORIGINAL TP distance (-100 to 100)
8197
+ * @param currentPrice - Current market price to check for intrusion
8198
+ * @returns boolean - true if trailingTake would execute, false otherwise
8199
+ */
8200
+ async validateTrailingTake(symbol, percentShift, currentPrice) {
8201
+ this.params.logger.debug("ClientStrategy validateTrailingTake", {
8202
+ symbol,
8203
+ percentShift,
8204
+ currentPrice,
8205
+ hasPendingSignal: this._pendingSignal !== null,
8206
+ });
8207
+ if (!this._pendingSignal)
8208
+ return false;
8209
+ if (typeof percentShift !== "number" || !isFinite(percentShift))
8210
+ return false;
8211
+ if (percentShift < -100 || percentShift > 100)
8212
+ return false;
8213
+ if (percentShift === 0)
8214
+ return false;
8215
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
8216
+ return false;
8217
+ const signal = this._pendingSignal;
8218
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
8219
+ const tpDistancePercent = Math.abs((signal.priceTakeProfit - effectivePriceOpen) / effectivePriceOpen * 100);
8220
+ const newTpDistancePercent = tpDistancePercent + percentShift;
8221
+ let newTakeProfit;
8222
+ if (signal.position === "long") {
8223
+ newTakeProfit = effectivePriceOpen * (1 + newTpDistancePercent / 100);
8224
+ }
8225
+ else {
8226
+ newTakeProfit = effectivePriceOpen * (1 - newTpDistancePercent / 100);
8227
+ }
8228
+ // Intrusion check (mirrors trailingTake method: applied before TRAILING_TAKE_PROFIT_FN, for all calls)
8229
+ if (signal.position === "long" && currentPrice > newTakeProfit)
8230
+ return false;
8231
+ if (signal.position === "short" && currentPrice < newTakeProfit)
8232
+ return false;
8233
+ const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
8234
+ if (signal.position === "long" && newTakeProfit <= effectiveStopLoss)
8235
+ return false;
8236
+ if (signal.position === "short" && newTakeProfit >= effectiveStopLoss)
8237
+ return false;
8238
+ // Absorption check (mirrors TRAILING_TAKE_PROFIT_FN: first call is unconditional)
8239
+ const currentTrailingTP = signal._trailingPriceTakeProfit;
8240
+ if (currentTrailingTP !== undefined) {
8241
+ if (signal.position === "long" && newTakeProfit >= currentTrailingTP)
8242
+ return false;
8243
+ if (signal.position === "short" && newTakeProfit <= currentTrailingTP)
8244
+ return false;
8245
+ }
8246
+ return true;
8247
+ }
7962
8248
  /**
7963
8249
  * Adjusts the trailing take-profit distance for an active pending signal.
7964
8250
  *
@@ -8133,6 +8419,41 @@ class ClientStrategy {
8133
8419
  });
8134
8420
  return true;
8135
8421
  }
8422
+ /**
8423
+ * Validates preconditions for averageBuy without mutating state.
8424
+ *
8425
+ * Returns false (never throws) when any condition would cause averageBuy to fail or skip.
8426
+ *
8427
+ * @param symbol - Trading pair symbol
8428
+ * @param currentPrice - New entry price to add
8429
+ * @returns boolean - true if averageBuy would execute, false otherwise
8430
+ */
8431
+ async validateAverageBuy(symbol, currentPrice) {
8432
+ this.params.logger.debug("ClientStrategy validateAverageBuy", {
8433
+ symbol,
8434
+ currentPrice,
8435
+ hasPendingSignal: this._pendingSignal !== null,
8436
+ });
8437
+ if (!this._pendingSignal)
8438
+ return false;
8439
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
8440
+ return false;
8441
+ const signal = this._pendingSignal;
8442
+ const entries = (!signal._entry || signal._entry.length === 0)
8443
+ ? [{ price: signal.priceOpen, cost: GLOBAL_CONFIG.CC_POSITION_ENTRY_COST }]
8444
+ : signal._entry;
8445
+ if (signal.position === "long") {
8446
+ const minEntryPrice = Math.min(...entries.map((e) => e.price));
8447
+ if (!GLOBAL_CONFIG.CC_ENABLE_DCA_EVERYWHERE && currentPrice >= minEntryPrice)
8448
+ return false;
8449
+ }
8450
+ else {
8451
+ const maxEntryPrice = Math.max(...entries.map((e) => e.price));
8452
+ if (!GLOBAL_CONFIG.CC_ENABLE_DCA_EVERYWHERE && currentPrice <= maxEntryPrice)
8453
+ return false;
8454
+ }
8455
+ return true;
8456
+ }
8136
8457
  /**
8137
8458
  * Adds a new averaging entry to an open position (DCA — Dollar Cost Averaging).
8138
8459
  *
@@ -8518,12 +8839,22 @@ const Risk = new RiskUtils();
8518
8839
  */
8519
8840
  const CREATE_SYNC_FN = (self, strategyName, exchangeName, frameName, backtest) => functoolsKit.trycatch(async (event) => {
8520
8841
  if (event.backtest) {
8521
- return;
8842
+ return true;
8522
8843
  }
8523
8844
  await syncSubject.next(event);
8524
8845
  await self.actionCoreService.signalSync(backtest, event, { strategyName, exchangeName, frameName });
8525
8846
  return true;
8526
8847
  }, {
8848
+ fallback: (error) => {
8849
+ const message = "StrategyConnectionService CREATE_SYNC_FN thrown. Broker rejected order request";
8850
+ const payload = {
8851
+ error: functoolsKit.errorData(error),
8852
+ message: functoolsKit.getErrorMessage(error),
8853
+ };
8854
+ self.loggerService.warn(message, payload);
8855
+ console.error(message, payload);
8856
+ errorEmitter.next(error);
8857
+ },
8527
8858
  defaultValue: false,
8528
8859
  });
8529
8860
  /**
@@ -9224,6 +9555,21 @@ class StrategyConnectionService {
9224
9555
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9225
9556
  await strategy.closePending(symbol, backtest, closeId);
9226
9557
  };
9558
+ /**
9559
+ * Checks whether `partialProfit` would succeed without executing it.
9560
+ * Delegates to `ClientStrategy.validatePartialProfit()` — no throws, pure boolean result.
9561
+ *
9562
+ * @param backtest - Whether running in backtest mode
9563
+ * @param symbol - Trading pair symbol
9564
+ * @param percentToClose - Percentage of position to check (0-100]
9565
+ * @param currentPrice - Current market price to validate against
9566
+ * @param context - Execution context with strategyName, exchangeName, frameName
9567
+ * @returns Promise<boolean> - true if `partialProfit` would execute, false otherwise
9568
+ */
9569
+ this.validatePartialProfit = (backtest, symbol, percentToClose, currentPrice, context) => {
9570
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9571
+ return Promise.resolve(strategy.validatePartialProfit(symbol, percentToClose, currentPrice));
9572
+ };
9227
9573
  /**
9228
9574
  * Executes partial close at profit level (moving toward TP).
9229
9575
  *
@@ -9265,6 +9611,21 @@ class StrategyConnectionService {
9265
9611
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9266
9612
  return await strategy.partialProfit(symbol, percentToClose, currentPrice, backtest);
9267
9613
  };
9614
+ /**
9615
+ * Checks whether `partialLoss` would succeed without executing it.
9616
+ * Delegates to `ClientStrategy.validatePartialLoss()` — no throws, pure boolean result.
9617
+ *
9618
+ * @param backtest - Whether running in backtest mode
9619
+ * @param symbol - Trading pair symbol
9620
+ * @param percentToClose - Percentage of position to check (0-100]
9621
+ * @param currentPrice - Current market price to validate against
9622
+ * @param context - Execution context with strategyName, exchangeName, frameName
9623
+ * @returns Promise<boolean> - true if `partialLoss` would execute, false otherwise
9624
+ */
9625
+ this.validatePartialLoss = (backtest, symbol, percentToClose, currentPrice, context) => {
9626
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9627
+ return Promise.resolve(strategy.validatePartialLoss(symbol, percentToClose, currentPrice));
9628
+ };
9268
9629
  /**
9269
9630
  * Executes partial close at loss level (moving toward SL).
9270
9631
  *
@@ -9306,6 +9667,21 @@ class StrategyConnectionService {
9306
9667
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9307
9668
  return await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest);
9308
9669
  };
9670
+ /**
9671
+ * Checks whether `trailingStop` would succeed without executing it.
9672
+ * Delegates to `ClientStrategy.validateTrailingStop()` — no throws, pure boolean result.
9673
+ *
9674
+ * @param backtest - Whether running in backtest mode
9675
+ * @param symbol - Trading pair symbol
9676
+ * @param percentShift - Percentage shift of ORIGINAL SL distance [-100, 100], excluding 0
9677
+ * @param currentPrice - Current market price to validate against
9678
+ * @param context - Execution context with strategyName, exchangeName, frameName
9679
+ * @returns Promise<boolean> - true if `trailingStop` would execute, false otherwise
9680
+ */
9681
+ this.validateTrailingStop = (backtest, symbol, percentShift, currentPrice, context) => {
9682
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9683
+ return Promise.resolve(strategy.validateTrailingStop(symbol, percentShift, currentPrice));
9684
+ };
9309
9685
  /**
9310
9686
  * Adjusts the trailing stop-loss distance for an active pending signal.
9311
9687
  *
@@ -9345,6 +9721,21 @@ class StrategyConnectionService {
9345
9721
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9346
9722
  return await strategy.trailingStop(symbol, percentShift, currentPrice, backtest);
9347
9723
  };
9724
+ /**
9725
+ * Checks whether `trailingTake` would succeed without executing it.
9726
+ * Delegates to `ClientStrategy.validateTrailingTake()` — no throws, pure boolean result.
9727
+ *
9728
+ * @param backtest - Whether running in backtest mode
9729
+ * @param symbol - Trading pair symbol
9730
+ * @param percentShift - Percentage adjustment to ORIGINAL TP distance [-100, 100], excluding 0
9731
+ * @param currentPrice - Current market price to validate against
9732
+ * @param context - Execution context with strategyName, exchangeName, frameName
9733
+ * @returns Promise<boolean> - true if `trailingTake` would execute, false otherwise
9734
+ */
9735
+ this.validateTrailingTake = (backtest, symbol, percentShift, currentPrice, context) => {
9736
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9737
+ return Promise.resolve(strategy.validateTrailingTake(symbol, percentShift, currentPrice));
9738
+ };
9348
9739
  /**
9349
9740
  * Adjusts the trailing take-profit distance for an active pending signal.
9350
9741
  *
@@ -9384,6 +9775,20 @@ class StrategyConnectionService {
9384
9775
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9385
9776
  return await strategy.trailingTake(symbol, percentShift, currentPrice, backtest);
9386
9777
  };
9778
+ /**
9779
+ * Checks whether `breakeven` would succeed without executing it.
9780
+ * Delegates to `ClientStrategy.validateBreakeven()` — no throws, pure boolean result.
9781
+ *
9782
+ * @param backtest - Whether running in backtest mode
9783
+ * @param symbol - Trading pair symbol
9784
+ * @param currentPrice - Current market price to validate against
9785
+ * @param context - Execution context with strategyName, exchangeName, frameName
9786
+ * @returns Promise<boolean> - true if `breakeven` would execute, false otherwise
9787
+ */
9788
+ this.validateBreakeven = (backtest, symbol, currentPrice, context) => {
9789
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9790
+ return Promise.resolve(strategy.validateBreakeven(symbol, currentPrice));
9791
+ };
9387
9792
  /**
9388
9793
  * Delegates to ClientStrategy.breakeven() with current execution context.
9389
9794
  *
@@ -9449,6 +9854,20 @@ class StrategyConnectionService {
9449
9854
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9450
9855
  return await strategy.activateScheduled(symbol, backtest, activateId);
9451
9856
  };
9857
+ /**
9858
+ * Checks whether `averageBuy` would succeed without executing it.
9859
+ * Delegates to `ClientStrategy.validateAverageBuy()` — no throws, pure boolean result.
9860
+ *
9861
+ * @param backtest - Whether running in backtest mode
9862
+ * @param symbol - Trading pair symbol
9863
+ * @param currentPrice - New entry price to validate
9864
+ * @param context - Execution context with strategyName, exchangeName, frameName
9865
+ * @returns Promise<boolean> - true if `averageBuy` would execute, false otherwise
9866
+ */
9867
+ this.validateAverageBuy = (backtest, symbol, currentPrice, context) => {
9868
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9869
+ return Promise.resolve(strategy.validateAverageBuy(symbol, currentPrice));
9870
+ };
9452
9871
  /**
9453
9872
  * Adds a new DCA entry to the active pending signal.
9454
9873
  *
@@ -12807,6 +13226,28 @@ class StrategyCoreService {
12807
13226
  }
12808
13227
  return await this.strategyConnectionService.clear(payload);
12809
13228
  };
13229
+ /**
13230
+ * Checks whether `partialProfit` would succeed without executing it.
13231
+ * Validates context, then delegates to StrategyConnectionService.validatePartialProfit().
13232
+ *
13233
+ * @param backtest - Whether running in backtest mode
13234
+ * @param symbol - Trading pair symbol
13235
+ * @param percentToClose - Percentage of position to check (0-100]
13236
+ * @param currentPrice - Current market price to validate against
13237
+ * @param context - Execution context with strategyName, exchangeName, frameName
13238
+ * @returns Promise<boolean> - true if `partialProfit` would execute, false otherwise
13239
+ */
13240
+ this.validatePartialProfit = async (backtest, symbol, percentToClose, currentPrice, context) => {
13241
+ this.loggerService.log("strategyCoreService validatePartialProfit", {
13242
+ symbol,
13243
+ percentToClose,
13244
+ currentPrice,
13245
+ context,
13246
+ backtest,
13247
+ });
13248
+ await this.validate(context);
13249
+ return await this.strategyConnectionService.validatePartialProfit(backtest, symbol, percentToClose, currentPrice, context);
13250
+ };
12810
13251
  /**
12811
13252
  * Executes partial close at profit level (moving toward TP).
12812
13253
  *
@@ -12848,6 +13289,28 @@ class StrategyCoreService {
12848
13289
  await this.validate(context);
12849
13290
  return await this.strategyConnectionService.partialProfit(backtest, symbol, percentToClose, currentPrice, context);
12850
13291
  };
13292
+ /**
13293
+ * Checks whether `partialLoss` would succeed without executing it.
13294
+ * Validates context, then delegates to StrategyConnectionService.validatePartialLoss().
13295
+ *
13296
+ * @param backtest - Whether running in backtest mode
13297
+ * @param symbol - Trading pair symbol
13298
+ * @param percentToClose - Percentage of position to check (0-100]
13299
+ * @param currentPrice - Current market price to validate against
13300
+ * @param context - Execution context with strategyName, exchangeName, frameName
13301
+ * @returns Promise<boolean> - true if `partialLoss` would execute, false otherwise
13302
+ */
13303
+ this.validatePartialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
13304
+ this.loggerService.log("strategyCoreService validatePartialLoss", {
13305
+ symbol,
13306
+ percentToClose,
13307
+ currentPrice,
13308
+ context,
13309
+ backtest,
13310
+ });
13311
+ await this.validate(context);
13312
+ return await this.strategyConnectionService.validatePartialLoss(backtest, symbol, percentToClose, currentPrice, context);
13313
+ };
12851
13314
  /**
12852
13315
  * Executes partial close at loss level (moving toward SL).
12853
13316
  *
@@ -12917,6 +13380,28 @@ class StrategyCoreService {
12917
13380
  * );
12918
13381
  * ```
12919
13382
  */
13383
+ this.validateTrailingStop = async (backtest, symbol, percentShift, currentPrice, context) => {
13384
+ this.loggerService.log("strategyCoreService validateTrailingStop", {
13385
+ symbol,
13386
+ percentShift,
13387
+ currentPrice,
13388
+ context,
13389
+ backtest,
13390
+ });
13391
+ await this.validate(context);
13392
+ return await this.strategyConnectionService.validateTrailingStop(backtest, symbol, percentShift, currentPrice, context);
13393
+ };
13394
+ /**
13395
+ * Checks whether `trailingStop` would succeed without executing it.
13396
+ * Validates context, then delegates to StrategyConnectionService.validateTrailingStop().
13397
+ *
13398
+ * @param backtest - Whether running in backtest mode
13399
+ * @param symbol - Trading pair symbol
13400
+ * @param percentShift - Percentage shift of ORIGINAL SL distance [-100, 100], excluding 0
13401
+ * @param currentPrice - Current market price to validate against
13402
+ * @param context - Execution context with strategyName, exchangeName, frameName
13403
+ * @returns Promise<boolean> - true if `trailingStop` would execute, false otherwise
13404
+ */
12920
13405
  this.trailingStop = async (backtest, symbol, percentShift, currentPrice, context) => {
12921
13406
  this.loggerService.log("strategyCoreService trailingStop", {
12922
13407
  symbol,
@@ -12952,6 +13437,28 @@ class StrategyCoreService {
12952
13437
  * );
12953
13438
  * ```
12954
13439
  */
13440
+ this.validateTrailingTake = async (backtest, symbol, percentShift, currentPrice, context) => {
13441
+ this.loggerService.log("strategyCoreService validateTrailingTake", {
13442
+ symbol,
13443
+ percentShift,
13444
+ currentPrice,
13445
+ context,
13446
+ backtest,
13447
+ });
13448
+ await this.validate(context);
13449
+ return await this.strategyConnectionService.validateTrailingTake(backtest, symbol, percentShift, currentPrice, context);
13450
+ };
13451
+ /**
13452
+ * Checks whether `trailingTake` would succeed without executing it.
13453
+ * Validates context, then delegates to StrategyConnectionService.validateTrailingTake().
13454
+ *
13455
+ * @param backtest - Whether running in backtest mode
13456
+ * @param symbol - Trading pair symbol
13457
+ * @param percentShift - Percentage adjustment to ORIGINAL TP distance [-100, 100], excluding 0
13458
+ * @param currentPrice - Current market price to validate against
13459
+ * @param context - Execution context with strategyName, exchangeName, frameName
13460
+ * @returns Promise<boolean> - true if `trailingTake` would execute, false otherwise
13461
+ */
12955
13462
  this.trailingTake = async (backtest, symbol, percentShift, currentPrice, context) => {
12956
13463
  this.loggerService.log("strategyCoreService trailingTake", {
12957
13464
  symbol,
@@ -12983,6 +13490,26 @@ class StrategyCoreService {
12983
13490
  * );
12984
13491
  * ```
12985
13492
  */
13493
+ this.validateBreakeven = async (backtest, symbol, currentPrice, context) => {
13494
+ this.loggerService.log("strategyCoreService validateBreakeven", {
13495
+ symbol,
13496
+ currentPrice,
13497
+ context,
13498
+ backtest,
13499
+ });
13500
+ await this.validate(context);
13501
+ return await this.strategyConnectionService.validateBreakeven(backtest, symbol, currentPrice, context);
13502
+ };
13503
+ /**
13504
+ * Checks whether `breakeven` would succeed without executing it.
13505
+ * Validates context, then delegates to StrategyConnectionService.validateBreakeven().
13506
+ *
13507
+ * @param backtest - Whether running in backtest mode
13508
+ * @param symbol - Trading pair symbol
13509
+ * @param currentPrice - Current market price to validate against
13510
+ * @param context - Execution context with strategyName, exchangeName, frameName
13511
+ * @returns Promise<boolean> - true if `breakeven` would execute, false otherwise
13512
+ */
12986
13513
  this.breakeven = async (backtest, symbol, currentPrice, context) => {
12987
13514
  this.loggerService.log("strategyCoreService breakeven", {
12988
13515
  symbol,
@@ -13038,6 +13565,26 @@ class StrategyCoreService {
13038
13565
  * @param context - Execution context with strategyName, exchangeName, frameName
13039
13566
  * @returns Promise<boolean> - true if entry added, false if rejected
13040
13567
  */
13568
+ this.validateAverageBuy = async (backtest, symbol, currentPrice, context) => {
13569
+ this.loggerService.log("strategyCoreService validateAverageBuy", {
13570
+ symbol,
13571
+ currentPrice,
13572
+ context,
13573
+ backtest,
13574
+ });
13575
+ await this.validate(context);
13576
+ return await this.strategyConnectionService.validateAverageBuy(backtest, symbol, currentPrice, context);
13577
+ };
13578
+ /**
13579
+ * Checks whether `averageBuy` would succeed without executing it.
13580
+ * Validates context, then delegates to StrategyConnectionService.validateAverageBuy().
13581
+ *
13582
+ * @param backtest - Whether running in backtest mode
13583
+ * @param symbol - Trading pair symbol
13584
+ * @param currentPrice - New entry price to validate
13585
+ * @param context - Execution context with strategyName, exchangeName, frameName
13586
+ * @returns Promise<boolean> - true if `averageBuy` would execute, false otherwise
13587
+ */
13041
13588
  this.averageBuy = async (backtest, symbol, currentPrice, context, cost) => {
13042
13589
  this.loggerService.log("strategyCoreService averageBuy", {
13043
13590
  symbol,
@@ -30157,6 +30704,1026 @@ const investedCostToPercent = (dollarAmount, investedCost) => {
30157
30704
  return (dollarAmount / investedCost) * 100;
30158
30705
  };
30159
30706
 
30707
+ /**
30708
+ * Convert an absolute stop-loss price to a percentShift for `commitTrailingStop`.
30709
+ *
30710
+ * percentShift = newSlDistancePercent - originalSlDistancePercent
30711
+ * where distance = Math.abs((effectivePriceOpen - slPrice) / effectivePriceOpen * 100)
30712
+ *
30713
+ * @param newStopLossPrice - Desired absolute stop-loss price
30714
+ * @param originalStopLossPrice - Original stop-loss price from the pending signal
30715
+ * @param effectivePriceOpen - Effective entry price (from `getPositionAveragePrice`)
30716
+ * @returns percentShift to pass to `commitTrailingStop`
30717
+ *
30718
+ * @example
30719
+ * // LONG: entry=100, originalSL=90, desired newSL=95
30720
+ * const shift = slPriceToPercentShift(95, 90, 100); // -5
30721
+ * await commitTrailingStop("BTCUSDT", shift, currentPrice);
30722
+ */
30723
+ const slPriceToPercentShift = (newStopLossPrice, originalStopLossPrice, effectivePriceOpen) => {
30724
+ const originalSlDistancePercent = Math.abs((effectivePriceOpen - originalStopLossPrice) / effectivePriceOpen * 100);
30725
+ const newSlDistancePercent = Math.abs((effectivePriceOpen - newStopLossPrice) / effectivePriceOpen * 100);
30726
+ return newSlDistancePercent - originalSlDistancePercent;
30727
+ };
30728
+
30729
+ /**
30730
+ * Convert an absolute take-profit price to a percentShift for `commitTrailingTake`.
30731
+ *
30732
+ * percentShift = newTpDistancePercent - originalTpDistancePercent
30733
+ * where distance = Math.abs((tpPrice - effectivePriceOpen) / effectivePriceOpen * 100)
30734
+ *
30735
+ * @param newTakeProfitPrice - Desired absolute take-profit price
30736
+ * @param originalTakeProfitPrice - Original take-profit price from the pending signal
30737
+ * @param effectivePriceOpen - Effective entry price (from `getPositionAveragePrice`)
30738
+ * @returns percentShift to pass to `commitTrailingTake`
30739
+ *
30740
+ * @example
30741
+ * // LONG: entry=100, originalTP=110, desired newTP=107
30742
+ * const shift = tpPriceToPercentShift(107, 110, 100); // -3
30743
+ * await commitTrailingTake("BTCUSDT", shift, currentPrice);
30744
+ */
30745
+ const tpPriceToPercentShift = (newTakeProfitPrice, originalTakeProfitPrice, effectivePriceOpen) => {
30746
+ const originalTpDistancePercent = Math.abs((originalTakeProfitPrice - effectivePriceOpen) / effectivePriceOpen * 100);
30747
+ const newTpDistancePercent = Math.abs((newTakeProfitPrice - effectivePriceOpen) / effectivePriceOpen * 100);
30748
+ return newTpDistancePercent - originalTpDistancePercent;
30749
+ };
30750
+
30751
+ /**
30752
+ * Convert a percentShift for `commitTrailingStop` back to an absolute stop-loss price.
30753
+ *
30754
+ * Inverse of `slPriceToPercentShift`.
30755
+ *
30756
+ * newSlDistancePercent = originalSlDistancePercent + percentShift
30757
+ * LONG: newStopLossPrice = effectivePriceOpen * (1 - newSlDistancePercent / 100)
30758
+ * SHORT: newStopLossPrice = effectivePriceOpen * (1 + newSlDistancePercent / 100)
30759
+ *
30760
+ * @param percentShift - Value returned by `slPriceToPercentShift` (or passed to `commitTrailingStop`)
30761
+ * @param originalStopLossPrice - Original stop-loss price from the pending signal
30762
+ * @param effectivePriceOpen - Effective entry price (from `getPositionAveragePrice`)
30763
+ * @param position - Position direction: "long" or "short"
30764
+ * @returns Absolute stop-loss price corresponding to the given percentShift
30765
+ *
30766
+ * @example
30767
+ * // LONG: entry=100, originalSL=90, percentShift=-5
30768
+ * const price = slPercentShiftToPrice(-5, 90, 100, "long"); // 95
30769
+ */
30770
+ const slPercentShiftToPrice = (percentShift, originalStopLossPrice, effectivePriceOpen, position) => {
30771
+ const originalSlDistancePercent = Math.abs((effectivePriceOpen - originalStopLossPrice) / effectivePriceOpen * 100);
30772
+ const newSlDistancePercent = originalSlDistancePercent + percentShift;
30773
+ return position === "long"
30774
+ ? effectivePriceOpen * (1 - newSlDistancePercent / 100)
30775
+ : effectivePriceOpen * (1 + newSlDistancePercent / 100);
30776
+ };
30777
+
30778
+ /**
30779
+ * Convert a percentShift for `commitTrailingTake` back to an absolute take-profit price.
30780
+ *
30781
+ * Inverse of `tpPriceToPercentShift`.
30782
+ *
30783
+ * newTpDistancePercent = originalTpDistancePercent + percentShift
30784
+ * LONG: newTakeProfitPrice = effectivePriceOpen * (1 + newTpDistancePercent / 100)
30785
+ * SHORT: newTakeProfitPrice = effectivePriceOpen * (1 - newTpDistancePercent / 100)
30786
+ *
30787
+ * @param percentShift - Value returned by `tpPriceToPercentShift` (or passed to `commitTrailingTake`)
30788
+ * @param originalTakeProfitPrice - Original take-profit price from the pending signal
30789
+ * @param effectivePriceOpen - Effective entry price (from `getPositionAveragePrice`)
30790
+ * @param position - Position direction: "long" or "short"
30791
+ * @returns Absolute take-profit price corresponding to the given percentShift
30792
+ *
30793
+ * @example
30794
+ * // LONG: entry=100, originalTP=110, percentShift=-3
30795
+ * const price = tpPercentShiftToPrice(-3, 110, 100, "long"); // 107
30796
+ */
30797
+ const tpPercentShiftToPrice = (percentShift, originalTakeProfitPrice, effectivePriceOpen, position) => {
30798
+ const originalTpDistancePercent = Math.abs((originalTakeProfitPrice - effectivePriceOpen) / effectivePriceOpen * 100);
30799
+ const newTpDistancePercent = originalTpDistancePercent + percentShift;
30800
+ return position === "long"
30801
+ ? effectivePriceOpen * (1 + newTpDistancePercent / 100)
30802
+ : effectivePriceOpen * (1 - newTpDistancePercent / 100);
30803
+ };
30804
+
30805
+ /**
30806
+ * Compute the dollar cost of a partial close from percentToClose and current invested cost basis.
30807
+ *
30808
+ * cost = (percentToClose / 100) * investedCost
30809
+ *
30810
+ * @param percentToClose - Percentage of position to close (0–100)
30811
+ * @param investedCost - Current invested cost basis (from `getPositionInvestedCost`)
30812
+ * @returns Dollar amount that will be closed
30813
+ *
30814
+ * @example
30815
+ * // Position investedCost=$1000, closing 25%
30816
+ * const cost = percentToCloseCost(25, 1000); // 250
30817
+ */
30818
+ const percentToCloseCost = (percentToClose, investedCost) => {
30819
+ return (percentToClose / 100) * investedCost;
30820
+ };
30821
+
30822
+ /**
30823
+ * Compute the new stop-loss price for a breakeven operation.
30824
+ *
30825
+ * Breakeven moves the SL to the effective entry price (effectivePriceOpen).
30826
+ * The value is the same regardless of position direction.
30827
+ *
30828
+ * @param effectivePriceOpen - Effective entry price (from `getPositionAveragePrice`)
30829
+ * @returns New stop-loss price equal to the effective entry price
30830
+ *
30831
+ * @example
30832
+ * // LONG: entry=100, SL was 90 → breakeven SL = 100
30833
+ * const newSL = breakevenNewStopLossPrice(100); // 100
30834
+ *
30835
+ * // SHORT: entry=100, SL was 110 → breakeven SL = 100
30836
+ * const newSL = breakevenNewStopLossPrice(100); // 100
30837
+ */
30838
+ const breakevenNewStopLossPrice = (effectivePriceOpen) => {
30839
+ return effectivePriceOpen;
30840
+ };
30841
+
30842
+ /**
30843
+ * Compute the effective take-profit price for a breakeven operation.
30844
+ *
30845
+ * Breakeven does not change the take-profit. Returns the currently effective TP:
30846
+ * `_trailingPriceTakeProfit` if set (trailing TP active), otherwise `priceTakeProfit`.
30847
+ *
30848
+ * @param priceTakeProfit - Original take-profit price from the pending signal
30849
+ * @param trailingPriceTakeProfit - Trailing take-profit override, or undefined if not set
30850
+ * @returns Effective take-profit price (unchanged by breakeven)
30851
+ *
30852
+ * @example
30853
+ * // No trailing TP set
30854
+ * const newTP = breakevenNewTakeProfitPrice(110, undefined); // 110
30855
+ *
30856
+ * // Trailing TP active
30857
+ * const newTP = breakevenNewTakeProfitPrice(110, 107); // 107
30858
+ */
30859
+ const breakevenNewTakeProfitPrice = (priceTakeProfit, trailingPriceTakeProfit) => {
30860
+ return trailingPriceTakeProfit ?? priceTakeProfit;
30861
+ };
30862
+
30863
+ const BROKER_METHOD_NAME_COMMIT_SIGNAL_OPEN = "BrokerAdapter.commitSignalOpen";
30864
+ const BROKER_METHOD_NAME_COMMIT_SIGNAL_CLOSE = "BrokerAdapter.commitSignalClose";
30865
+ const BROKER_METHOD_NAME_COMMIT_PARTIAL_PROFIT = "BrokerAdapter.commitPartialProfit";
30866
+ const BROKER_METHOD_NAME_COMMIT_PARTIAL_LOSS = "BrokerAdapter.commitPartialLoss";
30867
+ const BROKER_METHOD_NAME_COMMIT_TRAILING_STOP = "BrokerAdapter.commitTrailingStop";
30868
+ const BROKER_METHOD_NAME_COMMIT_TRAILING_TAKE = "BrokerAdapter.commitTrailingTake";
30869
+ const BROKER_METHOD_NAME_COMMIT_BREAKEVEN = "BrokerAdapter.commitBreakeven";
30870
+ const BROKER_METHOD_NAME_COMMIT_AVERAGE_BUY = "BrokerAdapter.commitAverageBuy";
30871
+ const BROKER_METHOD_NAME_USE_BROKER_ADAPTER = "BrokerAdapter.useBrokerAdapter";
30872
+ const BROKER_METHOD_NAME_ENABLE = "BrokerAdapter.enable";
30873
+ const BROKER_METHOD_NAME_DISABLE = "BrokerAdapter.disable";
30874
+ const BROKER_BASE_METHOD_NAME_WAIT_FOR_INIT = "BrokerBase.waitForInit";
30875
+ const BROKER_BASE_METHOD_NAME_ON_SIGNAL_OPEN = "BrokerBase.onSignalOpenCommit";
30876
+ const BROKER_BASE_METHOD_NAME_ON_SIGNAL_CLOSE = "BrokerBase.onSignalCloseCommit";
30877
+ const BROKER_BASE_METHOD_NAME_ON_PARTIAL_PROFIT = "BrokerBase.onPartialProfitCommit";
30878
+ const BROKER_BASE_METHOD_NAME_ON_PARTIAL_LOSS = "BrokerBase.onPartialLossCommit";
30879
+ const BROKER_BASE_METHOD_NAME_ON_TRAILING_STOP = "BrokerBase.onTrailingStopCommit";
30880
+ const BROKER_BASE_METHOD_NAME_ON_TRAILING_TAKE = "BrokerBase.onTrailingTakeCommit";
30881
+ const BROKER_BASE_METHOD_NAME_ON_BREAKEVEN = "BrokerBase.onBreakevenCommit";
30882
+ const BROKER_BASE_METHOD_NAME_ON_AVERAGE_BUY = "BrokerBase.onAverageBuyCommit";
30883
+ class BrokerProxy {
30884
+ constructor(_instance) {
30885
+ this._instance = _instance;
30886
+ this.waitForInit = functoolsKit.singleshot(async () => {
30887
+ if (this._instance.waitForInit) {
30888
+ await this._instance.waitForInit();
30889
+ }
30890
+ });
30891
+ }
30892
+ async onSignalOpenCommit(payload) {
30893
+ if (this._instance.onSignalOpenCommit) {
30894
+ await this._instance.onSignalOpenCommit(payload);
30895
+ }
30896
+ }
30897
+ async onSignalCloseCommit(payload) {
30898
+ if (this._instance.onSignalCloseCommit) {
30899
+ await this._instance.onSignalCloseCommit(payload);
30900
+ }
30901
+ }
30902
+ async onPartialProfitCommit(payload) {
30903
+ if (this._instance.onPartialProfitCommit) {
30904
+ await this._instance.onPartialProfitCommit(payload);
30905
+ }
30906
+ }
30907
+ async onPartialLossCommit(payload) {
30908
+ if (this._instance.onPartialLossCommit) {
30909
+ await this._instance.onPartialLossCommit(payload);
30910
+ }
30911
+ }
30912
+ async onTrailingStopCommit(payload) {
30913
+ if (this._instance.onTrailingStopCommit) {
30914
+ await this._instance.onTrailingStopCommit(payload);
30915
+ }
30916
+ }
30917
+ async onTrailingTakeCommit(payload) {
30918
+ if (this._instance.onTrailingTakeCommit) {
30919
+ await this._instance.onTrailingTakeCommit(payload);
30920
+ }
30921
+ }
30922
+ async onBreakevenCommit(payload) {
30923
+ if (this._instance.onBreakevenCommit) {
30924
+ await this._instance.onBreakevenCommit(payload);
30925
+ }
30926
+ }
30927
+ async onAverageBuyCommit(payload) {
30928
+ if (this._instance.onAverageBuyCommit) {
30929
+ await this._instance.onAverageBuyCommit(payload);
30930
+ }
30931
+ }
30932
+ }
30933
+ /**
30934
+ * Facade for broker integration — intercepts all commit* operations before DI-core mutations.
30935
+ *
30936
+ * Acts as a transaction control point: if any commit* method throws, the DI-core mutation
30937
+ * is never reached and the state remains unchanged.
30938
+ *
30939
+ * In backtest mode all commit* calls are silently skipped (payload.backtest === true).
30940
+ * In live mode the call is forwarded to the registered IBroker adapter via BrokerProxy.
30941
+ *
30942
+ * signal-open and signal-close events are routed automatically via syncSubject subscription
30943
+ * (activated on `enable()`). All other commit* methods are called explicitly from
30944
+ * Live.ts / Backtest.ts / strategy.ts before the corresponding strategyCoreService call.
30945
+ *
30946
+ * @example
30947
+ * ```typescript
30948
+ * import { Broker } from "backtest-kit";
30949
+ *
30950
+ * // Register a custom broker adapter
30951
+ * Broker.useBrokerAdapter(MyBrokerAdapter);
30952
+ *
30953
+ * // Activate syncSubject subscription (signal-open / signal-close routing)
30954
+ * const dispose = Broker.enable();
30955
+ *
30956
+ * // ... run strategy ...
30957
+ *
30958
+ * // Deactivate when done
30959
+ * Broker.disable();
30960
+ * ```
30961
+ */
30962
+ class BrokerAdapter {
30963
+ constructor() {
30964
+ this._brokerInstance = null;
30965
+ /**
30966
+ * Forwards a signal-open event to the registered broker adapter.
30967
+ *
30968
+ * Called automatically via syncSubject when `enable()` is active.
30969
+ * Skipped silently in backtest mode or when no adapter is registered.
30970
+ *
30971
+ * @param payload - Signal open details: symbol, cost, position, prices, context, backtest flag
30972
+ *
30973
+ * @example
30974
+ * ```typescript
30975
+ * await Broker.commitSignalOpen({
30976
+ * symbol: "BTCUSDT",
30977
+ * cost: 100,
30978
+ * position: "long",
30979
+ * priceOpen: 50000,
30980
+ * priceTakeProfit: 55000,
30981
+ * priceStopLoss: 48000,
30982
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
30983
+ * backtest: false,
30984
+ * });
30985
+ * ```
30986
+ */
30987
+ this.commitSignalOpen = async (payload) => {
30988
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_SIGNAL_OPEN, {
30989
+ symbol: payload.symbol,
30990
+ context: payload.context,
30991
+ });
30992
+ if (!this.enable.hasValue()) {
30993
+ return;
30994
+ }
30995
+ if (payload.backtest) {
30996
+ return;
30997
+ }
30998
+ await this._brokerInstance?.onSignalOpenCommit(payload);
30999
+ };
31000
+ /**
31001
+ * Forwards a signal-close event to the registered broker adapter.
31002
+ *
31003
+ * Called automatically via syncSubject when `enable()` is active.
31004
+ * Skipped silently in backtest mode or when no adapter is registered.
31005
+ *
31006
+ * @param payload - Signal close details: symbol, cost, position, currentPrice, pnl, context, backtest flag
31007
+ *
31008
+ * @example
31009
+ * ```typescript
31010
+ * await Broker.commitSignalClose({
31011
+ * symbol: "BTCUSDT",
31012
+ * cost: 100,
31013
+ * position: "long",
31014
+ * currentPrice: 54000,
31015
+ * priceTakeProfit: 55000,
31016
+ * priceStopLoss: 48000,
31017
+ * totalEntries: 2,
31018
+ * totalPartials: 1,
31019
+ * pnl: { profit: 80, loss: 0, volume: 100 },
31020
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
31021
+ * backtest: false,
31022
+ * });
31023
+ * ```
31024
+ */
31025
+ this.commitSignalClose = async (payload) => {
31026
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_SIGNAL_CLOSE, {
31027
+ symbol: payload.symbol,
31028
+ context: payload.context,
31029
+ });
31030
+ if (!this.enable.hasValue()) {
31031
+ return;
31032
+ }
31033
+ if (payload.backtest) {
31034
+ return;
31035
+ }
31036
+ await this._brokerInstance?.onSignalCloseCommit(payload);
31037
+ };
31038
+ /**
31039
+ * Intercepts a partial-profit close before DI-core mutation.
31040
+ *
31041
+ * Called explicitly from Live.ts / Backtest.ts / strategy.ts after all validations pass,
31042
+ * but before `strategyCoreService.partialProfit()`. If this method throws, the DI mutation
31043
+ * is skipped and state remains unchanged.
31044
+ *
31045
+ * Skipped silently in backtest mode or when no adapter is registered.
31046
+ *
31047
+ * @param payload - Partial profit details: symbol, percentToClose, cost (dollar value), currentPrice, context, backtest flag
31048
+ *
31049
+ * @example
31050
+ * ```typescript
31051
+ * await Broker.commitPartialProfit({
31052
+ * symbol: "BTCUSDT",
31053
+ * percentToClose: 30,
31054
+ * cost: 30,
31055
+ * currentPrice: 52000,
31056
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
31057
+ * backtest: false,
31058
+ * });
31059
+ * ```
31060
+ */
31061
+ this.commitPartialProfit = async (payload) => {
31062
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_PARTIAL_PROFIT, {
31063
+ symbol: payload.symbol,
31064
+ context: payload.context,
31065
+ });
31066
+ if (!this.enable.hasValue()) {
31067
+ return;
31068
+ }
31069
+ if (payload.backtest) {
31070
+ return;
31071
+ }
31072
+ await this._brokerInstance?.onPartialProfitCommit(payload);
31073
+ };
31074
+ /**
31075
+ * Intercepts a partial-loss close before DI-core mutation.
31076
+ *
31077
+ * Called explicitly from Live.ts / Backtest.ts / strategy.ts after all validations pass,
31078
+ * but before `strategyCoreService.partialLoss()`. If this method throws, the DI mutation
31079
+ * is skipped and state remains unchanged.
31080
+ *
31081
+ * Skipped silently in backtest mode or when no adapter is registered.
31082
+ *
31083
+ * @param payload - Partial loss details: symbol, percentToClose, cost (dollar value), currentPrice, context, backtest flag
31084
+ *
31085
+ * @example
31086
+ * ```typescript
31087
+ * await Broker.commitPartialLoss({
31088
+ * symbol: "BTCUSDT",
31089
+ * percentToClose: 40,
31090
+ * cost: 40,
31091
+ * currentPrice: 48500,
31092
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
31093
+ * backtest: false,
31094
+ * });
31095
+ * ```
31096
+ */
31097
+ this.commitPartialLoss = async (payload) => {
31098
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_PARTIAL_LOSS, {
31099
+ symbol: payload.symbol,
31100
+ context: payload.context,
31101
+ });
31102
+ if (!this.enable.hasValue()) {
31103
+ return;
31104
+ }
31105
+ if (payload.backtest) {
31106
+ return;
31107
+ }
31108
+ await this._brokerInstance?.onPartialLossCommit(payload);
31109
+ };
31110
+ /**
31111
+ * Intercepts a trailing stop-loss update before DI-core mutation.
31112
+ *
31113
+ * Called explicitly after all validations pass, but before `strategyCoreService.trailingStop()`.
31114
+ * `newStopLossPrice` is the absolute price computed from percentShift + original SL + effectivePriceOpen.
31115
+ *
31116
+ * Skipped silently in backtest mode or when no adapter is registered.
31117
+ *
31118
+ * @param payload - Trailing stop details: symbol, percentShift, currentPrice, newStopLossPrice, context, backtest flag
31119
+ *
31120
+ * @example
31121
+ * ```typescript
31122
+ * // LONG: entry=100, originalSL=90, percentShift=-5 → newSL=95
31123
+ * await Broker.commitTrailingStop({
31124
+ * symbol: "BTCUSDT",
31125
+ * percentShift: -5,
31126
+ * currentPrice: 102,
31127
+ * newStopLossPrice: 95,
31128
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
31129
+ * backtest: false,
31130
+ * });
31131
+ * ```
31132
+ */
31133
+ this.commitTrailingStop = async (payload) => {
31134
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_TRAILING_STOP, {
31135
+ symbol: payload.symbol,
31136
+ context: payload.context,
31137
+ });
31138
+ if (!this.enable.hasValue()) {
31139
+ return;
31140
+ }
31141
+ if (payload.backtest) {
31142
+ return;
31143
+ }
31144
+ await this._brokerInstance?.onTrailingStopCommit(payload);
31145
+ };
31146
+ /**
31147
+ * Intercepts a trailing take-profit update before DI-core mutation.
31148
+ *
31149
+ * Called explicitly after all validations pass, but before `strategyCoreService.trailingTake()`.
31150
+ * `newTakeProfitPrice` is the absolute price computed from percentShift + original TP + effectivePriceOpen.
31151
+ *
31152
+ * Skipped silently in backtest mode or when no adapter is registered.
31153
+ *
31154
+ * @param payload - Trailing take details: symbol, percentShift, currentPrice, newTakeProfitPrice, context, backtest flag
31155
+ *
31156
+ * @example
31157
+ * ```typescript
31158
+ * // LONG: entry=100, originalTP=110, percentShift=-3 → newTP=107
31159
+ * await Broker.commitTrailingTake({
31160
+ * symbol: "BTCUSDT",
31161
+ * percentShift: -3,
31162
+ * currentPrice: 102,
31163
+ * newTakeProfitPrice: 107,
31164
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
31165
+ * backtest: false,
31166
+ * });
31167
+ * ```
31168
+ */
31169
+ this.commitTrailingTake = async (payload) => {
31170
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_TRAILING_TAKE, {
31171
+ symbol: payload.symbol,
31172
+ context: payload.context,
31173
+ });
31174
+ if (!this.enable.hasValue()) {
31175
+ return;
31176
+ }
31177
+ if (payload.backtest) {
31178
+ return;
31179
+ }
31180
+ await this._brokerInstance?.onTrailingTakeCommit(payload);
31181
+ };
31182
+ /**
31183
+ * Intercepts a breakeven operation before DI-core mutation.
31184
+ *
31185
+ * Called explicitly after all validations pass, but before `strategyCoreService.breakeven()`.
31186
+ * `newStopLossPrice` equals effectivePriceOpen (entry price).
31187
+ * `newTakeProfitPrice` equals `_trailingPriceTakeProfit ?? priceTakeProfit` (TP is unchanged by breakeven).
31188
+ *
31189
+ * Skipped silently in backtest mode or when no adapter is registered.
31190
+ *
31191
+ * @param payload - Breakeven details: symbol, currentPrice, newStopLossPrice, newTakeProfitPrice, context, backtest flag
31192
+ *
31193
+ * @example
31194
+ * ```typescript
31195
+ * // LONG: entry=100, currentPrice=100.5, newSL=100 (entry), newTP=110 (unchanged)
31196
+ * await Broker.commitBreakeven({
31197
+ * symbol: "BTCUSDT",
31198
+ * currentPrice: 100.5,
31199
+ * newStopLossPrice: 100,
31200
+ * newTakeProfitPrice: 110,
31201
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
31202
+ * backtest: false,
31203
+ * });
31204
+ * ```
31205
+ */
31206
+ this.commitBreakeven = async (payload) => {
31207
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_BREAKEVEN, {
31208
+ symbol: payload.symbol,
31209
+ context: payload.context,
31210
+ });
31211
+ if (!this.enable.hasValue()) {
31212
+ return;
31213
+ }
31214
+ if (payload.backtest) {
31215
+ return;
31216
+ }
31217
+ await this._brokerInstance?.onBreakevenCommit(payload);
31218
+ };
31219
+ /**
31220
+ * Intercepts a DCA average-buy entry before DI-core mutation.
31221
+ *
31222
+ * Called explicitly after all validations pass, but before `strategyCoreService.averageBuy()`.
31223
+ * `currentPrice` is the market price at which the new DCA entry is added.
31224
+ * `cost` is the dollar amount of the new entry (default: CC_POSITION_ENTRY_COST).
31225
+ *
31226
+ * Skipped silently in backtest mode or when no adapter is registered.
31227
+ *
31228
+ * @param payload - Average buy details: symbol, currentPrice, cost, context, backtest flag
31229
+ *
31230
+ * @example
31231
+ * ```typescript
31232
+ * // Add DCA entry at current market price
31233
+ * await Broker.commitAverageBuy({
31234
+ * symbol: "BTCUSDT",
31235
+ * currentPrice: 42000,
31236
+ * cost: 100,
31237
+ * context: { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" },
31238
+ * backtest: false,
31239
+ * });
31240
+ * ```
31241
+ */
31242
+ this.commitAverageBuy = async (payload) => {
31243
+ bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_AVERAGE_BUY, {
31244
+ symbol: payload.symbol,
31245
+ context: payload.context,
31246
+ });
31247
+ if (!this.enable.hasValue()) {
31248
+ return;
31249
+ }
31250
+ if (payload.backtest) {
31251
+ return;
31252
+ }
31253
+ await this._brokerInstance?.onAverageBuyCommit(payload);
31254
+ };
31255
+ /**
31256
+ * Registers a broker adapter instance or constructor to receive commit* callbacks.
31257
+ *
31258
+ * Must be called before `enable()`. Accepts either a class constructor (called with `new`)
31259
+ * or an already-instantiated object implementing `Partial<IBroker>`.
31260
+ *
31261
+ * @param broker - IBroker constructor or instance
31262
+ *
31263
+ * @example
31264
+ * ```typescript
31265
+ * import { Broker } from "backtest-kit";
31266
+ *
31267
+ * // Register via constructor
31268
+ * Broker.useBrokerAdapter(MyBrokerAdapter);
31269
+ *
31270
+ * // Register via instance
31271
+ * Broker.useBrokerAdapter(new MyBrokerAdapter());
31272
+ * ```
31273
+ */
31274
+ this.useBrokerAdapter = (broker) => {
31275
+ bt.loggerService.info(BROKER_METHOD_NAME_USE_BROKER_ADAPTER, {});
31276
+ if (typeof broker === "function") {
31277
+ const instance = Reflect.construct(broker, []);
31278
+ this._brokerInstance = new BrokerProxy(instance);
31279
+ return;
31280
+ }
31281
+ this._brokerInstance = new BrokerProxy(broker);
31282
+ };
31283
+ /**
31284
+ * Activates the broker: subscribes to syncSubject for signal-open / signal-close routing.
31285
+ *
31286
+ * Must be called after `useBrokerAdapter()`. Returns a dispose function that unsubscribes
31287
+ * from syncSubject (equivalent to calling `disable()`).
31288
+ *
31289
+ * Calling `enable()` without a registered adapter throws immediately.
31290
+ * Calling `enable()` more than once is idempotent (singleshot guard).
31291
+ *
31292
+ * @returns Dispose function — call it to deactivate the broker subscription
31293
+ *
31294
+ * @example
31295
+ * ```typescript
31296
+ * import { Broker } from "backtest-kit";
31297
+ *
31298
+ * Broker.useBrokerAdapter(MyBrokerAdapter);
31299
+ * const dispose = Broker.enable();
31300
+ *
31301
+ * // ... run backtest or live session ...
31302
+ *
31303
+ * dispose(); // or Broker.disable()
31304
+ * ```
31305
+ */
31306
+ this.enable = functoolsKit.singleshot(() => {
31307
+ bt.loggerService.info(BROKER_METHOD_NAME_ENABLE, {});
31308
+ if (!this._brokerInstance) {
31309
+ this.enable.clear();
31310
+ throw new Error("No broker instance provided. Call Broker.useBrokerAdapter first.");
31311
+ }
31312
+ const unSignalOpen = syncSubject.subscribe(async (event) => {
31313
+ if (event.action !== "signal-open") {
31314
+ return;
31315
+ }
31316
+ await this.commitSignalOpen({
31317
+ position: event.signal.position,
31318
+ cost: event.signal.cost,
31319
+ symbol: event.symbol,
31320
+ priceTakeProfit: event.signal.priceTakeProfit,
31321
+ priceStopLoss: event.signal.priceStopLoss,
31322
+ priceOpen: event.signal.priceOpen,
31323
+ context: {
31324
+ strategyName: event.strategyName,
31325
+ exchangeName: event.exchangeName,
31326
+ frameName: event.frameName,
31327
+ },
31328
+ backtest: event.backtest,
31329
+ });
31330
+ });
31331
+ const unSignalClose = syncSubject.subscribe(async (event) => {
31332
+ if (event.action !== "signal-close") {
31333
+ return;
31334
+ }
31335
+ await this.commitSignalClose({
31336
+ position: event.signal.position,
31337
+ currentPrice: event.currentPrice,
31338
+ cost: event.signal.cost,
31339
+ symbol: event.symbol,
31340
+ pnl: event.pnl,
31341
+ totalEntries: event.totalEntries,
31342
+ totalPartials: event.totalPartials,
31343
+ priceStopLoss: event.signal.priceStopLoss,
31344
+ priceTakeProfit: event.signal.priceTakeProfit,
31345
+ context: {
31346
+ strategyName: event.strategyName,
31347
+ exchangeName: event.exchangeName,
31348
+ frameName: event.frameName,
31349
+ },
31350
+ backtest: event.backtest,
31351
+ });
31352
+ });
31353
+ const disposeFn = functoolsKit.compose(() => unSignalOpen(), () => unSignalClose());
31354
+ return () => {
31355
+ this.enable.clear();
31356
+ disposeFn();
31357
+ };
31358
+ });
31359
+ /**
31360
+ * Deactivates the broker: unsubscribes from syncSubject and resets the singleshot guard.
31361
+ *
31362
+ * Idempotent — safe to call even if `enable()` was never called.
31363
+ * After `disable()`, `enable()` can be called again to reactivate.
31364
+ *
31365
+ * @example
31366
+ * ```typescript
31367
+ * import { Broker } from "backtest-kit";
31368
+ *
31369
+ * Broker.useBrokerAdapter(MyBrokerAdapter);
31370
+ * Broker.enable();
31371
+ *
31372
+ * // Stop receiving events
31373
+ * Broker.disable();
31374
+ * ```
31375
+ */
31376
+ this.disable = () => {
31377
+ bt.loggerService.info(BROKER_METHOD_NAME_DISABLE, {});
31378
+ if (this.enable.hasValue()) {
31379
+ const lastSubscription = this.enable();
31380
+ lastSubscription();
31381
+ }
31382
+ };
31383
+ }
31384
+ }
31385
+ /**
31386
+ * Base class for custom broker adapter implementations.
31387
+ *
31388
+ * Provides default no-op implementations for all IBroker methods that log events.
31389
+ * Extend this class to implement a real exchange adapter for:
31390
+ * - Placing and canceling limit/market orders
31391
+ * - Updating stop-loss and take-profit levels on exchange
31392
+ * - Tracking position state in an external system
31393
+ * - Sending trade notifications (Telegram, Discord, Email)
31394
+ * - Recording trades to a database or analytics service
31395
+ *
31396
+ * Key features:
31397
+ * - All methods have default implementations (no need to override unused methods)
31398
+ * - Automatic logging of all events via bt.loggerService
31399
+ * - Implements the full IBroker interface
31400
+ * - `makeExtendable` applied for correct subclass instantiation
31401
+ *
31402
+ * Lifecycle:
31403
+ * 1. Constructor called (no arguments)
31404
+ * 2. `waitForInit()` called once for async initialization (e.g. exchange login)
31405
+ * 3. Event methods called as strategy executes
31406
+ * 4. No explicit dispose — clean up in `waitForInit` teardown or externally
31407
+ *
31408
+ * Event flow (called only in live mode, skipped in backtest):
31409
+ * - `onSignalOpenCommit` — new position opened
31410
+ * - `onSignalCloseCommit` — position closed (SL/TP hit or manual close)
31411
+ * - `onPartialProfitCommit` — partial close at profit executed
31412
+ * - `onPartialLossCommit` — partial close at loss executed
31413
+ * - `onTrailingStopCommit` — trailing stop-loss updated
31414
+ * - `onTrailingTakeCommit` — trailing take-profit updated
31415
+ * - `onBreakevenCommit` — stop-loss moved to entry price
31416
+ * - `onAverageBuyCommit` — new DCA entry added to position
31417
+ *
31418
+ * @example
31419
+ * ```typescript
31420
+ * import { BrokerBase, Broker } from "backtest-kit";
31421
+ *
31422
+ * // Extend BrokerBase and override only needed methods
31423
+ * class BinanceBroker extends BrokerBase {
31424
+ * private client: BinanceClient | null = null;
31425
+ *
31426
+ * async waitForInit() {
31427
+ * super.waitForInit(); // Call parent for logging
31428
+ * this.client = new BinanceClient(process.env.API_KEY, process.env.SECRET);
31429
+ * await this.client.connect();
31430
+ * }
31431
+ *
31432
+ * async onSignalOpenCommit(payload: BrokerSignalOpenPayload) {
31433
+ * super.onSignalOpenCommit(payload); // Call parent for logging
31434
+ * await this.client!.placeOrder({
31435
+ * symbol: payload.symbol,
31436
+ * side: payload.position === "long" ? "BUY" : "SELL",
31437
+ * quantity: payload.cost / payload.priceOpen,
31438
+ * });
31439
+ * }
31440
+ *
31441
+ * async onSignalCloseCommit(payload: BrokerSignalClosePayload) {
31442
+ * super.onSignalCloseCommit(payload); // Call parent for logging
31443
+ * await this.client!.closePosition(payload.symbol);
31444
+ * }
31445
+ * }
31446
+ *
31447
+ * // Register the adapter
31448
+ * Broker.useBrokerAdapter(BinanceBroker);
31449
+ * Broker.enable();
31450
+ * ```
31451
+ *
31452
+ * @example
31453
+ * ```typescript
31454
+ * // Minimal implementation — only handle opens and closes
31455
+ * class NotifyBroker extends BrokerBase {
31456
+ * async onSignalOpenCommit(payload: BrokerSignalOpenPayload) {
31457
+ * await sendTelegram(`Opened ${payload.position} on ${payload.symbol}`);
31458
+ * }
31459
+ *
31460
+ * async onSignalCloseCommit(payload: BrokerSignalClosePayload) {
31461
+ * const pnl = payload.pnl.profit - payload.pnl.loss;
31462
+ * await sendTelegram(`Closed ${payload.symbol}: PnL $${pnl.toFixed(2)}`);
31463
+ * }
31464
+ * }
31465
+ * ```
31466
+ */
31467
+ class BrokerBase {
31468
+ /**
31469
+ * Performs async initialization before the broker starts receiving events.
31470
+ *
31471
+ * Called once by BrokerProxy via `waitForInit()` (singleshot) before the first event.
31472
+ * Override to establish exchange connections, authenticate API clients, load configuration.
31473
+ *
31474
+ * Default implementation: Logs initialization event.
31475
+ *
31476
+ * @example
31477
+ * ```typescript
31478
+ * async waitForInit() {
31479
+ * super.waitForInit(); // Keep parent logging
31480
+ * this.exchange = new ExchangeClient(process.env.API_KEY);
31481
+ * await this.exchange.authenticate();
31482
+ * }
31483
+ * ```
31484
+ */
31485
+ async waitForInit() {
31486
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_WAIT_FOR_INIT, {});
31487
+ }
31488
+ /**
31489
+ * Called when a new position is opened (signal activated).
31490
+ *
31491
+ * Triggered automatically via syncSubject when a scheduled signal's priceOpen is hit.
31492
+ * Use to place the actual entry order on the exchange.
31493
+ *
31494
+ * Default implementation: Logs signal-open event.
31495
+ *
31496
+ * @param payload - Signal open details: symbol, cost, position, priceOpen, priceTakeProfit, priceStopLoss, context, backtest
31497
+ *
31498
+ * @example
31499
+ * ```typescript
31500
+ * async onSignalOpenCommit(payload: BrokerSignalOpenPayload) {
31501
+ * super.onSignalOpenCommit(payload); // Keep parent logging
31502
+ * await this.exchange.placeMarketOrder({
31503
+ * symbol: payload.symbol,
31504
+ * side: payload.position === "long" ? "BUY" : "SELL",
31505
+ * quantity: payload.cost / payload.priceOpen,
31506
+ * });
31507
+ * }
31508
+ * ```
31509
+ */
31510
+ async onSignalOpenCommit(payload) {
31511
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SIGNAL_OPEN, {
31512
+ symbol: payload.symbol,
31513
+ context: payload.context,
31514
+ });
31515
+ }
31516
+ /**
31517
+ * Called when a position is fully closed (SL/TP hit or manual close).
31518
+ *
31519
+ * Triggered automatically via syncSubject when a pending signal is closed.
31520
+ * Use to place the exit order and record final PnL.
31521
+ *
31522
+ * Default implementation: Logs signal-close event.
31523
+ *
31524
+ * @param payload - Signal close details: symbol, cost, position, currentPrice, pnl, totalEntries, totalPartials, context, backtest
31525
+ *
31526
+ * @example
31527
+ * ```typescript
31528
+ * async onSignalCloseCommit(payload: BrokerSignalClosePayload) {
31529
+ * super.onSignalCloseCommit(payload); // Keep parent logging
31530
+ * await this.exchange.closePosition(payload.symbol);
31531
+ * await this.db.recordTrade({ symbol: payload.symbol, pnl: payload.pnl });
31532
+ * }
31533
+ * ```
31534
+ */
31535
+ async onSignalCloseCommit(payload) {
31536
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SIGNAL_CLOSE, {
31537
+ symbol: payload.symbol,
31538
+ context: payload.context,
31539
+ });
31540
+ }
31541
+ /**
31542
+ * Called when a partial close at profit is executed.
31543
+ *
31544
+ * Triggered explicitly from strategy.ts / Live.ts / Backtest.ts after all validations pass,
31545
+ * before `strategyCoreService.partialProfit()`. If this method throws, the DI mutation is skipped.
31546
+ * Use to partially close the position on the exchange at the profit level.
31547
+ *
31548
+ * Default implementation: Logs partial profit event.
31549
+ *
31550
+ * @param payload - Partial profit details: symbol, percentToClose, cost (dollar value), currentPrice, context, backtest
31551
+ *
31552
+ * @example
31553
+ * ```typescript
31554
+ * async onPartialProfitCommit(payload: BrokerPartialProfitPayload) {
31555
+ * super.onPartialProfitCommit(payload); // Keep parent logging
31556
+ * await this.exchange.reducePosition({
31557
+ * symbol: payload.symbol,
31558
+ * dollarAmount: payload.cost,
31559
+ * price: payload.currentPrice,
31560
+ * });
31561
+ * }
31562
+ * ```
31563
+ */
31564
+ async onPartialProfitCommit(payload) {
31565
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_PARTIAL_PROFIT, {
31566
+ symbol: payload.symbol,
31567
+ context: payload.context,
31568
+ });
31569
+ }
31570
+ /**
31571
+ * Called when a partial close at loss is executed.
31572
+ *
31573
+ * Triggered explicitly from strategy.ts / Live.ts / Backtest.ts after all validations pass,
31574
+ * before `strategyCoreService.partialLoss()`. If this method throws, the DI mutation is skipped.
31575
+ * Use to partially close the position on the exchange at the loss level.
31576
+ *
31577
+ * Default implementation: Logs partial loss event.
31578
+ *
31579
+ * @param payload - Partial loss details: symbol, percentToClose, cost (dollar value), currentPrice, context, backtest
31580
+ *
31581
+ * @example
31582
+ * ```typescript
31583
+ * async onPartialLossCommit(payload: BrokerPartialLossPayload) {
31584
+ * super.onPartialLossCommit(payload); // Keep parent logging
31585
+ * await this.exchange.reducePosition({
31586
+ * symbol: payload.symbol,
31587
+ * dollarAmount: payload.cost,
31588
+ * price: payload.currentPrice,
31589
+ * });
31590
+ * }
31591
+ * ```
31592
+ */
31593
+ async onPartialLossCommit(payload) {
31594
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_PARTIAL_LOSS, {
31595
+ symbol: payload.symbol,
31596
+ context: payload.context,
31597
+ });
31598
+ }
31599
+ /**
31600
+ * Called when the trailing stop-loss level is updated.
31601
+ *
31602
+ * Triggered explicitly after all validations pass, before `strategyCoreService.trailingStop()`.
31603
+ * `newStopLossPrice` is the absolute SL price — use it to update the exchange order directly.
31604
+ *
31605
+ * Default implementation: Logs trailing stop event.
31606
+ *
31607
+ * @param payload - Trailing stop details: symbol, percentShift, currentPrice, newStopLossPrice, context, backtest
31608
+ *
31609
+ * @example
31610
+ * ```typescript
31611
+ * async onTrailingStopCommit(payload: BrokerTrailingStopPayload) {
31612
+ * super.onTrailingStopCommit(payload); // Keep parent logging
31613
+ * await this.exchange.updateStopLoss({
31614
+ * symbol: payload.symbol,
31615
+ * price: payload.newStopLossPrice,
31616
+ * });
31617
+ * }
31618
+ * ```
31619
+ */
31620
+ async onTrailingStopCommit(payload) {
31621
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_TRAILING_STOP, {
31622
+ symbol: payload.symbol,
31623
+ context: payload.context,
31624
+ });
31625
+ }
31626
+ /**
31627
+ * Called when the trailing take-profit level is updated.
31628
+ *
31629
+ * Triggered explicitly after all validations pass, before `strategyCoreService.trailingTake()`.
31630
+ * `newTakeProfitPrice` is the absolute TP price — use it to update the exchange order directly.
31631
+ *
31632
+ * Default implementation: Logs trailing take event.
31633
+ *
31634
+ * @param payload - Trailing take details: symbol, percentShift, currentPrice, newTakeProfitPrice, context, backtest
31635
+ *
31636
+ * @example
31637
+ * ```typescript
31638
+ * async onTrailingTakeCommit(payload: BrokerTrailingTakePayload) {
31639
+ * super.onTrailingTakeCommit(payload); // Keep parent logging
31640
+ * await this.exchange.updateTakeProfit({
31641
+ * symbol: payload.symbol,
31642
+ * price: payload.newTakeProfitPrice,
31643
+ * });
31644
+ * }
31645
+ * ```
31646
+ */
31647
+ async onTrailingTakeCommit(payload) {
31648
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_TRAILING_TAKE, {
31649
+ symbol: payload.symbol,
31650
+ context: payload.context,
31651
+ });
31652
+ }
31653
+ /**
31654
+ * Called when the stop-loss is moved to breakeven (entry price).
31655
+ *
31656
+ * Triggered explicitly after all validations pass, before `strategyCoreService.breakeven()`.
31657
+ * `newStopLossPrice` equals `effectivePriceOpen` — the position's effective entry price.
31658
+ * `newTakeProfitPrice` is unchanged by breakeven.
31659
+ *
31660
+ * Default implementation: Logs breakeven event.
31661
+ *
31662
+ * @param payload - Breakeven details: symbol, currentPrice, newStopLossPrice, newTakeProfitPrice, context, backtest
31663
+ *
31664
+ * @example
31665
+ * ```typescript
31666
+ * async onBreakevenCommit(payload: BrokerBreakevenPayload) {
31667
+ * super.onBreakevenCommit(payload); // Keep parent logging
31668
+ * await this.exchange.updateStopLoss({
31669
+ * symbol: payload.symbol,
31670
+ * price: payload.newStopLossPrice, // = entry price
31671
+ * });
31672
+ * }
31673
+ * ```
31674
+ */
31675
+ async onBreakevenCommit(payload) {
31676
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_BREAKEVEN, {
31677
+ symbol: payload.symbol,
31678
+ context: payload.context,
31679
+ });
31680
+ }
31681
+ /**
31682
+ * Called when a new DCA entry is added to the active position.
31683
+ *
31684
+ * Triggered explicitly after all validations pass, before `strategyCoreService.averageBuy()`.
31685
+ * `currentPrice` is the market price at which the new averaging entry is placed.
31686
+ * `cost` is the dollar amount of the new DCA entry.
31687
+ *
31688
+ * Default implementation: Logs average buy event.
31689
+ *
31690
+ * @param payload - Average buy details: symbol, currentPrice, cost, context, backtest
31691
+ *
31692
+ * @example
31693
+ * ```typescript
31694
+ * async onAverageBuyCommit(payload: BrokerAverageBuyPayload) {
31695
+ * super.onAverageBuyCommit(payload); // Keep parent logging
31696
+ * await this.exchange.placeMarketOrder({
31697
+ * symbol: payload.symbol,
31698
+ * side: "BUY",
31699
+ * quantity: payload.cost / payload.currentPrice,
31700
+ * });
31701
+ * }
31702
+ * ```
31703
+ */
31704
+ async onAverageBuyCommit(payload) {
31705
+ bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_AVERAGE_BUY, {
31706
+ symbol: payload.symbol,
31707
+ context: payload.context,
31708
+ });
31709
+ }
31710
+ }
31711
+ // @ts-ignore
31712
+ BrokerBase = functoolsKit.makeExtendable(BrokerBase);
31713
+ /**
31714
+ * Global singleton instance of BrokerAdapter.
31715
+ * Provides static-like access to all broker commit methods and lifecycle controls.
31716
+ *
31717
+ * @example
31718
+ * ```typescript
31719
+ * import { Broker } from "backtest-kit";
31720
+ *
31721
+ * Broker.useBrokerAdapter(MyBrokerAdapter);
31722
+ * const dispose = Broker.enable();
31723
+ * ```
31724
+ */
31725
+ const Broker = new BrokerAdapter();
31726
+
30160
31727
  const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
30161
31728
  const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
30162
31729
  const PARTIAL_PROFIT_METHOD_NAME = "strategy.commitPartialProfit";
@@ -30165,6 +31732,8 @@ const PARTIAL_PROFIT_COST_METHOD_NAME = "strategy.commitPartialProfitCost";
30165
31732
  const PARTIAL_LOSS_COST_METHOD_NAME = "strategy.commitPartialLossCost";
30166
31733
  const TRAILING_STOP_METHOD_NAME = "strategy.commitTrailingStop";
30167
31734
  const TRAILING_PROFIT_METHOD_NAME = "strategy.commitTrailingTake";
31735
+ const TRAILING_STOP_COST_METHOD_NAME = "strategy.commitTrailingStopCost";
31736
+ const TRAILING_PROFIT_COST_METHOD_NAME = "strategy.commitTrailingTakeCost";
30168
31737
  const BREAKEVEN_METHOD_NAME = "strategy.commitBreakeven";
30169
31738
  const ACTIVATE_SCHEDULED_METHOD_NAME = "strategy.commitActivateScheduled";
30170
31739
  const AVERAGE_BUY_METHOD_NAME = "strategy.commitAverageBuy";
@@ -30294,6 +31863,28 @@ async function commitPartialProfit(symbol, percentToClose) {
30294
31863
  const currentPrice = await getAveragePrice(symbol);
30295
31864
  const { backtest: isBacktest } = bt.executionContextService.context;
30296
31865
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
31866
+ const investedCostForProfit = await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
31867
+ if (investedCostForProfit === null) {
31868
+ return false;
31869
+ }
31870
+ const signalForProfit = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
31871
+ if (!signalForProfit) {
31872
+ return false;
31873
+ }
31874
+ if (await functoolsKit.not(bt.strategyCoreService.validatePartialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName }))) {
31875
+ return false;
31876
+ }
31877
+ await Broker.commitPartialProfit({
31878
+ symbol,
31879
+ percentToClose,
31880
+ cost: percentToCloseCost(percentToClose, investedCostForProfit),
31881
+ currentPrice,
31882
+ position: signalForProfit.position,
31883
+ priceTakeProfit: signalForProfit.priceTakeProfit,
31884
+ priceStopLoss: signalForProfit.priceStopLoss,
31885
+ context: { exchangeName, frameName, strategyName },
31886
+ backtest: isBacktest,
31887
+ });
30297
31888
  return await bt.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
30298
31889
  }
30299
31890
  /**
@@ -30337,6 +31928,28 @@ async function commitPartialLoss(symbol, percentToClose) {
30337
31928
  const currentPrice = await getAveragePrice(symbol);
30338
31929
  const { backtest: isBacktest } = bt.executionContextService.context;
30339
31930
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
31931
+ const investedCostForLoss = await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
31932
+ if (investedCostForLoss === null) {
31933
+ return false;
31934
+ }
31935
+ const signalForLoss = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
31936
+ if (!signalForLoss) {
31937
+ return false;
31938
+ }
31939
+ if (await functoolsKit.not(bt.strategyCoreService.validatePartialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName }))) {
31940
+ return false;
31941
+ }
31942
+ await Broker.commitPartialLoss({
31943
+ symbol,
31944
+ percentToClose,
31945
+ cost: percentToCloseCost(percentToClose, investedCostForLoss),
31946
+ currentPrice,
31947
+ position: signalForLoss.position,
31948
+ priceTakeProfit: signalForLoss.priceTakeProfit,
31949
+ priceStopLoss: signalForLoss.priceStopLoss,
31950
+ context: { exchangeName, frameName, strategyName },
31951
+ backtest: isBacktest,
31952
+ });
30340
31953
  return await bt.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
30341
31954
  }
30342
31955
  /**
@@ -30396,6 +32009,26 @@ async function commitTrailingStop(symbol, percentShift, currentPrice) {
30396
32009
  }
30397
32010
  const { backtest: isBacktest } = bt.executionContextService.context;
30398
32011
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32012
+ const signal = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32013
+ if (!signal) {
32014
+ return false;
32015
+ }
32016
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
32017
+ if (effectivePriceOpen === null) {
32018
+ return false;
32019
+ }
32020
+ if (await functoolsKit.not(bt.strategyCoreService.validateTrailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName }))) {
32021
+ return false;
32022
+ }
32023
+ await Broker.commitTrailingStop({
32024
+ symbol,
32025
+ percentShift,
32026
+ currentPrice,
32027
+ newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
32028
+ position: signal.position,
32029
+ context: { exchangeName, frameName, strategyName },
32030
+ backtest: isBacktest,
32031
+ });
30399
32032
  return await bt.strategyCoreService.trailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
30400
32033
  }
30401
32034
  /**
@@ -30455,6 +32088,126 @@ async function commitTrailingTake(symbol, percentShift, currentPrice) {
30455
32088
  }
30456
32089
  const { backtest: isBacktest } = bt.executionContextService.context;
30457
32090
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32091
+ const signal = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32092
+ if (!signal) {
32093
+ return false;
32094
+ }
32095
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
32096
+ if (effectivePriceOpen === null) {
32097
+ return false;
32098
+ }
32099
+ if (await functoolsKit.not(bt.strategyCoreService.validateTrailingTake(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName }))) {
32100
+ return false;
32101
+ }
32102
+ await Broker.commitTrailingTake({
32103
+ symbol,
32104
+ percentShift,
32105
+ currentPrice,
32106
+ newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
32107
+ position: signal.position,
32108
+ context: { exchangeName, frameName, strategyName },
32109
+ backtest: isBacktest,
32110
+ });
32111
+ return await bt.strategyCoreService.trailingTake(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
32112
+ }
32113
+ /**
32114
+ * Adjusts the trailing stop-loss to an absolute price level.
32115
+ *
32116
+ * Convenience wrapper around commitTrailingStop that converts an absolute
32117
+ * stop-loss price to a percentShift relative to the ORIGINAL SL distance.
32118
+ *
32119
+ * Automatically detects backtest/live mode from execution context.
32120
+ * Automatically fetches current price via getAveragePrice.
32121
+ *
32122
+ * @param symbol - Trading pair symbol
32123
+ * @param newStopLossPrice - Desired absolute stop-loss price
32124
+ * @returns Promise<boolean> - true if trailing SL was set/updated, false if rejected
32125
+ */
32126
+ async function commitTrailingStopCost(symbol, newStopLossPrice) {
32127
+ bt.loggerService.info(TRAILING_STOP_COST_METHOD_NAME, {
32128
+ symbol,
32129
+ newStopLossPrice,
32130
+ });
32131
+ if (!ExecutionContextService.hasContext()) {
32132
+ throw new Error("commitTrailingStopCost requires an execution context");
32133
+ }
32134
+ if (!MethodContextService.hasContext()) {
32135
+ throw new Error("commitTrailingStopCost requires a method context");
32136
+ }
32137
+ const currentPrice = await getAveragePrice(symbol);
32138
+ const { backtest: isBacktest } = bt.executionContextService.context;
32139
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32140
+ const signal = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32141
+ if (!signal) {
32142
+ return false;
32143
+ }
32144
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
32145
+ if (effectivePriceOpen === null) {
32146
+ return false;
32147
+ }
32148
+ const percentShift = slPriceToPercentShift(newStopLossPrice, signal.priceStopLoss, effectivePriceOpen);
32149
+ if (await functoolsKit.not(bt.strategyCoreService.validateTrailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName }))) {
32150
+ return false;
32151
+ }
32152
+ await Broker.commitTrailingStop({
32153
+ symbol,
32154
+ percentShift,
32155
+ currentPrice,
32156
+ newStopLossPrice,
32157
+ position: signal.position,
32158
+ context: { exchangeName, frameName, strategyName },
32159
+ backtest: isBacktest,
32160
+ });
32161
+ return await bt.strategyCoreService.trailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
32162
+ }
32163
+ /**
32164
+ * Adjusts the trailing take-profit to an absolute price level.
32165
+ *
32166
+ * Convenience wrapper around commitTrailingTake that converts an absolute
32167
+ * take-profit price to a percentShift relative to the ORIGINAL TP distance.
32168
+ *
32169
+ * Automatically detects backtest/live mode from execution context.
32170
+ * Automatically fetches current price via getAveragePrice.
32171
+ *
32172
+ * @param symbol - Trading pair symbol
32173
+ * @param newTakeProfitPrice - Desired absolute take-profit price
32174
+ * @returns Promise<boolean> - true if trailing TP was set/updated, false if rejected
32175
+ */
32176
+ async function commitTrailingTakeCost(symbol, newTakeProfitPrice) {
32177
+ bt.loggerService.info(TRAILING_PROFIT_COST_METHOD_NAME, {
32178
+ symbol,
32179
+ newTakeProfitPrice,
32180
+ });
32181
+ if (!ExecutionContextService.hasContext()) {
32182
+ throw new Error("commitTrailingTakeCost requires an execution context");
32183
+ }
32184
+ if (!MethodContextService.hasContext()) {
32185
+ throw new Error("commitTrailingTakeCost requires a method context");
32186
+ }
32187
+ const currentPrice = await getAveragePrice(symbol);
32188
+ const { backtest: isBacktest } = bt.executionContextService.context;
32189
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32190
+ const signal = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32191
+ if (!signal) {
32192
+ return false;
32193
+ }
32194
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
32195
+ if (effectivePriceOpen === null) {
32196
+ return false;
32197
+ }
32198
+ const percentShift = tpPriceToPercentShift(newTakeProfitPrice, signal.priceTakeProfit, effectivePriceOpen);
32199
+ if (await functoolsKit.not(bt.strategyCoreService.validateTrailingTake(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName }))) {
32200
+ return false;
32201
+ }
32202
+ await Broker.commitTrailingTake({
32203
+ symbol,
32204
+ percentShift,
32205
+ currentPrice,
32206
+ newTakeProfitPrice,
32207
+ position: signal.position,
32208
+ context: { exchangeName, frameName, strategyName },
32209
+ backtest: isBacktest,
32210
+ });
30458
32211
  return await bt.strategyCoreService.trailingTake(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
30459
32212
  }
30460
32213
  /**
@@ -30495,6 +32248,26 @@ async function commitBreakeven(symbol) {
30495
32248
  const currentPrice = await getAveragePrice(symbol);
30496
32249
  const { backtest: isBacktest } = bt.executionContextService.context;
30497
32250
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32251
+ const signal = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32252
+ if (!signal) {
32253
+ return false;
32254
+ }
32255
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
32256
+ if (effectivePriceOpen === null) {
32257
+ return false;
32258
+ }
32259
+ if (await functoolsKit.not(bt.strategyCoreService.validateBreakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName }))) {
32260
+ return false;
32261
+ }
32262
+ await Broker.commitBreakeven({
32263
+ symbol,
32264
+ currentPrice,
32265
+ newStopLossPrice: breakevenNewStopLossPrice(effectivePriceOpen),
32266
+ newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
32267
+ position: signal.position,
32268
+ context: { exchangeName, frameName, strategyName },
32269
+ backtest: isBacktest,
32270
+ });
30498
32271
  return await bt.strategyCoreService.breakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
30499
32272
  }
30500
32273
  /**
@@ -30569,6 +32342,23 @@ async function commitAverageBuy(symbol, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_C
30569
32342
  const currentPrice = await getAveragePrice(symbol);
30570
32343
  const { backtest: isBacktest } = bt.executionContextService.context;
30571
32344
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32345
+ if (await functoolsKit.not(bt.strategyCoreService.validateAverageBuy(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName }))) {
32346
+ return false;
32347
+ }
32348
+ const signalForAvgBuy = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32349
+ if (!signalForAvgBuy) {
32350
+ return false;
32351
+ }
32352
+ await Broker.commitAverageBuy({
32353
+ symbol,
32354
+ currentPrice,
32355
+ cost,
32356
+ position: signalForAvgBuy.position,
32357
+ priceTakeProfit: signalForAvgBuy.priceTakeProfit,
32358
+ priceStopLoss: signalForAvgBuy.priceStopLoss,
32359
+ context: { exchangeName, frameName, strategyName },
32360
+ backtest: isBacktest,
32361
+ });
30572
32362
  return await bt.strategyCoreService.averageBuy(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName }, cost);
30573
32363
  }
30574
32364
  /**
@@ -30741,7 +32531,9 @@ async function getBreakeven(symbol, currentPrice) {
30741
32531
  return await bt.strategyCoreService.getBreakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
30742
32532
  }
30743
32533
  async function getPositionAveragePrice(symbol) {
30744
- bt.loggerService.info(GET_POSITION_AVERAGE_PRICE_METHOD_NAME, { symbol });
32534
+ bt.loggerService.info(GET_POSITION_AVERAGE_PRICE_METHOD_NAME, {
32535
+ symbol,
32536
+ });
30745
32537
  if (!ExecutionContextService.hasContext()) {
30746
32538
  throw new Error("getPositionAveragePrice requires an execution context");
30747
32539
  }
@@ -30753,7 +32545,9 @@ async function getPositionAveragePrice(symbol) {
30753
32545
  return await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
30754
32546
  }
30755
32547
  async function getPositionInvestedCount(symbol) {
30756
- bt.loggerService.info(GET_POSITION_INVESTED_COUNT_METHOD_NAME, { symbol });
32548
+ bt.loggerService.info(GET_POSITION_INVESTED_COUNT_METHOD_NAME, {
32549
+ symbol,
32550
+ });
30757
32551
  if (!ExecutionContextService.hasContext()) {
30758
32552
  throw new Error("getPositionInvestedCount requires an execution context");
30759
32553
  }
@@ -30765,7 +32559,9 @@ async function getPositionInvestedCount(symbol) {
30765
32559
  return await bt.strategyCoreService.getPositionInvestedCount(isBacktest, symbol, { exchangeName, frameName, strategyName });
30766
32560
  }
30767
32561
  async function getPositionInvestedCost(symbol) {
30768
- bt.loggerService.info(GET_POSITION_INVESTED_COST_METHOD_NAME, { symbol });
32562
+ bt.loggerService.info(GET_POSITION_INVESTED_COST_METHOD_NAME, {
32563
+ symbol,
32564
+ });
30769
32565
  if (!ExecutionContextService.hasContext()) {
30770
32566
  throw new Error("getPositionInvestedCost requires an execution context");
30771
32567
  }
@@ -30819,7 +32615,10 @@ async function getPositionPnlPercent(symbol) {
30819
32615
  * ```
30820
32616
  */
30821
32617
  async function commitPartialProfitCost(symbol, dollarAmount) {
30822
- bt.loggerService.info(PARTIAL_PROFIT_COST_METHOD_NAME, { symbol, dollarAmount });
32618
+ bt.loggerService.info(PARTIAL_PROFIT_COST_METHOD_NAME, {
32619
+ symbol,
32620
+ dollarAmount,
32621
+ });
30823
32622
  if (!ExecutionContextService.hasContext()) {
30824
32623
  throw new Error("commitPartialProfitCost requires an execution context");
30825
32624
  }
@@ -30830,9 +32629,28 @@ async function commitPartialProfitCost(symbol, dollarAmount) {
30830
32629
  const { backtest: isBacktest } = bt.executionContextService.context;
30831
32630
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
30832
32631
  const investedCost = await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
30833
- if (investedCost === null)
32632
+ if (investedCost === null) {
32633
+ return false;
32634
+ }
32635
+ const signalForProfitCost = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32636
+ if (!signalForProfitCost) {
30834
32637
  return false;
32638
+ }
30835
32639
  const percentToClose = investedCostToPercent(dollarAmount, investedCost);
32640
+ if (await functoolsKit.not(bt.strategyCoreService.validatePartialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName }))) {
32641
+ return false;
32642
+ }
32643
+ await Broker.commitPartialProfit({
32644
+ symbol,
32645
+ percentToClose,
32646
+ cost: dollarAmount,
32647
+ currentPrice,
32648
+ position: signalForProfitCost.position,
32649
+ priceTakeProfit: signalForProfitCost.priceTakeProfit,
32650
+ priceStopLoss: signalForProfitCost.priceStopLoss,
32651
+ context: { exchangeName, frameName, strategyName },
32652
+ backtest: isBacktest,
32653
+ });
30836
32654
  return await bt.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
30837
32655
  }
30838
32656
  /**
@@ -30865,7 +32683,10 @@ async function commitPartialProfitCost(symbol, dollarAmount) {
30865
32683
  * ```
30866
32684
  */
30867
32685
  async function commitPartialLossCost(symbol, dollarAmount) {
30868
- bt.loggerService.info(PARTIAL_LOSS_COST_METHOD_NAME, { symbol, dollarAmount });
32686
+ bt.loggerService.info(PARTIAL_LOSS_COST_METHOD_NAME, {
32687
+ symbol,
32688
+ dollarAmount,
32689
+ });
30869
32690
  if (!ExecutionContextService.hasContext()) {
30870
32691
  throw new Error("commitPartialLossCost requires an execution context");
30871
32692
  }
@@ -30876,9 +32697,28 @@ async function commitPartialLossCost(symbol, dollarAmount) {
30876
32697
  const { backtest: isBacktest } = bt.executionContextService.context;
30877
32698
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
30878
32699
  const investedCost = await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
30879
- if (investedCost === null)
32700
+ if (investedCost === null) {
30880
32701
  return false;
32702
+ }
32703
+ const signalForLossCost = await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32704
+ if (!signalForLossCost) {
32705
+ return false;
32706
+ }
30881
32707
  const percentToClose = investedCostToPercent(dollarAmount, investedCost);
32708
+ if (await functoolsKit.not(bt.strategyCoreService.validatePartialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName }))) {
32709
+ return false;
32710
+ }
32711
+ await Broker.commitPartialLoss({
32712
+ symbol,
32713
+ percentToClose,
32714
+ cost: dollarAmount,
32715
+ currentPrice,
32716
+ position: signalForLossCost.position,
32717
+ priceTakeProfit: signalForLossCost.priceTakeProfit,
32718
+ priceStopLoss: signalForLossCost.priceStopLoss,
32719
+ context: { exchangeName, frameName, strategyName },
32720
+ backtest: isBacktest,
32721
+ });
30882
32722
  return await bt.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
30883
32723
  }
30884
32724
  async function getPositionPnlCost(symbol) {
@@ -30927,6 +32767,34 @@ async function getPositionLevels(symbol) {
30927
32767
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
30928
32768
  return await bt.strategyCoreService.getPositionLevels(isBacktest, symbol, { exchangeName, frameName, strategyName });
30929
32769
  }
32770
+ /**
32771
+ * Returns the list of partial close events for the current pending signal.
32772
+ *
32773
+ * Each element represents a partial profit or loss close executed via
32774
+ * commitPartialProfit / commitPartialLoss (or their Cost variants).
32775
+ *
32776
+ * Returns null if no pending signal exists.
32777
+ * Returns an empty array if no partials were executed yet.
32778
+ *
32779
+ * Each entry contains:
32780
+ * - `type` — "profit" or "loss"
32781
+ * - `percent` — percentage of position closed at this partial
32782
+ * - `currentPrice` — execution price of the partial close
32783
+ * - `costBasisAtClose` — accounting cost basis at the moment of this partial
32784
+ * - `entryCountAtClose` — number of DCA entries accumulated at this partial
32785
+ *
32786
+ * @param symbol - Trading pair symbol
32787
+ * @returns Promise resolving to array of partial close records or null
32788
+ *
32789
+ * @example
32790
+ * ```typescript
32791
+ * import { getPositionPartials } from "backtest-kit";
32792
+ *
32793
+ * const partials = await getPositionPartials("BTCUSDT");
32794
+ * // No partials yet: []
32795
+ * // After one partial profit: [{ type: "profit", percent: 50, currentPrice: 45000, ... }]
32796
+ * ```
32797
+ */
30930
32798
  async function getPositionPartials(symbol) {
30931
32799
  bt.loggerService.info(GET_POSITION_PARTIALS_METHOD_NAME, { symbol });
30932
32800
  if (!ExecutionContextService.hasContext()) {
@@ -32173,6 +34041,8 @@ const BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST = "BacktestUtils.commitPartialPro
32173
34041
  const BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST = "BacktestUtils.commitPartialLossCost";
32174
34042
  const BACKTEST_METHOD_NAME_TRAILING_STOP = "BacktestUtils.commitTrailingStop";
32175
34043
  const BACKTEST_METHOD_NAME_TRAILING_PROFIT = "BacktestUtils.commitTrailingTake";
34044
+ const BACKTEST_METHOD_NAME_TRAILING_STOP_COST = "BacktestUtils.commitTrailingStopCost";
34045
+ const BACKTEST_METHOD_NAME_TRAILING_PROFIT_COST = "BacktestUtils.commitTrailingTakeCost";
32176
34046
  const BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED = "Backtest.commitActivateScheduled";
32177
34047
  const BACKTEST_METHOD_NAME_AVERAGE_BUY = "Backtest.commitAverageBuy";
32178
34048
  const BACKTEST_METHOD_NAME_GET_DATA = "BacktestUtils.getData";
@@ -32352,12 +34222,13 @@ class BacktestInstance {
32352
34222
  }
32353
34223
  {
32354
34224
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
32355
- riskName && bt.riskGlobalService.clear({
32356
- riskName,
32357
- exchangeName: context.exchangeName,
32358
- frameName: context.frameName,
32359
- backtest: true,
32360
- });
34225
+ riskName &&
34226
+ bt.riskGlobalService.clear({
34227
+ riskName,
34228
+ exchangeName: context.exchangeName,
34229
+ frameName: context.frameName,
34230
+ backtest: true,
34231
+ });
32361
34232
  riskList &&
32362
34233
  riskList.forEach((riskName) => bt.riskGlobalService.clear({
32363
34234
  riskName,
@@ -32707,6 +34578,16 @@ class BacktestUtils {
32707
34578
  }
32708
34579
  return await bt.strategyCoreService.getBreakeven(true, symbol, currentPrice, context);
32709
34580
  };
34581
+ /**
34582
+ * Returns the effective (weighted average) entry price for the current pending signal.
34583
+ *
34584
+ * Accounts for all DCA entries via commitAverageBuy.
34585
+ * Returns null if no pending signal exists.
34586
+ *
34587
+ * @param symbol - Trading pair symbol
34588
+ * @param context - Execution context with strategyName, exchangeName, and frameName
34589
+ * @returns Effective entry price, or null if no active position
34590
+ */
32710
34591
  this.getPositionAveragePrice = async (symbol, context) => {
32711
34592
  bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE, {
32712
34593
  symbol,
@@ -32725,6 +34606,15 @@ class BacktestUtils {
32725
34606
  }
32726
34607
  return await bt.strategyCoreService.getPositionAveragePrice(true, symbol, context);
32727
34608
  };
34609
+ /**
34610
+ * Returns the total number of base-asset units currently held in the position.
34611
+ *
34612
+ * Includes units from all DCA entries. Returns null if no pending signal exists.
34613
+ *
34614
+ * @param symbol - Trading pair symbol
34615
+ * @param context - Execution context with strategyName, exchangeName, and frameName
34616
+ * @returns Total units held, or null if no active position
34617
+ */
32728
34618
  this.getPositionInvestedCount = async (symbol, context) => {
32729
34619
  bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT, {
32730
34620
  symbol,
@@ -32743,6 +34633,15 @@ class BacktestUtils {
32743
34633
  }
32744
34634
  return await bt.strategyCoreService.getPositionInvestedCount(true, symbol, context);
32745
34635
  };
34636
+ /**
34637
+ * Returns the total dollar cost invested in the current position.
34638
+ *
34639
+ * Sum of all entry costs across DCA entries. Returns null if no pending signal exists.
34640
+ *
34641
+ * @param symbol - Trading pair symbol
34642
+ * @param context - Execution context with strategyName, exchangeName, and frameName
34643
+ * @returns Total invested cost in quote currency, or null if no active position
34644
+ */
32746
34645
  this.getPositionInvestedCost = async (symbol, context) => {
32747
34646
  bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST, {
32748
34647
  symbol,
@@ -32761,6 +34660,17 @@ class BacktestUtils {
32761
34660
  }
32762
34661
  return await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
32763
34662
  };
34663
+ /**
34664
+ * Returns the current unrealized PnL as a percentage of the invested cost.
34665
+ *
34666
+ * Calculated relative to the effective (weighted average) entry price.
34667
+ * Positive for profit, negative for loss. Returns null if no pending signal exists.
34668
+ *
34669
+ * @param symbol - Trading pair symbol
34670
+ * @param currentPrice - Current market price
34671
+ * @param context - Execution context with strategyName, exchangeName, and frameName
34672
+ * @returns PnL percentage, or null if no active position
34673
+ */
32764
34674
  this.getPositionPnlPercent = async (symbol, currentPrice, context) => {
32765
34675
  bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT, {
32766
34676
  symbol,
@@ -32780,6 +34690,17 @@ class BacktestUtils {
32780
34690
  }
32781
34691
  return await bt.strategyCoreService.getPositionPnlPercent(true, symbol, currentPrice, context);
32782
34692
  };
34693
+ /**
34694
+ * Returns the current unrealized PnL in quote currency (dollar amount).
34695
+ *
34696
+ * Calculated as (currentPrice - effectiveEntry) * units for LONG,
34697
+ * reversed for SHORT. Returns null if no pending signal exists.
34698
+ *
34699
+ * @param symbol - Trading pair symbol
34700
+ * @param currentPrice - Current market price
34701
+ * @param context - Execution context with strategyName, exchangeName, and frameName
34702
+ * @returns PnL in quote currency, or null if no active position
34703
+ */
32783
34704
  this.getPositionPnlCost = async (symbol, currentPrice, context) => {
32784
34705
  bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST, {
32785
34706
  symbol,
@@ -32799,6 +34720,18 @@ class BacktestUtils {
32799
34720
  }
32800
34721
  return await bt.strategyCoreService.getPositionPnlCost(true, symbol, currentPrice, context);
32801
34722
  };
34723
+ /**
34724
+ * Returns the list of DCA entry prices for the current pending signal.
34725
+ *
34726
+ * The first element is always the original priceOpen (initial entry).
34727
+ * Each subsequent element is a price added by commitAverageBuy().
34728
+ * Returns null if no pending signal exists.
34729
+ * Returns a single-element array [priceOpen] if no DCA entries were made.
34730
+ *
34731
+ * @param symbol - Trading pair symbol
34732
+ * @param context - Execution context with strategyName, exchangeName, and frameName
34733
+ * @returns Array of entry prices, or null if no active position
34734
+ */
32802
34735
  this.getPositionLevels = async (symbol, context) => {
32803
34736
  bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_LEVELS, {
32804
34737
  symbol,
@@ -32817,6 +34750,25 @@ class BacktestUtils {
32817
34750
  }
32818
34751
  return await bt.strategyCoreService.getPositionLevels(true, symbol, context);
32819
34752
  };
34753
+ /**
34754
+ * Returns the list of partial close events for the current pending signal.
34755
+ *
34756
+ * Each element represents a partial profit or loss close executed via
34757
+ * commitPartialProfit / commitPartialLoss (or their Cost variants).
34758
+ * Returns null if no pending signal exists.
34759
+ * Returns an empty array if no partials were executed yet.
34760
+ *
34761
+ * Each entry contains:
34762
+ * - `type` — "profit" or "loss"
34763
+ * - `percent` — percentage of position closed at this partial
34764
+ * - `currentPrice` — execution price of the partial close
34765
+ * - `costBasisAtClose` — accounting cost basis at the moment of this partial
34766
+ * - `entryCountAtClose` — number of DCA entries accumulated at this partial
34767
+ *
34768
+ * @param symbol - Trading pair symbol
34769
+ * @param context - Execution context with strategyName, exchangeName, and frameName
34770
+ * @returns Array of partial close records, or null if no active position
34771
+ */
32820
34772
  this.getPositionPartials = async (symbol, context) => {
32821
34773
  bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS, {
32822
34774
  symbol,
@@ -33005,6 +34957,28 @@ class BacktestUtils {
33005
34957
  actions &&
33006
34958
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT));
33007
34959
  }
34960
+ const investedCostForProfit = await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
34961
+ if (investedCostForProfit === null) {
34962
+ return false;
34963
+ }
34964
+ const signalForProfit = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
34965
+ if (!signalForProfit) {
34966
+ return false;
34967
+ }
34968
+ if (await functoolsKit.not(bt.strategyCoreService.validatePartialProfit(true, symbol, percentToClose, currentPrice, context))) {
34969
+ return false;
34970
+ }
34971
+ await Broker.commitPartialProfit({
34972
+ symbol,
34973
+ percentToClose,
34974
+ cost: percentToCloseCost(percentToClose, investedCostForProfit),
34975
+ currentPrice,
34976
+ position: signalForProfit.position,
34977
+ priceTakeProfit: signalForProfit.priceTakeProfit,
34978
+ priceStopLoss: signalForProfit.priceStopLoss,
34979
+ context,
34980
+ backtest: true,
34981
+ });
33008
34982
  return await bt.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
33009
34983
  };
33010
34984
  /**
@@ -33054,6 +35028,28 @@ class BacktestUtils {
33054
35028
  actions &&
33055
35029
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_PARTIAL_LOSS));
33056
35030
  }
35031
+ const investedCostForLoss = await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
35032
+ if (investedCostForLoss === null) {
35033
+ return false;
35034
+ }
35035
+ const signalForLoss = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35036
+ if (!signalForLoss) {
35037
+ return false;
35038
+ }
35039
+ if (await functoolsKit.not(bt.strategyCoreService.validatePartialLoss(true, symbol, percentToClose, currentPrice, context))) {
35040
+ return false;
35041
+ }
35042
+ await Broker.commitPartialLoss({
35043
+ symbol,
35044
+ percentToClose,
35045
+ cost: percentToCloseCost(percentToClose, investedCostForLoss),
35046
+ currentPrice,
35047
+ position: signalForLoss.position,
35048
+ priceTakeProfit: signalForLoss.priceTakeProfit,
35049
+ priceStopLoss: signalForLoss.priceStopLoss,
35050
+ context,
35051
+ backtest: true,
35052
+ });
33057
35053
  return await bt.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
33058
35054
  };
33059
35055
  /**
@@ -33105,9 +35101,28 @@ class BacktestUtils {
33105
35101
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST));
33106
35102
  }
33107
35103
  const investedCost = await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
33108
- if (investedCost === null)
35104
+ if (investedCost === null) {
35105
+ return false;
35106
+ }
35107
+ const signalForProfitCost = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35108
+ if (!signalForProfitCost) {
33109
35109
  return false;
35110
+ }
33110
35111
  const percentToClose = (dollarAmount / investedCost) * 100;
35112
+ if (await functoolsKit.not(bt.strategyCoreService.validatePartialProfit(true, symbol, percentToClose, currentPrice, context))) {
35113
+ return false;
35114
+ }
35115
+ await Broker.commitPartialProfit({
35116
+ symbol,
35117
+ percentToClose,
35118
+ cost: dollarAmount,
35119
+ currentPrice,
35120
+ position: signalForProfitCost.position,
35121
+ priceTakeProfit: signalForProfitCost.priceTakeProfit,
35122
+ priceStopLoss: signalForProfitCost.priceStopLoss,
35123
+ context,
35124
+ backtest: true,
35125
+ });
33111
35126
  return await bt.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
33112
35127
  };
33113
35128
  /**
@@ -33159,9 +35174,28 @@ class BacktestUtils {
33159
35174
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST));
33160
35175
  }
33161
35176
  const investedCost = await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
33162
- if (investedCost === null)
35177
+ if (investedCost === null) {
35178
+ return false;
35179
+ }
35180
+ const signalForLossCost = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35181
+ if (!signalForLossCost) {
33163
35182
  return false;
35183
+ }
33164
35184
  const percentToClose = (dollarAmount / investedCost) * 100;
35185
+ if (await functoolsKit.not(bt.strategyCoreService.validatePartialLoss(true, symbol, percentToClose, currentPrice, context))) {
35186
+ return false;
35187
+ }
35188
+ await Broker.commitPartialLoss({
35189
+ symbol,
35190
+ percentToClose,
35191
+ cost: dollarAmount,
35192
+ currentPrice,
35193
+ position: signalForLossCost.position,
35194
+ priceTakeProfit: signalForLossCost.priceTakeProfit,
35195
+ priceStopLoss: signalForLossCost.priceStopLoss,
35196
+ context,
35197
+ backtest: true,
35198
+ });
33165
35199
  return await bt.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
33166
35200
  };
33167
35201
  /**
@@ -33226,6 +35260,26 @@ class BacktestUtils {
33226
35260
  actions &&
33227
35261
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_TRAILING_STOP));
33228
35262
  }
35263
+ const signal = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35264
+ if (!signal) {
35265
+ return false;
35266
+ }
35267
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(true, symbol, context);
35268
+ if (effectivePriceOpen === null) {
35269
+ return false;
35270
+ }
35271
+ if (await functoolsKit.not(bt.strategyCoreService.validateTrailingStop(true, symbol, percentShift, currentPrice, context))) {
35272
+ return false;
35273
+ }
35274
+ await Broker.commitTrailingStop({
35275
+ symbol,
35276
+ percentShift,
35277
+ currentPrice,
35278
+ newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
35279
+ position: signal.position,
35280
+ context,
35281
+ backtest: true,
35282
+ });
33229
35283
  return await bt.strategyCoreService.trailingStop(true, symbol, percentShift, currentPrice, context);
33230
35284
  };
33231
35285
  /**
@@ -33290,6 +35344,132 @@ class BacktestUtils {
33290
35344
  actions &&
33291
35345
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_TRAILING_PROFIT));
33292
35346
  }
35347
+ const signal = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35348
+ if (!signal) {
35349
+ return false;
35350
+ }
35351
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(true, symbol, context);
35352
+ if (effectivePriceOpen === null) {
35353
+ return false;
35354
+ }
35355
+ if (await functoolsKit.not(bt.strategyCoreService.validateTrailingTake(true, symbol, percentShift, currentPrice, context))) {
35356
+ return false;
35357
+ }
35358
+ await Broker.commitTrailingTake({
35359
+ symbol,
35360
+ percentShift,
35361
+ currentPrice,
35362
+ newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
35363
+ position: signal.position,
35364
+ context,
35365
+ backtest: true,
35366
+ });
35367
+ return await bt.strategyCoreService.trailingTake(true, symbol, percentShift, currentPrice, context);
35368
+ };
35369
+ /**
35370
+ * Adjusts the trailing stop-loss to an absolute price level.
35371
+ *
35372
+ * Convenience wrapper around commitTrailingStop that converts an absolute
35373
+ * stop-loss price to a percentShift relative to the ORIGINAL SL distance.
35374
+ *
35375
+ * @param symbol - Trading pair symbol
35376
+ * @param newStopLossPrice - Desired absolute stop-loss price
35377
+ * @param currentPrice - Current market price to check for intrusion
35378
+ * @param context - Execution context with strategyName, exchangeName, and frameName
35379
+ * @returns Promise<boolean> - true if trailing SL was set/updated, false if rejected
35380
+ */
35381
+ this.commitTrailingStopCost = async (symbol, newStopLossPrice, currentPrice, context) => {
35382
+ bt.loggerService.info(BACKTEST_METHOD_NAME_TRAILING_STOP_COST, {
35383
+ symbol,
35384
+ newStopLossPrice,
35385
+ currentPrice,
35386
+ context,
35387
+ });
35388
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_TRAILING_STOP_COST);
35389
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_TRAILING_STOP_COST);
35390
+ {
35391
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
35392
+ riskName &&
35393
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_STOP_COST);
35394
+ riskList &&
35395
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_STOP_COST));
35396
+ actions &&
35397
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_TRAILING_STOP_COST));
35398
+ }
35399
+ const signal = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35400
+ if (!signal) {
35401
+ return false;
35402
+ }
35403
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(true, symbol, context);
35404
+ if (effectivePriceOpen === null) {
35405
+ return false;
35406
+ }
35407
+ const percentShift = slPriceToPercentShift(newStopLossPrice, signal.priceStopLoss, effectivePriceOpen);
35408
+ if (await functoolsKit.not(bt.strategyCoreService.validateTrailingStop(true, symbol, percentShift, currentPrice, context))) {
35409
+ return false;
35410
+ }
35411
+ await Broker.commitTrailingStop({
35412
+ symbol,
35413
+ percentShift,
35414
+ currentPrice,
35415
+ newStopLossPrice,
35416
+ position: signal.position,
35417
+ context,
35418
+ backtest: true,
35419
+ });
35420
+ return await bt.strategyCoreService.trailingStop(true, symbol, percentShift, currentPrice, context);
35421
+ };
35422
+ /**
35423
+ * Adjusts the trailing take-profit to an absolute price level.
35424
+ *
35425
+ * Convenience wrapper around commitTrailingTake that converts an absolute
35426
+ * take-profit price to a percentShift relative to the ORIGINAL TP distance.
35427
+ *
35428
+ * @param symbol - Trading pair symbol
35429
+ * @param newTakeProfitPrice - Desired absolute take-profit price
35430
+ * @param currentPrice - Current market price to check for intrusion
35431
+ * @param context - Execution context with strategyName, exchangeName, and frameName
35432
+ * @returns Promise<boolean> - true if trailing TP was set/updated, false if rejected
35433
+ */
35434
+ this.commitTrailingTakeCost = async (symbol, newTakeProfitPrice, currentPrice, context) => {
35435
+ bt.loggerService.info(BACKTEST_METHOD_NAME_TRAILING_PROFIT_COST, {
35436
+ symbol,
35437
+ newTakeProfitPrice,
35438
+ currentPrice,
35439
+ context,
35440
+ });
35441
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_TRAILING_PROFIT_COST);
35442
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_TRAILING_PROFIT_COST);
35443
+ {
35444
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
35445
+ riskName &&
35446
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_PROFIT_COST);
35447
+ riskList &&
35448
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_PROFIT_COST));
35449
+ actions &&
35450
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_TRAILING_PROFIT_COST));
35451
+ }
35452
+ const signal = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35453
+ if (!signal) {
35454
+ return false;
35455
+ }
35456
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(true, symbol, context);
35457
+ if (effectivePriceOpen === null) {
35458
+ return false;
35459
+ }
35460
+ const percentShift = tpPriceToPercentShift(newTakeProfitPrice, signal.priceTakeProfit, effectivePriceOpen);
35461
+ if (await functoolsKit.not(bt.strategyCoreService.validateTrailingTake(true, symbol, percentShift, currentPrice, context))) {
35462
+ return false;
35463
+ }
35464
+ await Broker.commitTrailingTake({
35465
+ symbol,
35466
+ percentShift,
35467
+ currentPrice,
35468
+ newTakeProfitPrice,
35469
+ position: signal.position,
35470
+ context,
35471
+ backtest: true,
35472
+ });
33293
35473
  return await bt.strategyCoreService.trailingTake(true, symbol, percentShift, currentPrice, context);
33294
35474
  };
33295
35475
  /**
@@ -33330,6 +35510,26 @@ class BacktestUtils {
33330
35510
  actions &&
33331
35511
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_BREAKEVEN));
33332
35512
  }
35513
+ const signal = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35514
+ if (!signal) {
35515
+ return false;
35516
+ }
35517
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(true, symbol, context);
35518
+ if (effectivePriceOpen === null) {
35519
+ return false;
35520
+ }
35521
+ if (await functoolsKit.not(bt.strategyCoreService.validateBreakeven(true, symbol, currentPrice, context))) {
35522
+ return false;
35523
+ }
35524
+ await Broker.commitBreakeven({
35525
+ symbol,
35526
+ currentPrice,
35527
+ newStopLossPrice: breakevenNewStopLossPrice(effectivePriceOpen),
35528
+ newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
35529
+ position: signal.position,
35530
+ context,
35531
+ backtest: true,
35532
+ });
33333
35533
  return await bt.strategyCoreService.breakeven(true, symbol, currentPrice, context);
33334
35534
  };
33335
35535
  /**
@@ -33413,6 +35613,23 @@ class BacktestUtils {
33413
35613
  actions &&
33414
35614
  actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_AVERAGE_BUY));
33415
35615
  }
35616
+ if (await functoolsKit.not(bt.strategyCoreService.validateAverageBuy(true, symbol, currentPrice, context))) {
35617
+ return false;
35618
+ }
35619
+ const signalForAvgBuy = await bt.strategyCoreService.getPendingSignal(true, symbol, currentPrice, context);
35620
+ if (!signalForAvgBuy) {
35621
+ return false;
35622
+ }
35623
+ await Broker.commitAverageBuy({
35624
+ symbol,
35625
+ currentPrice,
35626
+ cost,
35627
+ position: signalForAvgBuy.position,
35628
+ priceTakeProfit: signalForAvgBuy.priceTakeProfit,
35629
+ priceStopLoss: signalForAvgBuy.priceStopLoss,
35630
+ context,
35631
+ backtest: true,
35632
+ });
33416
35633
  return await bt.strategyCoreService.averageBuy(true, symbol, currentPrice, context, cost);
33417
35634
  };
33418
35635
  /**
@@ -33601,6 +35818,8 @@ const LIVE_METHOD_NAME_PARTIAL_PROFIT_COST = "LiveUtils.commitPartialProfitCost"
33601
35818
  const LIVE_METHOD_NAME_PARTIAL_LOSS_COST = "LiveUtils.commitPartialLossCost";
33602
35819
  const LIVE_METHOD_NAME_TRAILING_STOP = "LiveUtils.commitTrailingStop";
33603
35820
  const LIVE_METHOD_NAME_TRAILING_PROFIT = "LiveUtils.commitTrailingTake";
35821
+ const LIVE_METHOD_NAME_TRAILING_STOP_COST = "LiveUtils.commitTrailingStopCost";
35822
+ const LIVE_METHOD_NAME_TRAILING_PROFIT_COST = "LiveUtils.commitTrailingTakeCost";
33604
35823
  const LIVE_METHOD_NAME_ACTIVATE_SCHEDULED = "Live.commitActivateScheduled";
33605
35824
  const LIVE_METHOD_NAME_AVERAGE_BUY = "Live.commitAverageBuy";
33606
35825
  /**
@@ -33727,43 +35946,82 @@ class LiveInstance {
33727
35946
  context,
33728
35947
  });
33729
35948
  {
33730
- bt.backtestMarkdownService.clear({ symbol, strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "", backtest: false });
33731
- bt.liveMarkdownService.clear({ symbol, strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "", backtest: false });
33732
- bt.scheduleMarkdownService.clear({ symbol, strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "", backtest: false });
33733
- bt.performanceMarkdownService.clear({ symbol, strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "", backtest: false });
33734
- bt.partialMarkdownService.clear({ symbol, strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "", backtest: false });
33735
- bt.riskMarkdownService.clear({ symbol, strategyName: context.strategyName, exchangeName: context.exchangeName, frameName: "", backtest: false });
33736
- }
33737
- {
33738
- bt.strategyCoreService.clear({
35949
+ bt.backtestMarkdownService.clear({
33739
35950
  symbol,
33740
35951
  strategyName: context.strategyName,
33741
35952
  exchangeName: context.exchangeName,
33742
35953
  frameName: "",
33743
35954
  backtest: false,
33744
35955
  });
33745
- }
33746
- {
33747
- const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33748
- riskName && bt.riskGlobalService.clear({
33749
- riskName,
35956
+ bt.liveMarkdownService.clear({
35957
+ symbol,
35958
+ strategyName: context.strategyName,
33750
35959
  exchangeName: context.exchangeName,
33751
35960
  frameName: "",
33752
- backtest: false
35961
+ backtest: false,
33753
35962
  });
33754
- riskList && riskList.forEach((riskName) => bt.riskGlobalService.clear({
33755
- riskName,
35963
+ bt.scheduleMarkdownService.clear({
35964
+ symbol,
35965
+ strategyName: context.strategyName,
35966
+ exchangeName: context.exchangeName,
35967
+ frameName: "",
35968
+ backtest: false,
35969
+ });
35970
+ bt.performanceMarkdownService.clear({
35971
+ symbol,
35972
+ strategyName: context.strategyName,
33756
35973
  exchangeName: context.exchangeName,
33757
35974
  frameName: "",
33758
- backtest: false
33759
- }));
33760
- actions && actions.forEach((actionName) => bt.actionCoreService.clear({
33761
- actionName,
35975
+ backtest: false,
35976
+ });
35977
+ bt.partialMarkdownService.clear({
35978
+ symbol,
35979
+ strategyName: context.strategyName,
35980
+ exchangeName: context.exchangeName,
35981
+ frameName: "",
35982
+ backtest: false,
35983
+ });
35984
+ bt.riskMarkdownService.clear({
35985
+ symbol,
33762
35986
  strategyName: context.strategyName,
33763
35987
  exchangeName: context.exchangeName,
33764
35988
  frameName: "",
33765
- backtest: false
33766
- }));
35989
+ backtest: false,
35990
+ });
35991
+ }
35992
+ {
35993
+ bt.strategyCoreService.clear({
35994
+ symbol,
35995
+ strategyName: context.strategyName,
35996
+ exchangeName: context.exchangeName,
35997
+ frameName: "",
35998
+ backtest: false,
35999
+ });
36000
+ }
36001
+ {
36002
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
36003
+ riskName &&
36004
+ bt.riskGlobalService.clear({
36005
+ riskName,
36006
+ exchangeName: context.exchangeName,
36007
+ frameName: "",
36008
+ backtest: false,
36009
+ });
36010
+ riskList &&
36011
+ riskList.forEach((riskName) => bt.riskGlobalService.clear({
36012
+ riskName,
36013
+ exchangeName: context.exchangeName,
36014
+ frameName: "",
36015
+ backtest: false,
36016
+ }));
36017
+ actions &&
36018
+ actions.forEach((actionName) => bt.actionCoreService.clear({
36019
+ actionName,
36020
+ strategyName: context.strategyName,
36021
+ exchangeName: context.exchangeName,
36022
+ frameName: "",
36023
+ backtest: false,
36024
+ }));
33767
36025
  }
33768
36026
  return bt.liveCommandService.run(symbol, context);
33769
36027
  };
@@ -33806,7 +36064,7 @@ class LiveInstance {
33806
36064
  bt.strategyCoreService.stopStrategy(false, symbol, {
33807
36065
  strategyName: context.strategyName,
33808
36066
  exchangeName: context.exchangeName,
33809
- frameName: ""
36067
+ frameName: "",
33810
36068
  });
33811
36069
  bt.strategyCoreService
33812
36070
  .hasPendingSignal(false, symbol, {
@@ -33887,9 +36145,12 @@ class LiveUtils {
33887
36145
  }
33888
36146
  {
33889
36147
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33890
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_RUN);
33891
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_RUN));
33892
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_RUN));
36148
+ riskName &&
36149
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_RUN);
36150
+ riskList &&
36151
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_RUN));
36152
+ actions &&
36153
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_RUN));
33893
36154
  }
33894
36155
  const instance = this._getInstance(symbol, context.strategyName, context.exchangeName);
33895
36156
  return instance.run(symbol, context);
@@ -33920,9 +36181,12 @@ class LiveUtils {
33920
36181
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_BACKGROUND);
33921
36182
  {
33922
36183
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33923
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BACKGROUND);
33924
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BACKGROUND));
33925
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_BACKGROUND));
36184
+ riskName &&
36185
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BACKGROUND);
36186
+ riskList &&
36187
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BACKGROUND));
36188
+ actions &&
36189
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_BACKGROUND));
33926
36190
  }
33927
36191
  const instance = this._getInstance(symbol, context.strategyName, context.exchangeName);
33928
36192
  return instance.background(symbol, context);
@@ -33952,9 +36216,12 @@ class LiveUtils {
33952
36216
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL);
33953
36217
  {
33954
36218
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33955
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL);
33956
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL));
33957
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL));
36219
+ riskName &&
36220
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL);
36221
+ riskList &&
36222
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL));
36223
+ actions &&
36224
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL));
33958
36225
  }
33959
36226
  return await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
33960
36227
  strategyName: context.strategyName,
@@ -33986,9 +36253,12 @@ class LiveUtils {
33986
36253
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
33987
36254
  {
33988
36255
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33989
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
33990
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
33991
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
36256
+ riskName &&
36257
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
36258
+ riskList &&
36259
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
36260
+ actions &&
36261
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
33992
36262
  }
33993
36263
  return await bt.strategyCoreService.getTotalPercentClosed(false, symbol, {
33994
36264
  strategyName: context.strategyName,
@@ -34019,9 +36289,12 @@ class LiveUtils {
34019
36289
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
34020
36290
  {
34021
36291
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34022
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
34023
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
34024
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
36292
+ riskName &&
36293
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
36294
+ riskList &&
36295
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
36296
+ actions &&
36297
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
34025
36298
  }
34026
36299
  return await bt.strategyCoreService.getTotalCostClosed(false, symbol, {
34027
36300
  strategyName: context.strategyName,
@@ -34054,9 +36327,12 @@ class LiveUtils {
34054
36327
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL);
34055
36328
  {
34056
36329
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34057
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL);
34058
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL));
34059
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL));
36330
+ riskName &&
36331
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL);
36332
+ riskList &&
36333
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL));
36334
+ actions &&
36335
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL));
34060
36336
  }
34061
36337
  return await bt.strategyCoreService.getScheduledSignal(false, symbol, currentPrice, {
34062
36338
  strategyName: context.strategyName,
@@ -34097,9 +36373,12 @@ class LiveUtils {
34097
36373
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_BREAKEVEN);
34098
36374
  {
34099
36375
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34100
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_BREAKEVEN);
34101
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_BREAKEVEN));
34102
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_BREAKEVEN));
36376
+ riskName &&
36377
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_BREAKEVEN);
36378
+ riskList &&
36379
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_BREAKEVEN));
36380
+ actions &&
36381
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_BREAKEVEN));
34103
36382
  }
34104
36383
  return await bt.strategyCoreService.getBreakeven(false, symbol, currentPrice, {
34105
36384
  strategyName: context.strategyName,
@@ -34107,15 +36386,31 @@ class LiveUtils {
34107
36386
  frameName: "",
34108
36387
  });
34109
36388
  };
36389
+ /**
36390
+ * Returns the effective (weighted average) entry price for the current pending signal.
36391
+ *
36392
+ * Accounts for all DCA entries via commitAverageBuy.
36393
+ * Returns null if no pending signal exists.
36394
+ *
36395
+ * @param symbol - Trading pair symbol
36396
+ * @param context - Execution context with strategyName and exchangeName
36397
+ * @returns Effective entry price, or null if no active position
36398
+ */
34110
36399
  this.getPositionAveragePrice = async (symbol, context) => {
34111
- bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE, { symbol, context });
36400
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE, {
36401
+ symbol,
36402
+ context,
36403
+ });
34112
36404
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
34113
36405
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
34114
36406
  {
34115
36407
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34116
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
34117
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
34118
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
36408
+ riskName &&
36409
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
36410
+ riskList &&
36411
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
36412
+ actions &&
36413
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
34119
36414
  }
34120
36415
  return await bt.strategyCoreService.getPositionAveragePrice(false, symbol, {
34121
36416
  strategyName: context.strategyName,
@@ -34123,15 +36418,30 @@ class LiveUtils {
34123
36418
  frameName: "",
34124
36419
  });
34125
36420
  };
36421
+ /**
36422
+ * Returns the total number of base-asset units currently held in the position.
36423
+ *
36424
+ * Includes units from all DCA entries. Returns null if no pending signal exists.
36425
+ *
36426
+ * @param symbol - Trading pair symbol
36427
+ * @param context - Execution context with strategyName and exchangeName
36428
+ * @returns Total units held, or null if no active position
36429
+ */
34126
36430
  this.getPositionInvestedCount = async (symbol, context) => {
34127
- bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT, { symbol, context });
36431
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT, {
36432
+ symbol,
36433
+ context,
36434
+ });
34128
36435
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
34129
36436
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
34130
36437
  {
34131
36438
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34132
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
34133
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
34134
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
36439
+ riskName &&
36440
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
36441
+ riskList &&
36442
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
36443
+ actions &&
36444
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
34135
36445
  }
34136
36446
  return await bt.strategyCoreService.getPositionInvestedCount(false, symbol, {
34137
36447
  strategyName: context.strategyName,
@@ -34139,15 +36449,30 @@ class LiveUtils {
34139
36449
  frameName: "",
34140
36450
  });
34141
36451
  };
36452
+ /**
36453
+ * Returns the total dollar cost invested in the current position.
36454
+ *
36455
+ * Sum of all entry costs across DCA entries. Returns null if no pending signal exists.
36456
+ *
36457
+ * @param symbol - Trading pair symbol
36458
+ * @param context - Execution context with strategyName and exchangeName
36459
+ * @returns Total invested cost in quote currency, or null if no active position
36460
+ */
34142
36461
  this.getPositionInvestedCost = async (symbol, context) => {
34143
- bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST, { symbol, context });
36462
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST, {
36463
+ symbol,
36464
+ context,
36465
+ });
34144
36466
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
34145
36467
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
34146
36468
  {
34147
36469
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34148
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
34149
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST));
34150
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST));
36470
+ riskName &&
36471
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
36472
+ riskList &&
36473
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST));
36474
+ actions &&
36475
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST));
34151
36476
  }
34152
36477
  return await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
34153
36478
  strategyName: context.strategyName,
@@ -34155,15 +36480,33 @@ class LiveUtils {
34155
36480
  frameName: "",
34156
36481
  });
34157
36482
  };
36483
+ /**
36484
+ * Returns the current unrealized PnL as a percentage of the invested cost.
36485
+ *
36486
+ * Calculated relative to the effective (weighted average) entry price.
36487
+ * Positive for profit, negative for loss. Returns null if no pending signal exists.
36488
+ *
36489
+ * @param symbol - Trading pair symbol
36490
+ * @param currentPrice - Current market price
36491
+ * @param context - Execution context with strategyName and exchangeName
36492
+ * @returns PnL percentage, or null if no active position
36493
+ */
34158
36494
  this.getPositionPnlPercent = async (symbol, currentPrice, context) => {
34159
- bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT, { symbol, currentPrice, context });
36495
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT, {
36496
+ symbol,
36497
+ currentPrice,
36498
+ context,
36499
+ });
34160
36500
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
34161
36501
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
34162
36502
  {
34163
36503
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34164
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
34165
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT));
34166
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT));
36504
+ riskName &&
36505
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
36506
+ riskList &&
36507
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT));
36508
+ actions &&
36509
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT));
34167
36510
  }
34168
36511
  return await bt.strategyCoreService.getPositionPnlPercent(false, symbol, currentPrice, {
34169
36512
  strategyName: context.strategyName,
@@ -34171,15 +36514,33 @@ class LiveUtils {
34171
36514
  frameName: "",
34172
36515
  });
34173
36516
  };
36517
+ /**
36518
+ * Returns the current unrealized PnL in quote currency (dollar amount).
36519
+ *
36520
+ * Calculated as (currentPrice - effectiveEntry) * units for LONG,
36521
+ * reversed for SHORT. Returns null if no pending signal exists.
36522
+ *
36523
+ * @param symbol - Trading pair symbol
36524
+ * @param currentPrice - Current market price
36525
+ * @param context - Execution context with strategyName and exchangeName
36526
+ * @returns PnL in quote currency, or null if no active position
36527
+ */
34174
36528
  this.getPositionPnlCost = async (symbol, currentPrice, context) => {
34175
- bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PNL_COST, { symbol, currentPrice, context });
36529
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PNL_COST, {
36530
+ symbol,
36531
+ currentPrice,
36532
+ context,
36533
+ });
34176
36534
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
34177
36535
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
34178
36536
  {
34179
36537
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34180
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
34181
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST));
34182
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST));
36538
+ riskName &&
36539
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
36540
+ riskList &&
36541
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST));
36542
+ actions &&
36543
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST));
34183
36544
  }
34184
36545
  return await bt.strategyCoreService.getPositionPnlCost(false, symbol, currentPrice, {
34185
36546
  strategyName: context.strategyName,
@@ -34187,15 +36548,33 @@ class LiveUtils {
34187
36548
  frameName: "",
34188
36549
  });
34189
36550
  };
36551
+ /**
36552
+ * Returns the list of DCA entry prices for the current pending signal.
36553
+ *
36554
+ * The first element is always the original priceOpen (initial entry).
36555
+ * Each subsequent element is a price added by commitAverageBuy().
36556
+ * Returns null if no pending signal exists.
36557
+ * Returns a single-element array [priceOpen] if no DCA entries were made.
36558
+ *
36559
+ * @param symbol - Trading pair symbol
36560
+ * @param context - Execution context with strategyName and exchangeName
36561
+ * @returns Array of entry prices, or null if no active position
36562
+ */
34190
36563
  this.getPositionLevels = async (symbol, context) => {
34191
- bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_LEVELS, { symbol, context });
36564
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_LEVELS, {
36565
+ symbol,
36566
+ context,
36567
+ });
34192
36568
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
34193
36569
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
34194
36570
  {
34195
36571
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34196
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
34197
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_LEVELS));
34198
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_LEVELS));
36572
+ riskName &&
36573
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
36574
+ riskList &&
36575
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_LEVELS));
36576
+ actions &&
36577
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_LEVELS));
34199
36578
  }
34200
36579
  return await bt.strategyCoreService.getPositionLevels(false, symbol, {
34201
36580
  strategyName: context.strategyName,
@@ -34203,15 +36582,40 @@ class LiveUtils {
34203
36582
  frameName: "",
34204
36583
  });
34205
36584
  };
36585
+ /**
36586
+ * Returns the list of partial close events for the current pending signal.
36587
+ *
36588
+ * Each element represents a partial profit or loss close executed via
36589
+ * commitPartialProfit / commitPartialLoss (or their Cost variants).
36590
+ * Returns null if no pending signal exists.
36591
+ * Returns an empty array if no partials were executed yet.
36592
+ *
36593
+ * Each entry contains:
36594
+ * - `type` — "profit" or "loss"
36595
+ * - `percent` — percentage of position closed at this partial
36596
+ * - `currentPrice` — execution price of the partial close
36597
+ * - `costBasisAtClose` — accounting cost basis at the moment of this partial
36598
+ * - `entryCountAtClose` — number of DCA entries accumulated at this partial
36599
+ *
36600
+ * @param symbol - Trading pair symbol
36601
+ * @param context - Execution context with strategyName and exchangeName
36602
+ * @returns Array of partial close records, or null if no active position
36603
+ */
34206
36604
  this.getPositionPartials = async (symbol, context) => {
34207
- bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PARTIALS, { symbol, context });
36605
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PARTIALS, {
36606
+ symbol,
36607
+ context,
36608
+ });
34208
36609
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
34209
36610
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
34210
36611
  {
34211
36612
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34212
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
34213
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS));
34214
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS));
36613
+ riskName &&
36614
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
36615
+ riskList &&
36616
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS));
36617
+ actions &&
36618
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS));
34215
36619
  }
34216
36620
  return await bt.strategyCoreService.getPositionPartials(false, symbol, {
34217
36621
  strategyName: context.strategyName,
@@ -34245,9 +36649,12 @@ class LiveUtils {
34245
36649
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_STOP);
34246
36650
  {
34247
36651
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34248
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_STOP);
34249
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_STOP));
34250
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_STOP));
36652
+ riskName &&
36653
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_STOP);
36654
+ riskList &&
36655
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_STOP));
36656
+ actions &&
36657
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_STOP));
34251
36658
  }
34252
36659
  await bt.strategyCoreService.stopStrategy(false, symbol, {
34253
36660
  strategyName: context.strategyName,
@@ -34288,9 +36695,12 @@ class LiveUtils {
34288
36695
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_CANCEL_SCHEDULED);
34289
36696
  {
34290
36697
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34291
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CANCEL_SCHEDULED);
34292
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CANCEL_SCHEDULED));
34293
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_CANCEL_SCHEDULED));
36698
+ riskName &&
36699
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CANCEL_SCHEDULED);
36700
+ riskList &&
36701
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CANCEL_SCHEDULED));
36702
+ actions &&
36703
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_CANCEL_SCHEDULED));
34294
36704
  }
34295
36705
  await bt.strategyCoreService.cancelScheduled(false, symbol, {
34296
36706
  strategyName: context.strategyName,
@@ -34329,9 +36739,12 @@ class LiveUtils {
34329
36739
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_CLOSE_PENDING);
34330
36740
  {
34331
36741
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34332
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CLOSE_PENDING);
34333
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CLOSE_PENDING));
34334
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_CLOSE_PENDING));
36742
+ riskName &&
36743
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CLOSE_PENDING);
36744
+ riskList &&
36745
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CLOSE_PENDING));
36746
+ actions &&
36747
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_CLOSE_PENDING));
34335
36748
  }
34336
36749
  await bt.strategyCoreService.closePending(false, symbol, {
34337
36750
  strategyName: context.strategyName,
@@ -34378,10 +36791,51 @@ class LiveUtils {
34378
36791
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
34379
36792
  {
34380
36793
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34381
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
34382
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT));
34383
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_PROFIT));
36794
+ riskName &&
36795
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
36796
+ riskList &&
36797
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT));
36798
+ actions &&
36799
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_PROFIT));
36800
+ }
36801
+ const investedCost = await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
36802
+ strategyName: context.strategyName,
36803
+ exchangeName: context.exchangeName,
36804
+ frameName: "",
36805
+ });
36806
+ if (investedCost === null) {
36807
+ return false;
36808
+ }
36809
+ const signalForProfit = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
36810
+ strategyName: context.strategyName,
36811
+ exchangeName: context.exchangeName,
36812
+ frameName: "",
36813
+ });
36814
+ if (!signalForProfit) {
36815
+ return false;
36816
+ }
36817
+ if (await functoolsKit.not(bt.strategyCoreService.validatePartialProfit(false, symbol, percentToClose, currentPrice, {
36818
+ strategyName: context.strategyName,
36819
+ exchangeName: context.exchangeName,
36820
+ frameName: "",
36821
+ }))) {
36822
+ return false;
34384
36823
  }
36824
+ await Broker.commitPartialProfit({
36825
+ symbol,
36826
+ percentToClose,
36827
+ cost: percentToCloseCost(percentToClose, investedCost),
36828
+ currentPrice,
36829
+ position: signalForProfit.position,
36830
+ priceTakeProfit: signalForProfit.priceTakeProfit,
36831
+ priceStopLoss: signalForProfit.priceStopLoss,
36832
+ context: {
36833
+ strategyName: context.strategyName,
36834
+ exchangeName: context.exchangeName,
36835
+ frameName: "",
36836
+ },
36837
+ backtest: false,
36838
+ });
34385
36839
  return await bt.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
34386
36840
  strategyName: context.strategyName,
34387
36841
  exchangeName: context.exchangeName,
@@ -34427,10 +36881,51 @@ class LiveUtils {
34427
36881
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_LOSS);
34428
36882
  {
34429
36883
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34430
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS);
34431
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS));
34432
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_LOSS));
36884
+ riskName &&
36885
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS);
36886
+ riskList &&
36887
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS));
36888
+ actions &&
36889
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_LOSS));
36890
+ }
36891
+ const investedCost = await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
36892
+ strategyName: context.strategyName,
36893
+ exchangeName: context.exchangeName,
36894
+ frameName: "",
36895
+ });
36896
+ if (investedCost === null) {
36897
+ return false;
36898
+ }
36899
+ const signalForLoss = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
36900
+ strategyName: context.strategyName,
36901
+ exchangeName: context.exchangeName,
36902
+ frameName: "",
36903
+ });
36904
+ if (!signalForLoss) {
36905
+ return false;
34433
36906
  }
36907
+ if (await functoolsKit.not(bt.strategyCoreService.validatePartialLoss(false, symbol, percentToClose, currentPrice, {
36908
+ strategyName: context.strategyName,
36909
+ exchangeName: context.exchangeName,
36910
+ frameName: "",
36911
+ }))) {
36912
+ return false;
36913
+ }
36914
+ await Broker.commitPartialLoss({
36915
+ symbol,
36916
+ percentToClose,
36917
+ cost: percentToCloseCost(percentToClose, investedCost),
36918
+ currentPrice,
36919
+ position: signalForLoss.position,
36920
+ priceTakeProfit: signalForLoss.priceTakeProfit,
36921
+ priceStopLoss: signalForLoss.priceStopLoss,
36922
+ context: {
36923
+ strategyName: context.strategyName,
36924
+ exchangeName: context.exchangeName,
36925
+ frameName: "",
36926
+ },
36927
+ backtest: false,
36928
+ });
34434
36929
  return await bt.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
34435
36930
  strategyName: context.strategyName,
34436
36931
  exchangeName: context.exchangeName,
@@ -34467,23 +36962,62 @@ class LiveUtils {
34467
36962
  * ```
34468
36963
  */
34469
36964
  this.commitPartialProfitCost = async (symbol, dollarAmount, currentPrice, context) => {
34470
- bt.loggerService.info(LIVE_METHOD_NAME_PARTIAL_PROFIT_COST, { symbol, dollarAmount, currentPrice, context });
36965
+ bt.loggerService.info(LIVE_METHOD_NAME_PARTIAL_PROFIT_COST, {
36966
+ symbol,
36967
+ dollarAmount,
36968
+ currentPrice,
36969
+ context,
36970
+ });
34471
36971
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
34472
36972
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
34473
36973
  {
34474
36974
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34475
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
34476
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST));
34477
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST));
36975
+ riskName &&
36976
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
36977
+ riskList &&
36978
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST));
36979
+ actions &&
36980
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST));
34478
36981
  }
34479
36982
  const investedCost = await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
34480
36983
  strategyName: context.strategyName,
34481
36984
  exchangeName: context.exchangeName,
34482
36985
  frameName: "",
34483
36986
  });
34484
- if (investedCost === null)
36987
+ if (investedCost === null) {
36988
+ return false;
36989
+ }
36990
+ const signalForProfitCost = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
36991
+ strategyName: context.strategyName,
36992
+ exchangeName: context.exchangeName,
36993
+ frameName: "",
36994
+ });
36995
+ if (!signalForProfitCost) {
34485
36996
  return false;
36997
+ }
34486
36998
  const percentToClose = (dollarAmount / investedCost) * 100;
36999
+ if (await functoolsKit.not(bt.strategyCoreService.validatePartialProfit(false, symbol, percentToClose, currentPrice, {
37000
+ strategyName: context.strategyName,
37001
+ exchangeName: context.exchangeName,
37002
+ frameName: "",
37003
+ }))) {
37004
+ return false;
37005
+ }
37006
+ await Broker.commitPartialProfit({
37007
+ symbol,
37008
+ percentToClose,
37009
+ cost: dollarAmount,
37010
+ currentPrice,
37011
+ position: signalForProfitCost.position,
37012
+ priceTakeProfit: signalForProfitCost.priceTakeProfit,
37013
+ priceStopLoss: signalForProfitCost.priceStopLoss,
37014
+ context: {
37015
+ strategyName: context.strategyName,
37016
+ exchangeName: context.exchangeName,
37017
+ frameName: "",
37018
+ },
37019
+ backtest: false,
37020
+ });
34487
37021
  return await bt.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
34488
37022
  strategyName: context.strategyName,
34489
37023
  exchangeName: context.exchangeName,
@@ -34520,23 +37054,62 @@ class LiveUtils {
34520
37054
  * ```
34521
37055
  */
34522
37056
  this.commitPartialLossCost = async (symbol, dollarAmount, currentPrice, context) => {
34523
- bt.loggerService.info(LIVE_METHOD_NAME_PARTIAL_LOSS_COST, { symbol, dollarAmount, currentPrice, context });
37057
+ bt.loggerService.info(LIVE_METHOD_NAME_PARTIAL_LOSS_COST, {
37058
+ symbol,
37059
+ dollarAmount,
37060
+ currentPrice,
37061
+ context,
37062
+ });
34524
37063
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
34525
37064
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
34526
37065
  {
34527
37066
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34528
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
34529
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST));
34530
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST));
37067
+ riskName &&
37068
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
37069
+ riskList &&
37070
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST));
37071
+ actions &&
37072
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST));
34531
37073
  }
34532
37074
  const investedCost = await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
34533
37075
  strategyName: context.strategyName,
34534
37076
  exchangeName: context.exchangeName,
34535
37077
  frameName: "",
34536
37078
  });
34537
- if (investedCost === null)
37079
+ if (investedCost === null) {
37080
+ return false;
37081
+ }
37082
+ const signalForLossCost = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
37083
+ strategyName: context.strategyName,
37084
+ exchangeName: context.exchangeName,
37085
+ frameName: "",
37086
+ });
37087
+ if (!signalForLossCost) {
34538
37088
  return false;
37089
+ }
34539
37090
  const percentToClose = (dollarAmount / investedCost) * 100;
37091
+ if (await functoolsKit.not(bt.strategyCoreService.validatePartialLoss(false, symbol, percentToClose, currentPrice, {
37092
+ strategyName: context.strategyName,
37093
+ exchangeName: context.exchangeName,
37094
+ frameName: "",
37095
+ }))) {
37096
+ return false;
37097
+ }
37098
+ await Broker.commitPartialLoss({
37099
+ symbol,
37100
+ percentToClose,
37101
+ cost: dollarAmount,
37102
+ currentPrice,
37103
+ position: signalForLossCost.position,
37104
+ priceTakeProfit: signalForLossCost.priceTakeProfit,
37105
+ priceStopLoss: signalForLossCost.priceStopLoss,
37106
+ context: {
37107
+ strategyName: context.strategyName,
37108
+ exchangeName: context.exchangeName,
37109
+ frameName: "",
37110
+ },
37111
+ backtest: false,
37112
+ });
34540
37113
  return await bt.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
34541
37114
  strategyName: context.strategyName,
34542
37115
  exchangeName: context.exchangeName,
@@ -34597,10 +37170,45 @@ class LiveUtils {
34597
37170
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_TRAILING_STOP);
34598
37171
  {
34599
37172
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34600
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP);
34601
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP));
34602
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_TRAILING_STOP));
37173
+ riskName &&
37174
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP);
37175
+ riskList &&
37176
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP));
37177
+ actions &&
37178
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_TRAILING_STOP));
34603
37179
  }
37180
+ const signal = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
37181
+ strategyName: context.strategyName,
37182
+ exchangeName: context.exchangeName,
37183
+ frameName: "",
37184
+ });
37185
+ if (!signal) {
37186
+ return false;
37187
+ }
37188
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(false, symbol, {
37189
+ strategyName: context.strategyName,
37190
+ exchangeName: context.exchangeName,
37191
+ frameName: "",
37192
+ });
37193
+ if (effectivePriceOpen === null) {
37194
+ return false;
37195
+ }
37196
+ if (await functoolsKit.not(bt.strategyCoreService.validateTrailingStop(false, symbol, percentShift, currentPrice, {
37197
+ strategyName: context.strategyName,
37198
+ exchangeName: context.exchangeName,
37199
+ frameName: "",
37200
+ }))) {
37201
+ return false;
37202
+ }
37203
+ await Broker.commitTrailingStop({
37204
+ symbol,
37205
+ percentShift,
37206
+ currentPrice,
37207
+ newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
37208
+ position: signal.position,
37209
+ context,
37210
+ backtest: false,
37211
+ });
34604
37212
  return await bt.strategyCoreService.trailingStop(false, symbol, percentShift, currentPrice, {
34605
37213
  strategyName: context.strategyName,
34606
37214
  exchangeName: context.exchangeName,
@@ -34661,10 +37269,183 @@ class LiveUtils {
34661
37269
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_TRAILING_PROFIT);
34662
37270
  {
34663
37271
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34664
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT);
34665
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT));
34666
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_TRAILING_PROFIT));
37272
+ riskName &&
37273
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT);
37274
+ riskList &&
37275
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT));
37276
+ actions &&
37277
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_TRAILING_PROFIT));
37278
+ }
37279
+ const signal = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
37280
+ strategyName: context.strategyName,
37281
+ exchangeName: context.exchangeName,
37282
+ frameName: "",
37283
+ });
37284
+ if (!signal) {
37285
+ return false;
37286
+ }
37287
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(false, symbol, {
37288
+ strategyName: context.strategyName,
37289
+ exchangeName: context.exchangeName,
37290
+ frameName: "",
37291
+ });
37292
+ if (effectivePriceOpen === null) {
37293
+ return false;
34667
37294
  }
37295
+ if (await functoolsKit.not(bt.strategyCoreService.validateTrailingTake(false, symbol, percentShift, currentPrice, {
37296
+ strategyName: context.strategyName,
37297
+ exchangeName: context.exchangeName,
37298
+ frameName: "",
37299
+ }))) {
37300
+ return false;
37301
+ }
37302
+ await Broker.commitTrailingTake({
37303
+ symbol,
37304
+ percentShift,
37305
+ currentPrice,
37306
+ newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
37307
+ position: signal.position,
37308
+ context,
37309
+ backtest: false,
37310
+ });
37311
+ return await bt.strategyCoreService.trailingTake(false, symbol, percentShift, currentPrice, {
37312
+ strategyName: context.strategyName,
37313
+ exchangeName: context.exchangeName,
37314
+ frameName: "",
37315
+ });
37316
+ };
37317
+ /**
37318
+ * Adjusts the trailing stop-loss to an absolute price level.
37319
+ *
37320
+ * Convenience wrapper around commitTrailingStop that converts an absolute
37321
+ * stop-loss price to a percentShift relative to the ORIGINAL SL distance.
37322
+ *
37323
+ * @param symbol - Trading pair symbol
37324
+ * @param newStopLossPrice - Desired absolute stop-loss price
37325
+ * @param currentPrice - Current market price to check for intrusion
37326
+ * @param context - Execution context with strategyName and exchangeName
37327
+ * @returns Promise<boolean> - true if trailing SL was set/updated, false if rejected
37328
+ */
37329
+ this.commitTrailingStopCost = async (symbol, newStopLossPrice, currentPrice, context) => {
37330
+ bt.loggerService.info(LIVE_METHOD_NAME_TRAILING_STOP_COST, {
37331
+ symbol,
37332
+ newStopLossPrice,
37333
+ currentPrice,
37334
+ context,
37335
+ });
37336
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_TRAILING_STOP_COST);
37337
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_TRAILING_STOP_COST);
37338
+ {
37339
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
37340
+ riskName &&
37341
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP_COST);
37342
+ riskList &&
37343
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP_COST));
37344
+ actions &&
37345
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_TRAILING_STOP_COST));
37346
+ }
37347
+ const signal = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
37348
+ strategyName: context.strategyName,
37349
+ exchangeName: context.exchangeName,
37350
+ frameName: "",
37351
+ });
37352
+ if (!signal) {
37353
+ return false;
37354
+ }
37355
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(false, symbol, {
37356
+ strategyName: context.strategyName,
37357
+ exchangeName: context.exchangeName,
37358
+ frameName: "",
37359
+ });
37360
+ if (effectivePriceOpen === null) {
37361
+ return false;
37362
+ }
37363
+ const percentShift = slPriceToPercentShift(newStopLossPrice, signal.priceStopLoss, effectivePriceOpen);
37364
+ if (await functoolsKit.not(bt.strategyCoreService.validateTrailingStop(false, symbol, percentShift, currentPrice, {
37365
+ strategyName: context.strategyName,
37366
+ exchangeName: context.exchangeName,
37367
+ frameName: "",
37368
+ }))) {
37369
+ return false;
37370
+ }
37371
+ await Broker.commitTrailingStop({
37372
+ symbol,
37373
+ percentShift,
37374
+ currentPrice,
37375
+ newStopLossPrice,
37376
+ position: signal.position,
37377
+ context,
37378
+ backtest: false,
37379
+ });
37380
+ return await bt.strategyCoreService.trailingStop(false, symbol, percentShift, currentPrice, {
37381
+ strategyName: context.strategyName,
37382
+ exchangeName: context.exchangeName,
37383
+ frameName: "",
37384
+ });
37385
+ };
37386
+ /**
37387
+ * Adjusts the trailing take-profit to an absolute price level.
37388
+ *
37389
+ * Convenience wrapper around commitTrailingTake that converts an absolute
37390
+ * take-profit price to a percentShift relative to the ORIGINAL TP distance.
37391
+ *
37392
+ * @param symbol - Trading pair symbol
37393
+ * @param newTakeProfitPrice - Desired absolute take-profit price
37394
+ * @param currentPrice - Current market price to check for intrusion
37395
+ * @param context - Execution context with strategyName and exchangeName
37396
+ * @returns Promise<boolean> - true if trailing TP was set/updated, false if rejected
37397
+ */
37398
+ this.commitTrailingTakeCost = async (symbol, newTakeProfitPrice, currentPrice, context) => {
37399
+ bt.loggerService.info(LIVE_METHOD_NAME_TRAILING_PROFIT_COST, {
37400
+ symbol,
37401
+ newTakeProfitPrice,
37402
+ currentPrice,
37403
+ context,
37404
+ });
37405
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_TRAILING_PROFIT_COST);
37406
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_TRAILING_PROFIT_COST);
37407
+ {
37408
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
37409
+ riskName &&
37410
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT_COST);
37411
+ riskList &&
37412
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT_COST));
37413
+ actions &&
37414
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_TRAILING_PROFIT_COST));
37415
+ }
37416
+ const signal = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
37417
+ strategyName: context.strategyName,
37418
+ exchangeName: context.exchangeName,
37419
+ frameName: "",
37420
+ });
37421
+ if (!signal) {
37422
+ return false;
37423
+ }
37424
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(false, symbol, {
37425
+ strategyName: context.strategyName,
37426
+ exchangeName: context.exchangeName,
37427
+ frameName: "",
37428
+ });
37429
+ if (effectivePriceOpen === null) {
37430
+ return false;
37431
+ }
37432
+ const percentShift = tpPriceToPercentShift(newTakeProfitPrice, signal.priceTakeProfit, effectivePriceOpen);
37433
+ if (await functoolsKit.not(bt.strategyCoreService.validateTrailingTake(false, symbol, percentShift, currentPrice, {
37434
+ strategyName: context.strategyName,
37435
+ exchangeName: context.exchangeName,
37436
+ frameName: "",
37437
+ }))) {
37438
+ return false;
37439
+ }
37440
+ await Broker.commitTrailingTake({
37441
+ symbol,
37442
+ percentShift,
37443
+ currentPrice,
37444
+ newTakeProfitPrice,
37445
+ position: signal.position,
37446
+ context,
37447
+ backtest: false,
37448
+ });
34668
37449
  return await bt.strategyCoreService.trailingTake(false, symbol, percentShift, currentPrice, {
34669
37450
  strategyName: context.strategyName,
34670
37451
  exchangeName: context.exchangeName,
@@ -34702,10 +37483,49 @@ class LiveUtils {
34702
37483
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_BREAKEVEN);
34703
37484
  {
34704
37485
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34705
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BREAKEVEN);
34706
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BREAKEVEN));
34707
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_BREAKEVEN));
37486
+ riskName &&
37487
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BREAKEVEN);
37488
+ riskList &&
37489
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_BREAKEVEN));
37490
+ actions &&
37491
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_BREAKEVEN));
37492
+ }
37493
+ const signal = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
37494
+ strategyName: context.strategyName,
37495
+ exchangeName: context.exchangeName,
37496
+ frameName: "",
37497
+ });
37498
+ if (!signal) {
37499
+ return false;
37500
+ }
37501
+ const effectivePriceOpen = await bt.strategyCoreService.getPositionAveragePrice(false, symbol, {
37502
+ strategyName: context.strategyName,
37503
+ exchangeName: context.exchangeName,
37504
+ frameName: "",
37505
+ });
37506
+ if (effectivePriceOpen === null) {
37507
+ return false;
34708
37508
  }
37509
+ if (await functoolsKit.not(bt.strategyCoreService.validateBreakeven(false, symbol, currentPrice, {
37510
+ strategyName: context.strategyName,
37511
+ exchangeName: context.exchangeName,
37512
+ frameName: "",
37513
+ }))) {
37514
+ return false;
37515
+ }
37516
+ await Broker.commitBreakeven({
37517
+ symbol,
37518
+ currentPrice,
37519
+ newStopLossPrice: breakevenNewStopLossPrice(effectivePriceOpen),
37520
+ newTakeProfitPrice: breakevenNewTakeProfitPrice(signal.priceTakeProfit, signal._trailingPriceTakeProfit),
37521
+ position: signal.position,
37522
+ context: {
37523
+ strategyName: context.strategyName,
37524
+ exchangeName: context.exchangeName,
37525
+ frameName: "",
37526
+ },
37527
+ backtest: false,
37528
+ });
34709
37529
  return await bt.strategyCoreService.breakeven(false, symbol, currentPrice, {
34710
37530
  strategyName: context.strategyName,
34711
37531
  exchangeName: context.exchangeName,
@@ -34742,9 +37562,12 @@ class LiveUtils {
34742
37562
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED);
34743
37563
  {
34744
37564
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34745
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED);
34746
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED));
34747
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED));
37565
+ riskName &&
37566
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED);
37567
+ riskList &&
37568
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED));
37569
+ actions &&
37570
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED));
34748
37571
  }
34749
37572
  await bt.strategyCoreService.activateScheduled(false, symbol, {
34750
37573
  strategyName: context.strategyName,
@@ -34785,10 +37608,42 @@ class LiveUtils {
34785
37608
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_AVERAGE_BUY);
34786
37609
  {
34787
37610
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34788
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_AVERAGE_BUY);
34789
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_AVERAGE_BUY));
34790
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_AVERAGE_BUY));
37611
+ riskName &&
37612
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_AVERAGE_BUY);
37613
+ riskList &&
37614
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_AVERAGE_BUY));
37615
+ actions &&
37616
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_AVERAGE_BUY));
37617
+ }
37618
+ if (await functoolsKit.not(bt.strategyCoreService.validateAverageBuy(false, symbol, currentPrice, {
37619
+ strategyName: context.strategyName,
37620
+ exchangeName: context.exchangeName,
37621
+ frameName: "",
37622
+ }))) {
37623
+ return false;
37624
+ }
37625
+ const signalForAvgBuy = await bt.strategyCoreService.getPendingSignal(false, symbol, currentPrice, {
37626
+ strategyName: context.strategyName,
37627
+ exchangeName: context.exchangeName,
37628
+ frameName: "",
37629
+ });
37630
+ if (!signalForAvgBuy) {
37631
+ return false;
34791
37632
  }
37633
+ await Broker.commitAverageBuy({
37634
+ symbol,
37635
+ currentPrice,
37636
+ cost,
37637
+ position: signalForAvgBuy.position,
37638
+ priceTakeProfit: signalForAvgBuy.priceTakeProfit,
37639
+ priceStopLoss: signalForAvgBuy.priceStopLoss,
37640
+ context: {
37641
+ strategyName: context.strategyName,
37642
+ exchangeName: context.exchangeName,
37643
+ frameName: "",
37644
+ },
37645
+ backtest: false,
37646
+ });
34792
37647
  return await bt.strategyCoreService.averageBuy(false, symbol, currentPrice, {
34793
37648
  strategyName: context.strategyName,
34794
37649
  exchangeName: context.exchangeName,
@@ -34822,9 +37677,12 @@ class LiveUtils {
34822
37677
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_DATA);
34823
37678
  {
34824
37679
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34825
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_DATA);
34826
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_DATA));
34827
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_DATA));
37680
+ riskName &&
37681
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_DATA);
37682
+ riskList &&
37683
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_DATA));
37684
+ actions &&
37685
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_DATA));
34828
37686
  }
34829
37687
  return await bt.liveMarkdownService.getData(symbol, context.strategyName, context.exchangeName, "", false);
34830
37688
  };
@@ -34856,9 +37714,12 @@ class LiveUtils {
34856
37714
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_REPORT);
34857
37715
  {
34858
37716
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34859
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_REPORT);
34860
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_REPORT));
34861
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_REPORT));
37717
+ riskName &&
37718
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_REPORT);
37719
+ riskList &&
37720
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_REPORT));
37721
+ actions &&
37722
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_REPORT));
34862
37723
  }
34863
37724
  return await bt.liveMarkdownService.getReport(symbol, context.strategyName, context.exchangeName, "", false, columns);
34864
37725
  };
@@ -34898,9 +37759,12 @@ class LiveUtils {
34898
37759
  bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_DUMP);
34899
37760
  {
34900
37761
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
34901
- riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_DUMP);
34902
- riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_DUMP));
34903
- actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_DUMP));
37762
+ riskName &&
37763
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_DUMP);
37764
+ riskList &&
37765
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_DUMP));
37766
+ actions &&
37767
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_DUMP));
34904
37768
  }
34905
37769
  await bt.liveMarkdownService.dump(symbol, context.strategyName, context.exchangeName, "", false, path, columns);
34906
37770
  };
@@ -42491,6 +45355,8 @@ const percentValue = (yesterdayValue, todayValue) => {
42491
45355
  exports.ActionBase = ActionBase;
42492
45356
  exports.Backtest = Backtest;
42493
45357
  exports.Breakeven = Breakeven;
45358
+ exports.Broker = Broker;
45359
+ exports.BrokerBase = BrokerBase;
42494
45360
  exports.Cache = Cache;
42495
45361
  exports.Constant = Constant;
42496
45362
  exports.Exchange = Exchange;
@@ -42548,7 +45414,9 @@ exports.commitPartialLossCost = commitPartialLossCost;
42548
45414
  exports.commitPartialProfit = commitPartialProfit;
42549
45415
  exports.commitPartialProfitCost = commitPartialProfitCost;
42550
45416
  exports.commitTrailingStop = commitTrailingStop;
45417
+ exports.commitTrailingStopCost = commitTrailingStopCost;
42551
45418
  exports.commitTrailingTake = commitTrailingTake;
45419
+ exports.commitTrailingTakeCost = commitTrailingTakeCost;
42552
45420
  exports.dumpMessages = dumpMessages;
42553
45421
  exports.emitters = emitters;
42554
45422
  exports.formatPrice = formatPrice;
@@ -42646,6 +45514,7 @@ exports.overrideStrategySchema = overrideStrategySchema;
42646
45514
  exports.overrideWalkerSchema = overrideWalkerSchema;
42647
45515
  exports.parseArgs = parseArgs;
42648
45516
  exports.percentDiff = percentDiff;
45517
+ exports.percentToCloseCost = percentToCloseCost;
42649
45518
  exports.percentValue = percentValue;
42650
45519
  exports.roundTicks = roundTicks;
42651
45520
  exports.set = set;
@@ -42653,8 +45522,12 @@ exports.setColumns = setColumns;
42653
45522
  exports.setConfig = setConfig;
42654
45523
  exports.setLogger = setLogger;
42655
45524
  exports.shutdown = shutdown;
45525
+ exports.slPercentShiftToPrice = slPercentShiftToPrice;
45526
+ exports.slPriceToPercentShift = slPriceToPercentShift;
42656
45527
  exports.stopStrategy = stopStrategy;
42657
45528
  exports.toProfitLossDto = toProfitLossDto;
45529
+ exports.tpPercentShiftToPrice = tpPercentShiftToPrice;
45530
+ exports.tpPriceToPercentShift = tpPriceToPercentShift;
42658
45531
  exports.validate = validate;
42659
45532
  exports.waitForCandle = waitForCandle;
42660
45533
  exports.warmCandles = warmCandles;