Skip to content

Commit b0afd6c

Browse files
committed
bollinger mark & transform
1 parent 194e1f4 commit b0afd6c

File tree

7 files changed

+212
-14
lines changed

7 files changed

+212
-14
lines changed

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export default defineConfig({
7979
{text: "Auto", link: "/marks/auto"},
8080
{text: "Axis", link: "/marks/axis"},
8181
{text: "Bar", link: "/marks/bar"},
82+
{text: "Bollinger", link: "/marks/bollinger"},
8283
{text: "Box", link: "/marks/box"},
8384
{text: "Cell", link: "/marks/cell"},
8485
{text: "Contour", link: "/marks/contour"},

docs/marks/bollinger.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<script setup>
2+
3+
import * as Plot from "@observablehq/plot";
4+
import * as d3 from "d3";
5+
import {ref} from "vue";
6+
import aapl from "../data/aapl.ts";
7+
8+
const n = ref(20);
9+
const k = ref(2);
10+
11+
</script>
12+
13+
# Bollinger mark
14+
15+
The **bollinger mark** is a [composite mark](../features/marks.md#marks) consisting of a [line](./line.md) representing a moving average, and an [area](./area.md) representing volatility as a band; the band thickness is proportional to the deviation of nearby values. The bollinger mark is often used to analyze the price of financial instruments such as stocks.
16+
17+
For example, the chart below shows the price of Apple stock from 2013–2018, with window size *n* of {{n}} days and radius *k* of {{k}} standard deviations.
18+
19+
<p>
20+
<label class="label-input">
21+
<span>Window size (n):</span>
22+
<input type="range" v-model.number="n" min="1" max="100" step="1" />
23+
<span style="font-variant-numeric: tabular-nums;">{{n.toLocaleString("en-US")}}</span>
24+
</label>
25+
<label class="label-input">
26+
<span>Radius (k):</span>
27+
<input type="range" v-model.number="k" min="0" max="10" step="0.1" />
28+
<span style="font-variant-numeric: tabular-nums;">{{k.toLocaleString("en-US")}}</span>
29+
</label>
30+
</p>
31+
32+
:::plot hidden
33+
```js
34+
Plot.bollingerY(aapl, {x: "Date", y: "Close", n, k}).plot()
35+
```
36+
:::
37+
38+
```js-vue
39+
Plot.bollingerY(aapl, {x: "Date", y: "Close", n: {{n}}, k: {{k}}}).plot()
40+
```
41+
42+
For more control, you can also use the [bollinger map method](#bollinger) directly with the [map transform](../transforms/map.md).
43+
44+
:::plot
45+
```js
46+
Plot.plot({
47+
marks: [
48+
Plot.lineY(aapl, Plot.mapY(Plot.bollinger(20, -2), {x: "Date", y: "Close", stroke: "red"})),
49+
Plot.lineY(aapl, Plot.mapY(Plot.bollinger(20, 2), {x: "Date", y: "Close", stroke: "green"})),
50+
Plot.lineY(aapl, Plot.mapY(Plot.bollinger(20, 0), {x: "Date", y: "Close"}))
51+
]
52+
})
53+
```
54+
:::
55+
56+
The bollinger mark has two constructors: the common [bollingerY](#bollingerY) for when time goes right→ (or ←left); and the rare [bollingerX](#bollingerX) for when time goes up↑ (or down↓).
57+
58+
:::plot
59+
```js
60+
Plot.bollingerX(aapl, {y: "Date", x: "Close"}).plot()
61+
```
62+
:::
63+
64+
As [shorthand](../features/shorthand.md), you can pass an array of numbers as data. Below, the *x* axis represents the zero-based index into the data (*i.e.*, trading days since May 13, 2013).
65+
66+
:::plot
67+
```js
68+
Plot.bollingerY(aapl.map((d) => d.Close)).plot()
69+
```
70+
:::
71+
72+
## Bollinger options
73+
74+
The bollinger mark is a [composite mark](../features/marks.md#marks) consisting of two marks:
75+
76+
* an [area](../marks/area.md) representing volatility as a band, and
77+
* a [line](../marks/line.md) representing a moving average
78+
79+
In addition to the standard mark options which are passed through to the underlying area and line, the bollinger mark supports the following options:
80+
81+
* **n** - the window size (corresponding to the window transform’s **k** option), an integer
82+
* **k** - the band radius, a number representing a multiple of standard deviations
83+
* **color** - the fill color of the area, and the stroke color of the line; defaults to *currentColor*
84+
* **opacity** - the fill opacity of the area; defaults to 0.2
85+
* **fill** - the fill color of the area; defaults to **color**
86+
* **fillOpacity** - the fill opacity of the area; defaults to **paocity**
87+
* **stroke** - the stroke color of the line; defaults to **color**
88+
* **strokeOpacity** - the stroke opacity of the line; defaults to 1
89+
* **strokeWidth** - the stroke width of the line in pixels; defaults to 1.5
90+
91+
## bollingerX(*data*, *options*) {#bollingerX}
92+
93+
```js
94+
Plot.bollingerX(aapl, {y: "Date", x: "Close"})
95+
```
96+
97+
Returns a bollinger mark for when time goes up↑ (or down↓). If the **x** option is not specified, it defaults to the identity function, as when *data* is an array of numbers [*x₀*, *x₁*, *x₂*, …]. If the **y** option is not specified, it defaults to [0, 1, 2, …].
98+
99+
## bollingerY(*data*, *options*) {#bollingerY}
100+
101+
```js
102+
Plot.bollingerY(aapl, {x: "Date", y: "Close"})
103+
```
104+
105+
Returns a bollinger mark for when time goes right→ (or ←left). If the **y** option is not specified, it defaults to the identity function, as when *data* is an array of numbers [*y₀*, *y₁*, *y₂*, …]. If the **x** option is not specified, it defaults to [0, 1, 2, …].
106+
107+
TODO Describe the **interval** option inherited from line/area.
108+
109+
## bollinger(*n*, *k*) {#bollinger}
110+
111+
```js
112+
Plot.lineY(data, Plot.map({y: Plot.bollinger(20, 0)}, {x: "Date", y: "Close"}))
113+
```
114+
115+
Returns a bollinger map method for use with the [map transform](../transforms/map.md).

src/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export * from "./marks/arrow.js";
1414
export * from "./marks/auto.js";
1515
export * from "./marks/axis.js";
1616
export * from "./marks/bar.js";
17+
export * from "./marks/bollinger.js";
1718
export * from "./marks/box.js";
1819
export * from "./marks/cell.js";
1920
export * from "./marks/contour.js";

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export {Arrow, arrow} from "./marks/arrow.js";
55
export {auto, autoSpec} from "./marks/auto.js";
66
export {axisX, axisY, axisFx, axisFy, gridX, gridY, gridFx, gridFy} from "./marks/axis.js";
77
export {BarX, BarY, barX, barY} from "./marks/bar.js";
8+
export {bollinger, bollingerX, bollingerY} from "./marks/bollinger.js";
89
export {boxX, boxY} from "./marks/box.js";
910
export {Cell, cell, cellX, cellY} from "./marks/cell.js";
1011
export {Contour, contour} from "./marks/contour.js";

src/marks/bollinger.d.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type {CompoundMark, Data, MarkOptions} from "../mark.js";
2+
import type {Map} from "../transforms/map.js";
3+
import type {AreaXOptions, AreaYOptions} from "./area.js";
4+
import type {LineXOptions, LineYOptions} from "./line.js";
5+
6+
/** Options for the bollinger marks. */
7+
export interface BollingerOptions {
8+
/** The number of consecutive values in the window; defaults to 20. */
9+
n?: number;
10+
11+
/** The number of standard deviations to offset the bands; defaults to 2. */
12+
k?: number;
13+
14+
/**
15+
* Shorthand for setting both **fill** and **stroke**; affects the stroke of
16+
* the line and the fill of the area; defaults to *currentColor*.
17+
*/
18+
color?: MarkOptions["stroke"];
19+
}
20+
21+
/** Options for the bollingerBandX mark. */
22+
export type BollingerXOptions = BollingerOptions & AreaXOptions & LineXOptions;
23+
24+
/** Options for the bollingerBandY mark. */
25+
export type BollingerYOptions = BollingerOptions & AreaYOptions & LineYOptions;
26+
27+
/** TODO */
28+
export function bollingerX(data?: Data, options?: BollingerXOptions): CompoundMark;
29+
30+
/** TODO */
31+
export function bollingerY(data?: Data, options?: BollingerYOptions): CompoundMark;
32+
33+
/** TODO */
34+
export function bollinger(n: number, k: number): Map;

src/marks/bollinger.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {deviation, mean} from "d3";
2+
import {marks} from "../mark.js";
3+
import {map} from "../transforms/map.js";
4+
import {window} from "../transforms/window.js";
5+
import {areaX, areaY} from "./area.js";
6+
import {lineX, lineY} from "./line.js";
7+
import {identity} from "../options.js";
8+
9+
export function bollingerX(
10+
data,
11+
{
12+
x = identity,
13+
y,
14+
n = 20,
15+
k = 2,
16+
color = "currentColor",
17+
opacity = 0.2,
18+
fill = color,
19+
fillOpacity = opacity,
20+
stroke = color,
21+
strokeOpacity,
22+
strokeWidth,
23+
...options
24+
} = {}
25+
) {
26+
return marks(
27+
areaX(data, map({x1: bollinger(n, -k), x2: bollinger(n, k)}, {x1: x, x2: x, y, fill, fillOpacity, ...options})),
28+
lineX(data, map({x: bollinger(n, 0)}, {x, y, stroke, strokeOpacity, strokeWidth, ...options}))
29+
);
30+
}
31+
32+
export function bollingerY(
33+
data,
34+
{
35+
x,
36+
y = identity,
37+
n = 20,
38+
k = 2,
39+
color = "currentColor",
40+
opacity = 0.2,
41+
fill = color,
42+
fillOpacity = opacity,
43+
stroke = color,
44+
strokeOpacity,
45+
strokeWidth,
46+
...options
47+
} = {}
48+
) {
49+
return marks(
50+
areaY(data, map({y1: bollinger(n, -k), y2: bollinger(n, k)}, {x, y1: y, y2: y, fill, fillOpacity, ...options})),
51+
lineY(data, map({y: bollinger(n, 0)}, {x, y, stroke, strokeOpacity, strokeWidth, ...options}))
52+
);
53+
}
54+
55+
export function bollinger(n, k) {
56+
return window({k: n, reduce: (Y) => mean(Y) + k * (deviation(Y) || 0), strict: true, anchor: "end"});
57+
}

test/plots/aapl-bollinger.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ export async function aaplBollinger() {
88
grid: true
99
},
1010
marks: [
11-
Plot.areaY(AAPL, bollingerBandY(20, 2, {x: "Date", y: "Close", fillOpacity: 0.2})),
12-
Plot.line(AAPL, Plot.map({y: bollinger(20, 0)}, {x: "Date", y: "Close", stroke: "blue"})),
11+
Plot.bollingerY(AAPL, {x: "Date", y: "Close", stroke: "blue"}),
1312
Plot.line(AAPL, {x: "Date", y: "Close", strokeWidth: 1})
1413
]
1514
});
@@ -26,8 +25,7 @@ export async function aaplBollingerGridInterval() {
2625
Plot.gridX({tickSpacing: 40, stroke: "#fff", strokeOpacity: 1, strokeWidth: 0.5}),
2726
Plot.gridX({tickSpacing: 80, stroke: "#fff", strokeOpacity: 1}),
2827
Plot.axisX({tickSpacing: 80}),
29-
Plot.areaY(AAPL, bollingerBandY(20, 2, {x: "Date", y: "Close", fillOpacity: 0.2})),
30-
Plot.line(AAPL, Plot.map({y: bollinger(20, 0)}, {x: "Date", y: "Close", stroke: "blue"})),
28+
Plot.bollingerY(AAPL, {x: "Date", y: "Close", stroke: "blue"}),
3129
Plot.line(AAPL, {x: "Date", y: "Close", strokeWidth: 1})
3230
]
3331
});
@@ -44,17 +42,8 @@ export async function aaplBollingerGridSpacing() {
4442
Plot.gridX({interval: "3 months", stroke: "#fff", strokeOpacity: 1, strokeWidth: 0.5}),
4543
Plot.gridX({interval: "1 year", stroke: "#fff", strokeOpacity: 1}),
4644
Plot.axisX({interval: "1 year"}),
47-
Plot.areaY(AAPL, bollingerBandY(20, 2, {x: "Date", y: "Close", fillOpacity: 0.2})),
48-
Plot.line(AAPL, Plot.map({y: bollinger(20, 0)}, {x: "Date", y: "Close", stroke: "blue"})),
45+
Plot.bollingerY(AAPL, {x: "Date", y: "Close", stroke: "blue"}),
4946
Plot.line(AAPL, {x: "Date", y: "Close", strokeWidth: 1})
5047
]
5148
});
5249
}
53-
54-
function bollingerBandY(N, K, options) {
55-
return Plot.map({y1: bollinger(N, -K), y2: bollinger(N, K)}, options);
56-
}
57-
58-
function bollinger(N, K) {
59-
return Plot.window({k: N, reduce: (Y) => d3.mean(Y) + K * d3.deviation(Y), strict: true, anchor: "end"});
60-
}

0 commit comments

Comments
 (0)