@luxalgo/vela 0.7.7 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10205,6 +10205,8 @@ var Vela = (function (exports) {
10205
10205
  this.liveBars = [];
10206
10206
  /** Sync-accessible symbol metadata, warmed by load()/symbolInfoFor (the engine reads it synchronously). */
10207
10207
  this.symInfoCache = /* @__PURE__ */ new Map();
10208
+ /** Requests in flight, keyed like the cache — every concurrent asker shares one round trip. */
10209
+ this.symInfoInflight = /* @__PURE__ */ new Map();
10208
10210
  this.cache = new CachingDataFeed(new RegistryFetchFeed(this.registry), store);
10209
10211
  }
10210
10212
  // ── Registry surface (driven by chart.data / DataControl) ──────────────
@@ -10265,9 +10267,36 @@ var Vela = (function (exports) {
10265
10267
  async symbolInfoFor(raw) {
10266
10268
  const resolved = this.registry.resolve(raw, { default: this.primaryProvider });
10267
10269
  if (!resolved) return void 0;
10268
- const info = await this.registry.get(resolved.provider)?.getSymbolInfo?.(resolved.ticker);
10269
- if (info) this.symInfoCache.set(symKey(resolved), info);
10270
- return info;
10270
+ return this.fetchSymbolInfo(resolved);
10271
+ }
10272
+ /**
10273
+ * One round trip per symbol, shared. A market switch probes this metadata from five
10274
+ * places at once (the bar load's prefetch, the renderer's tick size, the market-status
10275
+ * badge, the session shading, the session toggle) — without the in-flight map each
10276
+ * would open its own request for the same answer, and the last one to land decided how
10277
+ * late the chrome settled.
10278
+ *
10279
+ * Cached for {@link SYMBOL_INFO_TTL_MS}: this metadata is stable within a session
10280
+ * (tick size, session vocabulary, listing prefix), but not immutable — a continuous
10281
+ * futures row rolls its current contract — so the entry expires rather than pinning the
10282
+ * session's first answer forever. A failed probe is NOT remembered: the next asker retries.
10283
+ */
10284
+ fetchSymbolInfo(resolved) {
10285
+ const key = symKey(resolved);
10286
+ const hit = this.symInfoCache.get(key);
10287
+ if (hit && Date.now() - hit.at < SYMBOL_INFO_TTL_MS) return Promise.resolve(hit.info);
10288
+ const inflight = this.symInfoInflight.get(key);
10289
+ if (inflight) return inflight;
10290
+ const getSymbolInfo = this.registry.get(resolved.provider)?.getSymbolInfo;
10291
+ if (!getSymbolInfo) return Promise.resolve(void 0);
10292
+ const p = Promise.resolve(getSymbolInfo.call(this.registry.get(resolved.provider), resolved.ticker)).then((info) => {
10293
+ if (info) this.symInfoCache.set(key, { info, at: Date.now() });
10294
+ return info;
10295
+ }).finally(() => {
10296
+ this.symInfoInflight.delete(key);
10297
+ });
10298
+ this.symInfoInflight.set(key, p);
10299
+ return p;
10271
10300
  }
10272
10301
  /**
10273
10302
  * Per-symbol capabilities, resolved through the owning provider's `capabilitiesFor`
@@ -10311,16 +10340,10 @@ var Vela = (function (exports) {
10311
10340
  symbolInfo(cfg) {
10312
10341
  if (cfg.data && cfg.data.length > 0) return void 0;
10313
10342
  const resolved = this.registry.resolve(rawSymbol(cfg), { default: this.primaryProvider });
10314
- return resolved ? this.symInfoCache.get(symKey(resolved)) : void 0;
10343
+ return resolved ? this.symInfoCache.get(symKey(resolved))?.info : void 0;
10315
10344
  }
10316
10345
  prefetchSymbolInfo(resolved) {
10317
- const key = symKey(resolved);
10318
- if (this.symInfoCache.has(key)) return;
10319
- const provider = this.registry.get(resolved.provider);
10320
- if (!provider?.getSymbolInfo) return;
10321
- void provider.getSymbolInfo(resolved.ticker).then((info) => {
10322
- if (info) this.symInfoCache.set(key, info);
10323
- }).catch(() => {
10346
+ void this.fetchSymbolInfo(resolved).catch(() => {
10324
10347
  });
10325
10348
  }
10326
10349
  async loadRange(cfg, range) {
@@ -10370,8 +10393,9 @@ var Vela = (function (exports) {
10370
10393
  return { ...cfg, symbol: `${resolved.provider}:${resolved.ticker}` };
10371
10394
  }
10372
10395
  function symKey(resolved) {
10373
- return `${resolved.provider}|${resolved.ticker}`;
10396
+ return `${resolved.provider}|${resolved.ticker.trim().toUpperCase()}`;
10374
10397
  }
10398
+ var SYMBOL_INFO_TTL_MS = 10 * 6e4;
10375
10399
  async function safeBars(provider, ticker, tf, range) {
10376
10400
  try {
10377
10401
  return await provider.getBars(ticker, tf, range);
@@ -29597,7 +29621,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
29597
29621
  if (hr.kind === "heading") body.append(this.sectionTitle(hr.label));
29598
29622
  else if (hr.kind === "toggle") body.append(this.boolRow(hr.label, hr.get(), (v) => hr.set(v)));
29599
29623
  else if (hr.kind === "color") body.append(this.colorRow(hr.label, hr.get(), (v) => hr.set(v)));
29600
- else body.append(this.selectRowLabeled(hr.label, hr.get(), hr.options.map((o) => [o, o]), (v) => hr.set(v)));
29624
+ else body.append(this.selectRowLabeled(hr.label, hr.get(), normalizeSelectOptions(hr.options), (v) => hr.set(v)));
29601
29625
  }
29602
29626
  }
29603
29627
  };
@@ -40866,7 +40890,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
40866
40890
  lineStyle: plot.lineStyle ?? "solid",
40867
40891
  ...plot.base != null ? { base: plot.base } : {}
40868
40892
  },
40869
- ...plot.overlay ? { overlay: true } : {}
40893
+ ...plot.overlay ? { overlay: true } : {},
40894
+ ...plot.display ? { display: plot.display } : {}
40870
40895
  });
40871
40896
  });
40872
40897
  if (out.markers && out.markers.length > 0) {
@@ -40888,7 +40913,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
40888
40913
  paneId: "",
40889
40914
  fromSeriesId: from,
40890
40915
  toSeriesId: to,
40891
- color: band.color
40916
+ color: band.color,
40917
+ ...band.colors ? { colors: [...band.colors] } : {},
40918
+ ...band.gradient ? { gradient: [...band.gradient] } : {}
40892
40919
  }
40893
40920
  ];
40894
40921
  });
@@ -41046,6 +41073,16 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41046
41073
  return Math.sqrt(sq / len);
41047
41074
  });
41048
41075
  }
41076
+ function meanDev(v, len) {
41077
+ return windowed(v, len, (w, from, to) => {
41078
+ let s = 0;
41079
+ for (let k = from; k <= to; k++) s += w[k];
41080
+ const mean = s / len;
41081
+ let dev = 0;
41082
+ for (let k = from; k <= to; k++) dev += Math.abs(w[k] - mean);
41083
+ return dev / len;
41084
+ });
41085
+ }
41049
41086
  function highest(v, len) {
41050
41087
  return windowed(v, len, (w, from, to) => {
41051
41088
  let m = -Infinity;
@@ -41177,7 +41214,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41177
41214
  }
41178
41215
  return out;
41179
41216
  }
41180
- function linreg(v, len) {
41217
+ function linreg(v, len, offset3 = 0) {
41181
41218
  const sx = (len - 1) * len / 2;
41182
41219
  const sxx = (len - 1) * len * (2 * len - 1) / 6;
41183
41220
  const denom = len * sxx - sx * sx;
@@ -41191,9 +41228,101 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41191
41228
  }
41192
41229
  const slope = denom === 0 ? 0 : (len * sxy - sx * sy) / denom;
41193
41230
  const intercept = (sy - slope * sx) / len;
41194
- return intercept + slope * (len - 1);
41231
+ return intercept + slope * (len - 1 - offset3);
41232
+ });
41233
+ }
41234
+ function hma(v, len) {
41235
+ const n = Math.max(2, Math.trunc(len));
41236
+ const raw = sub(map(wma(v, Math.max(1, Math.trunc(n / 2))), (x) => 2 * x), wma(v, n));
41237
+ return wma(raw, Math.max(1, Math.round(Math.sqrt(n))));
41238
+ }
41239
+ function alma(v, len, offset3, sigma) {
41240
+ const m = offset3 * (len - 1);
41241
+ const s = len / Math.max(sigma, 1e-9);
41242
+ const weights = new Array(len);
41243
+ let norm = 0;
41244
+ for (let i = 0; i < len; i++) {
41245
+ const d = i - m;
41246
+ const w = Math.exp(-(d * d) / (2 * s * s));
41247
+ weights[i] = w;
41248
+ norm += w;
41249
+ }
41250
+ return windowed(v, len, (win, from, to) => {
41251
+ let acc = 0;
41252
+ for (let k = from; k <= to; k++) acc += win[k] * weights[k - from];
41253
+ return norm === 0 ? Number.NaN : acc / norm;
41254
+ });
41255
+ }
41256
+ function dema(v, len) {
41257
+ const e1 = ema(v, len);
41258
+ return sub(map(e1, (x) => 2 * x), ema(e1, len));
41259
+ }
41260
+ function tema(v, len) {
41261
+ const e1 = ema(v, len);
41262
+ const e2 = ema(e1, len);
41263
+ const e3 = ema(e2, len);
41264
+ return zip(zip(map(e1, (x) => 3 * x), map(e2, (x) => 3 * x), (a, b) => a - b), e3, (a, b) => a + b);
41265
+ }
41266
+ function kama(v, len, fastAlpha = 2 / 3, slowAlpha = 2 / 31) {
41267
+ const direction = map(change(v, len), Math.abs);
41268
+ const volatility = sum(map(change(v), Math.abs), len);
41269
+ const out = new Array(v.length).fill(Number.NaN);
41270
+ let prev3 = Number.NaN;
41271
+ for (let i = 0; i < v.length; i++) {
41272
+ const x = v[i];
41273
+ if (!Number.isFinite(x)) continue;
41274
+ const d = direction[i];
41275
+ const w = volatility[i];
41276
+ if (!Number.isFinite(d) || !Number.isFinite(w)) {
41277
+ prev3 = Number.isFinite(prev3) ? prev3 : x;
41278
+ out[i] = prev3;
41279
+ continue;
41280
+ }
41281
+ const er = w === 0 ? 0 : d / w;
41282
+ const sc = (er * (fastAlpha - slowAlpha) + slowAlpha) ** 2;
41283
+ prev3 = Number.isFinite(prev3) ? prev3 + sc * (x - prev3) : x;
41284
+ out[i] = prev3;
41285
+ }
41286
+ return out;
41287
+ }
41288
+ function mcginley(v, len) {
41289
+ const out = new Array(v.length).fill(Number.NaN);
41290
+ let prev3 = Number.NaN;
41291
+ for (let i = 0; i < v.length; i++) {
41292
+ const x = v[i];
41293
+ if (!Number.isFinite(x)) continue;
41294
+ if (!Number.isFinite(prev3)) {
41295
+ prev3 = x;
41296
+ } else {
41297
+ const ratio = prev3 === 0 ? 0 : x / prev3;
41298
+ const denom = len * ratio ** 4;
41299
+ prev3 += denom === 0 ? 0 : (x - prev3) / denom;
41300
+ }
41301
+ out[i] = prev3;
41302
+ }
41303
+ return out;
41304
+ }
41305
+ function hamming(v, len, a0 = 0.54, a1 = 0.46) {
41306
+ if (len <= 1) return v.map((x) => Number.isFinite(x) ? x : Number.NaN);
41307
+ const weights = new Array(len);
41308
+ let norm = 0;
41309
+ for (let i = 0; i < len; i++) {
41310
+ const w = a0 - a1 * Math.cos(2 * Math.PI * i / (len - 1));
41311
+ weights[i] = w;
41312
+ norm += w;
41313
+ }
41314
+ return windowed(v, len, (win, from, to) => {
41315
+ let acc = 0;
41316
+ for (let k = from; k <= to; k++) acc += win[k] * weights[to - k];
41317
+ return norm === 0 ? Number.NaN : acc / norm;
41195
41318
  });
41196
41319
  }
41320
+ function cmo(v, len) {
41321
+ const d = change(v);
41322
+ const up = sum(map(d, (x) => Math.max(x, 0)), len);
41323
+ const dn = sum(map(d, (x) => Math.max(-x, 0)), len);
41324
+ return zip(up, dn, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
41325
+ }
41197
41326
  function swma(v) {
41198
41327
  const out = new Array(v.length).fill(Number.NaN);
41199
41328
  for (let i = 3; i < v.length; i++) {
@@ -41243,38 +41372,253 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41243
41372
  }
41244
41373
 
41245
41374
  // src/core/native-indicators/classics/shared.ts
41246
- function lengthInput(defval, key = "length", title = "Length", max2 = 5e3) {
41247
- return { key, title, type: "int", defval, min: 1, max: max2, step: 1 };
41375
+ var SETTINGS = "Settings";
41376
+ var STYLE = "Style";
41377
+ function lengthInput(defval, key = "length", title = "Length", max2 = 5e3, tooltip) {
41378
+ return { key, title, type: "int", defval, min: 1, max: max2, step: 1, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
41379
+ }
41380
+ function intInput(key, title, defval, min2 = 1, max2 = 5e3, tooltip) {
41381
+ return { key, title, type: "int", defval, min: min2, max: max2, step: 1, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
41382
+ }
41383
+ function floatInput(key, title, defval, min2 = 0, max2 = 1e3, step = 0.1, tooltip) {
41384
+ return { key, title, type: "float", defval, min: min2, max: max2, step, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
41248
41385
  }
41249
- function sourceInput(defval = "Close") {
41250
- return { key: "source", title: "Source", type: "string", defval, options: SOURCES2 };
41386
+ function boolInput(key, title, defval, group = SETTINGS, tooltip) {
41387
+ return { key, title, type: "bool", defval, group, ...tooltip != null ? { tooltip } : {} };
41251
41388
  }
41252
- function colorInput(defval = SERIES_LINE, title = "Color", key = "color") {
41253
- return { key, title, type: "color", defval };
41389
+ function optionInput(key, title, defval, options, tooltip) {
41390
+ return { key, title, type: "string", defval, options, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
41391
+ }
41392
+ function sourceInput(defval = "Close", key = "source", title = "Source", tooltip) {
41393
+ return { key, title, type: "string", defval, options: SOURCES2, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
41394
+ }
41395
+ function colorInput(defval = INFO, title = "Color", key = "color", tooltip) {
41396
+ return { key, title, type: "color", defval, group: STYLE, ...tooltip != null ? { tooltip } : {} };
41397
+ }
41398
+ function widthInput(defval = 2, key = "lineWidth", title = "Line Width") {
41399
+ return { key, title, type: "int", defval, min: 1, max: 5, step: 1, group: STYLE, tooltip: "Width of the plotted line, in pixels." };
41400
+ }
41401
+ function transp(hex, transparency) {
41402
+ return withAlpha2(hex, (100 - transparency) / 100);
41254
41403
  }
41255
41404
  function withAlpha2(hex, alpha = 0.08) {
41256
41405
  const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
41257
41406
  if (!m) return hex;
41258
- const a = Math.round(alpha * 255).toString(16).padStart(2, "0");
41407
+ const a = Math.round(Math.max(0, Math.min(1, alpha)) * 255).toString(16).padStart(2, "0");
41259
41408
  return `#${m[1]}${a}`;
41260
41409
  }
41410
+ function channelStyleInputs(basisTitle = "Basis") {
41411
+ return [
41412
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the lower band and of the basis while price holds above it."),
41413
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the upper band and of the basis while price sits below it."),
41414
+ colorInput(NEUTRAL, basisTitle, "basisColor", "Color of the basis line while price sits exactly on it.")
41415
+ ];
41416
+ }
41417
+ function directionalChannel(bars, basis, upper, lower, ink, key) {
41418
+ const basisColors = basis.map((b, i) => {
41419
+ if (!Number.isFinite(b)) return null;
41420
+ const c = bars[i].close;
41421
+ return c > b ? ink.bull : c < b ? ink.bear : ink.neutral;
41422
+ });
41423
+ const wash = upper.map((u, i) => {
41424
+ const l = lower[i];
41425
+ if (!Number.isFinite(u) || !Number.isFinite(l)) return null;
41426
+ return { topValue: u, bottomValue: l, topColor: transp(ink.bear, 90), bottomColor: transp(ink.bull, 90) };
41427
+ });
41428
+ return {
41429
+ plots: [
41430
+ { key: "basis", title: "Basis", values: [...basis], color: ink.neutral, colors: basisColors },
41431
+ { key: "upper", title: "Upper", values: [...upper], color: transp(ink.bear, 30) },
41432
+ { key: "lower", title: "Lower", values: [...lower], color: transp(ink.bull, 30) }
41433
+ ],
41434
+ bands: [{ key, from: "upper", to: "lower", color: transp(NEUTRAL, 90), gradient: wash }]
41435
+ };
41436
+ }
41437
+ function anchorPlot(key, title, length, level) {
41438
+ return {
41439
+ key,
41440
+ title,
41441
+ values: new Array(length).fill(level),
41442
+ color: transp(NEUTRAL, 100),
41443
+ display: { pane: false, legend: false, dataWindow: false }
41444
+ };
41445
+ }
41446
+ function thresholdZones(length, levels, ink, transparency = 92) {
41447
+ return {
41448
+ plots: [
41449
+ anchorPlot("obAnchor", "Overbought Anchor", length, levels.overbought),
41450
+ anchorPlot("osAnchor", "Oversold Anchor", length, levels.oversold),
41451
+ anchorPlot("topAnchor", "Top Anchor", length, levels.top),
41452
+ anchorPlot("bottomAnchor", "Bottom Anchor", length, levels.bottom)
41453
+ ],
41454
+ bands: [
41455
+ { key: "obZone", from: "obAnchor", to: "topAnchor", color: transp(ink.overbought, transparency) },
41456
+ { key: "osZone", from: "osAnchor", to: "bottomAnchor", color: transp(ink.oversold, transparency) }
41457
+ ]
41458
+ };
41459
+ }
41460
+ function rampColors(values, levels, ink) {
41461
+ return values.map((x) => {
41462
+ if (!Number.isFinite(x)) return null;
41463
+ return x <= levels.mid ? gradient(x, levels.low, levels.mid, ink.low, ink.mid) : gradient(x, levels.mid, levels.high, ink.mid, ink.high);
41464
+ });
41465
+ }
41466
+ function baselineGradient(values, baseline, inkOf, valueTransparency = 50, baseTransparency = 100) {
41467
+ return values.map((x, i) => {
41468
+ if (!Number.isFinite(x)) return null;
41469
+ const ink = inkOf(x, i);
41470
+ const above = x > baseline;
41471
+ return {
41472
+ topValue: Math.max(x, baseline),
41473
+ bottomValue: Math.min(x, baseline),
41474
+ topColor: transp(ink, above ? valueTransparency : baseTransparency),
41475
+ bottomColor: transp(ink, above ? baseTransparency : valueTransparency)
41476
+ };
41477
+ });
41478
+ }
41479
+ function momentumColumnColors(values, ink) {
41480
+ return values.map((x, i) => {
41481
+ if (!Number.isFinite(x)) return null;
41482
+ const prev3 = i > 0 && Number.isFinite(values[i - 1]) ? values[i - 1] : x;
41483
+ const rising = x > prev3;
41484
+ if (x >= 0) return rising ? ink.growAbove : ink.fallAbove;
41485
+ return rising ? ink.growBelow : ink.fallBelow;
41486
+ });
41487
+ }
41488
+ function boundedOscillator(opts) {
41489
+ const mid = (opts.overbought + opts.oversold) / 2;
41490
+ const ramp = rampColors(opts.values, { low: opts.oversold, mid, high: opts.overbought }, opts.ink);
41491
+ const zones = thresholdZones(
41492
+ opts.values.length,
41493
+ { overbought: opts.overbought, oversold: opts.oversold, top: opts.scale.top, bottom: opts.scale.bottom },
41494
+ { overbought: opts.ink.high, oversold: opts.ink.low },
41495
+ opts.zoneTransparency
41496
+ );
41497
+ const plots = [{ key: opts.key, title: opts.title, values: opts.values, color: opts.ink.mid, width: opts.width ?? 1, colors: ramp }, ...zones.plots];
41498
+ const bands = [...zones.bands];
41499
+ if (opts.bandFill != null) bands.push({ key: "band", from: "obAnchor", to: "osAnchor", color: opts.bandFill });
41500
+ if (opts.midpointWash) {
41501
+ plots.push(anchorPlot("midAnchor", "Midpoint Anchor", opts.values.length, mid));
41502
+ bands.push({ key: "wash", from: opts.key, to: "midAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(opts.values, mid, (_x, i) => ramp[i] ?? opts.ink.mid) });
41503
+ }
41504
+ const levels = [
41505
+ { key: "overbought", price: opts.overbought, color: opts.levelInk, lineStyle: "dashed", title: opts.levelTitles?.overbought ?? "Overbought" },
41506
+ { key: "oversold", price: opts.oversold, color: opts.levelInk, lineStyle: "dashed", title: opts.levelTitles?.oversold ?? "Oversold" }
41507
+ ];
41508
+ if (opts.midline != null) levels.splice(1, 0, { key: "midline", price: opts.midline, color: opts.levelInk, lineStyle: "dotted", title: "Midline" });
41509
+ return { plots, bands, levels };
41510
+ }
41511
+ function centeredOscillator(opts) {
41512
+ const baseline = opts.baseline ?? 0;
41513
+ const inkOf = (x) => x > baseline ? opts.ink.bull : x < baseline ? opts.ink.bear : opts.ink.neutral;
41514
+ const plots = [
41515
+ { key: opts.key, title: opts.title, values: opts.values, color: opts.ink.neutral, width: opts.width ?? 1, colors: opts.values.map((x) => Number.isFinite(x) ? inkOf(x) : null) },
41516
+ ...opts.extraPlots ?? []
41517
+ ];
41518
+ const bands = [];
41519
+ if (opts.fill !== false) {
41520
+ plots.push(anchorPlot("baseline", "Baseline Anchor", opts.values.length, baseline));
41521
+ bands.push({ key: "wash", from: opts.key, to: "baseline", color: transp(NEUTRAL, 100), gradient: baselineGradient(opts.values, baseline, inkOf, opts.valueTransparency) });
41522
+ }
41523
+ return {
41524
+ plots,
41525
+ bands,
41526
+ levels: [{ key: "zero", price: baseline, color: opts.zeroInk, lineStyle: opts.zeroLineStyle ?? "dashed", title: opts.zeroTitle ?? "Zero Line" }, ...opts.extraLevels ?? []]
41527
+ };
41528
+ }
41529
+ function channels(hex) {
41530
+ const m = /^#([0-9a-f]{6})/i.exec(hex.trim());
41531
+ if (!m) return null;
41532
+ const n = parseInt(m[1], 16);
41533
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
41534
+ }
41535
+ function gradient(value, low, high, from, to) {
41536
+ const a = channels(from);
41537
+ const b = channels(to);
41538
+ if (!a || !b || !Number.isFinite(value)) return from;
41539
+ const span = high - low;
41540
+ const t = span === 0 ? 0 : Math.max(0, Math.min(1, (value - low) / span));
41541
+ const mix2 = (i) => Math.round(a[i] + (b[i] - a[i]) * t).toString(16).padStart(2, "0");
41542
+ return `#${mix2(0)}${mix2(1)}${mix2(2)}`;
41543
+ }
41261
41544
 
41262
41545
  // src/core/native-indicators/classics/averages.ts
41263
- var MA_KINDS = ["SMA", "EMA", "WMA", "RMA", "VWMA"];
41264
- var offsetInput = { key: "offset", title: "Offset", type: "int", defval: 0, min: -500, max: 500, step: 1 };
41546
+ var MA_TYPES = ["SMA", "EMA", "WMA", "RMA (SMMA)", "HMA", "ALMA", "VWMA", "DEMA", "TEMA", "KAMA", "LSMA", "McGinley", "Hamming"];
41547
+ var ENVELOPE_MA_TYPES = ["SMA", "EMA", "WMA", "RMA (SMMA)", "HMA", "VWMA"];
41548
+ function movingAverageOf(kind, src, bars, len, opts = {}) {
41549
+ switch (kind) {
41550
+ case "EMA":
41551
+ return ema(src, len);
41552
+ case "WMA":
41553
+ return wma(src, len);
41554
+ case "RMA":
41555
+ case "RMA (SMMA)":
41556
+ return rma(src, len);
41557
+ case "HMA":
41558
+ return hma(src, len);
41559
+ case "ALMA":
41560
+ return alma(src, len, opts.almaOffset ?? 0.85, opts.almaSigma ?? 6);
41561
+ case "VWMA":
41562
+ return vwma(src, volumes(bars), len);
41563
+ case "DEMA":
41564
+ return dema(src, len);
41565
+ case "TEMA":
41566
+ return tema(src, len);
41567
+ case "KAMA":
41568
+ return kama(src, len);
41569
+ case "LSMA":
41570
+ return linreg(src, len, opts.lsmaOffset ?? 0);
41571
+ case "McGinley":
41572
+ return mcginley(src, len);
41573
+ case "Hamming":
41574
+ return hamming(src, len);
41575
+ default:
41576
+ return sma(src, len);
41577
+ }
41578
+ }
41579
+ function slopeColors(values, bull, bear, flat) {
41580
+ return values.map((x, i) => {
41581
+ if (!Number.isFinite(x)) return null;
41582
+ const prev3 = i > 0 ? values[i - 1] : Number.NaN;
41583
+ if (!Number.isFinite(prev3)) return flat;
41584
+ return x > prev3 ? bull : x < prev3 ? bear : flat;
41585
+ });
41586
+ }
41587
+ function directionInputs() {
41588
+ return [
41589
+ boolInput("trendColor", "Slope Trend Coloring", true, STYLE, "Colors the line by its slope \u2014 bullish while it rises, bearish while it falls. Disable to draw it in the neutral color."),
41590
+ colorInput(BULLISH, "Bullish", "bullColor"),
41591
+ colorInput(BEARISH, "Bearish", "bearColor"),
41592
+ colorInput(NEUTRAL, "Neutral", "neutralColor"),
41593
+ widthInput()
41594
+ ];
41595
+ }
41596
+ function slopeColoredPlot(key, title, values, inputs) {
41597
+ const neutral = str3(inputs, "neutralColor", NEUTRAL);
41598
+ const colored = bool(inputs, "trendColor", true);
41599
+ return {
41600
+ key,
41601
+ title,
41602
+ values,
41603
+ color: neutral,
41604
+ width: num3(inputs, "lineWidth", 2),
41605
+ ...colored ? { colors: slopeColors(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), neutral) } : {}
41606
+ };
41607
+ }
41608
+ var offsetInput = { key: "offset", title: "Offset", type: "int", defval: 0, min: -500, max: 500, step: 1, group: SETTINGS, tooltip: "Bars the plotted line is displaced by. Positive values push it to the right." };
41265
41609
  function fixedAverage(type, title, shortTitle, smooth) {
41266
41610
  return {
41267
41611
  type,
41268
41612
  title,
41269
41613
  shortTitle,
41270
41614
  overlay: true,
41271
- inputs: [lengthInput(20), sourceInput(), offsetInput, colorInput()],
41615
+ inputs: [lengthInput(20, "length", "Length", 5e3, `Number of bars used in the ${shortTitle} calculation.`), sourceInput("Close", "source", "Source", `Price series the ${shortTitle} is calculated on.`), offsetInput, colorInput()],
41272
41616
  compute: (bars, inputs) => {
41273
41617
  const len = num3(inputs, "length", 20);
41274
41618
  let values = smooth(sourceValues(bars, str3(inputs, "source", "Close")), len);
41275
41619
  const offset3 = Math.trunc(num3(inputs, "offset", 0));
41276
41620
  if (offset3 !== 0) values = shift3(values, offset3);
41277
- return { plots: [{ key: shortTitle.toLowerCase(), title: shortTitle, values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
41621
+ return { plots: [{ key: shortTitle.toLowerCase(), title: shortTitle, values, color: str3(inputs, "color", INFO), width: 2 }] };
41278
41622
  }
41279
41623
  };
41280
41624
  }
@@ -41286,36 +41630,37 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41286
41630
  shortTitle: "MA",
41287
41631
  overlay: true,
41288
41632
  inputs: [
41289
- { key: "maType", title: "Type", type: "string", defval: "SMA", options: MA_KINDS },
41290
- lengthInput(20),
41291
- sourceInput(),
41633
+ optionInput("maType", "MA 1 Type", "SMA", MA_TYPES, "Moving average calculation used for MA 1."),
41634
+ lengthInput(20, "length", "MA 1 Length", 5e3, "Number of bars used in the MA 1 calculation."),
41635
+ sourceInput("Close", "source", "MA 1 Source", "Price series MA 1 is calculated on."),
41636
+ boolInput("showMa2", "Show MA 2", false, SETTINGS, "Displays the optional second moving average."),
41637
+ optionInput("ma2Type", "MA 2 Type", "SMA", MA_TYPES, "Moving average calculation used for MA 2."),
41638
+ lengthInput(50, "ma2Length", "MA 2 Length", 5e3, "Number of bars used in the MA 2 calculation."),
41639
+ sourceInput("Close", "ma2Source", "MA 2 Source", "Price series MA 2 is calculated on."),
41640
+ floatInput("almaOffset", "ALMA Offset", 0.85, 0, 1, 0.05, "ALMA type only. Values near 1 make the average more responsive, values near 0 make it smoother."),
41641
+ floatInput("almaSigma", "ALMA Sigma", 6, 0.1, 100, 0.5, "ALMA type only. Larger values widen the Gaussian window for a smoother average."),
41642
+ intInput("lsmaOffset", "LSMA Offset", 0, 0, 500, "LSMA type only. Bar offset applied to the linear regression value."),
41292
41643
  offsetInput,
41293
- colorInput()
41644
+ colorInput(INFO, "MA 1", "color", "MA 1 line color."),
41645
+ colorInput(WARNING, "MA 2", "ma2Color", "MA 2 line color.")
41294
41646
  ],
41295
41647
  compute: (bars, inputs) => {
41296
- const len = num3(inputs, "length", 20);
41297
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
41648
+ const opts = {
41649
+ almaOffset: num3(inputs, "almaOffset", 0.85),
41650
+ almaSigma: num3(inputs, "almaSigma", 6),
41651
+ lsmaOffset: Math.trunc(num3(inputs, "lsmaOffset", 0))
41652
+ };
41653
+ const offset3 = Math.trunc(num3(inputs, "offset", 0));
41654
+ const displace = (v) => offset3 === 0 ? v : shift3(v, offset3);
41298
41655
  const kind = str3(inputs, "maType", "SMA");
41299
- let values;
41300
- switch (kind) {
41301
- case "EMA":
41302
- values = ema(src, len);
41303
- break;
41304
- case "WMA":
41305
- values = wma(src, len);
41306
- break;
41307
- case "RMA":
41308
- values = rma(src, len);
41309
- break;
41310
- case "VWMA":
41311
- values = vwma(src, volumes(bars), len);
41312
- break;
41313
- default:
41314
- values = sma(src, len);
41656
+ const ma1 = displace(movingAverageOf(kind, sourceValues(bars, str3(inputs, "source", "Close")), bars, num3(inputs, "length", 20), opts));
41657
+ const plots = [{ key: "ma", title: kind, values: ma1, color: str3(inputs, "color", INFO), width: 2 }];
41658
+ if (bool(inputs, "showMa2", false)) {
41659
+ const kind2 = str3(inputs, "ma2Type", "SMA");
41660
+ const ma2 = displace(movingAverageOf(kind2, sourceValues(bars, str3(inputs, "ma2Source", "Close")), bars, num3(inputs, "ma2Length", 50), opts));
41661
+ plots.push({ key: "ma2", title: `${kind2} 2`, values: ma2, color: str3(inputs, "ma2Color", WARNING), width: 2 });
41315
41662
  }
41316
- const offset3 = Math.trunc(num3(inputs, "offset", 0));
41317
- if (offset3 !== 0) values = shift3(values, offset3);
41318
- return { plots: [{ key: "ma", title: kind, values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
41663
+ return { plots };
41319
41664
  }
41320
41665
  };
41321
41666
  var smoothedMa = {
@@ -41323,17 +41668,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41323
41668
  title: "Smoothed Moving Average",
41324
41669
  shortTitle: "RMA",
41325
41670
  overlay: true,
41326
- inputs: [lengthInput(14), sourceInput(), colorInput()],
41671
+ inputs: [
41672
+ sourceInput("Close", "source", "Source", "Price series the average smooths. Wilder computed his 1978 indicators on the close, but the recursion accepts any source."),
41673
+ lengthInput(14, "length", "RMA Length", 5e3, "Number of bars N in Wilder's smoothing: the line keeps (N \u2212 1)/N of its previous value and blends in 1/N of the new price. That alpha is smaller than an EMA's, so an N-period RMA responds like a (2N \u2212 1)-period EMA."),
41674
+ ...directionInputs()
41675
+ ],
41327
41676
  compute: (bars, inputs) => ({
41328
- plots: [
41329
- {
41330
- key: "rma",
41331
- title: "RMA",
41332
- values: rma(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
41333
- color: str3(inputs, "color", SERIES_LINE),
41334
- width: 2
41335
- }
41336
- ]
41677
+ plots: [slopeColoredPlot("rma", "RMA", rma(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)), inputs)]
41337
41678
  })
41338
41679
  };
41339
41680
  var zlema = {
@@ -41341,61 +41682,86 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41341
41682
  title: "Zero-Lag Exponential Moving Average",
41342
41683
  shortTitle: "ZLEMA",
41343
41684
  overlay: true,
41344
- inputs: [lengthInput(14), sourceInput(), colorInput()],
41685
+ inputs: [
41686
+ sourceInput("Close", "source", "Source", "Price series the average de-lags and then smooths."),
41687
+ lengthInput(21, "length", "ZLEMA Length", 5e3, "EMA length N. It sets both the smoothing (alpha = 2 / (N + 1)) and the correction: an EMA of length N lags by about (N \u2212 1)/2 bars, and the de-lagging step is sized to cancel exactly that."),
41688
+ ...directionInputs()
41689
+ ],
41345
41690
  compute: (bars, inputs) => {
41346
- const len = num3(inputs, "length", 14);
41691
+ const len = num3(inputs, "length", 21);
41347
41692
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
41348
- const lag = Math.floor((len - 1) / 2);
41349
- const lagged = shift3(src, lag);
41350
- const delagged = zip(src, lagged, (a, b) => 2 * a - b);
41351
- return { plots: [{ key: "zlema", title: "ZLEMA", values: ema(delagged, len), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
41693
+ const lag = Math.round((len - 1) / 2);
41694
+ const delagged = zip(src, shift3(src, lag), (a, b) => 2 * a - b);
41695
+ return { plots: [slopeColoredPlot("zlema", "ZLEMA", ema(delagged, len), inputs)] };
41352
41696
  }
41353
41697
  };
41698
+ var MOMENTUM_SCALER = "Momentum (|CMO|)";
41699
+ var VOLATILITY_SCALER = "Volatility (StDev Ratio)";
41700
+ var REGIME = "Trend Regime";
41354
41701
  var vidya = {
41355
41702
  type: "vidya",
41356
41703
  title: "Variable Index Dynamic Average",
41357
41704
  shortTitle: "VIDYA",
41358
41705
  overlay: true,
41359
- inputs: [lengthInput(14), { key: "cmoLength", title: "Momentum length", type: "int", defval: 9, min: 1, max: 500, step: 1 }, sourceInput(), colorInput()],
41706
+ inputs: [
41707
+ sourceInput("Close", "source", "Source", "Price series the average smooths. VIDYA is classically computed on the close."),
41708
+ lengthInput(12, "length", "VIDYA Length", 5e3, "EMA-equivalent length N of the base smoothing constant, alpha = 2 / (N + 1). The adaptive scaler rescales this base speed bar by bar."),
41709
+ optionInput("scaler", "Adaptive Scaler", MOMENTUM_SCALER, [MOMENTUM_SCALER, VOLATILITY_SCALER], "Bar-by-bar measure that rescales the smoothing constant. Momentum divides the absolute Chande Momentum Oscillator by 100; Volatility is Chande's original short-term over reference standard deviation."),
41710
+ intInput("scalerLength", "Scaler Lookback", 9, 1, 5e3, "Lookback of the adaptive scaler \u2014 the CMO period in momentum mode, or the short-term standard deviation window in volatility mode."),
41711
+ intInput("refLength", "Reference StDev Length", 30, 2, 5e3, "Volatility mode only \u2014 window of the reference standard deviation the short-term reading is divided by."),
41712
+ { ...floatInput("flatThreshold", "Flat Threshold", 0.05, 0, 10, 0.01, "One-bar change of VIDYA, as a fraction of ATR, below which the line reads as flat."), group: REGIME },
41713
+ { ...intInput("atrLength", "ATR Normalization Length", 14, 1, 5e3, "ATR window used to normalize VIDYA's one-bar slope, so the flat threshold behaves consistently across symbols and timeframes."), group: REGIME },
41714
+ boolInput("trendColor", "Trend Regime Coloring", true, STYLE, "Colors VIDYA by its regime read \u2014 bullish while sloping up beyond the flat threshold, bearish while sloping down, flat inside congestion."),
41715
+ boolInput("adaptiveGradient", "Adaptive Gradient", true, STYLE, "Blends the trend color toward the flat color according to the adaptive scaler, so the line pales as momentum dies."),
41716
+ colorInput(BULLISH, "Bullish", "bullColor"),
41717
+ colorInput(BEARISH, "Bearish", "bearColor"),
41718
+ colorInput(NEUTRAL, "Flat", "flatColor"),
41719
+ colorInput(NEUTRAL, "Neutral", "neutralColor"),
41720
+ widthInput()
41721
+ ],
41360
41722
  compute: (bars, inputs) => {
41361
- const len = num3(inputs, "length", 14);
41362
- const cmoLen = num3(inputs, "cmoLength", 9);
41723
+ const len = num3(inputs, "length", 12);
41363
41724
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
41364
- const d = change(src);
41365
- const su = new Array(src.length).fill(Number.NaN);
41366
- const sd = new Array(src.length).fill(Number.NaN);
41367
- for (let i = cmoLen; i < src.length; i++) {
41368
- let u = 0;
41369
- let w = 0;
41370
- let ok = true;
41371
- for (let k = i - cmoLen + 1; k <= i; k++) {
41372
- const x = d[k];
41373
- if (!Number.isFinite(x)) {
41374
- ok = false;
41375
- break;
41376
- }
41377
- if (x > 0) u += x;
41378
- else w -= x;
41379
- }
41380
- if (ok) {
41381
- su[i] = u;
41382
- sd[i] = w;
41383
- }
41384
- }
41385
- const alpha = 2 / (len + 1);
41725
+ const scalerLen = num3(inputs, "scalerLength", 9);
41726
+ const momentum = map(cmo(src, scalerLen), (x) => Math.abs(x) / 100);
41727
+ const shortDev = stdev(src, scalerLen);
41728
+ const refDev = stdev(src, num3(inputs, "refLength", 30));
41729
+ const volatility = zip(shortDev, refDev, (s, r) => r > 0 ? s / r : 0);
41730
+ const scale = str3(inputs, "scaler", MOMENTUM_SCALER) === VOLATILITY_SCALER ? volatility : momentum;
41731
+ const alphaBase = 2 / (len + 1);
41386
41732
  const out = new Array(src.length).fill(Number.NaN);
41387
41733
  let prev3 = Number.NaN;
41388
41734
  for (let i = 0; i < src.length; i++) {
41389
41735
  const x = src[i];
41390
- const u = su[i];
41391
- const w = sd[i];
41392
- if (!Number.isFinite(x) || !Number.isFinite(u) || !Number.isFinite(w)) continue;
41393
- const total = u + w;
41394
- const k = total === 0 ? 0 : Math.abs((u - w) / total);
41395
- prev3 = Number.isFinite(prev3) ? alpha * k * x + (1 - alpha * k) * prev3 : x;
41736
+ if (!Number.isFinite(x)) continue;
41737
+ const k = scale[i];
41738
+ if (!Number.isFinite(prev3) || !Number.isFinite(k)) prev3 = x;
41739
+ else {
41740
+ const alpha = Math.min(alphaBase * k, 1);
41741
+ prev3 = alpha * x + (1 - alpha) * prev3;
41742
+ }
41396
41743
  out[i] = prev3;
41397
41744
  }
41398
- return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
41745
+ const neutral = str3(inputs, "neutralColor", NEUTRAL);
41746
+ if (!bool(inputs, "trendColor", true)) {
41747
+ return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: neutral, width: num3(inputs, "lineWidth", 2) }] };
41748
+ }
41749
+ const bull = str3(inputs, "bullColor", BULLISH);
41750
+ const bear = str3(inputs, "bearColor", BEARISH);
41751
+ const flat = str3(inputs, "flatColor", NEUTRAL);
41752
+ const threshold = num3(inputs, "flatThreshold", 0.05);
41753
+ const range = atr(bars, num3(inputs, "atrLength", 14));
41754
+ const fade = bool(inputs, "adaptiveGradient", true);
41755
+ const colors = out.map((x, i) => {
41756
+ if (!Number.isFinite(x)) return null;
41757
+ const r = range[i];
41758
+ const slope = i > 0 && Number.isFinite(out[i - 1]) ? x - out[i - 1] : 0;
41759
+ const normalized = Number.isFinite(r) && r !== 0 ? slope / r : 0;
41760
+ const ink = normalized > threshold ? bull : normalized < -threshold ? bear : null;
41761
+ if (ink == null) return flat;
41762
+ return fade ? gradient(Math.min(scale[i] ?? 0, 1), 0, 1, flat, ink) : ink;
41763
+ });
41764
+ return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: flat, width: num3(inputs, "lineWidth", 2), colors }] };
41399
41765
  }
41400
41766
  };
41401
41767
  var maEnvelope = {
@@ -41404,115 +41770,218 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41404
41770
  shortTitle: "MA Env",
41405
41771
  overlay: true,
41406
41772
  inputs: [
41407
- { key: "maType", title: "Type", type: "string", defval: "SMA", options: ["SMA", "EMA"] },
41408
- lengthInput(20),
41409
- sourceInput(),
41410
- { key: "percent", title: "Percent", type: "float", defval: 2.5, min: 0.01, max: 50, step: 0.1 },
41411
- colorInput()
41773
+ optionInput("maType", "MA Type", "SMA", ENVELOPE_MA_TYPES, "Moving average type used as the envelope basis."),
41774
+ { ...lengthInput(20, "length", "Length", 5e3, "Number of bars in the basis moving average."), min: 2 },
41775
+ sourceInput("Close", "source", "Source", "Price series the basis moving average is calculated on."),
41776
+ floatInput("percent", "Percent", 2.5, 0, 50, 0.1, "Envelope offset as a percentage of the basis. Upper = basis \xD7 (1 + percent / 100), lower = basis \xD7 (1 \u2212 percent / 100)."),
41777
+ ...channelStyleInputs()
41412
41778
  ],
41413
41779
  compute: (bars, inputs) => {
41414
- const len = num3(inputs, "length", 20);
41415
41780
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
41416
- const basis = str3(inputs, "maType", "SMA") === "EMA" ? ema(src, len) : sma(src, len);
41781
+ const basis = movingAverageOf(str3(inputs, "maType", "SMA"), src, bars, num3(inputs, "length", 20));
41417
41782
  const k = num3(inputs, "percent", 2.5) / 100;
41418
- const ink = str3(inputs, "color", SERIES_LINE);
41419
- return {
41420
- plots: [
41421
- { key: "basis", title: "Basis", values: basis, color: ink, width: 2 },
41422
- { key: "upper", title: "Upper", values: map(basis, (x) => x * (1 + k)), color: ink },
41423
- { key: "lower", title: "Lower", values: map(basis, (x) => x * (1 - k)), color: ink }
41424
- ],
41425
- bands: [{ key: "envelope", from: "upper", to: "lower", color: withAlpha2(ink) }]
41426
- };
41783
+ const ink = { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "basisColor", NEUTRAL) };
41784
+ return directionalChannel(bars, basis, map(basis, (x) => x * (1 + k)), map(basis, (x) => x * (1 - k)), ink, "envelope");
41427
41785
  }
41428
41786
  };
41787
+ var LINREG_CURVE = "Curve";
41788
+ var LINREG_SLOPE = "Slope";
41429
41789
  var linearRegression = {
41430
41790
  type: "linear-regression",
41431
41791
  title: "Linear Regression Curve",
41432
41792
  shortTitle: "LinReg",
41433
- overlay: true,
41434
- inputs: [lengthInput(14), sourceInput(), colorInput()],
41435
- compute: (bars, inputs) => ({
41436
- plots: [
41437
- {
41438
- key: "linreg",
41439
- title: "LinReg",
41440
- values: linreg(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
41441
- color: str3(inputs, "color", SERIES_LINE),
41442
- width: 2
41443
- }
41444
- ]
41445
- })
41793
+ // Slope lives in its own pane; Curve force-overlays itself onto the price candles.
41794
+ overlay: false,
41795
+ inputs: [
41796
+ optionInput("mode", "Mode", LINREG_CURVE, [LINREG_CURVE, LINREG_SLOPE], "Curve plots the regression value on the price chart; Slope plots the per-bar slope of the fit in the pane."),
41797
+ { ...lengthInput(100, "length", "Length", 5e3, "Number of bars used to fit the linear regression."), min: 2 },
41798
+ sourceInput("Close", "source", "Source", "Price series the regression is fitted to."),
41799
+ colorInput(BULLISH, "Rising", "bullColor", "Color of the regression curve and slope line while the fit slopes upward."),
41800
+ colorInput(BEARISH, "Falling", "bearColor", "Color of the regression curve and slope line while the fit slopes downward."),
41801
+ colorInput(NEUTRAL, "Regression Line", "lineColor", "Color of the regression curve and slope line while the fit is flat.")
41802
+ ],
41803
+ compute: (bars, inputs) => {
41804
+ const len = num3(inputs, "length", 100);
41805
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
41806
+ const curve = linreg(src, len, 0);
41807
+ const slope = zip(curve, linreg(src, len, 1), (a, b) => a - b);
41808
+ const bull = str3(inputs, "bullColor", BULLISH);
41809
+ const bear = str3(inputs, "bearColor", BEARISH);
41810
+ const flat = str3(inputs, "lineColor", NEUTRAL);
41811
+ const colors = slope.map((s) => Number.isFinite(s) ? s > 0 ? bull : s < 0 ? bear : flat : null);
41812
+ if (str3(inputs, "mode", LINREG_CURVE) === LINREG_SLOPE) {
41813
+ return {
41814
+ plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
41815
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
41816
+ };
41817
+ }
41818
+ return { plots: [{ key: "curve", title: "Curve", values: curve, color: flat, width: 2, colors, overlay: true }] };
41819
+ }
41446
41820
  };
41447
41821
  var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope, linearRegression];
41448
41822
 
41449
41823
  // src/core/native-indicators/classics/bands.ts
41824
+ var JAW_COLOR = "#ab47bc";
41825
+ var TEETH_COLOR = WARNING;
41826
+ var LIPS_COLOR = "#ffd700";
41450
41827
  function bollinger(bars, inputs) {
41451
41828
  const len = num3(inputs, "length", 20);
41452
41829
  const mult = num3(inputs, "mult", 2);
41453
41830
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
41454
41831
  const basis = sma(src, len);
41455
41832
  const dev = map(stdev(src, len), (x) => x * mult);
41456
- return { basis, upper: zip(basis, dev, (b, d) => b + d), lower: zip(basis, dev, (b, d) => b - d) };
41833
+ return { src, basis, upper: zip(basis, dev, (b, d) => b + d), lower: zip(basis, dev, (b, d) => b - d) };
41834
+ }
41835
+ function bollingerInputs(multMin, multStep) {
41836
+ return [
41837
+ { ...lengthInput(20, "length", "Length", 5e3, "Number of bars used for the Bollinger Bands basis (a simple moving average) and for the standard deviation."), min: 2 },
41838
+ sourceInput("Close", "source", "Source", "Price series the bands are computed from."),
41839
+ floatInput("mult", "Multiplier", 2, multMin, 50, multStep, "Standard deviation multiplier for the upper and lower bands.")
41840
+ ];
41457
41841
  }
41458
- var BB_INPUTS = [lengthInput(20), sourceInput(), { key: "mult", title: "StdDev", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 }];
41459
41842
  var bollingerBands = {
41460
41843
  type: "bollinger-bands",
41461
41844
  title: "Bollinger Bands",
41462
41845
  shortTitle: "BB",
41463
41846
  overlay: true,
41464
- inputs: [...BB_INPUTS, colorInput()],
41847
+ inputs: [
41848
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used to calculate the basis SMA and the standard deviation."),
41849
+ sourceInput("Close", "source", "Source", "Price series the bands are calculated on."),
41850
+ floatInput("mult", "Multiplier", 2, 0, 50, 0.1, "Number of standard deviations added above and below the basis to form the bands."),
41851
+ colorInput(WARNING, "Basis", "basisColor", "Color of the basis (middle band) moving average."),
41852
+ colorInput(INFO, "Bands", "color", "Color of the upper and lower bands."),
41853
+ colorInput(transp(INFO, 92), "Fill", "fillColor", "Fill color of the area between the upper and lower bands.")
41854
+ ],
41465
41855
  compute: (bars, inputs) => {
41466
41856
  const { basis, upper, lower } = bollinger(bars, inputs);
41467
- const ink = str3(inputs, "color", SERIES_LINE);
41857
+ const ink = str3(inputs, "color", INFO);
41468
41858
  return {
41469
41859
  plots: [
41470
- { key: "basis", title: "Basis", values: basis, color: WARNING },
41860
+ { key: "basis", title: "Basis", values: basis, color: str3(inputs, "basisColor", WARNING) },
41471
41861
  { key: "upper", title: "Upper", values: upper, color: ink },
41472
41862
  { key: "lower", title: "Lower", values: lower, color: ink }
41473
41863
  ],
41474
- bands: [{ key: "bb", from: "upper", to: "lower", color: withAlpha2(ink) }]
41864
+ bands: [{ key: "bb", from: "upper", to: "lower", color: str3(inputs, "fillColor", transp(INFO, 92)) }]
41475
41865
  };
41476
41866
  }
41477
41867
  };
41868
+ var PERCENT_B_MID = 0.5;
41869
+ var THRESHOLDS = "Thresholds";
41478
41870
  var percentB = {
41479
41871
  type: "percent-b",
41480
41872
  title: "Bollinger Bands %B",
41481
41873
  shortTitle: "%B",
41482
41874
  overlay: false,
41483
- inputs: [...BB_INPUTS, colorInput()],
41875
+ inputs: [
41876
+ ...bollingerInputs(1e-3, 0.25),
41877
+ { ...floatInput("overbought", "Overbought", 1, -10, 10, 0.05, "Level treated as overbought. 1 is the upper band itself, so readings above the default mean price sits outside the bands."), group: THRESHOLDS },
41878
+ { ...floatInput("oversold", "Oversold", 0, -10, 10, 0.05, "Level treated as oversold. 0 is the lower band itself, so readings below the default mean price sits outside the bands."), group: THRESHOLDS },
41879
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of %B above the 0.5 midline, where price trades in the upper half of the bands."),
41880
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of %B below the 0.5 midline, where price trades in the lower half of the bands."),
41881
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Vertical gradient between %B and the 0.5 midline, fully transparent at the midline and more opaque at the oscillator value."),
41882
+ colorInput(NEUTRAL, "Levels Color", "levelsColor", "Color of the dashed overbought/oversold levels and of the dotted midline.")
41883
+ ],
41484
41884
  compute: (bars, inputs) => {
41485
- const { upper, lower } = bollinger(bars, inputs);
41486
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
41885
+ const { src, upper, lower } = bollinger(bars, inputs);
41487
41886
  const values = src.map((x, i) => {
41488
41887
  const u = upper[i];
41489
41888
  const l = lower[i];
41490
41889
  return Number.isFinite(u) && Number.isFinite(l) && u !== l ? (x - l) / (u - l) : Number.NaN;
41491
41890
  });
41891
+ const bull = str3(inputs, "bullColor", BULLISH);
41892
+ const bear = str3(inputs, "bearColor", BEARISH);
41893
+ const levels = str3(inputs, "levelsColor", NEUTRAL);
41894
+ const colors = values.map((x) => Number.isFinite(x) ? x > PERCENT_B_MID ? bull : bear : null);
41895
+ const plots = [
41896
+ { key: "pb", title: "%B", values, color: bear, colors },
41897
+ { key: "mid", title: "Midline", values: values.map((x) => Number.isFinite(x) ? PERCENT_B_MID : Number.NaN), color: levels, display: { pane: false, legend: false, dataWindow: false } }
41898
+ ];
41899
+ const bands = bool(inputs, "gradientFill", true) ? [
41900
+ {
41901
+ key: "pbFill",
41902
+ from: "pb",
41903
+ to: "mid",
41904
+ color: transp(NEUTRAL, 100),
41905
+ // Opaque at the oscillator, invisible where it meets the midline.
41906
+ gradient: values.map((x) => {
41907
+ if (!Number.isFinite(x)) return null;
41908
+ const up = x > PERCENT_B_MID;
41909
+ const ink = up ? bull : bear;
41910
+ return {
41911
+ topValue: Math.max(x, PERCENT_B_MID),
41912
+ bottomValue: Math.min(x, PERCENT_B_MID),
41913
+ topColor: up ? transp(ink, 50) : transp(ink, 100),
41914
+ bottomColor: up ? transp(ink, 100) : transp(ink, 50)
41915
+ };
41916
+ })
41917
+ }
41918
+ ] : [];
41492
41919
  return {
41493
- plots: [{ key: "pb", title: "%B", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41920
+ plots,
41921
+ bands,
41494
41922
  levels: [
41495
- { key: "upper", price: 1, color: NEUTRAL },
41496
- { key: "middle", price: 0.5, color: NEUTRAL, lineStyle: "dotted" },
41497
- { key: "lower", price: 0, color: NEUTRAL }
41923
+ { key: "overbought", price: num3(inputs, "overbought", 1), color: levels, lineStyle: "dashed", title: "Overbought" },
41924
+ { key: "oversold", price: num3(inputs, "oversold", 0), color: levels, lineStyle: "dashed", title: "Oversold" },
41925
+ { key: "midline", price: PERCENT_B_MID, color: transp(levels, 50), lineStyle: "dotted", title: "Midline" }
41498
41926
  ]
41499
41927
  };
41500
41928
  }
41501
41929
  };
41930
+ var SQUEEZE_BULGE = "Squeeze & Bulge";
41502
41931
  var bandwidth = {
41503
41932
  type: "bandwidth",
41504
41933
  title: "Bollinger Bands Width",
41505
41934
  shortTitle: "BandWidth",
41506
41935
  overlay: false,
41507
- inputs: [...BB_INPUTS, colorInput()],
41936
+ inputs: [
41937
+ ...bollingerInputs(1e-3, 0.25),
41938
+ { ...intInput("lookback", "Reference Lookback", 125, 2, 5e3, "Window used to locate the lowest (Squeeze) and highest (Bulge) BandWidth references."), group: SQUEEZE_BULGE },
41939
+ boolInput("showSqueeze", "Show Squeeze Level", true, SQUEEZE_BULGE, "Plots the lowest BandWidth of the reference lookback \u2014 unusually compressed volatility, a condition rather than a directional signal."),
41940
+ boolInput("showBulge", "Show Bulge Level", true, SQUEEZE_BULGE, "Plots the highest BandWidth of the reference lookback \u2014 extreme width that is unlikely to be sustained."),
41941
+ colorInput(NEUTRAL, "BandWidth", "color", "Color of the BandWidth line. At its Squeeze reference the line takes the Squeeze color, at its Bulge reference the Bulge color."),
41942
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between zero and BandWidth with a vertical gradient, fully transparent at zero."),
41943
+ colorInput(WARNING, "Squeeze Level", "squeezeColor", "Color of the lowest-BandWidth reference line."),
41944
+ colorInput(BEARISH, "Bulge Level", "bulgeColor", "Color of the highest-BandWidth reference line.")
41945
+ ],
41508
41946
  compute: (bars, inputs) => {
41509
41947
  const { basis, upper, lower } = bollinger(bars, inputs);
41510
41948
  const values = basis.map((b, i) => {
41511
41949
  const u = upper[i];
41512
41950
  const l = lower[i];
41513
- return Number.isFinite(b) && b !== 0 && Number.isFinite(u) && Number.isFinite(l) ? (u - l) / b * 100 : Number.NaN;
41951
+ return Number.isFinite(b) && b !== 0 && Number.isFinite(u) && Number.isFinite(l) ? (u - l) / b : Number.NaN;
41952
+ });
41953
+ const lookback = num3(inputs, "lookback", 125);
41954
+ const squeezeLevel = lowest(values, lookback);
41955
+ const bulgeLevel = highest(values, lookback);
41956
+ const base = str3(inputs, "color", NEUTRAL);
41957
+ const squeezeInk = str3(inputs, "squeezeColor", WARNING);
41958
+ const bulgeInk = str3(inputs, "bulgeColor", BEARISH);
41959
+ const colors = values.map((x, i) => {
41960
+ if (!Number.isFinite(x)) return null;
41961
+ if (x <= squeezeLevel[i]) return squeezeInk;
41962
+ if (x >= bulgeLevel[i]) return bulgeInk;
41963
+ return base;
41514
41964
  });
41515
- return { plots: [{ key: "bbw", title: "BBW", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
41965
+ const plots = [
41966
+ { key: "bbw", title: "BandWidth", values, color: base, colors },
41967
+ { key: "zero", title: "Zero Line", values: values.map((x) => Number.isFinite(x) ? 0 : Number.NaN), color: transp(NEUTRAL, 90) }
41968
+ ];
41969
+ if (bool(inputs, "showSqueeze", true)) plots.push({ key: "squeeze", title: "Squeeze Level", values: squeezeLevel, color: squeezeInk });
41970
+ if (bool(inputs, "showBulge", true)) plots.push({ key: "bulge", title: "Bulge Level", values: bulgeLevel, color: bulgeInk });
41971
+ const bands = bool(inputs, "gradientFill", true) ? [
41972
+ {
41973
+ key: "bbwFill",
41974
+ from: "bbw",
41975
+ to: "zero",
41976
+ color: transp(NEUTRAL, 100),
41977
+ gradient: values.map((x, i) => {
41978
+ if (!Number.isFinite(x)) return null;
41979
+ const ink = colors[i] ?? base;
41980
+ return { topValue: x, bottomValue: 0, topColor: transp(ink, 50), bottomColor: transp(ink, 100) };
41981
+ })
41982
+ }
41983
+ ] : [];
41984
+ return { plots, bands };
41516
41985
  }
41517
41986
  };
41518
41987
  var keltner = {
@@ -41521,26 +41990,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41521
41990
  shortTitle: "KC",
41522
41991
  overlay: true,
41523
41992
  inputs: [
41524
- lengthInput(20),
41525
- { key: "mult", title: "Multiplier", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 },
41526
- { key: "atrLength", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
41527
- sourceInput(),
41528
- colorInput()
41993
+ lengthInput(20, "length", "EMA Length", 5e3, "Number of bars used to compute the exponential moving average basis line."),
41994
+ intInput("atrLength", "ATR Length", 10, 1, 5e3, "Number of bars used to compute the Average True Range (Wilder smoothing)."),
41995
+ floatInput("mult", "Multiplier", 2, 0, 50, 0.1, "ATR multiple used to offset the upper and lower bands from the basis line."),
41996
+ sourceInput("Close", "source", "Source", "Price series used to compute the basis line."),
41997
+ ...channelStyleInputs("Basis Color")
41529
41998
  ],
41530
41999
  compute: (bars, inputs) => {
41531
- const len = num3(inputs, "length", 20);
41532
- const mult = num3(inputs, "mult", 2);
41533
- const basis = ema(sourceValues(bars, str3(inputs, "source", "Close")), len);
41534
- const range = map(atr(bars, num3(inputs, "atrLength", 10)), (x) => x * mult);
41535
- const ink = str3(inputs, "color", SERIES_LINE);
41536
- return {
41537
- plots: [
41538
- { key: "basis", title: "Basis", values: basis, color: WARNING },
41539
- { key: "upper", title: "Upper", values: zip(basis, range, (b, r) => b + r), color: ink },
41540
- { key: "lower", title: "Lower", values: zip(basis, range, (b, r) => b - r), color: ink }
41541
- ],
41542
- bands: [{ key: "kc", from: "upper", to: "lower", color: withAlpha2(ink) }]
41543
- };
42000
+ const basis = ema(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20));
42001
+ const range = map(atr(bars, num3(inputs, "atrLength", 10)), (x) => x * num3(inputs, "mult", 2));
42002
+ const ink = { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "basisColor", NEUTRAL) };
42003
+ return directionalChannel(bars, basis, zip(basis, range, (b, r) => b + r), zip(basis, range, (b, r) => b - r), ink, "kc");
41544
42004
  }
41545
42005
  };
41546
42006
  var donchian = {
@@ -41548,19 +42008,24 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41548
42008
  title: "Donchian Channels",
41549
42009
  shortTitle: "DC",
41550
42010
  overlay: true,
41551
- inputs: [lengthInput(20), colorInput()],
42011
+ inputs: [
42012
+ lengthInput(20, "length", "Length", 5e3, "Lookback length. The upper and lower channels are the highest high and lowest low over this many bars."),
42013
+ colorInput(BEARISH, "Upper", "upperColor", "Color of the upper channel line."),
42014
+ colorInput(NEUTRAL, "Middle", "middleColor", "Color of the channel midline."),
42015
+ colorInput(BULLISH, "Lower", "lowerColor", "Color of the lower channel line."),
42016
+ colorInput(transp(NEUTRAL, 92), "Fill", "fillColor", "Color of the channel interior fill.")
42017
+ ],
41552
42018
  compute: (bars, inputs) => {
41553
42019
  const len = num3(inputs, "length", 20);
41554
42020
  const upper = highest(highs(bars), len);
41555
42021
  const lower = lowest(lows(bars), len);
41556
- const ink = str3(inputs, "color", SERIES_LINE);
41557
42022
  return {
41558
42023
  plots: [
41559
- { key: "basis", title: "Basis", values: zip(upper, lower, (u, l) => (u + l) / 2), color: WARNING },
41560
- { key: "upper", title: "Upper", values: upper, color: ink },
41561
- { key: "lower", title: "Lower", values: lower, color: ink }
42024
+ { key: "upper", title: "Upper", values: upper, color: str3(inputs, "upperColor", BEARISH) },
42025
+ { key: "middle", title: "Middle", kind: "circles", values: zip(upper, lower, (u, l) => (u + l) / 2), color: str3(inputs, "middleColor", NEUTRAL) },
42026
+ { key: "lower", title: "Lower", values: lower, color: str3(inputs, "lowerColor", BULLISH) }
41562
42027
  ],
41563
- bands: [{ key: "dc", from: "upper", to: "lower", color: withAlpha2(ink) }]
42028
+ bands: [{ key: "dc", from: "upper", to: "lower", color: str3(inputs, "fillColor", transp(NEUTRAL, 92)) }]
41564
42029
  };
41565
42030
  }
41566
42031
  };
@@ -41569,16 +42034,37 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41569
42034
  title: "Chandelier Exit",
41570
42035
  shortTitle: "CE",
41571
42036
  overlay: true,
41572
- inputs: [lengthInput(22), { key: "mult", title: "ATR multiplier", type: "float", defval: 3, min: 0.1, max: 50, step: 0.1 }],
42037
+ inputs: [
42038
+ lengthInput(22, "length", "Length", 5e3, "Lookback used for both the highest high / lowest low extremes and the ATR."),
42039
+ floatInput("mult", "ATR Multiplier", 3, 0, 50, 0.1, "Multiple of ATR subtracted from the highest high (long stop) and added to the lowest low (short stop)."),
42040
+ boolInput("ratchet", "Ratchet Stops", false, void 0, "Prevents the long stop from falling and the short stop from rising, resetting once close crosses through the stop."),
42041
+ colorInput(BULLISH, "Long Stop", "longColor", "Color of the long trailing stop line."),
42042
+ colorInput(BEARISH, "Short Stop", "shortColor", "Color of the short trailing stop line.")
42043
+ ],
41573
42044
  compute: (bars, inputs) => {
41574
42045
  const len = num3(inputs, "length", 22);
41575
42046
  const range = map(atr(bars, len), (x) => x * num3(inputs, "mult", 3));
41576
- const long = zip(highest(highs(bars), len), range, (h, r) => h - r);
41577
- const short = zip(lowest(lows(bars), len), range, (l, r) => l + r);
42047
+ const basicLong = zip(highest(highs(bars), len), range, (h, r) => h - r);
42048
+ const basicShort = zip(lowest(lows(bars), len), range, (l, r) => l + r);
42049
+ let long = basicLong;
42050
+ let short = basicShort;
42051
+ if (bool(inputs, "ratchet", false)) {
42052
+ long = new Array(bars.length).fill(Number.NaN);
42053
+ short = new Array(bars.length).fill(Number.NaN);
42054
+ for (let i = 0; i < bars.length; i++) {
42055
+ const bl = basicLong[i];
42056
+ const bs = basicShort[i];
42057
+ const prevClose = i > 0 ? bars[i - 1].close : bars[i].close;
42058
+ const prevLong = i > 0 && Number.isFinite(long[i - 1]) ? long[i - 1] : bl;
42059
+ const prevShort = i > 0 && Number.isFinite(short[i - 1]) ? short[i - 1] : bs;
42060
+ long[i] = Number.isFinite(bl) ? prevClose > prevLong ? Math.max(bl, prevLong) : bl : Number.NaN;
42061
+ short[i] = Number.isFinite(bs) ? prevClose < prevShort ? Math.min(bs, prevShort) : bs : Number.NaN;
42062
+ }
42063
+ }
41578
42064
  return {
41579
42065
  plots: [
41580
- { key: "long", title: "Long stop", values: long, color: BULLISH },
41581
- { key: "short", title: "Short stop", values: short, color: BEARISH }
42066
+ { key: "long", title: "Long Stop", values: long, color: str3(inputs, "longColor", BULLISH) },
42067
+ { key: "short", title: "Short Stop", values: short, color: str3(inputs, "shortColor", BEARISH) }
41582
42068
  ]
41583
42069
  };
41584
42070
  }
@@ -41589,21 +42075,22 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41589
42075
  shortTitle: "CKS",
41590
42076
  overlay: true,
41591
42077
  inputs: [
41592
- { key: "p", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
41593
- { key: "x", title: "ATR multiplier", type: "float", defval: 1, min: 0.1, max: 50, step: 0.1 },
41594
- { key: "q", title: "Stop length", type: "int", defval: 9, min: 1, max: 500, step: 1 }
42078
+ intInput("p", "ATR Length (P)", 10, 1, 5e3, "Lookback used for both the ATR and the highest high / lowest low price extremes."),
42079
+ floatInput("x", "ATR Multiplier (X)", 1, 0, 50, 0.1, "Multiplier applied to the ATR to offset the preliminary stops from the price extremes."),
42080
+ intInput("q", "Stop Length (Q)", 9, 1, 5e3, "Lookback over which the preliminary stops are extended into the final stop levels."),
42081
+ colorInput(BEARISH, "Stop Short", "shortColor", "Color of the protective stop line for short positions."),
42082
+ colorInput(BULLISH, "Stop Long", "longColor", "Color of the protective stop line for long positions.")
41595
42083
  ],
41596
42084
  compute: (bars, inputs) => {
41597
42085
  const p = num3(inputs, "p", 10);
41598
- const x = num3(inputs, "x", 1);
41599
42086
  const q = num3(inputs, "q", 9);
41600
- const range = map(atr(bars, p), (v) => v * x);
42087
+ const range = map(atr(bars, p), (v) => v * num3(inputs, "x", 1));
41601
42088
  const firstHigh = zip(highest(highs(bars), p), range, (h, r) => h - r);
41602
42089
  const firstLow = zip(lowest(lows(bars), p), range, (l, r) => l + r);
41603
42090
  return {
41604
42091
  plots: [
41605
- { key: "stopShort", title: "Stop short", values: highest(firstHigh, q), color: BEARISH },
41606
- { key: "stopLong", title: "Stop long", values: lowest(firstLow, q), color: BULLISH }
42092
+ { key: "stopShort", title: "Stop Short", values: highest(firstHigh, q), color: str3(inputs, "shortColor", BEARISH) },
42093
+ { key: "stopLong", title: "Stop Long", values: lowest(firstLow, q), color: str3(inputs, "longColor", BULLISH) }
41607
42094
  ]
41608
42095
  };
41609
42096
  }
@@ -41614,36 +42101,42 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41614
42101
  shortTitle: "SuperTrend",
41615
42102
  overlay: true,
41616
42103
  inputs: [
41617
- { key: "atrLength", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
41618
- { key: "mult", title: "Factor", type: "float", defval: 3, min: 0.1, max: 50, step: 0.1 }
42104
+ intInput("atrLength", "ATR Length", 10, 1, 5e3, "Number of bars used to compute the Wilder ATR."),
42105
+ floatInput("mult", "Factor", 3, 0, 50, 0.1, "ATR multiplier setting how far the trailing stop sits from the bar midpoint."),
42106
+ colorInput(BULLISH, "Uptrend Color", "upColor", "Line color while the trend is up."),
42107
+ colorInput(BEARISH, "Downtrend Color", "downColor", "Line color while the trend is down.")
41619
42108
  ],
41620
42109
  compute: (bars, inputs) => {
41621
- const len = num3(inputs, "atrLength", 10);
41622
42110
  const mult = num3(inputs, "mult", 3);
41623
- const range = atr(bars, len);
42111
+ const range = atr(bars, num3(inputs, "atrLength", 10));
41624
42112
  const n = bars.length;
41625
- const st = new Array(n).fill(Number.NaN);
41626
- const dir = new Array(n).fill(1);
41627
- let up = Number.NaN;
41628
- let dn = Number.NaN;
41629
- let trend = 1;
42113
+ const up = new Array(n).fill(Number.NaN);
42114
+ const down = new Array(n).fill(Number.NaN);
42115
+ let finalUpper = Number.NaN;
42116
+ let finalLower = Number.NaN;
42117
+ let trend = -1;
41630
42118
  for (let i = 0; i < n; i++) {
41631
42119
  const r = range[i];
41632
42120
  if (!Number.isFinite(r)) continue;
41633
42121
  const b = bars[i];
41634
42122
  const mid = (b.high + b.low) / 2;
41635
- const basicUp = mid - mult * r;
41636
- const basicDn = mid + mult * r;
42123
+ const basicUpper = mid + mult * r;
42124
+ const basicLower = mid - mult * r;
42125
+ const prevUpper = Number.isFinite(finalUpper) ? finalUpper : basicUpper;
42126
+ const prevLower = Number.isFinite(finalLower) ? finalLower : basicLower;
41637
42127
  const prevClose = i > 0 ? bars[i - 1].close : b.close;
41638
- up = Number.isFinite(up) && prevClose > up ? Math.max(basicUp, up) : basicUp;
41639
- dn = Number.isFinite(dn) && prevClose < dn ? Math.min(basicDn, dn) : basicDn;
41640
- if (trend === 1 && b.close < up) trend = -1;
41641
- else if (trend === -1 && b.close > dn) trend = 1;
41642
- dir[i] = trend;
41643
- st[i] = trend === 1 ? up : dn;
42128
+ finalUpper = prevClose <= prevUpper ? Math.min(basicUpper, prevUpper) : basicUpper;
42129
+ finalLower = prevClose >= prevLower ? Math.max(basicLower, prevLower) : basicLower;
42130
+ trend = b.close > finalUpper ? 1 : b.close < finalLower ? -1 : trend;
42131
+ if (trend === 1) up[i] = finalLower;
42132
+ else down[i] = finalUpper;
41644
42133
  }
41645
- const colors = dir.map((d, i) => Number.isFinite(st[i]) ? d === 1 ? BULLISH : BEARISH : null);
41646
- return { plots: [{ key: "st", title: "SuperTrend", values: st, color: BULLISH, width: 2, colors }] };
42134
+ return {
42135
+ plots: [
42136
+ { key: "up", title: "Uptrend", values: up, color: str3(inputs, "upColor", BULLISH) },
42137
+ { key: "down", title: "Downtrend", values: down, color: str3(inputs, "downColor", BEARISH) }
42138
+ ]
42139
+ };
41647
42140
  }
41648
42141
  };
41649
42142
  var alligator = {
@@ -41652,12 +42145,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41652
42145
  shortTitle: "Alligator",
41653
42146
  overlay: true,
41654
42147
  inputs: [
41655
- { key: "jawLength", title: "Jaw length", type: "int", defval: 13, min: 1, max: 500, step: 1 },
41656
- { key: "teethLength", title: "Teeth length", type: "int", defval: 8, min: 1, max: 500, step: 1 },
41657
- { key: "lipsLength", title: "Lips length", type: "int", defval: 5, min: 1, max: 500, step: 1 },
41658
- { key: "jawOffset", title: "Jaw offset", type: "int", defval: 8, min: 0, max: 100, step: 1 },
41659
- { key: "teethOffset", title: "Teeth offset", type: "int", defval: 5, min: 0, max: 100, step: 1 },
41660
- { key: "lipsOffset", title: "Lips offset", type: "int", defval: 3, min: 0, max: 100, step: 1 }
42148
+ intInput("jawLength", "Jaw Length", 13, 1, 5e3, "Smoothing length of the Jaw SMMA of median price (hl2), 13 in the classic setup."),
42149
+ intInput("teethLength", "Teeth Length", 8, 1, 5e3, "Smoothing length of the Teeth SMMA of median price (hl2), 8 in the classic setup."),
42150
+ intInput("lipsLength", "Lips Length", 5, 1, 5e3, "Smoothing length of the Lips SMMA of median price (hl2), 5 in the classic setup."),
42151
+ intInput("jawOffset", "Jaw Offset", 8, 0, 100, "Number of bars the Jaw plot is shifted into the future, 8 in the classic setup."),
42152
+ intInput("teethOffset", "Teeth Offset", 5, 0, 100, "Number of bars the Teeth plot is shifted into the future, 5 in the classic setup."),
42153
+ intInput("lipsOffset", "Lips Offset", 3, 0, 100, "Number of bars the Lips plot is shifted into the future, 3 in the classic setup."),
42154
+ colorInput(JAW_COLOR, "Jaw", "jawColor", "Color of the Jaw line."),
42155
+ colorInput(TEETH_COLOR, "Teeth", "teethColor", "Color of the Teeth line."),
42156
+ colorInput(LIPS_COLOR, "Lips", "lipsColor", "Color of the Lips line.")
41661
42157
  ],
41662
42158
  compute: (bars, inputs) => {
41663
42159
  const hl2 = sourceValues(bars, "HL2");
@@ -41666,99 +42162,134 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41666
42162
  const lips = shift3(rma(hl2, num3(inputs, "lipsLength", 5)), num3(inputs, "lipsOffset", 3));
41667
42163
  return {
41668
42164
  plots: [
41669
- { key: "jaw", title: "Jaw", values: jaw, color: SERIES_LINE },
41670
- { key: "teeth", title: "Teeth", values: teeth, color: BEARISH },
41671
- { key: "lips", title: "Lips", values: lips, color: BULLISH }
42165
+ { key: "jaw", title: "Jaw", values: jaw, color: str3(inputs, "jawColor", JAW_COLOR) },
42166
+ { key: "teeth", title: "Teeth", values: teeth, color: str3(inputs, "teethColor", TEETH_COLOR) },
42167
+ { key: "lips", title: "Lips", values: lips, color: str3(inputs, "lipsColor", LIPS_COLOR) }
41672
42168
  ]
41673
42169
  };
41674
42170
  }
41675
42171
  };
42172
+ var GATOR_LINES = "Alligator Lines";
42173
+ var SMMA_TYPE = "SMMA (Wilder)";
41676
42174
  var gator = {
41677
42175
  type: "gator-oscillator",
41678
42176
  title: "Gator Oscillator",
41679
42177
  shortTitle: "Gator Oscillator",
41680
42178
  overlay: false,
41681
- inputs: [],
41682
- compute: (bars) => {
41683
- const hl2 = sourceValues(bars, "HL2");
41684
- const jaw = shift3(rma(hl2, 13), 8);
41685
- const teeth = shift3(rma(hl2, 8), 5);
41686
- const lips = shift3(rma(hl2, 5), 3);
42179
+ inputs: [
42180
+ { ...sourceInput("HL2", "source", "Source", "Price series fed to the three Alligator averages. Bill Williams' original recipe uses the median price (high + low) / 2."), group: GATOR_LINES },
42181
+ { ...optionInput("maType", "Smoothing", SMMA_TYPE, [SMMA_TYPE, "SMA", "EMA"], "Moving average applied to the source for the jaw, teeth and lips. The original uses the smoothed (Wilder-style) moving average."), group: GATOR_LINES },
42182
+ { ...intInput("jawLength", "Jaw Length", 13, 1, 500, "Averaging length of the jaw line."), group: GATOR_LINES },
42183
+ { ...intInput("jawOffset", "Jaw Offset", 8, 0, 100, "Bars the jaw is displaced forward, 8 in the original recipe. The upper histogram measures the absolute spread between the displaced jaw and teeth."), group: GATOR_LINES },
42184
+ { ...intInput("teethLength", "Teeth Length", 8, 1, 500, "Averaging length of the teeth line."), group: GATOR_LINES },
42185
+ { ...intInput("teethOffset", "Teeth Offset", 5, 0, 100, "Bars the teeth are displaced forward, 5 in the original recipe. The teeth line enters both spreads."), group: GATOR_LINES },
42186
+ { ...intInput("lipsLength", "Lips Length", 5, 1, 500, "Averaging length of the lips line."), group: GATOR_LINES },
42187
+ { ...intInput("lipsOffset", "Lips Offset", 3, 0, 100, "Bars the lips are displaced forward, 3 in the original recipe. The lower histogram measures the negative absolute spread between the displaced teeth and lips."), group: GATOR_LINES },
42188
+ colorInput(BULLISH, "Expanding", "growColor", "Column color while a spread grew against its own previous bar \u2014 the averages are spreading apart."),
42189
+ colorInput(BEARISH, "Contracting", "shrinkColor", "Column color while a spread shrank \u2014 the averages are converging. Color encodes expansion, not trade direction."),
42190
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero line separating the two histograms.")
42191
+ ],
42192
+ compute: (bars, inputs) => {
42193
+ const src = sourceValues(bars, str3(inputs, "source", "HL2"));
42194
+ const kind = str3(inputs, "maType", SMMA_TYPE);
42195
+ const smooth = (len) => kind === "SMA" ? sma(src, len) : kind === "EMA" ? ema(src, len) : rma(src, len);
42196
+ const jaw = shift3(smooth(num3(inputs, "jawLength", 13)), num3(inputs, "jawOffset", 8));
42197
+ const teeth = shift3(smooth(num3(inputs, "teethLength", 8)), num3(inputs, "teethOffset", 5));
42198
+ const lips = shift3(smooth(num3(inputs, "lipsLength", 5)), num3(inputs, "lipsOffset", 3));
41687
42199
  const upper = map(sub(jaw, teeth), Math.abs);
41688
- const lower = map(sub(teeth, lips), (x) => -Math.abs(x));
41689
- const colorByExpansion = (v) => v.map((x, i) => {
41690
- if (!Number.isFinite(x)) return null;
41691
- const prev3 = i > 0 ? v[i - 1] : Number.NaN;
41692
- return Number.isFinite(prev3) && Math.abs(x) > Math.abs(prev3) ? BULLISH : BEARISH;
41693
- });
42200
+ const lower = map(sub(teeth, lips), Math.abs);
42201
+ const grow = str3(inputs, "growColor", BULLISH);
42202
+ const shrink = str3(inputs, "shrinkColor", BEARISH);
42203
+ const expansionColors2 = (v) => {
42204
+ let growing = false;
42205
+ return v.map((x, i) => {
42206
+ if (!Number.isFinite(x)) return null;
42207
+ const prev3 = i > 0 ? v[i - 1] : Number.NaN;
42208
+ if (Number.isFinite(prev3)) growing = x > prev3 ? true : x < prev3 ? false : growing;
42209
+ return growing ? grow : shrink;
42210
+ });
42211
+ };
41694
42212
  return {
41695
42213
  plots: [
41696
- { key: "upper", title: "Upper", values: upper, kind: "histogram", color: BULLISH, colors: colorByExpansion(upper), base: 0 },
41697
- { key: "lower", title: "Lower", values: lower, kind: "histogram", color: BEARISH, colors: colorByExpansion(lower), base: 0 }
41698
- ]
42214
+ { key: "upper", title: "Upper Histogram (Jaw - Teeth)", values: upper, kind: "columns", color: grow, colors: expansionColors2(upper), base: 0 },
42215
+ { key: "lower", title: "Lower Histogram (Teeth - Lips)", values: map(lower, (x) => -x), kind: "columns", color: shrink, colors: expansionColors2(lower), base: 0 }
42216
+ ],
42217
+ levels: [{ key: "zero", price: 0, color: str3(inputs, "zeroColor", NEUTRAL), lineStyle: "solid", title: "Zero Line" }]
41699
42218
  };
41700
42219
  }
41701
42220
  };
41702
42221
  var bandSpecs = [bollingerBands, percentB, bandwidth, keltner, donchian, chandelier, chandeKroll, supertrend, alligator, gator];
41703
42222
 
41704
42223
  // src/core/native-indicators/classics/oscillators.ts
41705
- function guideLevels(low, high, mid) {
41706
- const levels = [
41707
- { key: "upper", price: high, color: NEUTRAL },
41708
- { key: "lower", price: low, color: NEUTRAL }
42224
+ function thresholdInputs(obDefault, osDefault, min2 = 0, max2 = 100, obTitle = "Overbought Level", osTitle = "Oversold Level") {
42225
+ return [
42226
+ floatInput("overbought", obTitle, obDefault, min2, max2, 1, "Level above which the oscillator is considered overbought."),
42227
+ floatInput("oversold", osTitle, osDefault, min2, max2, 1, "Level below which the oscillator is considered oversold.")
41709
42228
  ];
41710
- if (mid != null) levels.splice(1, 0, { key: "middle", price: mid, color: NEUTRAL, lineStyle: "dotted" });
41711
- return levels;
41712
- }
41713
- var ZERO_LEVEL = [{ key: "zero", price: 0, color: NEUTRAL }];
41714
- function histColors(values) {
41715
- return values.map((x, i) => {
41716
- if (!Number.isFinite(x)) return null;
41717
- const prev3 = i > 0 ? values[i - 1] : Number.NaN;
41718
- const rising = Number.isFinite(prev3) ? x >= prev3 : true;
41719
- if (x >= 0) return rising ? BULLISH : `${BULLISH}80`;
41720
- return rising ? `${BEARISH}80` : BEARISH;
41721
- });
41722
42229
  }
41723
42230
  var rsiSpec = {
41724
42231
  type: "rsi",
41725
42232
  title: "Relative Strength Index",
41726
42233
  shortTitle: "RSI",
41727
42234
  overlay: false,
41728
- inputs: [lengthInput(14), sourceInput(), colorInput()],
41729
- compute: (bars, inputs) => ({
41730
- plots: [
41731
- {
41732
- key: "rsi",
41733
- title: "RSI",
41734
- values: rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
41735
- color: str3(inputs, "color", SERIES_LINE),
41736
- width: 2
41737
- }
41738
- ],
41739
- levels: guideLevels(30, 70, 50)
42235
+ inputs: [
42236
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used in the Wilder smoothing of average gains and losses."),
42237
+ sourceInput("Close", "source", "Source", "Price series the RSI is calculated on."),
42238
+ ...thresholdInputs(70, 30, 0, 100, "Overbought", "Oversold"),
42239
+ colorInput(NEUTRAL, "RSI", "color", "Color of the RSI line at the midline. Toward either threshold it blends into that threshold\u2019s color."),
42240
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level."),
42241
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
42242
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought, midline and oversold levels."),
42243
+ colorInput(transp(NEUTRAL, 90), "Band Fill", "fillColor", "Color of the fill between the overbought and oversold levels.")
42244
+ ],
42245
+ compute: (bars, inputs) => boundedOscillator({
42246
+ key: "rsi",
42247
+ title: "RSI",
42248
+ values: rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
42249
+ scale: { top: 100, bottom: 0 },
42250
+ overbought: num3(inputs, "overbought", 70),
42251
+ oversold: num3(inputs, "oversold", 30),
42252
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
42253
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
42254
+ width: 2,
42255
+ midline: 50,
42256
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 90))
41740
42257
  })
41741
42258
  };
42259
+ function stochasticOutput(k, d, inputs) {
42260
+ const overbought = num3(inputs, "overbought", 80);
42261
+ const oversold = num3(inputs, "oversold", 20);
42262
+ return {
42263
+ plots: [
42264
+ { key: "k", title: "%K", values: k, color: str3(inputs, "kColor", INFO) },
42265
+ { key: "d", title: "%D", values: d, color: str3(inputs, "dColor", WARNING) },
42266
+ anchorPlot("obAnchor", "Overbought Anchor", k.length, overbought),
42267
+ anchorPlot("osAnchor", "Oversold Anchor", k.length, oversold)
42268
+ ],
42269
+ bands: [{ key: "band", from: "obAnchor", to: "osAnchor", color: str3(inputs, "fillColor", transp(INFO, 90)) }],
42270
+ levels: [
42271
+ { key: "overbought", price: overbought, color: NEUTRAL, lineStyle: "dashed", title: "Overbought" },
42272
+ { key: "oversold", price: oversold, color: NEUTRAL, lineStyle: "dashed", title: "Oversold" }
42273
+ ]
42274
+ };
42275
+ }
41742
42276
  var stochastic = {
41743
42277
  type: "stochastic",
41744
42278
  title: "Stochastic",
41745
42279
  shortTitle: "Stoch",
41746
42280
  overlay: false,
41747
42281
  inputs: [
41748
- lengthInput(14, "kLength", "%K length"),
41749
- lengthInput(1, "kSmoothing", "%K smoothing", 500),
41750
- lengthInput(3, "dLength", "%D smoothing", 500)
42282
+ lengthInput(14, "kLength", "%K Length", 5e3, "Number of bars used for the highest high / lowest low range of the raw %K."),
42283
+ lengthInput(3, "kSmoothing", "%K Smoothing", 500, "Simple moving average length applied to the raw %K. 1 gives the fast stochastic, 3 the classic slow stochastic."),
42284
+ lengthInput(3, "dLength", "%D Smoothing", 500, "Simple moving average length of the %D signal line, applied to the smoothed %K."),
42285
+ ...thresholdInputs(80, 20),
42286
+ colorInput(INFO, "%K", "kColor", "Color of the %K line."),
42287
+ colorInput(WARNING, "%D", "dColor", "Color of the %D signal line."),
42288
+ colorInput(transp(INFO, 90), "Band Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
41751
42289
  ],
41752
42290
  compute: (bars, inputs) => {
41753
- const k = sma(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "kLength", 14)), num3(inputs, "kSmoothing", 1));
41754
- const d = sma(k, num3(inputs, "dLength", 3));
41755
- return {
41756
- plots: [
41757
- { key: "k", title: "%K", values: k, color: SERIES_LINE, width: 2 },
41758
- { key: "d", title: "%D", values: d, color: WARNING }
41759
- ],
41760
- levels: guideLevels(20, 80)
41761
- };
42291
+ const k = sma(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "kLength", 14)), num3(inputs, "kSmoothing", 3));
42292
+ return stochasticOutput(k, sma(k, num3(inputs, "dLength", 3)), inputs);
41762
42293
  }
41763
42294
  };
41764
42295
  var stochasticRsi = {
@@ -41767,23 +42298,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41767
42298
  shortTitle: "Stoch RSI",
41768
42299
  overlay: false,
41769
42300
  inputs: [
41770
- lengthInput(14, "rsiLength", "RSI length"),
41771
- lengthInput(14, "stochLength", "Stochastic length"),
41772
- lengthInput(3, "kSmoothing", "%K smoothing", 500),
41773
- lengthInput(3, "dLength", "%D smoothing", 500),
41774
- sourceInput()
42301
+ sourceInput("Close", "source", "RSI Source", "Price series the RSI is computed on."),
42302
+ lengthInput(14, "rsiLength", "RSI Length", 5e3, "Lookback length of the Wilder RSI."),
42303
+ lengthInput(14, "stochLength", "Stochastic Length", 5e3, "Lookback over which the highest and lowest RSI values are taken."),
42304
+ lengthInput(3, "kSmoothing", "%K Smoothing", 500, "Simple moving average length applied to the stochastic of RSI to form %K."),
42305
+ lengthInput(3, "dLength", "%D Smoothing", 500, "Simple moving average length applied to %K to form the %D signal line."),
42306
+ ...thresholdInputs(80, 20),
42307
+ colorInput(INFO, "%K Color", "kColor", "Color of the %K line."),
42308
+ colorInput(WARNING, "%D Color", "dColor", "Color of the %D signal line."),
42309
+ colorInput(transp(INFO, 90), "Zone Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
41775
42310
  ],
41776
42311
  compute: (bars, inputs) => {
41777
42312
  const r = rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "rsiLength", 14));
41778
42313
  const k = sma(stoch(r, r, r, num3(inputs, "stochLength", 14)), num3(inputs, "kSmoothing", 3));
41779
- const d = sma(k, num3(inputs, "dLength", 3));
41780
- return {
41781
- plots: [
41782
- { key: "k", title: "%K", values: k, color: SERIES_LINE, width: 2 },
41783
- { key: "d", title: "%D", values: d, color: WARNING }
41784
- ],
41785
- levels: guideLevels(20, 80)
41786
- };
42314
+ return stochasticOutput(k, sma(k, num3(inputs, "dLength", 3)), inputs);
41787
42315
  }
41788
42316
  };
41789
42317
  var macd = {
@@ -41791,23 +42319,35 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41791
42319
  title: "MACD",
41792
42320
  overlay: false,
41793
42321
  inputs: [
41794
- lengthInput(12, "fastLength", "Fast length"),
41795
- lengthInput(26, "slowLength", "Slow length"),
41796
- lengthInput(9, "signalLength", "Signal smoothing", 500),
41797
- sourceInput()
42322
+ lengthInput(12, "fastLength", "Fast Length", 5e3, "Length of the fast EMA."),
42323
+ lengthInput(26, "slowLength", "Slow Length", 5e3, "Length of the slow EMA."),
42324
+ lengthInput(9, "signalLength", "Signal Smoothing", 500, "Length of the EMA applied to the MACD line to obtain the signal line."),
42325
+ sourceInput("Close", "source", "Source", "Price series used to compute the fast and slow EMAs."),
42326
+ colorInput(INFO, "MACD Line", "color", "Color of the MACD line."),
42327
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
42328
+ colorInput(BULLISH, "Grow Above", "growAboveColor", "Histogram color when above zero and rising."),
42329
+ colorInput(transp(BULLISH, 50), "Fall Above", "fallAboveColor", "Histogram color when above zero and falling."),
42330
+ colorInput(transp(BEARISH, 50), "Grow Below", "growBelowColor", "Histogram color when below zero and rising."),
42331
+ colorInput(BEARISH, "Fall Below", "fallBelowColor", "Histogram color when below zero and falling.")
41798
42332
  ],
41799
42333
  compute: (bars, inputs) => {
41800
42334
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
41801
42335
  const line = sub(ema(src, num3(inputs, "fastLength", 12)), ema(src, num3(inputs, "slowLength", 26)));
41802
42336
  const signal = ema(line, num3(inputs, "signalLength", 9));
41803
42337
  const hist = sub(line, signal);
42338
+ const colors = momentumColumnColors(hist, {
42339
+ growAbove: str3(inputs, "growAboveColor", BULLISH),
42340
+ fallAbove: str3(inputs, "fallAboveColor", transp(BULLISH, 50)),
42341
+ growBelow: str3(inputs, "growBelowColor", transp(BEARISH, 50)),
42342
+ fallBelow: str3(inputs, "fallBelowColor", BEARISH)
42343
+ });
41804
42344
  return {
41805
42345
  plots: [
41806
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: histColors(hist), base: 0 },
41807
- { key: "macd", title: "MACD", values: line, color: SERIES_LINE, width: 2 },
41808
- { key: "signal", title: "Signal", values: signal, color: WARNING }
42346
+ { key: "hist", title: "Histogram", values: hist, kind: "columns", color: BULLISH, colors, base: 0 },
42347
+ { key: "macd", title: "MACD", values: line, color: str3(inputs, "color", INFO) },
42348
+ { key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) }
41809
42349
  ],
41810
- levels: ZERO_LEVEL
42350
+ levels: [{ key: "zero", price: 0, color: NEUTRAL, lineStyle: "dashed", title: "Zero Line" }]
41811
42351
  };
41812
42352
  }
41813
42353
  };
@@ -41817,26 +42357,49 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41817
42357
  shortTitle: "PPO",
41818
42358
  overlay: false,
41819
42359
  inputs: [
41820
- lengthInput(12, "fastLength", "Fast length"),
41821
- lengthInput(26, "slowLength", "Slow length"),
41822
- lengthInput(9, "signalLength", "Signal smoothing", 500),
41823
- sourceInput()
42360
+ sourceInput("Close", "source", "Source", "Price series fed into the fast and slow averages."),
42361
+ lengthInput(12, "fastLength", "Fast Length", 5e3, "Length of the fast moving average."),
42362
+ lengthInput(26, "slowLength", "Slow Length", 5e3, "Length of the slow moving average. The spread is divided by it and scaled to a percentage of price."),
42363
+ optionInput("maType", "Average Type", "EMA", ["EMA", "SMA"], "Moving average type used for the fast and slow averages."),
42364
+ lengthInput(9, "signalLength", "Signal Smoothing", 500, "Length of the signal line that smooths the PPO line."),
42365
+ optionInput("signalType", "Signal Type", "EMA", ["EMA", "SMA"], "Moving average type used for the signal line."),
42366
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the PPO line, gradient fill and histogram above the zero line."),
42367
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the PPO line, gradient fill and histogram below the zero line."),
42368
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
42369
+ boolInput("showHistogram", "Show Histogram", true, STYLE, "Displays the histogram (PPO minus signal line) as columns. Bright columns mark a spread still widening, faded ones a spread narrowing back."),
42370
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the zero line and the PPO line with a vertical gradient.")
41824
42371
  ],
41825
42372
  compute: (bars, inputs) => {
41826
42373
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
41827
- const fast = ema(src, num3(inputs, "fastLength", 12));
41828
- const slow = ema(src, num3(inputs, "slowLength", 26));
42374
+ const average = (kind2, v, len) => kind2 === "SMA" ? sma(v, len) : ema(v, len);
42375
+ const kind = str3(inputs, "maType", "EMA");
42376
+ const fast = average(kind, src, num3(inputs, "fastLength", 12));
42377
+ const slow = average(kind, src, num3(inputs, "slowLength", 26));
41829
42378
  const line = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : (f - s) / s * 100);
41830
- const signal = ema(line, num3(inputs, "signalLength", 9));
42379
+ const signal = average(str3(inputs, "signalType", "EMA"), line, num3(inputs, "signalLength", 9));
41831
42380
  const hist = sub(line, signal);
41832
- return {
41833
- plots: [
41834
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: histColors(hist), base: 0 },
41835
- { key: "ppo", title: "PPO", values: line, color: SERIES_LINE, width: 2 },
41836
- { key: "signal", title: "Signal", values: signal, color: WARNING }
41837
- ],
41838
- levels: ZERO_LEVEL
41839
- };
42381
+ const bull = str3(inputs, "bullColor", BULLISH);
42382
+ const bear = str3(inputs, "bearColor", BEARISH);
42383
+ const out = centeredOscillator({
42384
+ key: "ppo",
42385
+ title: "PPO Line",
42386
+ values: line,
42387
+ ink: { bull, bear, neutral: bear },
42388
+ zeroInk: transp(NEUTRAL, 90),
42389
+ fill: bool(inputs, "gradientFill", true),
42390
+ extraPlots: [{ key: "signal", title: "Signal Line", values: signal, color: str3(inputs, "signalColor", WARNING) }]
42391
+ });
42392
+ if (bool(inputs, "showHistogram", true)) {
42393
+ const colors = hist.map((x, i) => {
42394
+ if (!Number.isFinite(x)) return null;
42395
+ const prev3 = i > 0 && Number.isFinite(hist[i - 1]) ? hist[i - 1] : x;
42396
+ const ink = x >= 0 ? bull : bear;
42397
+ const expanding = x >= 0 ? x > prev3 : x < prev3;
42398
+ return transp(ink, expanding ? 40 : 70);
42399
+ });
42400
+ out.plots.unshift({ key: "hist", title: "Histogram", values: hist, kind: "columns", color: bull, colors, base: 0 });
42401
+ }
42402
+ return out;
41840
42403
  }
41841
42404
  };
41842
42405
  var awesome = {
@@ -41844,48 +42407,67 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41844
42407
  title: "Awesome Oscillator",
41845
42408
  shortTitle: "AO",
41846
42409
  overlay: false,
41847
- inputs: [],
41848
- compute: (bars) => {
42410
+ inputs: [
42411
+ colorInput(BULLISH, "Rising", "risingColor", "Histogram color when the oscillator is higher than on the previous bar."),
42412
+ colorInput(BEARISH, "Falling", "fallingColor", "Histogram color when the oscillator is lower than or equal to the previous bar."),
42413
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
42414
+ ],
42415
+ compute: (bars, inputs) => {
41849
42416
  const hl2 = sourceValues(bars, "HL2");
41850
42417
  const ao = sub(sma(hl2, 5), sma(hl2, 34));
42418
+ const rising = str3(inputs, "risingColor", BULLISH);
42419
+ const falling = str3(inputs, "fallingColor", BEARISH);
41851
42420
  const colors = ao.map((x, i) => {
41852
42421
  if (!Number.isFinite(x)) return null;
41853
42422
  const prev3 = i > 0 ? ao[i - 1] : Number.NaN;
41854
- return Number.isFinite(prev3) && x < prev3 ? BEARISH : BULLISH;
42423
+ return Number.isFinite(prev3) && x > prev3 ? rising : falling;
41855
42424
  });
41856
42425
  return {
41857
- plots: [{ key: "ao", title: "AO", values: ao, kind: "histogram", color: BULLISH, colors, base: 0 }],
41858
- levels: ZERO_LEVEL
42426
+ plots: [{ key: "ao", title: "AO", values: ao, kind: "columns", color: rising, colors, base: 0 }],
42427
+ levels: [{ key: "zero", price: 0, color: str3(inputs, "zeroColor", NEUTRAL), lineStyle: "dashed", title: "Zero Line" }]
41859
42428
  };
41860
42429
  }
41861
42430
  };
42431
+ var CCI_SCALING = 0.015;
41862
42432
  var cci = {
41863
42433
  type: "commodity-channel-index",
41864
42434
  title: "Commodity Channel Index",
41865
42435
  shortTitle: "CCI",
41866
42436
  overlay: false,
41867
- inputs: [lengthInput(20), sourceInput("HLC3"), colorInput()],
42437
+ inputs: [
42438
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used for the moving average and the mean absolute deviation of the source."),
42439
+ sourceInput("HLC3", "source", "Source", "Price series measured by the CCI; the typical price hlc3 is the classic choice."),
42440
+ floatInput("upper", "Upper Level", 100, -1e3, 1e3, 10, "Overbought reference level, +100 classically."),
42441
+ floatInput("lower", "Lower Level", -100, -1e3, 1e3, 10, "Oversold reference level, \u2212100 classically."),
42442
+ colorInput(NEUTRAL, "CCI Line", "color", "Color of the CCI line."),
42443
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the CCI reads above zero."),
42444
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the CCI reads below zero."),
42445
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the upper, lower and zero reference lines.")
42446
+ ],
41868
42447
  compute: (bars, inputs) => {
41869
42448
  const len = num3(inputs, "length", 20);
41870
42449
  const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
41871
42450
  const basis = sma(src, len);
41872
- const dev = new Array(src.length).fill(Number.NaN);
41873
- for (let i = len - 1; i < src.length; i++) {
41874
- const mean = basis[i];
41875
- if (!Number.isFinite(mean)) continue;
41876
- let d = 0;
41877
- for (let k = i - len + 1; k <= i; k++) d += Math.abs(src[k] - mean);
41878
- dev[i] = d / len;
41879
- }
42451
+ const dev = meanDev(src, len);
41880
42452
  const values = src.map((x, i) => {
41881
42453
  const b = basis[i];
41882
42454
  const d = dev[i];
41883
- return Number.isFinite(b) && Number.isFinite(d) && d !== 0 ? (x - b) / (0.015 * d) : Number.NaN;
42455
+ if (!Number.isFinite(b) || !Number.isFinite(d)) return Number.NaN;
42456
+ return d === 0 ? 0 : (x - b) / (CCI_SCALING * d);
42457
+ });
42458
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
42459
+ return centeredOscillator({
42460
+ key: "cci",
42461
+ title: "CCI",
42462
+ values,
42463
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
42464
+ zeroInk: levelInk,
42465
+ zeroLineStyle: "dotted",
42466
+ extraLevels: [
42467
+ { key: "upper", price: num3(inputs, "upper", 100), color: levelInk, lineStyle: "dashed", title: "Upper Level" },
42468
+ { key: "lower", price: num3(inputs, "lower", -100), color: levelInk, lineStyle: "dashed", title: "Lower Level" }
42469
+ ]
41884
42470
  });
41885
- return {
41886
- plots: [{ key: "cci", title: "CCI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41887
- levels: guideLevels(-100, 100, 0)
41888
- };
41889
42471
  }
41890
42472
  };
41891
42473
  var williamsR = {
@@ -41893,31 +42475,78 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41893
42475
  title: "Williams %R",
41894
42476
  shortTitle: "%R",
41895
42477
  overlay: false,
41896
- inputs: [lengthInput(14), colorInput()],
42478
+ inputs: [
42479
+ lengthInput(14, "length", "Length", 5e3, "Lookback length of the highest high / lowest low range. 14 is the classic setting."),
42480
+ sourceInput("Close", "source", "Source", "Series located within the lookback range. Close is the classic choice."),
42481
+ ...thresholdInputs(-20, -80, -100, 0),
42482
+ colorInput(NEUTRAL, "%R Line", "color", "Color of the %R line between the thresholds. Toward either level it blends into that level\u2019s color."),
42483
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
42484
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level.")
42485
+ ],
41897
42486
  compute: (bars, inputs) => {
41898
- const values = map(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "length", 14)), (x) => x - 100);
41899
- return {
41900
- plots: [{ key: "wr", title: "%R", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41901
- levels: guideLevels(-80, -20)
41902
- };
42487
+ const len = num3(inputs, "length", 14);
42488
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
42489
+ const hh = highest(highs(bars), len);
42490
+ const ll = lowest(lows(bars), len);
42491
+ const values = src.map((x, i) => {
42492
+ const h = hh[i];
42493
+ const l = ll[i];
42494
+ return Number.isFinite(h) && Number.isFinite(l) && h > l ? -100 * (h - x) / (h - l) : Number.NaN;
42495
+ });
42496
+ return boundedOscillator({
42497
+ key: "wr",
42498
+ title: "%R",
42499
+ values,
42500
+ scale: { top: 0, bottom: -100 },
42501
+ overbought: num3(inputs, "overbought", -20),
42502
+ oversold: num3(inputs, "oversold", -80),
42503
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
42504
+ levelInk: NEUTRAL
42505
+ });
41903
42506
  }
41904
42507
  };
41905
- var cmo = {
42508
+ var cmoSpec = {
41906
42509
  type: "chande-momentum-oscillator",
41907
42510
  title: "Chande Momentum Oscillator",
41908
42511
  shortTitle: "CMO",
41909
42512
  overlay: false,
41910
- inputs: [lengthInput(9), sourceInput(), colorInput()],
42513
+ inputs: [
42514
+ lengthInput(9, "length", "Length", 5e3, "Number of bars over which up moves and down moves are summed."),
42515
+ sourceInput("Close", "source", "Source", "Price series used to measure bar-to-bar changes."),
42516
+ floatInput("overbought", "Overbought Level", 50, 0, 100, 1, "Level above which the oscillator is considered overbought."),
42517
+ floatInput("oversold", "Oversold Level", -50, -100, 0, 1, "Level below which the oscillator is considered oversold."),
42518
+ colorInput(NEUTRAL, "CMO Line", "color", "Color of the CMO line."),
42519
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while momentum is positive, and shading of the oversold zone."),
42520
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while momentum is negative, and shading of the overbought zone."),
42521
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought, oversold, and zero lines.")
42522
+ ],
41911
42523
  compute: (bars, inputs) => {
41912
42524
  const len = num3(inputs, "length", 9);
41913
42525
  const d = change(sourceValues(bars, str3(inputs, "source", "Close")));
41914
- const su = sum(map(d, (x) => Math.max(x, 0)), len);
41915
- const sd = sum(map(d, (x) => Math.max(-x, 0)), len);
41916
- const values = zip(su, sd, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
41917
- return {
41918
- plots: [{ key: "cmo", title: "CMO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41919
- levels: guideLevels(-50, 50, 0)
41920
- };
42526
+ const up = sum(map(d, (x) => Math.max(x, 0)), len);
42527
+ const down = sum(map(d, (x) => Math.max(-x, 0)), len);
42528
+ const values = zip(up, down, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
42529
+ const bull = str3(inputs, "bullColor", BULLISH);
42530
+ const bear = str3(inputs, "bearColor", BEARISH);
42531
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
42532
+ const overbought = num3(inputs, "overbought", 50);
42533
+ const oversold = num3(inputs, "oversold", -50);
42534
+ const out = centeredOscillator({
42535
+ key: "cmo",
42536
+ title: "CMO",
42537
+ values,
42538
+ ink: { bull, bear, neutral: str3(inputs, "color", NEUTRAL) },
42539
+ zeroInk: levelInk,
42540
+ zeroLineStyle: "dotted",
42541
+ extraLevels: [
42542
+ { key: "overbought", price: overbought, color: levelInk, lineStyle: "dashed", title: "Overbought" },
42543
+ { key: "oversold", price: oversold, color: levelInk, lineStyle: "dashed", title: "Oversold" }
42544
+ ]
42545
+ });
42546
+ const zones = thresholdZones(values.length, { overbought, oversold, top: 100, bottom: -100 }, { overbought: bear, oversold: bull });
42547
+ out.plots.push(...zones.plots);
42548
+ out.bands = [...out.bands ?? [], ...zones.bands];
42549
+ return out;
41921
42550
  }
41922
42551
  };
41923
42552
  var connorsRsi = {
@@ -41926,10 +42555,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41926
42555
  shortTitle: "CRSI",
41927
42556
  overlay: false,
41928
42557
  inputs: [
41929
- lengthInput(3, "rsiLength", "RSI length"),
41930
- lengthInput(2, "streakLength", "Streak RSI length"),
41931
- lengthInput(100, "rankLength", "Percent-rank length"),
41932
- colorInput()
42558
+ lengthInput(3, "rsiLength", "RSI Length", 5e3, "Length of the Wilder RSI applied to the closing price."),
42559
+ lengthInput(2, "streakLength", "Streak RSI Length", 5e3, "Length of the Wilder RSI applied to the up/down close streak."),
42560
+ lengthInput(100, "rankLength", "Percent Rank Length", 5e3, "Lookback of the percent rank applied to the 1-bar rate of change of the closing price."),
42561
+ ...thresholdInputs(90, 10),
42562
+ colorInput(NEUTRAL, "CRSI Line", "color", "Color of the Connors RSI line."),
42563
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Line, fill and zone color toward and below the oversold level."),
42564
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Line, fill and zone color toward and above the overbought level."),
42565
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought and oversold levels.")
41933
42566
  ],
41934
42567
  compute: (bars, inputs) => {
41935
42568
  const c = closes(bars);
@@ -41947,10 +42580,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41947
42580
  const z = r[i];
41948
42581
  return Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) ? (x + y + z) / 3 : Number.NaN;
41949
42582
  });
41950
- return {
41951
- plots: [{ key: "crsi", title: "CRSI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41952
- levels: guideLevels(20, 80, 50)
41953
- };
42583
+ return boundedOscillator({
42584
+ key: "crsi",
42585
+ title: "Connors RSI",
42586
+ values,
42587
+ scale: { top: 100, bottom: 0 },
42588
+ overbought: num3(inputs, "overbought", 90),
42589
+ oversold: num3(inputs, "oversold", 10),
42590
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
42591
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
42592
+ midpointWash: true
42593
+ });
41954
42594
  }
41955
42595
  };
41956
42596
  var fisher = {
@@ -41958,7 +42598,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41958
42598
  title: "Fisher Transform",
41959
42599
  shortTitle: "Fisher",
41960
42600
  overlay: false,
41961
- inputs: [lengthInput(9)],
42601
+ inputs: [
42602
+ lengthInput(9, "length", "Length", 5e3, "Lookback period used to locate the median price hl2 within its highest-lowest range. Ehlers' original length is 10; 9 is the common platform default."),
42603
+ colorInput(NEUTRAL, "Fisher Line", "color", "Color of the Fisher line when it sits exactly on its trigger."),
42604
+ colorInput(NEUTRAL, "Trigger Line", "triggerColor", "Color of the trigger line, the Fisher value delayed one bar."),
42605
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the Fisher line and fill while it is above the trigger."),
42606
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the Fisher line and fill while it is below the trigger.")
42607
+ ],
41962
42608
  compute: (bars, inputs) => {
41963
42609
  const len = num3(inputs, "length", 9);
41964
42610
  const hl2 = sourceValues(bars, "HL2");
@@ -41966,23 +42612,39 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41966
42612
  const lo = lowest(hl2, len);
41967
42613
  const n = bars.length;
41968
42614
  const out = new Array(n).fill(Number.NaN);
42615
+ let position = 0;
41969
42616
  let value = 0;
41970
- let fish = 0;
41971
42617
  for (let i = 0; i < n; i++) {
41972
42618
  const h = hi[i];
41973
42619
  const l = lo[i];
41974
42620
  if (!Number.isFinite(h) || !Number.isFinite(l)) continue;
41975
- const norm = h === l ? 0 : (hl2[i] - l) / (h - l) - 0.5;
41976
- value = Math.max(-0.999, Math.min(0.999, 0.66 * norm + 0.67 * value));
41977
- fish = 0.5 * Math.log((1 + value) / (1 - value)) + 0.5 * fish;
41978
- out[i] = fish;
41979
- }
42621
+ const raw = h > l ? (hl2[i] - l) / (h - l) - 0.5 : 0;
42622
+ position = 0.66 * raw + 0.67 * position;
42623
+ position = position > 0.99 ? 0.999 : position < -0.99 ? -0.999 : position;
42624
+ value = 0.5 * Math.log((1 + position) / (1 - position)) + 0.5 * value;
42625
+ out[i] = value;
42626
+ }
42627
+ const trigger = shift3(out, 1);
42628
+ const neutral = str3(inputs, "color", NEUTRAL);
42629
+ const bull = str3(inputs, "bullColor", BULLISH);
42630
+ const bear = str3(inputs, "bearColor", BEARISH);
42631
+ const inkOf = (i) => {
42632
+ const f = out[i];
42633
+ const t = trigger[i];
42634
+ if (!Number.isFinite(t)) return neutral;
42635
+ return f > t ? bull : f < t ? bear : neutral;
42636
+ };
41980
42637
  return {
41981
42638
  plots: [
41982
- { key: "fisher", title: "Fisher", values: out, color: SERIES_LINE, width: 2 },
41983
- { key: "trigger", title: "Trigger", values: shift3(out, 1), color: WARNING }
42639
+ { key: "fisher", title: "Fisher", values: out, color: neutral, width: 2, colors: out.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) },
42640
+ { key: "trigger", title: "Trigger", values: trigger, color: str3(inputs, "triggerColor", NEUTRAL) }
41984
42641
  ],
41985
- levels: ZERO_LEVEL
42642
+ bands: [{ key: "spread", from: "fisher", to: "trigger", color: transp(neutral, 80), colors: out.map((x, i) => Number.isFinite(x) ? transp(inkOf(i), 80) : null) }],
42643
+ levels: [
42644
+ { key: "upper", price: 1.5, color: NEUTRAL, lineStyle: "dashed", title: "Upper Level" },
42645
+ { key: "lower", price: -1.5, color: NEUTRAL, lineStyle: "dashed", title: "Lower Level" },
42646
+ { key: "zero", price: 0, color: NEUTRAL, lineStyle: "dotted", title: "Zero Line" }
42647
+ ]
41986
42648
  };
41987
42649
  }
41988
42650
  };
@@ -41990,93 +42652,180 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41990
42652
  type: "trix",
41991
42653
  title: "TRIX",
41992
42654
  overlay: false,
41993
- inputs: [lengthInput(18), colorInput()],
42655
+ inputs: [
42656
+ lengthInput(18, "length", "Length", 5e3, "Length of each of the three EMA smoothing passes applied to the close price."),
42657
+ lengthInput(9, "signalLength", "Signal Length", 500, "Length of the EMA applied to TRIX to form the signal line."),
42658
+ colorInput(NEUTRAL, "TRIX Color", "color", "Color of the TRIX line."),
42659
+ colorInput(WARNING, "Signal Color", "signalColor", "Color of the signal line."),
42660
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the TRIX line and its gradient fill while TRIX holds above zero."),
42661
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the TRIX line and its gradient fill while TRIX holds below zero.")
42662
+ ],
41994
42663
  compute: (bars, inputs) => {
41995
42664
  const len = num3(inputs, "length", 18);
41996
- const logClose = map(closes(bars), Math.log);
41997
- const smooth = ema(ema(ema(logClose, len), len), len);
41998
- const values = map(change(smooth), (x) => x * 1e4);
41999
- return {
42000
- plots: [{ key: "trix", title: "TRIX", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42001
- levels: ZERO_LEVEL
42002
- };
42665
+ const smooth = ema(ema(ema(closes(bars), len), len), len);
42666
+ const values = roc(smooth, 1);
42667
+ return centeredOscillator({
42668
+ key: "trix",
42669
+ title: "TRIX",
42670
+ values,
42671
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
42672
+ zeroInk: NEUTRAL,
42673
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(values, num3(inputs, "signalLength", 9)), color: str3(inputs, "signalColor", WARNING) }]
42674
+ });
42003
42675
  }
42004
42676
  };
42005
- function tsiLine(src, long, short) {
42677
+ function tsiRatio(src, long, short) {
42006
42678
  const mom = change(src);
42007
42679
  const numer = ema(ema(mom, long), short);
42008
42680
  const denom = ema(ema(map(mom, Math.abs), long), short);
42009
- return zip(numer, denom, (a, b) => b === 0 ? 0 : 100 * a / b);
42681
+ return zip(numer, denom, (a, b) => b === 0 ? 0 : a / b);
42010
42682
  }
42011
42683
  var tsi = {
42012
42684
  type: "true-strength-index",
42013
42685
  title: "True Strength Index",
42014
42686
  shortTitle: "TSI",
42015
42687
  overlay: false,
42016
- inputs: [lengthInput(25, "longLength", "Long length"), lengthInput(13, "shortLength", "Short length"), lengthInput(13, "signalLength", "Signal length")],
42688
+ inputs: [
42689
+ lengthInput(25, "longLength", "Long Length", 5e3, "Length of the first (long) EMA applied to the 1-bar price change."),
42690
+ lengthInput(13, "shortLength", "Short Length", 5e3, "Length of the second (short) EMA applied to the long-smoothed price change."),
42691
+ lengthInput(13, "signalLength", "Signal Length", 500, "Length of the EMA of TSI used as the signal line."),
42692
+ colorInput(NEUTRAL, "TSI", "color", "Color of the TSI line."),
42693
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line."),
42694
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the TSI line and its gradient fill while TSI holds above zero."),
42695
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the TSI line and its gradient fill while TSI holds below zero.")
42696
+ ],
42017
42697
  compute: (bars, inputs) => {
42018
- const line = tsiLine(closes(bars), num3(inputs, "longLength", 25), num3(inputs, "shortLength", 13));
42019
- return {
42020
- plots: [
42021
- { key: "tsi", title: "TSI", values: line, color: SERIES_LINE, width: 2 },
42022
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: WARNING }
42023
- ],
42024
- levels: ZERO_LEVEL
42025
- };
42698
+ const values = map(tsiRatio(closes(bars), num3(inputs, "longLength", 25), num3(inputs, "shortLength", 13)), (x) => 100 * x);
42699
+ return centeredOscillator({
42700
+ key: "tsi",
42701
+ title: "TSI",
42702
+ values,
42703
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
42704
+ zeroInk: NEUTRAL,
42705
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(values, num3(inputs, "signalLength", 13)), color: str3(inputs, "signalColor", WARNING) }]
42706
+ });
42026
42707
  }
42027
42708
  };
42709
+ var SMI_INDICATOR = "Indicator";
42710
+ var SMI_OSCILLATOR = "Oscillator";
42028
42711
  var smiErgodic = {
42029
42712
  type: "smi-ergodic",
42030
42713
  title: "SMI Ergodic Oscillator",
42031
42714
  shortTitle: "SMIE",
42032
42715
  overlay: false,
42033
- inputs: [lengthInput(5, "shortLength", "Short length"), lengthInput(20, "longLength", "Long length"), lengthInput(5, "signalLength", "Signal length")],
42716
+ inputs: [
42717
+ lengthInput(20, "longLength", "Long Length", 5e3, "Length of the first EMA smoothing applied to the momentum of the closing price."),
42718
+ lengthInput(5, "shortLength", "Short Length", 5e3, "Length of the second EMA smoothing applied to the momentum of the closing price."),
42719
+ lengthInput(5, "signalLength", "Signal Length", 500, "Length of the EMA of the SMI that forms the signal line."),
42720
+ optionInput("display", "Display", SMI_INDICATOR, [SMI_INDICATOR, SMI_OSCILLATOR], "Indicator plots the SMI and its signal line. Oscillator plots the difference between them as a histogram."),
42721
+ colorInput(NEUTRAL, "SMI", "color", "Color of the SMI line while it sits exactly on its signal line."),
42722
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line."),
42723
+ colorInput(BULLISH, "Oscillator Grow", "growColor", "Color of the rising oscillator histogram, and of the SMI line while it leads its signal."),
42724
+ colorInput(BEARISH, "Oscillator Fall", "fallColor", "Color of the falling oscillator histogram, and of the SMI line while it lags its signal.")
42725
+ ],
42034
42726
  compute: (bars, inputs) => {
42035
- const line = tsiLine(closes(bars), num3(inputs, "longLength", 20), num3(inputs, "shortLength", 5));
42036
- const signal = ema(line, num3(inputs, "signalLength", 5));
42037
- const osc = sub(line, signal);
42727
+ const smi = tsiRatio(closes(bars), num3(inputs, "longLength", 20), num3(inputs, "shortLength", 5));
42728
+ const signal = ema(smi, num3(inputs, "signalLength", 5));
42729
+ const grow = str3(inputs, "growColor", BULLISH);
42730
+ const fall = str3(inputs, "fallColor", BEARISH);
42731
+ const zero = [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed", title: "Zero" }];
42732
+ if (str3(inputs, "display", SMI_INDICATOR) === SMI_OSCILLATOR) {
42733
+ const osc = sub(smi, signal);
42734
+ const colors = osc.map((x, i) => {
42735
+ if (!Number.isFinite(x)) return null;
42736
+ const prev3 = i > 0 && Number.isFinite(osc[i - 1]) ? osc[i - 1] : x;
42737
+ return x > prev3 ? grow : fall;
42738
+ });
42739
+ return { plots: [{ key: "osc", title: "Oscillator", values: osc, kind: "columns", color: grow, colors, base: 0 }], levels: zero };
42740
+ }
42741
+ const neutral = str3(inputs, "color", NEUTRAL);
42742
+ const inkOf = (i) => {
42743
+ const a = smi[i];
42744
+ const b = signal[i];
42745
+ if (!Number.isFinite(b)) return neutral;
42746
+ return a > b ? grow : a < b ? fall : neutral;
42747
+ };
42038
42748
  return {
42039
42749
  plots: [
42040
- { key: "osc", title: "Oscillator", values: osc, kind: "histogram", color: BULLISH, colors: histColors(osc), base: 0 },
42041
- { key: "smi", title: "SMI", values: line, color: SERIES_LINE, width: 2 },
42042
- { key: "signal", title: "Signal", values: signal, color: WARNING }
42750
+ { key: "smi", title: "SMI", values: smi, color: neutral, colors: smi.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) },
42751
+ { key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) }
42043
42752
  ],
42044
- levels: ZERO_LEVEL
42753
+ bands: [{ key: "spread", from: "smi", to: "signal", color: transp(neutral, 75), colors: smi.map((x, i) => Number.isFinite(x) ? transp(inkOf(i), 75) : null) }],
42754
+ levels: zero
42045
42755
  };
42046
42756
  }
42047
42757
  };
42758
+ var STC_MID = 50;
42759
+ var TRIGGERS = "Trigger Levels";
42048
42760
  var stc = {
42049
42761
  type: "schaff-trend-cycle",
42050
42762
  title: "Schaff Trend Cycle",
42051
42763
  shortTitle: "Schaff Trend Cycle",
42052
42764
  overlay: false,
42053
- inputs: [lengthInput(10, "cycleLength", "Cycle length"), lengthInput(23, "fastLength", "Fast length"), lengthInput(50, "slowLength", "Slow length")],
42765
+ inputs: [
42766
+ sourceInput("Close", "source", "Source", "Price series used to build the MACD that the trend cycle is measured on."),
42767
+ lengthInput(23, "fastLength", "Fast Length", 5e3, "Fast EMA length of the underlying MACD."),
42768
+ lengthInput(50, "slowLength", "Slow Length", 5e3, "Slow EMA length of the underlying MACD."),
42769
+ { ...lengthInput(10, "cycleLength", "Cycle Length", 5e3, "Window of the two stochastic passes. Each pass measures where its input sits inside the range of its own values over this many bars."), min: 2 },
42770
+ floatInput("factor", "Smoothing Factor", 0.5, 0.01, 1, 0.05, "%D style recursive smoothing applied after each stochastic pass. 0.5 is the published default."),
42771
+ { ...floatInput("upper", "Upper Trigger", 75, 0, 100, 1, "Upper trigger line. A cross down through it flags an emerging downtrend."), group: TRIGGERS },
42772
+ { ...floatInput("lower", "Lower Trigger", 25, 0, 100, 1, "Lower trigger line. A cross up through it flags an emerging uptrend."), group: TRIGGERS },
42773
+ colorInput(BULLISH, "Bullish Color", "bullColor", "Color of the trend cycle line and gradient while the cycle holds the upper half of its 0-100 scale."),
42774
+ colorInput(BEARISH, "Bearish Color", "bearColor", "Color of the trend cycle line and gradient while the cycle holds the lower half of its 0-100 scale."),
42775
+ colorInput(NEUTRAL, "Levels Color", "levelsColor", "Color of the dashed trigger lines and the dotted midline."),
42776
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Vertical gradient between the midline and the trend cycle, fading out toward the midline.")
42777
+ ],
42054
42778
  compute: (bars, inputs) => {
42055
42779
  const len = num3(inputs, "cycleLength", 10);
42056
- const src = closes(bars);
42780
+ const factor = num3(inputs, "factor", 0.5);
42781
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
42057
42782
  const macdLine = sub(ema(src, num3(inputs, "fastLength", 23)), ema(src, num3(inputs, "slowLength", 50)));
42058
- const stochOf = (v) => {
42783
+ const stochNorm = (v) => {
42059
42784
  const lo = lowest(v, len);
42060
42785
  const hi = highest(v, len);
42061
42786
  const out = new Array(v.length).fill(Number.NaN);
42062
- let prevRaw = Number.NaN;
42063
- let smoothed = Number.NaN;
42787
+ let pct = Number.NaN;
42064
42788
  for (let i = 0; i < v.length; i++) {
42065
42789
  const x = v[i];
42066
42790
  const l = lo[i];
42067
42791
  const h = hi[i];
42068
42792
  if (!Number.isFinite(x) || !Number.isFinite(l) || !Number.isFinite(h)) continue;
42069
- const raw = h > l ? (x - l) / (h - l) * 100 : Number.isFinite(prevRaw) ? prevRaw : 50;
42070
- prevRaw = raw;
42071
- smoothed = Number.isFinite(smoothed) ? smoothed + 0.5 * (raw - smoothed) : raw;
42793
+ if (h > l) pct = 100 * (x - l) / (h - l);
42794
+ out[i] = pct;
42795
+ }
42796
+ return out;
42797
+ };
42798
+ const smoothStep = (v) => {
42799
+ const out = new Array(v.length).fill(Number.NaN);
42800
+ let smoothed = Number.NaN;
42801
+ for (let i = 0; i < v.length; i++) {
42802
+ const x = v[i];
42803
+ if (!Number.isFinite(x)) continue;
42804
+ smoothed = Number.isFinite(smoothed) ? smoothed + factor * (x - smoothed) : x;
42072
42805
  out[i] = smoothed;
42073
42806
  }
42074
42807
  return out;
42075
42808
  };
42076
- const values = stochOf(stochOf(macdLine));
42809
+ const values = smoothStep(stochNorm(smoothStep(stochNorm(macdLine))));
42810
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
42811
+ const bull = str3(inputs, "bullColor", BULLISH);
42812
+ const bear = str3(inputs, "bearColor", BEARISH);
42813
+ const plots = [
42814
+ { key: "stc", title: "Schaff Trend Cycle", values, color: bear, width: 2, colors: values.map((x) => Number.isFinite(x) ? x > STC_MID ? bull : bear : null) }
42815
+ ];
42816
+ const bands = [];
42817
+ if (bool(inputs, "gradientFill", true)) {
42818
+ plots.push(anchorPlot("midAnchor", "Midline Anchor", values.length, STC_MID));
42819
+ bands.push({ key: "wash", from: "stc", to: "midAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, STC_MID, (x) => x > STC_MID ? bull : bear) });
42820
+ }
42077
42821
  return {
42078
- plots: [{ key: "stc", title: "STC", values, color: SERIES_LINE, width: 2 }],
42079
- levels: guideLevels(25, 75)
42822
+ plots,
42823
+ bands,
42824
+ levels: [
42825
+ { key: "upper", price: num3(inputs, "upper", 75), color: transp(levelInk, 25), lineStyle: "dashed", title: "Upper Trigger" },
42826
+ { key: "lower", price: num3(inputs, "lower", 25), color: transp(levelInk, 25), lineStyle: "dashed", title: "Lower Trigger" },
42827
+ { key: "midline", price: STC_MID, color: transp(levelInk, 60), lineStyle: "dotted", title: "Midline" }
42828
+ ]
42080
42829
  };
42081
42830
  }
42082
42831
  };
@@ -42085,22 +42834,37 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42085
42834
  title: "Know Sure Thing",
42086
42835
  shortTitle: "KST",
42087
42836
  overlay: false,
42088
- inputs: [sourceInput(), lengthInput(9, "signalLength", "Signal length")],
42837
+ inputs: [
42838
+ lengthInput(10, "rocLength1", "ROC Length #1", 5e3, "Lookback of the first (shortest) rate of change."),
42839
+ lengthInput(15, "rocLength2", "ROC Length #2", 5e3, "Lookback of the second rate of change."),
42840
+ lengthInput(20, "rocLength3", "ROC Length #3", 5e3, "Lookback of the third rate of change."),
42841
+ lengthInput(30, "rocLength4", "ROC Length #4", 5e3, "Lookback of the fourth (longest) rate of change."),
42842
+ lengthInput(10, "smaLength1", "SMA Length #1", 5e3, "Smoothing length applied to the first rate of change (RCMA #1)."),
42843
+ lengthInput(10, "smaLength2", "SMA Length #2", 5e3, "Smoothing length applied to the second rate of change (RCMA #2)."),
42844
+ lengthInput(10, "smaLength3", "SMA Length #3", 5e3, "Smoothing length applied to the third rate of change (RCMA #3)."),
42845
+ lengthInput(15, "smaLength4", "SMA Length #4", 5e3, "Smoothing length applied to the fourth rate of change (RCMA #4)."),
42846
+ lengthInput(9, "signalLength", "Signal Length", 500, "Simple moving average length of the signal line."),
42847
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the KST line while above zero."),
42848
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the KST line while below zero."),
42849
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line.")
42850
+ ],
42089
42851
  compute: (bars, inputs) => {
42090
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
42091
- const line = [
42092
- sma(roc(src, 10), 10),
42093
- sma(roc(src, 15), 10),
42094
- sma(roc(src, 20), 10),
42095
- sma(roc(src, 30), 15)
42096
- ].reduce((acc, r, i) => zip(acc, r, (a, b) => a + b * (i + 1)), new Array(src.length).fill(0));
42097
- return {
42098
- plots: [
42099
- { key: "kst", title: "KST", values: line, color: SERIES_LINE, width: 2 },
42100
- { key: "signal", title: "Signal", values: sma(line, num3(inputs, "signalLength", 9)), color: WARNING }
42101
- ],
42102
- levels: ZERO_LEVEL
42103
- };
42852
+ const c = closes(bars);
42853
+ const rcma = (rocKey, rocDefault, smaKey, smaDefault) => sma(roc(c, num3(inputs, rocKey, rocDefault)), num3(inputs, smaKey, smaDefault));
42854
+ const values = [rcma("rocLength1", 10, "smaLength1", 10), rcma("rocLength2", 15, "smaLength2", 10), rcma("rocLength3", 20, "smaLength3", 10), rcma("rocLength4", 30, "smaLength4", 15)].reduce(
42855
+ (acc, r, i) => zip(acc, r, (a, b) => a + b * (i + 1)),
42856
+ new Array(c.length).fill(0)
42857
+ );
42858
+ const bull = str3(inputs, "bullColor", BULLISH);
42859
+ return centeredOscillator({
42860
+ key: "kst",
42861
+ title: "KST",
42862
+ values,
42863
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
42864
+ zeroInk: NEUTRAL,
42865
+ valueTransparency: 60,
42866
+ extraPlots: [{ key: "signal", title: "Signal", values: sma(values, num3(inputs, "signalLength", 9)), color: str3(inputs, "signalColor", WARNING) }]
42867
+ });
42104
42868
  }
42105
42869
  };
42106
42870
  var coppock = {
@@ -42109,19 +42873,25 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42109
42873
  shortTitle: "Coppock",
42110
42874
  overlay: false,
42111
42875
  inputs: [
42112
- lengthInput(10, "wmaLength", "WMA length"),
42113
- lengthInput(14, "longRoc", "Long RoC length"),
42114
- lengthInput(11, "shortRoc", "Short RoC length"),
42115
- sourceInput(),
42116
- colorInput()
42876
+ lengthInput(10, "wmaLength", "WMA Length", 5e3, "Length of the weighted moving average applied to the sum of the two rates of change."),
42877
+ lengthInput(14, "longRoc", "Long RoC Length", 5e3, "Lookback of the long rate of change, in bars. The original definition uses 14 monthly closes."),
42878
+ lengthInput(11, "shortRoc", "Short RoC Length", 5e3, "Lookback of the short rate of change, in bars. The original definition uses 11 monthly closes."),
42879
+ sourceInput("Close", "source", "Source", "Price series used for the rate of change calculations."),
42880
+ colorInput(NEUTRAL, "Coppock Curve", "color", "Color of the Coppock Curve line."),
42881
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the curve reads above zero."),
42882
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the curve reads below zero."),
42883
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
42117
42884
  ],
42118
42885
  compute: (bars, inputs) => {
42119
42886
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
42120
42887
  const values = wma(zip(roc(src, num3(inputs, "longRoc", 14)), roc(src, num3(inputs, "shortRoc", 11)), (a, b) => a + b), num3(inputs, "wmaLength", 10));
42121
- return {
42122
- plots: [{ key: "coppock", title: "Coppock", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42123
- levels: ZERO_LEVEL
42124
- };
42888
+ return centeredOscillator({
42889
+ key: "coppock",
42890
+ title: "Coppock Curve",
42891
+ values,
42892
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
42893
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
42894
+ });
42125
42895
  }
42126
42896
  };
42127
42897
  var dpo = {
@@ -42129,42 +42899,76 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42129
42899
  title: "Detrended Price Oscillator",
42130
42900
  shortTitle: "DPO",
42131
42901
  overlay: false,
42132
- inputs: [lengthInput(21), colorInput()],
42902
+ inputs: [
42903
+ lengthInput(21, "length", "Length", 5e3, "Lookback of the simple moving average that price is detrended against. The comparison is displaced by Length / 2 + 1 bars."),
42904
+ sourceInput("Close", "source", "Source", "Price series used in the detrending calculation."),
42905
+ boolInput("centered", "Centered", false, void 0, "Shifts the oscillator back by Length / 2 + 1 bars so it is centered on the prices it detrends. The most recent bars then show no value."),
42906
+ colorInput(NEUTRAL, "DPO", "color", "Color of the line when it sits exactly at zero."),
42907
+ colorInput(BULLISH, "Above Zero", "bullColor", "Color of the line and fill while the oscillator is above zero."),
42908
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while the oscillator is below zero."),
42909
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
42910
+ ],
42133
42911
  compute: (bars, inputs) => {
42134
42912
  const len = num3(inputs, "length", 21);
42135
- const barsback = Math.floor(len / 2) + 1;
42136
- const values = sub(closes(bars), shift3(sma(closes(bars), len), barsback));
42137
- return {
42138
- plots: [{ key: "dpo", title: "DPO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42139
- levels: ZERO_LEVEL
42140
- };
42913
+ const barsBack = Math.trunc(len / 2) + 1;
42914
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
42915
+ const basis = sma(src, len);
42916
+ const centered = bool(inputs, "centered", false);
42917
+ const values = centered ? shift3(sub(shift3(src, barsBack), basis), -barsBack) : sub(src, shift3(basis, barsBack));
42918
+ return centeredOscillator({
42919
+ key: "dpo",
42920
+ title: "DPO",
42921
+ values,
42922
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
42923
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
42924
+ });
42141
42925
  }
42142
42926
  };
42927
+ var ROC_PERCENT = "ROC %";
42928
+ var ROC_MOMENTUM = "Momentum";
42143
42929
  var rocSpec = {
42144
42930
  type: "rate-of-change",
42145
42931
  title: "Rate of Change",
42146
42932
  shortTitle: "ROC",
42147
42933
  overlay: false,
42148
- inputs: [lengthInput(9), sourceInput(), colorInput()],
42149
- compute: (bars, inputs) => ({
42150
- plots: [
42151
- {
42152
- key: "roc",
42153
- title: "RoC",
42154
- values: roc(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 9)),
42155
- color: str3(inputs, "color", SERIES_LINE),
42156
- width: 2
42157
- }
42158
- ],
42159
- levels: ZERO_LEVEL
42160
- })
42934
+ inputs: [
42935
+ lengthInput(9, "length", "Length", 5e3, "Lookback period; the source is compared with its value this many bars ago."),
42936
+ sourceInput("Close", "source", "Source", "Series used as input for the calculation."),
42937
+ optionInput("mode", "Mode", ROC_PERCENT, [ROC_PERCENT, ROC_MOMENTUM], "ROC % plots the change as a percentage of the source value from the lookback; Momentum plots the raw difference."),
42938
+ colorInput(NEUTRAL, "Line", "color", "Color of the line when the reading sits exactly at zero."),
42939
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the line and fill while the reading is above zero."),
42940
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line and fill while the reading is below zero."),
42941
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the line and zero, following the line color.")
42942
+ ],
42943
+ compute: (bars, inputs) => {
42944
+ const len = num3(inputs, "length", 9);
42945
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
42946
+ const values = str3(inputs, "mode", ROC_PERCENT) === ROC_MOMENTUM ? change(src, len) : roc(src, len);
42947
+ return centeredOscillator({
42948
+ key: "roc",
42949
+ title: "ROC",
42950
+ values,
42951
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
42952
+ zeroInk: NEUTRAL,
42953
+ fill: bool(inputs, "gradientFill", true)
42954
+ });
42955
+ }
42161
42956
  };
42162
42957
  var ultimate = {
42163
42958
  type: "ultimate-oscillator",
42164
42959
  title: "Ultimate Oscillator",
42165
42960
  shortTitle: "UO",
42166
42961
  overlay: false,
42167
- inputs: [lengthInput(7, "fastLength", "Fast length"), lengthInput(14, "middleLength", "Middle length"), lengthInput(28, "slowLength", "Slow length"), colorInput()],
42962
+ inputs: [
42963
+ lengthInput(7, "fastLength", "Fast Length", 5e3, "Bars in the fastest averaging window. Weighted 4 in the final blend, so it dominates the oscillator."),
42964
+ lengthInput(14, "middleLength", "Middle Length", 5e3, "Bars in the intermediate averaging window. Weighted 2 in the final blend."),
42965
+ lengthInput(28, "slowLength", "Slow Length", 5e3, "Bars in the slowest averaging window. Weighted 1 in the final blend."),
42966
+ ...thresholdInputs(70, 30, 0, 100, "Overbought", "Oversold"),
42967
+ colorInput(NEUTRAL, "Oscillator", "color", "Color of the line midway between the levels. Toward either level it blends into that level\u2019s color."),
42968
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought and oversold levels."),
42969
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line at the oversold level and of the oversold zone shading."),
42970
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line at the overbought level and of the overbought zone shading.")
42971
+ ],
42168
42972
  compute: (bars, inputs) => {
42169
42973
  const n = bars.length;
42170
42974
  const bp = new Array(n).fill(Number.NaN);
@@ -42172,10 +42976,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42172
42976
  for (let i = 0; i < n; i++) {
42173
42977
  const b = bars[i];
42174
42978
  const pc = i > 0 ? bars[i - 1].close : b.close;
42175
- const lo = Math.min(b.low, pc);
42176
- const hi = Math.max(b.high, pc);
42177
- bp[i] = b.close - lo;
42178
- tr[i] = hi - lo;
42979
+ const trueLow = Math.min(b.low, pc);
42980
+ bp[i] = b.close - trueLow;
42981
+ tr[i] = Math.max(b.high, pc) - trueLow;
42179
42982
  }
42180
42983
  const avg = (len) => zip(sum(bp, len), sum(tr, len), (a, b) => b === 0 ? Number.NaN : a / b);
42181
42984
  const a7 = avg(num3(inputs, "fastLength", 7));
@@ -42186,10 +42989,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42186
42989
  const z = a28[i];
42187
42990
  return Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) ? 100 * (4 * x + 2 * y + z) / 7 : Number.NaN;
42188
42991
  });
42189
- return {
42190
- plots: [{ key: "uo", title: "UO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42191
- levels: guideLevels(30, 70, 50)
42192
- };
42992
+ return boundedOscillator({
42993
+ key: "uo",
42994
+ title: "UO",
42995
+ values,
42996
+ scale: { top: 100, bottom: 0 },
42997
+ overbought: num3(inputs, "overbought", 70),
42998
+ oversold: num3(inputs, "oversold", 30),
42999
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
43000
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
43001
+ zoneTransparency: 91
43002
+ });
42193
43003
  }
42194
43004
  };
42195
43005
  var rvgi = {
@@ -42197,7 +43007,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42197
43007
  title: "Relative Vigor Index",
42198
43008
  shortTitle: "RVGI",
42199
43009
  overlay: false,
42200
- inputs: [lengthInput(10)],
43010
+ inputs: [
43011
+ lengthInput(10, "length", "Length", 5e3, "Number of bars summed for the smoothed numerator (close \u2212 open) and denominator (high \u2212 low)."),
43012
+ colorInput(BULLISH, "RVGI", "color", "Color of the RVGI line."),
43013
+ colorInput(BEARISH, "Signal", "signalColor", "Color of the signal line.")
43014
+ ],
42201
43015
  compute: (bars, inputs) => {
42202
43016
  const len = num3(inputs, "length", 10);
42203
43017
  const co = swma(sub(closes(bars), opens(bars)));
@@ -42205,31 +43019,49 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42205
43019
  const line = zip(sum(co, len), sum(hl, len), (a, b) => b === 0 ? 0 : a / b);
42206
43020
  return {
42207
43021
  plots: [
42208
- { key: "rvgi", title: "RVGI", values: line, color: SERIES_LINE, width: 2 },
42209
- { key: "signal", title: "Signal", values: swma(line), color: WARNING }
43022
+ { key: "rvgi", title: "RVGI", values: line, color: str3(inputs, "color", BULLISH) },
43023
+ { key: "signal", title: "Signal", values: swma(line), color: str3(inputs, "signalColor", BEARISH) }
42210
43024
  ],
42211
- levels: ZERO_LEVEL
43025
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed", title: "Zero" }]
42212
43026
  };
42213
43027
  }
42214
43028
  };
43029
+ var RVI_EMA = "EMA";
43030
+ var RVI_RMA = "Wilder (RMA)";
42215
43031
  var relVolatility = {
42216
43032
  type: "relative-volatility-index",
42217
43033
  title: "Relative Volatility Index",
42218
43034
  shortTitle: "RVI",
42219
43035
  overlay: false,
42220
- inputs: [lengthInput(10, "stdevLength", "StdDev length"), lengthInput(14, "smoothLength", "Smoothing length"), sourceInput(), colorInput()],
43036
+ inputs: [
43037
+ lengthInput(10, "stdevLength", "Stdev Length", 5e3, "Number of bars used for the standard deviation of closing prices that serves as each bar's volatility reading."),
43038
+ lengthInput(14, "smoothLength", "Smoothing Length", 5e3, "Number of bars used to smooth the up and down volatility streams."),
43039
+ optionInput("smoothType", "Smoothing Type", RVI_EMA, [RVI_EMA, RVI_RMA], "Moving average applied to the volatility streams. EMA is Dorsey's classic calculation; Wilder (RMA) matches platforms that use Wilder smoothing."),
43040
+ ...thresholdInputs(80, 20),
43041
+ colorInput(NEUTRAL, "RVI Line", "color", "Color of the RVI line at the midline. Toward either threshold it blends into that threshold\u2019s color."),
43042
+ colorInput(BULLISH, "Up Volatility", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
43043
+ colorInput(BEARISH, "Down Volatility", "oversoldColor", "Color of the line and zone shading at and below the oversold level.")
43044
+ ],
42221
43045
  compute: (bars, inputs) => {
43046
+ const c = closes(bars);
43047
+ const sigma = stdev(c, num3(inputs, "stdevLength", 10));
43048
+ const d = change(c);
43049
+ const up = zip(sigma, d, (s, x) => x > 0 ? s : 0);
43050
+ const down = zip(sigma, d, (s, x) => x < 0 ? s : 0);
42222
43051
  const smoothLen = num3(inputs, "smoothLength", 14);
42223
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
42224
- const dev = stdev(src, num3(inputs, "stdevLength", 10));
42225
- const d = change(src);
42226
- const up = rma(zip(dev, d, (s, x) => x > 0 ? s : 0), smoothLen);
42227
- const dn = rma(zip(dev, d, (s, x) => x <= 0 ? s : 0), smoothLen);
42228
- const values = zip(up, dn, (u, w) => u + w === 0 ? 50 : 100 * u / (u + w));
42229
- return {
42230
- plots: [{ key: "rvi", title: "RVI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42231
- levels: guideLevels(20, 80, 50)
42232
- };
43052
+ const smooth = str3(inputs, "smoothType", RVI_EMA) === RVI_RMA ? rma : ema;
43053
+ const values = zip(smooth(up, smoothLen), smooth(down, smoothLen), (u, w) => u + w === 0 ? 0 : 100 * u / (u + w));
43054
+ return boundedOscillator({
43055
+ key: "rvi",
43056
+ title: "RVI",
43057
+ values,
43058
+ scale: { top: 100, bottom: 0 },
43059
+ overbought: num3(inputs, "overbought", 80),
43060
+ oversold: num3(inputs, "oversold", 20),
43061
+ ink: { low: str3(inputs, "oversoldColor", BEARISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BULLISH) },
43062
+ levelInk: NEUTRAL,
43063
+ width: 2
43064
+ });
42233
43065
  }
42234
43066
  };
42235
43067
  var bop = {
@@ -42237,28 +43069,43 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42237
43069
  title: "Balance of Power",
42238
43070
  shortTitle: "BOP",
42239
43071
  overlay: false,
42240
- inputs: [colorInput()],
43072
+ inputs: [
43073
+ boolInput("smoothing", "Smoothing", false, void 0, "Smooth the raw Balance of Power with a simple moving average instead of plotting the single-bar value."),
43074
+ lengthInput(14, "smoothLength", "Length", 5e3, "Simple moving average length applied when smoothing is enabled."),
43075
+ colorInput(NEUTRAL, "BOP Color", "color", "Color of the line at exactly zero."),
43076
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the Balance of Power is above zero \u2014 buyers in control of the bar."),
43077
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the Balance of Power is below zero \u2014 sellers in control of the bar.")
43078
+ ],
42241
43079
  compute: (bars, inputs) => {
42242
- const values = bars.map((b) => b.high === b.low ? 0 : (b.close - b.open) / (b.high - b.low));
42243
- return {
42244
- plots: [{ key: "bop", title: "BOP", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42245
- levels: ZERO_LEVEL
42246
- };
43080
+ const raw = bars.map((b) => b.high === b.low ? 0 : (b.close - b.open) / (b.high - b.low));
43081
+ const values = bool(inputs, "smoothing", false) ? sma(raw, num3(inputs, "smoothLength", 14)) : raw;
43082
+ return centeredOscillator({
43083
+ key: "bop",
43084
+ title: "BOP",
43085
+ values,
43086
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
43087
+ zeroInk: NEUTRAL,
43088
+ valueTransparency: 60
43089
+ });
42247
43090
  }
42248
43091
  };
42249
43092
  var elderRay = {
42250
43093
  type: "elder-ray",
42251
43094
  title: "Elder Ray",
42252
43095
  overlay: false,
42253
- inputs: [lengthInput(13)],
43096
+ inputs: [
43097
+ lengthInput(13, "length", "EMA Length", 5e3, "Number of bars in the exponential moving average of closing prices used as the reference level for Bull Power and Bear Power. Alexander Elder's classic setting is 13."),
43098
+ colorInput(BULLISH, "Bull Power", "bullColor", "Color of the Bull Power columns."),
43099
+ colorInput(BEARISH, "Bear Power", "bearColor", "Color of the Bear Power columns.")
43100
+ ],
42254
43101
  compute: (bars, inputs) => {
42255
43102
  const basis = ema(closes(bars), num3(inputs, "length", 13));
42256
43103
  return {
42257
43104
  plots: [
42258
- { key: "bull", title: "Bull power", values: sub(highs(bars), basis), kind: "histogram", color: BULLISH, base: 0 },
42259
- { key: "bear", title: "Bear power", values: sub(lows(bars), basis), kind: "histogram", color: BEARISH, base: 0 }
43105
+ { key: "bull", title: "Bull Power", values: sub(highs(bars), basis), kind: "columns", color: str3(inputs, "bullColor", BULLISH), base: 0 },
43106
+ { key: "bear", title: "Bear Power", values: sub(lows(bars), basis), kind: "columns", color: str3(inputs, "bearColor", BEARISH), base: 0 }
42260
43107
  ],
42261
- levels: ZERO_LEVEL
43108
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 30), lineStyle: "dashed", title: "Zero Line" }]
42262
43109
  };
42263
43110
  }
42264
43111
  };
@@ -42267,31 +43114,66 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42267
43114
  title: "TTM Squeeze",
42268
43115
  overlay: false,
42269
43116
  inputs: [
42270
- lengthInput(20),
42271
- { key: "bbMult", title: "Bollinger multiplier", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 },
42272
- { key: "kcMult", title: "Keltner multiplier", type: "float", defval: 1.5, min: 0.1, max: 50, step: 0.1 }
43117
+ { ...lengthInput(20, "length", "Length", 5e3, "Shared lookback for the Bollinger Bands, the Keltner Channels, and the momentum regression. John Carter's original setting is 20."), min: 2 },
43118
+ sourceInput("Close", "source", "Source", "Price series used for the shared moving average basis, the standard deviation, and the momentum displacement."),
43119
+ floatInput("bbMult", "Bollinger Bands Multiplier", 2, 0.1, 50, 0.1, "Standard deviation multiplier for the Bollinger Bands."),
43120
+ floatInput("kcMult", "Keltner Channels Multiplier", 1.5, 0.1, 50, 0.1, "ATR multiplier for the Keltner Channels. A smaller multiplier demands a tighter compression before the squeeze turns on."),
43121
+ colorInput(BULLISH, "Momentum Up", "bullColor", "Histogram color while momentum is positive. Bars building away from zero print solid, bars fading back print dimmed."),
43122
+ colorInput(BEARISH, "Momentum Down", "bearColor", "Histogram color while momentum is negative. Bars building away from zero print solid, bars fading back print dimmed."),
43123
+ colorInput(WARNING, "Squeeze On", "squeezeOnColor", "Zero-line dot color while both Bollinger Bands sit fully inside the Keltner Channels."),
43124
+ colorInput(NEUTRAL, "Squeeze Off", "squeezeOffColor", "Zero-line dot color while the Bollinger Bands trade outside the Keltner Channels."),
43125
+ boolInput("markFires", "Mark Squeeze Fires", true, STYLE, "Draws a cross on the zero line on the first bar where the bands re-emerge outside the channels, colored by the side of the momentum histogram.")
42273
43126
  ],
42274
43127
  compute: (bars, inputs) => {
42275
43128
  const len = num3(inputs, "length", 20);
42276
- const c = closes(bars);
42277
- const basis = sma(c, len);
42278
- const dev = map(stdev(c, len), (x) => x * num3(inputs, "bbMult", 2));
42279
- const kcRange = map(rma(trueRange(bars), len), (x) => x * num3(inputs, "kcMult", 1.5));
42280
- const donchianMid = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => (h + l) / 2);
42281
- const anchor = zip(donchianMid, basis, (d, b) => (d + b) / 2);
42282
- const mom = linreg(sub(c, anchor), len);
42283
- const squeezed = c.map((_, i) => {
43129
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
43130
+ const basis = sma(src, len);
43131
+ const dev = map(stdev(src, len), (x) => x * num3(inputs, "bbMult", 2));
43132
+ const kcRange = map(atr(bars, len), (x) => x * num3(inputs, "kcMult", 1.5));
43133
+ const rangeMid = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => (h + l) / 2);
43134
+ const anchor = zip(rangeMid, basis, (d, b) => (d + b) / 2);
43135
+ const mom = linreg(sub(src, anchor), len, 0);
43136
+ const squeezeOn = basis.map((b, i) => {
42284
43137
  const d = dev[i];
42285
43138
  const r = kcRange[i];
42286
- return Number.isFinite(d) && Number.isFinite(r) ? d < r : false;
43139
+ if (!Number.isFinite(b) || !Number.isFinite(d) || !Number.isFinite(r)) return null;
43140
+ return d < r;
42287
43141
  });
42288
- const stateColors = squeezed.map((s, i) => Number.isFinite(mom[i]) ? s ? BEARISH : BULLISH : null);
42289
- return {
42290
- plots: [
42291
- { key: "mom", title: "Momentum", values: mom, kind: "histogram", color: BULLISH, colors: histColors(mom), base: 0 },
42292
- { key: "squeeze", title: "Squeeze", values: mom.map((x) => Number.isFinite(x) ? 0 : Number.NaN), kind: "cross", color: INFO, colors: stateColors }
42293
- ]
42294
- };
43142
+ const bull = str3(inputs, "bullColor", BULLISH);
43143
+ const bear = str3(inputs, "bearColor", BEARISH);
43144
+ const onInk = str3(inputs, "squeezeOnColor", WARNING);
43145
+ const offInk = str3(inputs, "squeezeOffColor", NEUTRAL);
43146
+ const momColors = mom.map((x, i) => {
43147
+ if (!Number.isFinite(x)) return null;
43148
+ const prev3 = i > 0 && Number.isFinite(mom[i - 1]) ? mom[i - 1] : x;
43149
+ const rising = x > prev3;
43150
+ return x >= 0 ? rising ? bull : transp(bull, 60) : rising ? transp(bear, 60) : bear;
43151
+ });
43152
+ const plots = [
43153
+ { key: "mom", title: "Momentum", values: mom, kind: "columns", color: bull, colors: momColors, base: 0 },
43154
+ {
43155
+ key: "state",
43156
+ title: "Squeeze State",
43157
+ values: squeezeOn.map((s) => s == null ? Number.NaN : 0),
43158
+ kind: "circles",
43159
+ color: offInk,
43160
+ width: 2,
43161
+ colors: squeezeOn.map((s) => s == null ? null : s ? onInk : offInk)
43162
+ }
43163
+ ];
43164
+ if (bool(inputs, "markFires", true)) {
43165
+ const fires = squeezeOn.map((s, i) => s === false && squeezeOn[i - 1] === true ? 0 : Number.NaN);
43166
+ plots.push({
43167
+ key: "fire",
43168
+ title: "Squeeze Fire",
43169
+ values: fires,
43170
+ kind: "cross",
43171
+ color: offInk,
43172
+ width: 3,
43173
+ colors: fires.map((x, i) => Number.isFinite(x) ? Number.isFinite(mom[i]) ? mom[i] >= 0 ? bull : bear : offInk : null)
43174
+ });
43175
+ }
43176
+ return { plots };
42295
43177
  }
42296
43178
  };
42297
43179
  var oscillatorSpecs = [
@@ -42303,7 +43185,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42303
43185
  awesome,
42304
43186
  cci,
42305
43187
  williamsR,
42306
- cmo,
43188
+ cmoSpec,
42307
43189
  connorsRsi,
42308
43190
  fisher,
42309
43191
  trix,
@@ -42327,15 +43209,19 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42327
43209
  type: "aroon",
42328
43210
  title: "Aroon",
42329
43211
  overlay: false,
42330
- inputs: [lengthInput(14)],
43212
+ inputs: [
43213
+ lengthInput(14, "length", "Length", 5e3, "Lookback length used to locate the highest high and lowest low."),
43214
+ colorInput(BULLISH, "Aroon Up", "upColor", "Color of the Aroon Up line."),
43215
+ colorInput(BEARISH, "Aroon Down", "downColor", "Color of the Aroon Down line.")
43216
+ ],
42331
43217
  compute: (bars, inputs) => {
42332
43218
  const len = num3(inputs, "length", 14);
42333
- const up = map(barsSinceHighest(highs(bars), len + 1), (x) => 100 * (len - x) / len);
42334
- const down = map(barsSinceLowest(lows(bars), len + 1), (x) => 100 * (len - x) / len);
43219
+ const up = map(barsSinceHighest(highs(bars), len), (x) => 100 * (len - x) / len);
43220
+ const down = map(barsSinceLowest(lows(bars), len), (x) => 100 * (len - x) / len);
42335
43221
  return {
42336
43222
  plots: [
42337
- { key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
42338
- { key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
43223
+ { key: "up", title: "Aroon Up", values: up, color: str3(inputs, "upColor", BULLISH) },
43224
+ { key: "down", title: "Aroon Down", values: down, color: str3(inputs, "downColor", BEARISH) }
42339
43225
  ]
42340
43226
  };
42341
43227
  }
@@ -42345,10 +43231,16 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42345
43231
  title: "Average Directional Index",
42346
43232
  shortTitle: "ADX",
42347
43233
  overlay: false,
42348
- inputs: [lengthInput(14, "diLength", "DI length"), lengthInput(14, "adxLength", "ADX smoothing")],
43234
+ inputs: [
43235
+ lengthInput(14, "diLength", "DI Length", 5e3, "Wilder RMA smoothing length applied to directional movement and true range when computing +DI and \u2212DI."),
43236
+ lengthInput(14, "adxLength", "ADX Smoothing", 5e3, "Wilder RMA smoothing length applied to DX to obtain the ADX line."),
43237
+ floatInput("keyLevel", "Key Level", 25, 0, 100, 1, "Trend strength reference level; ADX above it is commonly read as a trending market."),
43238
+ colorInput(BULLISH, "+DI Color", "plusDiColor", "Color of the plus directional indicator, and of the ADX line above the key level while +DI leads."),
43239
+ colorInput(BEARISH, "-DI Color", "minusDiColor", "Color of the minus directional indicator, and of the ADX line above the key level while \u2212DI leads."),
43240
+ colorInput(NEUTRAL, "ADX Color", "adxColor", "Color of the ADX line while it sits below the key level \u2014 no trend strong enough to trade.")
43241
+ ],
42349
43242
  compute: (bars, inputs) => {
42350
43243
  const diLen = num3(inputs, "diLength", 14);
42351
- const adxLen = num3(inputs, "adxLength", 14);
42352
43244
  const n = bars.length;
42353
43245
  const plusDm = new Array(n).fill(Number.NaN);
42354
43246
  const minusDm = new Array(n).fill(Number.NaN);
@@ -42362,13 +43254,23 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42362
43254
  const plusDi = zip(rma(plusDm, diLen), atrLine, (d, a) => a === 0 ? 0 : 100 * d / a);
42363
43255
  const minusDi = zip(rma(minusDm, diLen), atrLine, (d, a) => a === 0 ? 0 : 100 * d / a);
42364
43256
  const dx = zip(plusDi, minusDi, (p, m) => p + m === 0 ? 0 : 100 * Math.abs(p - m) / (p + m));
43257
+ const adxLine = rma(dx, num3(inputs, "adxLength", 14));
43258
+ const plusInk = str3(inputs, "plusDiColor", BULLISH);
43259
+ const minusInk = str3(inputs, "minusDiColor", BEARISH);
43260
+ const neutralInk = str3(inputs, "adxColor", NEUTRAL);
43261
+ const keyLevel = num3(inputs, "keyLevel", 25);
43262
+ const adxColors = adxLine.map((x, i) => {
43263
+ if (!Number.isFinite(x)) return null;
43264
+ if (x <= keyLevel) return neutralInk;
43265
+ return (plusDi[i] ?? 0) > (minusDi[i] ?? 0) ? plusInk : minusInk;
43266
+ });
42365
43267
  return {
42366
43268
  plots: [
42367
- { key: "adx", title: "ADX", values: rma(dx, adxLen), color: SERIES_LINE, width: 2 },
42368
- { key: "plusDi", title: "+DI", values: plusDi, color: BULLISH },
42369
- { key: "minusDi", title: "-DI", values: minusDi, color: BEARISH }
43269
+ { key: "plusDi", title: "+DI", values: plusDi, color: transp(plusInk, 30) },
43270
+ { key: "minusDi", title: "-DI", values: minusDi, color: transp(minusInk, 30) },
43271
+ { key: "adx", title: "ADX", values: adxLine, color: neutralInk, width: 2, colors: adxColors }
42370
43272
  ],
42371
- levels: [{ key: "threshold", price: 25, color: NEUTRAL, lineStyle: "dotted" }]
43273
+ levels: [{ key: "keyLevel", price: keyLevel, color: NEUTRAL, lineStyle: "dashed", title: "Key Level" }]
42372
43274
  };
42373
43275
  }
42374
43276
  };
@@ -42377,7 +43279,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42377
43279
  title: "Vortex Indicator",
42378
43280
  shortTitle: "VI",
42379
43281
  overlay: false,
42380
- inputs: [lengthInput(14)],
43282
+ inputs: [
43283
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to sum the vortex movements (VM+ and VM\u2212) and the true range."),
43284
+ colorInput(BULLISH, "VI+", "plusColor", "Color of the positive vortex line (VI+)."),
43285
+ colorInput(BEARISH, "VI-", "minusColor", "Color of the negative vortex line (VI\u2212)."),
43286
+ colorInput(NEUTRAL, "Baseline", "baselineColor", "Color of the dotted baseline drawn at 1.")
43287
+ ],
42381
43288
  compute: (bars, inputs) => {
42382
43289
  const len = num3(inputs, "length", 14);
42383
43290
  const n = bars.length;
@@ -42390,25 +43297,48 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42390
43297
  const trSum = sum(trueRange(bars), len);
42391
43298
  return {
42392
43299
  plots: [
42393
- { key: "viPlus", title: "VI+", values: zip(sum(vmPlus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: BULLISH, width: 2 },
42394
- { key: "viMinus", title: "VI-", values: zip(sum(vmMinus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: BEARISH, width: 2 }
43300
+ { key: "viPlus", title: "VI+", values: zip(sum(vmPlus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: str3(inputs, "plusColor", BULLISH) },
43301
+ { key: "viMinus", title: "VI-", values: zip(sum(vmMinus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: str3(inputs, "minusColor", BEARISH) }
42395
43302
  ],
42396
- levels: [{ key: "one", price: 1, color: NEUTRAL, lineStyle: "dotted" }]
43303
+ levels: [{ key: "baseline", price: 1, color: str3(inputs, "baselineColor", NEUTRAL), lineStyle: "dotted", title: "Baseline" }]
42397
43304
  };
42398
43305
  }
42399
43306
  };
42400
43307
  var trendSpecs = [aroon, adx, vortex];
42401
43308
 
42402
43309
  // src/core/native-indicators/classics/volatility.ts
43310
+ function expansionColors(values, calm, expanding) {
43311
+ return values.map((x, i) => {
43312
+ if (!Number.isFinite(x)) return null;
43313
+ const prev3 = i > 0 && Number.isFinite(values[i - 1]) ? values[i - 1] : x;
43314
+ return x > prev3 ? expanding : calm;
43315
+ });
43316
+ }
43317
+ function washedToZero(key, title, values, colors, fallback, valueTransparency, width = 1) {
43318
+ return {
43319
+ plots: [{ key, title, values, color: fallback, width, colors }, anchorPlot("zeroAnchor", "Zero Anchor", values.length, 0)],
43320
+ bands: [{ key: "wash", from: key, to: "zeroAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, 0, (_x, i) => colors[i] ?? fallback, valueTransparency) }]
43321
+ };
43322
+ }
42403
43323
  var atrSpec = {
42404
43324
  type: "average-true-range",
42405
43325
  title: "Average True Range",
42406
43326
  shortTitle: "ATR",
42407
43327
  overlay: false,
42408
- inputs: [lengthInput(14), colorInput()],
42409
- compute: (bars, inputs) => ({
42410
- plots: [{ key: "atr", title: "ATR", values: atr(bars, num3(inputs, "length", 14)), color: str3(inputs, "color", SERIES_LINE), width: 2 }]
42411
- })
43328
+ inputs: [
43329
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to average the true range."),
43330
+ optionInput("smoothing", "Smoothing", "RMA", ["RMA", "SMA", "EMA", "WMA"], "Moving average applied to the true range. RMA is Wilder's original smoothing used by the classic ATR."),
43331
+ colorInput(NEUTRAL, "ATR Line", "color", "Color of the ATR line while volatility contracts or holds flat."),
43332
+ colorInput(WARNING, "Expanding", "expandColor", "Color of the ATR line while it rises \u2014 volatility expanding bar to bar.")
43333
+ ],
43334
+ compute: (bars, inputs) => {
43335
+ const len = num3(inputs, "length", 14);
43336
+ const tr = trueRange(bars);
43337
+ const kind = str3(inputs, "smoothing", "RMA");
43338
+ const values = kind === "SMA" ? sma(tr, len) : kind === "EMA" ? ema(tr, len) : kind === "WMA" ? wma(tr, len) : rma(tr, len);
43339
+ const calm = str3(inputs, "color", NEUTRAL);
43340
+ return washedToZero("atr", "ATR", values, expansionColors(values, calm, str3(inputs, "expandColor", WARNING)), calm, 75);
43341
+ }
42412
43342
  };
42413
43343
  var historicalVolatility = {
42414
43344
  type: "historical-volatility",
@@ -42416,14 +43346,23 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42416
43346
  shortTitle: "HV",
42417
43347
  overlay: false,
42418
43348
  inputs: [
42419
- lengthInput(10),
42420
- { key: "annual", title: "Periods per year", type: "int", defval: 365, min: 1, max: 1e5, step: 1, tooltip: "Annualization factor (365 for daily crypto bars, 252 for stock sessions)" },
42421
- colorInput()
43349
+ { ...lengthInput(10, "length", "Length", 5e3, "Lookback window in bars for the standard deviation of close-to-close log returns."), min: 2 },
43350
+ boolInput("annualize", "Annualize", true, void 0, "Scale the per-bar volatility to an annual figure by multiplying by the square root of the periods per year."),
43351
+ intInput("periodsPerYear", "Periods Per Year", 252, 1, 1e5, "Number of periods in a year used for annualization: 252 for daily bars, 52 for weekly, 12 for monthly."),
43352
+ colorInput(WARNING, "Volatility Line", "color", "Color of the line at its recent highs; it fades toward grey as volatility contracts.")
42422
43353
  ],
42423
43354
  compute: (bars, inputs) => {
42424
- const logReturns = map(change(map(closes(bars), Math.log)), (x) => x);
42425
- const values = map(stdev(logReturns, num3(inputs, "length", 10)), (x) => x * Math.sqrt(num3(inputs, "annual", 365)) * 100);
42426
- return { plots: [{ key: "hv", title: "HV", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
43355
+ const len = num3(inputs, "length", 10);
43356
+ const logReturns = sub(map(closes(bars), Math.log), shift3(map(closes(bars), Math.log), 1));
43357
+ const meanReturn = map(sum(logReturns, len), (s) => s / len);
43358
+ const squaredSum = sum(map(logReturns, (r) => r * r), len);
43359
+ const factor = bool(inputs, "annualize", true) ? Math.sqrt(num3(inputs, "periodsPerYear", 252)) : 1;
43360
+ const values = zip(squaredSum, meanReturn, (sq, m) => Math.sqrt(Math.max(sq - len * m * m, 0) / (len - 1)) * factor * 100);
43361
+ const low = lowest(values, 100);
43362
+ const high = highest(values, 100);
43363
+ const accent = str3(inputs, "color", WARNING);
43364
+ const colors = values.map((x, i) => Number.isFinite(x) ? gradient(x, low[i] ?? 0, high[i] ?? 0, NEUTRAL, accent) : null);
43365
+ return washedToZero("hv", "Historical Volatility", values, colors, accent, 75, 2);
42427
43366
  }
42428
43367
  };
42429
43368
  var chaikinVolatility = {
@@ -42431,13 +43370,26 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42431
43370
  title: "Chaikin Volatility",
42432
43371
  shortTitle: "CHV",
42433
43372
  overlay: false,
42434
- inputs: [lengthInput(10, "emaLength", "EMA length"), lengthInput(10, "rocLength", "RoC length"), colorInput()],
43373
+ inputs: [
43374
+ lengthInput(10, "emaLength", "EMA Length", 5e3, "Length of the exponential moving average applied to the bar high-low range."),
43375
+ lengthInput(10, "rocLength", "ROC Length", 5e3, "Number of bars used for the rate of change of the smoothed high-low range."),
43376
+ colorInput(NEUTRAL, "CHV Color", "color", "Color of the Chaikin Volatility line."),
43377
+ colorInput(WARNING, "Expansion", "expansionColor", "Line and fill color while the reading is above zero \u2014 the smoothed range is wider than it was ROC Length bars ago.")
43378
+ ],
42435
43379
  compute: (bars, inputs) => {
42436
43380
  const range = ema(sub(highs(bars), lows(bars)), num3(inputs, "emaLength", 10));
42437
- return {
42438
- plots: [{ key: "cv", title: "Chaikin Vol", values: roc(range, num3(inputs, "rocLength", 10)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42439
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
42440
- };
43381
+ const prior = shift3(range, num3(inputs, "rocLength", 10));
43382
+ const values = zip(range, prior, (r, p) => p === 0 ? Number.NaN : 100 * (r - p) / p);
43383
+ const calm = str3(inputs, "color", NEUTRAL);
43384
+ const expansion = str3(inputs, "expansionColor", WARNING);
43385
+ return centeredOscillator({
43386
+ key: "chv",
43387
+ title: "Chaikin Volatility",
43388
+ values,
43389
+ // Only expansion is a state worth accenting; contraction stays structural grey.
43390
+ ink: { bull: expansion, bear: calm, neutral: calm },
43391
+ zeroInk: NEUTRAL
43392
+ });
42441
43393
  }
42442
43394
  };
42443
43395
  var massIndex = {
@@ -42445,14 +43397,42 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42445
43397
  title: "Mass Index",
42446
43398
  shortTitle: "MI",
42447
43399
  overlay: false,
42448
- inputs: [lengthInput(25), colorInput()],
43400
+ inputs: [
43401
+ lengthInput(25, "length", "Sum Length", 5e3, "Number of bars over which the single/double EMA ratio is summed."),
43402
+ lengthInput(9, "emaLength", "EMA Length", 5e3, "Length of the single EMA of the high-low range and of its double smoothing."),
43403
+ floatInput("setupLevel", "Setup Level", 27, 0, 1e3, 0.1, "Reversal bulge set-up level; a rise above it arms the pattern."),
43404
+ floatInput("triggerLevel", "Trigger Level", 26.5, 0, 1e3, 0.1, "Reversal bulge trigger level; a decline below it after a set-up completes the pattern."),
43405
+ colorInput(NEUTRAL, "Mass Index", "color", "Line color while no reversal bulge is armed."),
43406
+ colorInput(WARNING, "Armed", "armedColor", "Line color from the rise above the setup level until the decline below the trigger level."),
43407
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Reversal bulge levels color.")
43408
+ ],
42449
43409
  compute: (bars, inputs) => {
42450
- const range = sub(highs(bars), lows(bars));
42451
- const single = ema(range, 9);
42452
- const ratio = zip(single, ema(single, 9), (a, b) => b === 0 ? Number.NaN : a / b);
43410
+ const emaLen = num3(inputs, "emaLength", 9);
43411
+ const single = ema(sub(highs(bars), lows(bars)), emaLen);
43412
+ const ratio = zip(single, ema(single, emaLen), (a, b) => b === 0 ? 1 : a / b);
43413
+ const values = sum(ratio, num3(inputs, "length", 25));
43414
+ const setup = num3(inputs, "setupLevel", 27);
43415
+ const trigger = num3(inputs, "triggerLevel", 26.5);
43416
+ const calm = str3(inputs, "color", NEUTRAL);
43417
+ const armedInk = str3(inputs, "armedColor", WARNING);
43418
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
43419
+ let armed = false;
43420
+ const colors = values.map((x) => {
43421
+ if (!Number.isFinite(x)) return null;
43422
+ armed = x > setup ? true : x < trigger ? false : armed;
43423
+ return armed ? armedInk : calm;
43424
+ });
42453
43425
  return {
42454
- plots: [{ key: "mi", title: "Mass Index", values: sum(ratio, num3(inputs, "length", 25)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42455
- levels: [{ key: "reversal", price: 27, color: NEUTRAL, lineStyle: "dotted" }]
43426
+ plots: [
43427
+ { key: "mi", title: "Mass Index", values, color: calm, colors },
43428
+ anchorPlot("setupAnchor", "Setup Anchor", values.length, setup),
43429
+ anchorPlot("triggerAnchor", "Trigger Anchor", values.length, trigger)
43430
+ ],
43431
+ bands: [{ key: "bulge", from: "setupAnchor", to: "triggerAnchor", color: transp(armedInk, 90) }],
43432
+ levels: [
43433
+ { key: "setup", price: setup, color: levelInk, lineStyle: "dashed", title: "Setup Level" },
43434
+ { key: "trigger", price: trigger, color: levelInk, lineStyle: "dashed", title: "Trigger Level" }
43435
+ ]
42456
43436
  };
42457
43437
  }
42458
43438
  };
@@ -42461,30 +43441,83 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42461
43441
  title: "Standard Deviation",
42462
43442
  shortTitle: "StdDev",
42463
43443
  overlay: false,
42464
- inputs: [lengthInput(20), sourceInput(), colorInput()],
42465
- compute: (bars, inputs) => ({
42466
- plots: [
42467
- {
42468
- key: "stdev",
42469
- title: "StdDev",
42470
- values: stdev(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20)),
42471
- color: str3(inputs, "color", SERIES_LINE),
42472
- width: 2
42473
- }
42474
- ]
42475
- })
43444
+ inputs: [
43445
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used to compute the standard deviation."),
43446
+ sourceInput("Close", "source", "Source", "Price series the standard deviation is calculated on."),
43447
+ colorInput(NEUTRAL, "Standard Deviation", "color", "Line color while volatility contracts or holds flat."),
43448
+ colorInput(WARNING, "Expanding", "expandColor", "Line and fill color while volatility expands bar to bar.")
43449
+ ],
43450
+ compute: (bars, inputs) => {
43451
+ const values = stdev(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20));
43452
+ const calm = str3(inputs, "color", NEUTRAL);
43453
+ return washedToZero("stdev", "Standard Deviation", values, expansionColors(values, calm, str3(inputs, "expandColor", WARNING)), calm, 80);
43454
+ }
42476
43455
  };
42477
43456
  var ulcerIndex = {
42478
43457
  type: "ulcer-index",
42479
43458
  title: "Ulcer Index",
42480
43459
  overlay: false,
42481
- inputs: [lengthInput(14), colorInput()],
43460
+ inputs: [
43461
+ { ...lengthInput(14, "length", "Length", 500, "Lookback window. At each bar the percentage drawdown from the window's running maximum is squared; the squares are averaged and the root taken."), min: 2 },
43462
+ sourceInput("Close", "source", "Source", "Price series the Ulcer Index is computed on. Close is the standard choice."),
43463
+ floatInput("threshold", "Alert Threshold", 5, 0, 1e3, 0.25, "Reference level drawn as a dashed line. The index scales with the instrument's volatility and the window length, so set it relative to its own history."),
43464
+ colorInput(NEUTRAL, "Ulcer Index", "color", "Color of the index near zero; it blends toward the stress color as the reading approaches the threshold."),
43465
+ colorInput(BEARISH, "Stress", "stressColor", "Color of the index at and beyond the alert threshold."),
43466
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Vertical gradient between zero and the Ulcer Index, fully transparent at zero."),
43467
+ boolInput("showDrawdown", "Drawdown %", false, STYLE, "Plot the current percentage drawdown from the window high as columns below zero \u2014 the raw ingredient the index averages."),
43468
+ colorInput(transp(BEARISH, 50), "Drawdown Color", "drawdownColor", "Color of the drawdown columns."),
43469
+ colorInput(NEUTRAL, "Threshold", "thresholdColor", "Color of the alert threshold line.")
43470
+ ],
42482
43471
  compute: (bars, inputs) => {
42483
43472
  const len = num3(inputs, "length", 14);
42484
- const c = closes(bars);
42485
- const drawdown = zip(c, highest(c, len), (x, h) => h === 0 ? 0 : 100 * (x - h) / h);
42486
- const values = map(sma(map(drawdown, (d) => d * d), len), Math.sqrt);
42487
- return { plots: [{ key: "ui", title: "Ulcer", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
43473
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
43474
+ const n = src.length;
43475
+ const values = new Array(n).fill(Number.NaN);
43476
+ const drawdown = new Array(n).fill(Number.NaN);
43477
+ for (let i = len - 1; i < n; i++) {
43478
+ let runMax = Number.NaN;
43479
+ let sumSq = 0;
43480
+ let valid = true;
43481
+ let last2 = Number.NaN;
43482
+ for (let k = i - len + 1; k <= i; k++) {
43483
+ const price = src[k];
43484
+ if (!Number.isFinite(price)) {
43485
+ valid = false;
43486
+ break;
43487
+ }
43488
+ runMax = Number.isFinite(runMax) ? Math.max(runMax, price) : price;
43489
+ if (runMax <= 0) {
43490
+ valid = false;
43491
+ break;
43492
+ }
43493
+ last2 = 100 * (price - runMax) / runMax;
43494
+ sumSq += last2 * last2;
43495
+ }
43496
+ if (!valid) continue;
43497
+ values[i] = Math.sqrt(sumSq / len);
43498
+ drawdown[i] = last2;
43499
+ }
43500
+ const calm = str3(inputs, "color", NEUTRAL);
43501
+ const stress = str3(inputs, "stressColor", BEARISH);
43502
+ const threshold = num3(inputs, "threshold", 5);
43503
+ const colors = values.map((x) => Number.isFinite(x) ? threshold > 0 ? gradient(x, 0, threshold, calm, stress) : stress : null);
43504
+ const plots = [{ key: "ui", title: "Ulcer Index", values, color: calm, colors }];
43505
+ const bands = [];
43506
+ if (bool(inputs, "gradientFill", true)) {
43507
+ plots.push(anchorPlot("zeroAnchor", "Zero Base", n, 0));
43508
+ bands.push({ key: "wash", from: "ui", to: "zeroAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, 0, (_x, i) => colors[i] ?? calm) });
43509
+ }
43510
+ if (bool(inputs, "showDrawdown", false)) {
43511
+ plots.push({ key: "dd", title: "Drawdown %", values: drawdown, kind: "columns", color: str3(inputs, "drawdownColor", transp(BEARISH, 50)), base: 0 });
43512
+ }
43513
+ return {
43514
+ plots,
43515
+ bands,
43516
+ levels: [
43517
+ { key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dotted", title: "Zero Level" },
43518
+ { key: "threshold", price: threshold, color: str3(inputs, "thresholdColor", NEUTRAL), lineStyle: "dashed", title: "Alert Threshold" }
43519
+ ]
43520
+ };
42488
43521
  }
42489
43522
  };
42490
43523
  var choppiness = {
@@ -42492,19 +43525,33 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42492
43525
  title: "Choppiness Index",
42493
43526
  shortTitle: "CHOP",
42494
43527
  overlay: false,
42495
- inputs: [lengthInput(14), colorInput()],
43528
+ inputs: [
43529
+ { ...lengthInput(14, "length", "Length", 5e3, "Number of bars used to sum true ranges and to measure the highest high \u2212 lowest low range."), min: 2 },
43530
+ floatInput("upperBand", "Upper Band", 61.8, 0, 100, 0.1, "Readings above this level indicate a choppy, range-bound market."),
43531
+ floatInput("lowerBand", "Lower Band", 38.2, 0, 100, 0.1, "Readings below this level indicate a strong trend."),
43532
+ colorInput(NEUTRAL, "CHOP Line", "color", "Color of the Choppiness Index line."),
43533
+ colorInput(BULLISH, "Trending", "trendingColor", "Line, fill and zone color toward and below the lower band \u2014 a trending market."),
43534
+ colorInput(BEARISH, "Choppy", "choppyColor", "Line, fill and zone color toward and above the upper band \u2014 a choppy, range-bound market."),
43535
+ colorInput(transp(NEUTRAL, 90), "Band Fill", "fillColor", "Fill color of the zone between the upper and lower bands.")
43536
+ ],
42496
43537
  compute: (bars, inputs) => {
42497
43538
  const len = num3(inputs, "length", 14);
42498
43539
  const trSum = sum(trueRange(bars), len);
42499
43540
  const span = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => h - l);
42500
43541
  const values = zip(trSum, span, (t, s) => s > 0 && t > 0 ? 100 * Math.log10(t / s) / Math.log10(len) : Number.NaN);
42501
- return {
42502
- plots: [{ key: "chop", title: "CHOP", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42503
- levels: [
42504
- { key: "upper", price: 61.8, color: NEUTRAL },
42505
- { key: "lower", price: 38.2, color: NEUTRAL }
42506
- ]
42507
- };
43542
+ return boundedOscillator({
43543
+ key: "chop",
43544
+ title: "CHOP",
43545
+ values,
43546
+ scale: { top: 100, bottom: 0 },
43547
+ overbought: num3(inputs, "upperBand", 61.8),
43548
+ oversold: num3(inputs, "lowerBand", 38.2),
43549
+ ink: { low: str3(inputs, "trendingColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "choppyColor", BEARISH) },
43550
+ levelInk: NEUTRAL,
43551
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 90)),
43552
+ midpointWash: true,
43553
+ levelTitles: { overbought: "Upper Band", oversold: "Lower Band" }
43554
+ });
42508
43555
  }
42509
43556
  };
42510
43557
  var volatilitySpecs = [atrSpec, historicalVolatility, chaikinVolatility, massIndex, standardDeviation, ulcerIndex, choppiness];
@@ -42517,17 +43564,85 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42517
43564
  return (2 * b.close - b.high - b.low) / (b.high - b.low) * b.volume;
42518
43565
  });
42519
43566
  }
43567
+ function slopeColors2(values, bull, bear, flat) {
43568
+ return values.map((x, i) => {
43569
+ if (!Number.isFinite(x)) return null;
43570
+ const prev3 = i > 0 ? values[i - 1] : Number.NaN;
43571
+ if (!Number.isFinite(prev3)) return flat;
43572
+ return x > prev3 ? bull : x < prev3 ? bear : flat;
43573
+ });
43574
+ }
43575
+ function volumeIndex(bars, src, base, when) {
43576
+ const out = new Array(bars.length).fill(Number.NaN);
43577
+ let index = base;
43578
+ for (let i = 0; i < bars.length; i++) {
43579
+ const v = bars[i].volume;
43580
+ const pv = i > 0 ? bars[i - 1].volume : null;
43581
+ const prev3 = i > 0 ? src[i - 1] : Number.NaN;
43582
+ const active = v != null && pv != null && Number.isFinite(v) && Number.isFinite(pv) && when(v, pv);
43583
+ if (active && Number.isFinite(prev3) && prev3 !== 0) index *= 1 + (src[i] - prev3) / prev3;
43584
+ out[i] = index;
43585
+ }
43586
+ return out;
43587
+ }
43588
+ function regimeIndex(key, title, values, signal, ink, showSignal, gradientFill, singleColor = false) {
43589
+ const inkOf = (i) => {
43590
+ const s = signal[i];
43591
+ const x = values[i];
43592
+ if (singleColor || !Number.isFinite(s)) return ink.neutral;
43593
+ return x > s ? ink.bull : x < s ? ink.bear : ink.neutral;
43594
+ };
43595
+ const plots = [
43596
+ {
43597
+ key: "signal",
43598
+ title: "Signal EMA",
43599
+ values: signal,
43600
+ color: ink.signal,
43601
+ ...showSignal ? {} : { display: { pane: false } }
43602
+ },
43603
+ { key, title, values, color: ink.neutral, width: 2, colors: values.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) }
43604
+ ];
43605
+ const bands = gradientFill ? [
43606
+ {
43607
+ key: "regime",
43608
+ from: key,
43609
+ to: "signal",
43610
+ color: transp(NEUTRAL, 100),
43611
+ gradient: values.map((x, i) => {
43612
+ const s = signal[i];
43613
+ if (!Number.isFinite(x) || !Number.isFinite(s)) return null;
43614
+ const above = x > s;
43615
+ const c = above ? ink.bull : ink.bear;
43616
+ return { topValue: Math.max(x, s), bottomValue: Math.min(x, s), topColor: transp(c, above ? 50 : 100), bottomColor: transp(c, above ? 100 : 50) };
43617
+ })
43618
+ }
43619
+ ] : [];
43620
+ return { plots, bands };
43621
+ }
42520
43622
  var obv = {
42521
43623
  type: "on-balance-volume",
42522
43624
  title: "On Balance Volume",
42523
43625
  shortTitle: "OBV",
42524
43626
  overlay: false,
42525
- inputs: [colorInput()],
43627
+ inputs: [
43628
+ boolInput("smoothing", "Smoothing MA", false, SETTINGS, "Display a moving average of OBV to act as a smoothing/signal line."),
43629
+ optionInput("maType", "MA Type", "SMA", ["SMA", "EMA", "WMA", "RMA (SMMA)"], "Moving average type used for the smoothing MA."),
43630
+ lengthInput(20, "maLength", "MA Length", 5e3, "Number of bars used for the smoothing MA."),
43631
+ colorInput(INFO, "OBV", "color", "Color of the OBV line."),
43632
+ colorInput(WARNING, "Smoothing MA", "maColor", "Color of the smoothing MA line.")
43633
+ ],
42526
43634
  compute: (bars, inputs) => {
42527
43635
  const vol = volumes(bars);
42528
43636
  const d = change(closes(bars));
42529
- const signed3 = zip(vol, d, (v, x) => x > 0 ? v : x < 0 ? -v : 0);
42530
- return { plots: [{ key: "obv", title: "OBV", values: cumSum(signed3), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
43637
+ const values = cumSum(zip(vol, d, (v, x) => x > 0 ? v : x < 0 ? -v : 0));
43638
+ const plots = [{ key: "obv", title: "OBV", values, color: str3(inputs, "color", INFO) }];
43639
+ if (bool(inputs, "smoothing", false)) {
43640
+ const len = num3(inputs, "maLength", 20);
43641
+ const kind = str3(inputs, "maType", "SMA");
43642
+ const ma = kind === "EMA" ? ema(values, len) : kind === "WMA" ? wma(values, len) : kind === "RMA (SMMA)" ? rma(values, len) : sma(values, len);
43643
+ plots.push({ key: "ma", title: "Smoothing MA", values: ma, color: str3(inputs, "maColor", WARNING) });
43644
+ }
43645
+ return { plots };
42531
43646
  }
42532
43647
  };
42533
43648
  var accDist = {
@@ -42535,24 +43650,50 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42535
43650
  title: "Accumulation / Distribution",
42536
43651
  shortTitle: "A/D",
42537
43652
  overlay: false,
42538
- inputs: [colorInput()],
42539
- compute: (bars, inputs) => ({
42540
- plots: [{ key: "ad", title: "A/D", values: cumSum(moneyFlowVolume(bars)), color: str3(inputs, "color", SERIES_LINE), width: 2 }]
42541
- })
43653
+ inputs: [
43654
+ boolInput("slopeColoring", "Slope Coloring", true, STYLE, "Color the line by its slope \u2014 bullish while rising (accumulation), bearish while falling (distribution)."),
43655
+ colorInput(NEUTRAL, "A/D Line", "color", "Color of the line when slope coloring is disabled or the line is flat."),
43656
+ colorInput(BULLISH, "Rising", "bullColor", "Line color while the A/D Line rises."),
43657
+ colorInput(BEARISH, "Falling", "bearColor", "Line color while the A/D Line falls.")
43658
+ ],
43659
+ compute: (bars, inputs) => {
43660
+ const values = cumSum(moneyFlowVolume(bars));
43661
+ const flat = str3(inputs, "color", NEUTRAL);
43662
+ return {
43663
+ plots: [
43664
+ {
43665
+ key: "ad",
43666
+ title: "A/D",
43667
+ values,
43668
+ color: flat,
43669
+ ...bool(inputs, "slopeColoring", true) ? { colors: slopeColors2(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), flat) } : {}
43670
+ }
43671
+ ]
43672
+ };
43673
+ }
42542
43674
  };
42543
43675
  var cmf = {
42544
43676
  type: "chaikin-money-flow",
42545
43677
  title: "Chaikin Money Flow",
42546
43678
  shortTitle: "CMF",
42547
43679
  overlay: false,
42548
- inputs: [lengthInput(20), colorInput()],
43680
+ inputs: [
43681
+ lengthInput(20, "length", "Length", 5e3, "Lookback length over which money flow volume and volume are summed."),
43682
+ colorInput(NEUTRAL, "CMF Line", "color", "Color of the Chaikin Money Flow line."),
43683
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the CMF reads above zero (net money flowing in)."),
43684
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the CMF reads below zero (net money flowing out)."),
43685
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
43686
+ ],
42549
43687
  compute: (bars, inputs) => {
42550
43688
  const len = num3(inputs, "length", 20);
42551
43689
  const values = zip(sum(moneyFlowVolume(bars), len), sum(volumes(bars), len), (m, v) => v === 0 ? 0 : m / v);
42552
- return {
42553
- plots: [{ key: "cmf", title: "CMF", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42554
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
42555
- };
43690
+ return centeredOscillator({
43691
+ key: "cmf",
43692
+ title: "CMF",
43693
+ values,
43694
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
43695
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
43696
+ });
42556
43697
  }
42557
43698
  };
42558
43699
  var chaikinOscillator = {
@@ -42560,14 +43701,23 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42560
43701
  title: "Chaikin Oscillator",
42561
43702
  shortTitle: "Chaikin Osc",
42562
43703
  overlay: false,
42563
- inputs: [lengthInput(3, "fastLength", "Fast length"), lengthInput(10, "slowLength", "Slow length"), colorInput()],
43704
+ inputs: [
43705
+ lengthInput(3, "fastLength", "Fast Length", 5e3, "Length of the fast EMA applied to the accumulation/distribution line."),
43706
+ lengthInput(10, "slowLength", "Slow Length", 5e3, "Length of the slow EMA applied to the accumulation/distribution line."),
43707
+ colorInput(NEUTRAL, "Oscillator", "color", "Color of the Chaikin Oscillator line."),
43708
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the oscillator reads above zero (accumulation)."),
43709
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the oscillator reads below zero (distribution).")
43710
+ ],
42564
43711
  compute: (bars, inputs) => {
42565
43712
  const ad = cumSum(moneyFlowVolume(bars));
42566
43713
  const values = sub(ema(ad, num3(inputs, "fastLength", 3)), ema(ad, num3(inputs, "slowLength", 10)));
42567
- return {
42568
- plots: [{ key: "co", title: "Chaikin Osc", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42569
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
42570
- };
43714
+ return centeredOscillator({
43715
+ key: "co",
43716
+ title: "Chaikin Oscillator",
43717
+ values,
43718
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
43719
+ zeroInk: NEUTRAL
43720
+ });
42571
43721
  }
42572
43722
  };
42573
43723
  var eom = {
@@ -42575,20 +43725,28 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42575
43725
  title: "Ease of Movement",
42576
43726
  shortTitle: "EOM",
42577
43727
  overlay: false,
42578
- inputs: [lengthInput(14), { key: "divisor", title: "Divisor", type: "int", defval: 1e4, min: 1, max: 1e9, step: 1 }, colorInput()],
43728
+ inputs: [
43729
+ lengthInput(14, "length", "Length", 5e3, "Number of bars in the simple moving average smoothing the 1-bar Ease of Movement values."),
43730
+ intInput("divisor", "Divisor", 1e4, 1, 1e9, "Volume scaling constant of the box ratio; larger values scale the oscillator up. 10000 is the conventional default."),
43731
+ colorInput(NEUTRAL, "EOM Line", "color", "Color of the line when it sits exactly at zero."),
43732
+ colorInput(BULLISH, "Above Zero", "bullColor", "Color of the line and fill while Ease of Movement is positive."),
43733
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while Ease of Movement is negative.")
43734
+ ],
42579
43735
  compute: (bars, inputs) => {
42580
43736
  const div = num3(inputs, "divisor", 1e4);
42581
- const hl2 = sourceValues(bars, "HL2");
42582
- const move = change(hl2);
43737
+ const move = change(sourceValues(bars, "HL2"));
42583
43738
  const raw = bars.map((b, i) => {
42584
43739
  const m = move[i];
42585
- if (b.volume == null || !Number.isFinite(b.volume) || b.volume === 0 || !Number.isFinite(m)) return Number.NaN;
42586
- return div * m * (b.high - b.low) / b.volume;
43740
+ if (b.volume == null || !Number.isFinite(b.volume) || !Number.isFinite(m)) return Number.NaN;
43741
+ return b.volume === 0 ? 0 : div * m * (b.high - b.low) / b.volume;
43742
+ });
43743
+ return centeredOscillator({
43744
+ key: "eom",
43745
+ title: "EOM",
43746
+ values: sma(raw, num3(inputs, "length", 14)),
43747
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
43748
+ zeroInk: NEUTRAL
42587
43749
  });
42588
- return {
42589
- plots: [{ key: "eom", title: "EOM", values: sma(raw, num3(inputs, "length", 14)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42590
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
42591
- };
42592
43750
  }
42593
43751
  };
42594
43752
  var forceIndex = {
@@ -42596,13 +43754,24 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42596
43754
  title: "Force Index",
42597
43755
  shortTitle: "FI",
42598
43756
  overlay: false,
42599
- inputs: [lengthInput(13), colorInput()],
43757
+ inputs: [
43758
+ lengthInput(13, "length", "Length", 5e3, "EMA length applied to the raw 1-bar force index, (close \u2212 close[1]) \xD7 volume. Elder used 13 for the intermediate trend and 2 for short-term timing."),
43759
+ colorInput(BULLISH, "Force Index", "color", "Color of the line while above zero (buying pressure)."),
43760
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line while below zero (selling pressure).")
43761
+ ],
42600
43762
  compute: (bars, inputs) => {
42601
43763
  const raw = zip(change(closes(bars)), volumes(bars), (d, v) => d * v);
42602
- return {
42603
- plots: [{ key: "force", title: "Force", values: ema(raw, num3(inputs, "length", 13)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42604
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
42605
- };
43764
+ const bull = str3(inputs, "color", BULLISH);
43765
+ return centeredOscillator({
43766
+ key: "force",
43767
+ title: "Force Index",
43768
+ values: ema(raw, num3(inputs, "length", 13)),
43769
+ // The reference reads an exact zero as buying pressure, so neutral shares the bull ink.
43770
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
43771
+ zeroInk: NEUTRAL,
43772
+ width: 2,
43773
+ valueTransparency: 60
43774
+ });
42606
43775
  }
42607
43776
  };
42608
43777
  var klinger = {
@@ -42610,19 +43779,44 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42610
43779
  title: "Klinger Oscillator",
42611
43780
  shortTitle: "KVO",
42612
43781
  overlay: false,
42613
- inputs: [lengthInput(34, "fastLength", "Fast length"), lengthInput(55, "slowLength", "Slow length"), lengthInput(13, "signalLength", "Signal length")],
43782
+ inputs: [
43783
+ lengthInput(34, "fastLength", "Fast Length", 5e3, "Length of the fast EMA applied to the volume force."),
43784
+ lengthInput(55, "slowLength", "Slow Length", 5e3, "Length of the slow EMA applied to the volume force."),
43785
+ lengthInput(13, "signalLength", "Signal Length", 500, "Length of the EMA applied to the oscillator to obtain the signal line."),
43786
+ colorInput(BULLISH, "Bullish", "color", "Color of the Klinger oscillator while above zero."),
43787
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the Klinger oscillator while below zero."),
43788
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line.")
43789
+ ],
42614
43790
  compute: (bars, inputs) => {
42615
- const hlc3 = sourceValues(bars, "HLC3");
42616
- const trend = change(hlc3);
42617
- const signedVolume = zip(volumes(bars), trend, (v, t) => t >= 0 ? v : -v);
42618
- const line = sub(ema(signedVolume, num3(inputs, "fastLength", 34)), ema(signedVolume, num3(inputs, "slowLength", 55)));
42619
- return {
42620
- plots: [
42621
- { key: "kvo", title: "KVO", values: line, color: SERIES_LINE, width: 2 },
42622
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: WARNING }
42623
- ],
42624
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
42625
- };
43791
+ const n = bars.length;
43792
+ const force = new Array(n).fill(Number.NaN);
43793
+ let trend = 1;
43794
+ let prevTrend = 1;
43795
+ let cm = 0;
43796
+ let prevDm = 0;
43797
+ for (let i = 0; i < n; i++) {
43798
+ const b = bars[i];
43799
+ const hlc = b.high + b.low + b.close;
43800
+ const prevHlc = i > 0 ? bars[i - 1].high + bars[i - 1].low + bars[i - 1].close : hlc;
43801
+ trend = hlc > prevHlc ? 1 : -1;
43802
+ const dm = b.high - b.low;
43803
+ cm = i > 0 && trend === prevTrend ? cm + dm : prevDm + dm;
43804
+ const v = b.volume;
43805
+ force[i] = v == null || !Number.isFinite(v) ? Number.NaN : cm === 0 ? 0 : v * Math.abs(2 * (dm / cm) - 1) * trend * 100;
43806
+ prevTrend = trend;
43807
+ prevDm = dm;
43808
+ }
43809
+ const line = sub(ema(force, num3(inputs, "fastLength", 34)), ema(force, num3(inputs, "slowLength", 55)));
43810
+ const bull = str3(inputs, "color", BULLISH);
43811
+ return centeredOscillator({
43812
+ key: "kvo",
43813
+ title: "KVO",
43814
+ values: line,
43815
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
43816
+ zeroInk: transp(NEUTRAL, 50),
43817
+ valueTransparency: 60,
43818
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: str3(inputs, "signalColor", WARNING) }]
43819
+ });
42626
43820
  }
42627
43821
  };
42628
43822
  var mfi = {
@@ -42630,7 +43824,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42630
43824
  title: "Money Flow Index",
42631
43825
  shortTitle: "MFI",
42632
43826
  overlay: false,
42633
- inputs: [lengthInput(14), colorInput()],
43827
+ inputs: [
43828
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to sum positive and negative money flow."),
43829
+ floatInput("overbought", "Overbought Level", 80, 0, 100, 1, "Level above which the MFI is considered overbought."),
43830
+ floatInput("oversold", "Oversold Level", 20, 0, 100, 1, "Level below which the MFI is considered oversold."),
43831
+ colorInput(NEUTRAL, "MFI", "color", "Color of the MFI line between the levels. Toward either level it blends into that level\u2019s color."),
43832
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
43833
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level."),
43834
+ colorInput(transp(NEUTRAL, 94), "OB/OS Zone Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
43835
+ ],
42634
43836
  compute: (bars, inputs) => {
42635
43837
  const len = num3(inputs, "length", 14);
42636
43838
  const tp = sourceValues(bars, "HLC3");
@@ -42638,48 +43840,57 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42638
43840
  const d = change(tp);
42639
43841
  const pos = sum(zip(rawFlow, d, (f, x) => x > 0 ? f : 0), len);
42640
43842
  const neg = sum(zip(rawFlow, d, (f, x) => x < 0 ? f : 0), len);
42641
- const values = zip(pos, neg, (p, n) => n === 0 ? 100 : 100 - 100 / (1 + p / n));
42642
- return {
42643
- plots: [{ key: "mfi", title: "MFI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42644
- levels: [
42645
- { key: "upper", price: 80, color: NEUTRAL },
42646
- { key: "lower", price: 20, color: NEUTRAL }
42647
- ]
42648
- };
43843
+ const values = zip(pos, neg, (p, w) => p + w > 0 ? 100 * p / (p + w) : Number.NaN);
43844
+ return boundedOscillator({
43845
+ key: "mfi",
43846
+ title: "MFI",
43847
+ values,
43848
+ scale: { top: 100, bottom: 0 },
43849
+ overbought: num3(inputs, "overbought", 80),
43850
+ oversold: num3(inputs, "oversold", 20),
43851
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
43852
+ levelInk: NEUTRAL,
43853
+ zoneTransparency: 91,
43854
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 94))
43855
+ });
42649
43856
  }
42650
43857
  };
42651
- function volumeIndex(bars, when) {
42652
- const out = new Array(bars.length).fill(Number.NaN);
42653
- let index = 1e3;
42654
- let started = false;
42655
- for (let i = 1; i < bars.length; i++) {
42656
- const v = bars[i].volume;
42657
- const pv = bars[i - 1].volume;
42658
- const pc = bars[i - 1].close;
42659
- if (v == null || pv == null || !Number.isFinite(v) || !Number.isFinite(pv) || pc === 0) {
42660
- if (started) out[i] = index;
42661
- continue;
42662
- }
42663
- started = true;
42664
- if (when(v, pv)) index += (bars[i].close - pc) / pc * index;
42665
- out[i] = index;
42666
- }
42667
- return out;
43858
+ function regimeStyleInputs(signalTitle) {
43859
+ return [
43860
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the line and gradient fill while it holds above its signal EMA."),
43861
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line and gradient fill while it holds below its signal EMA."),
43862
+ colorInput(NEUTRAL, "Neutral", "neutralColor", "Color used while the signal EMA is still forming, or the line sits exactly on it."),
43863
+ colorInput(NEUTRAL, signalTitle, "signalColor", "Color of the signal EMA overlay."),
43864
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the gap between the line and its signal EMA with a vertical gradient \u2014 transparent at the average, strongest at the line.")
43865
+ ];
42668
43866
  }
43867
+ var NVI_POSITION = "Position vs Average";
43868
+ var NVI_SINGLE = "Single Color";
42669
43869
  var nvi = {
42670
43870
  type: "negative-volume-index",
42671
43871
  title: "Negative Volume Index",
42672
43872
  shortTitle: "Negative Volume Index",
42673
43873
  overlay: false,
42674
- inputs: [lengthInput(255, "signalLength", "Signal length"), colorInput()],
43874
+ inputs: [
43875
+ floatInput("base", "Base Level", 1e3, 0.01, 1e6, 1, "Arbitrary seed of the cumulative line, conventionally 1000. The absolute level carries no information \u2014 the line is read by slope and by position against the signal EMA."),
43876
+ boolInput("showSignal", "Signal EMA", true, SETTINGS, "Displays the conventional signal overlay. Hiding it only removes the plotted average; the regime coloring and fill still use it."),
43877
+ lengthInput(255, "signalLength", "Signal Length", 5e3, "Length of the signal EMA. The convention is one trading year \u2014 255 sessions on daily charts."),
43878
+ optionInput("colorMode", "Line Coloring", NVI_POSITION, [NVI_POSITION, NVI_SINGLE], "'Position vs Average' colors the line by the classic regime read; 'Single Color' draws the whole line in the neutral color."),
43879
+ ...regimeStyleInputs("Signal EMA")
43880
+ ],
42675
43881
  compute: (bars, inputs) => {
42676
- const line = volumeIndex(bars, (v, pv) => v < pv);
42677
- return {
42678
- plots: [
42679
- { key: "nvi", title: "NVI", values: line, color: str3(inputs, "color", SERIES_LINE), width: 2 },
42680
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 255)), color: WARNING }
42681
- ]
42682
- };
43882
+ const values = volumeIndex(bars, closes(bars), num3(inputs, "base", 1e3), (v, pv) => v < pv);
43883
+ const signal = ema(values, num3(inputs, "signalLength", 255));
43884
+ return regimeIndex(
43885
+ "nvi",
43886
+ "NVI",
43887
+ values,
43888
+ signal,
43889
+ { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "neutralColor", NEUTRAL), signal: str3(inputs, "signalColor", NEUTRAL) },
43890
+ bool(inputs, "showSignal", true),
43891
+ bool(inputs, "gradientFill", true),
43892
+ str3(inputs, "colorMode", NVI_POSITION) === NVI_SINGLE
43893
+ );
42683
43894
  }
42684
43895
  };
42685
43896
  var pvi = {
@@ -42687,15 +43898,26 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42687
43898
  title: "Positive Volume Index",
42688
43899
  shortTitle: "Positive Volume Index",
42689
43900
  overlay: false,
42690
- inputs: [lengthInput(255, "signalLength", "Signal length"), colorInput()],
43901
+ inputs: [
43902
+ sourceInput("Close", "source", "Source", "Price series whose one-bar percentage change moves PVI on its active sessions."),
43903
+ floatInput("base", "Starting Value", 1e3, 1, 1e6, 1, "Seed of the cumulative line. The absolute level carries no information, so the reads are the slope and the position against the signal EMA."),
43904
+ boolInput("showSignal", "Signal EMA", true, SETTINGS, "Plots the long EMA of PVI that anchors the classic regime read. Hiding it keeps the regime coloring and fill, which are always measured against it."),
43905
+ lengthInput(255, "signalLength", "Signal Length", 5e3, "Length of the signal EMA. The classic reference is a one-year average \u2014 255 trading days on daily charts."),
43906
+ ...regimeStyleInputs("Signal Line")
43907
+ ],
42691
43908
  compute: (bars, inputs) => {
42692
- const line = volumeIndex(bars, (v, pv) => v > pv);
42693
- return {
42694
- plots: [
42695
- { key: "pvi", title: "PVI", values: line, color: str3(inputs, "color", SERIES_LINE), width: 2 },
42696
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 255)), color: WARNING }
42697
- ]
42698
- };
43909
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
43910
+ const values = volumeIndex(bars, src, num3(inputs, "base", 1e3), (v, pv) => v > pv);
43911
+ const signal = ema(values, num3(inputs, "signalLength", 255));
43912
+ return regimeIndex(
43913
+ "pvi",
43914
+ "PVI",
43915
+ values,
43916
+ signal,
43917
+ { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "neutralColor", NEUTRAL), signal: str3(inputs, "signalColor", NEUTRAL) },
43918
+ bool(inputs, "showSignal", true),
43919
+ bool(inputs, "gradientFill", true)
43920
+ );
42699
43921
  }
42700
43922
  };
42701
43923
  var pvt = {
@@ -42703,7 +43925,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42703
43925
  title: "Price Volume Trend",
42704
43926
  shortTitle: "PVT",
42705
43927
  overlay: false,
42706
- inputs: [colorInput()],
43928
+ inputs: [
43929
+ boolInput("showSignal", "Signal Line", false, SETTINGS, "Display a moving average of PVT acting as a signal line."),
43930
+ optionInput("signalType", "Signal Type", "SMA", ["SMA", "EMA"], "Moving average type used for the signal line."),
43931
+ lengthInput(21, "signalLength", "Signal Length", 5e3, "Number of bars used to compute the signal line."),
43932
+ colorInput(NEUTRAL, "PVT Color", "color", "Color of the PVT line while it is flat."),
43933
+ colorInput(BULLISH, "Rising", "bullColor", "Color of the PVT line while it rises bar to bar."),
43934
+ colorInput(BEARISH, "Falling", "bearColor", "Color of the PVT line while it falls bar to bar."),
43935
+ colorInput(WARNING, "Signal Color", "signalColor", "Color of the signal line.")
43936
+ ],
42707
43937
  compute: (bars, inputs) => {
42708
43938
  const c = closes(bars);
42709
43939
  const raw = bars.map((b, i) => {
@@ -42711,7 +43941,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42711
43941
  const pc = c[i - 1];
42712
43942
  return pc === 0 ? 0 : (c[i] - pc) / pc * b.volume;
42713
43943
  });
42714
- return { plots: [{ key: "pvt", title: "PVT", values: cumSum(raw), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
43944
+ const values = cumSum(raw);
43945
+ const flat = str3(inputs, "color", NEUTRAL);
43946
+ const plots = [
43947
+ { key: "pvt", title: "PVT", values, color: flat, width: 2, colors: slopeColors2(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), flat) }
43948
+ ];
43949
+ if (bool(inputs, "showSignal", false)) {
43950
+ const len = num3(inputs, "signalLength", 21);
43951
+ const signal = str3(inputs, "signalType", "SMA") === "EMA" ? ema(values, len) : sma(values, len);
43952
+ plots.push({ key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) });
43953
+ }
43954
+ return { plots };
42715
43955
  }
42716
43956
  };
42717
43957
  var volumeOscillator = {
@@ -42719,39 +43959,75 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42719
43959
  title: "Volume Oscillator",
42720
43960
  shortTitle: "Vol Osc",
42721
43961
  overlay: false,
42722
- inputs: [lengthInput(5, "fastLength", "Fast length"), lengthInput(10, "slowLength", "Slow length"), colorInput()],
43962
+ inputs: [
43963
+ lengthInput(5, "fastLength", "Fast Length", 5e3, "Length of the fast EMA of volume."),
43964
+ lengthInput(10, "slowLength", "Slow Length", 5e3, "Length of the slow EMA of volume."),
43965
+ colorInput(BULLISH, "Above Zero", "color", "Color of the line and fill while it holds above zero \u2014 volume expanding."),
43966
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while it holds below zero \u2014 volume contracting.")
43967
+ ],
42723
43968
  compute: (bars, inputs) => {
42724
43969
  const vol = volumes(bars);
42725
43970
  const fast = ema(vol, num3(inputs, "fastLength", 5));
42726
43971
  const slow = ema(vol, num3(inputs, "slowLength", 10));
42727
- const values = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : 100 * (f - s) / s);
42728
- return {
42729
- plots: [{ key: "vo", title: "Volume Osc", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42730
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
42731
- };
43972
+ const values = zip(fast, slow, (f, s) => s === 0 ? 0 : 100 * (f - s) / s);
43973
+ const bull = str3(inputs, "color", BULLISH);
43974
+ return centeredOscillator({
43975
+ key: "vo",
43976
+ title: "Volume Oscillator",
43977
+ values,
43978
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
43979
+ zeroInk: NEUTRAL,
43980
+ valueTransparency: 60
43981
+ });
42732
43982
  }
42733
43983
  };
43984
+ var OSCILLATOR = "Oscillator";
42734
43985
  var pvo = {
42735
43986
  type: "pvo",
42736
43987
  title: "Percentage Volume Oscillator",
42737
43988
  shortTitle: "PVO",
42738
43989
  overlay: false,
42739
- inputs: [lengthInput(12, "fastLength", "Fast length"), lengthInput(26, "slowLength", "Slow length"), lengthInput(9, "signalLength", "Signal length")],
43990
+ inputs: [
43991
+ { ...lengthInput(12, "fastLength", "Fast Length", 5e3, "Period of the fast EMA of volume. The conventional setting is 12."), group: OSCILLATOR },
43992
+ { ...lengthInput(26, "slowLength", "Slow Length", 5e3, "Period of the slow EMA of volume. The PVO expresses the fast/slow spread as a percentage of this slow average."), group: OSCILLATOR },
43993
+ { ...lengthInput(9, "signalLength", "Signal Length", 500, "Period of the EMA applied to the PVO to form the signal line."), group: OSCILLATOR },
43994
+ colorInput(BULLISH, "Expansion Color", "color", "Color of the oscillator and fill while participation expands."),
43995
+ colorInput(BEARISH, "Contraction Color", "bearColor", "Color of the oscillator and fill while participation contracts."),
43996
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
43997
+ colorInput(transp(NEUTRAL, 50), "Zero Line", "zeroColor", "Color of the zero reference line."),
43998
+ boolInput("showSignal", "Show Signal Line", true, STYLE, "Plot the signal line, an EMA of the PVO."),
43999
+ boolInput("showHistogram", "Show Histogram", true, STYLE, "Plot the difference between the PVO and its signal line as columns behind the oscillator.")
44000
+ ],
42740
44001
  compute: (bars, inputs) => {
42741
44002
  const vol = volumes(bars);
42742
44003
  const fast = ema(vol, num3(inputs, "fastLength", 12));
42743
44004
  const slow = ema(vol, num3(inputs, "slowLength", 26));
42744
- const line = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : 100 * (f - s) / s);
42745
- const signal = ema(line, num3(inputs, "signalLength", 9));
42746
- const hist = sub(line, signal);
42747
- return {
42748
- plots: [
42749
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: hist.map((x) => Number.isFinite(x) ? x >= 0 ? BULLISH : BEARISH : null), base: 0 },
42750
- { key: "pvo", title: "PVO", values: line, color: SERIES_LINE, width: 2 },
42751
- { key: "signal", title: "Signal", values: signal, color: WARNING }
42752
- ],
42753
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
42754
- };
44005
+ const values = zip(fast, slow, (f, s) => s > 0 ? 100 * (f - s) / s : Number.NaN);
44006
+ const signal = ema(values, num3(inputs, "signalLength", 9));
44007
+ const expansion = str3(inputs, "color", BULLISH);
44008
+ const contraction = str3(inputs, "bearColor", BEARISH);
44009
+ const extraPlots = [];
44010
+ if (bool(inputs, "showHistogram", true)) {
44011
+ const hist = sub(values, signal);
44012
+ extraPlots.push({
44013
+ key: "hist",
44014
+ title: "Histogram",
44015
+ values: hist,
44016
+ kind: "columns",
44017
+ color: transp(expansion, 60),
44018
+ colors: hist.map((x) => Number.isFinite(x) ? transp(x > 0 ? expansion : contraction, 60) : null),
44019
+ base: 0
44020
+ });
44021
+ }
44022
+ if (bool(inputs, "showSignal", true)) extraPlots.push({ key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) });
44023
+ return centeredOscillator({
44024
+ key: "pvo",
44025
+ title: "PVO",
44026
+ values,
44027
+ ink: { bull: expansion, bear: contraction, neutral: contraction },
44028
+ zeroInk: str3(inputs, "zeroColor", transp(NEUTRAL, 50)),
44029
+ extraPlots
44030
+ });
42755
44031
  }
42756
44032
  };
42757
44033
  var vfi = {
@@ -42760,55 +44036,86 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42760
44036
  shortTitle: "Volume Flow Indicator",
42761
44037
  overlay: false,
42762
44038
  inputs: [
42763
- lengthInput(130),
42764
- { key: "coef", title: "Cutoff coefficient", type: "float", defval: 0.2, min: 0.01, max: 10, step: 0.01 },
42765
- { key: "volCoef", title: "Volume cap", type: "float", defval: 2.5, min: 0.1, max: 50, step: 0.1 },
42766
- lengthInput(3, "smoothLength", "Smoothing length")
44039
+ { ...lengthInput(130, "length", "Length", 5e3, "Rolling summation window, also used for the average volume that scales the result. The published default of 130 bars targets position-horizon work on daily charts."), min: 2 },
44040
+ floatInput("coef", "Cutoff Coefficient", 0.2, 0, 10, 0.05, "Scales the volatility-based minimum change in typical price. Moves smaller than the cutoff count as noise and contribute no volume."),
44041
+ floatInput("volCoef", "Volume Cap Multiplier", 2.5, 0.1, 50, 0.1, "Caps each bar's volume contribution at this multiple of the average volume, so a single climactic print stays bounded."),
44042
+ { ...lengthInput(30, "volLength", "Volatility Length", 5e3, "Window of the standard deviation of the log change in typical price used to build the adaptive cutoff."), min: 2 },
44043
+ boolInput("smooth", "Smooth VFI", true, SETTINGS, "Applies the published light smoothing \u2014 an EMA of the summed line \u2014 before plotting."),
44044
+ lengthInput(3, "smoothLength", "Smoothing Length", 500, "Length of the smoothing EMA. The published version uses 3 bars."),
44045
+ boolInput("showSignal", "Signal Line", true, SETTINGS, "Plots an EMA of VFI as a signal line."),
44046
+ lengthInput(5, "signalLength", "Signal Length", 500, "Length of the signal-line EMA."),
44047
+ colorInput(BULLISH, "Bullish", "color", "Color of the VFI line and gradient fill above the zero line \u2014 the net accumulation regime."),
44048
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the VFI line and gradient fill below the zero line \u2014 the net distribution regime."),
44049
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
44050
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the zero line and the VFI line with a vertical gradient.")
42767
44051
  ],
42768
44052
  compute: (bars, inputs) => {
42769
44053
  const len = num3(inputs, "length", 130);
44054
+ const typical = sourceValues(bars, "HLC3");
44055
+ const logChange = sub(map(typical, Math.log), shift3(map(typical, Math.log), 1));
44056
+ const volatility = stdev(logChange, num3(inputs, "volLength", 30));
42770
44057
  const coef = num3(inputs, "coef", 0.2);
42771
44058
  const volCoef = num3(inputs, "volCoef", 2.5);
42772
- const typical = map(sourceValues(bars, "HLC3"), Math.log);
42773
- const inter = change(typical);
42774
- const vInter = stdev(inter, 30);
42775
44059
  const vol = volumes(bars);
42776
- const vAve = sma(vol, len);
42777
- const mf = change(sourceValues(bars, "HLC3"));
42778
- const raw = bars.map((b, i) => {
42779
- const cutoff = Number.isFinite(vInter[i]) ? coef * vInter[i] * b.close : Number.NaN;
44060
+ const avgVolume = shift3(sma(vol, len), 1);
44061
+ const typicalChange = change(typical);
44062
+ const signed3 = bars.map((b, i) => {
44063
+ const sd = volatility[i];
44064
+ const m = typicalChange[i];
42780
44065
  const v = vol[i];
42781
- const ave = i > 0 ? vAve[i - 1] : Number.NaN;
42782
- const m = mf[i];
42783
- if (!Number.isFinite(cutoff) || !Number.isFinite(v) || !Number.isFinite(ave) || !Number.isFinite(m)) return Number.NaN;
42784
- const capped = Math.min(v, ave * volCoef);
44066
+ if (!Number.isFinite(sd) || !Number.isFinite(m) || !Number.isFinite(v)) return Number.NaN;
44067
+ const cutoff = coef * sd * b.close;
44068
+ const avg = avgVolume[i];
44069
+ const capped = Number.isFinite(avg) ? Math.min(v, avg * volCoef) : v;
42785
44070
  return m > cutoff ? capped : m < -cutoff ? -capped : 0;
42786
44071
  });
42787
- const values = bars.map((_, i) => {
42788
- const ave = i > 0 ? vAve[i - 1] : Number.NaN;
42789
- return Number.isFinite(ave) && ave !== 0 ? ave : Number.NaN;
44072
+ const raw = zip(sum(signed3, len), avgVolume, (f, a) => a > 0 ? f / a : Number.NaN);
44073
+ const values = bool(inputs, "smooth", true) ? ema(raw, num3(inputs, "smoothLength", 3)) : raw;
44074
+ const bull = str3(inputs, "color", BULLISH);
44075
+ const extraPlots = bool(inputs, "showSignal", true) ? [{ key: "signal", title: "Signal Line", values: ema(values, num3(inputs, "signalLength", 5)), color: str3(inputs, "signalColor", WARNING) }] : [];
44076
+ return centeredOscillator({
44077
+ key: "vfi",
44078
+ title: "VFI",
44079
+ values,
44080
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "bearColor", BEARISH) },
44081
+ zeroInk: transp(NEUTRAL, 90),
44082
+ fill: bool(inputs, "gradientFill", true),
44083
+ extraPlots
42790
44084
  });
42791
- const flowSum = sum(raw, len);
42792
- const line = zip(flowSum, values, (f, a) => f / a);
42793
- return {
42794
- plots: [{ key: "vfi", title: "VFI", values: sma(line, num3(inputs, "smoothLength", 3)), color: SERIES_LINE, width: 2 }],
42795
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
42796
- };
42797
44085
  }
42798
44086
  };
44087
+ var II_OSCILLATOR = "Normalized Oscillator (II%)";
44088
+ var II_CUMULATIVE = "Cumulative Line";
42799
44089
  var intradayIntensity = {
42800
44090
  type: "intraday-intensity",
42801
44091
  title: "Intraday Intensity",
42802
44092
  shortTitle: "Intraday Intensity",
42803
44093
  overlay: false,
42804
- inputs: [lengthInput(21), colorInput()],
44094
+ inputs: [
44095
+ optionInput("mode", "Display", II_OSCILLATOR, [II_OSCILLATOR, II_CUMULATIVE], "The two published forms. The cumulative line is open-ended and read by slope against price; the normalized oscillator divides a rolling sum of intensity by the rolling sum of volume and oscillates around zero."),
44096
+ lengthInput(21, "length", "Oscillator Length", 5e3, "Window of the rolling sums in the normalized form. The common setting is 21 bars. It has no effect on the cumulative line."),
44097
+ colorInput(BULLISH, "Bullish", "color", "Color of positive readings, the rising cumulative line and the fill above zero \u2014 the accumulation side."),
44098
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of negative readings, the falling cumulative line and the fill below zero \u2014 the distribution side."),
44099
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the zero line and the oscillator with a vertical gradient. Applies to the normalized oscillator only.")
44100
+ ],
42805
44101
  compute: (bars, inputs) => {
44102
+ const intensity = moneyFlowVolume(bars);
44103
+ const bull = str3(inputs, "color", BULLISH);
44104
+ const bear = str3(inputs, "bearColor", BEARISH);
44105
+ if (str3(inputs, "mode", II_OSCILLATOR) === II_CUMULATIVE) {
44106
+ const values2 = cumSum(intensity);
44107
+ return { plots: [{ key: "ii", title: "Intraday Intensity", values: values2, color: bull, colors: slopeColors2(values2, bull, bear, bull) }] };
44108
+ }
42806
44109
  const len = num3(inputs, "length", 21);
42807
- const values = zip(sum(moneyFlowVolume(bars), len), sum(volumes(bars), len), (m, v) => v === 0 ? 0 : 100 * m / v);
42808
- return {
42809
- plots: [{ key: "ii", title: "II%", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
42810
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
42811
- };
44110
+ const values = zip(sum(intensity, len), sum(volumes(bars), len), (m, v) => v > 0 ? 100 * m / v : Number.NaN);
44111
+ return centeredOscillator({
44112
+ key: "ii",
44113
+ title: "Intraday Intensity",
44114
+ values,
44115
+ ink: { bull, bear, neutral: bear },
44116
+ zeroInk: transp(NEUTRAL, 90),
44117
+ fill: bool(inputs, "gradientFill", true)
44118
+ });
42812
44119
  }
42813
44120
  };
42814
44121
  var volumeSpecs = [
@@ -42841,15 +44148,24 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42841
44148
  }
42842
44149
  return Math.floor(time / DAY_MS2);
42843
44150
  }
44151
+ function barInterval(bars) {
44152
+ if (bars.length < 2) return 0;
44153
+ const gaps = [];
44154
+ for (let i = 1; i < bars.length; i++) gaps.push(bars[i].time - bars[i - 1].time);
44155
+ gaps.sort((a, b) => a - b);
44156
+ return gaps[Math.floor(gaps.length / 2)] ?? 0;
44157
+ }
42844
44158
  var parabolicSar = {
42845
44159
  type: "parabolic-sar",
42846
44160
  title: "Parabolic SAR",
42847
44161
  shortTitle: "SAR",
42848
44162
  overlay: true,
42849
44163
  inputs: [
42850
- { key: "start", title: "Start", type: "float", defval: 0.02, min: 1e-3, max: 1, step: 1e-3 },
42851
- { key: "increment", title: "Increment", type: "float", defval: 0.02, min: 1e-3, max: 1, step: 1e-3 },
42852
- { key: "maximum", title: "Max value", type: "float", defval: 0.2, min: 0.01, max: 1, step: 0.01 }
44164
+ floatInput("start", "Start", 0.02, 0, 1, 0.01, "Initial acceleration factor applied when a new trend begins."),
44165
+ floatInput("increment", "Increment", 0.02, 0, 1, 0.01, "Amount added to the acceleration factor each time the trend makes a new extreme point."),
44166
+ floatInput("maximum", "Maximum", 0.2, 0.01, 1, 0.01, "Upper cap on the acceleration factor."),
44167
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the SAR crosses while they trail below price \u2014 uptrend, the stop sits under the market."),
44168
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the SAR crosses while they sit above price \u2014 downtrend, the stop sits over the market.")
42853
44169
  ],
42854
44170
  compute: (bars, inputs) => {
42855
44171
  const start = num3(inputs, "start", 0.02);
@@ -42857,7 +44173,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42857
44173
  const max2 = num3(inputs, "maximum", 0.2);
42858
44174
  const n = bars.length;
42859
44175
  const values = new Array(n).fill(Number.NaN);
42860
- const colors = new Array(n).fill(null);
42861
44176
  if (n >= 2) {
42862
44177
  let long = bars[1].close >= bars[0].close;
42863
44178
  let sar = long ? bars[0].low : bars[0].high;
@@ -42882,18 +44197,21 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42882
44197
  af = Math.min(max2, af + inc);
42883
44198
  }
42884
44199
  values[i] = sar;
42885
- colors[i] = long ? BULLISH : BEARISH;
42886
44200
  }
42887
44201
  }
42888
- return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
44202
+ const bull = str3(inputs, "bullColor", BULLISH);
44203
+ const bear = str3(inputs, "bearColor", BEARISH);
44204
+ const colors = values.map((x, i) => Number.isFinite(x) ? x < bars[i].close ? bull : bear : null);
44205
+ return { plots: [{ key: "sar", title: "SAR", values, kind: "cross", color: bull, colors }] };
42889
44206
  }
42890
44207
  };
42891
- var VWAP_BANDS_GROUP = "Bands";
42892
- var VWAP_STYLE = "Style";
44208
+ var VWAP_ANCHORS = ["Session", "Week", "Month", "Quarter", "Year"];
44209
+ var BANDS_STDEV = "Standard Deviation";
44210
+ var BANDS_PERCENT = "Percentage";
42893
44211
  var VWAP_BANDS = [
42894
- { on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
42895
- { on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
42896
- { on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
44212
+ { show: "band1", mult: "band1Mult", defMult: 1, defOn: true },
44213
+ { show: "band2", mult: "band2Mult", defMult: 2, defOn: false },
44214
+ { show: "band3", mult: "band3Mult", defMult: 3, defOn: false }
42897
44215
  ];
42898
44216
  var vwap = {
42899
44217
  type: "vwap",
@@ -42901,39 +44219,36 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42901
44219
  shortTitle: "VWAP",
42902
44220
  overlay: true,
42903
44221
  inputs: [
42904
- { key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
42905
- sourceInput("HLC3"),
42906
- // Each band is one row: its toggle leads, the multiplier follows unlabeled.
44222
+ optionInput("anchor", "Anchor Period", "Session", VWAP_ANCHORS, "Period anchoring the average. VWAP and its bands reset on the first bar of each new session, week, month, quarter or year (UTC)."),
44223
+ sourceInput("HLC3", "source", "Source", "Price used in the volume weighted average. Typical price (HLC3) is the classic choice."),
44224
+ optionInput("bandsMode", "Bands Mode", BANDS_STDEV, [BANDS_STDEV, BANDS_PERCENT], "Bands offset method: multiples of the volume weighted standard deviation of price around VWAP, or a percentage of the VWAP value."),
42907
44225
  ...VWAP_BANDS.flatMap((b, i) => [
42908
- { key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
42909
- { key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
44226
+ boolInput(b.show, `Band #${i + 1}`, b.defOn, SETTINGS, `Show the ${["first", "second", "third"][i]} band pair.`),
44227
+ floatInput(b.mult, `Band #${i + 1} Multiplier`, b.defMult, 0, 50, 0.5, "Multiplier for this band pair, in standard deviations or percent depending on the bands mode.")
42910
44228
  ]),
42911
- { ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
42912
- // One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
42913
- ...VWAP_BANDS.flatMap((b, i) => [
42914
- { ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
42915
- { key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
42916
- { ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
42917
- ])
44229
+ boolInput("hideDwm", "Hide VWAP on 1D or Above", true, SETTINGS, "Hide the VWAP and its bands on daily and higher timeframes, where an intraday anchored VWAP is not meaningful."),
44230
+ colorInput(BULLISH, "Bullish", "bullColor", "VWAP line color while price closes above it, and the color of the lower (support) bands."),
44231
+ colorInput(BEARISH, "Bearish", "bearColor", "VWAP line color while price closes below it, and the color of the upper (resistance) bands."),
44232
+ colorInput(NEUTRAL, "Bands", "bandsColor", "Deviation bands fill color."),
44233
+ boolInput("fill", "Bands Fill", true, STYLE, "Fill the area between each visible band pair.")
42918
44234
  ],
42919
44235
  compute: (bars, inputs) => {
42920
- const anchor = str3(inputs, "anchor", "Day");
44236
+ const anchor = str3(inputs, "anchor", "Session");
42921
44237
  const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
42922
44238
  const n = bars.length;
44239
+ const hidden = bool(inputs, "hideDwm", true) && barInterval(bars) >= DAY_MS2;
42923
44240
  const values = new Array(n).fill(Number.NaN);
42924
- const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
44241
+ const bands = VWAP_BANDS.filter((b) => bool(inputs, b.show, b.defOn)).map((b) => ({
42925
44242
  mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
42926
- color: str3(inputs, b.color, b.defColor),
42927
- fill: bool(inputs, b.fill, true),
42928
- fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
42929
44243
  up: new Array(n).fill(Number.NaN),
42930
44244
  down: new Array(n).fill(Number.NaN)
42931
44245
  }));
44246
+ const percentMode = str3(inputs, "bandsMode", BANDS_STDEV) === BANDS_PERCENT;
42932
44247
  let period = Number.NaN;
42933
44248
  let cumPV = 0;
42934
44249
  let cumPV2 = 0;
42935
44250
  let cumV = 0;
42936
- for (let i = 0; i < n; i++) {
44251
+ for (let i = 0; i < n && !hidden; i++) {
42937
44252
  const b = bars[i];
42938
44253
  const key = periodKey(anchor, b.time);
42939
44254
  if (key !== period) {
@@ -42951,38 +44266,58 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42951
44266
  }
42952
44267
  if (cumV <= 0) continue;
42953
44268
  const mean = cumPV / cumV;
42954
- const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
44269
+ const offset3 = percentMode ? mean / 100 : Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
42955
44270
  values[i] = mean;
42956
44271
  for (const band of bands) {
42957
- band.up[i] = mean + band.mult * sd;
42958
- band.down[i] = mean - band.mult * sd;
44272
+ band.up[i] = mean + band.mult * offset3;
44273
+ band.down[i] = mean - band.mult * offset3;
42959
44274
  }
42960
44275
  }
42961
- const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
44276
+ const bull = str3(inputs, "bullColor", BULLISH);
44277
+ const bear = str3(inputs, "bearColor", BEARISH);
44278
+ const fillColor = transp(str3(inputs, "bandsColor", NEUTRAL), 95);
44279
+ const plots = [
44280
+ { key: "vwap", title: "VWAP", values, color: bull, colors: values.map((x, i) => Number.isFinite(x) ? bars[i].close >= x ? bull : bear : null) }
44281
+ ];
42962
44282
  const fills = [];
42963
44283
  bands.forEach((band, i) => {
42964
44284
  plots.push(
42965
- { key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
42966
- { key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
44285
+ { key: `up${i}`, title: `Upper Band #${i + 1}`, values: band.up, color: transp(bear, 40) },
44286
+ { key: `down${i}`, title: `Lower Band #${i + 1}`, values: band.down, color: transp(bull, 40) }
42967
44287
  );
42968
- if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
44288
+ if (bool(inputs, "fill", true)) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: fillColor });
42969
44289
  });
42970
44290
  return { plots, bands: fills };
42971
44291
  }
42972
44292
  };
44293
+ var PIVOT_TRADITIONAL = "Traditional";
44294
+ var PIVOT_FIBONACCI = "Fibonacci";
44295
+ var PIVOT_CAMARILLA = "Camarilla";
44296
+ var PIVOT_WOODIE = "Woodie";
44297
+ var PIVOT_AUTO = "Auto";
42973
44298
  var pivotPoints = {
42974
44299
  type: "pivot-points",
42975
- title: "Pivot Points",
44300
+ title: "Pivot Points Standard",
42976
44301
  shortTitle: "Pivots",
42977
44302
  overlay: true,
42978
- inputs: [{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month"] }],
44303
+ inputs: [
44304
+ optionInput("kind", "Type", PIVOT_TRADITIONAL, [PIVOT_TRADITIONAL, PIVOT_FIBONACCI, PIVOT_CAMARILLA, PIVOT_WOODIE], "Formula used to calculate the levels. Traditional is the classic floor-trader method; Fibonacci projects 38.2/61.8/100% of the prior range around the pivot; Camarilla scales the prior range around the prior close; Woodie gives extra weight to the period open."),
44305
+ optionInput("anchor", "Pivots Timeframe", PIVOT_AUTO, [PIVOT_AUTO, "Daily", "Weekly", "Monthly", "Yearly"], "Period the pivots are anchored to. 'Auto' follows the chart's bar spacing: daily pivots up to 15-minute bars, weekly on other intraday bars, monthly on daily bars, yearly above."),
44306
+ colorInput(WARNING, "Pivot", "pivotColor", "Color of the central pivot (P) level."),
44307
+ colorInput(BEARISH, "Resistances", "resistanceColor", "Color of the resistance (R) levels."),
44308
+ colorInput(BULLISH, "Supports", "supportColor", "Color of the support (S) levels.")
44309
+ ],
42979
44310
  compute: (bars, inputs) => {
42980
- const anchor = str3(inputs, "anchor", "Day");
44311
+ const interval = barInterval(bars);
44312
+ const requested = str3(inputs, "anchor", PIVOT_AUTO);
44313
+ const auto = interval <= 15 * 6e4 ? "Session" : interval < DAY_MS2 ? "Week" : interval <= DAY_MS2 ? "Month" : "Year";
44314
+ const anchor = requested === "Daily" ? "Session" : requested === "Weekly" ? "Week" : requested === "Monthly" ? "Month" : requested === "Yearly" ? "Year" : auto;
44315
+ const kind = str3(inputs, "kind", PIVOT_TRADITIONAL);
42981
44316
  const aggs = /* @__PURE__ */ new Map();
42982
44317
  for (const b of bars) {
42983
44318
  const key = periodKey(anchor, b.time);
42984
44319
  const a = aggs.get(key);
42985
- if (!a) aggs.set(key, { high: b.high, low: b.low, close: b.close });
44320
+ if (!a) aggs.set(key, { open: b.open, high: b.high, low: b.low, close: b.close });
42986
44321
  else {
42987
44322
  a.high = Math.max(a.high, b.high);
42988
44323
  a.low = Math.min(a.low, b.low);
@@ -42991,31 +44326,61 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42991
44326
  }
42992
44327
  const n = bars.length;
42993
44328
  const mk = () => new Array(n).fill(Number.NaN);
42994
- const levels = { p: mk(), r1: mk(), s1: mk(), r2: mk(), s2: mk(), r3: mk(), s3: mk() };
44329
+ const levels = { p: mk(), r1: mk(), s1: mk(), r2: mk(), s2: mk(), r3: mk(), s3: mk(), r4: mk(), s4: mk() };
44330
+ let lastKey = Number.NaN;
42995
44331
  for (let i = 0; i < n; i++) {
42996
- const prev3 = aggs.get(periodKey(anchor, bars[i].time) - 1);
42997
- if (!prev3) continue;
42998
- const p = (prev3.high + prev3.low + prev3.close) / 3;
44332
+ const key = periodKey(anchor, bars[i].time);
44333
+ const fresh = key !== lastKey;
44334
+ lastKey = key;
44335
+ if (fresh) continue;
44336
+ const prev3 = aggs.get(key - 1);
44337
+ const curr = aggs.get(key);
44338
+ if (!prev3 || !curr) continue;
44339
+ const range = prev3.high - prev3.low;
44340
+ const traditional = (prev3.high + prev3.low + prev3.close) / 3;
44341
+ const p = kind === PIVOT_WOODIE ? (prev3.high + prev3.low + 2 * curr.open) / 4 : traditional;
42999
44342
  levels.p[i] = p;
43000
- levels.r1[i] = 2 * p - prev3.low;
43001
- levels.s1[i] = 2 * p - prev3.high;
43002
- levels.r2[i] = p + (prev3.high - prev3.low);
43003
- levels.s2[i] = p - (prev3.high - prev3.low);
43004
- levels.r3[i] = prev3.high + 2 * (p - prev3.low);
43005
- levels.s3[i] = prev3.low - 2 * (prev3.high - p);
43006
- }
43007
- const plot = (key, title, color) => ({ key, title, kind: "step", values: levels[key], color });
43008
- return {
43009
- plots: [
43010
- plot("p", "P", WARNING),
43011
- plot("r1", "R1", BEARISH),
43012
- plot("s1", "S1", BULLISH),
43013
- plot("r2", "R2", BEARISH),
43014
- plot("s2", "S2", BULLISH),
43015
- plot("r3", "R3", BEARISH),
43016
- plot("s3", "S3", BULLISH)
43017
- ]
43018
- };
44343
+ if (kind === PIVOT_FIBONACCI) {
44344
+ levels.r1[i] = traditional + 0.382 * range;
44345
+ levels.s1[i] = traditional - 0.382 * range;
44346
+ levels.r2[i] = traditional + 0.618 * range;
44347
+ levels.s2[i] = traditional - 0.618 * range;
44348
+ levels.r3[i] = traditional + range;
44349
+ levels.s3[i] = traditional - range;
44350
+ } else if (kind === PIVOT_CAMARILLA) {
44351
+ const cam = 1.1 * range;
44352
+ levels.r1[i] = prev3.close + cam / 12;
44353
+ levels.s1[i] = prev3.close - cam / 12;
44354
+ levels.r2[i] = prev3.close + cam / 6;
44355
+ levels.s2[i] = prev3.close - cam / 6;
44356
+ levels.r3[i] = prev3.close + cam / 4;
44357
+ levels.s3[i] = prev3.close - cam / 4;
44358
+ levels.r4[i] = prev3.close + cam / 2;
44359
+ levels.s4[i] = prev3.close - cam / 2;
44360
+ } else {
44361
+ levels.r1[i] = 2 * p - prev3.low;
44362
+ levels.s1[i] = 2 * p - prev3.high;
44363
+ levels.r2[i] = p + range;
44364
+ levels.s2[i] = p - range;
44365
+ if (kind !== PIVOT_WOODIE) {
44366
+ levels.r3[i] = prev3.high + 2 * (p - prev3.low);
44367
+ levels.s3[i] = prev3.low - 2 * (prev3.high - p);
44368
+ }
44369
+ }
44370
+ }
44371
+ const resistance = str3(inputs, "resistanceColor", BEARISH);
44372
+ const support = str3(inputs, "supportColor", BULLISH);
44373
+ const plot = (key, title, color) => ({ key, title, values: levels[key], color });
44374
+ const plots = [
44375
+ plot("p", "P", str3(inputs, "pivotColor", WARNING)),
44376
+ plot("r1", "R1", resistance),
44377
+ plot("s1", "S1", support),
44378
+ plot("r2", "R2", resistance),
44379
+ plot("s2", "S2", support)
44380
+ ];
44381
+ if (kind !== PIVOT_WOODIE) plots.push(plot("r3", "R3", resistance), plot("s3", "S3", support));
44382
+ if (kind === PIVOT_CAMARILLA) plots.push(plot("r4", "R4", resistance), plot("s4", "S4", support));
44383
+ return { plots };
43019
44384
  }
43020
44385
  };
43021
44386
  var fiftyTwoWeek = {
@@ -43023,15 +44388,25 @@ ${overlayScrollbarCss(".vela-sd-pane")}
43023
44388
  title: "52 Week High/Low",
43024
44389
  shortTitle: "52W H/L",
43025
44390
  overlay: true,
43026
- inputs: [{ key: "weeks", title: "Weeks", type: "int", defval: 52, min: 1, max: 520, step: 1 }],
44391
+ inputs: [
44392
+ intInput("weeks", "Weeks", 52, 1, 520, "Length of the rolling window, in weeks."),
44393
+ boolInput("showAllTime", "Show All-Time High/Low", false, SETTINGS, "Additionally display the all-time high and low, tracked over the symbol's full loaded history."),
44394
+ colorInput(BULLISH, "52 Week High", "highColor", "Color of the 52 week high level."),
44395
+ colorInput(BEARISH, "52 Week Low", "lowColor", "Color of the 52 week low level."),
44396
+ colorInput(NEUTRAL, "All-Time High/Low", "allTimeColor", "Color of the all-time high and all-time low levels.")
44397
+ ],
43027
44398
  compute: (bars, inputs) => {
43028
44399
  const span = num3(inputs, "weeks", 52) * 7 * DAY_MS2;
43029
44400
  const n = bars.length;
43030
44401
  const hi = new Array(n).fill(Number.NaN);
43031
44402
  const lo = new Array(n).fill(Number.NaN);
44403
+ const ath = new Array(n).fill(Number.NaN);
44404
+ const atl = new Array(n).fill(Number.NaN);
43032
44405
  const maxIdx = [];
43033
44406
  const minIdx = [];
43034
44407
  let from = 0;
44408
+ let runHigh = -Infinity;
44409
+ let runLow = Infinity;
43035
44410
  for (let i = 0; i < n; i++) {
43036
44411
  const b = bars[i];
43037
44412
  while (maxIdx.length > 0 && bars[maxIdx[maxIdx.length - 1]].high <= b.high) maxIdx.pop();
@@ -43043,13 +44418,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
43043
44418
  while (minIdx[0] < from) minIdx.shift();
43044
44419
  hi[i] = bars[maxIdx[0]].high;
43045
44420
  lo[i] = bars[minIdx[0]].low;
44421
+ runHigh = Math.max(runHigh, b.high);
44422
+ runLow = Math.min(runLow, b.low);
44423
+ ath[i] = runHigh;
44424
+ atl[i] = runLow;
44425
+ }
44426
+ const plots = [
44427
+ { key: "high", title: "52 Week High", values: hi, kind: "step", color: str3(inputs, "highColor", BULLISH), width: 2 },
44428
+ { key: "low", title: "52 Week Low", values: lo, kind: "step", color: str3(inputs, "lowColor", BEARISH), width: 2 }
44429
+ ];
44430
+ if (bool(inputs, "showAllTime", false)) {
44431
+ const ink = str3(inputs, "allTimeColor", NEUTRAL);
44432
+ plots.push({ key: "ath", title: "All-Time High", values: ath, kind: "step", color: ink }, { key: "atl", title: "All-Time Low", values: atl, kind: "step", color: ink });
43046
44433
  }
43047
- return {
43048
- plots: [
43049
- { key: "high", title: "52W high", values: hi, kind: "step", color: BULLISH },
43050
- { key: "low", title: "52W low", values: lo, kind: "step", color: BEARISH }
43051
- ]
43052
- };
44434
+ return { plots };
43053
44435
  }
43054
44436
  };
43055
44437
  var zigzag = {
@@ -43057,9 +44439,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
43057
44439
  title: "ZigZag",
43058
44440
  overlay: true,
43059
44441
  inputs: [
43060
- { key: "deviation", title: "Deviation %", type: "float", defval: 5, min: 0.01, max: 100, step: 0.1 },
43061
- { key: "depth", title: "Depth", type: "int", defval: 10, min: 1, max: 500, step: 1 },
43062
- colorInput()
44442
+ floatInput("deviation", "Deviation (%)", 5, 0.01, 100, 0.1, "Minimum reversal from the leg extreme, as a percentage of that extreme, required to confirm a new pivot."),
44443
+ { ...intInput("depth", "Depth", 10, 2, 500, "Minimum number of bars required between two consecutive pivots.") },
44444
+ colorInput(BULLISH, "Up Leg", "bullColor", "Color of rising zigzag segments."),
44445
+ colorInput(BEARISH, "Down Leg", "bearColor", "Color of falling zigzag segments."),
44446
+ { key: "lineWidth", title: "Width", type: "int", defval: 2, min: 1, max: 10, step: 1, group: STYLE, tooltip: "Width of the zigzag segments." }
43063
44447
  ],
43064
44448
  compute: (bars, inputs) => {
43065
44449
  const dev = num3(inputs, "deviation", 5) / 100;
@@ -43072,14 +44456,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
43072
44456
  const b = bars[i];
43073
44457
  if (up) {
43074
44458
  if (b.high >= ext.price) ext = { i, price: b.high };
43075
- else if (b.low <= ext.price * (1 - dev) && i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
44459
+ else if (b.low <= ext.price * (1 - dev) && ext.i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
43076
44460
  pivots.push(ext);
43077
44461
  up = false;
43078
44462
  ext = { i, price: b.low };
43079
44463
  }
43080
44464
  } else if (b.low <= ext.price) {
43081
44465
  ext = { i, price: b.low };
43082
- } else if (b.high >= ext.price * (1 + dev) && i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
44466
+ } else if (b.high >= ext.price * (1 + dev) && ext.i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
43083
44467
  pivots.push(ext);
43084
44468
  up = true;
43085
44469
  ext = { i, price: b.high };
@@ -43087,22 +44471,29 @@ ${overlayScrollbarCss(".vela-sd-pane")}
43087
44471
  }
43088
44472
  pivots.push(ext);
43089
44473
  }
43090
- return {
43091
- plots: [],
43092
- polylines: [
43093
- {
43094
- key: "zigzag",
43095
- points: pivots.map((p) => ({ xloc: "bar_time", x: bars[p.i].time, price: p.price })),
43096
- curved: false,
43097
- closed: false,
43098
- lineColor: str3(inputs, "color", SERIES_LINE),
43099
- lineWidth: 2,
43100
- lineStyle: "solid",
43101
- arrowLeft: false,
43102
- arrowRight: false
43103
- }
43104
- ]
43105
- };
44474
+ const bull = str3(inputs, "bullColor", BULLISH);
44475
+ const bear = str3(inputs, "bearColor", BEARISH);
44476
+ const width = num3(inputs, "lineWidth", 2);
44477
+ const polylines = [];
44478
+ for (let k = 1; k < pivots.length; k++) {
44479
+ const a = pivots[k - 1];
44480
+ const b = pivots[k];
44481
+ polylines.push({
44482
+ key: `leg${k}`,
44483
+ points: [
44484
+ { xloc: "bar_time", x: bars[a.i].time, price: a.price },
44485
+ { xloc: "bar_time", x: bars[b.i].time, price: b.price }
44486
+ ],
44487
+ curved: false,
44488
+ closed: false,
44489
+ lineColor: b.price >= a.price ? bull : bear,
44490
+ lineWidth: width,
44491
+ lineStyle: "solid",
44492
+ arrowLeft: false,
44493
+ arrowRight: false
44494
+ });
44495
+ }
44496
+ return { plots: [], polylines };
43106
44497
  }
43107
44498
  };
43108
44499
  var williamsFractal = {
@@ -43110,10 +44501,16 @@ ${overlayScrollbarCss(".vela-sd-pane")}
43110
44501
  title: "Williams Fractal",
43111
44502
  shortTitle: "Fractals",
43112
44503
  overlay: true,
43113
- inputs: [{ key: "periods", title: "Periods", type: "int", defval: 2, min: 1, max: 50, step: 1 }],
44504
+ inputs: [
44505
+ intInput("periods", "Periods", 2, 2, 50, "Bars required on each side of the candidate bar. 2 is Bill Williams' classic 5-bar fractal; a fractal is confirmed only once this many later bars have closed."),
44506
+ colorInput(BEARISH, "Up Fractal", "upColor", "Color of the up fractal marker (swing high), drawn above the fractal bar."),
44507
+ colorInput(BULLISH, "Down Fractal", "downColor", "Color of the down fractal marker (swing low), drawn below the fractal bar.")
44508
+ ],
43114
44509
  compute: (bars, inputs) => {
43115
44510
  const p = num3(inputs, "periods", 2);
43116
44511
  const markers = [];
44512
+ const up = str3(inputs, "upColor", BEARISH);
44513
+ const down = str3(inputs, "downColor", BULLISH);
43117
44514
  const isExtreme = (i, pick, better) => {
43118
44515
  const v = pick(bars[i]);
43119
44516
  for (let k = i - p; k <= i + p; k++) {
@@ -43123,12 +44520,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
43123
44520
  return true;
43124
44521
  };
43125
44522
  for (let i = p; i < bars.length - p; i++) {
43126
- if (isExtreme(i, (b) => b.high, (a, b) => a >= b)) {
43127
- markers.push({ time: bars[i].time, position: "aboveBar", shape: "triangleup", color: BULLISH });
43128
- }
43129
- if (isExtreme(i, (b) => b.low, (a, b) => a <= b)) {
43130
- markers.push({ time: bars[i].time, position: "belowBar", shape: "triangledown", color: BEARISH });
43131
- }
44523
+ if (isExtreme(i, (b) => b.high, (a, b) => a > b)) markers.push({ time: bars[i].time, position: "aboveBar", shape: "triangleup", color: up });
44524
+ if (isExtreme(i, (b) => b.low, (a, b) => a < b)) markers.push({ time: bars[i].time, position: "belowBar", shape: "triangledown", color: down });
43132
44525
  }
43133
44526
  return { plots: [], markers };
43134
44527
  }