@tradejs/node 2.0.1 → 2.0.3

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
@@ -1838,11 +1838,12 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1838
1838
 
1839
1839
  // src/strategyHelpers/derivativesContext.ts
1840
1840
  var import_indicators2 = require("@tradejs/core/indicators");
1841
+ var import_data = require("@tradejs/core/data");
1841
1842
  var import_strategies3 = require("@tradejs/core/strategies");
1842
1843
  var import_constants = require("@tradejs/core/constants");
1843
1844
  var import_timescale3 = require("@tradejs/infra/timescale");
1844
1845
  var import_logger5 = require("@tradejs/infra/logger");
1845
- var SOURCE_INTERVALS = ["15m"];
1846
+ var STORED_INTERVALS = ["15m", "1h"];
1846
1847
  var CONTEXT_INTERVALS = ["15m", "1h"];
1847
1848
  var DEFAULT_LOOKBACK_HOURS = 48;
1848
1849
  var PRIMARY_DERIVATIVES_REFERENCE_SYMBOL = import_constants.DERIVATIVES_CONTEXT_BASE_REFERENCE_SYMBOLS[0];
@@ -1868,9 +1869,12 @@ var parseLookbackMs = () => {
1868
1869
  const normalizedHours = Number.isFinite(hours) && hours > 0 ? hours : DEFAULT_LOOKBACK_HOURS;
1869
1870
  return normalizedHours * 60 * 60 * 1e3;
1870
1871
  };
