Skip to content
COLD HARDCUSTODY

Charts & data · Free tool

CHC Accumulation Zones

Get the script · Pine Script v6

Download .pine
View the full source (184 lines)
//@version=6
// ============================================================================
//  CHC Accumulation Zones  ·  Cold Hard Custody ("Everything Bitcoin")
// ----------------------------------------------------------------------------
//  EDUCATION ONLY. NOT FINANCIAL ADVICE. This script shows where BTC price
//  sits relative to well-known long-term valuation models and labels the
//  result as a historical ZONE ("Historically cheap", "Neutral",
//  "Historically overheated"). It never produces buy or sell signals and it
//  makes no prediction about future price. Models can and do fail.
//
//  Components (all computed from the chart's own price data):
//    1. 200-week SMA and price / 200WMA ratio
//    2. Mayer Multiple = price / 200-day SMA (Trace Mayer)
//    3. Power-law fair value: A * (days since genesis block)^B
//       Defaults A = 1.0117e-17, B = 5.82 (Giovanni Santostasi's published
//       model, as reproduced at charts.bgeometrics.com/power_law.html and
//       bitcoinfairprice.com). Independent OLS refits report B near 5.69
//       (btcpowerlaw.nl/research/reproducing-power-law/), so both are inputs.
//    4. Pi Cycle Top: 111-day SMA vs 2 x 350-day SMA (Philip Swift).
//       Pi Cycle can ONLY add to the "overheated" side; it never says "cheap".
//
//  Zone thresholds are adjustable inputs. Defaults marked "CHC setting" are
//  editorial choices for display, not sourced statistics.
//  v2: table position input, soft always-on zone tint, MVRV via
//  COINMETRICS:BTC_MVRV (display only unless toggled into the composite).
//  WEEKLY (1W) timeframe is best for viewing; daily also works.
//  Intended for BTC daily charts with long history (e.g. INDEX:BTCUSD or
//  BITSTAMP:BTCUSD) so the 200-week and 350-day averages have enough data.
// ============================================================================
indicator("CHC Accumulation Zones", shorttitle = "CHC Zones", overlay = true, max_bars_back = 2000)

// ---------- Colours (CHC theme) ----------
color ORANGE   = #F7931A          // Bitcoin orange: highlights / overheated
color COOL     = #4FA3D9          // cool blue: historically cheap
color GREY     = #8A8F98          // neutral
color DARK     = #15171C          // table background
color TEXT     = #E6E6E6

// ---------- Inputs ----------
string G1 = "200-week SMA"
int    wmaLen     = input.int(200, "Length (weeks)", minval = 10, group = G1)
float  wmaCheap   = input.float(1.0, "Cheap below ratio", step = 0.05, group = G1, tooltip = "Price below the 200-week SMA. CHC setting.")
float  wmaHot     = input.float(3.0, "Overheated above ratio", step = 0.1, group = G1, tooltip = "CHC setting, adjust to taste.")

string G2 = "Mayer Multiple"
int    dmaLen     = input.int(200, "SMA length (days)", minval = 10, group = G2)
float  mayerCheap = input.float(1.0, "Cheap below", step = 0.05, group = G2, tooltip = "Price below its 200-day SMA. CHC setting.")
float  mayerHot   = input.float(2.4, "Overheated above", step = 0.1, group = G2, tooltip = "2.4 is the level Trace Mayer originally discussed.")

string G3 = "Power law (Santostasi)"
float  plA        = input.float(1.0117e-17, "Coefficient A", group = G3, tooltip = "Default 1.0117e-17 (Santostasi model as published by BGeometrics / bitcoinfairprice.com).")
float  plB        = input.float(5.82, "Exponent B", step = 0.01, group = G3, tooltip = "Default 5.82 (Santostasi). An independent OLS refit gives ~5.69 (btcpowerlaw.nl).")
float  plCheap    = input.float(1.0, "Cheap below price / fair value", step = 0.05, group = G3, tooltip = "Below modelled fair value. CHC setting.")
float  plHot      = input.float(2.5, "Overheated above price / fair value", step = 0.1, group = G3, tooltip = "CHC setting, adjust to taste.")
bool   showPL     = input.bool(true, "Plot power-law fair value", group = G3)

