@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/index.cjs CHANGED
@@ -10229,6 +10229,8 @@ var MultiProviderFeed = class {
10229
10229
  this.liveBars = [];
10230
10230
  /** Sync-accessible symbol metadata, warmed by load()/symbolInfoFor (the engine reads it synchronously). */
10231
10231
  this.symInfoCache = /* @__PURE__ */ new Map();
10232
+ /** Requests in flight, keyed like the cache — every concurrent asker shares one round trip. */
10233
+ this.symInfoInflight = /* @__PURE__ */ new Map();
10232
10234
  this.cache = new CachingDataFeed(new RegistryFetchFeed(this.registry), store);
10233
10235
  }
10234
10236
  // ── Registry surface (driven by chart.data / DataControl) ──────────────
@@ -10289,9 +10291,36 @@ var MultiProviderFeed = class {
10289
10291
  async symbolInfoFor(raw) {
10290
10292
  const resolved = this.registry.resolve(raw, { default: this.primaryProvider });
10291
10293
  if (!resolved) return void 0;
10292
- const info = await this.registry.get(resolved.provider)?.getSymbolInfo?.(resolved.ticker);
10293
- if (info) this.symInfoCache.set(symKey(resolved), info);
10294
- return info;
10294
+ return this.fetchSymbolInfo(resolved);
10295
+ }
10296
+ /**
10297
+ * One round trip per symbol, shared. A market switch probes this metadata from five
10298
+ * places at once (the bar load's prefetch, the renderer's tick size, the market-status
10299
+ * badge, the session shading, the session toggle) — without the in-flight map each
10300
+ * would open its own request for the same answer, and the last one to land decided how
10301
+ * late the chrome settled.
10302
+ *
10303
+ * Cached for {@link SYMBOL_INFO_TTL_MS}: this metadata is stable within a session
10304
+ * (tick size, session vocabulary, listing prefix), but not immutable — a continuous
10305
+ * futures row rolls its current contract — so the entry expires rather than pinning the
10306
+ * session's first answer forever. A failed probe is NOT remembered: the next asker retries.
10307
+ */
10308
+ fetchSymbolInfo(resolved) {
10309
+ const key = symKey(resolved);
10310
+ const hit = this.symInfoCache.get(key);
10311
+ if (hit && Date.now() - hit.at < SYMBOL_INFO_TTL_MS) return Promise.resolve(hit.info);
10312
+ const inflight = this.symInfoInflight.get(key);
10313
+ if (inflight) return inflight;
10314
+ const getSymbolInfo = this.registry.get(resolved.provider)?.getSymbolInfo;
10315
+ if (!getSymbolInfo) return Promise.resolve(void 0);
10316
+ const p = Promise.resolve(getSymbolInfo.call(this.registry.get(resolved.provider), resolved.ticker)).then((info) => {
10317
+ if (info) this.symInfoCache.set(key, { info, at: Date.now() });
10318
+ return info;
10319
+ }).finally(() => {
10320
+ this.symInfoInflight.delete(key);
10321
+ });
10322
+ this.symInfoInflight.set(key, p);
10323
+ return p;
10295
10324
  }
10296
10325
  /**
10297
10326
  * Per-symbol capabilities, resolved through the owning provider's `capabilitiesFor`
@@ -10335,16 +10364,10 @@ var MultiProviderFeed = class {
10335
10364
  symbolInfo(cfg) {
10336
10365
  if (cfg.data && cfg.data.length > 0) return void 0;
10337
10366
  const resolved = this.registry.resolve(rawSymbol(cfg), { default: this.primaryProvider });
10338
- return resolved ? this.symInfoCache.get(symKey(resolved)) : void 0;
10367
+ return resolved ? this.symInfoCache.get(symKey(resolved))?.info : void 0;
10339
10368
  }
10340
10369
  prefetchSymbolInfo(resolved) {
10341
- const key = symKey(resolved);
10342
- if (this.symInfoCache.has(key)) return;
10343
- const provider = this.registry.get(resolved.provider);
10344
- if (!provider?.getSymbolInfo) return;
10345
- void provider.getSymbolInfo(resolved.ticker).then((info) => {
10346
- if (info) this.symInfoCache.set(key, info);
10347
- }).catch(() => {
10370
+ void this.fetchSymbolInfo(resolved).catch(() => {
10348
10371
  });
10349
10372
  }
10350
10373
  async loadRange(cfg, range) {
@@ -10394,8 +10417,9 @@ function canonical(cfg, resolved) {
10394
10417
  return { ...cfg, symbol: `${resolved.provider}:${resolved.ticker}` };
10395
10418
  }
10396
10419
  function symKey(resolved) {
10397
- return `${resolved.provider}|${resolved.ticker}`;
10420
+ return `${resolved.provider}|${resolved.ticker.trim().toUpperCase()}`;
10398
10421
  }
10422
+ var SYMBOL_INFO_TTL_MS = 10 * 6e4;
10399
10423
  async function safeBars(provider, ticker, tf, range) {
10400
10424
  try {
10401
10425
  return await provider.getBars(ticker, tf, range);
@@ -22181,7 +22205,7 @@ var SettingsDialog = class {
22181
22205
  if (hr.kind === "heading") body.append(this.sectionTitle(hr.label));
22182
22206
  else if (hr.kind === "toggle") body.append(this.boolRow(hr.label, hr.get(), (v) => hr.set(v)));
22183
22207
  else if (hr.kind === "color") body.append(this.colorRow(hr.label, hr.get(), (v) => hr.set(v)));
22184
- else body.append(this.selectRowLabeled(hr.label, hr.get(), hr.options.map((o) => [o, o]), (v) => hr.set(v)));
22208
+ else body.append(this.selectRowLabeled(hr.label, hr.get(), normalizeSelectOptions(hr.options), (v) => hr.set(v)));
22185
22209
  }
22186
22210
  }
22187
22211
  };
@@ -33450,7 +33474,8 @@ var ClassicIndicator = class {
33450
33474
  lineStyle: plot.lineStyle ?? "solid",
33451
33475
  ...plot.base != null ? { base: plot.base } : {}
33452
33476
  },
33453
- ...plot.overlay ? { overlay: true } : {}
33477
+ ...plot.overlay ? { overlay: true } : {},
33478
+ ...plot.display ? { display: plot.display } : {}
33454
33479
  });
33455
33480
  });
33456
33481
  if (out.markers && out.markers.length > 0) {
@@ -33472,7 +33497,9 @@ var ClassicIndicator = class {
33472
33497
  paneId: "",
33473
33498
  fromSeriesId: from,
33474
33499
  toSeriesId: to,
33475
- color: band.color
33500
+ color: band.color,
33501
+ ...band.colors ? { colors: [...band.colors] } : {},
33502
+ ...band.gradient ? { gradient: [...band.gradient] } : {}
33476
33503
  }
33477
33504
  ];
33478
33505
  });
@@ -33630,6 +33657,16 @@ function stdev(v, len) {
33630
33657
  return Math.sqrt(sq / len);
33631
33658
  });
33632
33659
  }
33660
+ function meanDev(v, len) {
33661
+ return windowed(v, len, (w, from, to) => {
33662
+ let s = 0;
33663
+ for (let k = from; k <= to; k++) s += w[k];
33664
+ const mean = s / len;
33665
+ let dev = 0;
33666
+ for (let k = from; k <= to; k++) dev += Math.abs(w[k] - mean);
33667
+ return dev / len;
33668
+ });
33669
+ }
33633
33670
  function highest(v, len) {
33634
33671
  return windowed(v, len, (w, from, to) => {
33635
33672
  let m = -Infinity;
@@ -33761,7 +33798,7 @@ function stoch(v, hi, lo, len) {
33761
33798
  }
33762
33799
  return out;
33763
33800
  }
33764
- function linreg(v, len) {
33801
+ function linreg(v, len, offset = 0) {
33765
33802
  const sx = (len - 1) * len / 2;
33766
33803
  const sxx = (len - 1) * len * (2 * len - 1) / 6;
33767
33804
  const denom = len * sxx - sx * sx;
@@ -33775,9 +33812,101 @@ function linreg(v, len) {
33775
33812
  }
33776
33813
  const slope = denom === 0 ? 0 : (len * sxy - sx * sy) / denom;
33777
33814
  const intercept = (sy - slope * sx) / len;
33778
- return intercept + slope * (len - 1);
33815
+ return intercept + slope * (len - 1 - offset);
33816
+ });
33817
+ }
33818
+ function hma(v, len) {
33819
+ const n = Math.max(2, Math.trunc(len));
33820
+ const raw = sub(map(wma(v, Math.max(1, Math.trunc(n / 2))), (x) => 2 * x), wma(v, n));
33821
+ return wma(raw, Math.max(1, Math.round(Math.sqrt(n))));
33822
+ }
33823
+ function alma(v, len, offset, sigma) {
33824
+ const m = offset * (len - 1);
33825
+ const s = len / Math.max(sigma, 1e-9);
33826
+ const weights = new Array(len);
33827
+ let norm = 0;
33828
+ for (let i = 0; i < len; i++) {
33829
+ const d = i - m;
33830
+ const w = Math.exp(-(d * d) / (2 * s * s));
33831
+ weights[i] = w;
33832
+ norm += w;
33833
+ }
33834
+ return windowed(v, len, (win, from, to) => {
33835
+ let acc = 0;
33836
+ for (let k = from; k <= to; k++) acc += win[k] * weights[k - from];
33837
+ return norm === 0 ? Number.NaN : acc / norm;
33838
+ });
33839
+ }
33840
+ function dema(v, len) {
33841
+ const e1 = ema(v, len);
33842
+ return sub(map(e1, (x) => 2 * x), ema(e1, len));
33843
+ }
33844
+ function tema(v, len) {
33845
+ const e1 = ema(v, len);
33846
+ const e2 = ema(e1, len);
33847
+ const e3 = ema(e2, len);
33848
+ return zip(zip(map(e1, (x) => 3 * x), map(e2, (x) => 3 * x), (a, b) => a - b), e3, (a, b) => a + b);
33849
+ }
33850
+ function kama(v, len, fastAlpha = 2 / 3, slowAlpha = 2 / 31) {
33851
+ const direction = map(change(v, len), Math.abs);
33852
+ const volatility = sum(map(change(v), Math.abs), len);
33853
+ const out = new Array(v.length).fill(Number.NaN);
33854
+ let prev2 = Number.NaN;
33855
+ for (let i = 0; i < v.length; i++) {
33856
+ const x = v[i];
33857
+ if (!Number.isFinite(x)) continue;
33858
+ const d = direction[i];
33859
+ const w = volatility[i];
33860
+ if (!Number.isFinite(d) || !Number.isFinite(w)) {
33861
+ prev2 = Number.isFinite(prev2) ? prev2 : x;
33862
+ out[i] = prev2;
33863
+ continue;
33864
+ }
33865
+ const er = w === 0 ? 0 : d / w;
33866
+ const sc = (er * (fastAlpha - slowAlpha) + slowAlpha) ** 2;
33867
+ prev2 = Number.isFinite(prev2) ? prev2 + sc * (x - prev2) : x;
33868
+ out[i] = prev2;
33869
+ }
33870
+ return out;
33871
+ }
33872
+ function mcginley(v, len) {
33873
+ const out = new Array(v.length).fill(Number.NaN);
33874
+ let prev2 = Number.NaN;
33875
+ for (let i = 0; i < v.length; i++) {
33876
+ const x = v[i];
33877
+ if (!Number.isFinite(x)) continue;
33878
+ if (!Number.isFinite(prev2)) {
33879
+ prev2 = x;
33880
+ } else {
33881
+ const ratio = prev2 === 0 ? 0 : x / prev2;
33882
+ const denom = len * ratio ** 4;
33883
+ prev2 += denom === 0 ? 0 : (x - prev2) / denom;
33884
+ }
33885
+ out[i] = prev2;
33886
+ }
33887
+ return out;
33888
+ }
33889
+ function hamming(v, len, a0 = 0.54, a1 = 0.46) {
33890
+ if (len <= 1) return v.map((x) => Number.isFinite(x) ? x : Number.NaN);
33891
+ const weights = new Array(len);
33892
+ let norm = 0;
33893
+ for (let i = 0; i < len; i++) {
33894
+ const w = a0 - a1 * Math.cos(2 * Math.PI * i / (len - 1));
33895
+ weights[i] = w;
33896
+ norm += w;
33897
+ }
33898
+ return windowed(v, len, (win, from, to) => {
33899
+ let acc = 0;
33900
+ for (let k = from; k <= to; k++) acc += win[k] * weights[to - k];
33901
+ return norm === 0 ? Number.NaN : acc / norm;
33779
33902
  });
33780
33903
  }
33904
+ function cmo(v, len) {
33905
+ const d = change(v);
33906
+ const up = sum(map(d, (x) => Math.max(x, 0)), len);
33907
+ const dn = sum(map(d, (x) => Math.max(-x, 0)), len);
33908
+ return zip(up, dn, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
33909
+ }
33781
33910
  function swma(v) {
33782
33911
  const out = new Array(v.length).fill(Number.NaN);
33783
33912
  for (let i = 3; i < v.length; i++) {
@@ -33827,38 +33956,253 @@ function cumSum(v) {
33827
33956
  }
33828
33957
 
33829
33958
  // src/core/native-indicators/classics/shared.ts
33830
- function lengthInput(defval, key = "length", title = "Length", max = 5e3) {
33831
- return { key, title, type: "int", defval, min: 1, max, step: 1 };
33959
+ var SETTINGS = "Settings";
33960
+ var STYLE = "Style";
33961
+ function lengthInput(defval, key = "length", title = "Length", max = 5e3, tooltip) {
33962
+ return { key, title, type: "int", defval, min: 1, max, step: 1, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
33963
+ }
33964
+ function intInput(key, title, defval, min = 1, max = 5e3, tooltip) {
33965
+ return { key, title, type: "int", defval, min, max, step: 1, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
33966
+ }
33967
+ function floatInput(key, title, defval, min = 0, max = 1e3, step = 0.1, tooltip) {
33968
+ return { key, title, type: "float", defval, min, max, step, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
33832
33969
  }
33833
- function sourceInput(defval = "Close") {
33834
- return { key: "source", title: "Source", type: "string", defval, options: SOURCES2 };
33970
+ function boolInput(key, title, defval, group = SETTINGS, tooltip) {
33971
+ return { key, title, type: "bool", defval, group, ...tooltip != null ? { tooltip } : {} };
33835
33972
  }
33836
- function colorInput(defval = SERIES_LINE, title = "Color", key = "color") {
33837
- return { key, title, type: "color", defval };
33973
+ function optionInput(key, title, defval, options, tooltip) {
33974
+ return { key, title, type: "string", defval, options, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
33975
+ }
33976
+ function sourceInput(defval = "Close", key = "source", title = "Source", tooltip) {
33977
+ return { key, title, type: "string", defval, options: SOURCES2, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
33978
+ }
33979
+ function colorInput(defval = INFO, title = "Color", key = "color", tooltip) {
33980
+ return { key, title, type: "color", defval, group: STYLE, ...tooltip != null ? { tooltip } : {} };
33981
+ }
33982
+ function widthInput(defval = 2, key = "lineWidth", title = "Line Width") {
33983
+ return { key, title, type: "int", defval, min: 1, max: 5, step: 1, group: STYLE, tooltip: "Width of the plotted line, in pixels." };
33984
+ }
33985
+ function transp(hex, transparency) {
33986
+ return withAlpha2(hex, (100 - transparency) / 100);
33838
33987
  }
33839
33988
  function withAlpha2(hex, alpha = 0.08) {
33840
33989
  const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
33841
33990
  if (!m) return hex;
33842
- const a = Math.round(alpha * 255).toString(16).padStart(2, "0");
33991
+ const a = Math.round(Math.max(0, Math.min(1, alpha)) * 255).toString(16).padStart(2, "0");
33843
33992
  return `#${m[1]}${a}`;
33844
33993
  }
33994
+ function channelStyleInputs(basisTitle = "Basis") {
33995
+ return [
33996
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the lower band and of the basis while price holds above it."),
33997
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the upper band and of the basis while price sits below it."),
33998
+ colorInput(NEUTRAL, basisTitle, "basisColor", "Color of the basis line while price sits exactly on it.")
33999
+ ];
34000
+ }
34001
+ function directionalChannel(bars, basis, upper, lower, ink, key) {
34002
+ const basisColors = basis.map((b, i) => {
34003
+ if (!Number.isFinite(b)) return null;
34004
+ const c = bars[i].close;
34005
+ return c > b ? ink.bull : c < b ? ink.bear : ink.neutral;
34006
+ });
34007
+ const wash = upper.map((u, i) => {
34008
+ const l = lower[i];
34009
+ if (!Number.isFinite(u) || !Number.isFinite(l)) return null;
34010
+ return { topValue: u, bottomValue: l, topColor: transp(ink.bear, 90), bottomColor: transp(ink.bull, 90) };
34011
+ });
34012
+ return {
34013
+ plots: [
34014
+ { key: "basis", title: "Basis", values: [...basis], color: ink.neutral, colors: basisColors },
34015
+ { key: "upper", title: "Upper", values: [...upper], color: transp(ink.bear, 30) },
34016
+ { key: "lower", title: "Lower", values: [...lower], color: transp(ink.bull, 30) }
34017
+ ],
34018
+ bands: [{ key, from: "upper", to: "lower", color: transp(NEUTRAL, 90), gradient: wash }]
34019
+ };
34020
+ }
34021
+ function anchorPlot(key, title, length, level) {
34022
+ return {
34023
+ key,
34024
+ title,
34025
+ values: new Array(length).fill(level),
34026
+ color: transp(NEUTRAL, 100),
34027
+ display: { pane: false, legend: false, dataWindow: false }
34028
+ };
34029
+ }
34030
+ function thresholdZones(length, levels, ink, transparency = 92) {
34031
+ return {
34032
+ plots: [
34033
+ anchorPlot("obAnchor", "Overbought Anchor", length, levels.overbought),
34034
+ anchorPlot("osAnchor", "Oversold Anchor", length, levels.oversold),
34035
+ anchorPlot("topAnchor", "Top Anchor", length, levels.top),
34036
+ anchorPlot("bottomAnchor", "Bottom Anchor", length, levels.bottom)
34037
+ ],
34038
+ bands: [
34039
+ { key: "obZone", from: "obAnchor", to: "topAnchor", color: transp(ink.overbought, transparency) },
34040
+ { key: "osZone", from: "osAnchor", to: "bottomAnchor", color: transp(ink.oversold, transparency) }
34041
+ ]
34042
+ };
34043
+ }
34044
+ function rampColors(values, levels, ink) {
34045
+ return values.map((x) => {
34046
+ if (!Number.isFinite(x)) return null;
34047
+ return x <= levels.mid ? gradient(x, levels.low, levels.mid, ink.low, ink.mid) : gradient(x, levels.mid, levels.high, ink.mid, ink.high);
34048
+ });
34049
+ }
34050
+ function baselineGradient(values, baseline, inkOf, valueTransparency = 50, baseTransparency = 100) {
34051
+ return values.map((x, i) => {
34052
+ if (!Number.isFinite(x)) return null;
34053
+ const ink = inkOf(x, i);
34054
+ const above = x > baseline;
34055
+ return {
34056
+ topValue: Math.max(x, baseline),
34057
+ bottomValue: Math.min(x, baseline),
34058
+ topColor: transp(ink, above ? valueTransparency : baseTransparency),
34059
+ bottomColor: transp(ink, above ? baseTransparency : valueTransparency)
34060
+ };
34061
+ });
34062
+ }
34063
+ function momentumColumnColors(values, ink) {
34064
+ return values.map((x, i) => {
34065
+ if (!Number.isFinite(x)) return null;
34066
+ const prev2 = i > 0 && Number.isFinite(values[i - 1]) ? values[i - 1] : x;
34067
+ const rising = x > prev2;
34068
+ if (x >= 0) return rising ? ink.growAbove : ink.fallAbove;
34069
+ return rising ? ink.growBelow : ink.fallBelow;
34070
+ });
34071
+ }
34072
+ function boundedOscillator(opts) {
34073
+ const mid = (opts.overbought + opts.oversold) / 2;
34074
+ const ramp = rampColors(opts.values, { low: opts.oversold, mid, high: opts.overbought }, opts.ink);
34075
+ const zones = thresholdZones(
34076
+ opts.values.length,
34077
+ { overbought: opts.overbought, oversold: opts.oversold, top: opts.scale.top, bottom: opts.scale.bottom },
34078
+ { overbought: opts.ink.high, oversold: opts.ink.low },
34079
+ opts.zoneTransparency
34080
+ );
34081
+ const plots = [{ key: opts.key, title: opts.title, values: opts.values, color: opts.ink.mid, width: opts.width ?? 1, colors: ramp }, ...zones.plots];
34082
+ const bands = [...zones.bands];
34083
+ if (opts.bandFill != null) bands.push({ key: "band", from: "obAnchor", to: "osAnchor", color: opts.bandFill });
34084
+ if (opts.midpointWash) {
34085
+ plots.push(anchorPlot("midAnchor", "Midpoint Anchor", opts.values.length, mid));
34086
+ 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) });
34087
+ }
34088
+ const levels = [
34089
+ { key: "overbought", price: opts.overbought, color: opts.levelInk, lineStyle: "dashed", title: opts.levelTitles?.overbought ?? "Overbought" },
34090
+ { key: "oversold", price: opts.oversold, color: opts.levelInk, lineStyle: "dashed", title: opts.levelTitles?.oversold ?? "Oversold" }
34091
+ ];
34092
+ if (opts.midline != null) levels.splice(1, 0, { key: "midline", price: opts.midline, color: opts.levelInk, lineStyle: "dotted", title: "Midline" });
34093
+ return { plots, bands, levels };
34094
+ }
34095
+ function centeredOscillator(opts) {
34096
+ const baseline = opts.baseline ?? 0;
34097
+ const inkOf = (x) => x > baseline ? opts.ink.bull : x < baseline ? opts.ink.bear : opts.ink.neutral;
34098
+ const plots = [
34099
+ { 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) },
34100
+ ...opts.extraPlots ?? []
34101
+ ];
34102
+ const bands = [];
34103
+ if (opts.fill !== false) {
34104
+ plots.push(anchorPlot("baseline", "Baseline Anchor", opts.values.length, baseline));
34105
+ bands.push({ key: "wash", from: opts.key, to: "baseline", color: transp(NEUTRAL, 100), gradient: baselineGradient(opts.values, baseline, inkOf, opts.valueTransparency) });
34106
+ }
34107
+ return {
34108
+ plots,
34109
+ bands,
34110
+ levels: [{ key: "zero", price: baseline, color: opts.zeroInk, lineStyle: opts.zeroLineStyle ?? "dashed", title: opts.zeroTitle ?? "Zero Line" }, ...opts.extraLevels ?? []]
34111
+ };
34112
+ }
34113
+ function channels(hex) {
34114
+ const m = /^#([0-9a-f]{6})/i.exec(hex.trim());
34115
+ if (!m) return null;
34116
+ const n = parseInt(m[1], 16);
34117
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
34118
+ }
34119
+ function gradient(value, low, high, from, to) {
34120
+ const a = channels(from);
34121
+ const b = channels(to);
34122
+ if (!a || !b || !Number.isFinite(value)) return from;
34123
+ const span = high - low;
34124
+ const t = span === 0 ? 0 : Math.max(0, Math.min(1, (value - low) / span));
34125
+ const mix2 = (i) => Math.round(a[i] + (b[i] - a[i]) * t).toString(16).padStart(2, "0");
34126
+ return `#${mix2(0)}${mix2(1)}${mix2(2)}`;
34127
+ }
33845
34128
 
33846
34129
  // src/core/native-indicators/classics/averages.ts
33847
- var MA_KINDS = ["SMA", "EMA", "WMA", "RMA", "VWMA"];
33848
- var offsetInput = { key: "offset", title: "Offset", type: "int", defval: 0, min: -500, max: 500, step: 1 };
34130
+ var MA_TYPES = ["SMA", "EMA", "WMA", "RMA (SMMA)", "HMA", "ALMA", "VWMA", "DEMA", "TEMA", "KAMA", "LSMA", "McGinley", "Hamming"];
34131
+ var ENVELOPE_MA_TYPES = ["SMA", "EMA", "WMA", "RMA (SMMA)", "HMA", "VWMA"];
34132
+ function movingAverageOf(kind, src, bars, len, opts = {}) {
34133
+ switch (kind) {
34134
+ case "EMA":
34135
+ return ema(src, len);
34136
+ case "WMA":
34137
+ return wma(src, len);
34138
+ case "RMA":
34139
+ case "RMA (SMMA)":
34140
+ return rma(src, len);
34141
+ case "HMA":
34142
+ return hma(src, len);
34143
+ case "ALMA":
34144
+ return alma(src, len, opts.almaOffset ?? 0.85, opts.almaSigma ?? 6);
34145
+ case "VWMA":
34146
+ return vwma(src, volumes(bars), len);
34147
+ case "DEMA":
34148
+ return dema(src, len);
34149
+ case "TEMA":
34150
+ return tema(src, len);
34151
+ case "KAMA":
34152
+ return kama(src, len);
34153
+ case "LSMA":
34154
+ return linreg(src, len, opts.lsmaOffset ?? 0);
34155
+ case "McGinley":
34156
+ return mcginley(src, len);
34157
+ case "Hamming":
34158
+ return hamming(src, len);
34159
+ default:
34160
+ return sma(src, len);
34161
+ }
34162
+ }
34163
+ function slopeColors(values, bull, bear, flat) {
34164
+ return values.map((x, i) => {
34165
+ if (!Number.isFinite(x)) return null;
34166
+ const prev2 = i > 0 ? values[i - 1] : Number.NaN;
34167
+ if (!Number.isFinite(prev2)) return flat;
34168
+ return x > prev2 ? bull : x < prev2 ? bear : flat;
34169
+ });
34170
+ }
34171
+ function directionInputs() {
34172
+ return [
34173
+ 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."),
34174
+ colorInput(BULLISH, "Bullish", "bullColor"),
34175
+ colorInput(BEARISH, "Bearish", "bearColor"),
34176
+ colorInput(NEUTRAL, "Neutral", "neutralColor"),
34177
+ widthInput()
34178
+ ];
34179
+ }
34180
+ function slopeColoredPlot(key, title, values, inputs) {
34181
+ const neutral = str3(inputs, "neutralColor", NEUTRAL);
34182
+ const colored = bool(inputs, "trendColor", true);
34183
+ return {
34184
+ key,
34185
+ title,
34186
+ values,
34187
+ color: neutral,
34188
+ width: num3(inputs, "lineWidth", 2),
34189
+ ...colored ? { colors: slopeColors(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), neutral) } : {}
34190
+ };
34191
+ }
34192
+ 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." };
33849
34193
  function fixedAverage(type, title, shortTitle, smooth) {
33850
34194
  return {
33851
34195
  type,
33852
34196
  title,
33853
34197
  shortTitle,
33854
34198
  overlay: true,
33855
- inputs: [lengthInput(20), sourceInput(), offsetInput, colorInput()],
34199
+ 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()],
33856
34200
  compute: (bars, inputs) => {
33857
34201
  const len = num3(inputs, "length", 20);
33858
34202
  let values = smooth(sourceValues(bars, str3(inputs, "source", "Close")), len);
33859
34203
  const offset = Math.trunc(num3(inputs, "offset", 0));
33860
34204
  if (offset !== 0) values = shift(values, offset);
33861
- return { plots: [{ key: shortTitle.toLowerCase(), title: shortTitle, values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
34205
+ return { plots: [{ key: shortTitle.toLowerCase(), title: shortTitle, values, color: str3(inputs, "color", INFO), width: 2 }] };
33862
34206
  }
33863
34207
  };
33864
34208
  }
@@ -33870,36 +34214,37 @@ var movingAverage = {
33870
34214
  shortTitle: "MA",
33871
34215
  overlay: true,
33872
34216
  inputs: [
33873
- { key: "maType", title: "Type", type: "string", defval: "SMA", options: MA_KINDS },
33874
- lengthInput(20),
33875
- sourceInput(),
34217
+ optionInput("maType", "MA 1 Type", "SMA", MA_TYPES, "Moving average calculation used for MA 1."),
34218
+ lengthInput(20, "length", "MA 1 Length", 5e3, "Number of bars used in the MA 1 calculation."),
34219
+ sourceInput("Close", "source", "MA 1 Source", "Price series MA 1 is calculated on."),
34220
+ boolInput("showMa2", "Show MA 2", false, SETTINGS, "Displays the optional second moving average."),
34221
+ optionInput("ma2Type", "MA 2 Type", "SMA", MA_TYPES, "Moving average calculation used for MA 2."),
34222
+ lengthInput(50, "ma2Length", "MA 2 Length", 5e3, "Number of bars used in the MA 2 calculation."),
34223
+ sourceInput("Close", "ma2Source", "MA 2 Source", "Price series MA 2 is calculated on."),
34224
+ 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."),
34225
+ floatInput("almaSigma", "ALMA Sigma", 6, 0.1, 100, 0.5, "ALMA type only. Larger values widen the Gaussian window for a smoother average."),
34226
+ intInput("lsmaOffset", "LSMA Offset", 0, 0, 500, "LSMA type only. Bar offset applied to the linear regression value."),
33876
34227
  offsetInput,
33877
- colorInput()
34228
+ colorInput(INFO, "MA 1", "color", "MA 1 line color."),
34229
+ colorInput(WARNING, "MA 2", "ma2Color", "MA 2 line color.")
33878
34230
  ],
33879
34231
  compute: (bars, inputs) => {
33880
- const len = num3(inputs, "length", 20);
33881
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
34232
+ const opts = {
34233
+ almaOffset: num3(inputs, "almaOffset", 0.85),
34234
+ almaSigma: num3(inputs, "almaSigma", 6),
34235
+ lsmaOffset: Math.trunc(num3(inputs, "lsmaOffset", 0))
34236
+ };
34237
+ const offset = Math.trunc(num3(inputs, "offset", 0));
34238
+ const displace = (v) => offset === 0 ? v : shift(v, offset);
33882
34239
  const kind = str3(inputs, "maType", "SMA");
33883
- let values;
33884
- switch (kind) {
33885
- case "EMA":
33886
- values = ema(src, len);
33887
- break;
33888
- case "WMA":
33889
- values = wma(src, len);
33890
- break;
33891
- case "RMA":
33892
- values = rma(src, len);
33893
- break;
33894
- case "VWMA":
33895
- values = vwma(src, volumes(bars), len);
33896
- break;
33897
- default:
33898
- values = sma(src, len);
34240
+ const ma1 = displace(movingAverageOf(kind, sourceValues(bars, str3(inputs, "source", "Close")), bars, num3(inputs, "length", 20), opts));
34241
+ const plots = [{ key: "ma", title: kind, values: ma1, color: str3(inputs, "color", INFO), width: 2 }];
34242
+ if (bool(inputs, "showMa2", false)) {
34243
+ const kind2 = str3(inputs, "ma2Type", "SMA");
34244
+ const ma2 = displace(movingAverageOf(kind2, sourceValues(bars, str3(inputs, "ma2Source", "Close")), bars, num3(inputs, "ma2Length", 50), opts));
34245
+ plots.push({ key: "ma2", title: `${kind2} 2`, values: ma2, color: str3(inputs, "ma2Color", WARNING), width: 2 });
33899
34246
  }
33900
- const offset = Math.trunc(num3(inputs, "offset", 0));
33901
- if (offset !== 0) values = shift(values, offset);
33902
- return { plots: [{ key: "ma", title: kind, values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
34247
+ return { plots };
33903
34248
  }
33904
34249
  };
33905
34250
  var smoothedMa = {
@@ -33907,17 +34252,13 @@ var smoothedMa = {
33907
34252
  title: "Smoothed Moving Average",
33908
34253
  shortTitle: "RMA",
33909
34254
  overlay: true,
33910
- inputs: [lengthInput(14), sourceInput(), colorInput()],
34255
+ inputs: [
34256
+ sourceInput("Close", "source", "Source", "Price series the average smooths. Wilder computed his 1978 indicators on the close, but the recursion accepts any source."),
34257
+ 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."),
34258
+ ...directionInputs()
34259
+ ],
33911
34260
  compute: (bars, inputs) => ({
33912
- plots: [
33913
- {
33914
- key: "rma",
33915
- title: "RMA",
33916
- values: rma(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
33917
- color: str3(inputs, "color", SERIES_LINE),
33918
- width: 2
33919
- }
33920
- ]
34261
+ plots: [slopeColoredPlot("rma", "RMA", rma(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)), inputs)]
33921
34262
  })
33922
34263
  };
33923
34264
  var zlema = {
@@ -33925,61 +34266,86 @@ var zlema = {
33925
34266
  title: "Zero-Lag Exponential Moving Average",
33926
34267
  shortTitle: "ZLEMA",
33927
34268
  overlay: true,
33928
- inputs: [lengthInput(14), sourceInput(), colorInput()],
34269
+ inputs: [
34270
+ sourceInput("Close", "source", "Source", "Price series the average de-lags and then smooths."),
34271
+ 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."),
34272
+ ...directionInputs()
34273
+ ],
33929
34274
  compute: (bars, inputs) => {
33930
- const len = num3(inputs, "length", 14);
34275
+ const len = num3(inputs, "length", 21);
33931
34276
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
33932
- const lag = Math.floor((len - 1) / 2);
33933
- const lagged = shift(src, lag);
33934
- const delagged = zip(src, lagged, (a, b) => 2 * a - b);
33935
- return { plots: [{ key: "zlema", title: "ZLEMA", values: ema(delagged, len), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
34277
+ const lag = Math.round((len - 1) / 2);
34278
+ const delagged = zip(src, shift(src, lag), (a, b) => 2 * a - b);
34279
+ return { plots: [slopeColoredPlot("zlema", "ZLEMA", ema(delagged, len), inputs)] };
33936
34280
  }
33937
34281
  };
34282
+ var MOMENTUM_SCALER = "Momentum (|CMO|)";
34283
+ var VOLATILITY_SCALER = "Volatility (StDev Ratio)";
34284
+ var REGIME = "Trend Regime";
33938
34285
  var vidya = {
33939
34286
  type: "vidya",
33940
34287
  title: "Variable Index Dynamic Average",
33941
34288
  shortTitle: "VIDYA",
33942
34289
  overlay: true,
33943
- inputs: [lengthInput(14), { key: "cmoLength", title: "Momentum length", type: "int", defval: 9, min: 1, max: 500, step: 1 }, sourceInput(), colorInput()],
34290
+ inputs: [
34291
+ sourceInput("Close", "source", "Source", "Price series the average smooths. VIDYA is classically computed on the close."),
34292
+ 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."),
34293
+ 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."),
34294
+ 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."),
34295
+ 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."),
34296
+ { ...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 },
34297
+ { ...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 },
34298
+ 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."),
34299
+ 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."),
34300
+ colorInput(BULLISH, "Bullish", "bullColor"),
34301
+ colorInput(BEARISH, "Bearish", "bearColor"),
34302
+ colorInput(NEUTRAL, "Flat", "flatColor"),
34303
+ colorInput(NEUTRAL, "Neutral", "neutralColor"),
34304
+ widthInput()
34305
+ ],
33944
34306
  compute: (bars, inputs) => {
33945
- const len = num3(inputs, "length", 14);
33946
- const cmoLen = num3(inputs, "cmoLength", 9);
34307
+ const len = num3(inputs, "length", 12);
33947
34308
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
33948
- const d = change(src);
33949
- const su = new Array(src.length).fill(Number.NaN);
33950
- const sd = new Array(src.length).fill(Number.NaN);
33951
- for (let i = cmoLen; i < src.length; i++) {
33952
- let u = 0;
33953
- let w = 0;
33954
- let ok = true;
33955
- for (let k = i - cmoLen + 1; k <= i; k++) {
33956
- const x = d[k];
33957
- if (!Number.isFinite(x)) {
33958
- ok = false;
33959
- break;
33960
- }
33961
- if (x > 0) u += x;
33962
- else w -= x;
33963
- }
33964
- if (ok) {
33965
- su[i] = u;
33966
- sd[i] = w;
33967
- }
33968
- }
33969
- const alpha = 2 / (len + 1);
34309
+ const scalerLen = num3(inputs, "scalerLength", 9);
34310
+ const momentum = map(cmo(src, scalerLen), (x) => Math.abs(x) / 100);
34311
+ const shortDev = stdev(src, scalerLen);
34312
+ const refDev = stdev(src, num3(inputs, "refLength", 30));
34313
+ const volatility = zip(shortDev, refDev, (s, r) => r > 0 ? s / r : 0);
34314
+ const scale = str3(inputs, "scaler", MOMENTUM_SCALER) === VOLATILITY_SCALER ? volatility : momentum;
34315
+ const alphaBase = 2 / (len + 1);
33970
34316
  const out = new Array(src.length).fill(Number.NaN);
33971
34317
  let prev2 = Number.NaN;
33972
34318
  for (let i = 0; i < src.length; i++) {
33973
34319
  const x = src[i];
33974
- const u = su[i];
33975
- const w = sd[i];
33976
- if (!Number.isFinite(x) || !Number.isFinite(u) || !Number.isFinite(w)) continue;
33977
- const total = u + w;
33978
- const k = total === 0 ? 0 : Math.abs((u - w) / total);
33979
- prev2 = Number.isFinite(prev2) ? alpha * k * x + (1 - alpha * k) * prev2 : x;
34320
+ if (!Number.isFinite(x)) continue;
34321
+ const k = scale[i];
34322
+ if (!Number.isFinite(prev2) || !Number.isFinite(k)) prev2 = x;
34323
+ else {
34324
+ const alpha = Math.min(alphaBase * k, 1);
34325
+ prev2 = alpha * x + (1 - alpha) * prev2;
34326
+ }
33980
34327
  out[i] = prev2;
33981
34328
  }
33982
- return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
34329
+ const neutral = str3(inputs, "neutralColor", NEUTRAL);
34330
+ if (!bool(inputs, "trendColor", true)) {
34331
+ return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: neutral, width: num3(inputs, "lineWidth", 2) }] };
34332
+ }
34333
+ const bull = str3(inputs, "bullColor", BULLISH);
34334
+ const bear = str3(inputs, "bearColor", BEARISH);
34335
+ const flat = str3(inputs, "flatColor", NEUTRAL);
34336
+ const threshold = num3(inputs, "flatThreshold", 0.05);
34337
+ const range = atr(bars, num3(inputs, "atrLength", 14));
34338
+ const fade = bool(inputs, "adaptiveGradient", true);
34339
+ const colors = out.map((x, i) => {
34340
+ if (!Number.isFinite(x)) return null;
34341
+ const r = range[i];
34342
+ const slope = i > 0 && Number.isFinite(out[i - 1]) ? x - out[i - 1] : 0;
34343
+ const normalized = Number.isFinite(r) && r !== 0 ? slope / r : 0;
34344
+ const ink = normalized > threshold ? bull : normalized < -threshold ? bear : null;
34345
+ if (ink == null) return flat;
34346
+ return fade ? gradient(Math.min(scale[i] ?? 0, 1), 0, 1, flat, ink) : ink;
34347
+ });
34348
+ return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: flat, width: num3(inputs, "lineWidth", 2), colors }] };
33983
34349
  }
33984
34350
  };
33985
34351
  var maEnvelope = {
@@ -33988,115 +34354,218 @@ var maEnvelope = {
33988
34354
  shortTitle: "MA Env",
33989
34355
  overlay: true,
33990
34356
  inputs: [
33991
- { key: "maType", title: "Type", type: "string", defval: "SMA", options: ["SMA", "EMA"] },
33992
- lengthInput(20),
33993
- sourceInput(),
33994
- { key: "percent", title: "Percent", type: "float", defval: 2.5, min: 0.01, max: 50, step: 0.1 },
33995
- colorInput()
34357
+ optionInput("maType", "MA Type", "SMA", ENVELOPE_MA_TYPES, "Moving average type used as the envelope basis."),
34358
+ { ...lengthInput(20, "length", "Length", 5e3, "Number of bars in the basis moving average."), min: 2 },
34359
+ sourceInput("Close", "source", "Source", "Price series the basis moving average is calculated on."),
34360
+ 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)."),
34361
+ ...channelStyleInputs()
33996
34362
  ],
33997
34363
  compute: (bars, inputs) => {
33998
- const len = num3(inputs, "length", 20);
33999
34364
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
34000
- const basis = str3(inputs, "maType", "SMA") === "EMA" ? ema(src, len) : sma(src, len);
34365
+ const basis = movingAverageOf(str3(inputs, "maType", "SMA"), src, bars, num3(inputs, "length", 20));
34001
34366
  const k = num3(inputs, "percent", 2.5) / 100;
34002
- const ink = str3(inputs, "color", SERIES_LINE);
34003
- return {
34004
- plots: [
34005
- { key: "basis", title: "Basis", values: basis, color: ink, width: 2 },
34006
- { key: "upper", title: "Upper", values: map(basis, (x) => x * (1 + k)), color: ink },
34007
- { key: "lower", title: "Lower", values: map(basis, (x) => x * (1 - k)), color: ink }
34008
- ],
34009
- bands: [{ key: "envelope", from: "upper", to: "lower", color: withAlpha2(ink) }]
34010
- };
34367
+ const ink = { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "basisColor", NEUTRAL) };
34368
+ return directionalChannel(bars, basis, map(basis, (x) => x * (1 + k)), map(basis, (x) => x * (1 - k)), ink, "envelope");
34011
34369
  }
34012
34370
  };
34371
+ var LINREG_CURVE = "Curve";
34372
+ var LINREG_SLOPE = "Slope";
34013
34373
  var linearRegression = {
34014
34374
  type: "linear-regression",
34015
34375
  title: "Linear Regression Curve",
34016
34376
  shortTitle: "LinReg",
34017
- overlay: true,
34018
- inputs: [lengthInput(14), sourceInput(), colorInput()],
34019
- compute: (bars, inputs) => ({
34020
- plots: [
34021
- {
34022
- key: "linreg",
34023
- title: "LinReg",
34024
- values: linreg(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
34025
- color: str3(inputs, "color", SERIES_LINE),
34026
- width: 2
34027
- }
34028
- ]
34029
- })
34377
+ // Slope lives in its own pane; Curve force-overlays itself onto the price candles.
34378
+ overlay: false,
34379
+ inputs: [
34380
+ 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."),
34381
+ { ...lengthInput(100, "length", "Length", 5e3, "Number of bars used to fit the linear regression."), min: 2 },
34382
+ sourceInput("Close", "source", "Source", "Price series the regression is fitted to."),
34383
+ colorInput(BULLISH, "Rising", "bullColor", "Color of the regression curve and slope line while the fit slopes upward."),
34384
+ colorInput(BEARISH, "Falling", "bearColor", "Color of the regression curve and slope line while the fit slopes downward."),
34385
+ colorInput(NEUTRAL, "Regression Line", "lineColor", "Color of the regression curve and slope line while the fit is flat.")
34386
+ ],
34387
+ compute: (bars, inputs) => {
34388
+ const len = num3(inputs, "length", 100);
34389
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
34390
+ const curve = linreg(src, len, 0);
34391
+ const slope = zip(curve, linreg(src, len, 1), (a, b) => a - b);
34392
+ const bull = str3(inputs, "bullColor", BULLISH);
34393
+ const bear = str3(inputs, "bearColor", BEARISH);
34394
+ const flat = str3(inputs, "lineColor", NEUTRAL);
34395
+ const colors = slope.map((s) => Number.isFinite(s) ? s > 0 ? bull : s < 0 ? bear : flat : null);
34396
+ if (str3(inputs, "mode", LINREG_CURVE) === LINREG_SLOPE) {
34397
+ return {
34398
+ plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
34399
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
34400
+ };
34401
+ }
34402
+ return { plots: [{ key: "curve", title: "Curve", values: curve, color: flat, width: 2, colors, overlay: true }] };
34403
+ }
34030
34404
  };
34031
34405
  var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope, linearRegression];
34032
34406
 
34033
34407
  // src/core/native-indicators/classics/bands.ts
34408
+ var JAW_COLOR = "#ab47bc";
34409
+ var TEETH_COLOR = WARNING;
34410
+ var LIPS_COLOR = "#ffd700";
34034
34411
  function bollinger(bars, inputs) {
34035
34412
  const len = num3(inputs, "length", 20);
34036
34413
  const mult = num3(inputs, "mult", 2);
34037
34414
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
34038
34415
  const basis = sma(src, len);
34039
34416
  const dev = map(stdev(src, len), (x) => x * mult);
34040
- return { basis, upper: zip(basis, dev, (b, d) => b + d), lower: zip(basis, dev, (b, d) => b - d) };
34417
+ return { src, basis, upper: zip(basis, dev, (b, d) => b + d), lower: zip(basis, dev, (b, d) => b - d) };
34418
+ }
34419
+ function bollingerInputs(multMin, multStep) {
34420
+ return [
34421
+ { ...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 },
34422
+ sourceInput("Close", "source", "Source", "Price series the bands are computed from."),
34423
+ floatInput("mult", "Multiplier", 2, multMin, 50, multStep, "Standard deviation multiplier for the upper and lower bands.")
34424
+ ];
34041
34425
  }
34042
- var BB_INPUTS = [lengthInput(20), sourceInput(), { key: "mult", title: "StdDev", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 }];
34043
34426
  var bollingerBands = {
34044
34427
  type: "bollinger-bands",
34045
34428
  title: "Bollinger Bands",
34046
34429
  shortTitle: "BB",
34047
34430
  overlay: true,
34048
- inputs: [...BB_INPUTS, colorInput()],
34431
+ inputs: [
34432
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used to calculate the basis SMA and the standard deviation."),
34433
+ sourceInput("Close", "source", "Source", "Price series the bands are calculated on."),
34434
+ floatInput("mult", "Multiplier", 2, 0, 50, 0.1, "Number of standard deviations added above and below the basis to form the bands."),
34435
+ colorInput(WARNING, "Basis", "basisColor", "Color of the basis (middle band) moving average."),
34436
+ colorInput(INFO, "Bands", "color", "Color of the upper and lower bands."),
34437
+ colorInput(transp(INFO, 92), "Fill", "fillColor", "Fill color of the area between the upper and lower bands.")
34438
+ ],
34049
34439
  compute: (bars, inputs) => {
34050
34440
  const { basis, upper, lower } = bollinger(bars, inputs);
34051
- const ink = str3(inputs, "color", SERIES_LINE);
34441
+ const ink = str3(inputs, "color", INFO);
34052
34442
  return {
34053
34443
  plots: [
34054
- { key: "basis", title: "Basis", values: basis, color: WARNING },
34444
+ { key: "basis", title: "Basis", values: basis, color: str3(inputs, "basisColor", WARNING) },
34055
34445
  { key: "upper", title: "Upper", values: upper, color: ink },
34056
34446
  { key: "lower", title: "Lower", values: lower, color: ink }
34057
34447
  ],
34058
- bands: [{ key: "bb", from: "upper", to: "lower", color: withAlpha2(ink) }]
34448
+ bands: [{ key: "bb", from: "upper", to: "lower", color: str3(inputs, "fillColor", transp(INFO, 92)) }]
34059
34449
  };
34060
34450
  }
34061
34451
  };
34452
+ var PERCENT_B_MID = 0.5;
34453
+ var THRESHOLDS = "Thresholds";
34062
34454
  var percentB = {
34063
34455
  type: "percent-b",
34064
34456
  title: "Bollinger Bands %B",
34065
34457
  shortTitle: "%B",
34066
34458
  overlay: false,
34067
- inputs: [...BB_INPUTS, colorInput()],
34459
+ inputs: [
34460
+ ...bollingerInputs(1e-3, 0.25),
34461
+ { ...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 },
34462
+ { ...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 },
34463
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of %B above the 0.5 midline, where price trades in the upper half of the bands."),
34464
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of %B below the 0.5 midline, where price trades in the lower half of the bands."),
34465
+ 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."),
34466
+ colorInput(NEUTRAL, "Levels Color", "levelsColor", "Color of the dashed overbought/oversold levels and of the dotted midline.")
34467
+ ],
34068
34468
  compute: (bars, inputs) => {
34069
- const { upper, lower } = bollinger(bars, inputs);
34070
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
34469
+ const { src, upper, lower } = bollinger(bars, inputs);
34071
34470
  const values = src.map((x, i) => {
34072
34471
  const u = upper[i];
34073
34472
  const l = lower[i];
34074
34473
  return Number.isFinite(u) && Number.isFinite(l) && u !== l ? (x - l) / (u - l) : Number.NaN;
34075
34474
  });
34475
+ const bull = str3(inputs, "bullColor", BULLISH);
34476
+ const bear = str3(inputs, "bearColor", BEARISH);
34477
+ const levels = str3(inputs, "levelsColor", NEUTRAL);
34478
+ const colors = values.map((x) => Number.isFinite(x) ? x > PERCENT_B_MID ? bull : bear : null);
34479
+ const plots = [
34480
+ { key: "pb", title: "%B", values, color: bear, colors },
34481
+ { 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 } }
34482
+ ];
34483
+ const bands = bool(inputs, "gradientFill", true) ? [
34484
+ {
34485
+ key: "pbFill",
34486
+ from: "pb",
34487
+ to: "mid",
34488
+ color: transp(NEUTRAL, 100),
34489
+ // Opaque at the oscillator, invisible where it meets the midline.
34490
+ gradient: values.map((x) => {
34491
+ if (!Number.isFinite(x)) return null;
34492
+ const up = x > PERCENT_B_MID;
34493
+ const ink = up ? bull : bear;
34494
+ return {
34495
+ topValue: Math.max(x, PERCENT_B_MID),
34496
+ bottomValue: Math.min(x, PERCENT_B_MID),
34497
+ topColor: up ? transp(ink, 50) : transp(ink, 100),
34498
+ bottomColor: up ? transp(ink, 100) : transp(ink, 50)
34499
+ };
34500
+ })
34501
+ }
34502
+ ] : [];
34076
34503
  return {
34077
- plots: [{ key: "pb", title: "%B", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34504
+ plots,
34505
+ bands,
34078
34506
  levels: [
34079
- { key: "upper", price: 1, color: NEUTRAL },
34080
- { key: "middle", price: 0.5, color: NEUTRAL, lineStyle: "dotted" },
34081
- { key: "lower", price: 0, color: NEUTRAL }
34507
+ { key: "overbought", price: num3(inputs, "overbought", 1), color: levels, lineStyle: "dashed", title: "Overbought" },
34508
+ { key: "oversold", price: num3(inputs, "oversold", 0), color: levels, lineStyle: "dashed", title: "Oversold" },
34509
+ { key: "midline", price: PERCENT_B_MID, color: transp(levels, 50), lineStyle: "dotted", title: "Midline" }
34082
34510
  ]
34083
34511
  };
34084
34512
  }
34085
34513
  };
34514
+ var SQUEEZE_BULGE = "Squeeze & Bulge";
34086
34515
  var bandwidth = {
34087
34516
  type: "bandwidth",
34088
34517
  title: "Bollinger Bands Width",
34089
34518
  shortTitle: "BandWidth",
34090
34519
  overlay: false,
34091
- inputs: [...BB_INPUTS, colorInput()],
34520
+ inputs: [
34521
+ ...bollingerInputs(1e-3, 0.25),
34522
+ { ...intInput("lookback", "Reference Lookback", 125, 2, 5e3, "Window used to locate the lowest (Squeeze) and highest (Bulge) BandWidth references."), group: SQUEEZE_BULGE },
34523
+ 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."),
34524
+ 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."),
34525
+ 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."),
34526
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between zero and BandWidth with a vertical gradient, fully transparent at zero."),
34527
+ colorInput(WARNING, "Squeeze Level", "squeezeColor", "Color of the lowest-BandWidth reference line."),
34528
+ colorInput(BEARISH, "Bulge Level", "bulgeColor", "Color of the highest-BandWidth reference line.")
34529
+ ],
34092
34530
  compute: (bars, inputs) => {
34093
34531
  const { basis, upper, lower } = bollinger(bars, inputs);
34094
34532
  const values = basis.map((b, i) => {
34095
34533
  const u = upper[i];
34096
34534
  const l = lower[i];
34097
- return Number.isFinite(b) && b !== 0 && Number.isFinite(u) && Number.isFinite(l) ? (u - l) / b * 100 : Number.NaN;
34535
+ return Number.isFinite(b) && b !== 0 && Number.isFinite(u) && Number.isFinite(l) ? (u - l) / b : Number.NaN;
34536
+ });
34537
+ const lookback = num3(inputs, "lookback", 125);
34538
+ const squeezeLevel = lowest(values, lookback);
34539
+ const bulgeLevel = highest(values, lookback);
34540
+ const base = str3(inputs, "color", NEUTRAL);
34541
+ const squeezeInk = str3(inputs, "squeezeColor", WARNING);
34542
+ const bulgeInk = str3(inputs, "bulgeColor", BEARISH);
34543
+ const colors = values.map((x, i) => {
34544
+ if (!Number.isFinite(x)) return null;
34545
+ if (x <= squeezeLevel[i]) return squeezeInk;
34546
+ if (x >= bulgeLevel[i]) return bulgeInk;
34547
+ return base;
34098
34548
  });
34099
- return { plots: [{ key: "bbw", title: "BBW", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
34549
+ const plots = [
34550
+ { key: "bbw", title: "BandWidth", values, color: base, colors },
34551
+ { key: "zero", title: "Zero Line", values: values.map((x) => Number.isFinite(x) ? 0 : Number.NaN), color: transp(NEUTRAL, 90) }
34552
+ ];
34553
+ if (bool(inputs, "showSqueeze", true)) plots.push({ key: "squeeze", title: "Squeeze Level", values: squeezeLevel, color: squeezeInk });
34554
+ if (bool(inputs, "showBulge", true)) plots.push({ key: "bulge", title: "Bulge Level", values: bulgeLevel, color: bulgeInk });
34555
+ const bands = bool(inputs, "gradientFill", true) ? [
34556
+ {
34557
+ key: "bbwFill",
34558
+ from: "bbw",
34559
+ to: "zero",
34560
+ color: transp(NEUTRAL, 100),
34561
+ gradient: values.map((x, i) => {
34562
+ if (!Number.isFinite(x)) return null;
34563
+ const ink = colors[i] ?? base;
34564
+ return { topValue: x, bottomValue: 0, topColor: transp(ink, 50), bottomColor: transp(ink, 100) };
34565
+ })
34566
+ }
34567
+ ] : [];
34568
+ return { plots, bands };
34100
34569
  }
34101
34570
  };
34102
34571
  var keltner = {
@@ -34105,26 +34574,17 @@ var keltner = {
34105
34574
  shortTitle: "KC",
34106
34575
  overlay: true,
34107
34576
  inputs: [
34108
- lengthInput(20),
34109
- { key: "mult", title: "Multiplier", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 },
34110
- { key: "atrLength", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
34111
- sourceInput(),
34112
- colorInput()
34577
+ lengthInput(20, "length", "EMA Length", 5e3, "Number of bars used to compute the exponential moving average basis line."),
34578
+ intInput("atrLength", "ATR Length", 10, 1, 5e3, "Number of bars used to compute the Average True Range (Wilder smoothing)."),
34579
+ floatInput("mult", "Multiplier", 2, 0, 50, 0.1, "ATR multiple used to offset the upper and lower bands from the basis line."),
34580
+ sourceInput("Close", "source", "Source", "Price series used to compute the basis line."),
34581
+ ...channelStyleInputs("Basis Color")
34113
34582
  ],
34114
34583
  compute: (bars, inputs) => {
34115
- const len = num3(inputs, "length", 20);
34116
- const mult = num3(inputs, "mult", 2);
34117
- const basis = ema(sourceValues(bars, str3(inputs, "source", "Close")), len);
34118
- const range = map(atr(bars, num3(inputs, "atrLength", 10)), (x) => x * mult);
34119
- const ink = str3(inputs, "color", SERIES_LINE);
34120
- return {
34121
- plots: [
34122
- { key: "basis", title: "Basis", values: basis, color: WARNING },
34123
- { key: "upper", title: "Upper", values: zip(basis, range, (b, r) => b + r), color: ink },
34124
- { key: "lower", title: "Lower", values: zip(basis, range, (b, r) => b - r), color: ink }
34125
- ],
34126
- bands: [{ key: "kc", from: "upper", to: "lower", color: withAlpha2(ink) }]
34127
- };
34584
+ const basis = ema(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20));
34585
+ const range = map(atr(bars, num3(inputs, "atrLength", 10)), (x) => x * num3(inputs, "mult", 2));
34586
+ const ink = { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "basisColor", NEUTRAL) };
34587
+ return directionalChannel(bars, basis, zip(basis, range, (b, r) => b + r), zip(basis, range, (b, r) => b - r), ink, "kc");
34128
34588
  }
34129
34589
  };
34130
34590
  var donchian = {
@@ -34132,19 +34592,24 @@ var donchian = {
34132
34592
  title: "Donchian Channels",
34133
34593
  shortTitle: "DC",
34134
34594
  overlay: true,
34135
- inputs: [lengthInput(20), colorInput()],
34595
+ inputs: [
34596
+ lengthInput(20, "length", "Length", 5e3, "Lookback length. The upper and lower channels are the highest high and lowest low over this many bars."),
34597
+ colorInput(BEARISH, "Upper", "upperColor", "Color of the upper channel line."),
34598
+ colorInput(NEUTRAL, "Middle", "middleColor", "Color of the channel midline."),
34599
+ colorInput(BULLISH, "Lower", "lowerColor", "Color of the lower channel line."),
34600
+ colorInput(transp(NEUTRAL, 92), "Fill", "fillColor", "Color of the channel interior fill.")
34601
+ ],
34136
34602
  compute: (bars, inputs) => {
34137
34603
  const len = num3(inputs, "length", 20);
34138
34604
  const upper = highest(highs(bars), len);
34139
34605
  const lower = lowest(lows(bars), len);
34140
- const ink = str3(inputs, "color", SERIES_LINE);
34141
34606
  return {
34142
34607
  plots: [
34143
- { key: "basis", title: "Basis", values: zip(upper, lower, (u, l) => (u + l) / 2), color: WARNING },
34144
- { key: "upper", title: "Upper", values: upper, color: ink },
34145
- { key: "lower", title: "Lower", values: lower, color: ink }
34608
+ { key: "upper", title: "Upper", values: upper, color: str3(inputs, "upperColor", BEARISH) },
34609
+ { key: "middle", title: "Middle", kind: "circles", values: zip(upper, lower, (u, l) => (u + l) / 2), color: str3(inputs, "middleColor", NEUTRAL) },
34610
+ { key: "lower", title: "Lower", values: lower, color: str3(inputs, "lowerColor", BULLISH) }
34146
34611
  ],
34147
- bands: [{ key: "dc", from: "upper", to: "lower", color: withAlpha2(ink) }]
34612
+ bands: [{ key: "dc", from: "upper", to: "lower", color: str3(inputs, "fillColor", transp(NEUTRAL, 92)) }]
34148
34613
  };
34149
34614
  }
34150
34615
  };
@@ -34153,16 +34618,37 @@ var chandelier = {
34153
34618
  title: "Chandelier Exit",
34154
34619
  shortTitle: "CE",
34155
34620
  overlay: true,
34156
- inputs: [lengthInput(22), { key: "mult", title: "ATR multiplier", type: "float", defval: 3, min: 0.1, max: 50, step: 0.1 }],
34621
+ inputs: [
34622
+ lengthInput(22, "length", "Length", 5e3, "Lookback used for both the highest high / lowest low extremes and the ATR."),
34623
+ 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)."),
34624
+ 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."),
34625
+ colorInput(BULLISH, "Long Stop", "longColor", "Color of the long trailing stop line."),
34626
+ colorInput(BEARISH, "Short Stop", "shortColor", "Color of the short trailing stop line.")
34627
+ ],
34157
34628
  compute: (bars, inputs) => {
34158
34629
  const len = num3(inputs, "length", 22);
34159
34630
  const range = map(atr(bars, len), (x) => x * num3(inputs, "mult", 3));
34160
- const long = zip(highest(highs(bars), len), range, (h, r) => h - r);
34161
- const short = zip(lowest(lows(bars), len), range, (l, r) => l + r);
34631
+ const basicLong = zip(highest(highs(bars), len), range, (h, r) => h - r);
34632
+ const basicShort = zip(lowest(lows(bars), len), range, (l, r) => l + r);
34633
+ let long = basicLong;
34634
+ let short = basicShort;
34635
+ if (bool(inputs, "ratchet", false)) {
34636
+ long = new Array(bars.length).fill(Number.NaN);
34637
+ short = new Array(bars.length).fill(Number.NaN);
34638
+ for (let i = 0; i < bars.length; i++) {
34639
+ const bl = basicLong[i];
34640
+ const bs = basicShort[i];
34641
+ const prevClose = i > 0 ? bars[i - 1].close : bars[i].close;
34642
+ const prevLong = i > 0 && Number.isFinite(long[i - 1]) ? long[i - 1] : bl;
34643
+ const prevShort = i > 0 && Number.isFinite(short[i - 1]) ? short[i - 1] : bs;
34644
+ long[i] = Number.isFinite(bl) ? prevClose > prevLong ? Math.max(bl, prevLong) : bl : Number.NaN;
34645
+ short[i] = Number.isFinite(bs) ? prevClose < prevShort ? Math.min(bs, prevShort) : bs : Number.NaN;
34646
+ }
34647
+ }
34162
34648
  return {
34163
34649
  plots: [
34164
- { key: "long", title: "Long stop", values: long, color: BULLISH },
34165
- { key: "short", title: "Short stop", values: short, color: BEARISH }
34650
+ { key: "long", title: "Long Stop", values: long, color: str3(inputs, "longColor", BULLISH) },
34651
+ { key: "short", title: "Short Stop", values: short, color: str3(inputs, "shortColor", BEARISH) }
34166
34652
  ]
34167
34653
  };
34168
34654
  }
@@ -34173,21 +34659,22 @@ var chandeKroll = {
34173
34659
  shortTitle: "CKS",
34174
34660
  overlay: true,
34175
34661
  inputs: [
34176
- { key: "p", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
34177
- { key: "x", title: "ATR multiplier", type: "float", defval: 1, min: 0.1, max: 50, step: 0.1 },
34178
- { key: "q", title: "Stop length", type: "int", defval: 9, min: 1, max: 500, step: 1 }
34662
+ intInput("p", "ATR Length (P)", 10, 1, 5e3, "Lookback used for both the ATR and the highest high / lowest low price extremes."),
34663
+ floatInput("x", "ATR Multiplier (X)", 1, 0, 50, 0.1, "Multiplier applied to the ATR to offset the preliminary stops from the price extremes."),
34664
+ intInput("q", "Stop Length (Q)", 9, 1, 5e3, "Lookback over which the preliminary stops are extended into the final stop levels."),
34665
+ colorInput(BEARISH, "Stop Short", "shortColor", "Color of the protective stop line for short positions."),
34666
+ colorInput(BULLISH, "Stop Long", "longColor", "Color of the protective stop line for long positions.")
34179
34667
  ],
34180
34668
  compute: (bars, inputs) => {
34181
34669
  const p = num3(inputs, "p", 10);
34182
- const x = num3(inputs, "x", 1);
34183
34670
  const q = num3(inputs, "q", 9);
34184
- const range = map(atr(bars, p), (v) => v * x);
34671
+ const range = map(atr(bars, p), (v) => v * num3(inputs, "x", 1));
34185
34672
  const firstHigh = zip(highest(highs(bars), p), range, (h, r) => h - r);
34186
34673
  const firstLow = zip(lowest(lows(bars), p), range, (l, r) => l + r);
34187
34674
  return {
34188
34675
  plots: [
34189
- { key: "stopShort", title: "Stop short", values: highest(firstHigh, q), color: BEARISH },
34190
- { key: "stopLong", title: "Stop long", values: lowest(firstLow, q), color: BULLISH }
34676
+ { key: "stopShort", title: "Stop Short", values: highest(firstHigh, q), color: str3(inputs, "shortColor", BEARISH) },
34677
+ { key: "stopLong", title: "Stop Long", values: lowest(firstLow, q), color: str3(inputs, "longColor", BULLISH) }
34191
34678
  ]
34192
34679
  };
34193
34680
  }
@@ -34198,36 +34685,42 @@ var supertrend = {
34198
34685
  shortTitle: "SuperTrend",
34199
34686
  overlay: true,
34200
34687
  inputs: [
34201
- { key: "atrLength", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
34202
- { key: "mult", title: "Factor", type: "float", defval: 3, min: 0.1, max: 50, step: 0.1 }
34688
+ intInput("atrLength", "ATR Length", 10, 1, 5e3, "Number of bars used to compute the Wilder ATR."),
34689
+ floatInput("mult", "Factor", 3, 0, 50, 0.1, "ATR multiplier setting how far the trailing stop sits from the bar midpoint."),
34690
+ colorInput(BULLISH, "Uptrend Color", "upColor", "Line color while the trend is up."),
34691
+ colorInput(BEARISH, "Downtrend Color", "downColor", "Line color while the trend is down.")
34203
34692
  ],
34204
34693
  compute: (bars, inputs) => {
34205
- const len = num3(inputs, "atrLength", 10);
34206
34694
  const mult = num3(inputs, "mult", 3);
34207
- const range = atr(bars, len);
34695
+ const range = atr(bars, num3(inputs, "atrLength", 10));
34208
34696
  const n = bars.length;
34209
- const st = new Array(n).fill(Number.NaN);
34210
- const dir = new Array(n).fill(1);
34211
- let up = Number.NaN;
34212
- let dn = Number.NaN;
34213
- let trend = 1;
34697
+ const up = new Array(n).fill(Number.NaN);
34698
+ const down = new Array(n).fill(Number.NaN);
34699
+ let finalUpper = Number.NaN;
34700
+ let finalLower = Number.NaN;
34701
+ let trend = -1;
34214
34702
  for (let i = 0; i < n; i++) {
34215
34703
  const r = range[i];
34216
34704
  if (!Number.isFinite(r)) continue;
34217
34705
  const b = bars[i];
34218
34706
  const mid = (b.high + b.low) / 2;
34219
- const basicUp = mid - mult * r;
34220
- const basicDn = mid + mult * r;
34707
+ const basicUpper = mid + mult * r;
34708
+ const basicLower = mid - mult * r;
34709
+ const prevUpper = Number.isFinite(finalUpper) ? finalUpper : basicUpper;
34710
+ const prevLower = Number.isFinite(finalLower) ? finalLower : basicLower;
34221
34711
  const prevClose = i > 0 ? bars[i - 1].close : b.close;
34222
- up = Number.isFinite(up) && prevClose > up ? Math.max(basicUp, up) : basicUp;
34223
- dn = Number.isFinite(dn) && prevClose < dn ? Math.min(basicDn, dn) : basicDn;
34224
- if (trend === 1 && b.close < up) trend = -1;
34225
- else if (trend === -1 && b.close > dn) trend = 1;
34226
- dir[i] = trend;
34227
- st[i] = trend === 1 ? up : dn;
34712
+ finalUpper = prevClose <= prevUpper ? Math.min(basicUpper, prevUpper) : basicUpper;
34713
+ finalLower = prevClose >= prevLower ? Math.max(basicLower, prevLower) : basicLower;
34714
+ trend = b.close > finalUpper ? 1 : b.close < finalLower ? -1 : trend;
34715
+ if (trend === 1) up[i] = finalLower;
34716
+ else down[i] = finalUpper;
34228
34717
  }
34229
- const colors = dir.map((d, i) => Number.isFinite(st[i]) ? d === 1 ? BULLISH : BEARISH : null);
34230
- return { plots: [{ key: "st", title: "SuperTrend", values: st, color: BULLISH, width: 2, colors }] };
34718
+ return {
34719
+ plots: [
34720
+ { key: "up", title: "Uptrend", values: up, color: str3(inputs, "upColor", BULLISH) },
34721
+ { key: "down", title: "Downtrend", values: down, color: str3(inputs, "downColor", BEARISH) }
34722
+ ]
34723
+ };
34231
34724
  }
34232
34725
  };
34233
34726
  var alligator = {
@@ -34236,12 +34729,15 @@ var alligator = {
34236
34729
  shortTitle: "Alligator",
34237
34730
  overlay: true,
34238
34731
  inputs: [
34239
- { key: "jawLength", title: "Jaw length", type: "int", defval: 13, min: 1, max: 500, step: 1 },
34240
- { key: "teethLength", title: "Teeth length", type: "int", defval: 8, min: 1, max: 500, step: 1 },
34241
- { key: "lipsLength", title: "Lips length", type: "int", defval: 5, min: 1, max: 500, step: 1 },
34242
- { key: "jawOffset", title: "Jaw offset", type: "int", defval: 8, min: 0, max: 100, step: 1 },
34243
- { key: "teethOffset", title: "Teeth offset", type: "int", defval: 5, min: 0, max: 100, step: 1 },
34244
- { key: "lipsOffset", title: "Lips offset", type: "int", defval: 3, min: 0, max: 100, step: 1 }
34732
+ intInput("jawLength", "Jaw Length", 13, 1, 5e3, "Smoothing length of the Jaw SMMA of median price (hl2), 13 in the classic setup."),
34733
+ intInput("teethLength", "Teeth Length", 8, 1, 5e3, "Smoothing length of the Teeth SMMA of median price (hl2), 8 in the classic setup."),
34734
+ intInput("lipsLength", "Lips Length", 5, 1, 5e3, "Smoothing length of the Lips SMMA of median price (hl2), 5 in the classic setup."),
34735
+ intInput("jawOffset", "Jaw Offset", 8, 0, 100, "Number of bars the Jaw plot is shifted into the future, 8 in the classic setup."),
34736
+ intInput("teethOffset", "Teeth Offset", 5, 0, 100, "Number of bars the Teeth plot is shifted into the future, 5 in the classic setup."),
34737
+ intInput("lipsOffset", "Lips Offset", 3, 0, 100, "Number of bars the Lips plot is shifted into the future, 3 in the classic setup."),
34738
+ colorInput(JAW_COLOR, "Jaw", "jawColor", "Color of the Jaw line."),
34739
+ colorInput(TEETH_COLOR, "Teeth", "teethColor", "Color of the Teeth line."),
34740
+ colorInput(LIPS_COLOR, "Lips", "lipsColor", "Color of the Lips line.")
34245
34741
  ],
34246
34742
  compute: (bars, inputs) => {
34247
34743
  const hl2 = sourceValues(bars, "HL2");
@@ -34250,99 +34746,134 @@ var alligator = {
34250
34746
  const lips = shift(rma(hl2, num3(inputs, "lipsLength", 5)), num3(inputs, "lipsOffset", 3));
34251
34747
  return {
34252
34748
  plots: [
34253
- { key: "jaw", title: "Jaw", values: jaw, color: SERIES_LINE },
34254
- { key: "teeth", title: "Teeth", values: teeth, color: BEARISH },
34255
- { key: "lips", title: "Lips", values: lips, color: BULLISH }
34749
+ { key: "jaw", title: "Jaw", values: jaw, color: str3(inputs, "jawColor", JAW_COLOR) },
34750
+ { key: "teeth", title: "Teeth", values: teeth, color: str3(inputs, "teethColor", TEETH_COLOR) },
34751
+ { key: "lips", title: "Lips", values: lips, color: str3(inputs, "lipsColor", LIPS_COLOR) }
34256
34752
  ]
34257
34753
  };
34258
34754
  }
34259
34755
  };
34756
+ var GATOR_LINES = "Alligator Lines";
34757
+ var SMMA_TYPE = "SMMA (Wilder)";
34260
34758
  var gator = {
34261
34759
  type: "gator-oscillator",
34262
34760
  title: "Gator Oscillator",
34263
34761
  shortTitle: "Gator Oscillator",
34264
34762
  overlay: false,
34265
- inputs: [],
34266
- compute: (bars) => {
34267
- const hl2 = sourceValues(bars, "HL2");
34268
- const jaw = shift(rma(hl2, 13), 8);
34269
- const teeth = shift(rma(hl2, 8), 5);
34270
- const lips = shift(rma(hl2, 5), 3);
34763
+ inputs: [
34764
+ { ...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 },
34765
+ { ...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 },
34766
+ { ...intInput("jawLength", "Jaw Length", 13, 1, 500, "Averaging length of the jaw line."), group: GATOR_LINES },
34767
+ { ...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 },
34768
+ { ...intInput("teethLength", "Teeth Length", 8, 1, 500, "Averaging length of the teeth line."), group: GATOR_LINES },
34769
+ { ...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 },
34770
+ { ...intInput("lipsLength", "Lips Length", 5, 1, 500, "Averaging length of the lips line."), group: GATOR_LINES },
34771
+ { ...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 },
34772
+ colorInput(BULLISH, "Expanding", "growColor", "Column color while a spread grew against its own previous bar \u2014 the averages are spreading apart."),
34773
+ colorInput(BEARISH, "Contracting", "shrinkColor", "Column color while a spread shrank \u2014 the averages are converging. Color encodes expansion, not trade direction."),
34774
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero line separating the two histograms.")
34775
+ ],
34776
+ compute: (bars, inputs) => {
34777
+ const src = sourceValues(bars, str3(inputs, "source", "HL2"));
34778
+ const kind = str3(inputs, "maType", SMMA_TYPE);
34779
+ const smooth = (len) => kind === "SMA" ? sma(src, len) : kind === "EMA" ? ema(src, len) : rma(src, len);
34780
+ const jaw = shift(smooth(num3(inputs, "jawLength", 13)), num3(inputs, "jawOffset", 8));
34781
+ const teeth = shift(smooth(num3(inputs, "teethLength", 8)), num3(inputs, "teethOffset", 5));
34782
+ const lips = shift(smooth(num3(inputs, "lipsLength", 5)), num3(inputs, "lipsOffset", 3));
34271
34783
  const upper = map(sub(jaw, teeth), Math.abs);
34272
- const lower = map(sub(teeth, lips), (x) => -Math.abs(x));
34273
- const colorByExpansion = (v) => v.map((x, i) => {
34274
- if (!Number.isFinite(x)) return null;
34275
- const prev2 = i > 0 ? v[i - 1] : Number.NaN;
34276
- return Number.isFinite(prev2) && Math.abs(x) > Math.abs(prev2) ? BULLISH : BEARISH;
34277
- });
34784
+ const lower = map(sub(teeth, lips), Math.abs);
34785
+ const grow = str3(inputs, "growColor", BULLISH);
34786
+ const shrink = str3(inputs, "shrinkColor", BEARISH);
34787
+ const expansionColors2 = (v) => {
34788
+ let growing = false;
34789
+ return v.map((x, i) => {
34790
+ if (!Number.isFinite(x)) return null;
34791
+ const prev2 = i > 0 ? v[i - 1] : Number.NaN;
34792
+ if (Number.isFinite(prev2)) growing = x > prev2 ? true : x < prev2 ? false : growing;
34793
+ return growing ? grow : shrink;
34794
+ });
34795
+ };
34278
34796
  return {
34279
34797
  plots: [
34280
- { key: "upper", title: "Upper", values: upper, kind: "histogram", color: BULLISH, colors: colorByExpansion(upper), base: 0 },
34281
- { key: "lower", title: "Lower", values: lower, kind: "histogram", color: BEARISH, colors: colorByExpansion(lower), base: 0 }
34282
- ]
34798
+ { key: "upper", title: "Upper Histogram (Jaw - Teeth)", values: upper, kind: "columns", color: grow, colors: expansionColors2(upper), base: 0 },
34799
+ { key: "lower", title: "Lower Histogram (Teeth - Lips)", values: map(lower, (x) => -x), kind: "columns", color: shrink, colors: expansionColors2(lower), base: 0 }
34800
+ ],
34801
+ levels: [{ key: "zero", price: 0, color: str3(inputs, "zeroColor", NEUTRAL), lineStyle: "solid", title: "Zero Line" }]
34283
34802
  };
34284
34803
  }
34285
34804
  };
34286
34805
  var bandSpecs = [bollingerBands, percentB, bandwidth, keltner, donchian, chandelier, chandeKroll, supertrend, alligator, gator];
34287
34806
 
34288
34807
  // src/core/native-indicators/classics/oscillators.ts
34289
- function guideLevels(low, high, mid) {
34290
- const levels = [
34291
- { key: "upper", price: high, color: NEUTRAL },
34292
- { key: "lower", price: low, color: NEUTRAL }
34808
+ function thresholdInputs(obDefault, osDefault, min = 0, max = 100, obTitle = "Overbought Level", osTitle = "Oversold Level") {
34809
+ return [
34810
+ floatInput("overbought", obTitle, obDefault, min, max, 1, "Level above which the oscillator is considered overbought."),
34811
+ floatInput("oversold", osTitle, osDefault, min, max, 1, "Level below which the oscillator is considered oversold.")
34293
34812
  ];
34294
- if (mid != null) levels.splice(1, 0, { key: "middle", price: mid, color: NEUTRAL, lineStyle: "dotted" });
34295
- return levels;
34296
- }
34297
- var ZERO_LEVEL = [{ key: "zero", price: 0, color: NEUTRAL }];
34298
- function histColors(values) {
34299
- return values.map((x, i) => {
34300
- if (!Number.isFinite(x)) return null;
34301
- const prev2 = i > 0 ? values[i - 1] : Number.NaN;
34302
- const rising = Number.isFinite(prev2) ? x >= prev2 : true;
34303
- if (x >= 0) return rising ? BULLISH : `${BULLISH}80`;
34304
- return rising ? `${BEARISH}80` : BEARISH;
34305
- });
34306
34813
  }
34307
34814
  var rsiSpec = {
34308
34815
  type: "rsi",
34309
34816
  title: "Relative Strength Index",
34310
34817
  shortTitle: "RSI",
34311
34818
  overlay: false,
34312
- inputs: [lengthInput(14), sourceInput(), colorInput()],
34313
- compute: (bars, inputs) => ({
34314
- plots: [
34315
- {
34316
- key: "rsi",
34317
- title: "RSI",
34318
- values: rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
34319
- color: str3(inputs, "color", SERIES_LINE),
34320
- width: 2
34321
- }
34322
- ],
34323
- levels: guideLevels(30, 70, 50)
34819
+ inputs: [
34820
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used in the Wilder smoothing of average gains and losses."),
34821
+ sourceInput("Close", "source", "Source", "Price series the RSI is calculated on."),
34822
+ ...thresholdInputs(70, 30, 0, 100, "Overbought", "Oversold"),
34823
+ colorInput(NEUTRAL, "RSI", "color", "Color of the RSI line at the midline. Toward either threshold it blends into that threshold\u2019s color."),
34824
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level."),
34825
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
34826
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought, midline and oversold levels."),
34827
+ colorInput(transp(NEUTRAL, 90), "Band Fill", "fillColor", "Color of the fill between the overbought and oversold levels.")
34828
+ ],
34829
+ compute: (bars, inputs) => boundedOscillator({
34830
+ key: "rsi",
34831
+ title: "RSI",
34832
+ values: rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
34833
+ scale: { top: 100, bottom: 0 },
34834
+ overbought: num3(inputs, "overbought", 70),
34835
+ oversold: num3(inputs, "oversold", 30),
34836
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
34837
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
34838
+ width: 2,
34839
+ midline: 50,
34840
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 90))
34324
34841
  })
34325
34842
  };
34843
+ function stochasticOutput(k, d, inputs) {
34844
+ const overbought = num3(inputs, "overbought", 80);
34845
+ const oversold = num3(inputs, "oversold", 20);
34846
+ return {
34847
+ plots: [
34848
+ { key: "k", title: "%K", values: k, color: str3(inputs, "kColor", INFO) },
34849
+ { key: "d", title: "%D", values: d, color: str3(inputs, "dColor", WARNING) },
34850
+ anchorPlot("obAnchor", "Overbought Anchor", k.length, overbought),
34851
+ anchorPlot("osAnchor", "Oversold Anchor", k.length, oversold)
34852
+ ],
34853
+ bands: [{ key: "band", from: "obAnchor", to: "osAnchor", color: str3(inputs, "fillColor", transp(INFO, 90)) }],
34854
+ levels: [
34855
+ { key: "overbought", price: overbought, color: NEUTRAL, lineStyle: "dashed", title: "Overbought" },
34856
+ { key: "oversold", price: oversold, color: NEUTRAL, lineStyle: "dashed", title: "Oversold" }
34857
+ ]
34858
+ };
34859
+ }
34326
34860
  var stochastic = {
34327
34861
  type: "stochastic",
34328
34862
  title: "Stochastic",
34329
34863
  shortTitle: "Stoch",
34330
34864
  overlay: false,
34331
34865
  inputs: [
34332
- lengthInput(14, "kLength", "%K length"),
34333
- lengthInput(1, "kSmoothing", "%K smoothing", 500),
34334
- lengthInput(3, "dLength", "%D smoothing", 500)
34866
+ lengthInput(14, "kLength", "%K Length", 5e3, "Number of bars used for the highest high / lowest low range of the raw %K."),
34867
+ 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."),
34868
+ lengthInput(3, "dLength", "%D Smoothing", 500, "Simple moving average length of the %D signal line, applied to the smoothed %K."),
34869
+ ...thresholdInputs(80, 20),
34870
+ colorInput(INFO, "%K", "kColor", "Color of the %K line."),
34871
+ colorInput(WARNING, "%D", "dColor", "Color of the %D signal line."),
34872
+ colorInput(transp(INFO, 90), "Band Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
34335
34873
  ],
34336
34874
  compute: (bars, inputs) => {
34337
- const k = sma(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "kLength", 14)), num3(inputs, "kSmoothing", 1));
34338
- const d = sma(k, num3(inputs, "dLength", 3));
34339
- return {
34340
- plots: [
34341
- { key: "k", title: "%K", values: k, color: SERIES_LINE, width: 2 },
34342
- { key: "d", title: "%D", values: d, color: WARNING }
34343
- ],
34344
- levels: guideLevels(20, 80)
34345
- };
34875
+ const k = sma(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "kLength", 14)), num3(inputs, "kSmoothing", 3));
34876
+ return stochasticOutput(k, sma(k, num3(inputs, "dLength", 3)), inputs);
34346
34877
  }
34347
34878
  };
34348
34879
  var stochasticRsi = {
@@ -34351,23 +34882,20 @@ var stochasticRsi = {
34351
34882
  shortTitle: "Stoch RSI",
34352
34883
  overlay: false,
34353
34884
  inputs: [
34354
- lengthInput(14, "rsiLength", "RSI length"),
34355
- lengthInput(14, "stochLength", "Stochastic length"),
34356
- lengthInput(3, "kSmoothing", "%K smoothing", 500),
34357
- lengthInput(3, "dLength", "%D smoothing", 500),
34358
- sourceInput()
34885
+ sourceInput("Close", "source", "RSI Source", "Price series the RSI is computed on."),
34886
+ lengthInput(14, "rsiLength", "RSI Length", 5e3, "Lookback length of the Wilder RSI."),
34887
+ lengthInput(14, "stochLength", "Stochastic Length", 5e3, "Lookback over which the highest and lowest RSI values are taken."),
34888
+ lengthInput(3, "kSmoothing", "%K Smoothing", 500, "Simple moving average length applied to the stochastic of RSI to form %K."),
34889
+ lengthInput(3, "dLength", "%D Smoothing", 500, "Simple moving average length applied to %K to form the %D signal line."),
34890
+ ...thresholdInputs(80, 20),
34891
+ colorInput(INFO, "%K Color", "kColor", "Color of the %K line."),
34892
+ colorInput(WARNING, "%D Color", "dColor", "Color of the %D signal line."),
34893
+ colorInput(transp(INFO, 90), "Zone Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
34359
34894
  ],
34360
34895
  compute: (bars, inputs) => {
34361
34896
  const r = rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "rsiLength", 14));
34362
34897
  const k = sma(stoch(r, r, r, num3(inputs, "stochLength", 14)), num3(inputs, "kSmoothing", 3));
34363
- const d = sma(k, num3(inputs, "dLength", 3));
34364
- return {
34365
- plots: [
34366
- { key: "k", title: "%K", values: k, color: SERIES_LINE, width: 2 },
34367
- { key: "d", title: "%D", values: d, color: WARNING }
34368
- ],
34369
- levels: guideLevels(20, 80)
34370
- };
34898
+ return stochasticOutput(k, sma(k, num3(inputs, "dLength", 3)), inputs);
34371
34899
  }
34372
34900
  };
34373
34901
  var macd = {
@@ -34375,23 +34903,35 @@ var macd = {
34375
34903
  title: "MACD",
34376
34904
  overlay: false,
34377
34905
  inputs: [
34378
- lengthInput(12, "fastLength", "Fast length"),
34379
- lengthInput(26, "slowLength", "Slow length"),
34380
- lengthInput(9, "signalLength", "Signal smoothing", 500),
34381
- sourceInput()
34906
+ lengthInput(12, "fastLength", "Fast Length", 5e3, "Length of the fast EMA."),
34907
+ lengthInput(26, "slowLength", "Slow Length", 5e3, "Length of the slow EMA."),
34908
+ lengthInput(9, "signalLength", "Signal Smoothing", 500, "Length of the EMA applied to the MACD line to obtain the signal line."),
34909
+ sourceInput("Close", "source", "Source", "Price series used to compute the fast and slow EMAs."),
34910
+ colorInput(INFO, "MACD Line", "color", "Color of the MACD line."),
34911
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
34912
+ colorInput(BULLISH, "Grow Above", "growAboveColor", "Histogram color when above zero and rising."),
34913
+ colorInput(transp(BULLISH, 50), "Fall Above", "fallAboveColor", "Histogram color when above zero and falling."),
34914
+ colorInput(transp(BEARISH, 50), "Grow Below", "growBelowColor", "Histogram color when below zero and rising."),
34915
+ colorInput(BEARISH, "Fall Below", "fallBelowColor", "Histogram color when below zero and falling.")
34382
34916
  ],
34383
34917
  compute: (bars, inputs) => {
34384
34918
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
34385
34919
  const line = sub(ema(src, num3(inputs, "fastLength", 12)), ema(src, num3(inputs, "slowLength", 26)));
34386
34920
  const signal = ema(line, num3(inputs, "signalLength", 9));
34387
34921
  const hist = sub(line, signal);
34922
+ const colors = momentumColumnColors(hist, {
34923
+ growAbove: str3(inputs, "growAboveColor", BULLISH),
34924
+ fallAbove: str3(inputs, "fallAboveColor", transp(BULLISH, 50)),
34925
+ growBelow: str3(inputs, "growBelowColor", transp(BEARISH, 50)),
34926
+ fallBelow: str3(inputs, "fallBelowColor", BEARISH)
34927
+ });
34388
34928
  return {
34389
34929
  plots: [
34390
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: histColors(hist), base: 0 },
34391
- { key: "macd", title: "MACD", values: line, color: SERIES_LINE, width: 2 },
34392
- { key: "signal", title: "Signal", values: signal, color: WARNING }
34930
+ { key: "hist", title: "Histogram", values: hist, kind: "columns", color: BULLISH, colors, base: 0 },
34931
+ { key: "macd", title: "MACD", values: line, color: str3(inputs, "color", INFO) },
34932
+ { key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) }
34393
34933
  ],
34394
- levels: ZERO_LEVEL
34934
+ levels: [{ key: "zero", price: 0, color: NEUTRAL, lineStyle: "dashed", title: "Zero Line" }]
34395
34935
  };
34396
34936
  }
34397
34937
  };
@@ -34401,26 +34941,49 @@ var ppo = {
34401
34941
  shortTitle: "PPO",
34402
34942
  overlay: false,
34403
34943
  inputs: [
34404
- lengthInput(12, "fastLength", "Fast length"),
34405
- lengthInput(26, "slowLength", "Slow length"),
34406
- lengthInput(9, "signalLength", "Signal smoothing", 500),
34407
- sourceInput()
34944
+ sourceInput("Close", "source", "Source", "Price series fed into the fast and slow averages."),
34945
+ lengthInput(12, "fastLength", "Fast Length", 5e3, "Length of the fast moving average."),
34946
+ 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."),
34947
+ optionInput("maType", "Average Type", "EMA", ["EMA", "SMA"], "Moving average type used for the fast and slow averages."),
34948
+ lengthInput(9, "signalLength", "Signal Smoothing", 500, "Length of the signal line that smooths the PPO line."),
34949
+ optionInput("signalType", "Signal Type", "EMA", ["EMA", "SMA"], "Moving average type used for the signal line."),
34950
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the PPO line, gradient fill and histogram above the zero line."),
34951
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the PPO line, gradient fill and histogram below the zero line."),
34952
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
34953
+ 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."),
34954
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the zero line and the PPO line with a vertical gradient.")
34408
34955
  ],
34409
34956
  compute: (bars, inputs) => {
34410
34957
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
34411
- const fast = ema(src, num3(inputs, "fastLength", 12));
34412
- const slow = ema(src, num3(inputs, "slowLength", 26));
34958
+ const average = (kind2, v, len) => kind2 === "SMA" ? sma(v, len) : ema(v, len);
34959
+ const kind = str3(inputs, "maType", "EMA");
34960
+ const fast = average(kind, src, num3(inputs, "fastLength", 12));
34961
+ const slow = average(kind, src, num3(inputs, "slowLength", 26));
34413
34962
  const line = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : (f - s) / s * 100);
34414
- const signal = ema(line, num3(inputs, "signalLength", 9));
34963
+ const signal = average(str3(inputs, "signalType", "EMA"), line, num3(inputs, "signalLength", 9));
34415
34964
  const hist = sub(line, signal);
34416
- return {
34417
- plots: [
34418
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: histColors(hist), base: 0 },
34419
- { key: "ppo", title: "PPO", values: line, color: SERIES_LINE, width: 2 },
34420
- { key: "signal", title: "Signal", values: signal, color: WARNING }
34421
- ],
34422
- levels: ZERO_LEVEL
34423
- };
34965
+ const bull = str3(inputs, "bullColor", BULLISH);
34966
+ const bear = str3(inputs, "bearColor", BEARISH);
34967
+ const out = centeredOscillator({
34968
+ key: "ppo",
34969
+ title: "PPO Line",
34970
+ values: line,
34971
+ ink: { bull, bear, neutral: bear },
34972
+ zeroInk: transp(NEUTRAL, 90),
34973
+ fill: bool(inputs, "gradientFill", true),
34974
+ extraPlots: [{ key: "signal", title: "Signal Line", values: signal, color: str3(inputs, "signalColor", WARNING) }]
34975
+ });
34976
+ if (bool(inputs, "showHistogram", true)) {
34977
+ const colors = hist.map((x, i) => {
34978
+ if (!Number.isFinite(x)) return null;
34979
+ const prev2 = i > 0 && Number.isFinite(hist[i - 1]) ? hist[i - 1] : x;
34980
+ const ink = x >= 0 ? bull : bear;
34981
+ const expanding = x >= 0 ? x > prev2 : x < prev2;
34982
+ return transp(ink, expanding ? 40 : 70);
34983
+ });
34984
+ out.plots.unshift({ key: "hist", title: "Histogram", values: hist, kind: "columns", color: bull, colors, base: 0 });
34985
+ }
34986
+ return out;
34424
34987
  }
34425
34988
  };
34426
34989
  var awesome = {
@@ -34428,48 +34991,67 @@ var awesome = {
34428
34991
  title: "Awesome Oscillator",
34429
34992
  shortTitle: "AO",
34430
34993
  overlay: false,
34431
- inputs: [],
34432
- compute: (bars) => {
34994
+ inputs: [
34995
+ colorInput(BULLISH, "Rising", "risingColor", "Histogram color when the oscillator is higher than on the previous bar."),
34996
+ colorInput(BEARISH, "Falling", "fallingColor", "Histogram color when the oscillator is lower than or equal to the previous bar."),
34997
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
34998
+ ],
34999
+ compute: (bars, inputs) => {
34433
35000
  const hl2 = sourceValues(bars, "HL2");
34434
35001
  const ao = sub(sma(hl2, 5), sma(hl2, 34));
35002
+ const rising = str3(inputs, "risingColor", BULLISH);
35003
+ const falling = str3(inputs, "fallingColor", BEARISH);
34435
35004
  const colors = ao.map((x, i) => {
34436
35005
  if (!Number.isFinite(x)) return null;
34437
35006
  const prev2 = i > 0 ? ao[i - 1] : Number.NaN;
34438
- return Number.isFinite(prev2) && x < prev2 ? BEARISH : BULLISH;
35007
+ return Number.isFinite(prev2) && x > prev2 ? rising : falling;
34439
35008
  });
34440
35009
  return {
34441
- plots: [{ key: "ao", title: "AO", values: ao, kind: "histogram", color: BULLISH, colors, base: 0 }],
34442
- levels: ZERO_LEVEL
35010
+ plots: [{ key: "ao", title: "AO", values: ao, kind: "columns", color: rising, colors, base: 0 }],
35011
+ levels: [{ key: "zero", price: 0, color: str3(inputs, "zeroColor", NEUTRAL), lineStyle: "dashed", title: "Zero Line" }]
34443
35012
  };
34444
35013
  }
34445
35014
  };
35015
+ var CCI_SCALING = 0.015;
34446
35016
  var cci = {
34447
35017
  type: "commodity-channel-index",
34448
35018
  title: "Commodity Channel Index",
34449
35019
  shortTitle: "CCI",
34450
35020
  overlay: false,
34451
- inputs: [lengthInput(20), sourceInput("HLC3"), colorInput()],
35021
+ inputs: [
35022
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used for the moving average and the mean absolute deviation of the source."),
35023
+ sourceInput("HLC3", "source", "Source", "Price series measured by the CCI; the typical price hlc3 is the classic choice."),
35024
+ floatInput("upper", "Upper Level", 100, -1e3, 1e3, 10, "Overbought reference level, +100 classically."),
35025
+ floatInput("lower", "Lower Level", -100, -1e3, 1e3, 10, "Oversold reference level, \u2212100 classically."),
35026
+ colorInput(NEUTRAL, "CCI Line", "color", "Color of the CCI line."),
35027
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the CCI reads above zero."),
35028
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the CCI reads below zero."),
35029
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the upper, lower and zero reference lines.")
35030
+ ],
34452
35031
  compute: (bars, inputs) => {
34453
35032
  const len = num3(inputs, "length", 20);
34454
35033
  const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
34455
35034
  const basis = sma(src, len);
34456
- const dev = new Array(src.length).fill(Number.NaN);
34457
- for (let i = len - 1; i < src.length; i++) {
34458
- const mean = basis[i];
34459
- if (!Number.isFinite(mean)) continue;
34460
- let d = 0;
34461
- for (let k = i - len + 1; k <= i; k++) d += Math.abs(src[k] - mean);
34462
- dev[i] = d / len;
34463
- }
35035
+ const dev = meanDev(src, len);
34464
35036
  const values = src.map((x, i) => {
34465
35037
  const b = basis[i];
34466
35038
  const d = dev[i];
34467
- return Number.isFinite(b) && Number.isFinite(d) && d !== 0 ? (x - b) / (0.015 * d) : Number.NaN;
35039
+ if (!Number.isFinite(b) || !Number.isFinite(d)) return Number.NaN;
35040
+ return d === 0 ? 0 : (x - b) / (CCI_SCALING * d);
35041
+ });
35042
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
35043
+ return centeredOscillator({
35044
+ key: "cci",
35045
+ title: "CCI",
35046
+ values,
35047
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
35048
+ zeroInk: levelInk,
35049
+ zeroLineStyle: "dotted",
35050
+ extraLevels: [
35051
+ { key: "upper", price: num3(inputs, "upper", 100), color: levelInk, lineStyle: "dashed", title: "Upper Level" },
35052
+ { key: "lower", price: num3(inputs, "lower", -100), color: levelInk, lineStyle: "dashed", title: "Lower Level" }
35053
+ ]
34468
35054
  });
34469
- return {
34470
- plots: [{ key: "cci", title: "CCI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34471
- levels: guideLevels(-100, 100, 0)
34472
- };
34473
35055
  }
34474
35056
  };
34475
35057
  var williamsR = {
@@ -34477,31 +35059,78 @@ var williamsR = {
34477
35059
  title: "Williams %R",
34478
35060
  shortTitle: "%R",
34479
35061
  overlay: false,
34480
- inputs: [lengthInput(14), colorInput()],
35062
+ inputs: [
35063
+ lengthInput(14, "length", "Length", 5e3, "Lookback length of the highest high / lowest low range. 14 is the classic setting."),
35064
+ sourceInput("Close", "source", "Source", "Series located within the lookback range. Close is the classic choice."),
35065
+ ...thresholdInputs(-20, -80, -100, 0),
35066
+ colorInput(NEUTRAL, "%R Line", "color", "Color of the %R line between the thresholds. Toward either level it blends into that level\u2019s color."),
35067
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
35068
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level.")
35069
+ ],
34481
35070
  compute: (bars, inputs) => {
34482
- const values = map(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "length", 14)), (x) => x - 100);
34483
- return {
34484
- plots: [{ key: "wr", title: "%R", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34485
- levels: guideLevels(-80, -20)
34486
- };
35071
+ const len = num3(inputs, "length", 14);
35072
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
35073
+ const hh = highest(highs(bars), len);
35074
+ const ll = lowest(lows(bars), len);
35075
+ const values = src.map((x, i) => {
35076
+ const h = hh[i];
35077
+ const l = ll[i];
35078
+ return Number.isFinite(h) && Number.isFinite(l) && h > l ? -100 * (h - x) / (h - l) : Number.NaN;
35079
+ });
35080
+ return boundedOscillator({
35081
+ key: "wr",
35082
+ title: "%R",
35083
+ values,
35084
+ scale: { top: 0, bottom: -100 },
35085
+ overbought: num3(inputs, "overbought", -20),
35086
+ oversold: num3(inputs, "oversold", -80),
35087
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
35088
+ levelInk: NEUTRAL
35089
+ });
34487
35090
  }
34488
35091
  };
34489
- var cmo = {
35092
+ var cmoSpec = {
34490
35093
  type: "chande-momentum-oscillator",
34491
35094
  title: "Chande Momentum Oscillator",
34492
35095
  shortTitle: "CMO",
34493
35096
  overlay: false,
34494
- inputs: [lengthInput(9), sourceInput(), colorInput()],
35097
+ inputs: [
35098
+ lengthInput(9, "length", "Length", 5e3, "Number of bars over which up moves and down moves are summed."),
35099
+ sourceInput("Close", "source", "Source", "Price series used to measure bar-to-bar changes."),
35100
+ floatInput("overbought", "Overbought Level", 50, 0, 100, 1, "Level above which the oscillator is considered overbought."),
35101
+ floatInput("oversold", "Oversold Level", -50, -100, 0, 1, "Level below which the oscillator is considered oversold."),
35102
+ colorInput(NEUTRAL, "CMO Line", "color", "Color of the CMO line."),
35103
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while momentum is positive, and shading of the oversold zone."),
35104
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while momentum is negative, and shading of the overbought zone."),
35105
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought, oversold, and zero lines.")
35106
+ ],
34495
35107
  compute: (bars, inputs) => {
34496
35108
  const len = num3(inputs, "length", 9);
34497
35109
  const d = change(sourceValues(bars, str3(inputs, "source", "Close")));
34498
- const su = sum(map(d, (x) => Math.max(x, 0)), len);
34499
- const sd = sum(map(d, (x) => Math.max(-x, 0)), len);
34500
- const values = zip(su, sd, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
34501
- return {
34502
- plots: [{ key: "cmo", title: "CMO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34503
- levels: guideLevels(-50, 50, 0)
34504
- };
35110
+ const up = sum(map(d, (x) => Math.max(x, 0)), len);
35111
+ const down = sum(map(d, (x) => Math.max(-x, 0)), len);
35112
+ const values = zip(up, down, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
35113
+ const bull = str3(inputs, "bullColor", BULLISH);
35114
+ const bear = str3(inputs, "bearColor", BEARISH);
35115
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
35116
+ const overbought = num3(inputs, "overbought", 50);
35117
+ const oversold = num3(inputs, "oversold", -50);
35118
+ const out = centeredOscillator({
35119
+ key: "cmo",
35120
+ title: "CMO",
35121
+ values,
35122
+ ink: { bull, bear, neutral: str3(inputs, "color", NEUTRAL) },
35123
+ zeroInk: levelInk,
35124
+ zeroLineStyle: "dotted",
35125
+ extraLevels: [
35126
+ { key: "overbought", price: overbought, color: levelInk, lineStyle: "dashed", title: "Overbought" },
35127
+ { key: "oversold", price: oversold, color: levelInk, lineStyle: "dashed", title: "Oversold" }
35128
+ ]
35129
+ });
35130
+ const zones = thresholdZones(values.length, { overbought, oversold, top: 100, bottom: -100 }, { overbought: bear, oversold: bull });
35131
+ out.plots.push(...zones.plots);
35132
+ out.bands = [...out.bands ?? [], ...zones.bands];
35133
+ return out;
34505
35134
  }
34506
35135
  };
34507
35136
  var connorsRsi = {
@@ -34510,10 +35139,14 @@ var connorsRsi = {
34510
35139
  shortTitle: "CRSI",
34511
35140
  overlay: false,
34512
35141
  inputs: [
34513
- lengthInput(3, "rsiLength", "RSI length"),
34514
- lengthInput(2, "streakLength", "Streak RSI length"),
34515
- lengthInput(100, "rankLength", "Percent-rank length"),
34516
- colorInput()
35142
+ lengthInput(3, "rsiLength", "RSI Length", 5e3, "Length of the Wilder RSI applied to the closing price."),
35143
+ lengthInput(2, "streakLength", "Streak RSI Length", 5e3, "Length of the Wilder RSI applied to the up/down close streak."),
35144
+ lengthInput(100, "rankLength", "Percent Rank Length", 5e3, "Lookback of the percent rank applied to the 1-bar rate of change of the closing price."),
35145
+ ...thresholdInputs(90, 10),
35146
+ colorInput(NEUTRAL, "CRSI Line", "color", "Color of the Connors RSI line."),
35147
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Line, fill and zone color toward and below the oversold level."),
35148
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Line, fill and zone color toward and above the overbought level."),
35149
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought and oversold levels.")
34517
35150
  ],
34518
35151
  compute: (bars, inputs) => {
34519
35152
  const c = closes(bars);
@@ -34531,10 +35164,17 @@ var connorsRsi = {
34531
35164
  const z = r[i];
34532
35165
  return Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) ? (x + y + z) / 3 : Number.NaN;
34533
35166
  });
34534
- return {
34535
- plots: [{ key: "crsi", title: "CRSI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34536
- levels: guideLevels(20, 80, 50)
34537
- };
35167
+ return boundedOscillator({
35168
+ key: "crsi",
35169
+ title: "Connors RSI",
35170
+ values,
35171
+ scale: { top: 100, bottom: 0 },
35172
+ overbought: num3(inputs, "overbought", 90),
35173
+ oversold: num3(inputs, "oversold", 10),
35174
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
35175
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
35176
+ midpointWash: true
35177
+ });
34538
35178
  }
34539
35179
  };
34540
35180
  var fisher = {
@@ -34542,7 +35182,13 @@ var fisher = {
34542
35182
  title: "Fisher Transform",
34543
35183
  shortTitle: "Fisher",
34544
35184
  overlay: false,
34545
- inputs: [lengthInput(9)],
35185
+ inputs: [
35186
+ 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."),
35187
+ colorInput(NEUTRAL, "Fisher Line", "color", "Color of the Fisher line when it sits exactly on its trigger."),
35188
+ colorInput(NEUTRAL, "Trigger Line", "triggerColor", "Color of the trigger line, the Fisher value delayed one bar."),
35189
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the Fisher line and fill while it is above the trigger."),
35190
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the Fisher line and fill while it is below the trigger.")
35191
+ ],
34546
35192
  compute: (bars, inputs) => {
34547
35193
  const len = num3(inputs, "length", 9);
34548
35194
  const hl2 = sourceValues(bars, "HL2");
@@ -34550,23 +35196,39 @@ var fisher = {
34550
35196
  const lo = lowest(hl2, len);
34551
35197
  const n = bars.length;
34552
35198
  const out = new Array(n).fill(Number.NaN);
35199
+ let position = 0;
34553
35200
  let value = 0;
34554
- let fish = 0;
34555
35201
  for (let i = 0; i < n; i++) {
34556
35202
  const h = hi[i];
34557
35203
  const l = lo[i];
34558
35204
  if (!Number.isFinite(h) || !Number.isFinite(l)) continue;
34559
- const norm = h === l ? 0 : (hl2[i] - l) / (h - l) - 0.5;
34560
- value = Math.max(-0.999, Math.min(0.999, 0.66 * norm + 0.67 * value));
34561
- fish = 0.5 * Math.log((1 + value) / (1 - value)) + 0.5 * fish;
34562
- out[i] = fish;
34563
- }
35205
+ const raw = h > l ? (hl2[i] - l) / (h - l) - 0.5 : 0;
35206
+ position = 0.66 * raw + 0.67 * position;
35207
+ position = position > 0.99 ? 0.999 : position < -0.99 ? -0.999 : position;
35208
+ value = 0.5 * Math.log((1 + position) / (1 - position)) + 0.5 * value;
35209
+ out[i] = value;
35210
+ }
35211
+ const trigger = shift(out, 1);
35212
+ const neutral = str3(inputs, "color", NEUTRAL);
35213
+ const bull = str3(inputs, "bullColor", BULLISH);
35214
+ const bear = str3(inputs, "bearColor", BEARISH);
35215
+ const inkOf = (i) => {
35216
+ const f = out[i];
35217
+ const t = trigger[i];
35218
+ if (!Number.isFinite(t)) return neutral;
35219
+ return f > t ? bull : f < t ? bear : neutral;
35220
+ };
34564
35221
  return {
34565
35222
  plots: [
34566
- { key: "fisher", title: "Fisher", values: out, color: SERIES_LINE, width: 2 },
34567
- { key: "trigger", title: "Trigger", values: shift(out, 1), color: WARNING }
35223
+ { key: "fisher", title: "Fisher", values: out, color: neutral, width: 2, colors: out.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) },
35224
+ { key: "trigger", title: "Trigger", values: trigger, color: str3(inputs, "triggerColor", NEUTRAL) }
34568
35225
  ],
34569
- levels: ZERO_LEVEL
35226
+ bands: [{ key: "spread", from: "fisher", to: "trigger", color: transp(neutral, 80), colors: out.map((x, i) => Number.isFinite(x) ? transp(inkOf(i), 80) : null) }],
35227
+ levels: [
35228
+ { key: "upper", price: 1.5, color: NEUTRAL, lineStyle: "dashed", title: "Upper Level" },
35229
+ { key: "lower", price: -1.5, color: NEUTRAL, lineStyle: "dashed", title: "Lower Level" },
35230
+ { key: "zero", price: 0, color: NEUTRAL, lineStyle: "dotted", title: "Zero Line" }
35231
+ ]
34570
35232
  };
34571
35233
  }
34572
35234
  };
@@ -34574,93 +35236,180 @@ var trix = {
34574
35236
  type: "trix",
34575
35237
  title: "TRIX",
34576
35238
  overlay: false,
34577
- inputs: [lengthInput(18), colorInput()],
35239
+ inputs: [
35240
+ lengthInput(18, "length", "Length", 5e3, "Length of each of the three EMA smoothing passes applied to the close price."),
35241
+ lengthInput(9, "signalLength", "Signal Length", 500, "Length of the EMA applied to TRIX to form the signal line."),
35242
+ colorInput(NEUTRAL, "TRIX Color", "color", "Color of the TRIX line."),
35243
+ colorInput(WARNING, "Signal Color", "signalColor", "Color of the signal line."),
35244
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the TRIX line and its gradient fill while TRIX holds above zero."),
35245
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the TRIX line and its gradient fill while TRIX holds below zero.")
35246
+ ],
34578
35247
  compute: (bars, inputs) => {
34579
35248
  const len = num3(inputs, "length", 18);
34580
- const logClose = map(closes(bars), Math.log);
34581
- const smooth = ema(ema(ema(logClose, len), len), len);
34582
- const values = map(change(smooth), (x) => x * 1e4);
34583
- return {
34584
- plots: [{ key: "trix", title: "TRIX", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34585
- levels: ZERO_LEVEL
34586
- };
35249
+ const smooth = ema(ema(ema(closes(bars), len), len), len);
35250
+ const values = roc(smooth, 1);
35251
+ return centeredOscillator({
35252
+ key: "trix",
35253
+ title: "TRIX",
35254
+ values,
35255
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
35256
+ zeroInk: NEUTRAL,
35257
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(values, num3(inputs, "signalLength", 9)), color: str3(inputs, "signalColor", WARNING) }]
35258
+ });
34587
35259
  }
34588
35260
  };
34589
- function tsiLine(src, long, short) {
35261
+ function tsiRatio(src, long, short) {
34590
35262
  const mom = change(src);
34591
35263
  const numer = ema(ema(mom, long), short);
34592
35264
  const denom = ema(ema(map(mom, Math.abs), long), short);
34593
- return zip(numer, denom, (a, b) => b === 0 ? 0 : 100 * a / b);
35265
+ return zip(numer, denom, (a, b) => b === 0 ? 0 : a / b);
34594
35266
  }
34595
35267
  var tsi = {
34596
35268
  type: "true-strength-index",
34597
35269
  title: "True Strength Index",
34598
35270
  shortTitle: "TSI",
34599
35271
  overlay: false,
34600
- inputs: [lengthInput(25, "longLength", "Long length"), lengthInput(13, "shortLength", "Short length"), lengthInput(13, "signalLength", "Signal length")],
35272
+ inputs: [
35273
+ lengthInput(25, "longLength", "Long Length", 5e3, "Length of the first (long) EMA applied to the 1-bar price change."),
35274
+ lengthInput(13, "shortLength", "Short Length", 5e3, "Length of the second (short) EMA applied to the long-smoothed price change."),
35275
+ lengthInput(13, "signalLength", "Signal Length", 500, "Length of the EMA of TSI used as the signal line."),
35276
+ colorInput(NEUTRAL, "TSI", "color", "Color of the TSI line."),
35277
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line."),
35278
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the TSI line and its gradient fill while TSI holds above zero."),
35279
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the TSI line and its gradient fill while TSI holds below zero.")
35280
+ ],
34601
35281
  compute: (bars, inputs) => {
34602
- const line = tsiLine(closes(bars), num3(inputs, "longLength", 25), num3(inputs, "shortLength", 13));
34603
- return {
34604
- plots: [
34605
- { key: "tsi", title: "TSI", values: line, color: SERIES_LINE, width: 2 },
34606
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: WARNING }
34607
- ],
34608
- levels: ZERO_LEVEL
34609
- };
35282
+ const values = map(tsiRatio(closes(bars), num3(inputs, "longLength", 25), num3(inputs, "shortLength", 13)), (x) => 100 * x);
35283
+ return centeredOscillator({
35284
+ key: "tsi",
35285
+ title: "TSI",
35286
+ values,
35287
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
35288
+ zeroInk: NEUTRAL,
35289
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(values, num3(inputs, "signalLength", 13)), color: str3(inputs, "signalColor", WARNING) }]
35290
+ });
34610
35291
  }
34611
35292
  };
35293
+ var SMI_INDICATOR = "Indicator";
35294
+ var SMI_OSCILLATOR = "Oscillator";
34612
35295
  var smiErgodic = {
34613
35296
  type: "smi-ergodic",
34614
35297
  title: "SMI Ergodic Oscillator",
34615
35298
  shortTitle: "SMIE",
34616
35299
  overlay: false,
34617
- inputs: [lengthInput(5, "shortLength", "Short length"), lengthInput(20, "longLength", "Long length"), lengthInput(5, "signalLength", "Signal length")],
35300
+ inputs: [
35301
+ lengthInput(20, "longLength", "Long Length", 5e3, "Length of the first EMA smoothing applied to the momentum of the closing price."),
35302
+ lengthInput(5, "shortLength", "Short Length", 5e3, "Length of the second EMA smoothing applied to the momentum of the closing price."),
35303
+ lengthInput(5, "signalLength", "Signal Length", 500, "Length of the EMA of the SMI that forms the signal line."),
35304
+ 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."),
35305
+ colorInput(NEUTRAL, "SMI", "color", "Color of the SMI line while it sits exactly on its signal line."),
35306
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line."),
35307
+ colorInput(BULLISH, "Oscillator Grow", "growColor", "Color of the rising oscillator histogram, and of the SMI line while it leads its signal."),
35308
+ colorInput(BEARISH, "Oscillator Fall", "fallColor", "Color of the falling oscillator histogram, and of the SMI line while it lags its signal.")
35309
+ ],
34618
35310
  compute: (bars, inputs) => {
34619
- const line = tsiLine(closes(bars), num3(inputs, "longLength", 20), num3(inputs, "shortLength", 5));
34620
- const signal = ema(line, num3(inputs, "signalLength", 5));
34621
- const osc = sub(line, signal);
35311
+ const smi = tsiRatio(closes(bars), num3(inputs, "longLength", 20), num3(inputs, "shortLength", 5));
35312
+ const signal = ema(smi, num3(inputs, "signalLength", 5));
35313
+ const grow = str3(inputs, "growColor", BULLISH);
35314
+ const fall = str3(inputs, "fallColor", BEARISH);
35315
+ const zero = [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed", title: "Zero" }];
35316
+ if (str3(inputs, "display", SMI_INDICATOR) === SMI_OSCILLATOR) {
35317
+ const osc = sub(smi, signal);
35318
+ const colors = osc.map((x, i) => {
35319
+ if (!Number.isFinite(x)) return null;
35320
+ const prev2 = i > 0 && Number.isFinite(osc[i - 1]) ? osc[i - 1] : x;
35321
+ return x > prev2 ? grow : fall;
35322
+ });
35323
+ return { plots: [{ key: "osc", title: "Oscillator", values: osc, kind: "columns", color: grow, colors, base: 0 }], levels: zero };
35324
+ }
35325
+ const neutral = str3(inputs, "color", NEUTRAL);
35326
+ const inkOf = (i) => {
35327
+ const a = smi[i];
35328
+ const b = signal[i];
35329
+ if (!Number.isFinite(b)) return neutral;
35330
+ return a > b ? grow : a < b ? fall : neutral;
35331
+ };
34622
35332
  return {
34623
35333
  plots: [
34624
- { key: "osc", title: "Oscillator", values: osc, kind: "histogram", color: BULLISH, colors: histColors(osc), base: 0 },
34625
- { key: "smi", title: "SMI", values: line, color: SERIES_LINE, width: 2 },
34626
- { key: "signal", title: "Signal", values: signal, color: WARNING }
35334
+ { key: "smi", title: "SMI", values: smi, color: neutral, colors: smi.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) },
35335
+ { key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) }
34627
35336
  ],
34628
- levels: ZERO_LEVEL
35337
+ bands: [{ key: "spread", from: "smi", to: "signal", color: transp(neutral, 75), colors: smi.map((x, i) => Number.isFinite(x) ? transp(inkOf(i), 75) : null) }],
35338
+ levels: zero
34629
35339
  };
34630
35340
  }
34631
35341
  };
35342
+ var STC_MID = 50;
35343
+ var TRIGGERS = "Trigger Levels";
34632
35344
  var stc = {
34633
35345
  type: "schaff-trend-cycle",
34634
35346
  title: "Schaff Trend Cycle",
34635
35347
  shortTitle: "Schaff Trend Cycle",
34636
35348
  overlay: false,
34637
- inputs: [lengthInput(10, "cycleLength", "Cycle length"), lengthInput(23, "fastLength", "Fast length"), lengthInput(50, "slowLength", "Slow length")],
35349
+ inputs: [
35350
+ sourceInput("Close", "source", "Source", "Price series used to build the MACD that the trend cycle is measured on."),
35351
+ lengthInput(23, "fastLength", "Fast Length", 5e3, "Fast EMA length of the underlying MACD."),
35352
+ lengthInput(50, "slowLength", "Slow Length", 5e3, "Slow EMA length of the underlying MACD."),
35353
+ { ...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 },
35354
+ 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."),
35355
+ { ...floatInput("upper", "Upper Trigger", 75, 0, 100, 1, "Upper trigger line. A cross down through it flags an emerging downtrend."), group: TRIGGERS },
35356
+ { ...floatInput("lower", "Lower Trigger", 25, 0, 100, 1, "Lower trigger line. A cross up through it flags an emerging uptrend."), group: TRIGGERS },
35357
+ 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."),
35358
+ 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."),
35359
+ colorInput(NEUTRAL, "Levels Color", "levelsColor", "Color of the dashed trigger lines and the dotted midline."),
35360
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Vertical gradient between the midline and the trend cycle, fading out toward the midline.")
35361
+ ],
34638
35362
  compute: (bars, inputs) => {
34639
35363
  const len = num3(inputs, "cycleLength", 10);
34640
- const src = closes(bars);
35364
+ const factor = num3(inputs, "factor", 0.5);
35365
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
34641
35366
  const macdLine = sub(ema(src, num3(inputs, "fastLength", 23)), ema(src, num3(inputs, "slowLength", 50)));
34642
- const stochOf = (v) => {
35367
+ const stochNorm = (v) => {
34643
35368
  const lo = lowest(v, len);
34644
35369
  const hi = highest(v, len);
34645
35370
  const out = new Array(v.length).fill(Number.NaN);
34646
- let prevRaw = Number.NaN;
34647
- let smoothed = Number.NaN;
35371
+ let pct = Number.NaN;
34648
35372
  for (let i = 0; i < v.length; i++) {
34649
35373
  const x = v[i];
34650
35374
  const l = lo[i];
34651
35375
  const h = hi[i];
34652
35376
  if (!Number.isFinite(x) || !Number.isFinite(l) || !Number.isFinite(h)) continue;
34653
- const raw = h > l ? (x - l) / (h - l) * 100 : Number.isFinite(prevRaw) ? prevRaw : 50;
34654
- prevRaw = raw;
34655
- smoothed = Number.isFinite(smoothed) ? smoothed + 0.5 * (raw - smoothed) : raw;
35377
+ if (h > l) pct = 100 * (x - l) / (h - l);
35378
+ out[i] = pct;
35379
+ }
35380
+ return out;
35381
+ };
35382
+ const smoothStep = (v) => {
35383
+ const out = new Array(v.length).fill(Number.NaN);
35384
+ let smoothed = Number.NaN;
35385
+ for (let i = 0; i < v.length; i++) {
35386
+ const x = v[i];
35387
+ if (!Number.isFinite(x)) continue;
35388
+ smoothed = Number.isFinite(smoothed) ? smoothed + factor * (x - smoothed) : x;
34656
35389
  out[i] = smoothed;
34657
35390
  }
34658
35391
  return out;
34659
35392
  };
34660
- const values = stochOf(stochOf(macdLine));
35393
+ const values = smoothStep(stochNorm(smoothStep(stochNorm(macdLine))));
35394
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
35395
+ const bull = str3(inputs, "bullColor", BULLISH);
35396
+ const bear = str3(inputs, "bearColor", BEARISH);
35397
+ const plots = [
35398
+ { key: "stc", title: "Schaff Trend Cycle", values, color: bear, width: 2, colors: values.map((x) => Number.isFinite(x) ? x > STC_MID ? bull : bear : null) }
35399
+ ];
35400
+ const bands = [];
35401
+ if (bool(inputs, "gradientFill", true)) {
35402
+ plots.push(anchorPlot("midAnchor", "Midline Anchor", values.length, STC_MID));
35403
+ bands.push({ key: "wash", from: "stc", to: "midAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, STC_MID, (x) => x > STC_MID ? bull : bear) });
35404
+ }
34661
35405
  return {
34662
- plots: [{ key: "stc", title: "STC", values, color: SERIES_LINE, width: 2 }],
34663
- levels: guideLevels(25, 75)
35406
+ plots,
35407
+ bands,
35408
+ levels: [
35409
+ { key: "upper", price: num3(inputs, "upper", 75), color: transp(levelInk, 25), lineStyle: "dashed", title: "Upper Trigger" },
35410
+ { key: "lower", price: num3(inputs, "lower", 25), color: transp(levelInk, 25), lineStyle: "dashed", title: "Lower Trigger" },
35411
+ { key: "midline", price: STC_MID, color: transp(levelInk, 60), lineStyle: "dotted", title: "Midline" }
35412
+ ]
34664
35413
  };
34665
35414
  }
34666
35415
  };
@@ -34669,22 +35418,37 @@ var kst = {
34669
35418
  title: "Know Sure Thing",
34670
35419
  shortTitle: "KST",
34671
35420
  overlay: false,
34672
- inputs: [sourceInput(), lengthInput(9, "signalLength", "Signal length")],
35421
+ inputs: [
35422
+ lengthInput(10, "rocLength1", "ROC Length #1", 5e3, "Lookback of the first (shortest) rate of change."),
35423
+ lengthInput(15, "rocLength2", "ROC Length #2", 5e3, "Lookback of the second rate of change."),
35424
+ lengthInput(20, "rocLength3", "ROC Length #3", 5e3, "Lookback of the third rate of change."),
35425
+ lengthInput(30, "rocLength4", "ROC Length #4", 5e3, "Lookback of the fourth (longest) rate of change."),
35426
+ lengthInput(10, "smaLength1", "SMA Length #1", 5e3, "Smoothing length applied to the first rate of change (RCMA #1)."),
35427
+ lengthInput(10, "smaLength2", "SMA Length #2", 5e3, "Smoothing length applied to the second rate of change (RCMA #2)."),
35428
+ lengthInput(10, "smaLength3", "SMA Length #3", 5e3, "Smoothing length applied to the third rate of change (RCMA #3)."),
35429
+ lengthInput(15, "smaLength4", "SMA Length #4", 5e3, "Smoothing length applied to the fourth rate of change (RCMA #4)."),
35430
+ lengthInput(9, "signalLength", "Signal Length", 500, "Simple moving average length of the signal line."),
35431
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the KST line while above zero."),
35432
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the KST line while below zero."),
35433
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line.")
35434
+ ],
34673
35435
  compute: (bars, inputs) => {
34674
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
34675
- const line = [
34676
- sma(roc(src, 10), 10),
34677
- sma(roc(src, 15), 10),
34678
- sma(roc(src, 20), 10),
34679
- sma(roc(src, 30), 15)
34680
- ].reduce((acc, r, i) => zip(acc, r, (a, b) => a + b * (i + 1)), new Array(src.length).fill(0));
34681
- return {
34682
- plots: [
34683
- { key: "kst", title: "KST", values: line, color: SERIES_LINE, width: 2 },
34684
- { key: "signal", title: "Signal", values: sma(line, num3(inputs, "signalLength", 9)), color: WARNING }
34685
- ],
34686
- levels: ZERO_LEVEL
34687
- };
35436
+ const c = closes(bars);
35437
+ const rcma = (rocKey, rocDefault, smaKey, smaDefault) => sma(roc(c, num3(inputs, rocKey, rocDefault)), num3(inputs, smaKey, smaDefault));
35438
+ const values = [rcma("rocLength1", 10, "smaLength1", 10), rcma("rocLength2", 15, "smaLength2", 10), rcma("rocLength3", 20, "smaLength3", 10), rcma("rocLength4", 30, "smaLength4", 15)].reduce(
35439
+ (acc, r, i) => zip(acc, r, (a, b) => a + b * (i + 1)),
35440
+ new Array(c.length).fill(0)
35441
+ );
35442
+ const bull = str3(inputs, "bullColor", BULLISH);
35443
+ return centeredOscillator({
35444
+ key: "kst",
35445
+ title: "KST",
35446
+ values,
35447
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
35448
+ zeroInk: NEUTRAL,
35449
+ valueTransparency: 60,
35450
+ extraPlots: [{ key: "signal", title: "Signal", values: sma(values, num3(inputs, "signalLength", 9)), color: str3(inputs, "signalColor", WARNING) }]
35451
+ });
34688
35452
  }
34689
35453
  };
34690
35454
  var coppock = {
@@ -34693,19 +35457,25 @@ var coppock = {
34693
35457
  shortTitle: "Coppock",
34694
35458
  overlay: false,
34695
35459
  inputs: [
34696
- lengthInput(10, "wmaLength", "WMA length"),
34697
- lengthInput(14, "longRoc", "Long RoC length"),
34698
- lengthInput(11, "shortRoc", "Short RoC length"),
34699
- sourceInput(),
34700
- colorInput()
35460
+ lengthInput(10, "wmaLength", "WMA Length", 5e3, "Length of the weighted moving average applied to the sum of the two rates of change."),
35461
+ lengthInput(14, "longRoc", "Long RoC Length", 5e3, "Lookback of the long rate of change, in bars. The original definition uses 14 monthly closes."),
35462
+ lengthInput(11, "shortRoc", "Short RoC Length", 5e3, "Lookback of the short rate of change, in bars. The original definition uses 11 monthly closes."),
35463
+ sourceInput("Close", "source", "Source", "Price series used for the rate of change calculations."),
35464
+ colorInput(NEUTRAL, "Coppock Curve", "color", "Color of the Coppock Curve line."),
35465
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the curve reads above zero."),
35466
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the curve reads below zero."),
35467
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
34701
35468
  ],
34702
35469
  compute: (bars, inputs) => {
34703
35470
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
34704
35471
  const values = wma(zip(roc(src, num3(inputs, "longRoc", 14)), roc(src, num3(inputs, "shortRoc", 11)), (a, b) => a + b), num3(inputs, "wmaLength", 10));
34705
- return {
34706
- plots: [{ key: "coppock", title: "Coppock", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34707
- levels: ZERO_LEVEL
34708
- };
35472
+ return centeredOscillator({
35473
+ key: "coppock",
35474
+ title: "Coppock Curve",
35475
+ values,
35476
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
35477
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
35478
+ });
34709
35479
  }
34710
35480
  };
34711
35481
  var dpo = {
@@ -34713,42 +35483,76 @@ var dpo = {
34713
35483
  title: "Detrended Price Oscillator",
34714
35484
  shortTitle: "DPO",
34715
35485
  overlay: false,
34716
- inputs: [lengthInput(21), colorInput()],
35486
+ inputs: [
35487
+ 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."),
35488
+ sourceInput("Close", "source", "Source", "Price series used in the detrending calculation."),
35489
+ 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."),
35490
+ colorInput(NEUTRAL, "DPO", "color", "Color of the line when it sits exactly at zero."),
35491
+ colorInput(BULLISH, "Above Zero", "bullColor", "Color of the line and fill while the oscillator is above zero."),
35492
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while the oscillator is below zero."),
35493
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
35494
+ ],
34717
35495
  compute: (bars, inputs) => {
34718
35496
  const len = num3(inputs, "length", 21);
34719
- const barsback = Math.floor(len / 2) + 1;
34720
- const values = sub(closes(bars), shift(sma(closes(bars), len), barsback));
34721
- return {
34722
- plots: [{ key: "dpo", title: "DPO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34723
- levels: ZERO_LEVEL
34724
- };
35497
+ const barsBack = Math.trunc(len / 2) + 1;
35498
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
35499
+ const basis = sma(src, len);
35500
+ const centered = bool(inputs, "centered", false);
35501
+ const values = centered ? shift(sub(shift(src, barsBack), basis), -barsBack) : sub(src, shift(basis, barsBack));
35502
+ return centeredOscillator({
35503
+ key: "dpo",
35504
+ title: "DPO",
35505
+ values,
35506
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
35507
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
35508
+ });
34725
35509
  }
34726
35510
  };
35511
+ var ROC_PERCENT = "ROC %";
35512
+ var ROC_MOMENTUM = "Momentum";
34727
35513
  var rocSpec = {
34728
35514
  type: "rate-of-change",
34729
35515
  title: "Rate of Change",
34730
35516
  shortTitle: "ROC",
34731
35517
  overlay: false,
34732
- inputs: [lengthInput(9), sourceInput(), colorInput()],
34733
- compute: (bars, inputs) => ({
34734
- plots: [
34735
- {
34736
- key: "roc",
34737
- title: "RoC",
34738
- values: roc(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 9)),
34739
- color: str3(inputs, "color", SERIES_LINE),
34740
- width: 2
34741
- }
34742
- ],
34743
- levels: ZERO_LEVEL
34744
- })
35518
+ inputs: [
35519
+ lengthInput(9, "length", "Length", 5e3, "Lookback period; the source is compared with its value this many bars ago."),
35520
+ sourceInput("Close", "source", "Source", "Series used as input for the calculation."),
35521
+ 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."),
35522
+ colorInput(NEUTRAL, "Line", "color", "Color of the line when the reading sits exactly at zero."),
35523
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the line and fill while the reading is above zero."),
35524
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line and fill while the reading is below zero."),
35525
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the line and zero, following the line color.")
35526
+ ],
35527
+ compute: (bars, inputs) => {
35528
+ const len = num3(inputs, "length", 9);
35529
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
35530
+ const values = str3(inputs, "mode", ROC_PERCENT) === ROC_MOMENTUM ? change(src, len) : roc(src, len);
35531
+ return centeredOscillator({
35532
+ key: "roc",
35533
+ title: "ROC",
35534
+ values,
35535
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
35536
+ zeroInk: NEUTRAL,
35537
+ fill: bool(inputs, "gradientFill", true)
35538
+ });
35539
+ }
34745
35540
  };
34746
35541
  var ultimate = {
34747
35542
  type: "ultimate-oscillator",
34748
35543
  title: "Ultimate Oscillator",
34749
35544
  shortTitle: "UO",
34750
35545
  overlay: false,
34751
- inputs: [lengthInput(7, "fastLength", "Fast length"), lengthInput(14, "middleLength", "Middle length"), lengthInput(28, "slowLength", "Slow length"), colorInput()],
35546
+ inputs: [
35547
+ lengthInput(7, "fastLength", "Fast Length", 5e3, "Bars in the fastest averaging window. Weighted 4 in the final blend, so it dominates the oscillator."),
35548
+ lengthInput(14, "middleLength", "Middle Length", 5e3, "Bars in the intermediate averaging window. Weighted 2 in the final blend."),
35549
+ lengthInput(28, "slowLength", "Slow Length", 5e3, "Bars in the slowest averaging window. Weighted 1 in the final blend."),
35550
+ ...thresholdInputs(70, 30, 0, 100, "Overbought", "Oversold"),
35551
+ colorInput(NEUTRAL, "Oscillator", "color", "Color of the line midway between the levels. Toward either level it blends into that level\u2019s color."),
35552
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought and oversold levels."),
35553
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line at the oversold level and of the oversold zone shading."),
35554
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line at the overbought level and of the overbought zone shading.")
35555
+ ],
34752
35556
  compute: (bars, inputs) => {
34753
35557
  const n = bars.length;
34754
35558
  const bp = new Array(n).fill(Number.NaN);
@@ -34756,10 +35560,9 @@ var ultimate = {
34756
35560
  for (let i = 0; i < n; i++) {
34757
35561
  const b = bars[i];
34758
35562
  const pc = i > 0 ? bars[i - 1].close : b.close;
34759
- const lo = Math.min(b.low, pc);
34760
- const hi = Math.max(b.high, pc);
34761
- bp[i] = b.close - lo;
34762
- tr[i] = hi - lo;
35563
+ const trueLow = Math.min(b.low, pc);
35564
+ bp[i] = b.close - trueLow;
35565
+ tr[i] = Math.max(b.high, pc) - trueLow;
34763
35566
  }
34764
35567
  const avg = (len) => zip(sum(bp, len), sum(tr, len), (a, b) => b === 0 ? Number.NaN : a / b);
34765
35568
  const a7 = avg(num3(inputs, "fastLength", 7));
@@ -34770,10 +35573,17 @@ var ultimate = {
34770
35573
  const z = a28[i];
34771
35574
  return Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) ? 100 * (4 * x + 2 * y + z) / 7 : Number.NaN;
34772
35575
  });
34773
- return {
34774
- plots: [{ key: "uo", title: "UO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34775
- levels: guideLevels(30, 70, 50)
34776
- };
35576
+ return boundedOscillator({
35577
+ key: "uo",
35578
+ title: "UO",
35579
+ values,
35580
+ scale: { top: 100, bottom: 0 },
35581
+ overbought: num3(inputs, "overbought", 70),
35582
+ oversold: num3(inputs, "oversold", 30),
35583
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
35584
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
35585
+ zoneTransparency: 91
35586
+ });
34777
35587
  }
34778
35588
  };
34779
35589
  var rvgi = {
@@ -34781,7 +35591,11 @@ var rvgi = {
34781
35591
  title: "Relative Vigor Index",
34782
35592
  shortTitle: "RVGI",
34783
35593
  overlay: false,
34784
- inputs: [lengthInput(10)],
35594
+ inputs: [
35595
+ lengthInput(10, "length", "Length", 5e3, "Number of bars summed for the smoothed numerator (close \u2212 open) and denominator (high \u2212 low)."),
35596
+ colorInput(BULLISH, "RVGI", "color", "Color of the RVGI line."),
35597
+ colorInput(BEARISH, "Signal", "signalColor", "Color of the signal line.")
35598
+ ],
34785
35599
  compute: (bars, inputs) => {
34786
35600
  const len = num3(inputs, "length", 10);
34787
35601
  const co = swma(sub(closes(bars), opens(bars)));
@@ -34789,31 +35603,49 @@ var rvgi = {
34789
35603
  const line = zip(sum(co, len), sum(hl, len), (a, b) => b === 0 ? 0 : a / b);
34790
35604
  return {
34791
35605
  plots: [
34792
- { key: "rvgi", title: "RVGI", values: line, color: SERIES_LINE, width: 2 },
34793
- { key: "signal", title: "Signal", values: swma(line), color: WARNING }
35606
+ { key: "rvgi", title: "RVGI", values: line, color: str3(inputs, "color", BULLISH) },
35607
+ { key: "signal", title: "Signal", values: swma(line), color: str3(inputs, "signalColor", BEARISH) }
34794
35608
  ],
34795
- levels: ZERO_LEVEL
35609
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed", title: "Zero" }]
34796
35610
  };
34797
35611
  }
34798
35612
  };
35613
+ var RVI_EMA = "EMA";
35614
+ var RVI_RMA = "Wilder (RMA)";
34799
35615
  var relVolatility = {
34800
35616
  type: "relative-volatility-index",
34801
35617
  title: "Relative Volatility Index",
34802
35618
  shortTitle: "RVI",
34803
35619
  overlay: false,
34804
- inputs: [lengthInput(10, "stdevLength", "StdDev length"), lengthInput(14, "smoothLength", "Smoothing length"), sourceInput(), colorInput()],
35620
+ inputs: [
35621
+ 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."),
35622
+ lengthInput(14, "smoothLength", "Smoothing Length", 5e3, "Number of bars used to smooth the up and down volatility streams."),
35623
+ 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."),
35624
+ ...thresholdInputs(80, 20),
35625
+ colorInput(NEUTRAL, "RVI Line", "color", "Color of the RVI line at the midline. Toward either threshold it blends into that threshold\u2019s color."),
35626
+ colorInput(BULLISH, "Up Volatility", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
35627
+ colorInput(BEARISH, "Down Volatility", "oversoldColor", "Color of the line and zone shading at and below the oversold level.")
35628
+ ],
34805
35629
  compute: (bars, inputs) => {
35630
+ const c = closes(bars);
35631
+ const sigma = stdev(c, num3(inputs, "stdevLength", 10));
35632
+ const d = change(c);
35633
+ const up = zip(sigma, d, (s, x) => x > 0 ? s : 0);
35634
+ const down = zip(sigma, d, (s, x) => x < 0 ? s : 0);
34806
35635
  const smoothLen = num3(inputs, "smoothLength", 14);
34807
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
34808
- const dev = stdev(src, num3(inputs, "stdevLength", 10));
34809
- const d = change(src);
34810
- const up = rma(zip(dev, d, (s, x) => x > 0 ? s : 0), smoothLen);
34811
- const dn = rma(zip(dev, d, (s, x) => x <= 0 ? s : 0), smoothLen);
34812
- const values = zip(up, dn, (u, w) => u + w === 0 ? 50 : 100 * u / (u + w));
34813
- return {
34814
- plots: [{ key: "rvi", title: "RVI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34815
- levels: guideLevels(20, 80, 50)
34816
- };
35636
+ const smooth = str3(inputs, "smoothType", RVI_EMA) === RVI_RMA ? rma : ema;
35637
+ const values = zip(smooth(up, smoothLen), smooth(down, smoothLen), (u, w) => u + w === 0 ? 0 : 100 * u / (u + w));
35638
+ return boundedOscillator({
35639
+ key: "rvi",
35640
+ title: "RVI",
35641
+ values,
35642
+ scale: { top: 100, bottom: 0 },
35643
+ overbought: num3(inputs, "overbought", 80),
35644
+ oversold: num3(inputs, "oversold", 20),
35645
+ ink: { low: str3(inputs, "oversoldColor", BEARISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BULLISH) },
35646
+ levelInk: NEUTRAL,
35647
+ width: 2
35648
+ });
34817
35649
  }
34818
35650
  };
34819
35651
  var bop = {
@@ -34821,28 +35653,43 @@ var bop = {
34821
35653
  title: "Balance of Power",
34822
35654
  shortTitle: "BOP",
34823
35655
  overlay: false,
34824
- inputs: [colorInput()],
35656
+ inputs: [
35657
+ boolInput("smoothing", "Smoothing", false, void 0, "Smooth the raw Balance of Power with a simple moving average instead of plotting the single-bar value."),
35658
+ lengthInput(14, "smoothLength", "Length", 5e3, "Simple moving average length applied when smoothing is enabled."),
35659
+ colorInput(NEUTRAL, "BOP Color", "color", "Color of the line at exactly zero."),
35660
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the Balance of Power is above zero \u2014 buyers in control of the bar."),
35661
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the Balance of Power is below zero \u2014 sellers in control of the bar.")
35662
+ ],
34825
35663
  compute: (bars, inputs) => {
34826
- const values = bars.map((b) => b.high === b.low ? 0 : (b.close - b.open) / (b.high - b.low));
34827
- return {
34828
- plots: [{ key: "bop", title: "BOP", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34829
- levels: ZERO_LEVEL
34830
- };
35664
+ const raw = bars.map((b) => b.high === b.low ? 0 : (b.close - b.open) / (b.high - b.low));
35665
+ const values = bool(inputs, "smoothing", false) ? sma(raw, num3(inputs, "smoothLength", 14)) : raw;
35666
+ return centeredOscillator({
35667
+ key: "bop",
35668
+ title: "BOP",
35669
+ values,
35670
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
35671
+ zeroInk: NEUTRAL,
35672
+ valueTransparency: 60
35673
+ });
34831
35674
  }
34832
35675
  };
34833
35676
  var elderRay = {
34834
35677
  type: "elder-ray",
34835
35678
  title: "Elder Ray",
34836
35679
  overlay: false,
34837
- inputs: [lengthInput(13)],
35680
+ inputs: [
35681
+ 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."),
35682
+ colorInput(BULLISH, "Bull Power", "bullColor", "Color of the Bull Power columns."),
35683
+ colorInput(BEARISH, "Bear Power", "bearColor", "Color of the Bear Power columns.")
35684
+ ],
34838
35685
  compute: (bars, inputs) => {
34839
35686
  const basis = ema(closes(bars), num3(inputs, "length", 13));
34840
35687
  return {
34841
35688
  plots: [
34842
- { key: "bull", title: "Bull power", values: sub(highs(bars), basis), kind: "histogram", color: BULLISH, base: 0 },
34843
- { key: "bear", title: "Bear power", values: sub(lows(bars), basis), kind: "histogram", color: BEARISH, base: 0 }
35689
+ { key: "bull", title: "Bull Power", values: sub(highs(bars), basis), kind: "columns", color: str3(inputs, "bullColor", BULLISH), base: 0 },
35690
+ { key: "bear", title: "Bear Power", values: sub(lows(bars), basis), kind: "columns", color: str3(inputs, "bearColor", BEARISH), base: 0 }
34844
35691
  ],
34845
- levels: ZERO_LEVEL
35692
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 30), lineStyle: "dashed", title: "Zero Line" }]
34846
35693
  };
34847
35694
  }
34848
35695
  };
@@ -34851,31 +35698,66 @@ var ttmSqueeze = {
34851
35698
  title: "TTM Squeeze",
34852
35699
  overlay: false,
34853
35700
  inputs: [
34854
- lengthInput(20),
34855
- { key: "bbMult", title: "Bollinger multiplier", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 },
34856
- { key: "kcMult", title: "Keltner multiplier", type: "float", defval: 1.5, min: 0.1, max: 50, step: 0.1 }
35701
+ { ...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 },
35702
+ sourceInput("Close", "source", "Source", "Price series used for the shared moving average basis, the standard deviation, and the momentum displacement."),
35703
+ floatInput("bbMult", "Bollinger Bands Multiplier", 2, 0.1, 50, 0.1, "Standard deviation multiplier for the Bollinger Bands."),
35704
+ 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."),
35705
+ colorInput(BULLISH, "Momentum Up", "bullColor", "Histogram color while momentum is positive. Bars building away from zero print solid, bars fading back print dimmed."),
35706
+ colorInput(BEARISH, "Momentum Down", "bearColor", "Histogram color while momentum is negative. Bars building away from zero print solid, bars fading back print dimmed."),
35707
+ colorInput(WARNING, "Squeeze On", "squeezeOnColor", "Zero-line dot color while both Bollinger Bands sit fully inside the Keltner Channels."),
35708
+ colorInput(NEUTRAL, "Squeeze Off", "squeezeOffColor", "Zero-line dot color while the Bollinger Bands trade outside the Keltner Channels."),
35709
+ 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.")
34857
35710
  ],
34858
35711
  compute: (bars, inputs) => {
34859
35712
  const len = num3(inputs, "length", 20);
34860
- const c = closes(bars);
34861
- const basis = sma(c, len);
34862
- const dev = map(stdev(c, len), (x) => x * num3(inputs, "bbMult", 2));
34863
- const kcRange = map(rma(trueRange(bars), len), (x) => x * num3(inputs, "kcMult", 1.5));
34864
- const donchianMid = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => (h + l) / 2);
34865
- const anchor = zip(donchianMid, basis, (d, b) => (d + b) / 2);
34866
- const mom = linreg(sub(c, anchor), len);
34867
- const squeezed = c.map((_, i) => {
35713
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
35714
+ const basis = sma(src, len);
35715
+ const dev = map(stdev(src, len), (x) => x * num3(inputs, "bbMult", 2));
35716
+ const kcRange = map(atr(bars, len), (x) => x * num3(inputs, "kcMult", 1.5));
35717
+ const rangeMid = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => (h + l) / 2);
35718
+ const anchor = zip(rangeMid, basis, (d, b) => (d + b) / 2);
35719
+ const mom = linreg(sub(src, anchor), len, 0);
35720
+ const squeezeOn = basis.map((b, i) => {
34868
35721
  const d = dev[i];
34869
35722
  const r = kcRange[i];
34870
- return Number.isFinite(d) && Number.isFinite(r) ? d < r : false;
35723
+ if (!Number.isFinite(b) || !Number.isFinite(d) || !Number.isFinite(r)) return null;
35724
+ return d < r;
34871
35725
  });
34872
- const stateColors = squeezed.map((s, i) => Number.isFinite(mom[i]) ? s ? BEARISH : BULLISH : null);
34873
- return {
34874
- plots: [
34875
- { key: "mom", title: "Momentum", values: mom, kind: "histogram", color: BULLISH, colors: histColors(mom), base: 0 },
34876
- { key: "squeeze", title: "Squeeze", values: mom.map((x) => Number.isFinite(x) ? 0 : Number.NaN), kind: "cross", color: INFO, colors: stateColors }
34877
- ]
34878
- };
35726
+ const bull = str3(inputs, "bullColor", BULLISH);
35727
+ const bear = str3(inputs, "bearColor", BEARISH);
35728
+ const onInk = str3(inputs, "squeezeOnColor", WARNING);
35729
+ const offInk = str3(inputs, "squeezeOffColor", NEUTRAL);
35730
+ const momColors = mom.map((x, i) => {
35731
+ if (!Number.isFinite(x)) return null;
35732
+ const prev2 = i > 0 && Number.isFinite(mom[i - 1]) ? mom[i - 1] : x;
35733
+ const rising = x > prev2;
35734
+ return x >= 0 ? rising ? bull : transp(bull, 60) : rising ? transp(bear, 60) : bear;
35735
+ });
35736
+ const plots = [
35737
+ { key: "mom", title: "Momentum", values: mom, kind: "columns", color: bull, colors: momColors, base: 0 },
35738
+ {
35739
+ key: "state",
35740
+ title: "Squeeze State",
35741
+ values: squeezeOn.map((s) => s == null ? Number.NaN : 0),
35742
+ kind: "circles",
35743
+ color: offInk,
35744
+ width: 2,
35745
+ colors: squeezeOn.map((s) => s == null ? null : s ? onInk : offInk)
35746
+ }
35747
+ ];
35748
+ if (bool(inputs, "markFires", true)) {
35749
+ const fires = squeezeOn.map((s, i) => s === false && squeezeOn[i - 1] === true ? 0 : Number.NaN);
35750
+ plots.push({
35751
+ key: "fire",
35752
+ title: "Squeeze Fire",
35753
+ values: fires,
35754
+ kind: "cross",
35755
+ color: offInk,
35756
+ width: 3,
35757
+ colors: fires.map((x, i) => Number.isFinite(x) ? Number.isFinite(mom[i]) ? mom[i] >= 0 ? bull : bear : offInk : null)
35758
+ });
35759
+ }
35760
+ return { plots };
34879
35761
  }
34880
35762
  };
34881
35763
  var oscillatorSpecs = [
@@ -34887,7 +35769,7 @@ var oscillatorSpecs = [
34887
35769
  awesome,
34888
35770
  cci,
34889
35771
  williamsR,
34890
- cmo,
35772
+ cmoSpec,
34891
35773
  connorsRsi,
34892
35774
  fisher,
34893
35775
  trix,
@@ -34911,15 +35793,19 @@ var aroon = {
34911
35793
  type: "aroon",
34912
35794
  title: "Aroon",
34913
35795
  overlay: false,
34914
- inputs: [lengthInput(14)],
35796
+ inputs: [
35797
+ lengthInput(14, "length", "Length", 5e3, "Lookback length used to locate the highest high and lowest low."),
35798
+ colorInput(BULLISH, "Aroon Up", "upColor", "Color of the Aroon Up line."),
35799
+ colorInput(BEARISH, "Aroon Down", "downColor", "Color of the Aroon Down line.")
35800
+ ],
34915
35801
  compute: (bars, inputs) => {
34916
35802
  const len = num3(inputs, "length", 14);
34917
- const up = map(barsSinceHighest(highs(bars), len + 1), (x) => 100 * (len - x) / len);
34918
- const down = map(barsSinceLowest(lows(bars), len + 1), (x) => 100 * (len - x) / len);
35803
+ const up = map(barsSinceHighest(highs(bars), len), (x) => 100 * (len - x) / len);
35804
+ const down = map(barsSinceLowest(lows(bars), len), (x) => 100 * (len - x) / len);
34919
35805
  return {
34920
35806
  plots: [
34921
- { key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
34922
- { key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
35807
+ { key: "up", title: "Aroon Up", values: up, color: str3(inputs, "upColor", BULLISH) },
35808
+ { key: "down", title: "Aroon Down", values: down, color: str3(inputs, "downColor", BEARISH) }
34923
35809
  ]
34924
35810
  };
34925
35811
  }
@@ -34929,10 +35815,16 @@ var adx = {
34929
35815
  title: "Average Directional Index",
34930
35816
  shortTitle: "ADX",
34931
35817
  overlay: false,
34932
- inputs: [lengthInput(14, "diLength", "DI length"), lengthInput(14, "adxLength", "ADX smoothing")],
35818
+ inputs: [
35819
+ lengthInput(14, "diLength", "DI Length", 5e3, "Wilder RMA smoothing length applied to directional movement and true range when computing +DI and \u2212DI."),
35820
+ lengthInput(14, "adxLength", "ADX Smoothing", 5e3, "Wilder RMA smoothing length applied to DX to obtain the ADX line."),
35821
+ floatInput("keyLevel", "Key Level", 25, 0, 100, 1, "Trend strength reference level; ADX above it is commonly read as a trending market."),
35822
+ colorInput(BULLISH, "+DI Color", "plusDiColor", "Color of the plus directional indicator, and of the ADX line above the key level while +DI leads."),
35823
+ colorInput(BEARISH, "-DI Color", "minusDiColor", "Color of the minus directional indicator, and of the ADX line above the key level while \u2212DI leads."),
35824
+ colorInput(NEUTRAL, "ADX Color", "adxColor", "Color of the ADX line while it sits below the key level \u2014 no trend strong enough to trade.")
35825
+ ],
34933
35826
  compute: (bars, inputs) => {
34934
35827
  const diLen = num3(inputs, "diLength", 14);
34935
- const adxLen = num3(inputs, "adxLength", 14);
34936
35828
  const n = bars.length;
34937
35829
  const plusDm = new Array(n).fill(Number.NaN);
34938
35830
  const minusDm = new Array(n).fill(Number.NaN);
@@ -34946,13 +35838,23 @@ var adx = {
34946
35838
  const plusDi = zip(rma(plusDm, diLen), atrLine, (d, a) => a === 0 ? 0 : 100 * d / a);
34947
35839
  const minusDi = zip(rma(minusDm, diLen), atrLine, (d, a) => a === 0 ? 0 : 100 * d / a);
34948
35840
  const dx = zip(plusDi, minusDi, (p, m) => p + m === 0 ? 0 : 100 * Math.abs(p - m) / (p + m));
35841
+ const adxLine = rma(dx, num3(inputs, "adxLength", 14));
35842
+ const plusInk = str3(inputs, "plusDiColor", BULLISH);
35843
+ const minusInk = str3(inputs, "minusDiColor", BEARISH);
35844
+ const neutralInk = str3(inputs, "adxColor", NEUTRAL);
35845
+ const keyLevel = num3(inputs, "keyLevel", 25);
35846
+ const adxColors = adxLine.map((x, i) => {
35847
+ if (!Number.isFinite(x)) return null;
35848
+ if (x <= keyLevel) return neutralInk;
35849
+ return (plusDi[i] ?? 0) > (minusDi[i] ?? 0) ? plusInk : minusInk;
35850
+ });
34949
35851
  return {
34950
35852
  plots: [
34951
- { key: "adx", title: "ADX", values: rma(dx, adxLen), color: SERIES_LINE, width: 2 },
34952
- { key: "plusDi", title: "+DI", values: plusDi, color: BULLISH },
34953
- { key: "minusDi", title: "-DI", values: minusDi, color: BEARISH }
35853
+ { key: "plusDi", title: "+DI", values: plusDi, color: transp(plusInk, 30) },
35854
+ { key: "minusDi", title: "-DI", values: minusDi, color: transp(minusInk, 30) },
35855
+ { key: "adx", title: "ADX", values: adxLine, color: neutralInk, width: 2, colors: adxColors }
34954
35856
  ],
34955
- levels: [{ key: "threshold", price: 25, color: NEUTRAL, lineStyle: "dotted" }]
35857
+ levels: [{ key: "keyLevel", price: keyLevel, color: NEUTRAL, lineStyle: "dashed", title: "Key Level" }]
34956
35858
  };
34957
35859
  }
34958
35860
  };
@@ -34961,7 +35863,12 @@ var vortex = {
34961
35863
  title: "Vortex Indicator",
34962
35864
  shortTitle: "VI",
34963
35865
  overlay: false,
34964
- inputs: [lengthInput(14)],
35866
+ inputs: [
35867
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to sum the vortex movements (VM+ and VM\u2212) and the true range."),
35868
+ colorInput(BULLISH, "VI+", "plusColor", "Color of the positive vortex line (VI+)."),
35869
+ colorInput(BEARISH, "VI-", "minusColor", "Color of the negative vortex line (VI\u2212)."),
35870
+ colorInput(NEUTRAL, "Baseline", "baselineColor", "Color of the dotted baseline drawn at 1.")
35871
+ ],
34965
35872
  compute: (bars, inputs) => {
34966
35873
  const len = num3(inputs, "length", 14);
34967
35874
  const n = bars.length;
@@ -34974,25 +35881,48 @@ var vortex = {
34974
35881
  const trSum = sum(trueRange(bars), len);
34975
35882
  return {
34976
35883
  plots: [
34977
- { key: "viPlus", title: "VI+", values: zip(sum(vmPlus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: BULLISH, width: 2 },
34978
- { key: "viMinus", title: "VI-", values: zip(sum(vmMinus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: BEARISH, width: 2 }
35884
+ { key: "viPlus", title: "VI+", values: zip(sum(vmPlus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: str3(inputs, "plusColor", BULLISH) },
35885
+ { key: "viMinus", title: "VI-", values: zip(sum(vmMinus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: str3(inputs, "minusColor", BEARISH) }
34979
35886
  ],
34980
- levels: [{ key: "one", price: 1, color: NEUTRAL, lineStyle: "dotted" }]
35887
+ levels: [{ key: "baseline", price: 1, color: str3(inputs, "baselineColor", NEUTRAL), lineStyle: "dotted", title: "Baseline" }]
34981
35888
  };
34982
35889
  }
34983
35890
  };
34984
35891
  var trendSpecs = [aroon, adx, vortex];
34985
35892
 
34986
35893
  // src/core/native-indicators/classics/volatility.ts
35894
+ function expansionColors(values, calm, expanding) {
35895
+ return values.map((x, i) => {
35896
+ if (!Number.isFinite(x)) return null;
35897
+ const prev2 = i > 0 && Number.isFinite(values[i - 1]) ? values[i - 1] : x;
35898
+ return x > prev2 ? expanding : calm;
35899
+ });
35900
+ }
35901
+ function washedToZero(key, title, values, colors, fallback, valueTransparency, width = 1) {
35902
+ return {
35903
+ plots: [{ key, title, values, color: fallback, width, colors }, anchorPlot("zeroAnchor", "Zero Anchor", values.length, 0)],
35904
+ bands: [{ key: "wash", from: key, to: "zeroAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, 0, (_x, i) => colors[i] ?? fallback, valueTransparency) }]
35905
+ };
35906
+ }
34987
35907
  var atrSpec = {
34988
35908
  type: "average-true-range",
34989
35909
  title: "Average True Range",
34990
35910
  shortTitle: "ATR",
34991
35911
  overlay: false,
34992
- inputs: [lengthInput(14), colorInput()],
34993
- compute: (bars, inputs) => ({
34994
- plots: [{ key: "atr", title: "ATR", values: atr(bars, num3(inputs, "length", 14)), color: str3(inputs, "color", SERIES_LINE), width: 2 }]
34995
- })
35912
+ inputs: [
35913
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to average the true range."),
35914
+ 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."),
35915
+ colorInput(NEUTRAL, "ATR Line", "color", "Color of the ATR line while volatility contracts or holds flat."),
35916
+ colorInput(WARNING, "Expanding", "expandColor", "Color of the ATR line while it rises \u2014 volatility expanding bar to bar.")
35917
+ ],
35918
+ compute: (bars, inputs) => {
35919
+ const len = num3(inputs, "length", 14);
35920
+ const tr = trueRange(bars);
35921
+ const kind = str3(inputs, "smoothing", "RMA");
35922
+ const values = kind === "SMA" ? sma(tr, len) : kind === "EMA" ? ema(tr, len) : kind === "WMA" ? wma(tr, len) : rma(tr, len);
35923
+ const calm = str3(inputs, "color", NEUTRAL);
35924
+ return washedToZero("atr", "ATR", values, expansionColors(values, calm, str3(inputs, "expandColor", WARNING)), calm, 75);
35925
+ }
34996
35926
  };
34997
35927
  var historicalVolatility = {
34998
35928
  type: "historical-volatility",
@@ -35000,14 +35930,23 @@ var historicalVolatility = {
35000
35930
  shortTitle: "HV",
35001
35931
  overlay: false,
35002
35932
  inputs: [
35003
- lengthInput(10),
35004
- { 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)" },
35005
- colorInput()
35933
+ { ...lengthInput(10, "length", "Length", 5e3, "Lookback window in bars for the standard deviation of close-to-close log returns."), min: 2 },
35934
+ 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."),
35935
+ 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."),
35936
+ colorInput(WARNING, "Volatility Line", "color", "Color of the line at its recent highs; it fades toward grey as volatility contracts.")
35006
35937
  ],
35007
35938
  compute: (bars, inputs) => {
35008
- const logReturns = map(change(map(closes(bars), Math.log)), (x) => x);
35009
- const values = map(stdev(logReturns, num3(inputs, "length", 10)), (x) => x * Math.sqrt(num3(inputs, "annual", 365)) * 100);
35010
- return { plots: [{ key: "hv", title: "HV", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
35939
+ const len = num3(inputs, "length", 10);
35940
+ const logReturns = sub(map(closes(bars), Math.log), shift(map(closes(bars), Math.log), 1));
35941
+ const meanReturn = map(sum(logReturns, len), (s) => s / len);
35942
+ const squaredSum = sum(map(logReturns, (r) => r * r), len);
35943
+ const factor = bool(inputs, "annualize", true) ? Math.sqrt(num3(inputs, "periodsPerYear", 252)) : 1;
35944
+ const values = zip(squaredSum, meanReturn, (sq, m) => Math.sqrt(Math.max(sq - len * m * m, 0) / (len - 1)) * factor * 100);
35945
+ const low = lowest(values, 100);
35946
+ const high = highest(values, 100);
35947
+ const accent = str3(inputs, "color", WARNING);
35948
+ const colors = values.map((x, i) => Number.isFinite(x) ? gradient(x, low[i] ?? 0, high[i] ?? 0, NEUTRAL, accent) : null);
35949
+ return washedToZero("hv", "Historical Volatility", values, colors, accent, 75, 2);
35011
35950
  }
35012
35951
  };
35013
35952
  var chaikinVolatility = {
@@ -35015,13 +35954,26 @@ var chaikinVolatility = {
35015
35954
  title: "Chaikin Volatility",
35016
35955
  shortTitle: "CHV",
35017
35956
  overlay: false,
35018
- inputs: [lengthInput(10, "emaLength", "EMA length"), lengthInput(10, "rocLength", "RoC length"), colorInput()],
35957
+ inputs: [
35958
+ lengthInput(10, "emaLength", "EMA Length", 5e3, "Length of the exponential moving average applied to the bar high-low range."),
35959
+ lengthInput(10, "rocLength", "ROC Length", 5e3, "Number of bars used for the rate of change of the smoothed high-low range."),
35960
+ colorInput(NEUTRAL, "CHV Color", "color", "Color of the Chaikin Volatility line."),
35961
+ 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.")
35962
+ ],
35019
35963
  compute: (bars, inputs) => {
35020
35964
  const range = ema(sub(highs(bars), lows(bars)), num3(inputs, "emaLength", 10));
35021
- return {
35022
- plots: [{ key: "cv", title: "Chaikin Vol", values: roc(range, num3(inputs, "rocLength", 10)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35023
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35024
- };
35965
+ const prior = shift(range, num3(inputs, "rocLength", 10));
35966
+ const values = zip(range, prior, (r, p) => p === 0 ? Number.NaN : 100 * (r - p) / p);
35967
+ const calm = str3(inputs, "color", NEUTRAL);
35968
+ const expansion = str3(inputs, "expansionColor", WARNING);
35969
+ return centeredOscillator({
35970
+ key: "chv",
35971
+ title: "Chaikin Volatility",
35972
+ values,
35973
+ // Only expansion is a state worth accenting; contraction stays structural grey.
35974
+ ink: { bull: expansion, bear: calm, neutral: calm },
35975
+ zeroInk: NEUTRAL
35976
+ });
35025
35977
  }
35026
35978
  };
35027
35979
  var massIndex = {
@@ -35029,14 +35981,42 @@ var massIndex = {
35029
35981
  title: "Mass Index",
35030
35982
  shortTitle: "MI",
35031
35983
  overlay: false,
35032
- inputs: [lengthInput(25), colorInput()],
35984
+ inputs: [
35985
+ lengthInput(25, "length", "Sum Length", 5e3, "Number of bars over which the single/double EMA ratio is summed."),
35986
+ lengthInput(9, "emaLength", "EMA Length", 5e3, "Length of the single EMA of the high-low range and of its double smoothing."),
35987
+ floatInput("setupLevel", "Setup Level", 27, 0, 1e3, 0.1, "Reversal bulge set-up level; a rise above it arms the pattern."),
35988
+ 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."),
35989
+ colorInput(NEUTRAL, "Mass Index", "color", "Line color while no reversal bulge is armed."),
35990
+ colorInput(WARNING, "Armed", "armedColor", "Line color from the rise above the setup level until the decline below the trigger level."),
35991
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Reversal bulge levels color.")
35992
+ ],
35033
35993
  compute: (bars, inputs) => {
35034
- const range = sub(highs(bars), lows(bars));
35035
- const single = ema(range, 9);
35036
- const ratio = zip(single, ema(single, 9), (a, b) => b === 0 ? Number.NaN : a / b);
35994
+ const emaLen = num3(inputs, "emaLength", 9);
35995
+ const single = ema(sub(highs(bars), lows(bars)), emaLen);
35996
+ const ratio = zip(single, ema(single, emaLen), (a, b) => b === 0 ? 1 : a / b);
35997
+ const values = sum(ratio, num3(inputs, "length", 25));
35998
+ const setup = num3(inputs, "setupLevel", 27);
35999
+ const trigger = num3(inputs, "triggerLevel", 26.5);
36000
+ const calm = str3(inputs, "color", NEUTRAL);
36001
+ const armedInk = str3(inputs, "armedColor", WARNING);
36002
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
36003
+ let armed = false;
36004
+ const colors = values.map((x) => {
36005
+ if (!Number.isFinite(x)) return null;
36006
+ armed = x > setup ? true : x < trigger ? false : armed;
36007
+ return armed ? armedInk : calm;
36008
+ });
35037
36009
  return {
35038
- plots: [{ key: "mi", title: "Mass Index", values: sum(ratio, num3(inputs, "length", 25)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35039
- levels: [{ key: "reversal", price: 27, color: NEUTRAL, lineStyle: "dotted" }]
36010
+ plots: [
36011
+ { key: "mi", title: "Mass Index", values, color: calm, colors },
36012
+ anchorPlot("setupAnchor", "Setup Anchor", values.length, setup),
36013
+ anchorPlot("triggerAnchor", "Trigger Anchor", values.length, trigger)
36014
+ ],
36015
+ bands: [{ key: "bulge", from: "setupAnchor", to: "triggerAnchor", color: transp(armedInk, 90) }],
36016
+ levels: [
36017
+ { key: "setup", price: setup, color: levelInk, lineStyle: "dashed", title: "Setup Level" },
36018
+ { key: "trigger", price: trigger, color: levelInk, lineStyle: "dashed", title: "Trigger Level" }
36019
+ ]
35040
36020
  };
35041
36021
  }
35042
36022
  };
@@ -35045,30 +36025,83 @@ var standardDeviation = {
35045
36025
  title: "Standard Deviation",
35046
36026
  shortTitle: "StdDev",
35047
36027
  overlay: false,
35048
- inputs: [lengthInput(20), sourceInput(), colorInput()],
35049
- compute: (bars, inputs) => ({
35050
- plots: [
35051
- {
35052
- key: "stdev",
35053
- title: "StdDev",
35054
- values: stdev(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20)),
35055
- color: str3(inputs, "color", SERIES_LINE),
35056
- width: 2
35057
- }
35058
- ]
35059
- })
36028
+ inputs: [
36029
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used to compute the standard deviation."),
36030
+ sourceInput("Close", "source", "Source", "Price series the standard deviation is calculated on."),
36031
+ colorInput(NEUTRAL, "Standard Deviation", "color", "Line color while volatility contracts or holds flat."),
36032
+ colorInput(WARNING, "Expanding", "expandColor", "Line and fill color while volatility expands bar to bar.")
36033
+ ],
36034
+ compute: (bars, inputs) => {
36035
+ const values = stdev(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20));
36036
+ const calm = str3(inputs, "color", NEUTRAL);
36037
+ return washedToZero("stdev", "Standard Deviation", values, expansionColors(values, calm, str3(inputs, "expandColor", WARNING)), calm, 80);
36038
+ }
35060
36039
  };
35061
36040
  var ulcerIndex = {
35062
36041
  type: "ulcer-index",
35063
36042
  title: "Ulcer Index",
35064
36043
  overlay: false,
35065
- inputs: [lengthInput(14), colorInput()],
36044
+ inputs: [
36045
+ { ...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 },
36046
+ sourceInput("Close", "source", "Source", "Price series the Ulcer Index is computed on. Close is the standard choice."),
36047
+ 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."),
36048
+ colorInput(NEUTRAL, "Ulcer Index", "color", "Color of the index near zero; it blends toward the stress color as the reading approaches the threshold."),
36049
+ colorInput(BEARISH, "Stress", "stressColor", "Color of the index at and beyond the alert threshold."),
36050
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Vertical gradient between zero and the Ulcer Index, fully transparent at zero."),
36051
+ 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."),
36052
+ colorInput(transp(BEARISH, 50), "Drawdown Color", "drawdownColor", "Color of the drawdown columns."),
36053
+ colorInput(NEUTRAL, "Threshold", "thresholdColor", "Color of the alert threshold line.")
36054
+ ],
35066
36055
  compute: (bars, inputs) => {
35067
36056
  const len = num3(inputs, "length", 14);
35068
- const c = closes(bars);
35069
- const drawdown = zip(c, highest(c, len), (x, h) => h === 0 ? 0 : 100 * (x - h) / h);
35070
- const values = map(sma(map(drawdown, (d) => d * d), len), Math.sqrt);
35071
- return { plots: [{ key: "ui", title: "Ulcer", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
36057
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
36058
+ const n = src.length;
36059
+ const values = new Array(n).fill(Number.NaN);
36060
+ const drawdown = new Array(n).fill(Number.NaN);
36061
+ for (let i = len - 1; i < n; i++) {
36062
+ let runMax = Number.NaN;
36063
+ let sumSq = 0;
36064
+ let valid = true;
36065
+ let last = Number.NaN;
36066
+ for (let k = i - len + 1; k <= i; k++) {
36067
+ const price = src[k];
36068
+ if (!Number.isFinite(price)) {
36069
+ valid = false;
36070
+ break;
36071
+ }
36072
+ runMax = Number.isFinite(runMax) ? Math.max(runMax, price) : price;
36073
+ if (runMax <= 0) {
36074
+ valid = false;
36075
+ break;
36076
+ }
36077
+ last = 100 * (price - runMax) / runMax;
36078
+ sumSq += last * last;
36079
+ }
36080
+ if (!valid) continue;
36081
+ values[i] = Math.sqrt(sumSq / len);
36082
+ drawdown[i] = last;
36083
+ }
36084
+ const calm = str3(inputs, "color", NEUTRAL);
36085
+ const stress = str3(inputs, "stressColor", BEARISH);
36086
+ const threshold = num3(inputs, "threshold", 5);
36087
+ const colors = values.map((x) => Number.isFinite(x) ? threshold > 0 ? gradient(x, 0, threshold, calm, stress) : stress : null);
36088
+ const plots = [{ key: "ui", title: "Ulcer Index", values, color: calm, colors }];
36089
+ const bands = [];
36090
+ if (bool(inputs, "gradientFill", true)) {
36091
+ plots.push(anchorPlot("zeroAnchor", "Zero Base", n, 0));
36092
+ bands.push({ key: "wash", from: "ui", to: "zeroAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, 0, (_x, i) => colors[i] ?? calm) });
36093
+ }
36094
+ if (bool(inputs, "showDrawdown", false)) {
36095
+ plots.push({ key: "dd", title: "Drawdown %", values: drawdown, kind: "columns", color: str3(inputs, "drawdownColor", transp(BEARISH, 50)), base: 0 });
36096
+ }
36097
+ return {
36098
+ plots,
36099
+ bands,
36100
+ levels: [
36101
+ { key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dotted", title: "Zero Level" },
36102
+ { key: "threshold", price: threshold, color: str3(inputs, "thresholdColor", NEUTRAL), lineStyle: "dashed", title: "Alert Threshold" }
36103
+ ]
36104
+ };
35072
36105
  }
35073
36106
  };
35074
36107
  var choppiness = {
@@ -35076,19 +36109,33 @@ var choppiness = {
35076
36109
  title: "Choppiness Index",
35077
36110
  shortTitle: "CHOP",
35078
36111
  overlay: false,
35079
- inputs: [lengthInput(14), colorInput()],
36112
+ inputs: [
36113
+ { ...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 },
36114
+ floatInput("upperBand", "Upper Band", 61.8, 0, 100, 0.1, "Readings above this level indicate a choppy, range-bound market."),
36115
+ floatInput("lowerBand", "Lower Band", 38.2, 0, 100, 0.1, "Readings below this level indicate a strong trend."),
36116
+ colorInput(NEUTRAL, "CHOP Line", "color", "Color of the Choppiness Index line."),
36117
+ colorInput(BULLISH, "Trending", "trendingColor", "Line, fill and zone color toward and below the lower band \u2014 a trending market."),
36118
+ colorInput(BEARISH, "Choppy", "choppyColor", "Line, fill and zone color toward and above the upper band \u2014 a choppy, range-bound market."),
36119
+ colorInput(transp(NEUTRAL, 90), "Band Fill", "fillColor", "Fill color of the zone between the upper and lower bands.")
36120
+ ],
35080
36121
  compute: (bars, inputs) => {
35081
36122
  const len = num3(inputs, "length", 14);
35082
36123
  const trSum = sum(trueRange(bars), len);
35083
36124
  const span = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => h - l);
35084
36125
  const values = zip(trSum, span, (t, s) => s > 0 && t > 0 ? 100 * Math.log10(t / s) / Math.log10(len) : Number.NaN);
35085
- return {
35086
- plots: [{ key: "chop", title: "CHOP", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35087
- levels: [
35088
- { key: "upper", price: 61.8, color: NEUTRAL },
35089
- { key: "lower", price: 38.2, color: NEUTRAL }
35090
- ]
35091
- };
36126
+ return boundedOscillator({
36127
+ key: "chop",
36128
+ title: "CHOP",
36129
+ values,
36130
+ scale: { top: 100, bottom: 0 },
36131
+ overbought: num3(inputs, "upperBand", 61.8),
36132
+ oversold: num3(inputs, "lowerBand", 38.2),
36133
+ ink: { low: str3(inputs, "trendingColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "choppyColor", BEARISH) },
36134
+ levelInk: NEUTRAL,
36135
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 90)),
36136
+ midpointWash: true,
36137
+ levelTitles: { overbought: "Upper Band", oversold: "Lower Band" }
36138
+ });
35092
36139
  }
35093
36140
  };
35094
36141
  var volatilitySpecs = [atrSpec, historicalVolatility, chaikinVolatility, massIndex, standardDeviation, ulcerIndex, choppiness];
@@ -35101,17 +36148,85 @@ function moneyFlowVolume(bars) {
35101
36148
  return (2 * b.close - b.high - b.low) / (b.high - b.low) * b.volume;
35102
36149
  });
35103
36150
  }
36151
+ function slopeColors2(values, bull, bear, flat) {
36152
+ return values.map((x, i) => {
36153
+ if (!Number.isFinite(x)) return null;
36154
+ const prev2 = i > 0 ? values[i - 1] : Number.NaN;
36155
+ if (!Number.isFinite(prev2)) return flat;
36156
+ return x > prev2 ? bull : x < prev2 ? bear : flat;
36157
+ });
36158
+ }
36159
+ function volumeIndex(bars, src, base, when) {
36160
+ const out = new Array(bars.length).fill(Number.NaN);
36161
+ let index = base;
36162
+ for (let i = 0; i < bars.length; i++) {
36163
+ const v = bars[i].volume;
36164
+ const pv = i > 0 ? bars[i - 1].volume : null;
36165
+ const prev2 = i > 0 ? src[i - 1] : Number.NaN;
36166
+ const active = v != null && pv != null && Number.isFinite(v) && Number.isFinite(pv) && when(v, pv);
36167
+ if (active && Number.isFinite(prev2) && prev2 !== 0) index *= 1 + (src[i] - prev2) / prev2;
36168
+ out[i] = index;
36169
+ }
36170
+ return out;
36171
+ }
36172
+ function regimeIndex(key, title, values, signal, ink, showSignal, gradientFill, singleColor = false) {
36173
+ const inkOf = (i) => {
36174
+ const s = signal[i];
36175
+ const x = values[i];
36176
+ if (singleColor || !Number.isFinite(s)) return ink.neutral;
36177
+ return x > s ? ink.bull : x < s ? ink.bear : ink.neutral;
36178
+ };
36179
+ const plots = [
36180
+ {
36181
+ key: "signal",
36182
+ title: "Signal EMA",
36183
+ values: signal,
36184
+ color: ink.signal,
36185
+ ...showSignal ? {} : { display: { pane: false } }
36186
+ },
36187
+ { key, title, values, color: ink.neutral, width: 2, colors: values.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) }
36188
+ ];
36189
+ const bands = gradientFill ? [
36190
+ {
36191
+ key: "regime",
36192
+ from: key,
36193
+ to: "signal",
36194
+ color: transp(NEUTRAL, 100),
36195
+ gradient: values.map((x, i) => {
36196
+ const s = signal[i];
36197
+ if (!Number.isFinite(x) || !Number.isFinite(s)) return null;
36198
+ const above = x > s;
36199
+ const c = above ? ink.bull : ink.bear;
36200
+ return { topValue: Math.max(x, s), bottomValue: Math.min(x, s), topColor: transp(c, above ? 50 : 100), bottomColor: transp(c, above ? 100 : 50) };
36201
+ })
36202
+ }
36203
+ ] : [];
36204
+ return { plots, bands };
36205
+ }
35104
36206
  var obv = {
35105
36207
  type: "on-balance-volume",
35106
36208
  title: "On Balance Volume",
35107
36209
  shortTitle: "OBV",
35108
36210
  overlay: false,
35109
- inputs: [colorInput()],
36211
+ inputs: [
36212
+ boolInput("smoothing", "Smoothing MA", false, SETTINGS, "Display a moving average of OBV to act as a smoothing/signal line."),
36213
+ optionInput("maType", "MA Type", "SMA", ["SMA", "EMA", "WMA", "RMA (SMMA)"], "Moving average type used for the smoothing MA."),
36214
+ lengthInput(20, "maLength", "MA Length", 5e3, "Number of bars used for the smoothing MA."),
36215
+ colorInput(INFO, "OBV", "color", "Color of the OBV line."),
36216
+ colorInput(WARNING, "Smoothing MA", "maColor", "Color of the smoothing MA line.")
36217
+ ],
35110
36218
  compute: (bars, inputs) => {
35111
36219
  const vol = volumes(bars);
35112
36220
  const d = change(closes(bars));
35113
- const signed3 = zip(vol, d, (v, x) => x > 0 ? v : x < 0 ? -v : 0);
35114
- return { plots: [{ key: "obv", title: "OBV", values: cumSum(signed3), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
36221
+ const values = cumSum(zip(vol, d, (v, x) => x > 0 ? v : x < 0 ? -v : 0));
36222
+ const plots = [{ key: "obv", title: "OBV", values, color: str3(inputs, "color", INFO) }];
36223
+ if (bool(inputs, "smoothing", false)) {
36224
+ const len = num3(inputs, "maLength", 20);
36225
+ const kind = str3(inputs, "maType", "SMA");
36226
+ const ma = kind === "EMA" ? ema(values, len) : kind === "WMA" ? wma(values, len) : kind === "RMA (SMMA)" ? rma(values, len) : sma(values, len);
36227
+ plots.push({ key: "ma", title: "Smoothing MA", values: ma, color: str3(inputs, "maColor", WARNING) });
36228
+ }
36229
+ return { plots };
35115
36230
  }
35116
36231
  };
35117
36232
  var accDist = {
@@ -35119,24 +36234,50 @@ var accDist = {
35119
36234
  title: "Accumulation / Distribution",
35120
36235
  shortTitle: "A/D",
35121
36236
  overlay: false,
35122
- inputs: [colorInput()],
35123
- compute: (bars, inputs) => ({
35124
- plots: [{ key: "ad", title: "A/D", values: cumSum(moneyFlowVolume(bars)), color: str3(inputs, "color", SERIES_LINE), width: 2 }]
35125
- })
36237
+ inputs: [
36238
+ boolInput("slopeColoring", "Slope Coloring", true, STYLE, "Color the line by its slope \u2014 bullish while rising (accumulation), bearish while falling (distribution)."),
36239
+ colorInput(NEUTRAL, "A/D Line", "color", "Color of the line when slope coloring is disabled or the line is flat."),
36240
+ colorInput(BULLISH, "Rising", "bullColor", "Line color while the A/D Line rises."),
36241
+ colorInput(BEARISH, "Falling", "bearColor", "Line color while the A/D Line falls.")
36242
+ ],
36243
+ compute: (bars, inputs) => {
36244
+ const values = cumSum(moneyFlowVolume(bars));
36245
+ const flat = str3(inputs, "color", NEUTRAL);
36246
+ return {
36247
+ plots: [
36248
+ {
36249
+ key: "ad",
36250
+ title: "A/D",
36251
+ values,
36252
+ color: flat,
36253
+ ...bool(inputs, "slopeColoring", true) ? { colors: slopeColors2(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), flat) } : {}
36254
+ }
36255
+ ]
36256
+ };
36257
+ }
35126
36258
  };
35127
36259
  var cmf = {
35128
36260
  type: "chaikin-money-flow",
35129
36261
  title: "Chaikin Money Flow",
35130
36262
  shortTitle: "CMF",
35131
36263
  overlay: false,
35132
- inputs: [lengthInput(20), colorInput()],
36264
+ inputs: [
36265
+ lengthInput(20, "length", "Length", 5e3, "Lookback length over which money flow volume and volume are summed."),
36266
+ colorInput(NEUTRAL, "CMF Line", "color", "Color of the Chaikin Money Flow line."),
36267
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the CMF reads above zero (net money flowing in)."),
36268
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the CMF reads below zero (net money flowing out)."),
36269
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
36270
+ ],
35133
36271
  compute: (bars, inputs) => {
35134
36272
  const len = num3(inputs, "length", 20);
35135
36273
  const values = zip(sum(moneyFlowVolume(bars), len), sum(volumes(bars), len), (m, v) => v === 0 ? 0 : m / v);
35136
- return {
35137
- plots: [{ key: "cmf", title: "CMF", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35138
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35139
- };
36274
+ return centeredOscillator({
36275
+ key: "cmf",
36276
+ title: "CMF",
36277
+ values,
36278
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
36279
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
36280
+ });
35140
36281
  }
35141
36282
  };
35142
36283
  var chaikinOscillator = {
@@ -35144,14 +36285,23 @@ var chaikinOscillator = {
35144
36285
  title: "Chaikin Oscillator",
35145
36286
  shortTitle: "Chaikin Osc",
35146
36287
  overlay: false,
35147
- inputs: [lengthInput(3, "fastLength", "Fast length"), lengthInput(10, "slowLength", "Slow length"), colorInput()],
36288
+ inputs: [
36289
+ lengthInput(3, "fastLength", "Fast Length", 5e3, "Length of the fast EMA applied to the accumulation/distribution line."),
36290
+ lengthInput(10, "slowLength", "Slow Length", 5e3, "Length of the slow EMA applied to the accumulation/distribution line."),
36291
+ colorInput(NEUTRAL, "Oscillator", "color", "Color of the Chaikin Oscillator line."),
36292
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the oscillator reads above zero (accumulation)."),
36293
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the oscillator reads below zero (distribution).")
36294
+ ],
35148
36295
  compute: (bars, inputs) => {
35149
36296
  const ad = cumSum(moneyFlowVolume(bars));
35150
36297
  const values = sub(ema(ad, num3(inputs, "fastLength", 3)), ema(ad, num3(inputs, "slowLength", 10)));
35151
- return {
35152
- plots: [{ key: "co", title: "Chaikin Osc", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35153
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35154
- };
36298
+ return centeredOscillator({
36299
+ key: "co",
36300
+ title: "Chaikin Oscillator",
36301
+ values,
36302
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
36303
+ zeroInk: NEUTRAL
36304
+ });
35155
36305
  }
35156
36306
  };
35157
36307
  var eom = {
@@ -35159,20 +36309,28 @@ var eom = {
35159
36309
  title: "Ease of Movement",
35160
36310
  shortTitle: "EOM",
35161
36311
  overlay: false,
35162
- inputs: [lengthInput(14), { key: "divisor", title: "Divisor", type: "int", defval: 1e4, min: 1, max: 1e9, step: 1 }, colorInput()],
36312
+ inputs: [
36313
+ lengthInput(14, "length", "Length", 5e3, "Number of bars in the simple moving average smoothing the 1-bar Ease of Movement values."),
36314
+ intInput("divisor", "Divisor", 1e4, 1, 1e9, "Volume scaling constant of the box ratio; larger values scale the oscillator up. 10000 is the conventional default."),
36315
+ colorInput(NEUTRAL, "EOM Line", "color", "Color of the line when it sits exactly at zero."),
36316
+ colorInput(BULLISH, "Above Zero", "bullColor", "Color of the line and fill while Ease of Movement is positive."),
36317
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while Ease of Movement is negative.")
36318
+ ],
35163
36319
  compute: (bars, inputs) => {
35164
36320
  const div = num3(inputs, "divisor", 1e4);
35165
- const hl2 = sourceValues(bars, "HL2");
35166
- const move = change(hl2);
36321
+ const move = change(sourceValues(bars, "HL2"));
35167
36322
  const raw = bars.map((b, i) => {
35168
36323
  const m = move[i];
35169
- if (b.volume == null || !Number.isFinite(b.volume) || b.volume === 0 || !Number.isFinite(m)) return Number.NaN;
35170
- return div * m * (b.high - b.low) / b.volume;
36324
+ if (b.volume == null || !Number.isFinite(b.volume) || !Number.isFinite(m)) return Number.NaN;
36325
+ return b.volume === 0 ? 0 : div * m * (b.high - b.low) / b.volume;
36326
+ });
36327
+ return centeredOscillator({
36328
+ key: "eom",
36329
+ title: "EOM",
36330
+ values: sma(raw, num3(inputs, "length", 14)),
36331
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
36332
+ zeroInk: NEUTRAL
35171
36333
  });
35172
- return {
35173
- plots: [{ key: "eom", title: "EOM", values: sma(raw, num3(inputs, "length", 14)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35174
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35175
- };
35176
36334
  }
35177
36335
  };
35178
36336
  var forceIndex = {
@@ -35180,13 +36338,24 @@ var forceIndex = {
35180
36338
  title: "Force Index",
35181
36339
  shortTitle: "FI",
35182
36340
  overlay: false,
35183
- inputs: [lengthInput(13), colorInput()],
36341
+ inputs: [
36342
+ 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."),
36343
+ colorInput(BULLISH, "Force Index", "color", "Color of the line while above zero (buying pressure)."),
36344
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line while below zero (selling pressure).")
36345
+ ],
35184
36346
  compute: (bars, inputs) => {
35185
36347
  const raw = zip(change(closes(bars)), volumes(bars), (d, v) => d * v);
35186
- return {
35187
- plots: [{ key: "force", title: "Force", values: ema(raw, num3(inputs, "length", 13)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35188
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35189
- };
36348
+ const bull = str3(inputs, "color", BULLISH);
36349
+ return centeredOscillator({
36350
+ key: "force",
36351
+ title: "Force Index",
36352
+ values: ema(raw, num3(inputs, "length", 13)),
36353
+ // The reference reads an exact zero as buying pressure, so neutral shares the bull ink.
36354
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
36355
+ zeroInk: NEUTRAL,
36356
+ width: 2,
36357
+ valueTransparency: 60
36358
+ });
35190
36359
  }
35191
36360
  };
35192
36361
  var klinger = {
@@ -35194,19 +36363,44 @@ var klinger = {
35194
36363
  title: "Klinger Oscillator",
35195
36364
  shortTitle: "KVO",
35196
36365
  overlay: false,
35197
- inputs: [lengthInput(34, "fastLength", "Fast length"), lengthInput(55, "slowLength", "Slow length"), lengthInput(13, "signalLength", "Signal length")],
36366
+ inputs: [
36367
+ lengthInput(34, "fastLength", "Fast Length", 5e3, "Length of the fast EMA applied to the volume force."),
36368
+ lengthInput(55, "slowLength", "Slow Length", 5e3, "Length of the slow EMA applied to the volume force."),
36369
+ lengthInput(13, "signalLength", "Signal Length", 500, "Length of the EMA applied to the oscillator to obtain the signal line."),
36370
+ colorInput(BULLISH, "Bullish", "color", "Color of the Klinger oscillator while above zero."),
36371
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the Klinger oscillator while below zero."),
36372
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line.")
36373
+ ],
35198
36374
  compute: (bars, inputs) => {
35199
- const hlc3 = sourceValues(bars, "HLC3");
35200
- const trend = change(hlc3);
35201
- const signedVolume = zip(volumes(bars), trend, (v, t) => t >= 0 ? v : -v);
35202
- const line = sub(ema(signedVolume, num3(inputs, "fastLength", 34)), ema(signedVolume, num3(inputs, "slowLength", 55)));
35203
- return {
35204
- plots: [
35205
- { key: "kvo", title: "KVO", values: line, color: SERIES_LINE, width: 2 },
35206
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: WARNING }
35207
- ],
35208
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35209
- };
36375
+ const n = bars.length;
36376
+ const force = new Array(n).fill(Number.NaN);
36377
+ let trend = 1;
36378
+ let prevTrend = 1;
36379
+ let cm = 0;
36380
+ let prevDm = 0;
36381
+ for (let i = 0; i < n; i++) {
36382
+ const b = bars[i];
36383
+ const hlc = b.high + b.low + b.close;
36384
+ const prevHlc = i > 0 ? bars[i - 1].high + bars[i - 1].low + bars[i - 1].close : hlc;
36385
+ trend = hlc > prevHlc ? 1 : -1;
36386
+ const dm = b.high - b.low;
36387
+ cm = i > 0 && trend === prevTrend ? cm + dm : prevDm + dm;
36388
+ const v = b.volume;
36389
+ force[i] = v == null || !Number.isFinite(v) ? Number.NaN : cm === 0 ? 0 : v * Math.abs(2 * (dm / cm) - 1) * trend * 100;
36390
+ prevTrend = trend;
36391
+ prevDm = dm;
36392
+ }
36393
+ const line = sub(ema(force, num3(inputs, "fastLength", 34)), ema(force, num3(inputs, "slowLength", 55)));
36394
+ const bull = str3(inputs, "color", BULLISH);
36395
+ return centeredOscillator({
36396
+ key: "kvo",
36397
+ title: "KVO",
36398
+ values: line,
36399
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
36400
+ zeroInk: transp(NEUTRAL, 50),
36401
+ valueTransparency: 60,
36402
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: str3(inputs, "signalColor", WARNING) }]
36403
+ });
35210
36404
  }
35211
36405
  };
35212
36406
  var mfi = {
@@ -35214,7 +36408,15 @@ var mfi = {
35214
36408
  title: "Money Flow Index",
35215
36409
  shortTitle: "MFI",
35216
36410
  overlay: false,
35217
- inputs: [lengthInput(14), colorInput()],
36411
+ inputs: [
36412
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to sum positive and negative money flow."),
36413
+ floatInput("overbought", "Overbought Level", 80, 0, 100, 1, "Level above which the MFI is considered overbought."),
36414
+ floatInput("oversold", "Oversold Level", 20, 0, 100, 1, "Level below which the MFI is considered oversold."),
36415
+ colorInput(NEUTRAL, "MFI", "color", "Color of the MFI line between the levels. Toward either level it blends into that level\u2019s color."),
36416
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
36417
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level."),
36418
+ colorInput(transp(NEUTRAL, 94), "OB/OS Zone Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
36419
+ ],
35218
36420
  compute: (bars, inputs) => {
35219
36421
  const len = num3(inputs, "length", 14);
35220
36422
  const tp = sourceValues(bars, "HLC3");
@@ -35222,48 +36424,57 @@ var mfi = {
35222
36424
  const d = change(tp);
35223
36425
  const pos = sum(zip(rawFlow, d, (f, x) => x > 0 ? f : 0), len);
35224
36426
  const neg = sum(zip(rawFlow, d, (f, x) => x < 0 ? f : 0), len);
35225
- const values = zip(pos, neg, (p, n) => n === 0 ? 100 : 100 - 100 / (1 + p / n));
35226
- return {
35227
- plots: [{ key: "mfi", title: "MFI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35228
- levels: [
35229
- { key: "upper", price: 80, color: NEUTRAL },
35230
- { key: "lower", price: 20, color: NEUTRAL }
35231
- ]
35232
- };
36427
+ const values = zip(pos, neg, (p, w) => p + w > 0 ? 100 * p / (p + w) : Number.NaN);
36428
+ return boundedOscillator({
36429
+ key: "mfi",
36430
+ title: "MFI",
36431
+ values,
36432
+ scale: { top: 100, bottom: 0 },
36433
+ overbought: num3(inputs, "overbought", 80),
36434
+ oversold: num3(inputs, "oversold", 20),
36435
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
36436
+ levelInk: NEUTRAL,
36437
+ zoneTransparency: 91,
36438
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 94))
36439
+ });
35233
36440
  }
35234
36441
  };
35235
- function volumeIndex(bars, when) {
35236
- const out = new Array(bars.length).fill(Number.NaN);
35237
- let index = 1e3;
35238
- let started = false;
35239
- for (let i = 1; i < bars.length; i++) {
35240
- const v = bars[i].volume;
35241
- const pv = bars[i - 1].volume;
35242
- const pc = bars[i - 1].close;
35243
- if (v == null || pv == null || !Number.isFinite(v) || !Number.isFinite(pv) || pc === 0) {
35244
- if (started) out[i] = index;
35245
- continue;
35246
- }
35247
- started = true;
35248
- if (when(v, pv)) index += (bars[i].close - pc) / pc * index;
35249
- out[i] = index;
35250
- }
35251
- return out;
36442
+ function regimeStyleInputs(signalTitle) {
36443
+ return [
36444
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the line and gradient fill while it holds above its signal EMA."),
36445
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line and gradient fill while it holds below its signal EMA."),
36446
+ colorInput(NEUTRAL, "Neutral", "neutralColor", "Color used while the signal EMA is still forming, or the line sits exactly on it."),
36447
+ colorInput(NEUTRAL, signalTitle, "signalColor", "Color of the signal EMA overlay."),
36448
+ 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.")
36449
+ ];
35252
36450
  }
36451
+ var NVI_POSITION = "Position vs Average";
36452
+ var NVI_SINGLE = "Single Color";
35253
36453
  var nvi = {
35254
36454
  type: "negative-volume-index",
35255
36455
  title: "Negative Volume Index",
35256
36456
  shortTitle: "Negative Volume Index",
35257
36457
  overlay: false,
35258
- inputs: [lengthInput(255, "signalLength", "Signal length"), colorInput()],
36458
+ inputs: [
36459
+ 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."),
36460
+ 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."),
36461
+ lengthInput(255, "signalLength", "Signal Length", 5e3, "Length of the signal EMA. The convention is one trading year \u2014 255 sessions on daily charts."),
36462
+ 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."),
36463
+ ...regimeStyleInputs("Signal EMA")
36464
+ ],
35259
36465
  compute: (bars, inputs) => {
35260
- const line = volumeIndex(bars, (v, pv) => v < pv);
35261
- return {
35262
- plots: [
35263
- { key: "nvi", title: "NVI", values: line, color: str3(inputs, "color", SERIES_LINE), width: 2 },
35264
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 255)), color: WARNING }
35265
- ]
35266
- };
36466
+ const values = volumeIndex(bars, closes(bars), num3(inputs, "base", 1e3), (v, pv) => v < pv);
36467
+ const signal = ema(values, num3(inputs, "signalLength", 255));
36468
+ return regimeIndex(
36469
+ "nvi",
36470
+ "NVI",
36471
+ values,
36472
+ signal,
36473
+ { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "neutralColor", NEUTRAL), signal: str3(inputs, "signalColor", NEUTRAL) },
36474
+ bool(inputs, "showSignal", true),
36475
+ bool(inputs, "gradientFill", true),
36476
+ str3(inputs, "colorMode", NVI_POSITION) === NVI_SINGLE
36477
+ );
35267
36478
  }
35268
36479
  };
35269
36480
  var pvi = {
@@ -35271,15 +36482,26 @@ var pvi = {
35271
36482
  title: "Positive Volume Index",
35272
36483
  shortTitle: "Positive Volume Index",
35273
36484
  overlay: false,
35274
- inputs: [lengthInput(255, "signalLength", "Signal length"), colorInput()],
36485
+ inputs: [
36486
+ sourceInput("Close", "source", "Source", "Price series whose one-bar percentage change moves PVI on its active sessions."),
36487
+ 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."),
36488
+ 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."),
36489
+ 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."),
36490
+ ...regimeStyleInputs("Signal Line")
36491
+ ],
35275
36492
  compute: (bars, inputs) => {
35276
- const line = volumeIndex(bars, (v, pv) => v > pv);
35277
- return {
35278
- plots: [
35279
- { key: "pvi", title: "PVI", values: line, color: str3(inputs, "color", SERIES_LINE), width: 2 },
35280
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 255)), color: WARNING }
35281
- ]
35282
- };
36493
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
36494
+ const values = volumeIndex(bars, src, num3(inputs, "base", 1e3), (v, pv) => v > pv);
36495
+ const signal = ema(values, num3(inputs, "signalLength", 255));
36496
+ return regimeIndex(
36497
+ "pvi",
36498
+ "PVI",
36499
+ values,
36500
+ signal,
36501
+ { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "neutralColor", NEUTRAL), signal: str3(inputs, "signalColor", NEUTRAL) },
36502
+ bool(inputs, "showSignal", true),
36503
+ bool(inputs, "gradientFill", true)
36504
+ );
35283
36505
  }
35284
36506
  };
35285
36507
  var pvt = {
@@ -35287,7 +36509,15 @@ var pvt = {
35287
36509
  title: "Price Volume Trend",
35288
36510
  shortTitle: "PVT",
35289
36511
  overlay: false,
35290
- inputs: [colorInput()],
36512
+ inputs: [
36513
+ boolInput("showSignal", "Signal Line", false, SETTINGS, "Display a moving average of PVT acting as a signal line."),
36514
+ optionInput("signalType", "Signal Type", "SMA", ["SMA", "EMA"], "Moving average type used for the signal line."),
36515
+ lengthInput(21, "signalLength", "Signal Length", 5e3, "Number of bars used to compute the signal line."),
36516
+ colorInput(NEUTRAL, "PVT Color", "color", "Color of the PVT line while it is flat."),
36517
+ colorInput(BULLISH, "Rising", "bullColor", "Color of the PVT line while it rises bar to bar."),
36518
+ colorInput(BEARISH, "Falling", "bearColor", "Color of the PVT line while it falls bar to bar."),
36519
+ colorInput(WARNING, "Signal Color", "signalColor", "Color of the signal line.")
36520
+ ],
35291
36521
  compute: (bars, inputs) => {
35292
36522
  const c = closes(bars);
35293
36523
  const raw = bars.map((b, i) => {
@@ -35295,7 +36525,17 @@ var pvt = {
35295
36525
  const pc = c[i - 1];
35296
36526
  return pc === 0 ? 0 : (c[i] - pc) / pc * b.volume;
35297
36527
  });
35298
- return { plots: [{ key: "pvt", title: "PVT", values: cumSum(raw), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
36528
+ const values = cumSum(raw);
36529
+ const flat = str3(inputs, "color", NEUTRAL);
36530
+ const plots = [
36531
+ { key: "pvt", title: "PVT", values, color: flat, width: 2, colors: slopeColors2(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), flat) }
36532
+ ];
36533
+ if (bool(inputs, "showSignal", false)) {
36534
+ const len = num3(inputs, "signalLength", 21);
36535
+ const signal = str3(inputs, "signalType", "SMA") === "EMA" ? ema(values, len) : sma(values, len);
36536
+ plots.push({ key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) });
36537
+ }
36538
+ return { plots };
35299
36539
  }
35300
36540
  };
35301
36541
  var volumeOscillator = {
@@ -35303,39 +36543,75 @@ var volumeOscillator = {
35303
36543
  title: "Volume Oscillator",
35304
36544
  shortTitle: "Vol Osc",
35305
36545
  overlay: false,
35306
- inputs: [lengthInput(5, "fastLength", "Fast length"), lengthInput(10, "slowLength", "Slow length"), colorInput()],
36546
+ inputs: [
36547
+ lengthInput(5, "fastLength", "Fast Length", 5e3, "Length of the fast EMA of volume."),
36548
+ lengthInput(10, "slowLength", "Slow Length", 5e3, "Length of the slow EMA of volume."),
36549
+ colorInput(BULLISH, "Above Zero", "color", "Color of the line and fill while it holds above zero \u2014 volume expanding."),
36550
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while it holds below zero \u2014 volume contracting.")
36551
+ ],
35307
36552
  compute: (bars, inputs) => {
35308
36553
  const vol = volumes(bars);
35309
36554
  const fast = ema(vol, num3(inputs, "fastLength", 5));
35310
36555
  const slow = ema(vol, num3(inputs, "slowLength", 10));
35311
- const values = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : 100 * (f - s) / s);
35312
- return {
35313
- plots: [{ key: "vo", title: "Volume Osc", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35314
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35315
- };
36556
+ const values = zip(fast, slow, (f, s) => s === 0 ? 0 : 100 * (f - s) / s);
36557
+ const bull = str3(inputs, "color", BULLISH);
36558
+ return centeredOscillator({
36559
+ key: "vo",
36560
+ title: "Volume Oscillator",
36561
+ values,
36562
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
36563
+ zeroInk: NEUTRAL,
36564
+ valueTransparency: 60
36565
+ });
35316
36566
  }
35317
36567
  };
36568
+ var OSCILLATOR = "Oscillator";
35318
36569
  var pvo = {
35319
36570
  type: "pvo",
35320
36571
  title: "Percentage Volume Oscillator",
35321
36572
  shortTitle: "PVO",
35322
36573
  overlay: false,
35323
- inputs: [lengthInput(12, "fastLength", "Fast length"), lengthInput(26, "slowLength", "Slow length"), lengthInput(9, "signalLength", "Signal length")],
36574
+ inputs: [
36575
+ { ...lengthInput(12, "fastLength", "Fast Length", 5e3, "Period of the fast EMA of volume. The conventional setting is 12."), group: OSCILLATOR },
36576
+ { ...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 },
36577
+ { ...lengthInput(9, "signalLength", "Signal Length", 500, "Period of the EMA applied to the PVO to form the signal line."), group: OSCILLATOR },
36578
+ colorInput(BULLISH, "Expansion Color", "color", "Color of the oscillator and fill while participation expands."),
36579
+ colorInput(BEARISH, "Contraction Color", "bearColor", "Color of the oscillator and fill while participation contracts."),
36580
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
36581
+ colorInput(transp(NEUTRAL, 50), "Zero Line", "zeroColor", "Color of the zero reference line."),
36582
+ boolInput("showSignal", "Show Signal Line", true, STYLE, "Plot the signal line, an EMA of the PVO."),
36583
+ boolInput("showHistogram", "Show Histogram", true, STYLE, "Plot the difference between the PVO and its signal line as columns behind the oscillator.")
36584
+ ],
35324
36585
  compute: (bars, inputs) => {
35325
36586
  const vol = volumes(bars);
35326
36587
  const fast = ema(vol, num3(inputs, "fastLength", 12));
35327
36588
  const slow = ema(vol, num3(inputs, "slowLength", 26));
35328
- const line = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : 100 * (f - s) / s);
35329
- const signal = ema(line, num3(inputs, "signalLength", 9));
35330
- const hist = sub(line, signal);
35331
- return {
35332
- plots: [
35333
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: hist.map((x) => Number.isFinite(x) ? x >= 0 ? BULLISH : BEARISH : null), base: 0 },
35334
- { key: "pvo", title: "PVO", values: line, color: SERIES_LINE, width: 2 },
35335
- { key: "signal", title: "Signal", values: signal, color: WARNING }
35336
- ],
35337
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35338
- };
36589
+ const values = zip(fast, slow, (f, s) => s > 0 ? 100 * (f - s) / s : Number.NaN);
36590
+ const signal = ema(values, num3(inputs, "signalLength", 9));
36591
+ const expansion = str3(inputs, "color", BULLISH);
36592
+ const contraction = str3(inputs, "bearColor", BEARISH);
36593
+ const extraPlots = [];
36594
+ if (bool(inputs, "showHistogram", true)) {
36595
+ const hist = sub(values, signal);
36596
+ extraPlots.push({
36597
+ key: "hist",
36598
+ title: "Histogram",
36599
+ values: hist,
36600
+ kind: "columns",
36601
+ color: transp(expansion, 60),
36602
+ colors: hist.map((x) => Number.isFinite(x) ? transp(x > 0 ? expansion : contraction, 60) : null),
36603
+ base: 0
36604
+ });
36605
+ }
36606
+ if (bool(inputs, "showSignal", true)) extraPlots.push({ key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) });
36607
+ return centeredOscillator({
36608
+ key: "pvo",
36609
+ title: "PVO",
36610
+ values,
36611
+ ink: { bull: expansion, bear: contraction, neutral: contraction },
36612
+ zeroInk: str3(inputs, "zeroColor", transp(NEUTRAL, 50)),
36613
+ extraPlots
36614
+ });
35339
36615
  }
35340
36616
  };
35341
36617
  var vfi = {
@@ -35344,55 +36620,86 @@ var vfi = {
35344
36620
  shortTitle: "Volume Flow Indicator",
35345
36621
  overlay: false,
35346
36622
  inputs: [
35347
- lengthInput(130),
35348
- { key: "coef", title: "Cutoff coefficient", type: "float", defval: 0.2, min: 0.01, max: 10, step: 0.01 },
35349
- { key: "volCoef", title: "Volume cap", type: "float", defval: 2.5, min: 0.1, max: 50, step: 0.1 },
35350
- lengthInput(3, "smoothLength", "Smoothing length")
36623
+ { ...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 },
36624
+ 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."),
36625
+ 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."),
36626
+ { ...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 },
36627
+ boolInput("smooth", "Smooth VFI", true, SETTINGS, "Applies the published light smoothing \u2014 an EMA of the summed line \u2014 before plotting."),
36628
+ lengthInput(3, "smoothLength", "Smoothing Length", 500, "Length of the smoothing EMA. The published version uses 3 bars."),
36629
+ boolInput("showSignal", "Signal Line", true, SETTINGS, "Plots an EMA of VFI as a signal line."),
36630
+ lengthInput(5, "signalLength", "Signal Length", 500, "Length of the signal-line EMA."),
36631
+ colorInput(BULLISH, "Bullish", "color", "Color of the VFI line and gradient fill above the zero line \u2014 the net accumulation regime."),
36632
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the VFI line and gradient fill below the zero line \u2014 the net distribution regime."),
36633
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
36634
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the zero line and the VFI line with a vertical gradient.")
35351
36635
  ],
35352
36636
  compute: (bars, inputs) => {
35353
36637
  const len = num3(inputs, "length", 130);
36638
+ const typical = sourceValues(bars, "HLC3");
36639
+ const logChange = sub(map(typical, Math.log), shift(map(typical, Math.log), 1));
36640
+ const volatility = stdev(logChange, num3(inputs, "volLength", 30));
35354
36641
  const coef = num3(inputs, "coef", 0.2);
35355
36642
  const volCoef = num3(inputs, "volCoef", 2.5);
35356
- const typical = map(sourceValues(bars, "HLC3"), Math.log);
35357
- const inter = change(typical);
35358
- const vInter = stdev(inter, 30);
35359
36643
  const vol = volumes(bars);
35360
- const vAve = sma(vol, len);
35361
- const mf = change(sourceValues(bars, "HLC3"));
35362
- const raw = bars.map((b, i) => {
35363
- const cutoff = Number.isFinite(vInter[i]) ? coef * vInter[i] * b.close : Number.NaN;
36644
+ const avgVolume = shift(sma(vol, len), 1);
36645
+ const typicalChange = change(typical);
36646
+ const signed3 = bars.map((b, i) => {
36647
+ const sd = volatility[i];
36648
+ const m = typicalChange[i];
35364
36649
  const v = vol[i];
35365
- const ave = i > 0 ? vAve[i - 1] : Number.NaN;
35366
- const m = mf[i];
35367
- if (!Number.isFinite(cutoff) || !Number.isFinite(v) || !Number.isFinite(ave) || !Number.isFinite(m)) return Number.NaN;
35368
- const capped = Math.min(v, ave * volCoef);
36650
+ if (!Number.isFinite(sd) || !Number.isFinite(m) || !Number.isFinite(v)) return Number.NaN;
36651
+ const cutoff = coef * sd * b.close;
36652
+ const avg = avgVolume[i];
36653
+ const capped = Number.isFinite(avg) ? Math.min(v, avg * volCoef) : v;
35369
36654
  return m > cutoff ? capped : m < -cutoff ? -capped : 0;
35370
36655
  });
35371
- const values = bars.map((_, i) => {
35372
- const ave = i > 0 ? vAve[i - 1] : Number.NaN;
35373
- return Number.isFinite(ave) && ave !== 0 ? ave : Number.NaN;
36656
+ const raw = zip(sum(signed3, len), avgVolume, (f, a) => a > 0 ? f / a : Number.NaN);
36657
+ const values = bool(inputs, "smooth", true) ? ema(raw, num3(inputs, "smoothLength", 3)) : raw;
36658
+ const bull = str3(inputs, "color", BULLISH);
36659
+ const extraPlots = bool(inputs, "showSignal", true) ? [{ key: "signal", title: "Signal Line", values: ema(values, num3(inputs, "signalLength", 5)), color: str3(inputs, "signalColor", WARNING) }] : [];
36660
+ return centeredOscillator({
36661
+ key: "vfi",
36662
+ title: "VFI",
36663
+ values,
36664
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "bearColor", BEARISH) },
36665
+ zeroInk: transp(NEUTRAL, 90),
36666
+ fill: bool(inputs, "gradientFill", true),
36667
+ extraPlots
35374
36668
  });
35375
- const flowSum = sum(raw, len);
35376
- const line = zip(flowSum, values, (f, a) => f / a);
35377
- return {
35378
- plots: [{ key: "vfi", title: "VFI", values: sma(line, num3(inputs, "smoothLength", 3)), color: SERIES_LINE, width: 2 }],
35379
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35380
- };
35381
36669
  }
35382
36670
  };
36671
+ var II_OSCILLATOR = "Normalized Oscillator (II%)";
36672
+ var II_CUMULATIVE = "Cumulative Line";
35383
36673
  var intradayIntensity = {
35384
36674
  type: "intraday-intensity",
35385
36675
  title: "Intraday Intensity",
35386
36676
  shortTitle: "Intraday Intensity",
35387
36677
  overlay: false,
35388
- inputs: [lengthInput(21), colorInput()],
36678
+ inputs: [
36679
+ 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."),
36680
+ 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."),
36681
+ colorInput(BULLISH, "Bullish", "color", "Color of positive readings, the rising cumulative line and the fill above zero \u2014 the accumulation side."),
36682
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of negative readings, the falling cumulative line and the fill below zero \u2014 the distribution side."),
36683
+ 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.")
36684
+ ],
35389
36685
  compute: (bars, inputs) => {
36686
+ const intensity = moneyFlowVolume(bars);
36687
+ const bull = str3(inputs, "color", BULLISH);
36688
+ const bear = str3(inputs, "bearColor", BEARISH);
36689
+ if (str3(inputs, "mode", II_OSCILLATOR) === II_CUMULATIVE) {
36690
+ const values2 = cumSum(intensity);
36691
+ return { plots: [{ key: "ii", title: "Intraday Intensity", values: values2, color: bull, colors: slopeColors2(values2, bull, bear, bull) }] };
36692
+ }
35390
36693
  const len = num3(inputs, "length", 21);
35391
- const values = zip(sum(moneyFlowVolume(bars), len), sum(volumes(bars), len), (m, v) => v === 0 ? 0 : 100 * m / v);
35392
- return {
35393
- plots: [{ key: "ii", title: "II%", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35394
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35395
- };
36694
+ const values = zip(sum(intensity, len), sum(volumes(bars), len), (m, v) => v > 0 ? 100 * m / v : Number.NaN);
36695
+ return centeredOscillator({
36696
+ key: "ii",
36697
+ title: "Intraday Intensity",
36698
+ values,
36699
+ ink: { bull, bear, neutral: bear },
36700
+ zeroInk: transp(NEUTRAL, 90),
36701
+ fill: bool(inputs, "gradientFill", true)
36702
+ });
35396
36703
  }
35397
36704
  };
35398
36705
  var volumeSpecs = [
@@ -35425,15 +36732,24 @@ function periodKey(anchor, time) {
35425
36732
  }
35426
36733
  return Math.floor(time / DAY_MS2);
35427
36734
  }
36735
+ function barInterval(bars) {
36736
+ if (bars.length < 2) return 0;
36737
+ const gaps = [];
36738
+ for (let i = 1; i < bars.length; i++) gaps.push(bars[i].time - bars[i - 1].time);
36739
+ gaps.sort((a, b) => a - b);
36740
+ return gaps[Math.floor(gaps.length / 2)] ?? 0;
36741
+ }
35428
36742
  var parabolicSar = {
35429
36743
  type: "parabolic-sar",
35430
36744
  title: "Parabolic SAR",
35431
36745
  shortTitle: "SAR",
35432
36746
  overlay: true,
35433
36747
  inputs: [
35434
- { key: "start", title: "Start", type: "float", defval: 0.02, min: 1e-3, max: 1, step: 1e-3 },
35435
- { key: "increment", title: "Increment", type: "float", defval: 0.02, min: 1e-3, max: 1, step: 1e-3 },
35436
- { key: "maximum", title: "Max value", type: "float", defval: 0.2, min: 0.01, max: 1, step: 0.01 }
36748
+ floatInput("start", "Start", 0.02, 0, 1, 0.01, "Initial acceleration factor applied when a new trend begins."),
36749
+ floatInput("increment", "Increment", 0.02, 0, 1, 0.01, "Amount added to the acceleration factor each time the trend makes a new extreme point."),
36750
+ floatInput("maximum", "Maximum", 0.2, 0.01, 1, 0.01, "Upper cap on the acceleration factor."),
36751
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the SAR crosses while they trail below price \u2014 uptrend, the stop sits under the market."),
36752
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the SAR crosses while they sit above price \u2014 downtrend, the stop sits over the market.")
35437
36753
  ],
35438
36754
  compute: (bars, inputs) => {
35439
36755
  const start = num3(inputs, "start", 0.02);
@@ -35441,7 +36757,6 @@ var parabolicSar = {
35441
36757
  const max = num3(inputs, "maximum", 0.2);
35442
36758
  const n = bars.length;
35443
36759
  const values = new Array(n).fill(Number.NaN);
35444
- const colors = new Array(n).fill(null);
35445
36760
  if (n >= 2) {
35446
36761
  let long = bars[1].close >= bars[0].close;
35447
36762
  let sar = long ? bars[0].low : bars[0].high;
@@ -35466,18 +36781,21 @@ var parabolicSar = {
35466
36781
  af = Math.min(max, af + inc);
35467
36782
  }
35468
36783
  values[i] = sar;
35469
- colors[i] = long ? BULLISH : BEARISH;
35470
36784
  }
35471
36785
  }
35472
- return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
36786
+ const bull = str3(inputs, "bullColor", BULLISH);
36787
+ const bear = str3(inputs, "bearColor", BEARISH);
36788
+ const colors = values.map((x, i) => Number.isFinite(x) ? x < bars[i].close ? bull : bear : null);
36789
+ return { plots: [{ key: "sar", title: "SAR", values, kind: "cross", color: bull, colors }] };
35473
36790
  }
35474
36791
  };
35475
- var VWAP_BANDS_GROUP = "Bands";
35476
- var VWAP_STYLE = "Style";
36792
+ var VWAP_ANCHORS = ["Session", "Week", "Month", "Quarter", "Year"];
36793
+ var BANDS_STDEV = "Standard Deviation";
36794
+ var BANDS_PERCENT = "Percentage";
35477
36795
  var VWAP_BANDS = [
35478
- { on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
35479
- { on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
35480
- { on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
36796
+ { show: "band1", mult: "band1Mult", defMult: 1, defOn: true },
36797
+ { show: "band2", mult: "band2Mult", defMult: 2, defOn: false },
36798
+ { show: "band3", mult: "band3Mult", defMult: 3, defOn: false }
35481
36799
  ];
35482
36800
  var vwap = {
35483
36801
  type: "vwap",
@@ -35485,39 +36803,36 @@ var vwap = {
35485
36803
  shortTitle: "VWAP",
35486
36804
  overlay: true,
35487
36805
  inputs: [
35488
- { key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
35489
- sourceInput("HLC3"),
35490
- // Each band is one row: its toggle leads, the multiplier follows unlabeled.
36806
+ 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)."),
36807
+ sourceInput("HLC3", "source", "Source", "Price used in the volume weighted average. Typical price (HLC3) is the classic choice."),
36808
+ 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."),
35491
36809
  ...VWAP_BANDS.flatMap((b, i) => [
35492
- { key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
35493
- { key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
36810
+ boolInput(b.show, `Band #${i + 1}`, b.defOn, SETTINGS, `Show the ${["first", "second", "third"][i]} band pair.`),
36811
+ 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.")
35494
36812
  ]),
35495
- { ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
35496
- // One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
35497
- ...VWAP_BANDS.flatMap((b, i) => [
35498
- { ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
35499
- { key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
35500
- { ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
35501
- ])
36813
+ 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."),
36814
+ colorInput(BULLISH, "Bullish", "bullColor", "VWAP line color while price closes above it, and the color of the lower (support) bands."),
36815
+ colorInput(BEARISH, "Bearish", "bearColor", "VWAP line color while price closes below it, and the color of the upper (resistance) bands."),
36816
+ colorInput(NEUTRAL, "Bands", "bandsColor", "Deviation bands fill color."),
36817
+ boolInput("fill", "Bands Fill", true, STYLE, "Fill the area between each visible band pair.")
35502
36818
  ],
35503
36819
  compute: (bars, inputs) => {
35504
- const anchor = str3(inputs, "anchor", "Day");
36820
+ const anchor = str3(inputs, "anchor", "Session");
35505
36821
  const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
35506
36822
  const n = bars.length;
36823
+ const hidden = bool(inputs, "hideDwm", true) && barInterval(bars) >= DAY_MS2;
35507
36824
  const values = new Array(n).fill(Number.NaN);
35508
- const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
36825
+ const bands = VWAP_BANDS.filter((b) => bool(inputs, b.show, b.defOn)).map((b) => ({
35509
36826
  mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
35510
- color: str3(inputs, b.color, b.defColor),
35511
- fill: bool(inputs, b.fill, true),
35512
- fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
35513
36827
  up: new Array(n).fill(Number.NaN),
35514
36828
  down: new Array(n).fill(Number.NaN)
35515
36829
  }));
36830
+ const percentMode = str3(inputs, "bandsMode", BANDS_STDEV) === BANDS_PERCENT;
35516
36831
  let period = Number.NaN;
35517
36832
  let cumPV = 0;
35518
36833
  let cumPV2 = 0;
35519
36834
  let cumV = 0;
35520
- for (let i = 0; i < n; i++) {
36835
+ for (let i = 0; i < n && !hidden; i++) {
35521
36836
  const b = bars[i];
35522
36837
  const key = periodKey(anchor, b.time);
35523
36838
  if (key !== period) {
@@ -35535,38 +36850,58 @@ var vwap = {
35535
36850
  }
35536
36851
  if (cumV <= 0) continue;
35537
36852
  const mean = cumPV / cumV;
35538
- const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
36853
+ const offset = percentMode ? mean / 100 : Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
35539
36854
  values[i] = mean;
35540
36855
  for (const band of bands) {
35541
- band.up[i] = mean + band.mult * sd;
35542
- band.down[i] = mean - band.mult * sd;
36856
+ band.up[i] = mean + band.mult * offset;
36857
+ band.down[i] = mean - band.mult * offset;
35543
36858
  }
35544
36859
  }
35545
- const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
36860
+ const bull = str3(inputs, "bullColor", BULLISH);
36861
+ const bear = str3(inputs, "bearColor", BEARISH);
36862
+ const fillColor = transp(str3(inputs, "bandsColor", NEUTRAL), 95);
36863
+ const plots = [
36864
+ { key: "vwap", title: "VWAP", values, color: bull, colors: values.map((x, i) => Number.isFinite(x) ? bars[i].close >= x ? bull : bear : null) }
36865
+ ];
35546
36866
  const fills = [];
35547
36867
  bands.forEach((band, i) => {
35548
36868
  plots.push(
35549
- { key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
35550
- { key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
36869
+ { key: `up${i}`, title: `Upper Band #${i + 1}`, values: band.up, color: transp(bear, 40) },
36870
+ { key: `down${i}`, title: `Lower Band #${i + 1}`, values: band.down, color: transp(bull, 40) }
35551
36871
  );
35552
- if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
36872
+ if (bool(inputs, "fill", true)) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: fillColor });
35553
36873
  });
35554
36874
  return { plots, bands: fills };
35555
36875
  }
35556
36876
  };
36877
+ var PIVOT_TRADITIONAL = "Traditional";
36878
+ var PIVOT_FIBONACCI = "Fibonacci";
36879
+ var PIVOT_CAMARILLA = "Camarilla";
36880
+ var PIVOT_WOODIE = "Woodie";
36881
+ var PIVOT_AUTO = "Auto";
35557
36882
  var pivotPoints = {
35558
36883
  type: "pivot-points",
35559
- title: "Pivot Points",
36884
+ title: "Pivot Points Standard",
35560
36885
  shortTitle: "Pivots",
35561
36886
  overlay: true,
35562
- inputs: [{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month"] }],
36887
+ inputs: [
36888
+ 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."),
36889
+ 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."),
36890
+ colorInput(WARNING, "Pivot", "pivotColor", "Color of the central pivot (P) level."),
36891
+ colorInput(BEARISH, "Resistances", "resistanceColor", "Color of the resistance (R) levels."),
36892
+ colorInput(BULLISH, "Supports", "supportColor", "Color of the support (S) levels.")
36893
+ ],
35563
36894
  compute: (bars, inputs) => {
35564
- const anchor = str3(inputs, "anchor", "Day");
36895
+ const interval = barInterval(bars);
36896
+ const requested = str3(inputs, "anchor", PIVOT_AUTO);
36897
+ const auto = interval <= 15 * 6e4 ? "Session" : interval < DAY_MS2 ? "Week" : interval <= DAY_MS2 ? "Month" : "Year";
36898
+ const anchor = requested === "Daily" ? "Session" : requested === "Weekly" ? "Week" : requested === "Monthly" ? "Month" : requested === "Yearly" ? "Year" : auto;
36899
+ const kind = str3(inputs, "kind", PIVOT_TRADITIONAL);
35565
36900
  const aggs = /* @__PURE__ */ new Map();
35566
36901
  for (const b of bars) {
35567
36902
  const key = periodKey(anchor, b.time);
35568
36903
  const a = aggs.get(key);
35569
- if (!a) aggs.set(key, { high: b.high, low: b.low, close: b.close });
36904
+ if (!a) aggs.set(key, { open: b.open, high: b.high, low: b.low, close: b.close });
35570
36905
  else {
35571
36906
  a.high = Math.max(a.high, b.high);
35572
36907
  a.low = Math.min(a.low, b.low);
@@ -35575,31 +36910,61 @@ var pivotPoints = {
35575
36910
  }
35576
36911
  const n = bars.length;
35577
36912
  const mk = () => new Array(n).fill(Number.NaN);
35578
- const levels = { p: mk(), r1: mk(), s1: mk(), r2: mk(), s2: mk(), r3: mk(), s3: mk() };
36913
+ const levels = { p: mk(), r1: mk(), s1: mk(), r2: mk(), s2: mk(), r3: mk(), s3: mk(), r4: mk(), s4: mk() };
36914
+ let lastKey = Number.NaN;
35579
36915
  for (let i = 0; i < n; i++) {
35580
- const prev2 = aggs.get(periodKey(anchor, bars[i].time) - 1);
35581
- if (!prev2) continue;
35582
- const p = (prev2.high + prev2.low + prev2.close) / 3;
36916
+ const key = periodKey(anchor, bars[i].time);
36917
+ const fresh = key !== lastKey;
36918
+ lastKey = key;
36919
+ if (fresh) continue;
36920
+ const prev2 = aggs.get(key - 1);
36921
+ const curr = aggs.get(key);
36922
+ if (!prev2 || !curr) continue;
36923
+ const range = prev2.high - prev2.low;
36924
+ const traditional = (prev2.high + prev2.low + prev2.close) / 3;
36925
+ const p = kind === PIVOT_WOODIE ? (prev2.high + prev2.low + 2 * curr.open) / 4 : traditional;
35583
36926
  levels.p[i] = p;
35584
- levels.r1[i] = 2 * p - prev2.low;
35585
- levels.s1[i] = 2 * p - prev2.high;
35586
- levels.r2[i] = p + (prev2.high - prev2.low);
35587
- levels.s2[i] = p - (prev2.high - prev2.low);
35588
- levels.r3[i] = prev2.high + 2 * (p - prev2.low);
35589
- levels.s3[i] = prev2.low - 2 * (prev2.high - p);
35590
- }
35591
- const plot = (key, title, color) => ({ key, title, kind: "step", values: levels[key], color });
35592
- return {
35593
- plots: [
35594
- plot("p", "P", WARNING),
35595
- plot("r1", "R1", BEARISH),
35596
- plot("s1", "S1", BULLISH),
35597
- plot("r2", "R2", BEARISH),
35598
- plot("s2", "S2", BULLISH),
35599
- plot("r3", "R3", BEARISH),
35600
- plot("s3", "S3", BULLISH)
35601
- ]
35602
- };
36927
+ if (kind === PIVOT_FIBONACCI) {
36928
+ levels.r1[i] = traditional + 0.382 * range;
36929
+ levels.s1[i] = traditional - 0.382 * range;
36930
+ levels.r2[i] = traditional + 0.618 * range;
36931
+ levels.s2[i] = traditional - 0.618 * range;
36932
+ levels.r3[i] = traditional + range;
36933
+ levels.s3[i] = traditional - range;
36934
+ } else if (kind === PIVOT_CAMARILLA) {
36935
+ const cam = 1.1 * range;
36936
+ levels.r1[i] = prev2.close + cam / 12;
36937
+ levels.s1[i] = prev2.close - cam / 12;
36938
+ levels.r2[i] = prev2.close + cam / 6;
36939
+ levels.s2[i] = prev2.close - cam / 6;
36940
+ levels.r3[i] = prev2.close + cam / 4;
36941
+ levels.s3[i] = prev2.close - cam / 4;
36942
+ levels.r4[i] = prev2.close + cam / 2;
36943
+ levels.s4[i] = prev2.close - cam / 2;
36944
+ } else {
36945
+ levels.r1[i] = 2 * p - prev2.low;
36946
+ levels.s1[i] = 2 * p - prev2.high;
36947
+ levels.r2[i] = p + range;
36948
+ levels.s2[i] = p - range;
36949
+ if (kind !== PIVOT_WOODIE) {
36950
+ levels.r3[i] = prev2.high + 2 * (p - prev2.low);
36951
+ levels.s3[i] = prev2.low - 2 * (prev2.high - p);
36952
+ }
36953
+ }
36954
+ }
36955
+ const resistance = str3(inputs, "resistanceColor", BEARISH);
36956
+ const support = str3(inputs, "supportColor", BULLISH);
36957
+ const plot = (key, title, color) => ({ key, title, values: levels[key], color });
36958
+ const plots = [
36959
+ plot("p", "P", str3(inputs, "pivotColor", WARNING)),
36960
+ plot("r1", "R1", resistance),
36961
+ plot("s1", "S1", support),
36962
+ plot("r2", "R2", resistance),
36963
+ plot("s2", "S2", support)
36964
+ ];
36965
+ if (kind !== PIVOT_WOODIE) plots.push(plot("r3", "R3", resistance), plot("s3", "S3", support));
36966
+ if (kind === PIVOT_CAMARILLA) plots.push(plot("r4", "R4", resistance), plot("s4", "S4", support));
36967
+ return { plots };
35603
36968
  }
35604
36969
  };
