This is the TradingView companion to the KairosBias meter: a table on your chart scoring all 8 major currencies from -100 to +100 and ranking them strongest to weakest, with a divergence row for whatever G8 pair you have open. It mirrors the two price-based layers of the full methodology, EMA structure and momentum, using the same production formulas, computed on daily data.
It is completely free and the entire source is below. TradingView currently paywalls publishing to its public script library (and we spend money on data, not badges), so instead of waiting we publish the code here. Free TradingView accounts can run their own pasted scripts without any paid plan, which means everyone gets the indicator anyway. When it eventually lands in the TradingView library, this page will link to it.
Install it in 60 seconds (works on free accounts)
- ·Open any chart on TradingView and click the Pine Editor tab at the bottom of the screen.
- ·Delete whatever starter code is in the editor and paste the full script from this page (Copy button below).
- ·Press Save (name it anything), then Add to chart. The ranked strength table appears in the top right.
- ·Leave the Calculation timeframe input at 1 day - that is what the site uses. Lower timeframes give a faster but much noisier read.
- ·If the score column says 'no data', your plan lacks the OANDA feed: open the indicator settings and switch Data source to FX: or FX_IDC:.
The full source
Open-source on purpose: transparency is the whole point of KairosBias. You can also download the raw .pine file directly.
//@version=6
// ─────────────────────────────────────────────────────────────────────────────
// KairosBias Currency Strength Meter
//
// Free, open-source companion to kairosbias.com. Mirrors the two price-based
// layers of the KairosBias methodology, using the same formulas the site runs
// in production:
// - EMA structure (graded 20/50/200 stack) - default 64% weight
// - Momentum (EMA-margin blend, 3 horizons) - default 36% weight
// Both are computed on DAILY data (the site uses daily EMAs), averaged across
// the same pairs the site uses: the 7 USD pairs + EURGBP.
//
// The full model on the website adds CFTC COT positioning (30%) and interest
// rate differentials (15%), which cannot be computed inside Pine - so compare
// this table to the EMA and momentum sub-scores on kairosbias.com/meter, not
// to the composite.
//
// Not financial advice. Analytical context only.
//
// NOTE: this file is republished on /learn/tradingview-strength-meter and as
// public/tradingview/kairosbias-strength-meter.pine. After editing, run:
// node scripts/generate-pine-source.mjs
// cp marketing/tradingview/kairosbias-strength-meter.pine public/tradingview/
// ─────────────────────────────────────────────────────────────────────────────
indicator("KairosBias Currency Strength Meter", shorttitle = "KairosBias", overlay = true)
// ── Inputs ───────────────────────────────────────────────────────────────────
grpCalc = "Calculation"
tf = input.timeframe("D", "Calculation timeframe", group = grpCalc, tooltip = "The site computes its EMA layers on daily candles - keep D to match kairosbias.com. Lower timeframes give a faster, noisier read.")
prefix = input.string("OANDA:", "Data source", options = ["OANDA:", "FX:", "FX_IDC:", "SAXO:", "PEPPERSTONE:", "FOREXCOM:"], group = grpCalc, tooltip = "If the table shows 'no data', your plan may not include this feed - try another source.")
emaFast = input.int(20, "Fast EMA", minval = 2, group = grpCalc)
emaMid = input.int(50, "Mid EMA", minval = 5, group = grpCalc)
emaSlow = input.int(200, "Slow EMA", minval = 20, group = grpCalc)
emaWeight = input.float(64, "EMA structure weight %", minval = 0, maxval = 100, group = grpCalc, tooltip = "Remainder goes to momentum. The site weighs EMA 35 and momentum 20 out of 100 with COT and rates taking the rest; with only these two layers available that renormalizes to 64/36.")
grpUi = "Display"
tablePos = input.string("Top right", "Table position", options = ["Top right", "Top left", "Bottom right", "Bottom left", "Middle right"], group = grpUi)
tableSize = input.string("Small", "Text size", options = ["Tiny", "Small", "Normal"], group = grpUi)
// ── Per-pair scores - same formulas as the site's production scorers ─────────
// EMA structure: graded stack (full stack ±100, partial ±60, vs slow EMA ±25).
// Momentum: three EMA-margin signals (price/fast, fast/mid, mid/slow), each
// ±(40 + up-to-30 margin bonus, capped at a 2% margin), blended 20/35/45.
f_pair() =>
e20 = ta.ema(close, emaFast)
e50 = ta.ema(close, emaMid)
e200 = ta.ema(close, emaSlow)
a20 = close > e20
a50 = close > e50
a200 = close > e200
s2050 = e20 > e50
s50200 = e50 > e200
stack = a20 and a50 and a200 and s2050 and s50200 ? 100.0 : not a20 and not a50 and not a200 and not s2050 and not s50200 ? -100.0 : a50 and a200 and s50200 ? 60.0 : not a50 and not a200 and not s50200 ? -60.0 : a200 ? 25.0 : -25.0
shortBonus = math.min(math.abs(close - e20) / e20 / 0.02, 1.0) * 30.0
medBonus = math.min(math.abs(e20 - e50) / e50 / 0.02, 1.0) * 30.0
longBonus = math.min(math.abs(e50 - e200) / e200 / 0.02, 1.0) * 30.0
sShort = (a20 ? 1.0 : -1.0) * (40.0 + shortBonus)
sMed = (s2050 ? 1.0 : -1.0) * (40.0 + medBonus)
sLong = (s50200 ? 1.0 : -1.0) * (40.0 + longBonus)
mom = sShort * 0.2 + sMed * 0.35 + sLong * 0.45
[stack, mom]
// The 8 pairs the site actually stores and scores.
[kEU, mEU] = request.security(prefix + "EURUSD", tf, f_pair(), ignore_invalid_symbol = true)
[kGB, mGB] = request.security(prefix + "GBPUSD", tf, f_pair(), ignore_invalid_symbol = true)
[kJP, mJP] = request.security(prefix + "USDJPY", tf, f_pair(), ignore_invalid_symbol = true)
[kCA, mCA] = request.security(prefix + "USDCAD", tf, f_pair(), ignore_invalid_symbol = true)
[kAU, mAU] = request.security(prefix + "AUDUSD", tf, f_pair(), ignore_invalid_symbol = true)
[kNZ, mNZ] = request.security(prefix + "NZDUSD", tf, f_pair(), ignore_invalid_symbol = true)
[kCH, mCH] = request.security(prefix + "USDCHF", tf, f_pair(), ignore_invalid_symbol = true)
[kEG, mEG] = request.security(prefix + "EURGBP", tf, f_pair(), ignore_invalid_symbol = true)
// ── Per-currency layers, mirroring the site's pair lists exactly ─────────────
// EMA: average of the pair stack scores, flipped where the currency is quote.
emaUSD = -(nz(kEU) + nz(kGB) + nz(kAU) + nz(kNZ)) / 4.0
emaEUR = (nz(kEU) + nz(kEG)) / 2.0
emaGBP = (nz(kGB) - nz(kEG)) / 2.0
emaJPY = -nz(kJP)
emaCAD = -nz(kCA)
emaAUD = nz(kAU)
emaNZD = nz(kNZ)
emaCHF = -nz(kCH)
// Momentum: one representative pair per currency, flipped where quote.
momUSD = -nz(mEU)
momEUR = nz(mEU)
momGBP = nz(mGB)
momJPY = -nz(mJP)
momCAD = -nz(mCA)
momAUD = nz(mAU)
momNZD = nz(mNZ)
momCHF = -nz(mCH)
f_blend(e, m) => (e * emaWeight + m * (100.0 - emaWeight)) / 100.0
scUSD = f_blend(emaUSD, momUSD)
scEUR = f_blend(emaEUR, momEUR)
scGBP = f_blend(emaGBP, momGBP)
scJPY = f_blend(emaJPY, momJPY)
scCAD = f_blend(emaCAD, momCAD)
scAUD = f_blend(emaAUD, momAUD)
scNZD = f_blend(emaNZD, momNZD)
scCHF = f_blend(emaCHF, momCHF)
// Missing feed detection (all core pairs na -> show "no data" instead of 0s).
feedOk = not (na(kEU) and na(kGB) and na(kJP) and na(kCA))
// ── Labels + colors (thresholds match kairosbias.com) ────────────────────────
f_bias(s) =>
na(s) ? "N/A" : s >= 60 ? "STRONG BULL" : s >= 20 ? "BULL" : s <= -60 ? "STRONG BEAR" : s <= -20 ? "BEAR" : "NEUTRAL"
f_color(s) =>
na(s) ? color.new(#8899BB, 0) : s >= 20 ? color.new(#20D468, 0) : s <= -20 ? color.new(#F1425A, 0) : color.new(#8899BB, 0)
f_bar(s) =>
if na(s)
""
else
n = math.min(5, math.max(0, math.round(math.abs(s) / 20.0)))
str.repeat("▮", n) + str.repeat("·", 5 - n)
// Subtle row heat: stronger score = slightly stronger tint behind the score.
f_rowBg(s) =>
na(s) ? color.new(#0A0E17, 100) :
s >= 20 ? color.new(#20D468, math.max(82, 96 - math.round(math.abs(s) / 8.0))) :
s <= -20 ? color.new(#F1425A, math.max(82, 96 - math.round(math.abs(s) / 8.0))) :
color.new(#8899BB, 96)
f_score(s) => na(s) ? "no data" : (s > 0 ? "+" : "") + str.tostring(math.round(s))
// ── Chart-pair divergence (only when the chart is a G8 pair) ─────────────────
f_strength(cur) =>
switch cur
"USD" => scUSD
"EUR" => scEUR
"GBP" => scGBP
"JPY" => scJPY
"AUD" => scAUD
"NZD" => scNZD
"CAD" => scCAD
"CHF" => scCHF
=> na
baseStr = f_strength(syminfo.basecurrency)
quoteStr = f_strength(syminfo.currency)
pairDiv = baseStr - quoteStr
pairBias = f_bias(pairDiv)
// Bias-flip alerts for the charted pair.
flippedBull = not na(pairDiv) and pairDiv >= 20 and pairDiv[1] < 20
flippedBear = not na(pairDiv) and pairDiv <= -20 and pairDiv[1] > -20
alertcondition(flippedBull, "Chart pair flipped BULL", "KairosBias: chart pair strength divergence crossed above +20 (BULL)")
alertcondition(flippedBear, "Chart pair flipped BEAR", "KairosBias: chart pair strength divergence crossed below -20 (BEAR)")
// ── Table ─────────────────────────────────────────────────────────────────────
posOpt = tablePos == "Top left" ? position.top_left : tablePos == "Bottom right" ? position.bottom_right : tablePos == "Bottom left" ? position.bottom_left : tablePos == "Middle right" ? position.middle_right : position.top_right
sizeOpt = tableSize == "Tiny" ? size.tiny : tableSize == "Normal" ? size.normal : size.small
var color BG = color.new(#0A0E17, 15)
var color BORDER = color.new(#1E2438, 0)
var color HEAD = color.new(#D4AF37, 0)
var color MUTED = color.new(#55678A, 0)
var color TEXT = color.new(#D4DCE8, 0)
var table t = table.new(posOpt, 5, 13, bgcolor = BG, frame_color = BORDER, frame_width = 1)
if barstate.islast
// Sort currencies by score, descending.
codes = array.from("USD", "EUR", "GBP", "JPY", "AUD", "NZD", "CAD", "CHF")
vals = array.from(scUSD, scEUR, scGBP, scJPY, scAUD, scNZD, scCAD, scCHF)
for i = 0 to 6
for j = 0 to 6 - i
a = array.get(vals, j)
b = array.get(vals, j + 1)
if nz(b, -999) > nz(a, -999)
array.set(vals, j, b)
array.set(vals, j + 1, a)
tmp = array.get(codes, j)
array.set(codes, j, array.get(codes, j + 1))
array.set(codes, j + 1, tmp)
// Header: brand left, column labels muted. Padded strings act as cell padding.
table.cell(t, 0, 0, " KAIROSBIAS ", text_color = HEAD, text_size = sizeOpt)
table.cell(t, 1, 0, "", text_color = MUTED, text_size = sizeOpt)
table.cell(t, 2, 0, "SCORE ", text_color = MUTED, text_size = sizeOpt, text_halign = text.align_right)
table.cell(t, 3, 0, "", text_color = MUTED, text_size = sizeOpt)
table.cell(t, 4, 0, "BIAS ", text_color = MUTED, text_size = sizeOpt)
table.merge_cells(t, 0, 0, 1, 0)
for i = 0 to 7
cur = array.get(codes, i)
s = feedOk ? array.get(vals, i) : na
c = f_color(s)
bg = f_rowBg(s)
table.cell(t, 0, i + 1, " " + str.tostring(i + 1), text_color = MUTED, text_size = size.tiny, bgcolor = bg, text_halign = text.align_right)
table.cell(t, 1, i + 1, cur, text_color = TEXT, text_size = sizeOpt, bgcolor = bg)
table.cell(t, 2, i + 1, f_score(s) + " ", text_color = c, text_size = sizeOpt, bgcolor = bg, text_halign = text.align_right)
table.cell(t, 3, i + 1, f_bar(s), text_color = c, text_size = sizeOpt, bgcolor = bg)
table.cell(t, 4, i + 1, f_bias(s) + " ", text_color = c, text_size = sizeOpt, bgcolor = bg)
// Chart-pair divergence row (only on G8 pairs), visually separated.
if feedOk and not na(pairDiv)
table.cell(t, 0, 9, "", text_size = size.tiny)
table.merge_cells(t, 0, 9, 4, 9)
table.cell(t, 0, 10, " Δ", text_color = HEAD, text_size = sizeOpt)
table.cell(t, 1, 10, syminfo.basecurrency + "/" + syminfo.currency, text_color = HEAD, text_size = sizeOpt)
table.cell(t, 2, 10, f_score(pairDiv) + " ", text_color = f_color(pairDiv), text_size = sizeOpt, text_halign = text.align_right)
table.cell(t, 3, 10, f_bar(pairDiv / 2), text_color = f_color(pairDiv), text_size = sizeOpt)
table.cell(t, 4, 10, pairBias + " ", text_color = f_color(pairDiv), text_size = sizeOpt)
table.cell(t, 0, 11, " EMA " + str.tostring(math.round(emaWeight)) + "% · MOM " + str.tostring(math.round(100 - emaWeight)) + "% · " + tf + " ", text_color = MUTED, text_size = size.tiny)
table.merge_cells(t, 0, 11, 4, 11)
table.cell(t, 0, 12, " kairosbias.com · adds COT + rates · not financial advice ", text_color = MUTED, text_size = size.tiny)
table.merge_cells(t, 0, 12, 4, 12)
What it can and cannot show
The indicator computes the price half of the model: the graded 20/50/200 EMA stack (64% weight here) and the three-horizon momentum blend (36%). The other half of the full KairosBias score, CFTC COT positioning and central-bank rate differentials, cannot exist in Pine: TradingView scripts run in a sandbox with no access to external data, so no indicator on the platform can compute them. That is not a limitation we chose, it is the platform boundary, and it is why the complete 4-layer model with all 28 ranked pairs lives on the site instead.
Common questions
Yes. TradingView's paywall applies to publishing scripts to the public library and commenting, not to running code you paste into your own Pine Editor. Copy the source from this page, paste, save, add to chart.
Pine Script cannot fetch external data - no API calls, no CFTC files, no rate feeds. Any TradingView indicator claiming to show live COT data is baking in stale numbers by hand. The COT and rate-differential layers are computed server-side on kairosbias.com, where the free meter shows them for four currencies.
Compare against the EMA and momentum sub-scores on the meter, not the headline composite (which is 45% COT and rates). Small remaining drift comes from feed differences and EMA history depth. Sign-level disagreements usually mean the Calculation timeframe input was changed off 1 day.
See it live, free.
Four of the 8 major currencies scored from EMA structure, COT positioning, momentum and rate differentials, refreshed every 4 hours. No card, no expiry.
Open the free meterAnalytical tool, not financial advice