string G4 = "Pi Cycle Top"
int    piFast     = input.int(111, "Fast SMA (days)", group = G4)
int    piSlow     = input.int(350, "Slow SMA (days, multiplied by 2)", group = G4)
int    piMemory   = input.int(30, "Count as overheated for N bars after a cross", minval = 0, group = G4, tooltip = "CHC setting.")

string G5 = "Composite"
int    minAgree   = input.int(2, "Components needed to agree", minval = 1, maxval = 5, group = G5, tooltip = "Best viewed on the WEEKLY (1W) timeframe, zoomed out, so the 200-week SMA and power-law lines and past zones are visible.")
bool   colorBars  = input.bool(true, "Colour price bars", group = G5)
bool   colorBg    = input.bool(true, "Colour background", group = G5)
int    bgTransp   = input.int(80, "Strong shading transparency (signals agree)", minval = 0, maxval = 100, group = G5)
bool   softTint   = input.bool(true, "Always-on soft tint (leans cheap / neutral / hot)", group = G5, tooltip = "Light tint on every bar: blue when more components lean cheap, orange when more lean hot, grey otherwise. Weekly (1W) timeframe is best.")
int    softTransp = input.int(90, "Soft tint transparency", minval = 0, maxval = 100, group = G5)
bool   tintNeutral = input.bool(true, "Tint neutral bars grey", group = G5)
string tablePos   = input.string("top_right", "Table position", options = ["top_left", "top_center", "top_right", "middle_left", "middle_center", "middle_right", "bottom_left", "bottom_center", "bottom_right"], group = G5, tooltip = "TradingView tables cannot be dragged; pick a spot here.")

string G6 = "MVRV (Coin Metrics on-chain data)"
string mvrvSym    = input.symbol("COINMETRICS:BTC_MVRV", "MVRV symbol (blank = off)", group = G6, tooltip = "Default: Coin Metrics BTC MVRV (market cap / realized cap). Shows n/a if the feed is not available on your plan.")
float  mvrvCheap  = input.float(1.0, "Cheap below", step = 0.05, group = G6, tooltip = "Market value below realized value. CHC setting.")
float  mvrvHot    = input.float(3.5, "Overheated above", step = 0.1, group = G6, tooltip = "CHC setting, adjust to taste.")
bool   mvrvInComp = input.bool(false, "Include MVRV in composite", group = G6, tooltip = "Off by default: MVRV is display only.")

// ---------- Helpers ----------
// Returns -1 (cheap), 0 (neutral), 1 (overheated) for a ratio.
zoneOf(float v, float cheapLvl, float hotLvl) =>
    na(v) ? 0 : v < cheapLvl ? -1 : v > hotLvl ? 1 : 0

zoneText(int z) =>
    z < 0 ? "Historically cheap" : z > 0 ? "Historically overheated" : "Neutral"

zoneColor(int z) =>
    z < 0 ? COOL : z > 0 ? ORANGE : GREY

// ---------- 1. 200-week SMA (non-repainting: last completed week) ----------
float wma200 = request.security(syminfo.tickerid, "W", ta.sma(close, wmaLen)[1], lookahead = barmerge.lookahead_on)
float wmaRatio = close / wma200
int   zWma = zoneOf(wmaRatio, wmaCheap, wmaHot)

// ---------- 2. Mayer Multiple (daily data) ----------
float dma200 = request.security(syminfo.tickerid, "D", ta.sma(close, dmaLen)[1], lookahead = barmerge.lookahead_on)
float mayer = close / dma200
int   zMayer = zoneOf(mayer, mayerCheap, mayerHot)

// ---------- 3. Power law ----------
// Days since the genesis block, 3 Jan 2009 00:00 UTC.
int   GENESIS = timestamp("UTC", 2009, 1, 3, 0, 0)
float days    = (time - GENESIS) / 86400000.0
float plFair  = days > 0 ? plA * math.pow(days, plB) : na
float plRatio = close / plFair
int   zPL     = zoneOf(plRatio, plCheap, plHot)

