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 +30 -50
- package/build/index.mjs +30 -50
- package/package.json +1 -1
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 ×
|
|
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
|
|
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
|
|
2963
|
-
|
|
2964
|
-
|
|
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
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
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
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
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
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
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
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
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
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
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 ×
|
|
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 *
|
|
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 ×
|
|
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
|
|
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
|
|
2943
|
-
|
|
2944
|
-
|
|
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
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
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
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
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
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
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
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
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
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
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 ×
|
|
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 *
|
|
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: прибыль при росте цены
|