Skip to content

Commit 5a2ab31

Browse files
committed
Add TA Methods : barssince, correlation highestbars, lowestbars, mode, percentile* percentrank, valuewhen
1 parent 621e596 commit 5a2ab31

File tree

16 files changed

+1099
-172
lines changed

16 files changed

+1099
-172
lines changed

docs/api-coverage/ta.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,13 @@ All functions listed below are verified to exist in Pine Script v5.
8989
| `ta.highest()` || Highest Value |
9090
| `ta.lowest()` || Lowest Value |
9191
| `ta.median()` || Median Value |
92-
| `ta.mode()` || Mode Value |
93-
| `ta.highestbars()` || Bars Since Highest |
94-
| `ta.lowestbars()` || Bars Since Lowest |
95-
| `ta.percentrank()` || Percentile Rank |
96-
| `ta.percentile_linear_interpolation()` || Percentile (Linear) |
97-
| `ta.percentile_nearest_rank()` || Percentile (Nearest Rank) |
98-
| `ta.correlation()` || Correlation Coefficient |
99-
| `ta.covariance()` || Covariance |
92+
| `ta.mode()` || Mode Value |
93+
| `ta.highestbars()` || Bars Since Highest |
94+
| `ta.lowestbars()` || Bars Since Lowest |
95+
| `ta.percentrank()` || Percentile Rank |
96+
| `ta.percentile_linear_interpolation()` || Percentile (Linear) |
97+
| `ta.percentile_nearest_rank()` || Percentile (Nearest Rank) |
98+
| `ta.correlation()` || Correlation Coefficient |
10099

101100
### Support & Resistance
102101

@@ -110,7 +109,7 @@ All functions listed below are verified to exist in Pine Script v5.
110109
| Function | Status | Description |
111110
| ---------------- | ------ | ------------------------ |
112111
| `ta.valuewhen()` || Value When Condition Met |
113-
| `ta.barssince()` | | Bars Since Condition |
112+
| `ta.barssince()` | | Bars Since Condition |
114113
| `ta.cum()` || Cumulative Sum |
115114

