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.mjs CHANGED
@@ -2880,6 +2880,27 @@ class ExchangeConnectionService {
2880
2880
  }
2881
2881
  }
2882
2882
 
2883
+ /**
2884
+ * Returns the effective entry price for price calculations.
2885
+ *
2886
+ * When the _entry array exists and has at least one element, returns
2887
+ * the simple arithmetic mean of all entry prices (DCA average).
2888
+ * Otherwise returns the original signal.priceOpen.
2889
+ *
2890
+ * This mirrors the _trailingPriceStopLoss pattern: original price is preserved
2891
+ * in signal.priceOpen (for identity/tracking), while calculations use the
2892
+ * effective averaged price returned by this function.
2893
+ *
2894
+ * @param signal - Signal row (ISignalRow or IScheduledSignalRow)
2895
+ * @returns Effective entry price for distance and PNL calculations
2896
+ */
2897
+ const getEffectivePriceOpen = (signal) => {
2898
+ if (signal._entry && signal._entry.length > 0) {
2899
+ return signal._entry.reduce((sum, e) => sum + e.price, 0) / signal._entry.length;
2900
+ }
2901
+ return signal.priceOpen;
2902
+ };
2903
+
2883
2904
  /**
2884
2905
  * Calculates profit/loss for a closed signal with slippage and fees.
2885
2906
  *
@@ -2887,7 +2908,7 @@ class ExchangeConnectionService {
2887
2908
  * - Calculates weighted PNL: Σ(percent_i × pnl_i) for each partial + (remaining% × final_pnl)
2888
2909
  * - Each partial close has its own slippage
2889
2910
  * - Open fee is charged once; close fees are proportional to each partial's size
2890
- * - Total fees = CC_PERCENT_FEE (open) + CC_PERCENT_FEE × 1 (closes sum to 100%) = 2 × CC_PERCENT_FEE
2911
+ * - Total fees = CC_PERCENT_FEE (open) + Σ CC_PERCENT_FEE × (partial% / 100) × (closeWithSlip / openWithSlip)
2891
2912
  *
2892
2913
  * Formula breakdown:
2893
2914
  * 1. Apply slippage to open/close prices (worse execution)
@@ -2896,7 +2917,7 @@ class ExchangeConnectionService {
2896
2917
  * 2. Calculate raw PNL percentage
2897
2918
  * - LONG: ((closePrice - openPrice) / openPrice) * 100
2898
2919
  * - SHORT: ((openPrice - closePrice) / openPrice) * 100
2899
- * 3. Subtract total fees (0.1% * 2 = 0.2% per transaction)
2920
+ * 3. Subtract total fees: open fee + close fee adjusted for slippage-affected execution price
2900
2921
  *
2901
2922
  * @param signal - Closed signal with position details and optional partial history
2902
2923
  * @param priceClose - Actual close price at final exit
@@ -2930,73 +2951,53 @@ class ExchangeConnectionService {
2930
2951
  * ```
2931
2952
  */
2932
2953
  const toProfitLossDto = (signal, priceClose) => {
2933
- const priceOpen = signal.priceOpen;
2954
+ const priceOpen = getEffectivePriceOpen(signal);
2934
2955
  // Calculate weighted PNL with partial closes
2935
2956
  if (signal._partial && signal._partial.length > 0) {
2936
2957
  let totalWeightedPnl = 0;
2937
2958
  // Open fee is paid once for the whole position
2938
2959
  let totalFees = GLOBAL_CONFIG.CC_PERCENT_FEE;
2960
+ // priceOpenWithSlippage is the same for all partials — compute once
2961
+ const priceOpenWithSlippage = signal.position === "long"
2962
+ ? priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
2963
+ : priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2939
2964
  // Calculate PNL for each partial close
2940
2965
  for (const partial of signal._partial) {
2941
2966
  const partialPercent = partial.percent;
2942
- const partialPrice = partial.price;
2943
- // Apply slippage to prices
2944
- let priceOpenWithSlippage;
2945
- let priceCloseWithSlippage;
2946
- if (signal.position === "long") {
2947
- priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2948
- priceCloseWithSlippage = partialPrice * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2949
- }
2950
- else {
2951
- priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2952
- priceCloseWithSlippage = partialPrice * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2953
- }
2967
+ const priceCloseWithSlippage = signal.position === "long"
2968
+ ? partial.price * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
2969
+ : partial.price * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2954
2970
  // Calculate PNL for this partial
2955
- let partialPnl;
2956
- if (signal.position === "long") {
2957
- partialPnl = ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
2958
- }
2959
- else {
2960
- partialPnl = ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
2961
- }
2971
+ const partialPnl = signal.position === "long"
2972
+ ? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
2973
+ : ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
2962
2974
  // Weight by percentage of position closed
2963
- const weightedPnl = (partialPercent / 100) * partialPnl;
2964
- totalWeightedPnl += weightedPnl;
2965
- // Close fee is proportional to the size of this partial
2966
- totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (partialPercent / 100);
2975
+ totalWeightedPnl += (partialPercent / 100) * partialPnl;
2976
+ // Close fee is proportional to the size of this partial and adjusted for slippage
2977
+ totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (partialPercent / 100) * (priceCloseWithSlippage / priceOpenWithSlippage);
2967
2978
  }
2968
2979
  // Calculate PNL for remaining position (if any)
2969
2980
  // Compute totalClosed from _partial array
2970
2981
  const totalClosed = signal._partial.reduce((sum, p) => sum + p.percent, 0);
2982
+ if (totalClosed > 100) {
2983
+ throw new Error(`Partial closes exceed 100%: ${totalClosed}% (signal id: ${signal.id})`);
2984
+ }
2971
2985
  const remainingPercent = 100 - totalClosed;
2972
2986
  if (remainingPercent > 0) {
2973
- // Apply slippage
2974
- let priceOpenWithSlippage;
2975
- let priceCloseWithSlippage;
2976
- if (signal.position === "long") {
2977
- priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2978
- priceCloseWithSlippage = priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2979
- }
2980
- else {
2981
- priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2982
- priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2983
- }
2987
+ const priceCloseWithSlippage = signal.position === "long"
2988
+ ? priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
2989
+ : priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2984
2990
  // Calculate PNL for remaining
2985
- let remainingPnl;
2986
- if (signal.position === "long") {
2987
- remainingPnl = ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
2988
- }
2989
- else {
2990
- remainingPnl = ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
2991
- }
2991
+ const remainingPnl = signal.position === "long"
2992
+ ? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
2993
+ : ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
2992
2994
  // Weight by remaining percentage
2993
- const weightedRemainingPnl = (remainingPercent / 100) * remainingPnl;
2994
- totalWeightedPnl += weightedRemainingPnl;
2995
- // Close fee is proportional to the remaining size
2996
- totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (remainingPercent / 100);
2995
+ totalWeightedPnl += (remainingPercent / 100) * remainingPnl;
2996
+ // Close fee is proportional to the remaining size and adjusted for slippage
2997
+ totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (remainingPercent / 100) * (priceCloseWithSlippage / priceOpenWithSlippage);
2997
2998
  }
2998
2999
  // Subtract total fees from weighted PNL
2999
- // totalFees = CC_PERCENT_FEE (open) + CC_PERCENT_FEE × 1 (all closes sum to 100%) = 2 × CC_PERCENT_FEE
3000
+ // totalFees = CC_PERCENT_FEE (open) + Σ CC_PERCENT_FEE × (partialPercent/100) × (closeWithSlip/openWithSlip)
3000
3001
  const pnlPercentage = totalWeightedPnl - totalFees;
3001
3002
  return {
3002
3003
  pnlPercentage,
@@ -3017,8 +3018,8 @@ const toProfitLossDto = (signal, priceClose) => {
3017
3018
  priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3018
3019
  priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3019
3020
  }
3020
- // Применяем комиссию дважды (при открытии и закрытии)
3021
- const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE * 2;
3021
+ // Открытие: комиссия от цены входа; закрытие: комиссия от фактической цены выхода (с учётом slippage)
3022
+ const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE * (1 + priceCloseWithSlippage / priceOpenWithSlippage);
3022
3023
  let pnlPercentage;
3023
3024
  if (signal.position === "long") {
3024
3025
  // LONG: прибыль при росте цены
@@ -3204,6 +3205,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3204
3205
  percentToClose: commit.percentToClose,
3205
3206
  currentPrice: commit.currentPrice,
3206
3207
  timestamp,
3208
+ totalEntries: publicSignal.totalEntries,
3207
3209
  position: publicSignal.position,
3208
3210
  priceOpen: publicSignal.priceOpen,
3209
3211
  signalId: publicSignal.id,
@@ -3211,6 +3213,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3211
3213
  priceStopLoss: publicSignal.priceStopLoss,
3212
3214
  originalPriceTakeProfit: publicSignal.originalPriceTakeProfit,
3213
3215
  originalPriceStopLoss: publicSignal.originalPriceStopLoss,
3216
+ originalPriceOpen: publicSignal.originalPriceOpen,
3214
3217
  scheduledAt: publicSignal.scheduledAt,
3215
3218
  pendingAt: publicSignal.pendingAt,
3216
3219
  });
@@ -3227,6 +3230,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3227
3230
  percentToClose: commit.percentToClose,
3228
3231
  currentPrice: commit.currentPrice,
3229
3232
  timestamp,
3233
+ totalEntries: publicSignal.totalEntries,
3230
3234
  position: publicSignal.position,
3231
3235
  priceOpen: publicSignal.priceOpen,
3232
3236
  signalId: publicSignal.id,
@@ -3234,6 +3238,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3234
3238
  priceStopLoss: publicSignal.priceStopLoss,
3235
3239
  originalPriceTakeProfit: publicSignal.originalPriceTakeProfit,
3236
3240
  originalPriceStopLoss: publicSignal.originalPriceStopLoss,
3241
+ originalPriceOpen: publicSignal.originalPriceOpen,
3237
3242
  scheduledAt: publicSignal.scheduledAt,
3238
3243
  pendingAt: publicSignal.pendingAt,
3239
3244
  });
@@ -3249,6 +3254,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3249
3254
  backtest: commit.backtest,
3250
3255
  currentPrice: commit.currentPrice,
3251
3256
  timestamp,
3257
+ totalEntries: publicSignal.totalEntries,
3252
3258
  signalId: publicSignal.id,
3253
3259
  position: publicSignal.position,
3254
3260
  priceOpen: publicSignal.priceOpen,
@@ -3256,6 +3262,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3256
3262
  priceStopLoss: publicSignal.priceStopLoss,
3257
3263
  originalPriceTakeProfit: publicSignal.originalPriceTakeProfit,
3258
3264
  originalPriceStopLoss: publicSignal.originalPriceStopLoss,
3265
+ originalPriceOpen: publicSignal.originalPriceOpen,
3259
3266
  scheduledAt: publicSignal.scheduledAt,
3260
3267
  pendingAt: publicSignal.pendingAt,
3261
3268
  });
@@ -3272,6 +3279,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3272
3279
  percentShift: commit.percentShift,
3273
3280
  currentPrice: commit.currentPrice,
3274
3281
  timestamp,
3282
+ totalEntries: publicSignal.totalEntries,
3275
3283
  signalId: publicSignal.id,
3276
3284
  position: publicSignal.position,
3277
3285
  priceOpen: publicSignal.priceOpen,
@@ -3279,6 +3287,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3279
3287
  priceStopLoss: publicSignal.priceStopLoss,
3280
3288
  originalPriceTakeProfit: publicSignal.originalPriceTakeProfit,
3281
3289
  originalPriceStopLoss: publicSignal.originalPriceStopLoss,
3290
+ originalPriceOpen: publicSignal.originalPriceOpen,
3282
3291
  scheduledAt: publicSignal.scheduledAt,
3283
3292
  pendingAt: publicSignal.pendingAt,
3284
3293
  });
@@ -3295,6 +3304,7 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3295
3304
  percentShift: commit.percentShift,
3296
3305
  currentPrice: commit.currentPrice,
3297
3306
  timestamp,
3307
+ totalEntries: publicSignal.totalEntries,
3298
3308
  signalId: publicSignal.id,
3299
3309
  position: publicSignal.position,
3300
3310
  priceOpen: publicSignal.priceOpen,
@@ -3302,6 +3312,33 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3302
3312
  priceStopLoss: publicSignal.priceStopLoss,
3303
3313
  originalPriceTakeProfit: publicSignal.originalPriceTakeProfit,
3304
3314
  originalPriceStopLoss: publicSignal.originalPriceStopLoss,
