@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.
package/dist/widget.cjs CHANGED
@@ -700,6 +700,8 @@ var MultiProviderFeed = class {
700
700
  this.liveBars = [];
701
701
  /** Sync-accessible symbol metadata, warmed by load()/symbolInfoFor (the engine reads it synchronously). */
702
702
  this.symInfoCache = /* @__PURE__ */ new Map();
703
+ /** Requests in flight, keyed like the cache — every concurrent asker shares one round trip. */
704
+ this.symInfoInflight = /* @__PURE__ */ new Map();
703
705
  this.cache = new CachingDataFeed(new RegistryFetchFeed(this.registry), store);
704
706
  }
705
707
  // ── Registry surface (driven by chart.data / DataControl) ──────────────
@@ -760,9 +762,36 @@ var MultiProviderFeed = class {
760
762
  async symbolInfoFor(raw) {
761
763
  const resolved = this.registry.resolve(raw, { default: this.primaryProvider });
762
764
  if (!resolved) return void 0;
763
- const info = await this.registry.get(resolved.provider)?.getSymbolInfo?.(resolved.ticker);
764
- if (info) this.symInfoCache.set(symKey(resolved), info);
765
- return info;
765
+ return this.fetchSymbolInfo(resolved);
766
+ }
767
+ /**
768
+ * One round trip per symbol, shared. A market switch probes this metadata from five
769
+ * places at once (the bar load's prefetch, the renderer's tick size, the market-status
770
+ * badge, the session shading, the session toggle) — without the in-flight map each
771
+ * would open its own request for the same answer, and the last one to land decided how
772
+ * late the chrome settled.
773
+ *
774
+ * Cached for {@link SYMBOL_INFO_TTL_MS}: this metadata is stable within a session
775
+ * (tick size, session vocabulary, listing prefix), but not immutable — a continuous
776
+ * futures row rolls its current contract — so the entry expires rather than pinning the
777
+ * session's first answer forever. A failed probe is NOT remembered: the next asker retries.
778
+ */
779
+ fetchSymbolInfo(resolved) {
780
+ const key = symKey(resolved);
781
+ const hit = this.symInfoCache.get(key);
782
+ if (hit && Date.now() - hit.at < SYMBOL_INFO_TTL_MS) return Promise.resolve(hit.info);
783
+ const inflight = this.symInfoInflight.get(key);
784
+ if (inflight) return inflight;
785
+ const getSymbolInfo = this.registry.get(resolved.provider)?.getSymbolInfo;
786
+ if (!getSymbolInfo) return Promise.resolve(void 0);
787
+ const p = Promise.resolve(getSymbolInfo.call(this.registry.get(resolved.provider), resolved.ticker)).then((info) => {
788
+ if (info) this.symInfoCache.set(key, { info, at: Date.now() });
789
+ return info;
790
+ }).finally(() => {
791
+ this.symInfoInflight.delete(key);
792
+ });
793
+ this.symInfoInflight.set(key, p);
794
+ return p;
766
795
  }
767
796
  /**
768
797
  * Per-symbol capabilities, resolved through the owning provider's `capabilitiesFor`
@@ -806,16 +835,10 @@ var MultiProviderFeed = class {
806
835
  symbolInfo(cfg) {
807
836
  if (cfg.data && cfg.data.length > 0) return void 0;
808
837
  const resolved = this.registry.resolve(rawSymbol(cfg), { default: this.primaryProvider });
809
- return resolved ? this.symInfoCache.get(symKey(resolved)) : void 0;
838
+ return resolved ? this.symInfoCache.get(symKey(resolved))?.info : void 0;
810
839
  }
811
840
  prefetchSymbolInfo(resolved) {
812
- const key = symKey(resolved);
813
- if (this.symInfoCache.has(key)) return;
814
- const provider = this.registry.get(resolved.provider);
815
- if (!provider?.getSymbolInfo) return;
816
- void provider.getSymbolInfo(resolved.ticker).then((info) => {
817
- if (info) this.symInfoCache.set(key, info);
818
- }).catch(() => {
841
+ void this.fetchSymbolInfo(resolved).catch(() => {
819
842
  });
820
843
  }
821
844
  async loadRange(cfg, range) {
@@ -865,8 +888,9 @@ function canonical(cfg, resolved) {
865
888
  return { ...cfg, symbol: `${resolved.provider}:${resolved.ticker}` };
866
889
  }
867
890
  function symKey(resolved) {
868
- return `${resolved.provider}|${resolved.ticker}`;
891
+ return `${resolved.provider}|${resolved.ticker.trim().toUpperCase()}`;
869
892
  }
893
+ var SYMBOL_INFO_TTL_MS = 10 * 6e4;
870
894
  async function safeBars(provider, ticker, tf, range) {
871
895
  try {
872
896
  return await provider.getBars(ticker, tf, range);
@@ -29402,7 +29426,7 @@ var SettingsDialog = class {
29402
29426
  if (hr.kind === "heading") body.append(this.sectionTitle(hr.label));
29403
29427
  else if (hr.kind === "toggle") body.append(this.boolRow(hr.label, hr.get(), (v) => hr.set(v)));
29404
29428
  else if (hr.kind === "color") body.append(this.colorRow(hr.label, hr.get(), (v) => hr.set(v)));
29405
- else body.append(this.selectRowLabeled(hr.label, hr.get(), hr.options.map((o) => [o, o]), (v) => hr.set(v)));
29429
+ else body.append(this.selectRowLabeled(hr.label, hr.get(), normalizeSelectOptions(hr.options), (v) => hr.set(v)));
29406
29430
  }
29407
29431
  }
29408
29432
  };
@@ -39861,7 +39885,8 @@ var ClassicIndicator = class {
39861
39885
  lineStyle: plot.lineStyle ?? "solid",
39862
39886
  ...plot.base != null ? { base: plot.base } : {}
39863
39887
  },
39864
- ...plot.overlay ? { overlay: true } : {}
39888
+ ...plot.overlay ? { overlay: true } : {},
39889
+ ...plot.display ? { display: plot.display } : {}
39865
39890
  });
39866
39891
  });
39867
39892
  if (out.markers && out.markers.length > 0) {
@@ -39883,7 +39908,9 @@ var ClassicIndicator = class {
39883
39908
  paneId: "",
39884
39909
  fromSeriesId: from,
39885
39910
  toSeriesId: to,
39886
- color: band.color
39911
+ color: band.color,
39912
+ ...band.colors ? { colors: [...band.colors] } : {},
39913
+ ...band.gradient ? { gradient: [...band.gradient] } : {}
39887
39914
  }
39888
39915
  ];
39889
39916
  });
@@ -40041,6 +40068,16 @@ function stdev(v, len) {
40041
40068
  return Math.sqrt(sq / len);
40042
40069
  });
40043
40070
  }
40071
+ function meanDev(v, len) {
40072
+ return windowed(v, len, (w, from, to) => {
40073
+ let s = 0;
40074
+ for (let k = from; k <= to; k++) s += w[k];
40075
+ const mean = s / len;
40076
+ let dev = 0;
40077
+ for (let k = from; k <= to; k++) dev += Math.abs(w[k] - mean);
40078
+ return dev / len;
40079
+ });
40080
+ }
40044
40081
  function highest(v, len) {
40045
40082
  return windowed(v, len, (w, from, to) => {
40046
40083
  let m = -Infinity;
@@ -40172,7 +40209,7 @@ function stoch(v, hi, lo, len) {
40172
40209
  }
40173
40210
  return out;
40174
40211
  }
40175
- function linreg(v, len) {
40212
+ function linreg(v, len, offset = 0) {
40176
40213
  const sx = (len - 1) * len / 2;
40177
40214
  const sxx = (len - 1) * len * (2 * len - 1) / 6;
40178
40215
  const denom = len * sxx - sx * sx;
@@ -40186,9 +40223,101 @@ function linreg(v, len) {
40186
40223
  }
40187
40224
  const slope = denom === 0 ? 0 : (len * sxy - sx * sy) / denom;
40188
40225
  const intercept = (sy - slope * sx) / len;
40189
- return intercept + slope * (len - 1);
40226
+ return intercept + slope * (len - 1 - offset);
40227
+ });
40228
+ }
40229
+ function hma(v, len) {
40230
+ const n = Math.max(2, Math.trunc(len));
40231
+ const raw = sub(map(wma(v, Math.max(1, Math.trunc(n / 2))), (x) => 2 * x), wma(v, n));
40232
+ return wma(raw, Math.max(1, Math.round(Math.sqrt(n))));
40233
+ }
40234
+ function alma(v, len, offset, sigma) {
40235
+ const m = offset * (len - 1);
40236
+ const s = len / Math.max(sigma, 1e-9);
40237
+ const weights = new Array(len);
40238
+ let norm = 0;
40239
+ for (let i = 0; i < len; i++) {
40240
+ const d = i - m;
40241
+ const w = Math.exp(-(d * d) / (2 * s * s));
40242
+ weights[i] = w;
40243
+ norm += w;
40244
+ }
40245
+ return windowed(v, len, (win, from, to) => {
40246
+ let acc = 0;
40247
+ for (let k = from; k <= to; k++) acc += win[k] * weights[k - from];
40248
+ return norm === 0 ? Number.NaN : acc / norm;
40249
+ });
40250
+ }
40251
+ function dema(v, len) {
40252
+ const e1 = ema(v, len);
40253
+ return sub(map(e1, (x) => 2 * x), ema(e1, len));
40254
+ }
40255
+ function tema(v, len) {
40256
+ const e1 = ema(v, len);
40257
+ const e2 = ema(e1, len);
40258
+ const e3 = ema(e2, len);
40259
+ return zip(zip(map(e1, (x) => 3 * x), map(e2, (x) => 3 * x), (a, b) => a - b), e3, (a, b) => a + b);
40260
+ }
40261
+ function kama(v, len, fastAlpha = 2 / 3, slowAlpha = 2 / 31) {
40262
+ const direction = map(change(v, len), Math.abs);
40263
+ const volatility = sum(map(change(v), Math.abs), len);
40264
+ const out = new Array(v.length).fill(Number.NaN);
40265
+ let prev2 = Number.NaN;
40266
+ for (let i = 0; i < v.length; i++) {
40267
+ const x = v[i];
40268
+ if (!Number.isFinite(x)) continue;
40269
+ const d = direction[i];
40270
+ const w = volatility[i];
40271
+ if (!Number.isFinite(d) || !Number.isFinite(w)) {
40272
+ prev2 = Number.isFinite(prev2) ? prev2 : x;
40273
+ out[i] = prev2;
40274
+ continue;
40275
+ }
40276
+ const er = w === 0 ? 0 : d / w;
40277
+ const sc = (er * (fastAlpha - slowAlpha) + slowAlpha) ** 2;
40278
+ prev2 = Number.isFinite(prev2) ? prev2 + sc * (x - prev2) : x;
40279
+ out[i] = prev2;
40280
+ }
40281
+ return out;
40282
+ }
40283
+ function mcginley(v, len) {
40284
+ const out = new Array(v.length).fill(Number.NaN);
40285
+ let prev2 = Number.NaN;
40286
+ for (let i = 0; i < v.length; i++) {
40287
+ const x = v[i];
40288
+ if (!Number.isFinite(x)) continue;
40289
+ if (!Number.isFinite(prev2)) {
40290
+ prev2 = x;
40291
+ } else {
40292
+ const ratio = prev2 === 0 ? 0 : x / prev2;
40293
+ const denom = len * ratio ** 4;
40294
+ prev2 += denom === 0 ? 0 : (x - prev2) / denom;
40295
+ }
40296
+ out[i] = prev2;
40297
+ }
40298
+ return out;
40299
+ }
40300
+ function hamming(v, len, a0 = 0.54, a1 = 0.46) {
40301
+ if (len <= 1) return v.map((x) => Number.isFinite(x) ? x : Number.NaN);
40302
+ const weights = new Array(len);
40303
+ let norm = 0;
40304
+ for (let i = 0; i < len; i++) {
40305
+ const w = a0 - a1 * Math.cos(2 * Math.PI * i / (len - 1));
40306
+ weights[i] = w;
40307
+ norm += w;
40308
+ }
40309
+ return windowed(v, len, (win, from, to) => {
40310
+ let acc = 0;
40311
+ for (let k = from; k <= to; k++) acc += win[k] * weights[to - k];
40312
+ return norm === 0 ? Number.NaN : acc / norm;
40190
40313
  });
40191
40314
  }
40315
+ function cmo(v, len) {
40316
+ const d = change(v);
40317
+ const up = sum(map(d, (x) => Math.max(x, 0)), len);
40318
+ const dn = sum(map(d, (x) => Math.max(-x, 0)), len);
40319
+ return zip(up, dn, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
40320
+ }
40192
40321
  function swma(v) {
40193
40322
  const out = new Array(v.length).fill(Number.NaN);
40194
40323
  for (let i = 3; i < v.length; i++) {
@@ -40238,38 +40367,253 @@ function cumSum(v) {
40238
40367
  }
40239
40368
 
40240
40369
  // src/core/native-indicators/classics/shared.ts
40241
- function lengthInput(defval, key = "length", title = "Length", max = 5e3) {
40242
- return { key, title, type: "int", defval, min: 1, max, step: 1 };
40370
+ var SETTINGS = "Settings";
40371
+ var STYLE = "Style";
40372
+ function lengthInput(defval, key = "length", title = "Length", max = 5e3, tooltip2) {
40373
+ return { key, title, type: "int", defval, min: 1, max, step: 1, group: SETTINGS, ...tooltip2 != null ? { tooltip: tooltip2 } : {} };
40374
+ }
40375
+ function intInput(key, title, defval, min = 1, max = 5e3, tooltip2) {
40376
+ return { key, title, type: "int", defval, min, max, step: 1, group: SETTINGS, ...tooltip2 != null ? { tooltip: tooltip2 } : {} };
40377
+ }
40378
+ function floatInput(key, title, defval, min = 0, max = 1e3, step = 0.1, tooltip2) {
40379
+ return { key, title, type: "float", defval, min, max, step, group: SETTINGS, ...tooltip2 != null ? { tooltip: tooltip2 } : {} };
40380
+ }
40381
+ function boolInput(key, title, defval, group = SETTINGS, tooltip2) {
40382
+ return { key, title, type: "bool", defval, group, ...tooltip2 != null ? { tooltip: tooltip2 } : {} };
40243
40383
  }
40244
- function sourceInput(defval = "Close") {
40245
- return { key: "source", title: "Source", type: "string", defval, options: SOURCES2 };
40384
+ function optionInput(key, title, defval, options, tooltip2) {
40385
+ return { key, title, type: "string", defval, options, group: SETTINGS, ...tooltip2 != null ? { tooltip: tooltip2 } : {} };
40246
40386
  }
40247
- function colorInput(defval = SERIES_LINE, title = "Color", key = "color") {
40248
- return { key, title, type: "color", defval };
40387
+ function sourceInput(defval = "Close", key = "source", title = "Source", tooltip2) {
40388
+ return { key, title, type: "string", defval, options: SOURCES2, group: SETTINGS, ...tooltip2 != null ? { tooltip: tooltip2 } : {} };
40389
+ }
40390
+ function colorInput(defval = INFO, title = "Color", key = "color", tooltip2) {
40391
+ return { key, title, type: "color", defval, group: STYLE, ...tooltip2 != null ? { tooltip: tooltip2 } : {} };
40392
+ }
40393
+ function widthInput(defval = 2, key = "lineWidth", title = "Line Width") {
40394
+ return { key, title, type: "int", defval, min: 1, max: 5, step: 1, group: STYLE, tooltip: "Width of the plotted line, in pixels." };
40395
+ }
40396
+ function transp(hex, transparency) {
40397
+ return withAlpha2(hex, (100 - transparency) / 100);
40249
40398
  }
40250
40399
  function withAlpha2(hex, alpha = 0.08) {
40251
40400
  const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
40252
40401
  if (!m) return hex;
40253
- const a = Math.round(alpha * 255).toString(16).padStart(2, "0");
40402
+ const a = Math.round(Math.max(0, Math.min(1, alpha)) * 255).toString(16).padStart(2, "0");
40254
40403
  return `#${m[1]}${a}`;
40255
40404
  }
40405
+ function channelStyleInputs(basisTitle = "Basis") {
40406
+ return [
40407
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the lower band and of the basis while price holds above it."),
40408
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the upper band and of the basis while price sits below it."),
40409
+ colorInput(NEUTRAL, basisTitle, "basisColor", "Color of the basis line while price sits exactly on it.")
40410
+ ];
40411
+ }
40412
+ function directionalChannel(bars, basis, upper, lower, ink, key) {
40413
+ const basisColors = basis.map((b, i) => {
40414
+ if (!Number.isFinite(b)) return null;
40415
+ const c = bars[i].close;
40416
+ return c > b ? ink.bull : c < b ? ink.bear : ink.neutral;
40417
+ });
40418
+ const wash = upper.map((u, i) => {
40419
+ const l = lower[i];
40420
+ if (!Number.isFinite(u) || !Number.isFinite(l)) return null;
40421
+ return { topValue: u, bottomValue: l, topColor: transp(ink.bear, 90), bottomColor: transp(ink.bull, 90) };
40422
+ });
40423
+ return {
40424
+ plots: [
40425
+ { key: "basis", title: "Basis", values: [...basis], color: ink.neutral, colors: basisColors },
40426
+ { key: "upper", title: "Upper", values: [...upper], color: transp(ink.bear, 30) },
40427
+ { key: "lower", title: "Lower", values: [...lower], color: transp(ink.bull, 30) }
40428
+ ],
40429
+ bands: [{ key, from: "upper", to: "lower", color: transp(NEUTRAL, 90), gradient: wash }]
40430
+ };
40431
+ }
40432
+ function anchorPlot(key, title, length, level) {
40433
+ return {
40434
+ key,
40435
+ title,
40436
+ values: new Array(length).fill(level),
40437
+ color: transp(NEUTRAL, 100),
40438
+ display: { pane: false, legend: false, dataWindow: false }
40439
+ };
40440
+ }
40441
+ function thresholdZones(length, levels, ink, transparency = 92) {
40442
+ return {
40443
+ plots: [
40444
+ anchorPlot("obAnchor", "Overbought Anchor", length, levels.overbought),
40445
+ anchorPlot("osAnchor", "Oversold Anchor", length, levels.oversold),
40446
+ anchorPlot("topAnchor", "Top Anchor", length, levels.top),
40447
+ anchorPlot("bottomAnchor", "Bottom Anchor", length, levels.bottom)
40448
+ ],
40449
+ bands: [
40450
+ { key: "obZone", from: "obAnchor", to: "topAnchor", color: transp(ink.overbought, transparency) },
40451
+ { key: "osZone", from: "osAnchor", to: "bottomAnchor", color: transp(ink.oversold, transparency) }
40452
+ ]
40453
+ };
40454
+ }
40455
+ function rampColors(values, levels, ink) {
40456
+ return values.map((x) => {
40457
+ if (!Number.isFinite(x)) return null;
40458
+ return x <= levels.mid ? gradient(x, levels.low, levels.mid, ink.low, ink.mid) : gradient(x, levels.mid, levels.high, ink.mid, ink.high);
40459
+ });
40460
+ }
40461
+ function baselineGradient(values, baseline, inkOf, valueTransparency = 50, baseTransparency = 100) {
40462
+ return values.map((x, i) => {
40463
+ if (!Number.isFinite(x)) return null;
40464
+ const ink = inkOf(x, i);
40465
+ const above = x > baseline;
40466
+ return {
40467
+ topValue: Math.max(x, baseline),
40468
+ bottomValue: Math.min(x, baseline),
40469
+ topColor: transp(ink, above ? valueTransparency : baseTransparency),
40470
+ bottomColor: transp(ink, above ? baseTransparency : valueTransparency)
40471
+ };
40472
+ });
40473
+ }
40474
+ function momentumColumnColors(values, ink) {
40475
+ return values.map((x, i) => {
40476
+ if (!Number.isFinite(x)) return null;
40477
+ const prev2 = i > 0 && Number.isFinite(values[i - 1]) ? values[i - 1] : x;
40478
+ const rising = x > prev2;
40479
+ if (x >= 0) return rising ? ink.growAbove : ink.fallAbove;
40480
+ return rising ? ink.growBelow : ink.fallBelow;
40481
+ });
40482
+ }
40483
+ function boundedOscillator(opts) {
40484
+ const mid = (opts.overbought + opts.oversold) / 2;
40485
+ const ramp = rampColors(opts.values, { low: opts.oversold, mid, high: opts.overbought }, opts.ink);
40486
+ const zones = thresholdZones(
40487
+ opts.values.length,
40488
+ { overbought: opts.overbought, oversold: opts.oversold, top: opts.scale.top, bottom: opts.scale.bottom },
40489
+ { overbought: opts.ink.high, oversold: opts.ink.low },
40490
+ opts.zoneTransparency
40491
+ );
40492
+ const plots = [{ key: opts.key, title: opts.title, values: opts.values, color: opts.ink.mid, width: opts.width ?? 1, colors: ramp }, ...zones.plots];
40493
+ const bands = [...zones.bands];
40494
+ if (opts.bandFill != null) bands.push({ key: "band", from: "obAnchor", to: "osAnchor", color: opts.bandFill });
40495
+ if (opts.midpointWash) {
40496
+ plots.push(anchorPlot("midAnchor", "Midpoint Anchor", opts.values.length, mid));
40497
+ 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) });
40498
+ }
40499
+ const levels = [
40500
+ { key: "overbought", price: opts.overbought, color: opts.levelInk, lineStyle: "dashed", title: opts.levelTitles?.overbought ?? "Overbought" },
40501
+ { key: "oversold", price: opts.oversold, color: opts.levelInk, lineStyle: "dashed", title: opts.levelTitles?.oversold ?? "Oversold" }
40502
+ ];
40503
+ if (opts.midline != null) levels.splice(1, 0, { key: "midline", price: opts.midline, color: opts.levelInk, lineStyle: "dotted", title: "Midline" });
40504
+ return { plots, bands, levels };
40505
+ }
40506
+ function centeredOscillator(opts) {
40507
+ const baseline = opts.baseline ?? 0;
40508
+ const inkOf = (x) => x > baseline ? opts.ink.bull : x < baseline ? opts.ink.bear : opts.ink.neutral;
40509
+ const plots = [
40510
+ { 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) },
40511
+ ...opts.extraPlots ?? []
40512
+ ];
40513
+ const bands = [];
40514
+ if (opts.fill !== false) {
40515
+ plots.push(anchorPlot("baseline", "Baseline Anchor", opts.values.length, baseline));
40516
+ bands.push({ key: "wash", from: opts.key, to: "baseline", color: transp(NEUTRAL, 100), gradient: baselineGradient(opts.values, baseline, inkOf, opts.valueTransparency) });
40517
+ }
40518
+ return {
40519
+ plots,
40520
+ bands,
40521
+ levels: [{ key: "zero", price: baseline, color: opts.zeroInk, lineStyle: opts.zeroLineStyle ?? "dashed", title: opts.zeroTitle ?? "Zero Line" }, ...opts.extraLevels ?? []]
40522
+ };
40523
+ }
40524
+ function channels(hex) {
40525
+ const m = /^#([0-9a-f]{6})/i.exec(hex.trim());
40526
+ if (!m) return null;
40527
+ const n = parseInt(m[1], 16);
40528
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
40529
+ }
40530
+ function gradient(value, low, high, from, to) {
40531
+ const a = channels(from);
40532
+ const b = channels(to);
40533
+ if (!a || !b || !Number.isFinite(value)) return from;
40534
+ const span = high - low;
40535
+ const t = span === 0 ? 0 : Math.max(0, Math.min(1, (value - low) / span));
40536
+ const mix2 = (i) => Math.round(a[i] + (b[i] - a[i]) * t).toString(16).padStart(2, "0");
40537
+ return `#${mix2(0)}${mix2(1)}${mix2(2)}`;
40538
+ }
40256
40539
 
40257
40540
  // src/core/native-indicators/classics/averages.ts
40258
- var MA_KINDS = ["SMA", "EMA", "WMA", "RMA", "VWMA"];
40259
- var offsetInput = { key: "offset", title: "Offset", type: "int", defval: 0, min: -500, max: 500, step: 1 };
40541
+ var MA_TYPES = ["SMA", "EMA", "WMA", "RMA (SMMA)", "HMA", "ALMA", "VWMA", "DEMA", "TEMA", "KAMA", "LSMA", "McGinley", "Hamming"];
40542
+ var ENVELOPE_MA_TYPES = ["SMA", "EMA", "WMA", "RMA (SMMA)", "HMA", "VWMA"];
40543
+ function movingAverageOf(kind, src, bars, len, opts = {}) {
40544
+ switch (kind) {
40545
+ case "EMA":
40546
+ return ema(src, len);
40547
+ case "WMA":
40548
+ return wma(src, len);
40549
+ case "RMA":
40550
+ case "RMA (SMMA)":
40551
+ return rma(src, len);
40552
+ case "HMA":
40553
+ return hma(src, len);
40554
+ case "ALMA":
40555
+ return alma(src, len, opts.almaOffset ?? 0.85, opts.almaSigma ?? 6);
40556
+ case "VWMA":
40557
+ return vwma(src, volumes(bars), len);
40558
+ case "DEMA":
40559
+ return dema(src, len);
40560
+ case "TEMA":
40561
+ return tema(src, len);
40562
+ case "KAMA":
40563
+ return kama(src, len);
40564
+ case "LSMA":
40565
+ return linreg(src, len, opts.lsmaOffset ?? 0);
40566
+ case "McGinley":
40567
+ return mcginley(src, len);
40568
+ case "Hamming":
40569
+ return hamming(src, len);
40570
+ default:
40571
+ return sma(src, len);
40572
+ }
40573
+ }
40574
+ function slopeColors(values, bull, bear, flat) {
40575
+ return values.map((x, i) => {
40576
+ if (!Number.isFinite(x)) return null;
40577
+ const prev2 = i > 0 ? values[i - 1] : Number.NaN;
40578
+ if (!Number.isFinite(prev2)) return flat;
40579
+ return x > prev2 ? bull : x < prev2 ? bear : flat;
40580
+ });
40581
+ }
40582
+ function directionInputs() {
40583
+ return [
40584
+ 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."),
40585
+ colorInput(BULLISH, "Bullish", "bullColor"),
40586
+ colorInput(BEARISH, "Bearish", "bearColor"),
40587
+ colorInput(NEUTRAL, "Neutral", "neutralColor"),
40588
+ widthInput()
40589
+ ];
40590
+ }
40591
+ function slopeColoredPlot(key, title, values, inputs) {
40592
+ const neutral = str3(inputs, "neutralColor", NEUTRAL);
40593
+ const colored = bool(inputs, "trendColor", true);
40594
+ return {
40595
+ key,
40596
+ title,
40597
+ values,
40598
+ color: neutral,
40599
+ width: num3(inputs, "lineWidth", 2),
40600
+ ...colored ? { colors: slopeColors(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), neutral) } : {}
40601
+ };
40602
+ }
40603
+ 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." };
40260
40604
  function fixedAverage(type, title, shortTitle, smooth) {
40261
40605
  return {
40262
40606
  type,
40263
40607
  title,
40264
40608
  shortTitle,
40265
40609
  overlay: true,
40266
- inputs: [lengthInput(20), sourceInput(), offsetInput, colorInput()],
40610
+ 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()],
40267
40611
  compute: (bars, inputs) => {
40268
40612
  const len = num3(inputs, "length", 20);
40269
40613
  let values = smooth(sourceValues(bars, str3(inputs, "source", "Close")), len);
40270
40614
  const offset = Math.trunc(num3(inputs, "offset", 0));
40271
40615
  if (offset !== 0) values = shift(values, offset);
40272
- return { plots: [{ key: shortTitle.toLowerCase(), title: shortTitle, values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
40616
+ return { plots: [{ key: shortTitle.toLowerCase(), title: shortTitle, values, color: str3(inputs, "color", INFO), width: 2 }] };
40273
40617
  }
40274
40618
  };
40275
40619
  }
@@ -40281,36 +40625,37 @@ var movingAverage = {
40281
40625
  shortTitle: "MA",
40282
40626
  overlay: true,
40283
40627
  inputs: [
40284
- { key: "maType", title: "Type", type: "string", defval: "SMA", options: MA_KINDS },
40285
- lengthInput(20),
40286
- sourceInput(),
40628
+ optionInput("maType", "MA 1 Type", "SMA", MA_TYPES, "Moving average calculation used for MA 1."),
40629
+ lengthInput(20, "length", "MA 1 Length", 5e3, "Number of bars used in the MA 1 calculation."),
40630
+ sourceInput("Close", "source", "MA 1 Source", "Price series MA 1 is calculated on."),
40631
+ boolInput("showMa2", "Show MA 2", false, SETTINGS, "Displays the optional second moving average."),
40632
+ optionInput("ma2Type", "MA 2 Type", "SMA", MA_TYPES, "Moving average calculation used for MA 2."),
40633
+ lengthInput(50, "ma2Length", "MA 2 Length", 5e3, "Number of bars used in the MA 2 calculation."),
40634
+ sourceInput("Close", "ma2Source", "MA 2 Source", "Price series MA 2 is calculated on."),
40635
+ 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."),
40636
+ floatInput("almaSigma", "ALMA Sigma", 6, 0.1, 100, 0.5, "ALMA type only. Larger values widen the Gaussian window for a smoother average."),
40637
+ intInput("lsmaOffset", "LSMA Offset", 0, 0, 500, "LSMA type only. Bar offset applied to the linear regression value."),
40287
40638
  offsetInput,
40288
- colorInput()
40639
+ colorInput(INFO, "MA 1", "color", "MA 1 line color."),
40640
+ colorInput(WARNING, "MA 2", "ma2Color", "MA 2 line color.")
40289
40641
  ],
40290
40642
  compute: (bars, inputs) => {
40291
- const len = num3(inputs, "length", 20);
40292
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
40643
+ const opts = {
40644
+ almaOffset: num3(inputs, "almaOffset", 0.85),
40645
+ almaSigma: num3(inputs, "almaSigma", 6),
40646
+ lsmaOffset: Math.trunc(num3(inputs, "lsmaOffset", 0))
40647
+ };
40648
+ const offset = Math.trunc(num3(inputs, "offset", 0));
40649
+ const displace = (v) => offset === 0 ? v : shift(v, offset);
40293
40650
  const kind = str3(inputs, "maType", "SMA");
40294
- let values;
40295
- switch (kind) {
40296
- case "EMA":
40297
- values = ema(src, len);
40298
- break;
40299
- case "WMA":
40300
- values = wma(src, len);
40301
- break;
40302
- case "RMA":
40303
- values = rma(src, len);
40304
- break;
40305
- case "VWMA":
40306
- values = vwma(src, volumes(bars), len);
40307
- break;
40308
- default:
40309
- values = sma(src, len);
40651
+ const ma1 = displace(movingAverageOf(kind, sourceValues(bars, str3(inputs, "source", "Close")), bars, num3(inputs, "length", 20), opts));
40652
+ const plots = [{ key: "ma", title: kind, values: ma1, color: str3(inputs, "color", INFO), width: 2 }];
40653
+ if (bool(inputs, "showMa2", false)) {
40654
+ const kind2 = str3(inputs, "ma2Type", "SMA");
40655
+ const ma2 = displace(movingAverageOf(kind2, sourceValues(bars, str3(inputs, "ma2Source", "Close")), bars, num3(inputs, "ma2Length", 50), opts));
40656
+ plots.push({ key: "ma2", title: `${kind2} 2`, values: ma2, color: str3(inputs, "ma2Color", WARNING), width: 2 });
40310
40657
  }
40311
- const offset = Math.trunc(num3(inputs, "offset", 0));
40312
- if (offset !== 0) values = shift(values, offset);
40313
- return { plots: [{ key: "ma", title: kind, values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
40658
+ return { plots };
40314
40659
  }
40315
40660
  };
40316
40661
  var smoothedMa = {
@@ -40318,17 +40663,13 @@ var smoothedMa = {
40318
40663
  title: "Smoothed Moving Average",
40319
40664
  shortTitle: "RMA",
40320
40665
  overlay: true,
40321
- inputs: [lengthInput(14), sourceInput(), colorInput()],
40666
+ inputs: [
40667
+ sourceInput("Close", "source", "Source", "Price series the average smooths. Wilder computed his 1978 indicators on the close, but the recursion accepts any source."),
40668
+ 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."),
40669
+ ...directionInputs()
40670
+ ],
40322
40671
  compute: (bars, inputs) => ({
40323
- plots: [
40324
- {
40325
- key: "rma",
40326
- title: "RMA",
40327
- values: rma(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
40328
- color: str3(inputs, "color", SERIES_LINE),
40329
- width: 2
40330
- }
40331
- ]
40672
+ plots: [slopeColoredPlot("rma", "RMA", rma(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)), inputs)]
40332
40673
  })
40333
40674
  };
40334
40675
  var zlema = {
@@ -40336,61 +40677,86 @@ var zlema = {
40336
40677
  title: "Zero-Lag Exponential Moving Average",
40337
40678
  shortTitle: "ZLEMA",
40338
40679
  overlay: true,
40339
- inputs: [lengthInput(14), sourceInput(), colorInput()],
40680
+ inputs: [
40681
+ sourceInput("Close", "source", "Source", "Price series the average de-lags and then smooths."),
40682
+ 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."),
40683
+ ...directionInputs()
40684
+ ],
40340
40685
  compute: (bars, inputs) => {
40341
- const len = num3(inputs, "length", 14);
40686
+ const len = num3(inputs, "length", 21);
40342
40687
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
40343
- const lag = Math.floor((len - 1) / 2);
40344
- const lagged = shift(src, lag);
40345
- const delagged = zip(src, lagged, (a, b) => 2 * a - b);
40346
- return { plots: [{ key: "zlema", title: "ZLEMA", values: ema(delagged, len), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
40688
+ const lag = Math.round((len - 1) / 2);
40689
+ const delagged = zip(src, shift(src, lag), (a, b) => 2 * a - b);
40690
+ return { plots: [slopeColoredPlot("zlema", "ZLEMA", ema(delagged, len), inputs)] };
40347
40691
  }
40348
40692
  };
40693
+ var MOMENTUM_SCALER = "Momentum (|CMO|)";
40694
+ var VOLATILITY_SCALER = "Volatility (StDev Ratio)";
40695
+ var REGIME = "Trend Regime";
40349
40696
  var vidya = {
40350
40697
  type: "vidya",
40351
40698
  title: "Variable Index Dynamic Average",
40352
40699
  shortTitle: "VIDYA",
40353
40700
  overlay: true,
40354
- inputs: [lengthInput(14), { key: "cmoLength", title: "Momentum length", type: "int", defval: 9, min: 1, max: 500, step: 1 }, sourceInput(), colorInput()],
40701
+ inputs: [
40702
+ sourceInput("Close", "source", "Source", "Price series the average smooths. VIDYA is classically computed on the close."),
40703
+ 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."),
40704
+ 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."),
40705
+ 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."),
40706
+ 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."),
40707
+ { ...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 },
40708
+ { ...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 },
40709
+ 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."),
40710
+ 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."),
40711
+ colorInput(BULLISH, "Bullish", "bullColor"),
40712
+ colorInput(BEARISH, "Bearish", "bearColor"),
40713
+ colorInput(NEUTRAL, "Flat", "flatColor"),
40714
+ colorInput(NEUTRAL, "Neutral", "neutralColor"),
40715
+ widthInput()
40716
+ ],
40355
40717
  compute: (bars, inputs) => {
40356
- const len = num3(inputs, "length", 14);
40357
- const cmoLen = num3(inputs, "cmoLength", 9);
40718
+ const len = num3(inputs, "length", 12);
40358
40719
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
40359
- const d = change(src);
40360
- const su = new Array(src.length).fill(Number.NaN);
40361
- const sd = new Array(src.length).fill(Number.NaN);
40362
- for (let i = cmoLen; i < src.length; i++) {
40363
- let u = 0;
40364
- let w = 0;
40365
- let ok = true;
40366
- for (let k = i - cmoLen + 1; k <= i; k++) {
40367
- const x = d[k];
40368
- if (!Number.isFinite(x)) {
40369
- ok = false;
40370
- break;
40371
- }
40372
- if (x > 0) u += x;
40373
- else w -= x;
40374
- }
40375
- if (ok) {
40376
- su[i] = u;
40377
- sd[i] = w;
40378
- }
40379
- }
40380
- const alpha = 2 / (len + 1);
40720
+ const scalerLen = num3(inputs, "scalerLength", 9);
40721
+ const momentum = map(cmo(src, scalerLen), (x) => Math.abs(x) / 100);
40722
+ const shortDev = stdev(src, scalerLen);
40723
+ const refDev = stdev(src, num3(inputs, "refLength", 30));
40724
+ const volatility = zip(shortDev, refDev, (s, r) => r > 0 ? s / r : 0);
40725
+ const scale = str3(inputs, "scaler", MOMENTUM_SCALER) === VOLATILITY_SCALER ? volatility : momentum;
40726
+ const alphaBase = 2 / (len + 1);
40381
40727
  const out = new Array(src.length).fill(Number.NaN);
40382
40728
  let prev2 = Number.NaN;
40383
40729
  for (let i = 0; i < src.length; i++) {
40384
40730
  const x = src[i];
40385
- const u = su[i];
40386
- const w = sd[i];
40387
- if (!Number.isFinite(x) || !Number.isFinite(u) || !Number.isFinite(w)) continue;
40388
- const total = u + w;
40389
- const k = total === 0 ? 0 : Math.abs((u - w) / total);
40390
- prev2 = Number.isFinite(prev2) ? alpha * k * x + (1 - alpha * k) * prev2 : x;
40731
+ if (!Number.isFinite(x)) continue;
40732
+ const k = scale[i];
40733
+ if (!Number.isFinite(prev2) || !Number.isFinite(k)) prev2 = x;
40734
+ else {
40735
+ const alpha = Math.min(alphaBase * k, 1);
40736
+ prev2 = alpha * x + (1 - alpha) * prev2;
40737
+ }
40391
40738
  out[i] = prev2;
40392
40739
  }
40393
- return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
40740
+ const neutral = str3(inputs, "neutralColor", NEUTRAL);
40741
+ if (!bool(inputs, "trendColor", true)) {
40742
+ return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: neutral, width: num3(inputs, "lineWidth", 2) }] };
40743
+ }
40744
+ const bull = str3(inputs, "bullColor", BULLISH);
40745
+ const bear = str3(inputs, "bearColor", BEARISH);
40746
+ const flat = str3(inputs, "flatColor", NEUTRAL);
40747
+ const threshold = num3(inputs, "flatThreshold", 0.05);
40748
+ const range = atr(bars, num3(inputs, "atrLength", 14));
40749
+ const fade = bool(inputs, "adaptiveGradient", true);
40750
+ const colors = out.map((x, i) => {
40751
+ if (!Number.isFinite(x)) return null;
40752
+ const r = range[i];
40753
+ const slope = i > 0 && Number.isFinite(out[i - 1]) ? x - out[i - 1] : 0;
40754
+ const normalized = Number.isFinite(r) && r !== 0 ? slope / r : 0;
40755
+ const ink = normalized > threshold ? bull : normalized < -threshold ? bear : null;
40756
+ if (ink == null) return flat;
40757
+ return fade ? gradient(Math.min(scale[i] ?? 0, 1), 0, 1, flat, ink) : ink;
40758
+ });
40759
+ return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: flat, width: num3(inputs, "lineWidth", 2), colors }] };
40394
40760
  }
40395
40761
  };
40396
40762
  var maEnvelope = {
@@ -40399,115 +40765,218 @@ var maEnvelope = {
40399
40765
  shortTitle: "MA Env",
40400
40766
  overlay: true,
40401
40767
  inputs: [
40402
- { key: "maType", title: "Type", type: "string", defval: "SMA", options: ["SMA", "EMA"] },
40403
- lengthInput(20),
40404
- sourceInput(),
40405
- { key: "percent", title: "Percent", type: "float", defval: 2.5, min: 0.01, max: 50, step: 0.1 },
40406
- colorInput()
40768
+ optionInput("maType", "MA Type", "SMA", ENVELOPE_MA_TYPES, "Moving average type used as the envelope basis."),
40769
+ { ...lengthInput(20, "length", "Length", 5e3, "Number of bars in the basis moving average."), min: 2 },
40770
+ sourceInput("Close", "source", "Source", "Price series the basis moving average is calculated on."),
40771
+ 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)."),
40772
+ ...channelStyleInputs()
40407
40773
  ],
40408
40774
  compute: (bars, inputs) => {
40409
- const len = num3(inputs, "length", 20);
40410
40775
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
40411
- const basis = str3(inputs, "maType", "SMA") === "EMA" ? ema(src, len) : sma(src, len);
40776
+ const basis = movingAverageOf(str3(inputs, "maType", "SMA"), src, bars, num3(inputs, "length", 20));
40412
40777
  const k = num3(inputs, "percent", 2.5) / 100;
40413
- const ink = str3(inputs, "color", SERIES_LINE);
40414
- return {
40415
- plots: [
40416
- { key: "basis", title: "Basis", values: basis, color: ink, width: 2 },
40417
- { key: "upper", title: "Upper", values: map(basis, (x) => x * (1 + k)), color: ink },
40418
- { key: "lower", title: "Lower", values: map(basis, (x) => x * (1 - k)), color: ink }
40419
- ],
40420
- bands: [{ key: "envelope", from: "upper", to: "lower", color: withAlpha2(ink) }]
40421
- };
40778
+ const ink = { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "basisColor", NEUTRAL) };
40779
+ return directionalChannel(bars, basis, map(basis, (x) => x * (1 + k)), map(basis, (x) => x * (1 - k)), ink, "envelope");
40422
40780
  }
40423
40781
  };
40782
+ var LINREG_CURVE = "Curve";
40783
+ var LINREG_SLOPE = "Slope";
40424
40784
  var linearRegression = {
40425
40785
  type: "linear-regression",
40426
40786
  title: "Linear Regression Curve",
40427
40787
  shortTitle: "LinReg",
40428
- overlay: true,
40429
- inputs: [lengthInput(14), sourceInput(), colorInput()],
40430
- compute: (bars, inputs) => ({
40431
- plots: [
40432
- {
40433
- key: "linreg",
40434
- title: "LinReg",
40435
- values: linreg(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
40436
- color: str3(inputs, "color", SERIES_LINE),
40437
- width: 2
40438
- }
40439
- ]
40440
- })
40788
+ // Slope lives in its own pane; Curve force-overlays itself onto the price candles.
40789
+ overlay: false,
40790
+ inputs: [
40791
+ 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."),
40792
+ { ...lengthInput(100, "length", "Length", 5e3, "Number of bars used to fit the linear regression."), min: 2 },
40793
+ sourceInput("Close", "source", "Source", "Price series the regression is fitted to."),
40794
+ colorInput(BULLISH, "Rising", "bullColor", "Color of the regression curve and slope line while the fit slopes upward."),
40795
+ colorInput(BEARISH, "Falling", "bearColor", "Color of the regression curve and slope line while the fit slopes downward."),
40796
+ colorInput(NEUTRAL, "Regression Line", "lineColor", "Color of the regression curve and slope line while the fit is flat.")
40797
+ ],
40798
+ compute: (bars, inputs) => {
40799
+ const len = num3(inputs, "length", 100);
40800
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
40801
+ const curve = linreg(src, len, 0);
40802
+ const slope = zip(curve, linreg(src, len, 1), (a, b) => a - b);
40803
+ const bull = str3(inputs, "bullColor", BULLISH);
40804
+ const bear = str3(inputs, "bearColor", BEARISH);
40805
+ const flat = str3(inputs, "lineColor", NEUTRAL);
40806
+ const colors = slope.map((s) => Number.isFinite(s) ? s > 0 ? bull : s < 0 ? bear : flat : null);
40807
+ if (str3(inputs, "mode", LINREG_CURVE) === LINREG_SLOPE) {
40808
+ return {
40809
+ plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
40810
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
40811
+ };
40812
+ }
40813
+ return { plots: [{ key: "curve", title: "Curve", values: curve, color: flat, width: 2, colors, overlay: true }] };
40814
+ }
40441
40815
  };
40442
40816
  var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope, linearRegression];
40443
40817
 
40444
40818
  // src/core/native-indicators/classics/bands.ts
40819
+ var JAW_COLOR = "#ab47bc";
40820
+ var TEETH_COLOR = WARNING;
40821
+ var LIPS_COLOR = "#ffd700";
40445
40822
  function bollinger(bars, inputs) {
40446
40823
  const len = num3(inputs, "length", 20);
40447
40824
  const mult = num3(inputs, "mult", 2);
40448
40825
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
40449
40826
  const basis = sma(src, len);
40450
40827
  const dev = map(stdev(src, len), (x) => x * mult);
40451
- return { basis, upper: zip(basis, dev, (b, d) => b + d), lower: zip(basis, dev, (b, d) => b - d) };
40828
+ return { src, basis, upper: zip(basis, dev, (b, d) => b + d), lower: zip(basis, dev, (b, d) => b - d) };
40829
+ }
40830
+ function bollingerInputs(multMin, multStep) {
40831
+ return [
40832
+ { ...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 },
40833
+ sourceInput("Close", "source", "Source", "Price series the bands are computed from."),
40834
+ floatInput("mult", "Multiplier", 2, multMin, 50, multStep, "Standard deviation multiplier for the upper and lower bands.")
40835
+ ];
40452
40836
  }
40453
- var BB_INPUTS = [lengthInput(20), sourceInput(), { key: "mult", title: "StdDev", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 }];
40454
40837
  var bollingerBands = {
40455
40838
  type: "bollinger-bands",
40456
40839
  title: "Bollinger Bands",
40457
40840
  shortTitle: "BB",
40458
40841
  overlay: true,
40459
- inputs: [...BB_INPUTS, colorInput()],
40842
+ inputs: [
40843
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used to calculate the basis SMA and the standard deviation."),
40844
+ sourceInput("Close", "source", "Source", "Price series the bands are calculated on."),
40845
+ floatInput("mult", "Multiplier", 2, 0, 50, 0.1, "Number of standard deviations added above and below the basis to form the bands."),
40846
+ colorInput(WARNING, "Basis", "basisColor", "Color of the basis (middle band) moving average."),
40847
+ colorInput(INFO, "Bands", "color", "Color of the upper and lower bands."),
40848
+ colorInput(transp(INFO, 92), "Fill", "fillColor", "Fill color of the area between the upper and lower bands.")
40849
+ ],
40460
40850
  compute: (bars, inputs) => {
40461
40851
  const { basis, upper, lower } = bollinger(bars, inputs);
40462
- const ink = str3(inputs, "color", SERIES_LINE);
40852
+ const ink = str3(inputs, "color", INFO);
40463
40853
  return {
40464
40854
  plots: [
40465
- { key: "basis", title: "Basis", values: basis, color: WARNING },
40855
+ { key: "basis", title: "Basis", values: basis, color: str3(inputs, "basisColor", WARNING) },
40466
40856
  { key: "upper", title: "Upper", values: upper, color: ink },
40467
40857
  { key: "lower", title: "Lower", values: lower, color: ink }
40468
40858
  ],
40469
- bands: [{ key: "bb", from: "upper", to: "lower", color: withAlpha2(ink) }]
40859
+ bands: [{ key: "bb", from: "upper", to: "lower", color: str3(inputs, "fillColor", transp(INFO, 92)) }]
40470
40860
  };
40471
40861
  }
40472
40862
  };
40863
+ var PERCENT_B_MID = 0.5;
40864
+ var THRESHOLDS = "Thresholds";
40473
40865
  var percentB = {
40474
40866
  type: "percent-b",
40475
40867
  title: "Bollinger Bands %B",
40476
40868
  shortTitle: "%B",
40477
40869
  overlay: false,
40478
- inputs: [...BB_INPUTS, colorInput()],
40870
+ inputs: [
40871
+ ...bollingerInputs(1e-3, 0.25),
40872
+ { ...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 },
40873
+ { ...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 },
40874
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of %B above the 0.5 midline, where price trades in the upper half of the bands."),
40875
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of %B below the 0.5 midline, where price trades in the lower half of the bands."),
40876
+ 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."),
40877
+ colorInput(NEUTRAL, "Levels Color", "levelsColor", "Color of the dashed overbought/oversold levels and of the dotted midline.")
40878
+ ],
40479
40879
  compute: (bars, inputs) => {
40480
- const { upper, lower } = bollinger(bars, inputs);
40481
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
40880
+ const { src, upper, lower } = bollinger(bars, inputs);
40482
40881
  const values = src.map((x, i) => {
40483
40882
  const u = upper[i];
40484
40883
  const l = lower[i];
40485
40884
  return Number.isFinite(u) && Number.isFinite(l) && u !== l ? (x - l) / (u - l) : Number.NaN;
40486
40885
  });
40886
+ const bull = str3(inputs, "bullColor", BULLISH);
40887
+ const bear = str3(inputs, "bearColor", BEARISH);
40888
+ const levels = str3(inputs, "levelsColor", NEUTRAL);
40889
+ const colors = values.map((x) => Number.isFinite(x) ? x > PERCENT_B_MID ? bull : bear : null);
40890
+ const plots = [
40891
+ { key: "pb", title: "%B", values, color: bear, colors },
40892
+ { 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 } }
40893
+ ];
40894
+ const bands = bool(inputs, "gradientFill", true) ? [
40895
+ {
40896
+ key: "pbFill",
40897
+ from: "pb",
40898
+ to: "mid",
40899
+ color: transp(NEUTRAL, 100),
40900
+ // Opaque at the oscillator, invisible where it meets the midline.
40901
+ gradient: values.map((x) => {
40902
+ if (!Number.isFinite(x)) return null;
40903
+ const up = x > PERCENT_B_MID;
40904
+ const ink = up ? bull : bear;
40905
+ return {
40906
+ topValue: Math.max(x, PERCENT_B_MID),
40907
+ bottomValue: Math.min(x, PERCENT_B_MID),
40908
+ topColor: up ? transp(ink, 50) : transp(ink, 100),
40909
+ bottomColor: up ? transp(ink, 100) : transp(ink, 50)
40910
+ };
40911
+ })
40912
+ }
40913
+ ] : [];
40487
40914
  return {
40488
- plots: [{ key: "pb", title: "%B", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
40915
+ plots,
40916
+ bands,
40489
40917
  levels: [
40490
- { key: "upper", price: 1, color: NEUTRAL },
40491
- { key: "middle", price: 0.5, color: NEUTRAL, lineStyle: "dotted" },
40492
- { key: "lower", price: 0, color: NEUTRAL }
40918
+ { key: "overbought", price: num3(inputs, "overbought", 1), color: levels, lineStyle: "dashed", title: "Overbought" },
40919
+ { key: "oversold", price: num3(inputs, "oversold", 0), color: levels, lineStyle: "dashed", title: "Oversold" },
40920
+ { key: "midline", price: PERCENT_B_MID, color: transp(levels, 50), lineStyle: "dotted", title: "Midline" }
40493
40921
  ]
40494
40922
  };
40495
40923
  }
40496
40924
  };
40925
+ var SQUEEZE_BULGE = "Squeeze & Bulge";
40497
40926
  var bandwidth = {
40498
40927
  type: "bandwidth",
40499
40928
  title: "Bollinger Bands Width",
40500
40929
  shortTitle: "BandWidth",
40501
40930
  overlay: false,
40502
- inputs: [...BB_INPUTS, colorInput()],
40931
+ inputs: [
40932
+ ...bollingerInputs(1e-3, 0.25),
40933
+ { ...intInput("lookback", "Reference Lookback", 125, 2, 5e3, "Window used to locate the lowest (Squeeze) and highest (Bulge) BandWidth references."), group: SQUEEZE_BULGE },
40934
+ 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."),
40935
+ 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."),
40936
+ 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."),
40937
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between zero and BandWidth with a vertical gradient, fully transparent at zero."),
40938
+ colorInput(WARNING, "Squeeze Level", "squeezeColor", "Color of the lowest-BandWidth reference line."),
40939
+ colorInput(BEARISH, "Bulge Level", "bulgeColor", "Color of the highest-BandWidth reference line.")
40940
+ ],
40503
40941
  compute: (bars, inputs) => {
40504
40942
  const { basis, upper, lower } = bollinger(bars, inputs);
40505
40943
  const values = basis.map((b, i) => {
40506
40944
  const u = upper[i];
40507
40945
  const l = lower[i];
40508
- return Number.isFinite(b) && b !== 0 && Number.isFinite(u) && Number.isFinite(l) ? (u - l) / b * 100 : Number.NaN;
40946
+ return Number.isFinite(b) && b !== 0 && Number.isFinite(u) && Number.isFinite(l) ? (u - l) / b : Number.NaN;
40947
+ });
40948
+ const lookback = num3(inputs, "lookback", 125);
40949
+ const squeezeLevel = lowest(values, lookback);
40950
+ const bulgeLevel = highest(values, lookback);
40951
+ const base = str3(inputs, "color", NEUTRAL);
40952
+ const squeezeInk = str3(inputs, "squeezeColor", WARNING);
40953
+ const bulgeInk = str3(inputs, "bulgeColor", BEARISH);
40954
+ const colors = values.map((x, i) => {
40955
+ if (!Number.isFinite(x)) return null;
40956
+ if (x <= squeezeLevel[i]) return squeezeInk;
40957
+ if (x >= bulgeLevel[i]) return bulgeInk;
40958
+ return base;
40509
40959
  });
40510
- return { plots: [{ key: "bbw", title: "BBW", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
40960
+ const plots = [
40961
+ { key: "bbw", title: "BandWidth", values, color: base, colors },
40962
+ { key: "zero", title: "Zero Line", values: values.map((x) => Number.isFinite(x) ? 0 : Number.NaN), color: transp(NEUTRAL, 90) }
40963
+ ];
40964
+ if (bool(inputs, "showSqueeze", true)) plots.push({ key: "squeeze", title: "Squeeze Level", values: squeezeLevel, color: squeezeInk });
40965
+ if (bool(inputs, "showBulge", true)) plots.push({ key: "bulge", title: "Bulge Level", values: bulgeLevel, color: bulgeInk });
40966
+ const bands = bool(inputs, "gradientFill", true) ? [
40967
+ {
40968
+ key: "bbwFill",
40969
+ from: "bbw",
40970
+ to: "zero",
40971
+ color: transp(NEUTRAL, 100),
40972
+ gradient: values.map((x, i) => {
40973
+ if (!Number.isFinite(x)) return null;
40974
+ const ink = colors[i] ?? base;
40975
+ return { topValue: x, bottomValue: 0, topColor: transp(ink, 50), bottomColor: transp(ink, 100) };
40976
+ })
40977
+ }
40978
+ ] : [];
40979
+ return { plots, bands };
40511
40980
  }
40512
40981
  };
40513
40982
  var keltner = {
@@ -40516,26 +40985,17 @@ var keltner = {
40516
40985
  shortTitle: "KC",
40517
40986
  overlay: true,
40518
40987
  inputs: [
40519
- lengthInput(20),
40520
- { key: "mult", title: "Multiplier", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 },
40521
- { key: "atrLength", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
40522
- sourceInput(),
40523
- colorInput()
40988
+ lengthInput(20, "length", "EMA Length", 5e3, "Number of bars used to compute the exponential moving average basis line."),
40989
+ intInput("atrLength", "ATR Length", 10, 1, 5e3, "Number of bars used to compute the Average True Range (Wilder smoothing)."),
40990
+ floatInput("mult", "Multiplier", 2, 0, 50, 0.1, "ATR multiple used to offset the upper and lower bands from the basis line."),
40991
+ sourceInput("Close", "source", "Source", "Price series used to compute the basis line."),
40992
+ ...channelStyleInputs("Basis Color")
40524
40993
  ],
40525
40994
  compute: (bars, inputs) => {
40526
- const len = num3(inputs, "length", 20);
40527
- const mult = num3(inputs, "mult", 2);
40528
- const basis = ema(sourceValues(bars, str3(inputs, "source", "Close")), len);
40529
- const range = map(atr(bars, num3(inputs, "atrLength", 10)), (x) => x * mult);
40530
- const ink = str3(inputs, "color", SERIES_LINE);
40531
- return {
40532
- plots: [
40533
- { key: "basis", title: "Basis", values: basis, color: WARNING },
40534
- { key: "upper", title: "Upper", values: zip(basis, range, (b, r) => b + r), color: ink },
40535
- { key: "lower", title: "Lower", values: zip(basis, range, (b, r) => b - r), color: ink }
40536
- ],
40537
- bands: [{ key: "kc", from: "upper", to: "lower", color: withAlpha2(ink) }]
40538
- };
40995
+ const basis = ema(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20));
40996
+ const range = map(atr(bars, num3(inputs, "atrLength", 10)), (x) => x * num3(inputs, "mult", 2));
40997
+ const ink = { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "basisColor", NEUTRAL) };
40998
+ return directionalChannel(bars, basis, zip(basis, range, (b, r) => b + r), zip(basis, range, (b, r) => b - r), ink, "kc");
40539
40999
  }
40540
41000
  };
40541
41001
  var donchian = {
@@ -40543,19 +41003,24 @@ var donchian = {
40543
41003
  title: "Donchian Channels",
40544
41004
  shortTitle: "DC",
40545
41005
  overlay: true,
40546
- inputs: [lengthInput(20), colorInput()],
41006
+ inputs: [
41007
+ lengthInput(20, "length", "Length", 5e3, "Lookback length. The upper and lower channels are the highest high and lowest low over this many bars."),
41008
+ colorInput(BEARISH, "Upper", "upperColor", "Color of the upper channel line."),
41009
+ colorInput(NEUTRAL, "Middle", "middleColor", "Color of the channel midline."),
41010
+ colorInput(BULLISH, "Lower", "lowerColor", "Color of the lower channel line."),
41011
+ colorInput(transp(NEUTRAL, 92), "Fill", "fillColor", "Color of the channel interior fill.")
41012
+ ],
40547
41013
  compute: (bars, inputs) => {
40548
41014
  const len = num3(inputs, "length", 20);
40549
41015
  const upper = highest(highs(bars), len);
40550
41016
  const lower = lowest(lows(bars), len);
40551
- const ink = str3(inputs, "color", SERIES_LINE);
40552
41017
  return {
40553
41018
  plots: [
40554
- { key: "basis", title: "Basis", values: zip(upper, lower, (u, l) => (u + l) / 2), color: WARNING },
40555
- { key: "upper", title: "Upper", values: upper, color: ink },
40556
- { key: "lower", title: "Lower", values: lower, color: ink }
41019
+ { key: "upper", title: "Upper", values: upper, color: str3(inputs, "upperColor", BEARISH) },
41020
+ { key: "middle", title: "Middle", kind: "circles", values: zip(upper, lower, (u, l) => (u + l) / 2), color: str3(inputs, "middleColor", NEUTRAL) },
41021
+ { key: "lower", title: "Lower", values: lower, color: str3(inputs, "lowerColor", BULLISH) }
40557
41022
  ],
40558
- bands: [{ key: "dc", from: "upper", to: "lower", color: withAlpha2(ink) }]
41023
+ bands: [{ key: "dc", from: "upper", to: "lower", color: str3(inputs, "fillColor", transp(NEUTRAL, 92)) }]
40559
41024
  };
40560
41025
  }
40561
41026
  };
@@ -40564,16 +41029,37 @@ var chandelier = {
40564
41029
  title: "Chandelier Exit",
40565
41030
  shortTitle: "CE",
40566
41031
  overlay: true,
40567
- inputs: [lengthInput(22), { key: "mult", title: "ATR multiplier", type: "float", defval: 3, min: 0.1, max: 50, step: 0.1 }],
41032
+ inputs: [
41033
+ lengthInput(22, "length", "Length", 5e3, "Lookback used for both the highest high / lowest low extremes and the ATR."),
41034
+ 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)."),
41035
+ 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."),
41036
+ colorInput(BULLISH, "Long Stop", "longColor", "Color of the long trailing stop line."),
41037
+ colorInput(BEARISH, "Short Stop", "shortColor", "Color of the short trailing stop line.")
41038
+ ],
40568
41039
  compute: (bars, inputs) => {
40569
41040
  const len = num3(inputs, "length", 22);
40570
41041
  const range = map(atr(bars, len), (x) => x * num3(inputs, "mult", 3));
40571
- const long = zip(highest(highs(bars), len), range, (h, r) => h - r);
40572
- const short = zip(lowest(lows(bars), len), range, (l, r) => l + r);
41042
+ const basicLong = zip(highest(highs(bars), len), range, (h, r) => h - r);
41043
+ const basicShort = zip(lowest(lows(bars), len), range, (l, r) => l + r);
41044
+ let long = basicLong;
41045
+ let short = basicShort;
41046
+ if (bool(inputs, "ratchet", false)) {
41047
+ long = new Array(bars.length).fill(Number.NaN);
41048
+ short = new Array(bars.length).fill(Number.NaN);
41049
+ for (let i = 0; i < bars.length; i++) {
41050
+ const bl = basicLong[i];
41051
+ const bs = basicShort[i];
41052
+ const prevClose = i > 0 ? bars[i - 1].close : bars[i].close;
41053
+ const prevLong = i > 0 && Number.isFinite(long[i - 1]) ? long[i - 1] : bl;
41054
+ const prevShort = i > 0 && Number.isFinite(short[i - 1]) ? short[i - 1] : bs;
41055
+ long[i] = Number.isFinite(bl) ? prevClose > prevLong ? Math.max(bl, prevLong) : bl : Number.NaN;
41056
+ short[i] = Number.isFinite(bs) ? prevClose < prevShort ? Math.min(bs, prevShort) : bs : Number.NaN;
41057
+ }
41058
+ }
40573
41059
  return {
40574
41060
  plots: [
40575
- { key: "long", title: "Long stop", values: long, color: BULLISH },
40576
- { key: "short", title: "Short stop", values: short, color: BEARISH }
41061
+ { key: "long", title: "Long Stop", values: long, color: str3(inputs, "longColor", BULLISH) },
41062
+ { key: "short", title: "Short Stop", values: short, color: str3(inputs, "shortColor", BEARISH) }
40577
41063
  ]
40578
41064
  };
40579
41065
  }
@@ -40584,21 +41070,22 @@ var chandeKroll = {
40584
41070
  shortTitle: "CKS",
40585
41071
  overlay: true,
40586
41072
  inputs: [
40587
- { key: "p", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
40588
- { key: "x", title: "ATR multiplier", type: "float", defval: 1, min: 0.1, max: 50, step: 0.1 },
40589
- { key: "q", title: "Stop length", type: "int", defval: 9, min: 1, max: 500, step: 1 }
41073
+ intInput("p", "ATR Length (P)", 10, 1, 5e3, "Lookback used for both the ATR and the highest high / lowest low price extremes."),
41074
+ floatInput("x", "ATR Multiplier (X)", 1, 0, 50, 0.1, "Multiplier applied to the ATR to offset the preliminary stops from the price extremes."),
41075
+ intInput("q", "Stop Length (Q)", 9, 1, 5e3, "Lookback over which the preliminary stops are extended into the final stop levels."),
41076
+ colorInput(BEARISH, "Stop Short", "shortColor", "Color of the protective stop line for short positions."),
41077
+ colorInput(BULLISH, "Stop Long", "longColor", "Color of the protective stop line for long positions.")
40590
41078
  ],
40591
41079
  compute: (bars, inputs) => {
40592
41080
  const p = num3(inputs, "p", 10);
40593
- const x = num3(inputs, "x", 1);
40594
41081
  const q = num3(inputs, "q", 9);
40595
- const range = map(atr(bars, p), (v) => v * x);
41082
+ const range = map(atr(bars, p), (v) => v * num3(inputs, "x", 1));
40596
41083
  const firstHigh = zip(highest(highs(bars), p), range, (h, r) => h - r);
40597
41084
  const firstLow = zip(lowest(lows(bars), p), range, (l, r) => l + r);
40598
41085
  return {
40599
41086
  plots: [
40600
- { key: "stopShort", title: "Stop short", values: highest(firstHigh, q), color: BEARISH },
40601
- { key: "stopLong", title: "Stop long", values: lowest(firstLow, q), color: BULLISH }
41087
+ { key: "stopShort", title: "Stop Short", values: highest(firstHigh, q), color: str3(inputs, "shortColor", BEARISH) },
41088
+ { key: "stopLong", title: "Stop Long", values: lowest(firstLow, q), color: str3(inputs, "longColor", BULLISH) }
40602
41089
  ]
40603
41090
  };
40604
41091
  }
@@ -40609,36 +41096,42 @@ var supertrend = {
40609
41096
  shortTitle: "SuperTrend",
40610
41097
  overlay: true,
40611
41098
  inputs: [
40612
- { key: "atrLength", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
40613
- { key: "mult", title: "Factor", type: "float", defval: 3, min: 0.1, max: 50, step: 0.1 }
41099
+ intInput("atrLength", "ATR Length", 10, 1, 5e3, "Number of bars used to compute the Wilder ATR."),
41100
+ floatInput("mult", "Factor", 3, 0, 50, 0.1, "ATR multiplier setting how far the trailing stop sits from the bar midpoint."),
41101
+ colorInput(BULLISH, "Uptrend Color", "upColor", "Line color while the trend is up."),
41102
+ colorInput(BEARISH, "Downtrend Color", "downColor", "Line color while the trend is down.")
40614
41103
  ],
40615
41104
  compute: (bars, inputs) => {
40616
- const len = num3(inputs, "atrLength", 10);
40617
41105
  const mult = num3(inputs, "mult", 3);
40618
- const range = atr(bars, len);
41106
+ const range = atr(bars, num3(inputs, "atrLength", 10));
40619
41107
  const n = bars.length;
40620
- const st = new Array(n).fill(Number.NaN);
40621
- const dir = new Array(n).fill(1);
40622
- let up = Number.NaN;
40623
- let dn = Number.NaN;
40624
- let trend = 1;
41108
+ const up = new Array(n).fill(Number.NaN);
41109
+ const down = new Array(n).fill(Number.NaN);
41110
+ let finalUpper = Number.NaN;
41111
+ let finalLower = Number.NaN;
41112
+ let trend = -1;
40625
41113
  for (let i = 0; i < n; i++) {
40626
41114
  const r = range[i];
40627
41115
  if (!Number.isFinite(r)) continue;
40628
41116
  const b = bars[i];
40629
41117
  const mid = (b.high + b.low) / 2;
40630
- const basicUp = mid - mult * r;
40631
- const basicDn = mid + mult * r;
41118
+ const basicUpper = mid + mult * r;
41119
+ const basicLower = mid - mult * r;
41120
+ const prevUpper = Number.isFinite(finalUpper) ? finalUpper : basicUpper;
41121
+ const prevLower = Number.isFinite(finalLower) ? finalLower : basicLower;
40632
41122
  const prevClose = i > 0 ? bars[i - 1].close : b.close;
40633
- up = Number.isFinite(up) && prevClose > up ? Math.max(basicUp, up) : basicUp;
40634
- dn = Number.isFinite(dn) && prevClose < dn ? Math.min(basicDn, dn) : basicDn;
40635
- if (trend === 1 && b.close < up) trend = -1;
40636
- else if (trend === -1 && b.close > dn) trend = 1;
40637
- dir[i] = trend;
40638
- st[i] = trend === 1 ? up : dn;
41123
+ finalUpper = prevClose <= prevUpper ? Math.min(basicUpper, prevUpper) : basicUpper;
41124
+ finalLower = prevClose >= prevLower ? Math.max(basicLower, prevLower) : basicLower;
41125
+ trend = b.close > finalUpper ? 1 : b.close < finalLower ? -1 : trend;
41126
+ if (trend === 1) up[i] = finalLower;
41127
+ else down[i] = finalUpper;
40639
41128
  }
40640
- const colors = dir.map((d, i) => Number.isFinite(st[i]) ? d === 1 ? BULLISH : BEARISH : null);
40641
- return { plots: [{ key: "st", title: "SuperTrend", values: st, color: BULLISH, width: 2, colors }] };
41129
+ return {
41130
+ plots: [
41131
+ { key: "up", title: "Uptrend", values: up, color: str3(inputs, "upColor", BULLISH) },
41132
+ { key: "down", title: "Downtrend", values: down, color: str3(inputs, "downColor", BEARISH) }
41133
+ ]
41134
+ };
40642
41135
  }
40643
41136
  };
40644
41137
  var alligator = {
@@ -40647,12 +41140,15 @@ var alligator = {
40647
41140
  shortTitle: "Alligator",
40648
41141
  overlay: true,
40649
41142
  inputs: [
40650
- { key: "jawLength", title: "Jaw length", type: "int", defval: 13, min: 1, max: 500, step: 1 },
40651
- { key: "teethLength", title: "Teeth length", type: "int", defval: 8, min: 1, max: 500, step: 1 },
40652
- { key: "lipsLength", title: "Lips length", type: "int", defval: 5, min: 1, max: 500, step: 1 },
40653
- { key: "jawOffset", title: "Jaw offset", type: "int", defval: 8, min: 0, max: 100, step: 1 },
40654
- { key: "teethOffset", title: "Teeth offset", type: "int", defval: 5, min: 0, max: 100, step: 1 },
40655
- { key: "lipsOffset", title: "Lips offset", type: "int", defval: 3, min: 0, max: 100, step: 1 }
41143
+ intInput("jawLength", "Jaw Length", 13, 1, 5e3, "Smoothing length of the Jaw SMMA of median price (hl2), 13 in the classic setup."),
41144
+ intInput("teethLength", "Teeth Length", 8, 1, 5e3, "Smoothing length of the Teeth SMMA of median price (hl2), 8 in the classic setup."),
41145
+ intInput("lipsLength", "Lips Length", 5, 1, 5e3, "Smoothing length of the Lips SMMA of median price (hl2), 5 in the classic setup."),
41146
+ intInput("jawOffset", "Jaw Offset", 8, 0, 100, "Number of bars the Jaw plot is shifted into the future, 8 in the classic setup."),
41147
+ intInput("teethOffset", "Teeth Offset", 5, 0, 100, "Number of bars the Teeth plot is shifted into the future, 5 in the classic setup."),
41148
+ intInput("lipsOffset", "Lips Offset", 3, 0, 100, "Number of bars the Lips plot is shifted into the future, 3 in the classic setup."),
41149
+ colorInput(JAW_COLOR, "Jaw", "jawColor", "Color of the Jaw line."),
41150
+ colorInput(TEETH_COLOR, "Teeth", "teethColor", "Color of the Teeth line."),
41151
+ colorInput(LIPS_COLOR, "Lips", "lipsColor", "Color of the Lips line.")
40656
41152
  ],
40657
41153
  compute: (bars, inputs) => {
40658
41154
  const hl2 = sourceValues(bars, "HL2");
@@ -40661,99 +41157,134 @@ var alligator = {
40661
41157
  const lips = shift(rma(hl2, num3(inputs, "lipsLength", 5)), num3(inputs, "lipsOffset", 3));
40662
41158
  return {
40663
41159
  plots: [
40664
- { key: "jaw", title: "Jaw", values: jaw, color: SERIES_LINE },
40665
- { key: "teeth", title: "Teeth", values: teeth, color: BEARISH },
40666
- { key: "lips", title: "Lips", values: lips, color: BULLISH }
41160
+ { key: "jaw", title: "Jaw", values: jaw, color: str3(inputs, "jawColor", JAW_COLOR) },
41161
+ { key: "teeth", title: "Teeth", values: teeth, color: str3(inputs, "teethColor", TEETH_COLOR) },
41162
+ { key: "lips", title: "Lips", values: lips, color: str3(inputs, "lipsColor", LIPS_COLOR) }
40667
41163
  ]
40668
41164
  };
40669
41165
  }
40670
41166
  };
41167
+ var GATOR_LINES = "Alligator Lines";
41168
+ var SMMA_TYPE = "SMMA (Wilder)";
40671
41169
  var gator = {
40672
41170
  type: "gator-oscillator",
40673
41171
  title: "Gator Oscillator",
40674
41172
  shortTitle: "Gator Oscillator",
40675
41173
  overlay: false,
40676
- inputs: [],
40677
- compute: (bars) => {
40678
- const hl2 = sourceValues(bars, "HL2");
40679
- const jaw = shift(rma(hl2, 13), 8);
40680
- const teeth = shift(rma(hl2, 8), 5);
40681
- const lips = shift(rma(hl2, 5), 3);
41174
+ inputs: [
41175
+ { ...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 },
41176
+ { ...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 },
41177
+ { ...intInput("jawLength", "Jaw Length", 13, 1, 500, "Averaging length of the jaw line."), group: GATOR_LINES },
41178
+ { ...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 },
41179
+ { ...intInput("teethLength", "Teeth Length", 8, 1, 500, "Averaging length of the teeth line."), group: GATOR_LINES },
41180
+ { ...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 },
41181
+ { ...intInput("lipsLength", "Lips Length", 5, 1, 500, "Averaging length of the lips line."), group: GATOR_LINES },
41182
+ { ...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 },
41183
+ colorInput(BULLISH, "Expanding", "growColor", "Column color while a spread grew against its own previous bar \u2014 the averages are spreading apart."),
41184
+ colorInput(BEARISH, "Contracting", "shrinkColor", "Column color while a spread shrank \u2014 the averages are converging. Color encodes expansion, not trade direction."),
41185
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero line separating the two histograms.")
41186
+ ],
41187
+ compute: (bars, inputs) => {
41188
+ const src = sourceValues(bars, str3(inputs, "source", "HL2"));
41189
+ const kind = str3(inputs, "maType", SMMA_TYPE);
41190
+ const smooth = (len) => kind === "SMA" ? sma(src, len) : kind === "EMA" ? ema(src, len) : rma(src, len);
41191
+ const jaw = shift(smooth(num3(inputs, "jawLength", 13)), num3(inputs, "jawOffset", 8));
41192
+ const teeth = shift(smooth(num3(inputs, "teethLength", 8)), num3(inputs, "teethOffset", 5));
41193
+ const lips = shift(smooth(num3(inputs, "lipsLength", 5)), num3(inputs, "lipsOffset", 3));
40682
41194
  const upper = map(sub(jaw, teeth), Math.abs);
40683
- const lower = map(sub(teeth, lips), (x) => -Math.abs(x));
40684
- const colorByExpansion = (v) => v.map((x, i) => {
40685
- if (!Number.isFinite(x)) return null;
40686
- const prev2 = i > 0 ? v[i - 1] : Number.NaN;
40687
- return Number.isFinite(prev2) && Math.abs(x) > Math.abs(prev2) ? BULLISH : BEARISH;
40688
- });
41195
+ const lower = map(sub(teeth, lips), Math.abs);
41196
+ const grow = str3(inputs, "growColor", BULLISH);
41197
+ const shrink = str3(inputs, "shrinkColor", BEARISH);
41198
+ const expansionColors2 = (v) => {
41199
+ let growing = false;
41200
+ return v.map((x, i) => {
41201
+ if (!Number.isFinite(x)) return null;
41202
+ const prev2 = i > 0 ? v[i - 1] : Number.NaN;
41203
+ if (Number.isFinite(prev2)) growing = x > prev2 ? true : x < prev2 ? false : growing;
41204
+ return growing ? grow : shrink;
41205
+ });
41206
+ };
40689
41207
  return {
40690
41208
  plots: [
40691
- { key: "upper", title: "Upper", values: upper, kind: "histogram", color: BULLISH, colors: colorByExpansion(upper), base: 0 },
40692
- { key: "lower", title: "Lower", values: lower, kind: "histogram", color: BEARISH, colors: colorByExpansion(lower), base: 0 }
40693
- ]
41209
+ { key: "upper", title: "Upper Histogram (Jaw - Teeth)", values: upper, kind: "columns", color: grow, colors: expansionColors2(upper), base: 0 },
41210
+ { key: "lower", title: "Lower Histogram (Teeth - Lips)", values: map(lower, (x) => -x), kind: "columns", color: shrink, colors: expansionColors2(lower), base: 0 }
41211
+ ],
41212
+ levels: [{ key: "zero", price: 0, color: str3(inputs, "zeroColor", NEUTRAL), lineStyle: "solid", title: "Zero Line" }]
40694
41213
  };
40695
41214
  }
40696
41215
  };
40697
41216
  var bandSpecs = [bollingerBands, percentB, bandwidth, keltner, donchian, chandelier, chandeKroll, supertrend, alligator, gator];
40698
41217
 
40699
41218
  // src/core/native-indicators/classics/oscillators.ts
40700
- function guideLevels(low, high, mid) {
40701
- const levels = [
40702
- { key: "upper", price: high, color: NEUTRAL },
40703
- { key: "lower", price: low, color: NEUTRAL }
41219
+ function thresholdInputs(obDefault, osDefault, min = 0, max = 100, obTitle = "Overbought Level", osTitle = "Oversold Level") {
41220
+ return [
41221
+ floatInput("overbought", obTitle, obDefault, min, max, 1, "Level above which the oscillator is considered overbought."),
41222
+ floatInput("oversold", osTitle, osDefault, min, max, 1, "Level below which the oscillator is considered oversold.")
40704
41223
  ];
40705
- if (mid != null) levels.splice(1, 0, { key: "middle", price: mid, color: NEUTRAL, lineStyle: "dotted" });
40706
- return levels;
40707
- }
40708
- var ZERO_LEVEL = [{ key: "zero", price: 0, color: NEUTRAL }];
40709
- function histColors(values) {
40710
- return values.map((x, i) => {
40711
- if (!Number.isFinite(x)) return null;
40712
- const prev2 = i > 0 ? values[i - 1] : Number.NaN;
40713
- const rising = Number.isFinite(prev2) ? x >= prev2 : true;
40714
- if (x >= 0) return rising ? BULLISH : `${BULLISH}80`;
40715
- return rising ? `${BEARISH}80` : BEARISH;
40716
- });
40717
41224
  }
40718
41225
  var rsiSpec = {
40719
41226
  type: "rsi",
40720
41227
  title: "Relative Strength Index",
40721
41228
  shortTitle: "RSI",
40722
41229
  overlay: false,
40723
- inputs: [lengthInput(14), sourceInput(), colorInput()],
40724
- compute: (bars, inputs) => ({
40725
- plots: [
40726
- {
40727
- key: "rsi",
40728
- title: "RSI",
40729
- values: rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
40730
- color: str3(inputs, "color", SERIES_LINE),
40731
- width: 2
40732
- }
40733
- ],
40734
- levels: guideLevels(30, 70, 50)
41230
+ inputs: [
41231
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used in the Wilder smoothing of average gains and losses."),
41232
+ sourceInput("Close", "source", "Source", "Price series the RSI is calculated on."),
41233
+ ...thresholdInputs(70, 30, 0, 100, "Overbought", "Oversold"),
41234
+ colorInput(NEUTRAL, "RSI", "color", "Color of the RSI line at the midline. Toward either threshold it blends into that threshold\u2019s color."),
41235
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level."),
41236
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
41237
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought, midline and oversold levels."),
41238
+ colorInput(transp(NEUTRAL, 90), "Band Fill", "fillColor", "Color of the fill between the overbought and oversold levels.")
41239
+ ],
41240
+ compute: (bars, inputs) => boundedOscillator({
41241
+ key: "rsi",
41242
+ title: "RSI",
41243
+ values: rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
41244
+ scale: { top: 100, bottom: 0 },
41245
+ overbought: num3(inputs, "overbought", 70),
41246
+ oversold: num3(inputs, "oversold", 30),
41247
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
41248
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
41249
+ width: 2,
41250
+ midline: 50,
41251
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 90))
40735
41252
  })
40736
41253
  };
41254
+ function stochasticOutput(k, d, inputs) {
41255
+ const overbought = num3(inputs, "overbought", 80);
41256
+ const oversold = num3(inputs, "oversold", 20);
41257
+ return {
41258
+ plots: [
41259
+ { key: "k", title: "%K", values: k, color: str3(inputs, "kColor", INFO) },
41260
+ { key: "d", title: "%D", values: d, color: str3(inputs, "dColor", WARNING) },
41261
+ anchorPlot("obAnchor", "Overbought Anchor", k.length, overbought),
41262
+ anchorPlot("osAnchor", "Oversold Anchor", k.length, oversold)
41263
+ ],
41264
+ bands: [{ key: "band", from: "obAnchor", to: "osAnchor", color: str3(inputs, "fillColor", transp(INFO, 90)) }],
41265
+ levels: [
41266
+ { key: "overbought", price: overbought, color: NEUTRAL, lineStyle: "dashed", title: "Overbought" },
41267
+ { key: "oversold", price: oversold, color: NEUTRAL, lineStyle: "dashed", title: "Oversold" }
41268
+ ]
41269
+ };
41270
+ }
40737
41271
  var stochastic = {
40738
41272
  type: "stochastic",
40739
41273
  title: "Stochastic",
40740
41274
  shortTitle: "Stoch",
40741
41275
  overlay: false,
40742
41276
  inputs: [
40743
- lengthInput(14, "kLength", "%K length"),
40744
- lengthInput(1, "kSmoothing", "%K smoothing", 500),
40745
- lengthInput(3, "dLength", "%D smoothing", 500)
41277
+ lengthInput(14, "kLength", "%K Length", 5e3, "Number of bars used for the highest high / lowest low range of the raw %K."),
41278
+ 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."),
41279
+ lengthInput(3, "dLength", "%D Smoothing", 500, "Simple moving average length of the %D signal line, applied to the smoothed %K."),
41280
+ ...thresholdInputs(80, 20),
41281
+ colorInput(INFO, "%K", "kColor", "Color of the %K line."),
41282
+ colorInput(WARNING, "%D", "dColor", "Color of the %D signal line."),
41283
+ colorInput(transp(INFO, 90), "Band Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
40746
41284
  ],
40747
41285
  compute: (bars, inputs) => {
40748
- const k = sma(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "kLength", 14)), num3(inputs, "kSmoothing", 1));
40749
- const d = sma(k, num3(inputs, "dLength", 3));
40750
- return {
40751
- plots: [
40752
- { key: "k", title: "%K", values: k, color: SERIES_LINE, width: 2 },
40753
- { key: "d", title: "%D", values: d, color: WARNING }
40754
- ],
40755
- levels: guideLevels(20, 80)
40756
- };
41286
+ const k = sma(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "kLength", 14)), num3(inputs, "kSmoothing", 3));
41287
+ return stochasticOutput(k, sma(k, num3(inputs, "dLength", 3)), inputs);
40757
41288
  }
40758
41289
  };
40759
41290
  var stochasticRsi = {
@@ -40762,23 +41293,20 @@ var stochasticRsi = {
40762
41293
  shortTitle: "Stoch RSI",
40763
41294
  overlay: false,
40764
41295
  inputs: [
40765
- lengthInput(14, "rsiLength", "RSI length"),
40766
- lengthInput(14, "stochLength", "Stochastic length"),
40767
- lengthInput(3, "kSmoothing", "%K smoothing", 500),
40768
- lengthInput(3, "dLength", "%D smoothing", 500),
40769
- sourceInput()
41296
+ sourceInput("Close", "source", "RSI Source", "Price series the RSI is computed on."),
41297
+ lengthInput(14, "rsiLength", "RSI Length", 5e3, "Lookback length of the Wilder RSI."),
41298
+ lengthInput(14, "stochLength", "Stochastic Length", 5e3, "Lookback over which the highest and lowest RSI values are taken."),
41299
+ lengthInput(3, "kSmoothing", "%K Smoothing", 500, "Simple moving average length applied to the stochastic of RSI to form %K."),
41300
+ lengthInput(3, "dLength", "%D Smoothing", 500, "Simple moving average length applied to %K to form the %D signal line."),
41301
+ ...thresholdInputs(80, 20),
41302
+ colorInput(INFO, "%K Color", "kColor", "Color of the %K line."),
41303
+ colorInput(WARNING, "%D Color", "dColor", "Color of the %D signal line."),
41304
+ colorInput(transp(INFO, 90), "Zone Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
40770
41305
  ],
40771
41306
  compute: (bars, inputs) => {
40772
41307
  const r = rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "rsiLength", 14));
40773
41308
  const k = sma(stoch(r, r, r, num3(inputs, "stochLength", 14)), num3(inputs, "kSmoothing", 3));
40774
- const d = sma(k, num3(inputs, "dLength", 3));
40775
- return {
40776
- plots: [
40777
- { key: "k", title: "%K", values: k, color: SERIES_LINE, width: 2 },
40778
- { key: "d", title: "%D", values: d, color: WARNING }
40779
- ],
40780
- levels: guideLevels(20, 80)
40781
- };
41309
+ return stochasticOutput(k, sma(k, num3(inputs, "dLength", 3)), inputs);
40782
41310
  }
40783
41311
  };
40784
41312
  var macd = {
@@ -40786,23 +41314,35 @@ var macd = {
40786
41314
  title: "MACD",
40787
41315
  overlay: false,
40788
41316
  inputs: [
40789
- lengthInput(12, "fastLength", "Fast length"),
40790
- lengthInput(26, "slowLength", "Slow length"),
40791
- lengthInput(9, "signalLength", "Signal smoothing", 500),
40792
- sourceInput()
41317
+ lengthInput(12, "fastLength", "Fast Length", 5e3, "Length of the fast EMA."),
41318
+ lengthInput(26, "slowLength", "Slow Length", 5e3, "Length of the slow EMA."),
41319
+ lengthInput(9, "signalLength", "Signal Smoothing", 500, "Length of the EMA applied to the MACD line to obtain the signal line."),
41320
+ sourceInput("Close", "source", "Source", "Price series used to compute the fast and slow EMAs."),
41321
+ colorInput(INFO, "MACD Line", "color", "Color of the MACD line."),
41322
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
41323
+ colorInput(BULLISH, "Grow Above", "growAboveColor", "Histogram color when above zero and rising."),
41324
+ colorInput(transp(BULLISH, 50), "Fall Above", "fallAboveColor", "Histogram color when above zero and falling."),
41325
+ colorInput(transp(BEARISH, 50), "Grow Below", "growBelowColor", "Histogram color when below zero and rising."),
41326
+ colorInput(BEARISH, "Fall Below", "fallBelowColor", "Histogram color when below zero and falling.")
40793
41327
  ],
40794
41328
  compute: (bars, inputs) => {
40795
41329
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
40796
41330
  const line = sub(ema(src, num3(inputs, "fastLength", 12)), ema(src, num3(inputs, "slowLength", 26)));
40797
41331
  const signal = ema(line, num3(inputs, "signalLength", 9));
40798
41332
  const hist = sub(line, signal);
41333
+ const colors = momentumColumnColors(hist, {
41334
+ growAbove: str3(inputs, "growAboveColor", BULLISH),
41335
+ fallAbove: str3(inputs, "fallAboveColor", transp(BULLISH, 50)),
41336
+ growBelow: str3(inputs, "growBelowColor", transp(BEARISH, 50)),
41337
+ fallBelow: str3(inputs, "fallBelowColor", BEARISH)
41338
+ });
40799
41339
  return {
40800
41340
  plots: [
40801
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: histColors(hist), base: 0 },
40802
- { key: "macd", title: "MACD", values: line, color: SERIES_LINE, width: 2 },
40803
- { key: "signal", title: "Signal", values: signal, color: WARNING }
41341
+ { key: "hist", title: "Histogram", values: hist, kind: "columns", color: BULLISH, colors, base: 0 },
41342
+ { key: "macd", title: "MACD", values: line, color: str3(inputs, "color", INFO) },
41343
+ { key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) }
40804
41344
  ],
40805
- levels: ZERO_LEVEL
41345
+ levels: [{ key: "zero", price: 0, color: NEUTRAL, lineStyle: "dashed", title: "Zero Line" }]
40806
41346
  };
40807
41347
  }
40808
41348
  };
@@ -40812,26 +41352,49 @@ var ppo = {
40812
41352
  shortTitle: "PPO",
40813
41353
  overlay: false,
40814
41354
  inputs: [
40815
- lengthInput(12, "fastLength", "Fast length"),
40816
- lengthInput(26, "slowLength", "Slow length"),
40817
- lengthInput(9, "signalLength", "Signal smoothing", 500),
40818
- sourceInput()
41355
+ sourceInput("Close", "source", "Source", "Price series fed into the fast and slow averages."),
41356
+ lengthInput(12, "fastLength", "Fast Length", 5e3, "Length of the fast moving average."),
41357
+ 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."),
41358
+ optionInput("maType", "Average Type", "EMA", ["EMA", "SMA"], "Moving average type used for the fast and slow averages."),
41359
+ lengthInput(9, "signalLength", "Signal Smoothing", 500, "Length of the signal line that smooths the PPO line."),
41360
+ optionInput("signalType", "Signal Type", "EMA", ["EMA", "SMA"], "Moving average type used for the signal line."),
41361
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the PPO line, gradient fill and histogram above the zero line."),
41362
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the PPO line, gradient fill and histogram below the zero line."),
41363
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
41364
+ 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."),
41365
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the zero line and the PPO line with a vertical gradient.")
40819
41366
  ],
40820
41367
  compute: (bars, inputs) => {
40821
41368
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
40822
- const fast = ema(src, num3(inputs, "fastLength", 12));
40823
- const slow = ema(src, num3(inputs, "slowLength", 26));
41369
+ const average = (kind2, v, len) => kind2 === "SMA" ? sma(v, len) : ema(v, len);
41370
+ const kind = str3(inputs, "maType", "EMA");
41371
+ const fast = average(kind, src, num3(inputs, "fastLength", 12));
41372
+ const slow = average(kind, src, num3(inputs, "slowLength", 26));
40824
41373
  const line = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : (f - s) / s * 100);
40825
- const signal = ema(line, num3(inputs, "signalLength", 9));
41374
+ const signal = average(str3(inputs, "signalType", "EMA"), line, num3(inputs, "signalLength", 9));
40826
41375
  const hist = sub(line, signal);
40827
- return {
40828
- plots: [
40829
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: histColors(hist), base: 0 },
40830
- { key: "ppo", title: "PPO", values: line, color: SERIES_LINE, width: 2 },
40831
- { key: "signal", title: "Signal", values: signal, color: WARNING }
40832
- ],
40833
- levels: ZERO_LEVEL
40834
- };
41376
+ const bull = str3(inputs, "bullColor", BULLISH);
41377
+ const bear = str3(inputs, "bearColor", BEARISH);
41378
+ const out = centeredOscillator({
41379
+ key: "ppo",
41380
+ title: "PPO Line",
41381
+ values: line,
41382
+ ink: { bull, bear, neutral: bear },
41383
+ zeroInk: transp(NEUTRAL, 90),
41384
+ fill: bool(inputs, "gradientFill", true),
41385
+ extraPlots: [{ key: "signal", title: "Signal Line", values: signal, color: str3(inputs, "signalColor", WARNING) }]
41386
+ });
41387
+ if (bool(inputs, "showHistogram", true)) {
41388
+ const colors = hist.map((x, i) => {
41389
+ if (!Number.isFinite(x)) return null;
41390
+ const prev2 = i > 0 && Number.isFinite(hist[i - 1]) ? hist[i - 1] : x;
41391
+ const ink = x >= 0 ? bull : bear;
41392
+ const expanding = x >= 0 ? x > prev2 : x < prev2;
41393
+ return transp(ink, expanding ? 40 : 70);
41394
+ });
41395
+ out.plots.unshift({ key: "hist", title: "Histogram", values: hist, kind: "columns", color: bull, colors, base: 0 });
41396
+ }
41397
+ return out;
40835
41398
  }
40836
41399
  };
40837
41400
  var awesome = {
@@ -40839,48 +41402,67 @@ var awesome = {
40839
41402
  title: "Awesome Oscillator",
40840
41403
  shortTitle: "AO",
40841
41404
  overlay: false,
40842
- inputs: [],
40843
- compute: (bars) => {
41405
+ inputs: [
41406
+ colorInput(BULLISH, "Rising", "risingColor", "Histogram color when the oscillator is higher than on the previous bar."),
41407
+ colorInput(BEARISH, "Falling", "fallingColor", "Histogram color when the oscillator is lower than or equal to the previous bar."),
41408
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
41409
+ ],
41410
+ compute: (bars, inputs) => {
40844
41411
  const hl2 = sourceValues(bars, "HL2");
40845
41412
  const ao = sub(sma(hl2, 5), sma(hl2, 34));
41413
+ const rising = str3(inputs, "risingColor", BULLISH);
41414
+ const falling = str3(inputs, "fallingColor", BEARISH);
40846
41415
  const colors = ao.map((x, i) => {
40847
41416
  if (!Number.isFinite(x)) return null;
40848
41417
  const prev2 = i > 0 ? ao[i - 1] : Number.NaN;
40849
- return Number.isFinite(prev2) && x < prev2 ? BEARISH : BULLISH;
41418
+ return Number.isFinite(prev2) && x > prev2 ? rising : falling;
40850
41419
  });
40851
41420
  return {
40852
- plots: [{ key: "ao", title: "AO", values: ao, kind: "histogram", color: BULLISH, colors, base: 0 }],
40853
- levels: ZERO_LEVEL
41421
+ plots: [{ key: "ao", title: "AO", values: ao, kind: "columns", color: rising, colors, base: 0 }],
41422
+ levels: [{ key: "zero", price: 0, color: str3(inputs, "zeroColor", NEUTRAL), lineStyle: "dashed", title: "Zero Line" }]
40854
41423
  };
40855
41424
  }
40856
41425
  };
41426
+ var CCI_SCALING = 0.015;
40857
41427
  var cci = {
40858
41428
  type: "commodity-channel-index",
40859
41429
  title: "Commodity Channel Index",
40860
41430
  shortTitle: "CCI",
40861
41431
  overlay: false,
40862
- inputs: [lengthInput(20), sourceInput("HLC3"), colorInput()],
41432
+ inputs: [
41433
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used for the moving average and the mean absolute deviation of the source."),
41434
+ sourceInput("HLC3", "source", "Source", "Price series measured by the CCI; the typical price hlc3 is the classic choice."),
41435
+ floatInput("upper", "Upper Level", 100, -1e3, 1e3, 10, "Overbought reference level, +100 classically."),
41436
+ floatInput("lower", "Lower Level", -100, -1e3, 1e3, 10, "Oversold reference level, \u2212100 classically."),
41437
+ colorInput(NEUTRAL, "CCI Line", "color", "Color of the CCI line."),
41438
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the CCI reads above zero."),
41439
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the CCI reads below zero."),
41440
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the upper, lower and zero reference lines.")
41441
+ ],
40863
41442
  compute: (bars, inputs) => {
40864
41443
  const len = num3(inputs, "length", 20);
40865
41444
  const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
40866
41445
  const basis = sma(src, len);
40867
- const dev = new Array(src.length).fill(Number.NaN);
40868
- for (let i = len - 1; i < src.length; i++) {
40869
- const mean = basis[i];
40870
- if (!Number.isFinite(mean)) continue;
40871
- let d = 0;
40872
- for (let k = i - len + 1; k <= i; k++) d += Math.abs(src[k] - mean);
40873
- dev[i] = d / len;
40874
- }
41446
+ const dev = meanDev(src, len);
40875
41447
  const values = src.map((x, i) => {
40876
41448
  const b = basis[i];
40877
41449
  const d = dev[i];
40878
- return Number.isFinite(b) && Number.isFinite(d) && d !== 0 ? (x - b) / (0.015 * d) : Number.NaN;
41450
+ if (!Number.isFinite(b) || !Number.isFinite(d)) return Number.NaN;
41451
+ return d === 0 ? 0 : (x - b) / (CCI_SCALING * d);
41452
+ });
41453
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
41454
+ return centeredOscillator({
41455
+ key: "cci",
41456
+ title: "CCI",
41457
+ values,
41458
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
41459
+ zeroInk: levelInk,
41460
+ zeroLineStyle: "dotted",
41461
+ extraLevels: [
41462
+ { key: "upper", price: num3(inputs, "upper", 100), color: levelInk, lineStyle: "dashed", title: "Upper Level" },
41463
+ { key: "lower", price: num3(inputs, "lower", -100), color: levelInk, lineStyle: "dashed", title: "Lower Level" }
41464
+ ]
40879
41465
  });
40880
- return {
40881
- plots: [{ key: "cci", title: "CCI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
40882
- levels: guideLevels(-100, 100, 0)
40883
- };
40884
41466
  }
40885
41467
  };
40886
41468
  var williamsR = {
@@ -40888,31 +41470,78 @@ var williamsR = {
40888
41470
  title: "Williams %R",
40889
41471
  shortTitle: "%R",
40890
41472
  overlay: false,
40891
- inputs: [lengthInput(14), colorInput()],
41473
+ inputs: [
41474
+ lengthInput(14, "length", "Length", 5e3, "Lookback length of the highest high / lowest low range. 14 is the classic setting."),
41475
+ sourceInput("Close", "source", "Source", "Series located within the lookback range. Close is the classic choice."),
41476
+ ...thresholdInputs(-20, -80, -100, 0),
41477
+ colorInput(NEUTRAL, "%R Line", "color", "Color of the %R line between the thresholds. Toward either level it blends into that level\u2019s color."),
41478
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
41479
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level.")
41480
+ ],
40892
41481
  compute: (bars, inputs) => {
40893
- const values = map(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "length", 14)), (x) => x - 100);
40894
- return {
40895
- plots: [{ key: "wr", title: "%R", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
40896
- levels: guideLevels(-80, -20)
40897
- };
41482
+ const len = num3(inputs, "length", 14);
41483
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
41484
+ const hh = highest(highs(bars), len);
41485
+ const ll = lowest(lows(bars), len);
41486
+ const values = src.map((x, i) => {
41487
+ const h = hh[i];
41488
+ const l = ll[i];
41489
+ return Number.isFinite(h) && Number.isFinite(l) && h > l ? -100 * (h - x) / (h - l) : Number.NaN;
41490
+ });
41491
+ return boundedOscillator({
41492
+ key: "wr",
41493
+ title: "%R",
41494
+ values,
41495
+ scale: { top: 0, bottom: -100 },
41496
+ overbought: num3(inputs, "overbought", -20),
41497
+ oversold: num3(inputs, "oversold", -80),
41498
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
41499
+ levelInk: NEUTRAL
41500
+ });
40898
41501
  }
40899
41502
  };
40900
- var cmo = {
41503
+ var cmoSpec = {
40901
41504
  type: "chande-momentum-oscillator",
40902
41505
  title: "Chande Momentum Oscillator",
40903
41506
  shortTitle: "CMO",
40904
41507
  overlay: false,
40905
- inputs: [lengthInput(9), sourceInput(), colorInput()],
41508
+ inputs: [
41509
+ lengthInput(9, "length", "Length", 5e3, "Number of bars over which up moves and down moves are summed."),
41510
+ sourceInput("Close", "source", "Source", "Price series used to measure bar-to-bar changes."),
41511
+ floatInput("overbought", "Overbought Level", 50, 0, 100, 1, "Level above which the oscillator is considered overbought."),
41512
+ floatInput("oversold", "Oversold Level", -50, -100, 0, 1, "Level below which the oscillator is considered oversold."),
41513
+ colorInput(NEUTRAL, "CMO Line", "color", "Color of the CMO line."),
41514
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while momentum is positive, and shading of the oversold zone."),
41515
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while momentum is negative, and shading of the overbought zone."),
41516
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought, oversold, and zero lines.")
41517
+ ],
40906
41518
  compute: (bars, inputs) => {
40907
41519
  const len = num3(inputs, "length", 9);
40908
41520
  const d = change(sourceValues(bars, str3(inputs, "source", "Close")));
40909
- const su = sum(map(d, (x) => Math.max(x, 0)), len);
40910
- const sd = sum(map(d, (x) => Math.max(-x, 0)), len);
40911
- const values = zip(su, sd, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
40912
- return {
40913
- plots: [{ key: "cmo", title: "CMO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
40914
- levels: guideLevels(-50, 50, 0)
40915
- };
41521
+ const up = sum(map(d, (x) => Math.max(x, 0)), len);
41522
+ const down = sum(map(d, (x) => Math.max(-x, 0)), len);
41523
+ const values = zip(up, down, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
41524
+ const bull = str3(inputs, "bullColor", BULLISH);
41525
+ const bear = str3(inputs, "bearColor", BEARISH);
41526
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
41527
+ const overbought = num3(inputs, "overbought", 50);
41528
+ const oversold = num3(inputs, "oversold", -50);
41529
+ const out = centeredOscillator({
41530
+ key: "cmo",
41531
+ title: "CMO",
41532
+ values,
41533
+ ink: { bull, bear, neutral: str3(inputs, "color", NEUTRAL) },
41534
+ zeroInk: levelInk,
41535
+ zeroLineStyle: "dotted",
41536
+ extraLevels: [
41537
+ { key: "overbought", price: overbought, color: levelInk, lineStyle: "dashed", title: "Overbought" },
41538
+ { key: "oversold", price: oversold, color: levelInk, lineStyle: "dashed", title: "Oversold" }
41539
+ ]
41540
+ });
41541
+ const zones = thresholdZones(values.length, { overbought, oversold, top: 100, bottom: -100 }, { overbought: bear, oversold: bull });
41542
+ out.plots.push(...zones.plots);
41543
+ out.bands = [...out.bands ?? [], ...zones.bands];
41544
+ return out;
40916
41545
  }
40917
41546
  };
40918
41547
  var connorsRsi = {
@@ -40921,10 +41550,14 @@ var connorsRsi = {
40921
41550
  shortTitle: "CRSI",
40922
41551
  overlay: false,
40923
41552
  inputs: [
40924
- lengthInput(3, "rsiLength", "RSI length"),
40925
- lengthInput(2, "streakLength", "Streak RSI length"),
40926
- lengthInput(100, "rankLength", "Percent-rank length"),
40927
- colorInput()
41553
+ lengthInput(3, "rsiLength", "RSI Length", 5e3, "Length of the Wilder RSI applied to the closing price."),
41554
+ lengthInput(2, "streakLength", "Streak RSI Length", 5e3, "Length of the Wilder RSI applied to the up/down close streak."),
41555
+ lengthInput(100, "rankLength", "Percent Rank Length", 5e3, "Lookback of the percent rank applied to the 1-bar rate of change of the closing price."),
41556
+ ...thresholdInputs(90, 10),
41557
+ colorInput(NEUTRAL, "CRSI Line", "color", "Color of the Connors RSI line."),
41558
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Line, fill and zone color toward and below the oversold level."),
41559
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Line, fill and zone color toward and above the overbought level."),
41560
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought and oversold levels.")
40928
41561
  ],
40929
41562
  compute: (bars, inputs) => {
40930
41563
  const c = closes(bars);
@@ -40942,10 +41575,17 @@ var connorsRsi = {
40942
41575
  const z = r[i];
40943
41576
  return Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) ? (x + y + z) / 3 : Number.NaN;
40944
41577
  });
40945
- return {
40946
- plots: [{ key: "crsi", title: "CRSI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
40947
- levels: guideLevels(20, 80, 50)
40948
- };
41578
+ return boundedOscillator({
41579
+ key: "crsi",
41580
+ title: "Connors RSI",
41581
+ values,
41582
+ scale: { top: 100, bottom: 0 },
41583
+ overbought: num3(inputs, "overbought", 90),
41584
+ oversold: num3(inputs, "oversold", 10),
41585
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
41586
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
41587
+ midpointWash: true
41588
+ });
40949
41589
  }
40950
41590
  };
40951
41591
  var fisher = {
@@ -40953,7 +41593,13 @@ var fisher = {
40953
41593
  title: "Fisher Transform",
40954
41594
  shortTitle: "Fisher",
40955
41595
  overlay: false,
40956
- inputs: [lengthInput(9)],
41596
+ inputs: [
41597
+ 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."),
41598
+ colorInput(NEUTRAL, "Fisher Line", "color", "Color of the Fisher line when it sits exactly on its trigger."),
41599
+ colorInput(NEUTRAL, "Trigger Line", "triggerColor", "Color of the trigger line, the Fisher value delayed one bar."),
41600
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the Fisher line and fill while it is above the trigger."),
41601
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the Fisher line and fill while it is below the trigger.")
41602
+ ],
40957
41603
  compute: (bars, inputs) => {
40958
41604
  const len = num3(inputs, "length", 9);
40959
41605
  const hl2 = sourceValues(bars, "HL2");
@@ -40961,23 +41607,39 @@ var fisher = {
40961
41607
  const lo = lowest(hl2, len);
40962
41608
  const n = bars.length;
40963
41609
  const out = new Array(n).fill(Number.NaN);
41610
+ let position = 0;
40964
41611
  let value = 0;
40965
- let fish = 0;
40966
41612
  for (let i = 0; i < n; i++) {
40967
41613
  const h = hi[i];
40968
41614
  const l = lo[i];
40969
41615
  if (!Number.isFinite(h) || !Number.isFinite(l)) continue;
40970
- const norm = h === l ? 0 : (hl2[i] - l) / (h - l) - 0.5;
40971
- value = Math.max(-0.999, Math.min(0.999, 0.66 * norm + 0.67 * value));
40972
- fish = 0.5 * Math.log((1 + value) / (1 - value)) + 0.5 * fish;
40973
- out[i] = fish;
40974
- }
41616
+ const raw = h > l ? (hl2[i] - l) / (h - l) - 0.5 : 0;
41617
+ position = 0.66 * raw + 0.67 * position;
41618
+ position = position > 0.99 ? 0.999 : position < -0.99 ? -0.999 : position;
41619
+ value = 0.5 * Math.log((1 + position) / (1 - position)) + 0.5 * value;
41620
+ out[i] = value;
41621
+ }
41622
+ const trigger = shift(out, 1);
41623
+ const neutral = str3(inputs, "color", NEUTRAL);
41624
+ const bull = str3(inputs, "bullColor", BULLISH);
41625
+ const bear = str3(inputs, "bearColor", BEARISH);
41626
+ const inkOf = (i) => {
41627
+ const f = out[i];
41628
+ const t = trigger[i];
41629
+ if (!Number.isFinite(t)) return neutral;
41630
+ return f > t ? bull : f < t ? bear : neutral;
41631
+ };
40975
41632
  return {
40976
41633
  plots: [
40977
- { key: "fisher", title: "Fisher", values: out, color: SERIES_LINE, width: 2 },
40978
- { key: "trigger", title: "Trigger", values: shift(out, 1), color: WARNING }
41634
+ { key: "fisher", title: "Fisher", values: out, color: neutral, width: 2, colors: out.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) },
41635
+ { key: "trigger", title: "Trigger", values: trigger, color: str3(inputs, "triggerColor", NEUTRAL) }
40979
41636
  ],
40980
- levels: ZERO_LEVEL
41637
+ bands: [{ key: "spread", from: "fisher", to: "trigger", color: transp(neutral, 80), colors: out.map((x, i) => Number.isFinite(x) ? transp(inkOf(i), 80) : null) }],
41638
+ levels: [
41639
+ { key: "upper", price: 1.5, color: NEUTRAL, lineStyle: "dashed", title: "Upper Level" },
41640
+ { key: "lower", price: -1.5, color: NEUTRAL, lineStyle: "dashed", title: "Lower Level" },
41641
+ { key: "zero", price: 0, color: NEUTRAL, lineStyle: "dotted", title: "Zero Line" }
41642
+ ]
40981
41643
  };
40982
41644
  }
40983
41645
  };
@@ -40985,93 +41647,180 @@ var trix = {
40985
41647
  type: "trix",
40986
41648
  title: "TRIX",
40987
41649
  overlay: false,
40988
- inputs: [lengthInput(18), colorInput()],
41650
+ inputs: [
41651
+ lengthInput(18, "length", "Length", 5e3, "Length of each of the three EMA smoothing passes applied to the close price."),
41652
+ lengthInput(9, "signalLength", "Signal Length", 500, "Length of the EMA applied to TRIX to form the signal line."),
41653
+ colorInput(NEUTRAL, "TRIX Color", "color", "Color of the TRIX line."),
41654
+ colorInput(WARNING, "Signal Color", "signalColor", "Color of the signal line."),
41655
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the TRIX line and its gradient fill while TRIX holds above zero."),
41656
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the TRIX line and its gradient fill while TRIX holds below zero.")
41657
+ ],
40989
41658
  compute: (bars, inputs) => {
40990
41659
  const len = num3(inputs, "length", 18);
40991
- const logClose = map(closes(bars), Math.log);
40992
- const smooth = ema(ema(ema(logClose, len), len), len);
40993
- const values = map(change(smooth), (x) => x * 1e4);
40994
- return {
40995
- plots: [{ key: "trix", title: "TRIX", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
40996
- levels: ZERO_LEVEL
40997
- };
41660
+ const smooth = ema(ema(ema(closes(bars), len), len), len);
41661
+ const values = roc(smooth, 1);
41662
+ return centeredOscillator({
41663
+ key: "trix",
41664
+ title: "TRIX",
41665
+ values,
41666
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
41667
+ zeroInk: NEUTRAL,
41668
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(values, num3(inputs, "signalLength", 9)), color: str3(inputs, "signalColor", WARNING) }]
41669
+ });
40998
41670
  }
40999
41671
  };
41000
- function tsiLine(src, long, short) {
41672
+ function tsiRatio(src, long, short) {
41001
41673
  const mom = change(src);
41002
41674
  const numer = ema(ema(mom, long), short);
41003
41675
  const denom = ema(ema(map(mom, Math.abs), long), short);
41004
- return zip(numer, denom, (a, b) => b === 0 ? 0 : 100 * a / b);
41676
+ return zip(numer, denom, (a, b) => b === 0 ? 0 : a / b);
41005
41677
  }
41006
41678
  var tsi = {
41007
41679
  type: "true-strength-index",
41008
41680
  title: "True Strength Index",
41009
41681
  shortTitle: "TSI",
41010
41682
  overlay: false,
41011
- inputs: [lengthInput(25, "longLength", "Long length"), lengthInput(13, "shortLength", "Short length"), lengthInput(13, "signalLength", "Signal length")],
41683
+ inputs: [
41684
+ lengthInput(25, "longLength", "Long Length", 5e3, "Length of the first (long) EMA applied to the 1-bar price change."),
41685
+ lengthInput(13, "shortLength", "Short Length", 5e3, "Length of the second (short) EMA applied to the long-smoothed price change."),
41686
+ lengthInput(13, "signalLength", "Signal Length", 500, "Length of the EMA of TSI used as the signal line."),
41687
+ colorInput(NEUTRAL, "TSI", "color", "Color of the TSI line."),
41688
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line."),
41689
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the TSI line and its gradient fill while TSI holds above zero."),
41690
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the TSI line and its gradient fill while TSI holds below zero.")
41691
+ ],
41012
41692
  compute: (bars, inputs) => {
41013
- const line = tsiLine(closes(bars), num3(inputs, "longLength", 25), num3(inputs, "shortLength", 13));
41014
- return {
41015
- plots: [
41016
- { key: "tsi", title: "TSI", values: line, color: SERIES_LINE, width: 2 },
41017
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: WARNING }
41018
- ],
41019
- levels: ZERO_LEVEL
41020
- };
41693
+ const values = map(tsiRatio(closes(bars), num3(inputs, "longLength", 25), num3(inputs, "shortLength", 13)), (x) => 100 * x);
41694
+ return centeredOscillator({
41695
+ key: "tsi",
41696
+ title: "TSI",
41697
+ values,
41698
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
41699
+ zeroInk: NEUTRAL,
41700
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(values, num3(inputs, "signalLength", 13)), color: str3(inputs, "signalColor", WARNING) }]
41701
+ });
41021
41702
  }
41022
41703
  };
41704
+ var SMI_INDICATOR = "Indicator";
41705
+ var SMI_OSCILLATOR = "Oscillator";
41023
41706
  var smiErgodic = {
41024
41707
  type: "smi-ergodic",
41025
41708
  title: "SMI Ergodic Oscillator",
41026
41709
  shortTitle: "SMIE",
41027
41710
  overlay: false,
41028
- inputs: [lengthInput(5, "shortLength", "Short length"), lengthInput(20, "longLength", "Long length"), lengthInput(5, "signalLength", "Signal length")],
41711
+ inputs: [
41712
+ lengthInput(20, "longLength", "Long Length", 5e3, "Length of the first EMA smoothing applied to the momentum of the closing price."),
41713
+ lengthInput(5, "shortLength", "Short Length", 5e3, "Length of the second EMA smoothing applied to the momentum of the closing price."),
41714
+ lengthInput(5, "signalLength", "Signal Length", 500, "Length of the EMA of the SMI that forms the signal line."),
41715
+ 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."),
41716
+ colorInput(NEUTRAL, "SMI", "color", "Color of the SMI line while it sits exactly on its signal line."),
41717
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line."),
41718
+ colorInput(BULLISH, "Oscillator Grow", "growColor", "Color of the rising oscillator histogram, and of the SMI line while it leads its signal."),
41719
+ colorInput(BEARISH, "Oscillator Fall", "fallColor", "Color of the falling oscillator histogram, and of the SMI line while it lags its signal.")
41720
+ ],
41029
41721
  compute: (bars, inputs) => {
41030
- const line = tsiLine(closes(bars), num3(inputs, "longLength", 20), num3(inputs, "shortLength", 5));
41031
- const signal = ema(line, num3(inputs, "signalLength", 5));
41032
- const osc = sub(line, signal);
41722
+ const smi = tsiRatio(closes(bars), num3(inputs, "longLength", 20), num3(inputs, "shortLength", 5));
41723
+ const signal = ema(smi, num3(inputs, "signalLength", 5));
41724
+ const grow = str3(inputs, "growColor", BULLISH);
41725
+ const fall = str3(inputs, "fallColor", BEARISH);
41726
+ const zero = [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed", title: "Zero" }];
41727
+ if (str3(inputs, "display", SMI_INDICATOR) === SMI_OSCILLATOR) {
41728
+ const osc = sub(smi, signal);
41729
+ const colors = osc.map((x, i) => {
41730
+ if (!Number.isFinite(x)) return null;
41731
+ const prev2 = i > 0 && Number.isFinite(osc[i - 1]) ? osc[i - 1] : x;
41732
+ return x > prev2 ? grow : fall;
41733
+ });
41734
+ return { plots: [{ key: "osc", title: "Oscillator", values: osc, kind: "columns", color: grow, colors, base: 0 }], levels: zero };
41735
+ }
41736
+ const neutral = str3(inputs, "color", NEUTRAL);
41737
+ const inkOf = (i) => {
41738
+ const a = smi[i];
41739
+ const b = signal[i];
41740
+ if (!Number.isFinite(b)) return neutral;
41741
+ return a > b ? grow : a < b ? fall : neutral;
41742
+ };
41033
41743
  return {
41034
41744
  plots: [
41035
- { key: "osc", title: "Oscillator", values: osc, kind: "histogram", color: BULLISH, colors: histColors(osc), base: 0 },
41036
- { key: "smi", title: "SMI", values: line, color: SERIES_LINE, width: 2 },
41037
- { key: "signal", title: "Signal", values: signal, color: WARNING }
41745
+ { key: "smi", title: "SMI", values: smi, color: neutral, colors: smi.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) },
41746
+ { key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) }
41038
41747
  ],
41039
- levels: ZERO_LEVEL
41748
+ bands: [{ key: "spread", from: "smi", to: "signal", color: transp(neutral, 75), colors: smi.map((x, i) => Number.isFinite(x) ? transp(inkOf(i), 75) : null) }],
41749
+ levels: zero
41040
41750
  };
41041
41751
  }
41042
41752
  };
41753
+ var STC_MID = 50;
41754
+ var TRIGGERS = "Trigger Levels";
41043
41755
  var stc = {
41044
41756
  type: "schaff-trend-cycle",
41045
41757
  title: "Schaff Trend Cycle",
41046
41758
  shortTitle: "Schaff Trend Cycle",
41047
41759
  overlay: false,
41048
- inputs: [lengthInput(10, "cycleLength", "Cycle length"), lengthInput(23, "fastLength", "Fast length"), lengthInput(50, "slowLength", "Slow length")],
41760
+ inputs: [
41761
+ sourceInput("Close", "source", "Source", "Price series used to build the MACD that the trend cycle is measured on."),
41762
+ lengthInput(23, "fastLength", "Fast Length", 5e3, "Fast EMA length of the underlying MACD."),
41763
+ lengthInput(50, "slowLength", "Slow Length", 5e3, "Slow EMA length of the underlying MACD."),
41764
+ { ...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 },
41765
+ 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."),
41766
+ { ...floatInput("upper", "Upper Trigger", 75, 0, 100, 1, "Upper trigger line. A cross down through it flags an emerging downtrend."), group: TRIGGERS },
41767
+ { ...floatInput("lower", "Lower Trigger", 25, 0, 100, 1, "Lower trigger line. A cross up through it flags an emerging uptrend."), group: TRIGGERS },
41768
+ 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."),
41769
+ 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."),
41770
+ colorInput(NEUTRAL, "Levels Color", "levelsColor", "Color of the dashed trigger lines and the dotted midline."),
41771
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Vertical gradient between the midline and the trend cycle, fading out toward the midline.")
41772
+ ],
41049
41773
  compute: (bars, inputs) => {
41050
41774
  const len = num3(inputs, "cycleLength", 10);
41051
- const src = closes(bars);
41775
+ const factor = num3(inputs, "factor", 0.5);
41776
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
41052
41777
  const macdLine = sub(ema(src, num3(inputs, "fastLength", 23)), ema(src, num3(inputs, "slowLength", 50)));
41053
- const stochOf = (v) => {
41778
+ const stochNorm = (v) => {
41054
41779
  const lo = lowest(v, len);
41055
41780
  const hi = highest(v, len);
41056
41781
  const out = new Array(v.length).fill(Number.NaN);
41057
- let prevRaw = Number.NaN;
41058
- let smoothed = Number.NaN;
41782
+ let pct = Number.NaN;
41059
41783
  for (let i = 0; i < v.length; i++) {
41060
41784
  const x = v[i];
41061
41785
  const l = lo[i];
41062
41786
  const h = hi[i];
41063
41787
  if (!Number.isFinite(x) || !Number.isFinite(l) || !Number.isFinite(h)) continue;
41064
- const raw = h > l ? (x - l) / (h - l) * 100 : Number.isFinite(prevRaw) ? prevRaw : 50;
41065
- prevRaw = raw;
41066
- smoothed = Number.isFinite(smoothed) ? smoothed + 0.5 * (raw - smoothed) : raw;
41788
+ if (h > l) pct = 100 * (x - l) / (h - l);
41789
+ out[i] = pct;
41790
+ }
41791
+ return out;
41792
+ };
41793
+ const smoothStep = (v) => {
41794
+ const out = new Array(v.length).fill(Number.NaN);
41795
+ let smoothed = Number.NaN;
41796
+ for (let i = 0; i < v.length; i++) {
41797
+ const x = v[i];
41798
+ if (!Number.isFinite(x)) continue;
41799
+ smoothed = Number.isFinite(smoothed) ? smoothed + factor * (x - smoothed) : x;
41067
41800
  out[i] = smoothed;
41068
41801
  }
41069
41802
  return out;
41070
41803
  };
41071
- const values = stochOf(stochOf(macdLine));
41804
+ const values = smoothStep(stochNorm(smoothStep(stochNorm(macdLine))));
41805
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
41806
+ const bull = str3(inputs, "bullColor", BULLISH);
41807
+ const bear = str3(inputs, "bearColor", BEARISH);
41808
+ const plots = [
41809
+ { key: "stc", title: "Schaff Trend Cycle", values, color: bear, width: 2, colors: values.map((x) => Number.isFinite(x) ? x > STC_MID ? bull : bear : null) }
41810
+ ];
41811
+ const bands = [];
41812
+ if (bool(inputs, "gradientFill", true)) {
41813
+ plots.push(anchorPlot("midAnchor", "Midline Anchor", values.length, STC_MID));
41814
+ bands.push({ key: "wash", from: "stc", to: "midAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, STC_MID, (x) => x > STC_MID ? bull : bear) });
41815
+ }
41072
41816
  return {
41073
- plots: [{ key: "stc", title: "STC", values, color: SERIES_LINE, width: 2 }],
41074
- levels: guideLevels(25, 75)
41817
+ plots,
41818
+ bands,
41819
+ levels: [
41820
+ { key: "upper", price: num3(inputs, "upper", 75), color: transp(levelInk, 25), lineStyle: "dashed", title: "Upper Trigger" },
41821
+ { key: "lower", price: num3(inputs, "lower", 25), color: transp(levelInk, 25), lineStyle: "dashed", title: "Lower Trigger" },
41822
+ { key: "midline", price: STC_MID, color: transp(levelInk, 60), lineStyle: "dotted", title: "Midline" }
41823
+ ]
41075
41824
  };
41076
41825
  }
41077
41826
  };
@@ -41080,22 +41829,37 @@ var kst = {
41080
41829
  title: "Know Sure Thing",
41081
41830
  shortTitle: "KST",
41082
41831
  overlay: false,
41083
- inputs: [sourceInput(), lengthInput(9, "signalLength", "Signal length")],
41832
+ inputs: [
41833
+ lengthInput(10, "rocLength1", "ROC Length #1", 5e3, "Lookback of the first (shortest) rate of change."),
41834
+ lengthInput(15, "rocLength2", "ROC Length #2", 5e3, "Lookback of the second rate of change."),
41835
+ lengthInput(20, "rocLength3", "ROC Length #3", 5e3, "Lookback of the third rate of change."),
41836
+ lengthInput(30, "rocLength4", "ROC Length #4", 5e3, "Lookback of the fourth (longest) rate of change."),
41837
+ lengthInput(10, "smaLength1", "SMA Length #1", 5e3, "Smoothing length applied to the first rate of change (RCMA #1)."),
41838
+ lengthInput(10, "smaLength2", "SMA Length #2", 5e3, "Smoothing length applied to the second rate of change (RCMA #2)."),
41839
+ lengthInput(10, "smaLength3", "SMA Length #3", 5e3, "Smoothing length applied to the third rate of change (RCMA #3)."),
41840
+ lengthInput(15, "smaLength4", "SMA Length #4", 5e3, "Smoothing length applied to the fourth rate of change (RCMA #4)."),
41841
+ lengthInput(9, "signalLength", "Signal Length", 500, "Simple moving average length of the signal line."),
41842
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the KST line while above zero."),
41843
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the KST line while below zero."),
41844
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line.")
41845
+ ],
41084
41846
  compute: (bars, inputs) => {
41085
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
41086
- const line = [
41087
- sma(roc(src, 10), 10),
41088
- sma(roc(src, 15), 10),
41089
- sma(roc(src, 20), 10),
41090
- sma(roc(src, 30), 15)
41091
- ].reduce((acc, r, i) => zip(acc, r, (a, b) => a + b * (i + 1)), new Array(src.length).fill(0));
41092
- return {
41093
- plots: [
41094
- { key: "kst", title: "KST", values: line, color: SERIES_LINE, width: 2 },
41095
- { key: "signal", title: "Signal", values: sma(line, num3(inputs, "signalLength", 9)), color: WARNING }
41096
- ],
41097
- levels: ZERO_LEVEL
41098
- };
41847
+ const c = closes(bars);
41848
+ const rcma = (rocKey, rocDefault, smaKey, smaDefault) => sma(roc(c, num3(inputs, rocKey, rocDefault)), num3(inputs, smaKey, smaDefault));
41849
+ const values = [rcma("rocLength1", 10, "smaLength1", 10), rcma("rocLength2", 15, "smaLength2", 10), rcma("rocLength3", 20, "smaLength3", 10), rcma("rocLength4", 30, "smaLength4", 15)].reduce(
41850
+ (acc, r, i) => zip(acc, r, (a, b) => a + b * (i + 1)),
41851
+ new Array(c.length).fill(0)
41852
+ );
41853
+ const bull = str3(inputs, "bullColor", BULLISH);
41854
+ return centeredOscillator({
41855
+ key: "kst",
41856
+ title: "KST",
41857
+ values,
41858
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
41859
+ zeroInk: NEUTRAL,
41860
+ valueTransparency: 60,
41861
+ extraPlots: [{ key: "signal", title: "Signal", values: sma(values, num3(inputs, "signalLength", 9)), color: str3(inputs, "signalColor", WARNING) }]
41862
+ });
41099
41863
  }
41100
41864
  };
41101
41865
  var coppock = {
@@ -41104,19 +41868,25 @@ var coppock = {
41104
41868
  shortTitle: "Coppock",
41105
41869
  overlay: false,
41106
41870
  inputs: [
41107
- lengthInput(10, "wmaLength", "WMA length"),
41108
- lengthInput(14, "longRoc", "Long RoC length"),
41109
- lengthInput(11, "shortRoc", "Short RoC length"),
41110
- sourceInput(),
41111
- colorInput()
41871
+ lengthInput(10, "wmaLength", "WMA Length", 5e3, "Length of the weighted moving average applied to the sum of the two rates of change."),
41872
+ lengthInput(14, "longRoc", "Long RoC Length", 5e3, "Lookback of the long rate of change, in bars. The original definition uses 14 monthly closes."),
41873
+ lengthInput(11, "shortRoc", "Short RoC Length", 5e3, "Lookback of the short rate of change, in bars. The original definition uses 11 monthly closes."),
41874
+ sourceInput("Close", "source", "Source", "Price series used for the rate of change calculations."),
41875
+ colorInput(NEUTRAL, "Coppock Curve", "color", "Color of the Coppock Curve line."),
41876
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the curve reads above zero."),
41877
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the curve reads below zero."),
41878
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
41112
41879
  ],
41113
41880
  compute: (bars, inputs) => {
41114
41881
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
41115
41882
  const values = wma(zip(roc(src, num3(inputs, "longRoc", 14)), roc(src, num3(inputs, "shortRoc", 11)), (a, b) => a + b), num3(inputs, "wmaLength", 10));
41116
- return {
41117
- plots: [{ key: "coppock", title: "Coppock", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41118
- levels: ZERO_LEVEL
41119
- };
41883
+ return centeredOscillator({
41884
+ key: "coppock",
41885
+ title: "Coppock Curve",
41886
+ values,
41887
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
41888
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
41889
+ });
41120
41890
  }
41121
41891
  };
41122
41892
  var dpo = {
@@ -41124,42 +41894,76 @@ var dpo = {
41124
41894
  title: "Detrended Price Oscillator",
41125
41895
  shortTitle: "DPO",
41126
41896
  overlay: false,
41127
- inputs: [lengthInput(21), colorInput()],
41897
+ inputs: [
41898
+ 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."),
41899
+ sourceInput("Close", "source", "Source", "Price series used in the detrending calculation."),
41900
+ 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."),
41901
+ colorInput(NEUTRAL, "DPO", "color", "Color of the line when it sits exactly at zero."),
41902
+ colorInput(BULLISH, "Above Zero", "bullColor", "Color of the line and fill while the oscillator is above zero."),
41903
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while the oscillator is below zero."),
41904
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
41905
+ ],
41128
41906
  compute: (bars, inputs) => {
41129
41907
  const len = num3(inputs, "length", 21);
41130
- const barsback = Math.floor(len / 2) + 1;
41131
- const values = sub(closes(bars), shift(sma(closes(bars), len), barsback));
41132
- return {
41133
- plots: [{ key: "dpo", title: "DPO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41134
- levels: ZERO_LEVEL
41135
- };
41908
+ const barsBack = Math.trunc(len / 2) + 1;
41909
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
41910
+ const basis = sma(src, len);
41911
+ const centered = bool(inputs, "centered", false);
41912
+ const values = centered ? shift(sub(shift(src, barsBack), basis), -barsBack) : sub(src, shift(basis, barsBack));
41913
+ return centeredOscillator({
41914
+ key: "dpo",
41915
+ title: "DPO",
41916
+ values,
41917
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
41918
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
41919
+ });
41136
41920
  }
41137
41921
  };
41922
+ var ROC_PERCENT = "ROC %";
41923
+ var ROC_MOMENTUM = "Momentum";
41138
41924
  var rocSpec = {
41139
41925
  type: "rate-of-change",
41140
41926
  title: "Rate of Change",
41141
41927
  shortTitle: "ROC",
41142
41928
  overlay: false,
41143
- inputs: [lengthInput(9), sourceInput(), colorInput()],
41144
- compute: (bars, inputs) => ({
41145
- plots: [
41146
- {
41147
- key: "roc",
41148
- title: "RoC",
41149
- values: roc(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 9)),
41150
- color: str3(inputs, "color", SERIES_LINE),
41151
- width: 2
41152
- }
41153
- ],
41154
- levels: ZERO_LEVEL
41155
- })
41929
+ inputs: [
41930
+ lengthInput(9, "length", "Length", 5e3, "Lookback period; the source is compared with its value this many bars ago."),
41931
+ sourceInput("Close", "source", "Source", "Series used as input for the calculation."),
41932
+ 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."),
41933
+ colorInput(NEUTRAL, "Line", "color", "Color of the line when the reading sits exactly at zero."),
41934
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the line and fill while the reading is above zero."),
41935
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line and fill while the reading is below zero."),
41936
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the line and zero, following the line color.")
41937
+ ],
41938
+ compute: (bars, inputs) => {
41939
+ const len = num3(inputs, "length", 9);
41940
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
41941
+ const values = str3(inputs, "mode", ROC_PERCENT) === ROC_MOMENTUM ? change(src, len) : roc(src, len);
41942
+ return centeredOscillator({
41943
+ key: "roc",
41944
+ title: "ROC",
41945
+ values,
41946
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
41947
+ zeroInk: NEUTRAL,
41948
+ fill: bool(inputs, "gradientFill", true)
41949
+ });
41950
+ }
41156
41951
  };
41157
41952
  var ultimate = {
41158
41953
  type: "ultimate-oscillator",
41159
41954
  title: "Ultimate Oscillator",
41160
41955
  shortTitle: "UO",
41161
41956
  overlay: false,
41162
- inputs: [lengthInput(7, "fastLength", "Fast length"), lengthInput(14, "middleLength", "Middle length"), lengthInput(28, "slowLength", "Slow length"), colorInput()],
41957
+ inputs: [
41958
+ lengthInput(7, "fastLength", "Fast Length", 5e3, "Bars in the fastest averaging window. Weighted 4 in the final blend, so it dominates the oscillator."),
41959
+ lengthInput(14, "middleLength", "Middle Length", 5e3, "Bars in the intermediate averaging window. Weighted 2 in the final blend."),
41960
+ lengthInput(28, "slowLength", "Slow Length", 5e3, "Bars in the slowest averaging window. Weighted 1 in the final blend."),
41961
+ ...thresholdInputs(70, 30, 0, 100, "Overbought", "Oversold"),
41962
+ colorInput(NEUTRAL, "Oscillator", "color", "Color of the line midway between the levels. Toward either level it blends into that level\u2019s color."),
41963
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought and oversold levels."),
41964
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line at the oversold level and of the oversold zone shading."),
41965
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line at the overbought level and of the overbought zone shading.")
41966
+ ],
41163
41967
  compute: (bars, inputs) => {
41164
41968
  const n = bars.length;
41165
41969
  const bp = new Array(n).fill(Number.NaN);
@@ -41167,10 +41971,9 @@ var ultimate = {
41167
41971
  for (let i = 0; i < n; i++) {
41168
41972
  const b = bars[i];
41169
41973
  const pc = i > 0 ? bars[i - 1].close : b.close;
41170
- const lo = Math.min(b.low, pc);
41171
- const hi = Math.max(b.high, pc);
41172
- bp[i] = b.close - lo;
41173
- tr[i] = hi - lo;
41974
+ const trueLow = Math.min(b.low, pc);
41975
+ bp[i] = b.close - trueLow;
41976
+ tr[i] = Math.max(b.high, pc) - trueLow;
41174
41977
  }
41175
41978
  const avg = (len) => zip(sum(bp, len), sum(tr, len), (a, b) => b === 0 ? Number.NaN : a / b);
41176
41979
  const a7 = avg(num3(inputs, "fastLength", 7));
@@ -41181,10 +41984,17 @@ var ultimate = {
41181
41984
  const z = a28[i];
41182
41985
  return Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) ? 100 * (4 * x + 2 * y + z) / 7 : Number.NaN;
41183
41986
  });
41184
- return {
41185
- plots: [{ key: "uo", title: "UO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41186
- levels: guideLevels(30, 70, 50)
41187
- };
41987
+ return boundedOscillator({
41988
+ key: "uo",
41989
+ title: "UO",
41990
+ values,
41991
+ scale: { top: 100, bottom: 0 },
41992
+ overbought: num3(inputs, "overbought", 70),
41993
+ oversold: num3(inputs, "oversold", 30),
41994
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
41995
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
41996
+ zoneTransparency: 91
41997
+ });
41188
41998
  }
41189
41999
  };
41190
42000
  var rvgi = {
@@ -41192,7 +42002,11 @@ var rvgi = {
41192
42002
  title: "Relative Vigor Index",
41193
42003
  shortTitle: "RVGI",
41194
42004
  overlay: false,
41195
- inputs: [lengthInput(10)],
42005
+ inputs: [
42006
+ lengthInput(10, "length", "Length", 5e3, "Number of bars summed for the smoothed numerator (close \u2212 open) and denominator (high \u2212 low)."),
42007
+ colorInput(BULLISH, "RVGI", "color", "Color of the RVGI line."),
42008
+ colorInput(BEARISH, "Signal", "signalColor", "Color of the signal line.")
42009
+ ],
41196
42010
  compute: (bars, inputs) => {
41197
42011
  const len = num3(inputs, "length", 10);
41198
42012
  const co = swma(sub(closes(bars), opens(bars)));
@@ -41200,31 +42014,49 @@ var rvgi = {
41200
42014
  const line = zip(sum(co, len), sum(hl, len), (a, b) => b === 0 ? 0 : a / b);
41201
42015
  return {
41202
42016
  plots: [
41203
- { key: "rvgi", title: "RVGI", values: line, color: SERIES_LINE, width: 2 },
41204
- { key: "signal", title: "Signal", values: swma(line), color: WARNING }
42017
+ { key: "rvgi", title: "RVGI", values: line, color: str3(inputs, "color", BULLISH) },
42018
+ { key: "signal", title: "Signal", values: swma(line), color: str3(inputs, "signalColor", BEARISH) }
41205
42019
  ],
41206
- levels: ZERO_LEVEL
42020
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed", title: "Zero" }]
41207
42021
  };
41208
42022
  }
41209
42023
  };
42024
+ var RVI_EMA = "EMA";
42025
+ var RVI_RMA = "Wilder (RMA)";
41210
42026
  var relVolatility = {
41211
42027
  type: "relative-volatility-index",
41212
42028
  title: "Relative Volatility Index",
41213
42029
  shortTitle: "RVI",
41214
42030
  overlay: false,
41215
- inputs: [lengthInput(10, "stdevLength", "StdDev length"), lengthInput(14, "smoothLength", "Smoothing length"), sourceInput(), colorInput()],
42031
+ inputs: [
42032
+ 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."),
42033
+ lengthInput(14, "smoothLength", "Smoothing Length", 5e3, "Number of bars used to smooth the up and down volatility streams."),
42034
+ 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."),
42035
+ ...thresholdInputs(80, 20),
42036
+ colorInput(NEUTRAL, "RVI Line", "color", "Color of the RVI line at the midline. Toward either threshold it blends into that threshold\u2019s color."),
42037
+ colorInput(BULLISH, "Up Volatility", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
42038
+ colorInput(BEARISH, "Down Volatility", "oversoldColor", "Color of the line and zone shading at and below the oversold level.")
42039
+ ],
41216
42040
  compute: (bars, inputs) => {
42041
+ const c = closes(bars);
42042
+ const sigma = stdev(c, num3(inputs, "stdevLength", 10));
42043
+ const d = change(c);
42044
+ const up = zip(sigma, d, (s, x) => x > 0 ? s : 0);
42045
+ const down = zip(sigma, d, (s, x) => x < 0 ? s : 0);
41217
42046
  const smoothLen = num3(inputs, "smoothLength", 14);
41218
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
41219
- const dev = stdev(src, num3(inputs, "stdevLength", 10));
41220
- const d = change(src);
41221
- const up = rma(zip(dev, d, (s, x) => x > 0 ? s : 0), smoothLen);
41222
- const dn = rma(zip(dev, d, (s, x) => x <= 0 ? s : 0), smoothLen);
41223
- const values = zip(up, dn, (u, w) => u + w === 0 ? 50 : 100 * u / (u + w));
41224
- return {
41225
- plots: [{ key: "rvi", title: "RVI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41226
- levels: guideLevels(20, 80, 50)
41227
- };
42047
+ const smooth = str3(inputs, "smoothType", RVI_EMA) === RVI_RMA ? rma : ema;
42048
+ const values = zip(smooth(up, smoothLen), smooth(down, smoothLen), (u, w) => u + w === 0 ? 0 : 100 * u / (u + w));
42049
+ return boundedOscillator({
42050
+ key: "rvi",
42051
+ title: "RVI",
42052
+ values,
42053
+ scale: { top: 100, bottom: 0 },
42054
+ overbought: num3(inputs, "overbought", 80),
42055
+ oversold: num3(inputs, "oversold", 20),
42056
+ ink: { low: str3(inputs, "oversoldColor", BEARISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BULLISH) },
42057
+ levelInk: NEUTRAL,
42058
+ width: 2
42059
+ });
41228
42060
  }
41229
42061
  };
41230
42062
  var bop = {
@@ -41232,28 +42064,43 @@ var bop = {
41232
42064
  title: "Balance of Power",
41233
42065
  shortTitle: "BOP",
41234
42066
  overlay: false,
41235
- inputs: [colorInput()],
42067
+ inputs: [
42068
+ boolInput("smoothing", "Smoothing", false, void 0, "Smooth the raw Balance of Power with a simple moving average instead of plotting the single-bar value."),
42069
+ lengthInput(14, "smoothLength", "Length", 5e3, "Simple moving average length applied when smoothing is enabled."),
42070
+ colorInput(NEUTRAL, "BOP Color", "color", "Color of the line at exactly zero."),
42071
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the Balance of Power is above zero \u2014 buyers in control of the bar."),
42072
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the Balance of Power is below zero \u2014 sellers in control of the bar.")
42073
+ ],
41236
42074
  compute: (bars, inputs) => {
41237
- const values = bars.map((b) => b.high === b.low ? 0 : (b.close - b.open) / (b.high - b.low));
41238
- return {
41239
- plots: [{ key: "bop", title: "BOP", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41240
- levels: ZERO_LEVEL
41241
- };
42075
+ const raw = bars.map((b) => b.high === b.low ? 0 : (b.close - b.open) / (b.high - b.low));
42076
+ const values = bool(inputs, "smoothing", false) ? sma(raw, num3(inputs, "smoothLength", 14)) : raw;
42077
+ return centeredOscillator({
42078
+ key: "bop",
42079
+ title: "BOP",
42080
+ values,
42081
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
42082
+ zeroInk: NEUTRAL,
42083
+ valueTransparency: 60
42084
+ });
41242
42085
  }
41243
42086
  };
41244
42087
  var elderRay = {
41245
42088
  type: "elder-ray",
41246
42089
  title: "Elder Ray",
41247
42090
  overlay: false,
41248
- inputs: [lengthInput(13)],
42091
+ inputs: [
42092
+ 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."),
42093
+ colorInput(BULLISH, "Bull Power", "bullColor", "Color of the Bull Power columns."),
42094
+ colorInput(BEARISH, "Bear Power", "bearColor", "Color of the Bear Power columns.")
42095
+ ],
41249
42096
  compute: (bars, inputs) => {
41250
42097
  const basis = ema(closes(bars), num3(inputs, "length", 13));
41251
42098
  return {
41252
42099
  plots: [
41253
- { key: "bull", title: "Bull power", values: sub(highs(bars), basis), kind: "histogram", color: BULLISH, base: 0 },
41254
- { key: "bear", title: "Bear power", values: sub(lows(bars), basis), kind: "histogram", color: BEARISH, base: 0 }
42100
+ { key: "bull", title: "Bull Power", values: sub(highs(bars), basis), kind: "columns", color: str3(inputs, "bullColor", BULLISH), base: 0 },
42101
+ { key: "bear", title: "Bear Power", values: sub(lows(bars), basis), kind: "columns", color: str3(inputs, "bearColor", BEARISH), base: 0 }
41255
42102
  ],
41256
- levels: ZERO_LEVEL
42103
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 30), lineStyle: "dashed", title: "Zero Line" }]
41257
42104
  };
41258
42105
  }
41259
42106
  };
@@ -41262,31 +42109,66 @@ var ttmSqueeze = {
41262
42109
  title: "TTM Squeeze",
41263
42110
  overlay: false,
41264
42111
  inputs: [
41265
- lengthInput(20),
41266
- { key: "bbMult", title: "Bollinger multiplier", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 },
41267
- { key: "kcMult", title: "Keltner multiplier", type: "float", defval: 1.5, min: 0.1, max: 50, step: 0.1 }
42112
+ { ...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 },
42113
+ sourceInput("Close", "source", "Source", "Price series used for the shared moving average basis, the standard deviation, and the momentum displacement."),
42114
+ floatInput("bbMult", "Bollinger Bands Multiplier", 2, 0.1, 50, 0.1, "Standard deviation multiplier for the Bollinger Bands."),
42115
+ 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."),
42116
+ colorInput(BULLISH, "Momentum Up", "bullColor", "Histogram color while momentum is positive. Bars building away from zero print solid, bars fading back print dimmed."),
42117
+ colorInput(BEARISH, "Momentum Down", "bearColor", "Histogram color while momentum is negative. Bars building away from zero print solid, bars fading back print dimmed."),
42118
+ colorInput(WARNING, "Squeeze On", "squeezeOnColor", "Zero-line dot color while both Bollinger Bands sit fully inside the Keltner Channels."),
42119
+ colorInput(NEUTRAL, "Squeeze Off", "squeezeOffColor", "Zero-line dot color while the Bollinger Bands trade outside the Keltner Channels."),
42120
+ 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.")
41268
42121
  ],
41269
42122
  compute: (bars, inputs) => {
41270
42123
  const len = num3(inputs, "length", 20);
41271
- const c = closes(bars);
41272
- const basis = sma(c, len);
41273
- const dev = map(stdev(c, len), (x) => x * num3(inputs, "bbMult", 2));
41274
- const kcRange = map(rma(trueRange(bars), len), (x) => x * num3(inputs, "kcMult", 1.5));
41275
- const donchianMid = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => (h + l) / 2);
41276
- const anchor = zip(donchianMid, basis, (d, b) => (d + b) / 2);
41277
- const mom = linreg(sub(c, anchor), len);
41278
- const squeezed = c.map((_, i) => {
42124
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
42125
+ const basis = sma(src, len);
42126
+ const dev = map(stdev(src, len), (x) => x * num3(inputs, "bbMult", 2));
42127
+ const kcRange = map(atr(bars, len), (x) => x * num3(inputs, "kcMult", 1.5));
42128
+ const rangeMid = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => (h + l) / 2);
42129
+ const anchor = zip(rangeMid, basis, (d, b) => (d + b) / 2);
42130
+ const mom = linreg(sub(src, anchor), len, 0);
42131
+ const squeezeOn = basis.map((b, i) => {
41279
42132
  const d = dev[i];
41280
42133
  const r = kcRange[i];
41281
- return Number.isFinite(d) && Number.isFinite(r) ? d < r : false;
42134
+ if (!Number.isFinite(b) || !Number.isFinite(d) || !Number.isFinite(r)) return null;
42135
+ return d < r;
41282
42136
  });
41283
- const stateColors = squeezed.map((s, i) => Number.isFinite(mom[i]) ? s ? BEARISH : BULLISH : null);
41284
- return {
41285
- plots: [
41286
- { key: "mom", title: "Momentum", values: mom, kind: "histogram", color: BULLISH, colors: histColors(mom), base: 0 },
41287
- { key: "squeeze", title: "Squeeze", values: mom.map((x) => Number.isFinite(x) ? 0 : Number.NaN), kind: "cross", color: INFO, colors: stateColors }
41288
- ]
41289
- };
42137
+ const bull = str3(inputs, "bullColor", BULLISH);
42138
+ const bear = str3(inputs, "bearColor", BEARISH);
42139
+ const onInk = str3(inputs, "squeezeOnColor", WARNING);
42140
+ const offInk = str3(inputs, "squeezeOffColor", NEUTRAL);
42141
+ const momColors = mom.map((x, i) => {
42142
+ if (!Number.isFinite(x)) return null;
42143
+ const prev2 = i > 0 && Number.isFinite(mom[i - 1]) ? mom[i - 1] : x;
42144
+ const rising = x > prev2;
42145
+ return x >= 0 ? rising ? bull : transp(bull, 60) : rising ? transp(bear, 60) : bear;
42146
+ });
42147
+ const plots = [
42148
+ { key: "mom", title: "Momentum", values: mom, kind: "columns", color: bull, colors: momColors, base: 0 },
42149
+ {
42150
+ key: "state",
42151
+ title: "Squeeze State",
42152
+ values: squeezeOn.map((s) => s == null ? Number.NaN : 0),
42153
+ kind: "circles",
42154
+ color: offInk,
42155
+ width: 2,
42156
+ colors: squeezeOn.map((s) => s == null ? null : s ? onInk : offInk)
42157
+ }
42158
+ ];
42159
+ if (bool(inputs, "markFires", true)) {
42160
+ const fires = squeezeOn.map((s, i) => s === false && squeezeOn[i - 1] === true ? 0 : Number.NaN);
42161
+ plots.push({
42162
+ key: "fire",
42163
+ title: "Squeeze Fire",
42164
+ values: fires,
42165
+ kind: "cross",
42166
+ color: offInk,
42167
+ width: 3,
42168
+ colors: fires.map((x, i) => Number.isFinite(x) ? Number.isFinite(mom[i]) ? mom[i] >= 0 ? bull : bear : offInk : null)
42169
+ });
42170
+ }
42171
+ return { plots };
41290
42172
  }
41291
42173
  };
41292
42174
  var oscillatorSpecs = [
@@ -41298,7 +42180,7 @@ var oscillatorSpecs = [
41298
42180
  awesome,
41299
42181
  cci,
41300
42182
  williamsR,
41301
- cmo,
42183
+ cmoSpec,
41302
42184
  connorsRsi,
41303
42185
  fisher,
41304
42186
  trix,
@@ -41322,15 +42204,19 @@ var aroon = {
41322
42204
  type: "aroon",
41323
42205
  title: "Aroon",
41324
42206
  overlay: false,
41325
- inputs: [lengthInput(14)],
42207
+ inputs: [
42208
+ lengthInput(14, "length", "Length", 5e3, "Lookback length used to locate the highest high and lowest low."),
42209
+ colorInput(BULLISH, "Aroon Up", "upColor", "Color of the Aroon Up line."),
42210
+ colorInput(BEARISH, "Aroon Down", "downColor", "Color of the Aroon Down line.")
42211
+ ],
41326
42212
  compute: (bars, inputs) => {
41327
42213
  const len = num3(inputs, "length", 14);
41328
- const up = map(barsSinceHighest(highs(bars), len + 1), (x) => 100 * (len - x) / len);
41329
- const down = map(barsSinceLowest(lows(bars), len + 1), (x) => 100 * (len - x) / len);
42214
+ const up = map(barsSinceHighest(highs(bars), len), (x) => 100 * (len - x) / len);
42215
+ const down = map(barsSinceLowest(lows(bars), len), (x) => 100 * (len - x) / len);
41330
42216
  return {
41331
42217
  plots: [
41332
- { key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
41333
- { key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
42218
+ { key: "up", title: "Aroon Up", values: up, color: str3(inputs, "upColor", BULLISH) },
42219
+ { key: "down", title: "Aroon Down", values: down, color: str3(inputs, "downColor", BEARISH) }
41334
42220
  ]
41335
42221
  };
41336
42222
  }
@@ -41340,10 +42226,16 @@ var adx = {
41340
42226
  title: "Average Directional Index",
41341
42227
  shortTitle: "ADX",
41342
42228
  overlay: false,
41343
- inputs: [lengthInput(14, "diLength", "DI length"), lengthInput(14, "adxLength", "ADX smoothing")],
42229
+ inputs: [
42230
+ lengthInput(14, "diLength", "DI Length", 5e3, "Wilder RMA smoothing length applied to directional movement and true range when computing +DI and \u2212DI."),
42231
+ lengthInput(14, "adxLength", "ADX Smoothing", 5e3, "Wilder RMA smoothing length applied to DX to obtain the ADX line."),
42232
+ floatInput("keyLevel", "Key Level", 25, 0, 100, 1, "Trend strength reference level; ADX above it is commonly read as a trending market."),
42233
+ colorInput(BULLISH, "+DI Color", "plusDiColor", "Color of the plus directional indicator, and of the ADX line above the key level while +DI leads."),
42234
+ colorInput(BEARISH, "-DI Color", "minusDiColor", "Color of the minus directional indicator, and of the ADX line above the key level while \u2212DI leads."),
42235
+ colorInput(NEUTRAL, "ADX Color", "adxColor", "Color of the ADX line while it sits below the key level \u2014 no trend strong enough to trade.")
42236
+ ],
41344
42237
  compute: (bars, inputs) => {
41345
42238
  const diLen = num3(inputs, "diLength", 14);
41346
- const adxLen = num3(inputs, "adxLength", 14);
41347
42239
  const n = bars.length;
41348
42240
  const plusDm = new Array(n).fill(Number.NaN);
41349
42241
  const minusDm = new Array(n).fill(Number.NaN);
@@ -41357,13 +42249,23 @@ var adx = {
41357
42249
  const plusDi = zip(rma(plusDm, diLen), atrLine, (d, a) => a === 0 ? 0 : 100 * d / a);
41358
42250
  const minusDi = zip(rma(minusDm, diLen), atrLine, (d, a) => a === 0 ? 0 : 100 * d / a);
41359
42251
  const dx = zip(plusDi, minusDi, (p, m) => p + m === 0 ? 0 : 100 * Math.abs(p - m) / (p + m));
42252
+ const adxLine = rma(dx, num3(inputs, "adxLength", 14));
42253
+ const plusInk = str3(inputs, "plusDiColor", BULLISH);
42254
+ const minusInk = str3(inputs, "minusDiColor", BEARISH);
42255
+ const neutralInk = str3(inputs, "adxColor", NEUTRAL);
42256
+ const keyLevel = num3(inputs, "keyLevel", 25);
42257
+ const adxColors = adxLine.map((x, i) => {
42258
+ if (!Number.isFinite(x)) return null;
42259
+ if (x <= keyLevel) return neutralInk;
42260
+ return (plusDi[i] ?? 0) > (minusDi[i] ?? 0) ? plusInk : minusInk;
42261
+ });
41360
42262
  return {
41361
42263
  plots: [
41362
- { key: "adx", title: "ADX", values: rma(dx, adxLen), color: SERIES_LINE, width: 2 },
41363
- { key: "plusDi", title: "+DI", values: plusDi, color: BULLISH },
41364
- { key: "minusDi", title: "-DI", values: minusDi, color: BEARISH }
42264
+ { key: "plusDi", title: "+DI", values: plusDi, color: transp(plusInk, 30) },
42265
+ { key: "minusDi", title: "-DI", values: minusDi, color: transp(minusInk, 30) },
42266
+ { key: "adx", title: "ADX", values: adxLine, color: neutralInk, width: 2, colors: adxColors }
41365
42267
  ],
41366
- levels: [{ key: "threshold", price: 25, color: NEUTRAL, lineStyle: "dotted" }]
42268
+ levels: [{ key: "keyLevel", price: keyLevel, color: NEUTRAL, lineStyle: "dashed", title: "Key Level" }]
41367
42269
  };
41368
42270
  }
41369
42271
  };
@@ -41372,7 +42274,12 @@ var vortex = {
41372
42274
  title: "Vortex Indicator",
41373
42275
  shortTitle: "VI",
41374
42276
  overlay: false,
41375
- inputs: [lengthInput(14)],
42277
+ inputs: [
42278
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to sum the vortex movements (VM+ and VM\u2212) and the true range."),
42279
+ colorInput(BULLISH, "VI+", "plusColor", "Color of the positive vortex line (VI+)."),
42280
+ colorInput(BEARISH, "VI-", "minusColor", "Color of the negative vortex line (VI\u2212)."),
42281
+ colorInput(NEUTRAL, "Baseline", "baselineColor", "Color of the dotted baseline drawn at 1.")
42282
+ ],
41376
42283
  compute: (bars, inputs) => {
41377
42284
  const len = num3(inputs, "length", 14);
41378
42285
  const n = bars.length;
@@ -41385,25 +42292,48 @@ var vortex = {
41385
42292
  const trSum = sum(trueRange(bars), len);
41386
42293
  return {
41387
42294
  plots: [
41388
- { key: "viPlus", title: "VI+", values: zip(sum(vmPlus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: BULLISH, width: 2 },
41389
- { key: "viMinus", title: "VI-", values: zip(sum(vmMinus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: BEARISH, width: 2 }
42295
+ { key: "viPlus", title: "VI+", values: zip(sum(vmPlus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: str3(inputs, "plusColor", BULLISH) },
42296
+ { key: "viMinus", title: "VI-", values: zip(sum(vmMinus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: str3(inputs, "minusColor", BEARISH) }
41390
42297
  ],
41391
- levels: [{ key: "one", price: 1, color: NEUTRAL, lineStyle: "dotted" }]
42298
+ levels: [{ key: "baseline", price: 1, color: str3(inputs, "baselineColor", NEUTRAL), lineStyle: "dotted", title: "Baseline" }]
41392
42299
  };
41393
42300
  }
41394
42301
  };
41395
42302
  var trendSpecs = [aroon, adx, vortex];
41396
42303
 
41397
42304
  // src/core/native-indicators/classics/volatility.ts
42305
+ function expansionColors(values, calm, expanding) {
42306
+ return values.map((x, i) => {
42307
+ if (!Number.isFinite(x)) return null;
42308
+ const prev2 = i > 0 && Number.isFinite(values[i - 1]) ? values[i - 1] : x;
42309
+ return x > prev2 ? expanding : calm;
42310
+ });
42311
+ }
42312
+ function washedToZero(key, title, values, colors, fallback, valueTransparency, width = 1) {
42313
+ return {
42314
+ plots: [{ key, title, values, color: fallback, width, colors }, anchorPlot("zeroAnchor", "Zero Anchor", values.length, 0)],
42315
+ bands: [{ key: "wash", from: key, to: "zeroAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, 0, (_x, i) => colors[i] ?? fallback, valueTransparency) }]
42316
+ };
42317
+ }
41398
42318
  var atrSpec = {
41399
42319
  type: "average-true-range",
41400
42320
  title: "Average True Range",
41401
42321
  shortTitle: "ATR",
41402
42322
  overlay: false,
41403
- inputs: [lengthInput(14), colorInput()],
41404
- compute: (bars, inputs) => ({
41405
- plots: [{ key: "atr", title: "ATR", values: atr(bars, num3(inputs, "length", 14)), color: str3(inputs, "color", SERIES_LINE), width: 2 }]
41406
- })
42323
+ inputs: [
42324
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to average the true range."),
42325
+ 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."),
42326
+ colorInput(NEUTRAL, "ATR Line", "color", "Color of the ATR line while volatility contracts or holds flat."),
42327
+ colorInput(WARNING, "Expanding", "expandColor", "Color of the ATR line while it rises \u2014 volatility expanding bar to bar.")
42328
+ ],
42329
+ compute: (bars, inputs) => {
42330
+ const len = num3(inputs, "length", 14);
42331
+ const tr = trueRange(bars);
42332
+ const kind = str3(inputs, "smoothing", "RMA");
42333
+ const values = kind === "SMA" ? sma(tr, len) : kind === "EMA" ? ema(tr, len) : kind === "WMA" ? wma(tr, len) : rma(tr, len);
42334
+ const calm = str3(inputs, "color", NEUTRAL);
42335
+ return washedToZero("atr", "ATR", values, expansionColors(values, calm, str3(inputs, "expandColor", WARNING)), calm, 75);
42336
+ }
41407
42337
  };
41408
42338
  var historicalVolatility = {
41409
42339
  type: "historical-volatility",
@@ -41411,14 +42341,23 @@ var historicalVolatility = {
41411
42341
  shortTitle: "HV",
41412
42342
  overlay: false,
41413
42343
  inputs: [
41414
- lengthInput(10),
41415
- { 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)" },
41416
- colorInput()
42344
+ { ...lengthInput(10, "length", "Length", 5e3, "Lookback window in bars for the standard deviation of close-to-close log returns."), min: 2 },
42345
+ 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."),
42346
+ 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."),
42347
+ colorInput(WARNING, "Volatility Line", "color", "Color of the line at its recent highs; it fades toward grey as volatility contracts.")
41417
42348
  ],
41418
42349
  compute: (bars, inputs) => {
41419
- const logReturns = map(change(map(closes(bars), Math.log)), (x) => x);
41420
- const values = map(stdev(logReturns, num3(inputs, "length", 10)), (x) => x * Math.sqrt(num3(inputs, "annual", 365)) * 100);
41421
- return { plots: [{ key: "hv", title: "HV", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
42350
+ const len = num3(inputs, "length", 10);
42351
+ const logReturns = sub(map(closes(bars), Math.log), shift(map(closes(bars), Math.log), 1));
42352
+ const meanReturn = map(sum(logReturns, len), (s) => s / len);
42353
+ const squaredSum = sum(map(logReturns, (r) => r * r), len);
42354
+ const factor = bool(inputs, "annualize", true) ? Math.sqrt(num3(inputs, "periodsPerYear", 252)) : 1;
42355
+ const values = zip(squaredSum, meanReturn, (sq, m) => Math.sqrt(Math.max(sq - len * m * m, 0) / (len - 1)) * factor * 100);
42356
+ const low = lowest(values, 100);
42357
+ const high = highest(values, 100);
42358
+ const accent = str3(inputs, "color", WARNING);
42359
+ const colors = values.map((x, i) => Number.isFinite(x) ? gradient(x, low[i] ?? 0, high[i] ?? 0, NEUTRAL, accent) : null);
42360
+ return washedToZero("hv", "Historical Volatility", values, colors, accent, 75, 2);
41422
42361
  }
41423
42362
  };
41424
42363
  var chaikinVolatility = {
@@ -41426,13 +42365,26 @@ var chaikinVolatility = {
41426
42365
  title: "Chaikin Volatility",
41427
42366
  shortTitle: "CHV",
41428
42367
  overlay: false,
41429
- inputs: [lengthInput(10, "emaLength", "EMA length"), lengthInput(10, "rocLength", "RoC length"), colorInput()],
42368
+ inputs: [
42369
+ lengthInput(10, "emaLength", "EMA Length", 5e3, "Length of the exponential moving average applied to the bar high-low range."),
42370
+ lengthInput(10, "rocLength", "ROC Length", 5e3, "Number of bars used for the rate of change of the smoothed high-low range."),
42371
+ colorInput(NEUTRAL, "CHV Color", "color", "Color of the Chaikin Volatility line."),
42372
+ 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.")
42373
+ ],
41430
42374
  compute: (bars, inputs) => {
41431
42375
  const range = ema(sub(highs(bars), lows(bars)), num3(inputs, "emaLength", 10));
41432
- return {
41433
- plots: [{ key: "cv", title: "Chaikin Vol", values: roc(range, num3(inputs, "rocLength", 10)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41434
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
41435
- };
42376
+ const prior = shift(range, num3(inputs, "rocLength", 10));
42377
+ const values = zip(range, prior, (r, p) => p === 0 ? Number.NaN : 100 * (r - p) / p);
42378
+ const calm = str3(inputs, "color", NEUTRAL);
42379
+ const expansion = str3(inputs, "expansionColor", WARNING);
42380
+ return centeredOscillator({
42381
+ key: "chv",
42382
+ title: "Chaikin Volatility",
42383
+ values,
42384
+ // Only expansion is a state worth accenting; contraction stays structural grey.
42385
+ ink: { bull: expansion, bear: calm, neutral: calm },
42386
+ zeroInk: NEUTRAL
42387
+ });
41436
42388
  }
41437
42389
  };
41438
42390
  var massIndex = {
@@ -41440,14 +42392,42 @@ var massIndex = {
41440
42392
  title: "Mass Index",
41441
42393
  shortTitle: "MI",
41442
42394
  overlay: false,
41443
- inputs: [lengthInput(25), colorInput()],
42395
+ inputs: [
42396
+ lengthInput(25, "length", "Sum Length", 5e3, "Number of bars over which the single/double EMA ratio is summed."),
42397
+ lengthInput(9, "emaLength", "EMA Length", 5e3, "Length of the single EMA of the high-low range and of its double smoothing."),
42398
+ floatInput("setupLevel", "Setup Level", 27, 0, 1e3, 0.1, "Reversal bulge set-up level; a rise above it arms the pattern."),
42399
+ 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."),
42400
+ colorInput(NEUTRAL, "Mass Index", "color", "Line color while no reversal bulge is armed."),
42401
+ colorInput(WARNING, "Armed", "armedColor", "Line color from the rise above the setup level until the decline below the trigger level."),
42402
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Reversal bulge levels color.")
42403
+ ],
41444
42404
  compute: (bars, inputs) => {
41445
- const range = sub(highs(bars), lows(bars));
41446
- const single = ema(range, 9);
41447
- const ratio = zip(single, ema(single, 9), (a, b) => b === 0 ? Number.NaN : a / b);
42405
+ const emaLen = num3(inputs, "emaLength", 9);
42406
+ const single = ema(sub(highs(bars), lows(bars)), emaLen);
42407
+ const ratio = zip(single, ema(single, emaLen), (a, b) => b === 0 ? 1 : a / b);
42408
+ const values = sum(ratio, num3(inputs, "length", 25));
42409
+ const setup = num3(inputs, "setupLevel", 27);
42410
+ const trigger = num3(inputs, "triggerLevel", 26.5);
42411
+ const calm = str3(inputs, "color", NEUTRAL);
42412
+ const armedInk = str3(inputs, "armedColor", WARNING);
42413
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
42414
+ let armed = false;
42415
+ const colors = values.map((x) => {
42416
+ if (!Number.isFinite(x)) return null;
42417
+ armed = x > setup ? true : x < trigger ? false : armed;
42418
+ return armed ? armedInk : calm;
42419
+ });
41448
42420
  return {
41449
- plots: [{ key: "mi", title: "Mass Index", values: sum(ratio, num3(inputs, "length", 25)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41450
- levels: [{ key: "reversal", price: 27, color: NEUTRAL, lineStyle: "dotted" }]
42421
+ plots: [
42422
+ { key: "mi", title: "Mass Index", values, color: calm, colors },
42423
+ anchorPlot("setupAnchor", "Setup Anchor", values.length, setup),
42424
+ anchorPlot("triggerAnchor", "Trigger Anchor", values.length, trigger)
42425
+ ],
42426
+ bands: [{ key: "bulge", from: "setupAnchor", to: "triggerAnchor", color: transp(armedInk, 90) }],
42427
+ levels: [
42428
+ { key: "setup", price: setup, color: levelInk, lineStyle: "dashed", title: "Setup Level" },
42429
+ { key: "trigger", price: trigger, color: levelInk, lineStyle: "dashed", title: "Trigger Level" }
42430
+ ]
41451
42431
  };
41452
42432
  }
41453
42433
  };
@@ -41456,30 +42436,83 @@ var standardDeviation = {
41456
42436
  title: "Standard Deviation",
41457
42437
  shortTitle: "StdDev",
41458
42438
  overlay: false,
41459
- inputs: [lengthInput(20), sourceInput(), colorInput()],
41460
- compute: (bars, inputs) => ({
41461
- plots: [
41462
- {
41463
- key: "stdev",
41464
- title: "StdDev",
41465
- values: stdev(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20)),
41466
- color: str3(inputs, "color", SERIES_LINE),
41467
- width: 2
41468
- }
41469
- ]
41470
- })
42439
+ inputs: [
42440
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used to compute the standard deviation."),
42441
+ sourceInput("Close", "source", "Source", "Price series the standard deviation is calculated on."),
42442
+ colorInput(NEUTRAL, "Standard Deviation", "color", "Line color while volatility contracts or holds flat."),
42443
+ colorInput(WARNING, "Expanding", "expandColor", "Line and fill color while volatility expands bar to bar.")
42444
+ ],
42445
+ compute: (bars, inputs) => {
42446
+ const values = stdev(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20));
42447
+ const calm = str3(inputs, "color", NEUTRAL);
42448
+ return washedToZero("stdev", "Standard Deviation", values, expansionColors(values, calm, str3(inputs, "expandColor", WARNING)), calm, 80);
42449
+ }
41471
42450
  };
41472
42451
  var ulcerIndex = {
41473
42452
  type: "ulcer-index",
41474
42453
  title: "Ulcer Index",
41475
42454
  overlay: false,
41476
- inputs: [lengthInput(14), colorInput()],
42455
+ inputs: [
42456
+ { ...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 },
42457
+ sourceInput("Close", "source", "Source", "Price series the Ulcer Index is computed on. Close is the standard choice."),
42458
+ 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."),
42459
+ colorInput(NEUTRAL, "Ulcer Index", "color", "Color of the index near zero; it blends toward the stress color as the reading approaches the threshold."),
42460
+ colorInput(BEARISH, "Stress", "stressColor", "Color of the index at and beyond the alert threshold."),
42461
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Vertical gradient between zero and the Ulcer Index, fully transparent at zero."),
42462
+ 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."),
42463
+ colorInput(transp(BEARISH, 50), "Drawdown Color", "drawdownColor", "Color of the drawdown columns."),
42464
+ colorInput(NEUTRAL, "Threshold", "thresholdColor", "Color of the alert threshold line.")
42465
+ ],
41477
42466
  compute: (bars, inputs) => {
41478
42467
  const len = num3(inputs, "length", 14);
41479
- const c = closes(bars);
41480
- const drawdown = zip(c, highest(c, len), (x, h) => h === 0 ? 0 : 100 * (x - h) / h);
41481
- const values = map(sma(map(drawdown, (d) => d * d), len), Math.sqrt);
41482
- return { plots: [{ key: "ui", title: "Ulcer", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
42468
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
42469
+ const n = src.length;
42470
+ const values = new Array(n).fill(Number.NaN);
42471
+ const drawdown = new Array(n).fill(Number.NaN);
42472
+ for (let i = len - 1; i < n; i++) {
42473
+ let runMax = Number.NaN;
42474
+ let sumSq = 0;
42475
+ let valid = true;
42476
+ let last = Number.NaN;
42477
+ for (let k = i - len + 1; k <= i; k++) {
42478
+ const price = src[k];
42479
+ if (!Number.isFinite(price)) {
42480
+ valid = false;
42481
+ break;
42482
+ }
42483
+ runMax = Number.isFinite(runMax) ? Math.max(runMax, price) : price;
42484
+ if (runMax <= 0) {
42485
+ valid = false;
42486
+ break;
42487
+ }
42488
+ last = 100 * (price - runMax) / runMax;
42489
+ sumSq += last * last;
42490
+ }
42491
+ if (!valid) continue;
42492
+ values[i] = Math.sqrt(sumSq / len);
42493
+ drawdown[i] = last;
42494
+ }
42495
+ const calm = str3(inputs, "color", NEUTRAL);
42496
+ const stress = str3(inputs, "stressColor", BEARISH);
42497
+ const threshold = num3(inputs, "threshold", 5);
42498
+ const colors = values.map((x) => Number.isFinite(x) ? threshold > 0 ? gradient(x, 0, threshold, calm, stress) : stress : null);
42499
+ const plots = [{ key: "ui", title: "Ulcer Index", values, color: calm, colors }];
42500
+ const bands = [];
42501
+ if (bool(inputs, "gradientFill", true)) {
42502
+ plots.push(anchorPlot("zeroAnchor", "Zero Base", n, 0));
42503
+ bands.push({ key: "wash", from: "ui", to: "zeroAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, 0, (_x, i) => colors[i] ?? calm) });
42504
+ }
42505
+ if (bool(inputs, "showDrawdown", false)) {
42506
+ plots.push({ key: "dd", title: "Drawdown %", values: drawdown, kind: "columns", color: str3(inputs, "drawdownColor", transp(BEARISH, 50)), base: 0 });
42507
+ }
42508
+ return {
42509
+ plots,
42510
+ bands,
42511
+ levels: [
42512
+ { key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dotted", title: "Zero Level" },
42513
+ { key: "threshold", price: threshold, color: str3(inputs, "thresholdColor", NEUTRAL), lineStyle: "dashed", title: "Alert Threshold" }
42514
+ ]
42515
+ };
41483
42516
  }
41484
42517
  };
41485
42518
  var choppiness = {
@@ -41487,19 +42520,33 @@ var choppiness = {
41487
42520
  title: "Choppiness Index",
41488
42521
  shortTitle: "CHOP",
41489
42522
  overlay: false,
41490
- inputs: [lengthInput(14), colorInput()],
42523
+ inputs: [
42524
+ { ...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 },
42525
+ floatInput("upperBand", "Upper Band", 61.8, 0, 100, 0.1, "Readings above this level indicate a choppy, range-bound market."),
42526
+ floatInput("lowerBand", "Lower Band", 38.2, 0, 100, 0.1, "Readings below this level indicate a strong trend."),
42527
+ colorInput(NEUTRAL, "CHOP Line", "color", "Color of the Choppiness Index line."),
42528
+ colorInput(BULLISH, "Trending", "trendingColor", "Line, fill and zone color toward and below the lower band \u2014 a trending market."),
42529
+ colorInput(BEARISH, "Choppy", "choppyColor", "Line, fill and zone color toward and above the upper band \u2014 a choppy, range-bound market."),
42530
+ colorInput(transp(NEUTRAL, 90), "Band Fill", "fillColor", "Fill color of the zone between the upper and lower bands.")
42531
+ ],
41491
42532
  compute: (bars, inputs) => {
41492
42533
  const len = num3(inputs, "length", 14);
41493
42534
  const trSum = sum(trueRange(bars), len);
41494
42535
  const span = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => h - l);
41495
42536
  const values = zip(trSum, span, (t, s) => s > 0 && t > 0 ? 100 * Math.log10(t / s) / Math.log10(len) : Number.NaN);
41496
- return {
41497
- plots: [{ key: "chop", title: "CHOP", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41498
- levels: [
41499
- { key: "upper", price: 61.8, color: NEUTRAL },
41500
- { key: "lower", price: 38.2, color: NEUTRAL }
41501
- ]
41502
- };
42537
+ return boundedOscillator({
42538
+ key: "chop",
42539
+ title: "CHOP",
42540
+ values,
42541
+ scale: { top: 100, bottom: 0 },
42542
+ overbought: num3(inputs, "upperBand", 61.8),
42543
+ oversold: num3(inputs, "lowerBand", 38.2),
42544
+ ink: { low: str3(inputs, "trendingColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "choppyColor", BEARISH) },
42545
+ levelInk: NEUTRAL,
42546
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 90)),
42547
+ midpointWash: true,
42548
+ levelTitles: { overbought: "Upper Band", oversold: "Lower Band" }
42549
+ });
41503
42550
  }
41504
42551
  };
41505
42552
  var volatilitySpecs = [atrSpec, historicalVolatility, chaikinVolatility, massIndex, standardDeviation, ulcerIndex, choppiness];
@@ -41512,17 +42559,85 @@ function moneyFlowVolume(bars) {
41512
42559
  return (2 * b.close - b.high - b.low) / (b.high - b.low) * b.volume;
41513
42560
  });
41514
42561
  }
42562
+ function slopeColors2(values, bull, bear, flat) {
42563
+ return values.map((x, i) => {
42564
+ if (!Number.isFinite(x)) return null;
42565
+ const prev2 = i > 0 ? values[i - 1] : Number.NaN;
42566
+ if (!Number.isFinite(prev2)) return flat;
42567
+ return x > prev2 ? bull : x < prev2 ? bear : flat;
42568
+ });
42569
+ }
42570
+ function volumeIndex(bars, src, base, when) {
42571
+ const out = new Array(bars.length).fill(Number.NaN);
42572
+ let index = base;
42573
+ for (let i = 0; i < bars.length; i++) {
42574
+ const v = bars[i].volume;
42575
+ const pv = i > 0 ? bars[i - 1].volume : null;
42576
+ const prev2 = i > 0 ? src[i - 1] : Number.NaN;
42577
+ const active = v != null && pv != null && Number.isFinite(v) && Number.isFinite(pv) && when(v, pv);
42578
+ if (active && Number.isFinite(prev2) && prev2 !== 0) index *= 1 + (src[i] - prev2) / prev2;
42579
+ out[i] = index;
42580
+ }
42581
+ return out;
42582
+ }
42583
+ function regimeIndex(key, title, values, signal, ink, showSignal, gradientFill, singleColor = false) {
42584
+ const inkOf = (i) => {
42585
+ const s = signal[i];
42586
+ const x = values[i];
42587
+ if (singleColor || !Number.isFinite(s)) return ink.neutral;
42588
+ return x > s ? ink.bull : x < s ? ink.bear : ink.neutral;
42589
+ };
42590
+ const plots = [
42591
+ {
42592
+ key: "signal",
42593
+ title: "Signal EMA",
42594
+ values: signal,
42595
+ color: ink.signal,
42596
+ ...showSignal ? {} : { display: { pane: false } }
42597
+ },
42598
+ { key, title, values, color: ink.neutral, width: 2, colors: values.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) }
42599
+ ];
42600
+ const bands = gradientFill ? [
42601
+ {
42602
+ key: "regime",
42603
+ from: key,
42604
+ to: "signal",
42605
+ color: transp(NEUTRAL, 100),
42606
+ gradient: values.map((x, i) => {
42607
+ const s = signal[i];
42608
+ if (!Number.isFinite(x) || !Number.isFinite(s)) return null;
42609
+ const above = x > s;
42610
+ const c = above ? ink.bull : ink.bear;
42611
+ return { topValue: Math.max(x, s), bottomValue: Math.min(x, s), topColor: transp(c, above ? 50 : 100), bottomColor: transp(c, above ? 100 : 50) };
42612
+ })
42613
+ }
42614
+ ] : [];
42615
+ return { plots, bands };
42616
+ }
41515
42617
  var obv = {
41516
42618
  type: "on-balance-volume",
41517
42619
  title: "On Balance Volume",
41518
42620
  shortTitle: "OBV",
41519
42621
  overlay: false,
41520
- inputs: [colorInput()],
42622
+ inputs: [
42623
+ boolInput("smoothing", "Smoothing MA", false, SETTINGS, "Display a moving average of OBV to act as a smoothing/signal line."),
42624
+ optionInput("maType", "MA Type", "SMA", ["SMA", "EMA", "WMA", "RMA (SMMA)"], "Moving average type used for the smoothing MA."),
42625
+ lengthInput(20, "maLength", "MA Length", 5e3, "Number of bars used for the smoothing MA."),
42626
+ colorInput(INFO, "OBV", "color", "Color of the OBV line."),
42627
+ colorInput(WARNING, "Smoothing MA", "maColor", "Color of the smoothing MA line.")
42628
+ ],
41521
42629
  compute: (bars, inputs) => {
41522
42630
  const vol = volumes(bars);
41523
42631
  const d = change(closes(bars));
41524
- const signed3 = zip(vol, d, (v, x) => x > 0 ? v : x < 0 ? -v : 0);
41525
- return { plots: [{ key: "obv", title: "OBV", values: cumSum(signed3), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
42632
+ const values = cumSum(zip(vol, d, (v, x) => x > 0 ? v : x < 0 ? -v : 0));
42633
+ const plots = [{ key: "obv", title: "OBV", values, color: str3(inputs, "color", INFO) }];
42634
+ if (bool(inputs, "smoothing", false)) {
42635
+ const len = num3(inputs, "maLength", 20);
42636
+ const kind = str3(inputs, "maType", "SMA");
42637
+ const ma = kind === "EMA" ? ema(values, len) : kind === "WMA" ? wma(values, len) : kind === "RMA (SMMA)" ? rma(values, len) : sma(values, len);
42638
+ plots.push({ key: "ma", title: "Smoothing MA", values: ma, color: str3(inputs, "maColor", WARNING) });
42639
+ }
42640
+ return { plots };
41526
42641
  }
41527
42642
  };
41528
42643
  var accDist = {
@@ -41530,24 +42645,50 @@ var accDist = {
41530
42645
  title: "Accumulation / Distribution",
41531
42646
  shortTitle: "A/D",
41532
42647
  overlay: false,
41533
- inputs: [colorInput()],
41534
- compute: (bars, inputs) => ({
41535
- plots: [{ key: "ad", title: "A/D", values: cumSum(moneyFlowVolume(bars)), color: str3(inputs, "color", SERIES_LINE), width: 2 }]
41536
- })
42648
+ inputs: [
42649
+ boolInput("slopeColoring", "Slope Coloring", true, STYLE, "Color the line by its slope \u2014 bullish while rising (accumulation), bearish while falling (distribution)."),
42650
+ colorInput(NEUTRAL, "A/D Line", "color", "Color of the line when slope coloring is disabled or the line is flat."),
42651
+ colorInput(BULLISH, "Rising", "bullColor", "Line color while the A/D Line rises."),
42652
+ colorInput(BEARISH, "Falling", "bearColor", "Line color while the A/D Line falls.")
42653
+ ],
42654
+ compute: (bars, inputs) => {
42655
+ const values = cumSum(moneyFlowVolume(bars));
42656
+ const flat = str3(inputs, "color", NEUTRAL);
42657
+ return {
42658
+ plots: [
42659
+ {
42660
+ key: "ad",
42661
+ title: "A/D",
42662
+ values,
42663
+ color: flat,
42664
+ ...bool(inputs, "slopeColoring", true) ? { colors: slopeColors2(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), flat) } : {}
42665
+ }
42666
+ ]
42667
+ };
42668
+ }
41537
42669
  };
41538
42670
  var cmf = {
41539
42671
  type: "chaikin-money-flow",
41540
42672
  title: "Chaikin Money Flow",
41541
42673
  shortTitle: "CMF",
41542
42674
  overlay: false,
41543
- inputs: [lengthInput(20), colorInput()],
42675
+ inputs: [
42676
+ lengthInput(20, "length", "Length", 5e3, "Lookback length over which money flow volume and volume are summed."),
42677
+ colorInput(NEUTRAL, "CMF Line", "color", "Color of the Chaikin Money Flow line."),
42678
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the CMF reads above zero (net money flowing in)."),
42679
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the CMF reads below zero (net money flowing out)."),
42680
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
42681
+ ],
41544
42682
  compute: (bars, inputs) => {
41545
42683
  const len = num3(inputs, "length", 20);
41546
42684
  const values = zip(sum(moneyFlowVolume(bars), len), sum(volumes(bars), len), (m, v) => v === 0 ? 0 : m / v);
41547
- return {
41548
- plots: [{ key: "cmf", title: "CMF", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41549
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
41550
- };
42685
+ return centeredOscillator({
42686
+ key: "cmf",
42687
+ title: "CMF",
42688
+ values,
42689
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
42690
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
42691
+ });
41551
42692
  }
41552
42693
  };
41553
42694
  var chaikinOscillator = {
@@ -41555,14 +42696,23 @@ var chaikinOscillator = {
41555
42696
  title: "Chaikin Oscillator",
41556
42697
  shortTitle: "Chaikin Osc",
41557
42698
  overlay: false,
41558
- inputs: [lengthInput(3, "fastLength", "Fast length"), lengthInput(10, "slowLength", "Slow length"), colorInput()],
42699
+ inputs: [
42700
+ lengthInput(3, "fastLength", "Fast Length", 5e3, "Length of the fast EMA applied to the accumulation/distribution line."),
42701
+ lengthInput(10, "slowLength", "Slow Length", 5e3, "Length of the slow EMA applied to the accumulation/distribution line."),
42702
+ colorInput(NEUTRAL, "Oscillator", "color", "Color of the Chaikin Oscillator line."),
42703
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the oscillator reads above zero (accumulation)."),
42704
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the oscillator reads below zero (distribution).")
42705
+ ],
41559
42706
  compute: (bars, inputs) => {
41560
42707
  const ad = cumSum(moneyFlowVolume(bars));
41561
42708
  const values = sub(ema(ad, num3(inputs, "fastLength", 3)), ema(ad, num3(inputs, "slowLength", 10)));
41562
- return {
41563
- plots: [{ key: "co", title: "Chaikin Osc", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41564
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
41565
- };
42709
+ return centeredOscillator({
42710
+ key: "co",
42711
+ title: "Chaikin Oscillator",
42712
+ values,
42713
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
42714
+ zeroInk: NEUTRAL
42715
+ });
41566
42716
  }
41567
42717
  };
41568
42718
  var eom = {
@@ -41570,20 +42720,28 @@ var eom = {
41570
42720
  title: "Ease of Movement",
41571
42721
  shortTitle: "EOM",
41572
42722
  overlay: false,
41573
- inputs: [lengthInput(14), { key: "divisor", title: "Divisor", type: "int", defval: 1e4, min: 1, max: 1e9, step: 1 }, colorInput()],
42723
+ inputs: [
42724
+ lengthInput(14, "length", "Length", 5e3, "Number of bars in the simple moving average smoothing the 1-bar Ease of Movement values."),
42725
+ intInput("divisor", "Divisor", 1e4, 1, 1e9, "Volume scaling constant of the box ratio; larger values scale the oscillator up. 10000 is the conventional default."),
42726
+ colorInput(NEUTRAL, "EOM Line", "color", "Color of the line when it sits exactly at zero."),
42727
+ colorInput(BULLISH, "Above Zero", "bullColor", "Color of the line and fill while Ease of Movement is positive."),
42728
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while Ease of Movement is negative.")
42729
+ ],
41574
42730
  compute: (bars, inputs) => {
41575
42731
  const div = num3(inputs, "divisor", 1e4);
41576
- const hl2 = sourceValues(bars, "HL2");
41577
- const move = change(hl2);
42732
+ const move = change(sourceValues(bars, "HL2"));
41578
42733
  const raw = bars.map((b, i) => {
41579
42734
  const m = move[i];
41580
- if (b.volume == null || !Number.isFinite(b.volume) || b.volume === 0 || !Number.isFinite(m)) return Number.NaN;
41581
- return div * m * (b.high - b.low) / b.volume;
42735
+ if (b.volume == null || !Number.isFinite(b.volume) || !Number.isFinite(m)) return Number.NaN;
42736
+ return b.volume === 0 ? 0 : div * m * (b.high - b.low) / b.volume;
42737
+ });
42738
+ return centeredOscillator({
42739
+ key: "eom",
42740
+ title: "EOM",
42741
+ values: sma(raw, num3(inputs, "length", 14)),
42742
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
42743
+ zeroInk: NEUTRAL
41582
42744
  });
41583
- return {
41584
- plots: [{ key: "eom", title: "EOM", values: sma(raw, num3(inputs, "length", 14)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41585
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
41586
- };
41587
42745
  }
41588
42746
  };
41589
42747
  var forceIndex = {
@@ -41591,13 +42749,24 @@ var forceIndex = {
41591
42749
  title: "Force Index",
41592
42750
  shortTitle: "FI",
41593
42751
  overlay: false,
41594
- inputs: [lengthInput(13), colorInput()],
42752
+ inputs: [
42753
+ 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."),
42754
+ colorInput(BULLISH, "Force Index", "color", "Color of the line while above zero (buying pressure)."),
42755
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line while below zero (selling pressure).")
42756
+ ],
41595
42757
  compute: (bars, inputs) => {
41596
42758
  const raw = zip(change(closes(bars)), volumes(bars), (d, v) => d * v);
41597
- return {
41598
- plots: [{ key: "force", title: "Force", values: ema(raw, num3(inputs, "length", 13)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41599
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
41600
- };
42759
+ const bull = str3(inputs, "color", BULLISH);
42760
+ return centeredOscillator({
42761
+ key: "force",
42762
+ title: "Force Index",
42763
+ values: ema(raw, num3(inputs, "length", 13)),
42764
+ // The reference reads an exact zero as buying pressure, so neutral shares the bull ink.
42765
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
42766
+ zeroInk: NEUTRAL,
42767
+ width: 2,
42768
+ valueTransparency: 60
42769
+ });
41601
42770
  }
41602
42771
  };
41603
42772
  var klinger = {
@@ -41605,19 +42774,44 @@ var klinger = {
41605
42774
  title: "Klinger Oscillator",
41606
42775
  shortTitle: "KVO",
41607
42776
  overlay: false,
41608
- inputs: [lengthInput(34, "fastLength", "Fast length"), lengthInput(55, "slowLength", "Slow length"), lengthInput(13, "signalLength", "Signal length")],
42777
+ inputs: [
42778
+ lengthInput(34, "fastLength", "Fast Length", 5e3, "Length of the fast EMA applied to the volume force."),
42779
+ lengthInput(55, "slowLength", "Slow Length", 5e3, "Length of the slow EMA applied to the volume force."),
42780
+ lengthInput(13, "signalLength", "Signal Length", 500, "Length of the EMA applied to the oscillator to obtain the signal line."),
42781
+ colorInput(BULLISH, "Bullish", "color", "Color of the Klinger oscillator while above zero."),
42782
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the Klinger oscillator while below zero."),
42783
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line.")
42784
+ ],
41609
42785
  compute: (bars, inputs) => {
41610
- const hlc3 = sourceValues(bars, "HLC3");
41611
- const trend = change(hlc3);
41612
- const signedVolume = zip(volumes(bars), trend, (v, t) => t >= 0 ? v : -v);
41613
- const line = sub(ema(signedVolume, num3(inputs, "fastLength", 34)), ema(signedVolume, num3(inputs, "slowLength", 55)));
41614
- return {
41615
- plots: [
41616
- { key: "kvo", title: "KVO", values: line, color: SERIES_LINE, width: 2 },
41617
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: WARNING }
41618
- ],
41619
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
41620
- };
42786
+ const n = bars.length;
42787
+ const force = new Array(n).fill(Number.NaN);
42788
+ let trend = 1;
42789
+ let prevTrend = 1;
42790
+ let cm = 0;
42791
+ let prevDm = 0;
42792
+ for (let i = 0; i < n; i++) {
42793
+ const b = bars[i];
42794
+ const hlc = b.high + b.low + b.close;
42795
+ const prevHlc = i > 0 ? bars[i - 1].high + bars[i - 1].low + bars[i - 1].close : hlc;
42796
+ trend = hlc > prevHlc ? 1 : -1;
42797
+ const dm = b.high - b.low;
42798
+ cm = i > 0 && trend === prevTrend ? cm + dm : prevDm + dm;
42799
+ const v = b.volume;
42800
+ force[i] = v == null || !Number.isFinite(v) ? Number.NaN : cm === 0 ? 0 : v * Math.abs(2 * (dm / cm) - 1) * trend * 100;
42801
+ prevTrend = trend;
42802
+ prevDm = dm;
42803
+ }
42804
+ const line = sub(ema(force, num3(inputs, "fastLength", 34)), ema(force, num3(inputs, "slowLength", 55)));
42805
+ const bull = str3(inputs, "color", BULLISH);
42806
+ return centeredOscillator({
42807
+ key: "kvo",
42808
+ title: "KVO",
42809
+ values: line,
42810
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
42811
+ zeroInk: transp(NEUTRAL, 50),
42812
+ valueTransparency: 60,
42813
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: str3(inputs, "signalColor", WARNING) }]
42814
+ });
41621
42815
  }
41622
42816
  };
41623
42817
  var mfi = {
@@ -41625,7 +42819,15 @@ var mfi = {
41625
42819
  title: "Money Flow Index",
41626
42820
  shortTitle: "MFI",
41627
42821
  overlay: false,
41628
- inputs: [lengthInput(14), colorInput()],
42822
+ inputs: [
42823
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to sum positive and negative money flow."),
42824
+ floatInput("overbought", "Overbought Level", 80, 0, 100, 1, "Level above which the MFI is considered overbought."),
42825
+ floatInput("oversold", "Oversold Level", 20, 0, 100, 1, "Level below which the MFI is considered oversold."),
42826
+ colorInput(NEUTRAL, "MFI", "color", "Color of the MFI line between the levels. Toward either level it blends into that level\u2019s color."),
42827
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
42828
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level."),
42829
+ colorInput(transp(NEUTRAL, 94), "OB/OS Zone Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
42830
+ ],
41629
42831
  compute: (bars, inputs) => {
41630
42832
  const len = num3(inputs, "length", 14);
41631
42833
  const tp = sourceValues(bars, "HLC3");
@@ -41633,48 +42835,57 @@ var mfi = {
41633
42835
  const d = change(tp);
41634
42836
  const pos = sum(zip(rawFlow, d, (f, x) => x > 0 ? f : 0), len);
41635
42837
  const neg = sum(zip(rawFlow, d, (f, x) => x < 0 ? f : 0), len);
41636
- const values = zip(pos, neg, (p, n) => n === 0 ? 100 : 100 - 100 / (1 + p / n));
41637
- return {
41638
- plots: [{ key: "mfi", title: "MFI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41639
- levels: [
41640
- { key: "upper", price: 80, color: NEUTRAL },
41641
- { key: "lower", price: 20, color: NEUTRAL }
41642
- ]
41643
- };
42838
+ const values = zip(pos, neg, (p, w) => p + w > 0 ? 100 * p / (p + w) : Number.NaN);
42839
+ return boundedOscillator({
42840
+ key: "mfi",
42841
+ title: "MFI",
42842
+ values,
42843
+ scale: { top: 100, bottom: 0 },
42844
+ overbought: num3(inputs, "overbought", 80),
42845
+ oversold: num3(inputs, "oversold", 20),
42846
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
42847
+ levelInk: NEUTRAL,
42848
+ zoneTransparency: 91,
42849
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 94))
42850
+ });
41644
42851
  }
41645
42852
  };
41646
- function volumeIndex(bars, when) {
41647
- const out = new Array(bars.length).fill(Number.NaN);
41648
- let index = 1e3;
41649
- let started = false;
41650
- for (let i = 1; i < bars.length; i++) {
41651
- const v = bars[i].volume;
41652
- const pv = bars[i - 1].volume;
41653
- const pc = bars[i - 1].close;
41654
- if (v == null || pv == null || !Number.isFinite(v) || !Number.isFinite(pv) || pc === 0) {
41655
- if (started) out[i] = index;
41656
- continue;
41657
- }
41658
- started = true;
41659
- if (when(v, pv)) index += (bars[i].close - pc) / pc * index;
41660
- out[i] = index;
41661
- }
41662
- return out;
42853
+ function regimeStyleInputs(signalTitle) {
42854
+ return [
42855
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the line and gradient fill while it holds above its signal EMA."),
42856
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line and gradient fill while it holds below its signal EMA."),
42857
+ colorInput(NEUTRAL, "Neutral", "neutralColor", "Color used while the signal EMA is still forming, or the line sits exactly on it."),
42858
+ colorInput(NEUTRAL, signalTitle, "signalColor", "Color of the signal EMA overlay."),
42859
+ 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.")
42860
+ ];
41663
42861
  }
42862
+ var NVI_POSITION = "Position vs Average";
42863
+ var NVI_SINGLE = "Single Color";
41664
42864
  var nvi = {
41665
42865
  type: "negative-volume-index",
41666
42866
  title: "Negative Volume Index",
41667
42867
  shortTitle: "Negative Volume Index",
41668
42868
  overlay: false,
41669
- inputs: [lengthInput(255, "signalLength", "Signal length"), colorInput()],
42869
+ inputs: [
42870
+ 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."),
42871
+ 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."),
42872
+ lengthInput(255, "signalLength", "Signal Length", 5e3, "Length of the signal EMA. The convention is one trading year \u2014 255 sessions on daily charts."),
42873
+ 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."),
42874
+ ...regimeStyleInputs("Signal EMA")
42875
+ ],
41670
42876
  compute: (bars, inputs) => {
41671
- const line = volumeIndex(bars, (v, pv) => v < pv);
41672
- return {
41673
- plots: [
41674
- { key: "nvi", title: "NVI", values: line, color: str3(inputs, "color", SERIES_LINE), width: 2 },
41675
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 255)), color: WARNING }
41676
- ]
41677
- };
42877
+ const values = volumeIndex(bars, closes(bars), num3(inputs, "base", 1e3), (v, pv) => v < pv);
42878
+ const signal = ema(values, num3(inputs, "signalLength", 255));
42879
+ return regimeIndex(
42880
+ "nvi",
42881
+ "NVI",
42882
+ values,
42883
+ signal,
42884
+ { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "neutralColor", NEUTRAL), signal: str3(inputs, "signalColor", NEUTRAL) },
42885
+ bool(inputs, "showSignal", true),
42886
+ bool(inputs, "gradientFill", true),
42887
+ str3(inputs, "colorMode", NVI_POSITION) === NVI_SINGLE
42888
+ );
41678
42889
  }
41679
42890
  };
41680
42891
  var pvi = {
@@ -41682,15 +42893,26 @@ var pvi = {
41682
42893
  title: "Positive Volume Index",
41683
42894
  shortTitle: "Positive Volume Index",
41684
42895
  overlay: false,
41685
- inputs: [lengthInput(255, "signalLength", "Signal length"), colorInput()],
42896
+ inputs: [
42897
+ sourceInput("Close", "source", "Source", "Price series whose one-bar percentage change moves PVI on its active sessions."),
42898
+ 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."),
42899
+ 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."),
42900
+ 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."),
42901
+ ...regimeStyleInputs("Signal Line")
42902
+ ],
41686
42903
  compute: (bars, inputs) => {
41687
- const line = volumeIndex(bars, (v, pv) => v > pv);
41688
- return {
41689
- plots: [
41690
- { key: "pvi", title: "PVI", values: line, color: str3(inputs, "color", SERIES_LINE), width: 2 },
41691
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 255)), color: WARNING }
41692
- ]
41693
- };
42904
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
42905
+ const values = volumeIndex(bars, src, num3(inputs, "base", 1e3), (v, pv) => v > pv);
42906
+ const signal = ema(values, num3(inputs, "signalLength", 255));
42907
+ return regimeIndex(
42908
+ "pvi",
42909
+ "PVI",
42910
+ values,
42911
+ signal,
42912
+ { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "neutralColor", NEUTRAL), signal: str3(inputs, "signalColor", NEUTRAL) },
42913
+ bool(inputs, "showSignal", true),
42914
+ bool(inputs, "gradientFill", true)
42915
+ );
41694
42916
  }
41695
42917
  };
41696
42918
  var pvt = {
@@ -41698,7 +42920,15 @@ var pvt = {
41698
42920
  title: "Price Volume Trend",
41699
42921
  shortTitle: "PVT",
41700
42922
  overlay: false,
41701
- inputs: [colorInput()],
42923
+ inputs: [
42924
+ boolInput("showSignal", "Signal Line", false, SETTINGS, "Display a moving average of PVT acting as a signal line."),
42925
+ optionInput("signalType", "Signal Type", "SMA", ["SMA", "EMA"], "Moving average type used for the signal line."),
42926
+ lengthInput(21, "signalLength", "Signal Length", 5e3, "Number of bars used to compute the signal line."),
42927
+ colorInput(NEUTRAL, "PVT Color", "color", "Color of the PVT line while it is flat."),
42928
+ colorInput(BULLISH, "Rising", "bullColor", "Color of the PVT line while it rises bar to bar."),
42929
+ colorInput(BEARISH, "Falling", "bearColor", "Color of the PVT line while it falls bar to bar."),
42930
+ colorInput(WARNING, "Signal Color", "signalColor", "Color of the signal line.")
42931
+ ],
41702
42932
  compute: (bars, inputs) => {
41703
42933
  const c = closes(bars);
41704
42934
  const raw = bars.map((b, i) => {
@@ -41706,7 +42936,17 @@ var pvt = {
41706
42936
  const pc = c[i - 1];
41707
42937
  return pc === 0 ? 0 : (c[i] - pc) / pc * b.volume;
41708
42938
  });
41709
- return { plots: [{ key: "pvt", title: "PVT", values: cumSum(raw), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
42939
+ const values = cumSum(raw);
42940
+ const flat = str3(inputs, "color", NEUTRAL);
42941
+ const plots = [
42942
+ { key: "pvt", title: "PVT", values, color: flat, width: 2, colors: slopeColors2(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), flat) }
42943
+ ];
42944
+ if (bool(inputs, "showSignal", false)) {
42945
+ const len = num3(inputs, "signalLength", 21);
42946
+ const signal = str3(inputs, "signalType", "SMA") === "EMA" ? ema(values, len) : sma(values, len);
42947
+ plots.push({ key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) });
42948
+ }
42949
+ return { plots };
41710
42950
  }
41711
42951
  };
41712
42952
  var volumeOscillator = {
@@ -41714,39 +42954,75 @@ var volumeOscillator = {
41714
42954
  title: "Volume Oscillator",
41715
42955
  shortTitle: "Vol Osc",
41716
42956
  overlay: false,
41717
- inputs: [lengthInput(5, "fastLength", "Fast length"), lengthInput(10, "slowLength", "Slow length"), colorInput()],
42957
+ inputs: [
42958
+ lengthInput(5, "fastLength", "Fast Length", 5e3, "Length of the fast EMA of volume."),
42959
+ lengthInput(10, "slowLength", "Slow Length", 5e3, "Length of the slow EMA of volume."),
42960
+ colorInput(BULLISH, "Above Zero", "color", "Color of the line and fill while it holds above zero \u2014 volume expanding."),
42961
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while it holds below zero \u2014 volume contracting.")
42962
+ ],
41718
42963
  compute: (bars, inputs) => {
41719
42964
  const vol = volumes(bars);
41720
42965
  const fast = ema(vol, num3(inputs, "fastLength", 5));
41721
42966
  const slow = ema(vol, num3(inputs, "slowLength", 10));
41722
- const values = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : 100 * (f - s) / s);
41723
- return {
41724
- plots: [{ key: "vo", title: "Volume Osc", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41725
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
41726
- };
42967
+ const values = zip(fast, slow, (f, s) => s === 0 ? 0 : 100 * (f - s) / s);
42968
+ const bull = str3(inputs, "color", BULLISH);
42969
+ return centeredOscillator({
42970
+ key: "vo",
42971
+ title: "Volume Oscillator",
42972
+ values,
42973
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
42974
+ zeroInk: NEUTRAL,
42975
+ valueTransparency: 60
42976
+ });
41727
42977
  }
41728
42978
  };
42979
+ var OSCILLATOR = "Oscillator";
41729
42980
  var pvo = {
41730
42981
  type: "pvo",
41731
42982
  title: "Percentage Volume Oscillator",
41732
42983
  shortTitle: "PVO",
41733
42984
  overlay: false,
41734
- inputs: [lengthInput(12, "fastLength", "Fast length"), lengthInput(26, "slowLength", "Slow length"), lengthInput(9, "signalLength", "Signal length")],
42985
+ inputs: [
42986
+ { ...lengthInput(12, "fastLength", "Fast Length", 5e3, "Period of the fast EMA of volume. The conventional setting is 12."), group: OSCILLATOR },
42987
+ { ...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 },
42988
+ { ...lengthInput(9, "signalLength", "Signal Length", 500, "Period of the EMA applied to the PVO to form the signal line."), group: OSCILLATOR },
42989
+ colorInput(BULLISH, "Expansion Color", "color", "Color of the oscillator and fill while participation expands."),
42990
+ colorInput(BEARISH, "Contraction Color", "bearColor", "Color of the oscillator and fill while participation contracts."),
42991
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
42992
+ colorInput(transp(NEUTRAL, 50), "Zero Line", "zeroColor", "Color of the zero reference line."),
42993
+ boolInput("showSignal", "Show Signal Line", true, STYLE, "Plot the signal line, an EMA of the PVO."),
42994
+ boolInput("showHistogram", "Show Histogram", true, STYLE, "Plot the difference between the PVO and its signal line as columns behind the oscillator.")
42995
+ ],
41735
42996
  compute: (bars, inputs) => {
41736
42997
  const vol = volumes(bars);
41737
42998
  const fast = ema(vol, num3(inputs, "fastLength", 12));
41738
42999
  const slow = ema(vol, num3(inputs, "slowLength", 26));
41739
- const line = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : 100 * (f - s) / s);
41740
- const signal = ema(line, num3(inputs, "signalLength", 9));
41741
- const hist = sub(line, signal);
41742
- return {
41743
- plots: [
41744
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: hist.map((x) => Number.isFinite(x) ? x >= 0 ? BULLISH : BEARISH : null), base: 0 },
41745
- { key: "pvo", title: "PVO", values: line, color: SERIES_LINE, width: 2 },
41746
- { key: "signal", title: "Signal", values: signal, color: WARNING }
41747
- ],
41748
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
41749
- };
43000
+ const values = zip(fast, slow, (f, s) => s > 0 ? 100 * (f - s) / s : Number.NaN);
43001
+ const signal = ema(values, num3(inputs, "signalLength", 9));
43002
+ const expansion = str3(inputs, "color", BULLISH);
43003
+ const contraction = str3(inputs, "bearColor", BEARISH);
43004
+ const extraPlots = [];
43005
+ if (bool(inputs, "showHistogram", true)) {
43006
+ const hist = sub(values, signal);
43007
+ extraPlots.push({
43008
+ key: "hist",
43009
+ title: "Histogram",
43010
+ values: hist,
43011
+ kind: "columns",
43012
+ color: transp(expansion, 60),
43013
+ colors: hist.map((x) => Number.isFinite(x) ? transp(x > 0 ? expansion : contraction, 60) : null),
43014
+ base: 0
43015
+ });
43016
+ }
43017
+ if (bool(inputs, "showSignal", true)) extraPlots.push({ key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) });
43018
+ return centeredOscillator({
43019
+ key: "pvo",
43020
+ title: "PVO",
43021
+ values,
43022
+ ink: { bull: expansion, bear: contraction, neutral: contraction },
43023
+ zeroInk: str3(inputs, "zeroColor", transp(NEUTRAL, 50)),
43024
+ extraPlots
43025
+ });
41750
43026
  }
41751
43027
  };
41752
43028
  var vfi = {
@@ -41755,55 +43031,86 @@ var vfi = {
41755
43031
  shortTitle: "Volume Flow Indicator",
41756
43032
  overlay: false,
41757
43033
  inputs: [
41758
- lengthInput(130),
41759
- { key: "coef", title: "Cutoff coefficient", type: "float", defval: 0.2, min: 0.01, max: 10, step: 0.01 },
41760
- { key: "volCoef", title: "Volume cap", type: "float", defval: 2.5, min: 0.1, max: 50, step: 0.1 },
41761
- lengthInput(3, "smoothLength", "Smoothing length")
43034
+ { ...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 },
43035
+ 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."),
43036
+ 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."),
43037
+ { ...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 },
43038
+ boolInput("smooth", "Smooth VFI", true, SETTINGS, "Applies the published light smoothing \u2014 an EMA of the summed line \u2014 before plotting."),
43039
+ lengthInput(3, "smoothLength", "Smoothing Length", 500, "Length of the smoothing EMA. The published version uses 3 bars."),
43040
+ boolInput("showSignal", "Signal Line", true, SETTINGS, "Plots an EMA of VFI as a signal line."),
43041
+ lengthInput(5, "signalLength", "Signal Length", 500, "Length of the signal-line EMA."),
43042
+ colorInput(BULLISH, "Bullish", "color", "Color of the VFI line and gradient fill above the zero line \u2014 the net accumulation regime."),
43043
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the VFI line and gradient fill below the zero line \u2014 the net distribution regime."),
43044
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
43045
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the zero line and the VFI line with a vertical gradient.")
41762
43046
  ],
41763
43047
  compute: (bars, inputs) => {
41764
43048
  const len = num3(inputs, "length", 130);
43049
+ const typical = sourceValues(bars, "HLC3");
43050
+ const logChange = sub(map(typical, Math.log), shift(map(typical, Math.log), 1));
43051
+ const volatility = stdev(logChange, num3(inputs, "volLength", 30));
41765
43052
  const coef = num3(inputs, "coef", 0.2);
41766
43053
  const volCoef = num3(inputs, "volCoef", 2.5);
41767
- const typical = map(sourceValues(bars, "HLC3"), Math.log);
41768
- const inter = change(typical);
41769
- const vInter = stdev(inter, 30);
41770
43054
  const vol = volumes(bars);
41771
- const vAve = sma(vol, len);
41772
- const mf = change(sourceValues(bars, "HLC3"));
41773
- const raw = bars.map((b, i) => {
41774
- const cutoff = Number.isFinite(vInter[i]) ? coef * vInter[i] * b.close : Number.NaN;
43055
+ const avgVolume = shift(sma(vol, len), 1);
43056
+ const typicalChange = change(typical);
43057
+ const signed3 = bars.map((b, i) => {
43058
+ const sd = volatility[i];
43059
+ const m = typicalChange[i];
41775
43060
  const v = vol[i];
41776
- const ave = i > 0 ? vAve[i - 1] : Number.NaN;
41777
- const m = mf[i];
41778
- if (!Number.isFinite(cutoff) || !Number.isFinite(v) || !Number.isFinite(ave) || !Number.isFinite(m)) return Number.NaN;
41779
- const capped = Math.min(v, ave * volCoef);
43061
+ if (!Number.isFinite(sd) || !Number.isFinite(m) || !Number.isFinite(v)) return Number.NaN;
43062
+ const cutoff = coef * sd * b.close;
43063
+ const avg = avgVolume[i];
43064
+ const capped = Number.isFinite(avg) ? Math.min(v, avg * volCoef) : v;
41780
43065
  return m > cutoff ? capped : m < -cutoff ? -capped : 0;
41781
43066
  });
41782
- const values = bars.map((_, i) => {
41783
- const ave = i > 0 ? vAve[i - 1] : Number.NaN;
41784
- return Number.isFinite(ave) && ave !== 0 ? ave : Number.NaN;
43067
+ const raw = zip(sum(signed3, len), avgVolume, (f, a) => a > 0 ? f / a : Number.NaN);
43068
+ const values = bool(inputs, "smooth", true) ? ema(raw, num3(inputs, "smoothLength", 3)) : raw;
43069
+ const bull = str3(inputs, "color", BULLISH);
43070
+ const extraPlots = bool(inputs, "showSignal", true) ? [{ key: "signal", title: "Signal Line", values: ema(values, num3(inputs, "signalLength", 5)), color: str3(inputs, "signalColor", WARNING) }] : [];
43071
+ return centeredOscillator({
43072
+ key: "vfi",
43073
+ title: "VFI",
43074
+ values,
43075
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "bearColor", BEARISH) },
43076
+ zeroInk: transp(NEUTRAL, 90),
43077
+ fill: bool(inputs, "gradientFill", true),
43078
+ extraPlots
41785
43079
  });
41786
- const flowSum = sum(raw, len);
41787
- const line = zip(flowSum, values, (f, a) => f / a);
41788
- return {
41789
- plots: [{ key: "vfi", title: "VFI", values: sma(line, num3(inputs, "smoothLength", 3)), color: SERIES_LINE, width: 2 }],
41790
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
41791
- };
41792
43080
  }
41793
43081
  };
43082
+ var II_OSCILLATOR = "Normalized Oscillator (II%)";
43083
+ var II_CUMULATIVE = "Cumulative Line";
41794
43084
  var intradayIntensity = {
41795
43085
  type: "intraday-intensity",
41796
43086
  title: "Intraday Intensity",
41797
43087
  shortTitle: "Intraday Intensity",
41798
43088
  overlay: false,
41799
- inputs: [lengthInput(21), colorInput()],
43089
+ inputs: [
43090
+ 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."),
43091
+ 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."),
43092
+ colorInput(BULLISH, "Bullish", "color", "Color of positive readings, the rising cumulative line and the fill above zero \u2014 the accumulation side."),
43093
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of negative readings, the falling cumulative line and the fill below zero \u2014 the distribution side."),
43094
+ 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.")
43095
+ ],
41800
43096
  compute: (bars, inputs) => {
43097
+ const intensity = moneyFlowVolume(bars);
43098
+ const bull = str3(inputs, "color", BULLISH);
43099
+ const bear = str3(inputs, "bearColor", BEARISH);
43100
+ if (str3(inputs, "mode", II_OSCILLATOR) === II_CUMULATIVE) {
43101
+ const values2 = cumSum(intensity);
43102
+ return { plots: [{ key: "ii", title: "Intraday Intensity", values: values2, color: bull, colors: slopeColors2(values2, bull, bear, bull) }] };
43103
+ }
41801
43104
  const len = num3(inputs, "length", 21);
41802
- const values = zip(sum(moneyFlowVolume(bars), len), sum(volumes(bars), len), (m, v) => v === 0 ? 0 : 100 * m / v);
41803
- return {
41804
- plots: [{ key: "ii", title: "II%", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
41805
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
41806
- };
43105
+ const values = zip(sum(intensity, len), sum(volumes(bars), len), (m, v) => v > 0 ? 100 * m / v : Number.NaN);
43106
+ return centeredOscillator({
43107
+ key: "ii",
43108
+ title: "Intraday Intensity",
43109
+ values,
43110
+ ink: { bull, bear, neutral: bear },
43111
+ zeroInk: transp(NEUTRAL, 90),
43112
+ fill: bool(inputs, "gradientFill", true)
43113
+ });
41807
43114
  }
41808
43115
  };
41809
43116
  var volumeSpecs = [
@@ -41836,15 +43143,24 @@ function periodKey(anchor, time) {
41836
43143
  }
41837
43144
  return Math.floor(time / DAY_MS2);
41838
43145
  }
43146
+ function barInterval(bars) {
43147
+ if (bars.length < 2) return 0;
43148
+ const gaps = [];
43149
+ for (let i = 1; i < bars.length; i++) gaps.push(bars[i].time - bars[i - 1].time);
43150
+ gaps.sort((a, b) => a - b);
43151
+ return gaps[Math.floor(gaps.length / 2)] ?? 0;
43152
+ }
41839
43153
  var parabolicSar = {
41840
43154
  type: "parabolic-sar",
41841
43155
  title: "Parabolic SAR",
41842
43156
  shortTitle: "SAR",
41843
43157
  overlay: true,
41844
43158
  inputs: [
41845
- { key: "start", title: "Start", type: "float", defval: 0.02, min: 1e-3, max: 1, step: 1e-3 },
41846
- { key: "increment", title: "Increment", type: "float", defval: 0.02, min: 1e-3, max: 1, step: 1e-3 },
41847
- { key: "maximum", title: "Max value", type: "float", defval: 0.2, min: 0.01, max: 1, step: 0.01 }
43159
+ floatInput("start", "Start", 0.02, 0, 1, 0.01, "Initial acceleration factor applied when a new trend begins."),
43160
+ floatInput("increment", "Increment", 0.02, 0, 1, 0.01, "Amount added to the acceleration factor each time the trend makes a new extreme point."),
43161
+ floatInput("maximum", "Maximum", 0.2, 0.01, 1, 0.01, "Upper cap on the acceleration factor."),
43162
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the SAR crosses while they trail below price \u2014 uptrend, the stop sits under the market."),
43163
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the SAR crosses while they sit above price \u2014 downtrend, the stop sits over the market.")
41848
43164
  ],
41849
43165
  compute: (bars, inputs) => {
41850
43166
  const start = num3(inputs, "start", 0.02);
@@ -41852,7 +43168,6 @@ var parabolicSar = {
41852
43168
  const max = num3(inputs, "maximum", 0.2);
41853
43169
  const n = bars.length;
41854
43170
  const values = new Array(n).fill(Number.NaN);
41855
- const colors = new Array(n).fill(null);
41856
43171
  if (n >= 2) {
41857
43172
  let long = bars[1].close >= bars[0].close;
41858
43173
  let sar = long ? bars[0].low : bars[0].high;
@@ -41877,18 +43192,21 @@ var parabolicSar = {
41877
43192
  af = Math.min(max, af + inc);
41878
43193
  }
41879
43194
  values[i] = sar;
41880
- colors[i] = long ? BULLISH : BEARISH;
41881
43195
  }
41882
43196
  }
41883
- return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
43197
+ const bull = str3(inputs, "bullColor", BULLISH);
43198
+ const bear = str3(inputs, "bearColor", BEARISH);
43199
+ const colors = values.map((x, i) => Number.isFinite(x) ? x < bars[i].close ? bull : bear : null);
43200
+ return { plots: [{ key: "sar", title: "SAR", values, kind: "cross", color: bull, colors }] };
41884
43201
  }
41885
43202
  };
41886
- var VWAP_BANDS_GROUP = "Bands";
41887
- var VWAP_STYLE = "Style";
43203
+ var VWAP_ANCHORS = ["Session", "Week", "Month", "Quarter", "Year"];
43204
+ var BANDS_STDEV = "Standard Deviation";
43205
+ var BANDS_PERCENT = "Percentage";
41888
43206
  var VWAP_BANDS = [
41889
- { on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
41890
- { on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
41891
- { on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
43207
+ { show: "band1", mult: "band1Mult", defMult: 1, defOn: true },
43208
+ { show: "band2", mult: "band2Mult", defMult: 2, defOn: false },
43209
+ { show: "band3", mult: "band3Mult", defMult: 3, defOn: false }
41892
43210
  ];
41893
43211
  var vwap = {
41894
43212
  type: "vwap",
@@ -41896,39 +43214,36 @@ var vwap = {
41896
43214
  shortTitle: "VWAP",
41897
43215
  overlay: true,
41898
43216
  inputs: [
41899
- { key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
41900
- sourceInput("HLC3"),
41901
- // Each band is one row: its toggle leads, the multiplier follows unlabeled.
43217
+ 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)."),
43218
+ sourceInput("HLC3", "source", "Source", "Price used in the volume weighted average. Typical price (HLC3) is the classic choice."),
43219
+ 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."),
41902
43220
  ...VWAP_BANDS.flatMap((b, i) => [
41903
- { key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
41904
- { key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
43221
+ boolInput(b.show, `Band #${i + 1}`, b.defOn, SETTINGS, `Show the ${["first", "second", "third"][i]} band pair.`),
43222
+ 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.")
41905
43223
  ]),
41906
- { ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
41907
- // One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
41908
- ...VWAP_BANDS.flatMap((b, i) => [
41909
- { ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
41910
- { key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
41911
- { ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
41912
- ])
43224
+ 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."),
43225
+ colorInput(BULLISH, "Bullish", "bullColor", "VWAP line color while price closes above it, and the color of the lower (support) bands."),
43226
+ colorInput(BEARISH, "Bearish", "bearColor", "VWAP line color while price closes below it, and the color of the upper (resistance) bands."),
43227
+ colorInput(NEUTRAL, "Bands", "bandsColor", "Deviation bands fill color."),
43228
+ boolInput("fill", "Bands Fill", true, STYLE, "Fill the area between each visible band pair.")
41913
43229
  ],
41914
43230
  compute: (bars, inputs) => {
41915
- const anchor = str3(inputs, "anchor", "Day");
43231
+ const anchor = str3(inputs, "anchor", "Session");
41916
43232
  const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
41917
43233
  const n = bars.length;
43234
+ const hidden = bool(inputs, "hideDwm", true) && barInterval(bars) >= DAY_MS2;
41918
43235
  const values = new Array(n).fill(Number.NaN);
41919
- const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
43236
+ const bands = VWAP_BANDS.filter((b) => bool(inputs, b.show, b.defOn)).map((b) => ({
41920
43237
  mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
41921
- color: str3(inputs, b.color, b.defColor),
41922
- fill: bool(inputs, b.fill, true),
41923
- fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
41924
43238
  up: new Array(n).fill(Number.NaN),
41925
43239
  down: new Array(n).fill(Number.NaN)
41926
43240
  }));
43241
+ const percentMode = str3(inputs, "bandsMode", BANDS_STDEV) === BANDS_PERCENT;
41927
43242
  let period = Number.NaN;
41928
43243
  let cumPV = 0;
41929
43244
  let cumPV2 = 0;
41930
43245
  let cumV = 0;
41931
- for (let i = 0; i < n; i++) {
43246
+ for (let i = 0; i < n && !hidden; i++) {
41932
43247
  const b = bars[i];
41933
43248
  const key = periodKey(anchor, b.time);
41934
43249
  if (key !== period) {
@@ -41946,38 +43261,58 @@ var vwap = {
41946
43261
  }
41947
43262
  if (cumV <= 0) continue;
41948
43263
  const mean = cumPV / cumV;
41949
- const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
43264
+ const offset = percentMode ? mean / 100 : Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
41950
43265
  values[i] = mean;
41951
43266
  for (const band of bands) {
41952
- band.up[i] = mean + band.mult * sd;
41953
- band.down[i] = mean - band.mult * sd;
43267
+ band.up[i] = mean + band.mult * offset;
43268
+ band.down[i] = mean - band.mult * offset;
41954
43269
  }
41955
43270
  }
41956
- const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
43271
+ const bull = str3(inputs, "bullColor", BULLISH);
43272
+ const bear = str3(inputs, "bearColor", BEARISH);
43273
+ const fillColor = transp(str3(inputs, "bandsColor", NEUTRAL), 95);
43274
+ const plots = [
43275
+ { key: "vwap", title: "VWAP", values, color: bull, colors: values.map((x, i) => Number.isFinite(x) ? bars[i].close >= x ? bull : bear : null) }
43276
+ ];
41957
43277
  const fills = [];
41958
43278
  bands.forEach((band, i) => {
41959
43279
  plots.push(
41960
- { key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
41961
- { key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
43280
+ { key: `up${i}`, title: `Upper Band #${i + 1}`, values: band.up, color: transp(bear, 40) },
43281
+ { key: `down${i}`, title: `Lower Band #${i + 1}`, values: band.down, color: transp(bull, 40) }
41962
43282
  );
41963
- if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
43283
+ if (bool(inputs, "fill", true)) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: fillColor });
41964
43284
  });
41965
43285
  return { plots, bands: fills };
41966
43286
  }
41967
43287
  };
43288
+ var PIVOT_TRADITIONAL = "Traditional";
43289
+ var PIVOT_FIBONACCI = "Fibonacci";
43290
+ var PIVOT_CAMARILLA = "Camarilla";
43291
+ var PIVOT_WOODIE = "Woodie";
43292
+ var PIVOT_AUTO = "Auto";
41968
43293
  var pivotPoints = {
41969
43294
  type: "pivot-points",
41970
- title: "Pivot Points",
43295
+ title: "Pivot Points Standard",
41971
43296
  shortTitle: "Pivots",
41972
43297
  overlay: true,
41973
- inputs: [{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month"] }],
43298
+ inputs: [
43299
+ 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."),
43300
+ 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."),
43301
+ colorInput(WARNING, "Pivot", "pivotColor", "Color of the central pivot (P) level."),
43302
+ colorInput(BEARISH, "Resistances", "resistanceColor", "Color of the resistance (R) levels."),
43303
+ colorInput(BULLISH, "Supports", "supportColor", "Color of the support (S) levels.")
43304
+ ],
41974
43305
  compute: (bars, inputs) => {
41975
- const anchor = str3(inputs, "anchor", "Day");
43306
+ const interval = barInterval(bars);
43307
+ const requested = str3(inputs, "anchor", PIVOT_AUTO);
43308
+ const auto = interval <= 15 * 6e4 ? "Session" : interval < DAY_MS2 ? "Week" : interval <= DAY_MS2 ? "Month" : "Year";
43309
+ const anchor = requested === "Daily" ? "Session" : requested === "Weekly" ? "Week" : requested === "Monthly" ? "Month" : requested === "Yearly" ? "Year" : auto;
43310
+ const kind = str3(inputs, "kind", PIVOT_TRADITIONAL);
41976
43311
  const aggs = /* @__PURE__ */ new Map();
41977
43312
  for (const b of bars) {
41978
43313
  const key = periodKey(anchor, b.time);
41979
43314
  const a = aggs.get(key);
41980
- if (!a) aggs.set(key, { high: b.high, low: b.low, close: b.close });
43315
+ if (!a) aggs.set(key, { open: b.open, high: b.high, low: b.low, close: b.close });
41981
43316
  else {
41982
43317
  a.high = Math.max(a.high, b.high);
41983
43318
  a.low = Math.min(a.low, b.low);
@@ -41986,31 +43321,61 @@ var pivotPoints = {
41986
43321
  }
41987
43322
  const n = bars.length;
41988
43323
  const mk = () => new Array(n).fill(Number.NaN);
41989
- const levels = { p: mk(), r1: mk(), s1: mk(), r2: mk(), s2: mk(), r3: mk(), s3: mk() };
43324
+ const levels = { p: mk(), r1: mk(), s1: mk(), r2: mk(), s2: mk(), r3: mk(), s3: mk(), r4: mk(), s4: mk() };
43325
+ let lastKey = Number.NaN;
41990
43326
  for (let i = 0; i < n; i++) {
41991
- const prev2 = aggs.get(periodKey(anchor, bars[i].time) - 1);
41992
- if (!prev2) continue;
41993
- const p = (prev2.high + prev2.low + prev2.close) / 3;
43327
+ const key = periodKey(anchor, bars[i].time);
43328
+ const fresh = key !== lastKey;
43329
+ lastKey = key;
43330
+ if (fresh) continue;
43331
+ const prev2 = aggs.get(key - 1);
43332
+ const curr = aggs.get(key);
43333
+ if (!prev2 || !curr) continue;
43334
+ const range = prev2.high - prev2.low;
43335
+ const traditional = (prev2.high + prev2.low + prev2.close) / 3;
43336
+ const p = kind === PIVOT_WOODIE ? (prev2.high + prev2.low + 2 * curr.open) / 4 : traditional;
41994
43337
  levels.p[i] = p;
41995
- levels.r1[i] = 2 * p - prev2.low;
41996
- levels.s1[i] = 2 * p - prev2.high;
41997
- levels.r2[i] = p + (prev2.high - prev2.low);
41998
- levels.s2[i] = p - (prev2.high - prev2.low);
41999
- levels.r3[i] = prev2.high + 2 * (p - prev2.low);
42000
- levels.s3[i] = prev2.low - 2 * (prev2.high - p);
42001
- }
42002
- const plot = (key, title, color) => ({ key, title, kind: "step", values: levels[key], color });
42003
- return {
42004
- plots: [
42005
- plot("p", "P", WARNING),
42006
- plot("r1", "R1", BEARISH),
42007
- plot("s1", "S1", BULLISH),
42008
- plot("r2", "R2", BEARISH),
42009
- plot("s2", "S2", BULLISH),
42010
- plot("r3", "R3", BEARISH),
42011
- plot("s3", "S3", BULLISH)
42012
- ]
42013
- };
43338
+ if (kind === PIVOT_FIBONACCI) {
43339
+ levels.r1[i] = traditional + 0.382 * range;
43340
+ levels.s1[i] = traditional - 0.382 * range;
43341
+ levels.r2[i] = traditional + 0.618 * range;
43342
+ levels.s2[i] = traditional - 0.618 * range;
43343
+ levels.r3[i] = traditional + range;
43344
+ levels.s3[i] = traditional - range;
43345
+ } else if (kind === PIVOT_CAMARILLA) {
43346
+ const cam = 1.1 * range;
43347
+ levels.r1[i] = prev2.close + cam / 12;
43348
+ levels.s1[i] = prev2.close - cam / 12;
43349
+ levels.r2[i] = prev2.close + cam / 6;
43350
+ levels.s2[i] = prev2.close - cam / 6;
43351
+ levels.r3[i] = prev2.close + cam / 4;
43352
+ levels.s3[i] = prev2.close - cam / 4;
43353
+ levels.r4[i] = prev2.close + cam / 2;
43354
+ levels.s4[i] = prev2.close - cam / 2;
43355
+ } else {
43356
+ levels.r1[i] = 2 * p - prev2.low;
43357
+ levels.s1[i] = 2 * p - prev2.high;
43358
+ levels.r2[i] = p + range;
43359
+ levels.s2[i] = p - range;
43360
+ if (kind !== PIVOT_WOODIE) {
43361
+ levels.r3[i] = prev2.high + 2 * (p - prev2.low);
43362
+ levels.s3[i] = prev2.low - 2 * (prev2.high - p);
43363
+ }
43364
+ }
43365
+ }
43366
+ const resistance = str3(inputs, "resistanceColor", BEARISH);
43367
+ const support = str3(inputs, "supportColor", BULLISH);
43368
+ const plot = (key, title, color) => ({ key, title, values: levels[key], color });
43369
+ const plots = [
43370
+ plot("p", "P", str3(inputs, "pivotColor", WARNING)),
43371
+ plot("r1", "R1", resistance),
43372
+ plot("s1", "S1", support),
43373
+ plot("r2", "R2", resistance),
43374
+ plot("s2", "S2", support)
43375
+ ];
43376
+ if (kind !== PIVOT_WOODIE) plots.push(plot("r3", "R3", resistance), plot("s3", "S3", support));
43377
+ if (kind === PIVOT_CAMARILLA) plots.push(plot("r4", "R4", resistance), plot("s4", "S4", support));
43378
+ return { plots };
42014
43379
  }
42015
43380
  };
