backtest-kit 3.1.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -2907,7 +2907,7 @@ class ExchangeConnectionService {
2907
2907
  * - Calculates weighted PNL: Σ(percent_i × pnl_i) for each partial + (remaining% × final_pnl)
2908
2908
  * - Each partial close has its own slippage
2909
2909
  * - Open fee is charged once; close fees are proportional to each partial's size
2910
- * - Total fees = CC_PERCENT_FEE (open) + CC_PERCENT_FEE × 1 (closes sum to 100%) = 2 × CC_PERCENT_FEE
2910
+ * - Total fees = CC_PERCENT_FEE (open) + Σ CC_PERCENT_FEE × (partial% / 100) × (closeWithSlip / openWithSlip)
2911
2911
  *
2912
2912
  * Formula breakdown:
2913
2913
  * 1. Apply slippage to open/close prices (worse execution)
@@ -2916,7 +2916,7 @@ class ExchangeConnectionService {
2916
2916
  * 2. Calculate raw PNL percentage
2917
2917
  * - LONG: ((closePrice - openPrice) / openPrice) * 100
2918
2918
  * - SHORT: ((openPrice - closePrice) / openPrice) * 100
2919
- * 3. Subtract total fees (0.1% * 2 = 0.2% per transaction)
2919
+ * 3. Subtract total fees: open fee + close fee adjusted for slippage-affected execution price
2920
2920
  *
2921
2921
  * @param signal - Closed signal with position details and optional partial history
2922
2922
  * @param priceClose - Actual close price at final exit
@@ -2956,67 +2956,47 @@ const toProfitLossDto = (signal, priceClose) => {
2956
2956
  let totalWeightedPnl = 0;
2957
2957
  // Open fee is paid once for the whole position
2958
2958
  let totalFees = GLOBAL_CONFIG.CC_PERCENT_FEE;
2959
+ // priceOpenWithSlippage is the same for all partials — compute once
2960
+ const priceOpenWithSlippage = signal.position === "long"
2961
+ ? priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
2962
+ : priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2959
2963
  // Calculate PNL for each partial close
2960
2964
  for (const partial of signal._partial) {
2961
2965
  const partialPercent = partial.percent;
2962
- const partialPrice = partial.price;
2963
- // Apply slippage to prices
2964
- let priceOpenWithSlippage;
2965
- let priceCloseWithSlippage;
2966
- if (signal.position === "long") {
2967
- priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2968
- priceCloseWithSlippage = partialPrice * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2969
- }
2970
- else {
2971
- priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2972
- priceCloseWithSlippage = partialPrice * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2973
- }
2966
+ const priceCloseWithSlippage = signal.position === "long"
2967
+ ? partial.price * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
2968
+ : partial.price * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2974
2969
  // Calculate PNL for this partial
2975
- let partialPnl;
2976
- if (signal.position === "long") {
2977
- partialPnl = ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
2978
- }
2979
- else {
2980
- partialPnl = ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
2981
- }
2970
+ const partialPnl = signal.position === "long"
2971
+ ? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
2972
+ : ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
2982
2973
  // Weight by percentage of position closed
2983
- const weightedPnl = (partialPercent / 100) * partialPnl;
2984
- totalWeightedPnl += weightedPnl;
2985
- // Close fee is proportional to the size of this partial
2986
- totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (partialPercent / 100);
2974
+ totalWeightedPnl += (partialPercent / 100) * partialPnl;
2975
+ // Close fee is proportional to the size of this partial and adjusted for slippage
2976
+ totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (partialPercent / 100) * (priceCloseWithSlippage / priceOpenWithSlippage);
2987
2977
  }
2988
2978
  // Calculate PNL for remaining position (if any)
2989
2979
  // Compute totalClosed from _partial array
2990
2980
  const totalClosed = signal._partial.reduce((sum, p) => sum + p.percent, 0);
2981
+ if (totalClosed > 100) {
2982
+ throw new Error(`Partial closes exceed 100%: ${totalClosed}% (signal id: ${signal.id})`);
2983
+ }
2991
2984
  const remainingPercent = 100 - totalClosed;
2992
2985
  if (remainingPercent > 0) {
2993
- // Apply slippage
2994
- let priceOpenWithSlippage;
2995
- let priceCloseWithSlippage;
2996
- if (signal.position === "long") {
2997
- priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2998
- priceCloseWithSlippage = priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2999
- }
3000
- else {
3001
- priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3002
- priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3003
- }
2986
+ const priceCloseWithSlippage = signal.position === "long"
2987
+ ? priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
2988
+ : priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3004
2989
  // Calculate PNL for remaining
3005
- let remainingPnl;
3006
- if (signal.position === "long") {
3007
- remainingPnl = ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
3008
- }
3009
- else {
3010
- remainingPnl = ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
3011
- }
2990
+ const remainingPnl = signal.position === "long"
2991
+ ? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
2992
+ : ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
3012
2993
  // Weight by remaining percentage
3013
- const weightedRemainingPnl = (remainingPercent / 100) * remainingPnl;
3014
- totalWeightedPnl += weightedRemainingPnl;
3015
- // Close fee is proportional to the remaining size
3016
- totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (remainingPercent / 100);
2994
+ totalWeightedPnl += (remainingPercent / 100) * remainingPnl;
2995
+ // Close fee is proportional to the remaining size and adjusted for slippage
2996
+ totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (remainingPercent / 100) * (priceCloseWithSlippage / priceOpenWithSlippage);
3017
2997
  }
3018
2998
  // Subtract total fees from weighted PNL
3019
- // totalFees = CC_PERCENT_FEE (open) + CC_PERCENT_FEE × 1 (all closes sum to 100%) = 2 × CC_PERCENT_FEE
2999
+ // totalFees = CC_PERCENT_FEE (open) + Σ CC_PERCENT_FEE × (partialPercent/100) × (closeWithSlip/openWithSlip)
3020
3000
  const pnlPercentage = totalWeightedPnl - totalFees;
3021
3001
  return {
3022
3002
  pnlPercentage,
@@ -3037,8 +3017,8 @@ const toProfitLossDto = (signal, priceClose) => {
3037
3017
  priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3038
3018
  priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3039
3019
  }
3040
- // Применяем комиссию дважды (при открытии и закрытии)
3041
- const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE * 2;
3020
+ // Открытие: комиссия от цены входа; закрытие: комиссия от фактической цены выхода (с учётом slippage)
3021
+ const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE * (1 + priceCloseWithSlippage / priceOpenWithSlippage);
3042
3022
  let pnlPercentage;
3043
3023
  if (signal.position === "long") {
3044
3024
  // LONG: прибыль при росте цены
package/build/index.mjs CHANGED
@@ -2887,7 +2887,7 @@ class ExchangeConnectionService {
2887
2887
  * - Calculates weighted PNL: Σ(percent_i × pnl_i) for each partial + (remaining% × final_pnl)
2888
2888
  * - Each partial close has its own slippage
2889
2889
  * - 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
2890
+ * - Total fees = CC_PERCENT_FEE (open) + Σ CC_PERCENT_FEE × (partial% / 100) × (closeWithSlip / openWithSlip)
2891
2891
  *
2892
2892
  * Formula breakdown:
2893
2893
  * 1. Apply slippage to open/close prices (worse execution)
@@ -2896,7 +2896,7 @@ class ExchangeConnectionService {
2896
2896
  * 2. Calculate raw PNL percentage
2897
2897
  * - LONG: ((closePrice - openPrice) / openPrice) * 100
2898
2898
  * - SHORT: ((openPrice - closePrice) / openPrice) * 100
2899
- * 3. Subtract total fees (0.1% * 2 = 0.2% per transaction)
2899
+ * 3. Subtract total fees: open fee + close fee adjusted for slippage-affected execution price
2900
2900
  *
2901
2901
  * @param signal - Closed signal with position details and optional partial history
2902
2902
  * @param priceClose - Actual close price at final exit
@@ -2936,67 +2936,47 @@ const toProfitLossDto = (signal, priceClose) => {
2936
2936
  let totalWeightedPnl = 0;
2937
2937
  // Open fee is paid once for the whole position
2938
2938
  let totalFees = GLOBAL_CONFIG.CC_PERCENT_FEE;
2939
+ // priceOpenWithSlippage is the same for all partials — compute once
2940
+ const priceOpenWithSlippage = signal.position === "long"
2941
+ ? priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
2942
+ : priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2939
2943
  // Calculate PNL for each partial close
2940
2944
  for (const partial of signal._partial) {
2941
2945
  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
- }
2946
+ const priceCloseWithSlippage = signal.position === "long"
2947
+ ? partial.price * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
2948
+ : partial.price * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2954
2949
  // 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
- }
2950
+ const partialPnl = signal.position === "long"
2951
+ ? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
2952
+ : ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
2962
2953
  // 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);
2954
+ totalWeightedPnl += (partialPercent / 100) * partialPnl;
2955
+ // Close fee is proportional to the size of this partial and adjusted for slippage
2956
+ totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (partialPercent / 100) * (priceCloseWithSlippage / priceOpenWithSlippage);
2967
2957
  }
2968
2958
  // Calculate PNL for remaining position (if any)
2969
2959
  // Compute totalClosed from _partial array
2970
2960
  const totalClosed = signal._partial.reduce((sum, p) => sum + p.percent, 0);
2961
+ if (totalClosed > 100) {
2962
+ throw new Error(`Partial closes exceed 100%: ${totalClosed}% (signal id: ${signal.id})`);
2963
+ }
2971
2964
  const remainingPercent = 100 - totalClosed;
2972
2965
  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
- }
2966
+ const priceCloseWithSlippage = signal.position === "long"
2967
+ ? priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
2968
+ : priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
2984
2969
  // 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
- }
2970
+ const remainingPnl = signal.position === "long"
2971
+ ? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
2972
+ : ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
2992
2973
  // 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);
2974
+ totalWeightedPnl += (remainingPercent / 100) * remainingPnl;
2975
+ // Close fee is proportional to the remaining size and adjusted for slippage
2976
+ totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (remainingPercent / 100) * (priceCloseWithSlippage / priceOpenWithSlippage);
2997
2977
  }
2998
2978
  // 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
2979
+ // totalFees = CC_PERCENT_FEE (open) + Σ CC_PERCENT_FEE × (partialPercent/100) × (closeWithSlip/openWithSlip)
3000
2980
  const pnlPercentage = totalWeightedPnl - totalFees;
3001
2981
  return {
3002
2982
  pnlPercentage,
@@ -3017,8 +2997,8 @@ const toProfitLossDto = (signal, priceClose) => {
3017
2997
  priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3018
2998
  priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3019
2999
  }
3020
- // Применяем комиссию дважды (при открытии и закрытии)
3021
- const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE * 2;
3000
+ // Открытие: комиссия от цены входа; закрытие: комиссия от фактической цены выхода (с учётом slippage)
3001
+ const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE * (1 + priceCloseWithSlippage / priceOpenWithSlippage);
3022
3002
  let pnlPercentage;
3023
3003
  if (signal.position === "long") {
3024
3004
  // LONG: прибыль при росте цены
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",