@tradejs/node 2.0.2 → 2.0.4

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
@@ -2895,7 +2895,10 @@ var createTestConnector = (connector, context) => {
2895
2895
  }
2896
2896
  },
2897
2897
  placeOrder: async (order) => {
2898
- if (currentPosition) {
2898
+ const isPositionIncrease = Boolean(
2899
+ currentPosition && order.positionIntent === "increase" && currentPosition.direction === order.direction
2900
+ );
2901
+ if (currentPosition && !isPositionIncrease) {
2899
2902
  return false;
2900
2903
  }
2901
2904
  const isLong = order.direction === "LONG";
@@ -2909,9 +2912,114 @@ var createTestConnector = (connector, context) => {
2909
2912
  stage: "entry",
2910
2913
  signal: order.signal
2911
2914
  });
2912
- currentPosition = { ...order, price: entryPrice, amount };
2915
+ const previousPosition = currentPosition;
2916
+ const previousQty = previousPosition?.qty ?? 0;
2917
+ const resultingQty = previousQty + order.qty;
2918
+ const resultingEntryPrice = previousPosition ? getWeightedAverage(
2919
+ previousPosition.price,
2920
+ previousQty,
2921
+ entryPrice,
2922
+ order.qty
2923
+ ) : entryPrice;
2924
+ currentPosition = previousPosition ? {
2925
+ ...previousPosition,
2926
+ qty: resultingQty,
2927
+ price: resultingEntryPrice
2928
+ } : { ...order, price: entryPrice, amount };
2929
+ originalQty = resultingQty;
2930
+ if (isPositionIncrease) {
2931
+ const { fee: fee2, profit: profit2 } = getNetProfit({
2932
+ grossProfit: 0,
2933
+ price: entryPrice,
2934
+ qty: order.qty,
2935
+ feeRate: order.isLimit ? makerFeeRate : takerFeeRate
2936
+ });
2937
+ const entrySlippageCost2 = getSlippageCost({
2938
+ requestedPrice: order.price,
2939
+ executionPrice: entryPrice,
2940
+ direction: order.direction,
2941
+ stage: "entry",
2942
+ qty: order.qty
2943
+ });
2944
+ amount += profit2;
2945
+ currentPositionProfit += profit2;
2946
+ if (currentTradeResult) {
2947
+ const requestedEntryPrice = getWeightedAverage(
2948
+ currentTradeResult.requestedEntryPrice,
2949
+ currentTradeResult.qty,
2950
+ order.price,
2951
+ order.qty
2952
+ );
2953
+ const weightedEntryPrice = getWeightedAverage(
2954
+ currentTradeResult.entryPrice,
2955
+ currentTradeResult.qty,
2956
+ entryPrice,
2957
+ order.qty
2958
+ );
2959
+ const entryBaseSlippageBps = getWeightedAverage(
2960
+ currentTradeResult.entryBaseSlippageBps,
2961
+ currentTradeResult.qty,
2962
+ entrySlippageBreakdown.baseSlippageBps,
2963
+ order.qty
2964
+ );
2965
+ const entrySpreadBps = getWeightedAverage(
2966
+ currentTradeResult.entrySpreadBps,
2967
+ currentTradeResult.qty,
2968
+ entrySlippageBreakdown.spreadBps,
2969
+ order.qty
2970
+ );
2971
+ const entrySpreadSlippageBps = getWeightedAverage(
2972
+ currentTradeResult.entrySpreadSlippageBps,
2973
+ currentTradeResult.qty,
2974
+ entrySlippageBreakdown.spreadSlippageBps,
2975
+ order.qty
2976
+ );
2977
+ const entryMarketImpactBps = getWeightedAverage(
2978
+ currentTradeResult.entryMarketImpactBps,
2979
+ currentTradeResult.qty,
2980
+ entrySlippageBreakdown.marketImpactBps,
2981
+ order.qty
2982
+ );
2983
+ const entryDelayRiskBps = getWeightedAverage(
2984
+ currentTradeResult.entryDelayRiskBps,
2985
+ currentTradeResult.qty,
2986
+ entrySlippageBreakdown.delayRiskBps,
2987
+ order.qty
2988
+ );
2989
+ const totalEntrySlippageCost = currentTradeResult.entrySlippageCost + entrySlippageCost2;
2990
+ currentTradeResult = {
2991
+ ...currentTradeResult,
2992
+ qty: currentTradeResult.qty + order.qty,
2993
+ requestedEntryPrice,
2994
+ entryPrice: weightedEntryPrice,
2995
+ netProfit: currentTradeResult.netProfit + profit2,
2996
+ openFee: currentTradeResult.openFee + fee2,
2997
+ totalFee: currentTradeResult.totalFee + fee2,
2998
+ entrySlippagePrice: weightedEntryPrice - requestedEntryPrice,
2999
+ entrySlippageBps: getSlippageBps(
3000
+ requestedEntryPrice,
3001
+ weightedEntryPrice
3002
+ ),
3003
+ entryBaseSlippageBps,
3004
+ entrySpreadBps,
3005
+ entrySpreadSlippageBps,
3006
+ entryMarketImpactBps,
3007
+ entryDelayRiskBps,
3008
+ entrySlippageCost: totalEntrySlippageCost,
3009
+ totalSlippageCost: totalEntrySlippageCost + currentTradeResult.exitSlippageCost
3010
+ };
3011
+ }
3012
+ logOrder({
3013
+ ...order,
3014
+ price: entryPrice,
3015
+ profit: profit2,
3016
+ fee: fee2,
3017
+ type: isLong ? "OPEN_LONG" : "OPEN_SHORT",
3018
+ ...getExecutionSlippageLogData(entrySlippageBreakdown, "entry")
3019
+ });
3020
+ return true;
3021
+ }
2913
3022
  currentSignalId = typeof order.signal?.signalId === "string" && order.signal.signalId ? order.signal.signalId : null;
2914
- originalQty = order.qty;
2915
3023
  const { fee, profit } = getNetProfit({
2916
3024
  grossProfit: 0,
2917
3025
  price: entryPrice,
@@ -2978,16 +3086,21 @@ var createTestConnector = (connector, context) => {
2978
3086
  return false;
2979
3087
  }
2980
3088
  takeProfits = Array.isArray(nextTakeProfits) ? nextTakeProfits.map((tp) => ({ ...tp })) : [];
3089
+ const fullTakeProfit = takeProfits.length === 1 && takeProfits[0]?.rate === 1 ? takeProfits[0].price : void 0;
3090
+ currentPosition = {
3091
+ ...currentPosition,
3092
+ tpPrice: fullTakeProfit
3093
+ };
2981
3094
  return true;
2982
3095
  },
2983
3096
  setStopLoss: async ({ stopLossPrice: nextStopLossPrice }) => {
2984
3097
  if (!currentPosition) {
2985
3098
  return false;
2986
3099
  }
2987
- stopLossPrice = nextStopLossPrice || null;
3100
+ stopLossPrice = nextStopLossPrice ?? null;
2988
3101
  currentPosition = {
2989
3102
  ...currentPosition,
2990
- ...stopLossPrice != null ? { slPrice: stopLossPrice } : {}
3103
+ slPrice: stopLossPrice ?? void 0
2991
3104
  };
2992
3105
  return true;
2993
3106
  },
package/dist/backtest.mjs CHANGED
@@ -625,7 +625,10 @@ var createTestConnector = (connector, context) => {
625
625
  }
626
626
  },
627
627
  placeOrder: async (order) => {
628
- if (currentPosition) {
628
+ const isPositionIncrease = Boolean(
629
+ currentPosition && order.positionIntent === "increase" && currentPosition.direction === order.direction
630
+ );
631
+ if (currentPosition && !isPositionIncrease) {
629
632
  return false;
630
633
  }
631
634
  const isLong = order.direction === "LONG";
@@ -639,9 +642,114 @@ var createTestConnector = (connector, context) => {
639
642
  stage: "entry",
640
643
  signal: order.signal
641
644
  });
642
- currentPosition = { ...order, price: entryPrice, amount };
645
+ const previousPosition = currentPosition;
646
+ const previousQty = previousPosition?.qty ?? 0;
647
+ const resultingQty = previousQty + order.qty;
648
+ const resultingEntryPrice = previousPosition ? getWeightedAverage(
649
+ previousPosition.price,
650
+ previousQty,
651
+ entryPrice,
652
+ order.qty
653
+ ) : entryPrice;
654
+ currentPosition = previousPosition ? {
655
+ ...previousPosition,
656
+ qty: resultingQty,
657
+ price: resultingEntryPrice
658
+ } : { ...order, price: entryPrice, amount };
659
+ originalQty = resultingQty;
660
+ if (isPositionIncrease) {
661
+ const { fee: fee2, profit: profit2 } = getNetProfit({
662
+ grossProfit: 0,
663
+ price: entryPrice,
664
+ qty: order.qty,
665
+ feeRate: order.isLimit ? makerFeeRate : takerFeeRate
666
+ });
667
+ const entrySlippageCost2 = getSlippageCost({
668
+ requestedPrice: order.price,
669
+ executionPrice: entryPrice,
670
+ direction: order.direction,
671
+ stage: "entry",
672
+ qty: order.qty
673
+ });
674
+ amount += profit2;
675
+ currentPositionProfit += profit2;
676
+ if (currentTradeResult) {
677
+ const requestedEntryPrice = getWeightedAverage(
678
+ currentTradeResult.requestedEntryPrice,
679
+ currentTradeResult.qty,
680
+ order.price,
681
+ order.qty
682
+ );
683
+ const weightedEntryPrice = getWeightedAverage(
684
+ currentTradeResult.entryPrice,
685
+ currentTradeResult.qty,
686
+ entryPrice,
687
+ order.qty
688
+ );
689
+ const entryBaseSlippageBps = getWeightedAverage(
690
+ currentTradeResult.entryBaseSlippageBps,
691
+ currentTradeResult.qty,
692
+ entrySlippageBreakdown.baseSlippageBps,
693
+ order.qty
694
+ );
695
+ const entrySpreadBps = getWeightedAverage(
696
+ currentTradeResult.entrySpreadBps,
697
+ currentTradeResult.qty,
698
+ entrySlippageBreakdown.spreadBps,
699
+ order.qty
700
+ );
701
+ const entrySpreadSlippageBps = getWeightedAverage(
702
+ currentTradeResult.entrySpreadSlippageBps,
703
+ currentTradeResult.qty,
704
+ entrySlippageBreakdown.spreadSlippageBps,
705
+ order.qty
706
+ );
707
+ const entryMarketImpactBps = getWeightedAverage(
708
+ currentTradeResult.entryMarketImpactBps,
709
+ currentTradeResult.qty,
710
+ entrySlippageBreakdown.marketImpactBps,
711
+ order.qty
712
+ );
713
+ const entryDelayRiskBps = getWeightedAverage(
714
+ currentTradeResult.entryDelayRiskBps,
715
+ currentTradeResult.qty,
716
+ entrySlippageBreakdown.delayRiskBps,
717
+ order.qty
718
+ );
719
+ const totalEntrySlippageCost = currentTradeResult.entrySlippageCost + entrySlippageCost2;
720
+ currentTradeResult = {
721
+ ...currentTradeResult,
722
+ qty: currentTradeResult.qty + order.qty,
723
+ requestedEntryPrice,
724
+ entryPrice: weightedEntryPrice,
725
+ netProfit: currentTradeResult.netProfit + profit2,
726
+ openFee: currentTradeResult.openFee + fee2,
727
+ totalFee: currentTradeResult.totalFee + fee2,
728
+ entrySlippagePrice: weightedEntryPrice - requestedEntryPrice,
729
+ entrySlippageBps: getSlippageBps(
730
+ requestedEntryPrice,
731
+ weightedEntryPrice
732
+ ),
733
+ entryBaseSlippageBps,
734
+ entrySpreadBps,
735
+ entrySpreadSlippageBps,
736
+ entryMarketImpactBps,
737
+ entryDelayRiskBps,
738
+ entrySlippageCost: totalEntrySlippageCost,
739
+ totalSlippageCost: totalEntrySlippageCost + currentTradeResult.exitSlippageCost
740
+ };
741
+ }
742
+ logOrder({
743
+ ...order,
744
+ price: entryPrice,
745
+ profit: profit2,
746
+ fee: fee2,
747
+ type: isLong ? "OPEN_LONG" : "OPEN_SHORT",
748
+ ...getExecutionSlippageLogData(entrySlippageBreakdown, "entry")
749
+ });
750
+ return true;
751
+ }
643
752
  currentSignalId = typeof order.signal?.signalId === "string" && order.signal.signalId ? order.signal.signalId : null;
644
- originalQty = order.qty;
645
753
  const { fee, profit } = getNetProfit({
646
754
  grossProfit: 0,
647
755
  price: entryPrice,
@@ -708,16 +816,21 @@ var createTestConnector = (connector, context) => {
708
816
  return false;
709
817
  }
710
818
  takeProfits = Array.isArray(nextTakeProfits) ? nextTakeProfits.map((tp) => ({ ...tp })) : [];
819
+ const fullTakeProfit = takeProfits.length === 1 && takeProfits[0]?.rate === 1 ? takeProfits[0].price : void 0;
820
+ currentPosition = {
821
+ ...currentPosition,
822
+ tpPrice: fullTakeProfit
823
+ };
711
824
  return true;
712
825
  },
713
826
  setStopLoss: async ({ stopLossPrice: nextStopLossPrice }) => {
714
827
  if (!currentPosition) {
715
828
  return false;
716
829
  }
717
- stopLossPrice = nextStopLossPrice || null;
830
+ stopLossPrice = nextStopLossPrice ?? null;
718
831
  currentPosition = {
719
832
  ...currentPosition,
720
- ...stopLossPrice != null ? { slPrice: stopLossPrice } : {}
833
+ slPrice: stopLossPrice ?? void 0
721
834
  };
722
835
  return true;
723
836
  },
package/dist/cli.js CHANGED
@@ -7071,7 +7071,8 @@ var SCREENSHOT_NAVIGATION_RETRY_DELAY_MS = 2e3;
7071
7071
  var SCREENSHOT_CAPTURE_ATTEMPTS = 2;
7072
7072
  var SCREENSHOT_CAPTURE_RETRY_DELAY_MS = 2e3;
7073
7073
  var SCREENSHOT_NAVIGATION_TIMEOUT_MS = 3e4;
7074
- var SCREENSHOT_RENDER_DELAY_MS = 1e4;
7074
+ var SCREENSHOT_READY_TIMEOUT_MS = 3e4;
7075
+ var SCREENSHOT_READY_SELECTOR = '[data-screenshot-ready="true"]';
7075
7076
  var SCREENSHOT_CONSOLE_LOG_LIMIT = 20;
7076
7077
  var SCREENSHOT_CONSOLE_TEXT_LIMIT = 500;
7077
7078
  var SCREENSHOT_BROWSER_STDERR = process.env.SCREENSHOT_BROWSER_STDERR === "1";
@@ -7400,7 +7401,16 @@ var screenDashboard = async (signal, projectRoot, userName = "root") => {
7400
7401
  `Failed to open dashboard ${dashboardUrl}: ${gotoError.message || String(gotoError)}`
7401
7402
  );
7402
7403
  }
7403
- await (0, import_async.delay)(SCREENSHOT_RENDER_DELAY_MS);
7404
+ await page.waitForSelector(SCREENSHOT_READY_SELECTOR, {
7405
+ timeout: SCREENSHOT_READY_TIMEOUT_MS
7406
+ });
7407
+ await page.evaluate(
7408
+ () => new Promise((resolve) => {
7409
+ requestAnimationFrame(
7410
+ () => requestAnimationFrame(() => resolve())
7411
+ );
7412
+ })
7413
+ );
7404
7414
  await import_promises.default.mkdir(getScreenshotsDir(projectRoot), { recursive: true });
7405
7415
  await page.screenshot({
7406
7416
  path: screenshotPath,
package/dist/cli.mjs CHANGED
@@ -74,7 +74,8 @@ var SCREENSHOT_NAVIGATION_RETRY_DELAY_MS = 2e3;
74
74
  var SCREENSHOT_CAPTURE_ATTEMPTS = 2;
75
75
  var SCREENSHOT_CAPTURE_RETRY_DELAY_MS = 2e3;
76
76
  var SCREENSHOT_NAVIGATION_TIMEOUT_MS = 3e4;
77
- var SCREENSHOT_RENDER_DELAY_MS = 1e4;
77
+ var SCREENSHOT_READY_TIMEOUT_MS = 3e4;
78
+ var SCREENSHOT_READY_SELECTOR = '[data-screenshot-ready="true"]';
78
79
  var SCREENSHOT_CONSOLE_LOG_LIMIT = 20;
79
80
  var SCREENSHOT_CONSOLE_TEXT_LIMIT = 500;
80
81
  var SCREENSHOT_BROWSER_STDERR = process.env.SCREENSHOT_BROWSER_STDERR === "1";
@@ -403,7 +404,16 @@ var screenDashboard = async (signal, projectRoot, userName = "root") => {
403
404
  `Failed to open dashboard ${dashboardUrl}: ${gotoError.message || String(gotoError)}`
404
405
  );
405
406
  }
406
- await delay(SCREENSHOT_RENDER_DELAY_MS);
407
+ await page.waitForSelector(SCREENSHOT_READY_SELECTOR, {
408
+ timeout: SCREENSHOT_READY_TIMEOUT_MS
409
+ });
410
+ await page.evaluate(
411
+ () => new Promise((resolve) => {
412
+ requestAnimationFrame(
413
+ () => requestAnimationFrame(() => resolve())
414
+ );
415
+ })
416
+ );
407
417
  await fs.mkdir(getScreenshotsDir(projectRoot), { recursive: true });
408
418
  await page.screenshot({
409
419
  path: screenshotPath,
@@ -50,12 +50,13 @@ interface ExecuteEntryOrderParams {
50
50
  timestamp: number;
51
51
  takeProfits: Tp[];
52
52
  stopLossPrice: number | null;
53
+ positionIntent?: 'open' | 'increase';
53
54
  leverage?: number;
54
55
  signal: Signal;
55
56
  beforePlaceOrder?: () => Promise<void>;
56
57
  recordRuntimeTrade?: boolean;
57
58
  }
58
- declare const executeEntryOrder: ({ connector, userName, symbol, direction, qty, currentPrice, timestamp, takeProfits, stopLossPrice, signal, beforePlaceOrder, recordRuntimeTrade, leverage, }: ExecuteEntryOrderParams) => Promise<number>;
59
+ declare const executeEntryOrder: ({ connector, userName, symbol, direction, qty, currentPrice, timestamp, takeProfits, stopLossPrice, positionIntent, signal, beforePlaceOrder, recordRuntimeTrade, leverage, }: ExecuteEntryOrderParams) => Promise<number>;
59
60
 
60
61
  declare const enrichSignalWithBinanceMarketContext: (params: {
61
62
  signal: Signal;
@@ -50,12 +50,13 @@ interface ExecuteEntryOrderParams {
50
50
  timestamp: number;
51
51
  takeProfits: Tp[];
52
52
  stopLossPrice: number | null;
53
+ positionIntent?: 'open' | 'increase';
53
54
  leverage?: number;
54
55
  signal: Signal;
55
56
  beforePlaceOrder?: () => Promise<void>;
56
57
  recordRuntimeTrade?: boolean;
57
58
  }
58
- declare const executeEntryOrder: ({ connector, userName, symbol, direction, qty, currentPrice, timestamp, takeProfits, stopLossPrice, signal, beforePlaceOrder, recordRuntimeTrade, leverage, }: ExecuteEntryOrderParams) => Promise<number>;
59
+ declare const executeEntryOrder: ({ connector, userName, symbol, direction, qty, currentPrice, timestamp, takeProfits, stopLossPrice, positionIntent, signal, beforePlaceOrder, recordRuntimeTrade, leverage, }: ExecuteEntryOrderParams) => Promise<number>;
59
60
 
60
61
  declare const enrichSignalWithBinanceMarketContext: (params: {
61
62
  signal: Signal;
@@ -7285,6 +7285,10 @@ var recordRuntimeTradeOpen = async (params) => {
7285
7285
  }
7286
7286
  const record = {
7287
7287
  ...params,
7288
+ entryCount: 1,
7289
+ lastEntryPrice: params.entryPrice,
7290
+ lastEntryQty: params.qty,
7291
+ lastEntryTimestamp: params.entryTimestamp,
7288
7292
  status: "active",
7289
7293
  currentPrice: params.entryPrice,
7290
7294
  currentPnl: 0,
@@ -7321,6 +7325,73 @@ var recordRuntimeTradeOpen = async (params) => {
7321
7325
  }
7322
7326
  return record;
7323
7327
  };
7328
+ var recordRuntimeTradeIncrease = async (params) => {
7329
+ const {
7330
+ userName,
7331
+ strategy,
7332
+ symbol,
7333
+ direction,
7334
+ resultingQty,
7335
+ resultingEntryPrice,
7336
+ addedQty,
7337
+ addedEntryPrice,
7338
+ entryTimestamp,
7339
+ fee,
7340
+ accountId,
7341
+ deploymentId
7342
+ } = params;
7343
+ if (!userName) {
7344
+ return null;
7345
+ }
7346
+ const existing = await getActiveRuntimeTrade({
7347
+ userName,
7348
+ symbol,
7349
+ accountId,
7350
+ deploymentId
7351
+ });
7352
+ if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
7353
+ return null;
7354
+ }
7355
+ const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
7356
+ const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
7357
+ const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
7358
+ const next = {
7359
+ ...existing,
7360
+ qty: resultingQty,
7361
+ entryPrice: resultingEntryPrice,
7362
+ entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
7363
+ lastEntryPrice: addedEntryPrice,
7364
+ lastEntryQty: addedQty,
7365
+ lastEntryTimestamp: entryTimestamp,
7366
+ currentPrice: resultingEntryPrice,
7367
+ currentPnl: 0,
7368
+ fee: openFee,
7369
+ openFee,
7370
+ totalFee,
7371
+ lastSyncedAt: now()
7372
+ };
7373
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
7374
+ try {
7375
+ await Promise.all([
7376
+ (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, existing.orderId), next, {
7377
+ expire: 0
7378
+ }),
7379
+ (0, import_redis2.setHashJsonField)(
7380
+ import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
7381
+ existing.orderId,
7382
+ next,
7383
+ { expire: 0 }
7384
+ )
7385
+ ]);
7386
+ } catch (error) {
7387
+ import_logger3.logger.error(
7388
+ "runtime trade increase journal failed: %s %s",
7389
+ symbol,
7390
+ error?.message || String(error)
7391
+ );
7392
+ }
7393
+ return next;
7394
+ };
7324
7395
  var getActiveRuntimeTrade = async (params) => {
7325
7396
  const { userName, symbol, accountId, deploymentId } = params;
7326
7397
  if (!userName) {
@@ -8633,12 +8704,14 @@ var executeEntryOrder = async ({
8633
8704
  timestamp,
8634
8705
  takeProfits,
8635
8706
  stopLossPrice,
8707
+ positionIntent = "open",
8636
8708
  signal,
8637
8709
  beforePlaceOrder,
8638
8710
  recordRuntimeTrade = true,
8639
8711
  leverage
8640
8712
  }) => {
8641
8713
  await beforePlaceOrder?.();
8714
+ const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
8642
8715
  const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
8643
8716
  const signalTimestamp = signal.timestamp;
8644
8717
  const signalClosePrice = currentPrice;
@@ -8653,6 +8726,7 @@ var executeEntryOrder = async ({
8653
8726
  qty,
8654
8727
  price: currentPrice,
8655
8728
  isLimit: false,
8729
+ positionIntent,
8656
8730
  timestamp,
8657
8731
  direction,
8658
8732
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
@@ -8663,27 +8737,36 @@ var executeEntryOrder = async ({
8663
8737
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
8664
8738
  const currentPosition = await connector.getPosition(symbol);
8665
8739
  const fillTime = Date.now();
8666
- const entryPrice = currentPosition?.price && Number.isFinite(currentPosition.price) ? currentPosition.price : currentPrice;
8667
- const fillSource = currentPosition?.price && Number.isFinite(currentPosition.price) ? "exchange_position" : orderPlaced ? "requested_price" : "unknown";
8668
- const entryQty = currentPosition?.qty && Number.isFinite(currentPosition.qty) ? currentPosition.qty : placedQty;
8669
- const estimatedOpenFee = entryPrice * entryQty * import_constants3.FEE_PERCENT;
8670
- signal.prices.currentPrice = entryPrice;
8671
- signal.orderQty = entryQty;
8672
- signal.orderValue = entryQty * entryPrice;
8740
+ const isPositionIncrease = positionIntent === "increase" && previousPosition?.direction === direction && Number.isFinite(previousPosition.qty) && previousPosition.qty > 0;
8741
+ const hasRefreshedIncreasedPosition = Boolean(
8742
+ isPositionIncrease && previousPosition && currentPosition?.qty && currentPosition.qty > previousPosition.qty
8743
+ );
8744
+ const hasUsableCurrentPosition = Boolean(
8745
+ currentPosition && Number.isFinite(currentPosition.price) && Number.isFinite(currentPosition.qty) && (!isPositionIncrease || hasRefreshedIncreasedPosition)
8746
+ );
8747
+ const resultingEntryPrice = hasUsableCurrentPosition && currentPosition ? currentPosition.price : isPositionIncrease && previousPosition ? (previousPosition.price * previousPosition.qty + currentPrice * placedQty) / (previousPosition.qty + placedQty) : currentPrice;
8748
+ const resultingQty = hasUsableCurrentPosition && currentPosition ? currentPosition.qty : isPositionIncrease && previousPosition ? previousPosition.qty + placedQty : placedQty;
8749
+ const filledQty = isPositionIncrease && previousPosition ? hasRefreshedIncreasedPosition && currentPosition ? currentPosition.qty - previousPosition.qty : placedQty : resultingQty;
8750
+ const fillPrice = isPositionIncrease && previousPosition && hasRefreshedIncreasedPosition && currentPosition ? (resultingEntryPrice * currentPosition.qty - previousPosition.price * previousPosition.qty) / (currentPosition.qty - previousPosition.qty) : isPositionIncrease ? currentPrice : resultingEntryPrice;
8751
+ const fillSource = hasUsableCurrentPosition ? "exchange_position" : orderPlaced ? "requested_price" : "unknown";
8752
+ const estimatedOpenFee = fillPrice * filledQty * import_constants3.FEE_PERCENT;
8753
+ signal.prices.currentPrice = fillPrice;
8754
+ signal.orderQty = filledQty;
8755
+ signal.orderValue = filledQty * fillPrice;
8673
8756
  if (orderPlaced) {
8674
8757
  try {
8675
8758
  await applyProtectiveOrders({
8676
8759
  connector,
8677
8760
  symbol,
8678
8761
  direction,
8679
- qty: entryQty,
8762
+ qty: resultingQty,
8680
8763
  takeProfits,
8681
8764
  stopLossPrice
8682
8765
  });
8683
8766
  } catch (error) {
8684
8767
  await connector.closePosition({
8685
8768
  symbol,
8686
- price: entryPrice,
8769
+ price: resultingEntryPrice,
8687
8770
  timestamp,
8688
8771
  direction,
8689
8772
  signal
@@ -8696,7 +8779,7 @@ var executeEntryOrder = async ({
8696
8779
  if (orderPlaced) {
8697
8780
  signal.orderFailureReason = void 0;
8698
8781
  }
8699
- if (orderPlaced && recordRuntimeTrade) {
8782
+ if (orderPlaced && recordRuntimeTrade && !isPositionIncrease) {
8700
8783
  await recordRuntimeTradeOpen({
8701
8784
  userName,
8702
8785
  orderId,
@@ -8705,8 +8788,8 @@ var executeEntryOrder = async ({
8705
8788
  symbol,
8706
8789
  interval: signal.interval,
8707
8790
  direction,
8708
- qty: entryQty,
8709
- entryPrice,
8791
+ qty: resultingQty,
8792
+ entryPrice: resultingEntryPrice,
8710
8793
  signalTimestamp,
8711
8794
  signalClosePrice,
8712
8795
  arrivalSnapshotTime: arrivalSnapshot.arrivalSnapshotTime,
@@ -8717,7 +8800,7 @@ var executeEntryOrder = async ({
8717
8800
  spreadBps: arrivalSnapshot.spreadBps,
8718
8801
  orderSubmitTime,
8719
8802
  orderAckTime,
8720
- fillAvgPrice: entryPrice,
8803
+ fillAvgPrice: fillPrice,
8721
8804
  fillSource,
8722
8805
  fillTime,
8723
8806
  telemetryQuality: resolveRuntimeTelemetryQuality({
@@ -8725,7 +8808,7 @@ var executeEntryOrder = async ({
8725
8808
  arrivalMid: arrivalSnapshot.arrivalMid,
8726
8809
  orderSubmitTime,
8727
8810
  orderAckTime,
8728
- fillAvgPrice: entryPrice,
8811
+ fillAvgPrice: fillPrice,
8729
8812
  fillTime
8730
8813
  }),
8731
8814
  fee: estimatedOpenFee,
@@ -8741,10 +8824,26 @@ var executeEntryOrder = async ({
8741
8824
  ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {}
8742
8825
  });
8743
8826
  }
8744
- if (currentPosition?.price) {
8827
+ if (orderPlaced && recordRuntimeTrade && isPositionIncrease) {
8828
+ await recordRuntimeTradeIncrease({
8829
+ userName,
8830
+ strategy: signal.strategy,
8831
+ symbol,
8832
+ direction,
8833
+ resultingQty,
8834
+ resultingEntryPrice,
8835
+ addedQty: filledQty,
8836
+ addedEntryPrice: fillPrice,
8837
+ entryTimestamp: timestamp,
8838
+ fee: estimatedOpenFee,
8839
+ accountId: signal.accountId,
8840
+ deploymentId: signal.deploymentId
8841
+ });
8842
+ }
8843
+ if (hasUsableCurrentPosition && currentPosition?.price) {
8745
8844
  return currentPosition.price;
8746
8845
  }
8747
- return entryPrice;
8846
+ return resultingEntryPrice;
8748
8847
  };
8749
8848
  var updatePositionProtection = async ({
8750
8849
  connector,
@@ -9517,6 +9616,7 @@ var executeEntryDecision = async ({
9517
9616
  timestamp: decision.entryContext.timestamp,
9518
9617
  takeProfits: decision.orderPlan.takeProfits,
9519
9618
  stopLossPrice: decision.orderPlan.stopLossPrice,
9619
+ positionIntent: decision.orderPlan.positionIntent,
9520
9620
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
9521
9621
  signal,
9522
9622
  beforePlaceOrder,
@@ -9548,6 +9648,7 @@ var executeEntryDecision = async ({
9548
9648
  price: decision.entryContext.prices.currentPrice,
9549
9649
  timestamp: decision.entryContext.timestamp,
9550
9650
  direction: decision.entryContext.direction,
9651
+ positionIntent: decision.orderPlan.positionIntent,
9551
9652
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
9552
9653
  });
9553
9654
  if (!orderPlaced) {
@@ -119,6 +119,10 @@ var recordRuntimeTradeOpen = async (params) => {
119
119
  }
120
120
  const record = {
121
121
  ...params,
122
+ entryCount: 1,
123
+ lastEntryPrice: params.entryPrice,
124
+ lastEntryQty: params.qty,
125
+ lastEntryTimestamp: params.entryTimestamp,
122
126
  status: "active",
123
127
  currentPrice: params.entryPrice,
124
128
  currentPnl: 0,
@@ -155,6 +159,73 @@ var recordRuntimeTradeOpen = async (params) => {
155
159
  }
156
160
  return record;
157
161
  };
162
+ var recordRuntimeTradeIncrease = async (params) => {
163
+ const {
164
+ userName,
165
+ strategy,
166
+ symbol,
167
+ direction,
168
+ resultingQty,
169
+ resultingEntryPrice,
170
+ addedQty,
171
+ addedEntryPrice,
172
+ entryTimestamp,
173
+ fee,
174
+ accountId,
175
+ deploymentId
176
+ } = params;
177
+ if (!userName) {
178
+ return null;
179
+ }
180
+ const existing = await getActiveRuntimeTrade({
181
+ userName,
182
+ symbol,
183
+ accountId,
184
+ deploymentId
185
+ });
186
+ if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
187
+ return null;
188
+ }
189
+ const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
190
+ const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
191
+ const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
192
+ const next = {
193
+ ...existing,
194
+ qty: resultingQty,
195
+ entryPrice: resultingEntryPrice,
196
+ entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
197
+ lastEntryPrice: addedEntryPrice,
198
+ lastEntryQty: addedQty,
199
+ lastEntryTimestamp: entryTimestamp,
200
+ currentPrice: resultingEntryPrice,
201
+ currentPnl: 0,
202
+ fee: openFee,
203
+ openFee,
204
+ totalFee,
205
+ lastSyncedAt: now()
206
+ };
207
+ const dayKey = getRuntimeStorageDayKey(existing.entryTimestamp);
208
+ try {
209
+ await Promise.all([
210
+ setData(redisKeys.runtimeTrade(userName, existing.orderId), next, {
211
+ expire: 0
212
+ }),
213
+ setHashJsonField(
214
+ redisKeys.runtimeTradeBucket(userName, dayKey),
215
+ existing.orderId,
216
+ next,
217
+ { expire: 0 }
218
+ )
219
+ ]);
220
+ } catch (error) {
221
+ logger.error(
222
+ "runtime trade increase journal failed: %s %s",
223
+ symbol,
224
+ error?.message || String(error)
225
+ );
226
+ }
227
+ return next;
228
+ };
158
229
  var getActiveRuntimeTrade = async (params) => {
159
230
  const { userName, symbol, accountId, deploymentId } = params;
160
231
  if (!userName) {
@@ -496,12 +567,14 @@ var executeEntryOrder = async ({
496
567
  timestamp,
497
568
  takeProfits,
498
569
  stopLossPrice,
570
+ positionIntent = "open",
499
571
  signal,
500
572
  beforePlaceOrder,
501
573
  recordRuntimeTrade = true,
502
574
  leverage
503
575
  }) => {
504
576
  await beforePlaceOrder?.();
577
+ const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
505
578
  const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
506
579
  const signalTimestamp = signal.timestamp;
507
580
  const signalClosePrice = currentPrice;
@@ -516,6 +589,7 @@ var executeEntryOrder = async ({
516
589
  qty,
517
590
  price: currentPrice,
518
591
  isLimit: false,
592
+ positionIntent,
519
593
  timestamp,
520
594
  direction,
521
595
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
@@ -526,27 +600,36 @@ var executeEntryOrder = async ({
526
600
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
527
601
  const currentPosition = await connector.getPosition(symbol);
528
602
  const fillTime = Date.now();
529
- const entryPrice = currentPosition?.price && Number.isFinite(currentPosition.price) ? currentPosition.price : currentPrice;
530
- const fillSource = currentPosition?.price && Number.isFinite(currentPosition.price) ? "exchange_position" : orderPlaced ? "requested_price" : "unknown";
531
- const entryQty = currentPosition?.qty && Number.isFinite(currentPosition.qty) ? currentPosition.qty : placedQty;
532
- const estimatedOpenFee = entryPrice * entryQty * FEE_PERCENT;
533
- signal.prices.currentPrice = entryPrice;
534
- signal.orderQty = entryQty;
535
- signal.orderValue = entryQty * entryPrice;
603
+ const isPositionIncrease = positionIntent === "increase" && previousPosition?.direction === direction && Number.isFinite(previousPosition.qty) && previousPosition.qty > 0;
604
+ const hasRefreshedIncreasedPosition = Boolean(
605
+ isPositionIncrease && previousPosition && currentPosition?.qty && currentPosition.qty > previousPosition.qty
606
+ );
607
+ const hasUsableCurrentPosition = Boolean(
608
+ currentPosition && Number.isFinite(currentPosition.price) && Number.isFinite(currentPosition.qty) && (!isPositionIncrease || hasRefreshedIncreasedPosition)
609
+ );
610
+ const resultingEntryPrice = hasUsableCurrentPosition && currentPosition ? currentPosition.price : isPositionIncrease && previousPosition ? (previousPosition.price * previousPosition.qty + currentPrice * placedQty) / (previousPosition.qty + placedQty) : currentPrice;
611
+ const resultingQty = hasUsableCurrentPosition && currentPosition ? currentPosition.qty : isPositionIncrease && previousPosition ? previousPosition.qty + placedQty : placedQty;
612
+ const filledQty = isPositionIncrease && previousPosition ? hasRefreshedIncreasedPosition && currentPosition ? currentPosition.qty - previousPosition.qty : placedQty : resultingQty;
613
+ const fillPrice = isPositionIncrease && previousPosition && hasRefreshedIncreasedPosition && currentPosition ? (resultingEntryPrice * currentPosition.qty - previousPosition.price * previousPosition.qty) / (currentPosition.qty - previousPosition.qty) : isPositionIncrease ? currentPrice : resultingEntryPrice;
614
+ const fillSource = hasUsableCurrentPosition ? "exchange_position" : orderPlaced ? "requested_price" : "unknown";
615
+ const estimatedOpenFee = fillPrice * filledQty * FEE_PERCENT;
616
+ signal.prices.currentPrice = fillPrice;
617
+ signal.orderQty = filledQty;
618
+ signal.orderValue = filledQty * fillPrice;
536
619
  if (orderPlaced) {
537
620
  try {
538
621
  await applyProtectiveOrders({
539
622
  connector,
540
623
  symbol,
541
624
  direction,
542
- qty: entryQty,
625
+ qty: resultingQty,
543
626
  takeProfits,
544
627
  stopLossPrice
545
628
  });
546
629
  } catch (error) {
547
630
  await connector.closePosition({
548
631
  symbol,
549
- price: entryPrice,
632
+ price: resultingEntryPrice,
550
633
  timestamp,
551
634
  direction,
552
635
  signal
@@ -559,7 +642,7 @@ var executeEntryOrder = async ({
559
642
  if (orderPlaced) {
560
643
  signal.orderFailureReason = void 0;
561
644
  }
562
- if (orderPlaced && recordRuntimeTrade) {
645
+ if (orderPlaced && recordRuntimeTrade && !isPositionIncrease) {
563
646
  await recordRuntimeTradeOpen({
564
647
  userName,
565
648
  orderId,
@@ -568,8 +651,8 @@ var executeEntryOrder = async ({
568
651
  symbol,
569
652
  interval: signal.interval,
570
653
  direction,
571
- qty: entryQty,
572
- entryPrice,
654
+ qty: resultingQty,
655
+ entryPrice: resultingEntryPrice,
573
656
  signalTimestamp,
574
657
  signalClosePrice,
575
658
  arrivalSnapshotTime: arrivalSnapshot.arrivalSnapshotTime,
@@ -580,7 +663,7 @@ var executeEntryOrder = async ({
580
663
  spreadBps: arrivalSnapshot.spreadBps,
581
664
  orderSubmitTime,
582
665
  orderAckTime,
583
- fillAvgPrice: entryPrice,
666
+ fillAvgPrice: fillPrice,
584
667
  fillSource,
585
668
  fillTime,
586
669
  telemetryQuality: resolveRuntimeTelemetryQuality({
@@ -588,7 +671,7 @@ var executeEntryOrder = async ({
588
671
  arrivalMid: arrivalSnapshot.arrivalMid,
589
672
  orderSubmitTime,
590
673
  orderAckTime,
591
- fillAvgPrice: entryPrice,
674
+ fillAvgPrice: fillPrice,
592
675
  fillTime
593
676
  }),
594
677
  fee: estimatedOpenFee,
@@ -604,10 +687,26 @@ var executeEntryOrder = async ({
604
687
  ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {}
605
688
  });
606
689
  }
607
- if (currentPosition?.price) {
690
+ if (orderPlaced && recordRuntimeTrade && isPositionIncrease) {
691
+ await recordRuntimeTradeIncrease({
692
+ userName,
693
+ strategy: signal.strategy,
694
+ symbol,
695
+ direction,
696
+ resultingQty,
697
+ resultingEntryPrice,
698
+ addedQty: filledQty,
699
+ addedEntryPrice: fillPrice,
700
+ entryTimestamp: timestamp,
701
+ fee: estimatedOpenFee,
702
+ accountId: signal.accountId,
703
+ deploymentId: signal.deploymentId
704
+ });
705
+ }
706
+ if (hasUsableCurrentPosition && currentPosition?.price) {
608
707
  return currentPosition.price;
609
708
  }
610
- return entryPrice;
709
+ return resultingEntryPrice;
611
710
  };
612
711
  var updatePositionProtection = async ({
613
712
  connector,
@@ -1354,6 +1453,7 @@ var executeEntryDecision = async ({
1354
1453
  timestamp: decision.entryContext.timestamp,
1355
1454
  takeProfits: decision.orderPlan.takeProfits,
1356
1455
  stopLossPrice: decision.orderPlan.stopLossPrice,
1456
+ positionIntent: decision.orderPlan.positionIntent,
1357
1457
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
1358
1458
  signal,
1359
1459
  beforePlaceOrder,
@@ -1385,6 +1485,7 @@ var executeEntryDecision = async ({
1385
1485
  price: decision.entryContext.prices.currentPrice,
1386
1486
  timestamp: decision.entryContext.timestamp,
1387
1487
  direction: decision.entryContext.direction,
1488
+ positionIntent: decision.orderPlan.positionIntent,
1388
1489
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
1389
1490
  });
1390
1491
  if (!orderPlaced) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/node",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
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.1.42",
69
69
  "@langchain/openai": "^1.4.5",
70
- "@tradejs/core": "^2.0.2",
71
- "@tradejs/infra": "^2.0.2",
72
- "@tradejs/types": "^2.0.2",
70
+ "@tradejs/core": "^2.0.4",
71
+ "@tradejs/infra": "^2.0.4",
72
+ "@tradejs/types": "^2.0.4",
73
73
  "chalk": "4.1.2",
74
74
  "ioredis": "5.8.0",
75
75
  "pinets": "0.8.12",