42016
43381
  var fiftyTwoWeek = {
@@ -42018,15 +43383,25 @@ var fiftyTwoWeek = {
42018
43383
  title: "52 Week High/Low",
42019
43384
  shortTitle: "52W H/L",
42020
43385
  overlay: true,
42021
- inputs: [{ key: "weeks", title: "Weeks", type: "int", defval: 52, min: 1, max: 520, step: 1 }],
43386
+ inputs: [
43387
+ intInput("weeks", "Weeks", 52, 1, 520, "Length of the rolling window, in weeks."),
43388
+ 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."),
43389
+ colorInput(BULLISH, "52 Week High", "highColor", "Color of the 52 week high level."),
43390
+ colorInput(BEARISH, "52 Week Low", "lowColor", "Color of the 52 week low level."),
43391
+ colorInput(NEUTRAL, "All-Time High/Low", "allTimeColor", "Color of the all-time high and all-time low levels.")
43392
+ ],
42022
43393
  compute: (bars, inputs) => {
42023
43394
  const span = num3(inputs, "weeks", 52) * 7 * DAY_MS2;
42024
43395
  const n = bars.length;
42025
43396
  const hi = new Array(n).fill(Number.NaN);
42026
43397
  const lo = new Array(n).fill(Number.NaN);
43398
+ const ath = new Array(n).fill(Number.NaN);
43399
+ const atl = new Array(n).fill(Number.NaN);
42027
43400
  const maxIdx = [];
42028
43401
  const minIdx = [];
42029
43402
  let from = 0;
43403
+ let runHigh = -Infinity;
43404
+ let runLow = Infinity;
42030
43405
  for (let i = 0; i < n; i++) {
42031
43406
  const b = bars[i];
42032
43407
  while (maxIdx.length > 0 && bars[maxIdx[maxIdx.length - 1]].high <= b.high) maxIdx.pop();
@@ -42038,13 +43413,20 @@ var fiftyTwoWeek = {
42038
43413
  while (minIdx[0] < from) minIdx.shift();
42039
43414
  hi[i] = bars[maxIdx[0]].high;
42040
43415
  lo[i] = bars[minIdx[0]].low;
43416
+ runHigh = Math.max(runHigh, b.high);
43417
+ runLow = Math.min(runLow, b.low);
43418
+ ath[i] = runHigh;
43419
+ atl[i] = runLow;
43420
+ }
43421
+ const plots = [
43422
+ { key: "high", title: "52 Week High", values: hi, kind: "step", color: str3(inputs, "highColor", BULLISH), width: 2 },
43423
+ { key: "low", title: "52 Week Low", values: lo, kind: "step", color: str3(inputs, "lowColor", BEARISH), width: 2 }
43424
+ ];
43425
+ if (bool(inputs, "showAllTime", false)) {
43426
+ const ink = str3(inputs, "allTimeColor", NEUTRAL);
43427
+ 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 });
42041
43428
  }
42042
- return {
42043
- plots: [
42044
- { key: "high", title: "52W high", values: hi, kind: "step", color: BULLISH },
42045
- { key: "low", title: "52W low", values: lo, kind: "step", color: BEARISH }
42046
- ]
42047
- };
43429
+ return { plots };
42048
43430
  }
42049
43431
  };
