backtest-kit 3.1.1 → 3.3.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
@@ -2900,6 +2900,27 @@ class ExchangeConnectionService {
2900
2900
  }
2901
2901
  }
2902
2902
 
2903
+ /**
2904
+ * Returns the effective entry price for price calculations.
2905
+ *
2906
+ * When the _entry array exists and has at least one element, returns
2907
+ * the simple arithmetic mean of all entry prices (DCA average).
2908
+ * Otherwise returns the original signal.priceOpen.
2909
+ *
2910
+ * This mirrors the _trailingPriceStopLoss pattern: original price is preserved
2911
+ * in signal.priceOpen (for identity/tracking), while calculations use the
2912
+ * effective averaged price returned by this function.
2913
+ *
2914
+ * @param signal - Signal row (ISignalRow or IScheduledSignalRow)
2915
+ * @returns Effective entry price for distance and PNL calculations
2916
+ */
2917
+ const getEffectivePriceOpen = (signal) => {
2918
+ if (signal._entry && signal._entry.length > 0) {
2919
+ return signal._entry.reduce((sum, e) => sum + e.price, 0) / signal._entry.length;
2920
+ }
2921
+ return signal.priceOpen;
2922
+ };
2923
+
2903
2924
  /**
2904
2925
  * Calculates profit/loss for a closed signal with slippage and fees.
2905
2926
  *
@@ -2907,7 +2928,7 @@ class ExchangeConnectionService {
2907
2928
  * - Calculates weighted PNL: Σ(percent_i × pnl_i) for each partial + (remaining% × final_pnl)
2908
2929
  * - Each partial close has its own slippage
2909
2930
  * - Open fee is charged once; close fees are proportional to each partial's size
2910
- * - Total fees = CC_PERCENT_FEE (open) + CC_PERCENT_FEE × 1 (closes sum to 100%) = 2 × CC_PERCENT_FEE
2931
+ * - Total fees = CC_PERCENT_FEE (open) + Σ CC_PERCENT_FEE × (partial% / 100) × (closeWithSlip / openWithSlip)
2911
2932
  *
2912
2933
  * Formula breakdown:
2913
2934
  * 1. Apply slippage to open/close prices (worse execution)
@@ -2916,7 +2937,7 @@ class ExchangeConnectionService {
2916
2937
  * 2. Calculate raw PNL percentage
2917
2938
  * - LONG: ((closePrice - openPrice) / openPrice) * 100
2918
2939
  * - SHORT: ((openPrice - closePrice) / openPrice) * 100
2919
- * 3. Subtract total fees (0.1% * 2 = 0.2% per transaction)
2940
+ * 3. Subtract total fees: open fee + close fee adjusted for slippage-affected execution price
2920
2941
  *
2921
2942
  * @param signal - Closed signal with position details and optional partial history
2922
2943
  * @param priceClose - Actual close price at final exit
@@ -2950,73 +2971,53 @@ class ExchangeConnectionService {
2950
2971
  * ```
2951
2972
  */
2952
2973
  const toProfitLossDto = (signal, priceClose) => {
2953
- const priceOpen = signal.priceOpen;
2974
+ const priceOpen = getEffectivePriceOpen(signal);
2954
2975
  // Calculate weighted PNL with partial closes
2955
2976
  if (signal._partial && signal._partial.length > 0) {
2956
2977
  let totalWeightedPnl = 0;
2957
2978
  // Open fee is paid once for the whole position
2958
2979
  let totalFees = GLOBAL_CONFIG.CC_PERCENT_FEE;
2980
+ // priceOpenWithSlippage is the same for all partials — compute once
2981
+ const priceOpenWithSlippage = signal.position === "long"
2982
+ ? priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
2983
+ : priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2959
2984
  // Calculate PNL for each partial close
2960
2985
  for (const partial of signal._partial) {
2961
2986
  const partialPercent = partial.percent;
2962
- const partialPrice = partial.price;
2963
- // Apply slippage to prices
2964
- let priceOpenWithSlippage;
2965
- let priceCloseWithSlippage;
2966
- if (signal.position === "long") {
2967
- priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2968
- priceCloseWithSlippage = partialPrice * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2969
- }
2970
- else {
2971
- priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2972
- priceCloseWithSlippage = partialPrice * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2973
- }
2987
+ const priceCloseWithSlippage = signal.position === "long"
2988
+ ? partial.price * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
2989
+ : partial.price * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2974
2990
  // Calculate PNL for this partial
2975
- let partialPnl;
2976
- if (signal.position === "long") {
2977
- partialPnl = ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
2978
- }
2979
- else {
2980
- partialPnl = ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
2981
- }
2991
+ const partialPnl = signal.position === "long"
2992
+ ? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
2993
+ : ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
2982
2994
  // Weight by percentage of position closed
2983
- const weightedPnl = (partialPercent / 100) * partialPnl;
2984
- totalWeightedPnl += weightedPnl;
2985
- // Close fee is proportional to the size of this partial
2986
- totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (partialPercent / 100);
2995
+ totalWeightedPnl += (partialPercent / 100) * partialPnl;
2996
+ // Close fee is proportional to the size of this partial and adjusted for slippage
2997
+ totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (partialPercent / 100) * (priceCloseWithSlippage / priceOpenWithSlippage);
2987
2998
  }
2988
2999
  // Calculate PNL for remaining position (if any)
2989
3000
  // Compute totalClosed from _partial array
2990
3001
  const totalClosed = signal._partial.reduce((sum, p) => sum + p.percent, 0);
3002
+ if (totalClosed > 100) {
3003
+ throw new Error(`Partial closes exceed 100%: ${totalClosed}% (signal id: ${signal.id})`);
3004
+ }
2991
3005
  const remainingPercent = 100 - totalClosed;
2992
3006
  if (remainingPercent > 0) {
2993
- // Apply slippage
2994
- let priceOpenWithSlippage;
2995
- let priceCloseWithSlippage;
2996
- if (signal.position === "long") {
2997
- priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2998
- priceCloseWithSlippage = priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2999
- }
3000
- else {
3001
- priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3002
- priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3003
- }
3007
+ const priceCloseWithSlippage = signal.position === "long"
3008
+ ? priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
3009
+ : priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3004
3010
  // Calculate PNL for remaining
3005
- let remainingPnl;
3006
- if (signal.position === "long") {
3007
- remainingPnl = ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
3008
- }
3009
- else {
3010
- remainingPnl = ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
3011
- }
3011
+ const remainingPnl = signal.position === "long"
3012
+ ? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
3013
+ : ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
3012
3014
  // Weight by remaining percentage
3013
- const weightedRemainingPnl = (remainingPercent / 100) * remainingPnl;
3014
- totalWeightedPnl += weightedRemainingPnl;
3015
- // Close fee is proportional to the remaining size
3016
- totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (remainingPercent / 100);
3015
+ totalWeightedPnl += (remainingPercent / 100) * remainingPnl;
3016
+ // Close fee is proportional to the remaining size and adjusted for slippage
3017
+ totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (remainingPercent / 100) * (priceCloseWithSlippage / priceOpenWithSlippage);
3017
3018
  }
3018
3019
  // Subtract total fees from weighted PNL
3019
- // totalFees = CC_PERCENT_FEE (open) + CC_PERCENT_FEE × 1 (all closes sum to 100%) = 2 × CC_PERCENT_FEE
3020
+ // totalFees = CC_PERCENT_FEE (open) + Σ CC_PERCENT_FEE × (partialPercent/100) × (closeWithSlip/openWithSlip)
3020
3021
  const pnlPercentage = totalWeightedPnl - totalFees;
3021
3022
  return {
3022
3023
  pnlPercentage,
@@ -3037,8 +3038,8 @@ const toProfitLossDto = (signal, priceClose) => {
3037
3038
  priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3038
3039
  priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3039
3040
  }
3040
- // Применяем комиссию дважды (при открытии и закрытии)
3041
- const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE * 2;
3041
+ // Открытие: комиссия от цены входа; закрытие: комиссия от фактической цены выхода (с учётом slippage)
3042
+ const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE * (1 + priceCloseWithSlippage / priceOpenWithSlippage);
3042
3043
  let pnlPercentage;
3043
3044
  if (signal.position === "long") {
3044
3045
  // LONG: прибыль при росте цены
@@ -3224,6 +3225,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3224
3225
  percentToClose: commit.percentToClose,
3225
3226
  currentPrice: commit.currentPrice,
3226
3227
  timestamp,
3228
+ totalEntries: publicSignal.totalEntries,
3227
3229
  position: publicSignal.position,
3228
3230
  priceOpen: publicSignal.priceOpen,
3229
3231
  signalId: publicSignal.id,
@@ -3231,6 +3233,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3231
3233
  priceStopLoss: publicSignal.priceStopLoss,
3232
3234
  originalPriceTakeProfit: publicSignal.originalPriceTakeProfit,
3233
3235
  originalPriceStopLoss: publicSignal.originalPriceStopLoss,
3236
+ originalPriceOpen: publicSignal.originalPriceOpen,
3234
3237
  scheduledAt: publicSignal.scheduledAt,
3235
3238
  pendingAt: publicSignal.pendingAt,
3236
3239
  });
@@ -3247,6 +3250,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3247
3250
  percentToClose: commit.percentToClose,
3248
3251
  currentPrice: commit.currentPrice,
3249
3252
  timestamp,
3253
+ totalEntries: publicSignal.totalEntries,
3250
3254
  position: publicSignal.position,
3251
3255
  priceOpen: publicSignal.priceOpen,
3252
3256
  signalId: publicSignal.id,
@@ -3254,6 +3258,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3254
3258
  priceStopLoss: publicSignal.priceStopLoss,
3255
3259
  originalPriceTakeProfit: publicSignal.originalPriceTakeProfit,
3256
3260
  originalPriceStopLoss: publicSignal.originalPriceStopLoss,
3261
+ originalPriceOpen: publicSignal.originalPriceOpen,
3257
3262
  scheduledAt: publicSignal.scheduledAt,
3258
3263
  pendingAt: publicSignal.pendingAt,
3259
3264
  });
@@ -3269,6 +3274,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3269
3274
  backtest: commit.backtest,
3270
3275
  currentPrice: commit.currentPrice,
3271
3276
  timestamp,
3277
+ totalEntries: publicSignal.totalEntries,
3272
3278
  signalId: publicSignal.id,
3273
3279
  position: publicSignal.position,
3274
3280
  priceOpen: publicSignal.priceOpen,
@@ -3276,6 +3282,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3276
3282
  priceStopLoss: publicSignal.priceStopLoss,
3277
3283
  originalPriceTakeProfit: publicSignal.originalPriceTakeProfit,
3278
3284
  originalPriceStopLoss: publicSignal.originalPriceStopLoss,
3285
+ originalPriceOpen: publicSignal.originalPriceOpen,
3279
3286
  scheduledAt: publicSignal.scheduledAt,
3280
3287
  pendingAt: publicSignal.pendingAt,
3281
3288
  });
@@ -3292,6 +3299,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3292
3299
  percentShift: commit.percentShift,
3293
3300
  currentPrice: commit.currentPrice,
3294
3301
  timestamp,
3302
+ totalEntries: publicSignal.totalEntries,
3295
3303
  signalId: publicSignal.id,
3296
3304
  position: publicSignal.position,
3297
3305
  priceOpen: publicSignal.priceOpen,
@@ -3299,6 +3307,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3299
3307
  priceStopLoss: publicSignal.priceStopLoss,
3300
3308
  originalPriceTakeProfit: publicSignal.originalPriceTakeProfit,
3301
3309
  originalPriceStopLoss: publicSignal.originalPriceStopLoss,
3310
+ originalPriceOpen: publicSignal.originalPriceOpen,
3302
3311
  scheduledAt: publicSignal.scheduledAt,
3303
3312
  pendingAt: publicSignal.pendingAt,
3304
3313
  });
@@ -3315,6 +3324,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3315
3324
  percentShift: commit.percentShift,
3316
3325
  currentPrice: commit.currentPrice,
3317
3326
  timestamp,
3327
+ totalEntries: publicSignal.totalEntries,
3318
3328
  signalId: publicSignal.id,
3319
3329
  position: publicSignal.position,
3320
3330
  priceOpen: publicSignal.priceOpen,
@@ -3322,6 +3332,33 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3322
3332
  priceStopLoss: publicSignal.priceStopLoss,
3323
3333
  originalPriceTakeProfit: publicSignal.originalPriceTakeProfit,
3324
3334
  originalPriceStopLoss: publicSignal.originalPriceStopLoss,
