Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Built-in Bollinger band transform #548

Closed
mbostock opened this issue Sep 22, 2021 · 6 comments · Fixed by #1772
Closed

Built-in Bollinger band transform #548

mbostock opened this issue Sep 22, 2021 · 6 comments · Fixed by #1772
Labels
enhancement New feature or request

Comments

@mbostock
Copy link
Member

Like so

function bollinger(N, K) {
  return values => {
    let i = 0;
    let sum = 0;
    let sum2 = 0;
    const Y = new Float64Array(values.length).fill(NaN);
    for (let n = Math.min(N - 1, values.length); i < n; ++i) {
      const value = values[i];
      sum += value, sum2 += value ** 2;
    }
    for (let n = values.length; i < n; ++i) {
      const value = values[i];
      sum += value, sum2 += value ** 2;
      const mean = sum / N;
      const deviation = Math.sqrt((sum2 - sum ** 2 / N) / (N - 1));
      Y[i] = mean + deviation * K;
      const value0 = values[i - N + 1];
      sum -= value0, sum2 -= value0 ** 2;
    }
    return Y;
  };
}
@mbostock mbostock added the enhancement New feature or request label Sep 22, 2021
@Fil
Copy link
Contributor

Fil commented Sep 22, 2021

Using Plot.window we benefit from standard options such as anchor, skipping NaNs &c:

bollinger = (K) => (data) => d3.mean(data) + K * d3.deviation(data)

Plot.windowY({
  reduce: bollinger(K),
  k: N,
  x: "date",
  y: "close",
  anchor: "end"
})

the current names of params is a bit unfortunate (k is N, K is the multiplier).

For K = 0 the solution is even simpler :)

Plot.windowY({
  reduce: "mean",
  k: N,
  x: "date",
  y: "close"
})

In terms of performance we loop a little bit more than your code (since sum and sum2 are not computed with a sliding window, but repeatedly). However I think it's a negligible cost compared to the simplicity of the formula. (We haven't optimized the variance/deviation reducer either at this point.)

It's a bit more work to {y1: bollinger(-K), y2: bollinger(K) } to show the band as an area.

@Fil
Copy link
Contributor

Fil commented Sep 23, 2021

For the band as an area, this doesn't work:
Plot.windowY({ y1: { value: "close", reduce: bollinger(-K) }, y2: { value: "close", reduce: bollinger(K) }, …

I'm opening #549 for this.

However we're not blocked: we can take advantage of the symmetry of the bollinger function, and write:

Plot.areaY(
  aapl,
  ((A) => ({ ...A, y1: { transform: () => A.y1.transform().map((d) => -d) } }))(
    Plot.windowY({
      reduce: (data) => d3.mean(data) + K * d3.deviation(data),
      y1: (d) => -d.close,
      y2: "close",
      k: N,
      x: "date",
      fill: "lightblue"
    })
  )
)

untitled (1)

And if you prefer the log version:

    Plot.areaY(
      aapl,
      ((A) => ({
        ...A,
        y1: {
          transform: () => A.y1.transform().map((d) => Math.exp(-d))
        },
        y2: {
          transform: () => A.y2.transform().map((d) => Math.exp(d))
        }
      }))(
        Plot.windowY({
          reduce: (data) => d3.mean(data) + K * d3.deviation(data),
          y1: (d) => -Math.log(d.close),
          y2: (d) => Math.log(d.close),
          k: N,
          x: "date",
          fill: "lightblue"
        })
      )
    ),

untitled (2)

The log version allows to see that the volatility (I think that's the name of the relative deviation?) was higher in the 2008 episodes than in 2012. I suppose that this doesn't matter much when you're using the band to base a decision on the latest values, but if if you're doing history/time analysis it seems wrong to work on linear values.

Note that you can compute the log version without using a log scale to plot, and it makes the chart arguably more interesting (or "accurate") than the all-linear chart; at least the prices never go below zero!

untitled (5)

@mbostock
Copy link
Member Author

I’m pretty happy with our solution using Plot.window, so going to close this now, but we could always add something more built-in in the future if desired.

@Fil
Copy link
Contributor

Fil commented Mar 23, 2023

@mbostock
Copy link
Member Author

I still think this would be nice to include, especially given how concise it could be with a compound mark that does both the bands and the center line. Something like:

function bollingerBandY(data, options) {
  return Plot.marks(
    Plot.areaY(data, bollingerMapY(4, 2, {x: "date", y: "value", fillOpacity: 0.2})),
    Plot.lineY(data, Plot.map({y: bollinger(4, 0)}, {x: "date", y: "value", stroke: "blue"})),
    Plot.lineY(data, {x: "date", y: "value", strokeWidth: 1})
  );
}
function bollingerMapY(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"});
}

@mbostock mbostock reopened this Mar 27, 2023
@Fil
Copy link
Contributor

Fil commented Mar 28, 2023

With options passed as k (window size, formerly N), and multiplier (formerly K)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants