Edge or Not

Bitcoin rainbow chart model

Bitcoin's price on a log scale against a regression line fitted to its whole history, with nine coloured bands around it. Blue is read as "cheap", red as "expensive". Updated every few hours.

Now (price as of 21 Oct 2026, 20:03 UTC): BTC at $84,455 sits in band 4 of 9 - "Still cheap" on this chart. Fitted only on data known before this month, it would be band 4 ("Still cheap"). Does buying blue and selling red actually work? Tested without hindsight: no.

BTC/USD (Bitstamp), weekly, log scale; bands extend to 2028
123456789— price

Band prices today

BandNameFromTo
9Maximum bubble$369,432$506,120
8Sell$269,660$369,432
7Is this a bubble?$196,833$269,660
6Hold$143,674$196,833
5Fair$104,872$143,674
4Still cheap$76,550$104,872
3Accumulate$55,876$76,550
2Buy$40,785$55,876
1Fire sale$29,771$40,785
These bands are drawn with hindsight. The line is fitted to all prices up to today, so every past top and bottom helped draw the bands that seem to predict them. Fitted only on data available at the time, the bands sat elsewhere - and a buy-blue, sell-red rule sold in May 2017 at $2,298 and did not buy back until June 2022 at $19,925. Full test.

How the chart is built

  1. Daily BTC/USD closes from Bitstamp since December 2012.
  2. A straight line is fitted to log10(price) against log10(days since the genesis block, 3 January 2009).
  3. Bands are drawn at 0.5 standard deviations of the distance from that line: band 5 is the line ±0.25 sd, bands 1 and 9 are beyond ±1.75 sd.

Popular rainbow charts use the same idea with slightly different curves and band widths. None of the bands is fixed: the fitted curve moves with every new price. Monthly returns since 2013 are on the bitcoin monthly returns page.

Use it on TradingView (free Pine Script)

The same model as an open-source indicator: both versions of the bands on one chart - fitted with hindsight (translucent) and refitted bar by bar on past data only (solid) - plus a table with today's band under each.

  1. Open a daily BITSTAMP:BTCUSD chart on TradingView and switch the price scale to logarithmic (Alt+L).
  2. Open Pine Editor, replace its contents with the code below and click "Add to chart".
  3. Zoom out with the mouse wheel (the 5Y/All buttons switch to weekly or monthly bars) and compare 2017 and 2021 in both versions.
download .pine
// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © edgeornot
//@version=6
indicator("Bitcoin Rainbow: with vs without hindsight [Edge or Not]", shorttitle = "Rainbow hindsight test", overlay = true,
     max_polylines_count = 20, max_bars_back = 5000)

// ─── Inputs ─────────────────────────────────────────────────────────────────────────────
showHonest    = input.bool(true,  "Bands without hindsight (fit on past bars only)", group = "Show")
showHindsight = input.bool(true,  "Bands with hindsight (one fit on all bars)",      group = "Show")
minBars       = input.int(1000, "Bars before the honest fit starts", minval = 100, group = "Fit")
stepBars      = input.int(5,    "Hindsight bands: draw every N bars", minval = 1, maxval = 30, group = "Fit")
showTable     = input.bool(true, "Show current band table", group = "Show")
fitStart      = input.time(timestamp("2012-12-01T00:00:00"), "Fit start date (earlier bars ignored)", group = "Fit",
     tooltip = "Default matches the Edge or Not live chart and test (Bitstamp data from Dec 2012). Popular rainbow charts often start in 2010-2011.")

