Charts & data · Free tool
CHC Accumulation Zones
Get the script · Pine Script v6
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
- 1Open a BTC chart on TradingView and click Pine Editor at the bottom of the screen.
- 2Click inside the editor, select all (Ctrl/Cmd + A) and delete the template code.
- 3Click Copy script above (or download the file) and paste it into the editor.
- 4Click Save and give it a name.
- 5Click Add to chart.
- 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.