42050
43432
  var zigzag = {
@@ -42052,9 +43434,11 @@ var zigzag = {
42052
43434
  title: "ZigZag",
42053
43435
  overlay: true,
42054
43436
  inputs: [
42055
- { key: "deviation", title: "Deviation %", type: "float", defval: 5, min: 0.01, max: 100, step: 0.1 },
42056
- { key: "depth", title: "Depth", type: "int", defval: 10, min: 1, max: 500, step: 1 },
42057
- colorInput()
43437
+ 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."),
43438
+ { ...intInput("depth", "Depth", 10, 2, 500, "Minimum number of bars required between two consecutive pivots.") },
43439
+ colorInput(BULLISH, "Up Leg", "bullColor", "Color of rising zigzag segments."),
43440
+ colorInput(BEARISH, "Down Leg", "bearColor", "Color of falling zigzag segments."),
43441
+ { key: "lineWidth", title: "Width", type: "int", defval: 2, min: 1, max: 10, step: 1, group: STYLE, tooltip: "Width of the zigzag segments." }
42058
43442
  ],
42059
43443
  compute: (bars, inputs) => {
42060
43444
  const dev = num3(inputs, "deviation", 5) / 100;
@@ -42067,14 +43451,14 @@ var zigzag = {
42067
43451
  const b = bars[i];
42068
43452
  if (up) {
42069
43453
  if (b.high >= ext.price) ext = { i, price: b.high };
42070
- else if (b.low <= ext.price * (1 - dev) && i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
43454
+ else if (b.low <= ext.price * (1 - dev) && ext.i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
42071
43455
  pivots.push(ext);
42072
43456
  up = false;
42073
43457
  ext = { i, price: b.low };
42074
43458
  }
42075
43459
  } else if (b.low <= ext.price) {
42076
43460
  ext = { i, price: b.low };
42077
- } else if (b.high >= ext.price * (1 + dev) && i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
43461
+ } else if (b.high >= ext.price * (1 + dev) && ext.i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
42078
43462
  pivots.push(ext);
42079
43463
  up = true;
42080
43464
  ext = { i, price: b.high };
@@ -42082,22 +43466,29 @@ var zigzag = {
42082
43466
  }
42083
43467
  pivots.push(ext);
42084
43468
  }
42085
- return {
42086
- plots: [],
42087
- polylines: [
42088
- {
42089
- key: "zigzag",
42090
- points: pivots.map((p) => ({ xloc: "bar_time", x: bars[p.i].time, price: p.price })),
42091
- curved: false,
42092
- closed: false,
42093
- lineColor: str3(inputs, "color", SERIES_LINE),
42094
- lineWidth: 2,
42095
- lineStyle: "solid",
42096
- arrowLeft: false,
42097
- arrowRight: false
42098
- }
42099
- ]
42100
- };
43469
+ const bull = str3(inputs, "bullColor", BULLISH);
43470
+ const bear = str3(inputs, "bearColor", BEARISH);
43471
+ const width = num3(inputs, "lineWidth", 2);
43472
+ const polylines = [];
43473
+ for (let k = 1; k < pivots.length; k++) {
43474
+ const a = pivots[k - 1];
43475
+ const b = pivots[k];
43476
+ polylines.push({
43477
+ key: `leg${k}`,
43478
+ points: [
43479
+ { xloc: "bar_time", x: bars[a.i].time, price: a.price },
43480
+ { xloc: "bar_time", x: bars[b.i].time, price: b.price }
43481
+ ],
43482
+ curved: false,
43483
+ closed: false,
43484
+ lineColor: b.price >= a.price ? bull : bear,
43485
+ lineWidth: width,
43486
+ lineStyle: "solid",
43487
+ arrowLeft: false,
43488
+ arrowRight: false
43489
+ });
43490
+ }
43491
+ return { plots: [], polylines };
42101
43492
  }
42102
43493
  };
42103
43494
  var williamsFractal = {
@@ -42105,10 +43496,16 @@ var williamsFractal = {
42105
43496
  title: "Williams Fractal",
42106
43497
  shortTitle: "Fractals",
42107
43498
  overlay: true,
42108
- inputs: [{ key: "periods", title: "Periods", type: "int", defval: 2, min: 1, max: 50, step: 1 }],
43499
+ inputs: [
43500
+ 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."),
43501
+ colorInput(BEARISH, "Up Fractal", "upColor", "Color of the up fractal marker (swing high), drawn above the fractal bar."),
43502
+ colorInput(BULLISH, "Down Fractal", "downColor", "Color of the down fractal marker (swing low), drawn below the fractal bar.")
43503
+ ],
42109
43504
  compute: (bars, inputs) => {
42110
43505
  const p = num3(inputs, "periods", 2);
42111
43506
  const markers = [];
43507
+ const up = str3(inputs, "upColor", BEARISH);
43508
+ const down = str3(inputs, "downColor", BULLISH);
42112
43509
  const isExtreme = (i, pick, better) => {
42113
43510
  const v = pick(bars[i]);
42114
43511
  for (let k = i - p; k <= i + p; k++) {
@@ -42118,12 +43515,8 @@ var williamsFractal = {
42118
43515
  return true;
42119
43516
  };
42120
43517
  for (let i = p; i < bars.length - p; i++) {
42121
- if (isExtreme(i, (b) => b.high, (a, b) => a >= b)) {
42122
- markers.push({ time: bars[i].time, position: "aboveBar", shape: "triangleup", color: BULLISH });
42123
- }
42124
- if (isExtreme(i, (b) => b.low, (a, b) => a <= b)) {
42125
- markers.push({ time: bars[i].time, position: "belowBar", shape: "triangledown", color: BEARISH });
42126
- }
43518
+ if (isExtreme(i, (b) => b.high, (a, b) => a > b)) markers.push({ time: bars[i].time, position: "aboveBar", shape: "triangleup", color: up });
43519
+ if (isExtreme(i, (b) => b.low, (a, b) => a < b)) markers.push({ time: bars[i].time, position: "belowBar", shape: "triangledown", color: down });
42127
43520
  }
42128
43521
  return { plots: [], markers };
42129
43522
  }
@@ -43824,6 +45217,8 @@ var ChartCell = class {
43824
45217
  this.activeRangeId = null;
43825
45218
  /** Latched verdict of {@link sessionAvailable} (async metadata, sticky per symbol). */
43826
45219
  this.sessionAvailableFlag = false;
45220
+ /** The symbol of the most recent metadata probe — an older one landing late is dropped. */
45221
+ this.metadataProbeSymbol = null;
43827
45222
  /** Latched: the symbol's extended tape wraps midnight (an overnight roll market) —
43828
45223
  * one extended-hours shading phase instead of the pre/post split. */
43829
45224
  this.sessionOvernightFlag = false;
@@ -43892,7 +45287,11 @@ var ChartCell = class {
43892
45287
  nativeBackend: deps.nativeBackend,
43893
45288
  // The user's drawings option minus its toolbar: one SHARED bar serves
43894
45289
  // the whole workspace (per-cell bars would cost a 44px gutter each).
43895
- drawings: cellDrawings(deps.chartDefaults.drawings)
45290
+ drawings: cellDrawings(deps.chartDefaults.drawings),
45291
+ // The renderer's own Time zone row edits a resolved IANA zone; the cell
45292
+ // contributes the workspace picker instead (`time-zone`, with the
45293
+ // exchange rule — see pushSettingsSections).
45294
+ settings: { ...deps.chartDefaults.settings, hidden: [...deps.chartDefaults.settings?.hidden ?? [], "symbol.timezone"] }
43896
45295
  },
43897
45296
  { dataFeed: deps.feed }
43898
45297
  );
@@ -43941,7 +45340,10 @@ var ChartCell = class {
43941
45340
  const list = providers.length > 0 ? providers.join(", ") : "none";
43942
45341
  this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
43943
45342
  });
43944
- this.inner.on("load:start", () => this.watermark?.setLoading(true));
45343
+ this.inner.on("load:start", ({ symbol: symbol2 }) => {
45344
+ this.watermark?.setLoading(true);
45345
+ this.refreshSymbolMetadata(symbol2);
45346
+ });
43945
45347
  this.inner.on("load:end", () => {
43946
45348
  this.watermark?.setLoading(false);
43947
45349
  this.refreshSessionShading();
@@ -44112,12 +45514,19 @@ var ChartCell = class {
44112
45514
  }
44113
45515
  /** Re-read the symbol's metadata: session posture (RTH/ETH toggle, shading) and its
44114
45516
  * trading zone (the exchange rule). Async — the workspace re-projects when a verdict lands. */
44115
- refreshSymbolMetadata() {
45517
+ refreshSymbolMetadata(symbol = this.state.symbol, retry = true) {
44116
45518
  const chart = this.inner;
44117
- const symbol = this.state.symbol;
44118
45519
  if (!chart || !symbol) return;
45520
+ this.metadataProbeSymbol = symbol;
44119
45521
  void chart.data.symbolInfo(symbol).then((si) => {
44120
- if (this.inner !== chart) return;
45522
+ if (this.inner !== chart || this.metadataProbeSymbol !== symbol) return;
45523
+ if (si === void 0 && retry) {
45524
+ void chart.data.ready().then(() => {
45525
+ if (this.inner === chart && this.metadataProbeSymbol === symbol) this.refreshSymbolMetadata(symbol, false);
45526
+ });
45527
+ return;
45528
+ }
45529
+ if (this.state.symbol !== symbol) return;
44121
45530
  const available = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
44122
45531
  const overnight = parseSessionSpec(si)?.overnight === true;
44123
45532
  const zone = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : void 0;
@@ -44232,6 +45641,24 @@ var ChartCell = class {
44232
45641
  }
44233
45642
  ]
44234
45643
  };
45644
+ const timezoneSection = {
45645
+ title: "Time zone",
45646
+ id: "time-zone",
45647
+ placement: "symbol",
45648
+ rows: [
45649
+ {
45650
+ kind: "select",
45651
+ label: "Time zone",
45652
+ id: "zone",
45653
+ options: timezoneMenuRows(this.deps.timezone()).map((r) => [r.value, r.label]),
45654
+ get: () => {
45655
+ const choice = this.deps.timezone();
45656
+ return isExchangeTimezone(choice) ? choice : normalizeTimezone(choice);
45657
+ },
45658
+ set: (v) => this.deps.setTimezone(v)
45659
+ }
45660
+ ]
45661
+ };
44235
45662
  const watermarkSection = {
44236
45663
  title: "Watermark",
44237
45664
  id: "watermark",
@@ -44300,7 +45727,7 @@ var ChartCell = class {
44300
45727
  ]
44301
45728
  });
44302
45729
  }
44303
- sections.push(advanced);
45730
+ sections.push(advanced, timezoneSection);
44304
45731
  if (this.sessionAvailableFlag) sections.push(sessionSection);
44305
45732
  sections.push(watermarkSection);
44306
45733
  chart.renderer.setSettingsSections(sections);