@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 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 recordExitResult = ({
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
- if (!currentTradeResult || !currentPosition) {
2820
- return;
2821
- }
2822
- const previousClosedQty = currentTradeResult.closedQty;
2884
+ const previousClosedQty = tradeResult.closedQty;
2823
2885
  const requestedExitPrice = getWeightedAverage(
2824
- currentTradeResult.requestedExitPrice,
2886
+ tradeResult.requestedExitPrice,
2825
2887
  previousClosedQty,
2826
2888
  requestedPrice,
2827
2889
  qty
2828
2890
  );
2829
2891
  const exitPrice = getWeightedAverage(
2830
- currentTradeResult.exitPrice,
2892
+ tradeResult.exitPrice,
2831
2893
  previousClosedQty,
2832
2894
  executionPrice,
2833
2895
  qty
2834
2896
  );
2835
2897
  const exitBaseSlippageBps = getWeightedAverage(
2836
- currentTradeResult.exitBaseSlippageBps,
2898
+ tradeResult.exitBaseSlippageBps,
2837
2899
  previousClosedQty,
2838
2900
  slippageBreakdown.baseSlippageBps,
2839
2901
  qty
2840
2902
  );
2841
2903
  const exitSpreadBps = getWeightedAverage(
2842
- currentTradeResult.exitSpreadBps,
2904
+ tradeResult.exitSpreadBps,
2843
2905
  previousClosedQty,
2844
2906
  slippageBreakdown.spreadBps,
2845
2907
  qty
2846
2908
  );
2847
2909
  const exitSpreadSlippageBps = getWeightedAverage(
2848
- currentTradeResult.exitSpreadSlippageBps,
2910
+ tradeResult.exitSpreadSlippageBps,
2849
2911
  previousClosedQty,
2850
2912
  slippageBreakdown.spreadSlippageBps,
2851
2913
  qty
2852
2914
  );
2853
2915
  const exitMarketImpactBps = getWeightedAverage(
2854
- currentTradeResult.exitMarketImpactBps,
2916
+ tradeResult.exitMarketImpactBps,
2855
2917
  previousClosedQty,
2856
2918
  slippageBreakdown.marketImpactBps,
2857
2919
  qty
2858
2920
  );
2859
2921
  const exitDelayRiskBps = null;
2860
- const exitSlippageCost = currentTradeResult.exitSlippageCost + getSlippageCost({
2922
+ const exitSlippageCost = tradeResult.exitSlippageCost + getSlippageCost({
2861
2923
  requestedPrice,
2862
2924
  executionPrice,
2863
- direction: currentPosition.direction,
2925
+ direction,
2864
2926
  stage: "exit",
2865
2927
  qty
2866
2928
  });
2867
- currentTradeResult = {
2868
- ...currentTradeResult,
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: currentTradeResult.grossProfit + grossProfit,
2875
- netProfit: currentTradeResult.netProfit + grossProfit - fee,
2876
- closeFee: currentTradeResult.closeFee + fee,
2877
- totalFee: currentTradeResult.openFee + currentTradeResult.closeFee + fee + (currentTradeResult.fundingFee ?? 0),
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: currentTradeResult.entrySlippageCost + exitSlippageCost
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
- if (currentSignalId) {
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 + order.qty;
3472
+ const resultingQty = previousQty + orderQty;
3298
3473
  const resultingEntryPrice = previousPosition ? getWeightedAverage(
3299
3474
  previousPosition.price,
3300
3475
  previousQty,
3301
3476
  entryPrice,
3302
- order.qty
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: order.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: order.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
- order.qty
3520
+ orderQty
3332
3521
  );
3333
3522
  const weightedEntryPrice = getWeightedAverage(
3334
3523
  currentTradeResult.entryPrice,
3335
3524
  currentTradeResult.qty,
3336
3525
  entryPrice,
3337
- order.qty
3526
+ orderQty
3338
3527
  );
3339
3528
  const entryBaseSlippageBps = getWeightedAverage(
3340
3529
  currentTradeResult.entryBaseSlippageBps,
3341
3530
  currentTradeResult.qty,
3342
3531
  entrySlippageBreakdown.baseSlippageBps,
3343
- order.qty
3532
+ orderQty
3344
3533
  );
3345
3534
  const entrySpreadBps = getWeightedAverage(
3346
3535
  currentTradeResult.entrySpreadBps,
3347
3536
  currentTradeResult.qty,
3348
3537
  entrySlippageBreakdown.spreadBps,
3349
- order.qty
3538
+ orderQty
3350
3539
  );
3351
3540
  const entrySpreadSlippageBps = getWeightedAverage(
3352
3541
  currentTradeResult.entrySpreadSlippageBps,
3353
3542
  currentTradeResult.qty,
3354
3543
  entrySlippageBreakdown.spreadSlippageBps,
3355
- order.qty
3544
+ orderQty
3356
3545
  );
3357
3546
  const entryMarketImpactBps = getWeightedAverage(
3358
3547
  currentTradeResult.entryMarketImpactBps,
3359
3548
  currentTradeResult.qty,
3360
3549
  entrySlippageBreakdown.marketImpactBps,
3361
- order.qty
3550
+ orderQty
3362
3551
  );
3363
3552
  const entryDelayRiskBps = getWeightedAverage(
3364
3553
  currentTradeResult.entryDelayRiskBps,
3365
3554
  currentTradeResult.qty,
3366
3555
  entrySlippageBreakdown.delayRiskBps,
3367
- order.qty
3556
+ orderQty
3368
3557
  );
3369
3558
  const totalEntrySlippageCost = currentTradeResult.entrySlippageCost + entrySlippageCost2;
3370
3559
  currentTradeResult = {
3371
3560
  ...currentTradeResult,
3372
- qty: currentTradeResult.qty + order.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: order.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: order.qty
3604
+ qty: orderQty
3415
3605
  });
3416
3606
  amount += profit;
3417
3607
  currentPositionProfit = profit;
3418
- currentTradeResult = currentSignalId ? {
3419
- signalId: currentSignalId,
3608
+ const openTradeResult = createOpenTradeResult({
3609
+ signalId: currentSignalId ?? "",
3420
3610
  direction: order.direction,
3421
- qty: order.qty,
3422
- closedQty: 0,
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
- requestedExitPrice: null,
3429
- exitPrice: null,
3430
- grossProfit: 0,
3431
- netProfit: profit,
3432
- openFee: fee,
3433
- closeFee: 0,
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 recordExitResult = ({
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
- if (!currentTradeResult || !currentPosition) {
173
- return;
174
- }
175
- const previousClosedQty = currentTradeResult.closedQty;
237
+ const previousClosedQty = tradeResult.closedQty;
176
238
  const requestedExitPrice = getWeightedAverage(
177
- currentTradeResult.requestedExitPrice,
239
+ tradeResult.requestedExitPrice,
178
240
  previousClosedQty,
179
241
  requestedPrice,
180
242
  qty
181
243
  );
182
244
  const exitPrice = getWeightedAverage(
183
- currentTradeResult.exitPrice,
245
+ tradeResult.exitPrice,
184
246
  previousClosedQty,
185
247
  executionPrice,
186
248
  qty
187
249
  );
188
250
  const exitBaseSlippageBps = getWeightedAverage(
189
- currentTradeResult.exitBaseSlippageBps,
251
+ tradeResult.exitBaseSlippageBps,
190
252
  previousClosedQty,
191
253
  slippageBreakdown.baseSlippageBps,
192
254
  qty
193
255
  );
194
256
  const exitSpreadBps = getWeightedAverage(
195
- currentTradeResult.exitSpreadBps,
257
+ tradeResult.exitSpreadBps,
196
258
  previousClosedQty,
197
259
  slippageBreakdown.spreadBps,
198
260
  qty
199
261
  );
200
262
  const exitSpreadSlippageBps = getWeightedAverage(
201
- currentTradeResult.exitSpreadSlippageBps,
263
+ tradeResult.exitSpreadSlippageBps,
202
264
  previousClosedQty,
203
265
  slippageBreakdown.spreadSlippageBps,
204
266
  qty
205
267
  );
206
268
  const exitMarketImpactBps = getWeightedAverage(
207
- currentTradeResult.exitMarketImpactBps,
269
+ tradeResult.exitMarketImpactBps,
208
270
  previousClosedQty,
209
271
  slippageBreakdown.marketImpactBps,
210
272
  qty
211
273
  );
212
274
  const exitDelayRiskBps = null;
213
- const exitSlippageCost = currentTradeResult.exitSlippageCost + getSlippageCost({
275
+ const exitSlippageCost = tradeResult.exitSlippageCost + getSlippageCost({
214
276
  requestedPrice,
215
277
  executionPrice,
216
- direction: currentPosition.direction,
278
+ direction,
217
279
  stage: "exit",
218
280
  qty
219
281
  });
220
- currentTradeResult = {
221
- ...currentTradeResult,
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: currentTradeResult.grossProfit + grossProfit,
228
- netProfit: currentTradeResult.netProfit + grossProfit - fee,
229
- closeFee: currentTradeResult.closeFee + fee,
230
- totalFee: currentTradeResult.openFee + currentTradeResult.closeFee + fee + (currentTradeResult.fundingFee ?? 0),
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: currentTradeResult.entrySlippageCost + exitSlippageCost
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
- if (currentSignalId) {
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 + order.qty;
825
+ const resultingQty = previousQty + orderQty;
651
826
  const resultingEntryPrice = previousPosition ? getWeightedAverage(
652
827
  previousPosition.price,
653
828
  previousQty,
654
829
  entryPrice,
655
- order.qty
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: order.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: order.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
- order.qty
873
+ orderQty
685
874
  );
686
875
  const weightedEntryPrice = getWeightedAverage(
687
876
  currentTradeResult.entryPrice,
688
877
  currentTradeResult.qty,
689
878
  entryPrice,
690
- order.qty
879
+ orderQty
691
880
  );
692
881
  const entryBaseSlippageBps = getWeightedAverage(
693
882
  currentTradeResult.entryBaseSlippageBps,
694
883
  currentTradeResult.qty,
695
884
  entrySlippageBreakdown.baseSlippageBps,
696
- order.qty
885
+ orderQty
697
886
  );
698
887
  const entrySpreadBps = getWeightedAverage(
699
888
  currentTradeResult.entrySpreadBps,
700
889
  currentTradeResult.qty,
701
890
  entrySlippageBreakdown.spreadBps,
702
- order.qty
891
+ orderQty
703
892
  );
704
893
  const entrySpreadSlippageBps = getWeightedAverage(
705
894
  currentTradeResult.entrySpreadSlippageBps,
706
895
  currentTradeResult.qty,
707
896
  entrySlippageBreakdown.spreadSlippageBps,
708
- order.qty
897
+ orderQty
709
898
  );
710
899
  const entryMarketImpactBps = getWeightedAverage(
711
900
  currentTradeResult.entryMarketImpactBps,
712
901
  currentTradeResult.qty,
713
902
  entrySlippageBreakdown.marketImpactBps,
714
- order.qty
903
+ orderQty
715
904
  );
716
905
  const entryDelayRiskBps = getWeightedAverage(
717
906
  currentTradeResult.entryDelayRiskBps,
718
907
  currentTradeResult.qty,
719
908
  entrySlippageBreakdown.delayRiskBps,
720
- order.qty
909
+ orderQty
721
910
  );
722
911
  const totalEntrySlippageCost = currentTradeResult.entrySlippageCost + entrySlippageCost2;
723
912
  currentTradeResult = {
724
913
  ...currentTradeResult,
725
- qty: currentTradeResult.qty + order.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: order.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: order.qty
957
+ qty: orderQty
768
958
  });
769
959
  amount += profit;
770
960
  currentPositionProfit = profit;
771
- currentTradeResult = currentSignalId ? {
772
- signalId: currentSignalId,
961
+ const openTradeResult = createOpenTradeResult({
962
+ signalId: currentSignalId ?? "",
773
963
  direction: order.direction,
774
- qty: order.qty,
775
- closedQty: 0,
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
- requestedExitPrice: null,
782
- exitPrice: null,
783
- grossProfit: 0,
784
- netProfit: profit,
785
- openFee: fee,
786
- closeFee: 0,
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.9",
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.9",
71
- "@tradejs/infra": "^2.0.9",
72
- "@tradejs/types": "^2.0.9",
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",