3315
+ originalPriceOpen: publicSignal.originalPriceOpen,
3316
+ scheduledAt: publicSignal.scheduledAt,
3317
+ pendingAt: publicSignal.pendingAt,
3318
+ });
3319
+ continue;
3320
+ }
3321
+ if (commit.action === "average-buy") {
3322
+ const effectivePriceOpen = getEffectivePriceOpen(self._pendingSignal);
3323
+ await CALL_COMMIT_FN(self, {
3324
+ action: "average-buy",
3325
+ symbol: commit.symbol,
3326
+ strategyName: self.params.strategyName,
3327
+ exchangeName: self.params.exchangeName,
3328
+ frameName: self.params.frameName,
3329
+ backtest: commit.backtest,
3330
+ currentPrice: commit.currentPrice,
3331
+ effectivePriceOpen,
3332
+ timestamp,
3333
+ totalEntries: publicSignal.totalEntries,
3334
+ signalId: publicSignal.id,
3335
+ position: publicSignal.position,
3336
+ priceOpen: publicSignal.originalPriceOpen,
3337
+ priceTakeProfit: publicSignal.priceTakeProfit,
3338
+ priceStopLoss: publicSignal.priceStopLoss,
3339
+ originalPriceTakeProfit: publicSignal.originalPriceTakeProfit,
3340
+ originalPriceStopLoss: publicSignal.originalPriceStopLoss,
3341
+ originalPriceOpen: publicSignal.originalPriceOpen,
3305
3342
  scheduledAt: publicSignal.scheduledAt,
3306
3343
  pendingAt: publicSignal.pendingAt,
3307
3344
  });
@@ -3362,13 +3399,20 @@ const TO_PUBLIC_SIGNAL = (signal) => {
3362
3399
  const partialExecuted = ("_partial" in signal && Array.isArray(signal._partial))
3363
3400
  ? signal._partial.reduce((sum, partial) => sum + partial.percent, 0)
3364
3401
  : 0;
3402
+ const totalEntries = ("_entry" in signal && Array.isArray(signal._entry))
3403
+ ? signal._entry.length
3404
+ : 1;
3405
+ const effectivePriceOpen = "_entry" in signal ? getEffectivePriceOpen(signal) : signal.priceOpen;
3365
3406
  return {
3366
3407
  ...structuredClone(signal),
3408
+ priceOpen: effectivePriceOpen,
3367
3409
  priceStopLoss: hasTrailingSL ? signal._trailingPriceStopLoss : signal.priceStopLoss,
3368
3410
  priceTakeProfit: hasTrailingTP ? signal._trailingPriceTakeProfit : signal.priceTakeProfit,
3411
+ originalPriceOpen: signal.priceOpen,
3369
3412
  originalPriceStopLoss: signal.priceStopLoss,
3370
3413
  originalPriceTakeProfit: signal.priceTakeProfit,
3371
3414
  partialExecuted,
3415
+ totalEntries,
3372
3416
  };
3373
3417
  };
3374
3418
  const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
@@ -3698,6 +3742,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
3698
3742
  scheduledAt: currentTime,
3699
3743
  pendingAt: currentTime, // Для immediate signal оба времени одинаковые
3700
3744
  _isScheduled: false,
3745
+ _entry: [{ price: signal.priceOpen }],
3701
3746
  };
3702
3747
  // Валидируем сигнал перед возвратом
3703
3748
  VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
@@ -3719,6 +3764,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
3719
3764
  scheduledAt: currentTime,
3720
3765
  pendingAt: SCHEDULED_SIGNAL_PENDING_MOCK, // Временно, обновится при активации
3721
3766
  _isScheduled: true,
3767
+ _entry: [{ price: signal.priceOpen }],
3722
3768
  };
3723
3769
  // Валидируем сигнал перед возвратом
3724
3770
  VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
@@ -3736,6 +3782,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
3736
3782
  scheduledAt: currentTime,
3737
3783
  pendingAt: currentTime, // Для immediate signal оба времени одинаковые
3738
3784
  _isScheduled: false,
3785
+ _entry: [{ price: currentPrice }],
3739
3786
  };
3740
3787
  // Валидируем сигнал перед возвратом
3741
3788
  VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
@@ -3882,9 +3929,10 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
3882
3929
  return true;
3883
3930
  };