// ---------- 4. Pi Cycle Top (overheated side only) ----------
[piF, piS] = request.security(syminfo.tickerid, "D", [ta.sma(close, piFast)[1], 2 * ta.sma(close, piSlow)[1]], lookahead = barmerge.lookahead_on)
bool  piCross = ta.crossover(piF, piS)
int   barsSincePi = ta.barssince(piCross)
bool  piHot = (not na(piF) and not na(piS) and piF >= piS) or (not na(barsSincePi) and barsSincePi <= piMemory)
int   zPi   = piHot ? 1 : 0

// ---------- Optional MVRV (display only) ----------
// Always call request.security (with a fallback symbol) so the call is not conditional; blank input => na.
float mvrvRaw = request.security(mvrvSym == "" ? syminfo.tickerid : mvrvSym, timeframe.period, close, ignore_invalid_symbol = true)
float mvrv = mvrvSym == "" ? na : mvrvRaw
int   zMvrv = zoneOf(mvrv, mvrvCheap, mvrvHot)
int   zMvrvComp = mvrvInComp ? zMvrv : 0

// ---------- Composite zone ----------
int nCheap = (zWma < 0 ? 1 : 0) + (zMayer < 0 ? 1 : 0) + (zPL < 0 ? 1 : 0) + (zMvrvComp < 0 ? 1 : 0)
int nHot   = (zWma > 0 ? 1 : 0) + (zMayer > 0 ? 1 : 0) + (zPL > 0 ? 1 : 0) + zPi + (zMvrvComp > 0 ? 1 : 0)
int zone   = nHot >= minAgree ? 1 : (nCheap >= minAgree and nHot == 0) ? -1 : 0

// Soft lean: which side has more components, regardless of agreement.
int lean = nCheap > nHot ? -1 : nHot > nCheap ? 1 : 0

// ---------- Plots ----------
plot(wma200, "200-week SMA", color = color.new(COOL, 0), linewidth = 3)
plot(showPL ? plFair : na, "Power-law fair value", color = color.new(ORANGE, 0), linewidth = 3)
plotshape(piCross, "Pi Cycle cross", shape.xcross, location.abovebar, ORANGE, size = size.small)
barcolor(colorBars ? zoneColor(zone) : na)
color softCol = (lean != 0 or tintNeutral) ? color.new(zoneColor(lean), lean == 0 ? math.min(100, softTransp + 6) : softTransp) : na
bgcolor(softTint ? softCol : na, title = "Soft tint")
bgcolor(colorBg and zone != 0 ? color.new(zoneColor(zone), bgTransp) : na, title = "Strong shading")

// ---------- Table ----------
tblPos(string p) =>
    switch p
        "top_left"      => position.top_left
        "top_center"    => position.top_center
        "middle_left"   => position.middle_left
        "middle_center" => position.middle_center
        "middle_right"  => position.middle_right
        "bottom_left"   => position.bottom_left
        "bottom_center" => position.bottom_center
        "bottom_right"  => position.bottom_right
        => position.top_right

var table t = table.new(tblPos(tablePos), 3, 7, bgcolor = color.new(DARK, 10), border_color = color.new(GREY, 60), border_width = 1)
fmt(float v) =>
    na(v) ? "n/a" : str.tostring(v, "#.##")

