@tradejs/core 3.1.27 → 3.1.28-beta.252

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.
@@ -1,4 +1,4 @@
1
- import { PositionLogData, TestStat, TestThresholdsKey, ThresholdLevel, TestWorkerResult } from '@tradejs/types';
1
+ import { ExecutionCostModel, BacktestExecutionCosts, PositionLogData, TestStat, TestThresholdsKey, ThresholdLevel, TestWorkerResult } from '@tradejs/types';
2
2
  export { c as compactOrderLog, g as getTimeline } from './time-BQ3AXmxo.mjs';
3
3
 
4
4
  declare const parseTestName: (testName: string) => {
@@ -7,6 +7,13 @@ declare const parseTestName: (testName: string) => {
7
7
  testId: string;
8
8
  };
9
9
 
10
+ declare const LEGACY_EXECUTION_CONFIG_FIELDS: readonly ["MAKER_FEE_RATE", "TAKER_FEE_RATE", "FUNDING_ENABLED", "SLIPPAGE_BASE_BPS", "SLIPPAGE_SPREAD_MULTIPLIER", "SLIPPAGE_MARKET_IMPACT_BPS", "SLIPPAGE_DELAY_RISK_MULTIPLIER", "EXECUTION_COSTS_CACHE_ONLY"];
11
+ declare const assertStrategyExecutionIsolation: (config: Record<string, unknown>) => void;
12
+ /** Validate JSON at ingestion, including explicit zero rates. Never coerce null. */
13
+ declare const parseBacktestExecutionCosts: (input: unknown) => BacktestExecutionCosts;
14
+ /** Stable economic fields only: timestamps and provenance are separate evidence. */
15
+ declare const executionCostsFromModel: (model: ExecutionCostModel) => BacktestExecutionCosts;
16
+
10
17
  /**
11
18
  * Максимальная просадка (Max Drawdown) в процентах от бегающего пика.
12
19
  * Формула по точкам amount_t:
@@ -142,4 +149,4 @@ declare const getFormatted: (stat: Partial<TestStat> | undefined, key: TestThres
142
149
  level: ThresholdLevel;
143
150
  };
144
151
 
145
- export { type AdvancedExitBreakdownBucket, type AdvancedQuarterlyPnl, type AdvancedTradeDirection, type AdvancedTradeInput, type AdvancedTradeMetrics, type AdvancedTradeMetricsInput, calculateAdvancedTradeMetrics, calculateMaxDrawdown, calculateStatsFull, classifyMetric, getBacktestScore, getFormatted, parseTestName, sortBestTests };
152
+ export { type AdvancedExitBreakdownBucket, type AdvancedQuarterlyPnl, type AdvancedTradeDirection, type AdvancedTradeInput, type AdvancedTradeMetrics, type AdvancedTradeMetricsInput, LEGACY_EXECUTION_CONFIG_FIELDS, assertStrategyExecutionIsolation, calculateAdvancedTradeMetrics, calculateMaxDrawdown, calculateStatsFull, classifyMetric, executionCostsFromModel, getBacktestScore, getFormatted, parseBacktestExecutionCosts, parseTestName, sortBestTests };
@@ -1,4 +1,4 @@
1
- import { PositionLogData, TestStat, TestThresholdsKey, ThresholdLevel, TestWorkerResult } from '@tradejs/types';
1
+ import { ExecutionCostModel, BacktestExecutionCosts, PositionLogData, TestStat, TestThresholdsKey, ThresholdLevel, TestWorkerResult } from '@tradejs/types';
2
2
  export { c as compactOrderLog, g as getTimeline } from './time-BQ3AXmxo.js';
3
3
 
4
4
  declare const parseTestName: (testName: string) => {
@@ -7,6 +7,13 @@ declare const parseTestName: (testName: string) => {
7
7
  testId: string;
8
8
  };
9
9
 
10
+ declare const LEGACY_EXECUTION_CONFIG_FIELDS: readonly ["MAKER_FEE_RATE", "TAKER_FEE_RATE", "FUNDING_ENABLED", "SLIPPAGE_BASE_BPS", "SLIPPAGE_SPREAD_MULTIPLIER", "SLIPPAGE_MARKET_IMPACT_BPS", "SLIPPAGE_DELAY_RISK_MULTIPLIER", "EXECUTION_COSTS_CACHE_ONLY"];
11
+ declare const assertStrategyExecutionIsolation: (config: Record<string, unknown>) => void;
12
+ /** Validate JSON at ingestion, including explicit zero rates. Never coerce null. */
13
+ declare const parseBacktestExecutionCosts: (input: unknown) => BacktestExecutionCosts;
14
+ /** Stable economic fields only: timestamps and provenance are separate evidence. */
15
+ declare const executionCostsFromModel: (model: ExecutionCostModel) => BacktestExecutionCosts;
16
+
10
17
  /**
11
18
  * Максимальная просадка (Max Drawdown) в процентах от бегающего пика.
12
19
  * Формула по точкам amount_t:
@@ -142,4 +149,4 @@ declare const getFormatted: (stat: Partial<TestStat> | undefined, key: TestThres
142
149
  level: ThresholdLevel;
143
150
  };
144
151
 
145
- export { type AdvancedExitBreakdownBucket, type AdvancedQuarterlyPnl, type AdvancedTradeDirection, type AdvancedTradeInput, type AdvancedTradeMetrics, type AdvancedTradeMetricsInput, calculateAdvancedTradeMetrics, calculateMaxDrawdown, calculateStatsFull, classifyMetric, getBacktestScore, getFormatted, parseTestName, sortBestTests };
152
+ export { type AdvancedExitBreakdownBucket, type AdvancedQuarterlyPnl, type AdvancedTradeDirection, type AdvancedTradeInput, type AdvancedTradeMetrics, type AdvancedTradeMetricsInput, LEGACY_EXECUTION_CONFIG_FIELDS, assertStrategyExecutionIsolation, calculateAdvancedTradeMetrics, calculateMaxDrawdown, calculateStatsFull, classifyMetric, executionCostsFromModel, getBacktestScore, getFormatted, parseBacktestExecutionCosts, parseTestName, sortBestTests };
package/dist/backtest.js CHANGED
@@ -30,14 +30,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/backtest.ts
31
31
  var backtest_exports = {};
32
32
  __export(backtest_exports, {
33
+ LEGACY_EXECUTION_CONFIG_FIELDS: () => LEGACY_EXECUTION_CONFIG_FIELDS,
34
+ assertStrategyExecutionIsolation: () => assertStrategyExecutionIsolation,
33
35
  calculateAdvancedTradeMetrics: () => calculateAdvancedTradeMetrics,
34
36
  calculateMaxDrawdown: () => calculateMaxDrawdown,
35
37
  calculateStatsFull: () => calculateStatsFull,
36
38
  classifyMetric: () => classifyMetric,
37
39
  compactOrderLog: () => compactOrderLog,
40
+ executionCostsFromModel: () => executionCostsFromModel,
38
41
  getBacktestScore: () => getBacktestScore,
39
42
  getFormatted: () => getFormatted,
40
43
  getTimeline: () => getTimeline,
44
+ parseBacktestExecutionCosts: () => parseBacktestExecutionCosts,
41
45
  parseTestName: () => parseTestName,
42
46
  sortBestTests: () => sortBestTests
43
47
  });
@@ -49,6 +53,90 @@ var parseTestName = (testName) => {
49
53
  return { symbol, testSuiteId, testId };
50
54
  };
51
55
 
56
+ // src/utils/backtestExecutionCosts.ts
57
+ var LEGACY_EXECUTION_CONFIG_FIELDS = [
58
+ "MAKER_FEE_RATE",
59
+ "TAKER_FEE_RATE",
60
+ "FUNDING_ENABLED",
61
+ "SLIPPAGE_BASE_BPS",
62
+ "SLIPPAGE_SPREAD_MULTIPLIER",
63
+ "SLIPPAGE_MARKET_IMPACT_BPS",
64
+ "SLIPPAGE_DELAY_RISK_MULTIPLIER",
65
+ "EXECUTION_COSTS_CACHE_ONLY"
66
+ ];
67
+ var assertStrategyExecutionIsolation = (config) => {
68
+ for (const key of LEGACY_EXECUTION_CONFIG_FIELDS) {
69
+ if (Object.prototype.hasOwnProperty.call(config, key)) {
70
+ throw new Error(
71
+ `${key} is an execution setting: use executionCosts separately; strategy estimates use RISK_FEE_RATE and RISK_SLIPPAGE_BPS`
72
+ );
73
+ }
74
+ }
75
+ };
76
+ var parseBacktestExecutionCosts = (input) => {
77
+ const record = (value, label) => {
78
+ if (!value || typeof value !== "object" || Array.isArray(value))
79
+ throw new Error(`${label} must be an object`);
80
+ return value;
81
+ };
82
+ const root = record(input, "executionCosts");
83
+ const fees = record(root.fees, "executionCosts.fees");
84
+ const slippage = record(root.slippage, "executionCosts.slippage");
85
+ const funding = record(root.funding, "executionCosts.funding");
86
+ const number = (value, label, min = 0, max = Infinity) => {
87
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value >= max)
88
+ throw new Error(`${label} must be a finite number in [${min}, ${max})`);
89
+ return value;
90
+ };
91
+ if (typeof funding.enabled !== "boolean")
92
+ throw new Error("executionCosts.funding.enabled must be boolean");
93
+ for (const [value, keys, label] of [
94
+ [root, ["fees", "slippage", "funding"], "executionCosts"],
95
+ [fees, ["makerRate", "takerRate"], "fees"],
96
+ [
97
+ slippage,
98
+ ["baseBps", "spreadMultiplier", "marketImpactBps", "delayRiskMultiplier"],
99
+ "slippage"
100
+ ],
101
+ [funding, ["enabled"], "funding"]
102
+ ]) {
103
+ for (const key of Object.keys(value))
104
+ if (!keys.includes(key))
105
+ throw new Error(`Unknown ${label}.${key}`);
106
+ }
107
+ return {
108
+ fees: {
109
+ makerRate: number(fees.makerRate, "makerRate", -1, 1),
110
+ takerRate: number(fees.takerRate, "takerRate", -1, 1)
111
+ },
112
+ slippage: {
113
+ baseBps: number(slippage.baseBps, "baseBps", 0, 1e4),
114
+ spreadMultiplier: number(slippage.spreadMultiplier, "spreadMultiplier"),
115
+ marketImpactBps: number(
116
+ slippage.marketImpactBps,
117
+ "marketImpactBps",
118
+ 0,
119
+ 1e4
120
+ ),
121
+ delayRiskMultiplier: number(
122
+ slippage.delayRiskMultiplier,
123
+ "delayRiskMultiplier"
124
+ )
125
+ },
126
+ funding: { enabled: funding.enabled }
127
+ };
128
+ };
129
+ var executionCostsFromModel = (model) => parseBacktestExecutionCosts({
130
+ fees: { makerRate: model.fees.makerRate, takerRate: model.fees.takerRate },
131
+ slippage: {
132
+ baseBps: model.slippage.baseBps,
133
+ spreadMultiplier: model.slippage.spreadMultiplier,
134
+ marketImpactBps: model.slippage.marketImpactBps,
135
+ delayRiskMultiplier: model.slippage.delayRiskMultiplier
136
+ },
137
+ funding: { enabled: model.funding.enabled }
138
+ });
139
+
52
140
  // src/utils/stat.ts
53
141
  var import_date_fns = require("date-fns");
54
142
 
@@ -904,14 +992,18 @@ var compactOrderLog = (timeline, orderLog) => {
904
992
  };
905
993
  // Annotate the CommonJS export names for ESM import in node:
906
994
  0 && (module.exports = {
995
+ LEGACY_EXECUTION_CONFIG_FIELDS,
996
+ assertStrategyExecutionIsolation,
907
997
  calculateAdvancedTradeMetrics,
908
998
  calculateMaxDrawdown,
909
999
  calculateStatsFull,
910
1000
  classifyMetric,
911
1001
  compactOrderLog,
1002
+ executionCostsFromModel,
912
1003
  getBacktestScore,
913
1004
  getFormatted,
914
1005
  getTimeline,
1006
+ parseBacktestExecutionCosts,
915
1007
  parseTestName,
916
1008
  sortBestTests
917
1009
  });
package/dist/backtest.mjs CHANGED
@@ -20,6 +20,90 @@ var parseTestName = (testName) => {
20
20
  return { symbol, testSuiteId, testId };
21
21
  };
22
22
 
23
+ // src/utils/backtestExecutionCosts.ts
24
+ var LEGACY_EXECUTION_CONFIG_FIELDS = [
25
+ "MAKER_FEE_RATE",
26
+ "TAKER_FEE_RATE",
27
+ "FUNDING_ENABLED",
28
+ "SLIPPAGE_BASE_BPS",
29
+ "SLIPPAGE_SPREAD_MULTIPLIER",
30
+ "SLIPPAGE_MARKET_IMPACT_BPS",
31
+ "SLIPPAGE_DELAY_RISK_MULTIPLIER",
32
+ "EXECUTION_COSTS_CACHE_ONLY"
33
+ ];
34
+ var assertStrategyExecutionIsolation = (config) => {
35
+ for (const key of LEGACY_EXECUTION_CONFIG_FIELDS) {
36
+ if (Object.prototype.hasOwnProperty.call(config, key)) {
37
+ throw new Error(
38
+ `${key} is an execution setting: use executionCosts separately; strategy estimates use RISK_FEE_RATE and RISK_SLIPPAGE_BPS`
39
+ );
40
+ }
41
+ }
42
+ };
43
+ var parseBacktestExecutionCosts = (input) => {
44
+ const record = (value, label) => {
45
+ if (!value || typeof value !== "object" || Array.isArray(value))
46
+ throw new Error(`${label} must be an object`);
47
+ return value;
48
+ };
49
+ const root = record(input, "executionCosts");
50
+ const fees = record(root.fees, "executionCosts.fees");
51
+ const slippage = record(root.slippage, "executionCosts.slippage");
52
+ const funding = record(root.funding, "executionCosts.funding");
53
+ const number = (value, label, min = 0, max = Infinity) => {
54
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value >= max)
55
+ throw new Error(`${label} must be a finite number in [${min}, ${max})`);
56
+ return value;
57
+ };
58
+ if (typeof funding.enabled !== "boolean")
59
+ throw new Error("executionCosts.funding.enabled must be boolean");
60
+ for (const [value, keys, label] of [
61
+ [root, ["fees", "slippage", "funding"], "executionCosts"],
62
+ [fees, ["makerRate", "takerRate"], "fees"],
63
+ [
64
+ slippage,
65
+ ["baseBps", "spreadMultiplier", "marketImpactBps", "delayRiskMultiplier"],
66
+ "slippage"
67
+ ],
68
+ [funding, ["enabled"], "funding"]
69
+ ]) {
70
+ for (const key of Object.keys(value))
71
+ if (!keys.includes(key))
72
+ throw new Error(`Unknown ${label}.${key}`);
73
+ }
74
+ return {
75
+ fees: {
76
+ makerRate: number(fees.makerRate, "makerRate", -1, 1),
77
+ takerRate: number(fees.takerRate, "takerRate", -1, 1)
78
+ },
79
+ slippage: {
80
+ baseBps: number(slippage.baseBps, "baseBps", 0, 1e4),
81
+ spreadMultiplier: number(slippage.spreadMultiplier, "spreadMultiplier"),
82
+ marketImpactBps: number(
83
+ slippage.marketImpactBps,
84
+ "marketImpactBps",
85
+ 0,
86
+ 1e4
87
+ ),
88
+ delayRiskMultiplier: number(
89
+ slippage.delayRiskMultiplier,
90
+ "delayRiskMultiplier"
91
+ )
92
+ },
93
+ funding: { enabled: funding.enabled }
94
+ };
95
+ };
96
+ var executionCostsFromModel = (model) => parseBacktestExecutionCosts({
97
+ fees: { makerRate: model.fees.makerRate, takerRate: model.fees.takerRate },
98
+ slippage: {
99
+ baseBps: model.slippage.baseBps,
100
+ spreadMultiplier: model.slippage.spreadMultiplier,
101
+ marketImpactBps: model.slippage.marketImpactBps,
102
+ delayRiskMultiplier: model.slippage.delayRiskMultiplier
103
+ },
104
+ funding: { enabled: model.funding.enabled }
105
+ });
106
+
23
107
  // src/utils/stat.ts
24
108
  import {
25
109
  startOfMonth,
@@ -674,14 +758,18 @@ var getFormatted = (stat, key) => {
674
758
  };
675
759
  };
676
760
  export {
761
+ LEGACY_EXECUTION_CONFIG_FIELDS,
762
+ assertStrategyExecutionIsolation,
677
763
  calculateAdvancedTradeMetrics,
678
764
  calculateMaxDrawdown,
679
765
  calculateStatsFull,
680
766
  classifyMetric,
681
767
  compactOrderLog,
768
+ executionCostsFromModel,
682
769
  getBacktestScore,
683
770
  getFormatted,
684
771
  getTimeline,
772
+ parseBacktestExecutionCosts,
685
773
  parseTestName,
686
774
  sortBestTests
687
775
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  cloneArrayValues
3
- } from "./chunk-M7QGVZ3J.mjs";
3
+ } from "./chunk-WJQRRGUY.mjs";
4
4
  import {
5
5
  toMs
6
6
  } from "./chunk-S4KHOAXM.mjs";
@@ -977,14 +977,26 @@ var PSYCHOLOGICAL_LEVEL_WINDOWS = {
977
977
  h1: 60 * 6e4,
978
978
  h4: 4 * 60 * 6e4
979
979
  };
980
+ var PSYCHOLOGICAL_LEVEL_WINDOW_ENTRIES = Object.entries(
981
+ PSYCHOLOGICAL_LEVEL_WINDOWS
982
+ );
980
983
  var buildSessionContext = (timestamp) => {
981
984
  const date = new Date(timestamp);
982
985
  const minuteUtc = date.getUTCHours() * 60 + date.getUTCMinutes();
983
986
  const dayOfWeekUtc = date.getUTCDay() || 7;
984
- const activeSessions = SESSION_WINDOWS.filter(
985
- ({ startMinuteUtc, endMinuteUtc }) => startMinuteUtc <= endMinuteUtc ? minuteUtc >= startMinuteUtc && minuteUtc < endMinuteUtc : minuteUtc >= startMinuteUtc || minuteUtc < endMinuteUtc
986
- ).map(({ name }) => name);
987
- const sessionPhase = activeSessions.includes("us") ? "us" : activeSessions.includes("europe") ? "europe" : activeSessions.includes("asia") ? "asia" : "off_hours";
987
+ let activeSessionCount = 0;
988
+ let asiaActive = false;
989
+ let europeActive = false;
990
+ let usActive = false;
991
+ for (const { name, startMinuteUtc, endMinuteUtc } of SESSION_WINDOWS) {
992
+ const active = startMinuteUtc <= endMinuteUtc ? minuteUtc >= startMinuteUtc && minuteUtc < endMinuteUtc : minuteUtc >= startMinuteUtc || minuteUtc < endMinuteUtc;
993
+ if (!active) continue;
994
+ activeSessionCount += 1;
995
+ if (name === "asia") asiaActive = true;
996
+ if (name === "europe") europeActive = true;
997
+ if (name === "us") usActive = true;
998
+ }
999
+ const sessionPhase = usActive ? "us" : europeActive ? "europe" : asiaActive ? "asia" : "off_hours";
988
1000
  const primaryWindow = SESSION_WINDOWS.find(
989
1001
  ({ name }) => name === sessionPhase
990
1002
  );
@@ -995,7 +1007,7 @@ var buildSessionContext = (timestamp) => {
995
1007
  return {
996
1008
  sessionPhase,
997
1009
  sessionWindowPhase,
998
- isOverlap: activeSessions.length > 1,
1010
+ isOverlap: activeSessionCount > 1,
999
1011
  minutesFromSessionOpen,
1000
1012
  minutesToSessionClose,
1001
1013
  minutesToFundingWindow,
@@ -1049,24 +1061,30 @@ var buildPsychologicalLevelWindow = (startPrice, endPrice, stepUsd) => {
1049
1061
  var buildPsychologicalLevelAssetContext = (candles, stepUsd) => {
1050
1062
  const endCandle = candles[candles.length - 1];
1051
1063
  if (!endCandle) return null;
1052
- const candlesByTimestamp = new Map(
1053
- candles.map((item) => [item.timestamp, item])
1054
- );
1055
- const windows = Object.fromEntries(
1056
- Object.entries(PSYCHOLOGICAL_LEVEL_WINDOWS).map(([window, durationMs]) => {
1057
- const startCandle = candlesByTimestamp.get(
1058
- endCandle.timestamp - durationMs
1059
- );
1060
- return [
1061
- window,
1062
- startCandle ? buildPsychologicalLevelWindow(
1063
- startCandle.close,
1064
- endCandle.close,
1065
- stepUsd
1066
- ) : unavailablePsychologicalLevelWindow()
1067
- ];
1068
- })
1069
- );
1064
+ const windows = {};
1065
+ for (const [window, durationMs] of PSYCHOLOGICAL_LEVEL_WINDOW_ENTRIES) {
1066
+ const targetTimestamp = endCandle.timestamp - durationMs;
1067
+ let low = 0;
1068
+ let high = candles.length - 1;
1069
+ let startCandle;
1070
+ while (low <= high) {
1071
+ const middle = low + high >>> 1;
1072
+ const candidate = candles[middle];
1073
+ if (candidate.timestamp <= targetTimestamp) {
1074
+ if (candidate.timestamp === targetTimestamp) {
1075
+ startCandle = candidate;
1076
+ }
1077
+ low = middle + 1;
1078
+ } else {
1079
+ high = middle - 1;
1080
+ }
1081
+ }
1082
+ windows[window] = startCandle ? buildPsychologicalLevelWindow(
1083
+ startCandle.close,
1084
+ endCandle.close,
1085
+ stepUsd
1086
+ ) : unavailablePsychologicalLevelWindow();
1087
+ }
1070
1088
  return { source: "aligned_15m_ohlcv", stepUsd, windows };
1071
1089
  };
1072
1090
  var returnPct = (candles) => {
@@ -7957,35 +7975,34 @@ var buildLiquidityZonesContext = (candles, price, previousPrice, atr2) => {
7957
7975
  }
7958
7976
  const lastIndex = candles.length - 1;
7959
7977
  const current = candles[lastIndex];
7960
- const activeZones = zones.filter((zone) => {
7978
+ let activeCount = 0;
7979
+ let nearestSupport = null;
7980
+ let nearestResistance = null;
7981
+ for (const zone of zones) {
7961
7982
  if (lastIndex - zone.startIndex > LIQUIDITY_ZONE_MAX_AGE) {
7962
- return false;
7983
+ continue;
7963
7984
  }
7964
7985
  const crossed = zone.crossed || (zone.kind === "swing_high_liquidity" ? current.close > zone.top : current.close < zone.bottom);
7965
7986
  zone.crossed = crossed;
7966
- return !crossed;
7967
- });
7968
- const supports = activeZones.filter(
7969
- (zone) => zone.kind === "swing_low_liquidity"
7970
- );
7971
- const resistances = activeZones.filter(
7972
- (zone) => zone.kind === "swing_high_liquidity"
7973
- );
7974
- const nearestSupport = supports.reduce(
7975
- (nearest, zone) => nearest == null || Math.abs(price - zone.level) < Math.abs(price - nearest.level) ? zone : nearest,
7976
- null
7977
- ) ?? null;
7978
- const nearestResistance = resistances.reduce(
7979
- (nearest, zone) => nearest == null || Math.abs(zone.level - price) < Math.abs(nearest.level - price) ? zone : nearest,
7980
- null
7981
- ) ?? null;
7987
+ if (crossed) {
7988
+ continue;
7989
+ }
7990
+ activeCount += 1;
7991
+ if (zone.kind === "swing_low_liquidity") {
7992
+ if (nearestSupport == null || Math.abs(price - zone.level) < Math.abs(price - nearestSupport.level)) {
7993
+ nearestSupport = zone;
7994
+ }
7995
+ } else if (nearestResistance == null || Math.abs(zone.level - price) < Math.abs(nearestResistance.level - price)) {
7996
+ nearestResistance = zone;
7997
+ }
7998
+ }
7982
7999
  const supportRetest = nearestSupport != null && current.low <= nearestSupport.top;
7983
8000
  const resistanceRetest = nearestResistance != null && current.high >= nearestResistance.bottom;
7984
8001
  const retestZone = supportRetest ? nearestSupport : resistanceRetest ? nearestResistance : null;
7985
8002
  const retestPenetration = retestZone == null ? null : retestZone.kind === "swing_low_liquidity" ? Math.max(0, retestZone.top - current.low) : Math.max(0, current.high - retestZone.bottom);
7986
8003
  const retestHeight = retestZone == null ? null : Math.max(retestZone.top - retestZone.bottom, 0);
7987
8004
  return {
7988
- activeCount: activeZones.length,
8005
+ activeCount,
7989
8006
  nearestSupport: {
7990
8007
  top: nearestSupport?.top ?? null,
7991
8008
  bottom: nearestSupport?.bottom ?? null,
@@ -8094,19 +8111,22 @@ var buildLiquidityTailsContext = (candles, price, atr2) => {
8094
8111
  }
8095
8112
  const lastIndex = candles.length - 1;
8096
8113
  const current = candles[lastIndex];
8097
- const activeZones = zones.filter(
8098
- (zone) => !zone.spent && lastIndex - zone.startIndex <= LIQUIDITY_TAIL_MAX_AGE
8099
- );
8100
- const buyZones = activeZones.filter((zone) => zone.kind === "buy_pressure");
8101
- const sellZones = activeZones.filter((zone) => zone.kind === "sell_pressure");
8102
- const nearestBuy = buyZones.reduce(
8103
- (nearest, zone) => nearest == null || Math.abs(price - zone.mid) < Math.abs(price - nearest.mid) ? zone : nearest,
8104
- null
8105
- ) ?? null;
8106
- const nearestSell = sellZones.reduce(
8107
- (nearest, zone) => nearest == null || Math.abs(zone.mid - price) < Math.abs(nearest.mid - price) ? zone : nearest,
8108
- null
8109
- ) ?? null;
8114
+ let activeCount = 0;
8115
+ let nearestBuy = null;
8116
+ let nearestSell = null;
8117
+ for (const zone of zones) {
8118
+ if (zone.spent || lastIndex - zone.startIndex > LIQUIDITY_TAIL_MAX_AGE) {
8119
+ continue;
8120
+ }
8121
+ activeCount += 1;
8122
+ if (zone.kind === "buy_pressure") {
8123
+ if (nearestBuy == null || Math.abs(price - zone.mid) < Math.abs(price - nearestBuy.mid)) {
8124
+ nearestBuy = zone;
8125
+ }
8126
+ } else if (nearestSell == null || Math.abs(zone.mid - price) < Math.abs(nearestSell.mid - price)) {
8127
+ nearestSell = zone;
8128
+ }
8129
+ }
8110
8130
  const topShadow = current.high - Math.max(current.open, current.close);
8111
8131
  const bottomShadow = Math.min(current.open, current.close) - current.low;
8112
8132
  const body = Math.max(Math.abs(current.close - current.open), 1e-9);
@@ -8115,7 +8135,7 @@ var buildLiquidityTailsContext = (candles, price, atr2) => {
8115
8135
  const dominantWick = dominantUpper ? topShadow : bottomShadow;
8116
8136
  const oppositeWick = dominantUpper ? bottomShadow : topShadow;
8117
8137
  return {
8118
- activeCount: activeZones.length,
8138
+ activeCount,
8119
8139
  nearestBuyPressure: {
8120
8140
  top: nearestBuy?.top ?? null,
8121
8141
  bottom: nearestBuy?.bottom ?? null,
@@ -9750,6 +9770,7 @@ var BASE_CONTEXT_PSAR_EMA_PERIOD = 50;
9750
9770
  var BASE_CONTEXT_PSAR_ADX_MIN = 15;
9751
9771
  var BASE_CONTEXT_PSAR_COOLDOWN_BARS = 1;
9752
9772
  var baseContextEmaKey = (period) => String(period);
9773
+ var sliceCapturedBaseContextTail = (values, length) => values.slice(Math.max(0, length - BASE_CONTEXT_CANDLE_WINDOW), length);
9753
9774
  var processLike = globalThis.process;
9754
9775
  var INDICATOR_NEXT_PROFILE = processLike?.env?.TRADEJS_INDICATOR_NEXT_PROFILE === "1";
9755
9776
  var indicatorNextProfileStats = {
@@ -10925,7 +10946,6 @@ var createIndicators = (data, btcData = [], options = {}) => {
10925
10946
  let cachedBaseContextSnapshot;
10926
10947
  const getCapturedCoinCandles = () => candlesHistory.slice(0, capturedCoinLength);
10927
10948
  const getCapturedBtcCandles = () => btcCandlesHistory.slice(0, capturedBtcLength);
10928
- const getCapturedEthCandles = () => ethCandlesHistory.slice(0, capturedEthLength);
10929
10949
  const getCapturedCoinResampled = () => ({
10930
10950
  h1: coin1hCache.snapshot(capturedCoin1hLength),
10931
10951
  h4: coin4hCache.snapshot(capturedCoin4hLength),
@@ -11015,12 +11035,30 @@ var createIndicators = (data, btcData = [], options = {}) => {
11015
11035
  candle: latestCandle,
11016
11036
  prevCandle: latestPrevCandle,
11017
11037
  baseResult,
11018
- candlesHistory: getCapturedCoinCandles(),
11019
- btcCandlesHistory: getCapturedBtcCandles(),
11020
- ethCandlesHistory: getCapturedEthCandles(),
11021
- closeSeries: closes.slice(0, capturedCoinLength),
11022
- volumeSeries: volumes.slice(0, capturedCoinLength),
11023
- btcCloseSeries: btcCloses.slice(0, capturedBtcLength),
11038
+ candlesHistory: sliceCapturedBaseContextTail(
11039
+ candlesHistory,
11040
+ capturedCoinLength
11041
+ ),
11042
+ btcCandlesHistory: sliceCapturedBaseContextTail(
11043
+ btcCandlesHistory,
11044
+ capturedBtcLength
11045
+ ),
11046
+ ethCandlesHistory: sliceCapturedBaseContextTail(
11047
+ ethCandlesHistory,
11048
+ capturedEthLength
11049
+ ),
11050
+ closeSeries: sliceCapturedBaseContextTail(
11051
+ closes,
11052
+ capturedCoinLength
11053
+ ),
11054
+ volumeSeries: sliceCapturedBaseContextTail(
11055
+ volumes,
11056
+ capturedCoinLength
11057
+ ),
11058
+ btcCloseSeries: sliceCapturedBaseContextTail(
11059
+ btcCloses,
11060
+ capturedBtcLength
11061
+ ),
11024
11062
  coinResampledCandles: getCapturedCoinResampled(),
11025
11063
  btcResampledCandles: getCapturedBtcResampled(),
11026
11064
  ethResampledCandles: getCapturedEthResampled(),
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  intervalToMs
3
- } from "./chunk-M7QGVZ3J.mjs";
3
+ } from "./chunk-WJQRRGUY.mjs";
4
4
  import {
5
5
  BACKTEST_BASE_SLIPPAGE_BPS,
6
6
  BACKTEST_DELAY_RISK_LOOKBACK_CANDLES,
@@ -46,12 +46,14 @@ var isWrongData = (interval, data) => {
46
46
  }
47
47
  return false;
48
48
  };
49
- var cloneArrayValues = (record) => Object.fromEntries(
50
- Object.entries(record).map(([key, value]) => [
51
- key,
52
- Array.isArray(value) ? value.slice() : value
53
- ])
54
- );
49
+ var cloneArrayValues = (record) => {
50
+ const clone = {};
51
+ for (const key of Object.keys(record)) {
52
+ const value = record[key];
53
+ clone[key] = Array.isArray(value) ? value.slice() : value;
54
+ }
55
+ return clone;
56
+ };
55
57
 
56
58
  export {
57
59
  intervalToMs,
package/dist/data.js CHANGED
@@ -86,12 +86,14 @@ var isWrongData = (interval, data) => {
86
86
  }
87
87
  return false;
88
88
  };
89
- var cloneArrayValues = (record) => Object.fromEntries(
90
- Object.entries(record).map(([key, value]) => [
91
- key,
92
- Array.isArray(value) ? value.slice() : value
93
- ])
94
- );
89
+ var cloneArrayValues = (record) => {
90
+ const clone = {};
91
+ for (const key of Object.keys(record)) {
92
+ const value = record[key];
93
+ clone[key] = Array.isArray(value) ? value.slice() : value;
94
+ }
95
+ return clone;
96
+ };
95
97
 
96
98
  // src/utils/toJson.ts
97
99
  var toJson = (data, stringify = false) => {
package/dist/data.mjs CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  intervalToMs,
7
7
  isWrongData,
8
8
  mergeData
9
- } from "./chunk-M7QGVZ3J.mjs";
9
+ } from "./chunk-WJQRRGUY.mjs";
10
10
  export {
11
11
  cloneArrayValues,
12
12
  intervalToMs,
@@ -754,12 +754,14 @@ var TRENDLINE_DEFAULTS = {
754
754
 
755
755
  // src/utils/array.ts
756
756
  var import_lodash2 = __toESM(require("lodash"));
757
- var cloneArrayValues = (record) => Object.fromEntries(
758
- Object.entries(record).map(([key, value]) => [
759
- key,
760
- Array.isArray(value) ? value.slice() : value
761
- ])
762
- );
757
+ var cloneArrayValues = (record) => {
758
+ const clone = {};
759
+ for (const key of Object.keys(record)) {
760
+ const value = record[key];
761
+ clone[key] = Array.isArray(value) ? value.slice() : value;
762
+ }
763
+ return clone;
764
+ };
763
765
 
764
766
  // src/utils/indicatorHistory.ts
765
767
  var createNumericHistoryBuffer = () => ({
@@ -1092,14 +1094,26 @@ var PSYCHOLOGICAL_LEVEL_WINDOWS = {
1092
1094
  h1: 60 * 6e4,
1093
1095
  h4: 4 * 60 * 6e4
1094
1096
  };
1097
+ var PSYCHOLOGICAL_LEVEL_WINDOW_ENTRIES = Object.entries(
1098
+ PSYCHOLOGICAL_LEVEL_WINDOWS
1099
+ );
1095
1100
  var buildSessionContext = (timestamp) => {
1096
1101
  const date = new Date(timestamp);
1097
1102
  const minuteUtc = date.getUTCHours() * 60 + date.getUTCMinutes();
1098
1103
  const dayOfWeekUtc = date.getUTCDay() || 7;
1099
- const activeSessions = SESSION_WINDOWS.filter(
1100
- ({ startMinuteUtc, endMinuteUtc }) => startMinuteUtc <= endMinuteUtc ? minuteUtc >= startMinuteUtc && minuteUtc < endMinuteUtc : minuteUtc >= startMinuteUtc || minuteUtc < endMinuteUtc
1101
- ).map(({ name }) => name);
1102
- const sessionPhase = activeSessions.includes("us") ? "us" : activeSessions.includes("europe") ? "europe" : activeSessions.includes("asia") ? "asia" : "off_hours";
1104
+ let activeSessionCount = 0;
1105
+ let asiaActive = false;
1106
+ let europeActive = false;
1107
+ let usActive = false;
1108
+ for (const { name, startMinuteUtc, endMinuteUtc } of SESSION_WINDOWS) {
1109
+ const active = startMinuteUtc <= endMinuteUtc ? minuteUtc >= startMinuteUtc && minuteUtc < endMinuteUtc : minuteUtc >= startMinuteUtc || minuteUtc < endMinuteUtc;
1110
+ if (!active) continue;
1111
+ activeSessionCount += 1;
1112
+ if (name === "asia") asiaActive = true;
1113
+ if (name === "europe") europeActive = true;
1114
+ if (name === "us") usActive = true;
1115
+ }
1116
+ const sessionPhase = usActive ? "us" : europeActive ? "europe" : asiaActive ? "asia" : "off_hours";
1103
1117
  const primaryWindow = SESSION_WINDOWS.find(
1104
1118
  ({ name }) => name === sessionPhase
1105
1119
  );
@@ -1110,7 +1124,7 @@ var buildSessionContext = (timestamp) => {
1110
1124
  return {
1111
1125
  sessionPhase,
1112
1126
  sessionWindowPhase,
1113
- isOverlap: activeSessions.length > 1,
1127
+ isOverlap: activeSessionCount > 1,
1114
1128
  minutesFromSessionOpen,
1115
1129
  minutesToSessionClose,
1116
1130
  minutesToFundingWindow,
@@ -1164,24 +1178,30 @@ var buildPsychologicalLevelWindow = (startPrice, endPrice, stepUsd) => {
1164
1178
  var buildPsychologicalLevelAssetContext = (candles, stepUsd) => {
1165
1179
  const endCandle = candles[candles.length - 1];
1166
1180
  if (!endCandle) return null;
1167
- const candlesByTimestamp = new Map(
1168
- candles.map((item) => [item.timestamp, item])
1169
- );
1170
- const windows = Object.fromEntries(
1171
- Object.entries(PSYCHOLOGICAL_LEVEL_WINDOWS).map(([window, durationMs]) => {
1172
- const startCandle = candlesByTimestamp.get(
1173
- endCandle.timestamp - durationMs
1174
- );
1175
- return [
1176
- window,
1177
- startCandle ? buildPsychologicalLevelWindow(
1178
- startCandle.close,
1179
- endCandle.close,
1180
- stepUsd
1181
- ) : unavailablePsychologicalLevelWindow()
1182
- ];
1183
- })
1184
- );
1181
+ const windows = {};
1182
+ for (const [window, durationMs] of PSYCHOLOGICAL_LEVEL_WINDOW_ENTRIES) {
1183
+ const targetTimestamp = endCandle.timestamp - durationMs;
1184
+ let low = 0;
1185
+ let high = candles.length - 1;
1186
+ let startCandle;
1187
+ while (low <= high) {
1188
+ const middle = low + high >>> 1;
1189
+ const candidate = candles[middle];
1190
+ if (candidate.timestamp <= targetTimestamp) {
1191
+ if (candidate.timestamp === targetTimestamp) {
1192
+ startCandle = candidate;
1193
+ }
1194
+ low = middle + 1;
1195
+ } else {
1196
+ high = middle - 1;
1197
+ }
1198
+ }
1199
+ windows[window] = startCandle ? buildPsychologicalLevelWindow(
1200
+ startCandle.close,
1201
+ endCandle.close,
1202
+ stepUsd
1203
+ ) : unavailablePsychologicalLevelWindow();
1204
+ }
1185
1205
  return { source: "aligned_15m_ohlcv", stepUsd, windows };
1186
1206
  };
1187
1207
  var returnPct = (candles) => {
@@ -8072,35 +8092,34 @@ var buildLiquidityZonesContext = (candles, price, previousPrice, atr2) => {
8072
8092
  }
8073
8093
  const lastIndex = candles.length - 1;
8074
8094
  const current = candles[lastIndex];
8075
- const activeZones = zones.filter((zone) => {
8095
+ let activeCount = 0;
8096
+ let nearestSupport = null;
8097
+ let nearestResistance = null;
8098
+ for (const zone of zones) {
8076
8099
  if (lastIndex - zone.startIndex > LIQUIDITY_ZONE_MAX_AGE) {
8077
- return false;
8100
+ continue;
8078
8101
  }
8079
8102
  const crossed = zone.crossed || (zone.kind === "swing_high_liquidity" ? current.close > zone.top : current.close < zone.bottom);
8080
8103
  zone.crossed = crossed;
8081
- return !crossed;
8082
- });
8083
- const supports = activeZones.filter(
8084
- (zone) => zone.kind === "swing_low_liquidity"
8085
- );
8086
- const resistances = activeZones.filter(
8087
- (zone) => zone.kind === "swing_high_liquidity"
8088
- );
8089
- const nearestSupport = supports.reduce(
8090
- (nearest, zone) => nearest == null || Math.abs(price - zone.level) < Math.abs(price - nearest.level) ? zone : nearest,
8091
- null
8092
- ) ?? null;
8093
- const nearestResistance = resistances.reduce(
8094
- (nearest, zone) => nearest == null || Math.abs(zone.level - price) < Math.abs(nearest.level - price) ? zone : nearest,
8095
- null
8096
- ) ?? null;
8104
+ if (crossed) {
8105
+ continue;
8106
+ }
8107
+ activeCount += 1;
8108
+ if (zone.kind === "swing_low_liquidity") {
8109
+ if (nearestSupport == null || Math.abs(price - zone.level) < Math.abs(price - nearestSupport.level)) {
8110
+ nearestSupport = zone;
8111
+ }
8112
+ } else if (nearestResistance == null || Math.abs(zone.level - price) < Math.abs(nearestResistance.level - price)) {
8113
+ nearestResistance = zone;
8114
+ }
8115
+ }
8097
8116
  const supportRetest = nearestSupport != null && current.low <= nearestSupport.top;
8098
8117
  const resistanceRetest = nearestResistance != null && current.high >= nearestResistance.bottom;
8099
8118
  const retestZone = supportRetest ? nearestSupport : resistanceRetest ? nearestResistance : null;
8100
8119
  const retestPenetration = retestZone == null ? null : retestZone.kind === "swing_low_liquidity" ? Math.max(0, retestZone.top - current.low) : Math.max(0, current.high - retestZone.bottom);
8101
8120
  const retestHeight = retestZone == null ? null : Math.max(retestZone.top - retestZone.bottom, 0);
8102
8121
  return {
8103
- activeCount: activeZones.length,
8122
+ activeCount,
8104
8123
  nearestSupport: {
8105
8124
  top: nearestSupport?.top ?? null,
8106
8125
  bottom: nearestSupport?.bottom ?? null,
@@ -8209,19 +8228,22 @@ var buildLiquidityTailsContext = (candles, price, atr2) => {
8209
8228
  }
8210
8229
  const lastIndex = candles.length - 1;
8211
8230
  const current = candles[lastIndex];
8212
- const activeZones = zones.filter(
8213
- (zone) => !zone.spent && lastIndex - zone.startIndex <= LIQUIDITY_TAIL_MAX_AGE
8214
- );
8215
- const buyZones = activeZones.filter((zone) => zone.kind === "buy_pressure");
8216
- const sellZones = activeZones.filter((zone) => zone.kind === "sell_pressure");
8217
- const nearestBuy = buyZones.reduce(
8218
- (nearest, zone) => nearest == null || Math.abs(price - zone.mid) < Math.abs(price - nearest.mid) ? zone : nearest,
8219
- null
8220
- ) ?? null;
8221
- const nearestSell = sellZones.reduce(
8222
- (nearest, zone) => nearest == null || Math.abs(zone.mid - price) < Math.abs(nearest.mid - price) ? zone : nearest,
8223
- null
8224
- ) ?? null;
8231
+ let activeCount = 0;
8232
+ let nearestBuy = null;
8233
+ let nearestSell = null;
8234
+ for (const zone of zones) {
8235
+ if (zone.spent || lastIndex - zone.startIndex > LIQUIDITY_TAIL_MAX_AGE) {
8236
+ continue;
8237
+ }
8238
+ activeCount += 1;
8239
+ if (zone.kind === "buy_pressure") {
8240
+ if (nearestBuy == null || Math.abs(price - zone.mid) < Math.abs(price - nearestBuy.mid)) {
8241
+ nearestBuy = zone;
8242
+ }
8243
+ } else if (nearestSell == null || Math.abs(zone.mid - price) < Math.abs(nearestSell.mid - price)) {
8244
+ nearestSell = zone;
8245
+ }
8246
+ }
8225
8247
  const topShadow = current.high - Math.max(current.open, current.close);
8226
8248
  const bottomShadow = Math.min(current.open, current.close) - current.low;
8227
8249
  const body = Math.max(Math.abs(current.close - current.open), 1e-9);
@@ -8230,7 +8252,7 @@ var buildLiquidityTailsContext = (candles, price, atr2) => {
8230
8252
  const dominantWick = dominantUpper ? topShadow : bottomShadow;
8231
8253
  const oppositeWick = dominantUpper ? bottomShadow : topShadow;
8232
8254
  return {
8233
- activeCount: activeZones.length,
8255
+ activeCount,
8234
8256
  nearestBuyPressure: {
8235
8257
  top: nearestBuy?.top ?? null,
8236
8258
  bottom: nearestBuy?.bottom ?? null,
@@ -9865,6 +9887,7 @@ var BASE_CONTEXT_PSAR_EMA_PERIOD = 50;
9865
9887
  var BASE_CONTEXT_PSAR_ADX_MIN = 15;
9866
9888
  var BASE_CONTEXT_PSAR_COOLDOWN_BARS = 1;
9867
9889
  var baseContextEmaKey = (period) => String(period);
9890
+ var sliceCapturedBaseContextTail = (values, length) => values.slice(Math.max(0, length - BASE_CONTEXT_CANDLE_WINDOW), length);
9868
9891
  var processLike = globalThis.process;
9869
9892
  var INDICATOR_NEXT_PROFILE = processLike?.env?.TRADEJS_INDICATOR_NEXT_PROFILE === "1";
9870
9893
  var indicatorNextProfileStats = {
@@ -11040,7 +11063,6 @@ var createIndicators = (data, btcData = [], options = {}) => {
11040
11063
  let cachedBaseContextSnapshot;
11041
11064
  const getCapturedCoinCandles = () => candlesHistory.slice(0, capturedCoinLength);
11042
11065
  const getCapturedBtcCandles = () => btcCandlesHistory.slice(0, capturedBtcLength);
11043
- const getCapturedEthCandles = () => ethCandlesHistory.slice(0, capturedEthLength);
11044
11066
  const getCapturedCoinResampled = () => ({
11045
11067
  h1: coin1hCache.snapshot(capturedCoin1hLength),
11046
11068
  h4: coin4hCache.snapshot(capturedCoin4hLength),
@@ -11130,12 +11152,30 @@ var createIndicators = (data, btcData = [], options = {}) => {
11130
11152
  candle: latestCandle,
11131
11153
  prevCandle: latestPrevCandle,
11132
11154
  baseResult,
11133
- candlesHistory: getCapturedCoinCandles(),
11134
- btcCandlesHistory: getCapturedBtcCandles(),
11135
- ethCandlesHistory: getCapturedEthCandles(),
11136
- closeSeries: closes.slice(0, capturedCoinLength),
11137
- volumeSeries: volumes.slice(0, capturedCoinLength),
11138
- btcCloseSeries: btcCloses.slice(0, capturedBtcLength),
11155
+ candlesHistory: sliceCapturedBaseContextTail(
11156
+ candlesHistory,
11157
+ capturedCoinLength
11158
+ ),
11159
+ btcCandlesHistory: sliceCapturedBaseContextTail(
11160
+ btcCandlesHistory,
11161
+ capturedBtcLength
11162
+ ),
11163
+ ethCandlesHistory: sliceCapturedBaseContextTail(
11164
+ ethCandlesHistory,
11165
+ capturedEthLength
11166
+ ),
11167
+ closeSeries: sliceCapturedBaseContextTail(
11168
+ closes,
11169
+ capturedCoinLength
11170
+ ),
11171
+ volumeSeries: sliceCapturedBaseContextTail(
11172
+ volumes,
11173
+ capturedCoinLength
11174
+ ),
11175
+ btcCloseSeries: sliceCapturedBaseContextTail(
11176
+ btcCloses,
11177
+ capturedBtcLength
11178
+ ),
11139
11179
  coinResampledCandles: getCapturedCoinResampled(),
11140
11180
  btcResampledCandles: getCapturedBtcResampled(),
11141
11181
  ethResampledCandles: getCapturedEthResampled(),
@@ -39,8 +39,8 @@ import {
39
39
  toArrayData,
40
40
  toCoinalyzeTimestampMs,
41
41
  toFiniteNumber
42
- } from "./chunk-5JMDX4IW.mjs";
43
- import "./chunk-M7QGVZ3J.mjs";
42
+ } from "./chunk-IS72ATRN.mjs";
43
+ import "./chunk-WJQRRGUY.mjs";
44
44
  import "./chunk-S4KHOAXM.mjs";
45
45
  import "./chunk-LI6FPPKB.mjs";
46
46
  import "./chunk-MKCQSB4H.mjs";
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  normalizeStrategyOrderLinkKey,
3
3
  parseStrategyOrderLinkKey
4
- } from "./chunk-NYJU3J7Y.mjs";
5
- import "./chunk-M7QGVZ3J.mjs";
4
+ } from "./chunk-V4KXTU6Y.mjs";
5
+ import "./chunk-WJQRRGUY.mjs";
6
6
  import {
7
7
  INITIAL_BACKTEST_AMOUNT
8
8
  } from "./chunk-MKCQSB4H.mjs";
@@ -186,12 +186,14 @@ var DERIVATIVES_CONTEXT_REFERENCE_SYMBOLS = [
186
186
 
187
187
  // src/utils/array.ts
188
188
  var import_lodash2 = __toESM(require("lodash"));
189
- var cloneArrayValues = (record) => Object.fromEntries(
190
- Object.entries(record).map(([key, value]) => [
191
- key,
192
- Array.isArray(value) ? value.slice() : value
193
- ])
194
- );
189
+ var cloneArrayValues = (record) => {
190
+ const clone = {};
191
+ for (const key of Object.keys(record)) {
192
+ const value = record[key];
193
+ clone[key] = Array.isArray(value) ? value.slice() : value;
194
+ }
195
+ return clone;
196
+ };
195
197
 
196
198
  // src/utils/indicatorHistory.ts
197
199
  var createNumericHistoryBuffer = () => ({
@@ -524,14 +526,26 @@ var PSYCHOLOGICAL_LEVEL_WINDOWS = {
524
526
  h1: 60 * 6e4,
525
527
  h4: 4 * 60 * 6e4
526
528
  };
529
+ var PSYCHOLOGICAL_LEVEL_WINDOW_ENTRIES = Object.entries(
530
+ PSYCHOLOGICAL_LEVEL_WINDOWS
531
+ );
527
532
  var buildSessionContext = (timestamp) => {
528
533
  const date = new Date(timestamp);
529
534
  const minuteUtc = date.getUTCHours() * 60 + date.getUTCMinutes();
530
535
  const dayOfWeekUtc = date.getUTCDay() || 7;
531
- const activeSessions = SESSION_WINDOWS.filter(
532
- ({ startMinuteUtc, endMinuteUtc }) => startMinuteUtc <= endMinuteUtc ? minuteUtc >= startMinuteUtc && minuteUtc < endMinuteUtc : minuteUtc >= startMinuteUtc || minuteUtc < endMinuteUtc
533
- ).map(({ name }) => name);
534
- const sessionPhase = activeSessions.includes("us") ? "us" : activeSessions.includes("europe") ? "europe" : activeSessions.includes("asia") ? "asia" : "off_hours";
536
+ let activeSessionCount = 0;
537
+ let asiaActive = false;
538
+ let europeActive = false;
539
+ let usActive = false;
540
+ for (const { name, startMinuteUtc, endMinuteUtc } of SESSION_WINDOWS) {
541
+ const active = startMinuteUtc <= endMinuteUtc ? minuteUtc >= startMinuteUtc && minuteUtc < endMinuteUtc : minuteUtc >= startMinuteUtc || minuteUtc < endMinuteUtc;
542
+ if (!active) continue;
543
+ activeSessionCount += 1;
544
+ if (name === "asia") asiaActive = true;
545
+ if (name === "europe") europeActive = true;
546
+ if (name === "us") usActive = true;
547
+ }
548
+ const sessionPhase = usActive ? "us" : europeActive ? "europe" : asiaActive ? "asia" : "off_hours";
535
549
  const primaryWindow = SESSION_WINDOWS.find(
536
550
  ({ name }) => name === sessionPhase
537
551
  );
@@ -542,7 +556,7 @@ var buildSessionContext = (timestamp) => {
542
556
  return {
543
557
  sessionPhase,
544
558
  sessionWindowPhase,
545
- isOverlap: activeSessions.length > 1,
559
+ isOverlap: activeSessionCount > 1,
546
560
  minutesFromSessionOpen,
547
561
  minutesToSessionClose,
548
562
  minutesToFundingWindow,
@@ -596,24 +610,30 @@ var buildPsychologicalLevelWindow = (startPrice, endPrice, stepUsd) => {
596
610
  var buildPsychologicalLevelAssetContext = (candles, stepUsd) => {
597
611
  const endCandle = candles[candles.length - 1];
598
612
  if (!endCandle) return null;
599
- const candlesByTimestamp = new Map(
600
- candles.map((item) => [item.timestamp, item])
601
- );
602
- const windows = Object.fromEntries(
603
- Object.entries(PSYCHOLOGICAL_LEVEL_WINDOWS).map(([window, durationMs]) => {
604
- const startCandle = candlesByTimestamp.get(
605
- endCandle.timestamp - durationMs
606
- );
607
- return [
608
- window,
609
- startCandle ? buildPsychologicalLevelWindow(
610
- startCandle.close,
611
- endCandle.close,
612
- stepUsd
613
- ) : unavailablePsychologicalLevelWindow()
614
- ];
615
- })
616
- );
613
+ const windows = {};
614
+ for (const [window, durationMs] of PSYCHOLOGICAL_LEVEL_WINDOW_ENTRIES) {
615
+ const targetTimestamp = endCandle.timestamp - durationMs;
616
+ let low = 0;
617
+ let high = candles.length - 1;
618
+ let startCandle;
619
+ while (low <= high) {
620
+ const middle = low + high >>> 1;
621
+ const candidate = candles[middle];
622
+ if (candidate.timestamp <= targetTimestamp) {
623
+ if (candidate.timestamp === targetTimestamp) {
624
+ startCandle = candidate;
625
+ }
626
+ low = middle + 1;
627
+ } else {
628
+ high = middle - 1;
629
+ }
630
+ }
631
+ windows[window] = startCandle ? buildPsychologicalLevelWindow(
632
+ startCandle.close,
633
+ endCandle.close,
634
+ stepUsd
635
+ ) : unavailablePsychologicalLevelWindow();
636
+ }
617
637
  return { source: "aligned_15m_ohlcv", stepUsd, windows };
618
638
  };
619
639
  var returnPct = (candles) => {
@@ -7504,35 +7524,34 @@ var buildLiquidityZonesContext = (candles, price, previousPrice, atr2) => {
7504
7524
  }
7505
7525
  const lastIndex = candles.length - 1;
7506
7526
  const current = candles[lastIndex];
7507
- const activeZones = zones.filter((zone) => {
7527
+ let activeCount = 0;
7528
+ let nearestSupport = null;
7529
+ let nearestResistance = null;
7530
+ for (const zone of zones) {
7508
7531
  if (lastIndex - zone.startIndex > LIQUIDITY_ZONE_MAX_AGE) {
7509
- return false;
7532
+ continue;
7510
7533
  }
7511
7534
  const crossed = zone.crossed || (zone.kind === "swing_high_liquidity" ? current.close > zone.top : current.close < zone.bottom);
7512
7535
  zone.crossed = crossed;
7513
- return !crossed;
7514
- });
7515
- const supports = activeZones.filter(
7516
- (zone) => zone.kind === "swing_low_liquidity"
7517
- );
7518
- const resistances = activeZones.filter(
7519
- (zone) => zone.kind === "swing_high_liquidity"
7520
- );
7521
- const nearestSupport = supports.reduce(
7522
- (nearest, zone) => nearest == null || Math.abs(price - zone.level) < Math.abs(price - nearest.level) ? zone : nearest,
7523
- null
7524
- ) ?? null;
7525
- const nearestResistance = resistances.reduce(
7526
- (nearest, zone) => nearest == null || Math.abs(zone.level - price) < Math.abs(nearest.level - price) ? zone : nearest,
7527
- null
7528
- ) ?? null;
7536
+ if (crossed) {
7537
+ continue;
7538
+ }
7539
+ activeCount += 1;
7540
+ if (zone.kind === "swing_low_liquidity") {
7541
+ if (nearestSupport == null || Math.abs(price - zone.level) < Math.abs(price - nearestSupport.level)) {
7542
+ nearestSupport = zone;
7543
+ }
7544
+ } else if (nearestResistance == null || Math.abs(zone.level - price) < Math.abs(nearestResistance.level - price)) {
7545
+ nearestResistance = zone;
7546
+ }
7547
+ }
7529
7548
  const supportRetest = nearestSupport != null && current.low <= nearestSupport.top;
7530
7549
  const resistanceRetest = nearestResistance != null && current.high >= nearestResistance.bottom;
7531
7550
  const retestZone = supportRetest ? nearestSupport : resistanceRetest ? nearestResistance : null;
7532
7551
  const retestPenetration = retestZone == null ? null : retestZone.kind === "swing_low_liquidity" ? Math.max(0, retestZone.top - current.low) : Math.max(0, current.high - retestZone.bottom);
7533
7552
  const retestHeight = retestZone == null ? null : Math.max(retestZone.top - retestZone.bottom, 0);
7534
7553
  return {
7535
- activeCount: activeZones.length,
7554
+ activeCount,
7536
7555
  nearestSupport: {
7537
7556
  top: nearestSupport?.top ?? null,
7538
7557
  bottom: nearestSupport?.bottom ?? null,
@@ -7641,19 +7660,22 @@ var buildLiquidityTailsContext = (candles, price, atr2) => {
7641
7660
  }
7642
7661
  const lastIndex = candles.length - 1;
7643
7662
  const current = candles[lastIndex];
7644
- const activeZones = zones.filter(
7645
- (zone) => !zone.spent && lastIndex - zone.startIndex <= LIQUIDITY_TAIL_MAX_AGE
7646
- );
7647
- const buyZones = activeZones.filter((zone) => zone.kind === "buy_pressure");
7648
- const sellZones = activeZones.filter((zone) => zone.kind === "sell_pressure");
7649
- const nearestBuy = buyZones.reduce(
7650
- (nearest, zone) => nearest == null || Math.abs(price - zone.mid) < Math.abs(price - nearest.mid) ? zone : nearest,
7651
- null
7652
- ) ?? null;
7653
- const nearestSell = sellZones.reduce(
7654
- (nearest, zone) => nearest == null || Math.abs(zone.mid - price) < Math.abs(nearest.mid - price) ? zone : nearest,
7655
- null
7656
- ) ?? null;
7663
+ let activeCount = 0;
7664
+ let nearestBuy = null;
7665
+ let nearestSell = null;
7666
+ for (const zone of zones) {
7667
+ if (zone.spent || lastIndex - zone.startIndex > LIQUIDITY_TAIL_MAX_AGE) {
7668
+ continue;
7669
+ }
7670
+ activeCount += 1;
7671
+ if (zone.kind === "buy_pressure") {
7672
+ if (nearestBuy == null || Math.abs(price - zone.mid) < Math.abs(price - nearestBuy.mid)) {
7673
+ nearestBuy = zone;
7674
+ }
7675
+ } else if (nearestSell == null || Math.abs(zone.mid - price) < Math.abs(nearestSell.mid - price)) {
7676
+ nearestSell = zone;
7677
+ }
7678
+ }
7657
7679
  const topShadow = current.high - Math.max(current.open, current.close);
7658
7680
  const bottomShadow = Math.min(current.open, current.close) - current.low;
7659
7681
  const body = Math.max(Math.abs(current.close - current.open), 1e-9);
@@ -7662,7 +7684,7 @@ var buildLiquidityTailsContext = (candles, price, atr2) => {
7662
7684
  const dominantWick = dominantUpper ? topShadow : bottomShadow;
7663
7685
  const oppositeWick = dominantUpper ? bottomShadow : topShadow;
7664
7686
  return {
7665
- activeCount: activeZones.length,
7687
+ activeCount,
7666
7688
  nearestBuyPressure: {
7667
7689
  top: nearestBuy?.top ?? null,
7668
7690
  bottom: nearestBuy?.bottom ?? null,
@@ -9164,6 +9186,7 @@ var BASE_CONTEXT_PSAR_EMA_PERIOD = 50;
9164
9186
  var BASE_CONTEXT_PSAR_ADX_MIN = 15;
9165
9187
  var BASE_CONTEXT_PSAR_COOLDOWN_BARS = 1;
9166
9188
  var baseContextEmaKey = (period) => String(period);
9189
+ var sliceCapturedBaseContextTail = (values, length) => values.slice(Math.max(0, length - BASE_CONTEXT_CANDLE_WINDOW), length);
9167
9190
  var processLike = globalThis.process;
9168
9191
  var INDICATOR_NEXT_PROFILE = processLike?.env?.TRADEJS_INDICATOR_NEXT_PROFILE === "1";
9169
9192
  var indicatorNextProfileStats = {
@@ -10339,7 +10362,6 @@ var createIndicators = (data, btcData = [], options = {}) => {
10339
10362
  let cachedBaseContextSnapshot;
10340
10363
  const getCapturedCoinCandles = () => candlesHistory.slice(0, capturedCoinLength);
10341
10364
  const getCapturedBtcCandles = () => btcCandlesHistory.slice(0, capturedBtcLength);
10342
- const getCapturedEthCandles = () => ethCandlesHistory.slice(0, capturedEthLength);
10343
10365
  const getCapturedCoinResampled = () => ({
10344
10366
  h1: coin1hCache.snapshot(capturedCoin1hLength),
10345
10367
  h4: coin4hCache.snapshot(capturedCoin4hLength),
@@ -10429,12 +10451,30 @@ var createIndicators = (data, btcData = [], options = {}) => {
10429
10451
  candle: latestCandle,
10430
10452
  prevCandle: latestPrevCandle,
10431
10453
  baseResult,
10432
- candlesHistory: getCapturedCoinCandles(),
10433
- btcCandlesHistory: getCapturedBtcCandles(),
10434
- ethCandlesHistory: getCapturedEthCandles(),
10435
- closeSeries: closes.slice(0, capturedCoinLength),
10436
- volumeSeries: volumes.slice(0, capturedCoinLength),
10437
- btcCloseSeries: btcCloses.slice(0, capturedBtcLength),
10454
+ candlesHistory: sliceCapturedBaseContextTail(
10455
+ candlesHistory,
10456
+ capturedCoinLength
10457
+ ),
10458
+ btcCandlesHistory: sliceCapturedBaseContextTail(
10459
+ btcCandlesHistory,
10460
+ capturedBtcLength
10461
+ ),
10462
+ ethCandlesHistory: sliceCapturedBaseContextTail(
10463
+ ethCandlesHistory,
10464
+ capturedEthLength
10465
+ ),
10466
+ closeSeries: sliceCapturedBaseContextTail(
10467
+ closes,
10468
+ capturedCoinLength
10469
+ ),
10470
+ volumeSeries: sliceCapturedBaseContextTail(
10471
+ volumes,
10472
+ capturedCoinLength
10473
+ ),
10474
+ btcCloseSeries: sliceCapturedBaseContextTail(
10475
+ btcCloses,
10476
+ capturedBtcLength
10477
+ ),
10438
10478
  coinResampledCandles: getCapturedCoinResampled(),
10439
10479
  btcResampledCandles: getCapturedBtcResampled(),
10440
10480
  ethResampledCandles: getCapturedEthResampled(),
@@ -11058,8 +11098,8 @@ var getDirectionalTpSlPrices = ({
11058
11098
  takeProfitPrice,
11059
11099
  stopLossPrice
11060
11100
  });
11061
- const slPercent = unit === "percent" ? stopLossDelta : stopLossDelta * 100;
11062
- const qty = typeof maxLossValue === "number" && Number.isFinite(maxLossValue) && maxLossValue > 0 ? maxLossValue / (price * (slPercent + feePercent) / 100) : void 0;
11101
+ const lossPerUnit = Math.abs(price - stopLossPrice) + (Math.abs(price) + Math.abs(stopLossPrice)) * Math.max(0, feePercent);
11102
+ const qty = typeof maxLossValue === "number" && Number.isFinite(maxLossValue) && maxLossValue > 0 ? lossPerUnit > 0 ? maxLossValue / lossPerUnit : 0 : void 0;
11063
11103
  return {
11064
11104
  stopLossPrice,
11065
11105
  takeProfitPrice,
@@ -4,8 +4,8 @@ import {
4
4
  import {
5
5
  createIndicators,
6
6
  getRequiredControllerSeedWindow
7
- } from "./chunk-5JMDX4IW.mjs";
8
- import "./chunk-M7QGVZ3J.mjs";
7
+ } from "./chunk-IS72ATRN.mjs";
8
+ import "./chunk-WJQRRGUY.mjs";
9
9
  import {
10
10
  getTimestamp
11
11
  } from "./chunk-S4KHOAXM.mjs";
@@ -369,8 +369,8 @@ var getDirectionalTpSlPrices = ({
369
369
  takeProfitPrice,
370
370
  stopLossPrice
371
371
  });
372
- const slPercent = unit === "percent" ? stopLossDelta : stopLossDelta * 100;
373
- const qty = typeof maxLossValue === "number" && Number.isFinite(maxLossValue) && maxLossValue > 0 ? maxLossValue / (price * (slPercent + feePercent) / 100) : void 0;
372
+ const lossPerUnit = Math.abs(price - stopLossPrice) + (Math.abs(price) + Math.abs(stopLossPrice)) * Math.max(0, feePercent);
373
+ const qty = typeof maxLossValue === "number" && Number.isFinite(maxLossValue) && maxLossValue > 0 ? lossPerUnit > 0 ? maxLossValue / lossPerUnit : 0 : void 0;
374
374
  return {
375
375
  stopLossPrice,
376
376
  takeProfitPrice,
package/dist/trade.mjs CHANGED
@@ -10,8 +10,8 @@ import {
10
10
  normalizeStrategyOrderLinkKey,
11
11
  parseStrategyOrderLinkKey,
12
12
  slippageBpsToRate
13
- } from "./chunk-NYJU3J7Y.mjs";
14
- import "./chunk-M7QGVZ3J.mjs";
13
+ } from "./chunk-V4KXTU6Y.mjs";
14
+ import "./chunk-WJQRRGUY.mjs";
15
15
  import "./chunk-MKCQSB4H.mjs";
16
16
  export {
17
17
  applyExecutionSlippage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/core",
3
- "version": "3.1.27",
3
+ "version": "3.1.28-beta.252",
4
4
  "description": "MIT-licensed browser-safe API for TradeJS config, strategy authoring, figures, and shared helpers.",
5
5
  "keywords": [
6
6
  "tradejs",
@@ -132,7 +132,7 @@
132
132
  }
133
133
  },
134
134
  "dependencies": {
135
- "@tradejs/types": "^3.1.27",
135
+ "@tradejs/types": "^3.1.28-beta.252",
136
136
  "date-fns": "^3.6.0",
137
137
  "fast-technical-indicators": "^1.1.4",
138
138
  "klinecharts": "10.0.0-alpha9",