Skip to content

Commit e04b0f3

Browse files
committed
unit tests for indicators
1 parent c32628e commit e04b0f3

15 files changed

Lines changed: 425 additions & 3 deletions

File tree

src/elder-ray.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ export class ElderRay {
3030
const bull = high - ema;
3131
const bear = low - ema;
3232
this.nextValue = (high: number, low: number, close: number) => {
33-
const ema = this.ema.nextValue(close);
34-
return { bull, bear };
33+
const e = this.ema.nextValue(close);
34+
return { bull: high - e, bear: low - e };
3535
};
3636
return { bull, bear };
3737
}

tests/cmo/cmo.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { CMO } from '../../src/cmo';
2+
3+
describe('CMO', () => {
4+
it('matches hand calculation for period 2', () => {
5+
const cmo = new CMO(2);
6+
expect(cmo.nextValue(100)).toBeUndefined();
7+
expect(cmo.nextValue(102)).toBeUndefined();
8+
const v = cmo.nextValue(101);
9+
// diffs: +2, -1 -> gains 2, losses 1 -> 100 * (2-1) / 3
10+
expect(v).toBeCloseTo(100 / 3, 5);
11+
});
12+
13+
it('returns 0 when gains and losses cancel in denominator edge', () => {
14+
const cmo = new CMO(1);
15+
cmo.nextValue(10);
16+
const v = cmo.nextValue(10);
17+
expect(v).toBe(0);
18+
});
19+
20+
it('momentValue matches a fresh instance with the same history', () => {
21+
const seq = [10, 11, 12, 11, 10];
22+
const a = new CMO(3);
23+
for (const x of seq) a.nextValue(x);
24+
const b = new CMO(3);
25+
for (const x of seq) b.nextValue(x);
26+
expect(b.momentValue(11)).toBeCloseTo(a.momentValue(11) as number, 10);
27+
});
28+
});

tests/dc/dc.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { DC } from '../../src/dc';
22
import { dcValues, ohlc } from './excel-data';
33

