Skip to content

Commit 188d11a

Browse files
feat(transducers-stats): add moving min/max, update donchian
- add movingMaximum() transducer - add movingMinimum() transducer - refactor donchian() - add internal Deque helper class - add tests
1 parent 9049d81 commit 188d11a

File tree

6 files changed

+131
-10
lines changed

6 files changed

+131
-10
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { Predicate2 } from "@thi.ng/api";
2+
3+
/** @internal */
4+
export class Deque {
5+
index: number[] = [];
6+
7+
constructor(public samples: number[], public pred: Predicate2<number>) {}
8+
9+
head() {
10+
return this.samples[this.index[0]];
11+
}
12+
13+
add(x: number) {
14+
const { index, samples, pred } = this;
15+
while (index.length && pred(samples[index[index.length - 1]], x)) {
16+
index.pop();
17+
}
18+
index.push(samples.length - 1);
19+
}
20+
21+
shift() {
22+
const { index } = this;
23+
if (index[0] === 0) index.shift();
24+
for (let i = 0; i < index.length; i++) index[i]--;
25+
}
26+
}

packages/transducers-stats/src/donchian.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import type { Transducer } from "@thi.ng/transducers";
2-
import { comp } from "@thi.ng/transducers/comp";
3-
import { iterator } from "@thi.ng/transducers/iterator";
4-
import { map } from "@thi.ng/transducers/map";
5-
import { partition } from "@thi.ng/transducers/partition";
6-
import { bounds } from "./bounds.js";
1+
import type { Reducer, Transducer } from "@thi.ng/transducers";
2+
import { compR } from "@thi.ng/transducers/compr";
3+
import { iterator1 } from "@thi.ng/transducers/iterator";
4+
import { Deque } from "./deque.js";
75

86
/**
97
* Computes Donchian channel, i.e. min/max values for sliding window.
@@ -22,6 +20,23 @@ export function donchian(
2220
): IterableIterator<[number, number]>;
2321
export function donchian(period: number, src?: Iterable<number>): any {
2422
return src
25-
? iterator(donchian(period), src)
26-
: comp<number, number[], number[]>(partition(period, 1), map(bounds));
23+
? iterator1(donchian(period), src)
24+
: (rfn: Reducer<[number, number], any>) => {
25+
const samples: number[] = [];
26+
const minDeque = new Deque(samples, (a, b) => a >= b);
27+
const maxDeque = new Deque(samples, (a, b) => a <= b);
28+
return compR(rfn, (acc: any, x: number) => {
29+
const num = samples.push(x);
30+
minDeque.add(x);
31+
maxDeque.add(x);
32+
if (num > period) {
33+
samples.shift();
34+
minDeque.shift();
35+
maxDeque.shift();
36+
}
37+
return num >= period
38+
? rfn[2](acc, [minDeque.head(), maxDeque.head()])
39+
: acc;
40+
});
41+
};
2742
}

packages/transducers-stats/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export * from "./ema.js";
44
export * from "./hma.js";
55
export * from "./macd.js";
66
export * from "./momentum.js";
7+
export * from "./moving-maximum.js";
8+
export * from "./moving-minimum.js";
79
export * from "./roc.js";
810
export * from "./rsi.js";
911
export * from "./sd.js";
@@ -13,5 +15,6 @@ export * from "./trix.js";
1315
export * from "./wma.js";
1416

1517
export * from "./bounds.js";
18+
export * from "./deque.js";
1619
export * from "./dot.js";
1720
export * from "./mse.js";
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Reducer, Transducer } from "@thi.ng/transducers";
2+
import { compR } from "@thi.ng/transducers/compr";
3+
import { iterator1 } from "@thi.ng/transducers/iterator";
4+
import { Deque } from "./deque.js";
5+
6+
export function movingMaximum(period: number): Transducer<number, number>;
7+
export function movingMaximum(
8+
period: number,
9+
src: Iterable<number>
10+
): IterableIterator<number>;
11+
export function movingMaximum(period: number, src?: Iterable<number>): any {
12+
return src
13+
? iterator1(movingMaximum(period), src)
14+
: (rfn: Reducer<number, any>) => {
15+
const samples: number[] = [];
16+
const deque = new Deque(samples, (a, b) => a <= b);
17+
return compR(rfn, (acc: any, x: number) => {
18+
const num = samples.push(x);
19+
deque.add(x);
20+
if (num > period) {
21+
samples.shift();
22+
deque.shift();
23+
}
24+
return num >= period ? rfn[2](acc, deque.head()) : acc;
25+
});
26+
};
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Reducer, Transducer } from "@thi.ng/transducers";
2+
import { compR } from "@thi.ng/transducers/compr";
3+
import { iterator1 } from "@thi.ng/transducers/iterator";
4+
import { Deque } from "./deque.js";
5+
6+
export function movingMinimum(period: number): Transducer<number, number>;
7+
export function movingMinimum(
8+
period: number,
9+
src: Iterable<number>
10+
): IterableIterator<number>;
11+
export function movingMinimum(period: number, src?: Iterable<number>): any {
12+
return src
13+
? iterator1(movingMinimum(period), src)
14+
: (rfn: Reducer<number, any>) => {
15+
const samples: number[] = [];
16+
const deque = new Deque(samples, (a, b) => a >= b);
17+
return compR(rfn, (acc: any, x: number) => {
18+
const num = samples.push(x);
19+
deque.add(x);
20+
if (num > period) {
21+
samples.shift();
22+
deque.shift();
23+
}
24+
return num >= period ? rfn[2](acc, deque.head()) : acc;
25+
});
26+
};
27+
}
Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,27 @@
11
import { expect, test } from "bun:test";
2-
// import * as transducers-stats from "../src/index.js"
2+
import { donchian, movingMaximum, movingMinimum } from "../src/index.js";
33

4-
test.todo("transducers-stats", () => {});
4+
test("movingMaxium", () => {
5+
expect([...movingMaximum(3, [1, 3, 1, 1, 4, 1, 1, 2, 5, 6])]).toEqual([
6+
3, 3, 4, 4, 4, 2, 5, 6,
7+
]);
8+
});
9+
10+
test("movingMinium", () => {
11+
expect([...movingMinimum(3, [1, 3, 1, 1, 4, 1, 1, 2, 5, 6])]).toEqual([
12+
1, 1, 1, 1, 1, 1, 1, 2,
13+
]);
14+
});
15+
16+
test("donchian", () => {
17+
expect([...donchian(3, [1, 3, 1, 1, 4, 1, 1, 2, 5, 6])]).toEqual([
18+
[1, 3],
19+
[1, 3],
20+
[1, 4],
21+
[1, 4],
22+
[1, 4],
23+
[1, 2],
24+
[1, 5],
25+
[2, 6],
26+
]);
27+
});

0 commit comments

Comments
 (0)