Skip to content

Commit

Permalink
bollinger mark & transform
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Aug 2, 2023
1 parent 194e1f4 commit b0afd6c
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 14 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export default defineConfig({
{text: "Auto", link: "/marks/auto"},
{text: "Axis", link: "/marks/axis"},
{text: "Bar", link: "/marks/bar"},
{text: "Bollinger", link: "/marks/bollinger"},
{text: "Box", link: "/marks/box"},
{text: "Cell", link: "/marks/cell"},
{text: "Contour", link: "/marks/contour"},
Expand Down
115 changes: 115 additions & 0 deletions docs/marks/bollinger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<script setup>

import * as Plot from "@observablehq/plot";
import * as d3 from "d3";
import {ref} from "vue";
import aapl from "../data/aapl.ts";

const n = ref(20);
const k = ref(2);

</script>

# Bollinger mark

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.

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.

<p>
<label class="label-input">
<span>Window size (n):</span>
<input type="range" v-model.number="n" min="1" max="100" step="1" />
<span style="font-variant-numeric: tabular-nums;">{{n.toLocaleString("en-US")}}</span>
</label>
<label class="label-input">
<span>Radius (k):</span>
<input type="range" v-model.number="k" min="0" max="10" step="0.1" />
<span style="font-variant-numeric: tabular-nums;">{{k.toLocaleString("en-US")}}</span>
</label>
</p>

:::plot hidden
```js
Plot.bollingerY(aapl, {x: "Date", y: "Close", n, k}).plot()
```
:::

```js-vue
Plot.bollingerY(aapl, {x: "Date", y: "Close", n: {{n}}, k: {{k}}}).plot()
```

For more control, you can also use the [bollinger map method](#bollinger) directly with the [map transform](../transforms/map.md).

:::plot
```js
Plot.plot({
marks: [
Plot.lineY(aapl, Plot.mapY(Plot.bollinger(20, -2), {x: "Date", y: "Close", stroke: "red"})),
Plot.lineY(aapl, Plot.mapY(Plot.bollinger(20, 2), {x: "Date", y: "Close", stroke: "green"})),
Plot.lineY(aapl, Plot.mapY(Plot.bollinger(20, 0), {x: "Date", y: "Close"}))
]
})
```
:::

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↓).

:::plot
```js
Plot.bollingerX(aapl, {y: "Date", x: "Close"}).plot()
```
:::

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).

:::plot
```js
Plot.bollingerY(aapl.map((d) => d.Close)).plot()
```
:::

## Bollinger options

The bollinger mark is a [composite mark](../features/marks.md#marks) consisting of two marks:

* an [area](../marks/area.md) representing volatility as a band, and
* a [line](../marks/line.md) representing a moving average

In addition to the standard mark options which are passed through to the underlying area and line, the bollinger mark supports the following options:

* **n** - the window size (corresponding to the window transform’s **k** option), an integer
* **k** - the band radius, a number representing a multiple of standard deviations
* **color** - the fill color of the area, and the stroke color of the line; defaults to *currentColor*
* **opacity** - the fill opacity of the area; defaults to 0.2
* **fill** - the fill color of the area; defaults to **color**
* **fillOpacity** - the fill opacity of the area; defaults to **paocity**
* **stroke** - the stroke color of the line; defaults to **color**
* **strokeOpacity** - the stroke opacity of the line; defaults to 1
* **strokeWidth** - the stroke width of the line in pixels; defaults to 1.5

## bollingerX(*data*, *options*) {#bollingerX}

```js
Plot.bollingerX(aapl, {y: "Date", x: "Close"})
```

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, …].

## bollingerY(*data*, *options*) {#bollingerY}

```js
Plot.bollingerY(aapl, {x: "Date", y: "Close"})
```

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, …].

TODO Describe the **interval** option inherited from line/area.

## bollinger(*n*, *k*) {#bollinger}

```js
Plot.lineY(data, Plot.map({y: Plot.bollinger(20, 0)}, {x: "Date", y: "Close"}))
```

Returns a bollinger map method for use with the [map transform](../transforms/map.md).
1 change: 1 addition & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from "./marks/arrow.js";
export * from "./marks/auto.js";
export * from "./marks/axis.js";
export * from "./marks/bar.js";
export * from "./marks/bollinger.js";
export * from "./marks/box.js";
export * from "./marks/cell.js";
export * from "./marks/contour.js";
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export {Arrow, arrow} from "./marks/arrow.js";
export {auto, autoSpec} from "./marks/auto.js";
export {axisX, axisY, axisFx, axisFy, gridX, gridY, gridFx, gridFy} from "./marks/axis.js";
export {BarX, BarY, barX, barY} from "./marks/bar.js";
export {bollinger, bollingerX, bollingerY} from "./marks/bollinger.js";
export {boxX, boxY} from "./marks/box.js";
export {Cell, cell, cellX, cellY} from "./marks/cell.js";
export {Contour, contour} from "./marks/contour.js";
Expand Down
34 changes: 34 additions & 0 deletions src/marks/bollinger.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type {CompoundMark, Data, MarkOptions} from "../mark.js";
import type {Map} from "../transforms/map.js";
import type {AreaXOptions, AreaYOptions} from "./area.js";
import type {LineXOptions, LineYOptions} from "./line.js";

