@tradejs/indicators 3.0.0 → 3.0.2

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/README.md CHANGED
@@ -18,6 +18,10 @@ This package contains the built-in indicator catalog used by strategies and char
18
18
 
19
19
  It is intended to be connected as an indicator plugin package through `tradejs.config.ts`.
20
20
 
21
+ It also exposes strategy-neutral, replay-safe indicator engines through explicit
22
+ subpaths. These modules are libraries and do not register plugin entries by
23
+ themselves.
24
+
21
25
  ## Install
22
26
 
23
27
  ```bash
@@ -38,6 +42,16 @@ export default defineConfig({
38
42
  });
39
43
  ```
40
44
 
45
+ For a causal range detector that only uses candles available at each call:
46
+
47
+ ```ts
48
+ import { createCausalRangeGeometryEngine } from '@tradejs/indicators/range-geometry';
49
+ ```
50
+
51
+ `@tradejs/indicators/range-geometry` is the public owner of the shared range
52
+ geometry used by the Grid strategy family. It does not depend on either
53
+ strategy or on StrategyAPI.
54
+
41
55
  ## For Custom Indicators
42
56
 
43
57
  Use `defineIndicatorPlugin(...)` from `@tradejs/core/config` in your own package and add that package to `indicators` in `tradejs.config.ts`.
