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