4-
describe.only('Donchian Channels', () => {
4+
describe('Donchian Channels', () => {
55
it('Excel Validate', () => {
66
const dc = new DC(21);
77
const EPSILON = 0.001;

tests/dmi/dmi.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { DMI } from '../../src/dmi';
2+
3+
describe('DMI', () => {
4+
it('is undefined on first bar', () => {
5+
const d = new DMI(14, false);
6+
expect(d.nextValue(10, 8, 9)).toBeUndefined();
7+
});
8+
9+
it('returns plusDI and minusDI as finite numbers when ready', () => {
10+
const d = new DMI(3, false);
11+
const highs = [10, 11, 12, 11, 10, 9, 10, 11, 12, 11, 10, 9, 10, 11, 12, 11, 10, 9, 10];
12+
const lows = [8, 9, 10, 9, 8, 7, 8, 9, 10, 9, 8, 7, 8, 9, 10, 9, 8, 7, 8];
13+
const closes = [9, 10, 11, 10, 9, 8, 9, 10, 11, 10, 9, 8, 9, 10, 11, 10, 9, 8, 9];
14+
let last: { plusDI: number; minusDI: number } | undefined;
15+
for (let i = 0; i < highs.length; i++) {
16+
const v = d.nextValue(highs[i], lows[i], closes[i]) as
17+
| { plusDI: number; minusDI: number }
18+
| undefined;
19+
if (v) last = v;
20+
}
21+
expect(last).toBeDefined();
22+
expect(Number.isFinite(last!.plusDI)).toBe(true);
23+
expect(Number.isFinite(last!.minusDI)).toBe(true);
24+
});
25+
26+
it('with ADX includes adx in result', () => {
27+
const d = new DMI(4, true);
28+
const h = [10, 12, 11, 10, 12, 11, 10, 12, 11, 10, 12];
29+
const l = [8, 9, 8, 7, 9, 8, 7, 9, 8, 7, 9];
30+
const c = [9, 11, 9, 8, 11, 9, 8, 11, 9, 8, 11];
31+
let last: { adx: number; plusDI: number; minusDI: number } | undefined;
32+
for (let i = 0; i < h.length; i++) {
33+
const v = d.nextValue(h[i], l[i], c[i]) as
34+
| { adx: number; plusDI: number; minusDI: number }
35+
| undefined;
36+
if (v && v.adx !== undefined) last = v;
37+
}
38+
expect(last).toBeDefined();
39+
expect(Number.isFinite(last!.adx)).toBe(true);
40+
});
41+
});

tests/dpo/dpo.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { DPO } from '../../src/dpo';
2+
3+
describe('DPO', () => {
4+
it('returns undefined until period + shift bars', () => {
5+
const dpo = new DPO(4);
6+
const shift = Math.floor(4 / 2 + 1);
7+
for (let i = 0; i < 4 + shift - 1; i++) {
8+
expect(dpo.nextValue(100 + i)).toBeUndefined();
9+
}
10+
const v = dpo.nextValue(100 + 4 + shift - 1);
11+
expect(v).toBeDefined();
12+
expect(typeof v).toBe('number');
13+
});
14+
15+
it('momentValue is stable with nextValue series', () => {
16+
const dpo = new DPO(5);
17+
const prices = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
18+
for (const p of prices) dpo.nextValue(p);
19+
const m = dpo.momentValue(11);
20+
const dpo2 = new DPO(5);
21+
for (let i = 0; i < prices.length; i++) dpo2.nextValue(prices[i]);
22+
const without = dpo2.momentValue(11);
23+
expect(m).toBeCloseTo(without as number, 10);
24+
});
25+
});

tests/elder-ray/elder-ray.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ElderRay } from '../../src/elder-ray';
2+
3+
describe('ElderRay', () => {
4+
it('bull and bear recompute on each nextValue after warmup', () => {
5+
const er = new ElderRay(3);
6+
er.nextValue(95, 90, 100);
7+
er.nextValue(100, 95, 100);
8+
const a = er.nextValue(105, 100, 102) as { bull: number; bear: number };
9+
const b = er.nextValue(110, 100, 104) as { bull: number; bear: number };
10+
expect(b.bull).toBeGreaterThan(a.bull);
11+
});
12+
13+
it('momentValue on a new bar matches nextValue when EMA is already warmed', () => {
14+
const period = 4;
15+
const er = new ElderRay(period);
16+
for (let i = 0; i < period - 1; i++) {
17+
er.nextValue(10 + i, 9 + i, 9.5 + i);
18+
}
19+
er.nextValue(20, 8, 12);
20+
const m = er.momentValue(22, 7, 13) as { bull: number; bear: number };
21+
const v = er.nextValue(22, 7, 13) as { bull: number; bear: number };
22+
expect(v.bull).toBeCloseTo(m.bull, 5);
23+
expect(v.bear).toBeCloseTo(m.bear, 5);
24+
});
25+
});

tests/envelopes/envelopes.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Envelopes } from '../../src/envelopes';
2+
import { SMA as SMA2 } from 'technicalindicators';
3+
import { closes } from '../macd/excel-data';
4+
5+
describe('Envelopes', () => {
6+
it('produces symmetric bands around SMA', () => {
7+
const e = new Envelopes(3, 10);
8+
e.nextValue(10);
9+
e.nextValue(10);
10+
const v = e.nextValue(10) as { lower: number; middle: number; upper: number };
11+
expect(v.middle).toBe(10);
12+
expect(v.upper).toBe(11);
13+
expect(v.lower).toBe(9);
14+
});
15+
16+
it('momentValue uses the same middle as SMA.momentValue', () => {
17+
const e = new Envelopes(4, 2);
18+
e.nextValue(1);
19+
e.nextValue(2);
20+
e.nextValue(3);
21+
e.nextValue(4);
22+
const m = e.momentValue(5) as { lower: number; middle: number; upper: number };
23+
expect(m.middle * 0.02).toBeCloseTo((m.upper - m.middle), 8);
24+
expect(m.middle * 0.02).toBeCloseTo((m.middle - m.lower), 8);
25+
});
26+
27+
it('Cross sdk: middle band matches technicalindicators SMA', () => {
28+
const period = 20;
29+
const pct = 2.5;
30+
const env = new Envelopes(period, pct);
31+
const refSma = new SMA2({ period, values: [] });
32+
closes.forEach((c) => {
33+
const e = env.nextValue(c) as { lower: number; middle: number; upper: number } | undefined;
34+
const m = refSma.nextValue(c);
35+
if (e && m !== undefined) {
36+
expect(e.middle).toBeCloseTo(m, 8);
37+
}
38+
});
39+
});
40+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ForceIndex } from '../../src/force-index';
2+
3+
describe('ForceIndex', () => {
4+
it('is undefined on the first close', () => {
5+
const f = new ForceIndex();
6+
expect(f.nextValue(100, 1000)).toBeUndefined();
7+
});
8+
9+
it('equals (close - prevClose) * volume', () => {
10+
const f = new ForceIndex();
11+
f.nextValue(100, 1000);
12+
expect(f.nextValue(102, 500)).toBe(2 * 500);
13+
expect(f.nextValue(99, 200)).toBe(-3 * 200);
14+
});
15+
16+
it('momentValue matches the formula without advancing state', () => {
17+
const f = new ForceIndex();
18+
f.nextValue(10, 100);
19+
f.nextValue(12, 200);
20+
const m = f.momentValue(11, 300);
21+
expect(m).toBe((11 - 12) * 300);
22+
});
23+
});

tests/fractal/fractal.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Fractal } from '../../src/fractal';
2+
3+
describe('Fractal', () => {
4+
it('detects a swing high (fractal up)', () => {
5+
const f = new Fractal(2, 2);
6+
const highs = [1, 1, 10, 1, 1];
7+
const lows = [0, 0, 0, 0, 0];
8+
let up: number | undefined;
9+
for (let i = 0; i < highs.length; i++) {
10+
const r = f.nextValue(highs[i], lows[i]);
11+
if (r && r.up !== undefined) up = r.up;
12+
}
13+
expect(up).toBe(10);
14+
});
15+
16+
it('with full window, momentValue returns a defined structure', () => {
17+
const f = new Fractal(1, 1);
18+
f.nextValue(1, 5);
19+
f.nextValue(3, 3);
20+
f.nextValue(2, 2);
21+
f.nextValue(1, 1);
22+
const m = f.momentValue(1, 1) as { up?: number; down?: number };
23+
expect(m).toBeDefined();
24+
expect('up' in m).toBe(true);
25+
expect('down' in m).toBe(true);
26+
});
27+
});

tests/ichimoku/ichimoku.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Ichimoku } from '../../src/ichimoku';
2+
3+
describe('Ichimoku', () => {
4+
it('returns flat lines on constant range with short periods', () => {
5+
const ichi = new Ichimoku(2, 2, 2, 1);
6+
const h = 100;
7+
const l = 100;
8+
const c = 100;
9+
ichi.nextValue(h, l, c);
10+
const v = ichi.nextValue(h, l, c);
11+
expect(v).toBeDefined();
12+
expect(v!.tenkan).toBe(100);
13+
expect(v!.kijun).toBe(100);
14+
expect(v!.senkouA).toBe(100);
15+
expect(v!.senkouB).toBe(100);
16+
});
17+
18+
it('tenkan reflects max/min over conversion period', () => {
19+
const ichi = new Ichimoku(2, 2, 2, 1);
20+
ichi.nextValue(8, 6, 7);
21+
const v = ichi.nextValue(12, 10, 11) as { tenkan: number; kijun: number };
22+
// max(8,12)=12, min(6,10)=6 -> (12+6)/2 = 9
23+
expect(v.tenkan).toBe(9);
24+
});
25+
});

0 commit comments

Comments
 (0)