3335
+ originalPriceOpen: publicSignal.originalPriceOpen,
3336
+ scheduledAt: publicSignal.scheduledAt,
3337
+ pendingAt: publicSignal.pendingAt,
3338
+ });
3339
+ continue;
3340
+ }
3341
+ if (commit.action === "average-buy") {
3342
+ const effectivePriceOpen = getEffectivePriceOpen(self._pendingSignal);
3343
+ await CALL_COMMIT_FN(self, {
3344
+ action: "average-buy",
3345
+ symbol: commit.symbol,
3346
+ strategyName: self.params.strategyName,
3347
+ exchangeName: self.params.exchangeName,
3348
+ frameName: self.params.frameName,
3349
+ backtest: commit.backtest,
3350
+ currentPrice: commit.currentPrice,
3351
+ effectivePriceOpen,
3352
+ timestamp,
3353
+ totalEntries: publicSignal.totalEntries,
3354
+ signalId: publicSignal.id,
3355
+ position: publicSignal.position,
3356
+ priceOpen: publicSignal.originalPriceOpen,
3357
+ priceTakeProfit: publicSignal.priceTakeProfit,
3358
+ priceStopLoss: publicSignal.priceStopLoss,
3359
+ originalPriceTakeProfit: publicSignal.originalPriceTakeProfit,
3360
+ originalPriceStopLoss: publicSignal.originalPriceStopLoss,
3361
+ originalPriceOpen: publicSignal.originalPriceOpen,
3325
3362
  scheduledAt: publicSignal.scheduledAt,
3326
3363
  pendingAt: publicSignal.pendingAt,
3327
3364
  });
@@ -3382,13 +3419,20 @@ const TO_PUBLIC_SIGNAL = (signal) => {
3382
3419
  const partialExecuted = ("_partial" in signal && Array.isArray(signal._partial))
3383
3420
  ? signal._partial.reduce((sum, partial) => sum + partial.percent, 0)
3384
3421
  : 0;
3422
+ const totalEntries = ("_entry" in signal && Array.isArray(signal._entry))
3423
+ ? signal._entry.length
3424
+ : 1;
3425
+ const effectivePriceOpen = "_entry" in signal ? getEffectivePriceOpen(signal) : signal.priceOpen;
3385
3426
  return {
3386
3427
  ...structuredClone(signal),
3428
+ priceOpen: effectivePriceOpen,
3387
3429
  priceStopLoss: hasTrailingSL ? signal._trailingPriceStopLoss : signal.priceStopLoss,
3388
3430
  priceTakeProfit: hasTrailingTP ? signal._trailingPriceTakeProfit : signal.priceTakeProfit,
3431
+ originalPriceOpen: signal.priceOpen,
3389
3432
  originalPriceStopLoss: signal.priceStopLoss,
3390
3433
  originalPriceTakeProfit: signal.priceTakeProfit,
3391
3434
  partialExecuted,
3435
+ totalEntries,
3392
3436
  };
3393
3437
  };
3394
3438
  const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
@@ -3718,6 +3762,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
3718
3762
  scheduledAt: currentTime,
3719
3763
  pendingAt: currentTime, // Для immediate signal оба времени одинаковые
3720
3764
  _isScheduled: false,
3765
+ _entry: [{ price: signal.priceOpen }],
3721
3766
  };
3722
3767
  // Валидируем сигнал перед возвратом
3723
3768
  VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
@@ -3739,6 +3784,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
3739
3784
  scheduledAt: currentTime,
3740
3785
  pendingAt: SCHEDULED_SIGNAL_PENDING_MOCK, // Временно, обновится при активации
3741
3786
  _isScheduled: true,
3787
+ _entry: [{ price: signal.priceOpen }],
3742
3788
  };
3743
3789
  // Валидируем сигнал перед возвратом
3744
3790
  VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
@@ -3756,6 +3802,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
3756
3802
  scheduledAt: currentTime,
3757
3803
  pendingAt: currentTime, // Для immediate signal оба времени одинаковые
3758
3804
  _isScheduled: false,
3805
+ _entry: [{ price: currentPrice }],
3759
3806
  };
3760
3807
  // Валидируем сигнал перед возвратом
3761
3808
  VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
@@ -3902,9 +3949,10 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
3902
3949
  return true;
3903
3950
  };
