backtest-kit 4.0.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -470,9 +470,17 @@ const GLOBAL_CONFIG = {
470
470
  * Allows to commitAverageBuy if currentPrice is not the lowest price since entry, but still lower than priceOpen.
471
471
  * This can help improve average entry price in cases where price has rebounded after entry but is still below priceOpen, without waiting for a new lower price.
472
472
  *
473
- * Default: true (DCA logic enabled everywhere, not just when antirecord is broken)
473
+ * Default: false (DCA logic enabled only when antirecord is broken)
474
474
  */
475
475
  CC_ENABLE_DCA_EVERYWHERE: false,
476
+ /**
477
+ * Enables PPPL (Partial Profit, Partial Loss) logic even if this breaks a direction of exits
478
+ * Allows to take partial profit or loss on a position even if it results in a mix of profit and loss exits
479
+ * This can help lock in profits or cut losses on part of the position without waiting for a perfect exit scenario.
480
+ *
481
+ * Default: false (PPPL logic is only applied when it does not break the direction of exits, ensuring clearer profit/loss outcomes)
482
+ */
483
+ CC_ENABLE_PPPL_EVERYWHERE: false,
476
484
  /**
477
485
  * Cost of entering a position (in USD).
478
486
  * This is used as a default value for calculating position size and risk management when cost data is not provided by the strategy
@@ -7448,10 +7456,12 @@ class ClientStrategy {
7448
7456
  if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
7449
7457
  return false;
7450
7458
  const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7451
- if (this._pendingSignal.position === "long" && currentPrice <= effectivePriceOpen)
7452
- return false;
7453
- if (this._pendingSignal.position === "short" && currentPrice >= effectivePriceOpen)
7454
- return false;
7459
+ if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
7460
+ if (this._pendingSignal.position === "long" && currentPrice <= effectivePriceOpen)
7461
+ return false;
7462
+ if (this._pendingSignal.position === "short" && currentPrice >= effectivePriceOpen)
7463
+ return false;
7464
+ }
7455
7465
  const effectiveTakeProfit = this._pendingSignal._trailingPriceTakeProfit ?? this._pendingSignal.priceTakeProfit;
7456
7466
  if (this._pendingSignal.position === "long" && currentPrice >= effectiveTakeProfit)
7457
7467
  return false;
@@ -7535,7 +7545,7 @@ class ClientStrategy {
7535
7545
  throw new Error(`ClientStrategy partialProfit: currentPrice must be a positive finite number, got ${currentPrice}`);
7536
7546
  }
7537
7547
  // Validation: currentPrice must be moving toward TP (profit direction)
7538
- {
7548
+ if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
7539
7549
  const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7540
7550
  if (this._pendingSignal.position === "long") {
7541
7551
  // For LONG: currentPrice must be higher than effectivePriceOpen (moving toward TP)
@@ -7629,10 +7639,12 @@ class ClientStrategy {
7629
7639
  if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
7630
7640
  return false;
7631
7641
  const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7632
- if (this._pendingSignal.position === "long" && currentPrice >= effectivePriceOpen)
7633
- return false;
7634
- if (this._pendingSignal.position === "short" && currentPrice <= effectivePriceOpen)
7635
- return false;
7642
+ if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
7643
+ if (this._pendingSignal.position === "long" && currentPrice >= effectivePriceOpen)
7644
+ return false;
7645
+ if (this._pendingSignal.position === "short" && currentPrice <= effectivePriceOpen)
7646
+ return false;
7647
+ }
7636
7648
  const effectiveStopLoss = this._pendingSignal._trailingPriceStopLoss ?? this._pendingSignal.priceStopLoss;
7637
7649
  if (this._pendingSignal.position === "long" && currentPrice <= effectiveStopLoss)
7638
7650
  return false;
@@ -7716,7 +7728,7 @@ class ClientStrategy {
7716
7728
  throw new Error(`ClientStrategy partialLoss: currentPrice must be a positive finite number, got ${currentPrice}`);
7717
7729
  }
7718
7730
  // Validation: currentPrice must be moving toward SL (loss direction)
7719
- {
7731
+ if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
7720
7732
  const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7721
7733
  if (this._pendingSignal.position === "long") {
7722
7734
  // For LONG: currentPrice must be lower than effectivePriceOpen (moving toward SL)
@@ -10864,7 +10876,6 @@ const METHOD_NAME_PARTIAL_LOSS_AVAILABLE = "ActionBase.partialLossAvailable";
10864
10876
  const METHOD_NAME_PING_SCHEDULED = "ActionBase.pingScheduled";
10865
10877
  const METHOD_NAME_PING_ACTIVE = "ActionBase.pingActive";
10866
10878
  const METHOD_NAME_RISK_REJECTION = "ActionBase.riskRejection";
10867
- const METHOD_NAME_SIGNAL_SYNC = "ActionBase.signalSync";
10868
10879
  const METHOD_NAME_DISPOSE = "ActionBase.dispose";
10869
10880
  const DEFAULT_SOURCE = "default";
10870
10881
  /**
@@ -11276,6 +11287,11 @@ class ActionProxy {
11276
11287
  */
11277
11288
  async signalSync(event) {
11278
11289
  if (this._target.signalSync) {
11290
+ console.error("Action::signalSync is unwanted cause exchange integration should be implemented in Broker.useBrokerAdapter as an infrastructure domain layer");
11291
+ console.error("If you need to implement custom logic on signal open/close, please use signal(), signalBacktest(), signalLive()");
11292
+ console.error("If Action::signalSync throws the exchange will not execute the order!");
11293
+ console.error("");
11294
+ console.error("You have been warned!");
11279
11295
  await this._target.signalSync(event);
11280
11296
  }
11281
11297
  }
@@ -11738,19 +11754,6 @@ class ActionBase {
11738
11754
  source,
11739
11755
  });
