Skip to Main Content
Back to website
Indicator Disclaimer | Report this indicator

LinkLine is a clean, volatility-aware Heikin Ashi channel designed to reveal directional bias without clutter. It blends dual smoothed Heikin Ashi boundaries with an adaptive z-score model, so the lines only widen and brighten when moves are statistically meaningful. #TSBuild25

  • Bullish bias → channel glows mint green.
  • Bearish bias → channel glows coral red.
  • Neutral bias → channel rests in soft gray.
  • Strength adapts dynamically: slim and calm in weak regimes, wide and bold in strong regimes.
// LinkLine — adaptive HA channel with volatility-aware strength
describe_indicator("LinkLine", "overlay", { shortName: "LinkLine" });

/* ── Inputs ───────────────────────────────────────────────────────────── */
const haLen        = input.number("HA Smoothing", 50, { min: 10, max: 200 });
const haLen2       = input.number("HA Secondary", 20, { min: 5, max: 100 });
const oscLen       = input.number("Strength Lookback", 14, { min: 5, max: 100 });
const sigmaLevel   = input.number("Strong @ σ", 1.0, { min: 0.3, max: 3.0, step: 0.1 });
const bpsMult      = input.number("Bias Units (bps)", 10000, { min: 1000, max: 20000, step: 1000 });

const baseWidth    = input.number("Base Width", 1, { min: 1, max: 5 });
const strongWidth  = input.number("Strong Width", 3, { min: 1, max: 8 });
const midWidth     = input.number("Mid Width", 1, { min: 0, max: 3 });

const bullColor    = input.color("Bull Color", "#23D18B");  // mint green
const bearColor    = input.color("Bear Color", "#FF5A5F");  // coral red
const neutralColor = input.color("Neutral",   "#A0A4B8");   // steel gray
const midColor     = input.color("Midline",   "#BAC2DE");   // soft gray-blue

/* ── Helpers ──────────────────────────────────────────────────────────── */
function sign(x){ return x > 0 ? 1 : (x < 0 ? -1 : 0); }
function tanh(x){ // safe tanh without relying on runtime availability
  const e2x = Math.exp(2 * Math.abs(x));
  const t = (e2x - 1) / (e2x + 1);
  return x >= 0 ? t : -t;
}

/* ── 1) Smooth price and build HA series (no lookahead) ───────────────── */
const sO = ema(open,  haLen);
const sH = ema(high,  haLen);
const sL = ema(low,   haLen);
const sC = ema(close, haLen);

const haClose = series_of(0);
const xOpen   = series_of(0);
for (let i = 0; i < close.length; i++){
  haClose[i] = (sO[i] + sH[i] + sL[i] + sC[i]) / 4;
  xOpen[i]   = (sO[i] + sC[i]) / 2;
}
const haOpen = series_of(0);
for (let i = 0; i < close.length; i++){
  haOpen[i] = (i === 0) ? xOpen[i] : (haOpen[i-1] + haClose[i-1]) / 2;
}
const haHigh = series_of(0);
const haLow  = series_of(0);
for (let i = 0; i < close.length; i++){
  haHigh[i] = Math.max(sH[i], Math.max(haOpen[i], haClose[i]));
  haLow[i]  = Math.min(sL[i], Math.min(haOpen[i], haClose[i]));
}

/* secondary smoothing */
const haOpen2  = ema(haOpen,  haLen2);
const haClose2 = ema(haClose, haLen2);
const haHigh2  = ema(haHigh,  haLen2);
const haLow2   = ema(haLow,   haLen2);
const midLine  = series_of(0);
for (let i = 0; i < close.length; i++){
  midLine[i] = (haHigh2[i] + haLow2[i]) / 2;
}

/* ── 2) Bias oscillator in bps ───────────────────────────────────────── */
const biasOsc = series_of(0);
for (let i = 0; i < close.length; i++){
  const base = haOpen2[i];
  biasOsc[i] = base !== 0 ? ((haClose2[i] - base) / base) * bpsMult : 0;
}

/* adaptive z-score using EMA mean/variance */
const mean    = ema(biasOsc, oscLen);
const varRaw  = series_of(0);
for (let i = 0; i < close.length; i++){
  const d = biasOsc[i] - mean[i];
  varRaw[i] = d * d;
}
const varEma  = ema(varRaw, oscLen);
const std     = series_of(0);
for (let i = 0; i < close.length; i++){
  std[i] = Math.sqrt(Math.max(varEma[i], 1e-12));
}
const zScore  = series_of(0);
for (let i = 0; i < close.length; i++){
  zScore[i] = std[i] > 0 ? (biasOsc[i] / std[i]) : 0;
}
const strength = series_of(0);      // 0..100 continuous
const dir      = series_of(0);      // -1, 0, +1
for (let i = 0; i < close.length; i++){
  dir[i] = sign(biasOsc[i]);
  strength[i] = Math.round(100 * Math.abs(tanh(zScore[i]))); // smooth & bounded
}

/* ── 3) Build dynamic styling series ──────────────────────────────────── */
const upColors   = series_of(bullColor);
const downColors = series_of(bearColor);
const useColorsU = series_of(neutralColor);
const useColorsL = series_of(neutralColor);
const widthsU    = series_of(baseWidth);
const widthsL    = series_of(baseWidth);
const widthsM    = series_of(midWidth);

for (let i = 0; i < close.length; i++){
  const strong = Math.abs(zScore[i]) >= sigmaLevel;
  const d      = dir[i];

  // pick color by side; neutral when d == 0
  const col = d > 0 ? bullColor : (d < 0 ? bearColor : neutralColor);
  useColorsU[i] = col;
  useColorsL[i] = col;

  // widen when statistically strong
  widthsU[i] = strong ? strongWidth : baseWidth;
  widthsL[i] = strong ? strongWidth : baseWidth;

  // midline slightly bolder when strong, else thin
  widthsM[i] = strong ? Math.max(midWidth, baseWidth) : midWidth;
}

/* ── 4) Paint — names unique to avoid clashes ────────────────────────── */
paint(haHigh2, { name: "LinkLine Upper", color: useColorsU, style: "line", width: widthsU, opacity: 0.95 });
paint(haLow2,  { name: "LinkLine Lower", color: useColorsL, style: "line", width: widthsL, opacity: 0.95 });
paint(midLine, { name: "LinkLine Mid",   color: midColor,   style: "line", width: widthsM, opacity: 0.65 });

/* ── 5) Output (for strategies/bots) ─────────────────────────────────── */
const i = close.length - 1;
return {
  dir: dir[i],                    // -1 bear, 0 neutral, +1 bull
  z: zScore[i],                   // current z-score of bias
  strength: strength[i],          // 0..100
  strong: Math.abs(zScore[i]) >= sigmaLevel,  // boolean-like
  upper: haHigh2[i],
  lower: haLow2[i],
  mid:   midLine[i]
};