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.mjs CHANGED
@@ -450,9 +450,17 @@ const GLOBAL_CONFIG = {
450
450
  * Allows to commitAverageBuy if currentPrice is not the lowest price since entry, but still lower than priceOpen.
451
451
  * 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.
452
452
  *
453
- * Default: true (DCA logic enabled everywhere, not just when antirecord is broken)
453
+ * Default: false (DCA logic enabled only when antirecord is broken)
454
454
  */
455
455
  CC_ENABLE_DCA_EVERYWHERE: false,
456
+ /**
457
+ * Enables PPPL (Partial Profit, Partial Loss) logic even if this breaks a direction of exits
458
+ * Allows to take partial profit or loss on a position even if it results in a mix of profit and loss exits
459
+ * This can help lock in profits or cut losses on part of the position without waiting for a perfect exit scenario.
460
+ *
461
+ * Default: false (PPPL logic is only applied when it does not break the direction of exits, ensuring clearer profit/loss outcomes)
462
+ */
463
+ CC_ENABLE_PPPL_EVERYWHERE: false,
456
464
  /**
457
465
  * Cost of entering a position (in USD).
458
466
  * This is used as a default value for calculating position size and risk management when cost data is not provided by the strategy
@@ -7428,10 +7436,12 @@ class ClientStrategy {
7428
7436
  if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
7429
7437
  return false;
7430
7438
  const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7431
- if (this._pendingSignal.position === "long" && currentPrice <= effectivePriceOpen)
7432
- return false;
7433
- if (this._pendingSignal.position === "short" && currentPrice >= effectivePriceOpen)
7434
- return false;
7439
+ if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
7440
+ if (this._pendingSignal.position === "long" && currentPrice <= effectivePriceOpen)
7441
+ return false;
7442
+ if (this._pendingSignal.position === "short" && currentPrice >= effectivePriceOpen)
7443
+ return false;
7444
+ }
7435
7445
  const effectiveTakeProfit = this._pendingSignal._trailingPriceTakeProfit ?? this._pendingSignal.priceTakeProfit;
7436
7446
  if (this._pendingSignal.position === "long" && currentPrice >= effectiveTakeProfit)
7437
7447
  return false;
@@ -7515,7 +7525,7 @@ class ClientStrategy {
7515
7525
  throw new Error(`ClientStrategy partialProfit: currentPrice must be a positive finite number, got ${currentPrice}`);
7516
7526
  }
7517
7527
  // Validation: currentPrice must be moving toward TP (profit direction)
7518
- {
7528
+ if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
7519
7529
  const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7520
7530
  if (this._pendingSignal.position === "long") {
7521
7531
  // For LONG: currentPrice must be higher than effectivePriceOpen (moving toward TP)
@@ -7609,10 +7619,12 @@ class ClientStrategy {
7609
7619
  if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
7610
7620
  return false;
7611
7621
  const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7612
- if (this._pendingSignal.position === "long" && currentPrice >= effectivePriceOpen)
7613
- return false;
7614
- if (this._pendingSignal.position === "short" && currentPrice <= effectivePriceOpen)
7615
- return false;
7622
+ if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
7623
+ if (this._pendingSignal.position === "long" && currentPrice >= effectivePriceOpen)
7624
+ return false;
7625
+ if (this._pendingSignal.position === "short" && currentPrice <= effectivePriceOpen)
7626
+ return false;
7627
+ }
7616
7628
  const effectiveStopLoss = this._pendingSignal._trailingPriceStopLoss ?? this._pendingSignal.priceStopLoss;
7617
7629
  if (this._pendingSignal.position === "long" && currentPrice <= effectiveStopLoss)
7618
7630
  return false;
@@ -7696,7 +7708,7 @@ class ClientStrategy {
7696
7708
  throw new Error(`ClientStrategy partialLoss: currentPrice must be a positive finite number, got ${currentPrice}`);
7697
7709
  }
7698
7710
  // Validation: currentPrice must be moving toward SL (loss direction)
7699
- {
7711
+ if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
7700
7712
  const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7701
7713
  if (this._pendingSignal.position === "long") {
7702
7714
  // For LONG: currentPrice must be lower than effectivePriceOpen (moving toward SL)
@@ -10844,7 +10856,6 @@ const METHOD_NAME_PARTIAL_LOSS_AVAILABLE = "ActionBase.partialLossAvailable";
10844
10856
  const METHOD_NAME_PING_SCHEDULED = "ActionBase.pingScheduled";
10845
10857
  const METHOD_NAME_PING_ACTIVE = "ActionBase.pingActive";
10846
10858
  const METHOD_NAME_RISK_REJECTION = "ActionBase.riskRejection";
10847
- const METHOD_NAME_SIGNAL_SYNC = "ActionBase.signalSync";
10848
10859
  const METHOD_NAME_DISPOSE = "ActionBase.dispose";
10849
10860
  const DEFAULT_SOURCE = "default";
10850
10861
  /**
@@ -11256,6 +11267,11 @@ class ActionProxy {
11256
11267
  */
11257
11268
  async signalSync(event) {
11258
11269
  if (this._target.signalSync) {
11270
+ console.error("Action::signalSync is unwanted cause exchange integration should be implemented in Broker.useBrokerAdapter as an infrastructure domain layer");
11271
+ console.error("If you need to implement custom logic on signal open/close, please use signal(), signalBacktest(), signalLive()");
11272
+ console.error("If Action::signalSync throws the exchange will not execute the order!");
11273
+ console.error("");
11274
+ console.error("You have been warned!");
11259
11275
  await this._target.signalSync(event);
11260
11276
  }
11261
11277
  }
@@ -11718,19 +11734,6 @@ class ActionBase {
11718
11734
  source,
11719
11735
  });