/** Options for the bollinger marks. */
export interface BollingerOptions {
/** The number of consecutive values in the window; defaults to 20. */
n?: number;

/** The number of standard deviations to offset the bands; defaults to 2. */
k?: number;

/**
* Shorthand for setting both **fill** and **stroke**; affects the stroke of
* the line and the fill of the area; defaults to *currentColor*.
*/
color?: MarkOptions["stroke"];
}

/** Options for the bollingerBandX mark. */
export type BollingerXOptions = BollingerOptions & AreaXOptions & LineXOptions;

/** Options for the bollingerBandY mark. */
export type BollingerYOptions = BollingerOptions & AreaYOptions & LineYOptions;

/** TODO */
export function bollingerX(data?: Data, options?: BollingerXOptions): CompoundMark;

/** TODO */
export function bollingerY(data?: Data, options?: BollingerYOptions): CompoundMark;

/** TODO */
export function bollinger(n: number, k: number): Map;
57 changes: 57 additions & 0 deletions src/marks/bollinger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {deviation, mean} from "d3";
import {marks} from "../mark.js";
import {map} from "../transforms/map.js";
import {window} from "../transforms/window.js";
import {areaX, areaY} from "./area.js";
import {lineX, lineY} from "./line.js";
import {identity} from "../options.js";

export function bollingerX(
data,
{
x = identity,
y,
n = 20,
k = 2,
color = "currentColor",
opacity = 0.2,
fill = color,
fillOpacity = opacity,
stroke = color,
strokeOpacity,
strokeWidth,
...options
} = {}
) {
return marks(
areaX(data, map({x1: bollinger(n, -k), x2: bollinger(n, k)}, {x1: x, x2: x, y, fill, fillOpacity, ...options})),
lineX(data, map({x: bollinger(n, 0)}, {x, y, stroke, strokeOpacity, strokeWidth, ...options}))
);
}

export function bollingerY(
data,
{
x,
y = identity,
n = 20,
k = 2,
color = "currentColor",
opacity = 0.2,
fill = color,
fillOpacity = opacity,
stroke = color,
strokeOpacity,
strokeWidth,
...options
} = {}
) {
return marks(
areaY(data, map({y1: bollinger(n, -k), y2: bollinger(n, k)}, {x, y1: y, y2: y, fill, fillOpacity, ...options})),
lineY(data, map({y: bollinger(n, 0)}, {x, y, stroke, strokeOpacity, strokeWidth, ...options}))
);
}

export function bollinger(n, k) {
return window({k: n, reduce: (Y) => mean(Y) + k * (deviation(Y) || 0), strict: true, anchor: "end"});
}
17 changes: 3 additions & 14 deletions test/plots/aapl-bollinger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ export async function aaplBollinger() {
grid: true
},
marks: [
Plot.areaY(AAPL, bollingerBandY(20, 2, {x: "Date", y: "Close", fillOpacity: 0.2})),
Plot.line(AAPL, Plot.map({y: bollinger(20, 0)}, {x: "Date", y: "Close", stroke: "blue"})),
Plot.bollingerY(AAPL, {x: "Date", y: "Close", stroke: "blue"}),
Plot.line(AAPL, {x: "Date", y: "Close", strokeWidth: 1})
]
});
Expand All @@ -26,8 +25,7 @@ export async function aaplBollingerGridInterval() {
Plot.gridX({tickSpacing: 40, stroke: "#fff", strokeOpacity: 1, strokeWidth: 0.5}),
Plot.gridX({tickSpacing: 80, stroke: "#fff", strokeOpacity: 1}),
Plot.axisX({tickSpacing: 80}),
Plot.areaY(AAPL, bollingerBandY(20, 2, {x: "Date", y: "Close", fillOpacity: 0.2})),
Plot.line(AAPL, Plot.map({y: bollinger(20, 0)}, {x: "Date", y: "Close", stroke: "blue"})),
Plot.bollingerY(AAPL, {x: "Date", y: "Close", stroke: "blue"}),
Plot.line(AAPL, {x: "Date", y: "Close", strokeWidth: 1})
]
});
Expand All @@ -44,17 +42,8 @@ export async function aaplBollingerGridSpacing() {
Plot.gridX({interval: "3 months", stroke: "#fff", strokeOpacity: 1, strokeWidth: 0.5}),
Plot.gridX({interval: "1 year", stroke: "#fff", strokeOpacity: 1}),
Plot.axisX({interval: "1 year"}),
Plot.areaY(AAPL, bollingerBandY(20, 2, {x: "Date", y: "Close", fillOpacity: 0.2})),
Plot.line(AAPL, Plot.map({y: bollinger(20, 0)}, {x: "Date", y: "Close", stroke: "blue"})),
Plot.bollingerY(AAPL, {x: "Date", y: "Close", stroke: "blue"}),
Plot.line(AAPL, {x: "Date", y: "Close", strokeWidth: 1})
]
});
}

function bollingerBandY(N, K, options) {
return Plot.map({y1: bollinger(N, -K), y2: bollinger(N, K)}, options);
}

function bollinger(N, K) {
return Plot.window({k: N, reduce: (Y) => d3.mean(Y) + K * d3.deviation(Y), strict: true, anchor: "end"});
}

0 comments on commit b0afd6c

Please sign in to comment.