35605
36970
  var fiftyTwoWeek = {
@@ -35607,15 +36972,25 @@ var fiftyTwoWeek = {
35607
36972
  title: "52 Week High/Low",
35608
36973
  shortTitle: "52W H/L",
35609
36974
  overlay: true,
35610
- inputs: [{ key: "weeks", title: "Weeks", type: "int", defval: 52, min: 1, max: 520, step: 1 }],
36975
+ inputs: [
36976
+ intInput("weeks", "Weeks", 52, 1, 520, "Length of the rolling window, in weeks."),
36977
+ 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."),
36978
+ colorInput(BULLISH, "52 Week High", "highColor", "Color of the 52 week high level."),
36979
+ colorInput(BEARISH, "52 Week Low", "lowColor", "Color of the 52 week low level."),
36980
+ colorInput(NEUTRAL, "All-Time High/Low", "allTimeColor", "Color of the all-time high and all-time low levels.")
36981
+ ],
35611
36982
  compute: (bars, inputs) => {
35612
36983
  const span = num3(inputs, "weeks", 52) * 7 * DAY_MS2;
35613
36984
  const n = bars.length;
35614
36985
  const hi = new Array(n).fill(Number.NaN);
35615
36986
  const lo = new Array(n).fill(Number.NaN);
36987
+ const ath = new Array(n).fill(Number.NaN);
36988
+ const atl = new Array(n).fill(Number.NaN);
35616
36989
  const maxIdx = [];
35617
36990
  const minIdx = [];
35618
36991
  let from = 0;
36992
+ let runHigh = -Infinity;
36993
+ let runLow = Infinity;
35619
36994
  for (let i = 0; i < n; i++) {
35620
36995
  const b = bars[i];
35621
36996
  while (maxIdx.length > 0 && bars[maxIdx[maxIdx.length - 1]].high <= b.high) maxIdx.pop();
@@ -35627,13 +37002,20 @@ var fiftyTwoWeek = {
35627
37002
  while (minIdx[0] < from) minIdx.shift();
35628
37003
  hi[i] = bars[maxIdx[0]].high;
35629
37004
  lo[i] = bars[minIdx[0]].low;
37005
+ runHigh = Math.max(runHigh, b.high);
37006
+ runLow = Math.min(runLow, b.low);
37007
+ ath[i] = runHigh;
37008
+ atl[i] = runLow;
37009
+ }
37010
+ const plots = [
37011
+ { key: "high", title: "52 Week High", values: hi, kind: "step", color: str3(inputs, "highColor", BULLISH), width: 2 },
37012
+ { key: "low", title: "52 Week Low", values: lo, kind: "step", color: str3(inputs, "lowColor", BEARISH), width: 2 }
37013
+ ];
37014
+ if (bool(inputs, "showAllTime", false)) {
37015
+ const ink = str3(inputs, "allTimeColor", NEUTRAL);
37016
+ 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 });
35630
37017
  }
35631
- return {
35632
- plots: [
35633
- { key: "high", title: "52W high", values: hi, kind: "step", color: BULLISH },
35634
- { key: "low", title: "52W low", values: lo, kind: "step", color: BEARISH }
35635
- ]
35636
- };
37018
+ return { plots };
35637
37019
  }
35638
37020
  };