11720
11736
  }
11721
- /**
11722
- * Gate for position open/close via limit order. Default allows all.
11723
- * Throw to reject — framework retries next tick.
11724
- *
11725
- * NOTE: Exceptions are NOT swallowed — they propagate to CREATE_SYNC_FN.
11726
- *
11727
- * @param event - Sync event with action "signal-open" or "signal-close"
11728
- */
11729
- signalSync(_event, source = DEFAULT_SOURCE) {
11730
- bt.loggerService.info(METHOD_NAME_SIGNAL_SYNC, {
11731
- source,
11732
- });
11733
- }
11734
11737
  /**
11735
11738
  * Cleans up resources and subscriptions when action handler is disposed.
11736
11739
  *
@@ -31761,6 +31764,11 @@ BrokerBase = makeExtendable(BrokerBase);
31761
31764
  */
31762
31765
  const Broker = new BrokerAdapter();
31763
31766
 
31767
+ const POSITION_OVERLAP_LADDER_DEFAULT = {
31768
+ upperPercent: 1.5,
31769
+ lowerPercent: 1.5,
31770
+ };
31771
+
31764
31772
  const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
31765
31773
  const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
31766
31774
  const PARTIAL_PROFIT_METHOD_NAME = "strategy.commitPartialProfit";
@@ -31786,6 +31794,8 @@ const GET_POSITION_PNL_PERCENT_METHOD_NAME = "strategy.getPositionPnlPercent";
31786
31794
  const GET_POSITION_PNL_COST_METHOD_NAME = "strategy.getPositionPnlCost";
31787
31795
  const GET_POSITION_LEVELS_METHOD_NAME = "strategy.getPositionLevels";
31788
31796
  const GET_POSITION_PARTIALS_METHOD_NAME = "strategy.getPositionPartials";