3884
3931
  const TRAILING_STOP_LOSS_FN = (self, signal, percentShift) => {
3932
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
3885
3933
  // CRITICAL: Always calculate from ORIGINAL SL, not from current trailing SL
3886
3934
  // This prevents error accumulation on repeated calls
3887
- const originalSlDistancePercent = Math.abs((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen * 100);
3935
+ const originalSlDistancePercent = Math.abs((effectivePriceOpen - signal.priceStopLoss) / effectivePriceOpen * 100);
3888
3936
  // Calculate new stop-loss distance percentage by adding shift to ORIGINAL distance
3889
3937
  // Negative percentShift: reduces distance % (tightens stop, moves SL toward entry or beyond)
3890
3938
  // Positive percentShift: increases distance % (loosens stop, moves SL away from entry)
@@ -3896,13 +3944,13 @@ const TRAILING_STOP_LOSS_FN = (self, signal, percentShift) => {
3896
3944
  // LONG: SL is below entry (or above entry if in profit zone)
3897
3945
  // Formula: entry * (1 - newDistance%)
3898
3946
  // Example: entry=100, originalSL=90 (10%), shift=-5% → newDistance=5% → 100 * 0.95 = 95 (tighter)
3899
- newStopLoss = signal.priceOpen * (1 - newSlDistancePercent / 100);
3947
+ newStopLoss = effectivePriceOpen * (1 - newSlDistancePercent / 100);
3900
3948
  }
3901
3949
  else {
3902
3950
  // SHORT: SL is above entry (or below entry if in profit zone)
3903
3951
  // Formula: entry * (1 + newDistance%)
3904
3952
  // Example: entry=100, originalSL=110 (10%), shift=-5% → newDistance=5% → 100 * 1.05 = 105 (tighter)
3905
- newStopLoss = signal.priceOpen * (1 + newSlDistancePercent / 100);
3953
+ newStopLoss = effectivePriceOpen * (1 + newSlDistancePercent / 100);
3906
3954
  }
3907
3955
  const currentTrailingSL = signal._trailingPriceStopLoss;
3908
3956
  const isFirstCall = currentTrailingSL === undefined;
@@ -3965,9 +4013,10 @@ const TRAILING_STOP_LOSS_FN = (self, signal, percentShift) => {
3965
4013
  }
3966
4014
  };
3967
4015
  const TRAILING_TAKE_PROFIT_FN = (self, signal, percentShift) => {
4016
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
3968
4017
  // CRITICAL: Always calculate from ORIGINAL TP, not from current trailing TP
3969
4018
  // This prevents error accumulation on repeated calls
3970
- const originalTpDistancePercent = Math.abs((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen * 100);
4019
+ const originalTpDistancePercent = Math.abs((signal.priceTakeProfit - effectivePriceOpen) / effectivePriceOpen * 100);
3971
4020
  // Calculate new take-profit distance percentage by adding shift to ORIGINAL distance
3972
4021
  // Negative percentShift: reduces distance % (brings TP closer to entry)
3973
4022
  // Positive percentShift: increases distance % (moves TP further from entry)
@@ -3978,13 +4027,13 @@ const TRAILING_TAKE_PROFIT_FN = (self, signal, percentShift) => {
3978
4027
  // LONG: TP is above entry
3979
4028
  // Formula: entry * (1 + newDistance%)
3980
4029
  // Example: entry=100, originalTP=110 (10%), shift=-3% → newDistance=7% → 100 * 1.07 = 107 (closer)
3981
- newTakeProfit = signal.priceOpen * (1 + newTpDistancePercent / 100);
4030
+ newTakeProfit = effectivePriceOpen * (1 + newTpDistancePercent / 100);
3982
4031
  }
3983
4032
  else {
3984
4033
  // SHORT: TP is below entry
3985
4034
  // Formula: entry * (1 - newDistance%)
3986
4035
  // Example: entry=100, originalTP=90 (10%), shift=-3% → newDistance=7% → 100 * 0.93 = 93 (closer)
3987
- newTakeProfit = signal.priceOpen * (1 - newTpDistancePercent / 100);
4036
+ newTakeProfit = effectivePriceOpen * (1 - newTpDistancePercent / 100);
3988
4037
  }
3989
4038
  const currentTrailingTP = signal._trailingPriceTakeProfit;
3990
4039
  const isFirstCall = currentTrailingTP === undefined;
@@ -4045,6 +4094,7 @@ const TRAILING_TAKE_PROFIT_FN = (self, signal, percentShift) => {
4045
4094
  }
4046
4095
  };
4047
4096
  const BREAKEVEN_FN = (self, signal, currentPrice) => {
4097
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
4048
4098
  // Calculate breakeven threshold based on slippage and fees
4049
4099
  // Need to cover: entry slippage + entry fee + exit slippage + exit fee
4050
4100
  // Total: (slippage + fee) * 2 transactions
@@ -4052,10 +4102,10 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4052
4102
  // Check if trailing stop is already set
4053
4103
  if (signal._trailingPriceStopLoss !== undefined) {
4054
4104
  const trailingStopLoss = signal._trailingPriceStopLoss;
4055
- const breakevenPrice = signal.priceOpen;
4105
+ const breakevenPrice = effectivePriceOpen;
4056
4106
  if (signal.position === "long") {
4057
4107
  // LONG: trailing SL is positive if it's above entry (in profit zone)
4058
- const isPositiveTrailing = trailingStopLoss > signal.priceOpen;
4108
+ const isPositiveTrailing = trailingStopLoss > effectivePriceOpen;
4059
4109
  if (isPositiveTrailing) {
4060
4110
  // Trailing stop is already protecting profit - consider breakeven achieved
4061
4111
  self.params.logger.debug("BREAKEVEN_FN: positive trailing stop already set, returning true", {
@@ -4071,7 +4121,7 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4071
4121
  else {
4072
4122
  // Trailing stop is negative (below entry)
4073
4123
  // Check if we can upgrade it to breakeven
4074
- const thresholdPrice = signal.priceOpen * (1 + breakevenThresholdPercent / 100);
4124
+ const thresholdPrice = effectivePriceOpen * (1 + breakevenThresholdPercent / 100);
4075
4125
  const isThresholdReached = currentPrice >= thresholdPrice;
4076
4126
  if (isThresholdReached && breakevenPrice > trailingStopLoss) {
4077
4127
  // Check for price intrusion before setting new SL
@@ -4120,7 +4170,7 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4120
4170
  }
4121
4171
  else {
4122
4172
  // SHORT: trailing SL is positive if it's below entry (in profit zone)
4123
- const isPositiveTrailing = trailingStopLoss < signal.priceOpen;
4173
+ const isPositiveTrailing = trailingStopLoss < effectivePriceOpen;
4124
4174
  if (isPositiveTrailing) {
4125
4175
  // Trailing stop is already protecting profit - consider breakeven achieved
4126
4176
  self.params.logger.debug("BREAKEVEN_FN: positive trailing stop already set, returning true", {
@@ -4136,7 +4186,7 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4136
4186
  else {
4137
4187
  // Trailing stop is negative (above entry)
4138
4188
  // Check if we can upgrade it to breakeven
4139
- const thresholdPrice = signal.priceOpen * (1 - breakevenThresholdPercent / 100);
4189
+ const thresholdPrice = effectivePriceOpen * (1 - breakevenThresholdPercent / 100);
4140
4190
  const isThresholdReached = currentPrice <= thresholdPrice;
4141
4191
  if (isThresholdReached && breakevenPrice < trailingStopLoss) {
4142
4192
  // Check for price intrusion before setting new SL
@@ -4186,21 +4236,21 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4186
4236
  }
4187
4237
  // No trailing stop set - proceed with normal breakeven logic
4188
4238
  const currentStopLoss = signal.priceStopLoss;
4189
- const breakevenPrice = signal.priceOpen;
4239
+ const breakevenPrice = effectivePriceOpen;
4190
4240
  // Calculate threshold price
4191
4241
  let thresholdPrice;
4192
4242
  let isThresholdReached;
4193
4243
  let canMoveToBreakeven;
4194
4244
  if (signal.position === "long") {
4195
4245
  // LONG: threshold reached when price goes UP by breakevenThresholdPercent from entry
4196
- thresholdPrice = signal.priceOpen * (1 + breakevenThresholdPercent / 100);
4246
+ thresholdPrice = effectivePriceOpen * (1 + breakevenThresholdPercent / 100);
4197
4247
  isThresholdReached = currentPrice >= thresholdPrice;
4198
4248
  // Can move to breakeven only if threshold reached and SL is below entry
4199
4249
  canMoveToBreakeven = isThresholdReached && currentStopLoss < breakevenPrice;
4200
4250
  }
4201
4251
  else {
4202
4252
  // SHORT: threshold reached when price goes DOWN by breakevenThresholdPercent from entry
4203
- thresholdPrice = signal.priceOpen * (1 - breakevenThresholdPercent / 100);
4253
+ thresholdPrice = effectivePriceOpen * (1 - breakevenThresholdPercent / 100);
4204
4254
  isThresholdReached = currentPrice <= thresholdPrice;
4205
4255
  // Can move to breakeven only if threshold reached and SL is above entry
4206
4256
  canMoveToBreakeven = isThresholdReached && currentStopLoss > breakevenPrice;
@@ -4260,8 +4310,51 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4260
4310
  thresholdPrice,
4261
4311
  breakevenThresholdPercent,
4262
4312
  profitDistancePercent: signal.position === "long"
4263
- ? ((currentPrice - signal.priceOpen) / signal.priceOpen * 100)
4264
- : ((signal.priceOpen - currentPrice) / signal.priceOpen * 100),
4313
+ ? ((currentPrice - effectivePriceOpen) / effectivePriceOpen * 100)
4314
+ : ((effectivePriceOpen - currentPrice) / effectivePriceOpen * 100),
4315
+ });
4316
+ return true;
4317
+ };
4318
+ const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
4319
+ // Ensure _entry is initialized (handles signals loaded from disk without _entry)
4320
+ if (!signal._entry || signal._entry.length === 0) {
4321
+ signal._entry = [{ price: signal.priceOpen }];
4322
+ }
4323
+ const lastEntry = signal._entry[signal._entry.length - 1];
4324
+ if (signal.position === "long") {
4325
+ // LONG: averaging down = currentPrice must be strictly lower than last entry
4326
+ if (currentPrice >= lastEntry.price) {
4327
+ self.params.logger.debug("AVERAGE_BUY_FN: rejected — currentPrice >= last entry (LONG)", {
4328
+ signalId: signal.id,
4329
+ position: signal.position,
4330
+ currentPrice,
4331
+ lastEntryPrice: lastEntry.price,
4332
+ reason: "must average down for LONG",
4333
+ });
4334
+ return false;
4335
+ }
4336
+ }
4337
+ else {
4338
+ // SHORT: averaging down = currentPrice must be strictly higher than last entry
4339
+ if (currentPrice <= lastEntry.price) {
4340
+ self.params.logger.debug("AVERAGE_BUY_FN: rejected — currentPrice <= last entry (SHORT)", {
4341
+ signalId: signal.id,
4342
+ position: signal.position,
4343
+ currentPrice,
4344
+ lastEntryPrice: lastEntry.price,
4345
+ reason: "must average down for SHORT",
4346
+ });
4347
+ return false;
4348
+ }
4349
+ }
4350
+ signal._entry.push({ price: currentPrice });
4351
+ self.params.logger.info("AVERAGE_BUY_FN executed", {
4352
+ signalId: signal.id,
4353
+ position: signal.position,
4354
+ originalPriceOpen: signal.priceOpen,
4355
+ newEntryPrice: currentPrice,
4356
+ newEffectivePrice: getEffectivePriceOpen(signal),
4357
+ totalEntries: signal._entry.length,
4265
4358
  });
4266
4359
  return true;
4267
4360
  };
@@ -4990,9 +5083,10 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
4990
5083
  await CALL_ACTIVE_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentTime, self.params.execution.context.backtest);
4991
5084
  // Calculate percentage of path to TP/SL for partial fill/loss callbacks
4992
5085
  {
5086
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
4993
5087
  if (signal.position === "long") {
4994
5088
  // For long: calculate progress towards TP or SL
4995
- const currentDistance = currentPrice - signal.priceOpen;
5089
+ const currentDistance = currentPrice - effectivePriceOpen;
4996
5090
  if (currentDistance > 0) {
4997
5091
  // Check if breakeven should be triggered
4998
5092
  await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
@@ -5000,7 +5094,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
5000
5094
  if (currentDistance > 0) {
5001
5095
  // Moving towards TP (use trailing TP if set)
5002
5096
  const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
5003
- const tpDistance = effectiveTakeProfit - signal.priceOpen;
5097
+ const tpDistance = effectiveTakeProfit - effectivePriceOpen;
5004
5098
  const progressPercent = (currentDistance / tpDistance) * 100;
5005
5099
  percentTp = Math.min(progressPercent, 100);
5006
5100
  await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
@@ -5008,7 +5102,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
5008
5102
  else if (currentDistance < 0) {
5009
5103
  // Moving towards SL (use trailing SL if set)
5010
5104
  const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
5011
- const slDistance = signal.priceOpen - effectiveStopLoss;
5105
+ const slDistance = effectivePriceOpen - effectiveStopLoss;
5012
5106
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
5013
5107
  percentSl = Math.min(progressPercent, 100);
5014
5108
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
@@ -5016,7 +5110,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
5016
5110
  }
5017
5111
  else if (signal.position === "short") {
5018
5112
  // For short: calculate progress towards TP or SL
5019
- const currentDistance = signal.priceOpen - currentPrice;
5113
+ const currentDistance = effectivePriceOpen - currentPrice;
5020
5114
  if (currentDistance > 0) {
5021
5115
  // Check if breakeven should be triggered
5022
5116
  await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
@@ -5024,7 +5118,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
5024
5118
  if (currentDistance > 0) {
5025
5119
  // Moving towards TP (use trailing TP if set)
5026
5120
  const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
5027
- const tpDistance = signal.priceOpen - effectiveTakeProfit;
5121
+ const tpDistance = effectivePriceOpen - effectiveTakeProfit;
5028
5122
  const progressPercent = (currentDistance / tpDistance) * 100;
5029
5123
  percentTp = Math.min(progressPercent, 100);
5030
5124
  await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
@@ -5032,7 +5126,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
5032
5126
  if (currentDistance < 0) {
5033
5127
  // Moving towards SL (use trailing SL if set)
5034
5128
  const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
5035
- const slDistance = effectiveStopLoss - signal.priceOpen;
5129
+ const slDistance = effectiveStopLoss - effectivePriceOpen;
5036
5130
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
5037
5131
  percentSl = Math.min(progressPercent, 100);
5038
5132
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentSl, currentTime, self.params.execution.context.backtest);
@@ -5252,8 +5346,10 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
5252
5346
  priceStopLoss: publicSignalForCommit.priceStopLoss,
5253
5347
  originalPriceTakeProfit: publicSignalForCommit.originalPriceTakeProfit,
5254
5348
  originalPriceStopLoss: publicSignalForCommit.originalPriceStopLoss,
5349
+ originalPriceOpen: publicSignalForCommit.originalPriceOpen,
5255
5350
  scheduledAt: publicSignalForCommit.scheduledAt,
5256
5351
  pendingAt: publicSignalForCommit.pendingAt,
5352
+ totalEntries: publicSignalForCommit.totalEntries,
5257
5353
  });
5258
5354
  await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, pendingSignal.priceOpen, candle.timestamp, self.params.execution.context.backtest);
5259
5355
  await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, pendingSignal, candle.timestamp, self.params.execution.context.backtest);
@@ -5400,9 +5496,10 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
5400
5496
  // Call onPartialProfit/onPartialLoss callbacks during backtest candle processing
5401
5497
  // Calculate percentage of path to TP/SL
5402
5498
  {
5499
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
5403
5500
  if (signal.position === "long") {
5404
5501
  // For long: calculate progress towards TP or SL
5405
- const currentDistance = averagePrice - signal.priceOpen;
5502
+ const currentDistance = averagePrice - effectivePriceOpen;
5406
5503
  if (currentDistance > 0) {
5407
5504
  // Check if breakeven should be triggered
5408
5505
  await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, averagePrice, currentCandleTimestamp, self.params.execution.context.backtest);
@@ -5410,21 +5507,21 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
5410
5507
  if (currentDistance > 0) {
5411
5508
  // Moving towards TP (use trailing TP if set)
5412
5509
  const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
5413
- const tpDistance = effectiveTakeProfit - signal.priceOpen;
5510
+ const tpDistance = effectiveTakeProfit - effectivePriceOpen;
5414
5511
  const progressPercent = (currentDistance / tpDistance) * 100;
5415
5512
  await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
5416
5513
  }
5417
5514
  else if (currentDistance < 0) {
5418
5515
  // Moving towards SL (use trailing SL if set)
5419
5516
  const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
5420
- const slDistance = signal.priceOpen - effectiveStopLoss;
5517
+ const slDistance = effectivePriceOpen - effectiveStopLoss;
5421
5518
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
5422
5519
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
5423
5520
  }
5424
5521
  }
5425
5522
  else if (signal.position === "short") {
5426
5523
  // For short: calculate progress towards TP or SL
5427
- const currentDistance = signal.priceOpen - averagePrice;
5524
+ const currentDistance = effectivePriceOpen - averagePrice;
5428
5525
  if (currentDistance > 0) {
5429
5526
  // Check if breakeven should be triggered
5430
5527
  await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, averagePrice, currentCandleTimestamp, self.params.execution.context.backtest);
@@ -5432,14 +5529,14 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
5432
5529
  if (currentDistance > 0) {
5433
5530
  // Moving towards TP (use trailing TP if set)
5434
5531
  const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
5435
- const tpDistance = signal.priceOpen - effectiveTakeProfit;
5532
+ const tpDistance = effectivePriceOpen - effectiveTakeProfit;
5436
5533
  const progressPercent = (currentDistance / tpDistance) * 100;
5437
5534
  await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
5438
5535
  }
5439
5536
  if (currentDistance < 0) {
5440
5537
  // Moving towards SL (use trailing SL if set)
5441
5538
  const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
5442
- const slDistance = effectiveStopLoss - signal.priceOpen;
5539
+ const slDistance = effectiveStopLoss - effectivePriceOpen;
5443
5540
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
5444
5541
  await CALL_PARTIAL_LOSS_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
5445
5542
  }
@@ -5637,6 +5734,7 @@ class ClientStrategy {
5637
5734
  return false;
5638
5735
  }
5639
5736
  const signal = this._pendingSignal;
5737
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
5640
5738
  // Calculate breakeven threshold based on slippage and fees
5641
5739
  // Need to cover: entry slippage + entry fee + exit slippage + exit fee
5642
5740
  // Total: (slippage + fee) * 2 transactions
@@ -5646,52 +5744,52 @@ class ClientStrategy {
5646
5744
  const trailingStopLoss = signal._trailingPriceStopLoss;
5647
5745
  if (signal.position === "long") {
5648
5746
  // LONG: trailing SL is positive if it's above entry (in profit zone)
5649
- const isPositiveTrailing = trailingStopLoss > signal.priceOpen;
5747
+ const isPositiveTrailing = trailingStopLoss > effectivePriceOpen;
5650
5748
  if (isPositiveTrailing) {
5651
5749
  // Trailing stop is already protecting profit - breakeven achieved
5652
5750
  return true;
5653
5751
  }
5654
5752
  // Trailing stop is negative (below entry)
5655
5753
  // Check if we can upgrade it to breakeven
5656
- const thresholdPrice = signal.priceOpen * (1 + breakevenThresholdPercent / 100);
5754
+ const thresholdPrice = effectivePriceOpen * (1 + breakevenThresholdPercent / 100);
5657
5755
  const isThresholdReached = currentPrice >= thresholdPrice;
5658
- const breakevenPrice = signal.priceOpen;
5756
+ const breakevenPrice = effectivePriceOpen;
5659
5757
  // Can upgrade to breakeven if threshold reached and breakeven is better than current trailing SL
5660
5758
  return isThresholdReached && breakevenPrice > trailingStopLoss;
5661
5759
  }
5662
5760
  else {
5663
5761
  // SHORT: trailing SL is positive if it's below entry (in profit zone)
5664
- const isPositiveTrailing = trailingStopLoss < signal.priceOpen;
5762
+ const isPositiveTrailing = trailingStopLoss < effectivePriceOpen;
5665
5763
  if (isPositiveTrailing) {
5666
5764
  // Trailing stop is already protecting profit - breakeven achieved
5667
5765
  return true;
5668
5766
  }
5669
5767
  // Trailing stop is negative (above entry)
5670
5768
  // Check if we can upgrade it to breakeven
5671
- const thresholdPrice = signal.priceOpen * (1 - breakevenThresholdPercent / 100);
5769
+ const thresholdPrice = effectivePriceOpen * (1 - breakevenThresholdPercent / 100);
5672
5770
  const isThresholdReached = currentPrice <= thresholdPrice;
5673
- const breakevenPrice = signal.priceOpen;
5771
+ const breakevenPrice = effectivePriceOpen;
5674
5772
  // Can upgrade to breakeven if threshold reached and breakeven is better than current trailing SL
5675
5773
  return isThresholdReached && breakevenPrice < trailingStopLoss;
5676
5774
  }
5677
5775
  }
5678
5776
  // No trailing stop set - proceed with normal breakeven logic
5679
5777
  const currentStopLoss = signal.priceStopLoss;
5680
- const breakevenPrice = signal.priceOpen;
5778
+ const breakevenPrice = effectivePriceOpen;
5681
5779
  // Calculate threshold price
5682
5780
  let thresholdPrice;
5683
5781
  let isThresholdReached;
5684
5782
  let canMoveToBreakeven;
5685
5783
  if (signal.position === "long") {
5686
5784
  // LONG: threshold reached when price goes UP by breakevenThresholdPercent from entry
5687
- thresholdPrice = signal.priceOpen * (1 + breakevenThresholdPercent / 100);
5785
+ thresholdPrice = effectivePriceOpen * (1 + breakevenThresholdPercent / 100);
5688
5786
  isThresholdReached = currentPrice >= thresholdPrice;
5689
5787
  // Can move to breakeven only if threshold reached and SL is below entry
5690
5788
  canMoveToBreakeven = isThresholdReached && currentStopLoss < breakevenPrice;
5691
5789
  }
5692
5790
  else {
5693
5791
  // SHORT: threshold reached when price goes DOWN by breakevenThresholdPercent from entry
5694
- thresholdPrice = signal.priceOpen * (1 - breakevenThresholdPercent / 100);
5792
+ thresholdPrice = effectivePriceOpen * (1 - breakevenThresholdPercent / 100);
5695
5793
  isThresholdReached = currentPrice <= thresholdPrice;
5696
5794
  // Can move to breakeven only if threshold reached and SL is above entry
5697
5795
  canMoveToBreakeven = isThresholdReached && currentStopLoss > breakevenPrice;
@@ -5774,6 +5872,8 @@ class ClientStrategy {
5774
5872
  backtest: this.params.execution.context.backtest,
5775
5873
  cancelId: cancelledSignal.cancelId,
5776
5874
  timestamp: currentTime,
5875
+ totalEntries: cancelledSignal._entry?.length ?? 1,
5876
+ originalPriceOpen: cancelledSignal.priceOpen,
5777
5877
  });
5778
5878
  // Call onCancel callback
5779
5879
  await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, currentTime, this.params.execution.context.backtest);
@@ -5814,6 +5914,8 @@ class ClientStrategy {
5814
5914
  backtest: this.params.execution.context.backtest,
5815
5915
  closeId: closedSignal.closeId,
5816
5916
  timestamp: currentTime,
5917
+ totalEntries: closedSignal._entry?.length ?? 1,
5918
+ originalPriceOpen: closedSignal.priceOpen,
5817
5919
  });
5818
5920
  // Call onClose callback
5819
5921
  await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, currentTime, this.params.execution.context.backtest);
@@ -5894,8 +5996,10 @@ class ClientStrategy {
5894
5996
  priceStopLoss: publicSignalForCommit.priceStopLoss,
5895
5997
  originalPriceTakeProfit: publicSignalForCommit.originalPriceTakeProfit,
5896
5998
  originalPriceStopLoss: publicSignalForCommit.originalPriceStopLoss,
5999
+ originalPriceOpen: publicSignalForCommit.originalPriceOpen,
5897
6000
  scheduledAt: publicSignalForCommit.scheduledAt,
5898
6001
  pendingAt: publicSignalForCommit.pendingAt,
6002
+ totalEntries: publicSignalForCommit.totalEntries,
5899
6003
  });
5900
6004
  // Call onOpen callback
5901
6005
  await CALL_OPEN_CALLBACKS_FN(this, this.params.execution.context.symbol, pendingSignal, currentPrice, currentTime, this.params.execution.context.backtest);
@@ -6026,6 +6130,8 @@ class ClientStrategy {
6026
6130
  backtest: true,
6027
6131
  cancelId: cancelledSignal.cancelId,
6028
6132
  timestamp: closeTimestamp,
6133
+ totalEntries: cancelledSignal._entry?.length ?? 1,
6134
+ originalPriceOpen: cancelledSignal.priceOpen,
6029
6135
  });
6030
6136
  await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
6031
6137
  const cancelledResult = {
@@ -6063,6 +6169,8 @@ class ClientStrategy {
6063
6169
  backtest: true,
6064
6170
  closeId: closedSignal.closeId,
6065
6171
  timestamp: closeTimestamp,
6172
+ totalEntries: closedSignal._entry?.length ?? 1,
6173
+ originalPriceOpen: closedSignal.priceOpen,
6066
6174
  });
6067
6175
  await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
6068
6176
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
@@ -6450,16 +6558,19 @@ class ClientStrategy {
6450
6558
  throw new Error(`ClientStrategy partialProfit: currentPrice must be a positive finite number, got ${currentPrice}`);
6451
6559
  }
6452
6560
  // Validation: currentPrice must be moving toward TP (profit direction)
6453
- if (this._pendingSignal.position === "long") {
6454
- // For LONG: currentPrice must be higher than priceOpen (moving toward TP)
6455
- if (currentPrice <= this._pendingSignal.priceOpen) {
6456
- throw new Error(`ClientStrategy partialProfit: For LONG position, currentPrice (${currentPrice}) must be > priceOpen (${this._pendingSignal.priceOpen})`);
6561
+ {
6562
+ const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
6563
+ if (this._pendingSignal.position === "long") {
6564
+ // For LONG: currentPrice must be higher than effectivePriceOpen (moving toward TP)
6565
+ if (currentPrice <= effectivePriceOpen) {
6566
+ throw new Error(`ClientStrategy partialProfit: For LONG position, currentPrice (${currentPrice}) must be > effectivePriceOpen (${effectivePriceOpen})`);
6567
+ }
6457
6568
  }
6458
- }
6459
- else {
6460
- // For SHORT: currentPrice must be lower than priceOpen (moving toward TP)
6461
- if (currentPrice >= this._pendingSignal.priceOpen) {
6462
- throw new Error(`ClientStrategy partialProfit: For SHORT position, currentPrice (${currentPrice}) must be < priceOpen (${this._pendingSignal.priceOpen})`);
6569
+ else {
6570
+ // For SHORT: currentPrice must be lower than effectivePriceOpen (moving toward TP)
6571
+ if (currentPrice >= effectivePriceOpen) {
6572
+ throw new Error(`ClientStrategy partialProfit: For SHORT position, currentPrice (${currentPrice}) must be < effectivePriceOpen (${effectivePriceOpen})`);
6573
+ }
6463
6574
  }
6464
6575
  }
6465
6576
  // Check if currentPrice already crossed take profit level
@@ -6582,16 +6693,19 @@ class ClientStrategy {
6582
6693
  throw new Error(`ClientStrategy partialLoss: currentPrice must be a positive finite number, got ${currentPrice}`);
6583
6694
  }
6584
6695
  // Validation: currentPrice must be moving toward SL (loss direction)
6585
- if (this._pendingSignal.position === "long") {
6586
- // For LONG: currentPrice must be lower than priceOpen (moving toward SL)
6587
- if (currentPrice >= this._pendingSignal.priceOpen) {
6588
- throw new Error(`ClientStrategy partialLoss: For LONG position, currentPrice (${currentPrice}) must be < priceOpen (${this._pendingSignal.priceOpen})`);
6696
+ {
6697
+ const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
6698
+ if (this._pendingSignal.position === "long") {
6699
+ // For LONG: currentPrice must be lower than effectivePriceOpen (moving toward SL)
6700
+ if (currentPrice >= effectivePriceOpen) {
6701
+ throw new Error(`ClientStrategy partialLoss: For LONG position, currentPrice (${currentPrice}) must be < effectivePriceOpen (${effectivePriceOpen})`);
6702
+ }
6589
6703
  }
6590
- }
6591
- else {
6592
- // For SHORT: currentPrice must be higher than priceOpen (moving toward SL)
6593
- if (currentPrice <= this._pendingSignal.priceOpen) {
6594
- throw new Error(`ClientStrategy partialLoss: For SHORT position, currentPrice (${currentPrice}) must be > priceOpen (${this._pendingSignal.priceOpen})`);
6704
+ else {
6705
+ // For SHORT: currentPrice must be higher than effectivePriceOpen (moving toward SL)
6706
+ if (currentPrice <= effectivePriceOpen) {
6707
+ throw new Error(`ClientStrategy partialLoss: For SHORT position, currentPrice (${currentPrice}) must be > effectivePriceOpen (${effectivePriceOpen})`);
6708
+ }
6595
6709
  }
6596
6710
  }
6597
6711
  // Check if currentPrice already crossed stop loss level
@@ -6712,7 +6826,7 @@ class ClientStrategy {
6712
6826
  }
6713
6827
  // Check for conflict with existing trailing take profit
6714
6828
  const signal = this._pendingSignal;
6715
- const breakevenPrice = signal.priceOpen;
6829
+ const breakevenPrice = getEffectivePriceOpen(signal);
6716
6830
  const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
6717
6831
  if (signal.position === "long" && breakevenPrice >= effectiveTakeProfit) {
6718
6832
  // LONG: Breakeven SL would be at or above current TP - invalid configuration
@@ -6864,14 +6978,15 @@ class ClientStrategy {
6864
6978
  }
6865
6979
  // Calculate what the new stop loss would be
6866
6980
  const signal = this._pendingSignal;
6867
- const slDistancePercent = Math.abs((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen * 100);
6981
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
6982
+ const slDistancePercent = Math.abs((effectivePriceOpen - signal.priceStopLoss) / effectivePriceOpen * 100);
6868
6983
  const newSlDistancePercent = slDistancePercent + percentShift;
6869
6984
  let newStopLoss;
6870
6985
  if (signal.position === "long") {
6871
- newStopLoss = signal.priceOpen * (1 - newSlDistancePercent / 100);
6986
+ newStopLoss = effectivePriceOpen * (1 - newSlDistancePercent / 100);
6872
6987
  }
6873
6988
  else {
6874
- newStopLoss = signal.priceOpen * (1 + newSlDistancePercent / 100);
6989
+ newStopLoss = effectivePriceOpen * (1 + newSlDistancePercent / 100);
6875
6990
  }
6876
6991
  // Check for price intrusion before executing trailing logic
6877
6992
  if (signal.position === "long" && currentPrice < newStopLoss) {
@@ -7037,14 +7152,15 @@ class ClientStrategy {
7037
7152
  }
7038
7153
  // Calculate what the new take profit would be
7039
7154
  const signal = this._pendingSignal;
7040
- const tpDistancePercent = Math.abs((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen * 100);
7155
+ const effectivePriceOpen = getEffectivePriceOpen(signal);
7156
+ const tpDistancePercent = Math.abs((signal.priceTakeProfit - effectivePriceOpen) / effectivePriceOpen * 100);
7041
7157
  const newTpDistancePercent = tpDistancePercent + percentShift;
7042
7158
  let newTakeProfit;
7043
7159
  if (signal.position === "long") {
7044
- newTakeProfit = signal.priceOpen * (1 + newTpDistancePercent / 100);
7160
+ newTakeProfit = effectivePriceOpen * (1 + newTpDistancePercent / 100);
7045
7161
  }
7046
7162
  else {
7047
- newTakeProfit = signal.priceOpen * (1 - newTpDistancePercent / 100);
7163
+ newTakeProfit = effectivePriceOpen * (1 - newTpDistancePercent / 100);
7048
7164
  }
7049
7165
  // Check for price intrusion before executing trailing logic
7050
7166
  if (signal.position === "long" && currentPrice > newTakeProfit) {
@@ -7126,6 +7242,70 @@ class ClientStrategy {
7126
7242
  });
7127
7243
  return true;
7128
7244
  }
7245
+ /**
7246
+ * Adds a new averaging entry to an open position (DCA — Dollar Cost Averaging).
7247
+ *
7248
+ * Appends currentPrice to the _entry array. The effective entry price used in all
7249
+ * distance and PNL calculations becomes the simple arithmetic mean of all _entry prices.
7250
+ * Original priceOpen is preserved unchanged for identity/audit purposes.
7251
+ *
7252
+ * Rejection rules (returns false without throwing):
7253
+ * - LONG: currentPrice >= last entry price (must average down, not up or equal)
7254
+ * - SHORT: currentPrice <= last entry price (must average down, not up or equal)
7255
+ *
7256
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
7257
+ * @param currentPrice - New entry price to add to the averaging history
7258
+ * @param backtest - Whether running in backtest mode
7259
+ * @returns Promise<boolean> - true if entry added, false if rejected by direction check
7260
+ */
7261
+ async averageBuy(symbol, currentPrice, backtest) {
7262
+ this.params.logger.debug("ClientStrategy averageBuy", {
7263
+ symbol,
7264
+ currentPrice,
7265
+ hasPendingSignal: this._pendingSignal !== null,
7266
+ });
7267
+ // Validation: must have pending signal
7268
+ if (!this._pendingSignal) {
7269
+ throw new Error(`ClientStrategy averageBuy: No pending signal exists for symbol=${symbol}`);
7270
+ }
7271
+ // Validation: currentPrice must be valid
7272
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
7273
+ throw new Error(`ClientStrategy averageBuy: currentPrice must be a positive finite number, got ${currentPrice}`);
7274
+ }
7275
+ // Reject if any partial closes have already been executed
7276
+ if (this._pendingSignal._partial && this._pendingSignal._partial.length > 0) {
7277
+ this.params.logger.debug("ClientStrategy averageBuy: rejected — partial closes already executed", {
7278
+ symbol,
7279
+ partialCount: this._pendingSignal._partial.length,
7280
+ });
7281
+ return false;
7282
+ }
7283
+ // Execute averaging logic
7284
+ const result = AVERAGE_BUY_FN(this, this._pendingSignal, currentPrice);
7285
+ if (!result) {
7286
+ return false;
7287
+ }
7288
+ // Persist updated signal state
7289
+ this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
7290
+ pendingSignal: this._pendingSignal,
7291
+ });
7292
+ // Call onWrite callback for testing persist storage
7293
+ if (this.params.callbacks?.onWrite) {
7294
+ this.params.callbacks.onWrite(this.params.execution.context.symbol, TO_PUBLIC_SIGNAL(this._pendingSignal), backtest);
7295
+ }
7296
+ if (!backtest) {
7297
+ await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
7298
+ }
7299
+ // Queue commit event for processing in tick()/backtest() with proper timestamp
7300
+ this._commitQueue.push({
7301
+ action: "average-buy",
7302
+ symbol,
7303
+ backtest,
7304
+ currentPrice,
7305
+ totalEntries: this._pendingSignal._entry?.length ?? 1,
7306
+ });
7307
+ return true;
7308
+ }
7129
7309
  }
7130
7310
 
7131
7311
  const RISK_METHOD_NAME_GET_DATA = "RiskUtils.getData";
@@ -8250,6 +8430,27 @@ class StrategyConnectionService {
8250
8430
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8251
8431
  return await strategy.activateScheduled(symbol, backtest, activateId);
8252
8432
  };
8433
+ /**
8434
+ * Adds a new DCA entry to the active pending signal.
8435
+ *
8436
+ * Delegates to ClientStrategy.averageBuy() with current execution context.
8437
+ *
8438
+ * @param backtest - Whether running in backtest mode
8439
+ * @param symbol - Trading pair symbol
8440
+ * @param currentPrice - New entry price to add to the averaging history
8441
+ * @param context - Execution context with strategyName, exchangeName, frameName
8442
+ * @returns Promise<boolean> - true if entry added, false if rejected
8443
+ */
8444
+ this.averageBuy = async (backtest, symbol, currentPrice, context) => {
8445
+ this.loggerService.log("strategyConnectionService averageBuy", {
8446
+ symbol,
8447
+ context,
8448
+ currentPrice,
8449
+ backtest,
8450
+ });
8451
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8452
+ return await strategy.averageBuy(symbol, currentPrice, backtest);
8453
+ };
8253
8454
  }
8254
8455
  }
8255
8456
 
@@ -8723,11 +8924,13 @@ const TO_RISK_SIGNAL = (signal, currentPrice) => {
8723
8924
  : 0;
8724
8925
  return {
8725
8926
  ...structuredClone(signal),
8927
+ totalEntries: 1,
8726
8928
  priceOpen: signal.priceOpen ?? currentPrice,
8727
8929
  priceStopLoss: hasTrailingSL ? signal._trailingPriceStopLoss : signal.priceStopLoss,
8728
8930
  priceTakeProfit: hasTrailingTP ? signal._trailingPriceTakeProfit : signal.priceTakeProfit,
8729
8931
  originalPriceStopLoss: signal.priceStopLoss,
8730
8932
  originalPriceTakeProfit: signal.priceTakeProfit,
8933
+ originalPriceOpen: signal.priceOpen ?? currentPrice,
8731
8934
  partialExecuted,
8732
8935
  };
8733
8936
  };
@@ -11595,6 +11798,28 @@ class StrategyCoreService {
11595
11798
  await this.validate(context);
11596
11799
  return await this.strategyConnectionService.activateScheduled(backtest, symbol, context, activateId);
11597
11800
  };
11801
+ /**
11802
+ * Adds a new DCA entry to the active pending signal.
11803
+ *
11804
+ * Validates strategy existence and delegates to connection service
11805
+ * to add a new averaging entry to the position.
11806
+ *
11807
+ * @param backtest - Whether running in backtest mode
11808
+ * @param symbol - Trading pair symbol
11809
+ * @param currentPrice - New entry price to add to the averaging history
11810
+ * @param context - Execution context with strategyName, exchangeName, frameName
11811
+ * @returns Promise<boolean> - true if entry added, false if rejected
11812
+ */
11813
+ this.averageBuy = async (backtest, symbol, currentPrice, context) => {
11814
+ this.loggerService.log("strategyCoreService averageBuy", {
11815
+ symbol,
11816
+ currentPrice,
11817
+ context,
11818
+ backtest,
11819
+ });
11820
+ await this.validate(context);
11821
+ return await this.strategyConnectionService.averageBuy(backtest, symbol, currentPrice, context);
11822
+ };
11598
11823
  }
11599
11824
  }
11600
11825
 
@@ -14142,6 +14367,18 @@ const backtest_columns = [
14142
14367
  format: (data) => `${data.signal.originalPriceStopLoss.toFixed(8)} USD`,
14143
14368
  isVisible: () => true,
14144
14369
  },
14370
+ {
14371
+ key: "originalPriceOpen",
14372
+ label: "Original Entry",
14373
+ format: (data) => `${data.signal.originalPriceOpen.toFixed(8)} USD`,
14374
+ isVisible: () => true,
14375
+ },
14376
+ {
14377
+ key: "totalEntries",
14378
+ label: "DCA Entries",
14379
+ format: (data) => String(data.signal.totalEntries),
14380
+ isVisible: () => true,
14381
+ },
14145
14382
  {
14146
14383
  key: "pnl",
14147
14384
  label: "PNL (net)",
@@ -14429,6 +14666,20 @@ const live_columns = [
14429
14666
  : "N/A",
14430
14667
  isVisible: () => true,
14431
14668
  },
14669
+ {
14670
+ key: "originalPriceOpen",
14671
+ label: "Original Entry",
14672
+ format: (data) => data.originalPriceOpen !== undefined
14673
+ ? `${data.originalPriceOpen.toFixed(8)} USD`
14674
+ : "N/A",
14675
+ isVisible: () => true,
14676
+ },
14677
+ {
14678
+ key: "totalEntries",
14679
+ label: "DCA Entries",
14680
+ format: (data) => data.totalEntries !== undefined ? String(data.totalEntries) : "N/A",
14681
+ isVisible: () => true,
14682
+ },
14432
14683
  {
14433
14684
  key: "partialExecuted",
14434
14685
  label: "Partial Executed %",
@@ -14593,6 +14844,18 @@ const partial_columns = [
14593
14844
  format: (data) => (data.originalPriceStopLoss ? `${data.originalPriceStopLoss.toFixed(8)} USD` : "N/A"),
14594
14845
  isVisible: () => true,
14595
14846
  },
14847
+ {
14848
+ key: "originalPriceOpen",
14849
+ label: "Original Entry",
14850
+ format: (data) => (data.originalPriceOpen ? `${data.originalPriceOpen.toFixed(8)} USD` : "N/A"),
14851
+ isVisible: () => true,
14852
+ },
14853
+ {
14854
+ key: "totalEntries",
14855
+ label: "DCA Entries",
14856
+ format: (data) => (data.totalEntries !== undefined ? String(data.totalEntries) : "N/A"),
14857
+ isVisible: () => true,
14858
+ },
14596
14859
  {
14597
14860
  key: "partialExecuted",
14598
14861
  label: "Partial Executed %",
@@ -14727,6 +14990,18 @@ const breakeven_columns = [
14727
14990
  format: (data) => (data.originalPriceStopLoss ? `${data.originalPriceStopLoss.toFixed(8)} USD` : "N/A"),
14728
14991
  isVisible: () => true,
14729
14992
  },
14993
+ {
14994
+ key: "originalPriceOpen",
14995
+ label: "Original Entry",
14996
+ format: (data) => (data.originalPriceOpen ? `${data.originalPriceOpen.toFixed(8)} USD` : "N/A"),
14997
+ isVisible: () => true,
14998
+ },
14999
+ {
15000
+ key: "totalEntries",
15001
+ label: "DCA Entries",
15002
+ format: (data) => (data.totalEntries !== undefined ? String(data.totalEntries) : "N/A"),
15003
+ isVisible: () => true,
15004
+ },
14730
15005
  {
14731
15006
  key: "partialExecuted",
14732
15007
  label: "Partial Executed %",
@@ -14997,6 +15272,22 @@ const risk_columns = [
14997
15272
  : "N/A",
14998
15273
  isVisible: () => true,
14999
15274
  },
15275
+ {
15276
+ key: "originalPriceOpen",
15277
+ label: "Original Entry",
15278
+ format: (data) => data.currentSignal.originalPriceOpen !== undefined
15279
+ ? `${data.currentSignal.originalPriceOpen.toFixed(8)} USD`
15280
+ : "N/A",
15281
+ isVisible: () => true,
15282
+ },
15283
+ {
15284
+ key: "totalEntries",
15285
+ label: "DCA Entries",
15286
+ format: (data) => data.currentSignal.totalEntries !== undefined
15287
+ ? String(data.currentSignal.totalEntries)
15288
+ : "N/A",
15289
+ isVisible: () => true,
15290
+ },
15000
15291
  {
15001
15292
  key: "partialExecuted",
15002
15293
  label: "Partial Executed %",
@@ -15171,6 +15462,20 @@ const schedule_columns = [
15171
15462
  : "N/A",
15172
15463
  isVisible: () => true,
15173
15464
  },
15465
+ {
15466
+ key: "originalPriceOpen",
15467
+ label: "Original Entry",
15468
+ format: (data) => data.originalPriceOpen !== undefined
15469
+ ? `${data.originalPriceOpen.toFixed(8)} USD`
15470
+ : "N/A",
15471
+ isVisible: () => true,
15472
+ },
15473
+ {
15474
+ key: "totalEntries",
15475
+ label: "DCA Entries",
15476
+ format: (data) => data.totalEntries !== undefined ? String(data.totalEntries) : "N/A",
15477
+ isVisible: () => true,
15478
+ },
15174
15479
  {
15175
15480
  key: "partialExecuted",
15176
15481
  label: "Partial Executed %",
@@ -17260,6 +17565,8 @@ let ReportStorage$5 = class ReportStorage {
17260
17565
  priceStopLoss: data.signal.priceStopLoss,
17261
17566
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
17262
17567
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
17568
+ totalEntries: data.signal.totalEntries,
17569
+ originalPriceOpen: data.signal.originalPriceOpen,
17263
17570
  partialExecuted: data.signal.partialExecuted,
17264
17571
  scheduledAt: data.signal.scheduledAt,
17265
17572
  });
@@ -17289,6 +17596,8 @@ let ReportStorage$5 = class ReportStorage {
17289
17596
  priceStopLoss: data.signal.priceStopLoss,
17290
17597
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
17291
17598
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
17599
+ totalEntries: data.signal.totalEntries,
17600
+ originalPriceOpen: data.signal.originalPriceOpen,
17292
17601
  partialExecuted: data.signal.partialExecuted,
17293
17602
  duration: durationMin,
17294
17603
  pendingAt: data.signal.pendingAt,
@@ -17321,6 +17630,8 @@ let ReportStorage$5 = class ReportStorage {
17321
17630
  priceStopLoss: data.signal.priceStopLoss,
17322
17631
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
17323
17632
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
17633
+ totalEntries: data.signal.totalEntries,
17634
+ originalPriceOpen: data.signal.originalPriceOpen,
17324
17635
  partialExecuted: data.signal.partialExecuted,
17325
17636
  closeTimestamp: data.closeTimestamp,
17326
17637
  duration: durationMin,
@@ -20466,6 +20777,8 @@ let ReportStorage$3 = class ReportStorage {
20466
20777
  priceStopLoss: data.priceStopLoss,
20467
20778
  originalPriceTakeProfit: data.originalPriceTakeProfit,
20468
20779
  originalPriceStopLoss: data.originalPriceStopLoss,
20780
+ totalEntries: data.totalEntries,
20781
+ originalPriceOpen: data.originalPriceOpen,
20469
20782
  partialExecuted: data.partialExecuted,
20470
20783
  note: data.note,
20471
20784
  pendingAt: data.pendingAt,
@@ -20500,6 +20813,8 @@ let ReportStorage$3 = class ReportStorage {
20500
20813
  priceStopLoss: data.priceStopLoss,
20501
20814
  originalPriceTakeProfit: data.originalPriceTakeProfit,
20502
20815
  originalPriceStopLoss: data.originalPriceStopLoss,
20816
+ totalEntries: data.totalEntries,
20817
+ originalPriceOpen: data.originalPriceOpen,
20503
20818
  partialExecuted: data.partialExecuted,
20504
20819
  note: data.note,
20505
20820
  pendingAt: data.pendingAt,
@@ -21599,6 +21914,8 @@ let ReportStorage$2 = class ReportStorage {
21599
21914
  priceStopLoss: data.priceStopLoss,
21600
21915
  originalPriceTakeProfit: data.originalPriceTakeProfit,
21601
21916
  originalPriceStopLoss: data.originalPriceStopLoss,
21917
+ totalEntries: data.totalEntries,
21918
+ originalPriceOpen: data.originalPriceOpen,
21602
21919
  partialExecuted: data.partialExecuted,
21603
21920
  note: data.note,
21604
21921
  pendingAt: data.pendingAt,
@@ -23602,6 +23919,8 @@ class ScheduleReportService {
23602
23919
  priceStopLoss: data.signal?.priceStopLoss,
23603
23920
  originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
23604
23921
  originalPriceStopLoss: data.signal?.originalPriceStopLoss,
23922
+ totalEntries: data.signal?.totalEntries,
23923
+ originalPriceOpen: data.signal?.originalPriceOpen,
23605
23924
  partialExecuted: data.signal?.partialExecuted,
23606
23925
  pendingAt: data.signal?.pendingAt,
23607
23926
  minuteEstimatedTime: data.signal?.minuteEstimatedTime,
@@ -23646,6 +23965,8 @@ class ScheduleReportService {
23646
23965
  priceStopLoss: data.signal?.priceStopLoss,
23647
23966
  originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
23648
23967
  originalPriceStopLoss: data.signal?.originalPriceStopLoss,
23968
+ totalEntries: data.signal?.totalEntries,
23969
+ originalPriceOpen: data.signal?.originalPriceOpen,
23649
23970
  partialExecuted: data.signal?.partialExecuted,
23650
23971
  scheduledAt: data.signal?.scheduledAt,
23651
23972
  pendingAt: data.signal?.pendingAt,
@@ -24123,6 +24444,8 @@ class PartialReportService {
24123
24444
  priceStopLoss: data.data.priceStopLoss,
24124
24445
  originalPriceTakeProfit: data.data.originalPriceTakeProfit,
24125
24446
  originalPriceStopLoss: data.data.originalPriceStopLoss,
24447
+ totalEntries: data.data.totalEntries,
24448
+ originalPriceOpen: data.data.originalPriceOpen,
24126
24449
  partialExecuted: data.data.partialExecuted,
24127
24450
  _partial: data.data._partial,
24128
24451
  note: data.data.note,
@@ -24164,6 +24487,8 @@ class PartialReportService {
24164
24487
  priceStopLoss: data.data.priceStopLoss,
24165
24488
  originalPriceTakeProfit: data.data.originalPriceTakeProfit,
24166
24489
  originalPriceStopLoss: data.data.originalPriceStopLoss,
24490
+ totalEntries: data.data.totalEntries,
24491
+ originalPriceOpen: data.data.originalPriceOpen,
24167
24492
  partialExecuted: data.data.partialExecuted,
24168
24493
  _partial: data.data._partial,
24169
24494
  note: data.data.note,
@@ -24286,6 +24611,8 @@ class BreakevenReportService {
24286
24611
  priceStopLoss: data.data.priceStopLoss,
24287
24612
  originalPriceTakeProfit: data.data.originalPriceTakeProfit,
24288
24613
  originalPriceStopLoss: data.data.originalPriceStopLoss,
24614
+ totalEntries: data.data.totalEntries,
24615
+ originalPriceOpen: data.data.originalPriceOpen,
24289
24616
  partialExecuted: data.data.partialExecuted,
24290
24617
  _partial: data.data._partial,
24291
24618
  note: data.data.note,
@@ -24594,7 +24921,7 @@ class StrategyReportService {
24594
24921
  * @param scheduledAt - Signal creation timestamp in milliseconds
24595
24922
  * @param pendingAt - Pending timestamp in milliseconds
24596
24923
  */
24597
- this.partialProfit = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
24924
+ this.partialProfit = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
24598
24925
  this.loggerService.log("strategyReportService partialProfit", {
24599
24926
  symbol,
24600
24927
  percentToClose,
@@ -24626,6 +24953,8 @@ class StrategyReportService {
24626
24953
  priceStopLoss,
24627
24954
  originalPriceTakeProfit,
24628
24955
  originalPriceStopLoss,
24956
+ originalPriceOpen,
24957
+ totalEntries,
24629
24958
  scheduledAt,
24630
24959
  pendingAt,
24631
24960
  }, {
@@ -24655,7 +24984,7 @@ class StrategyReportService {
24655
24984
  * @param scheduledAt - Signal creation timestamp in milliseconds
24656
24985
  * @param pendingAt - Pending timestamp in milliseconds
24657
24986
  */
24658
- this.partialLoss = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
24987
+ this.partialLoss = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
24659
24988
  this.loggerService.log("strategyReportService partialLoss", {
24660
24989
  symbol,
24661
24990
  percentToClose,
@@ -24687,6 +25016,8 @@ class StrategyReportService {
24687
25016
  priceStopLoss,
24688
25017
  originalPriceTakeProfit,
24689
25018
  originalPriceStopLoss,
25019
+ originalPriceOpen,
25020
+ totalEntries,
24690
25021
  scheduledAt,
24691
25022
  pendingAt,
24692
25023
  }, {
@@ -24716,7 +25047,7 @@ class StrategyReportService {
24716
25047
  * @param scheduledAt - Signal creation timestamp in milliseconds
24717
25048
  * @param pendingAt - Pending timestamp in milliseconds
24718
25049
  */
24719
- this.trailingStop = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
25050
+ this.trailingStop = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
24720
25051
  this.loggerService.log("strategyReportService trailingStop", {
24721
25052
  symbol,
24722
25053
  percentShift,
@@ -24748,6 +25079,8 @@ class StrategyReportService {
24748
25079
  priceStopLoss,
24749
25080
  originalPriceTakeProfit,
24750
25081
  originalPriceStopLoss,
25082
+ originalPriceOpen,
25083
+ totalEntries,
24751
25084
  scheduledAt,
24752
25085
  pendingAt,
24753
25086
  }, {
@@ -24777,7 +25110,7 @@ class StrategyReportService {
24777
25110
  * @param scheduledAt - Signal creation timestamp in milliseconds
24778
25111
  * @param pendingAt - Pending timestamp in milliseconds
24779
25112
  */
24780
- this.trailingTake = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
25113
+ this.trailingTake = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
24781
25114
  this.loggerService.log("strategyReportService trailingTake", {
24782
25115
  symbol,
24783
25116
  percentShift,
@@ -24809,6 +25142,8 @@ class StrategyReportService {
24809
25142
  priceStopLoss,
24810
25143
  originalPriceTakeProfit,
24811
25144
  originalPriceStopLoss,
25145
+ originalPriceOpen,
25146
+ totalEntries,
24812
25147
  scheduledAt,
24813
25148
  pendingAt,
24814
25149
  }, {
@@ -24837,7 +25172,7 @@ class StrategyReportService {
24837
25172
  * @param scheduledAt - Signal creation timestamp in milliseconds
24838
25173
  * @param pendingAt - Pending timestamp in milliseconds
24839
25174
  */
24840
- this.breakeven = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
25175
+ this.breakeven = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
24841
25176
  this.loggerService.log("strategyReportService breakeven", {
24842
25177
  symbol,
24843
25178
  currentPrice,
@@ -24867,6 +25202,8 @@ class StrategyReportService {
24867
25202
  priceStopLoss,
24868
25203
  originalPriceTakeProfit,
24869
25204
  originalPriceStopLoss,
25205
+ originalPriceOpen,
25206
+ totalEntries,
24870
25207
  scheduledAt,
24871
25208
  pendingAt,
24872
25209
  }, {
@@ -24896,7 +25233,7 @@ class StrategyReportService {
24896
25233
  * @param pendingAt - Pending timestamp in milliseconds
24897
25234
  * @param activateId - Optional identifier for the activation reason
24898
25235
  */
24899
- this.activateScheduled = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, activateId) => {
25236
+ this.activateScheduled = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen, activateId) => {
24900
25237
  this.loggerService.log("strategyReportService activateScheduled", {
24901
25238
  symbol,
24902
25239
  currentPrice,
@@ -24928,6 +25265,8 @@ class StrategyReportService {
24928
25265
  priceStopLoss,
24929
25266
  originalPriceTakeProfit,
24930
25267
  originalPriceStopLoss,
25268
+ originalPriceOpen,
25269
+ totalEntries,
24931
25270
  scheduledAt,
24932
25271
  pendingAt,
24933
25272
  }, {
@@ -24939,6 +25278,71 @@ class StrategyReportService {
24939
25278
  walkerName: "",
24940
25279
  });
24941
25280
  };
25281
+ /**
25282
+ * Logs an average-buy (DCA) event when a new averaging entry is added to an open position.
25283
+ *
25284
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
25285
+ * @param currentPrice - Price at which the new averaging entry was executed
25286
+ * @param effectivePriceOpen - Averaged entry price after this addition
25287
+ * @param totalEntries - Total number of DCA entries after this addition
25288
+ * @param isBacktest - Whether this is a backtest or live trading event
25289
+ * @param context - Strategy context with strategyName, exchangeName, frameName
25290
+ * @param timestamp - Timestamp from StrategyCommitContract (execution context time)
25291
+ * @param position - Trade direction: "long" or "short"
25292
+ * @param priceOpen - Original entry price (unchanged by averaging)
25293
+ * @param priceTakeProfit - Effective take profit price
25294
+ * @param priceStopLoss - Effective stop loss price
25295
+ * @param originalPriceTakeProfit - Original take profit before trailing
25296
+ * @param originalPriceStopLoss - Original stop loss before trailing
25297
+ * @param scheduledAt - Signal creation timestamp in milliseconds
25298
+ * @param pendingAt - Pending timestamp in milliseconds
25299
+ */
25300
+ this.averageBuy = async (symbol, currentPrice, effectivePriceOpen, totalEntries, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, originalPriceOpen) => {
25301
+ this.loggerService.log("strategyReportService averageBuy", {
25302
+ symbol,
25303
+ currentPrice,
25304
+ effectivePriceOpen,
25305
+ totalEntries,
25306
+ isBacktest,
25307
+ });
25308
+ if (!this.subscribe.hasValue()) {
25309
+ return;
25310
+ }
25311
+ const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
25312
+ exchangeName: context.exchangeName,
25313
+ strategyName: context.strategyName,
25314
+ frameName: context.frameName,
25315
+ });
25316
+ if (!pendingRow) {
25317
+ return;
25318
+ }
25319
+ const createdAt = new Date(timestamp).toISOString();
25320
+ await Report.writeData("strategy", {
25321
+ action: "average-buy",
25322
+ currentPrice,
25323
+ effectivePriceOpen,
25324
+ totalEntries,
25325
+ symbol,
25326
+ timestamp,
25327
+ createdAt,
25328
+ position,
25329
+ priceOpen,
25330
+ priceTakeProfit,
25331
+ priceStopLoss,
25332
+ originalPriceTakeProfit,
25333
+ originalPriceStopLoss,
25334
+ originalPriceOpen,
25335
+ scheduledAt,
25336
+ pendingAt,
25337
+ }, {
25338
+ signalId: pendingRow.id,
25339
+ exchangeName: context.exchangeName,
25340
+ frameName: context.frameName,
25341
+ strategyName: context.strategyName,
25342
+ symbol,
25343
+ walkerName: "",
25344
+ });
25345
+ };
24942
25346
  /**
24943
25347
  * Initializes the service for event logging.
24944
25348
  *
@@ -24969,43 +25373,50 @@ class StrategyReportService {
24969
25373
  exchangeName: event.exchangeName,
24970
25374
  frameName: event.frameName,
24971
25375
  strategyName: event.strategyName,
24972
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
25376
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
24973
25377
  const unPartialLoss = strategyCommitSubject
24974
25378
  .filter(({ action }) => action === "partial-loss")
24975
25379
  .connect(async (event) => await this.partialLoss(event.symbol, event.percentToClose, event.currentPrice, event.backtest, {
24976
25380
  exchangeName: event.exchangeName,
24977
25381
  frameName: event.frameName,
24978
25382
  strategyName: event.strategyName,
24979
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
25383
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
24980
25384
  const unTrailingStop = strategyCommitSubject
24981
25385
  .filter(({ action }) => action === "trailing-stop")
24982
25386
  .connect(async (event) => await this.trailingStop(event.symbol, event.percentShift, event.currentPrice, event.backtest, {
24983
25387
  exchangeName: event.exchangeName,
24984
25388
  frameName: event.frameName,
24985
25389
  strategyName: event.strategyName,
24986
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
25390
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
24987
25391
  const unTrailingTake = strategyCommitSubject
24988
25392
  .filter(({ action }) => action === "trailing-take")
24989
25393
  .connect(async (event) => await this.trailingTake(event.symbol, event.percentShift, event.currentPrice, event.backtest, {
24990
25394
  exchangeName: event.exchangeName,
24991
25395
  frameName: event.frameName,
24992
25396
  strategyName: event.strategyName,
24993
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
25397
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
24994
25398
  const unBreakeven = strategyCommitSubject
24995
25399
  .filter(({ action }) => action === "breakeven")
24996
25400
  .connect(async (event) => await this.breakeven(event.symbol, event.currentPrice, event.backtest, {
24997
25401
  exchangeName: event.exchangeName,
24998
25402
  frameName: event.frameName,
24999
25403
  strategyName: event.strategyName,
25000
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
25404
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
25001
25405
  const unActivateScheduled = strategyCommitSubject
25002
25406
  .filter(({ action }) => action === "activate-scheduled")
25003
25407
  .connect(async (event) => await this.activateScheduled(event.symbol, event.currentPrice, event.backtest, {
25004
25408
  exchangeName: event.exchangeName,
25005
25409
  frameName: event.frameName,
25006
25410
  strategyName: event.strategyName,
25007
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.activateId));
25008
- const disposeFn = compose(() => unCancelSchedule(), () => unClosePending(), () => unPartialProfit(), () => unPartialLoss(), () => unTrailingStop(), () => unTrailingTake(), () => unBreakeven(), () => unActivateScheduled());
25411
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen, event.activateId));
25412
+ const unAverageBuy = strategyCommitSubject
25413
+ .filter(({ action }) => action === "average-buy")
25414
+ .connect(async (event) => await this.averageBuy(event.symbol, event.currentPrice, event.effectivePriceOpen, event.totalEntries, event.backtest, {
25415
+ exchangeName: event.exchangeName,
25416
+ frameName: event.frameName,
25417
+ strategyName: event.strategyName,
25418
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.originalPriceOpen));
25419
+ const disposeFn = compose(() => unCancelSchedule(), () => unClosePending(), () => unPartialProfit(), () => unPartialLoss(), () => unTrailingStop(), () => unTrailingTake(), () => unBreakeven(), () => unActivateScheduled(), () => unAverageBuy());
25009
25420
  return () => {
25010
25421
  disposeFn();
25011
25422
  this.subscribe.clear();
@@ -25137,6 +25548,7 @@ class ReportStorage {
25137
25548
  trailingTakeCount: 0,
25138
25549
  breakevenCount: 0,
25139
25550
  activateScheduledCount: 0,
25551
+ averageBuyCount: 0,
25140
25552
  };
25141
25553
  }
25142
25554
  return {
@@ -25150,6 +25562,7 @@ class ReportStorage {
25150
25562
  trailingTakeCount: this._eventList.filter(e => e.action === "trailing-take").length,
25151
25563
  breakevenCount: this._eventList.filter(e => e.action === "breakeven").length,
25152
25564
  activateScheduledCount: this._eventList.filter(e => e.action === "activate-scheduled").length,
25565
+ averageBuyCount: this._eventList.filter(e => e.action === "average-buy").length,
25153
25566
  };
25154
25567
  }
25155
25568
  /**
@@ -25199,6 +25612,7 @@ class ReportStorage {
25199
25612
  `- Trailing take: ${stats.trailingTakeCount}`,
25200
25613
  `- Breakeven: ${stats.breakevenCount}`,
25201
25614
  `- Activate scheduled: ${stats.activateScheduledCount}`,
25615
+ `- Average buy: ${stats.averageBuyCount}`,
25202
25616
  ].join("\n");
25203
25617
  }
25204
25618
  /**
@@ -25386,7 +25800,7 @@ class StrategyMarkdownService {
25386
25800
  * @param scheduledAt - Signal creation timestamp in milliseconds
25387
25801
  * @param pendingAt - Pending timestamp in milliseconds
25388
25802
  */
25389
- this.partialProfit = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
25803
+ this.partialProfit = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
25390
25804
  this.loggerService.log("strategyMarkdownService partialProfit", {
25391
25805
  symbol,
25392
25806
  percentToClose,
@@ -25424,6 +25838,8 @@ class StrategyMarkdownService {
25424
25838
  priceStopLoss,
25425
25839
  originalPriceTakeProfit,
25426
25840
  originalPriceStopLoss,
25841
+ originalPriceOpen,
25842
+ totalEntries,
25427
25843
  scheduledAt,
25428
25844
  pendingAt,
25429
25845
  });
@@ -25446,7 +25862,7 @@ class StrategyMarkdownService {
25446
25862
  * @param scheduledAt - Signal creation timestamp in milliseconds
25447
25863
  * @param pendingAt - Pending timestamp in milliseconds
25448
25864
  */
25449
- this.partialLoss = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
25865
+ this.partialLoss = async (symbol, percentToClose, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
25450
25866
  this.loggerService.log("strategyMarkdownService partialLoss", {
25451
25867
  symbol,
25452
25868
  percentToClose,
@@ -25484,6 +25900,8 @@ class StrategyMarkdownService {
25484
25900
  priceStopLoss,
25485
25901
  originalPriceTakeProfit,
25486
25902
  originalPriceStopLoss,
25903
+ originalPriceOpen,
25904
+ totalEntries,
25487
25905
  scheduledAt,
25488
25906
  pendingAt,
25489
25907
  });
@@ -25506,7 +25924,7 @@ class StrategyMarkdownService {
25506
25924
  * @param scheduledAt - Signal creation timestamp in milliseconds
25507
25925
  * @param pendingAt - Pending timestamp in milliseconds
25508
25926
  */
25509
- this.trailingStop = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
25927
+ this.trailingStop = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
25510
25928
  this.loggerService.log("strategyMarkdownService trailingStop", {
25511
25929
  symbol,
25512
25930
  percentShift,
@@ -25544,6 +25962,8 @@ class StrategyMarkdownService {
25544
25962
  priceStopLoss,
25545
25963
  originalPriceTakeProfit,
25546
25964
  originalPriceStopLoss,
25965
+ originalPriceOpen,
25966
+ totalEntries,
25547
25967
  scheduledAt,
25548
25968
  pendingAt,
25549
25969
  });
@@ -25566,7 +25986,7 @@ class StrategyMarkdownService {
25566
25986
  * @param scheduledAt - Signal creation timestamp in milliseconds
25567
25987
  * @param pendingAt - Pending timestamp in milliseconds
25568
25988
  */
25569
- this.trailingTake = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
25989
+ this.trailingTake = async (symbol, percentShift, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
25570
25990
  this.loggerService.log("strategyMarkdownService trailingTake", {
25571
25991
  symbol,
25572
25992
  percentShift,
@@ -25604,6 +26024,8 @@ class StrategyMarkdownService {
25604
26024
  priceStopLoss,
25605
26025
  originalPriceTakeProfit,
25606
26026
  originalPriceStopLoss,
26027
+ originalPriceOpen,
26028
+ totalEntries,
25607
26029
  scheduledAt,
25608
26030
  pendingAt,
25609
26031
  });
@@ -25625,7 +26047,7 @@ class StrategyMarkdownService {
25625
26047
  * @param scheduledAt - Signal creation timestamp in milliseconds
25626
26048
  * @param pendingAt - Pending timestamp in milliseconds
25627
26049
  */
25628
- this.breakeven = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt) => {
26050
+ this.breakeven = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen) => {
25629
26051
  this.loggerService.log("strategyMarkdownService breakeven", {
25630
26052
  symbol,
25631
26053
  currentPrice,
@@ -25661,6 +26083,8 @@ class StrategyMarkdownService {
25661
26083
  priceStopLoss,
25662
26084
  originalPriceTakeProfit,
25663
26085
  originalPriceStopLoss,
26086
+ originalPriceOpen,
26087
+ totalEntries,
25664
26088
  scheduledAt,
25665
26089
  pendingAt,
25666
26090
  });
@@ -25683,7 +26107,7 @@ class StrategyMarkdownService {
25683
26107
  * @param pendingAt - Pending timestamp in milliseconds
25684
26108
  * @param activateId - Optional identifier for the activation reason
25685
26109
  */
25686
- this.activateScheduled = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, activateId) => {
26110
+ this.activateScheduled = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, totalEntries, originalPriceOpen, activateId) => {
25687
26111
  this.loggerService.log("strategyMarkdownService activateScheduled", {
25688
26112
  symbol,
25689
26113
  currentPrice,
@@ -25721,6 +26145,72 @@ class StrategyMarkdownService {
25721
26145
  priceStopLoss,
25722
26146
  originalPriceTakeProfit,
25723
26147
  originalPriceStopLoss,
26148
+ originalPriceOpen,
26149
+ totalEntries,
26150
+ scheduledAt,
26151
+ pendingAt,
26152
+ });
26153
+ };
26154
+ /**
26155
+ * Records an average-buy (DCA) event when a new averaging entry is added to an open position.
26156
+ *
26157
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
26158
+ * @param currentPrice - Price at which the new averaging entry was executed
26159
+ * @param effectivePriceOpen - Averaged entry price after this addition
26160
+ * @param totalEntries - Total number of DCA entries after this addition
26161
+ * @param isBacktest - Whether this is a backtest or live trading event
26162
+ * @param context - Strategy context with strategyName, exchangeName, frameName
26163
+ * @param timestamp - Timestamp from StrategyCommitContract (execution context time)
26164
+ * @param position - Trade direction: "long" or "short"
26165
+ * @param priceOpen - Original entry price (unchanged by averaging)
26166
+ * @param priceTakeProfit - Effective take profit price
26167
+ * @param priceStopLoss - Effective stop loss price
26168
+ * @param originalPriceTakeProfit - Original take profit before trailing
26169
+ * @param originalPriceStopLoss - Original stop loss before trailing
26170
+ * @param scheduledAt - Signal creation timestamp in milliseconds
26171
+ * @param pendingAt - Pending timestamp in milliseconds
26172
+ */
26173
+ this.averageBuy = async (symbol, currentPrice, effectivePriceOpen, totalEntries, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, originalPriceOpen) => {
26174
+ this.loggerService.log("strategyMarkdownService averageBuy", {
26175
+ symbol,
26176
+ currentPrice,
26177
+ effectivePriceOpen,
26178
+ totalEntries,
26179
+ isBacktest,
26180
+ });
26181
+ if (!this.subscribe.hasValue()) {
26182
+ return;
26183
+ }
26184
+ const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
26185
+ exchangeName: context.exchangeName,
26186
+ strategyName: context.strategyName,
26187
+ frameName: context.frameName,
26188
+ });
26189
+ if (!pendingRow) {
26190
+ return;
26191
+ }
26192
+ const createdAt = new Date(timestamp).toISOString();
26193
+ const storage = this.getStorage(symbol, context.strategyName, context.exchangeName, context.frameName, isBacktest);
26194
+ storage.addEvent({
26195
+ timestamp,
26196
+ symbol,
26197
+ strategyName: context.strategyName,
26198
+ exchangeName: context.exchangeName,
26199
+ frameName: context.frameName,
26200
+ signalId: pendingRow.id,
26201
+ action: "average-buy",
26202
+ currentPrice,
26203
+ effectivePriceOpen,
26204
+ totalEntries,
26205
+ createdAt,
26206
+ backtest: isBacktest,
26207
+ position,
26208
+ priceOpen,
26209
+ priceTakeProfit,
26210
+ priceStopLoss,
26211
+ originalPriceTakeProfit,
26212
+ originalPriceStopLoss,
26213
+ originalPriceOpen,
25724
26214
  scheduledAt,
25725
26215
  pendingAt,
25726
26216
  });
@@ -25869,43 +26359,50 @@ class StrategyMarkdownService {
25869
26359
  exchangeName: event.exchangeName,
25870
26360
  frameName: event.frameName,
25871
26361
  strategyName: event.strategyName,
25872
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
26362
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
25873
26363
  const unPartialLoss = strategyCommitSubject
25874
26364
  .filter(({ action }) => action === "partial-loss")
25875
26365
  .connect(async (event) => await this.partialLoss(event.symbol, event.percentToClose, event.currentPrice, event.backtest, {
25876
26366
  exchangeName: event.exchangeName,
25877
26367
  frameName: event.frameName,
25878
26368
  strategyName: event.strategyName,
25879
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
26369
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
25880
26370
  const unTrailingStop = strategyCommitSubject
25881
26371
  .filter(({ action }) => action === "trailing-stop")
25882
26372
  .connect(async (event) => await this.trailingStop(event.symbol, event.percentShift, event.currentPrice, event.backtest, {
25883
26373
  exchangeName: event.exchangeName,
25884
26374
  frameName: event.frameName,
25885
26375
  strategyName: event.strategyName,
25886
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
26376
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
25887
26377
  const unTrailingTake = strategyCommitSubject
25888
26378
  .filter(({ action }) => action === "trailing-take")
25889
26379
  .connect(async (event) => await this.trailingTake(event.symbol, event.percentShift, event.currentPrice, event.backtest, {
25890
26380
  exchangeName: event.exchangeName,
25891
26381
  frameName: event.frameName,
25892
26382
  strategyName: event.strategyName,
25893
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
26383
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
25894
26384
  const unBreakeven = strategyCommitSubject
25895
26385
  .filter(({ action }) => action === "breakeven")
25896
26386
  .connect(async (event) => await this.breakeven(event.symbol, event.currentPrice, event.backtest, {
25897
26387
  exchangeName: event.exchangeName,
25898
26388
  frameName: event.frameName,
25899
26389
  strategyName: event.strategyName,
25900
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
26390
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen));
25901
26391
  const unActivateScheduled = strategyCommitSubject
25902
26392
  .filter(({ action }) => action === "activate-scheduled")
25903
26393
  .connect(async (event) => await this.activateScheduled(event.symbol, event.currentPrice, event.backtest, {
25904
26394
  exchangeName: event.exchangeName,
25905
26395
  frameName: event.frameName,
25906
26396
  strategyName: event.strategyName,
25907
- }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.activateId));
25908
- const disposeFn = compose(() => unCancelSchedule(), () => unClosePending(), () => unPartialProfit(), () => unPartialLoss(), () => unTrailingStop(), () => unTrailingTake(), () => unBreakeven(), () => unActivateScheduled());
26397
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.totalEntries, event.originalPriceOpen, event.activateId));
26398
+ const unAverageBuy = strategyCommitSubject
26399
+ .filter(({ action }) => action === "average-buy")
26400
+ .connect(async (event) => await this.averageBuy(event.symbol, event.currentPrice, event.effectivePriceOpen, event.totalEntries, event.backtest, {
26401
+ exchangeName: event.exchangeName,
26402
+ frameName: event.frameName,
26403
+ strategyName: event.strategyName,
26404
+ }, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.originalPriceOpen));
26405
+ const disposeFn = compose(() => unCancelSchedule(), () => unClosePending(), () => unPartialProfit(), () => unPartialLoss(), () => unTrailingStop(), () => unTrailingTake(), () => unBreakeven(), () => unActivateScheduled(), () => unAverageBuy());
25909
26406
  return () => {
25910
26407
  disposeFn();
25911
26408
  this.subscribe.clear();
@@ -27711,6 +28208,7 @@ const TRAILING_STOP_METHOD_NAME = "strategy.commitTrailingStop";
27711
28208
  const TRAILING_PROFIT_METHOD_NAME = "strategy.commitTrailingTake";
27712
28209
  const BREAKEVEN_METHOD_NAME = "strategy.commitBreakeven";
27713
28210
  const ACTIVATE_SCHEDULED_METHOD_NAME = "strategy.commitActivateScheduled";
28211
+ const AVERAGE_BUY_METHOD_NAME = "strategy.commitAverageBuy";
27714
28212
  /**
27715
28213
  * Cancels the scheduled signal without stopping the strategy.
27716
28214
  *
@@ -28063,6 +28561,45 @@ async function commitActivateScheduled(symbol, activateId) {
28063
28561
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
28064
28562
  await bt.strategyCoreService.activateScheduled(isBacktest, symbol, { exchangeName, frameName, strategyName }, activateId);
28065
28563
  }
28564
+ /**
28565
+ * Adds a new DCA entry to the active pending signal.
28566
+ *
28567
+ * Adds a new averaging entry at the current market price to the position's
28568
+ * entry history. Updates effectivePriceOpen (mean of all entries) and emits
28569
+ * an average-buy commit event.
28570
+ *
28571
+ * Automatically detects backtest/live mode from execution context.
28572
+ * Automatically fetches current price via getAveragePrice.
28573
+ *
28574
+ * @param symbol - Trading pair symbol
28575
+ * @returns Promise<boolean> - true if entry added, false if rejected
28576
+ *
28577
+ * @example
28578
+ * ```typescript
28579
+ * import { commitAverageBuy } from "backtest-kit";
28580
+ *
28581
+ * // Add DCA entry at current market price
28582
+ * const success = await commitAverageBuy("BTCUSDT");
28583
+ * if (success) {
28584
+ * console.log("DCA entry added");
28585
+ * }
28586
+ * ```
28587
+ */
28588
+ async function commitAverageBuy(symbol) {
28589
+ bt.loggerService.info(AVERAGE_BUY_METHOD_NAME, {
28590
+ symbol,
28591
+ });
28592
+ if (!ExecutionContextService.hasContext()) {
28593
+ throw new Error("commitAverageBuy requires an execution context");
28594
+ }
28595
+ if (!MethodContextService.hasContext()) {
28596
+ throw new Error("commitAverageBuy requires a method context");
28597
+ }
28598
+ const currentPrice = await getAveragePrice(symbol);
28599
+ const { backtest: isBacktest } = bt.executionContextService.context;
28600
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
28601
+ return await bt.strategyCoreService.averageBuy(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
28602
+ }
28066
28603
 
28067
28604
  const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
28068
28605
  /**
@@ -30343,6 +30880,7 @@ const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.commitPartialLoss";
30343
30880
  const BACKTEST_METHOD_NAME_TRAILING_STOP = "BacktestUtils.commitTrailingStop";
30344
30881
  const BACKTEST_METHOD_NAME_TRAILING_PROFIT = "BacktestUtils.commitTrailingTake";
30345
30882
  const BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED = "Backtest.commitActivateScheduled";
30883
+ const BACKTEST_METHOD_NAME_AVERAGE_BUY = "Backtest.commitAverageBuy";
30346
30884
  const BACKTEST_METHOD_NAME_GET_DATA = "BacktestUtils.getData";
30347
30885
  /**
30348
30886
  * Internal task function that runs backtest and handles completion.
@@ -31239,6 +31777,49 @@ class BacktestUtils {
31239
31777
  }
31240
31778
  await bt.strategyCoreService.activateScheduled(true, symbol, context, activateId);
31241
31779
  };
31780
+ /**
31781
+ * Adds a new DCA entry to the active pending signal.
31782
+ *
31783
+ * Adds a new averaging entry at currentPrice to the position's entry history.
31784
+ * Updates effectivePriceOpen (mean of all entries) and emits average-buy commit event.
31785
+ *
31786
+ * @param symbol - Trading pair symbol
31787
+ * @param currentPrice - New entry price to add to the averaging history
31788
+ * @param context - Execution context with strategyName, exchangeName, frameName
31789
+ * @returns Promise<boolean> - true if entry added, false if rejected
31790
+ *
31791
+ * @example
31792
+ * ```typescript
31793
+ * // Add DCA entry at current price
31794
+ * const success = await Backtest.commitAverageBuy("BTCUSDT", 42000, {
31795
+ * strategyName: "my-strategy",
31796
+ * exchangeName: "binance",
31797
+ * frameName: "1h"
31798
+ * });
31799
+ * if (success) {
31800
+ * console.log('DCA entry added');
31801
+ * }
31802
+ * ```
31803
+ */
31804
+ this.commitAverageBuy = async (symbol, currentPrice, context) => {
31805
+ bt.loggerService.info(BACKTEST_METHOD_NAME_AVERAGE_BUY, {
31806
+ symbol,
31807
+ currentPrice,
31808
+ context,
31809
+ });
31810
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_AVERAGE_BUY);
31811
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_AVERAGE_BUY);
31812
+ {
31813
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
31814
+ riskName &&
31815
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_AVERAGE_BUY);
31816
+ riskList &&
31817
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_AVERAGE_BUY));
31818
+ actions &&
31819
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_AVERAGE_BUY));
31820
+ }
31821
+ return await bt.strategyCoreService.averageBuy(true, symbol, currentPrice, context);
31822
+ };
31242
31823
  /**
31243
31824
  * Gets statistical data from all closed signals for a symbol-strategy pair.
31244
31825
  *
@@ -31415,6 +31996,7 @@ const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.commitPartialLoss";
31415
31996
  const LIVE_METHOD_NAME_TRAILING_STOP = "LiveUtils.commitTrailingStop";
31416
31997
  const LIVE_METHOD_NAME_TRAILING_PROFIT = "LiveUtils.commitTrailingTake";
31417
31998
  const LIVE_METHOD_NAME_ACTIVATE_SCHEDULED = "Live.commitActivateScheduled";
31999
+ const LIVE_METHOD_NAME_AVERAGE_BUY = "Live.commitAverageBuy";
31418
32000
  /**
31419
32001
  * Internal task function that runs live trading and handles completion.
31420
32002
  * Consumes live trading results and updates instance state flags.
@@ -32279,6 +32861,49 @@ class LiveUtils {
32279
32861
  frameName: "",
32280
32862
  }, activateId);
32281
32863
  };
32864
+ /**
32865
+ * Adds a new DCA entry to the active pending signal.
32866
+ *
32867
+ * Adds a new averaging entry at currentPrice to the position's entry history.
32868
+ * Updates effectivePriceOpen (mean of all entries) and emits average-buy commit event.
32869
+ *
32870
+ * @param symbol - Trading pair symbol
32871
+ * @param currentPrice - New entry price to add to the averaging history
32872
+ * @param context - Execution context with strategyName and exchangeName
32873
+ * @returns Promise<boolean> - true if entry added, false if rejected
32874
+ *
32875
+ * @example
32876
+ * ```typescript
32877
+ * // Add DCA entry at current price
32878
+ * const success = await Live.commitAverageBuy("BTCUSDT", 42000, {
32879
+ * strategyName: "my-strategy",
32880
+ * exchangeName: "binance"
32881
+ * });
32882
+ * if (success) {
32883
+ * console.log('DCA entry added');
32884
+ * }
32885
+ * ```
32886
+ */
32887
+ this.commitAverageBuy = async (symbol, currentPrice, context) => {
32888
+ bt.loggerService.info(LIVE_METHOD_NAME_AVERAGE_BUY, {
32889
+ symbol,
32890
+ currentPrice,
32891
+ context,
32892
+ });
32893
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_AVERAGE_BUY);
32894
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_AVERAGE_BUY);
32895
+ {
32896
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
32897
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_AVERAGE_BUY);
32898
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_AVERAGE_BUY));
32899
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_AVERAGE_BUY));
32900
+ }
32901
+ return await bt.strategyCoreService.averageBuy(false, symbol, currentPrice, {
32902
+ strategyName: context.strategyName,
32903
+ exchangeName: context.exchangeName,
32904
+ frameName: "",
32905
+ });
32906
+ };
32282
32907
  /**
32283
32908
  * Gets statistical data from all live trading events for a symbol-strategy pair.
32284
32909
  *
@@ -34942,6 +35567,8 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
34942
35567
  priceStopLoss: data.signal.priceStopLoss,
34943
35568
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
34944
35569
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
35570
+ originalPriceOpen: data.signal.originalPriceOpen,
35571
+ totalEntries: data.signal.totalEntries,
34945
35572
  note: data.signal.note,
34946
35573
  scheduledAt: data.signal.scheduledAt,
34947
35574
  pendingAt: data.signal.pendingAt,
@@ -34967,6 +35594,8 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
34967
35594
  priceStopLoss: data.signal.priceStopLoss,
34968
35595
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
34969
35596
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
35597
+ originalPriceOpen: data.signal.originalPriceOpen,
35598
+ totalEntries: data.signal.totalEntries,
34970
35599
  pnlPercentage: data.pnl.pnlPercentage,
34971
35600
  closeReason: data.closeReason,
34972
35601
  duration: durationMin,
@@ -34992,6 +35621,8 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
34992
35621
  priceStopLoss: data.signal.priceStopLoss,
34993
35622
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
34994
35623
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
35624
+ originalPriceOpen: data.signal.originalPriceOpen,
35625
+ totalEntries: data.signal.totalEntries,
34995
35626
  scheduledAt: data.signal.scheduledAt,
34996
35627
  currentPrice: data.currentPrice,
34997
35628
  createdAt: data.createdAt,
@@ -35015,6 +35646,8 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
35015
35646
  priceStopLoss: data.signal.priceStopLoss,
35016
35647
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
35017
35648
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
35649
+ originalPriceOpen: data.signal.originalPriceOpen,
35650
+ totalEntries: data.signal.totalEntries,
35018
35651
  cancelReason: data.reason,
35019
35652
  cancelId: data.cancelId,
35020
35653
  duration: durationMin,
@@ -35047,6 +35680,8 @@ const CREATE_PARTIAL_PROFIT_NOTIFICATION_FN = (data) => ({
35047
35680
  priceStopLoss: data.data.priceStopLoss,
35048
35681
  originalPriceTakeProfit: data.data.originalPriceTakeProfit,
35049
35682
  originalPriceStopLoss: data.data.originalPriceStopLoss,
35683
+ originalPriceOpen: data.data.originalPriceOpen,
35684
+ totalEntries: data.data.totalEntries,
35050
35685
  scheduledAt: data.data.scheduledAt,
35051
35686
  pendingAt: data.data.pendingAt,
35052
35687
  createdAt: data.timestamp,
@@ -35073,6 +35708,8 @@ const CREATE_PARTIAL_LOSS_NOTIFICATION_FN = (data) => ({
35073
35708
  priceStopLoss: data.data.priceStopLoss,
35074
35709
  originalPriceTakeProfit: data.data.originalPriceTakeProfit,
35075
35710
  originalPriceStopLoss: data.data.originalPriceStopLoss,
35711
+ originalPriceOpen: data.data.originalPriceOpen,
35712
+ totalEntries: data.data.totalEntries,
35076
35713
  scheduledAt: data.data.scheduledAt,
35077
35714
  pendingAt: data.data.pendingAt,
35078
35715
  createdAt: data.timestamp,
@@ -35098,6 +35735,8 @@ const CREATE_BREAKEVEN_NOTIFICATION_FN = (data) => ({
35098
35735
  priceStopLoss: data.data.priceStopLoss,
35099
35736
  originalPriceTakeProfit: data.data.originalPriceTakeProfit,
35100
35737
  originalPriceStopLoss: data.data.originalPriceStopLoss,
35738
+ originalPriceOpen: data.data.originalPriceOpen,
35739
+ totalEntries: data.data.totalEntries,
35101
35740
  scheduledAt: data.data.scheduledAt,
35102
35741
  pendingAt: data.data.pendingAt,
35103
35742
  createdAt: data.timestamp,
@@ -35128,6 +35767,8 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
35128
35767
  priceStopLoss: data.priceStopLoss,
35129
35768
  originalPriceTakeProfit: data.originalPriceTakeProfit,
35130
35769
  originalPriceStopLoss: data.originalPriceStopLoss,
35770
+ originalPriceOpen: data.originalPriceOpen,
35771
+ totalEntries: data.totalEntries,
35131
35772
  scheduledAt: data.scheduledAt,
35132
35773
  pendingAt: data.pendingAt,
35133
35774
  createdAt: data.timestamp,
@@ -35151,6 +35792,8 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
35151
35792
  priceStopLoss: data.priceStopLoss,
35152
35793
  originalPriceTakeProfit: data.originalPriceTakeProfit,
35153
35794
  originalPriceStopLoss: data.originalPriceStopLoss,
35795
+ originalPriceOpen: data.originalPriceOpen,
35796
+ totalEntries: data.totalEntries,
35154
35797
  scheduledAt: data.scheduledAt,
35155
35798
  pendingAt: data.pendingAt,
35156
35799
  createdAt: data.timestamp,
@@ -35173,6 +35816,8 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
35173
35816
  priceStopLoss: data.priceStopLoss,
35174
35817
  originalPriceTakeProfit: data.originalPriceTakeProfit,
35175
35818
  originalPriceStopLoss: data.originalPriceStopLoss,
35819
+ originalPriceOpen: data.originalPriceOpen,
35820
+ totalEntries: data.totalEntries,
35176
35821
  scheduledAt: data.scheduledAt,
35177
35822
  pendingAt: data.pendingAt,
35178
35823
  createdAt: data.timestamp,
@@ -35196,6 +35841,8 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
35196
35841
  priceStopLoss: data.priceStopLoss,
35197
35842
  originalPriceTakeProfit: data.originalPriceTakeProfit,
35198
35843
  originalPriceStopLoss: data.originalPriceStopLoss,
35844
+ originalPriceOpen: data.originalPriceOpen,
35845
+ totalEntries: data.totalEntries,
35199
35846
  scheduledAt: data.scheduledAt,
35200
35847
  pendingAt: data.pendingAt,
35201
35848
  createdAt: data.timestamp,
@@ -35219,6 +35866,8 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
35219
35866
  priceStopLoss: data.priceStopLoss,
35220
35867
  originalPriceTakeProfit: data.originalPriceTakeProfit,
35221
35868
  originalPriceStopLoss: data.originalPriceStopLoss,
35869
+ originalPriceOpen: data.originalPriceOpen,
35870
+ totalEntries: data.totalEntries,
35222
35871
  scheduledAt: data.scheduledAt,
35223
35872
  pendingAt: data.pendingAt,
35224
35873
  createdAt: data.timestamp,
@@ -35242,6 +35891,33 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
35242
35891
  priceStopLoss: data.priceStopLoss,
35243
35892
  originalPriceTakeProfit: data.originalPriceTakeProfit,
35244
35893
  originalPriceStopLoss: data.originalPriceStopLoss,
35894
+ originalPriceOpen: data.originalPriceOpen,
35895
+ totalEntries: data.totalEntries,
35896
+ scheduledAt: data.scheduledAt,
35897
+ pendingAt: data.pendingAt,
35898
+ createdAt: data.timestamp,
35899
+ };
35900
+ }
35901
+ if (data.action === "average-buy") {
35902
+ return {
35903
+ type: "average_buy.commit",
35904
+ id: CREATE_KEY_FN$1(),
35905
+ timestamp: data.timestamp,
35906
+ backtest: data.backtest,
35907
+ symbol: data.symbol,
35908
+ strategyName: data.strategyName,
35909
+ exchangeName: data.exchangeName,
35910
+ signalId: data.signalId,
35911
+ currentPrice: data.currentPrice,
35912
+ effectivePriceOpen: data.effectivePriceOpen,
35913
+ totalEntries: data.totalEntries,
35914
+ position: data.position,
35915
+ priceOpen: data.priceOpen,
35916
+ priceTakeProfit: data.priceTakeProfit,
35917
+ priceStopLoss: data.priceStopLoss,
35918
+ originalPriceTakeProfit: data.originalPriceTakeProfit,
35919
+ originalPriceStopLoss: data.originalPriceStopLoss,
35920
+ originalPriceOpen: data.originalPriceOpen,
35245
35921
  scheduledAt: data.scheduledAt,
35246
35922
  pendingAt: data.pendingAt,
35247
35923
  createdAt: data.timestamp,
@@ -37584,4 +38260,4 @@ const set = (object, path, value) => {
37584
38260
  }
37585
38261
  };
37586
38262
 
37587
- export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, dumpMessages, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate, waitForCandle, warmCandles };
38263
+ export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, dumpMessages, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate, waitForCandle, warmCandles };