35639
37021
  var zigzag = {
@@ -35641,9 +37023,11 @@ var zigzag = {
35641
37023
  title: "ZigZag",
35642
37024
  overlay: true,
35643
37025
  inputs: [
35644
- { key: "deviation", title: "Deviation %", type: "float", defval: 5, min: 0.01, max: 100, step: 0.1 },
35645
- { key: "depth", title: "Depth", type: "int", defval: 10, min: 1, max: 500, step: 1 },
35646
- colorInput()
37026
+ 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."),
37027
+ { ...intInput("depth", "Depth", 10, 2, 500, "Minimum number of bars required between two consecutive pivots.") },
37028
+ colorInput(BULLISH, "Up Leg", "bullColor", "Color of rising zigzag segments."),
37029
+ colorInput(BEARISH, "Down Leg", "bearColor", "Color of falling zigzag segments."),
37030
+ { key: "lineWidth", title: "Width", type: "int", defval: 2, min: 1, max: 10, step: 1, group: STYLE, tooltip: "Width of the zigzag segments." }
35647
37031
  ],
35648
37032
  compute: (bars, inputs) => {
35649
37033
  const dev = num3(inputs, "deviation", 5) / 100;
@@ -35656,14 +37040,14 @@ var zigzag = {
35656
37040
  const b = bars[i];
35657
37041
  if (up) {
35658
37042
  if (b.high >= ext.price) ext = { i, price: b.high };
35659
- else if (b.low <= ext.price * (1 - dev) && i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
37043
+ else if (b.low <= ext.price * (1 - dev) && ext.i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
35660
37044
  pivots.push(ext);
35661
37045
  up = false;
35662
37046
  ext = { i, price: b.low };
35663
37047
  }
35664
37048
  } else if (b.low <= ext.price) {
35665
37049
  ext = { i, price: b.low };
35666
- } else if (b.high >= ext.price * (1 + dev) && i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
37050
+ } else if (b.high >= ext.price * (1 + dev) && ext.i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
35667
37051
  pivots.push(ext);
35668
37052
  up = true;
35669
37053
  ext = { i, price: b.high };
@@ -35671,22 +37055,29 @@ var zigzag = {
35671
37055
  }
35672
37056
  pivots.push(ext);
35673
37057
  }
35674
- return {
35675
- plots: [],
35676
- polylines: [
35677
- {
35678
- key: "zigzag",
35679
- points: pivots.map((p) => ({ xloc: "bar_time", x: bars[p.i].time, price: p.price })),
35680
- curved: false,
35681
- closed: false,
35682
- lineColor: str3(inputs, "color", SERIES_LINE),
35683
- lineWidth: 2,
35684
- lineStyle: "solid",
35685
- arrowLeft: false,
35686
- arrowRight: false
35687
- }
35688
- ]
35689
- };
37058
+ const bull = str3(inputs, "bullColor", BULLISH);
37059
+ const bear = str3(inputs, "bearColor", BEARISH);
37060
+ const width = num3(inputs, "lineWidth", 2);
37061
+ const polylines = [];
37062
+ for (let k = 1; k < pivots.length; k++) {
37063
+ const a = pivots[k - 1];
37064
+ const b = pivots[k];
37065
+ polylines.push({
37066
+ key: `leg${k}`,
37067
+ points: [
37068
+ { xloc: "bar_time", x: bars[a.i].time, price: a.price },
37069
+ { xloc: "bar_time", x: bars[b.i].time, price: b.price }
37070
+ ],
37071
+ curved: false,
37072
+ closed: false,
37073
+ lineColor: b.price >= a.price ? bull : bear,
37074
+ lineWidth: width,
37075
+ lineStyle: "solid",
37076
+ arrowLeft: false,
37077
+ arrowRight: false
37078
+ });
37079
+ }
37080
+ return { plots: [], polylines };
35690
37081
  }
35691
37082
  };
35692
37083
  var williamsFractal = {
@@ -35694,10 +37085,16 @@ var williamsFractal = {
35694
37085
  title: "Williams Fractal",
35695
37086
  shortTitle: "Fractals",
35696
37087
  overlay: true,
35697
- inputs: [{ key: "periods", title: "Periods", type: "int", defval: 2, min: 1, max: 50, step: 1 }],
37088
+ inputs: [
37089
+ 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."),
37090
+ colorInput(BEARISH, "Up Fractal", "upColor", "Color of the up fractal marker (swing high), drawn above the fractal bar."),
37091
+ colorInput(BULLISH, "Down Fractal", "downColor", "Color of the down fractal marker (swing low), drawn below the fractal bar.")
37092
+ ],
35698
37093
  compute: (bars, inputs) => {
35699
37094
  const p = num3(inputs, "periods", 2);
35700
37095
  const markers = [];
37096
+ const up = str3(inputs, "upColor", BEARISH);
37097
+ const down = str3(inputs, "downColor", BULLISH);
35701
37098
  const isExtreme = (i, pick, better) => {
35702
37099
  const v = pick(bars[i]);
35703
37100
  for (let k = i - p; k <= i + p; k++) {
@@ -35707,12 +37104,8 @@ var williamsFractal = {
35707
37104
  return true;
35708
37105
  };
35709
37106
  for (let i = p; i < bars.length - p; i++) {
35710
- if (isExtreme(i, (b) => b.high, (a, b) => a >= b)) {
35711
- markers.push({ time: bars[i].time, position: "aboveBar", shape: "triangleup", color: BULLISH });
35712
- }
35713
- if (isExtreme(i, (b) => b.low, (a, b) => a <= b)) {
35714
- markers.push({ time: bars[i].time, position: "belowBar", shape: "triangledown", color: BEARISH });
35715
- }
37107
+ if (isExtreme(i, (b) => b.high, (a, b) => a > b)) markers.push({ time: bars[i].time, position: "aboveBar", shape: "triangleup", color: up });
37108
+ if (isExtreme(i, (b) => b.low, (a, b) => a < b)) markers.push({ time: bars[i].time, position: "belowBar", shape: "triangledown", color: down });
35716
37109
  }
35717
37110
  return { plots: [], markers };
35718
37111
  }