31797
+ const GET_POSITION_ENTRY_OVERLAP_METHOD_NAME = "strategy.getPositionEntryOverlap";
31798
+ const GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME = "strategy.getPositionPartialOverlap";
31789
31799
  /**
31790
31800
  * Cancels the scheduled signal without stopping the strategy.
31791
31801
  *
@@ -32062,6 +32072,7 @@ async function commitTrailingStop(symbol, percentShift, currentPrice) {
32062
32072
  percentShift,
32063
32073
  currentPrice,
32064
32074
  newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
32075
+ takeProfitPrice: signal.priceTakeProfit,
32065
32076
  position: signal.position,
32066
32077
  context: { exchangeName, frameName, strategyName },
32067
32078
  backtest: isBacktest,
@@ -32141,6 +32152,7 @@ async function commitTrailingTake(symbol, percentShift, currentPrice) {
32141
32152
  percentShift,
32142
32153
  currentPrice,
32143
32154
  newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
32155
+ takeProfitPrice: signal.priceTakeProfit,
32144
32156
  position: signal.position,
32145
32157
  context: { exchangeName, frameName, strategyName },
32146
32158
  backtest: isBacktest,
@@ -32192,6 +32204,7 @@ async function commitTrailingStopCost(symbol, newStopLossPrice) {
32192
32204
  currentPrice,
32193
32205
  newStopLossPrice,
32194
32206
  position: signal.position,
32207
+ takeProfitPrice: signal.priceTakeProfit,
32195
32208
  context: { exchangeName, frameName, strategyName },
32196
32209
  backtest: isBacktest,
32197
32210
  });
@@ -32241,6 +32254,7 @@ async function commitTrailingTakeCost(symbol, newTakeProfitPrice) {
32241
32254
  percentShift,
32242
32255
  currentPrice,
32243
32256
  newTakeProfitPrice,
32257
+ takeProfitPrice: signal.priceTakeProfit,
32244
32258
  position: signal.position,
32245
32259
  context: { exchangeName, frameName, strategyName },
32246
32260
  backtest: isBacktest,
@@ -32567,6 +32581,31 @@ async function getBreakeven(symbol, currentPrice) {
32567
32581
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32568
32582
  return await bt.strategyCoreService.getBreakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32569
32583
  }
32584
+ /**
32585
+ * Returns the effective (DCA-weighted) entry price for the current pending signal.
32586
+ *
32587
+ * Uses cost-weighted harmonic mean: Σcost / Σ(cost/price).
32588
+ * When partial closes exist, the price is computed iteratively using
32589
+ * costBasisAtClose snapshots from each partial, then blended with any
32590
+ * DCA entries added after the last partial.
32591
+ * With no DCA entries, equals the original priceOpen.
32592
+ *
32593
+ * Returns null if no pending signal exists.
32594
+ *
32595
+ * Automatically detects backtest/live mode from execution context.
32596
+ *
32597
+ * @param symbol - Trading pair symbol
32598
+ * @returns Promise resolving to effective entry price or null
32599
+ *
32600
+ * @example
32601
+ * ```typescript
32602
+ * import { getPositionAveragePrice } from "backtest-kit";
32603
+ *
32604
+ * const avgPrice = await getPositionAveragePrice("BTCUSDT");
32605
+ * // No DCA: avgPrice === priceOpen
32606
+ * // After DCA at lower price: avgPrice < priceOpen
32607
+ * ```
32608
+ */
32570
32609
  async function getPositionAveragePrice(symbol) {
32571
32610
  bt.loggerService.info(GET_POSITION_AVERAGE_PRICE_METHOD_NAME, {
32572
32611
  symbol,
@@ -32581,6 +32620,28 @@ async function getPositionAveragePrice(symbol) {
32581
32620
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32582
32621
  return await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
32583
32622
  }
32623
+ /**
32624
+ * Returns the number of DCA entries made for the current pending signal.
32625
+ *
32626
+ * 1 = original entry only (no DCA).
32627
+ * Increases by 1 with each successful commitAverageBuy().
32628
+ *
32629
+ * Returns null if no pending signal exists.
32630
+ *
32631
+ * Automatically detects backtest/live mode from execution context.
32632
+ *
32633
+ * @param symbol - Trading pair symbol
32634
+ * @returns Promise resolving to entry count or null
32635
+ *
32636
+ * @example
32637
+ * ```typescript
32638
+ * import { getPositionInvestedCount } from "backtest-kit";
32639
+ *
32640
+ * const count = await getPositionInvestedCount("BTCUSDT");
32641
+ * // No DCA: count === 1
32642
+ * // After one DCA: count === 2
32643
+ * ```
32644
+ */
32584
32645
  async function getPositionInvestedCount(symbol) {
32585
32646
  bt.loggerService.info(GET_POSITION_INVESTED_COUNT_METHOD_NAME, {
32586
32647
  symbol,
@@ -32595,6 +32656,28 @@ async function getPositionInvestedCount(symbol) {
32595
32656
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32596
32657
  return await bt.strategyCoreService.getPositionInvestedCount(isBacktest, symbol, { exchangeName, frameName, strategyName });
32597
32658
  }
32659
+ /**
32660
+ * Returns the total invested cost basis in dollars for the current pending signal.
32661
+ *
32662
+ * Equal to the sum of all _entry costs (Σ entry.cost).
32663
+ * Each entry cost is set at the time of commitAverageBuy (defaults to CC_POSITION_ENTRY_COST).
32664
+ *
32665
+ * Returns null if no pending signal exists.
32666
+ *
32667
+ * Automatically detects backtest/live mode from execution context.
32668
+ *
32669
+ * @param symbol - Trading pair symbol
32670
+ * @returns Promise resolving to total invested cost in dollars or null
32671
+ *
32672
+ * @example
32673
+ * ```typescript
32674
+ * import { getPositionInvestedCost } from "backtest-kit";
32675
+ *
32676
+ * const cost = await getPositionInvestedCost("BTCUSDT");
32677
+ * // No DCA, default cost: cost === 100
32678
+ * // After one DCA with default cost: cost === 200
32679
+ * ```
32680
+ */
32598
32681
  async function getPositionInvestedCost(symbol) {
32599
32682
  bt.loggerService.info(GET_POSITION_INVESTED_COST_METHOD_NAME, {
32600
32683
  symbol,
@@ -32609,6 +32692,29 @@ async function getPositionInvestedCost(symbol) {
32609
32692
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32610
32693
  return await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
32611
32694
  }
32695
+ /**
32696
+ * Returns the unrealized PNL percentage for the current pending signal at current market price.
32697
+ *
32698
+ * Accounts for partial closes, DCA entries, slippage and fees
32699
+ * (delegates to toProfitLossDto).
32700
+ *
32701
+ * Returns null if no pending signal exists.
32702
+ *
32703
+ * Automatically detects backtest/live mode from execution context.
32704
+ * Automatically fetches current price via getAveragePrice.
32705
+ *
32706
+ * @param symbol - Trading pair symbol
32707
+ * @returns Promise resolving to PNL percentage or null
32708
+ *
32709
+ * @example
32710
+ * ```typescript
32711
+ * import { getPositionPnlPercent } from "backtest-kit";
32712
+ *
32713
+ * const pnlPct = await getPositionPnlPercent("BTCUSDT");
32714
+ * // LONG at 100, current=105: pnlPct ≈ 5
32715
+ * // LONG at 100, current=95: pnlPct ≈ -5
32716
+ * ```
32717
+ */
32612
32718
  async function getPositionPnlPercent(symbol) {
32613
32719
  bt.loggerService.info(GET_POSITION_PNL_PERCENT_METHOD_NAME, { symbol });
32614
32720
  if (!ExecutionContextService.hasContext()) {
@@ -32758,6 +32864,29 @@ async function commitPartialLossCost(symbol, dollarAmount) {
32758
32864
  });
32759
32865
  return await bt.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
32760
32866
  }
32867
+ /**
32868
+ * Returns the unrealized PNL in dollars for the current pending signal at current market price.
32869
+ *
32870
+ * Calculated as: pnlPercentage / 100 × totalInvestedCost.
32871
+ * Accounts for partial closes, DCA entries, slippage and fees.
32872
+ *
32873
+ * Returns null if no pending signal exists.
32874
+ *
32875
+ * Automatically detects backtest/live mode from execution context.
32876
+ * Automatically fetches current price via getAveragePrice.
32877
+ *
32878
+ * @param symbol - Trading pair symbol
32879
+ * @returns Promise resolving to PNL in dollars or null
32880
+ *
32881
+ * @example
32882
+ * ```typescript
32883
+ * import { getPositionPnlCost } from "backtest-kit";
32884
+ *
32885
+ * const pnlCost = await getPositionPnlCost("BTCUSDT");
32886
+ * // LONG at 100, invested $100, current=105: pnlCost ≈ 5
32887
+ * // LONG at 100, invested $200 (DCA), current=95: pnlCost ≈ -10
32888
+ * ```
32889
+ */
32761
32890
  async function getPositionPnlCost(symbol) {
32762
32891
  bt.loggerService.info(GET_POSITION_PNL_COST_METHOD_NAME, { symbol });
32763
32892
  if (!ExecutionContextService.hasContext()) {
@@ -32844,6 +32973,104 @@ async function getPositionPartials(symbol) {
32844
32973
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32845
32974
  return await bt.strategyCoreService.getPositionPartials(isBacktest, symbol, { exchangeName, frameName, strategyName });
32846
32975
  }
32976
+ /**
32977
+ * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
32978
+ * Use this to prevent duplicate DCA entries at the same price area.
32979
+ *
32980
+ * Returns true if currentPrice is within [level - lowerStep, level + upperStep] for any level,
32981
+ * where step = level * percent / 100.
32982
+ * Returns false if no pending signal exists.
32983
+ *
32984
+ * @param symbol - Trading pair symbol
32985
+ * @param currentPrice - Price to check against existing DCA levels
32986
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
32987
+ * @returns Promise<boolean> - true if price overlaps an existing entry level (DCA not recommended)
32988
+ *
32989
+ * @example
32990
+ * ```typescript
32991
+ * import { getPositionEntryOverlap } from "backtest-kit";
32992
+ *
32993
+ * // LONG with levels [43000, 42000], check if 42100 is too close to 42000
32994
+ * const overlap = await getPositionEntryOverlap("BTCUSDT", 42100, { upperPercent: 5, lowerPercent: 5 });
32995
+ * // overlap = true (42100 is within 5% of 42000 = [39900, 44100])
32996
+ * if (!overlap) {
32997
+ * await commitAverageBuy("BTCUSDT");
32998
+ * }
32999
+ * ```
33000
+ */
33001
+ async function getPositionEntryOverlap(symbol, currentPrice, ladder = POSITION_OVERLAP_LADDER_DEFAULT) {
33002
+ bt.loggerService.info(GET_POSITION_ENTRY_OVERLAP_METHOD_NAME, {
33003
+ symbol,
33004
+ currentPrice,
33005
+ ladder,
33006
+ });
33007
+ if (!ExecutionContextService.hasContext()) {
33008
+ throw new Error("getPositionEntryOverlap requires an execution context");
33009
+ }
33010
+ if (!MethodContextService.hasContext()) {
33011
+ throw new Error("getPositionEntryOverlap requires a method context");
33012
+ }
33013
+ const { backtest: isBacktest } = bt.executionContextService.context;
33014
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
33015
+ const levels = await bt.strategyCoreService.getPositionLevels(isBacktest, symbol, { exchangeName, frameName, strategyName });
33016
+ if (!levels) {
33017
+ return false;
33018
+ }
33019
+ return levels.some((level) => {
33020
+ const upperStep = (level * ladder.upperPercent) / 100;
33021
+ const lowerStep = (level * ladder.lowerPercent) / 100;
33022
+ return currentPrice >= level - lowerStep && currentPrice <= level + upperStep;
33023
+ });
33024
+ }
33025
+ /**
33026
+ * Checks whether the current price falls within the tolerance zone of any existing partial close price.
33027
+ * Use this to prevent duplicate partial closes at the same price area.
33028
+ *
33029
+ * Returns true if currentPrice is within [partial.currentPrice - lowerStep, partial.currentPrice + upperStep]
33030
+ * for any partial, where step = partial.currentPrice * percent / 100.
33031
+ * Returns false if no pending signal exists or no partials have been executed yet.
33032
+ *
33033
+ * @param symbol - Trading pair symbol
33034
+ * @param currentPrice - Price to check against existing partial close prices
33035
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
33036
+ * @returns Promise<boolean> - true if price overlaps an existing partial price (partial not recommended)
33037
+ *
33038
+ * @example
33039
+ * ```typescript
33040
+ * import { getPositionPartialOverlap } from "backtest-kit";
33041
+ *
33042
+ * // Partials at [45000], check if 45100 is too close
33043
+ * const overlap = await getPositionPartialOverlap("BTCUSDT", 45100, { upperPercent: 1.5, lowerPercent: 1.5 });
33044
+ * // overlap = true (45100 is within 1.5% of 45000)
33045
+ * if (!overlap) {
33046
+ * await commitPartialProfit("BTCUSDT", 50);
33047
+ * }
33048
+ * ```
33049
+ */
33050
+ async function getPositionPartialOverlap(symbol, currentPrice, ladder = POSITION_OVERLAP_LADDER_DEFAULT) {
33051
+ bt.loggerService.info(GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME, {
33052
+ symbol,
33053
+ currentPrice,
33054
+ ladder,
33055
+ });
33056
+ if (!ExecutionContextService.hasContext()) {
33057
+ throw new Error("getPositionPartialOverlap requires an execution context");
33058
+ }
33059
+ if (!MethodContextService.hasContext()) {
33060
+ throw new Error("getPositionPartialOverlap requires a method context");
33061
+ }
33062
+ const { backtest: isBacktest } = bt.executionContextService.context;
33063
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
33064
+ const partials = await bt.strategyCoreService.getPositionPartials(isBacktest, symbol, { exchangeName, frameName, strategyName });
33065
+ if (!partials) {
33066
+ return false;
33067
+ }
33068
+ return partials.some((partial) => {
33069
+ const upperStep = (partial.currentPrice * ladder.upperPercent) / 100;
33070
+ const lowerStep = (partial.currentPrice * ladder.lowerPercent) / 100;
33071
+ return currentPrice >= partial.currentPrice - lowerStep && currentPrice <= partial.currentPrice + upperStep;
33072
+ });
33073
+ }
32847
33074
 
32848
33075
  const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
32849
33076
  /**
@@ -34070,6 +34297,8 @@ const BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST = "BacktestUtils.getPositionPnl
34070
34297
  const BACKTEST_METHOD_NAME_GET_POSITION_LEVELS = "BacktestUtils.getPositionLevels";
34071
34298
  const BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS = "BacktestUtils.getPositionPartials";
34072
34299
  const BACKTEST_METHOD_NAME_GET_POSITION_ENTRIES = "BacktestUtils.getPositionEntries";
34300
+ const BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "BacktestUtils.getPositionEntryOverlap";
34301
+ const BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "BacktestUtils.getPositionPartialOverlap";
34073
34302
  const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
34074
34303
  const BACKTEST_METHOD_NAME_CANCEL_SCHEDULED = "Backtest.commitCancelScheduled";
34075
34304
  const BACKTEST_METHOD_NAME_CLOSE_PENDING = "Backtest.commitClosePending";
@@ -34860,6 +35089,90 @@ class BacktestUtils {
34860
35089
  }
34861
35090
  return await bt.strategyCoreService.getPositionEntries(true, symbol, context);
34862
35091
  };
35092
+ /**
35093
+ * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
35094
+ * Use this to prevent duplicate DCA entries at the same price area.
35095
+ *
35096
+ * Returns true if currentPrice is within [level - lowerStep, level + upperStep] for any level,
35097
+ * where step = level * percent / 100.
35098
+ * Returns false if no pending signal exists.
35099
+ *
35100
+ * @param symbol - Trading pair symbol
35101
+ * @param currentPrice - Price to check against existing DCA levels
35102
+ * @param context - Execution context with strategyName, exchangeName, and frameName
35103
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
35104
+ * @returns true if price overlaps an existing entry level (DCA not recommended)
35105
+ */
35106
+ this.getPositionEntryOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
35107
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP, {
35108
+ symbol,
35109
+ currentPrice,
35110
+ context,
35111
+ ladder,
35112
+ });
35113
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
35114
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
35115
+ {
35116
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
35117
+ riskName &&
35118
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
35119
+ riskList &&
35120
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
35121
+ actions &&
35122
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
35123
+ }
35124
+ const levels = await bt.strategyCoreService.getPositionLevels(true, symbol, context);
35125
+ if (!levels) {
35126
+ return false;
35127
+ }
35128
+ return levels.some((level) => {
35129
+ const upperStep = (level * ladder.upperPercent) / 100;
35130
+ const lowerStep = (level * ladder.lowerPercent) / 100;
35131
+ return currentPrice >= level - lowerStep && currentPrice <= level + upperStep;
35132
+ });
35133
+ };
35134
+ /**
35135
+ * Checks whether the current price falls within the tolerance zone of any existing partial close price.
35136
+ * Use this to prevent duplicate partial closes at the same price area.
35137
+ *
35138
+ * Returns true if currentPrice is within [partial.currentPrice - lowerStep, partial.currentPrice + upperStep]
35139
+ * for any partial, where step = partial.currentPrice * percent / 100.
35140
+ * Returns false if no pending signal exists or no partials have been executed yet.
35141
+ *
35142
+ * @param symbol - Trading pair symbol
35143
+ * @param currentPrice - Price to check against existing partial close prices
35144
+ * @param context - Execution context with strategyName, exchangeName, and frameName
35145
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
35146
+ * @returns true if price overlaps an existing partial price (partial not recommended)
35147
+ */
35148
+ this.getPositionPartialOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
35149
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP, {
35150
+ symbol,
35151
+ currentPrice,
35152
+ context,
35153
+ ladder,
35154
+ });
35155
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
35156
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
35157
+ {
35158
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
35159
+ riskName &&
35160
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
35161
+ riskList &&
35162
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
35163
+ actions &&
35164
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
35165
+ }
35166
+ const partials = await bt.strategyCoreService.getPositionPartials(true, symbol, context);
35167
+ if (!partials) {
35168
+ return false;
35169
+ }
35170
+ return partials.some((partial) => {
35171
+ const upperStep = (partial.currentPrice * ladder.upperPercent) / 100;
35172
+ const lowerStep = (partial.currentPrice * ladder.lowerPercent) / 100;
35173
+ return currentPrice >= partial.currentPrice - lowerStep && currentPrice <= partial.currentPrice + upperStep;
35174
+ });
35175
+ };
34863
35176
  /**
34864
35177
  * Stops the strategy from generating new signals.
34865
35178
  *
@@ -35349,6 +35662,7 @@ class BacktestUtils {
35349
35662
  percentShift,
35350
35663
  currentPrice,
35351
35664
  newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
35665
+ takeProfitPrice: signal.priceTakeProfit,
35352
35666
  position: signal.position,
35353
35667
  context,
35354
35668
  backtest: true,
@@ -35433,6 +35747,7 @@ class BacktestUtils {
35433
35747
  percentShift,
35434
35748
  currentPrice,
35435
35749
  newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
35750
+ takeProfitPrice: signal.priceTakeProfit,
35436
35751
  position: signal.position,
35437
35752
  context,
35438
35753
  backtest: true,
@@ -35487,6 +35802,7 @@ class BacktestUtils {
35487
35802
  currentPrice,
35488
35803
  newStopLossPrice,
35489
35804
  position: signal.position,
35805
+ takeProfitPrice: signal.priceTakeProfit,
35490
35806
  context,
35491
35807
  backtest: true,
35492
35808
  });
@@ -35540,6 +35856,7 @@ class BacktestUtils {
35540
35856
  currentPrice,
35541
35857
  newTakeProfitPrice,
35542
35858
  position: signal.position,
35859
+ takeProfitPrice: signal.priceTakeProfit,
35543
35860
  context,
35544
35861
  backtest: true,
35545
35862
  });
@@ -35883,6 +36200,8 @@ const LIVE_METHOD_NAME_GET_POSITION_PNL_COST = "LiveUtils.getPositionPnlCost";
35883
36200
  const LIVE_METHOD_NAME_GET_POSITION_LEVELS = "LiveUtils.getPositionLevels";
35884
36201
  const LIVE_METHOD_NAME_GET_POSITION_PARTIALS = "LiveUtils.getPositionPartials";
35885
36202
  const LIVE_METHOD_NAME_GET_POSITION_ENTRIES = "LiveUtils.getPositionEntries";
36203
+ const LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "LiveUtils.getPositionEntryOverlap";
36204
+ const LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "LiveUtils.getPositionPartialOverlap";
35886
36205
  const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
35887
36206
  const LIVE_METHOD_NAME_CANCEL_SCHEDULED = "Live.cancelScheduled";
35888
36207
  const LIVE_METHOD_NAME_CLOSE_PENDING = "Live.closePending";
@@ -36736,6 +37055,98 @@ class LiveUtils {
36736
37055
  frameName: "",
36737
37056
  });
36738
37057
  };
37058
+ /**
37059
+ * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
37060
+ * Use this to prevent duplicate DCA entries at the same price area.
37061
+ *
37062
+ * Returns true if currentPrice is within [level - lowerStep, level + upperStep] for any level,
37063
+ * where step = level * percent / 100.
37064
+ * Returns false if no pending signal exists.
37065
+ *
37066
+ * @param symbol - Trading pair symbol
37067
+ * @param currentPrice - Price to check against existing DCA levels
37068
+ * @param context - Execution context with strategyName and exchangeName
37069
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
37070
+ * @returns true if price overlaps an existing entry level (DCA not recommended)
37071
+ */
37072
+ this.getPositionEntryOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
37073
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP, {
37074
+ symbol,
37075
+ currentPrice,
37076
+ context,
37077
+ ladder,
37078
+ });
37079
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
37080
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
37081
+ {
37082
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
37083
+ riskName &&
37084
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
37085
+ riskList &&
37086
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
37087
+ actions &&
37088
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
37089
+ }
37090
+ const levels = await bt.strategyCoreService.getPositionLevels(false, symbol, {
37091
+ strategyName: context.strategyName,
37092
+ exchangeName: context.exchangeName,
37093
+ frameName: "",
37094
+ });
37095
+ if (!levels) {
37096
+ return false;
37097
+ }
37098
+ return levels.some((level) => {
37099
+ const upperStep = (level * ladder.upperPercent) / 100;
37100
+ const lowerStep = (level * ladder.lowerPercent) / 100;
37101
+ return currentPrice >= level - lowerStep && currentPrice <= level + upperStep;
37102
+ });
37103
+ };
37104
+ /**
37105
+ * Checks whether the current price falls within the tolerance zone of any existing partial close price.
37106
+ * Use this to prevent duplicate partial closes at the same price area.
37107
+ *
37108
+ * Returns true if currentPrice is within [partial.currentPrice - lowerStep, partial.currentPrice + upperStep]
37109
+ * for any partial, where step = partial.currentPrice * percent / 100.
37110
+ * Returns false if no pending signal exists or no partials have been executed yet.
37111
+ *
37112
+ * @param symbol - Trading pair symbol
37113
+ * @param currentPrice - Price to check against existing partial close prices
37114
+ * @param context - Execution context with strategyName and exchangeName
37115
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
37116
+ * @returns true if price overlaps an existing partial price (partial not recommended)
37117
+ */
37118
+ this.getPositionPartialOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
37119
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP, {
37120
+ symbol,
37121
+ currentPrice,
37122
+ context,
37123
+ ladder,
37124
+ });
37125
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
37126
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
37127
+ {
37128
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
37129
+ riskName &&
37130
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
37131
+ riskList &&
37132
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
37133
+ actions &&
37134
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
37135
+ }
37136
+ const partials = await bt.strategyCoreService.getPositionPartials(false, symbol, {
37137
+ strategyName: context.strategyName,
37138
+ exchangeName: context.exchangeName,
37139
+ frameName: "",
37140
+ });
37141
+ if (!partials) {
37142
+ return false;
37143
+ }
37144
+ return partials.some((partial) => {
37145
+ const upperStep = (partial.currentPrice * ladder.upperPercent) / 100;
37146
+ const lowerStep = (partial.currentPrice * ladder.lowerPercent) / 100;
37147
+ return currentPrice >= partial.currentPrice - lowerStep && currentPrice <= partial.currentPrice + upperStep;
37148
+ });
37149
+ };
36739
37150
  /**
36740
37151
  * Stops the strategy from generating new signals.
36741
37152
  *
@@ -37318,6 +37729,7 @@ class LiveUtils {
37318
37729
  percentShift,
37319
37730
  currentPrice,
37320
37731
  newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
37732
+ takeProfitPrice: signal.priceTakeProfit,
37321
37733
  position: signal.position,
37322
37734
  context,
37323
37735
  backtest: false,
@@ -37417,6 +37829,7 @@ class LiveUtils {
37417
37829
  percentShift,
37418
37830
  currentPrice,
37419
37831
  newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
37832
+ takeProfitPrice: signal.priceTakeProfit,
37420
37833
  position: signal.position,
37421
37834
  context,
37422
37835
  backtest: false,
@@ -37486,6 +37899,7 @@ class LiveUtils {
37486
37899
  percentShift,
37487
37900
  currentPrice,
37488
37901
  newStopLossPrice,
37902
+ takeProfitPrice: signal.priceTakeProfit,
37489
37903
  position: signal.position,
37490
37904
  context,
37491
37905
  backtest: false,
@@ -37555,6 +37969,7 @@ class LiveUtils {
37555
37969
  percentShift,
37556
37970
  currentPrice,
37557
37971
  newTakeProfitPrice,
37972
+ takeProfitPrice: signal.priceTakeProfit,
37558
37973
  position: signal.position,
37559
37974
  context,
37560
37975
  backtest: false,
@@ -45465,4 +45880,4 @@ const percentValue = (yesterdayValue, todayValue) => {
45465
45880
  return yesterdayValue / todayValue - 1;
45466
45881
  };
45467
45882
 
45468
- export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Log, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Sync, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, dumpMessages, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionAveragePrice, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getRawCandles, getRiskSchema, getScheduledSignal, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasTradeContext, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, roundTicks, set, setColumns, setConfig, setLogger, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, waitForCandle, warmCandles };
45883
+ export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Log, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Sync, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, dumpMessages, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionAveragePrice, getPositionEntryOverlap, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getRawCandles, getRiskSchema, getScheduledSignal, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasTradeContext, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, roundTicks, set, setColumns, setConfig, setLogger, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, waitForCandle, warmCandles };