// ─── Model: log10(price) = a + b * log10(days since genesis block), 9 bands by residual z ─
GENESIS = timestamp("UTC", 2009, 1, 3, 0, 0)
var array<float> Z = array.from(-2.25, -1.75, -1.25, -0.75, -0.25, 0.25, 0.75, 1.25, 1.75, 2.25)
var array<color> COL = array.from(#2b3a98, #3570d0, #36aecb, #37c48a, #9fd24a, #f0cf3a, #f39b37, #ec6436, #d63737)
var array<string> NAMES = array.from("Fire sale", "Buy", "Accumulate", "Still cheap", "Fair", "Hold", "Is this a bubble?", "Sell", "Maximum bubble")

if not timeframe.isdaily
    runtime.error("Use a daily chart of BTCUSD (for example BITSTAMP:BTCUSD).")

days = (time - GENESIS) / 86400000.0
x = math.log10(math.max(days, 1))
y = math.log10(close)

// running sums of all bars BEFORE the current one -> the honest fit never sees the current or future bars
var float n = 0.0
var float sx = 0.0
var float sy = 0.0
var float sxx = 0.0
var float sxy = 0.0
var float syy = 0.0

fit(float _n, float _sx, float _sy, float _sxx, float _sxy, float _syy) =>
    sxxC = _sxx - _sx * _sx / _n
    sxyC = _sxy - _sx * _sy / _n
    syyC = _syy - _sy * _sy / _n
    b = sxyC / sxxC
    a = (_sy - b * _sx) / _n
    sig = math.sqrt(math.max(syyC - b * sxyC, 0) / (_n - 2))
    [a, b, sig]

bandOf(float z) =>
    int k = 1
    for i = 1 to 8
        if z > Z.get(i)
            k += 1
    k

inFit = time >= fitStart

float ha = na
float hb = na
float hs = na
if inFit and n >= minBars
    [a0, b0, s0] = fit(n, sx, sy, sxx, sxy, syy)
    ha := a0
    hb := b0
    hs := s0

honestEdge(int k) => showHonest and not na(ha) ? math.pow(10, ha + hb * x + Z.get(k) * hs) : na

p0 = plot(honestEdge(0), "Honest edge 0", color.new(COL.get(0), 60), display = display.pane)
p1 = plot(honestEdge(1), "Honest edge 1", color.new(COL.get(0), 60), display = display.pane)
p2 = plot(honestEdge(2), "Honest edge 2", color.new(COL.get(1), 60), display = display.pane)
p3 = plot(honestEdge(3), "Honest edge 3", color.new(COL.get(2), 60), display = display.pane)
p4 = plot(honestEdge(4), "Honest edge 4", color.new(COL.get(3), 60), display = display.pane)
p5 = plot(honestEdge(5), "Honest edge 5", color.new(COL.get(4), 60), display = display.pane)
p6 = plot(honestEdge(6), "Honest edge 6", color.new(COL.get(5), 60), display = display.pane)
p7 = plot(honestEdge(7), "Honest edge 7", color.new(COL.get(6), 60), display = display.pane)
p8 = plot(honestEdge(8), "Honest edge 8", color.new(COL.get(7), 60), display = display.pane)
p9 = plot(honestEdge(9), "Honest edge 9", color.new(COL.get(8), 60), display = display.pane)
fill(p0, p1, color.new(COL.get(0), 78))
fill(p1, p2, color.new(COL.get(1), 78))
fill(p2, p3, color.new(COL.get(2), 78))
fill(p3, p4, color.new(COL.get(3), 78))
fill(p4, p5, color.new(COL.get(4), 78))
fill(p5, p6, color.new(COL.get(5), 78))
fill(p6, p7, color.new(COL.get(6), 78))
fill(p7, p8, color.new(COL.get(7), 78))
fill(p8, p9, color.new(COL.get(8), 78))

// add the current bar to the sums only AFTER using them
if inFit
    n += 1
    sx += x
    sy += y
    sxx += x * x
    sxy += x * y
    syy += y * y

// keep x and time of every bar for the hindsight drawing on the last bar
var array<float> xs = array.new<float>()
var array<int> ts = array.new<int>()
if inFit
    xs.push(x)
    ts.push(time)

if barstate.islast and n > 3
    [fa, fb, fs] = fit(n, sx, sy, sxx, sxy, syy)
    if showHindsight
        for k = 0 to 8
            // sample every stepBars-th bar plus the last one, so the right edge of each band is vertical
            idx = array.new<int>()
            for i = 0 to xs.size() - 1 by stepBars
                idx.push(i)
            if idx.last() != xs.size() - 1
                idx.push(xs.size() - 1)
            pts = array.new<chart.point>()
            for j = 0 to idx.size() - 1
                i = idx.get(j)
                pts.push(chart.point.from_time(ts.get(i), math.pow(10, fa + fb * xs.get(i) + Z.get(k + 1) * fs)))
            for j = idx.size() - 1 to 0
                i = idx.get(j)
                pts.push(chart.point.from_time(ts.get(i), math.pow(10, fa + fb * xs.get(i) + Z.get(k) * fs)))
            polyline.new(pts, closed = true, xloc = xloc.bar_time, line_color = color.new(COL.get(k), 100), fill_color = color.new(COL.get(k), 88))
    if showTable
        zH = (y - (fa + fb * x)) / fs
        string honestTxt = "n/a"
        if not na(ha)
            zO = (y - (ha + hb * x)) / hs
            kO = bandOf(zO)
            honestTxt := str.tostring(kO) + " - " + NAMES.get(kO - 1)
        kH = bandOf(zH)
        tb = table.new(position.top_right, 2, 4, bgcolor = color.new(#0e1117, 10), border_color = color.new(color.gray, 70), border_width = 1)
        tb.cell(0, 0, "Band today", text_color = color.white)
        tb.cell(1, 0, "1 = cheapest, 9 = most expensive", text_color = color.gray)
        tb.cell(0, 1, "With hindsight", text_color = color.white)
        tb.cell(1, 1, str.tostring(kH) + " - " + NAMES.get(kH - 1), text_color = COL.get(kH - 1))
        tb.cell(0, 2, "Without hindsight", text_color = color.white)
        tb.cell(1, 2, honestTxt, text_color = color.white)
        tb.cell(0, 3, "Edge or Not", text_color = #ffb020)
        tb.cell(1, 3, "Hindsight bands fit past tops by construction", text_color = color.gray)

Mozilla Public License 2.0. Default fit start: 1 Dec 2012, as in our test; change it in the inputs.

FAQ

What is the bitcoin rainbow chart?

A logarithmic regression of bitcoin's price against time, with coloured bands around the fitted line. Lower bands are read as cheap, upper bands as expensive.

Does the bitcoin rainbow chart work?

Only in hindsight. With bands fitted on all data, buying in bands 1-3 and selling in 7-9 turned $1 into $398 from 2016 to 2026 against $194 for buy-and-hold. With bands fitted only on data known at the time, the same rule made $22.

Is it a price prediction?

No. It assumes bitcoin keeps growing along the same curve; the curve itself changes as new prices arrive. Not investment advice.