@luxalgo/vela 0.7.6 → 0.7.8

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