if barstate.islast
    table.cell(t, 0, 0, "CHC Accumulation Zones", text_color = ORANGE, text_size = size.small)
    table.cell(t, 1, 0, "Value", text_color = TEXT, text_size = size.small)
    table.cell(t, 2, 0, "Zone", text_color = TEXT, text_size = size.small)
    table.cell(t, 0, 1, "Price / 200W SMA", text_color = TEXT, text_size = size.small)
    table.cell(t, 1, 1, fmt(wmaRatio), text_color = TEXT, text_size = size.small)
    table.cell(t, 2, 1, zoneText(zWma), text_color = zoneColor(zWma), text_size = size.small)
    table.cell(t, 0, 2, "Mayer Multiple", text_color = TEXT, text_size = size.small)
    table.cell(t, 1, 2, fmt(mayer), text_color = TEXT, text_size = size.small)
    table.cell(t, 2, 2, zoneText(zMayer), text_color = zoneColor(zMayer), text_size = size.small)
    table.cell(t, 0, 3, "Price / power law", text_color = TEXT, text_size = size.small)
    table.cell(t, 1, 3, fmt(plRatio), text_color = TEXT, text_size = size.small)
    table.cell(t, 2, 3, zoneText(zPL), text_color = zoneColor(zPL), text_size = size.small)
    table.cell(t, 0, 4, "Pi Cycle (111 vs 2x350)", text_color = TEXT, text_size = size.small)
    table.cell(t, 1, 4, fmt(piF / piS), text_color = TEXT, text_size = size.small)
    table.cell(t, 2, 4, piHot ? "Historically overheated" : "Neutral", text_color = zoneColor(zPi), text_size = size.small)
    table.cell(t, 0, 5, "MVRV (Coin Metrics)", text_color = TEXT, text_size = size.small)
    table.cell(t, 1, 5, fmt(mvrv), text_color = TEXT, text_size = size.small)
    table.cell(t, 2, 5, na(mvrv) ? "n/a on this plan" : zoneText(zMvrv) + (mvrvInComp ? "" : " (display)"), text_color = na(mvrv) ? GREY : zoneColor(zMvrv), text_size = size.small)
    table.cell(t, 0, 6, "COMPOSITE", text_color = ORANGE, text_size = size.small)
    table.cell(t, 1, 6, str.tostring(nCheap) + " cheap / " + str.tostring(nHot) + " hot", text_color = TEXT, text_size = size.small)
    table.cell(t, 2, 6, zoneText(zone), text_color = zoneColor(zone), text_size = size.small)

// ---------- Alerts (zone changes only; informational, not signals) ----------
bool zoneChanged = zone != zone[1]
alertcondition(zoneChanged and zone == -1, "Entered: Historically cheap", "CHC Zones: {{ticker}} entered the Historically cheap zone. Education only, not advice.")
alertcondition(zoneChanged and zone == 0, "Entered: Neutral", "CHC Zones: {{ticker}} entered the Neutral zone. Education only, not advice.")
alertcondition(zoneChanged and zone == 1, "Entered: Historically overheated", "CHC Zones: {{ticker}} entered the Historically overheated zone. Education only, not advice.")
alertcondition(zoneChanged, "Any zone change", "CHC Zones: {{ticker}} changed zone. Education only, not advice.")

Setup in six steps

  1. 1Open a BTC chart on TradingView and click Pine Editor at the bottom of the screen.
  2. 2Click inside the editor, select all (Ctrl/Cmd + A) and delete the template code.
  3. 3Click Copy script above (or download the file) and paste it into the editor.
  4. 4Click Save and give it a name.
  5. 5Click Add to chart.
  6. 6Switch the chart to the 1W (weekly) timeframe and zoom out to see the long-run lines and past zones.

The table can't be dragged (a TradingView limit), but the gear icon has a Table position setting with nine spots. Shading, tint and thresholds are adjustable there too.

What the five signals mean

Each signal reads cheap, neutral or hot. Strong shading appears when at least two agree (adjustable); a soft tint follows whichever side has more.

200-week SMA
Price relative to its ~4-year average. Below 1.0× leans cheap; above 3.0× leans hot. Learn more
Mayer Multiple
Price ÷ 200-day average. Below 1.0 leans cheap; above 2.4 (the level Trace Mayer discussed) leans hot. Learn more
Power-law fair value
Price ÷ a long-run log-log trend line. Below 1.0× leans cheap; above 2.5× leans hot. A descriptive fit, not a forecast. Learn more
Pi Cycle Top
Flags overheating when the 111-day average crosses above 2× the 350-day average; it counts as hot for 30 bars after a cross. Learn more
MVRV (via Coin Metrics)
Market cap ÷ realized cap, loaded from Coin Metrics data on TradingView. Below 1.0 leans cheap; above 3.5 leans hot. It shows n/a if your TradingView plan doesn't include the feed, and it is display-only unless you switch it into the composite in settings. Learn more

Want the history, formulas and failures behind each one? See Market indicators.

Get updates

New tools and the weekly brief, free.

Signups are handled by beehiiv; until the form loads, the button opens beehiiv’s own subscribe page (privacy policy). Unsubscribe any time.