@tradejs/node 2.0.9 → 2.0.10
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/dist/backtest.js +239 -69
- package/dist/backtest.mjs +239 -69
- package/package.json +4 -4
package/dist/backtest.js
CHANGED
|
@@ -2704,6 +2704,22 @@ var import_backtest = require("@tradejs/core/backtest");
|
|
|
2704
2704
|
var import_trade = require("@tradejs/core/trade");
|
|
2705
2705
|
var import_math = require("@tradejs/core/math");
|
|
2706
2706
|
var PRICE_PRECISION = 8;
|
|
2707
|
+
var normalizeInstrumentOrderQty = ({
|
|
2708
|
+
qty,
|
|
2709
|
+
symbol,
|
|
2710
|
+
instrument
|
|
2711
|
+
}) => {
|
|
2712
|
+
if (!instrument || instrument.symbol.trim().toUpperCase() !== symbol.trim().toUpperCase()) {
|
|
2713
|
+
return { qty, minOrderQty: null };
|
|
2714
|
+
}
|
|
2715
|
+
const qtyStep = Number(instrument.venueMetadata?.qtyStep);
|
|
2716
|
+
const minOrderQty = Number(instrument.venueMetadata?.minOrderQty);
|
|
2717
|
+
const normalizedQty = Number.isFinite(qtyStep) && qtyStep > 0 ? Number((Math.floor(qty / qtyStep) * qtyStep).toFixed(12)) : qty;
|
|
2718
|
+
return {
|
|
2719
|
+
qty: normalizedQty,
|
|
2720
|
+
minOrderQty: Number.isFinite(minOrderQty) && minOrderQty > 0 ? minOrderQty : null
|
|
2721
|
+
};
|
|
2722
|
+
};
|
|
2707
2723
|
var createTestConnector = (connector, context) => {
|
|
2708
2724
|
let state = {};
|
|
2709
2725
|
const orderLog = [];
|
|
@@ -2724,6 +2740,7 @@ var createTestConnector = (connector, context) => {
|
|
|
2724
2740
|
let takeProfits = [];
|
|
2725
2741
|
let stopLossPrice = null;
|
|
2726
2742
|
let currentTradeResult = null;
|
|
2743
|
+
let currentEntryLegResults = [];
|
|
2727
2744
|
const closedSignalResults = [];
|
|
2728
2745
|
const logOrder = (data) => {
|
|
2729
2746
|
const nextEntry = {
|
|
@@ -2767,6 +2784,52 @@ var createTestConnector = (connector, context) => {
|
|
|
2767
2784
|
}
|
|
2768
2785
|
return (previousValue * previousQty + nextValue * nextQty) / (previousQty + nextQty);
|
|
2769
2786
|
};
|
|
2787
|
+
const createOpenTradeResult = ({
|
|
2788
|
+
signalId,
|
|
2789
|
+
direction,
|
|
2790
|
+
qty,
|
|
2791
|
+
timestamp,
|
|
2792
|
+
requestedEntryPrice,
|
|
2793
|
+
entryPrice,
|
|
2794
|
+
fee,
|
|
2795
|
+
slippageBreakdown,
|
|
2796
|
+
entrySlippageCost
|
|
2797
|
+
}) => ({
|
|
2798
|
+
signalId,
|
|
2799
|
+
direction,
|
|
2800
|
+
qty,
|
|
2801
|
+
closedQty: 0,
|
|
2802
|
+
entryTimestamp: timestamp,
|
|
2803
|
+
exitTimestamp: null,
|
|
2804
|
+
exitReason: null,
|
|
2805
|
+
requestedEntryPrice,
|
|
2806
|
+
entryPrice,
|
|
2807
|
+
requestedExitPrice: null,
|
|
2808
|
+
exitPrice: null,
|
|
2809
|
+
grossProfit: 0,
|
|
2810
|
+
netProfit: -fee,
|
|
2811
|
+
openFee: fee,
|
|
2812
|
+
closeFee: 0,
|
|
2813
|
+
fundingFee: executionCostModel?.funding.enabled ? 0 : null,
|
|
2814
|
+
totalFee: fee,
|
|
2815
|
+
entrySlippagePrice: entryPrice - requestedEntryPrice,
|
|
2816
|
+
entrySlippageBps: getSlippageBps(requestedEntryPrice, entryPrice),
|
|
2817
|
+
entryBaseSlippageBps: slippageBreakdown.baseSlippageBps,
|
|
2818
|
+
entrySpreadBps: slippageBreakdown.spreadBps,
|
|
2819
|
+
entrySpreadSlippageBps: slippageBreakdown.spreadSlippageBps,
|
|
2820
|
+
entryMarketImpactBps: slippageBreakdown.marketImpactBps,
|
|
2821
|
+
entryDelayRiskBps: slippageBreakdown.delayRiskBps,
|
|
2822
|
+
entrySlippageCost,
|
|
2823
|
+
exitSlippagePrice: null,
|
|
2824
|
+
exitSlippageBps: null,
|
|
2825
|
+
exitBaseSlippageBps: null,
|
|
2826
|
+
exitSpreadBps: null,
|
|
2827
|
+
exitSpreadSlippageBps: null,
|
|
2828
|
+
exitMarketImpactBps: null,
|
|
2829
|
+
exitDelayRiskBps: null,
|
|
2830
|
+
exitSlippageCost: 0,
|
|
2831
|
+
totalSlippageCost: entrySlippageCost
|
|
2832
|
+
});
|
|
2770
2833
|
const finalizeTradeResult = (tradeResult, timestamp) => {
|
|
2771
2834
|
if (!tradeResult.exitReason) {
|
|
2772
2835
|
return void 0;
|
|
@@ -2806,7 +2869,9 @@ var createTestConnector = (connector, context) => {
|
|
|
2806
2869
|
closedQty: (0, import_math.round)(tradeResult.closedQty)
|
|
2807
2870
|
};
|
|
2808
2871
|
};
|
|
2809
|
-
const
|
|
2872
|
+
const appendExitToTradeResult = ({
|
|
2873
|
+
tradeResult,
|
|
2874
|
+
direction,
|
|
2810
2875
|
timestamp,
|
|
2811
2876
|
reason,
|
|
2812
2877
|
requestedPrice,
|
|
@@ -2816,65 +2881,62 @@ var createTestConnector = (connector, context) => {
|
|
|
2816
2881
|
fee,
|
|
2817
2882
|
slippageBreakdown
|
|
2818
2883
|
}) => {
|
|
2819
|
-
|
|
2820
|
-
return;
|
|
2821
|
-
}
|
|
2822
|
-
const previousClosedQty = currentTradeResult.closedQty;
|
|
2884
|
+
const previousClosedQty = tradeResult.closedQty;
|
|
2823
2885
|
const requestedExitPrice = getWeightedAverage(
|
|
2824
|
-
|
|
2886
|
+
tradeResult.requestedExitPrice,
|
|
2825
2887
|
previousClosedQty,
|
|
2826
2888
|
requestedPrice,
|
|
2827
2889
|
qty
|
|
2828
2890
|
);
|
|
2829
2891
|
const exitPrice = getWeightedAverage(
|
|
2830
|
-
|
|
2892
|
+
tradeResult.exitPrice,
|
|
2831
2893
|
previousClosedQty,
|
|
2832
2894
|
executionPrice,
|
|
2833
2895
|
qty
|
|
2834
2896
|
);
|
|
2835
2897
|
const exitBaseSlippageBps = getWeightedAverage(
|
|
2836
|
-
|
|
2898
|
+
tradeResult.exitBaseSlippageBps,
|
|
2837
2899
|
previousClosedQty,
|
|
2838
2900
|
slippageBreakdown.baseSlippageBps,
|
|
2839
2901
|
qty
|
|
2840
2902
|
);
|
|
2841
2903
|
const exitSpreadBps = getWeightedAverage(
|
|
2842
|
-
|
|
2904
|
+
tradeResult.exitSpreadBps,
|
|
2843
2905
|
previousClosedQty,
|
|
2844
2906
|
slippageBreakdown.spreadBps,
|
|
2845
2907
|
qty
|
|
2846
2908
|
);
|
|
2847
2909
|
const exitSpreadSlippageBps = getWeightedAverage(
|
|
2848
|
-
|
|
2910
|
+
tradeResult.exitSpreadSlippageBps,
|
|
2849
2911
|
previousClosedQty,
|
|
2850
2912
|
slippageBreakdown.spreadSlippageBps,
|
|
2851
2913
|
qty
|
|
2852
2914
|
);
|
|
2853
2915
|
const exitMarketImpactBps = getWeightedAverage(
|
|
2854
|
-
|
|
2916
|
+
tradeResult.exitMarketImpactBps,
|
|
2855
2917
|
previousClosedQty,
|
|
2856
2918
|
slippageBreakdown.marketImpactBps,
|
|
2857
2919
|
qty
|
|
2858
2920
|
);
|
|
2859
2921
|
const exitDelayRiskBps = null;
|
|
2860
|
-
const exitSlippageCost =
|
|
2922
|
+
const exitSlippageCost = tradeResult.exitSlippageCost + getSlippageCost({
|
|
2861
2923
|
requestedPrice,
|
|
2862
2924
|
executionPrice,
|
|
2863
|
-
direction
|
|
2925
|
+
direction,
|
|
2864
2926
|
stage: "exit",
|
|
2865
2927
|
qty
|
|
2866
2928
|
});
|
|
2867
|
-
|
|
2868
|
-
...
|
|
2929
|
+
return {
|
|
2930
|
+
...tradeResult,
|
|
2869
2931
|
closedQty: previousClosedQty + qty,
|
|
2870
2932
|
exitTimestamp: timestamp,
|
|
2871
2933
|
exitReason: reason,
|
|
2872
2934
|
requestedExitPrice,
|
|
2873
2935
|
exitPrice,
|
|
2874
|
-
grossProfit:
|
|
2875
|
-
netProfit:
|
|
2876
|
-
closeFee:
|
|
2877
|
-
totalFee:
|
|
2936
|
+
grossProfit: tradeResult.grossProfit + grossProfit,
|
|
2937
|
+
netProfit: tradeResult.netProfit + grossProfit - fee,
|
|
2938
|
+
closeFee: tradeResult.closeFee + fee,
|
|
2939
|
+
totalFee: tradeResult.openFee + tradeResult.closeFee + fee + (tradeResult.fundingFee ?? 0),
|
|
2878
2940
|
exitSlippagePrice: exitPrice - requestedExitPrice,
|
|
2879
2941
|
exitSlippageBps: getSlippageBps(requestedExitPrice, exitPrice),
|
|
2880
2942
|
exitBaseSlippageBps,
|
|
@@ -2883,9 +2945,73 @@ var createTestConnector = (connector, context) => {
|
|
|
2883
2945
|
exitMarketImpactBps,
|
|
2884
2946
|
exitDelayRiskBps,
|
|
2885
2947
|
exitSlippageCost,
|
|
2886
|
-
totalSlippageCost:
|
|
2948
|
+
totalSlippageCost: tradeResult.entrySlippageCost + exitSlippageCost
|
|
2887
2949
|
};
|
|
2888
2950
|
};
|
|
2951
|
+
const recordExitResult = ({
|
|
2952
|
+
timestamp,
|
|
2953
|
+
reason,
|
|
2954
|
+
requestedPrice,
|
|
2955
|
+
executionPrice,
|
|
2956
|
+
qty,
|
|
2957
|
+
grossProfit,
|
|
2958
|
+
fee,
|
|
2959
|
+
slippageBreakdown
|
|
2960
|
+
}) => {
|
|
2961
|
+
if (!currentPosition) {
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
if (currentTradeResult) {
|
|
2965
|
+
currentTradeResult = appendExitToTradeResult({
|
|
2966
|
+
tradeResult: currentTradeResult,
|
|
2967
|
+
direction: currentPosition.direction,
|
|
2968
|
+
timestamp,
|
|
2969
|
+
reason,
|
|
2970
|
+
requestedPrice,
|
|
2971
|
+
executionPrice,
|
|
2972
|
+
qty,
|
|
2973
|
+
grossProfit,
|
|
2974
|
+
fee,
|
|
2975
|
+
slippageBreakdown
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
const remainingQtyByLeg = currentEntryLegResults.map(
|
|
2979
|
+
(tradeResult) => Math.max(0, tradeResult.qty - tradeResult.closedQty)
|
|
2980
|
+
);
|
|
2981
|
+
const totalRemainingQty = remainingQtyByLeg.reduce(
|
|
2982
|
+
(total, remainingQty) => total + remainingQty,
|
|
2983
|
+
0
|
|
2984
|
+
);
|
|
2985
|
+
if (totalRemainingQty <= 0) {
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
let allocatedQty = 0;
|
|
2989
|
+
const activeLegIndexes = remainingQtyByLeg.map((remainingQty, index) => ({ remainingQty, index })).filter(({ remainingQty }) => remainingQty > 0);
|
|
2990
|
+
for (const [activeIndex, leg] of activeLegIndexes.entries()) {
|
|
2991
|
+
const isLast = activeIndex === activeLegIndexes.length - 1;
|
|
2992
|
+
const legExitQty = Math.min(
|
|
2993
|
+
leg.remainingQty,
|
|
2994
|
+
isLast ? Math.max(0, qty - allocatedQty) : qty * (leg.remainingQty / totalRemainingQty)
|
|
2995
|
+
);
|
|
2996
|
+
if (legExitQty <= 0) continue;
|
|
2997
|
+
allocatedQty += legExitQty;
|
|
2998
|
+
const legResult = currentEntryLegResults[leg.index];
|
|
2999
|
+
const legGrossProfit = currentPosition.direction === "LONG" ? (executionPrice - legResult.entryPrice) * legExitQty : (legResult.entryPrice - executionPrice) * legExitQty;
|
|
3000
|
+
const legFee = executionPrice * legExitQty * takerFeeRate;
|
|
3001
|
+
currentEntryLegResults[leg.index] = appendExitToTradeResult({
|
|
3002
|
+
tradeResult: legResult,
|
|
3003
|
+
direction: currentPosition.direction,
|
|
3004
|
+
timestamp,
|
|
3005
|
+
reason,
|
|
3006
|
+
requestedPrice,
|
|
3007
|
+
executionPrice,
|
|
3008
|
+
qty: legExitQty,
|
|
3009
|
+
grossProfit: legGrossProfit,
|
|
3010
|
+
fee: legFee,
|
|
3011
|
+
slippageBreakdown
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
};
|
|
2889
3015
|
const clearPosition = (timestamp) => {
|
|
2890
3016
|
takeProfits = [];
|
|
2891
3017
|
stopLossPrice = null;
|
|
@@ -2894,7 +3020,18 @@ var createTestConnector = (connector, context) => {
|
|
|
2894
3020
|
return;
|
|
2895
3021
|
}
|
|
2896
3022
|
if (context?.mlEnabled || context?.aiEnabled) {
|
|
2897
|
-
|
|
3023
|
+
const attributedResults = currentEntryLegResults.filter(({ signalId }) => signalId).map((tradeResult) => finalizeTradeResult(tradeResult, timestamp)).filter(
|
|
3024
|
+
(tradeResult) => Boolean(tradeResult)
|
|
3025
|
+
);
|
|
3026
|
+
if (attributedResults.length) {
|
|
3027
|
+
for (const tradeResult of attributedResults) {
|
|
3028
|
+
closedSignalResults.push({
|
|
3029
|
+
signalId: tradeResult.signalId,
|
|
3030
|
+
profit: (0, import_math.round)(tradeResult.netProfit),
|
|
3031
|
+
tradeResult
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
} else if (currentSignalId) {
|
|
2898
3035
|
const tradeResult = currentTradeResult ? finalizeTradeResult(currentTradeResult, timestamp) : void 0;
|
|
2899
3036
|
closedSignalResults.push({
|
|
2900
3037
|
signalId: currentSignalId,
|
|
@@ -2917,6 +3054,7 @@ var createTestConnector = (connector, context) => {
|
|
|
2917
3054
|
currentPosition = null;
|
|
2918
3055
|
currentSignalId = null;
|
|
2919
3056
|
currentTradeResult = null;
|
|
3057
|
+
currentEntryLegResults = [];
|
|
2920
3058
|
currentPositionProfit = 0;
|
|
2921
3059
|
};
|
|
2922
3060
|
const getNetProfit = ({
|
|
@@ -2949,6 +3087,25 @@ var createTestConnector = (connector, context) => {
|
|
|
2949
3087
|
currentTradeResult.netProfit -= fundingCost;
|
|
2950
3088
|
currentTradeResult.totalFee += fundingCost;
|
|
2951
3089
|
}
|
|
3090
|
+
const remainingLegQty = currentEntryLegResults.reduce(
|
|
3091
|
+
(total, tradeResult) => total + Math.max(0, tradeResult.qty - tradeResult.closedQty),
|
|
3092
|
+
0
|
|
3093
|
+
);
|
|
3094
|
+
if (remainingLegQty > 0) {
|
|
3095
|
+
currentEntryLegResults = currentEntryLegResults.map((tradeResult) => {
|
|
3096
|
+
const activeQty = Math.max(
|
|
3097
|
+
0,
|
|
3098
|
+
tradeResult.qty - tradeResult.closedQty
|
|
3099
|
+
);
|
|
3100
|
+
const legFundingCost = fundingCost * (activeQty / remainingLegQty);
|
|
3101
|
+
return {
|
|
3102
|
+
...tradeResult,
|
|
3103
|
+
fundingFee: tradeResult.fundingFee == null ? null : tradeResult.fundingFee + legFundingCost,
|
|
3104
|
+
netProfit: tradeResult.netProfit - legFundingCost,
|
|
3105
|
+
totalFee: tradeResult.totalFee + legFundingCost
|
|
3106
|
+
};
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
2952
3109
|
}
|
|
2953
3110
|
};
|
|
2954
3111
|
const getExitTimestamp = (candle) => currentPosition ? Math.max(candle.timestamp, currentPosition.timestamp) : candle.timestamp;
|
|
@@ -3281,6 +3438,24 @@ var createTestConnector = (connector, context) => {
|
|
|
3281
3438
|
if (currentPosition && !isPositionIncrease) {
|
|
3282
3439
|
return false;
|
|
3283
3440
|
}
|
|
3441
|
+
const normalizedOrder = normalizeInstrumentOrderQty({
|
|
3442
|
+
qty: order.qty,
|
|
3443
|
+
symbol: order.symbol,
|
|
3444
|
+
instrument: context?.instrument
|
|
3445
|
+
});
|
|
3446
|
+
const orderQty = normalizedOrder.qty;
|
|
3447
|
+
if (orderQty <= 0 || normalizedOrder.minOrderQty != null && orderQty < normalizedOrder.minOrderQty) {
|
|
3448
|
+
if (order.signal) {
|
|
3449
|
+
order.signal.orderQty = orderQty;
|
|
3450
|
+
order.signal.orderValue = orderQty * order.price;
|
|
3451
|
+
order.signal.orderFailureReason = "QTY_BELOW_MIN_ORDER";
|
|
3452
|
+
}
|
|
3453
|
+
return false;
|
|
3454
|
+
}
|
|
3455
|
+
if (order.signal) {
|
|
3456
|
+
order.signal.orderQty = orderQty;
|
|
3457
|
+
order.signal.orderValue = orderQty * order.price;
|
|
3458
|
+
}
|
|
3284
3459
|
const isLong = order.direction === "LONG";
|
|
3285
3460
|
const entrySlippageBreakdown = getExecutionSlippageBreakdown({
|
|
3286
3461
|
stage: "entry",
|
|
@@ -3294,24 +3469,24 @@ var createTestConnector = (connector, context) => {
|
|
|
3294
3469
|
});
|
|
3295
3470
|
const previousPosition = currentPosition;
|
|
3296
3471
|
const previousQty = previousPosition?.qty ?? 0;
|
|
3297
|
-
const resultingQty = previousQty +
|
|
3472
|
+
const resultingQty = previousQty + orderQty;
|
|
3298
3473
|
const resultingEntryPrice = previousPosition ? getWeightedAverage(
|
|
3299
3474
|
previousPosition.price,
|
|
3300
3475
|
previousQty,
|
|
3301
3476
|
entryPrice,
|
|
3302
|
-
|
|
3477
|
+
orderQty
|
|
3303
3478
|
) : entryPrice;
|
|
3304
3479
|
currentPosition = previousPosition ? {
|
|
3305
3480
|
...previousPosition,
|
|
3306
3481
|
qty: resultingQty,
|
|
3307
3482
|
price: resultingEntryPrice
|
|
3308
|
-
} : { ...order, price: entryPrice, amount };
|
|
3483
|
+
} : { ...order, qty: orderQty, price: entryPrice, amount };
|
|
3309
3484
|
originalQty = resultingQty;
|
|
3310
3485
|
if (isPositionIncrease) {
|
|
3311
3486
|
const { fee: fee2, profit: profit2 } = getNetProfit({
|
|
3312
3487
|
grossProfit: 0,
|
|
3313
3488
|
price: entryPrice,
|
|
3314
|
-
qty:
|
|
3489
|
+
qty: orderQty,
|
|
3315
3490
|
feeRate: order.isLimit ? makerFeeRate : takerFeeRate
|
|
3316
3491
|
});
|
|
3317
3492
|
const entrySlippageCost2 = getSlippageCost({
|
|
@@ -3319,57 +3494,71 @@ var createTestConnector = (connector, context) => {
|
|
|
3319
3494
|
executionPrice: entryPrice,
|
|
3320
3495
|
direction: order.direction,
|
|
3321
3496
|
stage: "entry",
|
|
3322
|
-
qty:
|
|
3497
|
+
qty: orderQty
|
|
3323
3498
|
});
|
|
3499
|
+
const increaseSignalId = typeof order.signal?.signalId === "string" && order.signal.signalId ? order.signal.signalId : "";
|
|
3324
3500
|
amount += profit2;
|
|
3325
3501
|
currentPositionProfit += profit2;
|
|
3502
|
+
currentEntryLegResults.push(
|
|
3503
|
+
createOpenTradeResult({
|
|
3504
|
+
signalId: increaseSignalId,
|
|
3505
|
+
direction: order.direction,
|
|
3506
|
+
qty: orderQty,
|
|
3507
|
+
timestamp: order.timestamp,
|
|
3508
|
+
requestedEntryPrice: order.price,
|
|
3509
|
+
entryPrice,
|
|
3510
|
+
fee: fee2,
|
|
3511
|
+
slippageBreakdown: entrySlippageBreakdown,
|
|
3512
|
+
entrySlippageCost: entrySlippageCost2
|
|
3513
|
+
})
|
|
3514
|
+
);
|
|
3326
3515
|
if (currentTradeResult) {
|
|
3327
3516
|
const requestedEntryPrice = getWeightedAverage(
|
|
3328
3517
|
currentTradeResult.requestedEntryPrice,
|
|
3329
3518
|
currentTradeResult.qty,
|
|
3330
3519
|
order.price,
|
|
3331
|
-
|
|
3520
|
+
orderQty
|
|
3332
3521
|
);
|
|
3333
3522
|
const weightedEntryPrice = getWeightedAverage(
|
|
3334
3523
|
currentTradeResult.entryPrice,
|
|
3335
3524
|
currentTradeResult.qty,
|
|
3336
3525
|
entryPrice,
|
|
3337
|
-
|
|
3526
|
+
orderQty
|
|
3338
3527
|
);
|
|
3339
3528
|
const entryBaseSlippageBps = getWeightedAverage(
|
|
3340
3529
|
currentTradeResult.entryBaseSlippageBps,
|
|
3341
3530
|
currentTradeResult.qty,
|
|
3342
3531
|
entrySlippageBreakdown.baseSlippageBps,
|
|
3343
|
-
|
|
3532
|
+
orderQty
|
|
3344
3533
|
);
|
|
3345
3534
|
const entrySpreadBps = getWeightedAverage(
|
|
3346
3535
|
currentTradeResult.entrySpreadBps,
|
|
3347
3536
|
currentTradeResult.qty,
|
|
3348
3537
|
entrySlippageBreakdown.spreadBps,
|
|
3349
|
-
|
|
3538
|
+
orderQty
|
|
3350
3539
|
);
|
|
3351
3540
|
const entrySpreadSlippageBps = getWeightedAverage(
|
|
3352
3541
|
currentTradeResult.entrySpreadSlippageBps,
|
|
3353
3542
|
currentTradeResult.qty,
|
|
3354
3543
|
entrySlippageBreakdown.spreadSlippageBps,
|
|
3355
|
-
|
|
3544
|
+
orderQty
|
|
3356
3545
|
);
|
|
3357
3546
|
const entryMarketImpactBps = getWeightedAverage(
|
|
3358
3547
|
currentTradeResult.entryMarketImpactBps,
|
|
3359
3548
|
currentTradeResult.qty,
|
|
3360
3549
|
entrySlippageBreakdown.marketImpactBps,
|
|
3361
|
-
|
|
3550
|
+
orderQty
|
|
3362
3551
|
);
|
|
3363
3552
|
const entryDelayRiskBps = getWeightedAverage(
|
|
3364
3553
|
currentTradeResult.entryDelayRiskBps,
|
|
3365
3554
|
currentTradeResult.qty,
|
|
3366
3555
|
entrySlippageBreakdown.delayRiskBps,
|
|
3367
|
-
|
|
3556
|
+
orderQty
|
|
3368
3557
|
);
|
|
3369
3558
|
const totalEntrySlippageCost = currentTradeResult.entrySlippageCost + entrySlippageCost2;
|
|
3370
3559
|
currentTradeResult = {
|
|
3371
3560
|
...currentTradeResult,
|
|
3372
|
-
qty: currentTradeResult.qty +
|
|
3561
|
+
qty: currentTradeResult.qty + orderQty,
|
|
3373
3562
|
requestedEntryPrice,
|
|
3374
3563
|
entryPrice: weightedEntryPrice,
|
|
3375
3564
|
netProfit: currentTradeResult.netProfit + profit2,
|
|
@@ -3391,6 +3580,7 @@ var createTestConnector = (connector, context) => {
|
|
|
3391
3580
|
}
|
|
3392
3581
|
logOrder({
|
|
3393
3582
|
...order,
|
|
3583
|
+
qty: orderQty,
|
|
3394
3584
|
price: entryPrice,
|
|
3395
3585
|
profit: profit2,
|
|
3396
3586
|
fee: fee2,
|
|
@@ -3403,7 +3593,7 @@ var createTestConnector = (connector, context) => {
|
|
|
3403
3593
|
const { fee, profit } = getNetProfit({
|
|
3404
3594
|
grossProfit: 0,
|
|
3405
3595
|
price: entryPrice,
|
|
3406
|
-
qty:
|
|
3596
|
+
qty: orderQty,
|
|
3407
3597
|
feeRate: order.isLimit ? makerFeeRate : takerFeeRate
|
|
3408
3598
|
});
|
|
3409
3599
|
const entrySlippageCost = getSlippageCost({
|
|
@@ -3411,48 +3601,26 @@ var createTestConnector = (connector, context) => {
|
|
|
3411
3601
|
executionPrice: entryPrice,
|
|
3412
3602
|
direction: order.direction,
|
|
3413
3603
|
stage: "entry",
|
|
3414
|
-
qty:
|
|
3604
|
+
qty: orderQty
|
|
3415
3605
|
});
|
|
3416
3606
|
amount += profit;
|
|
3417
3607
|
currentPositionProfit = profit;
|
|
3418
|
-
|
|
3419
|
-
signalId: currentSignalId,
|
|
3608
|
+
const openTradeResult = createOpenTradeResult({
|
|
3609
|
+
signalId: currentSignalId ?? "",
|
|
3420
3610
|
direction: order.direction,
|
|
3421
|
-
qty:
|
|
3422
|
-
|
|
3423
|
-
entryTimestamp: order.timestamp,
|
|
3424
|
-
exitTimestamp: null,
|
|
3425
|
-
exitReason: null,
|
|
3611
|
+
qty: orderQty,
|
|
3612
|
+
timestamp: order.timestamp,
|
|
3426
3613
|
requestedEntryPrice: order.price,
|
|
3427
3614
|
entryPrice,
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
fundingFee: executionCostModel?.funding.enabled ? 0 : null,
|
|
3435
|
-
totalFee: fee,
|
|
3436
|
-
entrySlippagePrice: entryPrice - order.price,
|
|
3437
|
-
entrySlippageBps: getSlippageBps(order.price, entryPrice),
|
|
3438
|
-
entryBaseSlippageBps: entrySlippageBreakdown.baseSlippageBps,
|
|
3439
|
-
entrySpreadBps: entrySlippageBreakdown.spreadBps,
|
|
3440
|
-
entrySpreadSlippageBps: entrySlippageBreakdown.spreadSlippageBps,
|
|
3441
|
-
entryMarketImpactBps: entrySlippageBreakdown.marketImpactBps,
|
|
3442
|
-
entryDelayRiskBps: entrySlippageBreakdown.delayRiskBps,
|
|
3443
|
-
entrySlippageCost,
|
|
3444
|
-
exitSlippagePrice: null,
|
|
3445
|
-
exitSlippageBps: null,
|
|
3446
|
-
exitBaseSlippageBps: null,
|
|
3447
|
-
exitSpreadBps: null,
|
|
3448
|
-
exitSpreadSlippageBps: null,
|
|
3449
|
-
exitMarketImpactBps: null,
|
|
3450
|
-
exitDelayRiskBps: null,
|
|
3451
|
-
exitSlippageCost: 0,
|
|
3452
|
-
totalSlippageCost: entrySlippageCost
|
|
3453
|
-
} : null;
|
|
3615
|
+
fee,
|
|
3616
|
+
slippageBreakdown: entrySlippageBreakdown,
|
|
3617
|
+
entrySlippageCost
|
|
3618
|
+
});
|
|
3619
|
+
currentEntryLegResults = [openTradeResult];
|
|
3620
|
+
currentTradeResult = currentSignalId ? { ...openTradeResult, signalId: currentSignalId } : null;
|
|
3454
3621
|
logOrder({
|
|
3455
3622
|
...order,
|
|
3623
|
+
qty: orderQty,
|
|
3456
3624
|
price: entryPrice,
|
|
3457
3625
|
profit,
|
|
3458
3626
|
fee,
|
|
@@ -4436,6 +4604,7 @@ var testing = async ({
|
|
|
4436
4604
|
mlEnabled: ml,
|
|
4437
4605
|
aiEnabled: ai,
|
|
4438
4606
|
fastMode: fast,
|
|
4607
|
+
instrument,
|
|
4439
4608
|
executionCostModel,
|
|
4440
4609
|
fundingRates
|
|
4441
4610
|
});
|
|
@@ -4846,6 +5015,7 @@ var testingGroupInSharedCandleLoop = async (tests) => {
|
|
|
4846
5015
|
mlEnabled: test.ml,
|
|
4847
5016
|
aiEnabled: test.ai,
|
|
4848
5017
|
fastMode: test.fast,
|
|
5018
|
+
instrument,
|
|
4849
5019
|
executionCostModel,
|
|
4850
5020
|
fundingRates
|
|
4851
5021
|
});
|
package/dist/backtest.mjs
CHANGED
|
@@ -57,6 +57,22 @@ import {
|
|
|
57
57
|
} from "@tradejs/core/trade";
|
|
58
58
|
import { round } from "@tradejs/core/math";
|
|
59
59
|
var PRICE_PRECISION = 8;
|
|
60
|
+
var normalizeInstrumentOrderQty = ({
|
|
61
|
+
qty,
|
|
62
|
+
symbol,
|
|
63
|
+
instrument
|
|
64
|
+
}) => {
|
|
65
|
+
if (!instrument || instrument.symbol.trim().toUpperCase() !== symbol.trim().toUpperCase()) {
|
|
66
|
+
return { qty, minOrderQty: null };
|
|
67
|
+
}
|
|
68
|
+
const qtyStep = Number(instrument.venueMetadata?.qtyStep);
|
|
69
|
+
const minOrderQty = Number(instrument.venueMetadata?.minOrderQty);
|
|
70
|
+
const normalizedQty = Number.isFinite(qtyStep) && qtyStep > 0 ? Number((Math.floor(qty / qtyStep) * qtyStep).toFixed(12)) : qty;
|
|
71
|
+
return {
|
|
72
|
+
qty: normalizedQty,
|
|
73
|
+
minOrderQty: Number.isFinite(minOrderQty) && minOrderQty > 0 ? minOrderQty : null
|
|
74
|
+
};
|
|
75
|
+
};
|
|
60
76
|
var createTestConnector = (connector, context) => {
|
|
61
77
|
let state = {};
|
|
62
78
|
const orderLog = [];
|
|
@@ -77,6 +93,7 @@ var createTestConnector = (connector, context) => {
|
|
|
77
93
|
let takeProfits = [];
|
|
78
94
|
let stopLossPrice = null;
|
|
79
95
|
let currentTradeResult = null;
|
|
96
|
+
let currentEntryLegResults = [];
|
|
80
97
|
const closedSignalResults = [];
|
|
81
98
|
const logOrder = (data) => {
|
|
82
99
|
const nextEntry = {
|
|
@@ -120,6 +137,52 @@ var createTestConnector = (connector, context) => {
|
|
|
120
137
|
}
|
|
121
138
|
return (previousValue * previousQty + nextValue * nextQty) / (previousQty + nextQty);
|
|
122
139
|
};
|
|
140
|
+
const createOpenTradeResult = ({
|
|
141
|
+
signalId,
|
|
142
|
+
direction,
|
|
143
|
+
qty,
|
|
144
|
+
timestamp,
|
|
145
|
+
requestedEntryPrice,
|
|
146
|
+
entryPrice,
|
|
147
|
+
fee,
|
|
148
|
+
slippageBreakdown,
|
|
149
|
+
entrySlippageCost
|
|
150
|
+
}) => ({
|
|
151
|
+
signalId,
|
|
152
|
+
direction,
|
|
153
|
+
qty,
|
|
154
|
+
closedQty: 0,
|
|
155
|
+
entryTimestamp: timestamp,
|
|
156
|
+
exitTimestamp: null,
|
|
157
|
+
exitReason: null,
|
|
158
|
+
requestedEntryPrice,
|
|
159
|
+
entryPrice,
|
|
160
|
+
requestedExitPrice: null,
|
|
161
|
+
exitPrice: null,
|
|
162
|
+
grossProfit: 0,
|
|
163
|
+
netProfit: -fee,
|
|
164
|
+
openFee: fee,
|
|
165
|
+
closeFee: 0,
|
|
166
|
+
fundingFee: executionCostModel?.funding.enabled ? 0 : null,
|
|
167
|
+
totalFee: fee,
|
|
168
|
+
entrySlippagePrice: entryPrice - requestedEntryPrice,
|
|
169
|
+
entrySlippageBps: getSlippageBps(requestedEntryPrice, entryPrice),
|
|
170
|
+
entryBaseSlippageBps: slippageBreakdown.baseSlippageBps,
|
|
171
|
+
entrySpreadBps: slippageBreakdown.spreadBps,
|
|
172
|
+
entrySpreadSlippageBps: slippageBreakdown.spreadSlippageBps,
|
|
173
|
+
entryMarketImpactBps: slippageBreakdown.marketImpactBps,
|
|
174
|
+
entryDelayRiskBps: slippageBreakdown.delayRiskBps,
|
|
175
|
+
entrySlippageCost,
|
|
176
|
+
exitSlippagePrice: null,
|
|
177
|
+
exitSlippageBps: null,
|
|
178
|
+
exitBaseSlippageBps: null,
|
|
179
|
+
exitSpreadBps: null,
|
|
180
|
+
exitSpreadSlippageBps: null,
|
|
181
|
+
exitMarketImpactBps: null,
|
|
182
|
+
exitDelayRiskBps: null,
|
|
183
|
+
exitSlippageCost: 0,
|
|
184
|
+
totalSlippageCost: entrySlippageCost
|
|
185
|
+
});
|
|
123
186
|
const finalizeTradeResult = (tradeResult, timestamp) => {
|
|
124
187
|
if (!tradeResult.exitReason) {
|
|
125
188
|
return void 0;
|
|
@@ -159,7 +222,9 @@ var createTestConnector = (connector, context) => {
|
|
|
159
222
|
closedQty: round(tradeResult.closedQty)
|
|
160
223
|
};
|
|
161
224
|
};
|
|
162
|
-
const
|
|
225
|
+
const appendExitToTradeResult = ({
|
|
226
|
+
tradeResult,
|
|
227
|
+
direction,
|
|
163
228
|
timestamp,
|
|
164
229
|
reason,
|
|
165
230
|
requestedPrice,
|
|
@@ -169,65 +234,62 @@ var createTestConnector = (connector, context) => {
|
|
|
169
234
|
fee,
|
|
170
235
|
slippageBreakdown
|
|
171
236
|
}) => {
|
|
172
|
-
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const previousClosedQty = currentTradeResult.closedQty;
|
|
237
|
+
const previousClosedQty = tradeResult.closedQty;
|
|
176
238
|
const requestedExitPrice = getWeightedAverage(
|
|
177
|
-
|
|
239
|
+
tradeResult.requestedExitPrice,
|
|
178
240
|
previousClosedQty,
|
|
179
241
|
requestedPrice,
|
|
180
242
|
qty
|
|
181
243
|
);
|
|
182
244
|
const exitPrice = getWeightedAverage(
|
|
183
|
-
|
|
245
|
+
tradeResult.exitPrice,
|
|
184
246
|
previousClosedQty,
|
|
185
247
|
executionPrice,
|
|
186
248
|
qty
|
|
187
249
|
);
|
|
188
250
|
const exitBaseSlippageBps = getWeightedAverage(
|
|
189
|
-
|
|
251
|
+
tradeResult.exitBaseSlippageBps,
|
|
190
252
|
previousClosedQty,
|
|
191
253
|
slippageBreakdown.baseSlippageBps,
|
|
192
254
|
qty
|
|
193
255
|
);
|
|
194
256
|
const exitSpreadBps = getWeightedAverage(
|
|
195
|
-
|
|
257
|
+
tradeResult.exitSpreadBps,
|
|
196
258
|
previousClosedQty,
|
|
197
259
|
slippageBreakdown.spreadBps,
|
|
198
260
|
qty
|
|
199
261
|
);
|
|
200
262
|
const exitSpreadSlippageBps = getWeightedAverage(
|
|
201
|
-
|
|
263
|
+
tradeResult.exitSpreadSlippageBps,
|
|
202
264
|
previousClosedQty,
|
|
203
265
|
slippageBreakdown.spreadSlippageBps,
|
|
204
266
|
qty
|
|
205
267
|
);
|
|
206
268
|
const exitMarketImpactBps = getWeightedAverage(
|
|
207
|
-
|
|
269
|
+
tradeResult.exitMarketImpactBps,
|
|
208
270
|
previousClosedQty,
|
|
209
271
|
slippageBreakdown.marketImpactBps,
|
|
210
272
|
qty
|
|
211
273
|
);
|
|
212
274
|
const exitDelayRiskBps = null;
|
|
213
|
-
const exitSlippageCost =
|
|
275
|
+
const exitSlippageCost = tradeResult.exitSlippageCost + getSlippageCost({
|
|
214
276
|
requestedPrice,
|
|
215
277
|
executionPrice,
|
|
216
|
-
direction
|
|
278
|
+
direction,
|
|
217
279
|
stage: "exit",
|
|
218
280
|
qty
|
|
219
281
|
});
|
|
220
|
-
|
|
221
|
-
...
|
|
282
|
+
return {
|
|
283
|
+
...tradeResult,
|
|
222
284
|
closedQty: previousClosedQty + qty,
|
|
223
285
|
exitTimestamp: timestamp,
|
|
224
286
|
exitReason: reason,
|
|
225
287
|
requestedExitPrice,
|
|
226
288
|
exitPrice,
|
|
227
|
-
grossProfit:
|
|
228
|
-
netProfit:
|
|
229
|
-
closeFee:
|
|
230
|
-
totalFee:
|
|
289
|
+
grossProfit: tradeResult.grossProfit + grossProfit,
|
|
290
|
+
netProfit: tradeResult.netProfit + grossProfit - fee,
|
|
291
|
+
closeFee: tradeResult.closeFee + fee,
|
|
292
|
+
totalFee: tradeResult.openFee + tradeResult.closeFee + fee + (tradeResult.fundingFee ?? 0),
|
|
231
293
|
exitSlippagePrice: exitPrice - requestedExitPrice,
|
|
232
294
|
exitSlippageBps: getSlippageBps(requestedExitPrice, exitPrice),
|
|
233
295
|
exitBaseSlippageBps,
|
|
@@ -236,9 +298,73 @@ var createTestConnector = (connector, context) => {
|
|
|
236
298
|
exitMarketImpactBps,
|
|
237
299
|
exitDelayRiskBps,
|
|
238
300
|
exitSlippageCost,
|
|
239
|
-
totalSlippageCost:
|
|
301
|
+
totalSlippageCost: tradeResult.entrySlippageCost + exitSlippageCost
|
|
240
302
|
};
|
|
241
303
|
};
|
|
304
|
+
const recordExitResult = ({
|
|
305
|
+
timestamp,
|
|
306
|
+
reason,
|
|
307
|
+
requestedPrice,
|
|
308
|
+
executionPrice,
|
|
309
|
+
qty,
|
|
310
|
+
grossProfit,
|
|
311
|
+
fee,
|
|
312
|
+
slippageBreakdown
|
|
313
|
+
}) => {
|
|
314
|
+
if (!currentPosition) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (currentTradeResult) {
|
|
318
|
+
currentTradeResult = appendExitToTradeResult({
|
|
319
|
+
tradeResult: currentTradeResult,
|
|
320
|
+
direction: currentPosition.direction,
|
|
321
|
+
timestamp,
|
|
322
|
+
reason,
|
|
323
|
+
requestedPrice,
|
|
324
|
+
executionPrice,
|
|
325
|
+
qty,
|
|
326
|
+
grossProfit,
|
|
327
|
+
fee,
|
|
328
|
+
slippageBreakdown
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
const remainingQtyByLeg = currentEntryLegResults.map(
|
|
332
|
+
(tradeResult) => Math.max(0, tradeResult.qty - tradeResult.closedQty)
|
|
333
|
+
);
|
|
334
|
+
const totalRemainingQty = remainingQtyByLeg.reduce(
|
|
335
|
+
(total, remainingQty) => total + remainingQty,
|
|
336
|
+
0
|
|
337
|
+
);
|
|
338
|
+
if (totalRemainingQty <= 0) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
let allocatedQty = 0;
|
|
342
|
+
const activeLegIndexes = remainingQtyByLeg.map((remainingQty, index) => ({ remainingQty, index })).filter(({ remainingQty }) => remainingQty > 0);
|
|
343
|
+
for (const [activeIndex, leg] of activeLegIndexes.entries()) {
|
|
344
|
+
const isLast = activeIndex === activeLegIndexes.length - 1;
|
|
345
|
+
const legExitQty = Math.min(
|
|
346
|
+
leg.remainingQty,
|
|
347
|
+
isLast ? Math.max(0, qty - allocatedQty) : qty * (leg.remainingQty / totalRemainingQty)
|
|
348
|
+
);
|
|
349
|
+
if (legExitQty <= 0) continue;
|
|
350
|
+
allocatedQty += legExitQty;
|
|
351
|
+
const legResult = currentEntryLegResults[leg.index];
|
|
352
|
+
const legGrossProfit = currentPosition.direction === "LONG" ? (executionPrice - legResult.entryPrice) * legExitQty : (legResult.entryPrice - executionPrice) * legExitQty;
|
|
353
|
+
const legFee = executionPrice * legExitQty * takerFeeRate;
|
|
354
|
+
currentEntryLegResults[leg.index] = appendExitToTradeResult({
|
|
355
|
+
tradeResult: legResult,
|
|
356
|
+
direction: currentPosition.direction,
|
|
357
|
+
timestamp,
|
|
358
|
+
reason,
|
|
359
|
+
requestedPrice,
|
|
360
|
+
executionPrice,
|
|
361
|
+
qty: legExitQty,
|
|
362
|
+
grossProfit: legGrossProfit,
|
|
363
|
+
fee: legFee,
|
|
364
|
+
slippageBreakdown
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
};
|
|
242
368
|
const clearPosition = (timestamp) => {
|
|
243
369
|
takeProfits = [];
|
|
244
370
|
stopLossPrice = null;
|
|
@@ -247,7 +373,18 @@ var createTestConnector = (connector, context) => {
|
|
|
247
373
|
return;
|
|
248
374
|
}
|
|
249
375
|
if (context?.mlEnabled || context?.aiEnabled) {
|
|
250
|
-
|
|
376
|
+
const attributedResults = currentEntryLegResults.filter(({ signalId }) => signalId).map((tradeResult) => finalizeTradeResult(tradeResult, timestamp)).filter(
|
|
377
|
+
(tradeResult) => Boolean(tradeResult)
|
|
378
|
+
);
|
|
379
|
+
if (attributedResults.length) {
|
|
380
|
+
for (const tradeResult of attributedResults) {
|
|
381
|
+
closedSignalResults.push({
|
|
382
|
+
signalId: tradeResult.signalId,
|
|
383
|
+
profit: round(tradeResult.netProfit),
|
|
384
|
+
tradeResult
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
} else if (currentSignalId) {
|
|
251
388
|
const tradeResult = currentTradeResult ? finalizeTradeResult(currentTradeResult, timestamp) : void 0;
|
|
252
389
|
closedSignalResults.push({
|
|
253
390
|
signalId: currentSignalId,
|
|
@@ -270,6 +407,7 @@ var createTestConnector = (connector, context) => {
|
|
|
270
407
|
currentPosition = null;
|
|
271
408
|
currentSignalId = null;
|
|
272
409
|
currentTradeResult = null;
|
|
410
|
+
currentEntryLegResults = [];
|
|
273
411
|
currentPositionProfit = 0;
|
|
274
412
|
};
|
|
275
413
|
const getNetProfit = ({
|
|
@@ -302,6 +440,25 @@ var createTestConnector = (connector, context) => {
|
|
|
302
440
|
currentTradeResult.netProfit -= fundingCost;
|
|
303
441
|
currentTradeResult.totalFee += fundingCost;
|
|
304
442
|
}
|
|
443
|
+
const remainingLegQty = currentEntryLegResults.reduce(
|
|
444
|
+
(total, tradeResult) => total + Math.max(0, tradeResult.qty - tradeResult.closedQty),
|
|
445
|
+
0
|
|
446
|
+
);
|
|
447
|
+
if (remainingLegQty > 0) {
|
|
448
|
+
currentEntryLegResults = currentEntryLegResults.map((tradeResult) => {
|
|
449
|
+
const activeQty = Math.max(
|
|
450
|
+
0,
|
|
451
|
+
tradeResult.qty - tradeResult.closedQty
|
|
452
|
+
);
|
|
453
|
+
const legFundingCost = fundingCost * (activeQty / remainingLegQty);
|
|
454
|
+
return {
|
|
455
|
+
...tradeResult,
|
|
456
|
+
fundingFee: tradeResult.fundingFee == null ? null : tradeResult.fundingFee + legFundingCost,
|
|
457
|
+
netProfit: tradeResult.netProfit - legFundingCost,
|
|
458
|
+
totalFee: tradeResult.totalFee + legFundingCost
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
}
|
|
305
462
|
}
|
|
306
463
|
};
|
|
307
464
|
const getExitTimestamp = (candle) => currentPosition ? Math.max(candle.timestamp, currentPosition.timestamp) : candle.timestamp;
|
|
@@ -634,6 +791,24 @@ var createTestConnector = (connector, context) => {
|
|
|
634
791
|
if (currentPosition && !isPositionIncrease) {
|
|
635
792
|
return false;
|
|
636
793
|
}
|
|
794
|
+
const normalizedOrder = normalizeInstrumentOrderQty({
|
|
795
|
+
qty: order.qty,
|
|
796
|
+
symbol: order.symbol,
|
|
797
|
+
instrument: context?.instrument
|
|
798
|
+
});
|
|
799
|
+
const orderQty = normalizedOrder.qty;
|
|
800
|
+
if (orderQty <= 0 || normalizedOrder.minOrderQty != null && orderQty < normalizedOrder.minOrderQty) {
|
|
801
|
+
if (order.signal) {
|
|
802
|
+
order.signal.orderQty = orderQty;
|
|
803
|
+
order.signal.orderValue = orderQty * order.price;
|
|
804
|
+
order.signal.orderFailureReason = "QTY_BELOW_MIN_ORDER";
|
|
805
|
+
}
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
if (order.signal) {
|
|
809
|
+
order.signal.orderQty = orderQty;
|
|
810
|
+
order.signal.orderValue = orderQty * order.price;
|
|
811
|
+
}
|
|
637
812
|
const isLong = order.direction === "LONG";
|
|
638
813
|
const entrySlippageBreakdown = getExecutionSlippageBreakdown({
|
|
639
814
|
stage: "entry",
|
|
@@ -647,24 +822,24 @@ var createTestConnector = (connector, context) => {
|
|
|
647
822
|
});
|
|
648
823
|
const previousPosition = currentPosition;
|
|
649
824
|
const previousQty = previousPosition?.qty ?? 0;
|
|
650
|
-
const resultingQty = previousQty +
|
|
825
|
+
const resultingQty = previousQty + orderQty;
|
|
651
826
|
const resultingEntryPrice = previousPosition ? getWeightedAverage(
|
|
652
827
|
previousPosition.price,
|
|
653
828
|
previousQty,
|
|
654
829
|
entryPrice,
|
|
655
|
-
|
|
830
|
+
orderQty
|
|
656
831
|
) : entryPrice;
|
|
657
832
|
currentPosition = previousPosition ? {
|
|
658
833
|
...previousPosition,
|
|
659
834
|
qty: resultingQty,
|
|
660
835
|
price: resultingEntryPrice
|
|
661
|
-
} : { ...order, price: entryPrice, amount };
|
|
836
|
+
} : { ...order, qty: orderQty, price: entryPrice, amount };
|
|
662
837
|
originalQty = resultingQty;
|
|
663
838
|
if (isPositionIncrease) {
|
|
664
839
|
const { fee: fee2, profit: profit2 } = getNetProfit({
|
|
665
840
|
grossProfit: 0,
|
|
666
841
|
price: entryPrice,
|
|
667
|
-
qty:
|
|
842
|
+
qty: orderQty,
|
|
668
843
|
feeRate: order.isLimit ? makerFeeRate : takerFeeRate
|
|
669
844
|
});
|
|
670
845
|
const entrySlippageCost2 = getSlippageCost({
|
|
@@ -672,57 +847,71 @@ var createTestConnector = (connector, context) => {
|
|
|
672
847
|
executionPrice: entryPrice,
|
|
673
848
|
direction: order.direction,
|
|
674
849
|
stage: "entry",
|
|
675
|
-
qty:
|
|
850
|
+
qty: orderQty
|
|
676
851
|
});
|
|
852
|
+
const increaseSignalId = typeof order.signal?.signalId === "string" && order.signal.signalId ? order.signal.signalId : "";
|
|
677
853
|
amount += profit2;
|
|
678
854
|
currentPositionProfit += profit2;
|
|
855
|
+
currentEntryLegResults.push(
|
|
856
|
+
createOpenTradeResult({
|
|
857
|
+
signalId: increaseSignalId,
|
|
858
|
+
direction: order.direction,
|
|
859
|
+
qty: orderQty,
|
|
860
|
+
timestamp: order.timestamp,
|
|
861
|
+
requestedEntryPrice: order.price,
|
|
862
|
+
entryPrice,
|
|
863
|
+
fee: fee2,
|
|
864
|
+
slippageBreakdown: entrySlippageBreakdown,
|
|
865
|
+
entrySlippageCost: entrySlippageCost2
|
|
866
|
+
})
|
|
867
|
+
);
|
|
679
868
|
if (currentTradeResult) {
|
|
680
869
|
const requestedEntryPrice = getWeightedAverage(
|
|
681
870
|
currentTradeResult.requestedEntryPrice,
|
|
682
871
|
currentTradeResult.qty,
|
|
683
872
|
order.price,
|
|
684
|
-
|
|
873
|
+
orderQty
|
|
685
874
|
);
|
|
686
875
|
const weightedEntryPrice = getWeightedAverage(
|
|
687
876
|
currentTradeResult.entryPrice,
|
|
688
877
|
currentTradeResult.qty,
|
|
689
878
|
entryPrice,
|
|
690
|
-
|
|
879
|
+
orderQty
|
|
691
880
|
);
|
|
692
881
|
const entryBaseSlippageBps = getWeightedAverage(
|
|
693
882
|
currentTradeResult.entryBaseSlippageBps,
|
|
694
883
|
currentTradeResult.qty,
|
|
695
884
|
entrySlippageBreakdown.baseSlippageBps,
|
|
696
|
-
|
|
885
|
+
orderQty
|
|
697
886
|
);
|
|
698
887
|
const entrySpreadBps = getWeightedAverage(
|
|
699
888
|
currentTradeResult.entrySpreadBps,
|
|
700
889
|
currentTradeResult.qty,
|
|
701
890
|
entrySlippageBreakdown.spreadBps,
|
|
702
|
-
|
|
891
|
+
orderQty
|
|
703
892
|
);
|
|
704
893
|
const entrySpreadSlippageBps = getWeightedAverage(
|
|
705
894
|
currentTradeResult.entrySpreadSlippageBps,
|
|
706
895
|
currentTradeResult.qty,
|
|
707
896
|
entrySlippageBreakdown.spreadSlippageBps,
|
|
708
|
-
|
|
897
|
+
orderQty
|
|
709
898
|
);
|
|
710
899
|
const entryMarketImpactBps = getWeightedAverage(
|
|
711
900
|
currentTradeResult.entryMarketImpactBps,
|
|
712
901
|
currentTradeResult.qty,
|
|
713
902
|
entrySlippageBreakdown.marketImpactBps,
|
|
714
|
-
|
|
903
|
+
orderQty
|
|
715
904
|
);
|
|
716
905
|
const entryDelayRiskBps = getWeightedAverage(
|
|
717
906
|
currentTradeResult.entryDelayRiskBps,
|
|
718
907
|
currentTradeResult.qty,
|
|
719
908
|
entrySlippageBreakdown.delayRiskBps,
|
|
720
|
-
|
|
909
|
+
orderQty
|
|
721
910
|
);
|
|
722
911
|
const totalEntrySlippageCost = currentTradeResult.entrySlippageCost + entrySlippageCost2;
|
|
723
912
|
currentTradeResult = {
|
|
724
913
|
...currentTradeResult,
|
|
725
|
-
qty: currentTradeResult.qty +
|
|
914
|
+
qty: currentTradeResult.qty + orderQty,
|
|
726
915
|
requestedEntryPrice,
|
|
727
916
|
entryPrice: weightedEntryPrice,
|
|
728
917
|
netProfit: currentTradeResult.netProfit + profit2,
|
|
@@ -744,6 +933,7 @@ var createTestConnector = (connector, context) => {
|
|
|
744
933
|
}
|
|
745
934
|
logOrder({
|
|
746
935
|
...order,
|
|
936
|
+
qty: orderQty,
|
|
747
937
|
price: entryPrice,
|
|
748
938
|
profit: profit2,
|
|
749
939
|
fee: fee2,
|
|
@@ -756,7 +946,7 @@ var createTestConnector = (connector, context) => {
|
|
|
756
946
|
const { fee, profit } = getNetProfit({
|
|
757
947
|
grossProfit: 0,
|
|
758
948
|
price: entryPrice,
|
|
759
|
-
qty:
|
|
949
|
+
qty: orderQty,
|
|
760
950
|
feeRate: order.isLimit ? makerFeeRate : takerFeeRate
|
|
761
951
|
});
|
|
762
952
|
const entrySlippageCost = getSlippageCost({
|
|
@@ -764,48 +954,26 @@ var createTestConnector = (connector, context) => {
|
|
|
764
954
|
executionPrice: entryPrice,
|
|
765
955
|
direction: order.direction,
|
|
766
956
|
stage: "entry",
|
|
767
|
-
qty:
|
|
957
|
+
qty: orderQty
|
|
768
958
|
});
|
|
769
959
|
amount += profit;
|
|
770
960
|
currentPositionProfit = profit;
|
|
771
|
-
|
|
772
|
-
signalId: currentSignalId,
|
|
961
|
+
const openTradeResult = createOpenTradeResult({
|
|
962
|
+
signalId: currentSignalId ?? "",
|
|
773
963
|
direction: order.direction,
|
|
774
|
-
qty:
|
|
775
|
-
|
|
776
|
-
entryTimestamp: order.timestamp,
|
|
777
|
-
exitTimestamp: null,
|
|
778
|
-
exitReason: null,
|
|
964
|
+
qty: orderQty,
|
|
965
|
+
timestamp: order.timestamp,
|
|
779
966
|
requestedEntryPrice: order.price,
|
|
780
967
|
entryPrice,
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
fundingFee: executionCostModel?.funding.enabled ? 0 : null,
|
|
788
|
-
totalFee: fee,
|
|
789
|
-
entrySlippagePrice: entryPrice - order.price,
|
|
790
|
-
entrySlippageBps: getSlippageBps(order.price, entryPrice),
|
|
791
|
-
entryBaseSlippageBps: entrySlippageBreakdown.baseSlippageBps,
|
|
792
|
-
entrySpreadBps: entrySlippageBreakdown.spreadBps,
|
|
793
|
-
entrySpreadSlippageBps: entrySlippageBreakdown.spreadSlippageBps,
|
|
794
|
-
entryMarketImpactBps: entrySlippageBreakdown.marketImpactBps,
|
|
795
|
-
entryDelayRiskBps: entrySlippageBreakdown.delayRiskBps,
|
|
796
|
-
entrySlippageCost,
|
|
797
|
-
exitSlippagePrice: null,
|
|
798
|
-
exitSlippageBps: null,
|
|
799
|
-
exitBaseSlippageBps: null,
|
|
800
|
-
exitSpreadBps: null,
|
|
801
|
-
exitSpreadSlippageBps: null,
|
|
802
|
-
exitMarketImpactBps: null,
|
|
803
|
-
exitDelayRiskBps: null,
|
|
804
|
-
exitSlippageCost: 0,
|
|
805
|
-
totalSlippageCost: entrySlippageCost
|
|
806
|
-
} : null;
|
|
968
|
+
fee,
|
|
969
|
+
slippageBreakdown: entrySlippageBreakdown,
|
|
970
|
+
entrySlippageCost
|
|
971
|
+
});
|
|
972
|
+
currentEntryLegResults = [openTradeResult];
|
|
973
|
+
currentTradeResult = currentSignalId ? { ...openTradeResult, signalId: currentSignalId } : null;
|
|
807
974
|
logOrder({
|
|
808
975
|
...order,
|
|
976
|
+
qty: orderQty,
|
|
809
977
|
price: entryPrice,
|
|
810
978
|
profit,
|
|
811
979
|
fee,
|
|
@@ -1795,6 +1963,7 @@ var testing = async ({
|
|
|
1795
1963
|
mlEnabled: ml,
|
|
1796
1964
|
aiEnabled: ai,
|
|
1797
1965
|
fastMode: fast,
|
|
1966
|
+
instrument,
|
|
1798
1967
|
executionCostModel,
|
|
1799
1968
|
fundingRates
|
|
1800
1969
|
});
|
|
@@ -2205,6 +2374,7 @@ var testingGroupInSharedCandleLoop = async (tests) => {
|
|
|
2205
2374
|
mlEnabled: test.ml,
|
|
2206
2375
|
aiEnabled: test.ai,
|
|
2207
2376
|
fastMode: test.fast,
|
|
2377
|
+
instrument,
|
|
2208
2378
|
executionCostModel,
|
|
2209
2379
|
fundingRates
|
|
2210
2380
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/node",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.10",
|
|
4
4
|
"description": "Node-only runtime for the TradeJS TypeScript framework: strategies, backtests, Pine strategy loading, and plugin registries.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tradejs",
|
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@langchain/core": "^1.2.3",
|
|
69
69
|
"@langchain/openai": "^1.5.5",
|
|
70
|
-
"@tradejs/core": "^2.0.
|
|
71
|
-
"@tradejs/infra": "^2.0.
|
|
72
|
-
"@tradejs/types": "^2.0.
|
|
70
|
+
"@tradejs/core": "^2.0.10",
|
|
71
|
+
"@tradejs/infra": "^2.0.10",
|
|
72
|
+
"@tradejs/types": "^2.0.10",
|
|
73
73
|
"chalk": "4.1.2",
|
|
74
74
|
"ioredis": "5.11.1",
|
|
75
75
|
"pinets": "0.8.12",
|