11740
11756
  }
11741
- /**
11742
- * Gate for position open/close via limit order. Default allows all.
11743
- * Throw to reject — framework retries next tick.
11744
- *
11745
- * NOTE: Exceptions are NOT swallowed — they propagate to CREATE_SYNC_FN.
11746
- *
11747
- * @param event - Sync event with action "signal-open" or "signal-close"
11748
- */
11749
- signalSync(_event, source = DEFAULT_SOURCE) {
11750
- bt.loggerService.info(METHOD_NAME_SIGNAL_SYNC, {
11751
- source,
11752
- });
11753
- }
11754
11757
  /**
11755
11758
  * Cleans up resources and subscriptions when action handler is disposed.
11756
11759
  *
@@ -31781,6 +31784,11 @@ BrokerBase = functoolsKit.makeExtendable(BrokerBase);
31781
31784
  */
31782
31785
  const Broker = new BrokerAdapter();
31783
31786
 
31787
+ const POSITION_OVERLAP_LADDER_DEFAULT = {
31788
+ upperPercent: 1.5,
31789
+ lowerPercent: 1.5,
31790
+ };
31791
+
31784
31792
  const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
31785
31793
  const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
31786
31794
  const PARTIAL_PROFIT_METHOD_NAME = "strategy.commitPartialProfit";
@@ -31806,6 +31814,8 @@ const GET_POSITION_PNL_PERCENT_METHOD_NAME = "strategy.getPositionPnlPercent";
31806
31814
  const GET_POSITION_PNL_COST_METHOD_NAME = "strategy.getPositionPnlCost";
31807
31815
  const GET_POSITION_LEVELS_METHOD_NAME = "strategy.getPositionLevels";
31808
31816
  const GET_POSITION_PARTIALS_METHOD_NAME = "strategy.getPositionPartials";
