@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.
@@ -1,6 +1,6 @@
1
1
  import { deserializeDrawing, chartTypes, rendererLayers, isLineLikeSeries, seriesShownOn, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, nativeInstanceChannel, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, DEFAULT_DRAWING_COLOR, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, MAGNIFIER_TIMEFRAME_OPTIONS, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, seriesInScale, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS, stableSeriesId } from './chunk-BFA32GOU.js';
2
2
  import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, CALLOUT_STYLE_ID, CALLOUT_CSS, Popover, blendOver, splitColor, CalloutBubble, Menu, TextArea, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-WTWGEB7K.js';
3
- import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BULLISH, CATEGORICAL, SERIES_LINE, BEARISH, isDarkColor, INFO, injectStyles, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, iconMarkup, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-CAFCLMPF.js';
3
+ import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, INFO, BULLISH, BEARISH, isDarkColor, injectStyles, SERIES_LINE, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, iconMarkup, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-CAFCLMPF.js';
4
4
 
5
5
  // src/core/events/EventBus.ts
6
6
  var TypedEventBus = class {
@@ -1179,6 +1179,8 @@ var MultiProviderFeed = class {
1179
1179
  this.liveBars = [];
1180
1180
  /** Sync-accessible symbol metadata, warmed by load()/symbolInfoFor (the engine reads it synchronously). */
1181
1181
  this.symInfoCache = /* @__PURE__ */ new Map();
1182
+ /** Requests in flight, keyed like the cache — every concurrent asker shares one round trip. */
1183
+ this.symInfoInflight = /* @__PURE__ */ new Map();
1182
1184
  this.cache = new CachingDataFeed(new RegistryFetchFeed(this.registry), store);
1183
1185
  }
1184
1186
  // ── Registry surface (driven by chart.data / DataControl) ──────────────
@@ -1239,9 +1241,36 @@ var MultiProviderFeed = class {
1239
1241
  async symbolInfoFor(raw) {
1240
1242
  const resolved = this.registry.resolve(raw, { default: this.primaryProvider });
1241
1243
  if (!resolved) return void 0;
1242
- const info = await this.registry.get(resolved.provider)?.getSymbolInfo?.(resolved.ticker);
1243
- if (info) this.symInfoCache.set(symKey(resolved), info);
1244
- return info;
1244
+ return this.fetchSymbolInfo(resolved);
1245
+ }
1246
+ /**
1247
+ * One round trip per symbol, shared. A market switch probes this metadata from five
1248
+ * places at once (the bar load's prefetch, the renderer's tick size, the market-status
1249
+ * badge, the session shading, the session toggle) — without the in-flight map each
1250
+ * would open its own request for the same answer, and the last one to land decided how
1251
+ * late the chrome settled.
1252
+ *
1253
+ * Cached for {@link SYMBOL_INFO_TTL_MS}: this metadata is stable within a session
1254
+ * (tick size, session vocabulary, listing prefix), but not immutable — a continuous
1255
+ * futures row rolls its current contract — so the entry expires rather than pinning the
1256
+ * session's first answer forever. A failed probe is NOT remembered: the next asker retries.
1257
+ */
1258
+ fetchSymbolInfo(resolved) {
1259
+ const key = symKey(resolved);
1260
+ const hit = this.symInfoCache.get(key);
1261
+ if (hit && Date.now() - hit.at < SYMBOL_INFO_TTL_MS) return Promise.resolve(hit.info);
1262
+ const inflight = this.symInfoInflight.get(key);
1263
+ if (inflight) return inflight;
1264
+ const getSymbolInfo = this.registry.get(resolved.provider)?.getSymbolInfo;
1265
+ if (!getSymbolInfo) return Promise.resolve(void 0);
1266
+ const p = Promise.resolve(getSymbolInfo.call(this.registry.get(resolved.provider), resolved.ticker)).then((info) => {
1267
+ if (info) this.symInfoCache.set(key, { info, at: Date.now() });
1268
+ return info;
1269
+ }).finally(() => {
1270
+ this.symInfoInflight.delete(key);
1271
+ });
1272
+ this.symInfoInflight.set(key, p);
1273
+ return p;
1245
1274
  }
1246
1275
  /**
1247
1276
  * Per-symbol capabilities, resolved through the owning provider's `capabilitiesFor`
@@ -1285,16 +1314,10 @@ var MultiProviderFeed = class {
1285
1314
  symbolInfo(cfg) {
1286
1315
  if (cfg.data && cfg.data.length > 0) return void 0;
1287
1316
  const resolved = this.registry.resolve(rawSymbol(cfg), { default: this.primaryProvider });
1288
- return resolved ? this.symInfoCache.get(symKey(resolved)) : void 0;
1317
+ return resolved ? this.symInfoCache.get(symKey(resolved))?.info : void 0;
1289
1318
  }
1290
1319
  prefetchSymbolInfo(resolved) {
1291
- const key = symKey(resolved);
1292
- if (this.symInfoCache.has(key)) return;
1293
- const provider = this.registry.get(resolved.provider);
1294
- if (!provider?.getSymbolInfo) return;
1295
- void provider.getSymbolInfo(resolved.ticker).then((info) => {
1296
- if (info) this.symInfoCache.set(key, info);
1297
- }).catch(() => {
1320
+ void this.fetchSymbolInfo(resolved).catch(() => {
1298
1321
  });
1299
1322
  }
1300
1323
  async loadRange(cfg, range) {
@@ -1344,8 +1367,9 @@ function canonical(cfg, resolved) {
1344
1367
  return { ...cfg, symbol: `${resolved.provider}:${resolved.ticker}` };
1345
1368
  }
1346
1369
  function symKey(resolved) {
1347
- return `${resolved.provider}|${resolved.ticker}`;
1370
+ return `${resolved.provider}|${resolved.ticker.trim().toUpperCase()}`;
1348
1371
  }
1372
+ var SYMBOL_INFO_TTL_MS = 10 * 6e4;
1349
1373
  async function safeBars(provider, ticker, tf, range) {
1350
1374
  try {
1351
1375
  return await provider.getBars(ticker, tf, range);
@@ -11470,7 +11494,7 @@ var SettingsDialog = class {
11470
11494
  if (hr.kind === "heading") body.append(this.sectionTitle(hr.label));
11471
11495
  else if (hr.kind === "toggle") body.append(this.boolRow(hr.label, hr.get(), (v) => hr.set(v)));
11472
11496
  else if (hr.kind === "color") body.append(this.colorRow(hr.label, hr.get(), (v) => hr.set(v)));
11473
- else body.append(this.selectRowLabeled(hr.label, hr.get(), hr.options.map((o) => [o, o]), (v) => hr.set(v)));
11497
+ else body.append(this.selectRowLabeled(hr.label, hr.get(), normalizeSelectOptions(hr.options), (v) => hr.set(v)));
11474
11498
  }
11475
11499
  }
11476
11500
  };
@@ -24397,7 +24421,8 @@ var ClassicIndicator = class {
24397
24421
  lineStyle: plot.lineStyle ?? "solid",
24398
24422
  ...plot.base != null ? { base: plot.base } : {}
24399
24423
  },
24400
- ...plot.overlay ? { overlay: true } : {}
24424
+ ...plot.overlay ? { overlay: true } : {},
24425
+ ...plot.display ? { display: plot.display } : {}
24401
24426
  });
24402
24427
  });
24403
24428
  if (out.markers && out.markers.length > 0) {
@@ -24419,7 +24444,9 @@ var ClassicIndicator = class {
24419
24444
  paneId: "",
24420
24445
  fromSeriesId: from,
24421
24446
  toSeriesId: to,
24422
- color: band.color
24447
+ color: band.color,
24448
+ ...band.colors ? { colors: [...band.colors] } : {},
24449
+ ...band.gradient ? { gradient: [...band.gradient] } : {}
24423
24450
  }
24424
24451
  ];
24425
24452
  });
@@ -24577,6 +24604,16 @@ function stdev(v, len) {
24577
24604
  return Math.sqrt(sq / len);
24578
24605
  });
24579
24606
  }
24607
+ function meanDev(v, len) {
24608
+ return windowed(v, len, (w, from, to) => {
24609
+ let s = 0;
24610
+ for (let k = from; k <= to; k++) s += w[k];
24611
+ const mean = s / len;
24612
+ let dev = 0;
24613
+ for (let k = from; k <= to; k++) dev += Math.abs(w[k] - mean);
24614
+ return dev / len;
24615
+ });
24616
+ }
24580
24617
  function highest(v, len) {
24581
24618
  return windowed(v, len, (w, from, to) => {
24582
24619
  let m = -Infinity;
@@ -24708,7 +24745,7 @@ function stoch(v, hi, lo, len) {
24708
24745
  }
24709
24746
  return out;
24710
24747
  }
24711
- function linreg(v, len) {
24748
+ function linreg(v, len, offset = 0) {
24712
24749
  const sx = (len - 1) * len / 2;
24713
24750
  const sxx = (len - 1) * len * (2 * len - 1) / 6;
24714
24751
  const denom = len * sxx - sx * sx;
@@ -24722,9 +24759,101 @@ function linreg(v, len) {
24722
24759
  }
24723
24760
  const slope = denom === 0 ? 0 : (len * sxy - sx * sy) / denom;
24724
24761
  const intercept = (sy - slope * sx) / len;
24725
- return intercept + slope * (len - 1);
24762
+ return intercept + slope * (len - 1 - offset);
24763
+ });
24764
+ }
24765
+ function hma(v, len) {
24766
+ const n = Math.max(2, Math.trunc(len));
24767
+ const raw = sub(map(wma(v, Math.max(1, Math.trunc(n / 2))), (x) => 2 * x), wma(v, n));
24768
+ return wma(raw, Math.max(1, Math.round(Math.sqrt(n))));
24769
+ }
24770
+ function alma(v, len, offset, sigma) {
24771
+ const m = offset * (len - 1);
24772
+ const s = len / Math.max(sigma, 1e-9);
24773
+ const weights = new Array(len);
24774
+ let norm = 0;
24775
+ for (let i = 0; i < len; i++) {
24776
+ const d = i - m;
24777
+ const w = Math.exp(-(d * d) / (2 * s * s));
24778
+ weights[i] = w;
24779
+ norm += w;
24780
+ }
24781
+ return windowed(v, len, (win, from, to) => {
24782
+ let acc = 0;
24783
+ for (let k = from; k <= to; k++) acc += win[k] * weights[k - from];
24784
+ return norm === 0 ? Number.NaN : acc / norm;
24785
+ });
24786
+ }
24787
+ function dema(v, len) {
24788
+ const e1 = ema(v, len);
24789
+ return sub(map(e1, (x) => 2 * x), ema(e1, len));
24790
+ }
24791
+ function tema(v, len) {
24792
+ const e1 = ema(v, len);
24793
+ const e2 = ema(e1, len);
24794
+ const e3 = ema(e2, len);
24795
+ return zip(zip(map(e1, (x) => 3 * x), map(e2, (x) => 3 * x), (a, b) => a - b), e3, (a, b) => a + b);
24796
+ }
24797
+ function kama(v, len, fastAlpha = 2 / 3, slowAlpha = 2 / 31) {
24798
+ const direction = map(change(v, len), Math.abs);
24799
+ const volatility = sum(map(change(v), Math.abs), len);
24800
+ const out = new Array(v.length).fill(Number.NaN);
24801
+ let prev2 = Number.NaN;
24802
+ for (let i = 0; i < v.length; i++) {
24803
+ const x = v[i];
24804
+ if (!Number.isFinite(x)) continue;
24805
+ const d = direction[i];
24806
+ const w = volatility[i];
24807
+ if (!Number.isFinite(d) || !Number.isFinite(w)) {
24808
+ prev2 = Number.isFinite(prev2) ? prev2 : x;
24809
+ out[i] = prev2;
24810
+ continue;
24811
+ }
24812
+ const er = w === 0 ? 0 : d / w;
24813
+ const sc = (er * (fastAlpha - slowAlpha) + slowAlpha) ** 2;
24814
+ prev2 = Number.isFinite(prev2) ? prev2 + sc * (x - prev2) : x;
24815
+ out[i] = prev2;
24816
+ }
24817
+ return out;
24818
+ }
24819
+ function mcginley(v, len) {
24820
+ const out = new Array(v.length).fill(Number.NaN);
24821
+ let prev2 = Number.NaN;
24822
+ for (let i = 0; i < v.length; i++) {
24823
+ const x = v[i];
24824
+ if (!Number.isFinite(x)) continue;
24825
+ if (!Number.isFinite(prev2)) {
24826
+ prev2 = x;
24827
+ } else {
24828
+ const ratio = prev2 === 0 ? 0 : x / prev2;
24829
+ const denom = len * ratio ** 4;
24830
+ prev2 += denom === 0 ? 0 : (x - prev2) / denom;
24831
+ }
24832
+ out[i] = prev2;
24833
+ }
24834
+ return out;
24835
+ }
24836
+ function hamming(v, len, a0 = 0.54, a1 = 0.46) {
24837
+ if (len <= 1) return v.map((x) => Number.isFinite(x) ? x : Number.NaN);
24838
+ const weights = new Array(len);
24839
+ let norm = 0;
24840
+ for (let i = 0; i < len; i++) {
24841
+ const w = a0 - a1 * Math.cos(2 * Math.PI * i / (len - 1));
24842
+ weights[i] = w;
24843
+ norm += w;
24844
+ }
24845
+ return windowed(v, len, (win, from, to) => {
24846
+ let acc = 0;
24847
+ for (let k = from; k <= to; k++) acc += win[k] * weights[to - k];
24848
+ return norm === 0 ? Number.NaN : acc / norm;
24726
24849
  });
24727
24850
  }
24851
+ function cmo(v, len) {
24852
+ const d = change(v);
24853
+ const up = sum(map(d, (x) => Math.max(x, 0)), len);
24854
+ const dn = sum(map(d, (x) => Math.max(-x, 0)), len);
24855
+ return zip(up, dn, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
24856
+ }
24728
24857
  function swma(v) {
24729
24858
  const out = new Array(v.length).fill(Number.NaN);
24730
24859
  for (let i = 3; i < v.length; i++) {
@@ -24774,38 +24903,253 @@ function cumSum(v) {
24774
24903
  }
24775
24904
 
24776
24905
  // src/core/native-indicators/classics/shared.ts
24777
- function lengthInput(defval, key = "length", title = "Length", max = 5e3) {
24778
- return { key, title, type: "int", defval, min: 1, max, step: 1 };
24906
+ var SETTINGS = "Settings";
24907
+ var STYLE = "Style";
24908
+ function lengthInput(defval, key = "length", title = "Length", max = 5e3, tooltip) {
24909
+ return { key, title, type: "int", defval, min: 1, max, step: 1, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
24910
+ }
24911
+ function intInput(key, title, defval, min = 1, max = 5e3, tooltip) {
24912
+ return { key, title, type: "int", defval, min, max, step: 1, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
24913
+ }
24914
+ function floatInput(key, title, defval, min = 0, max = 1e3, step = 0.1, tooltip) {
24915
+ return { key, title, type: "float", defval, min, max, step, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
24779
24916
  }
24780
- function sourceInput(defval = "Close") {
24781
- return { key: "source", title: "Source", type: "string", defval, options: SOURCES2 };
24917
+ function boolInput(key, title, defval, group = SETTINGS, tooltip) {
24918
+ return { key, title, type: "bool", defval, group, ...tooltip != null ? { tooltip } : {} };
24782
24919
  }
24783
- function colorInput(defval = SERIES_LINE, title = "Color", key = "color") {
24784
- return { key, title, type: "color", defval };
24920
+ function optionInput(key, title, defval, options, tooltip) {
24921
+ return { key, title, type: "string", defval, options, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
24922
+ }
24923
+ function sourceInput(defval = "Close", key = "source", title = "Source", tooltip) {
24924
+ return { key, title, type: "string", defval, options: SOURCES2, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
24925
+ }
24926
+ function colorInput(defval = INFO, title = "Color", key = "color", tooltip) {
24927
+ return { key, title, type: "color", defval, group: STYLE, ...tooltip != null ? { tooltip } : {} };
24928
+ }
24929
+ function widthInput(defval = 2, key = "lineWidth", title = "Line Width") {
24930
+ return { key, title, type: "int", defval, min: 1, max: 5, step: 1, group: STYLE, tooltip: "Width of the plotted line, in pixels." };
24931
+ }
24932
+ function transp(hex, transparency) {
24933
+ return withAlpha2(hex, (100 - transparency) / 100);
24785
24934
  }
24786
24935
  function withAlpha2(hex, alpha = 0.08) {
24787
24936
  const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
24788
24937
  if (!m) return hex;
24789
- const a = Math.round(alpha * 255).toString(16).padStart(2, "0");
24938
+ const a = Math.round(Math.max(0, Math.min(1, alpha)) * 255).toString(16).padStart(2, "0");
24790
24939
  return `#${m[1]}${a}`;
24791
24940
  }
24941
+ function channelStyleInputs(basisTitle = "Basis") {
24942
+ return [
24943
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the lower band and of the basis while price holds above it."),
24944
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the upper band and of the basis while price sits below it."),
24945
+ colorInput(NEUTRAL, basisTitle, "basisColor", "Color of the basis line while price sits exactly on it.")
24946
+ ];
24947
+ }
24948
+ function directionalChannel(bars, basis, upper, lower, ink, key) {
24949
+ const basisColors = basis.map((b, i) => {
24950
+ if (!Number.isFinite(b)) return null;
24951
+ const c = bars[i].close;
24952
+ return c > b ? ink.bull : c < b ? ink.bear : ink.neutral;
24953
+ });
24954
+ const wash = upper.map((u, i) => {
24955
+ const l = lower[i];
24956
+ if (!Number.isFinite(u) || !Number.isFinite(l)) return null;
24957
+ return { topValue: u, bottomValue: l, topColor: transp(ink.bear, 90), bottomColor: transp(ink.bull, 90) };
24958
+ });
24959
+ return {
24960
+ plots: [
24961
+ { key: "basis", title: "Basis", values: [...basis], color: ink.neutral, colors: basisColors },
24962
+ { key: "upper", title: "Upper", values: [...upper], color: transp(ink.bear, 30) },
24963
+ { key: "lower", title: "Lower", values: [...lower], color: transp(ink.bull, 30) }
24964
+ ],
24965
+ bands: [{ key, from: "upper", to: "lower", color: transp(NEUTRAL, 90), gradient: wash }]
24966
+ };
24967
+ }
24968
+ function anchorPlot(key, title, length, level) {
24969
+ return {
24970
+ key,
24971
+ title,
24972
+ values: new Array(length).fill(level),
24973
+ color: transp(NEUTRAL, 100),
24974
+ display: { pane: false, legend: false, dataWindow: false }
24975
+ };
24976
+ }
24977
+ function thresholdZones(length, levels, ink, transparency = 92) {
24978
+ return {
24979
+ plots: [
24980
+ anchorPlot("obAnchor", "Overbought Anchor", length, levels.overbought),
24981
+ anchorPlot("osAnchor", "Oversold Anchor", length, levels.oversold),
24982
+ anchorPlot("topAnchor", "Top Anchor", length, levels.top),
24983
+ anchorPlot("bottomAnchor", "Bottom Anchor", length, levels.bottom)
24984
+ ],
24985
+ bands: [
24986
+ { key: "obZone", from: "obAnchor", to: "topAnchor", color: transp(ink.overbought, transparency) },
24987
+ { key: "osZone", from: "osAnchor", to: "bottomAnchor", color: transp(ink.oversold, transparency) }
24988
+ ]
24989
+ };
24990
+ }
24991
+ function rampColors(values, levels, ink) {
24992
+ return values.map((x) => {
24993
+ if (!Number.isFinite(x)) return null;
24994
+ return x <= levels.mid ? gradient(x, levels.low, levels.mid, ink.low, ink.mid) : gradient(x, levels.mid, levels.high, ink.mid, ink.high);
24995
+ });
24996
+ }
24997
+ function baselineGradient(values, baseline, inkOf, valueTransparency = 50, baseTransparency = 100) {
24998
+ return values.map((x, i) => {
24999
+ if (!Number.isFinite(x)) return null;
25000
+ const ink = inkOf(x, i);
25001
+ const above = x > baseline;
25002
+ return {
25003
+ topValue: Math.max(x, baseline),
25004
+ bottomValue: Math.min(x, baseline),
25005
+ topColor: transp(ink, above ? valueTransparency : baseTransparency),
25006
+ bottomColor: transp(ink, above ? baseTransparency : valueTransparency)
25007
+ };
25008
+ });
25009
+ }
25010
+ function momentumColumnColors(values, ink) {
25011
+ return values.map((x, i) => {
25012
+ if (!Number.isFinite(x)) return null;
25013
+ const prev2 = i > 0 && Number.isFinite(values[i - 1]) ? values[i - 1] : x;
25014
+ const rising = x > prev2;
25015
+ if (x >= 0) return rising ? ink.growAbove : ink.fallAbove;
25016
+ return rising ? ink.growBelow : ink.fallBelow;
25017
+ });
25018
+ }
25019
+ function boundedOscillator(opts) {
25020
+ const mid = (opts.overbought + opts.oversold) / 2;
25021
+ const ramp = rampColors(opts.values, { low: opts.oversold, mid, high: opts.overbought }, opts.ink);
25022
+ const zones = thresholdZones(
25023
+ opts.values.length,
25024
+ { overbought: opts.overbought, oversold: opts.oversold, top: opts.scale.top, bottom: opts.scale.bottom },
25025
+ { overbought: opts.ink.high, oversold: opts.ink.low },
25026
+ opts.zoneTransparency
25027
+ );
25028
+ const plots = [{ key: opts.key, title: opts.title, values: opts.values, color: opts.ink.mid, width: opts.width ?? 1, colors: ramp }, ...zones.plots];
25029
+ const bands = [...zones.bands];
25030
+ if (opts.bandFill != null) bands.push({ key: "band", from: "obAnchor", to: "osAnchor", color: opts.bandFill });
25031
+ if (opts.midpointWash) {
25032
+ plots.push(anchorPlot("midAnchor", "Midpoint Anchor", opts.values.length, mid));
25033
+ 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) });
25034
+ }
25035
+ const levels = [
25036
+ { key: "overbought", price: opts.overbought, color: opts.levelInk, lineStyle: "dashed", title: opts.levelTitles?.overbought ?? "Overbought" },
25037
+ { key: "oversold", price: opts.oversold, color: opts.levelInk, lineStyle: "dashed", title: opts.levelTitles?.oversold ?? "Oversold" }
25038
+ ];
25039
+ if (opts.midline != null) levels.splice(1, 0, { key: "midline", price: opts.midline, color: opts.levelInk, lineStyle: "dotted", title: "Midline" });
25040
+ return { plots, bands, levels };
25041
+ }
25042
+ function centeredOscillator(opts) {
25043
+ const baseline = opts.baseline ?? 0;
25044
+ const inkOf = (x) => x > baseline ? opts.ink.bull : x < baseline ? opts.ink.bear : opts.ink.neutral;
25045
+ const plots = [
25046
+ { 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) },
25047
+ ...opts.extraPlots ?? []
25048
+ ];
25049
+ const bands = [];
25050
+ if (opts.fill !== false) {
25051
+ plots.push(anchorPlot("baseline", "Baseline Anchor", opts.values.length, baseline));
25052
+ bands.push({ key: "wash", from: opts.key, to: "baseline", color: transp(NEUTRAL, 100), gradient: baselineGradient(opts.values, baseline, inkOf, opts.valueTransparency) });
25053
+ }
25054
+ return {
25055
+ plots,
25056
+ bands,
25057
+ levels: [{ key: "zero", price: baseline, color: opts.zeroInk, lineStyle: opts.zeroLineStyle ?? "dashed", title: opts.zeroTitle ?? "Zero Line" }, ...opts.extraLevels ?? []]
25058
+ };
25059
+ }
25060
+ function channels(hex) {
25061
+ const m = /^#([0-9a-f]{6})/i.exec(hex.trim());
25062
+ if (!m) return null;
25063
+ const n = parseInt(m[1], 16);
25064
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
25065
+ }
25066
+ function gradient(value, low, high, from, to) {
25067
+ const a = channels(from);
25068
+ const b = channels(to);
25069
+ if (!a || !b || !Number.isFinite(value)) return from;
25070
+ const span = high - low;
25071
+ const t = span === 0 ? 0 : Math.max(0, Math.min(1, (value - low) / span));
25072
+ const mix = (i) => Math.round(a[i] + (b[i] - a[i]) * t).toString(16).padStart(2, "0");
25073
+ return `#${mix(0)}${mix(1)}${mix(2)}`;
25074
+ }
24792
25075
 
24793
25076
  // src/core/native-indicators/classics/averages.ts
24794
- var MA_KINDS = ["SMA", "EMA", "WMA", "RMA", "VWMA"];
24795
- var offsetInput = { key: "offset", title: "Offset", type: "int", defval: 0, min: -500, max: 500, step: 1 };
25077
+ var MA_TYPES = ["SMA", "EMA", "WMA", "RMA (SMMA)", "HMA", "ALMA", "VWMA", "DEMA", "TEMA", "KAMA", "LSMA", "McGinley", "Hamming"];
25078
+ var ENVELOPE_MA_TYPES = ["SMA", "EMA", "WMA", "RMA (SMMA)", "HMA", "VWMA"];
25079
+ function movingAverageOf(kind, src, bars, len, opts = {}) {
25080
+ switch (kind) {
25081
+ case "EMA":
25082
+ return ema(src, len);
25083
+ case "WMA":
25084
+ return wma(src, len);
25085
+ case "RMA":
25086
+ case "RMA (SMMA)":
25087
+ return rma(src, len);
25088
+ case "HMA":
25089
+ return hma(src, len);
25090
+ case "ALMA":
25091
+ return alma(src, len, opts.almaOffset ?? 0.85, opts.almaSigma ?? 6);
25092
+ case "VWMA":
25093
+ return vwma(src, volumes(bars), len);
25094
+ case "DEMA":
25095
+ return dema(src, len);
25096
+ case "TEMA":
25097
+ return tema(src, len);
25098
+ case "KAMA":
25099
+ return kama(src, len);
25100
+ case "LSMA":
25101
+ return linreg(src, len, opts.lsmaOffset ?? 0);
25102
+ case "McGinley":
25103
+ return mcginley(src, len);
25104
+ case "Hamming":
25105
+ return hamming(src, len);
25106
+ default:
25107
+ return sma(src, len);
25108
+ }
25109
+ }
25110
+ function slopeColors(values, bull, bear, flat) {
25111
+ return values.map((x, i) => {
25112
+ if (!Number.isFinite(x)) return null;
25113
+ const prev2 = i > 0 ? values[i - 1] : Number.NaN;
25114
+ if (!Number.isFinite(prev2)) return flat;
25115
+ return x > prev2 ? bull : x < prev2 ? bear : flat;
25116
+ });
25117
+ }
25118
+ function directionInputs() {
25119
+ return [
25120
+ 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."),
25121
+ colorInput(BULLISH, "Bullish", "bullColor"),
25122
+ colorInput(BEARISH, "Bearish", "bearColor"),
25123
+ colorInput(NEUTRAL, "Neutral", "neutralColor"),
25124
+ widthInput()
25125
+ ];
25126
+ }
25127
+ function slopeColoredPlot(key, title, values, inputs) {
25128
+ const neutral = str3(inputs, "neutralColor", NEUTRAL);
25129
+ const colored = bool(inputs, "trendColor", true);
25130
+ return {
25131
+ key,
25132
+ title,
25133
+ values,
25134
+ color: neutral,
25135
+ width: num3(inputs, "lineWidth", 2),
25136
+ ...colored ? { colors: slopeColors(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), neutral) } : {}
25137
+ };
25138
+ }
25139
+ 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." };
24796
25140
  function fixedAverage(type, title, shortTitle, smooth) {
24797
25141
  return {
24798
25142
  type,
24799
25143
  title,
24800
25144
  shortTitle,
24801
25145
  overlay: true,
24802
- inputs: [lengthInput(20), sourceInput(), offsetInput, colorInput()],
25146
+ 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()],
24803
25147
  compute: (bars, inputs) => {
24804
25148
  const len = num3(inputs, "length", 20);
24805
25149
  let values = smooth(sourceValues(bars, str3(inputs, "source", "Close")), len);
24806
25150
  const offset = Math.trunc(num3(inputs, "offset", 0));
24807
25151
  if (offset !== 0) values = shift(values, offset);
24808
- return { plots: [{ key: shortTitle.toLowerCase(), title: shortTitle, values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
25152
+ return { plots: [{ key: shortTitle.toLowerCase(), title: shortTitle, values, color: str3(inputs, "color", INFO), width: 2 }] };
24809
25153
  }
24810
25154
  };
24811
25155
  }
@@ -24817,36 +25161,37 @@ var movingAverage = {
24817
25161
  shortTitle: "MA",
24818
25162
  overlay: true,
24819
25163
  inputs: [
24820
- { key: "maType", title: "Type", type: "string", defval: "SMA", options: MA_KINDS },
24821
- lengthInput(20),
24822
- sourceInput(),
25164
+ optionInput("maType", "MA 1 Type", "SMA", MA_TYPES, "Moving average calculation used for MA 1."),
25165
+ lengthInput(20, "length", "MA 1 Length", 5e3, "Number of bars used in the MA 1 calculation."),
25166
+ sourceInput("Close", "source", "MA 1 Source", "Price series MA 1 is calculated on."),
25167
+ boolInput("showMa2", "Show MA 2", false, SETTINGS, "Displays the optional second moving average."),
25168
+ optionInput("ma2Type", "MA 2 Type", "SMA", MA_TYPES, "Moving average calculation used for MA 2."),
25169
+ lengthInput(50, "ma2Length", "MA 2 Length", 5e3, "Number of bars used in the MA 2 calculation."),
25170
+ sourceInput("Close", "ma2Source", "MA 2 Source", "Price series MA 2 is calculated on."),
25171
+ 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."),
25172
+ floatInput("almaSigma", "ALMA Sigma", 6, 0.1, 100, 0.5, "ALMA type only. Larger values widen the Gaussian window for a smoother average."),
25173
+ intInput("lsmaOffset", "LSMA Offset", 0, 0, 500, "LSMA type only. Bar offset applied to the linear regression value."),
24823
25174
  offsetInput,
24824
- colorInput()
25175
+ colorInput(INFO, "MA 1", "color", "MA 1 line color."),
25176
+ colorInput(WARNING, "MA 2", "ma2Color", "MA 2 line color.")
24825
25177
  ],
24826
25178
  compute: (bars, inputs) => {
24827
- const len = num3(inputs, "length", 20);
24828
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
25179
+ const opts = {
25180
+ almaOffset: num3(inputs, "almaOffset", 0.85),
25181
+ almaSigma: num3(inputs, "almaSigma", 6),
25182
+ lsmaOffset: Math.trunc(num3(inputs, "lsmaOffset", 0))
25183
+ };
25184
+ const offset = Math.trunc(num3(inputs, "offset", 0));
25185
+ const displace = (v) => offset === 0 ? v : shift(v, offset);
24829
25186
  const kind = str3(inputs, "maType", "SMA");
24830
- let values;
24831
- switch (kind) {
24832
- case "EMA":
24833
- values = ema(src, len);
24834
- break;
24835
- case "WMA":
24836
- values = wma(src, len);
24837
- break;
24838
- case "RMA":
24839
- values = rma(src, len);
24840
- break;
24841
- case "VWMA":
24842
- values = vwma(src, volumes(bars), len);
24843
- break;
24844
- default:
24845
- values = sma(src, len);
25187
+ const ma1 = displace(movingAverageOf(kind, sourceValues(bars, str3(inputs, "source", "Close")), bars, num3(inputs, "length", 20), opts));
25188
+ const plots = [{ key: "ma", title: kind, values: ma1, color: str3(inputs, "color", INFO), width: 2 }];
25189
+ if (bool(inputs, "showMa2", false)) {
25190
+ const kind2 = str3(inputs, "ma2Type", "SMA");
25191
+ const ma2 = displace(movingAverageOf(kind2, sourceValues(bars, str3(inputs, "ma2Source", "Close")), bars, num3(inputs, "ma2Length", 50), opts));
25192
+ plots.push({ key: "ma2", title: `${kind2} 2`, values: ma2, color: str3(inputs, "ma2Color", WARNING), width: 2 });
24846
25193
  }
24847
- const offset = Math.trunc(num3(inputs, "offset", 0));
24848
- if (offset !== 0) values = shift(values, offset);
24849
- return { plots: [{ key: "ma", title: kind, values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
25194
+ return { plots };
24850
25195
  }
24851
25196
  };
24852
25197
  var smoothedMa = {
@@ -24854,17 +25199,13 @@ var smoothedMa = {
24854
25199
  title: "Smoothed Moving Average",
24855
25200
  shortTitle: "RMA",
24856
25201
  overlay: true,
24857
- inputs: [lengthInput(14), sourceInput(), colorInput()],
25202
+ inputs: [
25203
+ sourceInput("Close", "source", "Source", "Price series the average smooths. Wilder computed his 1978 indicators on the close, but the recursion accepts any source."),
25204
+ 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."),
25205
+ ...directionInputs()
25206
+ ],
24858
25207
  compute: (bars, inputs) => ({
24859
- plots: [
24860
- {
24861
- key: "rma",
24862
- title: "RMA",
24863
- values: rma(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
24864
- color: str3(inputs, "color", SERIES_LINE),
24865
- width: 2
24866
- }
24867
- ]
25208
+ plots: [slopeColoredPlot("rma", "RMA", rma(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)), inputs)]
24868
25209
  })
24869
25210
  };
24870
25211
  var zlema = {
@@ -24872,61 +25213,86 @@ var zlema = {
24872
25213
  title: "Zero-Lag Exponential Moving Average",
24873
25214
  shortTitle: "ZLEMA",
24874
25215
  overlay: true,
24875
- inputs: [lengthInput(14), sourceInput(), colorInput()],
25216
+ inputs: [
25217
+ sourceInput("Close", "source", "Source", "Price series the average de-lags and then smooths."),
25218
+ 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."),
25219
+ ...directionInputs()
25220
+ ],
24876
25221
  compute: (bars, inputs) => {
24877
- const len = num3(inputs, "length", 14);
25222
+ const len = num3(inputs, "length", 21);
24878
25223
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
24879
- const lag = Math.floor((len - 1) / 2);
24880
- const lagged = shift(src, lag);
24881
- const delagged = zip(src, lagged, (a, b) => 2 * a - b);
24882
- return { plots: [{ key: "zlema", title: "ZLEMA", values: ema(delagged, len), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
25224
+ const lag = Math.round((len - 1) / 2);
25225
+ const delagged = zip(src, shift(src, lag), (a, b) => 2 * a - b);
25226
+ return { plots: [slopeColoredPlot("zlema", "ZLEMA", ema(delagged, len), inputs)] };
24883
25227
  }
24884
25228
  };
25229
+ var MOMENTUM_SCALER = "Momentum (|CMO|)";
25230
+ var VOLATILITY_SCALER = "Volatility (StDev Ratio)";
25231
+ var REGIME = "Trend Regime";
24885
25232
  var vidya = {
24886
25233
  type: "vidya",
24887
25234
  title: "Variable Index Dynamic Average",
24888
25235
  shortTitle: "VIDYA",
24889
25236
  overlay: true,
24890
- inputs: [lengthInput(14), { key: "cmoLength", title: "Momentum length", type: "int", defval: 9, min: 1, max: 500, step: 1 }, sourceInput(), colorInput()],
25237
+ inputs: [
25238
+ sourceInput("Close", "source", "Source", "Price series the average smooths. VIDYA is classically computed on the close."),
25239
+ 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."),
25240
+ 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."),
25241
+ 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."),
25242
+ 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."),
25243
+ { ...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 },
25244
+ { ...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 },
25245
+ 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."),
25246
+ 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."),
25247
+ colorInput(BULLISH, "Bullish", "bullColor"),
25248
+ colorInput(BEARISH, "Bearish", "bearColor"),
25249
+ colorInput(NEUTRAL, "Flat", "flatColor"),
25250
+ colorInput(NEUTRAL, "Neutral", "neutralColor"),
25251
+ widthInput()
25252
+ ],
24891
25253
  compute: (bars, inputs) => {
24892
- const len = num3(inputs, "length", 14);
24893
- const cmoLen = num3(inputs, "cmoLength", 9);
25254
+ const len = num3(inputs, "length", 12);
24894
25255
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
24895
- const d = change(src);
24896
- const su = new Array(src.length).fill(Number.NaN);
24897
- const sd = new Array(src.length).fill(Number.NaN);
24898
- for (let i = cmoLen; i < src.length; i++) {
24899
- let u = 0;
24900
- let w = 0;
24901
- let ok = true;
24902
- for (let k = i - cmoLen + 1; k <= i; k++) {
24903
- const x = d[k];
24904
- if (!Number.isFinite(x)) {
24905
- ok = false;
24906
- break;
24907
- }
24908
- if (x > 0) u += x;
24909
- else w -= x;
24910
- }
24911
- if (ok) {
24912
- su[i] = u;
24913
- sd[i] = w;
24914
- }
24915
- }
24916
- const alpha = 2 / (len + 1);
25256
+ const scalerLen = num3(inputs, "scalerLength", 9);
25257
+ const momentum = map(cmo(src, scalerLen), (x) => Math.abs(x) / 100);
25258
+ const shortDev = stdev(src, scalerLen);
25259
+ const refDev = stdev(src, num3(inputs, "refLength", 30));
25260
+ const volatility = zip(shortDev, refDev, (s, r) => r > 0 ? s / r : 0);
25261
+ const scale = str3(inputs, "scaler", MOMENTUM_SCALER) === VOLATILITY_SCALER ? volatility : momentum;
25262
+ const alphaBase = 2 / (len + 1);
24917
25263
  const out = new Array(src.length).fill(Number.NaN);
24918
25264
  let prev2 = Number.NaN;
24919
25265
  for (let i = 0; i < src.length; i++) {
24920
25266
  const x = src[i];
24921
- const u = su[i];
24922
- const w = sd[i];
24923
- if (!Number.isFinite(x) || !Number.isFinite(u) || !Number.isFinite(w)) continue;
24924
- const total = u + w;
24925
- const k = total === 0 ? 0 : Math.abs((u - w) / total);
24926
- prev2 = Number.isFinite(prev2) ? alpha * k * x + (1 - alpha * k) * prev2 : x;
25267
+ if (!Number.isFinite(x)) continue;
25268
+ const k = scale[i];
25269
+ if (!Number.isFinite(prev2) || !Number.isFinite(k)) prev2 = x;
25270
+ else {
25271
+ const alpha = Math.min(alphaBase * k, 1);
25272
+ prev2 = alpha * x + (1 - alpha) * prev2;
25273
+ }
24927
25274
  out[i] = prev2;
24928
25275
  }
24929
- return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
25276
+ const neutral = str3(inputs, "neutralColor", NEUTRAL);
25277
+ if (!bool(inputs, "trendColor", true)) {
25278
+ return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: neutral, width: num3(inputs, "lineWidth", 2) }] };
25279
+ }
25280
+ const bull = str3(inputs, "bullColor", BULLISH);
25281
+ const bear = str3(inputs, "bearColor", BEARISH);
25282
+ const flat = str3(inputs, "flatColor", NEUTRAL);
25283
+ const threshold = num3(inputs, "flatThreshold", 0.05);
25284
+ const range = atr(bars, num3(inputs, "atrLength", 14));
25285
+ const fade = bool(inputs, "adaptiveGradient", true);
25286
+ const colors = out.map((x, i) => {
25287
+ if (!Number.isFinite(x)) return null;
25288
+ const r = range[i];
25289
+ const slope = i > 0 && Number.isFinite(out[i - 1]) ? x - out[i - 1] : 0;
25290
+ const normalized = Number.isFinite(r) && r !== 0 ? slope / r : 0;
25291
+ const ink = normalized > threshold ? bull : normalized < -threshold ? bear : null;
25292
+ if (ink == null) return flat;
25293
+ return fade ? gradient(Math.min(scale[i] ?? 0, 1), 0, 1, flat, ink) : ink;
25294
+ });
25295
+ return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: flat, width: num3(inputs, "lineWidth", 2), colors }] };
24930
25296
  }
24931
25297
  };
24932
25298
  var maEnvelope = {
@@ -24935,115 +25301,218 @@ var maEnvelope = {
24935
25301
  shortTitle: "MA Env",
24936
25302
  overlay: true,
24937
25303
  inputs: [
24938
- { key: "maType", title: "Type", type: "string", defval: "SMA", options: ["SMA", "EMA"] },
24939
- lengthInput(20),
24940
- sourceInput(),
24941
- { key: "percent", title: "Percent", type: "float", defval: 2.5, min: 0.01, max: 50, step: 0.1 },
24942
- colorInput()
25304
+ optionInput("maType", "MA Type", "SMA", ENVELOPE_MA_TYPES, "Moving average type used as the envelope basis."),
25305
+ { ...lengthInput(20, "length", "Length", 5e3, "Number of bars in the basis moving average."), min: 2 },
25306
+ sourceInput("Close", "source", "Source", "Price series the basis moving average is calculated on."),
25307
+ 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)."),
25308
+ ...channelStyleInputs()
24943
25309
  ],
24944
25310
  compute: (bars, inputs) => {
24945
- const len = num3(inputs, "length", 20);
24946
25311
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
24947
- const basis = str3(inputs, "maType", "SMA") === "EMA" ? ema(src, len) : sma(src, len);
25312
+ const basis = movingAverageOf(str3(inputs, "maType", "SMA"), src, bars, num3(inputs, "length", 20));
24948
25313
  const k = num3(inputs, "percent", 2.5) / 100;
24949
- const ink = str3(inputs, "color", SERIES_LINE);
24950
- return {
24951
- plots: [
24952
- { key: "basis", title: "Basis", values: basis, color: ink, width: 2 },
24953
- { key: "upper", title: "Upper", values: map(basis, (x) => x * (1 + k)), color: ink },
24954
- { key: "lower", title: "Lower", values: map(basis, (x) => x * (1 - k)), color: ink }
24955
- ],
24956
- bands: [{ key: "envelope", from: "upper", to: "lower", color: withAlpha2(ink) }]
24957
- };
25314
+ const ink = { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "basisColor", NEUTRAL) };
25315
+ return directionalChannel(bars, basis, map(basis, (x) => x * (1 + k)), map(basis, (x) => x * (1 - k)), ink, "envelope");
24958
25316
  }
24959
25317
  };
25318
+ var LINREG_CURVE = "Curve";
25319
+ var LINREG_SLOPE = "Slope";
24960
25320
  var linearRegression = {
24961
25321
  type: "linear-regression",
24962
25322
  title: "Linear Regression Curve",
24963
25323
  shortTitle: "LinReg",
24964
- overlay: true,
24965
- inputs: [lengthInput(14), sourceInput(), colorInput()],
24966
- compute: (bars, inputs) => ({
24967
- plots: [
24968
- {
24969
- key: "linreg",
24970
- title: "LinReg",
24971
- values: linreg(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
24972
- color: str3(inputs, "color", SERIES_LINE),
24973
- width: 2
24974
- }
24975
- ]
24976
- })
25324
+ // Slope lives in its own pane; Curve force-overlays itself onto the price candles.
25325
+ overlay: false,
25326
+ inputs: [
25327
+ 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."),
25328
+ { ...lengthInput(100, "length", "Length", 5e3, "Number of bars used to fit the linear regression."), min: 2 },
25329
+ sourceInput("Close", "source", "Source", "Price series the regression is fitted to."),
25330
+ colorInput(BULLISH, "Rising", "bullColor", "Color of the regression curve and slope line while the fit slopes upward."),
25331
+ colorInput(BEARISH, "Falling", "bearColor", "Color of the regression curve and slope line while the fit slopes downward."),
25332
+ colorInput(NEUTRAL, "Regression Line", "lineColor", "Color of the regression curve and slope line while the fit is flat.")
25333
+ ],
25334
+ compute: (bars, inputs) => {
25335
+ const len = num3(inputs, "length", 100);
25336
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
25337
+ const curve = linreg(src, len, 0);
25338
+ const slope = zip(curve, linreg(src, len, 1), (a, b) => a - b);
25339
+ const bull = str3(inputs, "bullColor", BULLISH);
25340
+ const bear = str3(inputs, "bearColor", BEARISH);
25341
+ const flat = str3(inputs, "lineColor", NEUTRAL);
25342
+ const colors = slope.map((s) => Number.isFinite(s) ? s > 0 ? bull : s < 0 ? bear : flat : null);
25343
+ if (str3(inputs, "mode", LINREG_CURVE) === LINREG_SLOPE) {
25344
+ return {
25345
+ plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
25346
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
25347
+ };
25348
+ }
25349
+ return { plots: [{ key: "curve", title: "Curve", values: curve, color: flat, width: 2, colors, overlay: true }] };
25350
+ }
24977
25351
  };
24978
25352
  var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope, linearRegression];
24979
25353
 
24980
25354
  // src/core/native-indicators/classics/bands.ts
25355
+ var JAW_COLOR = "#ab47bc";
25356
+ var TEETH_COLOR = WARNING;
25357
+ var LIPS_COLOR = "#ffd700";
24981
25358
  function bollinger(bars, inputs) {
24982
25359
  const len = num3(inputs, "length", 20);
24983
25360
  const mult = num3(inputs, "mult", 2);
24984
25361
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
24985
25362
  const basis = sma(src, len);
24986
25363
  const dev = map(stdev(src, len), (x) => x * mult);
24987
- return { basis, upper: zip(basis, dev, (b, d) => b + d), lower: zip(basis, dev, (b, d) => b - d) };
25364
+ return { src, basis, upper: zip(basis, dev, (b, d) => b + d), lower: zip(basis, dev, (b, d) => b - d) };
25365
+ }
25366
+ function bollingerInputs(multMin, multStep) {
25367
+ return [
25368
+ { ...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 },
25369
+ sourceInput("Close", "source", "Source", "Price series the bands are computed from."),
25370
+ floatInput("mult", "Multiplier", 2, multMin, 50, multStep, "Standard deviation multiplier for the upper and lower bands.")
25371
+ ];
24988
25372
  }
24989
- var BB_INPUTS = [lengthInput(20), sourceInput(), { key: "mult", title: "StdDev", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 }];
24990
25373
  var bollingerBands = {
24991
25374
  type: "bollinger-bands",
24992
25375
  title: "Bollinger Bands",
24993
25376
  shortTitle: "BB",
24994
25377
  overlay: true,
24995
- inputs: [...BB_INPUTS, colorInput()],
25378
+ inputs: [
25379
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used to calculate the basis SMA and the standard deviation."),
25380
+ sourceInput("Close", "source", "Source", "Price series the bands are calculated on."),
25381
+ floatInput("mult", "Multiplier", 2, 0, 50, 0.1, "Number of standard deviations added above and below the basis to form the bands."),
25382
+ colorInput(WARNING, "Basis", "basisColor", "Color of the basis (middle band) moving average."),
25383
+ colorInput(INFO, "Bands", "color", "Color of the upper and lower bands."),
25384
+ colorInput(transp(INFO, 92), "Fill", "fillColor", "Fill color of the area between the upper and lower bands.")
25385
+ ],
24996
25386
  compute: (bars, inputs) => {
24997
25387
  const { basis, upper, lower } = bollinger(bars, inputs);
24998
- const ink = str3(inputs, "color", SERIES_LINE);
25388
+ const ink = str3(inputs, "color", INFO);
24999
25389
  return {
25000
25390
  plots: [
25001
- { key: "basis", title: "Basis", values: basis, color: WARNING },
25391
+ { key: "basis", title: "Basis", values: basis, color: str3(inputs, "basisColor", WARNING) },
25002
25392
  { key: "upper", title: "Upper", values: upper, color: ink },
25003
25393
  { key: "lower", title: "Lower", values: lower, color: ink }
25004
25394
  ],
25005
- bands: [{ key: "bb", from: "upper", to: "lower", color: withAlpha2(ink) }]
25395
+ bands: [{ key: "bb", from: "upper", to: "lower", color: str3(inputs, "fillColor", transp(INFO, 92)) }]
25006
25396
  };
25007
25397
  }
25008
25398
  };
25399
+ var PERCENT_B_MID = 0.5;
25400
+ var THRESHOLDS = "Thresholds";
25009
25401
  var percentB = {
25010
25402
  type: "percent-b",
25011
25403
  title: "Bollinger Bands %B",
25012
25404
  shortTitle: "%B",
25013
25405
  overlay: false,
25014
- inputs: [...BB_INPUTS, colorInput()],
25406
+ inputs: [
25407
+ ...bollingerInputs(1e-3, 0.25),
25408
+ { ...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 },
25409
+ { ...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 },
25410
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of %B above the 0.5 midline, where price trades in the upper half of the bands."),
25411
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of %B below the 0.5 midline, where price trades in the lower half of the bands."),
25412
+ 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."),
25413
+ colorInput(NEUTRAL, "Levels Color", "levelsColor", "Color of the dashed overbought/oversold levels and of the dotted midline.")
25414
+ ],
25015
25415
  compute: (bars, inputs) => {
25016
- const { upper, lower } = bollinger(bars, inputs);
25017
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
25416
+ const { src, upper, lower } = bollinger(bars, inputs);
25018
25417
  const values = src.map((x, i) => {
25019
25418
  const u = upper[i];
25020
25419
  const l = lower[i];
25021
25420
  return Number.isFinite(u) && Number.isFinite(l) && u !== l ? (x - l) / (u - l) : Number.NaN;
25022
25421
  });
25422
+ const bull = str3(inputs, "bullColor", BULLISH);
25423
+ const bear = str3(inputs, "bearColor", BEARISH);
25424
+ const levels = str3(inputs, "levelsColor", NEUTRAL);
25425
+ const colors = values.map((x) => Number.isFinite(x) ? x > PERCENT_B_MID ? bull : bear : null);
25426
+ const plots = [
25427
+ { key: "pb", title: "%B", values, color: bear, colors },
25428
+ { 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 } }
25429
+ ];
25430
+ const bands = bool(inputs, "gradientFill", true) ? [
25431
+ {
25432
+ key: "pbFill",
25433
+ from: "pb",
25434
+ to: "mid",
25435
+ color: transp(NEUTRAL, 100),
25436
+ // Opaque at the oscillator, invisible where it meets the midline.
25437
+ gradient: values.map((x) => {
25438
+ if (!Number.isFinite(x)) return null;
25439
+ const up = x > PERCENT_B_MID;
25440
+ const ink = up ? bull : bear;
25441
+ return {
25442
+ topValue: Math.max(x, PERCENT_B_MID),
25443
+ bottomValue: Math.min(x, PERCENT_B_MID),
25444
+ topColor: up ? transp(ink, 50) : transp(ink, 100),
25445
+ bottomColor: up ? transp(ink, 100) : transp(ink, 50)
25446
+ };
25447
+ })
25448
+ }
25449
+ ] : [];
25023
25450
  return {
25024
- plots: [{ key: "pb", title: "%B", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25451
+ plots,
25452
+ bands,
25025
25453
  levels: [
25026
- { key: "upper", price: 1, color: NEUTRAL },
25027
- { key: "middle", price: 0.5, color: NEUTRAL, lineStyle: "dotted" },
25028
- { key: "lower", price: 0, color: NEUTRAL }
25454
+ { key: "overbought", price: num3(inputs, "overbought", 1), color: levels, lineStyle: "dashed", title: "Overbought" },
25455
+ { key: "oversold", price: num3(inputs, "oversold", 0), color: levels, lineStyle: "dashed", title: "Oversold" },
25456
+ { key: "midline", price: PERCENT_B_MID, color: transp(levels, 50), lineStyle: "dotted", title: "Midline" }
25029
25457
  ]
25030
25458
  };
25031
25459
  }
25032
25460
  };
25461
+ var SQUEEZE_BULGE = "Squeeze & Bulge";
25033
25462
  var bandwidth = {
25034
25463
  type: "bandwidth",
25035
25464
  title: "Bollinger Bands Width",
25036
25465
  shortTitle: "BandWidth",
25037
25466
  overlay: false,
25038
- inputs: [...BB_INPUTS, colorInput()],
25467
+ inputs: [
25468
+ ...bollingerInputs(1e-3, 0.25),
25469
+ { ...intInput("lookback", "Reference Lookback", 125, 2, 5e3, "Window used to locate the lowest (Squeeze) and highest (Bulge) BandWidth references."), group: SQUEEZE_BULGE },
25470
+ 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."),
25471
+ 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."),
25472
+ 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."),
25473
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between zero and BandWidth with a vertical gradient, fully transparent at zero."),
25474
+ colorInput(WARNING, "Squeeze Level", "squeezeColor", "Color of the lowest-BandWidth reference line."),
25475
+ colorInput(BEARISH, "Bulge Level", "bulgeColor", "Color of the highest-BandWidth reference line.")
25476
+ ],
25039
25477
  compute: (bars, inputs) => {
25040
25478
  const { basis, upper, lower } = bollinger(bars, inputs);
25041
25479
  const values = basis.map((b, i) => {
25042
25480
  const u = upper[i];
25043
25481
  const l = lower[i];
25044
- return Number.isFinite(b) && b !== 0 && Number.isFinite(u) && Number.isFinite(l) ? (u - l) / b * 100 : Number.NaN;
25482
+ return Number.isFinite(b) && b !== 0 && Number.isFinite(u) && Number.isFinite(l) ? (u - l) / b : Number.NaN;
25045
25483
  });
25046
- return { plots: [{ key: "bbw", title: "BBW", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
25484
+ const lookback = num3(inputs, "lookback", 125);
25485
+ const squeezeLevel = lowest(values, lookback);
25486
+ const bulgeLevel = highest(values, lookback);
25487
+ const base = str3(inputs, "color", NEUTRAL);
25488
+ const squeezeInk = str3(inputs, "squeezeColor", WARNING);
25489
+ const bulgeInk = str3(inputs, "bulgeColor", BEARISH);
25490
+ const colors = values.map((x, i) => {
25491
+ if (!Number.isFinite(x)) return null;
25492
+ if (x <= squeezeLevel[i]) return squeezeInk;
25493
+ if (x >= bulgeLevel[i]) return bulgeInk;
25494
+ return base;
25495
+ });
25496
+ const plots = [
25497
+ { key: "bbw", title: "BandWidth", values, color: base, colors },
25498
+ { key: "zero", title: "Zero Line", values: values.map((x) => Number.isFinite(x) ? 0 : Number.NaN), color: transp(NEUTRAL, 90) }
25499
+ ];
25500
+ if (bool(inputs, "showSqueeze", true)) plots.push({ key: "squeeze", title: "Squeeze Level", values: squeezeLevel, color: squeezeInk });
25501
+ if (bool(inputs, "showBulge", true)) plots.push({ key: "bulge", title: "Bulge Level", values: bulgeLevel, color: bulgeInk });
25502
+ const bands = bool(inputs, "gradientFill", true) ? [
25503
+ {
25504
+ key: "bbwFill",
25505
+ from: "bbw",
25506
+ to: "zero",
25507
+ color: transp(NEUTRAL, 100),
25508
+ gradient: values.map((x, i) => {
25509
+ if (!Number.isFinite(x)) return null;
25510
+ const ink = colors[i] ?? base;
25511
+ return { topValue: x, bottomValue: 0, topColor: transp(ink, 50), bottomColor: transp(ink, 100) };
25512
+ })
25513
+ }
25514
+ ] : [];
25515
+ return { plots, bands };
25047
25516
  }
25048
25517
  };
25049
25518
  var keltner = {
@@ -25052,26 +25521,17 @@ var keltner = {
25052
25521
  shortTitle: "KC",
25053
25522
  overlay: true,
25054
25523
  inputs: [
25055
- lengthInput(20),
25056
- { key: "mult", title: "Multiplier", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 },
25057
- { key: "atrLength", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
25058
- sourceInput(),
25059
- colorInput()
25524
+ lengthInput(20, "length", "EMA Length", 5e3, "Number of bars used to compute the exponential moving average basis line."),
25525
+ intInput("atrLength", "ATR Length", 10, 1, 5e3, "Number of bars used to compute the Average True Range (Wilder smoothing)."),
25526
+ floatInput("mult", "Multiplier", 2, 0, 50, 0.1, "ATR multiple used to offset the upper and lower bands from the basis line."),
25527
+ sourceInput("Close", "source", "Source", "Price series used to compute the basis line."),
25528
+ ...channelStyleInputs("Basis Color")
25060
25529
  ],
25061
25530
  compute: (bars, inputs) => {
25062
- const len = num3(inputs, "length", 20);
25063
- const mult = num3(inputs, "mult", 2);
25064
- const basis = ema(sourceValues(bars, str3(inputs, "source", "Close")), len);
25065
- const range = map(atr(bars, num3(inputs, "atrLength", 10)), (x) => x * mult);
25066
- const ink = str3(inputs, "color", SERIES_LINE);
25067
- return {
25068
- plots: [
25069
- { key: "basis", title: "Basis", values: basis, color: WARNING },
25070
- { key: "upper", title: "Upper", values: zip(basis, range, (b, r) => b + r), color: ink },
25071
- { key: "lower", title: "Lower", values: zip(basis, range, (b, r) => b - r), color: ink }
25072
- ],
25073
- bands: [{ key: "kc", from: "upper", to: "lower", color: withAlpha2(ink) }]
25074
- };
25531
+ const basis = ema(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20));
25532
+ const range = map(atr(bars, num3(inputs, "atrLength", 10)), (x) => x * num3(inputs, "mult", 2));
25533
+ const ink = { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "basisColor", NEUTRAL) };
25534
+ return directionalChannel(bars, basis, zip(basis, range, (b, r) => b + r), zip(basis, range, (b, r) => b - r), ink, "kc");
25075
25535
  }
25076
25536
  };
25077
25537
  var donchian = {
@@ -25079,19 +25539,24 @@ var donchian = {
25079
25539
  title: "Donchian Channels",
25080
25540
  shortTitle: "DC",
25081
25541
  overlay: true,
25082
- inputs: [lengthInput(20), colorInput()],
25542
+ inputs: [
25543
+ lengthInput(20, "length", "Length", 5e3, "Lookback length. The upper and lower channels are the highest high and lowest low over this many bars."),
25544
+ colorInput(BEARISH, "Upper", "upperColor", "Color of the upper channel line."),
25545
+ colorInput(NEUTRAL, "Middle", "middleColor", "Color of the channel midline."),
25546
+ colorInput(BULLISH, "Lower", "lowerColor", "Color of the lower channel line."),
25547
+ colorInput(transp(NEUTRAL, 92), "Fill", "fillColor", "Color of the channel interior fill.")
25548
+ ],
25083
25549
  compute: (bars, inputs) => {
25084
25550
  const len = num3(inputs, "length", 20);
25085
25551
  const upper = highest(highs(bars), len);
25086
25552
  const lower = lowest(lows(bars), len);
25087
- const ink = str3(inputs, "color", SERIES_LINE);
25088
25553
  return {
25089
25554
  plots: [
25090
- { key: "basis", title: "Basis", values: zip(upper, lower, (u, l) => (u + l) / 2), color: WARNING },
25091
- { key: "upper", title: "Upper", values: upper, color: ink },
25092
- { key: "lower", title: "Lower", values: lower, color: ink }
25555
+ { key: "upper", title: "Upper", values: upper, color: str3(inputs, "upperColor", BEARISH) },
25556
+ { key: "middle", title: "Middle", kind: "circles", values: zip(upper, lower, (u, l) => (u + l) / 2), color: str3(inputs, "middleColor", NEUTRAL) },
25557
+ { key: "lower", title: "Lower", values: lower, color: str3(inputs, "lowerColor", BULLISH) }
25093
25558
  ],
25094
- bands: [{ key: "dc", from: "upper", to: "lower", color: withAlpha2(ink) }]
25559
+ bands: [{ key: "dc", from: "upper", to: "lower", color: str3(inputs, "fillColor", transp(NEUTRAL, 92)) }]
25095
25560
  };
25096
25561
  }
25097
25562
  };
@@ -25100,16 +25565,37 @@ var chandelier = {
25100
25565
  title: "Chandelier Exit",
25101
25566
  shortTitle: "CE",
25102
25567
  overlay: true,
25103
- inputs: [lengthInput(22), { key: "mult", title: "ATR multiplier", type: "float", defval: 3, min: 0.1, max: 50, step: 0.1 }],
25568
+ inputs: [
25569
+ lengthInput(22, "length", "Length", 5e3, "Lookback used for both the highest high / lowest low extremes and the ATR."),
25570
+ 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)."),
25571
+ 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."),
25572
+ colorInput(BULLISH, "Long Stop", "longColor", "Color of the long trailing stop line."),
25573
+ colorInput(BEARISH, "Short Stop", "shortColor", "Color of the short trailing stop line.")
25574
+ ],
25104
25575
  compute: (bars, inputs) => {
25105
25576
  const len = num3(inputs, "length", 22);
25106
25577
  const range = map(atr(bars, len), (x) => x * num3(inputs, "mult", 3));
25107
- const long = zip(highest(highs(bars), len), range, (h, r) => h - r);
25108
- const short = zip(lowest(lows(bars), len), range, (l, r) => l + r);
25578
+ const basicLong = zip(highest(highs(bars), len), range, (h, r) => h - r);
25579
+ const basicShort = zip(lowest(lows(bars), len), range, (l, r) => l + r);
25580
+ let long = basicLong;
25581
+ let short = basicShort;
25582
+ if (bool(inputs, "ratchet", false)) {
25583
+ long = new Array(bars.length).fill(Number.NaN);
25584
+ short = new Array(bars.length).fill(Number.NaN);
25585
+ for (let i = 0; i < bars.length; i++) {
25586
+ const bl = basicLong[i];
25587
+ const bs = basicShort[i];
25588
+ const prevClose = i > 0 ? bars[i - 1].close : bars[i].close;
25589
+ const prevLong = i > 0 && Number.isFinite(long[i - 1]) ? long[i - 1] : bl;
25590
+ const prevShort = i > 0 && Number.isFinite(short[i - 1]) ? short[i - 1] : bs;
25591
+ long[i] = Number.isFinite(bl) ? prevClose > prevLong ? Math.max(bl, prevLong) : bl : Number.NaN;
25592
+ short[i] = Number.isFinite(bs) ? prevClose < prevShort ? Math.min(bs, prevShort) : bs : Number.NaN;
25593
+ }
25594
+ }
25109
25595
  return {
25110
25596
  plots: [
25111
- { key: "long", title: "Long stop", values: long, color: BULLISH },
25112
- { key: "short", title: "Short stop", values: short, color: BEARISH }
25597
+ { key: "long", title: "Long Stop", values: long, color: str3(inputs, "longColor", BULLISH) },
25598
+ { key: "short", title: "Short Stop", values: short, color: str3(inputs, "shortColor", BEARISH) }
25113
25599
  ]
25114
25600
  };
25115
25601
  }
@@ -25120,21 +25606,22 @@ var chandeKroll = {
25120
25606
  shortTitle: "CKS",
25121
25607
  overlay: true,
25122
25608
  inputs: [
25123
- { key: "p", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
25124
- { key: "x", title: "ATR multiplier", type: "float", defval: 1, min: 0.1, max: 50, step: 0.1 },
25125
- { key: "q", title: "Stop length", type: "int", defval: 9, min: 1, max: 500, step: 1 }
25609
+ intInput("p", "ATR Length (P)", 10, 1, 5e3, "Lookback used for both the ATR and the highest high / lowest low price extremes."),
25610
+ floatInput("x", "ATR Multiplier (X)", 1, 0, 50, 0.1, "Multiplier applied to the ATR to offset the preliminary stops from the price extremes."),
25611
+ intInput("q", "Stop Length (Q)", 9, 1, 5e3, "Lookback over which the preliminary stops are extended into the final stop levels."),
25612
+ colorInput(BEARISH, "Stop Short", "shortColor", "Color of the protective stop line for short positions."),
25613
+ colorInput(BULLISH, "Stop Long", "longColor", "Color of the protective stop line for long positions.")
25126
25614
  ],
25127
25615
  compute: (bars, inputs) => {
25128
25616
  const p = num3(inputs, "p", 10);
25129
- const x = num3(inputs, "x", 1);
25130
25617
  const q = num3(inputs, "q", 9);
25131
- const range = map(atr(bars, p), (v) => v * x);
25618
+ const range = map(atr(bars, p), (v) => v * num3(inputs, "x", 1));
25132
25619
  const firstHigh = zip(highest(highs(bars), p), range, (h, r) => h - r);
25133
25620
  const firstLow = zip(lowest(lows(bars), p), range, (l, r) => l + r);
25134
25621
  return {
25135
25622
  plots: [
25136
- { key: "stopShort", title: "Stop short", values: highest(firstHigh, q), color: BEARISH },
25137
- { key: "stopLong", title: "Stop long", values: lowest(firstLow, q), color: BULLISH }
25623
+ { key: "stopShort", title: "Stop Short", values: highest(firstHigh, q), color: str3(inputs, "shortColor", BEARISH) },
25624
+ { key: "stopLong", title: "Stop Long", values: lowest(firstLow, q), color: str3(inputs, "longColor", BULLISH) }
25138
25625
  ]
25139
25626
  };
25140
25627
  }
@@ -25145,36 +25632,42 @@ var supertrend = {
25145
25632
  shortTitle: "SuperTrend",
25146
25633
  overlay: true,
25147
25634
  inputs: [
25148
- { key: "atrLength", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
25149
- { key: "mult", title: "Factor", type: "float", defval: 3, min: 0.1, max: 50, step: 0.1 }
25635
+ intInput("atrLength", "ATR Length", 10, 1, 5e3, "Number of bars used to compute the Wilder ATR."),
25636
+ floatInput("mult", "Factor", 3, 0, 50, 0.1, "ATR multiplier setting how far the trailing stop sits from the bar midpoint."),
25637
+ colorInput(BULLISH, "Uptrend Color", "upColor", "Line color while the trend is up."),
25638
+ colorInput(BEARISH, "Downtrend Color", "downColor", "Line color while the trend is down.")
25150
25639
  ],
25151
25640
  compute: (bars, inputs) => {
25152
- const len = num3(inputs, "atrLength", 10);
25153
25641
  const mult = num3(inputs, "mult", 3);
25154
- const range = atr(bars, len);
25642
+ const range = atr(bars, num3(inputs, "atrLength", 10));
25155
25643
  const n = bars.length;
25156
- const st = new Array(n).fill(Number.NaN);
25157
- const dir = new Array(n).fill(1);
25158
- let up = Number.NaN;
25159
- let dn = Number.NaN;
25160
- let trend = 1;
25644
+ const up = new Array(n).fill(Number.NaN);
25645
+ const down = new Array(n).fill(Number.NaN);
25646
+ let finalUpper = Number.NaN;
25647
+ let finalLower = Number.NaN;
25648
+ let trend = -1;
25161
25649
  for (let i = 0; i < n; i++) {
25162
25650
  const r = range[i];
25163
25651
  if (!Number.isFinite(r)) continue;
25164
25652
  const b = bars[i];
25165
25653
  const mid = (b.high + b.low) / 2;
25166
- const basicUp = mid - mult * r;
25167
- const basicDn = mid + mult * r;
25654
+ const basicUpper = mid + mult * r;
25655
+ const basicLower = mid - mult * r;
25656
+ const prevUpper = Number.isFinite(finalUpper) ? finalUpper : basicUpper;
25657
+ const prevLower = Number.isFinite(finalLower) ? finalLower : basicLower;
25168
25658
  const prevClose = i > 0 ? bars[i - 1].close : b.close;
25169
- up = Number.isFinite(up) && prevClose > up ? Math.max(basicUp, up) : basicUp;
25170
- dn = Number.isFinite(dn) && prevClose < dn ? Math.min(basicDn, dn) : basicDn;
25171
- if (trend === 1 && b.close < up) trend = -1;
25172
- else if (trend === -1 && b.close > dn) trend = 1;
25173
- dir[i] = trend;
25174
- st[i] = trend === 1 ? up : dn;
25659
+ finalUpper = prevClose <= prevUpper ? Math.min(basicUpper, prevUpper) : basicUpper;
25660
+ finalLower = prevClose >= prevLower ? Math.max(basicLower, prevLower) : basicLower;
25661
+ trend = b.close > finalUpper ? 1 : b.close < finalLower ? -1 : trend;
25662
+ if (trend === 1) up[i] = finalLower;
25663
+ else down[i] = finalUpper;
25175
25664
  }
25176
- const colors = dir.map((d, i) => Number.isFinite(st[i]) ? d === 1 ? BULLISH : BEARISH : null);
25177
- return { plots: [{ key: "st", title: "SuperTrend", values: st, color: BULLISH, width: 2, colors }] };
25665
+ return {
25666
+ plots: [
25667
+ { key: "up", title: "Uptrend", values: up, color: str3(inputs, "upColor", BULLISH) },
25668
+ { key: "down", title: "Downtrend", values: down, color: str3(inputs, "downColor", BEARISH) }
25669
+ ]
25670
+ };
25178
25671
  }
25179
25672
  };
25180
25673
  var alligator = {
@@ -25183,12 +25676,15 @@ var alligator = {
25183
25676
  shortTitle: "Alligator",
25184
25677
  overlay: true,
25185
25678
  inputs: [
25186
- { key: "jawLength", title: "Jaw length", type: "int", defval: 13, min: 1, max: 500, step: 1 },
25187
- { key: "teethLength", title: "Teeth length", type: "int", defval: 8, min: 1, max: 500, step: 1 },
25188
- { key: "lipsLength", title: "Lips length", type: "int", defval: 5, min: 1, max: 500, step: 1 },
25189
- { key: "jawOffset", title: "Jaw offset", type: "int", defval: 8, min: 0, max: 100, step: 1 },
25190
- { key: "teethOffset", title: "Teeth offset", type: "int", defval: 5, min: 0, max: 100, step: 1 },
25191
- { key: "lipsOffset", title: "Lips offset", type: "int", defval: 3, min: 0, max: 100, step: 1 }
25679
+ intInput("jawLength", "Jaw Length", 13, 1, 5e3, "Smoothing length of the Jaw SMMA of median price (hl2), 13 in the classic setup."),
25680
+ intInput("teethLength", "Teeth Length", 8, 1, 5e3, "Smoothing length of the Teeth SMMA of median price (hl2), 8 in the classic setup."),
25681
+ intInput("lipsLength", "Lips Length", 5, 1, 5e3, "Smoothing length of the Lips SMMA of median price (hl2), 5 in the classic setup."),
25682
+ intInput("jawOffset", "Jaw Offset", 8, 0, 100, "Number of bars the Jaw plot is shifted into the future, 8 in the classic setup."),
25683
+ intInput("teethOffset", "Teeth Offset", 5, 0, 100, "Number of bars the Teeth plot is shifted into the future, 5 in the classic setup."),
25684
+ intInput("lipsOffset", "Lips Offset", 3, 0, 100, "Number of bars the Lips plot is shifted into the future, 3 in the classic setup."),
25685
+ colorInput(JAW_COLOR, "Jaw", "jawColor", "Color of the Jaw line."),
25686
+ colorInput(TEETH_COLOR, "Teeth", "teethColor", "Color of the Teeth line."),
25687
+ colorInput(LIPS_COLOR, "Lips", "lipsColor", "Color of the Lips line.")
25192
25688
  ],
25193
25689
  compute: (bars, inputs) => {
25194
25690
  const hl2 = sourceValues(bars, "HL2");
@@ -25197,99 +25693,134 @@ var alligator = {
25197
25693
  const lips = shift(rma(hl2, num3(inputs, "lipsLength", 5)), num3(inputs, "lipsOffset", 3));
25198
25694
  return {
25199
25695
  plots: [
25200
- { key: "jaw", title: "Jaw", values: jaw, color: SERIES_LINE },
25201
- { key: "teeth", title: "Teeth", values: teeth, color: BEARISH },
25202
- { key: "lips", title: "Lips", values: lips, color: BULLISH }
25696
+ { key: "jaw", title: "Jaw", values: jaw, color: str3(inputs, "jawColor", JAW_COLOR) },
25697
+ { key: "teeth", title: "Teeth", values: teeth, color: str3(inputs, "teethColor", TEETH_COLOR) },
25698
+ { key: "lips", title: "Lips", values: lips, color: str3(inputs, "lipsColor", LIPS_COLOR) }
25203
25699
  ]
25204
25700
  };
25205
25701
  }
25206
25702
  };
25703
+ var GATOR_LINES = "Alligator Lines";
25704
+ var SMMA_TYPE = "SMMA (Wilder)";
25207
25705
  var gator = {
25208
25706
  type: "gator-oscillator",
25209
25707
  title: "Gator Oscillator",
25210
25708
  shortTitle: "Gator Oscillator",
25211
25709
  overlay: false,
25212
- inputs: [],
25213
- compute: (bars) => {
25214
- const hl2 = sourceValues(bars, "HL2");
25215
- const jaw = shift(rma(hl2, 13), 8);
25216
- const teeth = shift(rma(hl2, 8), 5);
25217
- const lips = shift(rma(hl2, 5), 3);
25710
+ inputs: [
25711
+ { ...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 },
25712
+ { ...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 },
25713
+ { ...intInput("jawLength", "Jaw Length", 13, 1, 500, "Averaging length of the jaw line."), group: GATOR_LINES },
25714
+ { ...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 },
25715
+ { ...intInput("teethLength", "Teeth Length", 8, 1, 500, "Averaging length of the teeth line."), group: GATOR_LINES },
25716
+ { ...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 },
25717
+ { ...intInput("lipsLength", "Lips Length", 5, 1, 500, "Averaging length of the lips line."), group: GATOR_LINES },
25718
+ { ...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 },
25719
+ colorInput(BULLISH, "Expanding", "growColor", "Column color while a spread grew against its own previous bar \u2014 the averages are spreading apart."),
25720
+ colorInput(BEARISH, "Contracting", "shrinkColor", "Column color while a spread shrank \u2014 the averages are converging. Color encodes expansion, not trade direction."),
25721
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero line separating the two histograms.")
25722
+ ],
25723
+ compute: (bars, inputs) => {
25724
+ const src = sourceValues(bars, str3(inputs, "source", "HL2"));
25725
+ const kind = str3(inputs, "maType", SMMA_TYPE);
25726
+ const smooth = (len) => kind === "SMA" ? sma(src, len) : kind === "EMA" ? ema(src, len) : rma(src, len);
25727
+ const jaw = shift(smooth(num3(inputs, "jawLength", 13)), num3(inputs, "jawOffset", 8));
25728
+ const teeth = shift(smooth(num3(inputs, "teethLength", 8)), num3(inputs, "teethOffset", 5));
25729
+ const lips = shift(smooth(num3(inputs, "lipsLength", 5)), num3(inputs, "lipsOffset", 3));
25218
25730
  const upper = map(sub(jaw, teeth), Math.abs);
25219
- const lower = map(sub(teeth, lips), (x) => -Math.abs(x));
25220
- const colorByExpansion = (v) => v.map((x, i) => {
25221
- if (!Number.isFinite(x)) return null;
25222
- const prev2 = i > 0 ? v[i - 1] : Number.NaN;
25223
- return Number.isFinite(prev2) && Math.abs(x) > Math.abs(prev2) ? BULLISH : BEARISH;
25224
- });
25731
+ const lower = map(sub(teeth, lips), Math.abs);
25732
+ const grow = str3(inputs, "growColor", BULLISH);
25733
+ const shrink = str3(inputs, "shrinkColor", BEARISH);
25734
+ const expansionColors2 = (v) => {
25735
+ let growing = false;
25736
+ return v.map((x, i) => {
25737
+ if (!Number.isFinite(x)) return null;
25738
+ const prev2 = i > 0 ? v[i - 1] : Number.NaN;
25739
+ if (Number.isFinite(prev2)) growing = x > prev2 ? true : x < prev2 ? false : growing;
25740
+ return growing ? grow : shrink;
25741
+ });
25742
+ };
25225
25743
  return {
25226
25744
  plots: [
25227
- { key: "upper", title: "Upper", values: upper, kind: "histogram", color: BULLISH, colors: colorByExpansion(upper), base: 0 },
25228
- { key: "lower", title: "Lower", values: lower, kind: "histogram", color: BEARISH, colors: colorByExpansion(lower), base: 0 }
25229
- ]
25745
+ { key: "upper", title: "Upper Histogram (Jaw - Teeth)", values: upper, kind: "columns", color: grow, colors: expansionColors2(upper), base: 0 },
25746
+ { key: "lower", title: "Lower Histogram (Teeth - Lips)", values: map(lower, (x) => -x), kind: "columns", color: shrink, colors: expansionColors2(lower), base: 0 }
25747
+ ],
25748
+ levels: [{ key: "zero", price: 0, color: str3(inputs, "zeroColor", NEUTRAL), lineStyle: "solid", title: "Zero Line" }]
25230
25749
  };
25231
25750
  }
25232
25751
  };
25233
25752
  var bandSpecs = [bollingerBands, percentB, bandwidth, keltner, donchian, chandelier, chandeKroll, supertrend, alligator, gator];
25234
25753
 
25235
25754
  // src/core/native-indicators/classics/oscillators.ts
25236
- function guideLevels(low, high, mid) {
25237
- const levels = [
25238
- { key: "upper", price: high, color: NEUTRAL },
25239
- { key: "lower", price: low, color: NEUTRAL }
25755
+ function thresholdInputs(obDefault, osDefault, min = 0, max = 100, obTitle = "Overbought Level", osTitle = "Oversold Level") {
25756
+ return [
25757
+ floatInput("overbought", obTitle, obDefault, min, max, 1, "Level above which the oscillator is considered overbought."),
25758
+ floatInput("oversold", osTitle, osDefault, min, max, 1, "Level below which the oscillator is considered oversold.")
25240
25759
  ];
25241
- if (mid != null) levels.splice(1, 0, { key: "middle", price: mid, color: NEUTRAL, lineStyle: "dotted" });
25242
- return levels;
25243
- }
25244
- var ZERO_LEVEL = [{ key: "zero", price: 0, color: NEUTRAL }];
25245
- function histColors(values) {
25246
- return values.map((x, i) => {
25247
- if (!Number.isFinite(x)) return null;
25248
- const prev2 = i > 0 ? values[i - 1] : Number.NaN;
25249
- const rising = Number.isFinite(prev2) ? x >= prev2 : true;
25250
- if (x >= 0) return rising ? BULLISH : `${BULLISH}80`;
25251
- return rising ? `${BEARISH}80` : BEARISH;
25252
- });
25253
25760
  }
25254
25761
  var rsiSpec = {
25255
25762
  type: "rsi",
25256
25763
  title: "Relative Strength Index",
25257
25764
  shortTitle: "RSI",
25258
25765
  overlay: false,
25259
- inputs: [lengthInput(14), sourceInput(), colorInput()],
25260
- compute: (bars, inputs) => ({
25261
- plots: [
25262
- {
25263
- key: "rsi",
25264
- title: "RSI",
25265
- values: rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
25266
- color: str3(inputs, "color", SERIES_LINE),
25267
- width: 2
25268
- }
25269
- ],
25270
- levels: guideLevels(30, 70, 50)
25766
+ inputs: [
25767
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used in the Wilder smoothing of average gains and losses."),
25768
+ sourceInput("Close", "source", "Source", "Price series the RSI is calculated on."),
25769
+ ...thresholdInputs(70, 30, 0, 100, "Overbought", "Oversold"),
25770
+ colorInput(NEUTRAL, "RSI", "color", "Color of the RSI line at the midline. Toward either threshold it blends into that threshold\u2019s color."),
25771
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level."),
25772
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
25773
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought, midline and oversold levels."),
25774
+ colorInput(transp(NEUTRAL, 90), "Band Fill", "fillColor", "Color of the fill between the overbought and oversold levels.")
25775
+ ],
25776
+ compute: (bars, inputs) => boundedOscillator({
25777
+ key: "rsi",
25778
+ title: "RSI",
25779
+ values: rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
25780
+ scale: { top: 100, bottom: 0 },
25781
+ overbought: num3(inputs, "overbought", 70),
25782
+ oversold: num3(inputs, "oversold", 30),
25783
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
25784
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
25785
+ width: 2,
25786
+ midline: 50,
25787
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 90))
25271
25788
  })
25272
25789
  };
25790
+ function stochasticOutput(k, d, inputs) {
25791
+ const overbought = num3(inputs, "overbought", 80);
25792
+ const oversold = num3(inputs, "oversold", 20);
25793
+ return {
25794
+ plots: [
25795
+ { key: "k", title: "%K", values: k, color: str3(inputs, "kColor", INFO) },
25796
+ { key: "d", title: "%D", values: d, color: str3(inputs, "dColor", WARNING) },
25797
+ anchorPlot("obAnchor", "Overbought Anchor", k.length, overbought),
25798
+ anchorPlot("osAnchor", "Oversold Anchor", k.length, oversold)
25799
+ ],
25800
+ bands: [{ key: "band", from: "obAnchor", to: "osAnchor", color: str3(inputs, "fillColor", transp(INFO, 90)) }],
25801
+ levels: [
25802
+ { key: "overbought", price: overbought, color: NEUTRAL, lineStyle: "dashed", title: "Overbought" },
25803
+ { key: "oversold", price: oversold, color: NEUTRAL, lineStyle: "dashed", title: "Oversold" }
25804
+ ]
25805
+ };
25806
+ }
25273
25807
  var stochastic = {
25274
25808
  type: "stochastic",
25275
25809
  title: "Stochastic",
25276
25810
  shortTitle: "Stoch",
25277
25811
  overlay: false,
25278
25812
  inputs: [
25279
- lengthInput(14, "kLength", "%K length"),
25280
- lengthInput(1, "kSmoothing", "%K smoothing", 500),
25281
- lengthInput(3, "dLength", "%D smoothing", 500)
25813
+ lengthInput(14, "kLength", "%K Length", 5e3, "Number of bars used for the highest high / lowest low range of the raw %K."),
25814
+ 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."),
25815
+ lengthInput(3, "dLength", "%D Smoothing", 500, "Simple moving average length of the %D signal line, applied to the smoothed %K."),
25816
+ ...thresholdInputs(80, 20),
25817
+ colorInput(INFO, "%K", "kColor", "Color of the %K line."),
25818
+ colorInput(WARNING, "%D", "dColor", "Color of the %D signal line."),
25819
+ colorInput(transp(INFO, 90), "Band Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
25282
25820
  ],
25283
25821
  compute: (bars, inputs) => {
25284
- const k = sma(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "kLength", 14)), num3(inputs, "kSmoothing", 1));
25285
- const d = sma(k, num3(inputs, "dLength", 3));
25286
- return {
25287
- plots: [
25288
- { key: "k", title: "%K", values: k, color: SERIES_LINE, width: 2 },
25289
- { key: "d", title: "%D", values: d, color: WARNING }
25290
- ],
25291
- levels: guideLevels(20, 80)
25292
- };
25822
+ const k = sma(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "kLength", 14)), num3(inputs, "kSmoothing", 3));
25823
+ return stochasticOutput(k, sma(k, num3(inputs, "dLength", 3)), inputs);
25293
25824
  }
25294
25825
  };
25295
25826
  var stochasticRsi = {
@@ -25298,23 +25829,20 @@ var stochasticRsi = {
25298
25829
  shortTitle: "Stoch RSI",
25299
25830
  overlay: false,
25300
25831
  inputs: [
25301
- lengthInput(14, "rsiLength", "RSI length"),
25302
- lengthInput(14, "stochLength", "Stochastic length"),
25303
- lengthInput(3, "kSmoothing", "%K smoothing", 500),
25304
- lengthInput(3, "dLength", "%D smoothing", 500),
25305
- sourceInput()
25832
+ sourceInput("Close", "source", "RSI Source", "Price series the RSI is computed on."),
25833
+ lengthInput(14, "rsiLength", "RSI Length", 5e3, "Lookback length of the Wilder RSI."),
25834
+ lengthInput(14, "stochLength", "Stochastic Length", 5e3, "Lookback over which the highest and lowest RSI values are taken."),
25835
+ lengthInput(3, "kSmoothing", "%K Smoothing", 500, "Simple moving average length applied to the stochastic of RSI to form %K."),
25836
+ lengthInput(3, "dLength", "%D Smoothing", 500, "Simple moving average length applied to %K to form the %D signal line."),
25837
+ ...thresholdInputs(80, 20),
25838
+ colorInput(INFO, "%K Color", "kColor", "Color of the %K line."),
25839
+ colorInput(WARNING, "%D Color", "dColor", "Color of the %D signal line."),
25840
+ colorInput(transp(INFO, 90), "Zone Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
25306
25841
  ],
25307
25842
  compute: (bars, inputs) => {
25308
25843
  const r = rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "rsiLength", 14));
25309
25844
  const k = sma(stoch(r, r, r, num3(inputs, "stochLength", 14)), num3(inputs, "kSmoothing", 3));
25310
- const d = sma(k, num3(inputs, "dLength", 3));
25311
- return {
25312
- plots: [
25313
- { key: "k", title: "%K", values: k, color: SERIES_LINE, width: 2 },
25314
- { key: "d", title: "%D", values: d, color: WARNING }
25315
- ],
25316
- levels: guideLevels(20, 80)
25317
- };
25845
+ return stochasticOutput(k, sma(k, num3(inputs, "dLength", 3)), inputs);
25318
25846
  }
25319
25847
  };
25320
25848
  var macd = {
@@ -25322,23 +25850,35 @@ var macd = {
25322
25850
  title: "MACD",
25323
25851
  overlay: false,
25324
25852
  inputs: [
25325
- lengthInput(12, "fastLength", "Fast length"),
25326
- lengthInput(26, "slowLength", "Slow length"),
25327
- lengthInput(9, "signalLength", "Signal smoothing", 500),
25328
- sourceInput()
25853
+ lengthInput(12, "fastLength", "Fast Length", 5e3, "Length of the fast EMA."),
25854
+ lengthInput(26, "slowLength", "Slow Length", 5e3, "Length of the slow EMA."),
25855
+ lengthInput(9, "signalLength", "Signal Smoothing", 500, "Length of the EMA applied to the MACD line to obtain the signal line."),
25856
+ sourceInput("Close", "source", "Source", "Price series used to compute the fast and slow EMAs."),
25857
+ colorInput(INFO, "MACD Line", "color", "Color of the MACD line."),
25858
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
25859
+ colorInput(BULLISH, "Grow Above", "growAboveColor", "Histogram color when above zero and rising."),
25860
+ colorInput(transp(BULLISH, 50), "Fall Above", "fallAboveColor", "Histogram color when above zero and falling."),
25861
+ colorInput(transp(BEARISH, 50), "Grow Below", "growBelowColor", "Histogram color when below zero and rising."),
25862
+ colorInput(BEARISH, "Fall Below", "fallBelowColor", "Histogram color when below zero and falling.")
25329
25863
  ],
25330
25864
  compute: (bars, inputs) => {
25331
25865
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
25332
25866
  const line = sub(ema(src, num3(inputs, "fastLength", 12)), ema(src, num3(inputs, "slowLength", 26)));
25333
25867
  const signal = ema(line, num3(inputs, "signalLength", 9));
25334
25868
  const hist = sub(line, signal);
25869
+ const colors = momentumColumnColors(hist, {
25870
+ growAbove: str3(inputs, "growAboveColor", BULLISH),
25871
+ fallAbove: str3(inputs, "fallAboveColor", transp(BULLISH, 50)),
25872
+ growBelow: str3(inputs, "growBelowColor", transp(BEARISH, 50)),
25873
+ fallBelow: str3(inputs, "fallBelowColor", BEARISH)
25874
+ });
25335
25875
  return {
25336
25876
  plots: [
25337
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: histColors(hist), base: 0 },
25338
- { key: "macd", title: "MACD", values: line, color: SERIES_LINE, width: 2 },
25339
- { key: "signal", title: "Signal", values: signal, color: WARNING }
25877
+ { key: "hist", title: "Histogram", values: hist, kind: "columns", color: BULLISH, colors, base: 0 },
25878
+ { key: "macd", title: "MACD", values: line, color: str3(inputs, "color", INFO) },
25879
+ { key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) }
25340
25880
  ],
25341
- levels: ZERO_LEVEL
25881
+ levels: [{ key: "zero", price: 0, color: NEUTRAL, lineStyle: "dashed", title: "Zero Line" }]
25342
25882
  };
25343
25883
  }
25344
25884
  };
@@ -25348,26 +25888,49 @@ var ppo = {
25348
25888
  shortTitle: "PPO",
25349
25889
  overlay: false,
25350
25890
  inputs: [
25351
- lengthInput(12, "fastLength", "Fast length"),
25352
- lengthInput(26, "slowLength", "Slow length"),
25353
- lengthInput(9, "signalLength", "Signal smoothing", 500),
25354
- sourceInput()
25891
+ sourceInput("Close", "source", "Source", "Price series fed into the fast and slow averages."),
25892
+ lengthInput(12, "fastLength", "Fast Length", 5e3, "Length of the fast moving average."),
25893
+ 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."),
25894
+ optionInput("maType", "Average Type", "EMA", ["EMA", "SMA"], "Moving average type used for the fast and slow averages."),
25895
+ lengthInput(9, "signalLength", "Signal Smoothing", 500, "Length of the signal line that smooths the PPO line."),
25896
+ optionInput("signalType", "Signal Type", "EMA", ["EMA", "SMA"], "Moving average type used for the signal line."),
25897
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the PPO line, gradient fill and histogram above the zero line."),
25898
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the PPO line, gradient fill and histogram below the zero line."),
25899
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
25900
+ 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."),
25901
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the zero line and the PPO line with a vertical gradient.")
25355
25902
  ],
25356
25903
  compute: (bars, inputs) => {
25357
25904
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
25358
- const fast = ema(src, num3(inputs, "fastLength", 12));
25359
- const slow = ema(src, num3(inputs, "slowLength", 26));
25905
+ const average = (kind2, v, len) => kind2 === "SMA" ? sma(v, len) : ema(v, len);
25906
+ const kind = str3(inputs, "maType", "EMA");
25907
+ const fast = average(kind, src, num3(inputs, "fastLength", 12));
25908
+ const slow = average(kind, src, num3(inputs, "slowLength", 26));
25360
25909
  const line = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : (f - s) / s * 100);
25361
- const signal = ema(line, num3(inputs, "signalLength", 9));
25910
+ const signal = average(str3(inputs, "signalType", "EMA"), line, num3(inputs, "signalLength", 9));
25362
25911
  const hist = sub(line, signal);
25363
- return {
25364
- plots: [
25365
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: histColors(hist), base: 0 },
25366
- { key: "ppo", title: "PPO", values: line, color: SERIES_LINE, width: 2 },
25367
- { key: "signal", title: "Signal", values: signal, color: WARNING }
25368
- ],
25369
- levels: ZERO_LEVEL
25370
- };
25912
+ const bull = str3(inputs, "bullColor", BULLISH);
25913
+ const bear = str3(inputs, "bearColor", BEARISH);
25914
+ const out = centeredOscillator({
25915
+ key: "ppo",
25916
+ title: "PPO Line",
25917
+ values: line,
25918
+ ink: { bull, bear, neutral: bear },
25919
+ zeroInk: transp(NEUTRAL, 90),
25920
+ fill: bool(inputs, "gradientFill", true),
25921
+ extraPlots: [{ key: "signal", title: "Signal Line", values: signal, color: str3(inputs, "signalColor", WARNING) }]
25922
+ });
25923
+ if (bool(inputs, "showHistogram", true)) {
25924
+ const colors = hist.map((x, i) => {
25925
+ if (!Number.isFinite(x)) return null;
25926
+ const prev2 = i > 0 && Number.isFinite(hist[i - 1]) ? hist[i - 1] : x;
25927
+ const ink = x >= 0 ? bull : bear;
25928
+ const expanding = x >= 0 ? x > prev2 : x < prev2;
25929
+ return transp(ink, expanding ? 40 : 70);
25930
+ });
25931
+ out.plots.unshift({ key: "hist", title: "Histogram", values: hist, kind: "columns", color: bull, colors, base: 0 });
25932
+ }
25933
+ return out;
25371
25934
  }
25372
25935
  };
25373
25936
  var awesome = {
@@ -25375,80 +25938,146 @@ var awesome = {
25375
25938
  title: "Awesome Oscillator",
25376
25939
  shortTitle: "AO",
25377
25940
  overlay: false,
25378
- inputs: [],
25379
- compute: (bars) => {
25941
+ inputs: [
25942
+ colorInput(BULLISH, "Rising", "risingColor", "Histogram color when the oscillator is higher than on the previous bar."),
25943
+ colorInput(BEARISH, "Falling", "fallingColor", "Histogram color when the oscillator is lower than or equal to the previous bar."),
25944
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
25945
+ ],
25946
+ compute: (bars, inputs) => {
25380
25947
  const hl2 = sourceValues(bars, "HL2");
25381
25948
  const ao = sub(sma(hl2, 5), sma(hl2, 34));
25949
+ const rising = str3(inputs, "risingColor", BULLISH);
25950
+ const falling = str3(inputs, "fallingColor", BEARISH);
25382
25951
  const colors = ao.map((x, i) => {
25383
25952
  if (!Number.isFinite(x)) return null;
25384
25953
  const prev2 = i > 0 ? ao[i - 1] : Number.NaN;
25385
- return Number.isFinite(prev2) && x < prev2 ? BEARISH : BULLISH;
25954
+ return Number.isFinite(prev2) && x > prev2 ? rising : falling;
25386
25955
  });
25387
25956
  return {
25388
- plots: [{ key: "ao", title: "AO", values: ao, kind: "histogram", color: BULLISH, colors, base: 0 }],
25389
- levels: ZERO_LEVEL
25957
+ plots: [{ key: "ao", title: "AO", values: ao, kind: "columns", color: rising, colors, base: 0 }],
25958
+ levels: [{ key: "zero", price: 0, color: str3(inputs, "zeroColor", NEUTRAL), lineStyle: "dashed", title: "Zero Line" }]
25390
25959
  };
25391
25960
  }
25392
25961
  };
25962
+ var CCI_SCALING = 0.015;
25393
25963
  var cci = {
25394
25964
  type: "commodity-channel-index",
25395
25965
  title: "Commodity Channel Index",
25396
25966
  shortTitle: "CCI",
25397
25967
  overlay: false,
25398
- inputs: [lengthInput(20), sourceInput("HLC3"), colorInput()],
25968
+ inputs: [
25969
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used for the moving average and the mean absolute deviation of the source."),
25970
+ sourceInput("HLC3", "source", "Source", "Price series measured by the CCI; the typical price hlc3 is the classic choice."),
25971
+ floatInput("upper", "Upper Level", 100, -1e3, 1e3, 10, "Overbought reference level, +100 classically."),
25972
+ floatInput("lower", "Lower Level", -100, -1e3, 1e3, 10, "Oversold reference level, \u2212100 classically."),
25973
+ colorInput(NEUTRAL, "CCI Line", "color", "Color of the CCI line."),
25974
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the CCI reads above zero."),
25975
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the CCI reads below zero."),
25976
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the upper, lower and zero reference lines.")
25977
+ ],
25399
25978
  compute: (bars, inputs) => {
25400
25979
  const len = num3(inputs, "length", 20);
25401
25980
  const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
25402
25981
  const basis = sma(src, len);
25403
- const dev = new Array(src.length).fill(Number.NaN);
25404
- for (let i = len - 1; i < src.length; i++) {
25405
- const mean = basis[i];
25406
- if (!Number.isFinite(mean)) continue;
25407
- let d = 0;
25408
- for (let k = i - len + 1; k <= i; k++) d += Math.abs(src[k] - mean);
25409
- dev[i] = d / len;
25410
- }
25982
+ const dev = meanDev(src, len);
25411
25983
  const values = src.map((x, i) => {
25412
25984
  const b = basis[i];
25413
25985
  const d = dev[i];
25414
- return Number.isFinite(b) && Number.isFinite(d) && d !== 0 ? (x - b) / (0.015 * d) : Number.NaN;
25986
+ if (!Number.isFinite(b) || !Number.isFinite(d)) return Number.NaN;
25987
+ return d === 0 ? 0 : (x - b) / (CCI_SCALING * d);
25415
25988
  });
25416
- return {
25417
- plots: [{ key: "cci", title: "CCI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25418
- levels: guideLevels(-100, 100, 0)
25419
- };
25420
- }
25989
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
25990
+ return centeredOscillator({
25991
+ key: "cci",
25992
+ title: "CCI",
25993
+ values,
25994
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
25995
+ zeroInk: levelInk,
25996
+ zeroLineStyle: "dotted",
25997
+ extraLevels: [
25998
+ { key: "upper", price: num3(inputs, "upper", 100), color: levelInk, lineStyle: "dashed", title: "Upper Level" },
25999
+ { key: "lower", price: num3(inputs, "lower", -100), color: levelInk, lineStyle: "dashed", title: "Lower Level" }
26000
+ ]
26001
+ });
26002
+ }
25421
26003
  };
25422
26004
  var williamsR = {
25423
26005
  type: "williams-percent-r",
25424
26006
  title: "Williams %R",
25425
26007
  shortTitle: "%R",
25426
26008
  overlay: false,
25427
- inputs: [lengthInput(14), colorInput()],
26009
+ inputs: [
26010
+ lengthInput(14, "length", "Length", 5e3, "Lookback length of the highest high / lowest low range. 14 is the classic setting."),
26011
+ sourceInput("Close", "source", "Source", "Series located within the lookback range. Close is the classic choice."),
26012
+ ...thresholdInputs(-20, -80, -100, 0),
26013
+ colorInput(NEUTRAL, "%R Line", "color", "Color of the %R line between the thresholds. Toward either level it blends into that level\u2019s color."),
26014
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
26015
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level.")
26016
+ ],
25428
26017
  compute: (bars, inputs) => {
25429
- const values = map(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "length", 14)), (x) => x - 100);
25430
- return {
25431
- plots: [{ key: "wr", title: "%R", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25432
- levels: guideLevels(-80, -20)
25433
- };
26018
+ const len = num3(inputs, "length", 14);
26019
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
26020
+ const hh = highest(highs(bars), len);
26021
+ const ll = lowest(lows(bars), len);
26022
+ const values = src.map((x, i) => {
26023
+ const h = hh[i];
26024
+ const l = ll[i];
26025
+ return Number.isFinite(h) && Number.isFinite(l) && h > l ? -100 * (h - x) / (h - l) : Number.NaN;
26026
+ });
26027
+ return boundedOscillator({
26028
+ key: "wr",
26029
+ title: "%R",
26030
+ values,
26031
+ scale: { top: 0, bottom: -100 },
26032
+ overbought: num3(inputs, "overbought", -20),
26033
+ oversold: num3(inputs, "oversold", -80),
26034
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
26035
+ levelInk: NEUTRAL
26036
+ });
25434
26037
  }
25435
26038
  };
25436
- var cmo = {
26039
+ var cmoSpec = {
25437
26040
  type: "chande-momentum-oscillator",
25438
26041
  title: "Chande Momentum Oscillator",
25439
26042
  shortTitle: "CMO",
25440
26043
  overlay: false,
25441
- inputs: [lengthInput(9), sourceInput(), colorInput()],
26044
+ inputs: [
26045
+ lengthInput(9, "length", "Length", 5e3, "Number of bars over which up moves and down moves are summed."),
26046
+ sourceInput("Close", "source", "Source", "Price series used to measure bar-to-bar changes."),
26047
+ floatInput("overbought", "Overbought Level", 50, 0, 100, 1, "Level above which the oscillator is considered overbought."),
26048
+ floatInput("oversold", "Oversold Level", -50, -100, 0, 1, "Level below which the oscillator is considered oversold."),
26049
+ colorInput(NEUTRAL, "CMO Line", "color", "Color of the CMO line."),
26050
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while momentum is positive, and shading of the oversold zone."),
26051
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while momentum is negative, and shading of the overbought zone."),
26052
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought, oversold, and zero lines.")
26053
+ ],
25442
26054
  compute: (bars, inputs) => {
25443
26055
  const len = num3(inputs, "length", 9);
25444
26056
  const d = change(sourceValues(bars, str3(inputs, "source", "Close")));
25445
- const su = sum(map(d, (x) => Math.max(x, 0)), len);
25446
- const sd = sum(map(d, (x) => Math.max(-x, 0)), len);
25447
- const values = zip(su, sd, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
25448
- return {
25449
- plots: [{ key: "cmo", title: "CMO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25450
- levels: guideLevels(-50, 50, 0)
25451
- };
26057
+ const up = sum(map(d, (x) => Math.max(x, 0)), len);
26058
+ const down = sum(map(d, (x) => Math.max(-x, 0)), len);
26059
+ const values = zip(up, down, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
26060
+ const bull = str3(inputs, "bullColor", BULLISH);
26061
+ const bear = str3(inputs, "bearColor", BEARISH);
26062
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
26063
+ const overbought = num3(inputs, "overbought", 50);
26064
+ const oversold = num3(inputs, "oversold", -50);
26065
+ const out = centeredOscillator({
26066
+ key: "cmo",
26067
+ title: "CMO",
26068
+ values,
26069
+ ink: { bull, bear, neutral: str3(inputs, "color", NEUTRAL) },
26070
+ zeroInk: levelInk,
26071
+ zeroLineStyle: "dotted",
26072
+ extraLevels: [
26073
+ { key: "overbought", price: overbought, color: levelInk, lineStyle: "dashed", title: "Overbought" },
26074
+ { key: "oversold", price: oversold, color: levelInk, lineStyle: "dashed", title: "Oversold" }
26075
+ ]
26076
+ });
26077
+ const zones = thresholdZones(values.length, { overbought, oversold, top: 100, bottom: -100 }, { overbought: bear, oversold: bull });
26078
+ out.plots.push(...zones.plots);
26079
+ out.bands = [...out.bands ?? [], ...zones.bands];
26080
+ return out;
25452
26081
  }
25453
26082
  };
25454
26083
  var connorsRsi = {
@@ -25457,10 +26086,14 @@ var connorsRsi = {
25457
26086
  shortTitle: "CRSI",
25458
26087
  overlay: false,
25459
26088
  inputs: [
25460
- lengthInput(3, "rsiLength", "RSI length"),
25461
- lengthInput(2, "streakLength", "Streak RSI length"),
25462
- lengthInput(100, "rankLength", "Percent-rank length"),
25463
- colorInput()
26089
+ lengthInput(3, "rsiLength", "RSI Length", 5e3, "Length of the Wilder RSI applied to the closing price."),
26090
+ lengthInput(2, "streakLength", "Streak RSI Length", 5e3, "Length of the Wilder RSI applied to the up/down close streak."),
26091
+ lengthInput(100, "rankLength", "Percent Rank Length", 5e3, "Lookback of the percent rank applied to the 1-bar rate of change of the closing price."),
26092
+ ...thresholdInputs(90, 10),
26093
+ colorInput(NEUTRAL, "CRSI Line", "color", "Color of the Connors RSI line."),
26094
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Line, fill and zone color toward and below the oversold level."),
26095
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Line, fill and zone color toward and above the overbought level."),
26096
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought and oversold levels.")
25464
26097
  ],
25465
26098
  compute: (bars, inputs) => {
25466
26099
  const c = closes(bars);
@@ -25478,10 +26111,17 @@ var connorsRsi = {
25478
26111
  const z = r[i];
25479
26112
  return Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) ? (x + y + z) / 3 : Number.NaN;
25480
26113
  });
25481
- return {
25482
- plots: [{ key: "crsi", title: "CRSI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25483
- levels: guideLevels(20, 80, 50)
25484
- };
26114
+ return boundedOscillator({
26115
+ key: "crsi",
26116
+ title: "Connors RSI",
26117
+ values,
26118
+ scale: { top: 100, bottom: 0 },
26119
+ overbought: num3(inputs, "overbought", 90),
26120
+ oversold: num3(inputs, "oversold", 10),
26121
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
26122
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
26123
+ midpointWash: true
26124
+ });
25485
26125
  }
25486
26126
  };
25487
26127
  var fisher = {
@@ -25489,7 +26129,13 @@ var fisher = {
25489
26129
  title: "Fisher Transform",
25490
26130
  shortTitle: "Fisher",
25491
26131
  overlay: false,
25492
- inputs: [lengthInput(9)],
26132
+ inputs: [
26133
+ 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."),
26134
+ colorInput(NEUTRAL, "Fisher Line", "color", "Color of the Fisher line when it sits exactly on its trigger."),
26135
+ colorInput(NEUTRAL, "Trigger Line", "triggerColor", "Color of the trigger line, the Fisher value delayed one bar."),
26136
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the Fisher line and fill while it is above the trigger."),
26137
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the Fisher line and fill while it is below the trigger.")
26138
+ ],
25493
26139
  compute: (bars, inputs) => {
25494
26140
  const len = num3(inputs, "length", 9);
25495
26141
  const hl2 = sourceValues(bars, "HL2");
@@ -25497,23 +26143,39 @@ var fisher = {
25497
26143
  const lo = lowest(hl2, len);
25498
26144
  const n = bars.length;
25499
26145
  const out = new Array(n).fill(Number.NaN);
26146
+ let position = 0;
25500
26147
  let value = 0;
25501
- let fish = 0;
25502
26148
  for (let i = 0; i < n; i++) {
25503
26149
  const h = hi[i];
25504
26150
  const l = lo[i];
25505
26151
  if (!Number.isFinite(h) || !Number.isFinite(l)) continue;
25506
- const norm = h === l ? 0 : (hl2[i] - l) / (h - l) - 0.5;
25507
- value = Math.max(-0.999, Math.min(0.999, 0.66 * norm + 0.67 * value));
25508
- fish = 0.5 * Math.log((1 + value) / (1 - value)) + 0.5 * fish;
25509
- out[i] = fish;
25510
- }
26152
+ const raw = h > l ? (hl2[i] - l) / (h - l) - 0.5 : 0;
26153
+ position = 0.66 * raw + 0.67 * position;
26154
+ position = position > 0.99 ? 0.999 : position < -0.99 ? -0.999 : position;
26155
+ value = 0.5 * Math.log((1 + position) / (1 - position)) + 0.5 * value;
26156
+ out[i] = value;
26157
+ }
26158
+ const trigger = shift(out, 1);
26159
+ const neutral = str3(inputs, "color", NEUTRAL);
26160
+ const bull = str3(inputs, "bullColor", BULLISH);
26161
+ const bear = str3(inputs, "bearColor", BEARISH);
26162
+ const inkOf = (i) => {
26163
+ const f = out[i];
26164
+ const t = trigger[i];
26165
+ if (!Number.isFinite(t)) return neutral;
26166
+ return f > t ? bull : f < t ? bear : neutral;
26167
+ };
25511
26168
  return {
25512
26169
  plots: [
25513
- { key: "fisher", title: "Fisher", values: out, color: SERIES_LINE, width: 2 },
25514
- { key: "trigger", title: "Trigger", values: shift(out, 1), color: WARNING }
26170
+ { key: "fisher", title: "Fisher", values: out, color: neutral, width: 2, colors: out.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) },
26171
+ { key: "trigger", title: "Trigger", values: trigger, color: str3(inputs, "triggerColor", NEUTRAL) }
25515
26172
  ],
25516
- levels: ZERO_LEVEL
26173
+ bands: [{ key: "spread", from: "fisher", to: "trigger", color: transp(neutral, 80), colors: out.map((x, i) => Number.isFinite(x) ? transp(inkOf(i), 80) : null) }],
26174
+ levels: [
26175
+ { key: "upper", price: 1.5, color: NEUTRAL, lineStyle: "dashed", title: "Upper Level" },
26176
+ { key: "lower", price: -1.5, color: NEUTRAL, lineStyle: "dashed", title: "Lower Level" },
26177
+ { key: "zero", price: 0, color: NEUTRAL, lineStyle: "dotted", title: "Zero Line" }
26178
+ ]
25517
26179
  };
25518
26180
  }
25519
26181
  };
@@ -25521,93 +26183,180 @@ var trix = {
25521
26183
  type: "trix",
25522
26184
  title: "TRIX",
25523
26185
  overlay: false,
25524
- inputs: [lengthInput(18), colorInput()],
26186
+ inputs: [
26187
+ lengthInput(18, "length", "Length", 5e3, "Length of each of the three EMA smoothing passes applied to the close price."),
26188
+ lengthInput(9, "signalLength", "Signal Length", 500, "Length of the EMA applied to TRIX to form the signal line."),
26189
+ colorInput(NEUTRAL, "TRIX Color", "color", "Color of the TRIX line."),
26190
+ colorInput(WARNING, "Signal Color", "signalColor", "Color of the signal line."),
26191
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the TRIX line and its gradient fill while TRIX holds above zero."),
26192
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the TRIX line and its gradient fill while TRIX holds below zero.")
26193
+ ],
25525
26194
  compute: (bars, inputs) => {
25526
26195
  const len = num3(inputs, "length", 18);
25527
- const logClose = map(closes(bars), Math.log);
25528
- const smooth = ema(ema(ema(logClose, len), len), len);
25529
- const values = map(change(smooth), (x) => x * 1e4);
25530
- return {
25531
- plots: [{ key: "trix", title: "TRIX", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25532
- levels: ZERO_LEVEL
25533
- };
26196
+ const smooth = ema(ema(ema(closes(bars), len), len), len);
26197
+ const values = roc(smooth, 1);
26198
+ return centeredOscillator({
26199
+ key: "trix",
26200
+ title: "TRIX",
26201
+ values,
26202
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
26203
+ zeroInk: NEUTRAL,
26204
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(values, num3(inputs, "signalLength", 9)), color: str3(inputs, "signalColor", WARNING) }]
26205
+ });
25534
26206
  }
25535
26207
  };
25536
- function tsiLine(src, long, short) {
26208
+ function tsiRatio(src, long, short) {
25537
26209
  const mom = change(src);
25538
26210
  const numer = ema(ema(mom, long), short);
25539
26211
  const denom = ema(ema(map(mom, Math.abs), long), short);
25540
- return zip(numer, denom, (a, b) => b === 0 ? 0 : 100 * a / b);
26212
+ return zip(numer, denom, (a, b) => b === 0 ? 0 : a / b);
25541
26213
  }
25542
26214
  var tsi = {
25543
26215
  type: "true-strength-index",
25544
26216
  title: "True Strength Index",
25545
26217
  shortTitle: "TSI",
25546
26218
  overlay: false,
25547
- inputs: [lengthInput(25, "longLength", "Long length"), lengthInput(13, "shortLength", "Short length"), lengthInput(13, "signalLength", "Signal length")],
26219
+ inputs: [
26220
+ lengthInput(25, "longLength", "Long Length", 5e3, "Length of the first (long) EMA applied to the 1-bar price change."),
26221
+ lengthInput(13, "shortLength", "Short Length", 5e3, "Length of the second (short) EMA applied to the long-smoothed price change."),
26222
+ lengthInput(13, "signalLength", "Signal Length", 500, "Length of the EMA of TSI used as the signal line."),
26223
+ colorInput(NEUTRAL, "TSI", "color", "Color of the TSI line."),
26224
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line."),
26225
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the TSI line and its gradient fill while TSI holds above zero."),
26226
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the TSI line and its gradient fill while TSI holds below zero.")
26227
+ ],
25548
26228
  compute: (bars, inputs) => {
25549
- const line = tsiLine(closes(bars), num3(inputs, "longLength", 25), num3(inputs, "shortLength", 13));
25550
- return {
25551
- plots: [
25552
- { key: "tsi", title: "TSI", values: line, color: SERIES_LINE, width: 2 },
25553
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: WARNING }
25554
- ],
25555
- levels: ZERO_LEVEL
25556
- };
26229
+ const values = map(tsiRatio(closes(bars), num3(inputs, "longLength", 25), num3(inputs, "shortLength", 13)), (x) => 100 * x);
26230
+ return centeredOscillator({
26231
+ key: "tsi",
26232
+ title: "TSI",
26233
+ values,
26234
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
26235
+ zeroInk: NEUTRAL,
26236
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(values, num3(inputs, "signalLength", 13)), color: str3(inputs, "signalColor", WARNING) }]
26237
+ });
25557
26238
  }
25558
26239
  };
26240
+ var SMI_INDICATOR = "Indicator";
26241
+ var SMI_OSCILLATOR = "Oscillator";
25559
26242
  var smiErgodic = {
25560
26243
  type: "smi-ergodic",
25561
26244
  title: "SMI Ergodic Oscillator",
25562
26245
  shortTitle: "SMIE",
25563
26246
  overlay: false,
25564
- inputs: [lengthInput(5, "shortLength", "Short length"), lengthInput(20, "longLength", "Long length"), lengthInput(5, "signalLength", "Signal length")],
26247
+ inputs: [
26248
+ lengthInput(20, "longLength", "Long Length", 5e3, "Length of the first EMA smoothing applied to the momentum of the closing price."),
26249
+ lengthInput(5, "shortLength", "Short Length", 5e3, "Length of the second EMA smoothing applied to the momentum of the closing price."),
26250
+ lengthInput(5, "signalLength", "Signal Length", 500, "Length of the EMA of the SMI that forms the signal line."),
26251
+ 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."),
26252
+ colorInput(NEUTRAL, "SMI", "color", "Color of the SMI line while it sits exactly on its signal line."),
26253
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line."),
26254
+ colorInput(BULLISH, "Oscillator Grow", "growColor", "Color of the rising oscillator histogram, and of the SMI line while it leads its signal."),
26255
+ colorInput(BEARISH, "Oscillator Fall", "fallColor", "Color of the falling oscillator histogram, and of the SMI line while it lags its signal.")
26256
+ ],
25565
26257
  compute: (bars, inputs) => {
25566
- const line = tsiLine(closes(bars), num3(inputs, "longLength", 20), num3(inputs, "shortLength", 5));
25567
- const signal = ema(line, num3(inputs, "signalLength", 5));
25568
- const osc = sub(line, signal);
26258
+ const smi = tsiRatio(closes(bars), num3(inputs, "longLength", 20), num3(inputs, "shortLength", 5));
26259
+ const signal = ema(smi, num3(inputs, "signalLength", 5));
26260
+ const grow = str3(inputs, "growColor", BULLISH);
26261
+ const fall = str3(inputs, "fallColor", BEARISH);
26262
+ const zero = [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed", title: "Zero" }];
26263
+ if (str3(inputs, "display", SMI_INDICATOR) === SMI_OSCILLATOR) {
26264
+ const osc = sub(smi, signal);
26265
+ const colors = osc.map((x, i) => {
26266
+ if (!Number.isFinite(x)) return null;
26267
+ const prev2 = i > 0 && Number.isFinite(osc[i - 1]) ? osc[i - 1] : x;
26268
+ return x > prev2 ? grow : fall;
26269
+ });
26270
+ return { plots: [{ key: "osc", title: "Oscillator", values: osc, kind: "columns", color: grow, colors, base: 0 }], levels: zero };
26271
+ }
26272
+ const neutral = str3(inputs, "color", NEUTRAL);
26273
+ const inkOf = (i) => {
26274
+ const a = smi[i];
26275
+ const b = signal[i];
26276
+ if (!Number.isFinite(b)) return neutral;
26277
+ return a > b ? grow : a < b ? fall : neutral;
26278
+ };
25569
26279
  return {
25570
26280
  plots: [
25571
- { key: "osc", title: "Oscillator", values: osc, kind: "histogram", color: BULLISH, colors: histColors(osc), base: 0 },
25572
- { key: "smi", title: "SMI", values: line, color: SERIES_LINE, width: 2 },
25573
- { key: "signal", title: "Signal", values: signal, color: WARNING }
26281
+ { key: "smi", title: "SMI", values: smi, color: neutral, colors: smi.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) },
26282
+ { key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) }
25574
26283
  ],
25575
- levels: ZERO_LEVEL
26284
+ bands: [{ key: "spread", from: "smi", to: "signal", color: transp(neutral, 75), colors: smi.map((x, i) => Number.isFinite(x) ? transp(inkOf(i), 75) : null) }],
26285
+ levels: zero
25576
26286
  };
25577
26287
  }
25578
26288
  };
26289
+ var STC_MID = 50;
26290
+ var TRIGGERS = "Trigger Levels";
25579
26291
  var stc = {
25580
26292
  type: "schaff-trend-cycle",
25581
26293
  title: "Schaff Trend Cycle",
25582
26294
  shortTitle: "Schaff Trend Cycle",
25583
26295
  overlay: false,
25584
- inputs: [lengthInput(10, "cycleLength", "Cycle length"), lengthInput(23, "fastLength", "Fast length"), lengthInput(50, "slowLength", "Slow length")],
26296
+ inputs: [
26297
+ sourceInput("Close", "source", "Source", "Price series used to build the MACD that the trend cycle is measured on."),
26298
+ lengthInput(23, "fastLength", "Fast Length", 5e3, "Fast EMA length of the underlying MACD."),
26299
+ lengthInput(50, "slowLength", "Slow Length", 5e3, "Slow EMA length of the underlying MACD."),
26300
+ { ...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 },
26301
+ 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."),
26302
+ { ...floatInput("upper", "Upper Trigger", 75, 0, 100, 1, "Upper trigger line. A cross down through it flags an emerging downtrend."), group: TRIGGERS },
26303
+ { ...floatInput("lower", "Lower Trigger", 25, 0, 100, 1, "Lower trigger line. A cross up through it flags an emerging uptrend."), group: TRIGGERS },
26304
+ 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."),
26305
+ 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."),
26306
+ colorInput(NEUTRAL, "Levels Color", "levelsColor", "Color of the dashed trigger lines and the dotted midline."),
26307
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Vertical gradient between the midline and the trend cycle, fading out toward the midline.")
26308
+ ],
25585
26309
  compute: (bars, inputs) => {
25586
26310
  const len = num3(inputs, "cycleLength", 10);
25587
- const src = closes(bars);
26311
+ const factor = num3(inputs, "factor", 0.5);
26312
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
25588
26313
  const macdLine = sub(ema(src, num3(inputs, "fastLength", 23)), ema(src, num3(inputs, "slowLength", 50)));
25589
- const stochOf = (v) => {
26314
+ const stochNorm = (v) => {
25590
26315
  const lo = lowest(v, len);
25591
26316
  const hi = highest(v, len);
25592
26317
  const out = new Array(v.length).fill(Number.NaN);
25593
- let prevRaw = Number.NaN;
25594
- let smoothed = Number.NaN;
26318
+ let pct = Number.NaN;
25595
26319
  for (let i = 0; i < v.length; i++) {
25596
26320
  const x = v[i];
25597
26321
  const l = lo[i];
25598
26322
  const h = hi[i];
25599
26323
  if (!Number.isFinite(x) || !Number.isFinite(l) || !Number.isFinite(h)) continue;
25600
- const raw = h > l ? (x - l) / (h - l) * 100 : Number.isFinite(prevRaw) ? prevRaw : 50;
25601
- prevRaw = raw;
25602
- smoothed = Number.isFinite(smoothed) ? smoothed + 0.5 * (raw - smoothed) : raw;
26324
+ if (h > l) pct = 100 * (x - l) / (h - l);
26325
+ out[i] = pct;
26326
+ }
26327
+ return out;
26328
+ };
26329
+ const smoothStep = (v) => {
26330
+ const out = new Array(v.length).fill(Number.NaN);
26331
+ let smoothed = Number.NaN;
26332
+ for (let i = 0; i < v.length; i++) {
26333
+ const x = v[i];
26334
+ if (!Number.isFinite(x)) continue;
26335
+ smoothed = Number.isFinite(smoothed) ? smoothed + factor * (x - smoothed) : x;
25603
26336
  out[i] = smoothed;
25604
26337
  }
25605
26338
  return out;
25606
26339
  };
25607
- const values = stochOf(stochOf(macdLine));
26340
+ const values = smoothStep(stochNorm(smoothStep(stochNorm(macdLine))));
26341
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
26342
+ const bull = str3(inputs, "bullColor", BULLISH);
26343
+ const bear = str3(inputs, "bearColor", BEARISH);
26344
+ const plots = [
26345
+ { key: "stc", title: "Schaff Trend Cycle", values, color: bear, width: 2, colors: values.map((x) => Number.isFinite(x) ? x > STC_MID ? bull : bear : null) }
26346
+ ];
26347
+ const bands = [];
26348
+ if (bool(inputs, "gradientFill", true)) {
26349
+ plots.push(anchorPlot("midAnchor", "Midline Anchor", values.length, STC_MID));
26350
+ bands.push({ key: "wash", from: "stc", to: "midAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, STC_MID, (x) => x > STC_MID ? bull : bear) });
26351
+ }
25608
26352
  return {
25609
- plots: [{ key: "stc", title: "STC", values, color: SERIES_LINE, width: 2 }],
25610
- levels: guideLevels(25, 75)
26353
+ plots,
26354
+ bands,
26355
+ levels: [
26356
+ { key: "upper", price: num3(inputs, "upper", 75), color: transp(levelInk, 25), lineStyle: "dashed", title: "Upper Trigger" },
26357
+ { key: "lower", price: num3(inputs, "lower", 25), color: transp(levelInk, 25), lineStyle: "dashed", title: "Lower Trigger" },
26358
+ { key: "midline", price: STC_MID, color: transp(levelInk, 60), lineStyle: "dotted", title: "Midline" }
26359
+ ]
25611
26360
  };
25612
26361
  }
25613
26362
  };
@@ -25616,22 +26365,37 @@ var kst = {
25616
26365
  title: "Know Sure Thing",
25617
26366
  shortTitle: "KST",
25618
26367
  overlay: false,
25619
- inputs: [sourceInput(), lengthInput(9, "signalLength", "Signal length")],
26368
+ inputs: [
26369
+ lengthInput(10, "rocLength1", "ROC Length #1", 5e3, "Lookback of the first (shortest) rate of change."),
26370
+ lengthInput(15, "rocLength2", "ROC Length #2", 5e3, "Lookback of the second rate of change."),
26371
+ lengthInput(20, "rocLength3", "ROC Length #3", 5e3, "Lookback of the third rate of change."),
26372
+ lengthInput(30, "rocLength4", "ROC Length #4", 5e3, "Lookback of the fourth (longest) rate of change."),
26373
+ lengthInput(10, "smaLength1", "SMA Length #1", 5e3, "Smoothing length applied to the first rate of change (RCMA #1)."),
26374
+ lengthInput(10, "smaLength2", "SMA Length #2", 5e3, "Smoothing length applied to the second rate of change (RCMA #2)."),
26375
+ lengthInput(10, "smaLength3", "SMA Length #3", 5e3, "Smoothing length applied to the third rate of change (RCMA #3)."),
26376
+ lengthInput(15, "smaLength4", "SMA Length #4", 5e3, "Smoothing length applied to the fourth rate of change (RCMA #4)."),
26377
+ lengthInput(9, "signalLength", "Signal Length", 500, "Simple moving average length of the signal line."),
26378
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the KST line while above zero."),
26379
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the KST line while below zero."),
26380
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line.")
26381
+ ],
25620
26382
  compute: (bars, inputs) => {
25621
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
25622
- const line = [
25623
- sma(roc(src, 10), 10),
25624
- sma(roc(src, 15), 10),
25625
- sma(roc(src, 20), 10),
25626
- sma(roc(src, 30), 15)
25627
- ].reduce((acc, r, i) => zip(acc, r, (a, b) => a + b * (i + 1)), new Array(src.length).fill(0));
25628
- return {
25629
- plots: [
25630
- { key: "kst", title: "KST", values: line, color: SERIES_LINE, width: 2 },
25631
- { key: "signal", title: "Signal", values: sma(line, num3(inputs, "signalLength", 9)), color: WARNING }
25632
- ],
25633
- levels: ZERO_LEVEL
25634
- };
26383
+ const c = closes(bars);
26384
+ const rcma = (rocKey, rocDefault, smaKey, smaDefault) => sma(roc(c, num3(inputs, rocKey, rocDefault)), num3(inputs, smaKey, smaDefault));
26385
+ const values = [rcma("rocLength1", 10, "smaLength1", 10), rcma("rocLength2", 15, "smaLength2", 10), rcma("rocLength3", 20, "smaLength3", 10), rcma("rocLength4", 30, "smaLength4", 15)].reduce(
26386
+ (acc, r, i) => zip(acc, r, (a, b) => a + b * (i + 1)),
26387
+ new Array(c.length).fill(0)
26388
+ );
26389
+ const bull = str3(inputs, "bullColor", BULLISH);
26390
+ return centeredOscillator({
26391
+ key: "kst",
26392
+ title: "KST",
26393
+ values,
26394
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
26395
+ zeroInk: NEUTRAL,
26396
+ valueTransparency: 60,
26397
+ extraPlots: [{ key: "signal", title: "Signal", values: sma(values, num3(inputs, "signalLength", 9)), color: str3(inputs, "signalColor", WARNING) }]
26398
+ });
25635
26399
  }
25636
26400
  };
25637
26401
  var coppock = {
@@ -25640,19 +26404,25 @@ var coppock = {
25640
26404
  shortTitle: "Coppock",
25641
26405
  overlay: false,
25642
26406
  inputs: [
25643
- lengthInput(10, "wmaLength", "WMA length"),
25644
- lengthInput(14, "longRoc", "Long RoC length"),
25645
- lengthInput(11, "shortRoc", "Short RoC length"),
25646
- sourceInput(),
25647
- colorInput()
26407
+ lengthInput(10, "wmaLength", "WMA Length", 5e3, "Length of the weighted moving average applied to the sum of the two rates of change."),
26408
+ lengthInput(14, "longRoc", "Long RoC Length", 5e3, "Lookback of the long rate of change, in bars. The original definition uses 14 monthly closes."),
26409
+ lengthInput(11, "shortRoc", "Short RoC Length", 5e3, "Lookback of the short rate of change, in bars. The original definition uses 11 monthly closes."),
26410
+ sourceInput("Close", "source", "Source", "Price series used for the rate of change calculations."),
26411
+ colorInput(NEUTRAL, "Coppock Curve", "color", "Color of the Coppock Curve line."),
26412
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the curve reads above zero."),
26413
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the curve reads below zero."),
26414
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
25648
26415
  ],
25649
26416
  compute: (bars, inputs) => {
25650
26417
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
25651
26418
  const values = wma(zip(roc(src, num3(inputs, "longRoc", 14)), roc(src, num3(inputs, "shortRoc", 11)), (a, b) => a + b), num3(inputs, "wmaLength", 10));
25652
- return {
25653
- plots: [{ key: "coppock", title: "Coppock", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25654
- levels: ZERO_LEVEL
25655
- };
26419
+ return centeredOscillator({
26420
+ key: "coppock",
26421
+ title: "Coppock Curve",
26422
+ values,
26423
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
26424
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
26425
+ });
25656
26426
  }
25657
26427
  };
25658
26428
  var dpo = {
@@ -25660,42 +26430,76 @@ var dpo = {
25660
26430
  title: "Detrended Price Oscillator",
25661
26431
  shortTitle: "DPO",
25662
26432
  overlay: false,
25663
- inputs: [lengthInput(21), colorInput()],
26433
+ inputs: [
26434
+ 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."),
26435
+ sourceInput("Close", "source", "Source", "Price series used in the detrending calculation."),
26436
+ 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."),
26437
+ colorInput(NEUTRAL, "DPO", "color", "Color of the line when it sits exactly at zero."),
26438
+ colorInput(BULLISH, "Above Zero", "bullColor", "Color of the line and fill while the oscillator is above zero."),
26439
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while the oscillator is below zero."),
26440
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
26441
+ ],
25664
26442
  compute: (bars, inputs) => {
25665
26443
  const len = num3(inputs, "length", 21);
25666
- const barsback = Math.floor(len / 2) + 1;
25667
- const values = sub(closes(bars), shift(sma(closes(bars), len), barsback));
25668
- return {
25669
- plots: [{ key: "dpo", title: "DPO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25670
- levels: ZERO_LEVEL
25671
- };
26444
+ const barsBack = Math.trunc(len / 2) + 1;
26445
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
26446
+ const basis = sma(src, len);
26447
+ const centered = bool(inputs, "centered", false);
26448
+ const values = centered ? shift(sub(shift(src, barsBack), basis), -barsBack) : sub(src, shift(basis, barsBack));
26449
+ return centeredOscillator({
26450
+ key: "dpo",
26451
+ title: "DPO",
26452
+ values,
26453
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
26454
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
26455
+ });
25672
26456
  }
25673
26457
  };
26458
+ var ROC_PERCENT = "ROC %";
26459
+ var ROC_MOMENTUM = "Momentum";
25674
26460
  var rocSpec = {
25675
26461
  type: "rate-of-change",
25676
26462
  title: "Rate of Change",
25677
26463
  shortTitle: "ROC",
25678
26464
  overlay: false,
25679
- inputs: [lengthInput(9), sourceInput(), colorInput()],
25680
- compute: (bars, inputs) => ({
25681
- plots: [
25682
- {
25683
- key: "roc",
25684
- title: "RoC",
25685
- values: roc(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 9)),
25686
- color: str3(inputs, "color", SERIES_LINE),
25687
- width: 2
25688
- }
25689
- ],
25690
- levels: ZERO_LEVEL
25691
- })
26465
+ inputs: [
26466
+ lengthInput(9, "length", "Length", 5e3, "Lookback period; the source is compared with its value this many bars ago."),
26467
+ sourceInput("Close", "source", "Source", "Series used as input for the calculation."),
26468
+ 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."),
26469
+ colorInput(NEUTRAL, "Line", "color", "Color of the line when the reading sits exactly at zero."),
26470
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the line and fill while the reading is above zero."),
26471
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line and fill while the reading is below zero."),
26472
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the line and zero, following the line color.")
26473
+ ],
26474
+ compute: (bars, inputs) => {
26475
+ const len = num3(inputs, "length", 9);
26476
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
26477
+ const values = str3(inputs, "mode", ROC_PERCENT) === ROC_MOMENTUM ? change(src, len) : roc(src, len);
26478
+ return centeredOscillator({
26479
+ key: "roc",
26480
+ title: "ROC",
26481
+ values,
26482
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
26483
+ zeroInk: NEUTRAL,
26484
+ fill: bool(inputs, "gradientFill", true)
26485
+ });
26486
+ }
25692
26487
  };
25693
26488
  var ultimate = {
25694
26489
  type: "ultimate-oscillator",
25695
26490
  title: "Ultimate Oscillator",
25696
26491
  shortTitle: "UO",
25697
26492
  overlay: false,
25698
- inputs: [lengthInput(7, "fastLength", "Fast length"), lengthInput(14, "middleLength", "Middle length"), lengthInput(28, "slowLength", "Slow length"), colorInput()],
26493
+ inputs: [
26494
+ lengthInput(7, "fastLength", "Fast Length", 5e3, "Bars in the fastest averaging window. Weighted 4 in the final blend, so it dominates the oscillator."),
26495
+ lengthInput(14, "middleLength", "Middle Length", 5e3, "Bars in the intermediate averaging window. Weighted 2 in the final blend."),
26496
+ lengthInput(28, "slowLength", "Slow Length", 5e3, "Bars in the slowest averaging window. Weighted 1 in the final blend."),
26497
+ ...thresholdInputs(70, 30, 0, 100, "Overbought", "Oversold"),
26498
+ colorInput(NEUTRAL, "Oscillator", "color", "Color of the line midway between the levels. Toward either level it blends into that level\u2019s color."),
26499
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought and oversold levels."),
26500
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line at the oversold level and of the oversold zone shading."),
26501
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line at the overbought level and of the overbought zone shading.")
26502
+ ],
25699
26503
  compute: (bars, inputs) => {
25700
26504
  const n = bars.length;
25701
26505
  const bp = new Array(n).fill(Number.NaN);
@@ -25703,10 +26507,9 @@ var ultimate = {
25703
26507
  for (let i = 0; i < n; i++) {
25704
26508
  const b = bars[i];
25705
26509
  const pc = i > 0 ? bars[i - 1].close : b.close;
25706
- const lo = Math.min(b.low, pc);
25707
- const hi = Math.max(b.high, pc);
25708
- bp[i] = b.close - lo;
25709
- tr[i] = hi - lo;
26510
+ const trueLow = Math.min(b.low, pc);
26511
+ bp[i] = b.close - trueLow;
26512
+ tr[i] = Math.max(b.high, pc) - trueLow;
25710
26513
  }
25711
26514
  const avg = (len) => zip(sum(bp, len), sum(tr, len), (a, b) => b === 0 ? Number.NaN : a / b);
25712
26515
  const a7 = avg(num3(inputs, "fastLength", 7));
@@ -25717,10 +26520,17 @@ var ultimate = {
25717
26520
  const z = a28[i];
25718
26521
  return Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) ? 100 * (4 * x + 2 * y + z) / 7 : Number.NaN;
25719
26522
  });
25720
- return {
25721
- plots: [{ key: "uo", title: "UO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25722
- levels: guideLevels(30, 70, 50)
25723
- };
26523
+ return boundedOscillator({
26524
+ key: "uo",
26525
+ title: "UO",
26526
+ values,
26527
+ scale: { top: 100, bottom: 0 },
26528
+ overbought: num3(inputs, "overbought", 70),
26529
+ oversold: num3(inputs, "oversold", 30),
26530
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
26531
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
26532
+ zoneTransparency: 91
26533
+ });
25724
26534
  }
25725
26535
  };
25726
26536
  var rvgi = {
@@ -25728,7 +26538,11 @@ var rvgi = {
25728
26538
  title: "Relative Vigor Index",
25729
26539
  shortTitle: "RVGI",
25730
26540
  overlay: false,
25731
- inputs: [lengthInput(10)],
26541
+ inputs: [
26542
+ lengthInput(10, "length", "Length", 5e3, "Number of bars summed for the smoothed numerator (close \u2212 open) and denominator (high \u2212 low)."),
26543
+ colorInput(BULLISH, "RVGI", "color", "Color of the RVGI line."),
26544
+ colorInput(BEARISH, "Signal", "signalColor", "Color of the signal line.")
26545
+ ],
25732
26546
  compute: (bars, inputs) => {
25733
26547
  const len = num3(inputs, "length", 10);
25734
26548
  const co = swma(sub(closes(bars), opens(bars)));
@@ -25736,31 +26550,49 @@ var rvgi = {
25736
26550
  const line = zip(sum(co, len), sum(hl, len), (a, b) => b === 0 ? 0 : a / b);
25737
26551
  return {
25738
26552
  plots: [
25739
- { key: "rvgi", title: "RVGI", values: line, color: SERIES_LINE, width: 2 },
25740
- { key: "signal", title: "Signal", values: swma(line), color: WARNING }
26553
+ { key: "rvgi", title: "RVGI", values: line, color: str3(inputs, "color", BULLISH) },
26554
+ { key: "signal", title: "Signal", values: swma(line), color: str3(inputs, "signalColor", BEARISH) }
25741
26555
  ],
25742
- levels: ZERO_LEVEL
26556
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed", title: "Zero" }]
25743
26557
  };
25744
26558
  }
25745
26559
  };
26560
+ var RVI_EMA = "EMA";
26561
+ var RVI_RMA = "Wilder (RMA)";
25746
26562
  var relVolatility = {
25747
26563
  type: "relative-volatility-index",
25748
26564
  title: "Relative Volatility Index",
25749
26565
  shortTitle: "RVI",
25750
26566
  overlay: false,
25751
- inputs: [lengthInput(10, "stdevLength", "StdDev length"), lengthInput(14, "smoothLength", "Smoothing length"), sourceInput(), colorInput()],
26567
+ inputs: [
26568
+ 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."),
26569
+ lengthInput(14, "smoothLength", "Smoothing Length", 5e3, "Number of bars used to smooth the up and down volatility streams."),
26570
+ 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."),
26571
+ ...thresholdInputs(80, 20),
26572
+ colorInput(NEUTRAL, "RVI Line", "color", "Color of the RVI line at the midline. Toward either threshold it blends into that threshold\u2019s color."),
26573
+ colorInput(BULLISH, "Up Volatility", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
26574
+ colorInput(BEARISH, "Down Volatility", "oversoldColor", "Color of the line and zone shading at and below the oversold level.")
26575
+ ],
25752
26576
  compute: (bars, inputs) => {
26577
+ const c = closes(bars);
26578
+ const sigma = stdev(c, num3(inputs, "stdevLength", 10));
26579
+ const d = change(c);
26580
+ const up = zip(sigma, d, (s, x) => x > 0 ? s : 0);
26581
+ const down = zip(sigma, d, (s, x) => x < 0 ? s : 0);
25753
26582
  const smoothLen = num3(inputs, "smoothLength", 14);
25754
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
25755
- const dev = stdev(src, num3(inputs, "stdevLength", 10));
25756
- const d = change(src);
25757
- const up = rma(zip(dev, d, (s, x) => x > 0 ? s : 0), smoothLen);
25758
- const dn = rma(zip(dev, d, (s, x) => x <= 0 ? s : 0), smoothLen);
25759
- const values = zip(up, dn, (u, w) => u + w === 0 ? 50 : 100 * u / (u + w));
25760
- return {
25761
- plots: [{ key: "rvi", title: "RVI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25762
- levels: guideLevels(20, 80, 50)
25763
- };
26583
+ const smooth = str3(inputs, "smoothType", RVI_EMA) === RVI_RMA ? rma : ema;
26584
+ const values = zip(smooth(up, smoothLen), smooth(down, smoothLen), (u, w) => u + w === 0 ? 0 : 100 * u / (u + w));
26585
+ return boundedOscillator({
26586
+ key: "rvi",
26587
+ title: "RVI",
26588
+ values,
26589
+ scale: { top: 100, bottom: 0 },
26590
+ overbought: num3(inputs, "overbought", 80),
26591
+ oversold: num3(inputs, "oversold", 20),
26592
+ ink: { low: str3(inputs, "oversoldColor", BEARISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BULLISH) },
26593
+ levelInk: NEUTRAL,
26594
+ width: 2
26595
+ });
25764
26596
  }
25765
26597
  };
25766
26598
  var bop = {
@@ -25768,28 +26600,43 @@ var bop = {
25768
26600
  title: "Balance of Power",
25769
26601
  shortTitle: "BOP",
25770
26602
  overlay: false,
25771
- inputs: [colorInput()],
26603
+ inputs: [
26604
+ boolInput("smoothing", "Smoothing", false, void 0, "Smooth the raw Balance of Power with a simple moving average instead of plotting the single-bar value."),
26605
+ lengthInput(14, "smoothLength", "Length", 5e3, "Simple moving average length applied when smoothing is enabled."),
26606
+ colorInput(NEUTRAL, "BOP Color", "color", "Color of the line at exactly zero."),
26607
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the Balance of Power is above zero \u2014 buyers in control of the bar."),
26608
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the Balance of Power is below zero \u2014 sellers in control of the bar.")
26609
+ ],
25772
26610
  compute: (bars, inputs) => {
25773
- const values = bars.map((b) => b.high === b.low ? 0 : (b.close - b.open) / (b.high - b.low));
25774
- return {
25775
- plots: [{ key: "bop", title: "BOP", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25776
- levels: ZERO_LEVEL
25777
- };
26611
+ const raw = bars.map((b) => b.high === b.low ? 0 : (b.close - b.open) / (b.high - b.low));
26612
+ const values = bool(inputs, "smoothing", false) ? sma(raw, num3(inputs, "smoothLength", 14)) : raw;
26613
+ return centeredOscillator({
26614
+ key: "bop",
26615
+ title: "BOP",
26616
+ values,
26617
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
26618
+ zeroInk: NEUTRAL,
26619
+ valueTransparency: 60
26620
+ });
25778
26621
  }
25779
26622
  };
25780
26623
  var elderRay = {
25781
26624
  type: "elder-ray",
25782
26625
  title: "Elder Ray",
25783
26626
  overlay: false,
25784
- inputs: [lengthInput(13)],
26627
+ inputs: [
26628
+ 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."),
26629
+ colorInput(BULLISH, "Bull Power", "bullColor", "Color of the Bull Power columns."),
26630
+ colorInput(BEARISH, "Bear Power", "bearColor", "Color of the Bear Power columns.")
26631
+ ],
25785
26632
  compute: (bars, inputs) => {
25786
26633
  const basis = ema(closes(bars), num3(inputs, "length", 13));
25787
26634
  return {
25788
26635
  plots: [
25789
- { key: "bull", title: "Bull power", values: sub(highs(bars), basis), kind: "histogram", color: BULLISH, base: 0 },
25790
- { key: "bear", title: "Bear power", values: sub(lows(bars), basis), kind: "histogram", color: BEARISH, base: 0 }
26636
+ { key: "bull", title: "Bull Power", values: sub(highs(bars), basis), kind: "columns", color: str3(inputs, "bullColor", BULLISH), base: 0 },
26637
+ { key: "bear", title: "Bear Power", values: sub(lows(bars), basis), kind: "columns", color: str3(inputs, "bearColor", BEARISH), base: 0 }
25791
26638
  ],
25792
- levels: ZERO_LEVEL
26639
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 30), lineStyle: "dashed", title: "Zero Line" }]
25793
26640
  };
25794
26641
  }
25795
26642
  };
@@ -25798,31 +26645,66 @@ var ttmSqueeze = {
25798
26645
  title: "TTM Squeeze",
25799
26646
  overlay: false,
25800
26647
  inputs: [
25801
- lengthInput(20),
25802
- { key: "bbMult", title: "Bollinger multiplier", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 },
25803
- { key: "kcMult", title: "Keltner multiplier", type: "float", defval: 1.5, min: 0.1, max: 50, step: 0.1 }
26648
+ { ...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 },
26649
+ sourceInput("Close", "source", "Source", "Price series used for the shared moving average basis, the standard deviation, and the momentum displacement."),
26650
+ floatInput("bbMult", "Bollinger Bands Multiplier", 2, 0.1, 50, 0.1, "Standard deviation multiplier for the Bollinger Bands."),
26651
+ 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."),
26652
+ colorInput(BULLISH, "Momentum Up", "bullColor", "Histogram color while momentum is positive. Bars building away from zero print solid, bars fading back print dimmed."),
26653
+ colorInput(BEARISH, "Momentum Down", "bearColor", "Histogram color while momentum is negative. Bars building away from zero print solid, bars fading back print dimmed."),
26654
+ colorInput(WARNING, "Squeeze On", "squeezeOnColor", "Zero-line dot color while both Bollinger Bands sit fully inside the Keltner Channels."),
26655
+ colorInput(NEUTRAL, "Squeeze Off", "squeezeOffColor", "Zero-line dot color while the Bollinger Bands trade outside the Keltner Channels."),
26656
+ 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.")
25804
26657
  ],
25805
26658
  compute: (bars, inputs) => {
25806
26659
  const len = num3(inputs, "length", 20);
25807
- const c = closes(bars);
25808
- const basis = sma(c, len);
25809
- const dev = map(stdev(c, len), (x) => x * num3(inputs, "bbMult", 2));
25810
- const kcRange = map(rma(trueRange(bars), len), (x) => x * num3(inputs, "kcMult", 1.5));
25811
- const donchianMid = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => (h + l) / 2);
25812
- const anchor = zip(donchianMid, basis, (d, b) => (d + b) / 2);
25813
- const mom = linreg(sub(c, anchor), len);
25814
- const squeezed = c.map((_, i) => {
26660
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
26661
+ const basis = sma(src, len);
26662
+ const dev = map(stdev(src, len), (x) => x * num3(inputs, "bbMult", 2));
26663
+ const kcRange = map(atr(bars, len), (x) => x * num3(inputs, "kcMult", 1.5));
26664
+ const rangeMid = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => (h + l) / 2);
26665
+ const anchor = zip(rangeMid, basis, (d, b) => (d + b) / 2);
26666
+ const mom = linreg(sub(src, anchor), len, 0);
26667
+ const squeezeOn = basis.map((b, i) => {
25815
26668
  const d = dev[i];
25816
26669
  const r = kcRange[i];
25817
- return Number.isFinite(d) && Number.isFinite(r) ? d < r : false;
26670
+ if (!Number.isFinite(b) || !Number.isFinite(d) || !Number.isFinite(r)) return null;
26671
+ return d < r;
25818
26672
  });
25819
- const stateColors = squeezed.map((s, i) => Number.isFinite(mom[i]) ? s ? BEARISH : BULLISH : null);
25820
- return {
25821
- plots: [
25822
- { key: "mom", title: "Momentum", values: mom, kind: "histogram", color: BULLISH, colors: histColors(mom), base: 0 },
25823
- { key: "squeeze", title: "Squeeze", values: mom.map((x) => Number.isFinite(x) ? 0 : Number.NaN), kind: "cross", color: INFO, colors: stateColors }
25824
- ]
25825
- };
26673
+ const bull = str3(inputs, "bullColor", BULLISH);
26674
+ const bear = str3(inputs, "bearColor", BEARISH);
26675
+ const onInk = str3(inputs, "squeezeOnColor", WARNING);
26676
+ const offInk = str3(inputs, "squeezeOffColor", NEUTRAL);
26677
+ const momColors = mom.map((x, i) => {
26678
+ if (!Number.isFinite(x)) return null;
26679
+ const prev2 = i > 0 && Number.isFinite(mom[i - 1]) ? mom[i - 1] : x;
26680
+ const rising = x > prev2;
26681
+ return x >= 0 ? rising ? bull : transp(bull, 60) : rising ? transp(bear, 60) : bear;
26682
+ });
26683
+ const plots = [
26684
+ { key: "mom", title: "Momentum", values: mom, kind: "columns", color: bull, colors: momColors, base: 0 },
26685
+ {
26686
+ key: "state",
26687
+ title: "Squeeze State",
26688
+ values: squeezeOn.map((s) => s == null ? Number.NaN : 0),
26689
+ kind: "circles",
26690
+ color: offInk,
26691
+ width: 2,
26692
+ colors: squeezeOn.map((s) => s == null ? null : s ? onInk : offInk)
26693
+ }
26694
+ ];
26695
+ if (bool(inputs, "markFires", true)) {
26696
+ const fires = squeezeOn.map((s, i) => s === false && squeezeOn[i - 1] === true ? 0 : Number.NaN);
26697
+ plots.push({
26698
+ key: "fire",
26699
+ title: "Squeeze Fire",
26700
+ values: fires,
26701
+ kind: "cross",
26702
+ color: offInk,
26703
+ width: 3,
26704
+ colors: fires.map((x, i) => Number.isFinite(x) ? Number.isFinite(mom[i]) ? mom[i] >= 0 ? bull : bear : offInk : null)
26705
+ });
26706
+ }
26707
+ return { plots };
25826
26708
  }
25827
26709
  };
25828
26710
  var oscillatorSpecs = [
@@ -25834,7 +26716,7 @@ var oscillatorSpecs = [
25834
26716
  awesome,
25835
26717
  cci,
25836
26718
  williamsR,
25837
- cmo,
26719
+ cmoSpec,
25838
26720
  connorsRsi,
25839
26721
  fisher,
25840
26722
  trix,
@@ -25858,15 +26740,19 @@ var aroon = {
25858
26740
  type: "aroon",
25859
26741
  title: "Aroon",
25860
26742
  overlay: false,
25861
- inputs: [lengthInput(14)],
26743
+ inputs: [
26744
+ lengthInput(14, "length", "Length", 5e3, "Lookback length used to locate the highest high and lowest low."),
26745
+ colorInput(BULLISH, "Aroon Up", "upColor", "Color of the Aroon Up line."),
26746
+ colorInput(BEARISH, "Aroon Down", "downColor", "Color of the Aroon Down line.")
26747
+ ],
25862
26748
  compute: (bars, inputs) => {
25863
26749
  const len = num3(inputs, "length", 14);
25864
- const up = map(barsSinceHighest(highs(bars), len + 1), (x) => 100 * (len - x) / len);
25865
- const down = map(barsSinceLowest(lows(bars), len + 1), (x) => 100 * (len - x) / len);
26750
+ const up = map(barsSinceHighest(highs(bars), len), (x) => 100 * (len - x) / len);
26751
+ const down = map(barsSinceLowest(lows(bars), len), (x) => 100 * (len - x) / len);
25866
26752
  return {
25867
26753
  plots: [
25868
- { key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
25869
- { key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
26754
+ { key: "up", title: "Aroon Up", values: up, color: str3(inputs, "upColor", BULLISH) },
26755
+ { key: "down", title: "Aroon Down", values: down, color: str3(inputs, "downColor", BEARISH) }
25870
26756
  ]
25871
26757
  };
25872
26758
  }
@@ -25876,10 +26762,16 @@ var adx = {
25876
26762
  title: "Average Directional Index",
25877
26763
  shortTitle: "ADX",
25878
26764
  overlay: false,
25879
- inputs: [lengthInput(14, "diLength", "DI length"), lengthInput(14, "adxLength", "ADX smoothing")],
26765
+ inputs: [
26766
+ lengthInput(14, "diLength", "DI Length", 5e3, "Wilder RMA smoothing length applied to directional movement and true range when computing +DI and \u2212DI."),
26767
+ lengthInput(14, "adxLength", "ADX Smoothing", 5e3, "Wilder RMA smoothing length applied to DX to obtain the ADX line."),
26768
+ floatInput("keyLevel", "Key Level", 25, 0, 100, 1, "Trend strength reference level; ADX above it is commonly read as a trending market."),
26769
+ colorInput(BULLISH, "+DI Color", "plusDiColor", "Color of the plus directional indicator, and of the ADX line above the key level while +DI leads."),
26770
+ colorInput(BEARISH, "-DI Color", "minusDiColor", "Color of the minus directional indicator, and of the ADX line above the key level while \u2212DI leads."),
26771
+ colorInput(NEUTRAL, "ADX Color", "adxColor", "Color of the ADX line while it sits below the key level \u2014 no trend strong enough to trade.")
26772
+ ],
25880
26773
  compute: (bars, inputs) => {
25881
26774
  const diLen = num3(inputs, "diLength", 14);
25882
- const adxLen = num3(inputs, "adxLength", 14);
25883
26775
  const n = bars.length;
25884
26776
  const plusDm = new Array(n).fill(Number.NaN);
25885
26777
  const minusDm = new Array(n).fill(Number.NaN);
@@ -25893,13 +26785,23 @@ var adx = {
25893
26785
  const plusDi = zip(rma(plusDm, diLen), atrLine, (d, a) => a === 0 ? 0 : 100 * d / a);
25894
26786
  const minusDi = zip(rma(minusDm, diLen), atrLine, (d, a) => a === 0 ? 0 : 100 * d / a);
25895
26787
  const dx = zip(plusDi, minusDi, (p, m) => p + m === 0 ? 0 : 100 * Math.abs(p - m) / (p + m));
26788
+ const adxLine = rma(dx, num3(inputs, "adxLength", 14));
26789
+ const plusInk = str3(inputs, "plusDiColor", BULLISH);
26790
+ const minusInk = str3(inputs, "minusDiColor", BEARISH);
26791
+ const neutralInk = str3(inputs, "adxColor", NEUTRAL);
26792
+ const keyLevel = num3(inputs, "keyLevel", 25);
26793
+ const adxColors = adxLine.map((x, i) => {
26794
+ if (!Number.isFinite(x)) return null;
26795
+ if (x <= keyLevel) return neutralInk;
26796
+ return (plusDi[i] ?? 0) > (minusDi[i] ?? 0) ? plusInk : minusInk;
26797
+ });
25896
26798
  return {
25897
26799
  plots: [
25898
- { key: "adx", title: "ADX", values: rma(dx, adxLen), color: SERIES_LINE, width: 2 },
25899
- { key: "plusDi", title: "+DI", values: plusDi, color: BULLISH },
25900
- { key: "minusDi", title: "-DI", values: minusDi, color: BEARISH }
26800
+ { key: "plusDi", title: "+DI", values: plusDi, color: transp(plusInk, 30) },
26801
+ { key: "minusDi", title: "-DI", values: minusDi, color: transp(minusInk, 30) },
26802
+ { key: "adx", title: "ADX", values: adxLine, color: neutralInk, width: 2, colors: adxColors }
25901
26803
  ],
25902
- levels: [{ key: "threshold", price: 25, color: NEUTRAL, lineStyle: "dotted" }]
26804
+ levels: [{ key: "keyLevel", price: keyLevel, color: NEUTRAL, lineStyle: "dashed", title: "Key Level" }]
25903
26805
  };
25904
26806
  }
25905
26807
  };
@@ -25908,7 +26810,12 @@ var vortex = {
25908
26810
  title: "Vortex Indicator",
25909
26811
  shortTitle: "VI",
25910
26812
  overlay: false,
25911
- inputs: [lengthInput(14)],
26813
+ inputs: [
26814
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to sum the vortex movements (VM+ and VM\u2212) and the true range."),
26815
+ colorInput(BULLISH, "VI+", "plusColor", "Color of the positive vortex line (VI+)."),
26816
+ colorInput(BEARISH, "VI-", "minusColor", "Color of the negative vortex line (VI\u2212)."),
26817
+ colorInput(NEUTRAL, "Baseline", "baselineColor", "Color of the dotted baseline drawn at 1.")
26818
+ ],
25912
26819
  compute: (bars, inputs) => {
25913
26820
  const len = num3(inputs, "length", 14);
25914
26821
  const n = bars.length;
@@ -25921,25 +26828,48 @@ var vortex = {
25921
26828
  const trSum = sum(trueRange(bars), len);
25922
26829
  return {
25923
26830
  plots: [
25924
- { key: "viPlus", title: "VI+", values: zip(sum(vmPlus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: BULLISH, width: 2 },
25925
- { key: "viMinus", title: "VI-", values: zip(sum(vmMinus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: BEARISH, width: 2 }
26831
+ { key: "viPlus", title: "VI+", values: zip(sum(vmPlus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: str3(inputs, "plusColor", BULLISH) },
26832
+ { key: "viMinus", title: "VI-", values: zip(sum(vmMinus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: str3(inputs, "minusColor", BEARISH) }
25926
26833
  ],
25927
- levels: [{ key: "one", price: 1, color: NEUTRAL, lineStyle: "dotted" }]
26834
+ levels: [{ key: "baseline", price: 1, color: str3(inputs, "baselineColor", NEUTRAL), lineStyle: "dotted", title: "Baseline" }]
25928
26835
  };
25929
26836
  }
25930
26837
  };
25931
26838
  var trendSpecs = [aroon, adx, vortex];
25932
26839
 
25933
26840
  // src/core/native-indicators/classics/volatility.ts
26841
+ function expansionColors(values, calm, expanding) {
26842
+ return values.map((x, i) => {
26843
+ if (!Number.isFinite(x)) return null;
26844
+ const prev2 = i > 0 && Number.isFinite(values[i - 1]) ? values[i - 1] : x;
26845
+ return x > prev2 ? expanding : calm;
26846
+ });
26847
+ }
26848
+ function washedToZero(key, title, values, colors, fallback, valueTransparency, width = 1) {
26849
+ return {
26850
+ plots: [{ key, title, values, color: fallback, width, colors }, anchorPlot("zeroAnchor", "Zero Anchor", values.length, 0)],
26851
+ bands: [{ key: "wash", from: key, to: "zeroAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, 0, (_x, i) => colors[i] ?? fallback, valueTransparency) }]
26852
+ };
26853
+ }
25934
26854
  var atrSpec = {
25935
26855
  type: "average-true-range",
25936
26856
  title: "Average True Range",
25937
26857
  shortTitle: "ATR",
25938
26858
  overlay: false,
25939
- inputs: [lengthInput(14), colorInput()],
25940
- compute: (bars, inputs) => ({
25941
- plots: [{ key: "atr", title: "ATR", values: atr(bars, num3(inputs, "length", 14)), color: str3(inputs, "color", SERIES_LINE), width: 2 }]
25942
- })
26859
+ inputs: [
26860
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to average the true range."),
26861
+ 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."),
26862
+ colorInput(NEUTRAL, "ATR Line", "color", "Color of the ATR line while volatility contracts or holds flat."),
26863
+ colorInput(WARNING, "Expanding", "expandColor", "Color of the ATR line while it rises \u2014 volatility expanding bar to bar.")
26864
+ ],
26865
+ compute: (bars, inputs) => {
26866
+ const len = num3(inputs, "length", 14);
26867
+ const tr = trueRange(bars);
26868
+ const kind = str3(inputs, "smoothing", "RMA");
26869
+ const values = kind === "SMA" ? sma(tr, len) : kind === "EMA" ? ema(tr, len) : kind === "WMA" ? wma(tr, len) : rma(tr, len);
26870
+ const calm = str3(inputs, "color", NEUTRAL);
26871
+ return washedToZero("atr", "ATR", values, expansionColors(values, calm, str3(inputs, "expandColor", WARNING)), calm, 75);
26872
+ }
25943
26873
  };
25944
26874
  var historicalVolatility = {
25945
26875
  type: "historical-volatility",
@@ -25947,14 +26877,23 @@ var historicalVolatility = {
25947
26877
  shortTitle: "HV",
25948
26878
  overlay: false,
25949
26879
  inputs: [
25950
- lengthInput(10),
25951
- { 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)" },
25952
- colorInput()
26880
+ { ...lengthInput(10, "length", "Length", 5e3, "Lookback window in bars for the standard deviation of close-to-close log returns."), min: 2 },
26881
+ 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."),
26882
+ 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."),
26883
+ colorInput(WARNING, "Volatility Line", "color", "Color of the line at its recent highs; it fades toward grey as volatility contracts.")
25953
26884
  ],
25954
26885
  compute: (bars, inputs) => {
25955
- const logReturns = map(change(map(closes(bars), Math.log)), (x) => x);
25956
- const values = map(stdev(logReturns, num3(inputs, "length", 10)), (x) => x * Math.sqrt(num3(inputs, "annual", 365)) * 100);
25957
- return { plots: [{ key: "hv", title: "HV", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
26886
+ const len = num3(inputs, "length", 10);
26887
+ const logReturns = sub(map(closes(bars), Math.log), shift(map(closes(bars), Math.log), 1));
26888
+ const meanReturn = map(sum(logReturns, len), (s) => s / len);
26889
+ const squaredSum = sum(map(logReturns, (r) => r * r), len);
26890
+ const factor = bool(inputs, "annualize", true) ? Math.sqrt(num3(inputs, "periodsPerYear", 252)) : 1;
26891
+ const values = zip(squaredSum, meanReturn, (sq, m) => Math.sqrt(Math.max(sq - len * m * m, 0) / (len - 1)) * factor * 100);
26892
+ const low = lowest(values, 100);
26893
+ const high = highest(values, 100);
26894
+ const accent = str3(inputs, "color", WARNING);
26895
+ const colors = values.map((x, i) => Number.isFinite(x) ? gradient(x, low[i] ?? 0, high[i] ?? 0, NEUTRAL, accent) : null);
26896
+ return washedToZero("hv", "Historical Volatility", values, colors, accent, 75, 2);
25958
26897
  }
25959
26898
  };
25960
26899
  var chaikinVolatility = {
@@ -25962,13 +26901,26 @@ var chaikinVolatility = {
25962
26901
  title: "Chaikin Volatility",
25963
26902
  shortTitle: "CHV",
25964
26903
  overlay: false,
25965
- inputs: [lengthInput(10, "emaLength", "EMA length"), lengthInput(10, "rocLength", "RoC length"), colorInput()],
26904
+ inputs: [
26905
+ lengthInput(10, "emaLength", "EMA Length", 5e3, "Length of the exponential moving average applied to the bar high-low range."),
26906
+ lengthInput(10, "rocLength", "ROC Length", 5e3, "Number of bars used for the rate of change of the smoothed high-low range."),
26907
+ colorInput(NEUTRAL, "CHV Color", "color", "Color of the Chaikin Volatility line."),
26908
+ 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.")
26909
+ ],
25966
26910
  compute: (bars, inputs) => {
25967
26911
  const range = ema(sub(highs(bars), lows(bars)), num3(inputs, "emaLength", 10));
25968
- return {
25969
- plots: [{ key: "cv", title: "Chaikin Vol", values: roc(range, num3(inputs, "rocLength", 10)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25970
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
25971
- };
26912
+ const prior = shift(range, num3(inputs, "rocLength", 10));
26913
+ const values = zip(range, prior, (r, p) => p === 0 ? Number.NaN : 100 * (r - p) / p);
26914
+ const calm = str3(inputs, "color", NEUTRAL);
26915
+ const expansion = str3(inputs, "expansionColor", WARNING);
26916
+ return centeredOscillator({
26917
+ key: "chv",
26918
+ title: "Chaikin Volatility",
26919
+ values,
26920
+ // Only expansion is a state worth accenting; contraction stays structural grey.
26921
+ ink: { bull: expansion, bear: calm, neutral: calm },
26922
+ zeroInk: NEUTRAL
26923
+ });
25972
26924
  }
25973
26925
  };
25974
26926
  var massIndex = {
@@ -25976,14 +26928,42 @@ var massIndex = {
25976
26928
  title: "Mass Index",
25977
26929
  shortTitle: "MI",
25978
26930
  overlay: false,
25979
- inputs: [lengthInput(25), colorInput()],
26931
+ inputs: [
26932
+ lengthInput(25, "length", "Sum Length", 5e3, "Number of bars over which the single/double EMA ratio is summed."),
26933
+ lengthInput(9, "emaLength", "EMA Length", 5e3, "Length of the single EMA of the high-low range and of its double smoothing."),
26934
+ floatInput("setupLevel", "Setup Level", 27, 0, 1e3, 0.1, "Reversal bulge set-up level; a rise above it arms the pattern."),
26935
+ 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."),
26936
+ colorInput(NEUTRAL, "Mass Index", "color", "Line color while no reversal bulge is armed."),
26937
+ colorInput(WARNING, "Armed", "armedColor", "Line color from the rise above the setup level until the decline below the trigger level."),
26938
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Reversal bulge levels color.")
26939
+ ],
25980
26940
  compute: (bars, inputs) => {
25981
- const range = sub(highs(bars), lows(bars));
25982
- const single = ema(range, 9);
25983
- const ratio = zip(single, ema(single, 9), (a, b) => b === 0 ? Number.NaN : a / b);
26941
+ const emaLen = num3(inputs, "emaLength", 9);
26942
+ const single = ema(sub(highs(bars), lows(bars)), emaLen);
26943
+ const ratio = zip(single, ema(single, emaLen), (a, b) => b === 0 ? 1 : a / b);
26944
+ const values = sum(ratio, num3(inputs, "length", 25));
26945
+ const setup = num3(inputs, "setupLevel", 27);
26946
+ const trigger = num3(inputs, "triggerLevel", 26.5);
26947
+ const calm = str3(inputs, "color", NEUTRAL);
26948
+ const armedInk = str3(inputs, "armedColor", WARNING);
26949
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
26950
+ let armed = false;
26951
+ const colors = values.map((x) => {
26952
+ if (!Number.isFinite(x)) return null;
26953
+ armed = x > setup ? true : x < trigger ? false : armed;
26954
+ return armed ? armedInk : calm;
26955
+ });
25984
26956
  return {
25985
- plots: [{ key: "mi", title: "Mass Index", values: sum(ratio, num3(inputs, "length", 25)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25986
- levels: [{ key: "reversal", price: 27, color: NEUTRAL, lineStyle: "dotted" }]
26957
+ plots: [
26958
+ { key: "mi", title: "Mass Index", values, color: calm, colors },
26959
+ anchorPlot("setupAnchor", "Setup Anchor", values.length, setup),
26960
+ anchorPlot("triggerAnchor", "Trigger Anchor", values.length, trigger)
26961
+ ],
26962
+ bands: [{ key: "bulge", from: "setupAnchor", to: "triggerAnchor", color: transp(armedInk, 90) }],
26963
+ levels: [
26964
+ { key: "setup", price: setup, color: levelInk, lineStyle: "dashed", title: "Setup Level" },
26965
+ { key: "trigger", price: trigger, color: levelInk, lineStyle: "dashed", title: "Trigger Level" }
26966
+ ]
25987
26967
  };
25988
26968
  }
25989
26969
  };
@@ -25992,30 +26972,83 @@ var standardDeviation = {
25992
26972
  title: "Standard Deviation",
25993
26973
  shortTitle: "StdDev",
25994
26974
  overlay: false,
25995
- inputs: [lengthInput(20), sourceInput(), colorInput()],
25996
- compute: (bars, inputs) => ({
25997
- plots: [
25998
- {
25999
- key: "stdev",
26000
- title: "StdDev",
26001
- values: stdev(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20)),
26002
- color: str3(inputs, "color", SERIES_LINE),
26003
- width: 2
26004
- }
26005
- ]
26006
- })
26975
+ inputs: [
26976
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used to compute the standard deviation."),
26977
+ sourceInput("Close", "source", "Source", "Price series the standard deviation is calculated on."),
26978
+ colorInput(NEUTRAL, "Standard Deviation", "color", "Line color while volatility contracts or holds flat."),
26979
+ colorInput(WARNING, "Expanding", "expandColor", "Line and fill color while volatility expands bar to bar.")
26980
+ ],
26981
+ compute: (bars, inputs) => {
26982
+ const values = stdev(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20));
26983
+ const calm = str3(inputs, "color", NEUTRAL);
26984
+ return washedToZero("stdev", "Standard Deviation", values, expansionColors(values, calm, str3(inputs, "expandColor", WARNING)), calm, 80);
26985
+ }
26007
26986
  };
26008
26987
  var ulcerIndex = {
26009
26988
  type: "ulcer-index",
26010
26989
  title: "Ulcer Index",
26011
26990
  overlay: false,
26012
- inputs: [lengthInput(14), colorInput()],
26991
+ inputs: [
26992
+ { ...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 },
26993
+ sourceInput("Close", "source", "Source", "Price series the Ulcer Index is computed on. Close is the standard choice."),
26994
+ 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."),
26995
+ colorInput(NEUTRAL, "Ulcer Index", "color", "Color of the index near zero; it blends toward the stress color as the reading approaches the threshold."),
26996
+ colorInput(BEARISH, "Stress", "stressColor", "Color of the index at and beyond the alert threshold."),
26997
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Vertical gradient between zero and the Ulcer Index, fully transparent at zero."),
26998
+ 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."),
26999
+ colorInput(transp(BEARISH, 50), "Drawdown Color", "drawdownColor", "Color of the drawdown columns."),
27000
+ colorInput(NEUTRAL, "Threshold", "thresholdColor", "Color of the alert threshold line.")
27001
+ ],
26013
27002
  compute: (bars, inputs) => {
26014
27003
  const len = num3(inputs, "length", 14);
26015
- const c = closes(bars);
26016
- const drawdown = zip(c, highest(c, len), (x, h) => h === 0 ? 0 : 100 * (x - h) / h);
26017
- const values = map(sma(map(drawdown, (d) => d * d), len), Math.sqrt);
26018
- return { plots: [{ key: "ui", title: "Ulcer", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
27004
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
27005
+ const n = src.length;
27006
+ const values = new Array(n).fill(Number.NaN);
27007
+ const drawdown = new Array(n).fill(Number.NaN);
27008
+ for (let i = len - 1; i < n; i++) {
27009
+ let runMax = Number.NaN;
27010
+ let sumSq = 0;
27011
+ let valid = true;
27012
+ let last = Number.NaN;
27013
+ for (let k = i - len + 1; k <= i; k++) {
27014
+ const price = src[k];
27015
+ if (!Number.isFinite(price)) {
27016
+ valid = false;
27017
+ break;
27018
+ }
27019
+ runMax = Number.isFinite(runMax) ? Math.max(runMax, price) : price;
27020
+ if (runMax <= 0) {
27021
+ valid = false;
27022
+ break;
27023
+ }
27024
+ last = 100 * (price - runMax) / runMax;
27025
+ sumSq += last * last;
27026
+ }
27027
+ if (!valid) continue;
27028
+ values[i] = Math.sqrt(sumSq / len);
27029
+ drawdown[i] = last;
27030
+ }
27031
+ const calm = str3(inputs, "color", NEUTRAL);
27032
+ const stress = str3(inputs, "stressColor", BEARISH);
27033
+ const threshold = num3(inputs, "threshold", 5);
27034
+ const colors = values.map((x) => Number.isFinite(x) ? threshold > 0 ? gradient(x, 0, threshold, calm, stress) : stress : null);
27035
+ const plots = [{ key: "ui", title: "Ulcer Index", values, color: calm, colors }];
27036
+ const bands = [];
27037
+ if (bool(inputs, "gradientFill", true)) {
27038
+ plots.push(anchorPlot("zeroAnchor", "Zero Base", n, 0));
27039
+ bands.push({ key: "wash", from: "ui", to: "zeroAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, 0, (_x, i) => colors[i] ?? calm) });
27040
+ }
27041
+ if (bool(inputs, "showDrawdown", false)) {
27042
+ plots.push({ key: "dd", title: "Drawdown %", values: drawdown, kind: "columns", color: str3(inputs, "drawdownColor", transp(BEARISH, 50)), base: 0 });
27043
+ }
27044
+ return {
27045
+ plots,
27046
+ bands,
27047
+ levels: [
27048
+ { key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dotted", title: "Zero Level" },
27049
+ { key: "threshold", price: threshold, color: str3(inputs, "thresholdColor", NEUTRAL), lineStyle: "dashed", title: "Alert Threshold" }
27050
+ ]
27051
+ };
26019
27052
  }
26020
27053
  };
26021
27054
  var choppiness = {
@@ -26023,19 +27056,33 @@ var choppiness = {
26023
27056
  title: "Choppiness Index",
26024
27057
  shortTitle: "CHOP",
26025
27058
  overlay: false,
26026
- inputs: [lengthInput(14), colorInput()],
27059
+ inputs: [
27060
+ { ...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 },
27061
+ floatInput("upperBand", "Upper Band", 61.8, 0, 100, 0.1, "Readings above this level indicate a choppy, range-bound market."),
27062
+ floatInput("lowerBand", "Lower Band", 38.2, 0, 100, 0.1, "Readings below this level indicate a strong trend."),
27063
+ colorInput(NEUTRAL, "CHOP Line", "color", "Color of the Choppiness Index line."),
27064
+ colorInput(BULLISH, "Trending", "trendingColor", "Line, fill and zone color toward and below the lower band \u2014 a trending market."),
27065
+ colorInput(BEARISH, "Choppy", "choppyColor", "Line, fill and zone color toward and above the upper band \u2014 a choppy, range-bound market."),
27066
+ colorInput(transp(NEUTRAL, 90), "Band Fill", "fillColor", "Fill color of the zone between the upper and lower bands.")
27067
+ ],
26027
27068
  compute: (bars, inputs) => {
26028
27069
  const len = num3(inputs, "length", 14);
26029
27070
  const trSum = sum(trueRange(bars), len);
26030
27071
  const span = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => h - l);
26031
27072
  const values = zip(trSum, span, (t, s) => s > 0 && t > 0 ? 100 * Math.log10(t / s) / Math.log10(len) : Number.NaN);
26032
- return {
26033
- plots: [{ key: "chop", title: "CHOP", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
26034
- levels: [
26035
- { key: "upper", price: 61.8, color: NEUTRAL },
26036
- { key: "lower", price: 38.2, color: NEUTRAL }
26037
- ]
26038
- };
27073
+ return boundedOscillator({
27074
+ key: "chop",
27075
+ title: "CHOP",
27076
+ values,
27077
+ scale: { top: 100, bottom: 0 },
27078
+ overbought: num3(inputs, "upperBand", 61.8),
27079
+ oversold: num3(inputs, "lowerBand", 38.2),
27080
+ ink: { low: str3(inputs, "trendingColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "choppyColor", BEARISH) },
27081
+ levelInk: NEUTRAL,
27082
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 90)),
27083
+ midpointWash: true,
27084
+ levelTitles: { overbought: "Upper Band", oversold: "Lower Band" }
27085
+ });
26039
27086
  }
26040
27087
  };
26041
27088
  var volatilitySpecs = [atrSpec, historicalVolatility, chaikinVolatility, massIndex, standardDeviation, ulcerIndex, choppiness];
@@ -26048,17 +27095,85 @@ function moneyFlowVolume(bars) {
26048
27095
  return (2 * b.close - b.high - b.low) / (b.high - b.low) * b.volume;
26049
27096
  });
26050
27097
  }
27098
+ function slopeColors2(values, bull, bear, flat) {
27099
+ return values.map((x, i) => {
27100
+ if (!Number.isFinite(x)) return null;
27101
+ const prev2 = i > 0 ? values[i - 1] : Number.NaN;
27102
+ if (!Number.isFinite(prev2)) return flat;
27103
+ return x > prev2 ? bull : x < prev2 ? bear : flat;
27104
+ });
27105
+ }
27106
+ function volumeIndex(bars, src, base, when) {
27107
+ const out = new Array(bars.length).fill(Number.NaN);
27108
+ let index = base;
27109
+ for (let i = 0; i < bars.length; i++) {
27110
+ const v = bars[i].volume;
27111
+ const pv = i > 0 ? bars[i - 1].volume : null;
27112
+ const prev2 = i > 0 ? src[i - 1] : Number.NaN;
27113
+ const active = v != null && pv != null && Number.isFinite(v) && Number.isFinite(pv) && when(v, pv);
27114
+ if (active && Number.isFinite(prev2) && prev2 !== 0) index *= 1 + (src[i] - prev2) / prev2;
27115
+ out[i] = index;
27116
+ }
27117
+ return out;
27118
+ }
27119
+ function regimeIndex(key, title, values, signal, ink, showSignal, gradientFill, singleColor = false) {
27120
+ const inkOf = (i) => {
27121
+ const s = signal[i];
27122
+ const x = values[i];
27123
+ if (singleColor || !Number.isFinite(s)) return ink.neutral;
27124
+ return x > s ? ink.bull : x < s ? ink.bear : ink.neutral;
27125
+ };
27126
+ const plots = [
27127
+ {
27128
+ key: "signal",
27129
+ title: "Signal EMA",
27130
+ values: signal,
27131
+ color: ink.signal,
27132
+ ...showSignal ? {} : { display: { pane: false } }
27133
+ },
27134
+ { key, title, values, color: ink.neutral, width: 2, colors: values.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) }
27135
+ ];
27136
+ const bands = gradientFill ? [
27137
+ {
27138
+ key: "regime",
27139
+ from: key,
27140
+ to: "signal",
27141
+ color: transp(NEUTRAL, 100),
27142
+ gradient: values.map((x, i) => {
27143
+ const s = signal[i];
27144
+ if (!Number.isFinite(x) || !Number.isFinite(s)) return null;
27145
+ const above = x > s;
27146
+ const c = above ? ink.bull : ink.bear;
27147
+ return { topValue: Math.max(x, s), bottomValue: Math.min(x, s), topColor: transp(c, above ? 50 : 100), bottomColor: transp(c, above ? 100 : 50) };
27148
+ })
27149
+ }
27150
+ ] : [];
27151
+ return { plots, bands };
27152
+ }
26051
27153
  var obv = {
26052
27154
  type: "on-balance-volume",
26053
27155
  title: "On Balance Volume",
26054
27156
  shortTitle: "OBV",
26055
27157
  overlay: false,
26056
- inputs: [colorInput()],
27158
+ inputs: [
27159
+ boolInput("smoothing", "Smoothing MA", false, SETTINGS, "Display a moving average of OBV to act as a smoothing/signal line."),
27160
+ optionInput("maType", "MA Type", "SMA", ["SMA", "EMA", "WMA", "RMA (SMMA)"], "Moving average type used for the smoothing MA."),
27161
+ lengthInput(20, "maLength", "MA Length", 5e3, "Number of bars used for the smoothing MA."),
27162
+ colorInput(INFO, "OBV", "color", "Color of the OBV line."),
27163
+ colorInput(WARNING, "Smoothing MA", "maColor", "Color of the smoothing MA line.")
27164
+ ],
26057
27165
  compute: (bars, inputs) => {
26058
27166
  const vol = volumes(bars);
26059
27167
  const d = change(closes(bars));
26060
- const signed2 = zip(vol, d, (v, x) => x > 0 ? v : x < 0 ? -v : 0);
26061
- return { plots: [{ key: "obv", title: "OBV", values: cumSum(signed2), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
27168
+ const values = cumSum(zip(vol, d, (v, x) => x > 0 ? v : x < 0 ? -v : 0));
27169
+ const plots = [{ key: "obv", title: "OBV", values, color: str3(inputs, "color", INFO) }];
27170
+ if (bool(inputs, "smoothing", false)) {
27171
+ const len = num3(inputs, "maLength", 20);
27172
+ const kind = str3(inputs, "maType", "SMA");
27173
+ const ma = kind === "EMA" ? ema(values, len) : kind === "WMA" ? wma(values, len) : kind === "RMA (SMMA)" ? rma(values, len) : sma(values, len);
27174
+ plots.push({ key: "ma", title: "Smoothing MA", values: ma, color: str3(inputs, "maColor", WARNING) });
27175
+ }
27176
+ return { plots };
26062
27177
  }
26063
27178
  };
26064
27179
  var accDist = {
@@ -26066,24 +27181,50 @@ var accDist = {
26066
27181
  title: "Accumulation / Distribution",
26067
27182
  shortTitle: "A/D",
26068
27183
  overlay: false,
26069
- inputs: [colorInput()],
26070
- compute: (bars, inputs) => ({
26071
- plots: [{ key: "ad", title: "A/D", values: cumSum(moneyFlowVolume(bars)), color: str3(inputs, "color", SERIES_LINE), width: 2 }]
26072
- })
27184
+ inputs: [
27185
+ boolInput("slopeColoring", "Slope Coloring", true, STYLE, "Color the line by its slope \u2014 bullish while rising (accumulation), bearish while falling (distribution)."),
27186
+ colorInput(NEUTRAL, "A/D Line", "color", "Color of the line when slope coloring is disabled or the line is flat."),
27187
+ colorInput(BULLISH, "Rising", "bullColor", "Line color while the A/D Line rises."),
27188
+ colorInput(BEARISH, "Falling", "bearColor", "Line color while the A/D Line falls.")
27189
+ ],
27190
+ compute: (bars, inputs) => {
27191
+ const values = cumSum(moneyFlowVolume(bars));
27192
+ const flat = str3(inputs, "color", NEUTRAL);
27193
+ return {
27194
+ plots: [
27195
+ {
27196
+ key: "ad",
27197
+ title: "A/D",
27198
+ values,
27199
+ color: flat,
27200
+ ...bool(inputs, "slopeColoring", true) ? { colors: slopeColors2(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), flat) } : {}
27201
+ }
27202
+ ]
27203
+ };
27204
+ }
26073
27205
  };
26074
27206
  var cmf = {
26075
27207
  type: "chaikin-money-flow",
26076
27208
  title: "Chaikin Money Flow",
26077
27209
  shortTitle: "CMF",
26078
27210
  overlay: false,
26079
- inputs: [lengthInput(20), colorInput()],
27211
+ inputs: [
27212
+ lengthInput(20, "length", "Length", 5e3, "Lookback length over which money flow volume and volume are summed."),
27213
+ colorInput(NEUTRAL, "CMF Line", "color", "Color of the Chaikin Money Flow line."),
27214
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the CMF reads above zero (net money flowing in)."),
27215
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the CMF reads below zero (net money flowing out)."),
27216
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
27217
+ ],
26080
27218
  compute: (bars, inputs) => {
26081
27219
  const len = num3(inputs, "length", 20);
26082
27220
  const values = zip(sum(moneyFlowVolume(bars), len), sum(volumes(bars), len), (m, v) => v === 0 ? 0 : m / v);
26083
- return {
26084
- plots: [{ key: "cmf", title: "CMF", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
26085
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26086
- };
27221
+ return centeredOscillator({
27222
+ key: "cmf",
27223
+ title: "CMF",
27224
+ values,
27225
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
27226
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
27227
+ });
26087
27228
  }
26088
27229
  };
26089
27230
  var chaikinOscillator = {
@@ -26091,14 +27232,23 @@ var chaikinOscillator = {
26091
27232
  title: "Chaikin Oscillator",
26092
27233
  shortTitle: "Chaikin Osc",
26093
27234
  overlay: false,
26094
- inputs: [lengthInput(3, "fastLength", "Fast length"), lengthInput(10, "slowLength", "Slow length"), colorInput()],
27235
+ inputs: [
27236
+ lengthInput(3, "fastLength", "Fast Length", 5e3, "Length of the fast EMA applied to the accumulation/distribution line."),
27237
+ lengthInput(10, "slowLength", "Slow Length", 5e3, "Length of the slow EMA applied to the accumulation/distribution line."),
27238
+ colorInput(NEUTRAL, "Oscillator", "color", "Color of the Chaikin Oscillator line."),
27239
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the oscillator reads above zero (accumulation)."),
27240
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the oscillator reads below zero (distribution).")
27241
+ ],
26095
27242
  compute: (bars, inputs) => {
26096
27243
  const ad = cumSum(moneyFlowVolume(bars));
26097
27244
  const values = sub(ema(ad, num3(inputs, "fastLength", 3)), ema(ad, num3(inputs, "slowLength", 10)));
26098
- return {
26099
- plots: [{ key: "co", title: "Chaikin Osc", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
26100
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26101
- };
27245
+ return centeredOscillator({
27246
+ key: "co",
27247
+ title: "Chaikin Oscillator",
27248
+ values,
27249
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
27250
+ zeroInk: NEUTRAL
27251
+ });
26102
27252
  }
26103
27253
  };
26104
27254
  var eom = {
@@ -26106,20 +27256,28 @@ var eom = {
26106
27256
  title: "Ease of Movement",
26107
27257
  shortTitle: "EOM",
26108
27258
  overlay: false,
26109
- inputs: [lengthInput(14), { key: "divisor", title: "Divisor", type: "int", defval: 1e4, min: 1, max: 1e9, step: 1 }, colorInput()],
27259
+ inputs: [
27260
+ lengthInput(14, "length", "Length", 5e3, "Number of bars in the simple moving average smoothing the 1-bar Ease of Movement values."),
27261
+ intInput("divisor", "Divisor", 1e4, 1, 1e9, "Volume scaling constant of the box ratio; larger values scale the oscillator up. 10000 is the conventional default."),
27262
+ colorInput(NEUTRAL, "EOM Line", "color", "Color of the line when it sits exactly at zero."),
27263
+ colorInput(BULLISH, "Above Zero", "bullColor", "Color of the line and fill while Ease of Movement is positive."),
27264
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while Ease of Movement is negative.")
27265
+ ],
26110
27266
  compute: (bars, inputs) => {
26111
27267
  const div = num3(inputs, "divisor", 1e4);
26112
- const hl2 = sourceValues(bars, "HL2");
26113
- const move = change(hl2);
27268
+ const move = change(sourceValues(bars, "HL2"));
26114
27269
  const raw = bars.map((b, i) => {
26115
27270
  const m = move[i];
26116
- if (b.volume == null || !Number.isFinite(b.volume) || b.volume === 0 || !Number.isFinite(m)) return Number.NaN;
26117
- return div * m * (b.high - b.low) / b.volume;
27271
+ if (b.volume == null || !Number.isFinite(b.volume) || !Number.isFinite(m)) return Number.NaN;
27272
+ return b.volume === 0 ? 0 : div * m * (b.high - b.low) / b.volume;
27273
+ });
27274
+ return centeredOscillator({
27275
+ key: "eom",
27276
+ title: "EOM",
27277
+ values: sma(raw, num3(inputs, "length", 14)),
27278
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
27279
+ zeroInk: NEUTRAL
26118
27280
  });
26119
- return {
26120
- plots: [{ key: "eom", title: "EOM", values: sma(raw, num3(inputs, "length", 14)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
26121
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26122
- };
26123
27281
  }
26124
27282
  };
26125
27283
  var forceIndex = {
@@ -26127,13 +27285,24 @@ var forceIndex = {
26127
27285
  title: "Force Index",
26128
27286
  shortTitle: "FI",
26129
27287
  overlay: false,
26130
- inputs: [lengthInput(13), colorInput()],
27288
+ inputs: [
27289
+ 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."),
27290
+ colorInput(BULLISH, "Force Index", "color", "Color of the line while above zero (buying pressure)."),
27291
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line while below zero (selling pressure).")
27292
+ ],
26131
27293
  compute: (bars, inputs) => {
26132
27294
  const raw = zip(change(closes(bars)), volumes(bars), (d, v) => d * v);
26133
- return {
26134
- plots: [{ key: "force", title: "Force", values: ema(raw, num3(inputs, "length", 13)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
26135
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26136
- };
27295
+ const bull = str3(inputs, "color", BULLISH);
27296
+ return centeredOscillator({
27297
+ key: "force",
27298
+ title: "Force Index",
27299
+ values: ema(raw, num3(inputs, "length", 13)),
27300
+ // The reference reads an exact zero as buying pressure, so neutral shares the bull ink.
27301
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
27302
+ zeroInk: NEUTRAL,
27303
+ width: 2,
27304
+ valueTransparency: 60
27305
+ });
26137
27306
  }
26138
27307
  };
26139
27308
  var klinger = {
@@ -26141,19 +27310,44 @@ var klinger = {
26141
27310
  title: "Klinger Oscillator",
26142
27311
  shortTitle: "KVO",
26143
27312
  overlay: false,
26144
- inputs: [lengthInput(34, "fastLength", "Fast length"), lengthInput(55, "slowLength", "Slow length"), lengthInput(13, "signalLength", "Signal length")],
27313
+ inputs: [
27314
+ lengthInput(34, "fastLength", "Fast Length", 5e3, "Length of the fast EMA applied to the volume force."),
27315
+ lengthInput(55, "slowLength", "Slow Length", 5e3, "Length of the slow EMA applied to the volume force."),
27316
+ lengthInput(13, "signalLength", "Signal Length", 500, "Length of the EMA applied to the oscillator to obtain the signal line."),
27317
+ colorInput(BULLISH, "Bullish", "color", "Color of the Klinger oscillator while above zero."),
27318
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the Klinger oscillator while below zero."),
27319
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line.")
27320
+ ],
26145
27321
  compute: (bars, inputs) => {
26146
- const hlc3 = sourceValues(bars, "HLC3");
26147
- const trend = change(hlc3);
26148
- const signedVolume = zip(volumes(bars), trend, (v, t) => t >= 0 ? v : -v);
26149
- const line = sub(ema(signedVolume, num3(inputs, "fastLength", 34)), ema(signedVolume, num3(inputs, "slowLength", 55)));
26150
- return {
26151
- plots: [
26152
- { key: "kvo", title: "KVO", values: line, color: SERIES_LINE, width: 2 },
26153
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: WARNING }
26154
- ],
26155
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26156
- };
27322
+ const n = bars.length;
27323
+ const force = new Array(n).fill(Number.NaN);
27324
+ let trend = 1;
27325
+ let prevTrend = 1;
27326
+ let cm = 0;
27327
+ let prevDm = 0;
27328
+ for (let i = 0; i < n; i++) {
27329
+ const b = bars[i];
27330
+ const hlc = b.high + b.low + b.close;
27331
+ const prevHlc = i > 0 ? bars[i - 1].high + bars[i - 1].low + bars[i - 1].close : hlc;
27332
+ trend = hlc > prevHlc ? 1 : -1;
27333
+ const dm = b.high - b.low;
27334
+ cm = i > 0 && trend === prevTrend ? cm + dm : prevDm + dm;
27335
+ const v = b.volume;
27336
+ force[i] = v == null || !Number.isFinite(v) ? Number.NaN : cm === 0 ? 0 : v * Math.abs(2 * (dm / cm) - 1) * trend * 100;
27337
+ prevTrend = trend;
27338
+ prevDm = dm;
27339
+ }
27340
+ const line = sub(ema(force, num3(inputs, "fastLength", 34)), ema(force, num3(inputs, "slowLength", 55)));
27341
+ const bull = str3(inputs, "color", BULLISH);
27342
+ return centeredOscillator({
27343
+ key: "kvo",
27344
+ title: "KVO",
27345
+ values: line,
27346
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
27347
+ zeroInk: transp(NEUTRAL, 50),
27348
+ valueTransparency: 60,
27349
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: str3(inputs, "signalColor", WARNING) }]
27350
+ });
26157
27351
  }
26158
27352
  };
26159
27353
  var mfi = {
@@ -26161,7 +27355,15 @@ var mfi = {
26161
27355
  title: "Money Flow Index",
26162
27356
  shortTitle: "MFI",
26163
27357
  overlay: false,
26164
- inputs: [lengthInput(14), colorInput()],
27358
+ inputs: [
27359
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to sum positive and negative money flow."),
27360
+ floatInput("overbought", "Overbought Level", 80, 0, 100, 1, "Level above which the MFI is considered overbought."),
27361
+ floatInput("oversold", "Oversold Level", 20, 0, 100, 1, "Level below which the MFI is considered oversold."),
27362
+ colorInput(NEUTRAL, "MFI", "color", "Color of the MFI line between the levels. Toward either level it blends into that level\u2019s color."),
27363
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
27364
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level."),
27365
+ colorInput(transp(NEUTRAL, 94), "OB/OS Zone Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
27366
+ ],
26165
27367
  compute: (bars, inputs) => {
26166
27368
  const len = num3(inputs, "length", 14);
26167
27369
  const tp = sourceValues(bars, "HLC3");
@@ -26169,48 +27371,57 @@ var mfi = {
26169
27371
  const d = change(tp);
26170
27372
  const pos = sum(zip(rawFlow, d, (f, x) => x > 0 ? f : 0), len);
26171
27373
  const neg = sum(zip(rawFlow, d, (f, x) => x < 0 ? f : 0), len);
26172
- const values = zip(pos, neg, (p, n) => n === 0 ? 100 : 100 - 100 / (1 + p / n));
26173
- return {
26174
- plots: [{ key: "mfi", title: "MFI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
26175
- levels: [
26176
- { key: "upper", price: 80, color: NEUTRAL },
26177
- { key: "lower", price: 20, color: NEUTRAL }
26178
- ]
26179
- };
27374
+ const values = zip(pos, neg, (p, w) => p + w > 0 ? 100 * p / (p + w) : Number.NaN);
27375
+ return boundedOscillator({
27376
+ key: "mfi",
27377
+ title: "MFI",
27378
+ values,
27379
+ scale: { top: 100, bottom: 0 },
27380
+ overbought: num3(inputs, "overbought", 80),
27381
+ oversold: num3(inputs, "oversold", 20),
27382
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
27383
+ levelInk: NEUTRAL,
27384
+ zoneTransparency: 91,
27385
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 94))
27386
+ });
26180
27387
  }
26181
27388
  };
26182
- function volumeIndex(bars, when) {
26183
- const out = new Array(bars.length).fill(Number.NaN);
26184
- let index = 1e3;
26185
- let started = false;
26186
- for (let i = 1; i < bars.length; i++) {
26187
- const v = bars[i].volume;
26188
- const pv = bars[i - 1].volume;
26189
- const pc = bars[i - 1].close;
26190
- if (v == null || pv == null || !Number.isFinite(v) || !Number.isFinite(pv) || pc === 0) {
26191
- if (started) out[i] = index;
26192
- continue;
26193
- }
26194
- started = true;
26195
- if (when(v, pv)) index += (bars[i].close - pc) / pc * index;
26196
- out[i] = index;
26197
- }
26198
- return out;
27389
+ function regimeStyleInputs(signalTitle) {
27390
+ return [
27391
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the line and gradient fill while it holds above its signal EMA."),
27392
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line and gradient fill while it holds below its signal EMA."),
27393
+ colorInput(NEUTRAL, "Neutral", "neutralColor", "Color used while the signal EMA is still forming, or the line sits exactly on it."),
27394
+ colorInput(NEUTRAL, signalTitle, "signalColor", "Color of the signal EMA overlay."),
27395
+ 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.")
27396
+ ];
26199
27397
  }
27398
+ var NVI_POSITION = "Position vs Average";
27399
+ var NVI_SINGLE = "Single Color";
26200
27400
  var nvi = {
26201
27401
  type: "negative-volume-index",
26202
27402
  title: "Negative Volume Index",
26203
27403
  shortTitle: "Negative Volume Index",
26204
27404
  overlay: false,
26205
- inputs: [lengthInput(255, "signalLength", "Signal length"), colorInput()],
27405
+ inputs: [
27406
+ 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."),
27407
+ 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."),
27408
+ lengthInput(255, "signalLength", "Signal Length", 5e3, "Length of the signal EMA. The convention is one trading year \u2014 255 sessions on daily charts."),
27409
+ 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."),
27410
+ ...regimeStyleInputs("Signal EMA")
27411
+ ],
26206
27412
  compute: (bars, inputs) => {
26207
- const line = volumeIndex(bars, (v, pv) => v < pv);
26208
- return {
26209
- plots: [
26210
- { key: "nvi", title: "NVI", values: line, color: str3(inputs, "color", SERIES_LINE), width: 2 },
26211
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 255)), color: WARNING }
26212
- ]
26213
- };
27413
+ const values = volumeIndex(bars, closes(bars), num3(inputs, "base", 1e3), (v, pv) => v < pv);
27414
+ const signal = ema(values, num3(inputs, "signalLength", 255));
27415
+ return regimeIndex(
27416
+ "nvi",
27417
+ "NVI",
27418
+ values,
27419
+ signal,
27420
+ { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "neutralColor", NEUTRAL), signal: str3(inputs, "signalColor", NEUTRAL) },
27421
+ bool(inputs, "showSignal", true),
27422
+ bool(inputs, "gradientFill", true),
27423
+ str3(inputs, "colorMode", NVI_POSITION) === NVI_SINGLE
27424
+ );
26214
27425
  }
26215
27426
  };
26216
27427
  var pvi = {
@@ -26218,15 +27429,26 @@ var pvi = {
26218
27429
  title: "Positive Volume Index",
26219
27430
  shortTitle: "Positive Volume Index",
26220
27431
  overlay: false,
26221
- inputs: [lengthInput(255, "signalLength", "Signal length"), colorInput()],
27432
+ inputs: [
27433
+ sourceInput("Close", "source", "Source", "Price series whose one-bar percentage change moves PVI on its active sessions."),
27434
+ 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."),
27435
+ 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."),
27436
+ 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."),
27437
+ ...regimeStyleInputs("Signal Line")
27438
+ ],
26222
27439
  compute: (bars, inputs) => {
26223
- const line = volumeIndex(bars, (v, pv) => v > pv);
26224
- return {
26225
- plots: [
26226
- { key: "pvi", title: "PVI", values: line, color: str3(inputs, "color", SERIES_LINE), width: 2 },
26227
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 255)), color: WARNING }
26228
- ]
26229
- };
27440
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
27441
+ const values = volumeIndex(bars, src, num3(inputs, "base", 1e3), (v, pv) => v > pv);
27442
+ const signal = ema(values, num3(inputs, "signalLength", 255));
27443
+ return regimeIndex(
27444
+ "pvi",
27445
+ "PVI",
27446
+ values,
27447
+ signal,
27448
+ { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "neutralColor", NEUTRAL), signal: str3(inputs, "signalColor", NEUTRAL) },
27449
+ bool(inputs, "showSignal", true),
27450
+ bool(inputs, "gradientFill", true)
27451
+ );
26230
27452
  }
26231
27453
  };
26232
27454
  var pvt = {
@@ -26234,7 +27456,15 @@ var pvt = {
26234
27456
  title: "Price Volume Trend",
26235
27457
  shortTitle: "PVT",
26236
27458
  overlay: false,
26237
- inputs: [colorInput()],
27459
+ inputs: [
27460
+ boolInput("showSignal", "Signal Line", false, SETTINGS, "Display a moving average of PVT acting as a signal line."),
27461
+ optionInput("signalType", "Signal Type", "SMA", ["SMA", "EMA"], "Moving average type used for the signal line."),
27462
+ lengthInput(21, "signalLength", "Signal Length", 5e3, "Number of bars used to compute the signal line."),
27463
+ colorInput(NEUTRAL, "PVT Color", "color", "Color of the PVT line while it is flat."),
27464
+ colorInput(BULLISH, "Rising", "bullColor", "Color of the PVT line while it rises bar to bar."),
27465
+ colorInput(BEARISH, "Falling", "bearColor", "Color of the PVT line while it falls bar to bar."),
27466
+ colorInput(WARNING, "Signal Color", "signalColor", "Color of the signal line.")
27467
+ ],
26238
27468
  compute: (bars, inputs) => {
26239
27469
  const c = closes(bars);
26240
27470
  const raw = bars.map((b, i) => {
@@ -26242,7 +27472,17 @@ var pvt = {
26242
27472
  const pc = c[i - 1];
26243
27473
  return pc === 0 ? 0 : (c[i] - pc) / pc * b.volume;
26244
27474
  });
26245
- return { plots: [{ key: "pvt", title: "PVT", values: cumSum(raw), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
27475
+ const values = cumSum(raw);
27476
+ const flat = str3(inputs, "color", NEUTRAL);
27477
+ const plots = [
27478
+ { key: "pvt", title: "PVT", values, color: flat, width: 2, colors: slopeColors2(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), flat) }
27479
+ ];
27480
+ if (bool(inputs, "showSignal", false)) {
27481
+ const len = num3(inputs, "signalLength", 21);
27482
+ const signal = str3(inputs, "signalType", "SMA") === "EMA" ? ema(values, len) : sma(values, len);
27483
+ plots.push({ key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) });
27484
+ }
27485
+ return { plots };
26246
27486
  }
26247
27487
  };
26248
27488
  var volumeOscillator = {
@@ -26250,39 +27490,75 @@ var volumeOscillator = {
26250
27490
  title: "Volume Oscillator",
26251
27491
  shortTitle: "Vol Osc",
26252
27492
  overlay: false,
26253
- inputs: [lengthInput(5, "fastLength", "Fast length"), lengthInput(10, "slowLength", "Slow length"), colorInput()],
27493
+ inputs: [
27494
+ lengthInput(5, "fastLength", "Fast Length", 5e3, "Length of the fast EMA of volume."),
27495
+ lengthInput(10, "slowLength", "Slow Length", 5e3, "Length of the slow EMA of volume."),
27496
+ colorInput(BULLISH, "Above Zero", "color", "Color of the line and fill while it holds above zero \u2014 volume expanding."),
27497
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while it holds below zero \u2014 volume contracting.")
27498
+ ],
26254
27499
  compute: (bars, inputs) => {
26255
27500
  const vol = volumes(bars);
26256
27501
  const fast = ema(vol, num3(inputs, "fastLength", 5));
26257
27502
  const slow = ema(vol, num3(inputs, "slowLength", 10));
26258
- const values = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : 100 * (f - s) / s);
26259
- return {
26260
- plots: [{ key: "vo", title: "Volume Osc", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
26261
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26262
- };
27503
+ const values = zip(fast, slow, (f, s) => s === 0 ? 0 : 100 * (f - s) / s);
27504
+ const bull = str3(inputs, "color", BULLISH);
27505
+ return centeredOscillator({
27506
+ key: "vo",
27507
+ title: "Volume Oscillator",
27508
+ values,
27509
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
27510
+ zeroInk: NEUTRAL,
27511
+ valueTransparency: 60
27512
+ });
26263
27513
  }
26264
27514
  };
27515
+ var OSCILLATOR = "Oscillator";
26265
27516
  var pvo = {
26266
27517
  type: "pvo",
26267
27518
  title: "Percentage Volume Oscillator",
26268
27519
  shortTitle: "PVO",
26269
27520
  overlay: false,
26270
- inputs: [lengthInput(12, "fastLength", "Fast length"), lengthInput(26, "slowLength", "Slow length"), lengthInput(9, "signalLength", "Signal length")],
27521
+ inputs: [
27522
+ { ...lengthInput(12, "fastLength", "Fast Length", 5e3, "Period of the fast EMA of volume. The conventional setting is 12."), group: OSCILLATOR },
27523
+ { ...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 },
27524
+ { ...lengthInput(9, "signalLength", "Signal Length", 500, "Period of the EMA applied to the PVO to form the signal line."), group: OSCILLATOR },
27525
+ colorInput(BULLISH, "Expansion Color", "color", "Color of the oscillator and fill while participation expands."),
27526
+ colorInput(BEARISH, "Contraction Color", "bearColor", "Color of the oscillator and fill while participation contracts."),
27527
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
27528
+ colorInput(transp(NEUTRAL, 50), "Zero Line", "zeroColor", "Color of the zero reference line."),
27529
+ boolInput("showSignal", "Show Signal Line", true, STYLE, "Plot the signal line, an EMA of the PVO."),
27530
+ boolInput("showHistogram", "Show Histogram", true, STYLE, "Plot the difference between the PVO and its signal line as columns behind the oscillator.")
27531
+ ],
26271
27532
  compute: (bars, inputs) => {
26272
27533
  const vol = volumes(bars);
26273
27534
  const fast = ema(vol, num3(inputs, "fastLength", 12));
26274
27535
  const slow = ema(vol, num3(inputs, "slowLength", 26));
26275
- const line = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : 100 * (f - s) / s);
26276
- const signal = ema(line, num3(inputs, "signalLength", 9));
26277
- const hist = sub(line, signal);
26278
- return {
26279
- plots: [
26280
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: hist.map((x) => Number.isFinite(x) ? x >= 0 ? BULLISH : BEARISH : null), base: 0 },
26281
- { key: "pvo", title: "PVO", values: line, color: SERIES_LINE, width: 2 },
26282
- { key: "signal", title: "Signal", values: signal, color: WARNING }
26283
- ],
26284
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26285
- };
27536
+ const values = zip(fast, slow, (f, s) => s > 0 ? 100 * (f - s) / s : Number.NaN);
27537
+ const signal = ema(values, num3(inputs, "signalLength", 9));
27538
+ const expansion = str3(inputs, "color", BULLISH);
27539
+ const contraction = str3(inputs, "bearColor", BEARISH);
27540
+ const extraPlots = [];
27541
+ if (bool(inputs, "showHistogram", true)) {
27542
+ const hist = sub(values, signal);
27543
+ extraPlots.push({
27544
+ key: "hist",
27545
+ title: "Histogram",
27546
+ values: hist,
27547
+ kind: "columns",
27548
+ color: transp(expansion, 60),
27549
+ colors: hist.map((x) => Number.isFinite(x) ? transp(x > 0 ? expansion : contraction, 60) : null),
27550
+ base: 0
27551
+ });
27552
+ }
27553
+ if (bool(inputs, "showSignal", true)) extraPlots.push({ key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) });
27554
+ return centeredOscillator({
27555
+ key: "pvo",
27556
+ title: "PVO",
27557
+ values,
27558
+ ink: { bull: expansion, bear: contraction, neutral: contraction },
27559
+ zeroInk: str3(inputs, "zeroColor", transp(NEUTRAL, 50)),
27560
+ extraPlots
27561
+ });
26286
27562
  }
26287
27563
  };
26288
27564
  var vfi = {
@@ -26291,55 +27567,86 @@ var vfi = {
26291
27567
  shortTitle: "Volume Flow Indicator",
26292
27568
  overlay: false,
26293
27569
  inputs: [
26294
- lengthInput(130),
26295
- { key: "coef", title: "Cutoff coefficient", type: "float", defval: 0.2, min: 0.01, max: 10, step: 0.01 },
26296
- { key: "volCoef", title: "Volume cap", type: "float", defval: 2.5, min: 0.1, max: 50, step: 0.1 },
26297
- lengthInput(3, "smoothLength", "Smoothing length")
27570
+ { ...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 },
27571
+ 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."),
27572
+ 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."),
27573
+ { ...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 },
27574
+ boolInput("smooth", "Smooth VFI", true, SETTINGS, "Applies the published light smoothing \u2014 an EMA of the summed line \u2014 before plotting."),
27575
+ lengthInput(3, "smoothLength", "Smoothing Length", 500, "Length of the smoothing EMA. The published version uses 3 bars."),
27576
+ boolInput("showSignal", "Signal Line", true, SETTINGS, "Plots an EMA of VFI as a signal line."),
27577
+ lengthInput(5, "signalLength", "Signal Length", 500, "Length of the signal-line EMA."),
27578
+ colorInput(BULLISH, "Bullish", "color", "Color of the VFI line and gradient fill above the zero line \u2014 the net accumulation regime."),
27579
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the VFI line and gradient fill below the zero line \u2014 the net distribution regime."),
27580
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
27581
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the zero line and the VFI line with a vertical gradient.")
26298
27582
  ],
26299
27583
  compute: (bars, inputs) => {
26300
27584
  const len = num3(inputs, "length", 130);
27585
+ const typical = sourceValues(bars, "HLC3");
27586
+ const logChange = sub(map(typical, Math.log), shift(map(typical, Math.log), 1));
27587
+ const volatility = stdev(logChange, num3(inputs, "volLength", 30));
26301
27588
  const coef = num3(inputs, "coef", 0.2);
26302
27589
  const volCoef = num3(inputs, "volCoef", 2.5);
26303
- const typical = map(sourceValues(bars, "HLC3"), Math.log);
26304
- const inter = change(typical);
26305
- const vInter = stdev(inter, 30);
26306
27590
  const vol = volumes(bars);
26307
- const vAve = sma(vol, len);
26308
- const mf = change(sourceValues(bars, "HLC3"));
26309
- const raw = bars.map((b, i) => {
26310
- const cutoff = Number.isFinite(vInter[i]) ? coef * vInter[i] * b.close : Number.NaN;
27591
+ const avgVolume = shift(sma(vol, len), 1);
27592
+ const typicalChange = change(typical);
27593
+ const signed2 = bars.map((b, i) => {
27594
+ const sd = volatility[i];
27595
+ const m = typicalChange[i];
26311
27596
  const v = vol[i];
26312
- const ave = i > 0 ? vAve[i - 1] : Number.NaN;
26313
- const m = mf[i];
26314
- if (!Number.isFinite(cutoff) || !Number.isFinite(v) || !Number.isFinite(ave) || !Number.isFinite(m)) return Number.NaN;
26315
- const capped = Math.min(v, ave * volCoef);
27597
+ if (!Number.isFinite(sd) || !Number.isFinite(m) || !Number.isFinite(v)) return Number.NaN;
27598
+ const cutoff = coef * sd * b.close;
27599
+ const avg = avgVolume[i];
27600
+ const capped = Number.isFinite(avg) ? Math.min(v, avg * volCoef) : v;
26316
27601
  return m > cutoff ? capped : m < -cutoff ? -capped : 0;
26317
27602
  });
26318
- const values = bars.map((_, i) => {
26319
- const ave = i > 0 ? vAve[i - 1] : Number.NaN;
26320
- return Number.isFinite(ave) && ave !== 0 ? ave : Number.NaN;
27603
+ const raw = zip(sum(signed2, len), avgVolume, (f, a) => a > 0 ? f / a : Number.NaN);
27604
+ const values = bool(inputs, "smooth", true) ? ema(raw, num3(inputs, "smoothLength", 3)) : raw;
27605
+ const bull = str3(inputs, "color", BULLISH);
27606
+ const extraPlots = bool(inputs, "showSignal", true) ? [{ key: "signal", title: "Signal Line", values: ema(values, num3(inputs, "signalLength", 5)), color: str3(inputs, "signalColor", WARNING) }] : [];
27607
+ return centeredOscillator({
27608
+ key: "vfi",
27609
+ title: "VFI",
27610
+ values,
27611
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "bearColor", BEARISH) },
27612
+ zeroInk: transp(NEUTRAL, 90),
27613
+ fill: bool(inputs, "gradientFill", true),
27614
+ extraPlots
26321
27615
  });
26322
- const flowSum = sum(raw, len);
26323
- const line = zip(flowSum, values, (f, a) => f / a);
26324
- return {
26325
- plots: [{ key: "vfi", title: "VFI", values: sma(line, num3(inputs, "smoothLength", 3)), color: SERIES_LINE, width: 2 }],
26326
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26327
- };
26328
27616
  }
26329
27617
  };
27618
+ var II_OSCILLATOR = "Normalized Oscillator (II%)";
27619
+ var II_CUMULATIVE = "Cumulative Line";
26330
27620
  var intradayIntensity = {
26331
27621
  type: "intraday-intensity",
26332
27622
  title: "Intraday Intensity",
26333
27623
  shortTitle: "Intraday Intensity",
26334
27624
  overlay: false,
26335
- inputs: [lengthInput(21), colorInput()],
27625
+ inputs: [
27626
+ 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."),
27627
+ 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."),
27628
+ colorInput(BULLISH, "Bullish", "color", "Color of positive readings, the rising cumulative line and the fill above zero \u2014 the accumulation side."),
27629
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of negative readings, the falling cumulative line and the fill below zero \u2014 the distribution side."),
27630
+ 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.")
27631
+ ],
26336
27632
  compute: (bars, inputs) => {
27633
+ const intensity = moneyFlowVolume(bars);
27634
+ const bull = str3(inputs, "color", BULLISH);
27635
+ const bear = str3(inputs, "bearColor", BEARISH);
27636
+ if (str3(inputs, "mode", II_OSCILLATOR) === II_CUMULATIVE) {
27637
+ const values2 = cumSum(intensity);
27638
+ return { plots: [{ key: "ii", title: "Intraday Intensity", values: values2, color: bull, colors: slopeColors2(values2, bull, bear, bull) }] };
27639
+ }
26337
27640
  const len = num3(inputs, "length", 21);
26338
- const values = zip(sum(moneyFlowVolume(bars), len), sum(volumes(bars), len), (m, v) => v === 0 ? 0 : 100 * m / v);
26339
- return {
26340
- plots: [{ key: "ii", title: "II%", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
26341
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26342
- };
27641
+ const values = zip(sum(intensity, len), sum(volumes(bars), len), (m, v) => v > 0 ? 100 * m / v : Number.NaN);
27642
+ return centeredOscillator({
27643
+ key: "ii",
27644
+ title: "Intraday Intensity",
27645
+ values,
27646
+ ink: { bull, bear, neutral: bear },
27647
+ zeroInk: transp(NEUTRAL, 90),
27648
+ fill: bool(inputs, "gradientFill", true)
27649
+ });
26343
27650
  }
26344
27651
  };
26345
27652
  var volumeSpecs = [
@@ -26372,15 +27679,24 @@ function periodKey(anchor, time) {
26372
27679
  }
26373
27680
  return Math.floor(time / DAY_MS2);
26374
27681
  }
27682
+ function barInterval(bars) {
27683
+ if (bars.length < 2) return 0;
27684
+ const gaps = [];
27685
+ for (let i = 1; i < bars.length; i++) gaps.push(bars[i].time - bars[i - 1].time);
27686
+ gaps.sort((a, b) => a - b);
27687
+ return gaps[Math.floor(gaps.length / 2)] ?? 0;
27688
+ }
26375
27689
  var parabolicSar = {
26376
27690
  type: "parabolic-sar",
26377
27691
  title: "Parabolic SAR",
26378
27692
  shortTitle: "SAR",
26379
27693
  overlay: true,
26380
27694
  inputs: [
26381
- { key: "start", title: "Start", type: "float", defval: 0.02, min: 1e-3, max: 1, step: 1e-3 },
26382
- { key: "increment", title: "Increment", type: "float", defval: 0.02, min: 1e-3, max: 1, step: 1e-3 },
26383
- { key: "maximum", title: "Max value", type: "float", defval: 0.2, min: 0.01, max: 1, step: 0.01 }
27695
+ floatInput("start", "Start", 0.02, 0, 1, 0.01, "Initial acceleration factor applied when a new trend begins."),
27696
+ floatInput("increment", "Increment", 0.02, 0, 1, 0.01, "Amount added to the acceleration factor each time the trend makes a new extreme point."),
27697
+ floatInput("maximum", "Maximum", 0.2, 0.01, 1, 0.01, "Upper cap on the acceleration factor."),
27698
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the SAR crosses while they trail below price \u2014 uptrend, the stop sits under the market."),
27699
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the SAR crosses while they sit above price \u2014 downtrend, the stop sits over the market.")
26384
27700
  ],
26385
27701
  compute: (bars, inputs) => {
26386
27702
  const start = num3(inputs, "start", 0.02);
@@ -26388,7 +27704,6 @@ var parabolicSar = {
26388
27704
  const max = num3(inputs, "maximum", 0.2);
26389
27705
  const n = bars.length;
26390
27706
  const values = new Array(n).fill(Number.NaN);
26391
- const colors = new Array(n).fill(null);
26392
27707
  if (n >= 2) {
26393
27708
  let long = bars[1].close >= bars[0].close;
26394
27709
  let sar = long ? bars[0].low : bars[0].high;
@@ -26413,18 +27728,21 @@ var parabolicSar = {
26413
27728
  af = Math.min(max, af + inc);
26414
27729
  }
26415
27730
  values[i] = sar;
26416
- colors[i] = long ? BULLISH : BEARISH;
26417
27731
  }
26418
27732
  }
26419
- return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
27733
+ const bull = str3(inputs, "bullColor", BULLISH);
27734
+ const bear = str3(inputs, "bearColor", BEARISH);
27735
+ const colors = values.map((x, i) => Number.isFinite(x) ? x < bars[i].close ? bull : bear : null);
27736
+ return { plots: [{ key: "sar", title: "SAR", values, kind: "cross", color: bull, colors }] };
26420
27737
  }
26421
27738
  };
26422
- var VWAP_BANDS_GROUP = "Bands";
26423
- var VWAP_STYLE = "Style";
27739
+ var VWAP_ANCHORS = ["Session", "Week", "Month", "Quarter", "Year"];
27740
+ var BANDS_STDEV = "Standard Deviation";
27741
+ var BANDS_PERCENT = "Percentage";
26424
27742
  var VWAP_BANDS = [
26425
- { on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
26426
- { on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
26427
- { on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
27743
+ { show: "band1", mult: "band1Mult", defMult: 1, defOn: true },
27744
+ { show: "band2", mult: "band2Mult", defMult: 2, defOn: false },
27745
+ { show: "band3", mult: "band3Mult", defMult: 3, defOn: false }
26428
27746
  ];
26429
27747
  var vwap = {
26430
27748
  type: "vwap",
@@ -26432,39 +27750,36 @@ var vwap = {
26432
27750
  shortTitle: "VWAP",
26433
27751
  overlay: true,
26434
27752
  inputs: [
26435
- { key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
26436
- sourceInput("HLC3"),
26437
- // Each band is one row: its toggle leads, the multiplier follows unlabeled.
27753
+ 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)."),
27754
+ sourceInput("HLC3", "source", "Source", "Price used in the volume weighted average. Typical price (HLC3) is the classic choice."),
27755
+ 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."),
26438
27756
  ...VWAP_BANDS.flatMap((b, i) => [
26439
- { key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
26440
- { key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
27757
+ boolInput(b.show, `Band #${i + 1}`, b.defOn, SETTINGS, `Show the ${["first", "second", "third"][i]} band pair.`),
27758
+ 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.")
26441
27759
  ]),
26442
- { ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
26443
- // One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
26444
- ...VWAP_BANDS.flatMap((b, i) => [
26445
- { ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
26446
- { key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
26447
- { ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
26448
- ])
27760
+ 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."),
27761
+ colorInput(BULLISH, "Bullish", "bullColor", "VWAP line color while price closes above it, and the color of the lower (support) bands."),
27762
+ colorInput(BEARISH, "Bearish", "bearColor", "VWAP line color while price closes below it, and the color of the upper (resistance) bands."),
27763
+ colorInput(NEUTRAL, "Bands", "bandsColor", "Deviation bands fill color."),
27764
+ boolInput("fill", "Bands Fill", true, STYLE, "Fill the area between each visible band pair.")
26449
27765
  ],
26450
27766
  compute: (bars, inputs) => {
26451
- const anchor = str3(inputs, "anchor", "Day");
27767
+ const anchor = str3(inputs, "anchor", "Session");
26452
27768
  const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
26453
27769
  const n = bars.length;
27770
+ const hidden = bool(inputs, "hideDwm", true) && barInterval(bars) >= DAY_MS2;
26454
27771
  const values = new Array(n).fill(Number.NaN);
26455
- const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
27772
+ const bands = VWAP_BANDS.filter((b) => bool(inputs, b.show, b.defOn)).map((b) => ({
26456
27773
  mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
26457
- color: str3(inputs, b.color, b.defColor),
26458
- fill: bool(inputs, b.fill, true),
26459
- fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
26460
27774
  up: new Array(n).fill(Number.NaN),
26461
27775
  down: new Array(n).fill(Number.NaN)
26462
27776
  }));
27777
+ const percentMode = str3(inputs, "bandsMode", BANDS_STDEV) === BANDS_PERCENT;
26463
27778
  let period = Number.NaN;
26464
27779
  let cumPV = 0;
26465
27780
  let cumPV2 = 0;
26466
27781
  let cumV = 0;
26467
- for (let i = 0; i < n; i++) {
27782
+ for (let i = 0; i < n && !hidden; i++) {
26468
27783
  const b = bars[i];
26469
27784
  const key = periodKey(anchor, b.time);
26470
27785
  if (key !== period) {
@@ -26482,38 +27797,58 @@ var vwap = {
26482
27797
  }
26483
27798
  if (cumV <= 0) continue;
26484
27799
  const mean = cumPV / cumV;
26485
- const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
27800
+ const offset = percentMode ? mean / 100 : Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
26486
27801
  values[i] = mean;
26487
27802
  for (const band of bands) {
26488
- band.up[i] = mean + band.mult * sd;
26489
- band.down[i] = mean - band.mult * sd;
27803
+ band.up[i] = mean + band.mult * offset;
27804
+ band.down[i] = mean - band.mult * offset;
26490
27805
  }
26491
27806
  }
26492
- const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
27807
+ const bull = str3(inputs, "bullColor", BULLISH);
27808
+ const bear = str3(inputs, "bearColor", BEARISH);
27809
+ const fillColor = transp(str3(inputs, "bandsColor", NEUTRAL), 95);
27810
+ const plots = [
27811
+ { key: "vwap", title: "VWAP", values, color: bull, colors: values.map((x, i) => Number.isFinite(x) ? bars[i].close >= x ? bull : bear : null) }
27812
+ ];
26493
27813
  const fills = [];
26494
27814
  bands.forEach((band, i) => {
26495
27815
  plots.push(
26496
- { key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
26497
- { key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
27816
+ { key: `up${i}`, title: `Upper Band #${i + 1}`, values: band.up, color: transp(bear, 40) },
27817
+ { key: `down${i}`, title: `Lower Band #${i + 1}`, values: band.down, color: transp(bull, 40) }
26498
27818
  );
26499
- if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
27819
+ if (bool(inputs, "fill", true)) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: fillColor });
26500
27820
  });
26501
27821
  return { plots, bands: fills };
26502
27822
  }
26503
27823
  };
27824
+ var PIVOT_TRADITIONAL = "Traditional";
27825
+ var PIVOT_FIBONACCI = "Fibonacci";
27826
+ var PIVOT_CAMARILLA = "Camarilla";
27827
+ var PIVOT_WOODIE = "Woodie";
27828
+ var PIVOT_AUTO = "Auto";
26504
27829
  var pivotPoints = {
26505
27830
  type: "pivot-points",
26506
- title: "Pivot Points",
27831
+ title: "Pivot Points Standard",
26507
27832
  shortTitle: "Pivots",
26508
27833
  overlay: true,
26509
- inputs: [{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month"] }],
27834
+ inputs: [
27835
+ 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."),
27836
+ 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."),
27837
+ colorInput(WARNING, "Pivot", "pivotColor", "Color of the central pivot (P) level."),
27838
+ colorInput(BEARISH, "Resistances", "resistanceColor", "Color of the resistance (R) levels."),
27839
+ colorInput(BULLISH, "Supports", "supportColor", "Color of the support (S) levels.")
27840
+ ],
26510
27841
  compute: (bars, inputs) => {
26511
- const anchor = str3(inputs, "anchor", "Day");
27842
+ const interval = barInterval(bars);
27843
+ const requested = str3(inputs, "anchor", PIVOT_AUTO);
27844
+ const auto = interval <= 15 * 6e4 ? "Session" : interval < DAY_MS2 ? "Week" : interval <= DAY_MS2 ? "Month" : "Year";
27845
+ const anchor = requested === "Daily" ? "Session" : requested === "Weekly" ? "Week" : requested === "Monthly" ? "Month" : requested === "Yearly" ? "Year" : auto;
27846
+ const kind = str3(inputs, "kind", PIVOT_TRADITIONAL);
26512
27847
  const aggs = /* @__PURE__ */ new Map();
26513
27848
  for (const b of bars) {
26514
27849
  const key = periodKey(anchor, b.time);
26515
27850
  const a = aggs.get(key);
26516
- if (!a) aggs.set(key, { high: b.high, low: b.low, close: b.close });
27851
+ if (!a) aggs.set(key, { open: b.open, high: b.high, low: b.low, close: b.close });
26517
27852
  else {
26518
27853
  a.high = Math.max(a.high, b.high);
26519
27854
  a.low = Math.min(a.low, b.low);
@@ -26522,31 +27857,61 @@ var pivotPoints = {
26522
27857
  }
26523
27858
  const n = bars.length;
26524
27859
  const mk = () => new Array(n).fill(Number.NaN);
26525
- const levels = { p: mk(), r1: mk(), s1: mk(), r2: mk(), s2: mk(), r3: mk(), s3: mk() };
27860
+ const levels = { p: mk(), r1: mk(), s1: mk(), r2: mk(), s2: mk(), r3: mk(), s3: mk(), r4: mk(), s4: mk() };
27861
+ let lastKey = Number.NaN;
26526
27862
  for (let i = 0; i < n; i++) {
26527
- const prev2 = aggs.get(periodKey(anchor, bars[i].time) - 1);
26528
- if (!prev2) continue;
26529
- const p = (prev2.high + prev2.low + prev2.close) / 3;
27863
+ const key = periodKey(anchor, bars[i].time);
27864
+ const fresh = key !== lastKey;
27865
+ lastKey = key;
27866
+ if (fresh) continue;
27867
+ const prev2 = aggs.get(key - 1);
27868
+ const curr = aggs.get(key);
27869
+ if (!prev2 || !curr) continue;
27870
+ const range = prev2.high - prev2.low;
27871
+ const traditional = (prev2.high + prev2.low + prev2.close) / 3;
27872
+ const p = kind === PIVOT_WOODIE ? (prev2.high + prev2.low + 2 * curr.open) / 4 : traditional;
26530
27873
  levels.p[i] = p;
26531
- levels.r1[i] = 2 * p - prev2.low;
26532
- levels.s1[i] = 2 * p - prev2.high;
26533
- levels.r2[i] = p + (prev2.high - prev2.low);
26534
- levels.s2[i] = p - (prev2.high - prev2.low);
26535
- levels.r3[i] = prev2.high + 2 * (p - prev2.low);
26536
- levels.s3[i] = prev2.low - 2 * (prev2.high - p);
26537
- }
26538
- const plot = (key, title, color) => ({ key, title, kind: "step", values: levels[key], color });
26539
- return {
26540
- plots: [
26541
- plot("p", "P", WARNING),
26542
- plot("r1", "R1", BEARISH),
26543
- plot("s1", "S1", BULLISH),
26544
- plot("r2", "R2", BEARISH),
26545
- plot("s2", "S2", BULLISH),
26546
- plot("r3", "R3", BEARISH),
26547
- plot("s3", "S3", BULLISH)
26548
- ]
26549
- };
27874
+ if (kind === PIVOT_FIBONACCI) {
27875
+ levels.r1[i] = traditional + 0.382 * range;
27876
+ levels.s1[i] = traditional - 0.382 * range;
27877
+ levels.r2[i] = traditional + 0.618 * range;
27878
+ levels.s2[i] = traditional - 0.618 * range;
27879
+ levels.r3[i] = traditional + range;
27880
+ levels.s3[i] = traditional - range;
27881
+ } else if (kind === PIVOT_CAMARILLA) {
27882
+ const cam = 1.1 * range;
27883
+ levels.r1[i] = prev2.close + cam / 12;
27884
+ levels.s1[i] = prev2.close - cam / 12;
27885
+ levels.r2[i] = prev2.close + cam / 6;
27886
+ levels.s2[i] = prev2.close - cam / 6;
27887
+ levels.r3[i] = prev2.close + cam / 4;
27888
+ levels.s3[i] = prev2.close - cam / 4;
27889
+ levels.r4[i] = prev2.close + cam / 2;
27890
+ levels.s4[i] = prev2.close - cam / 2;
27891
+ } else {
27892
+ levels.r1[i] = 2 * p - prev2.low;
27893
+ levels.s1[i] = 2 * p - prev2.high;
27894
+ levels.r2[i] = p + range;
27895
+ levels.s2[i] = p - range;
27896
+ if (kind !== PIVOT_WOODIE) {
27897
+ levels.r3[i] = prev2.high + 2 * (p - prev2.low);
27898
+ levels.s3[i] = prev2.low - 2 * (prev2.high - p);
27899
+ }
27900
+ }
27901
+ }
27902
+ const resistance = str3(inputs, "resistanceColor", BEARISH);
27903
+ const support = str3(inputs, "supportColor", BULLISH);
27904
+ const plot = (key, title, color) => ({ key, title, values: levels[key], color });
27905
+ const plots = [
27906
+ plot("p", "P", str3(inputs, "pivotColor", WARNING)),
27907
+ plot("r1", "R1", resistance),
27908
+ plot("s1", "S1", support),
27909
+ plot("r2", "R2", resistance),
27910
+ plot("s2", "S2", support)
27911
+ ];
27912
+ if (kind !== PIVOT_WOODIE) plots.push(plot("r3", "R3", resistance), plot("s3", "S3", support));
27913
+ if (kind === PIVOT_CAMARILLA) plots.push(plot("r4", "R4", resistance), plot("s4", "S4", support));
27914
+ return { plots };
26550
27915
  }
26551
27916
  };
26552
27917
  var fiftyTwoWeek = {
@@ -26554,15 +27919,25 @@ var fiftyTwoWeek = {
26554
27919
  title: "52 Week High/Low",
26555
27920
  shortTitle: "52W H/L",
26556
27921
  overlay: true,
26557
- inputs: [{ key: "weeks", title: "Weeks", type: "int", defval: 52, min: 1, max: 520, step: 1 }],
27922
+ inputs: [
27923
+ intInput("weeks", "Weeks", 52, 1, 520, "Length of the rolling window, in weeks."),
27924
+ 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."),
27925
+ colorInput(BULLISH, "52 Week High", "highColor", "Color of the 52 week high level."),
27926
+ colorInput(BEARISH, "52 Week Low", "lowColor", "Color of the 52 week low level."),
27927
+ colorInput(NEUTRAL, "All-Time High/Low", "allTimeColor", "Color of the all-time high and all-time low levels.")
27928
+ ],
26558
27929
  compute: (bars, inputs) => {
26559
27930
  const span = num3(inputs, "weeks", 52) * 7 * DAY_MS2;
26560
27931
  const n = bars.length;
26561
27932
  const hi = new Array(n).fill(Number.NaN);
26562
27933
  const lo = new Array(n).fill(Number.NaN);
27934
+ const ath = new Array(n).fill(Number.NaN);
27935
+ const atl = new Array(n).fill(Number.NaN);
26563
27936
  const maxIdx = [];
26564
27937
  const minIdx = [];
26565
27938
  let from = 0;
27939
+ let runHigh = -Infinity;
27940
+ let runLow = Infinity;
26566
27941
  for (let i = 0; i < n; i++) {
26567
27942
  const b = bars[i];
26568
27943
  while (maxIdx.length > 0 && bars[maxIdx[maxIdx.length - 1]].high <= b.high) maxIdx.pop();
@@ -26574,13 +27949,20 @@ var fiftyTwoWeek = {
26574
27949
  while (minIdx[0] < from) minIdx.shift();
26575
27950
  hi[i] = bars[maxIdx[0]].high;
26576
27951
  lo[i] = bars[minIdx[0]].low;
27952
+ runHigh = Math.max(runHigh, b.high);
27953
+ runLow = Math.min(runLow, b.low);
27954
+ ath[i] = runHigh;
27955
+ atl[i] = runLow;
27956
+ }
27957
+ const plots = [
27958
+ { key: "high", title: "52 Week High", values: hi, kind: "step", color: str3(inputs, "highColor", BULLISH), width: 2 },
27959
+ { key: "low", title: "52 Week Low", values: lo, kind: "step", color: str3(inputs, "lowColor", BEARISH), width: 2 }
27960
+ ];
27961
+ if (bool(inputs, "showAllTime", false)) {
27962
+ const ink = str3(inputs, "allTimeColor", NEUTRAL);
27963
+ 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 });
26577
27964
  }
26578
- return {
26579
- plots: [
26580
- { key: "high", title: "52W high", values: hi, kind: "step", color: BULLISH },
26581
- { key: "low", title: "52W low", values: lo, kind: "step", color: BEARISH }
26582
- ]
26583
- };
27965
+ return { plots };
26584
27966
  }
26585
27967
  };
26586
27968
  var zigzag = {
@@ -26588,9 +27970,11 @@ var zigzag = {
26588
27970
  title: "ZigZag",
26589
27971
  overlay: true,
26590
27972
  inputs: [
26591
- { key: "deviation", title: "Deviation %", type: "float", defval: 5, min: 0.01, max: 100, step: 0.1 },
26592
- { key: "depth", title: "Depth", type: "int", defval: 10, min: 1, max: 500, step: 1 },
26593
- colorInput()
27973
+ 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."),
27974
+ { ...intInput("depth", "Depth", 10, 2, 500, "Minimum number of bars required between two consecutive pivots.") },
27975
+ colorInput(BULLISH, "Up Leg", "bullColor", "Color of rising zigzag segments."),
27976
+ colorInput(BEARISH, "Down Leg", "bearColor", "Color of falling zigzag segments."),
27977
+ { key: "lineWidth", title: "Width", type: "int", defval: 2, min: 1, max: 10, step: 1, group: STYLE, tooltip: "Width of the zigzag segments." }
26594
27978
  ],
26595
27979
  compute: (bars, inputs) => {
26596
27980
  const dev = num3(inputs, "deviation", 5) / 100;
@@ -26603,14 +27987,14 @@ var zigzag = {
26603
27987
  const b = bars[i];
26604
27988
  if (up) {
26605
27989
  if (b.high >= ext.price) ext = { i, price: b.high };
26606
- else if (b.low <= ext.price * (1 - dev) && i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
27990
+ else if (b.low <= ext.price * (1 - dev) && ext.i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
26607
27991
  pivots.push(ext);
26608
27992
  up = false;
26609
27993
  ext = { i, price: b.low };
26610
27994
  }
26611
27995
  } else if (b.low <= ext.price) {
26612
27996
  ext = { i, price: b.low };
26613
- } else if (b.high >= ext.price * (1 + dev) && i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
27997
+ } else if (b.high >= ext.price * (1 + dev) && ext.i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
26614
27998
  pivots.push(ext);
26615
27999
  up = true;
26616
28000
  ext = { i, price: b.high };
@@ -26618,22 +28002,29 @@ var zigzag = {
26618
28002
  }
26619
28003
  pivots.push(ext);
26620
28004
  }
26621
- return {
26622
- plots: [],
26623
- polylines: [
26624
- {
26625
- key: "zigzag",
26626
- points: pivots.map((p) => ({ xloc: "bar_time", x: bars[p.i].time, price: p.price })),
26627
- curved: false,
26628
- closed: false,
26629
- lineColor: str3(inputs, "color", SERIES_LINE),
26630
- lineWidth: 2,
26631
- lineStyle: "solid",
26632
- arrowLeft: false,
26633
- arrowRight: false
26634
- }
26635
- ]
26636
- };
28005
+ const bull = str3(inputs, "bullColor", BULLISH);
28006
+ const bear = str3(inputs, "bearColor", BEARISH);
28007
+ const width = num3(inputs, "lineWidth", 2);
28008
+ const polylines = [];
28009
+ for (let k = 1; k < pivots.length; k++) {
28010
+ const a = pivots[k - 1];
28011
+ const b = pivots[k];
28012
+ polylines.push({
28013
+ key: `leg${k}`,
28014
+ points: [
28015
+ { xloc: "bar_time", x: bars[a.i].time, price: a.price },
28016
+ { xloc: "bar_time", x: bars[b.i].time, price: b.price }
28017
+ ],
28018
+ curved: false,
28019
+ closed: false,
28020
+ lineColor: b.price >= a.price ? bull : bear,
28021
+ lineWidth: width,
28022
+ lineStyle: "solid",
28023
+ arrowLeft: false,
28024
+ arrowRight: false
28025
+ });
28026
+ }
28027
+ return { plots: [], polylines };
26637
28028
  }
26638
28029
  };
26639
28030
  var williamsFractal = {
@@ -26641,10 +28032,16 @@ var williamsFractal = {
26641
28032
  title: "Williams Fractal",
26642
28033
  shortTitle: "Fractals",
26643
28034
  overlay: true,
26644
- inputs: [{ key: "periods", title: "Periods", type: "int", defval: 2, min: 1, max: 50, step: 1 }],
28035
+ inputs: [
28036
+ 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."),
28037
+ colorInput(BEARISH, "Up Fractal", "upColor", "Color of the up fractal marker (swing high), drawn above the fractal bar."),
28038
+ colorInput(BULLISH, "Down Fractal", "downColor", "Color of the down fractal marker (swing low), drawn below the fractal bar.")
28039
+ ],
26645
28040
  compute: (bars, inputs) => {
26646
28041
  const p = num3(inputs, "periods", 2);
26647
28042
  const markers = [];
28043
+ const up = str3(inputs, "upColor", BEARISH);
28044
+ const down = str3(inputs, "downColor", BULLISH);
26648
28045
  const isExtreme = (i, pick, better) => {
26649
28046
  const v = pick(bars[i]);
26650
28047
  for (let k = i - p; k <= i + p; k++) {
@@ -26654,12 +28051,8 @@ var williamsFractal = {
26654
28051
  return true;
26655
28052
  };
26656
28053
  for (let i = p; i < bars.length - p; i++) {
26657
- if (isExtreme(i, (b) => b.high, (a, b) => a >= b)) {
26658
- markers.push({ time: bars[i].time, position: "aboveBar", shape: "triangleup", color: BULLISH });
26659
- }
26660
- if (isExtreme(i, (b) => b.low, (a, b) => a <= b)) {
26661
- markers.push({ time: bars[i].time, position: "belowBar", shape: "triangledown", color: BEARISH });
26662
- }
28054
+ if (isExtreme(i, (b) => b.high, (a, b) => a > b)) markers.push({ time: bars[i].time, position: "aboveBar", shape: "triangleup", color: up });
28055
+ if (isExtreme(i, (b) => b.low, (a, b) => a < b)) markers.push({ time: bars[i].time, position: "belowBar", shape: "triangledown", color: down });
26663
28056
  }
26664
28057
  return { plots: [], markers };
26665
28058
  }
@@ -27031,4 +28424,4 @@ function resolveElement(container) {
27031
28424
  return element;
27032
28425
  }
27033
28426
 
27034
- export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MarksControl, MultiProviderFeed, NativeRenderer, RendererControl, SecondClock, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isGroupRow, normalizeSession, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, resolveTimezone, sharedBarStore, timeframeToMs, timezoneMenuRows, tzButtonLabel, tzMenuLabel, tzOffset };
28427
+ export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MarksControl, MultiProviderFeed, NativeRenderer, RendererControl, SecondClock, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isExchangeTimezone, isGroupRow, normalizeSession, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, resolveTimezone, sharedBarStore, timeframeToMs, timezoneMenuRows, tzButtonLabel, tzMenuLabel, tzOffset };