@@ -0,0 +1,105 @@
1
+ import { Candle } from '@tradejs/types';
2
+
3
+ interface CausalRangePivot {
4
+ kind: 'high' | 'low';
5
+ barIndex: number;
6
+ timestamp: number;
7
+ price: number;
8
+ }
9
+ interface CausalRangeLine {
10
+ startTimestamp: number;
11
+ startPrice: number;
12
+ endTimestamp: number;
13
+ endPrice: number;
14
+ }
15
+ interface CausalRangeGeometry {
16
+ ready: boolean;
17
+ detected: boolean;
18
+ upperPrice: number | null;
19
+ lowerPrice: number | null;
20
+ centerPrice: number | null;
21
+ position: number | null;
22
+ widthAtr: number | null;
23
+ centerSlopeAtrPerBar: number | null;
24
+ boundaryDivergenceAtr: number | null;
25
+ containmentRatio: number | null;
26
+ highPivotCount: number;
27
+ lowPivotCount: number;
28
+ rangeAgeBars: number;
29
+ breakoutDirection: 'UP' | 'DOWN' | null;
30
+ volatilityExpansionRatio: number | null;
31
+ volatilityExpansion: boolean;
32
+ upperLine: CausalRangeLine | null;
33
+ lowerLine: CausalRangeLine | null;
34
+ centerLine: CausalRangeLine | null;
35
+ pivots: CausalRangePivot[];
36
+ historySize: number;
37
+ pivotHistorySize: number;
38
+ }
39
+ interface CausalRangeGeometryOptions {
40
+ pivotLeftBars: number;
41
+ pivotRightBars: number;
42
+ lookbackBars: number;
43
+ minPivotsPerSide: number;
44
+ minWidthAtr: number;
45
+ maxWidthAtr: number;
46
+ maxCenterSlopeAtrPerBar: number;
47
+ maxBoundaryDivergenceAtr: number;
48
+ minContainmentRatio: number;
49
+ containmentToleranceAtr: number;
50
+ breakoutToleranceAtr: number;
51
+ minRangeAgeBars: number;
52
+ maxVolatilityExpansion: number;
53
+ lineStartMode?: 'history' | 'oldest_pivot';
54
+ }
55
+ declare const createCausalRangeGeometryEngine: ({ options, }: {
56
+ options: CausalRangeGeometryOptions;
57
+ }) => {
58
+ next: (candle: Candle, atr: number) => CausalRangeGeometry;
59
+ getState: () => {
60
+ pivots: {
61
+ kind: "high" | "low";
62
+ barIndex: number;
63
+ timestamp: number;
64
+ price: number;
65
+ }[];
66
+ upperLine: {
67
+ startTimestamp: number;
68
+ startPrice: number;
69
+ endTimestamp: number;
70
+ endPrice: number;
71
+ } | null;
72
+ lowerLine: {
73
+ startTimestamp: number;
74
+ startPrice: number;
75
+ endTimestamp: number;
76
+ endPrice: number;
77
+ } | null;
78
+ centerLine: {
79
+ startTimestamp: number;
80
+ startPrice: number;
81
+ endTimestamp: number;
82
+ endPrice: number;
83
+ } | null;
84
+ ready: boolean;
85
+ detected: boolean;
86
+ upperPrice: number | null;
87
+ lowerPrice: number | null;
88
+ centerPrice: number | null;
89
+ position: number | null;
90
+ widthAtr: number | null;
91
+ centerSlopeAtrPerBar: number | null;
92
+ boundaryDivergenceAtr: number | null;
93
+ containmentRatio: number | null;
94
+ highPivotCount: number;
95
+ lowPivotCount: number;
96
+ rangeAgeBars: number;
97
+ breakoutDirection: "UP" | "DOWN" | null;
98
+ volatilityExpansionRatio: number | null;
99
+ volatilityExpansion: boolean;
100
+ historySize: number;
101
+ pivotHistorySize: number;
102
+ };
103
+ };
104
+
105
+ export { type CausalRangeGeometry, type CausalRangeGeometryOptions, type CausalRangeLine, type CausalRangePivot, createCausalRangeGeometryEngine };
@@ -0,0 +1,105 @@
1
+ import { Candle } from '@tradejs/types';
2
+
3
+ interface CausalRangePivot {
4
+ kind: 'high' | 'low';
5
+ barIndex: number;
6
+ timestamp: number;
7
+ price: number;
8
+ }
9
+ interface CausalRangeLine {
10
+ startTimestamp: number;
11
+ startPrice: number;
12
+ endTimestamp: number;
13
+ endPrice: number;
14
+ }
15
+ interface CausalRangeGeometry {
16
+ ready: boolean;
17
+ detected: boolean;
18
+ upperPrice: number | null;
19
+ lowerPrice: number | null;
20
+ centerPrice: number | null;
21
+ position: number | null;
22
+ widthAtr: number | null;
23
+ centerSlopeAtrPerBar: number | null;
24
+ boundaryDivergenceAtr: number | null;
25
+ containmentRatio: number | null;
26
+ highPivotCount: number;
27
+ lowPivotCount: number;
28
+ rangeAgeBars: number;
29
+ breakoutDirection: 'UP' | 'DOWN' | null;
30
+ volatilityExpansionRatio: number | null;
31
+ volatilityExpansion: boolean;
32
+ upperLine: CausalRangeLine | null;
33
+ lowerLine: CausalRangeLine | null;
34
+ centerLine: CausalRangeLine | null;
35
+ pivots: CausalRangePivot[];
36
+ historySize: number;
37
+ pivotHistorySize: number;
38
+ }
39
+ interface CausalRangeGeometryOptions {
40
+ pivotLeftBars: number;
41
+ pivotRightBars: number;
42
+ lookbackBars: number;
43
+ minPivotsPerSide: number;
44
+ minWidthAtr: number;
45
+ maxWidthAtr: number;
46
+ maxCenterSlopeAtrPerBar: number;
47
+ maxBoundaryDivergenceAtr: number;
48
+ minContainmentRatio: number;
49
+ containmentToleranceAtr: number;
50
+ breakoutToleranceAtr: number;
51
+ minRangeAgeBars: number;
52
+ maxVolatilityExpansion: number;
53
+ lineStartMode?: 'history' | 'oldest_pivot';
54
+ }
55
+ declare const createCausalRangeGeometryEngine: ({ options, }: {
56
+ options: CausalRangeGeometryOptions;
57
+ }) => {
58
+ next: (candle: Candle, atr: number) => CausalRangeGeometry;
59
+ getState: () => {
60
+ pivots: {
61
+ kind: "high" | "low";
62
+ barIndex: number;
63
+ timestamp: number;
64
+ price: number;
65
+ }[];
66
+ upperLine: {
67
+ startTimestamp: number;
68
+ startPrice: number;
69
+ endTimestamp: number;
70
+ endPrice: number;
71
+ } | null;
72
+ lowerLine: {
73
+ startTimestamp: number;
74
+ startPrice: number;
75
+ endTimestamp: number;
76
+ endPrice: number;
77
+ } | null;
78
+ centerLine: {
79
+ startTimestamp: number;
80
+ startPrice: number;
81
+ endTimestamp: number;
82
+ endPrice: number;
83
+ } | null;
84
+ ready: boolean;
85
+ detected: boolean;
86
+ upperPrice: number | null;
87
+ lowerPrice: number | null;
88
+ centerPrice: number | null;
89
+ position: number | null;
90
+ widthAtr: number | null;
91
+ centerSlopeAtrPerBar: number | null;
92
+ boundaryDivergenceAtr: number | null;
93
+ containmentRatio: number | null;
94
+ highPivotCount: number;
95
+ lowPivotCount: number;
96
+ rangeAgeBars: number;
97
+ breakoutDirection: "UP" | "DOWN" | null;
98
+ volatilityExpansionRatio: number | null;
99
+ volatilityExpansion: boolean;
100
+ historySize: number;
101
+ pivotHistorySize: number;
102
+ };
103
+ };
104
+
105
+ export { type CausalRangeGeometry, type CausalRangeGeometryOptions, type CausalRangeLine, type CausalRangePivot, createCausalRangeGeometryEngine };
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/range-geometry.ts
21
+ var range_geometry_exports = {};
22
+ __export(range_geometry_exports, {
23
+ createCausalRangeGeometryEngine: () => createCausalRangeGeometryEngine
24
+ });
25
+ module.exports = __toCommonJS(range_geometry_exports);
26
+ var getEmptyGeometry = () => ({
27
+ ready: false,
28
+ detected: false,
29
+ upperPrice: null,
30
+ lowerPrice: null,
31
+ centerPrice: null,
32
+ position: null,
33
+ widthAtr: null,
34
+ centerSlopeAtrPerBar: null,
35
+ boundaryDivergenceAtr: null,
36
+ containmentRatio: null,
37
+ highPivotCount: 0,
38
+ lowPivotCount: 0,
39
+ rangeAgeBars: 0,
40
+ breakoutDirection: null,
41
+ volatilityExpansionRatio: null,
42
+ volatilityExpansion: false,
43
+ upperLine: null,
44
+ lowerLine: null,
45
+ centerLine: null,
46
+ pivots: [],
47
+ historySize: 0,
48
+ pivotHistorySize: 0
49
+ });
50
+ var fitRegression = (pivots, currentBarIndex) => {
51
+ if (pivots.length < 2) return null;
52
+ const points = pivots.map((pivot) => ({
53
+ x: pivot.barIndex - currentBarIndex,
54
+ y: pivot.price
55
+ }));
56
+ const meanX = points.reduce((sum, point) => sum + point.x, 0) / points.length;
57
+ const meanY = points.reduce((sum, point) => sum + point.y, 0) / points.length;
58
+ let covariance = 0;
59
+ let variance = 0;
60
+ for (const point of points) {
61
+ const offsetX = point.x - meanX;
62
+ covariance += offsetX * (point.y - meanY);
63
+ variance += offsetX * offsetX;
64
+ }
65
+ if (variance <= Number.EPSILON) return null;
66
+ const slopePerBar = covariance / variance;
67
+ return {
68
+ currentValue: meanY - slopePerBar * meanX,
69
+ slopePerBar
70
+ };
71
+ };
72
+ var hasConfirmedPivotHigh = ({
73
+ candles,
74
+ candidateIndex,
75
+ leftBars,
76
+ rightBars
77
+ }) => {
78
+ const candidate = candles[candidateIndex];
79
+ if (!candidate) return false;
80
+ for (let index = candidateIndex - leftBars; index <= candidateIndex + rightBars; index += 1) {
81
+ if (index === candidateIndex) continue;
82
+ if ((candles[index]?.high ?? Number.POSITIVE_INFINITY) >= candidate.high) {
83
+ return false;
84
+ }
85
+ }
86
+ return true;
87
+ };
88
+ var hasConfirmedPivotLow = ({
89
+ candles,
90
+ candidateIndex,
91
+ leftBars,
92
+ rightBars
93
+ }) => {
94
+ const candidate = candles[candidateIndex];
95
+ if (!candidate) return false;
96
+ for (let index = candidateIndex - leftBars; index <= candidateIndex + rightBars; index += 1) {
97
+ if (index === candidateIndex) continue;
98
+ if ((candles[index]?.low ?? Number.NEGATIVE_INFINITY) <= candidate.low) {
99
+ return false;
100
+ }
101
+ }
102
+ return true;
103
+ };
104
+ var isFiniteCandle = (candle) => [candle.timestamp, candle.open, candle.high, candle.low, candle.close].every(
105
+ Number.isFinite
106
+ );
107
+ var createCausalRangeGeometryEngine = ({
108
+ options
109
+ }) => {
110
+ const candleLimit = Math.max(
111
+ options.lookbackBars,
112
+ options.pivotLeftBars + options.pivotRightBars + 1
113
+ );
114
+ const candles = [];
115
+ const pivots = [];
116
+ const atrHistory = [];
117
+ let barIndex = -1;
118
+ let lastTimestamp = null;
119
+ let geometry = getEmptyGeometry();
120
+ const next = (candle, atr) => {
121
+ if (!isFiniteCandle(candle)) return geometry;
122
+ if (lastTimestamp != null && candle.timestamp <= lastTimestamp) {
123
+ return geometry;
124
+ }
125
+ lastTimestamp = candle.timestamp;
126
+ barIndex += 1;
127
+ candles.push({ ...candle, barIndex });
128
+ if (candles.length > candleLimit) {
129
+ candles.splice(0, candles.length - candleLimit);
130
+ }
131
+ const finiteAtr = Number.isFinite(atr) && atr > 0 ? atr : null;
132
+ if (finiteAtr != null) {
133
+ atrHistory.push(finiteAtr);
134
+ if (atrHistory.length > options.lookbackBars) {
135
+ atrHistory.splice(0, atrHistory.length - options.lookbackBars);
136
+ }
137
+ }
138
+ const candidateArrayIndex = candles.length - 1 - options.pivotRightBars;
139
+ if (candidateArrayIndex >= options.pivotLeftBars) {
140
+ const candidate = candles[candidateArrayIndex];
141
+ if (hasConfirmedPivotHigh({
142
+ candles,
143
+ candidateIndex: candidateArrayIndex,
144
+ leftBars: options.pivotLeftBars,
145
+ rightBars: options.pivotRightBars
146
+ })) {
147
+ pivots.push({
148
+ kind: "high",
149
+ barIndex: candidate.barIndex,
150
+ timestamp: candidate.timestamp,
151
+ price: candidate.high
152
+ });
153
+ }
154
+ if (hasConfirmedPivotLow({
155
+ candles,
156
+ candidateIndex: candidateArrayIndex,
157
+ leftBars: options.pivotLeftBars,
158
+ rightBars: options.pivotRightBars
159
+ })) {
160
+ pivots.push({
161
+ kind: "low",
162
+ barIndex: candidate.barIndex,
163
+ timestamp: candidate.timestamp,
164
+ price: candidate.low
165
+ });
166
+ }
167
+ }
168
+ const firstRetainedBarIndex = barIndex - options.lookbackBars + 1;
169
+ while (pivots.length > 0 && pivots[0].barIndex < firstRetainedBarIndex) {
170
+ pivots.shift();
171
+ }
172
+ const highPivots = pivots.filter((pivot) => pivot.kind === "high");
173
+ const lowPivots = pivots.filter((pivot) => pivot.kind === "low");
174
+ const upper = fitRegression(highPivots, barIndex);
175
+ const lower = fitRegression(lowPivots, barIndex);
176
+ const volatilityBaseline = atrHistory.length > 0 ? atrHistory.reduce((sum, value) => sum + value, 0) / atrHistory.length : null;
177
+ const volatilityExpansionRatio = finiteAtr != null && volatilityBaseline != null && volatilityBaseline > 0 ? finiteAtr / volatilityBaseline : null;
178
+ const volatilityExpansion = options.maxVolatilityExpansion > 0 && volatilityExpansionRatio != null && volatilityExpansionRatio > options.maxVolatilityExpansion;
179
+ if (upper == null || lower == null || finiteAtr == null || upper.currentValue <= lower.currentValue) {
180
+ geometry = {
181
+ ...getEmptyGeometry(),
182
+ highPivotCount: highPivots.length,
183
+ lowPivotCount: lowPivots.length,
184
+ volatilityExpansionRatio,
185
+ volatilityExpansion,
186
+ pivots: pivots.map((pivot) => ({ ...pivot })),
187
+ historySize: candles.length,
188
+ pivotHistorySize: pivots.length
189
+ };
190
+ return geometry;
191
+ }
192
+ const oldestPivotIndex = Math.min(...pivots.map((pivot) => pivot.barIndex));
193
+ const firstCandle = options.lineStartMode === "history" ? candles[0] : candles.find((item) => item.barIndex >= oldestPivotIndex) ?? candles[0];
194
+ const startOffset = firstCandle.barIndex - barIndex;
195
+ const upperStartPrice = upper.currentValue + upper.slopePerBar * startOffset;
196
+ const lowerStartPrice = lower.currentValue + lower.slopePerBar * startOffset;
197
+ const centerPrice = (upper.currentValue + lower.currentValue) / 2;
198
+ const centerStartPrice = (upperStartPrice + lowerStartPrice) / 2;
199
+ const width = upper.currentValue - lower.currentValue;
200
+ const widthAtr = width / finiteAtr;
201
+ const centerSlopeAtrPerBar = (upper.slopePerBar + lower.slopePerBar) / 2 / finiteAtr;
202
+ const spanBars = Math.max(1, barIndex - firstCandle.barIndex);
203
+ const boundaryDivergenceAtr = Math.abs(upper.slopePerBar - lower.slopePerBar) * spanBars / finiteAtr;
204
+ const tolerance = finiteAtr * options.containmentToleranceAtr;
205
+ const containedCandles = candles.filter((item) => {
206
+ const offset = item.barIndex - barIndex;
207
+ const projectedUpper = upper.currentValue + upper.slopePerBar * offset + tolerance;
208
+ const projectedLower = lower.currentValue + lower.slopePerBar * offset - tolerance;
209
+ return item.close <= projectedUpper && item.close >= projectedLower;
210
+ }).length;
211
+ const containmentRatio = candles.length > 0 ? containedCandles / candles.length : 0;
212
+ const rangeAgeBars = spanBars + 1;
213
+ const enoughPivots = highPivots.length >= options.minPivotsPerSide && lowPivots.length >= options.minPivotsPerSide;
214
+ const ready = enoughPivots;
215
+ const detected = ready && rangeAgeBars >= options.minRangeAgeBars && widthAtr >= options.minWidthAtr && (options.maxWidthAtr === 0 || widthAtr <= options.maxWidthAtr) && Math.abs(centerSlopeAtrPerBar) <= options.maxCenterSlopeAtrPerBar && boundaryDivergenceAtr <= options.maxBoundaryDivergenceAtr && containmentRatio >= options.minContainmentRatio && !volatilityExpansion;
216
+ const breakoutBuffer = finiteAtr * options.breakoutToleranceAtr;
217
+ const breakoutDirection = candle.close > upper.currentValue + breakoutBuffer ? "UP" : candle.close < lower.currentValue - breakoutBuffer ? "DOWN" : null;
218
+ const endTimestamp = candle.timestamp;
219
+ geometry = {
220
+ ready,
221
+ detected,
222
+ upperPrice: upper.currentValue,
223
+ lowerPrice: lower.currentValue,
224
+ centerPrice,
225
+ position: width > 0 ? (candle.close - lower.currentValue) / width : null,
226
+ widthAtr,
227
+ centerSlopeAtrPerBar,
228
+ boundaryDivergenceAtr,
229
+ containmentRatio,
230
+ highPivotCount: highPivots.length,
231
+ lowPivotCount: lowPivots.length,
232
+ rangeAgeBars,
233
+ breakoutDirection,
234
+ volatilityExpansionRatio,
235
+ volatilityExpansion,
236
+ upperLine: {
237
+ startTimestamp: firstCandle.timestamp,
238
+ startPrice: upperStartPrice,
239
+ endTimestamp,
240
+ endPrice: upper.currentValue
241
+ },
242
+ lowerLine: {
243
+ startTimestamp: firstCandle.timestamp,
244
+ startPrice: lowerStartPrice,
245
+ endTimestamp,
246
+ endPrice: lower.currentValue
247
+ },
248
+ centerLine: {
249
+ startTimestamp: firstCandle.timestamp,
250
+ startPrice: centerStartPrice,
251
+ endTimestamp,
252
+ endPrice: centerPrice
253
+ },
254
+ pivots: pivots.map((pivot) => ({ ...pivot })),
255
+ historySize: candles.length,
256
+ pivotHistorySize: pivots.length
257
+ };
258
+ return geometry;
259
+ };
260
+ return {
261
+ next,
262
+ getState: () => ({
263
+ ...geometry,
264
+ pivots: geometry.pivots.map((pivot) => ({ ...pivot })),
265
+ upperLine: geometry.upperLine ? { ...geometry.upperLine } : null,
266
+ lowerLine: geometry.lowerLine ? { ...geometry.lowerLine } : null,
267
+ centerLine: geometry.centerLine ? { ...geometry.centerLine } : null
268
+ })
269
+ };
270
+ };
271
+ // Annotate the CommonJS export names for ESM import in node:
272
+ 0 && (module.exports = {
273
+ createCausalRangeGeometryEngine
274
+ });
@@ -0,0 +1,249 @@
1
+ // src/range-geometry.ts
2
+ var getEmptyGeometry = () => ({
3
+ ready: false,
4
+ detected: false,
5
+ upperPrice: null,
6
+ lowerPrice: null,
7
+ centerPrice: null,
8
+ position: null,
9
+ widthAtr: null,
10
+ centerSlopeAtrPerBar: null,
11
+ boundaryDivergenceAtr: null,
12
+ containmentRatio: null,
13
+ highPivotCount: 0,
14
+ lowPivotCount: 0,
15
+ rangeAgeBars: 0,
16
+ breakoutDirection: null,
17
+ volatilityExpansionRatio: null,
18
+ volatilityExpansion: false,
19
+ upperLine: null,
20
+ lowerLine: null,
21
+ centerLine: null,
22
+ pivots: [],
23
+ historySize: 0,
24
+ pivotHistorySize: 0
25
+ });
26
+ var fitRegression = (pivots, currentBarIndex) => {
27
+ if (pivots.length < 2) return null;
28
+ const points = pivots.map((pivot) => ({
29
+ x: pivot.barIndex - currentBarIndex,
30
+ y: pivot.price
31
+ }));
32
+ const meanX = points.reduce((sum, point) => sum + point.x, 0) / points.length;
33
+ const meanY = points.reduce((sum, point) => sum + point.y, 0) / points.length;
34
+ let covariance = 0;
35
+ let variance = 0;
36
+ for (const point of points) {
37
+ const offsetX = point.x - meanX;
38
+ covariance += offsetX * (point.y - meanY);
39
+ variance += offsetX * offsetX;
40
+ }
41
+ if (variance <= Number.EPSILON) return null;
42
+ const slopePerBar = covariance / variance;
43
+ return {
44
+ currentValue: meanY - slopePerBar * meanX,
45
+ slopePerBar
46
+ };
47
+ };
48
+ var hasConfirmedPivotHigh = ({
49
+ candles,
50
+ candidateIndex,
51
+ leftBars,
52
+ rightBars
53
+ }) => {
54
+ const candidate = candles[candidateIndex];
55
+ if (!candidate) return false;
56
+ for (let index = candidateIndex - leftBars; index <= candidateIndex + rightBars; index += 1) {
57
+ if (index === candidateIndex) continue;
58
+ if ((candles[index]?.high ?? Number.POSITIVE_INFINITY) >= candidate.high) {
59
+ return false;
60
+ }
61
+ }
62
+ return true;
63
+ };
64
+ var hasConfirmedPivotLow = ({
65
+ candles,
66
+ candidateIndex,
67
+ leftBars,
68
+ rightBars
69
+ }) => {
70
+ const candidate = candles[candidateIndex];
71
+ if (!candidate) return false;
72
+ for (let index = candidateIndex - leftBars; index <= candidateIndex + rightBars; index += 1) {
73
+ if (index === candidateIndex) continue;
74
+ if ((candles[index]?.low ?? Number.NEGATIVE_INFINITY) <= candidate.low) {
75
+ return false;
76
+ }
77
+ }
78
+ return true;
79
+ };
80
+ var isFiniteCandle = (candle) => [candle.timestamp, candle.open, candle.high, candle.low, candle.close].every(
81
+ Number.isFinite
82
+ );
83
+ var createCausalRangeGeometryEngine = ({
84
+ options
85
+ }) => {
86
+ const candleLimit = Math.max(
87
+ options.lookbackBars,
88
+ options.pivotLeftBars + options.pivotRightBars + 1
89
+ );
90
+ const candles = [];
91
+ const pivots = [];
92
+ const atrHistory = [];
93
+ let barIndex = -1;
94
+ let lastTimestamp = null;
95
+ let geometry = getEmptyGeometry();
96
+ const next = (candle, atr) => {
97
+ if (!isFiniteCandle(candle)) return geometry;
98
+ if (lastTimestamp != null && candle.timestamp <= lastTimestamp) {
99
+ return geometry;
100
+ }
101
+ lastTimestamp = candle.timestamp;
102
+ barIndex += 1;
103
+ candles.push({ ...candle, barIndex });
104
+ if (candles.length > candleLimit) {
105
+ candles.splice(0, candles.length - candleLimit);
106
+ }
107
+ const finiteAtr = Number.isFinite(atr) && atr > 0 ? atr : null;
108
+ if (finiteAtr != null) {
109
+ atrHistory.push(finiteAtr);
110
+ if (atrHistory.length > options.lookbackBars) {
111
+ atrHistory.splice(0, atrHistory.length - options.lookbackBars);
112
+ }
113
+ }
114
+ const candidateArrayIndex = candles.length - 1 - options.pivotRightBars;
115
+ if (candidateArrayIndex >= options.pivotLeftBars) {
116
+ const candidate = candles[candidateArrayIndex];
117
+ if (hasConfirmedPivotHigh({
118
+ candles,
119
+ candidateIndex: candidateArrayIndex,
120
+ leftBars: options.pivotLeftBars,
121
+ rightBars: options.pivotRightBars
122
+ })) {
123
+ pivots.push({
124
+ kind: "high",
125
+ barIndex: candidate.barIndex,
126
+ timestamp: candidate.timestamp,
127
+ price: candidate.high
128
+ });
129
+ }
130
+ if (hasConfirmedPivotLow({
131
+ candles,
132
+ candidateIndex: candidateArrayIndex,
133
+ leftBars: options.pivotLeftBars,
134
+ rightBars: options.pivotRightBars
135
+ })) {
136
+ pivots.push({
137
+ kind: "low",
138
+ barIndex: candidate.barIndex,
139
+ timestamp: candidate.timestamp,
140
+ price: candidate.low
141
+ });
142
+ }
143
+ }
144
+ const firstRetainedBarIndex = barIndex - options.lookbackBars + 1;
145
+ while (pivots.length > 0 && pivots[0].barIndex < firstRetainedBarIndex) {
146
+ pivots.shift();
147
+ }
148
+ const highPivots = pivots.filter((pivot) => pivot.kind === "high");
149
+ const lowPivots = pivots.filter((pivot) => pivot.kind === "low");
150
+ const upper = fitRegression(highPivots, barIndex);
151
+ const lower = fitRegression(lowPivots, barIndex);
152
+ const volatilityBaseline = atrHistory.length > 0 ? atrHistory.reduce((sum, value) => sum + value, 0) / atrHistory.length : null;
153
+ const volatilityExpansionRatio = finiteAtr != null && volatilityBaseline != null && volatilityBaseline > 0 ? finiteAtr / volatilityBaseline : null;
154
+ const volatilityExpansion = options.maxVolatilityExpansion > 0 && volatilityExpansionRatio != null && volatilityExpansionRatio > options.maxVolatilityExpansion;
155
+ if (upper == null || lower == null || finiteAtr == null || upper.currentValue <= lower.currentValue) {
156
+ geometry = {
157
+ ...getEmptyGeometry(),
158
+ highPivotCount: highPivots.length,
159
+ lowPivotCount: lowPivots.length,
160
+ volatilityExpansionRatio,
161
+ volatilityExpansion,
162
+ pivots: pivots.map((pivot) => ({ ...pivot })),
163
+ historySize: candles.length,
164
+ pivotHistorySize: pivots.length
165
+ };
166
+ return geometry;
167
+ }
168
+ const oldestPivotIndex = Math.min(...pivots.map((pivot) => pivot.barIndex));
169
+ const firstCandle = options.lineStartMode === "history" ? candles[0] : candles.find((item) => item.barIndex >= oldestPivotIndex) ?? candles[0];
170
+ const startOffset = firstCandle.barIndex - barIndex;
171
+ const upperStartPrice = upper.currentValue + upper.slopePerBar * startOffset;
172
+ const lowerStartPrice = lower.currentValue + lower.slopePerBar * startOffset;
173
+ const centerPrice = (upper.currentValue + lower.currentValue) / 2;
174
+ const centerStartPrice = (upperStartPrice + lowerStartPrice) / 2;
175
+ const width = upper.currentValue - lower.currentValue;
176
+ const widthAtr = width / finiteAtr;
177
+ const centerSlopeAtrPerBar = (upper.slopePerBar + lower.slopePerBar) / 2 / finiteAtr;
178
+ const spanBars = Math.max(1, barIndex - firstCandle.barIndex);
179
+ const boundaryDivergenceAtr = Math.abs(upper.slopePerBar - lower.slopePerBar) * spanBars / finiteAtr;
180
+ const tolerance = finiteAtr * options.containmentToleranceAtr;
181
+ const containedCandles = candles.filter((item) => {
182
+ const offset = item.barIndex - barIndex;
183
+ const projectedUpper = upper.currentValue + upper.slopePerBar * offset + tolerance;
184
+ const projectedLower = lower.currentValue + lower.slopePerBar * offset - tolerance;
185
+ return item.close <= projectedUpper && item.close >= projectedLower;
186
+ }).length;
187
+ const containmentRatio = candles.length > 0 ? containedCandles / candles.length : 0;
188
+ const rangeAgeBars = spanBars + 1;
189
+ const enoughPivots = highPivots.length >= options.minPivotsPerSide && lowPivots.length >= options.minPivotsPerSide;
190
+ const ready = enoughPivots;
191
+ const detected = ready && rangeAgeBars >= options.minRangeAgeBars && widthAtr >= options.minWidthAtr && (options.maxWidthAtr === 0 || widthAtr <= options.maxWidthAtr) && Math.abs(centerSlopeAtrPerBar) <= options.maxCenterSlopeAtrPerBar && boundaryDivergenceAtr <= options.maxBoundaryDivergenceAtr && containmentRatio >= options.minContainmentRatio && !volatilityExpansion;
192
+ const breakoutBuffer = finiteAtr * options.breakoutToleranceAtr;
193
+ const breakoutDirection = candle.close > upper.currentValue + breakoutBuffer ? "UP" : candle.close < lower.currentValue - breakoutBuffer ? "DOWN" : null;
194
+ const endTimestamp = candle.timestamp;
195
+ geometry = {
196
+ ready,
197
+ detected,
198
+ upperPrice: upper.currentValue,
199
+ lowerPrice: lower.currentValue,
200
+ centerPrice,
201
+ position: width > 0 ? (candle.close - lower.currentValue) / width : null,
202
+ widthAtr,
203
+ centerSlopeAtrPerBar,
204
+ boundaryDivergenceAtr,
205
+ containmentRatio,
206
+ highPivotCount: highPivots.length,
207
+ lowPivotCount: lowPivots.length,
208
+ rangeAgeBars,
209
+ breakoutDirection,
210
+ volatilityExpansionRatio,
211
+ volatilityExpansion,
212
+ upperLine: {
213
+ startTimestamp: firstCandle.timestamp,
214
+ startPrice: upperStartPrice,
215
+ endTimestamp,
216
+ endPrice: upper.currentValue
217
+ },
218
+ lowerLine: {
219
+ startTimestamp: firstCandle.timestamp,
220
+ startPrice: lowerStartPrice,
221
+ endTimestamp,
222
+ endPrice: lower.currentValue
223
+ },
224
+ centerLine: {
225
+ startTimestamp: firstCandle.timestamp,
226
+ startPrice: centerStartPrice,
227
+ endTimestamp,
228
+ endPrice: centerPrice
229
+ },
230
+ pivots: pivots.map((pivot) => ({ ...pivot })),
231
+ historySize: candles.length,
232
+ pivotHistorySize: pivots.length
233
+ };
234
+ return geometry;
235
+ };
236
+ return {
237
+ next,
238
+ getState: () => ({
239
+ ...geometry,
240
+ pivots: geometry.pivots.map((pivot) => ({ ...pivot })),
241
+ upperLine: geometry.upperLine ? { ...geometry.upperLine } : null,
242
+ lowerLine: geometry.lowerLine ? { ...geometry.lowerLine } : null,
243
+ centerLine: geometry.centerLine ? { ...geometry.centerLine } : null
244
+ })
245
+ };
246
+ };
247
+ export {
248
+ createCausalRangeGeometryEngine
249
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/indicators",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "MIT-licensed indicator plugin catalog for the TradeJS framework.",
5
5
  "keywords": [
6
6
  "tradejs",
@@ -29,11 +29,16 @@
29
29
  "types": "./dist/index.d.ts",
30
30
  "import": "./dist/index.mjs",
31
31
  "require": "./dist/index.js"
32
+ },
33
+ "./range-geometry": {
34
+ "types": "./dist/range-geometry.d.ts",
35
+ "import": "./dist/range-geometry.mjs",
36
+ "require": "./dist/range-geometry.js"
32
37
  }
33
38
  },
34
39
  "dependencies": {
35
- "@tradejs/core": "^3.0.0",
36
- "@tradejs/types": "^3.0.0",
40
+ "@tradejs/core": "^3.0.1",
41
+ "@tradejs/types": "^3.0.1",
37
42
  "fast-technical-indicators": "^1.1.4",
38
43
  "klinecharts": "10.0.0-alpha9",
39
44
  "technicalindicators": "^3.1.0"