116115
### Notes
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
3+
import { Series } from '../../../Series';
4+
5+
/**
6+
* Bars Since
7+
*
8+
* Counts the number of bars since the last time the condition was true.
9+
*/
10+
export function barssince(context: any) {
11+
return (condition: any, _callId?: string) => {
12+
if (!context.taState) context.taState = {};
13+
const stateKey = _callId || 'barssince';
14+
15+
if (!context.taState[stateKey]) {
16+
context.taState[stateKey] = {
17+
lastTrueIndex: null,
18+
};
19+
}
20+
const state = context.taState[stateKey];
21+
22+
const cond = Series.from(condition).get(0);
23+
24+
if (cond) {
25+
state.lastTrueIndex = context.idx;
26+
return 0;
27+
}
28+
29+
if (state.lastTrueIndex === null) {
30+
return NaN;
31+
}
32+
33+
return context.idx - state.lastTrueIndex;
34+
};
35+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
3+
import { Series } from '../../../Series';
4+
5+
/**
6+
* Correlation Coefficient
7+
*
8+
* Describes the degree to which two series tend to deviate from their ta.sma() values.
9+
* r = cov(X, Y) / (stdev(X) * stdev(Y))
10+
*/
11+
export function correlation(context: any) {
12+
return (source1: any, source2: any, _length: any, _callId?: string) => {
13+
const length = Series.from(_length).get(0);
14+
const s1 = Series.from(source1);
15+
const s2 = Series.from(source2);
16+
17+
if (context.idx < length - 1) {
18+
return NaN;
19+
}
20+
21+
let sumX = 0;
22+
let sumY = 0;
23+
let sumXY = 0;
24+
let sumX2 = 0;
25+
let sumY2 = 0;
26+
let count = 0;
27+
28+
for (let i = 0; i < length; i++) {
29+
const x = s1.get(i);
30+
const y = s2.get(i);
31+
32+
if (isNaN(x) || isNaN(y)) continue;
33+
34+
sumX += x;
35+
sumY += y;
36+
sumXY += x * y;
37+
sumX2 += x * x;
38+
sumY2 += y * y;
39+
count++;
40+
}
41+
42+
if (count < 2) return NaN; // Need at least 2 points
43+
44+
const numerator = count * sumXY - sumX * sumY;
45+
const denominatorX = count * sumX2 - sumX * sumX;
46+
const denominatorY = count * sumY2 - sumY * sumY;
47+
48+
if (denominatorX <= 0 || denominatorY <= 0) return context.precision(0);
49+
50+
const r = numerator / Math.sqrt(denominatorX * denominatorY);
51+
return context.precision(r);
52+
};
53+
}
54+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
3+
import { Series } from '../../../Series';
4+
5+
/**
6+
* Highest Bars
7+
*
8+
* Returns the offset to the highest value over a given length.
9+
* Formula: Offset to the highest bar (negative value).
10+
*/
11+
export function highestbars(context: any) {
12+
return (source: any, _length: any, _callId?: string) => {
13+
const length = Series.from(_length).get(0);
14+
const series = Series.from(source);
15+
16+
if (context.idx < length - 1) {
17+
return NaN;
18+
}
19+
20+
let maxVal = -Infinity;
21+
let maxOffset = NaN;
22+
23+
for (let i = 0; i < length; i++) {
24+
const val = series.get(i);
25+
26+
if (isNaN(val)) continue;
27+
28+
if (isNaN(maxOffset) || val > maxVal) {
29+
maxVal = val;
30+
maxOffset = -i;
31+
}
32+
}
33+
34+
return maxOffset;
35+
};
36+
}
37+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
3+
import { Series } from '../../../Series';
4+
5+
/**
6+
* Lowest Bars
7+
*
8+
* Returns the offset to the lowest value over a given length.
9+
* Formula: Offset to the lowest bar (negative value).
10+
*/
11+
export function lowestbars(context: any) {
12+
return (source: any, _length: any, _callId?: string) => {
13+
const length = Series.from(_length).get(0);
14+
const series = Series.from(source);
15+
16+
if (context.idx < length - 1) {
17+
return NaN;
18+
}
19+
20+
let minVal = Infinity;
21+
let minOffset = NaN;
22+
23+
for (let i = 0; i < length; i++) {
24+
const val = series.get(i);
25+
26+
if (isNaN(val)) continue;
27+
28+
if (isNaN(minOffset) || val < minVal) {
29+
minVal = val;
30+
minOffset = -i;
31+
}
32+
}
33+
34+
return minOffset;
35+
};
36+
}
37+

src/namespaces/ta/methods/mode.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
3+
import { Series } from '../../../Series';
4+
5+
/**
6+
* Mode
7+
*
8+
* Returns the mode of the series. If there are several values with the same frequency, returns the smallest value.
9+
*/
10+
export function mode(context: any) {
11+
return (source: any, _length: any, _callId?: string) => {
12+
const length = Series.from(_length).get(0);
13+
const series = Series.from(source);
14+
15+
if (context.idx < length - 1) {
16+
return NaN;
17+
}
18+
19+
const counts = new Map<number, number>();
20+
21+
for (let i = 0; i < length; i++) {
22+
const val = series.get(i);
23+
if (isNaN(val)) continue;
24+
25+
counts.set(val, (counts.get(val) || 0) + 1);
26+
}
27+
28+
if (counts.size === 0) return NaN;
29+
30+
let modeVal = NaN;
31+
let maxFreq = -1;
32+
33+
for (const [val, freq] of counts.entries()) {
34+
if (freq > maxFreq) {
35+
maxFreq = freq;
36+
modeVal = val;
37+
} else if (freq === maxFreq) {
38+
if (val < modeVal) {
39+
modeVal = val;
40+
}
41+
}
42+
}
43+
44+
return modeVal; // Should I return int or float? Input determines it. Return value is as-is.
45+
};
46+
}
47+
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
3+
import { Series } from '../../../Series';
4+
5+
/**
6+
* Percentile Linear Interpolation
7+
*
8+
* Calculates percentile using method of linear interpolation between the two nearest ranks.
9+
*/
10+
export function percentile_linear_interpolation(context: any) {
11+
return (source: any, _length: any, _percentage: any, _callId?: string) => {
12+
const length = Series.from(_length).get(0);
13+
const percentage = Series.from(_percentage).get(0);
14+
const series = Series.from(source);
15+
16+
if (context.idx < length - 1) {
17+
return NaN;
18+
}
19+
20+
const values: number[] = [];
21+
for (let i = 0; i < length; i++) {
22+
const val = series.get(i);
23+
if (isNaN(val)) return NaN;
24+
values.push(val);
25+
}
26+
27+
values.sort((a, b) => a - b);
28+
29+
// Formula inferred from test data: index = (percentage / 100) * length - 0.5
30+
let index = (percentage / 100) * length - 0.5;
31+
32+
if (index < 0) index = 0;
33+
if (index > length - 1) index = length - 1;
34+
35+
const lowerIndex = Math.floor(index);
36+
const upperIndex = Math.ceil(index);
37+
38+
if (lowerIndex === upperIndex) {
39+
return context.precision(values[lowerIndex]);
40+
}
41+
42+
const fraction = index - lowerIndex;
43+
const result = values[lowerIndex] + fraction * (values[upperIndex] - values[lowerIndex]);
44+
45+
return context.precision(result);
46+
};
47+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
3+
import { Series } from '../../../Series';
4+
5+
/**
6+
* Percentile Nearest Rank
7+
*
8+
* Calculates percentile using method of Nearest Rank.
9+
* A percentile calculated using the Nearest Rank method will always be a member of the input data set.
10+
*/
11+
export function percentile_nearest_rank(context: any) {
12+
return (source: any, _length: any, _percentage: any, _callId?: string) => {
13+
const length = Series.from(_length).get(0);
14+
const percentage = Series.from(_percentage).get(0);
15+
const series = Series.from(source);
16+
17+
if (context.idx < length - 1) {
18+
return NaN;
19+
}
20+
21+
const values: number[] = [];
22+
for (let i = 0; i < length; i++) {
23+
const val = series.get(i);
24+
if (!isNaN(val)) {
25+
values.push(val);
26+
}
27+
}
28+
29+
if (values.length === 0) return NaN;
30+
31+
values.sort((a, b) => a - b);
32+
33+
// Nearest Rank: index = ceil(P/100 * N) - 1
34+
let index = Math.ceil((percentage / 100) * values.length) - 1;
35+
36+
if (index < 0) index = 0;
37+
if (index >= values.length) index = values.length - 1;
38+
39+
return context.precision(values[index]);
40+
};
41+
}
42+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
3+
import { Series } from '../../../Series';
4+
5+
/**
6+
* Percent Rank
7+
*
8+
* Returns the percentage of values in the last length previous bars that are less than or equal to the current value.
9+
*/
10+
export function percentrank(context: any) {
11+
return (source: any, _length: any, _callId?: string) => {
12+
const length = Series.from(_length).get(0);
13+
const series = Series.from(source);
14+
15+
if (context.idx < length) {
16+
return NaN;
17+
}
18+
19+
const currentValue = series.get(0);
20+
if (isNaN(currentValue)) return NaN;
21+
22+
let count = 0;
23+
let validValues = 0;
24+
25+
for (let i = 1; i <= length; i++) {
26+
const val = series.get(i);
27+
28+
if (isNaN(val)) continue;
29+
validValues++;
30+
31+
if (val <= currentValue) {
32+
count++;
33+
}
34+
}
35+
36+
if (validValues === 0) return NaN;
37+
38+
return context.precision((count / validValues) * 100);
39+
};
40+
}

0 commit comments

Comments
 (0)