31817
+ const GET_POSITION_ENTRY_OVERLAP_METHOD_NAME = "strategy.getPositionEntryOverlap";
31818
+ const GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME = "strategy.getPositionPartialOverlap";
31809
31819
  /**
31810
31820
  * Cancels the scheduled signal without stopping the strategy.
31811
31821
  *
@@ -32082,6 +32092,7 @@ async function commitTrailingStop(symbol, percentShift, currentPrice) {
32082
32092
  percentShift,
32083
32093
  currentPrice,
32084
32094
  newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
32095
+ takeProfitPrice: signal.priceTakeProfit,
32085
32096
  position: signal.position,
32086
32097
  context: { exchangeName, frameName, strategyName },
32087
32098
  backtest: isBacktest,
@@ -32161,6 +32172,7 @@ async function commitTrailingTake(symbol, percentShift, currentPrice) {
32161
32172
  percentShift,
32162
32173
  currentPrice,
32163
32174
  newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
32175
+ takeProfitPrice: signal.priceTakeProfit,
32164
32176
  position: signal.position,
32165
32177
  context: { exchangeName, frameName, strategyName },
32166
32178
  backtest: isBacktest,
@@ -32212,6 +32224,7 @@ async function commitTrailingStopCost(symbol, newStopLossPrice) {
32212
32224
  currentPrice,
32213
32225
  newStopLossPrice,
32214
32226
  position: signal.position,
32227
+ takeProfitPrice: signal.priceTakeProfit,
32215
32228
  context: { exchangeName, frameName, strategyName },
32216
32229
  backtest: isBacktest,
32217
32230
  });
@@ -32261,6 +32274,7 @@ async function commitTrailingTakeCost(symbol, newTakeProfitPrice) {
32261
32274
  percentShift,
32262
32275
  currentPrice,
32263
32276
  newTakeProfitPrice,
32277
+ takeProfitPrice: signal.priceTakeProfit,
32264
32278
  position: signal.position,
32265
32279
  context: { exchangeName, frameName, strategyName },
32266
32280
  backtest: isBacktest,
@@ -32587,6 +32601,31 @@ async function getBreakeven(symbol, currentPrice) {
32587
32601
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32588
32602
  return await bt.strategyCoreService.getBreakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32589
32603
  }
32604
+ /**
32605
+ * Returns the effective (DCA-weighted) entry price for the current pending signal.
32606
+ *
32607
+ * Uses cost-weighted harmonic mean: Σcost / Σ(cost/price).
32608
+ * When partial closes exist, the price is computed iteratively using
32609
+ * costBasisAtClose snapshots from each partial, then blended with any
32610
+ * DCA entries added after the last partial.
32611
+ * With no DCA entries, equals the original priceOpen.
32612
+ *
32613
+ * Returns null if no pending signal exists.
32614
+ *
32615
+ * Automatically detects backtest/live mode from execution context.
32616
+ *
32617
+ * @param symbol - Trading pair symbol
32618
+ * @returns Promise resolving to effective entry price or null
32619
+ *
32620
+ * @example
32621
+ * ```typescript
32622
+ * import { getPositionAveragePrice } from "backtest-kit";
32623
+ *
32624
+ * const avgPrice = await getPositionAveragePrice("BTCUSDT");
32625
+ * // No DCA: avgPrice === priceOpen
32626
+ * // After DCA at lower price: avgPrice < priceOpen
32627
+ * ```
32628
+ */
32590
32629
  async function getPositionAveragePrice(symbol) {
32591
32630
  bt.loggerService.info(GET_POSITION_AVERAGE_PRICE_METHOD_NAME, {
32592
32631
  symbol,
@@ -32601,6 +32640,28 @@ async function getPositionAveragePrice(symbol) {
32601
32640
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32602
32641
  return await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
32603
32642
  }
32643
+ /**
32644
+ * Returns the number of DCA entries made for the current pending signal.
32645
+ *
32646
+ * 1 = original entry only (no DCA).
32647
+ * Increases by 1 with each successful commitAverageBuy().
32648
+ *
32649
+ * Returns null if no pending signal exists.
32650
+ *
32651
+ * Automatically detects backtest/live mode from execution context.
32652
+ *
32653
+ * @param symbol - Trading pair symbol
32654
+ * @returns Promise resolving to entry count or null
32655
+ *
32656
+ * @example
32657
+ * ```typescript
32658
+ * import { getPositionInvestedCount } from "backtest-kit";
32659
+ *
32660
+ * const count = await getPositionInvestedCount("BTCUSDT");
32661
+ * // No DCA: count === 1
32662
+ * // After one DCA: count === 2
32663
+ * ```
32664
+ */
32604
32665
  async function getPositionInvestedCount(symbol) {
32605
32666
  bt.loggerService.info(GET_POSITION_INVESTED_COUNT_METHOD_NAME, {
32606
32667
  symbol,
@@ -32615,6 +32676,28 @@ async function getPositionInvestedCount(symbol) {
32615
32676
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32616
32677
  return await bt.strategyCoreService.getPositionInvestedCount(isBacktest, symbol, { exchangeName, frameName, strategyName });
32617
32678
  }
32679
+ /**
32680
+ * Returns the total invested cost basis in dollars for the current pending signal.
32681
+ *
32682
+ * Equal to the sum of all _entry costs (Σ entry.cost).
32683
+ * Each entry cost is set at the time of commitAverageBuy (defaults to CC_POSITION_ENTRY_COST).
32684
+ *
32685
+ * Returns null if no pending signal exists.
32686
+ *
32687
+ * Automatically detects backtest/live mode from execution context.
32688
+ *
32689
+ * @param symbol - Trading pair symbol
32690
+ * @returns Promise resolving to total invested cost in dollars or null
32691
+ *
32692
+ * @example
32693
+ * ```typescript
32694
+ * import { getPositionInvestedCost } from "backtest-kit";
32695
+ *
32696
+ * const cost = await getPositionInvestedCost("BTCUSDT");
32697
+ * // No DCA, default cost: cost === 100
32698
+ * // After one DCA with default cost: cost === 200
32699
+ * ```
32700
+ */
32618
32701
  async function getPositionInvestedCost(symbol) {
32619
32702
  bt.loggerService.info(GET_POSITION_INVESTED_COST_METHOD_NAME, {
32620
32703
  symbol,
@@ -32629,6 +32712,29 @@ async function getPositionInvestedCost(symbol) {
32629
32712
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32630
32713
  return await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
32631
32714
  }
32715
+ /**
32716
+ * Returns the unrealized PNL percentage for the current pending signal at current market price.
32717
+ *
32718
+ * Accounts for partial closes, DCA entries, slippage and fees
32719
+ * (delegates to toProfitLossDto).
32720
+ *
32721
+ * Returns null if no pending signal exists.
32722
+ *
32723
+ * Automatically detects backtest/live mode from execution context.
32724
+ * Automatically fetches current price via getAveragePrice.
32725
+ *
32726
+ * @param symbol - Trading pair symbol
32727
+ * @returns Promise resolving to PNL percentage or null
32728
+ *
32729
+ * @example
32730
+ * ```typescript
32731
+ * import { getPositionPnlPercent } from "backtest-kit";
32732
+ *
32733
+ * const pnlPct = await getPositionPnlPercent("BTCUSDT");
32734
+ * // LONG at 100, current=105: pnlPct ≈ 5
32735
+ * // LONG at 100, current=95: pnlPct ≈ -5
32736
+ * ```
32737
+ */
32632
32738
  async function getPositionPnlPercent(symbol) {
32633
32739
  bt.loggerService.info(GET_POSITION_PNL_PERCENT_METHOD_NAME, { symbol });
32634
32740
  if (!ExecutionContextService.hasContext()) {
@@ -32778,6 +32884,29 @@ async function commitPartialLossCost(symbol, dollarAmount) {
32778
32884
  });
32779
32885
  return await bt.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
32780
32886
  }
32887
+ /**
32888
+ * Returns the unrealized PNL in dollars for the current pending signal at current market price.
32889
+ *
32890
+ * Calculated as: pnlPercentage / 100 × totalInvestedCost.
32891
+ * Accounts for partial closes, DCA entries, slippage and fees.
32892
+ *
32893
+ * Returns null if no pending signal exists.
32894
+ *
32895
+ * Automatically detects backtest/live mode from execution context.
32896
+ * Automatically fetches current price via getAveragePrice.
32897
+ *
32898
+ * @param symbol - Trading pair symbol
32899
+ * @returns Promise resolving to PNL in dollars or null
32900
+ *
32901
+ * @example
32902
+ * ```typescript
32903
+ * import { getPositionPnlCost } from "backtest-kit";
32904
+ *
32905
+ * const pnlCost = await getPositionPnlCost("BTCUSDT");
32906
+ * // LONG at 100, invested $100, current=105: pnlCost ≈ 5
32907
+ * // LONG at 100, invested $200 (DCA), current=95: pnlCost ≈ -10
32908
+ * ```
32909
+ */
32781
32910
  async function getPositionPnlCost(symbol) {
32782
32911
  bt.loggerService.info(GET_POSITION_PNL_COST_METHOD_NAME, { symbol });
32783
32912
  if (!ExecutionContextService.hasContext()) {
@@ -32864,6 +32993,104 @@ async function getPositionPartials(symbol) {
32864
32993
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32865
32994
  return await bt.strategyCoreService.getPositionPartials(isBacktest, symbol, { exchangeName, frameName, strategyName });
32866
32995
  }
32996
+ /**
32997
+ * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
32998
+ * Use this to prevent duplicate DCA entries at the same price area.
32999
+ *
33000
+ * Returns true if currentPrice is within [level - lowerStep, level + upperStep] for any level,
33001
+ * where step = level * percent / 100.
33002
+ * Returns false if no pending signal exists.
33003
+ *
33004
+ * @param symbol - Trading pair symbol
33005
+ * @param currentPrice - Price to check against existing DCA levels
33006
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
33007
+ * @returns Promise<boolean> - true if price overlaps an existing entry level (DCA not recommended)
33008
+ *
33009
+ * @example
33010
+ * ```typescript
33011
+ * import { getPositionEntryOverlap } from "backtest-kit";
33012
+ *
33013
+ * // LONG with levels [43000, 42000], check if 42100 is too close to 42000
33014
+ * const overlap = await getPositionEntryOverlap("BTCUSDT", 42100, { upperPercent: 5, lowerPercent: 5 });
33015
+ * // overlap = true (42100 is within 5% of 42000 = [39900, 44100])
33016
+ * if (!overlap) {
33017
+ * await commitAverageBuy("BTCUSDT");
33018
+ * }
33019
+ * ```
33020
+ */
33021
+ async function getPositionEntryOverlap(symbol, currentPrice, ladder = POSITION_OVERLAP_LADDER_DEFAULT) {
33022
+ bt.loggerService.info(GET_POSITION_ENTRY_OVERLAP_METHOD_NAME, {
33023
+ symbol,
33024
+ currentPrice,
33025
+ ladder,
33026
+ });
33027
+ if (!ExecutionContextService.hasContext()) {
33028
+ throw new Error("getPositionEntryOverlap requires an execution context");
33029
+ }
33030
+ if (!MethodContextService.hasContext()) {
33031
+ throw new Error("getPositionEntryOverlap requires a method context");
33032
+ }
33033
+ const { backtest: isBacktest } = bt.executionContextService.context;
33034
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
33035
+ const levels = await bt.strategyCoreService.getPositionLevels(isBacktest, symbol, { exchangeName, frameName, strategyName });
33036
+ if (!levels) {
33037
+ return false;
33038
+ }
33039
+ return levels.some((level) => {
33040
+ const upperStep = (level * ladder.upperPercent) / 100;
33041
+ const lowerStep = (level * ladder.lowerPercent) / 100;
33042
+ return currentPrice >= level - lowerStep && currentPrice <= level + upperStep;
33043
+ });
33044
+ }
33045
+ /**
33046
+ * Checks whether the current price falls within the tolerance zone of any existing partial close price.
33047
+ * Use this to prevent duplicate partial closes at the same price area.
33048
+ *
33049
+ * Returns true if currentPrice is within [partial.currentPrice - lowerStep, partial.currentPrice + upperStep]
33050
+ * for any partial, where step = partial.currentPrice * percent / 100.
33051
+ * Returns false if no pending signal exists or no partials have been executed yet.
33052
+ *
33053
+ * @param symbol - Trading pair symbol
33054
+ * @param currentPrice - Price to check against existing partial close prices
33055
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
33056
+ * @returns Promise<boolean> - true if price overlaps an existing partial price (partial not recommended)
33057
+ *
33058
+ * @example
33059
+ * ```typescript
33060
+ * import { getPositionPartialOverlap } from "backtest-kit";
33061
+ *
33062
+ * // Partials at [45000], check if 45100 is too close
33063
+ * const overlap = await getPositionPartialOverlap("BTCUSDT", 45100, { upperPercent: 1.5, lowerPercent: 1.5 });
33064
+ * // overlap = true (45100 is within 1.5% of 45000)
33065
+ * if (!overlap) {
33066
+ * await commitPartialProfit("BTCUSDT", 50);
33067
+ * }
33068
+ * ```
33069
+ */
33070
+ async function getPositionPartialOverlap(symbol, currentPrice, ladder = POSITION_OVERLAP_LADDER_DEFAULT) {
33071
+ bt.loggerService.info(GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME, {
33072
+ symbol,
33073
+ currentPrice,
33074
+ ladder,
33075
+ });
33076
+ if (!ExecutionContextService.hasContext()) {
33077
+ throw new Error("getPositionPartialOverlap requires an execution context");
33078
+ }
33079
+ if (!MethodContextService.hasContext()) {
33080
+ throw new Error("getPositionPartialOverlap requires a method context");
33081
+ }
33082
+ const { backtest: isBacktest } = bt.executionContextService.context;
33083
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
33084
+ const partials = await bt.strategyCoreService.getPositionPartials(isBacktest, symbol, { exchangeName, frameName, strategyName });
33085
+ if (!partials) {
33086
+ return false;
33087
+ }
33088
+ return partials.some((partial) => {
33089
+ const upperStep = (partial.currentPrice * ladder.upperPercent) / 100;
33090
+ const lowerStep = (partial.currentPrice * ladder.lowerPercent) / 100;
33091
+ return currentPrice >= partial.currentPrice - lowerStep && currentPrice <= partial.currentPrice + upperStep;
33092
+ });
33093
+ }
32867
33094
 
32868
33095
  const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
32869
33096
  /**
@@ -34090,6 +34317,8 @@ const BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST = "BacktestUtils.getPositionPnl
34090
34317
  const BACKTEST_METHOD_NAME_GET_POSITION_LEVELS = "BacktestUtils.getPositionLevels";
34091
34318
  const BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS = "BacktestUtils.getPositionPartials";
34092
34319
  const BACKTEST_METHOD_NAME_GET_POSITION_ENTRIES = "BacktestUtils.getPositionEntries";
34320
+ const BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "BacktestUtils.getPositionEntryOverlap";
34321
+ const BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "BacktestUtils.getPositionPartialOverlap";
34093
34322
  const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
34094
34323
  const BACKTEST_METHOD_NAME_CANCEL_SCHEDULED = "Backtest.commitCancelScheduled";
34095
34324
  const BACKTEST_METHOD_NAME_CLOSE_PENDING = "Backtest.commitClosePending";
@@ -34880,6 +35109,90 @@ class BacktestUtils {
34880
35109
  }
34881
35110
  return await bt.strategyCoreService.getPositionEntries(true, symbol, context);
34882
35111
  };
35112
+ /**
35113
+ * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
35114
+ * Use this to prevent duplicate DCA entries at the same price area.
35115
+ *
35116
+ * Returns true if currentPrice is within [level - lowerStep, level + upperStep] for any level,
35117
+ * where step = level * percent / 100.
35118
+ * Returns false if no pending signal exists.
35119
+ *
35120
+ * @param symbol - Trading pair symbol
35121
+ * @param currentPrice - Price to check against existing DCA levels
35122
+ * @param context - Execution context with strategyName, exchangeName, and frameName
35123
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
35124
+ * @returns true if price overlaps an existing entry level (DCA not recommended)
35125
+ */
35126
+ this.getPositionEntryOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
35127
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP, {
35128
+ symbol,
35129
+ currentPrice,
35130
+ context,
35131
+ ladder,
35132
+ });
35133
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
35134
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
35135
+ {
35136
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
35137
+ riskName &&
35138
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
35139
+ riskList &&
35140
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
35141
+ actions &&
35142
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
35143
+ }
35144
+ const levels = await bt.strategyCoreService.getPositionLevels(true, symbol, context);
35145
+ if (!levels) {
35146
+ return false;
35147
+ }
35148
+ return levels.some((level) => {
35149
+ const upperStep = (level * ladder.upperPercent) / 100;
35150
+ const lowerStep = (level * ladder.lowerPercent) / 100;
35151
+ return currentPrice >= level - lowerStep && currentPrice <= level + upperStep;
35152
+ });
35153
+ };
35154
+ /**
35155
+ * Checks whether the current price falls within the tolerance zone of any existing partial close price.
35156
+ * Use this to prevent duplicate partial closes at the same price area.
35157
+ *
35158
+ * Returns true if currentPrice is within [partial.currentPrice - lowerStep, partial.currentPrice + upperStep]
35159
+ * for any partial, where step = partial.currentPrice * percent / 100.
35160
+ * Returns false if no pending signal exists or no partials have been executed yet.
35161
+ *
35162
+ * @param symbol - Trading pair symbol
35163
+ * @param currentPrice - Price to check against existing partial close prices
35164
+ * @param context - Execution context with strategyName, exchangeName, and frameName
35165
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
35166
+ * @returns true if price overlaps an existing partial price (partial not recommended)
35167
+ */
35168
+ this.getPositionPartialOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
35169
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP, {
35170
+ symbol,
35171
+ currentPrice,
35172
+ context,
35173
+ ladder,
35174
+ });
35175
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
35176
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
35177
+ {
35178
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
35179
+ riskName &&
35180
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
35181
+ riskList &&
35182
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
35183
+ actions &&
35184
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
35185
+ }
35186
+ const partials = await bt.strategyCoreService.getPositionPartials(true, symbol, context);
35187
+ if (!partials) {
35188
+ return false;
35189
+ }
35190
+ return partials.some((partial) => {
35191
+ const upperStep = (partial.currentPrice * ladder.upperPercent) / 100;
35192
+ const lowerStep = (partial.currentPrice * ladder.lowerPercent) / 100;
35193
+ return currentPrice >= partial.currentPrice - lowerStep && currentPrice <= partial.currentPrice + upperStep;
35194
+ });
35195
+ };
34883
35196
  /**
34884
35197
  * Stops the strategy from generating new signals.
34885
35198
  *
@@ -35369,6 +35682,7 @@ class BacktestUtils {
35369
35682
  percentShift,
35370
35683
  currentPrice,
35371
35684
  newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
35685
+ takeProfitPrice: signal.priceTakeProfit,
35372
35686
  position: signal.position,
35373
35687
  context,
35374
35688
  backtest: true,
@@ -35453,6 +35767,7 @@ class BacktestUtils {
35453
35767
  percentShift,
35454
35768
  currentPrice,
35455
35769
  newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
35770
+ takeProfitPrice: signal.priceTakeProfit,
35456
35771
  position: signal.position,
35457
35772
  context,
35458
35773
  backtest: true,
@@ -35507,6 +35822,7 @@ class BacktestUtils {
35507
35822
  currentPrice,
35508
35823
  newStopLossPrice,
35509
35824
  position: signal.position,
35825
+ takeProfitPrice: signal.priceTakeProfit,
35510
35826
  context,
35511
35827
  backtest: true,
35512
35828
  });
@@ -35560,6 +35876,7 @@ class BacktestUtils {
35560
35876
  currentPrice,
35561
35877
  newTakeProfitPrice,
35562
35878
  position: signal.position,
35879
+ takeProfitPrice: signal.priceTakeProfit,
35563
35880
  context,
35564
35881
  backtest: true,
35565
35882
  });
@@ -35903,6 +36220,8 @@ const LIVE_METHOD_NAME_GET_POSITION_PNL_COST = "LiveUtils.getPositionPnlCost";
35903
36220
  const LIVE_METHOD_NAME_GET_POSITION_LEVELS = "LiveUtils.getPositionLevels";
35904
36221
  const LIVE_METHOD_NAME_GET_POSITION_PARTIALS = "LiveUtils.getPositionPartials";
35905
36222
  const LIVE_METHOD_NAME_GET_POSITION_ENTRIES = "LiveUtils.getPositionEntries";
36223
+ const LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "LiveUtils.getPositionEntryOverlap";
36224
+ const LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "LiveUtils.getPositionPartialOverlap";
35906
36225
  const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
35907
36226
  const LIVE_METHOD_NAME_CANCEL_SCHEDULED = "Live.cancelScheduled";
35908
36227
  const LIVE_METHOD_NAME_CLOSE_PENDING = "Live.closePending";
@@ -36756,6 +37075,98 @@ class LiveUtils {
36756
37075
  frameName: "",
36757
37076
  });
36758
37077
  };
37078
+ /**
37079
+ * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
37080
+ * Use this to prevent duplicate DCA entries at the same price area.
37081
+ *
37082
+ * Returns true if currentPrice is within [level - lowerStep, level + upperStep] for any level,
37083
+ * where step = level * percent / 100.
37084
+ * Returns false if no pending signal exists.
37085
+ *
37086
+ * @param symbol - Trading pair symbol
37087
+ * @param currentPrice - Price to check against existing DCA levels
37088
+ * @param context - Execution context with strategyName and exchangeName
37089
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
37090
+ * @returns true if price overlaps an existing entry level (DCA not recommended)
37091
+ */
37092
+ this.getPositionEntryOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
37093
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP, {
37094
+ symbol,
37095
+ currentPrice,
37096
+ context,
37097
+ ladder,
37098
+ });
37099
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
37100
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
37101
+ {
37102
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
37103
+ riskName &&
37104
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
37105
+ riskList &&
37106
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
37107
+ actions &&
37108
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
37109
+ }
37110
+ const levels = await bt.strategyCoreService.getPositionLevels(false, symbol, {
37111
+ strategyName: context.strategyName,
37112
+ exchangeName: context.exchangeName,
37113
+ frameName: "",
37114
+ });
37115
+ if (!levels) {
37116
+ return false;
37117
+ }
37118
+ return levels.some((level) => {
37119
+ const upperStep = (level * ladder.upperPercent) / 100;
37120
+ const lowerStep = (level * ladder.lowerPercent) / 100;
37121
+ return currentPrice >= level - lowerStep && currentPrice <= level + upperStep;
37122
+ });
37123
+ };
37124
+ /**
37125
+ * Checks whether the current price falls within the tolerance zone of any existing partial close price.
37126
+ * Use this to prevent duplicate partial closes at the same price area.
37127
+ *
37128
+ * Returns true if currentPrice is within [partial.currentPrice - lowerStep, partial.currentPrice + upperStep]
37129
+ * for any partial, where step = partial.currentPrice * percent / 100.
37130
+ * Returns false if no pending signal exists or no partials have been executed yet.
37131
+ *
37132
+ * @param symbol - Trading pair symbol
37133
+ * @param currentPrice - Price to check against existing partial close prices
37134
+ * @param context - Execution context with strategyName and exchangeName
37135
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
37136
+ * @returns true if price overlaps an existing partial price (partial not recommended)
37137
+ */
37138
+ this.getPositionPartialOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
37139
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP, {
37140
+ symbol,
37141
+ currentPrice,
37142
+ context,
37143
+ ladder,
37144
+ });
37145
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
37146
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
37147
+ {
37148
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
37149
+ riskName &&
37150
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
37151
+ riskList &&
37152
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
37153
+ actions &&
37154
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
37155
+ }
37156
+ const partials = await bt.strategyCoreService.getPositionPartials(false, symbol, {
37157
+ strategyName: context.strategyName,
37158
+ exchangeName: context.exchangeName,
37159
+ frameName: "",
37160
+ });
37161
+ if (!partials) {
37162
+ return false;
37163
+ }
37164
+ return partials.some((partial) => {
37165
+ const upperStep = (partial.currentPrice * ladder.upperPercent) / 100;
37166
+ const lowerStep = (partial.currentPrice * ladder.lowerPercent) / 100;
37167
+ return currentPrice >= partial.currentPrice - lowerStep && currentPrice <= partial.currentPrice + upperStep;
37168
+ });
37169
+ };
36759
37170
  /**
36760
37171
  * Stops the strategy from generating new signals.
36761
37172
  *
@@ -37338,6 +37749,7 @@ class LiveUtils {
37338
37749
  percentShift,
37339
37750
  currentPrice,
37340
37751
  newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
37752
+ takeProfitPrice: signal.priceTakeProfit,
37341
37753
  position: signal.position,
37342
37754
  context,
37343
37755
  backtest: false,
@@ -37437,6 +37849,7 @@ class LiveUtils {
37437
37849
  percentShift,
37438
37850
  currentPrice,
37439
37851
  newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
37852
+ takeProfitPrice: signal.priceTakeProfit,
37440
37853
  position: signal.position,
37441
37854
  context,
37442
37855
  backtest: false,
@@ -37506,6 +37919,7 @@ class LiveUtils {
37506
37919
  percentShift,
37507
37920
  currentPrice,
37508
37921
  newStopLossPrice,
37922
+ takeProfitPrice: signal.priceTakeProfit,
37509
37923
  position: signal.position,
37510
37924
  context,
37511
37925
  backtest: false,
@@ -37575,6 +37989,7 @@ class LiveUtils {
37575
37989
  percentShift,
37576
37990
  currentPrice,
37577
37991
  newTakeProfitPrice,
37992
+ takeProfitPrice: signal.priceTakeProfit,
37578
37993
  position: signal.position,
37579
37994
  context,
37580
37995
  backtest: false,
@@ -45575,9 +45990,11 @@ exports.getNextCandles = getNextCandles;
45575
45990
  exports.getOrderBook = getOrderBook;
45576
45991
  exports.getPendingSignal = getPendingSignal;
45577
45992
  exports.getPositionAveragePrice = getPositionAveragePrice;
45993
+ exports.getPositionEntryOverlap = getPositionEntryOverlap;
45578
45994
  exports.getPositionInvestedCost = getPositionInvestedCost;
45579
45995
  exports.getPositionInvestedCount = getPositionInvestedCount;
45580
45996
  exports.getPositionLevels = getPositionLevels;
45997
+ exports.getPositionPartialOverlap = getPositionPartialOverlap;
45581
45998
  exports.getPositionPartials = getPositionPartials;
45582
45999
  exports.getPositionPnlCost = getPositionPnlCost;
45583
46000
  exports.getPositionPnlPercent = getPositionPnlPercent;