3904
3951
  const TRAILING_STOP_LOSS_FN = (self, signal, percentShift) => {
3952
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
3905
3953
  // CRITICAL: Always calculate from ORIGINAL SL, not from current trailing SL
3906
3954
  // This prevents error accumulation on repeated calls
3907
- const originalSlDistancePercent = Math.abs((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen * 100);
3955
+ const originalSlDistancePercent = Math.abs((effectivePriceOpen - signal.priceStopLoss) / effectivePriceOpen * 100);
3908
3956
  // Calculate new stop-loss distance percentage by adding shift to ORIGINAL distance
3909
3957
  // Negative percentShift: reduces distance % (tightens stop, moves SL toward entry or beyond)
3910
3958
  // Positive percentShift: increases distance % (loosens stop, moves SL away from entry)
@@ -3916,13 +3964,13 @@ const TRAILING_STOP_LOSS_FN = (self, signal, percentShift) => {
3916
3964
  // LONG: SL is below entry (or above entry if in profit zone)
3917
3965
  // Formula: entry * (1 - newDistance%)
3918
3966
  // Example: entry=100, originalSL=90 (10%), shift=-5% → newDistance=5% → 100 * 0.95 = 95 (tighter)
3919
- newStopLoss = signal.priceOpen * (1 - newSlDistancePercent / 100);
3967
+ newStopLoss = effectivePriceOpen * (1 - newSlDistancePercent / 100);
3920
3968
  }
3921
3969
  else {
3922
3970
  // SHORT: SL is above entry (or below entry if in profit zone)
3923
3971
  // Formula: entry * (1 + newDistance%)
3924
3972
  // Example: entry=100, originalSL=110 (10%), shift=-5% → newDistance=5% → 100 * 1.05 = 105 (tighter)
3925
- newStopLoss = signal.priceOpen * (1 + newSlDistancePercent / 100);
3973
+ newStopLoss = effectivePriceOpen * (1 + newSlDistancePercent / 100);
3926
3974
  }
3927
3975
  const currentTrailingSL = signal._trailingPriceStopLoss;
3928
3976
  const isFirstCall = currentTrailingSL === undefined;
@@ -3985,9 +4033,10 @@ const TRAILING_STOP_LOSS_FN = (self, signal, percentShift) => {
3985
4033
  }
3986
4034
  };
3987
4035
  const TRAILING_TAKE_PROFIT_FN = (self, signal, percentShift) => {
4036
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
3988
4037
  // CRITICAL: Always calculate from ORIGINAL TP, not from current trailing TP
3989
4038
  // This prevents error accumulation on repeated calls
3990
- const originalTpDistancePercent = Math.abs((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen * 100);
4039
+ const originalTpDistancePercent = Math.abs((signal.priceTakeProfit - effectivePriceOpen) / effectivePriceOpen * 100);
3991
4040
  // Calculate new take-profit distance percentage by adding shift to ORIGINAL distance
3992
4041
  // Negative percentShift: reduces distance % (brings TP closer to entry)
3993
4042
  // Positive percentShift: increases distance % (moves TP further from entry)
@@ -3998,13 +4047,13 @@ const TRAILING_TAKE_PROFIT_FN = (self, signal, percentShift) => {
3998
4047
  // LONG: TP is above entry
3999
4048
  // Formula: entry * (1 + newDistance%)
4000
4049
  // Example: entry=100, originalTP=110 (10%), shift=-3% → newDistance=7% → 100 * 1.07 = 107 (closer)
4001
- newTakeProfit = signal.priceOpen * (1 + newTpDistancePercent / 100);
4050
+ newTakeProfit = effectivePriceOpen * (1 + newTpDistancePercent / 100);
4002
4051
  }
4003
4052
  else {
4004
4053
  // SHORT: TP is below entry
4005
4054
  // Formula: entry * (1 - newDistance%)
4006
4055
  // Example: entry=100, originalTP=90 (10%), shift=-3% → newDistance=7% → 100 * 0.93 = 93 (closer)
4007
- newTakeProfit = signal.priceOpen * (1 - newTpDistancePercent / 100);
4056
+ newTakeProfit = effectivePriceOpen * (1 - newTpDistancePercent / 100);
4008
4057
  }
4009
4058
  const currentTrailingTP = signal._trailingPriceTakeProfit;
4010
4059
  const isFirstCall = currentTrailingTP === undefined;
@@ -4065,6 +4114,7 @@ const TRAILING_TAKE_PROFIT_FN = (self, signal, percentShift) => {
4065
4114
  }
4066
4115
  };
4067
4116
  const BREAKEVEN_FN = (self, signal, currentPrice) => {
4117
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
4068
4118
  // Calculate breakeven threshold based on slippage and fees
4069
4119
  // Need to cover: entry slippage + entry fee + exit slippage + exit fee
4070
4120
  // Total: (slippage + fee) * 2 transactions
@@ -4072,10 +4122,10 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4072
4122
  // Check if trailing stop is already set
4073
4123
  if (signal._trailingPriceStopLoss !== undefined) {
4074
4124
  const trailingStopLoss = signal._trailingPriceStopLoss;
4075
- const breakevenPrice = signal.priceOpen;
4125
+ const breakevenPrice = effectivePriceOpen;
4076
4126
  if (signal.position === "long") {
4077
4127
  // LONG: trailing SL is positive if it's above entry (in profit zone)
4078
- const isPositiveTrailing = trailingStopLoss > signal.priceOpen;
4128
+ const isPositiveTrailing = trailingStopLoss > effectivePriceOpen;
4079
4129
  if (isPositiveTrailing) {
4080
4130
  // Trailing stop is already protecting profit - consider breakeven achieved
4081
4131
  self.params.logger.debug("BREAKEVEN_FN: positive trailing stop already set, returning true", {
@@ -4091,7 +4141,7 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4091
4141
  else {
4092
4142
  // Trailing stop is negative (below entry)
4093
4143
  // Check if we can upgrade it to breakeven
4094
- const thresholdPrice = signal.priceOpen * (1 + breakevenThresholdPercent / 100);
4144
+ const thresholdPrice = effectivePriceOpen * (1 + breakevenThresholdPercent / 100);
4095
4145
  const isThresholdReached = currentPrice >= thresholdPrice;
4096
4146
  if (isThresholdReached && breakevenPrice > trailingStopLoss) {
4097
4147
  // Check for price intrusion before setting new SL
@@ -4140,7 +4190,7 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4140
4190
  }
4141
4191
  else {
4142
4192
  // SHORT: trailing SL is positive if it's below entry (in profit zone)
4143
- const isPositiveTrailing = trailingStopLoss < signal.priceOpen;
4193
+ const isPositiveTrailing = trailingStopLoss < effectivePriceOpen;
4144
4194
  if (isPositiveTrailing) {
4145
4195
  // Trailing stop is already protecting profit - consider breakeven achieved
4146
4196
  self.params.logger.debug("BREAKEVEN_FN: positive trailing stop already set, returning true", {
@@ -4156,7 +4206,7 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4156
4206
  else {
4157
4207
  // Trailing stop is negative (above entry)
4158
4208
  // Check if we can upgrade it to breakeven
4159
- const thresholdPrice = signal.priceOpen * (1 - breakevenThresholdPercent / 100);
4209
+ const thresholdPrice = effectivePriceOpen * (1 - breakevenThresholdPercent / 100);
4160
4210
  const isThresholdReached = currentPrice <= thresholdPrice;
4161
4211
  if (isThresholdReached && breakevenPrice < trailingStopLoss) {
4162
4212
  // Check for price intrusion before setting new SL
@@ -4206,21 +4256,21 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4206
4256
  }
4207
4257
  // No trailing stop set - proceed with normal breakeven logic
4208
4258
  const currentStopLoss = signal.priceStopLoss;
4209
- const breakevenPrice = signal.priceOpen;
4259
+ const breakevenPrice = effectivePriceOpen;
4210
4260
  // Calculate threshold price
4211
4261
  let thresholdPrice;
4212
4262
  let isThresholdReached;
4213
4263
  let canMoveToBreakeven;
4214
4264
  if (signal.position === "long") {
4215
4265
  // LONG: threshold reached when price goes UP by breakevenThresholdPercent from entry
4216
- thresholdPrice = signal.priceOpen * (1 + breakevenThresholdPercent / 100);
4266
+ thresholdPrice = effectivePriceOpen * (1 + breakevenThresholdPercent / 100);
4217
4267
  isThresholdReached = currentPrice >= thresholdPrice;
4218
4268
  // Can move to breakeven only if threshold reached and SL is below entry
4219
4269
  canMoveToBreakeven = isThresholdReached && currentStopLoss < breakevenPrice;
4220
4270
  }
4221
4271
  else {
4222
4272
  // SHORT: threshold reached when price goes DOWN by breakevenThresholdPercent from entry
4223
- thresholdPrice = signal.priceOpen * (1 - breakevenThresholdPercent / 100);
4273
+ thresholdPrice = effectivePriceOpen * (1 - breakevenThresholdPercent / 100);
4224
4274
  isThresholdReached = currentPrice <= thresholdPrice;
4225
4275
  // Can move to breakeven only if threshold reached and SL is above entry
4226
4276
  canMoveToBreakeven = isThresholdReached && currentStopLoss > breakevenPrice;
@@ -4280,8 +4330,51 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4280
4330
  thresholdPrice,
4281
4331
  breakevenThresholdPercent,
4282
4332
  profitDistancePercent: signal.position === "long"
4283
- ? ((currentPrice - signal.priceOpen) / signal.priceOpen * 100)
4284
- : ((signal.priceOpen - currentPrice) / signal.priceOpen * 100),
4333
+ ? ((currentPrice - effectivePriceOpen) / effectivePriceOpen * 100)
4334
+ : ((effectivePriceOpen - currentPrice) / effectivePriceOpen * 100),
4335
+ });
4336
+ return true;
4337
+ };
4338
+ const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
4339
+ // Ensure _entry is initialized (handles signals loaded from disk without _entry)
4340
+ if (!signal._entry || signal._entry.length === 0) {
4341
+ signal._entry = [{ price: signal.priceOpen }];
4342
+ }
4343
+ const lastEntry = signal._entry[signal._entry.length - 1];
4344
+ if (signal.position === "long") {
4345
+ // LONG: averaging down = currentPrice must be strictly lower than last entry
4346
+ if (currentPrice >= lastEntry.price) {
4347
+ self.params.logger.debug("AVERAGE_BUY_FN: rejected — currentPrice >= last entry (LONG)", {
4348
+ signalId: signal.id,
4349
+ position: signal.position,
4350
+ currentPrice,
4351
+ lastEntryPrice: lastEntry.price,
4352
+ reason: "must average down for LONG",
4353
+ });
4354
+ return false;
4355
+ }
4356
+ }
4357
+ else {
4358
+ // SHORT: averaging down = currentPrice must be strictly higher than last entry
4359
+ if (currentPrice <= lastEntry.price) {
4360
+ self.params.logger.debug("AVERAGE_BUY_FN: rejected — currentPrice <= last entry (SHORT)", {
4361
+ signalId: signal.id,
4362
+ position: signal.position,
4363
+ currentPrice,
4364
+ lastEntryPrice: lastEntry.price,
4365
+ reason: "must average down for SHORT",
4366
+ });
4367
+ return false;
4368
+ }
4369
+ }
4370
+ signal._entry.push({ price: currentPrice });
4371
+ self.params.logger.info("AVERAGE_BUY_FN executed", {
4372
+ signalId: signal.id,
4373
+ position: signal.position,
4374
+ originalPriceOpen: signal.priceOpen,
4375
+ newEntryPrice: currentPrice,
4376
+ newEffectivePrice: getEffectivePriceOpen(signal),
4377
+ totalEntries: signal._entry.length,
4285
4378
  });
4286
4379
  return true;
4287
4380
  };
@@ -5010,9 +5103,10 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
5010
5103
  await CALL_ACTIVE_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentTime, self.params.execution.context.backtest);
5011
5104
  // Calculate percentage of path to TP/SL for partial fill/loss callbacks
5012
5105
  {
5106
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
5013
5107
  if (signal.position === "long") {
5014
5108
  // For long: calculate progress towards TP or SL
5015
- const currentDistance = currentPrice - signal.priceOpen;
5109
+ const currentDistance = currentPrice - effectivePriceOpen;
5016
5110
  if (currentDistance > 0) {
5017
5111
  // Check if breakeven should be triggered
5018
5112
  await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
@@ -5020,7 +5114,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
5020
5114
  if (currentDistance > 0) {
5021
5115
  // Moving towards TP (use trailing TP if set)
5022
5116
  const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
5023
- const tpDistance = effectiveTakeProfit - signal.priceOpen;
5117
+ const tpDistance = effectiveTakeProfit - effectivePriceOpen;
5024
5118
  const progressPercent = (currentDistance / tpDistance) * 100;
5025
5119
  percentTp = Math.min(progressPercent, 100);
5026
5120
  await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
@@ -5028,7 +5122,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
5028
5122
  else if (currentDistance < 0) {
5029
5123
  // Moving towards SL (use trailing SL if set)
5030
5124
  const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
5031
- const slDistance = signal.priceOpen - effectiveStopLoss;
5125
+ const slDistance = effectivePriceOpen - effectiveStopLoss;
5032
5126
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
5033
5127
  percentSl = Math.min(progressPercent, 100);
5034
5128
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
@@ -5036,7 +5130,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
5036
5130
  }
5037
5131
  else if (signal.position === "short") {
5038
5132
  // For short: calculate progress towards TP or SL
5039
- const currentDistance = signal.priceOpen - currentPrice;
5133
+ const currentDistance = effectivePriceOpen - currentPrice;
5040
5134
  if (currentDistance > 0) {
5041
5135
  // Check if breakeven should be triggered
5042
5136
  await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
@@ -5044,7 +5138,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
5044
5138
  if (currentDistance > 0) {
5045
5139
  // Moving towards TP (use trailing TP if set)
5046
5140
  const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
5047
- const tpDistance = signal.priceOpen - effectiveTakeProfit;
5141
+ const tpDistance = effectivePriceOpen - effectiveTakeProfit;
5048
5142
  const progressPercent = (currentDistance / tpDistance) * 100;
5049
5143
  percentTp = Math.min(progressPercent, 100);
5050
5144
  await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
@@ -5052,7 +5146,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
5052
5146
  if (currentDistance < 0) {
5053
5147
  // Moving towards SL (use trailing SL if set)
5054
5148
  const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
5055
- const slDistance = effectiveStopLoss - signal.priceOpen;
5149
+ const slDistance = effectiveStopLoss - effectivePriceOpen;
5056
5150
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
5057
5151
  percentSl = Math.min(progressPercent, 100);
5058
5152
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
@@ -5272,8 +5366,10 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
5272
5366
  priceStopLoss: publicSignalForCommit.priceStopLoss,
5273
5367
  originalPriceTakeProfit: publicSignalForCommit.originalPriceTakeProfit,
5274
5368
  originalPriceStopLoss: publicSignalForCommit.originalPriceStopLoss,
5369
+ originalPriceOpen: publicSignalForCommit.originalPriceOpen,
5275
5370
  scheduledAt: publicSignalForCommit.scheduledAt,
5276
5371
  pendingAt: publicSignalForCommit.pendingAt,
5372
+ totalEntries: publicSignalForCommit.totalEntries,
5277
5373
  });
5278
5374
  await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, pendingSignal.priceOpen, candle.timestamp, self.params.execution.context.backtest);
5279
5375
  await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, pendingSignal, candle.timestamp, self.params.execution.context.backtest);
@@ -5420,9 +5516,10 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
5420
5516
  // Call onPartialProfit/onPartialLoss callbacks during backtest candle processing
5421
5517
  // Calculate percentage of path to TP/SL
5422
5518
  {
5519
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
5423
5520
  if (signal.position === "long") {
5424
5521
  // For long: calculate progress towards TP or SL
5425
- const currentDistance = averagePrice - signal.priceOpen;
5522
+ const currentDistance = averagePrice - effectivePriceOpen;
5426
5523
  if (currentDistance > 0) {
5427
5524
  // Check if breakeven should be triggered
5428
5525
  await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, averagePrice, currentCandleTimestamp, self.params.execution.context.backtest);
@@ -5430,21 +5527,21 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
5430
5527
  if (currentDistance > 0) {
5431
5528
  // Moving towards TP (use trailing TP if set)
5432
5529
  const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
5433
- const tpDistance = effectiveTakeProfit - signal.priceOpen;
5530
+ const tpDistance = effectiveTakeProfit - effectivePriceOpen;
5434
5531
  const progressPercent = (currentDistance / tpDistance) * 100;
5435
5532
  await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
5436
5533
  }
5437
5534
  else if (currentDistance < 0) {
5438
5535
  // Moving towards SL (use trailing SL if set)
5439
5536
  const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
5440
- const slDistance = signal.priceOpen - effectiveStopLoss;
5537
+ const slDistance = effectivePriceOpen - effectiveStopLoss;
5441
5538
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
5442
5539
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
5443
5540
  }
5444
5541
  }
5445
5542
  else if (signal.position === "short") {
5446
5543
  // For short: calculate progress towards TP or SL
5447
- const currentDistance = signal.priceOpen - averagePrice;
5544
+ const currentDistance = effectivePriceOpen - averagePrice;
5448
5545
  if (currentDistance > 0) {
5449
5546
  // Check if breakeven should be triggered
5450
5547
  await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, averagePrice, currentCandleTimestamp, self.params.execution.context.backtest);
@@ -5452,14 +5549,14 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
5452
5549
  if (currentDistance > 0) {
5453
5550
  // Moving towards TP (use trailing TP if set)
5454
5551
  const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
5455
- const tpDistance = signal.priceOpen - effectiveTakeProfit;
5552
+ const tpDistance = effectivePriceOpen - effectiveTakeProfit;
5456
5553
  const progressPercent = (currentDistance / tpDistance) * 100;
5457
5554
  await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
5458
5555
  }
5459
5556
  if (currentDistance < 0) {
5460
5557
  // Moving towards SL (use trailing SL if set)
5461
5558
  const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
5462
- const slDistance = effectiveStopLoss - signal.priceOpen;
5559
+ const slDistance = effectiveStopLoss - effectivePriceOpen;
5463
5560
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
5464
5561
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
5465
5562
  }
@@ -5657,6 +5754,7 @@ class ClientStrategy {
5657
5754
  return false;
5658
5755
  }
5659
5756
  const signal = this._pendingSignal;
5757
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
5660
5758
  // Calculate breakeven threshold based on slippage and fees
5661
5759
  // Need to cover: entry slippage + entry fee + exit slippage + exit fee
5662
5760
  // Total: (slippage + fee) * 2 transactions
@@ -5666,52 +5764,52 @@ class ClientStrategy {
5666
5764
  const trailingStopLoss = signal._trailingPriceStopLoss;
5667
5765
  if (signal.position === "long") {
5668
5766
  // LONG: trailing SL is positive if it's above entry (in profit zone)
5669
- const isPositiveTrailing = trailingStopLoss > signal.priceOpen;
5767
+ const isPositiveTrailing = trailingStopLoss > effectivePriceOpen;
5670
5768
  if (isPositiveTrailing) {
5671
5769
  // Trailing stop is already protecting profit - breakeven achieved
5672
5770
  return true;
5673
5771
  }
5674
5772
  // Trailing stop is negative (below entry)
5675
5773
  // Check if we can upgrade it to breakeven
5676
- const thresholdPrice = signal.priceOpen * (1 + breakevenThresholdPercent / 100);
5774
+ const thresholdPrice = effectivePriceOpen * (1 + breakevenThresholdPercent / 100);
5677
5775
  const isThresholdReached = currentPrice >= thresholdPrice;
5678
- const breakevenPrice = signal.priceOpen;
5776
+ const breakevenPrice = effectivePriceOpen;
5679
5777
  // Can upgrade to breakeven if threshold reached and breakeven is better than current trailing SL
5680
5778
  return isThresholdReached && breakevenPrice > trailingStopLoss;
5681
5779
  }
5682
5780
  else {
5683
5781
  // SHORT: trailing SL is positive if it's below entry (in profit zone)
5684
- const isPositiveTrailing = trailingStopLoss < signal.priceOpen;
5782
+ const isPositiveTrailing = trailingStopLoss < effectivePriceOpen;
5685
5783
  if (isPositiveTrailing) {
5686
5784
  // Trailing stop is already protecting profit - breakeven achieved
5687
5785
  return true;
5688
5786
  }
5689
5787
  // Trailing stop is negative (above entry)
5690
5788
  // Check if we can upgrade it to breakeven
5691
- const thresholdPrice = signal.priceOpen * (1 - breakevenThresholdPercent / 100);
5789
+ const thresholdPrice = effectivePriceOpen * (1 - breakevenThresholdPercent / 100);
5692
5790
  const isThresholdReached = currentPrice <= thresholdPrice;
5693
- const breakevenPrice = signal.priceOpen;
5791
+ const breakevenPrice = effectivePriceOpen;
5694
5792
  // Can upgrade to breakeven if threshold reached and breakeven is better than current trailing SL
5695
5793
  return isThresholdReached && breakevenPrice < trailingStopLoss;
5696
5794
  }
5697
5795
  }
5698
5796
  // No trailing stop set - proceed with normal breakeven logic
5699
5797
  const currentStopLoss = signal.priceStopLoss;
5700
- const breakevenPrice = signal.priceOpen;
5798
+ const breakevenPrice = effectivePriceOpen;
5701
5799
  // Calculate threshold price
5702
5800
  let thresholdPrice;
5703
5801
  let isThresholdReached;
5704
5802
  let canMoveToBreakeven;
5705
5803
  if (signal.position === "long") {
5706
5804
  // LONG: threshold reached when price goes UP by breakevenThresholdPercent from entry
5707
- thresholdPrice = signal.priceOpen * (1 + breakevenThresholdPercent / 100);
5805
+ thresholdPrice = effectivePriceOpen * (1 + breakevenThresholdPercent / 100);
5708
5806
  isThresholdReached = currentPrice >= thresholdPrice;
5709
5807
  // Can move to breakeven only if threshold reached and SL is below entry
5710
5808
  canMoveToBreakeven = isThresholdReached && currentStopLoss < breakevenPrice;
5711
5809
  }
5712
5810
  else {
5713
5811
  // SHORT: threshold reached when price goes DOWN by breakevenThresholdPercent from entry
5714
- thresholdPrice = signal.priceOpen * (1 - breakevenThresholdPercent / 100);
5812
+ thresholdPrice = effectivePriceOpen * (1 - breakevenThresholdPercent / 100);
5715
5813
  isThresholdReached = currentPrice <= thresholdPrice;
5716
5814
  // Can move to breakeven only if threshold reached and SL is above entry
5717
5815
  canMoveToBreakeven = isThresholdReached && currentStopLoss > breakevenPrice;
@@ -5794,6 +5892,8 @@ class ClientStrategy {
5794
5892
  backtest: this.params.execution.context.backtest,
5795
5893
  cancelId: cancelledSignal.cancelId,
5796
5894
  timestamp: currentTime,
5895
+ totalEntries: cancelledSignal._entry?.length ?? 1,
5896
+ originalPriceOpen: cancelledSignal.priceOpen,
5797
5897
  });
5798
5898
  // Call onCancel callback
5799
5899
  await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, currentTime, this.params.execution.context.backtest);
@@ -5834,6 +5934,8 @@ class ClientStrategy {
5834
5934
  backtest: this.params.execution.context.backtest,
5835
5935
  closeId: closedSignal.closeId,
5836
5936
  timestamp: currentTime,
5937
+ totalEntries: closedSignal._entry?.length ?? 1,
5938
+ originalPriceOpen: closedSignal.priceOpen,
5837
5939
  });
5838
5940
  // Call onClose callback
5839
5941
  await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, currentTime, this.params.execution.context.backtest);
@@ -5914,8 +6016,10 @@ class ClientStrategy {
5914
6016
  priceStopLoss: publicSignalForCommit.priceStopLoss,
5915
6017
  originalPriceTakeProfit: publicSignalForCommit.originalPriceTakeProfit,
5916
6018
  originalPriceStopLoss: publicSignalForCommit.originalPriceStopLoss,
6019
+ originalPriceOpen: publicSignalForCommit.originalPriceOpen,
5917
6020
  scheduledAt: publicSignalForCommit.scheduledAt,
5918
6021
  pendingAt: publicSignalForCommit.pendingAt,
6022
+ totalEntries: publicSignalForCommit.totalEntries,
5919
6023
  });
5920
6024
  // Call onOpen callback
5921
6025
  await CALL_OPEN_CALLBACKS_FN(this, this.params.execution.context.symbol, pendingSignal, currentPrice, currentTime, this.params.execution.context.backtest);
@@ -6046,6 +6150,8 @@ class ClientStrategy {
6046
6150
  backtest: true,
6047
6151
  cancelId: cancelledSignal.cancelId,
6048
6152
  timestamp: closeTimestamp,
6153
+ totalEntries: cancelledSignal._entry?.length ?? 1,
6154
+ originalPriceOpen: cancelledSignal.priceOpen,
6049
6155
  });
6050
6156
  await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
6051
6157
  const cancelledResult = {
@@ -6083,6 +6189,8 @@ class ClientStrategy {
6083
6189
  backtest: true,
6084
6190
  closeId: closedSignal.closeId,
6085
6191
  timestamp: closeTimestamp,
6192
+ totalEntries: closedSignal._entry?.length ?? 1,
6193
+ originalPriceOpen: closedSignal.priceOpen,
6086
6194
  });
6087
6195
  await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
6088
6196
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
@@ -6470,16 +6578,19 @@ class ClientStrategy {
6470
6578
  throw new Error(`ClientStrategy partialProfit: currentPrice must be a positive finite number, got ${currentPrice}`);
6471
6579
  }
6472
6580
  // Validation: currentPrice must be moving toward TP (profit direction)
6473
- if (this._pendingSignal.position === "long") {
6474
- // For LONG: currentPrice must be higher than priceOpen (moving toward TP)
6475
- if (currentPrice <= this._pendingSignal.priceOpen) {
6476
- throw new Error(`ClientStrategy partialProfit: For LONG position, currentPrice (${currentPrice}) must be > priceOpen (${this._pendingSignal.priceOpen})`);
6581
+ {
6582
+ const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
6583
+ if (this._pendingSignal.position === "long") {
6584
+ // For LONG: currentPrice must be higher than effectivePriceOpen (moving toward TP)
6585
+ if (currentPrice <= effectivePriceOpen) {
6586
+ throw new Error(`ClientStrategy partialProfit: For LONG position, currentPrice (${currentPrice}) must be > effectivePriceOpen (${effectivePriceOpen})`);
6587
+ }
6477
6588
  }
6478
- }
6479
- else {
6480
- // For SHORT: currentPrice must be lower than priceOpen (moving toward TP)
6481
- if (currentPrice >= this._pendingSignal.priceOpen) {
6482
- throw new Error(`ClientStrategy partialProfit: For SHORT position, currentPrice (${currentPrice}) must be < priceOpen (${this._pendingSignal.priceOpen})`);
6589
+ else {
6590
+ // For SHORT: currentPrice must be lower than effectivePriceOpen (moving toward TP)
6591
+ if (currentPrice >= effectivePriceOpen) {
6592
+ throw new Error(`ClientStrategy partialProfit: For SHORT position, currentPrice (${currentPrice}) must be < effectivePriceOpen (${effectivePriceOpen})`);
6593
+ }
6483
6594
  }
6484
6595
  }
6485
6596
  // Check if currentPrice already crossed take profit level
@@ -6602,16 +6713,19 @@ class ClientStrategy {
6602
6713
  throw new Error(`ClientStrategy partialLoss: currentPrice must be a positive finite number, got ${currentPrice}`);
6603
6714
  }
6604
6715
  // Validation: currentPrice must be moving toward SL (loss direction)
6605
- if (this._pendingSignal.position === "long") {
6606
- // For LONG: currentPrice must be lower than priceOpen (moving toward SL)
6607
- if (currentPrice >= this._pendingSignal.priceOpen) {
6608
- throw new Error(`ClientStrategy partialLoss: For LONG position, currentPrice (${currentPrice}) must be < priceOpen (${this._pendingSignal.priceOpen})`);
6716
+ {
6717
+ const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
6718
+ if (this._pendingSignal.position === "long") {
6719
+ // For LONG: currentPrice must be lower than effectivePriceOpen (moving toward SL)
6720
+ if (currentPrice >= effectivePriceOpen) {
6721
+ throw new Error(`ClientStrategy partialLoss: For LONG position, currentPrice (${currentPrice}) must be < effectivePriceOpen (${effectivePriceOpen})`);
6722
+ }
6609
6723
  }
6610
- }
6611
- else {
6612
- // For SHORT: currentPrice must be higher than priceOpen (moving toward SL)
6613
- if (currentPrice <= this._pendingSignal.priceOpen) {
6614
- throw new Error(`ClientStrategy partialLoss: For SHORT position, currentPrice (${currentPrice}) must be > priceOpen (${this._pendingSignal.priceOpen})`);
6724
+ else {
6725
+ // For SHORT: currentPrice must be higher than effectivePriceOpen (moving toward SL)
6726
+ if (currentPrice <= effectivePriceOpen) {
6727
+ throw new Error(`ClientStrategy partialLoss: For SHORT position, currentPrice (${currentPrice}) must be > effectivePriceOpen (${effectivePriceOpen})`);
6728
+ }
6615
6729
  }
6616
6730
  }
6617
6731
  // Check if currentPrice already crossed stop loss level
@@ -6732,7 +6846,7 @@ class ClientStrategy {
6732
6846
  }
6733
6847
  // Check for conflict with existing trailing take profit
6734
6848
  const signal = this._pendingSignal;
6735
- const breakevenPrice = signal.priceOpen;
6849
+ const breakevenPrice = getEffectivePriceOpen(signal);
6736
6850
  const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
6737
6851
  if (signal.position === "long" && breakevenPrice >= effectiveTakeProfit) {
6738
6852
  // LONG: Breakeven SL would be at or above current TP - invalid configuration
@@ -6884,14 +6998,15 @@ class ClientStrategy {
6884
6998
  }
6885
6999
  // Calculate what the new stop loss would be
6886
7000
  const signal = this._pendingSignal;
6887
- const slDistancePercent = Math.abs((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen * 100);
7001
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
7002
+ const slDistancePercent = Math.abs((effectivePriceOpen - signal.priceStopLoss) / effectivePriceOpen * 100);
6888
7003
  const newSlDistancePercent = slDistancePercent + percentShift;
6889
7004
  let newStopLoss;
6890
7005
  if (signal.position === "long") {
6891
- newStopLoss = signal.priceOpen * (1 - newSlDistancePercent / 100);
7006
+ newStopLoss = effectivePriceOpen * (1 - newSlDistancePercent / 100);
6892
7007
  }
6893
7008
  else {
6894
- newStopLoss = signal.priceOpen * (1 + newSlDistancePercent / 100);
7009
+ newStopLoss = effectivePriceOpen * (1 + newSlDistancePercent / 100);
6895
7010
  }
6896
7011
  // Check for price intrusion before executing trailing logic
6897
7012
  if (signal.position === "long" && currentPrice < newStopLoss) {
@@ -7057,14 +7172,15 @@ class ClientStrategy {
7057
7172
  }
7058
7173
  // Calculate what the new take profit would be
7059
7174
  const signal = this._pendingSignal;
7060
- const tpDistancePercent = Math.abs((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen * 100);
7175
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
7176
+ const tpDistancePercent = Math.abs((signal.priceTakeProfit - effectivePriceOpen) / effectivePriceOpen * 100);
7061
7177
  const newTpDistancePercent = tpDistancePercent + percentShift;
7062
7178
  let newTakeProfit;
7063
7179
  if (signal.position === "long") {
7064
- newTakeProfit = signal.priceOpen * (1 + newTpDistancePercent / 100);
7180
+ newTakeProfit = effectivePriceOpen * (1 + newTpDistancePercent / 100);
7065
7181
  }
7066
7182
  else {
7067
- newTakeProfit = signal.priceOpen * (1 - newTpDistancePercent / 100);
7183
+ newTakeProfit = effectivePriceOpen * (1 - newTpDistancePercent / 100);
7068
7184
  }
7069
7185
  // Check for price intrusion before executing trailing logic
7070
7186
  if (signal.position === "long" && currentPrice > newTakeProfit) {
@@ -7146,6 +7262,70 @@ class ClientStrategy {
7146
7262
  });
7147
7263
  return true;
7148
7264
  }
7265
+ /**
7266
+ * Adds a new averaging entry to an open position (DCA — Dollar Cost Averaging).
7267
+ *
7268
+ * Appends currentPrice to the _entry array. The effective entry price used in all
7269
+ * distance and PNL calculations becomes the simple arithmetic mean of all _entry prices.
7270
+ * Original priceOpen is preserved unchanged for identity/audit purposes.
7271
+ *
7272
+ * Rejection rules (returns false without throwing):
7273
+ * - LONG: currentPrice >= last entry price (must average down, not up or equal)
7274
+ * - SHORT: currentPrice <= last entry price (must average down, not up or equal)
7275
+ *
7276
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
7277
+ * @param currentPrice - New entry price to add to the averaging history
7278
+ * @param backtest - Whether running in backtest mode
7279
+ * @returns Promise<boolean> - true if entry added, false if rejected by direction check
7280
+ */
7281
+ async averageBuy(symbol, currentPrice, backtest) {
7282
+ this.params.logger.debug("ClientStrategy averageBuy", {
7283
+ symbol,
7284
+ currentPrice,
7285
+ hasPendingSignal: this._pendingSignal !== null,
7286
+ });
7287
+ // Validation: must have pending signal
7288
+ if (!this._pendingSignal) {
7289
+ throw new Error(`ClientStrategy averageBuy: No pending signal exists for symbol=${symbol}`);
7290
+ }
7291
+ // Validation: currentPrice must be valid
7292
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
7293
+ throw new Error(`ClientStrategy averageBuy: currentPrice must be a positive finite number, got ${currentPrice}`);
7294
+ }
7295
+ // Reject if any partial closes have already been executed
7296
+ if (this._pendingSignal._partial && this._pendingSignal._partial.length > 0) {
7297
+ this.params.logger.debug("ClientStrategy averageBuy: rejected — partial closes already executed", {
7298
+ symbol,
7299
+ partialCount: this._pendingSignal._partial.length,
7300
+ });
7301
+ return false;
7302
+ }
7303
+ // Execute averaging logic
7304
+ const result = AVERAGE_BUY_FN(this, this._pendingSignal, currentPrice);
7305
+ if (!result) {
7306
+ return false;
7307
+ }
7308
+ // Persist updated signal state
7309
+ this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
7310
+ pendingSignal: this._pendingSignal,
7311
+ });
7312
+ // Call onWrite callback for testing persist storage
7313
+ if (this.params.callbacks?.onWrite) {
7314
+ this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL(this._pendingSignal), backtest);
7315
+ }
7316
+ if (!backtest) {
7317
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
7318
+ }
7319
+ // Queue commit event for processing in tick()/backtest() with proper timestamp
7320
+ this._commitQueue.push({
7321
+ action: "average-buy",
7322
+ symbol,
7323
+ backtest,
7324
+ currentPrice,
7325
+ totalEntries: this._pendingSignal._entry?.length ?? 1,
7326
+ });
7327
+ return true;
7328
+ }
7149
7329
  }
7150
7330
 
7151
7331
  const RISK_METHOD_NAME_GET_DATA = "RiskUtils.getData";
@@ -8270,6 +8450,27 @@ class StrategyConnectionService {
8270
8450
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8271
8451
  return await strategy.activateScheduled(symbol, backtest, activateId);
8272
8452
  };
8453
+ /**
8454
+ * Adds a new DCA entry to the active pending signal.
8455
+ *
8456
+ * Delegates to ClientStrategy.averageBuy() with current execution context.
8457
+ *
8458
+ * @param backtest - Whether running in backtest mode
8459
+ * @param symbol - Trading pair symbol
8460
+ * @param currentPrice - New entry price to add to the averaging history
8461
+ * @param context - Execution context with strategyName, exchangeName, frameName
8462
+ * @returns Promise<boolean> - true if entry added, false if rejected
8463
+ */
8464
+ this.averageBuy = async (backtest, symbol, currentPrice, context) => {
8465
+ this.loggerService.log("strategyConnectionService averageBuy", {
8466
+ symbol,
8467
+ context,
8468
+ currentPrice,
8469
+ backtest,
8470
+ });
8471
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8472
+ return await strategy.averageBuy(symbol, currentPrice, backtest);
8473
+ };
8273
8474
  }
8274
8475
  }
8275
8476
 
@@ -8743,11 +8944,13 @@ const TO_RISK_SIGNAL = (signal, currentPrice) => {
8743
8944
  : 0;
8744
8945
  return {
8745
8946
  ...structuredClone(signal),
8947
+ totalEntries: 1,
8746
8948
  priceOpen: signal.priceOpen ?? currentPrice,
8747
8949
  priceStopLoss: hasTrailingSL ? signal._trailingPriceStopLoss : signal.priceStopLoss,
8748
8950
  priceTakeProfit: hasTrailingTP ? signal._trailingPriceTakeProfit : signal.priceTakeProfit,
8749
8951
  originalPriceStopLoss: signal.priceStopLoss,
8750
8952
  originalPriceTakeProfit: signal.priceTakeProfit,
8953
+ originalPriceOpen: signal.priceOpen ?? currentPrice,
8751
8954
  partialExecuted,
8752
8955
  };
8753
8956
  };
@@ -11615,6 +11818,28 @@ class StrategyCoreService {
11615
11818
  await this.validate(context);
11616
11819
  return await this.strategyConnectionService.activateScheduled(backtest, symbol, context, activateId);
11617
11820
  };
11821
+ /**
11822
+ * Adds a new DCA entry to the active pending signal.
11823
+ *
11824
+ * Validates strategy existence and delegates to connection service
11825
+ * to add a new averaging entry to the position.
11826
+ *
11827
+ * @param backtest - Whether running in backtest mode
11828
+ * @param symbol - Trading pair symbol
11829
+ * @param currentPrice - New entry price to add to the averaging history
11830
+ * @param context - Execution context with strategyName, exchangeName, frameName
11831
+ * @returns Promise<boolean> - true if entry added, false if rejected
11832
+ */
11833
+ this.averageBuy = async (backtest, symbol, currentPrice, context) => {
11834
+ this.loggerService.log("strategyCoreService averageBuy", {
11835
+ symbol,
11836
+ currentPrice,
11837
+ context,
11838
+ backtest,
11839
+ });
11840
+ await this.validate(context);
11841
+ return await this.strategyConnectionService.averageBuy(backtest, symbol, currentPrice, context);
11842
+ };
11618
11843
  }
11619
11844
  }
11620
11845
 
@@ -14162,6 +14387,18 @@ const backtest_columns = [
14162
14387
  format: (data) => `${data.signal.originalPriceStopLoss.toFixed(8)} USD`,
14163
14388
  isVisible: () => true,
14164
14389
  },
14390
+ {
14391
+ key: "originalPriceOpen",
14392
+ label: "Original Entry",
14393
+ format: (data) => `${data.signal.originalPriceOpen.toFixed(8)} USD`,
14394
+ isVisible: () => true,
14395
+ },
14396
+ {
14397
+ key: "totalEntries",
14398
+ label: "DCA Entries",
14399
+ format: (data) => String(data.signal.totalEntries),
14400
+ isVisible: () => true,
14401
+ },
14165
14402
  {
14166
14403
  key: "pnl",
14167
14404
  label: "PNL (net)",
@@ -14449,6 +14686,20 @@ const live_columns = [
14449
14686
  : "N/A",
14450
14687
  isVisible: () => true,
14451
14688
  },
14689
+ {
14690
+ key: "originalPriceOpen",
14691
+ label: "Original Entry",
14692
+ format: (data) => data.originalPriceOpen !== undefined
14693
+ ? `${data.originalPriceOpen.toFixed(8)} USD`
14694
+ : "N/A",
14695
+ isVisible: () => true,
14696
+ },
14697
+ {
14698
+ key: "totalEntries",
14699
+ label: "DCA Entries",
14700
+ format: (data) => data.totalEntries !== undefined ? String(data.totalEntries) : "N/A",
14701
+ isVisible: () => true,
14702
+ },
14452
14703
  {
14453
14704
  key: "partialExecuted",
14454
14705
  label: "Partial Executed %",
@@ -14613,6 +14864,18 @@ const partial_columns = [
14613
14864
  format: (data) => (data.originalPriceStopLoss ? `${data.originalPriceStopLoss.toFixed(8)} USD` : "N/A"),
14614
14865
  isVisible: () => true,
14615
14866
  },
14867
+ {
14868
+ key: "originalPriceOpen",
14869
+ label: "Original Entry",
14870
+ format: (data) => (data.originalPriceOpen ? `${data.originalPriceOpen.toFixed(8)} USD` : "N/A"),
14871
+ isVisible: () => true,
14872
+ },
14873
+ {
14874
+ key: "totalEntries",
14875
+ label: "DCA Entries",
14876
+ format: (data) => (data.totalEntries !== undefined ? String(data.totalEntries) : "N/A"),
14877
+ isVisible: () => true,
14878
+ },
14616
14879
  {
14617
14880
  key: "partialExecuted",
14618
14881
  label: "Partial Executed %",
@@ -14747,6 +15010,18 @@ const breakeven_columns = [
14747
15010
  format: (data) => (data.originalPriceStopLoss ? `${data.originalPriceStopLoss.toFixed(8)} USD` : "N/A"),
14748
15011
  isVisible: () => true,
14749
15012
  },
15013
+ {
15014
+ key: "originalPriceOpen",
15015
+ label: "Original Entry",
15016
+ format: (data) => (data.originalPriceOpen ? `${data.originalPriceOpen.toFixed(8)} USD` : "N/A"),
15017
+ isVisible: () => true,
15018
+ },
15019
+ {
15020
+ key: "totalEntries",
15021
+ label: "DCA Entries",
15022
+ format: (data) => (data.totalEntries !== undefined ? String(data.totalEntries) : "N/A"),
15023
+ isVisible: () => true,
15024
+ },
14750
15025
  {
14751
15026
  key: "partialExecuted",
14752
15027
  label: "Partial Executed %",
@@ -15017,6 +15292,22 @@ const risk_columns = [
15017
15292
  : "N/A",
15018
15293
  isVisible: () => true,
15019
15294
  },
15295
+ {
15296
+ key: "originalPriceOpen",
15297
+ label: "Original Entry",
15298
+ format: (data) => data.currentSignal.originalPriceOpen !== undefined
15299
+ ? `${data.currentSignal.originalPriceOpen.toFixed(8)} USD`
15300
+ : "N/A",
15301
+ isVisible: () => true,
15302
+ },
15303
+ {
15304
+ key: "totalEntries",
15305
+ label: "DCA Entries",
15306
+ format: (data) => data.currentSignal.totalEntries !== undefined
15307
+ ? String(data.currentSignal.totalEntries)
15308
+ : "N/A",
15309
+ isVisible: () => true,
15310
+ },
15020
15311
  {
15021
15312
  key: "partialExecuted",
15022
15313
  label: "Partial Executed %",
@@ -15191,6 +15482,20 @@ const schedule_columns = [
15191
15482
  : "N/A",
15192
15483
  isVisible: () => true,
15193
15484
  },
15485
+ {
15486
+ key: "originalPriceOpen",
15487
+ label: "Original Entry",
15488
+ format: (data) => data.originalPriceOpen !== undefined
15489
+ ? `${data.originalPriceOpen.toFixed(8)} USD`
15490
+ : "N/A",
15491
+ isVisible: () => true,
15492
+ },
15493
+ {
15494
+ key: "totalEntries",
15495
+ label: "DCA Entries",
15496
+ format: (data) => data.totalEntries !== undefined ? String(data.totalEntries) : "N/A",
15497
+ isVisible: () => true,
15498
+ },
15194
15499
  {
15195
15500
  key: "partialExecuted",
15196
15501
  label: "Partial Executed %",
@@ -17280,6 +17585,8 @@ let ReportStorage$5 = class ReportStorage {
17280
17585
  priceStopLoss: data.signal.priceStopLoss,
17281
17586
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
17282
17587
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
17588
+ totalEntries: data.signal.totalEntries,
17589
+ originalPriceOpen: data.signal.originalPriceOpen,
17283
17590
  partialExecuted: data.signal.partialExecuted,
17284
17591
  scheduledAt: data.signal.scheduledAt,
17285
17592
  });
@@ -17309,6 +17616,8 @@ let ReportStorage$5 = class ReportStorage {
17309
17616
  priceStopLoss: data.signal.priceStopLoss,
17310
17617
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
17311
17618
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
17619
+ totalEntries: data.signal.totalEntries,
17620
+ originalPriceOpen: data.signal.originalPriceOpen,
17312
17621
  partialExecuted: data.signal.partialExecuted,
17313
17622
  duration: durationMin,
17314
17623
  pendingAt: data.signal.pendingAt,
@@ -17341,6 +17650,8 @@ let ReportStorage$5 = class ReportStorage {
17341
17650
  priceStopLoss: data.signal.priceStopLoss,
17342
17651
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
17343
17652
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
17653
+ totalEntries: data.signal.totalEntries,
17654
+ originalPriceOpen: data.signal.originalPriceOpen,
17344
17655
  partialExecuted: data.signal.partialExecuted,
17345
17656
  closeTimestamp: data.closeTimestamp,
17346
17657
  duration: durationMin,
@@ -20486,6 +20797,8 @@ let ReportStorage$3 = class ReportStorage {
20486
20797
  priceStopLoss: data.priceStopLoss,
20487
20798
  originalPriceTakeProfit: data.originalPriceTakeProfit,
20488
20799
  originalPriceStopLoss: data.originalPriceStopLoss,
20800
+ totalEntries: data.totalEntries,
20801
+ originalPriceOpen: data.originalPriceOpen,
20489
20802
  partialExecuted: data.partialExecuted,
20490
20803
  note: data.note,
20491
20804
  pendingAt: data.pendingAt,
@@ -20520,6 +20833,8 @@ let ReportStorage$3 = class ReportStorage {
20520
20833
  priceStopLoss: data.priceStopLoss,
20521
20834
  originalPriceTakeProfit: data.originalPriceTakeProfit,
20522
20835
  originalPriceStopLoss: data.originalPriceStopLoss,
20836
+ totalEntries: data.totalEntries,
20837
+ originalPriceOpen: data.originalPriceOpen,
20523
20838
  partialExecuted: data.partialExecuted,
20524
20839
  note: data.note,
20525
20840
  pendingAt: data.pendingAt,
@@ -21619,6 +21934,8 @@ let ReportStorage$2 = class ReportStorage {
21619
21934
  priceStopLoss: data.priceStopLoss,
21620
21935
  originalPriceTakeProfit: data.originalPriceTakeProfit,
21621
21936
  originalPriceStopLoss: data.originalPriceStopLoss,
21937
+ totalEntries: data.totalEntries,
21938
+ originalPriceOpen: data.originalPriceOpen,
21622
21939
  partialExecuted: data.partialExecuted,
21623
21940
  note: data.note,
21624
21941
  pendingAt: data.pendingAt,
@@ -23622,6 +23939,8 @@ class ScheduleReportService {
23622
23939
  priceStopLoss: data.signal?.priceStopLoss,
23623
23940
  originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
23624
23941
  originalPriceStopLoss: data.signal?.originalPriceStopLoss,
23942
+ totalEntries: data.signal?.totalEntries,
23943
+ originalPriceOpen: data.signal?.originalPriceOpen,
23625
23944
  partialExecuted: data.signal?.partialExecuted,
23626
23945
  pendingAt: data.signal?.pendingAt,
23627
23946
  minuteEstimatedTime: data.signal?.minuteEstimatedTime,
@@ -23666,6 +23985,8 @@ class ScheduleReportService {
23666
23985
  priceStopLoss: data.signal?.priceStopLoss,
23667
23986
  originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
23668
23987
  originalPriceStopLoss: data.signal?.originalPriceStopLoss,
23988
+ totalEntries: data.signal?.totalEntries,
23989
+ originalPriceOpen: data.signal?.originalPriceOpen,
23669
23990
  partialExecuted: data.signal?.partialExecuted,
23670
23991
  scheduledAt: data.signal?.scheduledAt,
23671
23992
  pendingAt: data.signal?.pendingAt,
@@ -24143,6 +24464,8 @@ class PartialReportService {
24143
24464
  priceStopLoss: data.data.priceStopLoss,
24144
24465
  originalPriceTakeProfit: data.data.originalPriceTakeProfit,
24145
24466
  originalPriceStopLoss: data.data.originalPriceStopLoss,
24467
+ totalEntries: data.data.totalEntries,
24468
+ originalPriceOpen: data.data.originalPriceOpen,
24146
24469
  partialExecuted: data.data.partialExecuted,
24147
24470
  _partial: data.data._partial,
24148
24471
  note: data.data.note,
@@ -24184,6 +24507,8 @@ class PartialReportService {
24184
24507
  priceStopLoss: data.data.priceStopLoss,
24185
24508
  originalPriceTakeProfit: data.data.originalPriceTakeProfit,
24186
24509
  originalPriceStopLoss: data.data.originalPriceStopLoss,
24510
+ totalEntries: data.data.totalEntries,
24511
+ originalPriceOpen: data.data.originalPriceOpen,
24187
24512
  partialExecuted: data.data.partialExecuted,
24188
24513
  _partial: data.data._partial,
24189
24514
  note: data.data.note,
@@ -24306,6 +24631,8 @@ class BreakevenReportService {
24306
24631
  priceStopLoss: data.data.priceStopLoss,
24307
24632
  originalPriceTakeProfit: data.data.originalPriceTakeProfit,
24308
24633
  originalPriceStopLoss: data.data.originalPriceStopLoss,
24634
+ totalEntries: data.data.totalEntries,
24635
+ originalPriceOpen: data.data.originalPriceOpen,
24309
24636
  partialExecuted: data.data.partialExecuted,
24310
24637
  _partial: data.data._partial,
24311
24638
  note: data.data.note,
@@ -24614,7 +24941,7 @@ class StrategyReportService {
24614
24941
  * @param scheduledAt - Signal creation timestamp in milliseconds
24615
24942
  * @param pendingAt - Pending timestamp in milliseconds
24616
24943
  */
24617
- this.partialProfit = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
24944
+ this.partialProfit = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
24618
24945
  this.loggerService.log("strategyReportService partialProfit", {
24619
24946
  symbol,
24620
24947
  percentToClose,
@@ -24646,6 +24973,8 @@ class StrategyReportService {
24646
24973
  priceStopLoss,
24647
24974
  originalPriceTakeProfit,
24648
24975
  originalPriceStopLoss,
24976
+ originalPriceOpen,
24977
+ totalEntries,
24649
24978
  scheduledAt,
24650
24979
  pendingAt,
24651
24980
  }, {
@@ -24675,7 +25004,7 @@ class StrategyReportService {
24675
25004
  * @param scheduledAt - Signal creation timestamp in milliseconds
24676
25005
  * @param pendingAt - Pending timestamp in milliseconds
24677
25006
  */
24678
- this.partialLoss = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
25007
+ this.partialLoss = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
24679
25008
  this.loggerService.log("strategyReportService partialLoss", {
24680
25009
  symbol,
24681
25010
  percentToClose,
@@ -24707,6 +25036,8 @@ class StrategyReportService {
24707
25036
  priceStopLoss,
24708
25037
  originalPriceTakeProfit,
24709
25038
  originalPriceStopLoss,
25039
+ originalPriceOpen,
25040
+ totalEntries,
24710
25041
  scheduledAt,
24711
25042
  pendingAt,
24712
25043
  }, {
@@ -24736,7 +25067,7 @@ class StrategyReportService {
24736
25067
  * @param scheduledAt - Signal creation timestamp in milliseconds
24737
25068
  * @param pendingAt - Pending timestamp in milliseconds
24738
25069
  */
24739
- this.trailingStop = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
25070
+ this.trailingStop = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
24740
25071
  this.loggerService.log("strategyReportService trailingStop", {
24741
25072
  symbol,
24742
25073
  percentShift,
@@ -24768,6 +25099,8 @@ class StrategyReportService {
24768
25099
  priceStopLoss,
24769
25100
  originalPriceTakeProfit,
24770
25101
  originalPriceStopLoss,
25102
+ originalPriceOpen,
25103
+ totalEntries,
24771
25104
  scheduledAt,
24772
25105
  pendingAt,
24773
25106
  }, {
@@ -24797,7 +25130,7 @@ class StrategyReportService {
24797
25130
  * @param scheduledAt - Signal creation timestamp in milliseconds
24798
25131
  * @param pendingAt - Pending timestamp in milliseconds
24799
25132
  */
24800
- this.trailingTake = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
25133
+ this.trailingTake = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
24801
25134
  this.loggerService.log("strategyReportService trailingTake", {
24802
25135
  symbol,
24803
25136
  percentShift,
@@ -24829,6 +25162,8 @@ class StrategyReportService {
24829
25162
  priceStopLoss,
24830
25163
  originalPriceTakeProfit,
24831
25164
  originalPriceStopLoss,
25165
+ originalPriceOpen,
25166
+ totalEntries,
24832
25167
  scheduledAt,
24833
25168
  pendingAt,
24834
25169
  }, {
@@ -24857,7 +25192,7 @@ class StrategyReportService {
24857
25192
  * @param scheduledAt - Signal creation timestamp in milliseconds
24858
25193
  * @param pendingAt - Pending timestamp in milliseconds
24859
25194
  */
24860
- this.breakeven = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
25195
+ this.breakeven = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
24861
25196
  this.loggerService.log("strategyReportService breakeven", {
24862
25197
  symbol,
24863
25198
  currentPrice,
@@ -24887,6 +25222,8 @@ class StrategyReportService {
24887
25222
  priceStopLoss,
24888
25223
  originalPriceTakeProfit,
24889
25224
  originalPriceStopLoss,
25225
+ originalPriceOpen,
25226
+ totalEntries,
24890
25227
  scheduledAt,
24891
25228
  pendingAt,
24892
25229
  }, {
@@ -24916,7 +25253,7 @@ class StrategyReportService {
24916
25253
  * @param pendingAt - Pending timestamp in milliseconds
24917
25254
  * @param activateId - Optional identifier for the activation reason
24918
25255
  */
24919
- this.activateScheduled = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, activateId) => {
25256
+ this.activateScheduled = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen, activateId) => {
24920
25257
  this.loggerService.log("strategyReportService activateScheduled", {
24921
25258
  symbol,
24922
25259
  currentPrice,
@@ -24948,6 +25285,8 @@ class StrategyReportService {
24948
25285
  priceStopLoss,
24949
25286
  originalPriceTakeProfit,
24950
25287
  originalPriceStopLoss,
25288
+ originalPriceOpen,
25289
+ totalEntries,
24951
25290
  scheduledAt,
24952
25291
  pendingAt,
24953
25292
  }, {
@@ -24959,6 +25298,71 @@ class StrategyReportService {
24959
25298
  walkerName: "",
24960
25299
  });
24961
25300
  };
25301
+ /**
25302
+ * Logs an average-buy (DCA) event when a new averaging entry is added to an open position.
25303
+ *
25304
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
25305
+ * @param currentPrice - Price at which the new averaging entry was executed
25306
+ * @param effectivePriceOpen - Averaged entry price after this addition
25307
+ * @param totalEntries - Total number of DCA entries after this addition
25308
+ * @param isBacktest - Whether this is a backtest or live trading event
25309
+ * @param context - Strategy context with strategyName, exchangeName, frameName
25310
+ * @param timestamp - Timestamp from StrategyCommitContract (execution context time)
25311
+ * @param position - Trade direction: "long" or "short"
25312
+ * @param priceOpen - Original entry price (unchanged by averaging)
25313
+ * @param priceTakeProfit - Effective take profit price
25314
+ * @param priceStopLoss - Effective stop loss price
25315
+ * @param originalPriceTakeProfit - Original take profit before trailing
25316
+ * @param originalPriceStopLoss - Original stop loss before trailing
25317
+ * @param scheduledAt - Signal creation timestamp in milliseconds
25318
+ * @param pendingAt - Pending timestamp in milliseconds
25319
+ */
25320
+ this.averageBuy = async (symbol, currentPrice, effectivePriceOpen, totalEntries, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, originalPriceOpen) => {
25321
+ this.loggerService.log("strategyReportService averageBuy", {
25322
+ symbol,
25323
+ currentPrice,
25324
+ effectivePriceOpen,
25325
+ totalEntries,
25326
+ isBacktest,
25327
+ });
25328
+ if (!this.subscribe.hasValue()) {
25329
+ return;
25330
+ }
25331
+ const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
25332
+ exchangeName: context.exchangeName,
25333
+ strategyName: context.strategyName,
25334
+ frameName: context.frameName,
25335
+ });
25336
+ if (!pendingRow) {
25337
+ return;
25338
+ }
25339
+ const createdAt = new Date(timestamp).toISOString();
25340
+ await Report.writeData("strategy", {
25341
+ action: "average-buy",
25342
+ currentPrice,
25343
+ effectivePriceOpen,
25344
+ totalEntries,
25345
+ symbol,
25346
+ timestamp,
25347
+ createdAt,
25348
+ position,
25349
+ priceOpen,
25350
+ priceTakeProfit,
25351
+ priceStopLoss,
25352
+ originalPriceTakeProfit,
25353
+ originalPriceStopLoss,
25354
+ originalPriceOpen,
25355
+ scheduledAt,
25356
+ pendingAt,
25357
+ }, {
25358
+ signalId: pendingRow.id,
25359
+ exchangeName: context.exchangeName,
25360
+ frameName: context.frameName,
25361
+ strategyName: context.strategyName,
25362
+ symbol,
25363
+ walkerName: "",
25364
+ });
25365
+ };
24962
25366
  /**
24963
25367
  * Initializes the service for event logging.
24964
25368
  *
@@ -24989,43 +25393,50 @@ class StrategyReportService {
24989
25393
  exchangeName: event.exchangeName,
24990
25394
  frameName: event.frameName,
24991
25395
  strategyName: event.strategyName,
24992
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
25396
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
24993
25397
  const unPartialLoss = strategyCommitSubject
24994
25398
  .filter(({ action }) => action === "partial-loss")
24995
25399
  .connect(async (event) => await this.partialLoss(event.symbol, event.percentToClose, event.currentPrice, event.backtest, {
24996
25400
  exchangeName: event.exchangeName,
24997
25401
  frameName: event.frameName,
24998
25402
  strategyName: event.strategyName,
24999
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
25403
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
25000
25404
  const unTrailingStop = strategyCommitSubject
25001
25405
  .filter(({ action }) => action === "trailing-stop")
25002
25406
  .connect(async (event) => await this.trailingStop(event.symbol, event.percentShift, event.currentPrice, event.backtest, {
25003
25407
  exchangeName: event.exchangeName,
25004
25408
  frameName: event.frameName,
25005
25409
  strategyName: event.strategyName,
25006
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
25410
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
25007
25411
  const unTrailingTake = strategyCommitSubject
25008
25412
  .filter(({ action }) => action === "trailing-take")
25009
25413
  .connect(async (event) => await this.trailingTake(event.symbol, event.percentShift, event.currentPrice, event.backtest, {
25010
25414
  exchangeName: event.exchangeName,
25011
25415
  frameName: event.frameName,
25012
25416
  strategyName: event.strategyName,
25013
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
25417
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
25014
25418
  const unBreakeven = strategyCommitSubject
25015
25419
  .filter(({ action }) => action === "breakeven")
25016
25420
  .connect(async (event) => await this.breakeven(event.symbol, event.currentPrice, event.backtest, {
25017
25421
  exchangeName: event.exchangeName,
25018
25422
  frameName: event.frameName,
25019
25423
  strategyName: event.strategyName,
25020
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
25424
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
25021
25425
  const unActivateScheduled = strategyCommitSubject
25022
25426
  .filter(({ action }) => action === "activate-scheduled")
25023
25427
  .connect(async (event) => await this.activateScheduled(event.symbol, event.currentPrice, event.backtest, {
25024
25428
  exchangeName: event.exchangeName,
25025
25429
  frameName: event.frameName,
25026
25430
  strategyName: event.strategyName,
25027
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.activateId));
25028
- const disposeFn = functoolsKit.compose(() => unCancelSchedule(), () => unClosePending(), () => unPartialProfit(), () => unPartialLoss(), () => unTrailingStop(), () => unTrailingTake(), () => unBreakeven(), () => unActivateScheduled());
25431
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen, event.activateId));
25432
+ const unAverageBuy = strategyCommitSubject
25433
+ .filter(({ action }) => action === "average-buy")
25434
+ .connect(async (event) => await this.averageBuy(event.symbol, event.currentPrice, event.effectivePriceOpen, event.totalEntries, event.backtest, {
25435
+ exchangeName: event.exchangeName,
25436
+ frameName: event.frameName,
25437
+ strategyName: event.strategyName,
25438
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.originalPriceOpen));
25439
+ const disposeFn = functoolsKit.compose(() => unCancelSchedule(), () => unClosePending(), () => unPartialProfit(), () => unPartialLoss(), () => unTrailingStop(), () => unTrailingTake(), () => unBreakeven(), () => unActivateScheduled(), () => unAverageBuy());
25029
25440
  return () => {
25030
25441
  disposeFn();
25031
25442
  this.subscribe.clear();
@@ -25157,6 +25568,7 @@ class ReportStorage {
25157
25568
  trailingTakeCount: 0,
25158
25569
  breakevenCount: 0,
25159
25570
  activateScheduledCount: 0,
25571
+ averageBuyCount: 0,
25160
25572
  };
25161
25573
  }
25162
25574
  return {
@@ -25170,6 +25582,7 @@ class ReportStorage {
25170
25582
  trailingTakeCount: this._eventList.filter(e => e.action === "trailing-take").length,
25171
25583
  breakevenCount: this._eventList.filter(e => e.action === "breakeven").length,
25172
25584
  activateScheduledCount: this._eventList.filter(e => e.action === "activate-scheduled").length,
25585
+ averageBuyCount: this._eventList.filter(e => e.action === "average-buy").length,
25173
25586
  };
25174
25587
  }
25175
25588
  /**
@@ -25219,6 +25632,7 @@ class ReportStorage {
25219
25632
  `- Trailing take: ${stats.trailingTakeCount}`,
25220
25633
  `- Breakeven: ${stats.breakevenCount}`,
25221
25634
  `- Activate scheduled: ${stats.activateScheduledCount}`,
25635
+ `- Average buy: ${stats.averageBuyCount}`,
25222
25636
  ].join("\n");
25223
25637
  }
25224
25638
  /**
@@ -25406,7 +25820,7 @@ class StrategyMarkdownService {
25406
25820
  * @param scheduledAt - Signal creation timestamp in milliseconds
25407
25821
  * @param pendingAt - Pending timestamp in milliseconds
25408
25822
  */
25409
- this.partialProfit = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
25823
+ this.partialProfit = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
25410
25824
  this.loggerService.log("strategyMarkdownService partialProfit", {
25411
25825
  symbol,
25412
25826
  percentToClose,
@@ -25444,6 +25858,8 @@ class StrategyMarkdownService {
25444
25858
  priceStopLoss,
25445
25859
  originalPriceTakeProfit,
25446
25860
  originalPriceStopLoss,
25861
+ originalPriceOpen,
25862
+ totalEntries,
25447
25863
  scheduledAt,
25448
25864
  pendingAt,
25449
25865
  });
@@ -25466,7 +25882,7 @@ class StrategyMarkdownService {
25466
25882
  * @param scheduledAt - Signal creation timestamp in milliseconds
25467
25883
  * @param pendingAt - Pending timestamp in milliseconds
25468
25884
  */
25469
- this.partialLoss = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
25885
+ this.partialLoss = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
25470
25886
  this.loggerService.log("strategyMarkdownService partialLoss", {
25471
25887
  symbol,
25472
25888
  percentToClose,
@@ -25504,6 +25920,8 @@ class StrategyMarkdownService {
25504
25920
  priceStopLoss,
25505
25921
  originalPriceTakeProfit,
25506
25922
  originalPriceStopLoss,
25923
+ originalPriceOpen,
25924
+ totalEntries,
25507
25925
  scheduledAt,
25508
25926
  pendingAt,
25509
25927
  });
@@ -25526,7 +25944,7 @@ class StrategyMarkdownService {
25526
25944
  * @param scheduledAt - Signal creation timestamp in milliseconds
25527
25945
  * @param pendingAt - Pending timestamp in milliseconds
25528
25946
  */
25529
- this.trailingStop = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
25947
+ this.trailingStop = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
25530
25948
  this.loggerService.log("strategyMarkdownService trailingStop", {
25531
25949
  symbol,
25532
25950
  percentShift,
@@ -25564,6 +25982,8 @@ class StrategyMarkdownService {
25564
25982
  priceStopLoss,
25565
25983
  originalPriceTakeProfit,
25566
25984
  originalPriceStopLoss,
25985
+ originalPriceOpen,
25986
+ totalEntries,
25567
25987
  scheduledAt,
25568
25988
  pendingAt,
25569
25989
  });
@@ -25586,7 +26006,7 @@ class StrategyMarkdownService {
25586
26006
  * @param scheduledAt - Signal creation timestamp in milliseconds
25587
26007
  * @param pendingAt - Pending timestamp in milliseconds
25588
26008
  */
25589
- this.trailingTake = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
26009
+ this.trailingTake = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
25590
26010
  this.loggerService.log("strategyMarkdownService trailingTake", {
25591
26011
  symbol,
25592
26012
  percentShift,
@@ -25624,6 +26044,8 @@ class StrategyMarkdownService {
25624
26044
  priceStopLoss,
25625
26045
  originalPriceTakeProfit,
25626
26046
  originalPriceStopLoss,
26047
+ originalPriceOpen,
26048
+ totalEntries,
25627
26049
  scheduledAt,
25628
26050
  pendingAt,
25629
26051
  });
@@ -25645,7 +26067,7 @@ class StrategyMarkdownService {
25645
26067
  * @param scheduledAt - Signal creation timestamp in milliseconds
25646
26068
  * @param pendingAt - Pending timestamp in milliseconds
25647
26069
  */
25648
- this.breakeven = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
26070
+ this.breakeven = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
25649
26071
  this.loggerService.log("strategyMarkdownService breakeven", {
25650
26072
  symbol,
25651
26073
  currentPrice,
@@ -25681,6 +26103,8 @@ class StrategyMarkdownService {
25681
26103
  priceStopLoss,
25682
26104
  originalPriceTakeProfit,
25683
26105
  originalPriceStopLoss,
26106
+ originalPriceOpen,
26107
+ totalEntries,
25684
26108
  scheduledAt,
25685
26109
  pendingAt,
25686
26110
  });
@@ -25703,7 +26127,7 @@ class StrategyMarkdownService {
25703
26127
  * @param pendingAt - Pending timestamp in milliseconds
25704
26128
  * @param activateId - Optional identifier for the activation reason
25705
26129
  */
25706
- this.activateScheduled = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, activateId) => {
26130
+ this.activateScheduled = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen, activateId) => {
25707
26131
  this.loggerService.log("strategyMarkdownService activateScheduled", {
25708
26132
  symbol,
25709
26133
  currentPrice,
@@ -25741,6 +26165,72 @@ class StrategyMarkdownService {
25741
26165
  priceStopLoss,
25742
26166
  originalPriceTakeProfit,
25743
26167
  originalPriceStopLoss,
26168
+ originalPriceOpen,
26169
+ totalEntries,
26170
+ scheduledAt,
26171
+ pendingAt,
26172
+ });
26173
+ };
26174
+ /**
26175
+ * Records an average-buy (DCA) event when a new averaging entry is added to an open position.
26176
+ *
26177
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
26178
+ * @param currentPrice - Price at which the new averaging entry was executed
26179
+ * @param effectivePriceOpen - Averaged entry price after this addition
26180
+ * @param totalEntries - Total number of DCA entries after this addition
26181
+ * @param isBacktest - Whether this is a backtest or live trading event
26182
+ * @param context - Strategy context with strategyName, exchangeName, frameName
26183
+ * @param timestamp - Timestamp from StrategyCommitContract (execution context time)
26184
+ * @param position - Trade direction: "long" or "short"
26185
+ * @param priceOpen - Original entry price (unchanged by averaging)
26186
+ * @param priceTakeProfit - Effective take profit price
26187
+ * @param priceStopLoss - Effective stop loss price
26188
+ * @param originalPriceTakeProfit - Original take profit before trailing
26189
+ * @param originalPriceStopLoss - Original stop loss before trailing
26190
+ * @param scheduledAt - Signal creation timestamp in milliseconds
26191
+ * @param pendingAt - Pending timestamp in milliseconds
26192
+ */
26193
+ this.averageBuy = async (symbol, currentPrice, effectivePriceOpen, totalEntries, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, originalPriceOpen) => {
26194
+ this.loggerService.log("strategyMarkdownService averageBuy", {
26195
+ symbol,
26196
+ currentPrice,
26197
+ effectivePriceOpen,
26198
+ totalEntries,
26199
+ isBacktest,
26200
+ });
26201
+ if (!this.subscribe.hasValue()) {
26202
+ return;
26203
+ }
26204
+ const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
26205
+ exchangeName: context.exchangeName,
26206
+ strategyName: context.strategyName,
26207
+ frameName: context.frameName,
26208
+ });
26209
+ if (!pendingRow) {
26210
+ return;
26211
+ }
26212
+ const createdAt = new Date(timestamp).toISOString();
26213
+ const storage = this.getStorage(symbol, context.strategyName, context.exchangeName, context.frameName, isBacktest);
26214
+ storage.addEvent({
26215
+ timestamp,
26216
+ symbol,
26217
+ strategyName: context.strategyName,
26218
+ exchangeName: context.exchangeName,
26219
+ frameName: context.frameName,
26220
+ signalId: pendingRow.id,
26221
+ action: "average-buy",
26222
+ currentPrice,
26223
+ effectivePriceOpen,
26224
+ totalEntries,
26225
+ createdAt,
26226
+ backtest: isBacktest,
26227
+ position,
26228
+ priceOpen,
26229
+ priceTakeProfit,
26230
+ priceStopLoss,
26231
+ originalPriceTakeProfit,
26232
+ originalPriceStopLoss,
26233
+ originalPriceOpen,
25744
26234
  scheduledAt,
25745
26235
  pendingAt,
25746
26236
  });
@@ -25889,43 +26379,50 @@ class StrategyMarkdownService {
25889
26379
  exchangeName: event.exchangeName,
25890
26380
  frameName: event.frameName,
25891
26381
  strategyName: event.strategyName,
25892
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
26382
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
25893
26383
  const unPartialLoss = strategyCommitSubject
25894
26384
  .filter(({ action }) => action === "partial-loss")
25895
26385
  .connect(async (event) => await this.partialLoss(event.symbol, event.percentToClose, event.currentPrice, event.backtest, {
25896
26386
  exchangeName: event.exchangeName,
25897
26387
  frameName: event.frameName,
25898
26388
  strategyName: event.strategyName,
25899
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
26389
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
25900
26390
  const unTrailingStop = strategyCommitSubject
25901
26391
  .filter(({ action }) => action === "trailing-stop")
25902
26392
  .connect(async (event) => await this.trailingStop(event.symbol, event.percentShift, event.currentPrice, event.backtest, {
25903
26393
  exchangeName: event.exchangeName,
25904
26394
  frameName: event.frameName,
25905
26395
  strategyName: event.strategyName,
25906
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
26396
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
25907
26397
  const unTrailingTake = strategyCommitSubject
25908
26398
  .filter(({ action }) => action === "trailing-take")
25909
26399
  .connect(async (event) => await this.trailingTake(event.symbol, event.percentShift, event.currentPrice, event.backtest, {
25910
26400
  exchangeName: event.exchangeName,
25911
26401
  frameName: event.frameName,
25912
26402
  strategyName: event.strategyName,
25913
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
26403
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
25914
26404
  const unBreakeven = strategyCommitSubject
25915
26405
  .filter(({ action }) => action === "breakeven")
25916
26406
  .connect(async (event) => await this.breakeven(event.symbol, event.currentPrice, event.backtest, {
25917
26407
  exchangeName: event.exchangeName,
25918
26408
  frameName: event.frameName,
25919
26409
  strategyName: event.strategyName,
25920
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
26410
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
25921
26411
  const unActivateScheduled = strategyCommitSubject
25922
26412
  .filter(({ action }) => action === "activate-scheduled")
25923
26413
  .connect(async (event) => await this.activateScheduled(event.symbol, event.currentPrice, event.backtest, {
25924
26414
  exchangeName: event.exchangeName,
25925
26415
  frameName: event.frameName,
25926
26416
  strategyName: event.strategyName,
25927
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.activateId));
25928
- const disposeFn = functoolsKit.compose(() => unCancelSchedule(), () => unClosePending(), () => unPartialProfit(), () => unPartialLoss(), () => unTrailingStop(), () => unTrailingTake(), () => unBreakeven(), () => unActivateScheduled());
26417
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen, event.activateId));
26418
+ const unAverageBuy = strategyCommitSubject
26419
+ .filter(({ action }) => action === "average-buy")
26420
+ .connect(async (event) => await this.averageBuy(event.symbol, event.currentPrice, event.effectivePriceOpen, event.totalEntries, event.backtest, {
26421
+ exchangeName: event.exchangeName,
26422
+ frameName: event.frameName,
26423
+ strategyName: event.strategyName,
26424
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.originalPriceOpen));
26425
+ const disposeFn = functoolsKit.compose(() => unCancelSchedule(), () => unClosePending(), () => unPartialProfit(), () => unPartialLoss(), () => unTrailingStop(), () => unTrailingTake(), () => unBreakeven(), () => unActivateScheduled(), () => unAverageBuy());
25929
26426
  return () => {
25930
26427
  disposeFn();
25931
26428
  this.subscribe.clear();
@@ -27731,6 +28228,7 @@ const TRAILING_STOP_METHOD_NAME = "strategy.commitTrailingStop";
27731
28228
  const TRAILING_PROFIT_METHOD_NAME = "strategy.commitTrailingTake";
27732
28229
  const BREAKEVEN_METHOD_NAME = "strategy.commitBreakeven";
27733
28230
  const ACTIVATE_SCHEDULED_METHOD_NAME = "strategy.commitActivateScheduled";
28231
+ const AVERAGE_BUY_METHOD_NAME = "strategy.commitAverageBuy";
27734
28232
  /**
27735
28233
  * Cancels the scheduled signal without stopping the strategy.
27736
28234
  *
@@ -28083,6 +28581,45 @@ async function commitActivateScheduled(symbol, activateId) {
28083
28581
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
28084
28582
  await bt.strategyCoreService.activateScheduled(isBacktest, symbol, { exchangeName, frameName, strategyName }, activateId);
28085
28583
  }
28584
+ /**
28585
+ * Adds a new DCA entry to the active pending signal.
28586
+ *
28587
+ * Adds a new averaging entry at the current market price to the position's
28588
+ * entry history. Updates effectivePriceOpen (mean of all entries) and emits
28589
+ * an average-buy commit event.
28590
+ *
28591
+ * Automatically detects backtest/live mode from execution context.
28592
+ * Automatically fetches current price via getAveragePrice.
28593
+ *
28594
+ * @param symbol - Trading pair symbol
28595
+ * @returns Promise<boolean> - true if entry added, false if rejected
28596
+ *
28597
+ * @example
28598
+ * ```typescript
28599
+ * import { commitAverageBuy } from "backtest-kit";
28600
+ *
28601
+ * // Add DCA entry at current market price
28602
+ * const success = await commitAverageBuy("BTCUSDT");
28603
+ * if (success) {
28604
+ * console.log("DCA entry added");
28605
+ * }
28606
+ * ```
28607
+ */
28608
+ async function commitAverageBuy(symbol) {
28609
+ bt.loggerService.info(AVERAGE_BUY_METHOD_NAME, {
28610
+ symbol,
28611
+ });
28612
+ if (!ExecutionContextService.hasContext()) {
28613
+ throw new Error("commitAverageBuy requires an execution context");
28614
+ }
28615
+ if (!MethodContextService.hasContext()) {
28616
+ throw new Error("commitAverageBuy requires a method context");
28617
+ }
28618
+ const currentPrice = await getAveragePrice(symbol);
28619
+ const { backtest: isBacktest } = bt.executionContextService.context;
28620
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
28621
+ return await bt.strategyCoreService.averageBuy(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
28622
+ }
28086
28623
 
28087
28624
  const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
28088
28625
  /**
@@ -30363,6 +30900,7 @@ const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.commitPartialLoss";
30363
30900
  const BACKTEST_METHOD_NAME_TRAILING_STOP = "BacktestUtils.commitTrailingStop";
30364
30901
  const BACKTEST_METHOD_NAME_TRAILING_PROFIT = "BacktestUtils.commitTrailingTake";
30365
30902
  const BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED = "Backtest.commitActivateScheduled";
30903
+ const BACKTEST_METHOD_NAME_AVERAGE_BUY = "Backtest.commitAverageBuy";
30366
30904
  const BACKTEST_METHOD_NAME_GET_DATA = "BacktestUtils.getData";
30367
30905
  /**
30368
30906
  * Internal task function that runs backtest and handles completion.
@@ -31259,6 +31797,49 @@ class BacktestUtils {
31259
31797
  }
31260
31798
  await bt.strategyCoreService.activateScheduled(true, symbol, context, activateId);
31261
31799
  };
31800
+ /**
31801
+ * Adds a new DCA entry to the active pending signal.
31802
+ *
31803
+ * Adds a new averaging entry at currentPrice to the position's entry history.
31804
+ * Updates effectivePriceOpen (mean of all entries) and emits average-buy commit event.
31805
+ *
31806
+ * @param symbol - Trading pair symbol
31807
+ * @param currentPrice - New entry price to add to the averaging history
31808
+ * @param context - Execution context with strategyName, exchangeName, frameName
31809
+ * @returns Promise<boolean> - true if entry added, false if rejected
31810
+ *
31811
+ * @example
31812
+ * ```typescript
31813
+ * // Add DCA entry at current price
31814
+ * const success = await Backtest.commitAverageBuy("BTCUSDT", 42000, {
31815
+ * strategyName: "my-strategy",
31816
+ * exchangeName: "binance",
31817
+ * frameName: "1h"
31818
+ * });
31819
+ * if (success) {
31820
+ * console.log('DCA entry added');
31821
+ * }
31822
+ * ```
31823
+ */
31824
+ this.commitAverageBuy = async (symbol, currentPrice, context) => {
31825
+ bt.loggerService.info(BACKTEST_METHOD_NAME_AVERAGE_BUY, {
31826
+ symbol,
31827
+ currentPrice,
31828
+ context,
31829
+ });
31830
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_AVERAGE_BUY);
31831
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_AVERAGE_BUY);
31832
+ {
31833
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
31834
+ riskName &&
31835
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_AVERAGE_BUY);
31836
+ riskList &&
31837
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_AVERAGE_BUY));
31838
+ actions &&
31839
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_AVERAGE_BUY));
31840
+ }
31841
+ return await bt.strategyCoreService.averageBuy(true, symbol, currentPrice, context);
31842
+ };
31262
31843
  /**
31263
31844
  * Gets statistical data from all closed signals for a symbol-strategy pair.
31264
31845
  *
@@ -31435,6 +32016,7 @@ const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.commitPartialLoss";
31435
32016
  const LIVE_METHOD_NAME_TRAILING_STOP = "LiveUtils.commitTrailingStop";
31436
32017
  const LIVE_METHOD_NAME_TRAILING_PROFIT = "LiveUtils.commitTrailingTake";
31437
32018
  const LIVE_METHOD_NAME_ACTIVATE_SCHEDULED = "Live.commitActivateScheduled";
32019
+ const LIVE_METHOD_NAME_AVERAGE_BUY = "Live.commitAverageBuy";
31438
32020
  /**
31439
32021
  * Internal task function that runs live trading and handles completion.
31440
32022
  * Consumes live trading results and updates instance state flags.
@@ -32299,6 +32881,49 @@ class LiveUtils {
32299
32881
  frameName: "",
32300
32882
  }, activateId);
32301
32883
  };
32884
+ /**
32885
+ * Adds a new DCA entry to the active pending signal.
32886
+ *
32887
+ * Adds a new averaging entry at currentPrice to the position's entry history.
32888
+ * Updates effectivePriceOpen (mean of all entries) and emits average-buy commit event.
32889
+ *
32890
+ * @param symbol - Trading pair symbol
32891
+ * @param currentPrice - New entry price to add to the averaging history
32892
+ * @param context - Execution context with strategyName and exchangeName
32893
+ * @returns Promise<boolean> - true if entry added, false if rejected
32894
+ *
32895
+ * @example
32896
+ * ```typescript
32897
+ * // Add DCA entry at current price
32898
+ * const success = await Live.commitAverageBuy("BTCUSDT", 42000, {
32899
+ * strategyName: "my-strategy",
32900
+ * exchangeName: "binance"
32901
+ * });
32902
+ * if (success) {
32903
+ * console.log('DCA entry added');
32904
+ * }
32905
+ * ```
32906
+ */
32907
+ this.commitAverageBuy = async (symbol, currentPrice, context) => {
32908
+ bt.loggerService.info(LIVE_METHOD_NAME_AVERAGE_BUY, {
32909
+ symbol,
32910
+ currentPrice,
32911
+ context,
32912
+ });
32913
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_AVERAGE_BUY);
32914
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_AVERAGE_BUY);
32915
+ {
32916
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
32917
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_AVERAGE_BUY);
32918
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_AVERAGE_BUY));
32919
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_AVERAGE_BUY));
32920
+ }
32921
+ return await bt.strategyCoreService.averageBuy(false, symbol, currentPrice, {
32922
+ strategyName: context.strategyName,
32923
+ exchangeName: context.exchangeName,
32924
+ frameName: "",
32925
+ });
32926
+ };
32302
32927
  /**
32303
32928
  * Gets statistical data from all live trading events for a symbol-strategy pair.
32304
32929
  *
@@ -34962,6 +35587,8 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
34962
35587
  priceStopLoss: data.signal.priceStopLoss,
34963
35588
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
34964
35589
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
35590
+ originalPriceOpen: data.signal.originalPriceOpen,
35591
+ totalEntries: data.signal.totalEntries,
34965
35592
  note: data.signal.note,
34966
35593
  scheduledAt: data.signal.scheduledAt,
34967
35594
  pendingAt: data.signal.pendingAt,
@@ -34987,6 +35614,8 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
34987
35614
  priceStopLoss: data.signal.priceStopLoss,
34988
35615
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
34989
35616
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
35617
+ originalPriceOpen: data.signal.originalPriceOpen,
35618
+ totalEntries: data.signal.totalEntries,
34990
35619
  pnlPercentage: data.pnl.pnlPercentage,
34991
35620
  closeReason: data.closeReason,
34992
35621
  duration: durationMin,
@@ -35012,6 +35641,8 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
35012
35641
  priceStopLoss: data.signal.priceStopLoss,
35013
35642
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
35014
35643
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
35644
+ originalPriceOpen: data.signal.originalPriceOpen,
35645
+ totalEntries: data.signal.totalEntries,
35015
35646
  scheduledAt: data.signal.scheduledAt,
35016
35647
  currentPrice: data.currentPrice,
35017
35648
  createdAt: data.createdAt,
@@ -35035,6 +35666,8 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
35035
35666
  priceStopLoss: data.signal.priceStopLoss,
35036
35667
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
35037
35668
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
35669
+ originalPriceOpen: data.signal.originalPriceOpen,
35670
+ totalEntries: data.signal.totalEntries,
35038
35671
  cancelReason: data.reason,
35039
35672
  cancelId: data.cancelId,
35040
35673
  duration: durationMin,
@@ -35067,6 +35700,8 @@ const CREATE_PARTIAL_PROFIT_NOTIFICATION_FN = (data) => ({
35067
35700
  priceStopLoss: data.data.priceStopLoss,
35068
35701
  originalPriceTakeProfit: data.data.originalPriceTakeProfit,
35069
35702
  originalPriceStopLoss: data.data.originalPriceStopLoss,
35703
+ originalPriceOpen: data.data.originalPriceOpen,
35704
+ totalEntries: data.data.totalEntries,
35070
35705
  scheduledAt: data.data.scheduledAt,
35071
35706
  pendingAt: data.data.pendingAt,
35072
35707
  createdAt: data.timestamp,
@@ -35093,6 +35728,8 @@ const CREATE_PARTIAL_LOSS_NOTIFICATION_FN = (data) => ({
35093
35728
  priceStopLoss: data.data.priceStopLoss,
35094
35729
  originalPriceTakeProfit: data.data.originalPriceTakeProfit,
35095
35730
  originalPriceStopLoss: data.data.originalPriceStopLoss,
35731
+ originalPriceOpen: data.data.originalPriceOpen,
35732
+ totalEntries: data.data.totalEntries,
35096
35733
  scheduledAt: data.data.scheduledAt,
35097
35734
  pendingAt: data.data.pendingAt,
35098
35735
  createdAt: data.timestamp,
@@ -35118,6 +35755,8 @@ const CREATE_BREAKEVEN_NOTIFICATION_FN = (data) => ({
35118
35755
  priceStopLoss: data.data.priceStopLoss,
35119
35756
  originalPriceTakeProfit: data.data.originalPriceTakeProfit,
35120
35757
  originalPriceStopLoss: data.data.originalPriceStopLoss,
35758
+ originalPriceOpen: data.data.originalPriceOpen,
35759
+ totalEntries: data.data.totalEntries,
35121
35760
  scheduledAt: data.data.scheduledAt,
35122
35761
  pendingAt: data.data.pendingAt,
35123
35762
  createdAt: data.timestamp,
@@ -35148,6 +35787,8 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
35148
35787
  priceStopLoss: data.priceStopLoss,
35149
35788
  originalPriceTakeProfit: data.originalPriceTakeProfit,
35150
35789
  originalPriceStopLoss: data.originalPriceStopLoss,
35790
+ originalPriceOpen: data.originalPriceOpen,
35791
+ totalEntries: data.totalEntries,
35151
35792
  scheduledAt: data.scheduledAt,
35152
35793
  pendingAt: data.pendingAt,
35153
35794
  createdAt: data.timestamp,
@@ -35171,6 +35812,8 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
35171
35812
  priceStopLoss: data.priceStopLoss,
35172
35813
  originalPriceTakeProfit: data.originalPriceTakeProfit,
35173
35814
  originalPriceStopLoss: data.originalPriceStopLoss,
35815
+ originalPriceOpen: data.originalPriceOpen,
35816
+ totalEntries: data.totalEntries,
35174
35817
  scheduledAt: data.scheduledAt,
35175
35818
  pendingAt: data.pendingAt,
35176
35819
  createdAt: data.timestamp,
@@ -35193,6 +35836,8 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
35193
35836
  priceStopLoss: data.priceStopLoss,
35194
35837
  originalPriceTakeProfit: data.originalPriceTakeProfit,
35195
35838
  originalPriceStopLoss: data.originalPriceStopLoss,
35839
+ originalPriceOpen: data.originalPriceOpen,
35840
+ totalEntries: data.totalEntries,
35196
35841
  scheduledAt: data.scheduledAt,
35197
35842
  pendingAt: data.pendingAt,
35198
35843
  createdAt: data.timestamp,
@@ -35216,6 +35861,8 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
35216
35861
  priceStopLoss: data.priceStopLoss,
35217
35862
  originalPriceTakeProfit: data.originalPriceTakeProfit,
35218
35863
  originalPriceStopLoss: data.originalPriceStopLoss,
35864
+ originalPriceOpen: data.originalPriceOpen,
35865
+ totalEntries: data.totalEntries,
35219
35866
  scheduledAt: data.scheduledAt,
35220
35867
  pendingAt: data.pendingAt,
35221
35868
  createdAt: data.timestamp,
@@ -35239,6 +35886,8 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
35239
35886
  priceStopLoss: data.priceStopLoss,
35240
35887
  originalPriceTakeProfit: data.originalPriceTakeProfit,
35241
35888
  originalPriceStopLoss: data.originalPriceStopLoss,
35889
+ originalPriceOpen: data.originalPriceOpen,
35890
+ totalEntries: data.totalEntries,
35242
35891
  scheduledAt: data.scheduledAt,
35243
35892
  pendingAt: data.pendingAt,
35244
35893
  createdAt: data.timestamp,
@@ -35262,6 +35911,33 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
35262
35911
  priceStopLoss: data.priceStopLoss,
35263
35912
  originalPriceTakeProfit: data.originalPriceTakeProfit,
35264
35913
  originalPriceStopLoss: data.originalPriceStopLoss,
35914
+ originalPriceOpen: data.originalPriceOpen,
35915
+ totalEntries: data.totalEntries,
35916
+ scheduledAt: data.scheduledAt,
35917
+ pendingAt: data.pendingAt,
35918
+ createdAt: data.timestamp,
35919
+ };
35920
+ }
35921
+ if (data.action === "average-buy") {
35922
+ return {
35923
+ type: "average_buy.commit",
35924
+ id: CREATE_KEY_FN$1(),
35925
+ timestamp: data.timestamp,
35926
+ backtest: data.backtest,
35927
+ symbol: data.symbol,
35928
+ strategyName: data.strategyName,
35929
+ exchangeName: data.exchangeName,
35930
+ signalId: data.signalId,
35931
+ currentPrice: data.currentPrice,
35932
+ effectivePriceOpen: data.effectivePriceOpen,
35933
+ totalEntries: data.totalEntries,
35934
+ position: data.position,
35935
+ priceOpen: data.priceOpen,
35936
+ priceTakeProfit: data.priceTakeProfit,
35937
+ priceStopLoss: data.priceStopLoss,
35938
+ originalPriceTakeProfit: data.originalPriceTakeProfit,
35939
+ originalPriceStopLoss: data.originalPriceStopLoss,
35940
+ originalPriceOpen: data.originalPriceOpen,
35265
35941
  scheduledAt: data.scheduledAt,
35266
35942
  pendingAt: data.pendingAt,
35267
35943
  createdAt: data.timestamp,
@@ -37651,6 +38327,7 @@ exports.addWalkerSchema = addWalkerSchema;
37651
38327
  exports.alignToInterval = alignToInterval;
37652
38328
  exports.checkCandles = checkCandles;
37653
38329
  exports.commitActivateScheduled = commitActivateScheduled;
38330
+ exports.commitAverageBuy = commitAverageBuy;
37654
38331
  exports.commitBreakeven = commitBreakeven;
37655
38332
  exports.commitCancelScheduled = commitCancelScheduled;
37656
38333
  exports.commitClosePending = commitClosePending;