@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 +14 -0
- package/dist/range-geometry.d.mts +105 -0
- package/dist/range-geometry.d.ts +105 -0
- package/dist/range-geometry.js +274 -0
- package/dist/range-geometry.mjs +249 -0
- package/package.json +8 -3
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.
|
|
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.
|
|
36
|
-
"@tradejs/types": "^3.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"
|