1871
- var withDerivedHourlyRows = (rowsByInterval) => ({
1872
+ var withHourlyFallbackRows = (rowsByInterval) => ({
1872
1873
  "15m": rowsByInterval["15m"] ?? [],
1873
- "1h": (0, import_indicators2.deriveCoinalyzeHourlyRowsFrom15m)(rowsByInterval["15m"])
1874
+ "1h": (0, import_indicators2.buildCoinalyzeHourlyRowsWithFallback)({
1875
+ rows15m: rowsByInterval["15m"],
1876
+ fallbackRows1h: rowsByInterval["1h"]
1877
+ })
1874
1878
  });
1875
1879
  var getDerivativesContextReferenceSymbols = () => [
1876
1880
  ...(0, import_constants.resolveDerivativesContextReferenceSymbols)(
@@ -1994,12 +1998,17 @@ var enrichSignalWithDerivativesContext = async (params) => {
1994
1998
  const referenceSymbols = getDerivativesContextReferenceSymbols();
1995
1999
  const targetSymbol = normalizeSymbol(signal.symbol);
1996
2000
  const lookbackMs = parseLookbackMs();
2001
+ const decisionTimeMs = signal.timestamp + (0, import_data.intervalToMs)(signal.interval);
2002
+ const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
2003
+ decisionTimeMs,
2004
+ "15m"
2005
+ );
1997
2006
  const contexts = await Promise.all(
1998
2007
  referenceSymbols.map(async (symbol) => {
1999
2008
  const rowsByInterval = await (0, import_timescale3.getDerivativesWindow)({
2000
2009
  symbol,
2001
- intervals: SOURCE_INTERVALS,
2002
- endMs: signal.timestamp,
2010
+ intervals: STORED_INTERVALS,
2011
+ endMs: derivativesEndMs,
2003
2012
  lookbackMs
2004
2013
  });
2005
2014
  return [
@@ -2007,8 +2016,8 @@ var enrichSignalWithDerivativesContext = async (params) => {
2007
2016
  (0, import_indicators2.buildDerivativesContext)({
2008
2017
  symbol,
2009
2018
  direction: signal.direction,
2010
- timestamp: signal.timestamp,
2011
- rowsByInterval: withDerivedHourlyRows(rowsByInterval),
2019
+ timestamp: derivativesEndMs,
2020
+ rowsByInterval: withHourlyFallbackRows(rowsByInterval),
2012
2021
  priceChangePct1h: getSignalPriceChangePct1h(signal),
2013
2022
  intervals: CONTEXT_INTERVALS
2014
2023
  })
@@ -2026,15 +2035,15 @@ var enrichSignalWithDerivativesContext = async (params) => {
2026
2035
  const fetchedTargetContext = shouldFetchTargetContext ? await (async () => {
2027
2036
  const rowsByInterval = await (0, import_timescale3.getDerivativesWindow)({
2028
2037
  symbol: targetSymbol,
2029
- intervals: SOURCE_INTERVALS,
2030
- endMs: signal.timestamp,
2038
+ intervals: STORED_INTERVALS,
2039
+ endMs: derivativesEndMs,
2031
2040
  lookbackMs
2032
2041
  });
2033
2042
  const context = (0, import_indicators2.buildDerivativesContext)({
2034
2043
  symbol: targetSymbol,
2035
2044
  direction: signal.direction,
2036
- timestamp: signal.timestamp,
2037
- rowsByInterval: withDerivedHourlyRows(rowsByInterval),
2045
+ timestamp: derivativesEndMs,
2046
+ rowsByInterval: withHourlyFallbackRows(rowsByInterval),
2038
2047
  priceChangePct1h: getSignalPriceChangePct1h(signal),
2039
2048
  intervals: CONTEXT_INTERVALS
2040
2049
  });
@@ -2886,7 +2895,10 @@ var createTestConnector = (connector, context) => {
2886
2895
  }
2887
2896
  },
2888
2897
  placeOrder: async (order) => {
2889
- if (currentPosition) {
2898
+ const isPositionIncrease = Boolean(
2899
+ currentPosition && order.positionIntent === "increase" && currentPosition.direction === order.direction
2900
+ );
2901
+ if (currentPosition && !isPositionIncrease) {
2890
2902
  return false;
2891
2903
  }
2892
2904
  const isLong = order.direction === "LONG";
@@ -2900,9 +2912,114 @@ var createTestConnector = (connector, context) => {
2900
2912
  stage: "entry",
2901
2913
  signal: order.signal
2902
2914
  });
2903
- 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
+ }
2904
3022
  currentSignalId = typeof order.signal?.signalId === "string" && order.signal.signalId ? order.signal.signalId : null;
2905
- originalQty = order.qty;
2906
3023
  const { fee, profit } = getNetProfit({
2907
3024
  grossProfit: 0,
2908
3025
  price: entryPrice,
@@ -2969,16 +3086,21 @@ var createTestConnector = (connector, context) => {
2969
3086
  return false;
2970
3087
  }
2971
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
+ };
2972
3094
  return true;
2973
3095
  },
2974
3096
  setStopLoss: async ({ stopLossPrice: nextStopLossPrice }) => {
2975
3097
  if (!currentPosition) {
2976
3098
  return false;
2977
3099
  }
2978
- stopLossPrice = nextStopLossPrice || null;
3100
+ stopLossPrice = nextStopLossPrice ?? null;
2979
3101
  currentPosition = {
2980
3102
  ...currentPosition,
2981
- ...stopLossPrice != null ? { slPrice: stopLossPrice } : {}
3103
+ slPrice: stopLossPrice ?? void 0
2982
3104
  };
2983
3105
  return true;
2984
3106
  },
package/dist/backtest.mjs CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  enrichSignalWithBinanceMarketContext,
8
8
  enrichSignalWithCoinMarketCapContext,
9
9
  enrichSignalWithDerivativesContext
10
- } from "./chunk-2XFDY6AT.mjs";
10
+ } from "./chunk-2DZCHRP6.mjs";
11
11
  import {
12
12
  buildAiPayload
13
13
  } from "./chunk-IUZML4RK.mjs";
@@ -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
  },
@@ -747,8 +747,10 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
747
747
  // src/strategyHelpers/derivativesContext.ts
748
748
  import {
749
749
  buildDerivativesContext,
750
- deriveCoinalyzeHourlyRowsFrom15m
750
+ buildCoinalyzeHourlyRowsWithFallback,
751
+ getLastClosedDerivativesBarStartMs
751
752
  } from "@tradejs/core/indicators";
753
+ import { intervalToMs } from "@tradejs/core/data";
752
754
  import { refreshSignalBaseContextGateFeatures as refreshSignalBaseContextGateFeatures3 } from "@tradejs/core/strategies";
753
755
  import {
754
756
  DERIVATIVES_CONTEXT_BASE_REFERENCE_SYMBOLS,
@@ -756,7 +758,7 @@ import {
756
758
  } from "@tradejs/core/constants";
757
759
  import { getDerivativesWindow } from "@tradejs/infra/timescale";
758
760
  import { logger as logger3 } from "@tradejs/infra/logger";
759
- var SOURCE_INTERVALS = ["15m"];
761
+ var STORED_INTERVALS = ["15m", "1h"];
760
762
  var CONTEXT_INTERVALS = ["15m", "1h"];
761
763
  var DEFAULT_LOOKBACK_HOURS = 48;
762
764
  var PRIMARY_DERIVATIVES_REFERENCE_SYMBOL = DERIVATIVES_CONTEXT_BASE_REFERENCE_SYMBOLS[0];
@@ -782,9 +784,12 @@ var parseLookbackMs = () => {
782
784
  const normalizedHours = Number.isFinite(hours) && hours > 0 ? hours : DEFAULT_LOOKBACK_HOURS;
783
785
  return normalizedHours * 60 * 60 * 1e3;
784
786
  };
785
- var withDerivedHourlyRows = (rowsByInterval) => ({
787
+ var withHourlyFallbackRows = (rowsByInterval) => ({
786
788
  "15m": rowsByInterval["15m"] ?? [],
787
- "1h": deriveCoinalyzeHourlyRowsFrom15m(rowsByInterval["15m"])
789
+ "1h": buildCoinalyzeHourlyRowsWithFallback({
790
+ rows15m: rowsByInterval["15m"],
791
+ fallbackRows1h: rowsByInterval["1h"]
792
+ })
788
793
  });
789
794
  var getDerivativesContextReferenceSymbols = () => [
790
795
  ...resolveDerivativesContextReferenceSymbols(
@@ -908,12 +913,17 @@ var enrichSignalWithDerivativesContext = async (params) => {
908
913
  const referenceSymbols = getDerivativesContextReferenceSymbols();
909
914
  const targetSymbol = normalizeSymbol(signal.symbol);
910
915
  const lookbackMs = parseLookbackMs();
916
+ const decisionTimeMs = signal.timestamp + intervalToMs(signal.interval);
917
+ const derivativesEndMs = getLastClosedDerivativesBarStartMs(
918
+ decisionTimeMs,
919
+ "15m"
920
+ );
911
921
  const contexts = await Promise.all(
912
922
  referenceSymbols.map(async (symbol) => {
913
923
  const rowsByInterval = await getDerivativesWindow({
914
924
  symbol,
915
- intervals: SOURCE_INTERVALS,
916
- endMs: signal.timestamp,
925
+ intervals: STORED_INTERVALS,
926
+ endMs: derivativesEndMs,
917
927
  lookbackMs
918
928
  });
919
929
  return [
@@ -921,8 +931,8 @@ var enrichSignalWithDerivativesContext = async (params) => {
921
931
  buildDerivativesContext({
922
932
  symbol,
923
933
  direction: signal.direction,
924
- timestamp: signal.timestamp,
925
- rowsByInterval: withDerivedHourlyRows(rowsByInterval),
934
+ timestamp: derivativesEndMs,
935
+ rowsByInterval: withHourlyFallbackRows(rowsByInterval),
926
936
  priceChangePct1h: getSignalPriceChangePct1h(signal),
927
937
  intervals: CONTEXT_INTERVALS
928
938
  })
@@ -940,15 +950,15 @@ var enrichSignalWithDerivativesContext = async (params) => {
940
950
  const fetchedTargetContext = shouldFetchTargetContext ? await (async () => {
941
951
  const rowsByInterval = await getDerivativesWindow({
942
952
  symbol: targetSymbol,
943
- intervals: SOURCE_INTERVALS,
944
- endMs: signal.timestamp,
953
+ intervals: STORED_INTERVALS,
954
+ endMs: derivativesEndMs,
945
955
  lookbackMs
946
956
  });
947
957
  const context = buildDerivativesContext({
948
958
  symbol: targetSymbol,
949
959
  direction: signal.direction,
950
- timestamp: signal.timestamp,
951
- rowsByInterval: withDerivedHourlyRows(rowsByInterval),
960
+ timestamp: derivativesEndMs,
961
+ rowsByInterval: withHourlyFallbackRows(rowsByInterval),
952
962
  priceChangePct1h: getSignalPriceChangePct1h(signal),
953
963
  intervals: CONTEXT_INTERVALS
954
964
  });
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;
@@ -7196,7 +7196,7 @@ var askAI = async (signal, options = {}) => {
7196
7196
  // src/strategyRuntime.ts
7197
7197
  var import_node_path2 = __toESM(require("path"));
7198
7198
  var import_constants4 = require("@tradejs/core/constants");
7199
- var import_data = require("@tradejs/core/data");
7199
+ var import_data2 = require("@tradejs/core/data");
7200
7200
  var import_strategies4 = require("@tradejs/core/strategies");
7201
7201
  var import_logger8 = require("@tradejs/infra/logger");
7202
7202
 
@@ -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) {
@@ -7424,11 +7495,12 @@ var markRuntimeTradeClosed = async (params) => {
7424
7495
 
7425
7496
  // src/strategyHelpers/derivativesContext.ts
7426
7497
  var import_indicators2 = require("@tradejs/core/indicators");
7498
+ var import_data = require("@tradejs/core/data");
7427
7499
  var import_strategies = require("@tradejs/core/strategies");
7428
7500
  var import_constants2 = require("@tradejs/core/constants");
7429
7501
  var import_timescale = require("@tradejs/infra/timescale");
7430
7502
  var import_logger4 = require("@tradejs/infra/logger");
7431
- var SOURCE_INTERVALS = ["15m"];
7503
+ var STORED_INTERVALS = ["15m", "1h"];
7432
7504
  var CONTEXT_INTERVALS = ["15m", "1h"];
7433
7505
  var DEFAULT_LOOKBACK_HOURS = 48;
7434
7506
  var PRIMARY_DERIVATIVES_REFERENCE_SYMBOL = import_constants2.DERIVATIVES_CONTEXT_BASE_REFERENCE_SYMBOLS[0];
@@ -7454,9 +7526,12 @@ var parseLookbackMs = () => {
7454
7526
  const normalizedHours = Number.isFinite(hours) && hours > 0 ? hours : DEFAULT_LOOKBACK_HOURS;
7455
7527
  return normalizedHours * 60 * 60 * 1e3;
7456
7528
  };
7457
- var withDerivedHourlyRows = (rowsByInterval) => ({
7529
+ var withHourlyFallbackRows = (rowsByInterval) => ({
7458
7530
  "15m": rowsByInterval["15m"] ?? [],
7459
- "1h": (0, import_indicators2.deriveCoinalyzeHourlyRowsFrom15m)(rowsByInterval["15m"])
7531
+ "1h": (0, import_indicators2.buildCoinalyzeHourlyRowsWithFallback)({
7532
+ rows15m: rowsByInterval["15m"],
7533
+ fallbackRows1h: rowsByInterval["1h"]
7534
+ })
7460
7535
  });
7461
7536
  var getDerivativesContextReferenceSymbols = () => [
7462
7537
  ...(0, import_constants2.resolveDerivativesContextReferenceSymbols)(
@@ -7580,12 +7655,17 @@ var enrichSignalWithDerivativesContext = async (params) => {
7580
7655
  const referenceSymbols = getDerivativesContextReferenceSymbols();
7581
7656
  const targetSymbol = normalizeSymbol(signal.symbol);
7582
7657
  const lookbackMs = parseLookbackMs();
7658
+ const decisionTimeMs = signal.timestamp + (0, import_data.intervalToMs)(signal.interval);
7659
+ const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
7660
+ decisionTimeMs,
7661
+ "15m"
7662
+ );
7583
7663
  const contexts = await Promise.all(
7584
7664
  referenceSymbols.map(async (symbol) => {
7585
7665
  const rowsByInterval = await (0, import_timescale.getDerivativesWindow)({
7586
7666
  symbol,
7587
- intervals: SOURCE_INTERVALS,
7588
- endMs: signal.timestamp,
7667
+ intervals: STORED_INTERVALS,
7668
+ endMs: derivativesEndMs,
7589
7669
  lookbackMs
7590
7670
  });
7591
7671
  return [
@@ -7593,8 +7673,8 @@ var enrichSignalWithDerivativesContext = async (params) => {
7593
7673
  (0, import_indicators2.buildDerivativesContext)({
7594
7674
  symbol,
7595
7675
  direction: signal.direction,
7596
- timestamp: signal.timestamp,
7597
- rowsByInterval: withDerivedHourlyRows(rowsByInterval),
7676
+ timestamp: derivativesEndMs,
7677
+ rowsByInterval: withHourlyFallbackRows(rowsByInterval),
7598
7678
  priceChangePct1h: getSignalPriceChangePct1h(signal),
7599
7679
  intervals: CONTEXT_INTERVALS
7600
7680
  })
@@ -7612,15 +7692,15 @@ var enrichSignalWithDerivativesContext = async (params) => {
7612
7692
  const fetchedTargetContext = shouldFetchTargetContext ? await (async () => {
7613
7693
  const rowsByInterval = await (0, import_timescale.getDerivativesWindow)({
7614
7694
  symbol: targetSymbol,
7615
- intervals: SOURCE_INTERVALS,
7616
- endMs: signal.timestamp,
7695
+ intervals: STORED_INTERVALS,
7696
+ endMs: derivativesEndMs,
7617
7697
  lookbackMs
7618
7698
  });
7619
7699
  const context = (0, import_indicators2.buildDerivativesContext)({
7620
7700
  symbol: targetSymbol,
7621
7701
  direction: signal.direction,
7622
- timestamp: signal.timestamp,
7623
- rowsByInterval: withDerivedHourlyRows(rowsByInterval),
7702
+ timestamp: derivativesEndMs,
7703
+ rowsByInterval: withHourlyFallbackRows(rowsByInterval),
7624
7704
  priceChangePct1h: getSignalPriceChangePct1h(signal),
7625
7705
  intervals: CONTEXT_INTERVALS
7626
7706
  });
@@ -8624,12 +8704,14 @@ var executeEntryOrder = async ({
8624
8704
  timestamp,
8625
8705
  takeProfits,
8626
8706
  stopLossPrice,
8707
+ positionIntent = "open",
8627
8708
  signal,
8628
8709
  beforePlaceOrder,
8629
8710
  recordRuntimeTrade = true,
8630
8711
  leverage
8631
8712
  }) => {
8632
8713
  await beforePlaceOrder?.();
8714
+ const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
8633
8715
  const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
8634
8716
  const signalTimestamp = signal.timestamp;
8635
8717
  const signalClosePrice = currentPrice;
@@ -8644,6 +8726,7 @@ var executeEntryOrder = async ({
8644
8726
  qty,
8645
8727
  price: currentPrice,
8646
8728
  isLimit: false,
8729
+ positionIntent,
8647
8730
  timestamp,
8648
8731
  direction,
8649
8732
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
@@ -8654,27 +8737,36 @@ var executeEntryOrder = async ({
8654
8737
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
8655
8738
  const currentPosition = await connector.getPosition(symbol);
8656
8739
  const fillTime = Date.now();
8657
- const entryPrice = currentPosition?.price && Number.isFinite(currentPosition.price) ? currentPosition.price : currentPrice;
8658
- const fillSource = currentPosition?.price && Number.isFinite(currentPosition.price) ? "exchange_position" : orderPlaced ? "requested_price" : "unknown";
8659
- const entryQty = currentPosition?.qty && Number.isFinite(currentPosition.qty) ? currentPosition.qty : placedQty;
8660
- const estimatedOpenFee = entryPrice * entryQty * import_constants3.FEE_PERCENT;
8661
- signal.prices.currentPrice = entryPrice;
8662
- signal.orderQty = entryQty;
8663
- 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;
8664
8756
  if (orderPlaced) {
8665
8757
  try {
8666
8758
  await applyProtectiveOrders({
8667
8759
  connector,
8668
8760
  symbol,
8669
8761
  direction,
8670
- qty: entryQty,
8762
+ qty: resultingQty,
8671
8763
  takeProfits,
8672
8764
  stopLossPrice
8673
8765
  });
8674
8766
  } catch (error) {
8675
8767
  await connector.closePosition({
8676
8768
  symbol,
8677
- price: entryPrice,
8769
+ price: resultingEntryPrice,
8678
8770
  timestamp,
8679
8771
  direction,
8680
8772
  signal
@@ -8687,7 +8779,7 @@ var executeEntryOrder = async ({
8687
8779
  if (orderPlaced) {
8688
8780
  signal.orderFailureReason = void 0;
8689
8781
  }
8690
- if (orderPlaced && recordRuntimeTrade) {
8782
+ if (orderPlaced && recordRuntimeTrade && !isPositionIncrease) {
8691
8783
  await recordRuntimeTradeOpen({
8692
8784
  userName,
8693
8785
  orderId,
@@ -8696,8 +8788,8 @@ var executeEntryOrder = async ({
8696
8788
  symbol,
8697
8789
  interval: signal.interval,
8698
8790
  direction,
8699
- qty: entryQty,
8700
- entryPrice,
8791
+ qty: resultingQty,
8792
+ entryPrice: resultingEntryPrice,
8701
8793
  signalTimestamp,
8702
8794
  signalClosePrice,
8703
8795
  arrivalSnapshotTime: arrivalSnapshot.arrivalSnapshotTime,
@@ -8708,7 +8800,7 @@ var executeEntryOrder = async ({
8708
8800
  spreadBps: arrivalSnapshot.spreadBps,
8709
8801
  orderSubmitTime,
8710
8802
  orderAckTime,
8711
- fillAvgPrice: entryPrice,
8803
+ fillAvgPrice: fillPrice,
8712
8804
  fillSource,
8713
8805
  fillTime,
8714
8806
  telemetryQuality: resolveRuntimeTelemetryQuality({
@@ -8716,7 +8808,7 @@ var executeEntryOrder = async ({
8716
8808
  arrivalMid: arrivalSnapshot.arrivalMid,
8717
8809
  orderSubmitTime,
8718
8810
  orderAckTime,
8719
- fillAvgPrice: entryPrice,
8811
+ fillAvgPrice: fillPrice,
8720
8812
  fillTime
8721
8813
  }),
8722
8814
  fee: estimatedOpenFee,
@@ -8732,10 +8824,26 @@ var executeEntryOrder = async ({
8732
8824
  ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {}
8733
8825
  });
8734
8826
  }
8735
- 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) {
8736
8844
  return currentPosition.price;
8737
8845
  }
8738
- return entryPrice;
8846
+ return resultingEntryPrice;
8739
8847
  };
8740
8848
  var updatePositionProtection = async ({
8741
8849
  connector,
@@ -8983,7 +9091,7 @@ var resolveBacktestExecutionDelayMs = (value, fallbackDelayMs) => {
8983
9091
  };
8984
9092
  var safeIntervalToMs = (interval) => {
8985
9093
  try {
8986
- return (0, import_data.intervalToMs)(interval);
9094
+ return (0, import_data2.intervalToMs)(interval);
8987
9095
  } catch {
8988
9096
  return null;
8989
9097
  }
@@ -9508,6 +9616,7 @@ var executeEntryDecision = async ({
9508
9616
  timestamp: decision.entryContext.timestamp,
9509
9617
  takeProfits: decision.orderPlan.takeProfits,
9510
9618
  stopLossPrice: decision.orderPlan.stopLossPrice,
9619
+ positionIntent: decision.orderPlan.positionIntent,
9511
9620
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
9512
9621
  signal,
9513
9622
  beforePlaceOrder,
@@ -9539,6 +9648,7 @@ var executeEntryDecision = async ({
9539
9648
  price: decision.entryContext.prices.currentPrice,
9540
9649
  timestamp: decision.entryContext.timestamp,
9541
9650
  direction: decision.entryContext.direction,
9651
+ positionIntent: decision.orderPlan.positionIntent,
9542
9652
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
9543
9653
  });
9544
9654
  if (!orderPlaced) {
@@ -3,7 +3,7 @@ import {
3
3
  enrichSignalWithBinanceMarketContext,
4
4
  enrichSignalWithCoinMarketCapContext,
5
5
  enrichSignalWithDerivativesContext
6
- } from "./chunk-2XFDY6AT.mjs";
6
+ } from "./chunk-2DZCHRP6.mjs";
7
7
  import {
8
8
  require_lodash
9
9
  } from "./chunk-KZDHZ56N.mjs";
@@ -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.1",
3
+ "version": "2.0.3",
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.1",
71
- "@tradejs/infra": "^2.0.1",
72
- "@tradejs/types": "^2.0.1",
70
+ "@tradejs/core": "^2.0.3",
71
+ "@tradejs/infra": "^2.0.3",
72
+ "@tradejs/types": "^2.0.3",
73
73
  "chalk": "4.1.2",
74
74
  "ioredis": "5.8.0",
75
75
  "pinets": "0.8.12",