@zuttora/charts 0.1.0 → 0.3.0
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 +16 -2
- package/dist/chart.js +24 -0
- package/dist/chartState.d.ts +6 -0
- package/dist/chartState.js +5 -0
- package/dist/core/drawings.d.ts +21 -0
- package/dist/core/drawings.js +46 -0
- package/dist/core/indicatorSet.d.ts +20 -0
- package/dist/core/indicatorSet.js +66 -0
- package/dist/core/indicators.d.ts +53 -0
- package/dist/core/indicators.js +187 -0
- package/dist/core/types.d.ts +21 -0
- package/dist/dom/indicatorReadings.d.ts +7 -0
- package/dist/dom/indicatorReadings.js +26 -0
- package/dist/dom/legend.d.ts +5 -0
- package/dist/dom/legend.js +10 -1
- package/dist/dom/legendSync.js +8 -1
- package/dist/draw/controller.d.ts +36 -0
- package/dist/draw/controller.js +151 -0
- package/dist/draw/geometry.d.ts +44 -0
- package/dist/draw/geometry.js +118 -0
- package/dist/draw/mapper.d.ts +30 -0
- package/dist/draw/mapper.js +32 -0
- package/dist/draw/pointer.d.ts +17 -0
- package/dist/draw/pointer.js +144 -0
- package/dist/draw/state.d.ts +61 -0
- package/dist/draw/state.js +8 -0
- package/dist/drawApi.d.ts +20 -0
- package/dist/drawApi.js +59 -0
- package/dist/host.d.ts +3 -0
- package/dist/host.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/interact/attach.js +33 -18
- package/dist/render/drawCrosshair.js +6 -3
- package/dist/render/drawDrawings.d.ts +10 -0
- package/dist/render/drawDrawings.js +146 -0
- package/dist/render/drawIndicators.d.ts +4 -0
- package/dist/render/drawIndicators.js +67 -0
- package/dist/render/drawMain.js +13 -0
- package/dist/render/drawSubPanes.d.ts +2 -0
- package/dist/render/drawSubPanes.js +104 -0
- package/dist/render/frame.d.ts +4 -0
- package/dist/render/frameBuilder.js +12 -2
- package/dist/render/indicatorFrame.d.ts +17 -0
- package/dist/render/indicatorFrame.js +55 -0
- package/dist/render/paintFrame.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,20 @@ chart.on('needMore', (oldest) => { /* load older candles, then chart.prependData
|
|
|
20
20
|
chart.on('liveChange', (atLive) => { /* show or hide a "Go to live" button */ });
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Also: `replaceClosed`, `setOptions`, `setPriceLines`, `setMarkers`, `scrollToLive`, `fitAll`, `destroy`.
|
|
23
|
+
Also: `replaceClosed`, `setOptions`, `setPriceLines`, `setMarkers`, `setIndicators`, `setDrawingTool`, `setDrawings`, `getDrawings`, `updateDrawing`, `deleteSelected`, `clearDrawings`, `setDrawingColor`, `scrollToLive`, `fitAll`, `destroy`.
|
|
24
|
+
|
|
25
|
+
Drawing tools (`setDrawingTool(tool)`): `hline`, `trend`, `ray`, `rect`, `fib`, `note` and `measure` (a ruler that is never saved). Tap or click to place each point, or press and drag. A finished drawing is picked; drag it to move it, drag a square to move one point, `Delete` removes it, `Escape` backs out. Drawings sit at a time and a price, so they stay in place when the candle size changes.
|
|
26
|
+
The host owns saving: listen to `drawingsChange(list)`, store it, and restore with `setDrawings(list)`. `drawingEdit(drawing)` fires when a note needs its words (`updateDrawing(id, { text })`); `drawProgress` tells a hint line how far along the trader is; `setDrawingColor(color)` colours the next drawing and the picked one.
|
|
27
|
+
|
|
28
|
+
Indicators (`setIndicators(specs)`, diffed by `id`): `sma`, `ema` and `bollinger` are drawn over the candles; `rsi` and `macd` get their own pane under the price.
|
|
29
|
+
They are calculated from every loaded close, the forming candle included, so they move live.
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
chart.setIndicators([
|
|
33
|
+
{ id: 'sma20', type: 'sma', period: 20, color: '#FFD34D' },
|
|
34
|
+
{ id: 'rsi', type: 'rsi', period: 14, color: '#C792EA' },
|
|
35
|
+
]);
|
|
36
|
+
```
|
|
24
37
|
|
|
25
38
|
Gestures: drag to pan, wheel or pinch to zoom, press and hold (touch) or hover (mouse) for the
|
|
26
39
|
crosshair, drag the price axis to scale it, double-click or double-tap to reset, arrow keys, `+`/`-`,
|
|
@@ -31,7 +44,8 @@ crosshair, drag the price axis to scale it, double-click or double-tap to reset,
|
|
|
31
44
|
| | |
|
|
32
45
|
|---|---|
|
|
33
46
|
| Core, rendering, interaction (M3 to M5) | done |
|
|
34
|
-
| Indicators (M8) | `0.2.0` |
|
|
47
|
+
| Indicators (M8) | done, `0.2.0` |
|
|
48
|
+
| Drawing tools (M9) | done, `0.3.0` |
|
|
35
49
|
| Published to npm | after review (needs the `NPM_TOKEN` secret) |
|
|
36
50
|
|
|
37
51
|
## Develop
|
package/dist/chart.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createDrawApi } from "./drawApi.js";
|
|
1
2
|
import { Emitter } from "./core/emitter.js";
|
|
2
3
|
import { buildDom } from "./dom/root.js";
|
|
3
4
|
import { watchEnvironment } from "./dom/environment.js";
|
|
@@ -69,6 +70,15 @@ export function createChartCore(container, options) {
|
|
|
69
70
|
visible: invalidateAll,
|
|
70
71
|
});
|
|
71
72
|
const animNow = perf;
|
|
73
|
+
const drawApi = createDrawApi({
|
|
74
|
+
s,
|
|
75
|
+
root,
|
|
76
|
+
events,
|
|
77
|
+
invalidate,
|
|
78
|
+
lastFrame: () => lastFrame,
|
|
79
|
+
now: animNow,
|
|
80
|
+
isDestroyed: () => destroyed,
|
|
81
|
+
});
|
|
72
82
|
const chart = {
|
|
73
83
|
setData(closed, forming, hasMore) {
|
|
74
84
|
if (destroyed)
|
|
@@ -80,6 +90,7 @@ export function createChartCore(container, options) {
|
|
|
80
90
|
const keepT = hadData && !wasLive ? s.series.timeAt(Math.min(edge, s.series.lastIndex())) : null;
|
|
81
91
|
s.series.setData(closed, forming, hasMore);
|
|
82
92
|
s.haBars.rebuild();
|
|
93
|
+
s.indicators.rebuild(s.series);
|
|
83
94
|
s.vp.setLastIndex(s.series.lastIndex());
|
|
84
95
|
s.moreGate.clear();
|
|
85
96
|
s.closeTween.jump(forming ? forming.c : 0);
|
|
@@ -105,6 +116,7 @@ export function createChartCore(container, options) {
|
|
|
105
116
|
if (added > 0) {
|
|
106
117
|
s.vp.onPrepend(added);
|
|
107
118
|
s.haBars.rebuild();
|
|
119
|
+
s.indicators.rebuild(s.series);
|
|
108
120
|
}
|
|
109
121
|
s.moreGate.clear();
|
|
110
122
|
updateLegend();
|
|
@@ -115,6 +127,7 @@ export function createChartCore(container, options) {
|
|
|
115
127
|
return;
|
|
116
128
|
if (s.series.replaceClosed(candles) > 0) {
|
|
117
129
|
s.haBars.rebuild();
|
|
130
|
+
s.indicators.rebuild(s.series);
|
|
118
131
|
updateLegend();
|
|
119
132
|
invalidateAll();
|
|
120
133
|
}
|
|
@@ -125,6 +138,7 @@ export function createChartCore(container, options) {
|
|
|
125
138
|
const r = s.series.applyTick(price, time, s.options.intervalMs, s.options.tzOffsetMs);
|
|
126
139
|
if (r.kind === 'ignored')
|
|
127
140
|
return;
|
|
141
|
+
s.indicators.onTick(s.series, r);
|
|
128
142
|
const t = animNow();
|
|
129
143
|
if (r.kind === 'updated') {
|
|
130
144
|
s.closeTween.set(price, t, s.options.reducedMotion ? 0 : CLOSE_TWEEN_MS);
|
|
@@ -175,6 +189,15 @@ export function createChartCore(container, options) {
|
|
|
175
189
|
updateLegend();
|
|
176
190
|
invalidate('main');
|
|
177
191
|
},
|
|
192
|
+
setIndicators(specs) {
|
|
193
|
+
if (destroyed)
|
|
194
|
+
return;
|
|
195
|
+
s.indicators.setSpecs(specs, s.series);
|
|
196
|
+
s.rangeReady = false;
|
|
197
|
+
updateLegend();
|
|
198
|
+
invalidateAll();
|
|
199
|
+
},
|
|
200
|
+
...drawApi.methods,
|
|
178
201
|
scrollToLive() {
|
|
179
202
|
host.scrollToLive();
|
|
180
203
|
},
|
|
@@ -202,6 +225,7 @@ export function createChartCore(container, options) {
|
|
|
202
225
|
const host = createHost({
|
|
203
226
|
root,
|
|
204
227
|
state: s,
|
|
228
|
+
draw: drawApi.controller,
|
|
205
229
|
events,
|
|
206
230
|
invalidate,
|
|
207
231
|
invalidateAll,
|
package/dist/chartState.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { CandleSeries } from './core/series.ts';
|
|
2
2
|
import { PriceScale } from './core/priceScale.ts';
|
|
3
|
+
import type { Drawing } from './core/drawings.ts';
|
|
4
|
+
import { IndicatorSet } from './core/indicatorSet.ts';
|
|
5
|
+
import type { DrawState } from './draw/controller.ts';
|
|
3
6
|
import { MoreGate, Viewport } from './core/viewport.ts';
|
|
4
7
|
import type { ChartLabels, ChartOptions, Marker, PriceLine } from './core/types.ts';
|
|
5
8
|
import { HeikinBars, SeriesBars } from './render/bars.ts';
|
|
@@ -23,6 +26,9 @@ export interface ChartState {
|
|
|
23
26
|
moreGate: MoreGate;
|
|
24
27
|
lines: readonly PriceLine[];
|
|
25
28
|
markers: readonly Marker[];
|
|
29
|
+
indicators: IndicatorSet;
|
|
30
|
+
drawings: Drawing[];
|
|
31
|
+
draw: DrawState;
|
|
26
32
|
rangeMin: Tween;
|
|
27
33
|
rangeMax: Tween;
|
|
28
34
|
rangeReady: boolean;
|
package/dist/chartState.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { CandleSeries } from "./core/series.js";
|
|
2
2
|
import { PriceScale } from "./core/priceScale.js";
|
|
3
|
+
import { IndicatorSet } from "./core/indicatorSet.js";
|
|
4
|
+
import { createDrawState } from "./draw/controller.js";
|
|
3
5
|
import { MoreGate, Viewport } from "./core/viewport.js";
|
|
4
6
|
import { HeikinBars, SeriesBars } from "./render/bars.js";
|
|
5
7
|
import { Tween } from "./render/tween.js";
|
|
@@ -41,6 +43,9 @@ export function createState(options) {
|
|
|
41
43
|
moreGate: new MoreGate(),
|
|
42
44
|
lines: [],
|
|
43
45
|
markers: [],
|
|
46
|
+
indicators: new IndicatorSet(),
|
|
47
|
+
drawings: [],
|
|
48
|
+
draw: createDrawState(options.theme.accent),
|
|
44
49
|
rangeMin: new Tween(0),
|
|
45
50
|
rangeMax: new Tween(1),
|
|
46
51
|
rangeReady: false,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Time } from './types.ts';
|
|
2
|
+
export type DrawingTool = 'hline' | 'trend' | 'ray' | 'rect' | 'fib' | 'note' | 'measure';
|
|
3
|
+
export type DrawingKind = Exclude<DrawingTool, 'measure'>;
|
|
4
|
+
export interface Point {
|
|
5
|
+
t: Time;
|
|
6
|
+
price: number;
|
|
7
|
+
}
|
|
8
|
+
export interface Drawing {
|
|
9
|
+
id: string;
|
|
10
|
+
type: DrawingKind;
|
|
11
|
+
points: Point[];
|
|
12
|
+
color: string;
|
|
13
|
+
text?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const MAX_DRAWINGS = 100;
|
|
16
|
+
export declare const MAX_NOTE_LENGTH = 60;
|
|
17
|
+
export declare const FIB_LEVELS: readonly number[];
|
|
18
|
+
export declare function pointsNeeded(tool: DrawingTool): number;
|
|
19
|
+
export declare function isKind(tool: string): tool is DrawingKind;
|
|
20
|
+
export declare function newDrawingId(): string;
|
|
21
|
+
export declare function sanitizeDrawings(list: readonly unknown[]): Drawing[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const MAX_DRAWINGS = 100;
|
|
2
|
+
export const MAX_NOTE_LENGTH = 60;
|
|
3
|
+
export const FIB_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
|
|
4
|
+
const KINDS = ['hline', 'trend', 'ray', 'rect', 'fib', 'note'];
|
|
5
|
+
export function pointsNeeded(tool) {
|
|
6
|
+
return tool === 'hline' || tool === 'note' ? 1 : 2;
|
|
7
|
+
}
|
|
8
|
+
export function isKind(tool) {
|
|
9
|
+
return KINDS.includes(tool);
|
|
10
|
+
}
|
|
11
|
+
let counter = 0;
|
|
12
|
+
export function newDrawingId() {
|
|
13
|
+
counter += 1;
|
|
14
|
+
return `d${Date.now().toString(36)}${counter.toString(36)}`;
|
|
15
|
+
}
|
|
16
|
+
const finite = (n) => typeof n === 'number' && Number.isFinite(n);
|
|
17
|
+
export function sanitizeDrawings(list) {
|
|
18
|
+
const out = [];
|
|
19
|
+
const ids = new Set();
|
|
20
|
+
for (const raw of list) {
|
|
21
|
+
if (out.length >= MAX_DRAWINGS)
|
|
22
|
+
break;
|
|
23
|
+
if (typeof raw !== 'object' || raw === null)
|
|
24
|
+
continue;
|
|
25
|
+
const d = raw;
|
|
26
|
+
if (typeof d.id !== 'string' || ids.has(d.id) || typeof d.type !== 'string' || !isKind(d.type))
|
|
27
|
+
continue;
|
|
28
|
+
if (typeof d.color !== 'string' || !Array.isArray(d.points))
|
|
29
|
+
continue;
|
|
30
|
+
const need = pointsNeeded(d.type);
|
|
31
|
+
const points = [];
|
|
32
|
+
for (const p of d.points.slice(0, need)) {
|
|
33
|
+
const q = p;
|
|
34
|
+
if (q && finite(q.t) && finite(q.price))
|
|
35
|
+
points.push({ t: q.t, price: q.price });
|
|
36
|
+
}
|
|
37
|
+
if (points.length !== need)
|
|
38
|
+
continue;
|
|
39
|
+
const drawing = { id: d.id, type: d.type, points, color: d.color };
|
|
40
|
+
if (d.type === 'note')
|
|
41
|
+
drawing.text = typeof d.text === 'string' ? d.text.slice(0, MAX_NOTE_LENGTH) : '';
|
|
42
|
+
ids.add(d.id);
|
|
43
|
+
out.push(drawing);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CandleSeries, TickResult } from './series.ts';
|
|
2
|
+
import type { Calc } from './indicators.ts';
|
|
3
|
+
import type { IndicatorSpec } from './types.ts';
|
|
4
|
+
export declare const MAX_INDICATORS = 8;
|
|
5
|
+
export interface IndicatorEntry {
|
|
6
|
+
spec: IndicatorSpec;
|
|
7
|
+
calc: Calc;
|
|
8
|
+
key: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class IndicatorSet {
|
|
11
|
+
entries: IndicatorEntry[];
|
|
12
|
+
private closes;
|
|
13
|
+
get subCount(): number;
|
|
14
|
+
main(): IndicatorEntry[];
|
|
15
|
+
subs(): IndicatorEntry[];
|
|
16
|
+
setSpecs(specs: readonly IndicatorSpec[], series: CandleSeries): void;
|
|
17
|
+
rebuild(series: CandleSeries): void;
|
|
18
|
+
onTick(series: CandleSeries, result: TickResult): void;
|
|
19
|
+
private loadCloses;
|
|
20
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { isSubPane, makeCalc } from "./indicators.js";
|
|
2
|
+
export const MAX_INDICATORS = 8;
|
|
3
|
+
const keyOf = (spec) => JSON.stringify({ ...spec, color: undefined, signalColor: undefined });
|
|
4
|
+
export class IndicatorSet {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.entries = [];
|
|
7
|
+
this.closes = [];
|
|
8
|
+
}
|
|
9
|
+
get subCount() {
|
|
10
|
+
let n = 0;
|
|
11
|
+
for (const e of this.entries)
|
|
12
|
+
if (isSubPane(e.spec))
|
|
13
|
+
n++;
|
|
14
|
+
return n;
|
|
15
|
+
}
|
|
16
|
+
main() {
|
|
17
|
+
return this.entries.filter((e) => !isSubPane(e.spec));
|
|
18
|
+
}
|
|
19
|
+
subs() {
|
|
20
|
+
return this.entries.filter((e) => isSubPane(e.spec));
|
|
21
|
+
}
|
|
22
|
+
setSpecs(specs, series) {
|
|
23
|
+
const old = new Map(this.entries.map((e) => [e.spec.id, e]));
|
|
24
|
+
const next = [];
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (const spec of specs) {
|
|
27
|
+
if (seen.has(spec.id) || next.length >= MAX_INDICATORS)
|
|
28
|
+
continue;
|
|
29
|
+
seen.add(spec.id);
|
|
30
|
+
const key = keyOf(spec);
|
|
31
|
+
const kept = old.get(spec.id);
|
|
32
|
+
next.push(kept && kept.key === key ? { ...kept, spec } : { spec, key, calc: makeCalc(spec) });
|
|
33
|
+
}
|
|
34
|
+
const fresh = next.filter((e) => !this.entries.some((o) => o.calc === e.calc));
|
|
35
|
+
this.entries = next;
|
|
36
|
+
if (fresh.length > 0) {
|
|
37
|
+
this.loadCloses(series);
|
|
38
|
+
for (const e of fresh)
|
|
39
|
+
e.calc.computeFrom(this.closes, 0);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
rebuild(series) {
|
|
43
|
+
if (this.entries.length === 0)
|
|
44
|
+
return;
|
|
45
|
+
this.loadCloses(series);
|
|
46
|
+
for (const e of this.entries)
|
|
47
|
+
e.calc.computeFrom(this.closes, 0);
|
|
48
|
+
}
|
|
49
|
+
onTick(series, result) {
|
|
50
|
+
if (this.entries.length === 0 || result.kind === 'ignored')
|
|
51
|
+
return;
|
|
52
|
+
const n = series.count;
|
|
53
|
+
if ((result.kind === 'closed' && result.shifted > 0) || this.closes.length < n - 1 || this.closes.length > n) {
|
|
54
|
+
this.rebuild(series);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.closes[n - 1] = series.closeAt(n - 1);
|
|
58
|
+
for (const e of this.entries)
|
|
59
|
+
e.calc.computeFrom(this.closes, n - 1);
|
|
60
|
+
}
|
|
61
|
+
loadCloses(series) {
|
|
62
|
+
this.closes = [];
|
|
63
|
+
for (let i = 0; i < series.count; i++)
|
|
64
|
+
this.closes.push(series.closeAt(i));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { IndicatorSpec } from './types.ts';
|
|
2
|
+
export type Values = (number | null)[];
|
|
3
|
+
export interface Calc {
|
|
4
|
+
computeFrom(closes: readonly number[], from: number): void;
|
|
5
|
+
lines: Record<string, Values>;
|
|
6
|
+
}
|
|
7
|
+
export declare class Sma implements Calc {
|
|
8
|
+
readonly values: Values;
|
|
9
|
+
readonly lines: Record<string, Values>;
|
|
10
|
+
private period;
|
|
11
|
+
constructor(period: number);
|
|
12
|
+
computeFrom(closes: readonly number[], from: number): void;
|
|
13
|
+
}
|
|
14
|
+
export declare class Ema implements Calc {
|
|
15
|
+
readonly values: Values;
|
|
16
|
+
readonly lines: Record<string, Values>;
|
|
17
|
+
private period;
|
|
18
|
+
private k;
|
|
19
|
+
constructor(period: number);
|
|
20
|
+
computeFrom(input: readonly (number | null)[], from: number): void;
|
|
21
|
+
}
|
|
22
|
+
export declare class Bollinger implements Calc {
|
|
23
|
+
readonly mid: Values;
|
|
24
|
+
readonly upper: Values;
|
|
25
|
+
readonly lower: Values;
|
|
26
|
+
readonly lines: Record<string, Values>;
|
|
27
|
+
private period;
|
|
28
|
+
private width;
|
|
29
|
+
constructor(period: number, stdDev: number);
|
|
30
|
+
computeFrom(closes: readonly number[], from: number): void;
|
|
31
|
+
}
|
|
32
|
+
export declare class Rsi implements Calc {
|
|
33
|
+
readonly values: Values;
|
|
34
|
+
readonly lines: Record<string, Values>;
|
|
35
|
+
private gain;
|
|
36
|
+
private loss;
|
|
37
|
+
private period;
|
|
38
|
+
constructor(period: number);
|
|
39
|
+
computeFrom(closes: readonly number[], from: number): void;
|
|
40
|
+
}
|
|
41
|
+
export declare class Macd implements Calc {
|
|
42
|
+
readonly macd: Values;
|
|
43
|
+
readonly hist: Values;
|
|
44
|
+
readonly lines: Record<string, Values>;
|
|
45
|
+
private fast;
|
|
46
|
+
private slow;
|
|
47
|
+
private signal;
|
|
48
|
+
constructor(fast: number, slow: number, signal: number);
|
|
49
|
+
computeFrom(closes: readonly number[], from: number): void;
|
|
50
|
+
}
|
|
51
|
+
export declare function makeCalc(spec: IndicatorSpec): Calc;
|
|
52
|
+
export declare function isSubPane(spec: IndicatorSpec): boolean;
|
|
53
|
+
export declare function indicatorTitle(spec: IndicatorSpec): string;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
function cut(values, n) {
|
|
2
|
+
if (values.length > n)
|
|
3
|
+
values.length = n;
|
|
4
|
+
}
|
|
5
|
+
function whole(n) {
|
|
6
|
+
return Math.max(1, Math.floor(Number.isFinite(n) ? n : 1));
|
|
7
|
+
}
|
|
8
|
+
function windowMean(x, end, period) {
|
|
9
|
+
let sum = 0;
|
|
10
|
+
for (let j = end - period + 1; j <= end; j++)
|
|
11
|
+
sum += x[j];
|
|
12
|
+
return sum / period;
|
|
13
|
+
}
|
|
14
|
+
function firstValue(x) {
|
|
15
|
+
for (let i = 0; i < x.length; i++)
|
|
16
|
+
if (x[i] !== null)
|
|
17
|
+
return i;
|
|
18
|
+
return -1;
|
|
19
|
+
}
|
|
20
|
+
export class Sma {
|
|
21
|
+
constructor(period) {
|
|
22
|
+
this.values = [];
|
|
23
|
+
this.lines = { main: this.values };
|
|
24
|
+
this.period = whole(period);
|
|
25
|
+
}
|
|
26
|
+
computeFrom(closes, from) {
|
|
27
|
+
cut(this.values, from);
|
|
28
|
+
for (let i = this.values.length; i < closes.length; i++) {
|
|
29
|
+
this.values.push(i < this.period - 1 ? null : windowMean(closes, i, this.period));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class Ema {
|
|
34
|
+
constructor(period) {
|
|
35
|
+
this.values = [];
|
|
36
|
+
this.lines = { main: this.values };
|
|
37
|
+
this.period = whole(period);
|
|
38
|
+
this.k = 2 / (this.period + 1);
|
|
39
|
+
}
|
|
40
|
+
computeFrom(input, from) {
|
|
41
|
+
cut(this.values, from);
|
|
42
|
+
const start = firstValue(input);
|
|
43
|
+
for (let i = this.values.length; i < input.length; i++) {
|
|
44
|
+
if (start < 0 || i < start + this.period - 1)
|
|
45
|
+
this.values.push(null);
|
|
46
|
+
else if (i === start + this.period - 1)
|
|
47
|
+
this.values.push(windowMean(input, i, this.period));
|
|
48
|
+
else {
|
|
49
|
+
const prev = this.values[i - 1];
|
|
50
|
+
this.values.push(prev + this.k * (input[i] - prev));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class Bollinger {
|
|
56
|
+
constructor(period, stdDev) {
|
|
57
|
+
this.mid = [];
|
|
58
|
+
this.upper = [];
|
|
59
|
+
this.lower = [];
|
|
60
|
+
this.lines = { mid: this.mid, upper: this.upper, lower: this.lower };
|
|
61
|
+
this.period = whole(period);
|
|
62
|
+
this.width = Number.isFinite(stdDev) ? stdDev : 2;
|
|
63
|
+
}
|
|
64
|
+
computeFrom(closes, from) {
|
|
65
|
+
for (const v of [this.mid, this.upper, this.lower])
|
|
66
|
+
cut(v, from);
|
|
67
|
+
for (let i = this.mid.length; i < closes.length; i++) {
|
|
68
|
+
if (i < this.period - 1) {
|
|
69
|
+
this.mid.push(null);
|
|
70
|
+
this.upper.push(null);
|
|
71
|
+
this.lower.push(null);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const mean = windowMean(closes, i, this.period);
|
|
75
|
+
let sq = 0;
|
|
76
|
+
for (let j = i - this.period + 1; j <= i; j++)
|
|
77
|
+
sq += (closes[j] - mean) ** 2;
|
|
78
|
+
const band = this.width * Math.sqrt(sq / this.period);
|
|
79
|
+
this.mid.push(mean);
|
|
80
|
+
this.upper.push(mean + band);
|
|
81
|
+
this.lower.push(mean - band);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export class Rsi {
|
|
86
|
+
constructor(period) {
|
|
87
|
+
this.values = [];
|
|
88
|
+
this.lines = { rsi: this.values };
|
|
89
|
+
this.gain = [];
|
|
90
|
+
this.loss = [];
|
|
91
|
+
this.period = whole(period);
|
|
92
|
+
}
|
|
93
|
+
computeFrom(closes, from) {
|
|
94
|
+
for (const v of [this.values, this.gain, this.loss])
|
|
95
|
+
cut(v, from);
|
|
96
|
+
const p = this.period;
|
|
97
|
+
for (let i = this.values.length; i < closes.length; i++) {
|
|
98
|
+
if (i < p) {
|
|
99
|
+
this.values.push(null);
|
|
100
|
+
this.gain.push(null);
|
|
101
|
+
this.loss.push(null);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
let g;
|
|
105
|
+
let l;
|
|
106
|
+
if (i === p) {
|
|
107
|
+
g = 0;
|
|
108
|
+
l = 0;
|
|
109
|
+
for (let j = 1; j <= p; j++) {
|
|
110
|
+
const d = closes[j] - closes[j - 1];
|
|
111
|
+
if (d > 0)
|
|
112
|
+
g += d;
|
|
113
|
+
else
|
|
114
|
+
l -= d;
|
|
115
|
+
}
|
|
116
|
+
g /= p;
|
|
117
|
+
l /= p;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const d = closes[i] - closes[i - 1];
|
|
121
|
+
g = (this.gain[i - 1] * (p - 1) + (d > 0 ? d : 0)) / p;
|
|
122
|
+
l = (this.loss[i - 1] * (p - 1) + (d < 0 ? -d : 0)) / p;
|
|
123
|
+
}
|
|
124
|
+
this.gain.push(g);
|
|
125
|
+
this.loss.push(l);
|
|
126
|
+
this.values.push(l === 0 ? (g === 0 ? 50 : 100) : 100 - 100 / (1 + g / l));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export class Macd {
|
|
131
|
+
constructor(fast, slow, signal) {
|
|
132
|
+
this.macd = [];
|
|
133
|
+
this.hist = [];
|
|
134
|
+
this.fast = new Ema(fast);
|
|
135
|
+
this.slow = new Ema(slow);
|
|
136
|
+
this.signal = new Ema(signal);
|
|
137
|
+
this.lines = { macd: this.macd, signal: this.signal.values, hist: this.hist };
|
|
138
|
+
}
|
|
139
|
+
computeFrom(closes, from) {
|
|
140
|
+
this.fast.computeFrom(closes, from);
|
|
141
|
+
this.slow.computeFrom(closes, from);
|
|
142
|
+
cut(this.macd, from);
|
|
143
|
+
for (let i = this.macd.length; i < closes.length; i++) {
|
|
144
|
+
const f = this.fast.values[i];
|
|
145
|
+
const s = this.slow.values[i];
|
|
146
|
+
this.macd.push(f !== null && f !== undefined && s !== null && s !== undefined ? f - s : null);
|
|
147
|
+
}
|
|
148
|
+
this.signal.computeFrom(this.macd, from);
|
|
149
|
+
cut(this.hist, from);
|
|
150
|
+
for (let i = this.hist.length; i < closes.length; i++) {
|
|
151
|
+
const m = this.macd[i];
|
|
152
|
+
const g = this.signal.values[i];
|
|
153
|
+
this.hist.push(m !== null && m !== undefined && g !== null && g !== undefined ? m - g : null);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export function makeCalc(spec) {
|
|
158
|
+
switch (spec.type) {
|
|
159
|
+
case 'sma':
|
|
160
|
+
return new Sma(spec.period);
|
|
161
|
+
case 'ema':
|
|
162
|
+
return new Ema(spec.period);
|
|
163
|
+
case 'bollinger':
|
|
164
|
+
return new Bollinger(spec.period, spec.stdDev);
|
|
165
|
+
case 'rsi':
|
|
166
|
+
return new Rsi(spec.period);
|
|
167
|
+
case 'macd':
|
|
168
|
+
return new Macd(spec.fast, spec.slow, spec.signal);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
export function isSubPane(spec) {
|
|
172
|
+
return spec.type === 'rsi' || spec.type === 'macd';
|
|
173
|
+
}
|
|
174
|
+
export function indicatorTitle(spec) {
|
|
175
|
+
switch (spec.type) {
|
|
176
|
+
case 'sma':
|
|
177
|
+
return `SMA ${spec.period}`;
|
|
178
|
+
case 'ema':
|
|
179
|
+
return `EMA ${spec.period}`;
|
|
180
|
+
case 'bollinger':
|
|
181
|
+
return `BB ${spec.period} ${spec.stdDev}`;
|
|
182
|
+
case 'rsi':
|
|
183
|
+
return `RSI ${spec.period}`;
|
|
184
|
+
case 'macd':
|
|
185
|
+
return `MACD ${spec.fast} ${spec.slow} ${spec.signal}`;
|
|
186
|
+
}
|
|
187
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface Marker {
|
|
|
62
62
|
label?: string;
|
|
63
63
|
tooltip?: string;
|
|
64
64
|
}
|
|
65
|
+
import type { Drawing, DrawingTool } from './drawings.ts';
|
|
66
|
+
export type { Drawing, DrawingKind, DrawingTool, Point } from './drawings.ts';
|
|
65
67
|
export type IndicatorSpec = {
|
|
66
68
|
id: string;
|
|
67
69
|
type: 'sma';
|
|
@@ -97,6 +99,14 @@ export interface ChartEvents {
|
|
|
97
99
|
candleClosed: (candle: Candle) => void;
|
|
98
100
|
liveChange: (atLive: boolean) => void;
|
|
99
101
|
crosshair: (candle: Candle | null) => void;
|
|
102
|
+
drawingsChange: (drawings: readonly Drawing[]) => void;
|
|
103
|
+
selectionChange: (id: string | null) => void;
|
|
104
|
+
drawProgress: (info: {
|
|
105
|
+
tool: DrawingTool;
|
|
106
|
+
placed: number;
|
|
107
|
+
needed: number;
|
|
108
|
+
} | null) => void;
|
|
109
|
+
drawingEdit: (drawing: Drawing) => void;
|
|
100
110
|
}
|
|
101
111
|
export interface Chart {
|
|
102
112
|
setData(closed: readonly Candle[], forming: Candle | null, hasMore: boolean): void;
|
|
@@ -106,6 +116,17 @@ export interface Chart {
|
|
|
106
116
|
setOptions(patch: Partial<ChartOptions>): void;
|
|
107
117
|
setPriceLines(lines: readonly PriceLine[]): void;
|
|
108
118
|
setMarkers(markers: readonly Marker[]): void;
|
|
119
|
+
setIndicators(specs: readonly IndicatorSpec[]): void;
|
|
120
|
+
setDrawingTool(tool: DrawingTool | null): void;
|
|
121
|
+
setDrawings(drawings: readonly Drawing[]): void;
|
|
122
|
+
getDrawings(): Drawing[];
|
|
123
|
+
updateDrawing(id: string, patch: {
|
|
124
|
+
text?: string;
|
|
125
|
+
color?: string;
|
|
126
|
+
}): void;
|
|
127
|
+
deleteSelected(): void;
|
|
128
|
+
clearDrawings(): void;
|
|
129
|
+
setDrawingColor(color: string): void;
|
|
109
130
|
scrollToLive(): void;
|
|
110
131
|
fitAll(): void;
|
|
111
132
|
on<E extends keyof ChartEvents>(event: E, listener: ChartEvents[E]): () => void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { indicatorTitle } from "../core/indicators.js";
|
|
2
|
+
export function indicatorReadings(entries, i, fmt) {
|
|
3
|
+
const out = [];
|
|
4
|
+
const at = (values) => {
|
|
5
|
+
const v = values?.[i];
|
|
6
|
+
return v === null || v === undefined ? '–' : fmt(v);
|
|
7
|
+
};
|
|
8
|
+
for (const e of entries) {
|
|
9
|
+
const l = e.calc.lines;
|
|
10
|
+
const name = indicatorTitle(e.spec);
|
|
11
|
+
switch (e.spec.type) {
|
|
12
|
+
case 'bollinger':
|
|
13
|
+
out.push({ text: `${name} ${at(l.lower)} ${at(l.mid)} ${at(l.upper)}`, color: e.spec.color });
|
|
14
|
+
break;
|
|
15
|
+
case 'macd':
|
|
16
|
+
out.push({ text: `MACD ${at(l.macd)} sig ${at(l.signal)}`, color: e.spec.color });
|
|
17
|
+
break;
|
|
18
|
+
case 'rsi':
|
|
19
|
+
out.push({ text: `${name} ${at(l.rsi) === '–' ? '–' : l.rsi[i].toFixed(1)}`, color: e.spec.color });
|
|
20
|
+
break;
|
|
21
|
+
default:
|
|
22
|
+
out.push({ text: `${name} ${at(l.main)}`, color: e.spec.color });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
package/dist/dom/legend.d.ts
CHANGED
|
@@ -3,11 +3,16 @@ export interface LegendInput {
|
|
|
3
3
|
candle: Candle | null;
|
|
4
4
|
notes: readonly string[];
|
|
5
5
|
caption: string | null;
|
|
6
|
+
indicators: readonly {
|
|
7
|
+
text: string;
|
|
8
|
+
color: string;
|
|
9
|
+
}[];
|
|
6
10
|
}
|
|
7
11
|
export declare class Legend {
|
|
8
12
|
readonly el: HTMLDivElement;
|
|
9
13
|
private line;
|
|
10
14
|
private extra;
|
|
15
|
+
private readings;
|
|
11
16
|
constructor(doc: Document, theme: ChartTheme);
|
|
12
17
|
applyTheme(theme: ChartTheme): void;
|
|
13
18
|
update(input: LegendInput, labels: ChartLabels, theme: ChartTheme, decimals: number, fmt: (v: number) => string, locale: string): void;
|
package/dist/dom/legend.js
CHANGED
|
@@ -4,8 +4,10 @@ export class Legend {
|
|
|
4
4
|
this.el = doc.createElement('div');
|
|
5
5
|
this.line = doc.createElement('div');
|
|
6
6
|
this.extra = doc.createElement('div');
|
|
7
|
+
this.readings = doc.createElement('div');
|
|
7
8
|
this.line.style.cssText = 'display:flex;flex-wrap:wrap;column-gap:12px';
|
|
8
|
-
this.
|
|
9
|
+
this.readings.style.cssText = 'display:flex;flex-wrap:wrap;column-gap:12px';
|
|
10
|
+
this.el.append(this.line, this.readings, this.extra);
|
|
9
11
|
this.el.setAttribute('aria-live', 'off');
|
|
10
12
|
this.applyTheme(theme);
|
|
11
13
|
}
|
|
@@ -27,6 +29,7 @@ export class Legend {
|
|
|
27
29
|
const k = input.candle;
|
|
28
30
|
this.line.textContent = '';
|
|
29
31
|
this.extra.textContent = '';
|
|
32
|
+
this.readings.textContent = '';
|
|
30
33
|
if (!k)
|
|
31
34
|
return;
|
|
32
35
|
const parts = [
|
|
@@ -46,6 +49,12 @@ export class Legend {
|
|
|
46
49
|
change.textContent = `${ch.arrow} ${ch.amount} (${ch.percent})`.trim();
|
|
47
50
|
change.style.color = ch.direction > 0 ? theme.up : ch.direction < 0 ? theme.down : theme.textMuted;
|
|
48
51
|
this.line.append(change);
|
|
52
|
+
for (const r of input.indicators) {
|
|
53
|
+
const span = doc.createElement('span');
|
|
54
|
+
span.textContent = r.text;
|
|
55
|
+
span.style.color = r.color;
|
|
56
|
+
this.readings.append(span);
|
|
57
|
+
}
|
|
49
58
|
const lines = input.caption ? [input.caption, ...input.notes] : [...input.notes];
|
|
50
59
|
this.extra.textContent = lines.join('\n');
|
|
51
60
|
this.extra.style.color = theme.textMuted;
|