@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.
package/dist/index.cjs CHANGED
@@ -10229,6 +10229,8 @@ var MultiProviderFeed = class {
10229
10229
  this.liveBars = [];
10230
10230
  /** Sync-accessible symbol metadata, warmed by load()/symbolInfoFor (the engine reads it synchronously). */
10231
10231
  this.symInfoCache = /* @__PURE__ */ new Map();
10232
+ /** Requests in flight, keyed like the cache — every concurrent asker shares one round trip. */
10233
+ this.symInfoInflight = /* @__PURE__ */ new Map();
10232
10234
  this.cache = new CachingDataFeed(new RegistryFetchFeed(this.registry), store);
10233
10235
  }
10234
10236
  // ── Registry surface (driven by chart.data / DataControl) ──────────────
@@ -10289,9 +10291,36 @@ var MultiProviderFeed = class {
10289
10291
  async symbolInfoFor(raw) {
10290
10292
  const resolved = this.registry.resolve(raw, { default: this.primaryProvider });
10291
10293
  if (!resolved) return void 0;
10292
- const info = await this.registry.get(resolved.provider)?.getSymbolInfo?.(resolved.ticker);
10293
- if (info) this.symInfoCache.set(symKey(resolved), info);
10294
- return info;
10294
+ return this.fetchSymbolInfo(resolved);
10295
+ }
10296
+ /**
10297
+ * One round trip per symbol, shared. A market switch probes this metadata from five
10298
+ * places at once (the bar load's prefetch, the renderer's tick size, the market-status
10299
+ * badge, the session shading, the session toggle) — without the in-flight map each
10300
+ * would open its own request for the same answer, and the last one to land decided how
10301
+ * late the chrome settled.
10302
+ *
10303
+ * Cached for {@link SYMBOL_INFO_TTL_MS}: this metadata is stable within a session
10304
+ * (tick size, session vocabulary, listing prefix), but not immutable — a continuous
10305
+ * futures row rolls its current contract — so the entry expires rather than pinning the
10306
+ * session's first answer forever. A failed probe is NOT remembered: the next asker retries.
10307
+ */
10308
+ fetchSymbolInfo(resolved) {
10309
+ const key = symKey(resolved);
10310
+ const hit = this.symInfoCache.get(key);
10311
+ if (hit && Date.now() - hit.at < SYMBOL_INFO_TTL_MS) return Promise.resolve(hit.info);
10312
+ const inflight = this.symInfoInflight.get(key);
10313
+ if (inflight) return inflight;
10314
+ const getSymbolInfo = this.registry.get(resolved.provider)?.getSymbolInfo;
10315
+ if (!getSymbolInfo) return Promise.resolve(void 0);
10316
+ const p = Promise.resolve(getSymbolInfo.call(this.registry.get(resolved.provider), resolved.ticker)).then((info) => {
10317
+ if (info) this.symInfoCache.set(key, { info, at: Date.now() });
10318
+ return info;
10319
+ }).finally(() => {
10320
+ this.symInfoInflight.delete(key);
10321
+ });
10322
+ this.symInfoInflight.set(key, p);
10323
+ return p;
10295
10324
  }
10296
10325
  /**
10297
10326
  * Per-symbol capabilities, resolved through the owning provider's `capabilitiesFor`
@@ -10335,16 +10364,10 @@ var MultiProviderFeed = class {
10335
10364
  symbolInfo(cfg) {
10336
10365
  if (cfg.data && cfg.data.length > 0) return void 0;
10337
10366
  const resolved = this.registry.resolve(rawSymbol(cfg), { default: this.primaryProvider });
10338
- return resolved ? this.symInfoCache.get(symKey(resolved)) : void 0;
10367
+ return resolved ? this.symInfoCache.get(symKey(resolved))?.info : void 0;
10339
10368
  }
10340
10369
  prefetchSymbolInfo(resolved) {
10341
- const key = symKey(resolved);
10342
- if (this.symInfoCache.has(key)) return;
10343
- const provider = this.registry.get(resolved.provider);
10344
- if (!provider?.getSymbolInfo) return;
10345
- void provider.getSymbolInfo(resolved.ticker).then((info) => {
10346
- if (info) this.symInfoCache.set(key, info);
10347
- }).catch(() => {
10370
+ void this.fetchSymbolInfo(resolved).catch(() => {
10348
10371
  });
10349
10372
  }
10350
10373
  async loadRange(cfg, range) {
@@ -10394,8 +10417,9 @@ function canonical(cfg, resolved) {
10394
10417
  return { ...cfg, symbol: `${resolved.provider}:${resolved.ticker}` };
10395
10418
  }
10396
10419
  function symKey(resolved) {
10397
- return `${resolved.provider}|${resolved.ticker}`;
10420
+ return `${resolved.provider}|${resolved.ticker.trim().toUpperCase()}`;
10398
10421
  }
10422
+ var SYMBOL_INFO_TTL_MS = 10 * 6e4;
10399
10423
  async function safeBars(provider, ticker, tf, range) {
10400
10424
  try {
10401
10425
  return await provider.getBars(ticker, tf, range);
@@ -13145,7 +13169,8 @@ function addRecent(hex6) {
13145
13169
  recents.unshift(hex6);
13146
13170
  if (recents.length > 10) recents.length = 10;
13147
13171
  }
13148
- function buildColorPicker(color, theme, onChange) {
13172
+ function buildColorPicker(color, theme, onChange, options = {}) {
13173
+ const commit = options.commit ?? "live";
13149
13174
  const parsed = splitColor(color);
13150
13175
  let curHex = parsed.hex6;
13151
13176
  let curAlpha = parsed.alpha;
@@ -13228,7 +13253,12 @@ function buildColorPicker(color, theme, onChange) {
13228
13253
  const r = track.getBoundingClientRect();
13229
13254
  curAlpha = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
13230
13255
  paintOpacity();
13231
- emit();
13256
+ if (commit === "live") emit();
13257
+ };
13258
+ const endDrag = () => {
13259
+ if (!dragging) return;
13260
+ dragging = false;
13261
+ if (commit === "release") emit();
13232
13262
  };
13233
13263
  track.addEventListener("pointerdown", (e) => {
13234
13264
  e.stopPropagation();
@@ -13239,7 +13269,8 @@ function buildColorPicker(color, theme, onChange) {
13239
13269
  track.addEventListener("pointermove", (e) => {
13240
13270
  if (dragging) onDrag(e.clientX);
13241
13271
  });
13242
- track.addEventListener("pointerup", () => dragging = false);
13272
+ track.addEventListener("pointerup", endDrag);
13273
+ track.addEventListener("pointercancel", endDrag);
13243
13274
  function emit() {
13244
13275
  onChange(combineColor(curHex, curAlpha));
13245
13276
  }
@@ -13262,7 +13293,7 @@ function buildColorPicker(color, theme, onChange) {
13262
13293
  return root;
13263
13294
  }
13264
13295
  function colorField(theme, getVal, onVal, opts) {
13265
- return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover }).el;
13296
+ return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover, commit: opts?.commit }).el;
13266
13297
  }
13267
13298
  var ColorField = class {
13268
13299
  constructor(opts) {
@@ -13271,6 +13302,7 @@ var ColorField = class {
13271
13302
  this.getVal = opts.getVal;
13272
13303
  this.onVal = opts.onVal;
13273
13304
  this.popoverOpts = opts.popover;
13305
+ this.commit = opts.commit;
13274
13306
  const trigger = document.createElement("button");
13275
13307
  trigger.type = "button";
13276
13308
  if (opts.id) trigger.id = opts.id;
@@ -13310,10 +13342,15 @@ var ColorField = class {
13310
13342
  boundary: this.popoverOpts?.boundary,
13311
13343
  zIndex: this.popoverOpts?.zIndex,
13312
13344
  className: "vela-color-field-pop",
13313
- content: buildColorPicker(this.getVal(), this.theme, (val) => {
13314
- this.onVal(val);
13315
- this.paint();
13316
- })
13345
+ content: buildColorPicker(
13346
+ this.getVal(),
13347
+ this.theme,
13348
+ (val) => {
13349
+ this.onVal(val);
13350
+ this.paint();
13351
+ },
13352
+ { commit: this.commit }
13353
+ )
13317
13354
  });
13318
13355
  pop.show();
13319
13356
  }
@@ -13794,7 +13831,8 @@ function buildFieldControl(desc) {
13794
13831
  const el2 = colorField(desc.theme, desc.get, desc.onChange, {
13795
13832
  shape: "circle",
13796
13833
  id: desc.id,
13797
- popover: desc.popover
13834
+ popover: desc.popover,
13835
+ commit: desc.commit
13798
13836
  });
13799
13837
  if (desc.title) el2.title = desc.title;
13800
13838
  return { el: el2 };
@@ -14172,7 +14210,10 @@ var IndicatorInputsDialog = class {
14172
14210
  id,
14173
14211
  theme: this.host.theme(),
14174
14212
  get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
14175
- onChange: (v) => emit(v)
14213
+ onChange: (v) => emit(v),
14214
+ // An input change re-executes the script over its whole history: commit the
14215
+ // opacity drag once on release, not once per pointer move.
14216
+ commit: "release"
14176
14217
  }).el;
14177
14218
  }
14178
14219
  if (inp.type === "symbol") return this.buildSymbol(id, String(current), emit);
@@ -14419,10 +14460,6 @@ var IndicatorInputsDialog = class {
14419
14460
  }
14420
14461
  /** Open a themed month calendar under `anchor` — same surface + shadow as the choice list. */
14421
14462
  openCalendar(anchor, current, onPick) {
14422
- const parsed = parseIsoDate(current);
14423
- let year = parsed?.getFullYear() ?? (/* @__PURE__ */ new Date()).getFullYear();
14424
- let month = parsed?.getMonth() ?? (/* @__PURE__ */ new Date()).getMonth();
14425
- const selected = parsed ? isoDate(parsed) : current;
14426
14463
  const pop = new Popover({
14427
14464
  trigger: anchor,
14428
14465
  theme: this.host.theme(),
@@ -14434,80 +14471,10 @@ var IndicatorInputsDialog = class {
14434
14471
  this.calendarPop = null;
14435
14472
  this.calendarAnchor = null;
14436
14473
  },
14437
- content: (el) => {
14438
- const title = document.createElement("div");
14439
- title.className = "vela-ind-cal-title";
14440
- const prev2 = document.createElement("button");
14441
- prev2.type = "button";
14442
- prev2.className = "vela-ind-cal-nav";
14443
- prev2.setAttribute("aria-label", "Previous month");
14444
- prev2.innerHTML = iconAt("chevron-left", 14);
14445
- const next = document.createElement("button");
14446
- next.type = "button";
14447
- next.className = "vela-ind-cal-nav";
14448
- next.setAttribute("aria-label", "Next month");
14449
- next.innerHTML = iconAt("chevron-right", 14);
14450
- const head = document.createElement("div");
14451
- head.className = "vela-ind-cal-head";
14452
- head.append(prev2, title, next);
14453
- const week = document.createElement("div");
14454
- week.className = "vela-ind-cal-week";
14455
- for (const d of WEEKDAY_LABELS) {
14456
- const cell = document.createElement("span");
14457
- cell.textContent = d;
14458
- week.appendChild(cell);
14459
- }
14460
- const grid = document.createElement("div");
14461
- grid.className = "vela-ind-cal-grid";
14462
- const paint = () => {
14463
- title.textContent = `${MONTH_LABELS[month]} ${year}`;
14464
- grid.replaceChildren();
14465
- const first = new Date(year, month, 1);
14466
- const startPad = first.getDay();
14467
- const days = new Date(year, month + 1, 0).getDate();
14468
- const today = isoDate(/* @__PURE__ */ new Date());
14469
- for (let i = 0; i < startPad; i++) {
14470
- const blank = document.createElement("span");
14471
- blank.className = "vela-ind-cal-blank";
14472
- grid.appendChild(blank);
14473
- }
14474
- for (let day = 1; day <= days; day++) {
14475
- const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
14476
- const b = document.createElement("button");
14477
- b.type = "button";
14478
- b.className = "vela-ind-cal-day";
14479
- b.textContent = String(day);
14480
- if (iso === selected) b.dataset.checked = "1";
14481
- if (iso === today) b.dataset.today = "1";
14482
- b.addEventListener("click", (e) => {
14483
- e.stopPropagation();
14484
- pop.hide();
14485
- onPick(iso);
14486
- });
14487
- grid.appendChild(b);
14488
- }
14489
- };
14490
- prev2.addEventListener("click", (e) => {
14491
- e.stopPropagation();
14492
- month -= 1;
14493
- if (month < 0) {
14494
- month = 11;
14495
- year -= 1;
14496
- }
14497
- paint();
14498
- });
14499
- next.addEventListener("click", (e) => {
14500
- e.stopPropagation();
14501
- month += 1;
14502
- if (month > 11) {
14503
- month = 0;
14504
- year += 1;
14505
- }
14506
- paint();
14507
- });
14508
- paint();
14509
- el.append(head, week, grid);
14510
- }
14474
+ content: (el) => fillCalendar(el, current, (iso) => {
14475
+ pop.hide();
14476
+ onPick(iso);
14477
+ })
14511
14478
  });
14512
14479
  this.calendarPop = pop;
14513
14480
  this.calendarAnchor = anchor;
@@ -14616,7 +14583,7 @@ function groupInputs(inputs) {
14616
14583
  var CALENDAR_SVG = iconAt("calendar", 14);
14617
14584
  var CLOCK_SVG = iconAt("clock", 14);
14618
14585
  var DIALOG_STYLE_ID2 = "vela-ind-dialog-styles";
14619
- var DIALOG_STYLE_REV = "29";
14586
+ var DIALOG_STYLE_REV = "30";
14620
14587
  var LEGEND_ICON_PX = 16;
14621
14588
  function ensureDialogStyles() {
14622
14589
  if (typeof document === "undefined") return;
@@ -14632,8 +14599,12 @@ function ensureDialogStyles() {
14632
14599
  .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;}
14633
14600
  .vela-ind-combo-chevron:hover{opacity:0.9;}
14634
14601
  .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;}
14602
+ .vela-ind-cal [hidden]{display:none !important;}
14635
14603
  .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
14636
- .vela-ind-cal-title{flex:1;text-align:center;font-weight:600;font-size:14px;color:var(--vela-fg-bright);}
14604
+ .vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
14605
+ .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;}
14606
+ .vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
14607
+ .vela-ind-cal-switch:disabled{cursor:default;}
14637
14608
  .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;}
14638
14609
  .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
14639
14610
  .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
@@ -14644,6 +14615,12 @@ function ensureDialogStyles() {
14644
14615
  .vela-ind-cal-day:hover{background:var(--vela-hover);}
14645
14616
  .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
14646
14617
  .vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
14618
+ .vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
14619
+ .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;}
14620
+ .vela-ind-cal-cell:hover{background:var(--vela-hover);}
14621
+ .vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
14622
+ .vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
14623
+ .vela-ind-cal-cell[data-outside]{opacity:0.45;}
14647
14624
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
14648
14625
  .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;}
14649
14626
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
@@ -14678,6 +14655,7 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
14678
14655
  return { value: v, label: v };
14679
14656
  });
14680
14657
  var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
14658
+ var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
14681
14659
  var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
14682
14660
  function normalizeDateInput(raw) {
14683
14661
  const t = raw.trim();
@@ -14699,6 +14677,151 @@ function parseIsoDate(raw) {
14699
14677
  function isoDate(d) {
14700
14678
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
14701
14679
  }
14680
+ function fillCalendar(el, current, onPick) {
14681
+ const today = /* @__PURE__ */ new Date();
14682
+ const selected = parseIsoDate(current);
14683
+ let year = selected?.getFullYear() ?? today.getFullYear();
14684
+ let month = selected?.getMonth() ?? today.getMonth();
14685
+ let mode = "date";
14686
+ const prev2 = document.createElement("button");
14687
+ prev2.type = "button";
14688
+ prev2.className = "vela-ind-cal-nav";
14689
+ prev2.innerHTML = iconAt("chevron-left", 14);
14690
+ const next = document.createElement("button");
14691
+ next.type = "button";
14692
+ next.className = "vela-ind-cal-nav";
14693
+ next.innerHTML = iconAt("chevron-right", 14);
14694
+ const monthBtn = document.createElement("button");
14695
+ monthBtn.type = "button";
14696
+ monthBtn.className = "vela-ind-cal-switch";
14697
+ monthBtn.setAttribute("aria-label", "Choose month");
14698
+ const yearBtn = document.createElement("button");
14699
+ yearBtn.type = "button";
14700
+ yearBtn.className = "vela-ind-cal-switch";
14701
+ yearBtn.setAttribute("aria-label", "Choose year");
14702
+ const title = document.createElement("div");
14703
+ title.className = "vela-ind-cal-title";
14704
+ title.append(monthBtn, yearBtn);
14705
+ const head = document.createElement("div");
14706
+ head.className = "vela-ind-cal-head";
14707
+ head.append(prev2, title, next);
14708
+ const week = document.createElement("div");
14709
+ week.className = "vela-ind-cal-week";
14710
+ for (const d of WEEKDAY_LABELS) {
14711
+ const cell = document.createElement("span");
14712
+ cell.textContent = d;
14713
+ week.appendChild(cell);
14714
+ }
14715
+ const grid = document.createElement("div");
14716
+ const paint = () => {
14717
+ const decade = Math.floor(year / 10) * 10;
14718
+ monthBtn.hidden = mode !== "date";
14719
+ monthBtn.textContent = MONTH_LABELS[month] ?? "";
14720
+ yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
14721
+ yearBtn.disabled = mode === "year";
14722
+ week.hidden = mode !== "date";
14723
+ prev2.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
14724
+ next.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
14725
+ grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
14726
+ grid.replaceChildren();
14727
+ if (mode === "date") {
14728
+ const startPad = new Date(year, month, 1).getDay();
14729
+ const days = new Date(year, month + 1, 0).getDate();
14730
+ const todayIso = isoDate(today);
14731
+ const selectedIso = selected ? isoDate(selected) : "";
14732
+ for (let i = 0; i < startPad; i++) {
14733
+ const blank = document.createElement("span");
14734
+ blank.className = "vela-ind-cal-blank";
14735
+ grid.appendChild(blank);
14736
+ }
14737
+ for (let day = 1; day <= days; day++) {
14738
+ const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
14739
+ const b = document.createElement("button");
14740
+ b.type = "button";
14741
+ b.className = "vela-ind-cal-day";
14742
+ b.textContent = String(day);
14743
+ if (iso === selectedIso) b.dataset.checked = "1";
14744
+ if (iso === todayIso) b.dataset.today = "1";
14745
+ b.addEventListener("click", (e) => {
14746
+ e.stopPropagation();
14747
+ onPick(iso);
14748
+ });
14749
+ grid.appendChild(b);
14750
+ }
14751
+ return;
14752
+ }
14753
+ if (mode === "month") {
14754
+ for (let m = 0; m < 12; m++) {
14755
+ const b = document.createElement("button");
14756
+ b.type = "button";
14757
+ b.className = "vela-ind-cal-cell";
14758
+ b.textContent = MONTH_SHORT[m] ?? "";
14759
+ if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
14760
+ if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
14761
+ b.addEventListener("click", (e) => {
14762
+ e.stopPropagation();
14763
+ month = m;
14764
+ mode = "date";
14765
+ paint();
14766
+ });
14767
+ grid.appendChild(b);
14768
+ }
14769
+ return;
14770
+ }
14771
+ for (let y = decade - 1; y <= decade + 10; y++) {
14772
+ const b = document.createElement("button");
14773
+ b.type = "button";
14774
+ b.className = "vela-ind-cal-cell";
14775
+ b.textContent = String(y);
14776
+ if (y < decade || y > decade + 9) b.dataset.outside = "1";
14777
+ if (selected?.getFullYear() === y) b.dataset.checked = "1";
14778
+ if (today.getFullYear() === y) b.dataset.today = "1";
14779
+ b.addEventListener("click", (e) => {
14780
+ e.stopPropagation();
14781
+ year = y;
14782
+ mode = "month";
14783
+ paint();
14784
+ });
14785
+ grid.appendChild(b);
14786
+ }
14787
+ };
14788
+ const step = (dir) => {
14789
+ if (mode === "date") {
14790
+ month += dir;
14791
+ if (month < 0) {
14792
+ month = 11;
14793
+ year -= 1;
14794
+ }
14795
+ if (month > 11) {
14796
+ month = 0;
14797
+ year += 1;
14798
+ }
14799
+ } else {
14800
+ year += mode === "month" ? dir : dir * 10;
14801
+ }
14802
+ paint();
14803
+ };
14804
+ prev2.addEventListener("click", (e) => {
14805
+ e.stopPropagation();
14806
+ step(-1);
14807
+ });
14808
+ next.addEventListener("click", (e) => {
14809
+ e.stopPropagation();
14810
+ step(1);
14811
+ });
14812
+ monthBtn.addEventListener("click", (e) => {
14813
+ e.stopPropagation();
14814
+ mode = "month";
14815
+ paint();
14816
+ });
14817
+ yearBtn.addEventListener("click", (e) => {
14818
+ e.stopPropagation();
14819
+ mode = "year";
14820
+ paint();
14821
+ });
14822
+ paint();
14823
+ el.append(head, week, grid);
14824
+ }
14702
14825
  function normalizeTimeInput(raw) {
14703
14826
  const t = raw.trim();
14704
14827
  const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
@@ -15980,10 +16103,12 @@ var BASELINE_LEVEL_DEFAULT = 50;
15980
16103
  var PREMARKET_SHADE = withAlpha(WARNING, 0.08);
15981
16104
  var POSTMARKET_SHADE = withAlpha(ACCENT, 0.08);
15982
16105
  var EXTENDED_SHADE = POSTMARKET_SHADE;
16106
+ var DEFAULT_MARGINS = { top: 10, bottom: 10, right: 10 };
15983
16107
  function defaultChartStyle() {
15984
16108
  return {
15985
16109
  chartTypes: {},
15986
16110
  fontSize: 11,
16111
+ margins: { ...DEFAULT_MARGINS },
15987
16112
  gridVert: { visible: true, color: null },
15988
16113
  gridHorz: { visible: true, color: null },
15989
16114
  borderColor: null,
@@ -16107,6 +16232,12 @@ function clampLevel(v) {
16107
16232
  function clampSpacing(v) {
16108
16233
  return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
16109
16234
  }
16235
+ function clampMarginPct(v) {
16236
+ return v < 0 ? 0 : v > 40 ? 40 : v;
16237
+ }
16238
+ function clampMarginBars(v) {
16239
+ return Math.round(v < 0 ? 0 : v > 200 ? 200 : v);
16240
+ }
16110
16241
  function factoryResetConfig(factory, priceStyle = factory.series.style) {
16111
16242
  const bag = {};
16112
16243
  for (const t of chartTypes()) {
@@ -16155,6 +16286,7 @@ function mergeConfig(base, patch) {
16155
16286
  const ps = asObject(p.priceScale);
16156
16287
  const anim = asObject(p.animations);
16157
16288
  const panes = asObject(p.panes);
16289
+ const margins = asObject(p.margins);
16158
16290
  const trades = asObject(p.trades);
16159
16291
  const ts = asObject(p.timeScale);
16160
16292
  const marks = asObject(p.marks);
@@ -16214,6 +16346,11 @@ function mergeConfig(base, patch) {
16214
16346
  panes: {
16215
16347
  separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
16216
16348
  },
16349
+ margins: {
16350
+ top: isNum(margins.top) ? clampMarginPct(margins.top) : base.margins.top,
16351
+ bottom: isNum(margins.bottom) ? clampMarginPct(margins.bottom) : base.margins.bottom,
16352
+ right: isNum(margins.right) ? clampMarginBars(margins.right) : base.margins.right
16353
+ },
16217
16354
  trades: {
16218
16355
  visible: isBool(trades.visible) ? trades.visible : base.trades.visible,
16219
16356
  labels: isBool(trades.labels) ? trades.labels : base.trades.labels,
@@ -21584,6 +21721,10 @@ var BUILTIN_SETTINGS_IDS = [
21584
21721
  "canvas.grid",
21585
21722
  "canvas.grid.vertical",
21586
21723
  "canvas.grid.horizontal",
21724
+ "canvas.margins",
21725
+ "canvas.margins.top",
21726
+ "canvas.margins.bottom",
21727
+ "canvas.margins.right",
21587
21728
  "canvas.theme"
21588
21729
  ];
21589
21730
  function settingsIdCatalog(hostSections, markGroups = []) {
@@ -22064,7 +22205,7 @@ var SettingsDialog = class {
22064
22205
  if (hr.kind === "heading") body.append(this.sectionTitle(hr.label));
22065
22206
  else if (hr.kind === "toggle") body.append(this.boolRow(hr.label, hr.get(), (v) => hr.set(v)));
22066
22207
  else if (hr.kind === "color") body.append(this.colorRow(hr.label, hr.get(), (v) => hr.set(v)));
22067
- else body.append(this.selectRowLabeled(hr.label, hr.get(), hr.options.map((o) => [o, o]), (v) => hr.set(v)));
22208
+ else body.append(this.selectRowLabeled(hr.label, hr.get(), normalizeSelectOptions(hr.options), (v) => hr.set(v)));
22068
22209
  }
22069
22210
  }
22070
22211
  };
@@ -22114,6 +22255,10 @@ var SettingsDialog = class {
22114
22255
  body.append(sid(this.toggleRow("Horizontal", config.grid.horzLines.visible, (v) => this.emit({ grid: { horzLines: { visible: v } } }), [
22115
22256
  this.swatch(config.grid.horzLines.color, (v) => this.emit({ grid: { horzLines: { color: v } } }))
22116
22257
  ]), "canvas.grid.horizontal"));
22258
+ body.append(sid(this.sectionTitle("Margins"), "canvas.margins"));
22259
+ body.append(sid(this.numberRow("Top", config.margins.top, 0, 40, 1, (v) => this.emit({ margins: { top: v } }), "%"), "canvas.margins.top"));
22260
+ body.append(sid(this.numberRow("Bottom", config.margins.bottom, 0, 40, 1, (v) => this.emit({ margins: { bottom: v } }), "%"), "canvas.margins.bottom"));
22261
+ body.append(sid(this.numberRow("Right", config.margins.right, 0, 200, 1, (v) => this.emit({ margins: { right: v } }), "bars"), "canvas.margins.right"));
22117
22262
  if (this.themeControl) {
22118
22263
  const tc = this.themeControl;
22119
22264
  body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
@@ -22685,23 +22830,16 @@ var SettingsDialog = class {
22685
22830
  this.hintTips.push(dispose);
22686
22831
  return el;
22687
22832
  }
22688
- numberRow(label, value, min, max, step, onChange) {
22689
- return fieldRow({
22690
- label,
22691
- labelSize: "sm",
22692
- className: "vela-sd-row",
22693
- control: buildFieldControl({
22694
- kind: "number",
22695
- value,
22696
- min,
22697
- max,
22698
- step,
22699
- fill: false,
22700
- commit: "live",
22701
- steppers: true,
22702
- onChange
22703
- }).el
22704
- });
22833
+ /** `unit` (optional) trails the input as muted text — "%", "bars". */
22834
+ numberRow(label, value, min, max, step, onChange, unit) {
22835
+ const controls = [buildFieldControl({ kind: "number", value, min, max, step, fill: false, commit: "live", steppers: true, onChange }).el];
22836
+ if (unit) {
22837
+ const u = document.createElement("span");
22838
+ u.textContent = unit;
22839
+ u.style.cssText = "color:var(--vela-fg-muted);";
22840
+ controls.push(u);
22841
+ }
22842
+ return this.rowWith(label, controls);
22705
22843
  }
22706
22844
  /** A dropdown whose option values differ from their display labels. */
22707
22845
  selectRowLabeled(label, value, options, onChange) {
@@ -28220,9 +28358,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange)
28220
28358
  }
28221
28359
 
28222
28360
  // src/renderers/native/core/autoscale.ts
28223
- var MARGIN_TOP = 2 / 7;
28224
- var MARGIN_BOTTOM = 1 / 7;
28225
- function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log = false, offsetOf = () => 0) {
28361
+ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log = false, offsetOf = () => 0, margins = DEFAULT_MARGINS) {
28226
28362
  let min = Infinity;
28227
28363
  let max = -Infinity;
28228
28364
  const consider = (v) => {
@@ -28257,14 +28393,17 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
28257
28393
  const pad = Math.abs(min) * 0.1 || 1;
28258
28394
  return { min: min - pad, max: max + pad, log: log && min - pad > 0 };
28259
28395
  }
28396
+ const content = Math.max(0.1, 1 - (margins.top + margins.bottom) / 100);
28397
+ const above = margins.top / 100 / content;
28398
+ const below = margins.bottom / 100 / content;
28260
28399
  if (log && min > 0) {
28261
28400
  const lmin = Math.log(min);
28262
28401
  const lmax = Math.log(max);
28263
28402
  const lspan = lmax - lmin;
28264
- return { min: Math.exp(lmin - lspan * MARGIN_BOTTOM), max: Math.exp(lmax + lspan * MARGIN_TOP), log: true };
28403
+ return { min: Math.exp(lmin - lspan * below), max: Math.exp(lmax + lspan * above), log: true };
28265
28404
  }
28266
28405
  const span = max - min;
28267
- return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
28406
+ return { min: min - span * below, max: max + span * above };
28268
28407
  }
28269
28408
  function considerSeries(s, i0, i1, off, consider) {
28270
28409
  if (!seriesInScale(s)) return;
@@ -30091,6 +30230,7 @@ var NativeRenderer = class {
30091
30230
  intro: this.intro.style !== false
30092
30231
  },
30093
30232
  panes: { separatorColor: s.separatorColor ?? t.borderColor },
30233
+ margins: { ...s.margins },
30094
30234
  trades: {
30095
30235
  visible: this.scene.tradeMarkers.visible,
30096
30236
  labels: this.scene.tradeMarkers.labels,
@@ -30203,6 +30343,10 @@ var NativeRenderer = class {
30203
30343
  this.animAutoscale.toggle(next.animations.autoscale);
30204
30344
  this.intro = { style: next.animations.intro ? this.introOnStyle : false, duration: this.intro.duration || INTRO_DURATION_DEFAULT_MS };
30205
30345
  s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
30346
+ if (next.margins.right !== s.margins.right && this.coords.barCount > 0) {
30347
+ this.applyViewport({ barSpacing: this.coords.getViewport().barSpacing, rightOffset: next.margins.right });
30348
+ }
30349
+ s.margins = { ...next.margins };
30206
30350
  this.scene.tradeMarkers = {
30207
30351
  visible: next.trades.visible,
30208
30352
  labels: next.trades.labels,
@@ -30446,7 +30590,7 @@ var NativeRenderer = class {
30446
30590
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
30447
30591
  scrollToRealtime() {
30448
30592
  if (this.coords.barCount === 0) return;
30449
- this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
30593
+ this.glideRightOffset(this.scene.style.margins.right);
30450
30594
  }
30451
30595
  /** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
30452
30596
  * instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
@@ -32388,6 +32532,7 @@ var NativeRenderer = class {
32388
32532
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
32389
32533
  this.chrome.prepare(this.scene, this.coords, this.theme);
32390
32534
  const animating = this.animator.active;
32535
+ const margins = this.scene.style.margins;
32391
32536
  for (const pane of panes) {
32392
32537
  if (pane.manualScale) {
32393
32538
  pane.scaleTarget = pane.manualScale;
@@ -32403,7 +32548,7 @@ var NativeRenderer = class {
32403
32548
  if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
32404
32549
  }
32405
32550
  const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels) || !this.paneHasMeasurableContent(masterModels, dr));
32406
- pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
32551
+ pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
32407
32552
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
32408
32553
  pane.axisFormat = void 0;
32409
32554
  pane.axisBands = void 0;
@@ -32414,7 +32559,7 @@ var NativeRenderer = class {
32414
32559
  pane.axisFormat = "volume";
32415
32560
  }
32416
32561
  } else if (this.layerNativesOwnPane(pane, masterModels)) {
32417
- pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
32562
+ pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
32418
32563
  pane.percentBaseline = this.bars[i0]?.close ?? 0;
32419
32564
  if (masterModels.every((m) => m.paneAxis != null)) {
32420
32565
  pane.axisFormat = "none";
@@ -32443,7 +32588,7 @@ var NativeRenderer = class {
32443
32588
  continue;
32444
32589
  }
32445
32590
  const mdr = this.chrome.paneDrawingsRange([model], this.scene, false, vr);
32446
- sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id));
32591
+ sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id), margins);
32447
32592
  if (!animating || !sl.initialized) {
32448
32593
  sl.scale = { ...sl.scaleTarget };
32449
32594
  sl.initialized = true;
@@ -32484,13 +32629,13 @@ var NativeRenderer = class {
32484
32629
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
32485
32630
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
32486
32631
  const visibleBars = Math.min(n, 200);
32487
- const rightOffset = 6;
32632
+ const rightOffset = this.scene.style.margins.right;
32488
32633
  const v = this.clampViewport(w / ((visibleBars + rightOffset) * this.coords.spacingScale), rightOffset);
32489
32634
  this.coords.setViewport(v);
32490
32635
  this.targetBarSpacing = v.barSpacing;
32491
32636
  }
32492
32637
  /** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
32493
- * zoom (bar spacing), re-anchor the newest bars at the default right offset.
32638
+ * zoom (bar spacing), re-anchor the newest bars at the configured right margin.
32494
32639
  * `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
32495
32640
  * head may still be backfilling toward the previous depth, and raising the spacing
32496
32641
  * to its temporary bar count would lose the zoom this exists to keep. */
@@ -32499,7 +32644,7 @@ var NativeRenderer = class {
32499
32644
  this.panVelocity = 0;
32500
32645
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
32501
32646
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
32502
- const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
32647
+ const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: this.scene.style.margins.right };
32503
32648
  this.coords.setViewport(v);
32504
32649
  this.targetBarSpacing = v.barSpacing;
32505
32650
  }
@@ -33329,7 +33474,8 @@ var ClassicIndicator = class {
33329
33474
  lineStyle: plot.lineStyle ?? "solid",
33330
33475
  ...plot.base != null ? { base: plot.base } : {}
33331
33476
  },
33332
- ...plot.overlay ? { overlay: true } : {}
33477
+ ...plot.overlay ? { overlay: true } : {},
33478
+ ...plot.display ? { display: plot.display } : {}
33333
33479
  });
33334
33480
  });
33335
33481
  if (out.markers && out.markers.length > 0) {
@@ -33351,7 +33497,9 @@ var ClassicIndicator = class {
33351
33497
  paneId: "",
33352
33498
  fromSeriesId: from,
33353
33499
  toSeriesId: to,
33354
- color: band.color
33500
+ color: band.color,
33501
+ ...band.colors ? { colors: [...band.colors] } : {},
33502
+ ...band.gradient ? { gradient: [...band.gradient] } : {}
33355
33503
  }
33356
33504
  ];
33357
33505
  });
@@ -33509,6 +33657,16 @@ function stdev(v, len) {
33509
33657
  return Math.sqrt(sq / len);
33510
33658
  });
33511
33659
  }
33660
+ function meanDev(v, len) {
33661
+ return windowed(v, len, (w, from, to) => {
33662
+ let s = 0;
33663
+ for (let k = from; k <= to; k++) s += w[k];
33664
+ const mean = s / len;
33665
+ let dev = 0;
33666
+ for (let k = from; k <= to; k++) dev += Math.abs(w[k] - mean);
33667
+ return dev / len;
33668
+ });
33669
+ }
33512
33670
  function highest(v, len) {
33513
33671
  return windowed(v, len, (w, from, to) => {
33514
33672
  let m = -Infinity;
@@ -33640,7 +33798,7 @@ function stoch(v, hi, lo, len) {
33640
33798
  }
33641
33799
  return out;
33642
33800
  }
33643
- function linreg(v, len) {
33801
+ function linreg(v, len, offset = 0) {
33644
33802
  const sx = (len - 1) * len / 2;
33645
33803
  const sxx = (len - 1) * len * (2 * len - 1) / 6;
33646
33804
  const denom = len * sxx - sx * sx;
@@ -33654,9 +33812,101 @@ function linreg(v, len) {
33654
33812
  }
33655
33813
  const slope = denom === 0 ? 0 : (len * sxy - sx * sy) / denom;
33656
33814
  const intercept = (sy - slope * sx) / len;
33657
- return intercept + slope * (len - 1);
33815
+ return intercept + slope * (len - 1 - offset);
33658
33816
  });
33659
33817
  }
33818
+ function hma(v, len) {
33819
+ const n = Math.max(2, Math.trunc(len));
33820
+ const raw = sub(map(wma(v, Math.max(1, Math.trunc(n / 2))), (x) => 2 * x), wma(v, n));
33821
+ return wma(raw, Math.max(1, Math.round(Math.sqrt(n))));
33822
+ }
33823
+ function alma(v, len, offset, sigma) {
33824
+ const m = offset * (len - 1);
33825
+ const s = len / Math.max(sigma, 1e-9);
33826
+ const weights = new Array(len);
33827
+ let norm = 0;
33828
+ for (let i = 0; i < len; i++) {
33829
+ const d = i - m;
33830
+ const w = Math.exp(-(d * d) / (2 * s * s));
33831
+ weights[i] = w;
33832
+ norm += w;
33833
+ }
33834
+ return windowed(v, len, (win, from, to) => {
33835
+ let acc = 0;
33836
+ for (let k = from; k <= to; k++) acc += win[k] * weights[k - from];
33837
+ return norm === 0 ? Number.NaN : acc / norm;
33838
+ });
33839
+ }
33840
+ function dema(v, len) {
33841
+ const e1 = ema(v, len);
33842
+ return sub(map(e1, (x) => 2 * x), ema(e1, len));
33843
+ }
33844
+ function tema(v, len) {
33845
+ const e1 = ema(v, len);
33846
+ const e2 = ema(e1, len);
33847
+ const e3 = ema(e2, len);
33848
+ return zip(zip(map(e1, (x) => 3 * x), map(e2, (x) => 3 * x), (a, b) => a - b), e3, (a, b) => a + b);
33849
+ }
33850
+ function kama(v, len, fastAlpha = 2 / 3, slowAlpha = 2 / 31) {
33851
+ const direction = map(change(v, len), Math.abs);
33852
+ const volatility = sum(map(change(v), Math.abs), len);
33853
+ const out = new Array(v.length).fill(Number.NaN);
33854
+ let prev2 = Number.NaN;
33855
+ for (let i = 0; i < v.length; i++) {
33856
+ const x = v[i];
33857
+ if (!Number.isFinite(x)) continue;
33858
+ const d = direction[i];
33859
+ const w = volatility[i];
33860
+ if (!Number.isFinite(d) || !Number.isFinite(w)) {
33861
+ prev2 = Number.isFinite(prev2) ? prev2 : x;
33862
+ out[i] = prev2;
33863
+ continue;
33864
+ }
33865
+ const er = w === 0 ? 0 : d / w;
33866
+ const sc = (er * (fastAlpha - slowAlpha) + slowAlpha) ** 2;
33867
+ prev2 = Number.isFinite(prev2) ? prev2 + sc * (x - prev2) : x;
33868
+ out[i] = prev2;
33869
+ }
33870
+ return out;
33871
+ }
33872
+ function mcginley(v, len) {
33873
+ const out = new Array(v.length).fill(Number.NaN);
33874
+ let prev2 = Number.NaN;
33875
+ for (let i = 0; i < v.length; i++) {
33876
+ const x = v[i];
33877
+ if (!Number.isFinite(x)) continue;
33878
+ if (!Number.isFinite(prev2)) {
33879
+ prev2 = x;
33880
+ } else {
33881
+ const ratio = prev2 === 0 ? 0 : x / prev2;
33882
+ const denom = len * ratio ** 4;
33883
+ prev2 += denom === 0 ? 0 : (x - prev2) / denom;
33884
+ }
33885
+ out[i] = prev2;
33886
+ }
33887
+ return out;
33888
+ }
33889
+ function hamming(v, len, a0 = 0.54, a1 = 0.46) {
33890
+ if (len <= 1) return v.map((x) => Number.isFinite(x) ? x : Number.NaN);
33891
+ const weights = new Array(len);
33892
+ let norm = 0;
33893
+ for (let i = 0; i < len; i++) {
33894
+ const w = a0 - a1 * Math.cos(2 * Math.PI * i / (len - 1));
33895
+ weights[i] = w;
33896
+ norm += w;
33897
+ }
33898
+ return windowed(v, len, (win, from, to) => {
33899
+ let acc = 0;
33900
+ for (let k = from; k <= to; k++) acc += win[k] * weights[to - k];
33901
+ return norm === 0 ? Number.NaN : acc / norm;
33902
+ });
33903
+ }
33904
+ function cmo(v, len) {
33905
+ const d = change(v);
33906
+ const up = sum(map(d, (x) => Math.max(x, 0)), len);
33907
+ const dn = sum(map(d, (x) => Math.max(-x, 0)), len);
33908
+ return zip(up, dn, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
33909
+ }
33660
33910
  function swma(v) {
33661
33911
  const out = new Array(v.length).fill(Number.NaN);
33662
33912
  for (let i = 3; i < v.length; i++) {
@@ -33706,38 +33956,253 @@ function cumSum(v) {
33706
33956
  }
33707
33957
 
33708
33958
  // src/core/native-indicators/classics/shared.ts
33709
- function lengthInput(defval, key = "length", title = "Length", max = 5e3) {
33710
- return { key, title, type: "int", defval, min: 1, max, step: 1 };
33959
+ var SETTINGS = "Settings";
33960
+ var STYLE = "Style";
33961
+ function lengthInput(defval, key = "length", title = "Length", max = 5e3, tooltip) {
33962
+ return { key, title, type: "int", defval, min: 1, max, step: 1, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
33963
+ }
33964
+ function intInput(key, title, defval, min = 1, max = 5e3, tooltip) {
33965
+ return { key, title, type: "int", defval, min, max, step: 1, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
33711
33966
  }
33712
- function sourceInput(defval = "Close") {
33713
- return { key: "source", title: "Source", type: "string", defval, options: SOURCES2 };
33967
+ function floatInput(key, title, defval, min = 0, max = 1e3, step = 0.1, tooltip) {
33968
+ return { key, title, type: "float", defval, min, max, step, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
33714
33969
  }
33715
- function colorInput(defval = SERIES_LINE, title = "Color", key = "color") {
33716
- return { key, title, type: "color", defval };
33970
+ function boolInput(key, title, defval, group = SETTINGS, tooltip) {
33971
+ return { key, title, type: "bool", defval, group, ...tooltip != null ? { tooltip } : {} };
33972
+ }
33973
+ function optionInput(key, title, defval, options, tooltip) {
33974
+ return { key, title, type: "string", defval, options, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
33975
+ }
33976
+ function sourceInput(defval = "Close", key = "source", title = "Source", tooltip) {
33977
+ return { key, title, type: "string", defval, options: SOURCES2, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
33978
+ }
33979
+ function colorInput(defval = INFO, title = "Color", key = "color", tooltip) {
33980
+ return { key, title, type: "color", defval, group: STYLE, ...tooltip != null ? { tooltip } : {} };
33981
+ }
33982
+ function widthInput(defval = 2, key = "lineWidth", title = "Line Width") {
33983
+ return { key, title, type: "int", defval, min: 1, max: 5, step: 1, group: STYLE, tooltip: "Width of the plotted line, in pixels." };
33984
+ }
33985
+ function transp(hex, transparency) {
33986
+ return withAlpha2(hex, (100 - transparency) / 100);
33717
33987
  }
33718
33988
  function withAlpha2(hex, alpha = 0.08) {
33719
33989
  const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
33720
33990
  if (!m) return hex;
33721
- const a = Math.round(alpha * 255).toString(16).padStart(2, "0");
33991
+ const a = Math.round(Math.max(0, Math.min(1, alpha)) * 255).toString(16).padStart(2, "0");
33722
33992
  return `#${m[1]}${a}`;
33723
33993
  }
33994
+ function channelStyleInputs(basisTitle = "Basis") {
33995
+ return [
33996
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the lower band and of the basis while price holds above it."),
33997
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the upper band and of the basis while price sits below it."),
33998
+ colorInput(NEUTRAL, basisTitle, "basisColor", "Color of the basis line while price sits exactly on it.")
33999
+ ];
34000
+ }
34001
+ function directionalChannel(bars, basis, upper, lower, ink, key) {
34002
+ const basisColors = basis.map((b, i) => {
34003
+ if (!Number.isFinite(b)) return null;
34004
+ const c = bars[i].close;
34005
+ return c > b ? ink.bull : c < b ? ink.bear : ink.neutral;
34006
+ });
34007
+ const wash = upper.map((u, i) => {
34008
+ const l = lower[i];
34009
+ if (!Number.isFinite(u) || !Number.isFinite(l)) return null;
34010
+ return { topValue: u, bottomValue: l, topColor: transp(ink.bear, 90), bottomColor: transp(ink.bull, 90) };
34011
+ });
34012
+ return {
34013
+ plots: [
34014
+ { key: "basis", title: "Basis", values: [...basis], color: ink.neutral, colors: basisColors },
34015
+ { key: "upper", title: "Upper", values: [...upper], color: transp(ink.bear, 30) },
34016
+ { key: "lower", title: "Lower", values: [...lower], color: transp(ink.bull, 30) }
34017
+ ],
34018
+ bands: [{ key, from: "upper", to: "lower", color: transp(NEUTRAL, 90), gradient: wash }]
34019
+ };
34020
+ }
34021
+ function anchorPlot(key, title, length, level) {
34022
+ return {
34023
+ key,
34024
+ title,
34025
+ values: new Array(length).fill(level),
34026
+ color: transp(NEUTRAL, 100),
34027
+ display: { pane: false, legend: false, dataWindow: false }
34028
+ };
34029
+ }
34030
+ function thresholdZones(length, levels, ink, transparency = 92) {
34031
+ return {
34032
+ plots: [
34033
+ anchorPlot("obAnchor", "Overbought Anchor", length, levels.overbought),
34034
+ anchorPlot("osAnchor", "Oversold Anchor", length, levels.oversold),
34035
+ anchorPlot("topAnchor", "Top Anchor", length, levels.top),
34036
+ anchorPlot("bottomAnchor", "Bottom Anchor", length, levels.bottom)
34037
+ ],
34038
+ bands: [
34039
+ { key: "obZone", from: "obAnchor", to: "topAnchor", color: transp(ink.overbought, transparency) },
34040
+ { key: "osZone", from: "osAnchor", to: "bottomAnchor", color: transp(ink.oversold, transparency) }
34041
+ ]
34042
+ };
34043
+ }
34044
+ function rampColors(values, levels, ink) {
34045
+ return values.map((x) => {
34046
+ if (!Number.isFinite(x)) return null;
34047
+ return x <= levels.mid ? gradient(x, levels.low, levels.mid, ink.low, ink.mid) : gradient(x, levels.mid, levels.high, ink.mid, ink.high);
34048
+ });
34049
+ }
34050
+ function baselineGradient(values, baseline, inkOf, valueTransparency = 50, baseTransparency = 100) {
34051
+ return values.map((x, i) => {
34052
+ if (!Number.isFinite(x)) return null;
34053
+ const ink = inkOf(x, i);
34054
+ const above = x > baseline;
34055
+ return {
34056
+ topValue: Math.max(x, baseline),
34057
+ bottomValue: Math.min(x, baseline),
34058
+ topColor: transp(ink, above ? valueTransparency : baseTransparency),
34059
+ bottomColor: transp(ink, above ? baseTransparency : valueTransparency)
34060
+ };
34061
+ });
34062
+ }
34063
+ function momentumColumnColors(values, ink) {
34064
+ return values.map((x, i) => {
34065
+ if (!Number.isFinite(x)) return null;
34066
+ const prev2 = i > 0 && Number.isFinite(values[i - 1]) ? values[i - 1] : x;
34067
+ const rising = x > prev2;
34068
+ if (x >= 0) return rising ? ink.growAbove : ink.fallAbove;
34069
+ return rising ? ink.growBelow : ink.fallBelow;
34070
+ });
34071
+ }
34072
+ function boundedOscillator(opts) {
34073
+ const mid = (opts.overbought + opts.oversold) / 2;
34074
+ const ramp = rampColors(opts.values, { low: opts.oversold, mid, high: opts.overbought }, opts.ink);
34075
+ const zones = thresholdZones(
34076
+ opts.values.length,
34077
+ { overbought: opts.overbought, oversold: opts.oversold, top: opts.scale.top, bottom: opts.scale.bottom },
34078
+ { overbought: opts.ink.high, oversold: opts.ink.low },
34079
+ opts.zoneTransparency
34080
+ );
34081
+ const plots = [{ key: opts.key, title: opts.title, values: opts.values, color: opts.ink.mid, width: opts.width ?? 1, colors: ramp }, ...zones.plots];
34082
+ const bands = [...zones.bands];
34083
+ if (opts.bandFill != null) bands.push({ key: "band", from: "obAnchor", to: "osAnchor", color: opts.bandFill });
34084
+ if (opts.midpointWash) {
34085
+ plots.push(anchorPlot("midAnchor", "Midpoint Anchor", opts.values.length, mid));
34086
+ bands.push({ key: "wash", from: opts.key, to: "midAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(opts.values, mid, (_x, i) => ramp[i] ?? opts.ink.mid) });
34087
+ }
34088
+ const levels = [
34089
+ { key: "overbought", price: opts.overbought, color: opts.levelInk, lineStyle: "dashed", title: opts.levelTitles?.overbought ?? "Overbought" },
34090
+ { key: "oversold", price: opts.oversold, color: opts.levelInk, lineStyle: "dashed", title: opts.levelTitles?.oversold ?? "Oversold" }
34091
+ ];
34092
+ if (opts.midline != null) levels.splice(1, 0, { key: "midline", price: opts.midline, color: opts.levelInk, lineStyle: "dotted", title: "Midline" });
34093
+ return { plots, bands, levels };
34094
+ }
34095
+ function centeredOscillator(opts) {
34096
+ const baseline = opts.baseline ?? 0;
34097
+ const inkOf = (x) => x > baseline ? opts.ink.bull : x < baseline ? opts.ink.bear : opts.ink.neutral;
34098
+ const plots = [
34099
+ { key: opts.key, title: opts.title, values: opts.values, color: opts.ink.neutral, width: opts.width ?? 1, colors: opts.values.map((x) => Number.isFinite(x) ? inkOf(x) : null) },
34100
+ ...opts.extraPlots ?? []
34101
+ ];
34102
+ const bands = [];
34103
+ if (opts.fill !== false) {
34104
+ plots.push(anchorPlot("baseline", "Baseline Anchor", opts.values.length, baseline));
34105
+ bands.push({ key: "wash", from: opts.key, to: "baseline", color: transp(NEUTRAL, 100), gradient: baselineGradient(opts.values, baseline, inkOf, opts.valueTransparency) });
34106
+ }
34107
+ return {
34108
+ plots,
34109
+ bands,
34110
+ levels: [{ key: "zero", price: baseline, color: opts.zeroInk, lineStyle: opts.zeroLineStyle ?? "dashed", title: opts.zeroTitle ?? "Zero Line" }, ...opts.extraLevels ?? []]
34111
+ };
34112
+ }
34113
+ function channels(hex) {
34114
+ const m = /^#([0-9a-f]{6})/i.exec(hex.trim());
34115
+ if (!m) return null;
34116
+ const n = parseInt(m[1], 16);
34117
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
34118
+ }
34119
+ function gradient(value, low, high, from, to) {
34120
+ const a = channels(from);
34121
+ const b = channels(to);
34122
+ if (!a || !b || !Number.isFinite(value)) return from;
34123
+ const span = high - low;
34124
+ const t = span === 0 ? 0 : Math.max(0, Math.min(1, (value - low) / span));
34125
+ const mix2 = (i) => Math.round(a[i] + (b[i] - a[i]) * t).toString(16).padStart(2, "0");
34126
+ return `#${mix2(0)}${mix2(1)}${mix2(2)}`;
34127
+ }
33724
34128
 
33725
34129
  // src/core/native-indicators/classics/averages.ts
33726
- var MA_KINDS = ["SMA", "EMA", "WMA", "RMA", "VWMA"];
33727
- var offsetInput = { key: "offset", title: "Offset", type: "int", defval: 0, min: -500, max: 500, step: 1 };
34130
+ var MA_TYPES = ["SMA", "EMA", "WMA", "RMA (SMMA)", "HMA", "ALMA", "VWMA", "DEMA", "TEMA", "KAMA", "LSMA", "McGinley", "Hamming"];
34131
+ var ENVELOPE_MA_TYPES = ["SMA", "EMA", "WMA", "RMA (SMMA)", "HMA", "VWMA"];
34132
+ function movingAverageOf(kind, src, bars, len, opts = {}) {
34133
+ switch (kind) {
34134
+ case "EMA":
34135
+ return ema(src, len);
34136
+ case "WMA":
34137
+ return wma(src, len);
34138
+ case "RMA":
34139
+ case "RMA (SMMA)":
34140
+ return rma(src, len);
34141
+ case "HMA":
34142
+ return hma(src, len);
34143
+ case "ALMA":
34144
+ return alma(src, len, opts.almaOffset ?? 0.85, opts.almaSigma ?? 6);
34145
+ case "VWMA":
34146
+ return vwma(src, volumes(bars), len);
34147
+ case "DEMA":
34148
+ return dema(src, len);
34149
+ case "TEMA":
34150
+ return tema(src, len);
34151
+ case "KAMA":
34152
+ return kama(src, len);
34153
+ case "LSMA":
34154
+ return linreg(src, len, opts.lsmaOffset ?? 0);
34155
+ case "McGinley":
34156
+ return mcginley(src, len);
34157
+ case "Hamming":
34158
+ return hamming(src, len);
34159
+ default:
34160
+ return sma(src, len);
34161
+ }
34162
+ }
34163
+ function slopeColors(values, bull, bear, flat) {
34164
+ return values.map((x, i) => {
34165
+ if (!Number.isFinite(x)) return null;
34166
+ const prev2 = i > 0 ? values[i - 1] : Number.NaN;
34167
+ if (!Number.isFinite(prev2)) return flat;
34168
+ return x > prev2 ? bull : x < prev2 ? bear : flat;
34169
+ });
34170
+ }
34171
+ function directionInputs() {
34172
+ return [
34173
+ boolInput("trendColor", "Slope Trend Coloring", true, STYLE, "Colors the line by its slope \u2014 bullish while it rises, bearish while it falls. Disable to draw it in the neutral color."),
34174
+ colorInput(BULLISH, "Bullish", "bullColor"),
34175
+ colorInput(BEARISH, "Bearish", "bearColor"),
34176
+ colorInput(NEUTRAL, "Neutral", "neutralColor"),
34177
+ widthInput()
34178
+ ];
34179
+ }
34180
+ function slopeColoredPlot(key, title, values, inputs) {
34181
+ const neutral = str3(inputs, "neutralColor", NEUTRAL);
34182
+ const colored = bool(inputs, "trendColor", true);
34183
+ return {
34184
+ key,
34185
+ title,
34186
+ values,
34187
+ color: neutral,
34188
+ width: num3(inputs, "lineWidth", 2),
34189
+ ...colored ? { colors: slopeColors(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), neutral) } : {}
34190
+ };
34191
+ }
34192
+ var offsetInput = { key: "offset", title: "Offset", type: "int", defval: 0, min: -500, max: 500, step: 1, group: SETTINGS, tooltip: "Bars the plotted line is displaced by. Positive values push it to the right." };
33728
34193
  function fixedAverage(type, title, shortTitle, smooth) {
33729
34194
  return {
33730
34195
  type,
33731
34196
  title,
33732
34197
  shortTitle,
33733
34198
  overlay: true,
33734
- inputs: [lengthInput(20), sourceInput(), offsetInput, colorInput()],
34199
+ inputs: [lengthInput(20, "length", "Length", 5e3, `Number of bars used in the ${shortTitle} calculation.`), sourceInput("Close", "source", "Source", `Price series the ${shortTitle} is calculated on.`), offsetInput, colorInput()],
33735
34200
  compute: (bars, inputs) => {
33736
34201
  const len = num3(inputs, "length", 20);
33737
34202
  let values = smooth(sourceValues(bars, str3(inputs, "source", "Close")), len);
33738
34203
  const offset = Math.trunc(num3(inputs, "offset", 0));
33739
34204
  if (offset !== 0) values = shift(values, offset);
33740
- return { plots: [{ key: shortTitle.toLowerCase(), title: shortTitle, values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
34205
+ return { plots: [{ key: shortTitle.toLowerCase(), title: shortTitle, values, color: str3(inputs, "color", INFO), width: 2 }] };
33741
34206
  }
33742
34207
  };
33743
34208
  }
@@ -33749,36 +34214,37 @@ var movingAverage = {
33749
34214
  shortTitle: "MA",
33750
34215
  overlay: true,
33751
34216
  inputs: [
33752
- { key: "maType", title: "Type", type: "string", defval: "SMA", options: MA_KINDS },
33753
- lengthInput(20),
33754
- sourceInput(),
34217
+ optionInput("maType", "MA 1 Type", "SMA", MA_TYPES, "Moving average calculation used for MA 1."),
34218
+ lengthInput(20, "length", "MA 1 Length", 5e3, "Number of bars used in the MA 1 calculation."),
34219
+ sourceInput("Close", "source", "MA 1 Source", "Price series MA 1 is calculated on."),
34220
+ boolInput("showMa2", "Show MA 2", false, SETTINGS, "Displays the optional second moving average."),
34221
+ optionInput("ma2Type", "MA 2 Type", "SMA", MA_TYPES, "Moving average calculation used for MA 2."),
34222
+ lengthInput(50, "ma2Length", "MA 2 Length", 5e3, "Number of bars used in the MA 2 calculation."),
34223
+ sourceInput("Close", "ma2Source", "MA 2 Source", "Price series MA 2 is calculated on."),
34224
+ floatInput("almaOffset", "ALMA Offset", 0.85, 0, 1, 0.05, "ALMA type only. Values near 1 make the average more responsive, values near 0 make it smoother."),
34225
+ floatInput("almaSigma", "ALMA Sigma", 6, 0.1, 100, 0.5, "ALMA type only. Larger values widen the Gaussian window for a smoother average."),
34226
+ intInput("lsmaOffset", "LSMA Offset", 0, 0, 500, "LSMA type only. Bar offset applied to the linear regression value."),
33755
34227
  offsetInput,
33756
- colorInput()
34228
+ colorInput(INFO, "MA 1", "color", "MA 1 line color."),
34229
+ colorInput(WARNING, "MA 2", "ma2Color", "MA 2 line color.")
33757
34230
  ],
33758
34231
  compute: (bars, inputs) => {
33759
- const len = num3(inputs, "length", 20);
33760
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
34232
+ const opts = {
34233
+ almaOffset: num3(inputs, "almaOffset", 0.85),
34234
+ almaSigma: num3(inputs, "almaSigma", 6),
34235
+ lsmaOffset: Math.trunc(num3(inputs, "lsmaOffset", 0))
34236
+ };
34237
+ const offset = Math.trunc(num3(inputs, "offset", 0));
34238
+ const displace = (v) => offset === 0 ? v : shift(v, offset);
33761
34239
  const kind = str3(inputs, "maType", "SMA");
33762
- let values;
33763
- switch (kind) {
33764
- case "EMA":
33765
- values = ema(src, len);
33766
- break;
33767
- case "WMA":
33768
- values = wma(src, len);
33769
- break;
33770
- case "RMA":
33771
- values = rma(src, len);
33772
- break;
33773
- case "VWMA":
33774
- values = vwma(src, volumes(bars), len);
33775
- break;
33776
- default:
33777
- values = sma(src, len);
34240
+ const ma1 = displace(movingAverageOf(kind, sourceValues(bars, str3(inputs, "source", "Close")), bars, num3(inputs, "length", 20), opts));
34241
+ const plots = [{ key: "ma", title: kind, values: ma1, color: str3(inputs, "color", INFO), width: 2 }];
34242
+ if (bool(inputs, "showMa2", false)) {
34243
+ const kind2 = str3(inputs, "ma2Type", "SMA");
34244
+ const ma2 = displace(movingAverageOf(kind2, sourceValues(bars, str3(inputs, "ma2Source", "Close")), bars, num3(inputs, "ma2Length", 50), opts));
34245
+ plots.push({ key: "ma2", title: `${kind2} 2`, values: ma2, color: str3(inputs, "ma2Color", WARNING), width: 2 });
33778
34246
  }
33779
- const offset = Math.trunc(num3(inputs, "offset", 0));
33780
- if (offset !== 0) values = shift(values, offset);
33781
- return { plots: [{ key: "ma", title: kind, values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
34247
+ return { plots };
33782
34248
  }
33783
34249
  };
33784
34250
  var smoothedMa = {
@@ -33786,17 +34252,13 @@ var smoothedMa = {
33786
34252
  title: "Smoothed Moving Average",
33787
34253
  shortTitle: "RMA",
33788
34254
  overlay: true,
33789
- inputs: [lengthInput(14), sourceInput(), colorInput()],
34255
+ inputs: [
34256
+ sourceInput("Close", "source", "Source", "Price series the average smooths. Wilder computed his 1978 indicators on the close, but the recursion accepts any source."),
34257
+ lengthInput(14, "length", "RMA Length", 5e3, "Number of bars N in Wilder's smoothing: the line keeps (N \u2212 1)/N of its previous value and blends in 1/N of the new price. That alpha is smaller than an EMA's, so an N-period RMA responds like a (2N \u2212 1)-period EMA."),
34258
+ ...directionInputs()
34259
+ ],
33790
34260
  compute: (bars, inputs) => ({
33791
- plots: [
33792
- {
33793
- key: "rma",
33794
- title: "RMA",
33795
- values: rma(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
33796
- color: str3(inputs, "color", SERIES_LINE),
33797
- width: 2
33798
- }
33799
- ]
34261
+ plots: [slopeColoredPlot("rma", "RMA", rma(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)), inputs)]
33800
34262
  })
33801
34263
  };
33802
34264
  var zlema = {
@@ -33804,61 +34266,86 @@ var zlema = {
33804
34266
  title: "Zero-Lag Exponential Moving Average",
33805
34267
  shortTitle: "ZLEMA",
33806
34268
  overlay: true,
33807
- inputs: [lengthInput(14), sourceInput(), colorInput()],
34269
+ inputs: [
34270
+ sourceInput("Close", "source", "Source", "Price series the average de-lags and then smooths."),
34271
+ lengthInput(21, "length", "ZLEMA Length", 5e3, "EMA length N. It sets both the smoothing (alpha = 2 / (N + 1)) and the correction: an EMA of length N lags by about (N \u2212 1)/2 bars, and the de-lagging step is sized to cancel exactly that."),
34272
+ ...directionInputs()
34273
+ ],
33808
34274
  compute: (bars, inputs) => {
33809
- const len = num3(inputs, "length", 14);
34275
+ const len = num3(inputs, "length", 21);
33810
34276
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
33811
- const lag = Math.floor((len - 1) / 2);
33812
- const lagged = shift(src, lag);
33813
- const delagged = zip(src, lagged, (a, b) => 2 * a - b);
33814
- return { plots: [{ key: "zlema", title: "ZLEMA", values: ema(delagged, len), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
34277
+ const lag = Math.round((len - 1) / 2);
34278
+ const delagged = zip(src, shift(src, lag), (a, b) => 2 * a - b);
34279
+ return { plots: [slopeColoredPlot("zlema", "ZLEMA", ema(delagged, len), inputs)] };
33815
34280
  }
33816
34281
  };
34282
+ var MOMENTUM_SCALER = "Momentum (|CMO|)";
34283
+ var VOLATILITY_SCALER = "Volatility (StDev Ratio)";
34284
+ var REGIME = "Trend Regime";
33817
34285
  var vidya = {
33818
34286
  type: "vidya",
33819
34287
  title: "Variable Index Dynamic Average",
33820
34288
  shortTitle: "VIDYA",
33821
34289
  overlay: true,
33822
- inputs: [lengthInput(14), { key: "cmoLength", title: "Momentum length", type: "int", defval: 9, min: 1, max: 500, step: 1 }, sourceInput(), colorInput()],
34290
+ inputs: [
34291
+ sourceInput("Close", "source", "Source", "Price series the average smooths. VIDYA is classically computed on the close."),
34292
+ lengthInput(12, "length", "VIDYA Length", 5e3, "EMA-equivalent length N of the base smoothing constant, alpha = 2 / (N + 1). The adaptive scaler rescales this base speed bar by bar."),
34293
+ optionInput("scaler", "Adaptive Scaler", MOMENTUM_SCALER, [MOMENTUM_SCALER, VOLATILITY_SCALER], "Bar-by-bar measure that rescales the smoothing constant. Momentum divides the absolute Chande Momentum Oscillator by 100; Volatility is Chande's original short-term over reference standard deviation."),
34294
+ intInput("scalerLength", "Scaler Lookback", 9, 1, 5e3, "Lookback of the adaptive scaler \u2014 the CMO period in momentum mode, or the short-term standard deviation window in volatility mode."),
34295
+ intInput("refLength", "Reference StDev Length", 30, 2, 5e3, "Volatility mode only \u2014 window of the reference standard deviation the short-term reading is divided by."),
34296
+ { ...floatInput("flatThreshold", "Flat Threshold", 0.05, 0, 10, 0.01, "One-bar change of VIDYA, as a fraction of ATR, below which the line reads as flat."), group: REGIME },
34297
+ { ...intInput("atrLength", "ATR Normalization Length", 14, 1, 5e3, "ATR window used to normalize VIDYA's one-bar slope, so the flat threshold behaves consistently across symbols and timeframes."), group: REGIME },
34298
+ boolInput("trendColor", "Trend Regime Coloring", true, STYLE, "Colors VIDYA by its regime read \u2014 bullish while sloping up beyond the flat threshold, bearish while sloping down, flat inside congestion."),
34299
+ boolInput("adaptiveGradient", "Adaptive Gradient", true, STYLE, "Blends the trend color toward the flat color according to the adaptive scaler, so the line pales as momentum dies."),
34300
+ colorInput(BULLISH, "Bullish", "bullColor"),
34301
+ colorInput(BEARISH, "Bearish", "bearColor"),
34302
+ colorInput(NEUTRAL, "Flat", "flatColor"),
34303
+ colorInput(NEUTRAL, "Neutral", "neutralColor"),
34304
+ widthInput()
34305
+ ],
33823
34306
  compute: (bars, inputs) => {
33824
- const len = num3(inputs, "length", 14);
33825
- const cmoLen = num3(inputs, "cmoLength", 9);
34307
+ const len = num3(inputs, "length", 12);
33826
34308
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
33827
- const d = change(src);
33828
- const su = new Array(src.length).fill(Number.NaN);
33829
- const sd = new Array(src.length).fill(Number.NaN);
33830
- for (let i = cmoLen; i < src.length; i++) {
33831
- let u = 0;
33832
- let w = 0;
33833
- let ok = true;
33834
- for (let k = i - cmoLen + 1; k <= i; k++) {
33835
- const x = d[k];
33836
- if (!Number.isFinite(x)) {
33837
- ok = false;
33838
- break;
33839
- }
33840
- if (x > 0) u += x;
33841
- else w -= x;
33842
- }
33843
- if (ok) {
33844
- su[i] = u;
33845
- sd[i] = w;
33846
- }
33847
- }
33848
- const alpha = 2 / (len + 1);
34309
+ const scalerLen = num3(inputs, "scalerLength", 9);
34310
+ const momentum = map(cmo(src, scalerLen), (x) => Math.abs(x) / 100);
34311
+ const shortDev = stdev(src, scalerLen);
34312
+ const refDev = stdev(src, num3(inputs, "refLength", 30));
34313
+ const volatility = zip(shortDev, refDev, (s, r) => r > 0 ? s / r : 0);
34314
+ const scale = str3(inputs, "scaler", MOMENTUM_SCALER) === VOLATILITY_SCALER ? volatility : momentum;
34315
+ const alphaBase = 2 / (len + 1);
33849
34316
  const out = new Array(src.length).fill(Number.NaN);
33850
34317
  let prev2 = Number.NaN;
33851
34318
  for (let i = 0; i < src.length; i++) {
33852
34319
  const x = src[i];
33853
- const u = su[i];
33854
- const w = sd[i];
33855
- if (!Number.isFinite(x) || !Number.isFinite(u) || !Number.isFinite(w)) continue;
33856
- const total = u + w;
33857
- const k = total === 0 ? 0 : Math.abs((u - w) / total);
33858
- prev2 = Number.isFinite(prev2) ? alpha * k * x + (1 - alpha * k) * prev2 : x;
34320
+ if (!Number.isFinite(x)) continue;
34321
+ const k = scale[i];
34322
+ if (!Number.isFinite(prev2) || !Number.isFinite(k)) prev2 = x;
34323
+ else {
34324
+ const alpha = Math.min(alphaBase * k, 1);
34325
+ prev2 = alpha * x + (1 - alpha) * prev2;
34326
+ }
33859
34327
  out[i] = prev2;
33860
34328
  }
33861
- return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
34329
+ const neutral = str3(inputs, "neutralColor", NEUTRAL);
34330
+ if (!bool(inputs, "trendColor", true)) {
34331
+ return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: neutral, width: num3(inputs, "lineWidth", 2) }] };
34332
+ }
34333
+ const bull = str3(inputs, "bullColor", BULLISH);
34334
+ const bear = str3(inputs, "bearColor", BEARISH);
34335
+ const flat = str3(inputs, "flatColor", NEUTRAL);
34336
+ const threshold = num3(inputs, "flatThreshold", 0.05);
34337
+ const range = atr(bars, num3(inputs, "atrLength", 14));
34338
+ const fade = bool(inputs, "adaptiveGradient", true);
34339
+ const colors = out.map((x, i) => {
34340
+ if (!Number.isFinite(x)) return null;
34341
+ const r = range[i];
34342
+ const slope = i > 0 && Number.isFinite(out[i - 1]) ? x - out[i - 1] : 0;
34343
+ const normalized = Number.isFinite(r) && r !== 0 ? slope / r : 0;
34344
+ const ink = normalized > threshold ? bull : normalized < -threshold ? bear : null;
34345
+ if (ink == null) return flat;
34346
+ return fade ? gradient(Math.min(scale[i] ?? 0, 1), 0, 1, flat, ink) : ink;
34347
+ });
34348
+ return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: flat, width: num3(inputs, "lineWidth", 2), colors }] };
33862
34349
  }
33863
34350
  };
33864
34351
  var maEnvelope = {
@@ -33867,115 +34354,218 @@ var maEnvelope = {
33867
34354
  shortTitle: "MA Env",
33868
34355
  overlay: true,
33869
34356
  inputs: [
33870
- { key: "maType", title: "Type", type: "string", defval: "SMA", options: ["SMA", "EMA"] },
33871
- lengthInput(20),
33872
- sourceInput(),
33873
- { key: "percent", title: "Percent", type: "float", defval: 2.5, min: 0.01, max: 50, step: 0.1 },
33874
- colorInput()
34357
+ optionInput("maType", "MA Type", "SMA", ENVELOPE_MA_TYPES, "Moving average type used as the envelope basis."),
34358
+ { ...lengthInput(20, "length", "Length", 5e3, "Number of bars in the basis moving average."), min: 2 },
34359
+ sourceInput("Close", "source", "Source", "Price series the basis moving average is calculated on."),
34360
+ floatInput("percent", "Percent", 2.5, 0, 50, 0.1, "Envelope offset as a percentage of the basis. Upper = basis \xD7 (1 + percent / 100), lower = basis \xD7 (1 \u2212 percent / 100)."),
34361
+ ...channelStyleInputs()
33875
34362
  ],
33876
34363
  compute: (bars, inputs) => {
33877
- const len = num3(inputs, "length", 20);
33878
34364
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
33879
- const basis = str3(inputs, "maType", "SMA") === "EMA" ? ema(src, len) : sma(src, len);
34365
+ const basis = movingAverageOf(str3(inputs, "maType", "SMA"), src, bars, num3(inputs, "length", 20));
33880
34366
  const k = num3(inputs, "percent", 2.5) / 100;
33881
- const ink = str3(inputs, "color", SERIES_LINE);
33882
- return {
33883
- plots: [
33884
- { key: "basis", title: "Basis", values: basis, color: ink, width: 2 },
33885
- { key: "upper", title: "Upper", values: map(basis, (x) => x * (1 + k)), color: ink },
33886
- { key: "lower", title: "Lower", values: map(basis, (x) => x * (1 - k)), color: ink }
33887
- ],
33888
- bands: [{ key: "envelope", from: "upper", to: "lower", color: withAlpha2(ink) }]
33889
- };
34367
+ const ink = { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "basisColor", NEUTRAL) };
34368
+ return directionalChannel(bars, basis, map(basis, (x) => x * (1 + k)), map(basis, (x) => x * (1 - k)), ink, "envelope");
33890
34369
  }
33891
34370
  };
34371
+ var LINREG_CURVE = "Curve";
34372
+ var LINREG_SLOPE = "Slope";
33892
34373
  var linearRegression = {
33893
34374
  type: "linear-regression",
33894
34375
  title: "Linear Regression Curve",
33895
34376
  shortTitle: "LinReg",
33896
- overlay: true,
33897
- inputs: [lengthInput(14), sourceInput(), colorInput()],
33898
- compute: (bars, inputs) => ({
33899
- plots: [
33900
- {
33901
- key: "linreg",
33902
- title: "LinReg",
33903
- values: linreg(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
33904
- color: str3(inputs, "color", SERIES_LINE),
33905
- width: 2
33906
- }
33907
- ]
33908
- })
34377
+ // Slope lives in its own pane; Curve force-overlays itself onto the price candles.
34378
+ overlay: false,
34379
+ inputs: [
34380
+ optionInput("mode", "Mode", LINREG_CURVE, [LINREG_CURVE, LINREG_SLOPE], "Curve plots the regression value on the price chart; Slope plots the per-bar slope of the fit in the pane."),
34381
+ { ...lengthInput(100, "length", "Length", 5e3, "Number of bars used to fit the linear regression."), min: 2 },
34382
+ sourceInput("Close", "source", "Source", "Price series the regression is fitted to."),
34383
+ colorInput(BULLISH, "Rising", "bullColor", "Color of the regression curve and slope line while the fit slopes upward."),
34384
+ colorInput(BEARISH, "Falling", "bearColor", "Color of the regression curve and slope line while the fit slopes downward."),
34385
+ colorInput(NEUTRAL, "Regression Line", "lineColor", "Color of the regression curve and slope line while the fit is flat.")
34386
+ ],
34387
+ compute: (bars, inputs) => {
34388
+ const len = num3(inputs, "length", 100);
34389
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
34390
+ const curve = linreg(src, len, 0);
34391
+ const slope = zip(curve, linreg(src, len, 1), (a, b) => a - b);
34392
+ const bull = str3(inputs, "bullColor", BULLISH);
34393
+ const bear = str3(inputs, "bearColor", BEARISH);
34394
+ const flat = str3(inputs, "lineColor", NEUTRAL);
34395
+ const colors = slope.map((s) => Number.isFinite(s) ? s > 0 ? bull : s < 0 ? bear : flat : null);
34396
+ if (str3(inputs, "mode", LINREG_CURVE) === LINREG_SLOPE) {
34397
+ return {
34398
+ plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
34399
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
34400
+ };
34401
+ }
34402
+ return { plots: [{ key: "curve", title: "Curve", values: curve, color: flat, width: 2, colors, overlay: true }] };
34403
+ }
33909
34404
  };
33910
34405
  var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope, linearRegression];
33911
34406
 
33912
34407
  // src/core/native-indicators/classics/bands.ts
34408
+ var JAW_COLOR = "#ab47bc";
34409
+ var TEETH_COLOR = WARNING;
34410
+ var LIPS_COLOR = "#ffd700";
33913
34411
  function bollinger(bars, inputs) {
33914
34412
  const len = num3(inputs, "length", 20);
33915
34413
  const mult = num3(inputs, "mult", 2);
33916
34414
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
33917
34415
  const basis = sma(src, len);
33918
34416
  const dev = map(stdev(src, len), (x) => x * mult);
33919
- return { basis, upper: zip(basis, dev, (b, d) => b + d), lower: zip(basis, dev, (b, d) => b - d) };
34417
+ return { src, basis, upper: zip(basis, dev, (b, d) => b + d), lower: zip(basis, dev, (b, d) => b - d) };
34418
+ }
34419
+ function bollingerInputs(multMin, multStep) {
34420
+ return [
34421
+ { ...lengthInput(20, "length", "Length", 5e3, "Number of bars used for the Bollinger Bands basis (a simple moving average) and for the standard deviation."), min: 2 },
34422
+ sourceInput("Close", "source", "Source", "Price series the bands are computed from."),
34423
+ floatInput("mult", "Multiplier", 2, multMin, 50, multStep, "Standard deviation multiplier for the upper and lower bands.")
34424
+ ];
33920
34425
  }
33921
- var BB_INPUTS = [lengthInput(20), sourceInput(), { key: "mult", title: "StdDev", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 }];
33922
34426
  var bollingerBands = {
33923
34427
  type: "bollinger-bands",
33924
34428
  title: "Bollinger Bands",
33925
34429
  shortTitle: "BB",
33926
34430
  overlay: true,
33927
- inputs: [...BB_INPUTS, colorInput()],
34431
+ inputs: [
34432
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used to calculate the basis SMA and the standard deviation."),
34433
+ sourceInput("Close", "source", "Source", "Price series the bands are calculated on."),
34434
+ floatInput("mult", "Multiplier", 2, 0, 50, 0.1, "Number of standard deviations added above and below the basis to form the bands."),
34435
+ colorInput(WARNING, "Basis", "basisColor", "Color of the basis (middle band) moving average."),
34436
+ colorInput(INFO, "Bands", "color", "Color of the upper and lower bands."),
34437
+ colorInput(transp(INFO, 92), "Fill", "fillColor", "Fill color of the area between the upper and lower bands.")
34438
+ ],
33928
34439
  compute: (bars, inputs) => {
33929
34440
  const { basis, upper, lower } = bollinger(bars, inputs);
33930
- const ink = str3(inputs, "color", SERIES_LINE);
34441
+ const ink = str3(inputs, "color", INFO);
33931
34442
  return {
33932
34443
  plots: [
33933
- { key: "basis", title: "Basis", values: basis, color: WARNING },
34444
+ { key: "basis", title: "Basis", values: basis, color: str3(inputs, "basisColor", WARNING) },
33934
34445
  { key: "upper", title: "Upper", values: upper, color: ink },
33935
34446
  { key: "lower", title: "Lower", values: lower, color: ink }
33936
34447
  ],
33937
- bands: [{ key: "bb", from: "upper", to: "lower", color: withAlpha2(ink) }]
34448
+ bands: [{ key: "bb", from: "upper", to: "lower", color: str3(inputs, "fillColor", transp(INFO, 92)) }]
33938
34449
  };
33939
34450
  }
33940
34451
  };
34452
+ var PERCENT_B_MID = 0.5;
34453
+ var THRESHOLDS = "Thresholds";
33941
34454
  var percentB = {
33942
34455
  type: "percent-b",
33943
34456
  title: "Bollinger Bands %B",
33944
34457
  shortTitle: "%B",
33945
34458
  overlay: false,
33946
- inputs: [...BB_INPUTS, colorInput()],
34459
+ inputs: [
34460
+ ...bollingerInputs(1e-3, 0.25),
34461
+ { ...floatInput("overbought", "Overbought", 1, -10, 10, 0.05, "Level treated as overbought. 1 is the upper band itself, so readings above the default mean price sits outside the bands."), group: THRESHOLDS },
34462
+ { ...floatInput("oversold", "Oversold", 0, -10, 10, 0.05, "Level treated as oversold. 0 is the lower band itself, so readings below the default mean price sits outside the bands."), group: THRESHOLDS },
34463
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of %B above the 0.5 midline, where price trades in the upper half of the bands."),
34464
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of %B below the 0.5 midline, where price trades in the lower half of the bands."),
34465
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Vertical gradient between %B and the 0.5 midline, fully transparent at the midline and more opaque at the oscillator value."),
34466
+ colorInput(NEUTRAL, "Levels Color", "levelsColor", "Color of the dashed overbought/oversold levels and of the dotted midline.")
34467
+ ],
33947
34468
  compute: (bars, inputs) => {
33948
- const { upper, lower } = bollinger(bars, inputs);
33949
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
34469
+ const { src, upper, lower } = bollinger(bars, inputs);
33950
34470
  const values = src.map((x, i) => {
33951
34471
  const u = upper[i];
33952
34472
  const l = lower[i];
33953
34473
  return Number.isFinite(u) && Number.isFinite(l) && u !== l ? (x - l) / (u - l) : Number.NaN;
33954
34474
  });
34475
+ const bull = str3(inputs, "bullColor", BULLISH);
34476
+ const bear = str3(inputs, "bearColor", BEARISH);
34477
+ const levels = str3(inputs, "levelsColor", NEUTRAL);
34478
+ const colors = values.map((x) => Number.isFinite(x) ? x > PERCENT_B_MID ? bull : bear : null);
34479
+ const plots = [
34480
+ { key: "pb", title: "%B", values, color: bear, colors },
34481
+ { key: "mid", title: "Midline", values: values.map((x) => Number.isFinite(x) ? PERCENT_B_MID : Number.NaN), color: levels, display: { pane: false, legend: false, dataWindow: false } }
34482
+ ];
34483
+ const bands = bool(inputs, "gradientFill", true) ? [
34484
+ {
34485
+ key: "pbFill",
34486
+ from: "pb",
34487
+ to: "mid",
34488
+ color: transp(NEUTRAL, 100),
34489
+ // Opaque at the oscillator, invisible where it meets the midline.
34490
+ gradient: values.map((x) => {
34491
+ if (!Number.isFinite(x)) return null;
34492
+ const up = x > PERCENT_B_MID;
34493
+ const ink = up ? bull : bear;
34494
+ return {
34495
+ topValue: Math.max(x, PERCENT_B_MID),
34496
+ bottomValue: Math.min(x, PERCENT_B_MID),
34497
+ topColor: up ? transp(ink, 50) : transp(ink, 100),
34498
+ bottomColor: up ? transp(ink, 100) : transp(ink, 50)
34499
+ };
34500
+ })
34501
+ }
34502
+ ] : [];
33955
34503
  return {
33956
- plots: [{ key: "pb", title: "%B", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34504
+ plots,
34505
+ bands,
33957
34506
  levels: [
33958
- { key: "upper", price: 1, color: NEUTRAL },
33959
- { key: "middle", price: 0.5, color: NEUTRAL, lineStyle: "dotted" },
33960
- { key: "lower", price: 0, color: NEUTRAL }
34507
+ { key: "overbought", price: num3(inputs, "overbought", 1), color: levels, lineStyle: "dashed", title: "Overbought" },
34508
+ { key: "oversold", price: num3(inputs, "oversold", 0), color: levels, lineStyle: "dashed", title: "Oversold" },
34509
+ { key: "midline", price: PERCENT_B_MID, color: transp(levels, 50), lineStyle: "dotted", title: "Midline" }
33961
34510
  ]
33962
34511
  };
33963
34512
  }
33964
34513
  };
34514
+ var SQUEEZE_BULGE = "Squeeze & Bulge";
33965
34515
  var bandwidth = {
33966
34516
  type: "bandwidth",
33967
34517
  title: "Bollinger Bands Width",
33968
34518
  shortTitle: "BandWidth",
33969
34519
  overlay: false,
33970
- inputs: [...BB_INPUTS, colorInput()],
34520
+ inputs: [
34521
+ ...bollingerInputs(1e-3, 0.25),
34522
+ { ...intInput("lookback", "Reference Lookback", 125, 2, 5e3, "Window used to locate the lowest (Squeeze) and highest (Bulge) BandWidth references."), group: SQUEEZE_BULGE },
34523
+ boolInput("showSqueeze", "Show Squeeze Level", true, SQUEEZE_BULGE, "Plots the lowest BandWidth of the reference lookback \u2014 unusually compressed volatility, a condition rather than a directional signal."),
34524
+ boolInput("showBulge", "Show Bulge Level", true, SQUEEZE_BULGE, "Plots the highest BandWidth of the reference lookback \u2014 extreme width that is unlikely to be sustained."),
34525
+ colorInput(NEUTRAL, "BandWidth", "color", "Color of the BandWidth line. At its Squeeze reference the line takes the Squeeze color, at its Bulge reference the Bulge color."),
34526
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between zero and BandWidth with a vertical gradient, fully transparent at zero."),
34527
+ colorInput(WARNING, "Squeeze Level", "squeezeColor", "Color of the lowest-BandWidth reference line."),
34528
+ colorInput(BEARISH, "Bulge Level", "bulgeColor", "Color of the highest-BandWidth reference line.")
34529
+ ],
33971
34530
  compute: (bars, inputs) => {
33972
34531
  const { basis, upper, lower } = bollinger(bars, inputs);
33973
34532
  const values = basis.map((b, i) => {
33974
34533
  const u = upper[i];
33975
34534
  const l = lower[i];
33976
- return Number.isFinite(b) && b !== 0 && Number.isFinite(u) && Number.isFinite(l) ? (u - l) / b * 100 : Number.NaN;
34535
+ return Number.isFinite(b) && b !== 0 && Number.isFinite(u) && Number.isFinite(l) ? (u - l) / b : Number.NaN;
34536
+ });
34537
+ const lookback = num3(inputs, "lookback", 125);
34538
+ const squeezeLevel = lowest(values, lookback);
34539
+ const bulgeLevel = highest(values, lookback);
34540
+ const base = str3(inputs, "color", NEUTRAL);
34541
+ const squeezeInk = str3(inputs, "squeezeColor", WARNING);
34542
+ const bulgeInk = str3(inputs, "bulgeColor", BEARISH);
34543
+ const colors = values.map((x, i) => {
34544
+ if (!Number.isFinite(x)) return null;
34545
+ if (x <= squeezeLevel[i]) return squeezeInk;
34546
+ if (x >= bulgeLevel[i]) return bulgeInk;
34547
+ return base;
33977
34548
  });
33978
- return { plots: [{ key: "bbw", title: "BBW", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
34549
+ const plots = [
34550
+ { key: "bbw", title: "BandWidth", values, color: base, colors },
34551
+ { key: "zero", title: "Zero Line", values: values.map((x) => Number.isFinite(x) ? 0 : Number.NaN), color: transp(NEUTRAL, 90) }
34552
+ ];
34553
+ if (bool(inputs, "showSqueeze", true)) plots.push({ key: "squeeze", title: "Squeeze Level", values: squeezeLevel, color: squeezeInk });
34554
+ if (bool(inputs, "showBulge", true)) plots.push({ key: "bulge", title: "Bulge Level", values: bulgeLevel, color: bulgeInk });
34555
+ const bands = bool(inputs, "gradientFill", true) ? [
34556
+ {
34557
+ key: "bbwFill",
34558
+ from: "bbw",
34559
+ to: "zero",
34560
+ color: transp(NEUTRAL, 100),
34561
+ gradient: values.map((x, i) => {
34562
+ if (!Number.isFinite(x)) return null;
34563
+ const ink = colors[i] ?? base;
34564
+ return { topValue: x, bottomValue: 0, topColor: transp(ink, 50), bottomColor: transp(ink, 100) };
34565
+ })
34566
+ }
34567
+ ] : [];
34568
+ return { plots, bands };
33979
34569
  }
33980
34570
  };
33981
34571
  var keltner = {
@@ -33984,26 +34574,17 @@ var keltner = {
33984
34574
  shortTitle: "KC",
33985
34575
  overlay: true,
33986
34576
  inputs: [
33987
- lengthInput(20),
33988
- { key: "mult", title: "Multiplier", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 },
33989
- { key: "atrLength", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
33990
- sourceInput(),
33991
- colorInput()
34577
+ lengthInput(20, "length", "EMA Length", 5e3, "Number of bars used to compute the exponential moving average basis line."),
34578
+ intInput("atrLength", "ATR Length", 10, 1, 5e3, "Number of bars used to compute the Average True Range (Wilder smoothing)."),
34579
+ floatInput("mult", "Multiplier", 2, 0, 50, 0.1, "ATR multiple used to offset the upper and lower bands from the basis line."),
34580
+ sourceInput("Close", "source", "Source", "Price series used to compute the basis line."),
34581
+ ...channelStyleInputs("Basis Color")
33992
34582
  ],
33993
34583
  compute: (bars, inputs) => {
33994
- const len = num3(inputs, "length", 20);
33995
- const mult = num3(inputs, "mult", 2);
33996
- const basis = ema(sourceValues(bars, str3(inputs, "source", "Close")), len);
33997
- const range = map(atr(bars, num3(inputs, "atrLength", 10)), (x) => x * mult);
33998
- const ink = str3(inputs, "color", SERIES_LINE);
33999
- return {
34000
- plots: [
34001
- { key: "basis", title: "Basis", values: basis, color: WARNING },
34002
- { key: "upper", title: "Upper", values: zip(basis, range, (b, r) => b + r), color: ink },
34003
- { key: "lower", title: "Lower", values: zip(basis, range, (b, r) => b - r), color: ink }
34004
- ],
34005
- bands: [{ key: "kc", from: "upper", to: "lower", color: withAlpha2(ink) }]
34006
- };
34584
+ const basis = ema(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20));
34585
+ const range = map(atr(bars, num3(inputs, "atrLength", 10)), (x) => x * num3(inputs, "mult", 2));
34586
+ const ink = { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "basisColor", NEUTRAL) };
34587
+ return directionalChannel(bars, basis, zip(basis, range, (b, r) => b + r), zip(basis, range, (b, r) => b - r), ink, "kc");
34007
34588
  }
34008
34589
  };
34009
34590
  var donchian = {
@@ -34011,19 +34592,24 @@ var donchian = {
34011
34592
  title: "Donchian Channels",
34012
34593
  shortTitle: "DC",
34013
34594
  overlay: true,
34014
- inputs: [lengthInput(20), colorInput()],
34595
+ inputs: [
34596
+ lengthInput(20, "length", "Length", 5e3, "Lookback length. The upper and lower channels are the highest high and lowest low over this many bars."),
34597
+ colorInput(BEARISH, "Upper", "upperColor", "Color of the upper channel line."),
34598
+ colorInput(NEUTRAL, "Middle", "middleColor", "Color of the channel midline."),
34599
+ colorInput(BULLISH, "Lower", "lowerColor", "Color of the lower channel line."),
34600
+ colorInput(transp(NEUTRAL, 92), "Fill", "fillColor", "Color of the channel interior fill.")
34601
+ ],
34015
34602
  compute: (bars, inputs) => {
34016
34603
  const len = num3(inputs, "length", 20);
34017
34604
  const upper = highest(highs(bars), len);
34018
34605
  const lower = lowest(lows(bars), len);
34019
- const ink = str3(inputs, "color", SERIES_LINE);
34020
34606
  return {
34021
34607
  plots: [
34022
- { key: "basis", title: "Basis", values: zip(upper, lower, (u, l) => (u + l) / 2), color: WARNING },
34023
- { key: "upper", title: "Upper", values: upper, color: ink },
34024
- { key: "lower", title: "Lower", values: lower, color: ink }
34608
+ { key: "upper", title: "Upper", values: upper, color: str3(inputs, "upperColor", BEARISH) },
34609
+ { key: "middle", title: "Middle", kind: "circles", values: zip(upper, lower, (u, l) => (u + l) / 2), color: str3(inputs, "middleColor", NEUTRAL) },
34610
+ { key: "lower", title: "Lower", values: lower, color: str3(inputs, "lowerColor", BULLISH) }
34025
34611
  ],
34026
- bands: [{ key: "dc", from: "upper", to: "lower", color: withAlpha2(ink) }]
34612
+ bands: [{ key: "dc", from: "upper", to: "lower", color: str3(inputs, "fillColor", transp(NEUTRAL, 92)) }]
34027
34613
  };
34028
34614
  }
34029
34615
  };
@@ -34032,16 +34618,37 @@ var chandelier = {
34032
34618
  title: "Chandelier Exit",
34033
34619
  shortTitle: "CE",
34034
34620
  overlay: true,
34035
- inputs: [lengthInput(22), { key: "mult", title: "ATR multiplier", type: "float", defval: 3, min: 0.1, max: 50, step: 0.1 }],
34621
+ inputs: [
34622
+ lengthInput(22, "length", "Length", 5e3, "Lookback used for both the highest high / lowest low extremes and the ATR."),
34623
+ floatInput("mult", "ATR Multiplier", 3, 0, 50, 0.1, "Multiple of ATR subtracted from the highest high (long stop) and added to the lowest low (short stop)."),
34624
+ boolInput("ratchet", "Ratchet Stops", false, void 0, "Prevents the long stop from falling and the short stop from rising, resetting once close crosses through the stop."),
34625
+ colorInput(BULLISH, "Long Stop", "longColor", "Color of the long trailing stop line."),
34626
+ colorInput(BEARISH, "Short Stop", "shortColor", "Color of the short trailing stop line.")
34627
+ ],
34036
34628
  compute: (bars, inputs) => {
34037
34629
  const len = num3(inputs, "length", 22);
34038
34630
  const range = map(atr(bars, len), (x) => x * num3(inputs, "mult", 3));
34039
- const long = zip(highest(highs(bars), len), range, (h, r) => h - r);
34040
- const short = zip(lowest(lows(bars), len), range, (l, r) => l + r);
34631
+ const basicLong = zip(highest(highs(bars), len), range, (h, r) => h - r);
34632
+ const basicShort = zip(lowest(lows(bars), len), range, (l, r) => l + r);
34633
+ let long = basicLong;
34634
+ let short = basicShort;
34635
+ if (bool(inputs, "ratchet", false)) {
34636
+ long = new Array(bars.length).fill(Number.NaN);
34637
+ short = new Array(bars.length).fill(Number.NaN);
34638
+ for (let i = 0; i < bars.length; i++) {
34639
+ const bl = basicLong[i];
34640
+ const bs = basicShort[i];
34641
+ const prevClose = i > 0 ? bars[i - 1].close : bars[i].close;
34642
+ const prevLong = i > 0 && Number.isFinite(long[i - 1]) ? long[i - 1] : bl;
34643
+ const prevShort = i > 0 && Number.isFinite(short[i - 1]) ? short[i - 1] : bs;
34644
+ long[i] = Number.isFinite(bl) ? prevClose > prevLong ? Math.max(bl, prevLong) : bl : Number.NaN;
34645
+ short[i] = Number.isFinite(bs) ? prevClose < prevShort ? Math.min(bs, prevShort) : bs : Number.NaN;
34646
+ }
34647
+ }
34041
34648
  return {
34042
34649
  plots: [
34043
- { key: "long", title: "Long stop", values: long, color: BULLISH },
34044
- { key: "short", title: "Short stop", values: short, color: BEARISH }
34650
+ { key: "long", title: "Long Stop", values: long, color: str3(inputs, "longColor", BULLISH) },
34651
+ { key: "short", title: "Short Stop", values: short, color: str3(inputs, "shortColor", BEARISH) }
34045
34652
  ]
34046
34653
  };
34047
34654
  }
@@ -34052,21 +34659,22 @@ var chandeKroll = {
34052
34659
  shortTitle: "CKS",
34053
34660
  overlay: true,
34054
34661
  inputs: [
34055
- { key: "p", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
34056
- { key: "x", title: "ATR multiplier", type: "float", defval: 1, min: 0.1, max: 50, step: 0.1 },
34057
- { key: "q", title: "Stop length", type: "int", defval: 9, min: 1, max: 500, step: 1 }
34662
+ intInput("p", "ATR Length (P)", 10, 1, 5e3, "Lookback used for both the ATR and the highest high / lowest low price extremes."),
34663
+ floatInput("x", "ATR Multiplier (X)", 1, 0, 50, 0.1, "Multiplier applied to the ATR to offset the preliminary stops from the price extremes."),
34664
+ intInput("q", "Stop Length (Q)", 9, 1, 5e3, "Lookback over which the preliminary stops are extended into the final stop levels."),
34665
+ colorInput(BEARISH, "Stop Short", "shortColor", "Color of the protective stop line for short positions."),
34666
+ colorInput(BULLISH, "Stop Long", "longColor", "Color of the protective stop line for long positions.")
34058
34667
  ],
34059
34668
  compute: (bars, inputs) => {
34060
34669
  const p = num3(inputs, "p", 10);
34061
- const x = num3(inputs, "x", 1);
34062
34670
  const q = num3(inputs, "q", 9);
34063
- const range = map(atr(bars, p), (v) => v * x);
34671
+ const range = map(atr(bars, p), (v) => v * num3(inputs, "x", 1));
34064
34672
  const firstHigh = zip(highest(highs(bars), p), range, (h, r) => h - r);
34065
34673
  const firstLow = zip(lowest(lows(bars), p), range, (l, r) => l + r);
34066
34674
  return {
34067
34675
  plots: [
34068
- { key: "stopShort", title: "Stop short", values: highest(firstHigh, q), color: BEARISH },
34069
- { key: "stopLong", title: "Stop long", values: lowest(firstLow, q), color: BULLISH }
34676
+ { key: "stopShort", title: "Stop Short", values: highest(firstHigh, q), color: str3(inputs, "shortColor", BEARISH) },
34677
+ { key: "stopLong", title: "Stop Long", values: lowest(firstLow, q), color: str3(inputs, "longColor", BULLISH) }
34070
34678
  ]
34071
34679
  };
34072
34680
  }
@@ -34077,36 +34685,42 @@ var supertrend = {
34077
34685
  shortTitle: "SuperTrend",
34078
34686
  overlay: true,
34079
34687
  inputs: [
34080
- { key: "atrLength", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
34081
- { key: "mult", title: "Factor", type: "float", defval: 3, min: 0.1, max: 50, step: 0.1 }
34688
+ intInput("atrLength", "ATR Length", 10, 1, 5e3, "Number of bars used to compute the Wilder ATR."),
34689
+ floatInput("mult", "Factor", 3, 0, 50, 0.1, "ATR multiplier setting how far the trailing stop sits from the bar midpoint."),
34690
+ colorInput(BULLISH, "Uptrend Color", "upColor", "Line color while the trend is up."),
34691
+ colorInput(BEARISH, "Downtrend Color", "downColor", "Line color while the trend is down.")
34082
34692
  ],
34083
34693
  compute: (bars, inputs) => {
34084
- const len = num3(inputs, "atrLength", 10);
34085
34694
  const mult = num3(inputs, "mult", 3);
34086
- const range = atr(bars, len);
34695
+ const range = atr(bars, num3(inputs, "atrLength", 10));
34087
34696
  const n = bars.length;
34088
- const st = new Array(n).fill(Number.NaN);
34089
- const dir = new Array(n).fill(1);
34090
- let up = Number.NaN;
34091
- let dn = Number.NaN;
34092
- let trend = 1;
34697
+ const up = new Array(n).fill(Number.NaN);
34698
+ const down = new Array(n).fill(Number.NaN);
34699
+ let finalUpper = Number.NaN;
34700
+ let finalLower = Number.NaN;
34701
+ let trend = -1;
34093
34702
  for (let i = 0; i < n; i++) {
34094
34703
  const r = range[i];
34095
34704
  if (!Number.isFinite(r)) continue;
34096
34705
  const b = bars[i];
34097
34706
  const mid = (b.high + b.low) / 2;
34098
- const basicUp = mid - mult * r;
34099
- const basicDn = mid + mult * r;
34707
+ const basicUpper = mid + mult * r;
34708
+ const basicLower = mid - mult * r;
34709
+ const prevUpper = Number.isFinite(finalUpper) ? finalUpper : basicUpper;
34710
+ const prevLower = Number.isFinite(finalLower) ? finalLower : basicLower;
34100
34711
  const prevClose = i > 0 ? bars[i - 1].close : b.close;
34101
- up = Number.isFinite(up) && prevClose > up ? Math.max(basicUp, up) : basicUp;
34102
- dn = Number.isFinite(dn) && prevClose < dn ? Math.min(basicDn, dn) : basicDn;
34103
- if (trend === 1 && b.close < up) trend = -1;
34104
- else if (trend === -1 && b.close > dn) trend = 1;
34105
- dir[i] = trend;
34106
- st[i] = trend === 1 ? up : dn;
34712
+ finalUpper = prevClose <= prevUpper ? Math.min(basicUpper, prevUpper) : basicUpper;
34713
+ finalLower = prevClose >= prevLower ? Math.max(basicLower, prevLower) : basicLower;
34714
+ trend = b.close > finalUpper ? 1 : b.close < finalLower ? -1 : trend;
34715
+ if (trend === 1) up[i] = finalLower;
34716
+ else down[i] = finalUpper;
34107
34717
  }
34108
- const colors = dir.map((d, i) => Number.isFinite(st[i]) ? d === 1 ? BULLISH : BEARISH : null);
34109
- return { plots: [{ key: "st", title: "SuperTrend", values: st, color: BULLISH, width: 2, colors }] };
34718
+ return {
34719
+ plots: [
34720
+ { key: "up", title: "Uptrend", values: up, color: str3(inputs, "upColor", BULLISH) },
34721
+ { key: "down", title: "Downtrend", values: down, color: str3(inputs, "downColor", BEARISH) }
34722
+ ]
34723
+ };
34110
34724
  }
34111
34725
  };
34112
34726
  var alligator = {
@@ -34115,12 +34729,15 @@ var alligator = {
34115
34729
  shortTitle: "Alligator",
34116
34730
  overlay: true,
34117
34731
  inputs: [
34118
- { key: "jawLength", title: "Jaw length", type: "int", defval: 13, min: 1, max: 500, step: 1 },
34119
- { key: "teethLength", title: "Teeth length", type: "int", defval: 8, min: 1, max: 500, step: 1 },
34120
- { key: "lipsLength", title: "Lips length", type: "int", defval: 5, min: 1, max: 500, step: 1 },
34121
- { key: "jawOffset", title: "Jaw offset", type: "int", defval: 8, min: 0, max: 100, step: 1 },
34122
- { key: "teethOffset", title: "Teeth offset", type: "int", defval: 5, min: 0, max: 100, step: 1 },
34123
- { key: "lipsOffset", title: "Lips offset", type: "int", defval: 3, min: 0, max: 100, step: 1 }
34732
+ intInput("jawLength", "Jaw Length", 13, 1, 5e3, "Smoothing length of the Jaw SMMA of median price (hl2), 13 in the classic setup."),
34733
+ intInput("teethLength", "Teeth Length", 8, 1, 5e3, "Smoothing length of the Teeth SMMA of median price (hl2), 8 in the classic setup."),
34734
+ intInput("lipsLength", "Lips Length", 5, 1, 5e3, "Smoothing length of the Lips SMMA of median price (hl2), 5 in the classic setup."),
34735
+ intInput("jawOffset", "Jaw Offset", 8, 0, 100, "Number of bars the Jaw plot is shifted into the future, 8 in the classic setup."),
34736
+ intInput("teethOffset", "Teeth Offset", 5, 0, 100, "Number of bars the Teeth plot is shifted into the future, 5 in the classic setup."),
34737
+ intInput("lipsOffset", "Lips Offset", 3, 0, 100, "Number of bars the Lips plot is shifted into the future, 3 in the classic setup."),
34738
+ colorInput(JAW_COLOR, "Jaw", "jawColor", "Color of the Jaw line."),
34739
+ colorInput(TEETH_COLOR, "Teeth", "teethColor", "Color of the Teeth line."),
34740
+ colorInput(LIPS_COLOR, "Lips", "lipsColor", "Color of the Lips line.")
34124
34741
  ],
34125
34742
  compute: (bars, inputs) => {
34126
34743
  const hl2 = sourceValues(bars, "HL2");
@@ -34129,99 +34746,134 @@ var alligator = {
34129
34746
  const lips = shift(rma(hl2, num3(inputs, "lipsLength", 5)), num3(inputs, "lipsOffset", 3));
34130
34747
  return {
34131
34748
  plots: [
34132
- { key: "jaw", title: "Jaw", values: jaw, color: SERIES_LINE },
34133
- { key: "teeth", title: "Teeth", values: teeth, color: BEARISH },
34134
- { key: "lips", title: "Lips", values: lips, color: BULLISH }
34749
+ { key: "jaw", title: "Jaw", values: jaw, color: str3(inputs, "jawColor", JAW_COLOR) },
34750
+ { key: "teeth", title: "Teeth", values: teeth, color: str3(inputs, "teethColor", TEETH_COLOR) },
34751
+ { key: "lips", title: "Lips", values: lips, color: str3(inputs, "lipsColor", LIPS_COLOR) }
34135
34752
  ]
34136
34753
  };
34137
34754
  }
34138
34755
  };
34756
+ var GATOR_LINES = "Alligator Lines";
34757
+ var SMMA_TYPE = "SMMA (Wilder)";
34139
34758
  var gator = {
34140
34759
  type: "gator-oscillator",
34141
34760
  title: "Gator Oscillator",
34142
34761
  shortTitle: "Gator Oscillator",
34143
34762
  overlay: false,
34144
- inputs: [],
34145
- compute: (bars) => {
34146
- const hl2 = sourceValues(bars, "HL2");
34147
- const jaw = shift(rma(hl2, 13), 8);
34148
- const teeth = shift(rma(hl2, 8), 5);
34149
- const lips = shift(rma(hl2, 5), 3);
34763
+ inputs: [
34764
+ { ...sourceInput("HL2", "source", "Source", "Price series fed to the three Alligator averages. Bill Williams' original recipe uses the median price (high + low) / 2."), group: GATOR_LINES },
34765
+ { ...optionInput("maType", "Smoothing", SMMA_TYPE, [SMMA_TYPE, "SMA", "EMA"], "Moving average applied to the source for the jaw, teeth and lips. The original uses the smoothed (Wilder-style) moving average."), group: GATOR_LINES },
34766
+ { ...intInput("jawLength", "Jaw Length", 13, 1, 500, "Averaging length of the jaw line."), group: GATOR_LINES },
34767
+ { ...intInput("jawOffset", "Jaw Offset", 8, 0, 100, "Bars the jaw is displaced forward, 8 in the original recipe. The upper histogram measures the absolute spread between the displaced jaw and teeth."), group: GATOR_LINES },
34768
+ { ...intInput("teethLength", "Teeth Length", 8, 1, 500, "Averaging length of the teeth line."), group: GATOR_LINES },
34769
+ { ...intInput("teethOffset", "Teeth Offset", 5, 0, 100, "Bars the teeth are displaced forward, 5 in the original recipe. The teeth line enters both spreads."), group: GATOR_LINES },
34770
+ { ...intInput("lipsLength", "Lips Length", 5, 1, 500, "Averaging length of the lips line."), group: GATOR_LINES },
34771
+ { ...intInput("lipsOffset", "Lips Offset", 3, 0, 100, "Bars the lips are displaced forward, 3 in the original recipe. The lower histogram measures the negative absolute spread between the displaced teeth and lips."), group: GATOR_LINES },
34772
+ colorInput(BULLISH, "Expanding", "growColor", "Column color while a spread grew against its own previous bar \u2014 the averages are spreading apart."),
34773
+ colorInput(BEARISH, "Contracting", "shrinkColor", "Column color while a spread shrank \u2014 the averages are converging. Color encodes expansion, not trade direction."),
34774
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero line separating the two histograms.")
34775
+ ],
34776
+ compute: (bars, inputs) => {
34777
+ const src = sourceValues(bars, str3(inputs, "source", "HL2"));
34778
+ const kind = str3(inputs, "maType", SMMA_TYPE);
34779
+ const smooth = (len) => kind === "SMA" ? sma(src, len) : kind === "EMA" ? ema(src, len) : rma(src, len);
34780
+ const jaw = shift(smooth(num3(inputs, "jawLength", 13)), num3(inputs, "jawOffset", 8));
34781
+ const teeth = shift(smooth(num3(inputs, "teethLength", 8)), num3(inputs, "teethOffset", 5));
34782
+ const lips = shift(smooth(num3(inputs, "lipsLength", 5)), num3(inputs, "lipsOffset", 3));
34150
34783
  const upper = map(sub(jaw, teeth), Math.abs);
34151
- const lower = map(sub(teeth, lips), (x) => -Math.abs(x));
34152
- const colorByExpansion = (v) => v.map((x, i) => {
34153
- if (!Number.isFinite(x)) return null;
34154
- const prev2 = i > 0 ? v[i - 1] : Number.NaN;
34155
- return Number.isFinite(prev2) && Math.abs(x) > Math.abs(prev2) ? BULLISH : BEARISH;
34156
- });
34784
+ const lower = map(sub(teeth, lips), Math.abs);
34785
+ const grow = str3(inputs, "growColor", BULLISH);
34786
+ const shrink = str3(inputs, "shrinkColor", BEARISH);
34787
+ const expansionColors2 = (v) => {
34788
+ let growing = false;
34789
+ return v.map((x, i) => {
34790
+ if (!Number.isFinite(x)) return null;
34791
+ const prev2 = i > 0 ? v[i - 1] : Number.NaN;
34792
+ if (Number.isFinite(prev2)) growing = x > prev2 ? true : x < prev2 ? false : growing;
34793
+ return growing ? grow : shrink;
34794
+ });
34795
+ };
34157
34796
  return {
34158
34797
  plots: [
34159
- { key: "upper", title: "Upper", values: upper, kind: "histogram", color: BULLISH, colors: colorByExpansion(upper), base: 0 },
34160
- { key: "lower", title: "Lower", values: lower, kind: "histogram", color: BEARISH, colors: colorByExpansion(lower), base: 0 }
34161
- ]
34798
+ { key: "upper", title: "Upper Histogram (Jaw - Teeth)", values: upper, kind: "columns", color: grow, colors: expansionColors2(upper), base: 0 },
34799
+ { key: "lower", title: "Lower Histogram (Teeth - Lips)", values: map(lower, (x) => -x), kind: "columns", color: shrink, colors: expansionColors2(lower), base: 0 }
34800
+ ],
34801
+ levels: [{ key: "zero", price: 0, color: str3(inputs, "zeroColor", NEUTRAL), lineStyle: "solid", title: "Zero Line" }]
34162
34802
  };
34163
34803
  }
34164
34804
  };
34165
34805
  var bandSpecs = [bollingerBands, percentB, bandwidth, keltner, donchian, chandelier, chandeKroll, supertrend, alligator, gator];
34166
34806
 
34167
34807
  // src/core/native-indicators/classics/oscillators.ts
34168
- function guideLevels(low, high, mid) {
34169
- const levels = [
34170
- { key: "upper", price: high, color: NEUTRAL },
34171
- { key: "lower", price: low, color: NEUTRAL }
34808
+ function thresholdInputs(obDefault, osDefault, min = 0, max = 100, obTitle = "Overbought Level", osTitle = "Oversold Level") {
34809
+ return [
34810
+ floatInput("overbought", obTitle, obDefault, min, max, 1, "Level above which the oscillator is considered overbought."),
34811
+ floatInput("oversold", osTitle, osDefault, min, max, 1, "Level below which the oscillator is considered oversold.")
34172
34812
  ];
34173
- if (mid != null) levels.splice(1, 0, { key: "middle", price: mid, color: NEUTRAL, lineStyle: "dotted" });
34174
- return levels;
34175
- }
34176
- var ZERO_LEVEL = [{ key: "zero", price: 0, color: NEUTRAL }];
34177
- function histColors(values) {
34178
- return values.map((x, i) => {
34179
- if (!Number.isFinite(x)) return null;
34180
- const prev2 = i > 0 ? values[i - 1] : Number.NaN;
34181
- const rising = Number.isFinite(prev2) ? x >= prev2 : true;
34182
- if (x >= 0) return rising ? BULLISH : `${BULLISH}80`;
34183
- return rising ? `${BEARISH}80` : BEARISH;
34184
- });
34185
34813
  }
34186
34814
  var rsiSpec = {
34187
34815
  type: "rsi",
34188
34816
  title: "Relative Strength Index",
34189
34817
  shortTitle: "RSI",
34190
34818
  overlay: false,
34191
- inputs: [lengthInput(14), sourceInput(), colorInput()],
34192
- compute: (bars, inputs) => ({
34193
- plots: [
34194
- {
34195
- key: "rsi",
34196
- title: "RSI",
34197
- values: rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
34198
- color: str3(inputs, "color", SERIES_LINE),
34199
- width: 2
34200
- }
34201
- ],
34202
- levels: guideLevels(30, 70, 50)
34819
+ inputs: [
34820
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used in the Wilder smoothing of average gains and losses."),
34821
+ sourceInput("Close", "source", "Source", "Price series the RSI is calculated on."),
34822
+ ...thresholdInputs(70, 30, 0, 100, "Overbought", "Oversold"),
34823
+ colorInput(NEUTRAL, "RSI", "color", "Color of the RSI line at the midline. Toward either threshold it blends into that threshold\u2019s color."),
34824
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level."),
34825
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
34826
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought, midline and oversold levels."),
34827
+ colorInput(transp(NEUTRAL, 90), "Band Fill", "fillColor", "Color of the fill between the overbought and oversold levels.")
34828
+ ],
34829
+ compute: (bars, inputs) => boundedOscillator({
34830
+ key: "rsi",
34831
+ title: "RSI",
34832
+ values: rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
34833
+ scale: { top: 100, bottom: 0 },
34834
+ overbought: num3(inputs, "overbought", 70),
34835
+ oversold: num3(inputs, "oversold", 30),
34836
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
34837
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
34838
+ width: 2,
34839
+ midline: 50,
34840
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 90))
34203
34841
  })
34204
34842
  };
34843
+ function stochasticOutput(k, d, inputs) {
34844
+ const overbought = num3(inputs, "overbought", 80);
34845
+ const oversold = num3(inputs, "oversold", 20);
34846
+ return {
34847
+ plots: [
34848
+ { key: "k", title: "%K", values: k, color: str3(inputs, "kColor", INFO) },
34849
+ { key: "d", title: "%D", values: d, color: str3(inputs, "dColor", WARNING) },
34850
+ anchorPlot("obAnchor", "Overbought Anchor", k.length, overbought),
34851
+ anchorPlot("osAnchor", "Oversold Anchor", k.length, oversold)
34852
+ ],
34853
+ bands: [{ key: "band", from: "obAnchor", to: "osAnchor", color: str3(inputs, "fillColor", transp(INFO, 90)) }],
34854
+ levels: [
34855
+ { key: "overbought", price: overbought, color: NEUTRAL, lineStyle: "dashed", title: "Overbought" },
34856
+ { key: "oversold", price: oversold, color: NEUTRAL, lineStyle: "dashed", title: "Oversold" }
34857
+ ]
34858
+ };
34859
+ }
34205
34860
  var stochastic = {
34206
34861
  type: "stochastic",
34207
34862
  title: "Stochastic",
34208
34863
  shortTitle: "Stoch",
34209
34864
  overlay: false,
34210
34865
  inputs: [
34211
- lengthInput(14, "kLength", "%K length"),
34212
- lengthInput(1, "kSmoothing", "%K smoothing", 500),
34213
- lengthInput(3, "dLength", "%D smoothing", 500)
34866
+ lengthInput(14, "kLength", "%K Length", 5e3, "Number of bars used for the highest high / lowest low range of the raw %K."),
34867
+ lengthInput(3, "kSmoothing", "%K Smoothing", 500, "Simple moving average length applied to the raw %K. 1 gives the fast stochastic, 3 the classic slow stochastic."),
34868
+ lengthInput(3, "dLength", "%D Smoothing", 500, "Simple moving average length of the %D signal line, applied to the smoothed %K."),
34869
+ ...thresholdInputs(80, 20),
34870
+ colorInput(INFO, "%K", "kColor", "Color of the %K line."),
34871
+ colorInput(WARNING, "%D", "dColor", "Color of the %D signal line."),
34872
+ colorInput(transp(INFO, 90), "Band Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
34214
34873
  ],
34215
34874
  compute: (bars, inputs) => {
34216
- const k = sma(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "kLength", 14)), num3(inputs, "kSmoothing", 1));
34217
- const d = sma(k, num3(inputs, "dLength", 3));
34218
- return {
34219
- plots: [
34220
- { key: "k", title: "%K", values: k, color: SERIES_LINE, width: 2 },
34221
- { key: "d", title: "%D", values: d, color: WARNING }
34222
- ],
34223
- levels: guideLevels(20, 80)
34224
- };
34875
+ const k = sma(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "kLength", 14)), num3(inputs, "kSmoothing", 3));
34876
+ return stochasticOutput(k, sma(k, num3(inputs, "dLength", 3)), inputs);
34225
34877
  }
34226
34878
  };
34227
34879
  var stochasticRsi = {
@@ -34230,23 +34882,20 @@ var stochasticRsi = {
34230
34882
  shortTitle: "Stoch RSI",
34231
34883
  overlay: false,
34232
34884
  inputs: [
34233
- lengthInput(14, "rsiLength", "RSI length"),
34234
- lengthInput(14, "stochLength", "Stochastic length"),
34235
- lengthInput(3, "kSmoothing", "%K smoothing", 500),
34236
- lengthInput(3, "dLength", "%D smoothing", 500),
34237
- sourceInput()
34885
+ sourceInput("Close", "source", "RSI Source", "Price series the RSI is computed on."),
34886
+ lengthInput(14, "rsiLength", "RSI Length", 5e3, "Lookback length of the Wilder RSI."),
34887
+ lengthInput(14, "stochLength", "Stochastic Length", 5e3, "Lookback over which the highest and lowest RSI values are taken."),
34888
+ lengthInput(3, "kSmoothing", "%K Smoothing", 500, "Simple moving average length applied to the stochastic of RSI to form %K."),
34889
+ lengthInput(3, "dLength", "%D Smoothing", 500, "Simple moving average length applied to %K to form the %D signal line."),
34890
+ ...thresholdInputs(80, 20),
34891
+ colorInput(INFO, "%K Color", "kColor", "Color of the %K line."),
34892
+ colorInput(WARNING, "%D Color", "dColor", "Color of the %D signal line."),
34893
+ colorInput(transp(INFO, 90), "Zone Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
34238
34894
  ],
34239
34895
  compute: (bars, inputs) => {
34240
34896
  const r = rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "rsiLength", 14));
34241
34897
  const k = sma(stoch(r, r, r, num3(inputs, "stochLength", 14)), num3(inputs, "kSmoothing", 3));
34242
- const d = sma(k, num3(inputs, "dLength", 3));
34243
- return {
34244
- plots: [
34245
- { key: "k", title: "%K", values: k, color: SERIES_LINE, width: 2 },
34246
- { key: "d", title: "%D", values: d, color: WARNING }
34247
- ],
34248
- levels: guideLevels(20, 80)
34249
- };
34898
+ return stochasticOutput(k, sma(k, num3(inputs, "dLength", 3)), inputs);
34250
34899
  }
34251
34900
  };
34252
34901
  var macd = {
@@ -34254,23 +34903,35 @@ var macd = {
34254
34903
  title: "MACD",
34255
34904
  overlay: false,
34256
34905
  inputs: [
34257
- lengthInput(12, "fastLength", "Fast length"),
34258
- lengthInput(26, "slowLength", "Slow length"),
34259
- lengthInput(9, "signalLength", "Signal smoothing", 500),
34260
- sourceInput()
34906
+ lengthInput(12, "fastLength", "Fast Length", 5e3, "Length of the fast EMA."),
34907
+ lengthInput(26, "slowLength", "Slow Length", 5e3, "Length of the slow EMA."),
34908
+ lengthInput(9, "signalLength", "Signal Smoothing", 500, "Length of the EMA applied to the MACD line to obtain the signal line."),
34909
+ sourceInput("Close", "source", "Source", "Price series used to compute the fast and slow EMAs."),
34910
+ colorInput(INFO, "MACD Line", "color", "Color of the MACD line."),
34911
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
34912
+ colorInput(BULLISH, "Grow Above", "growAboveColor", "Histogram color when above zero and rising."),
34913
+ colorInput(transp(BULLISH, 50), "Fall Above", "fallAboveColor", "Histogram color when above zero and falling."),
34914
+ colorInput(transp(BEARISH, 50), "Grow Below", "growBelowColor", "Histogram color when below zero and rising."),
34915
+ colorInput(BEARISH, "Fall Below", "fallBelowColor", "Histogram color when below zero and falling.")
34261
34916
  ],
34262
34917
  compute: (bars, inputs) => {
34263
34918
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
34264
34919
  const line = sub(ema(src, num3(inputs, "fastLength", 12)), ema(src, num3(inputs, "slowLength", 26)));
34265
34920
  const signal = ema(line, num3(inputs, "signalLength", 9));
34266
34921
  const hist = sub(line, signal);
34922
+ const colors = momentumColumnColors(hist, {
34923
+ growAbove: str3(inputs, "growAboveColor", BULLISH),
34924
+ fallAbove: str3(inputs, "fallAboveColor", transp(BULLISH, 50)),
34925
+ growBelow: str3(inputs, "growBelowColor", transp(BEARISH, 50)),
34926
+ fallBelow: str3(inputs, "fallBelowColor", BEARISH)
34927
+ });
34267
34928
  return {
34268
34929
  plots: [
34269
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: histColors(hist), base: 0 },
34270
- { key: "macd", title: "MACD", values: line, color: SERIES_LINE, width: 2 },
34271
- { key: "signal", title: "Signal", values: signal, color: WARNING }
34930
+ { key: "hist", title: "Histogram", values: hist, kind: "columns", color: BULLISH, colors, base: 0 },
34931
+ { key: "macd", title: "MACD", values: line, color: str3(inputs, "color", INFO) },
34932
+ { key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) }
34272
34933
  ],
34273
- levels: ZERO_LEVEL
34934
+ levels: [{ key: "zero", price: 0, color: NEUTRAL, lineStyle: "dashed", title: "Zero Line" }]
34274
34935
  };
34275
34936
  }
34276
34937
  };
@@ -34280,26 +34941,49 @@ var ppo = {
34280
34941
  shortTitle: "PPO",
34281
34942
  overlay: false,
34282
34943
  inputs: [
34283
- lengthInput(12, "fastLength", "Fast length"),
34284
- lengthInput(26, "slowLength", "Slow length"),
34285
- lengthInput(9, "signalLength", "Signal smoothing", 500),
34286
- sourceInput()
34944
+ sourceInput("Close", "source", "Source", "Price series fed into the fast and slow averages."),
34945
+ lengthInput(12, "fastLength", "Fast Length", 5e3, "Length of the fast moving average."),
34946
+ lengthInput(26, "slowLength", "Slow Length", 5e3, "Length of the slow moving average. The spread is divided by it and scaled to a percentage of price."),
34947
+ optionInput("maType", "Average Type", "EMA", ["EMA", "SMA"], "Moving average type used for the fast and slow averages."),
34948
+ lengthInput(9, "signalLength", "Signal Smoothing", 500, "Length of the signal line that smooths the PPO line."),
34949
+ optionInput("signalType", "Signal Type", "EMA", ["EMA", "SMA"], "Moving average type used for the signal line."),
34950
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the PPO line, gradient fill and histogram above the zero line."),
34951
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the PPO line, gradient fill and histogram below the zero line."),
34952
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
34953
+ boolInput("showHistogram", "Show Histogram", true, STYLE, "Displays the histogram (PPO minus signal line) as columns. Bright columns mark a spread still widening, faded ones a spread narrowing back."),
34954
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the zero line and the PPO line with a vertical gradient.")
34287
34955
  ],
34288
34956
  compute: (bars, inputs) => {
34289
34957
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
34290
- const fast = ema(src, num3(inputs, "fastLength", 12));
34291
- const slow = ema(src, num3(inputs, "slowLength", 26));
34958
+ const average = (kind2, v, len) => kind2 === "SMA" ? sma(v, len) : ema(v, len);
34959
+ const kind = str3(inputs, "maType", "EMA");
34960
+ const fast = average(kind, src, num3(inputs, "fastLength", 12));
34961
+ const slow = average(kind, src, num3(inputs, "slowLength", 26));
34292
34962
  const line = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : (f - s) / s * 100);
34293
- const signal = ema(line, num3(inputs, "signalLength", 9));
34963
+ const signal = average(str3(inputs, "signalType", "EMA"), line, num3(inputs, "signalLength", 9));
34294
34964
  const hist = sub(line, signal);
34295
- return {
34296
- plots: [
34297
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: histColors(hist), base: 0 },
34298
- { key: "ppo", title: "PPO", values: line, color: SERIES_LINE, width: 2 },
34299
- { key: "signal", title: "Signal", values: signal, color: WARNING }
34300
- ],
34301
- levels: ZERO_LEVEL
34302
- };
34965
+ const bull = str3(inputs, "bullColor", BULLISH);
34966
+ const bear = str3(inputs, "bearColor", BEARISH);
34967
+ const out = centeredOscillator({
34968
+ key: "ppo",
34969
+ title: "PPO Line",
34970
+ values: line,
34971
+ ink: { bull, bear, neutral: bear },
34972
+ zeroInk: transp(NEUTRAL, 90),
34973
+ fill: bool(inputs, "gradientFill", true),
34974
+ extraPlots: [{ key: "signal", title: "Signal Line", values: signal, color: str3(inputs, "signalColor", WARNING) }]
34975
+ });
34976
+ if (bool(inputs, "showHistogram", true)) {
34977
+ const colors = hist.map((x, i) => {
34978
+ if (!Number.isFinite(x)) return null;
34979
+ const prev2 = i > 0 && Number.isFinite(hist[i - 1]) ? hist[i - 1] : x;
34980
+ const ink = x >= 0 ? bull : bear;
34981
+ const expanding = x >= 0 ? x > prev2 : x < prev2;
34982
+ return transp(ink, expanding ? 40 : 70);
34983
+ });
34984
+ out.plots.unshift({ key: "hist", title: "Histogram", values: hist, kind: "columns", color: bull, colors, base: 0 });
34985
+ }
34986
+ return out;
34303
34987
  }
34304
34988
  };
34305
34989
  var awesome = {
@@ -34307,48 +34991,67 @@ var awesome = {
34307
34991
  title: "Awesome Oscillator",
34308
34992
  shortTitle: "AO",
34309
34993
  overlay: false,
34310
- inputs: [],
34311
- compute: (bars) => {
34994
+ inputs: [
34995
+ colorInput(BULLISH, "Rising", "risingColor", "Histogram color when the oscillator is higher than on the previous bar."),
34996
+ colorInput(BEARISH, "Falling", "fallingColor", "Histogram color when the oscillator is lower than or equal to the previous bar."),
34997
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
34998
+ ],
34999
+ compute: (bars, inputs) => {
34312
35000
  const hl2 = sourceValues(bars, "HL2");
34313
35001
  const ao = sub(sma(hl2, 5), sma(hl2, 34));
35002
+ const rising = str3(inputs, "risingColor", BULLISH);
35003
+ const falling = str3(inputs, "fallingColor", BEARISH);
34314
35004
  const colors = ao.map((x, i) => {
34315
35005
  if (!Number.isFinite(x)) return null;
34316
35006
  const prev2 = i > 0 ? ao[i - 1] : Number.NaN;
34317
- return Number.isFinite(prev2) && x < prev2 ? BEARISH : BULLISH;
35007
+ return Number.isFinite(prev2) && x > prev2 ? rising : falling;
34318
35008
  });
34319
35009
  return {
34320
- plots: [{ key: "ao", title: "AO", values: ao, kind: "histogram", color: BULLISH, colors, base: 0 }],
34321
- levels: ZERO_LEVEL
35010
+ plots: [{ key: "ao", title: "AO", values: ao, kind: "columns", color: rising, colors, base: 0 }],
35011
+ levels: [{ key: "zero", price: 0, color: str3(inputs, "zeroColor", NEUTRAL), lineStyle: "dashed", title: "Zero Line" }]
34322
35012
  };
34323
35013
  }
34324
35014
  };
35015
+ var CCI_SCALING = 0.015;
34325
35016
  var cci = {
34326
35017
  type: "commodity-channel-index",
34327
35018
  title: "Commodity Channel Index",
34328
35019
  shortTitle: "CCI",
34329
35020
  overlay: false,
34330
- inputs: [lengthInput(20), sourceInput("HLC3"), colorInput()],
35021
+ inputs: [
35022
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used for the moving average and the mean absolute deviation of the source."),
35023
+ sourceInput("HLC3", "source", "Source", "Price series measured by the CCI; the typical price hlc3 is the classic choice."),
35024
+ floatInput("upper", "Upper Level", 100, -1e3, 1e3, 10, "Overbought reference level, +100 classically."),
35025
+ floatInput("lower", "Lower Level", -100, -1e3, 1e3, 10, "Oversold reference level, \u2212100 classically."),
35026
+ colorInput(NEUTRAL, "CCI Line", "color", "Color of the CCI line."),
35027
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the CCI reads above zero."),
35028
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the CCI reads below zero."),
35029
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the upper, lower and zero reference lines.")
35030
+ ],
34331
35031
  compute: (bars, inputs) => {
34332
35032
  const len = num3(inputs, "length", 20);
34333
35033
  const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
34334
35034
  const basis = sma(src, len);
34335
- const dev = new Array(src.length).fill(Number.NaN);
34336
- for (let i = len - 1; i < src.length; i++) {
34337
- const mean = basis[i];
34338
- if (!Number.isFinite(mean)) continue;
34339
- let d = 0;
34340
- for (let k = i - len + 1; k <= i; k++) d += Math.abs(src[k] - mean);
34341
- dev[i] = d / len;
34342
- }
35035
+ const dev = meanDev(src, len);
34343
35036
  const values = src.map((x, i) => {
34344
35037
  const b = basis[i];
34345
35038
  const d = dev[i];
34346
- return Number.isFinite(b) && Number.isFinite(d) && d !== 0 ? (x - b) / (0.015 * d) : Number.NaN;
35039
+ if (!Number.isFinite(b) || !Number.isFinite(d)) return Number.NaN;
35040
+ return d === 0 ? 0 : (x - b) / (CCI_SCALING * d);
35041
+ });
35042
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
35043
+ return centeredOscillator({
35044
+ key: "cci",
35045
+ title: "CCI",
35046
+ values,
35047
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
35048
+ zeroInk: levelInk,
35049
+ zeroLineStyle: "dotted",
35050
+ extraLevels: [
35051
+ { key: "upper", price: num3(inputs, "upper", 100), color: levelInk, lineStyle: "dashed", title: "Upper Level" },
35052
+ { key: "lower", price: num3(inputs, "lower", -100), color: levelInk, lineStyle: "dashed", title: "Lower Level" }
35053
+ ]
34347
35054
  });
34348
- return {
34349
- plots: [{ key: "cci", title: "CCI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34350
- levels: guideLevels(-100, 100, 0)
34351
- };
34352
35055
  }
34353
35056
  };
34354
35057
  var williamsR = {
@@ -34356,31 +35059,78 @@ var williamsR = {
34356
35059
  title: "Williams %R",
34357
35060
  shortTitle: "%R",
34358
35061
  overlay: false,
34359
- inputs: [lengthInput(14), colorInput()],
35062
+ inputs: [
35063
+ lengthInput(14, "length", "Length", 5e3, "Lookback length of the highest high / lowest low range. 14 is the classic setting."),
35064
+ sourceInput("Close", "source", "Source", "Series located within the lookback range. Close is the classic choice."),
35065
+ ...thresholdInputs(-20, -80, -100, 0),
35066
+ colorInput(NEUTRAL, "%R Line", "color", "Color of the %R line between the thresholds. Toward either level it blends into that level\u2019s color."),
35067
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
35068
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level.")
35069
+ ],
34360
35070
  compute: (bars, inputs) => {
34361
- const values = map(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "length", 14)), (x) => x - 100);
34362
- return {
34363
- plots: [{ key: "wr", title: "%R", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34364
- levels: guideLevels(-80, -20)
34365
- };
35071
+ const len = num3(inputs, "length", 14);
35072
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
35073
+ const hh = highest(highs(bars), len);
35074
+ const ll = lowest(lows(bars), len);
35075
+ const values = src.map((x, i) => {
35076
+ const h = hh[i];
35077
+ const l = ll[i];
35078
+ return Number.isFinite(h) && Number.isFinite(l) && h > l ? -100 * (h - x) / (h - l) : Number.NaN;
35079
+ });
35080
+ return boundedOscillator({
35081
+ key: "wr",
35082
+ title: "%R",
35083
+ values,
35084
+ scale: { top: 0, bottom: -100 },
35085
+ overbought: num3(inputs, "overbought", -20),
35086
+ oversold: num3(inputs, "oversold", -80),
35087
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
35088
+ levelInk: NEUTRAL
35089
+ });
34366
35090
  }
34367
35091
  };
34368
- var cmo = {
35092
+ var cmoSpec = {
34369
35093
  type: "chande-momentum-oscillator",
34370
35094
  title: "Chande Momentum Oscillator",
34371
35095
  shortTitle: "CMO",
34372
35096
  overlay: false,
34373
- inputs: [lengthInput(9), sourceInput(), colorInput()],
35097
+ inputs: [
35098
+ lengthInput(9, "length", "Length", 5e3, "Number of bars over which up moves and down moves are summed."),
35099
+ sourceInput("Close", "source", "Source", "Price series used to measure bar-to-bar changes."),
35100
+ floatInput("overbought", "Overbought Level", 50, 0, 100, 1, "Level above which the oscillator is considered overbought."),
35101
+ floatInput("oversold", "Oversold Level", -50, -100, 0, 1, "Level below which the oscillator is considered oversold."),
35102
+ colorInput(NEUTRAL, "CMO Line", "color", "Color of the CMO line."),
35103
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while momentum is positive, and shading of the oversold zone."),
35104
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while momentum is negative, and shading of the overbought zone."),
35105
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought, oversold, and zero lines.")
35106
+ ],
34374
35107
  compute: (bars, inputs) => {
34375
35108
  const len = num3(inputs, "length", 9);
34376
35109
  const d = change(sourceValues(bars, str3(inputs, "source", "Close")));
34377
- const su = sum(map(d, (x) => Math.max(x, 0)), len);
34378
- const sd = sum(map(d, (x) => Math.max(-x, 0)), len);
34379
- const values = zip(su, sd, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
34380
- return {
34381
- plots: [{ key: "cmo", title: "CMO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34382
- levels: guideLevels(-50, 50, 0)
34383
- };
35110
+ const up = sum(map(d, (x) => Math.max(x, 0)), len);
35111
+ const down = sum(map(d, (x) => Math.max(-x, 0)), len);
35112
+ const values = zip(up, down, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
35113
+ const bull = str3(inputs, "bullColor", BULLISH);
35114
+ const bear = str3(inputs, "bearColor", BEARISH);
35115
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
35116
+ const overbought = num3(inputs, "overbought", 50);
35117
+ const oversold = num3(inputs, "oversold", -50);
35118
+ const out = centeredOscillator({
35119
+ key: "cmo",
35120
+ title: "CMO",
35121
+ values,
35122
+ ink: { bull, bear, neutral: str3(inputs, "color", NEUTRAL) },
35123
+ zeroInk: levelInk,
35124
+ zeroLineStyle: "dotted",
35125
+ extraLevels: [
35126
+ { key: "overbought", price: overbought, color: levelInk, lineStyle: "dashed", title: "Overbought" },
35127
+ { key: "oversold", price: oversold, color: levelInk, lineStyle: "dashed", title: "Oversold" }
35128
+ ]
35129
+ });
35130
+ const zones = thresholdZones(values.length, { overbought, oversold, top: 100, bottom: -100 }, { overbought: bear, oversold: bull });
35131
+ out.plots.push(...zones.plots);
35132
+ out.bands = [...out.bands ?? [], ...zones.bands];
35133
+ return out;
34384
35134
  }
34385
35135
  };
34386
35136
  var connorsRsi = {
@@ -34389,10 +35139,14 @@ var connorsRsi = {
34389
35139
  shortTitle: "CRSI",
34390
35140
  overlay: false,
34391
35141
  inputs: [
34392
- lengthInput(3, "rsiLength", "RSI length"),
34393
- lengthInput(2, "streakLength", "Streak RSI length"),
34394
- lengthInput(100, "rankLength", "Percent-rank length"),
34395
- colorInput()
35142
+ lengthInput(3, "rsiLength", "RSI Length", 5e3, "Length of the Wilder RSI applied to the closing price."),
35143
+ lengthInput(2, "streakLength", "Streak RSI Length", 5e3, "Length of the Wilder RSI applied to the up/down close streak."),
35144
+ lengthInput(100, "rankLength", "Percent Rank Length", 5e3, "Lookback of the percent rank applied to the 1-bar rate of change of the closing price."),
35145
+ ...thresholdInputs(90, 10),
35146
+ colorInput(NEUTRAL, "CRSI Line", "color", "Color of the Connors RSI line."),
35147
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Line, fill and zone color toward and below the oversold level."),
35148
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Line, fill and zone color toward and above the overbought level."),
35149
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought and oversold levels.")
34396
35150
  ],
34397
35151
  compute: (bars, inputs) => {
34398
35152
  const c = closes(bars);
@@ -34410,10 +35164,17 @@ var connorsRsi = {
34410
35164
  const z = r[i];
34411
35165
  return Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) ? (x + y + z) / 3 : Number.NaN;
34412
35166
  });
34413
- return {
34414
- plots: [{ key: "crsi", title: "CRSI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34415
- levels: guideLevels(20, 80, 50)
34416
- };
35167
+ return boundedOscillator({
35168
+ key: "crsi",
35169
+ title: "Connors RSI",
35170
+ values,
35171
+ scale: { top: 100, bottom: 0 },
35172
+ overbought: num3(inputs, "overbought", 90),
35173
+ oversold: num3(inputs, "oversold", 10),
35174
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
35175
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
35176
+ midpointWash: true
35177
+ });
34417
35178
  }
34418
35179
  };
34419
35180
  var fisher = {
@@ -34421,7 +35182,13 @@ var fisher = {
34421
35182
  title: "Fisher Transform",
34422
35183
  shortTitle: "Fisher",
34423
35184
  overlay: false,
34424
- inputs: [lengthInput(9)],
35185
+ inputs: [
35186
+ lengthInput(9, "length", "Length", 5e3, "Lookback period used to locate the median price hl2 within its highest-lowest range. Ehlers' original length is 10; 9 is the common platform default."),
35187
+ colorInput(NEUTRAL, "Fisher Line", "color", "Color of the Fisher line when it sits exactly on its trigger."),
35188
+ colorInput(NEUTRAL, "Trigger Line", "triggerColor", "Color of the trigger line, the Fisher value delayed one bar."),
35189
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the Fisher line and fill while it is above the trigger."),
35190
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the Fisher line and fill while it is below the trigger.")
35191
+ ],
34425
35192
  compute: (bars, inputs) => {
34426
35193
  const len = num3(inputs, "length", 9);
34427
35194
  const hl2 = sourceValues(bars, "HL2");
@@ -34429,23 +35196,39 @@ var fisher = {
34429
35196
  const lo = lowest(hl2, len);
34430
35197
  const n = bars.length;
34431
35198
  const out = new Array(n).fill(Number.NaN);
35199
+ let position = 0;
34432
35200
  let value = 0;
34433
- let fish = 0;
34434
35201
  for (let i = 0; i < n; i++) {
34435
35202
  const h = hi[i];
34436
35203
  const l = lo[i];
34437
35204
  if (!Number.isFinite(h) || !Number.isFinite(l)) continue;
34438
- const norm = h === l ? 0 : (hl2[i] - l) / (h - l) - 0.5;
34439
- value = Math.max(-0.999, Math.min(0.999, 0.66 * norm + 0.67 * value));
34440
- fish = 0.5 * Math.log((1 + value) / (1 - value)) + 0.5 * fish;
34441
- out[i] = fish;
34442
- }
35205
+ const raw = h > l ? (hl2[i] - l) / (h - l) - 0.5 : 0;
35206
+ position = 0.66 * raw + 0.67 * position;
35207
+ position = position > 0.99 ? 0.999 : position < -0.99 ? -0.999 : position;
35208
+ value = 0.5 * Math.log((1 + position) / (1 - position)) + 0.5 * value;
35209
+ out[i] = value;
35210
+ }
35211
+ const trigger = shift(out, 1);
35212
+ const neutral = str3(inputs, "color", NEUTRAL);
35213
+ const bull = str3(inputs, "bullColor", BULLISH);
35214
+ const bear = str3(inputs, "bearColor", BEARISH);
35215
+ const inkOf = (i) => {
35216
+ const f = out[i];
35217
+ const t = trigger[i];
35218
+ if (!Number.isFinite(t)) return neutral;
35219
+ return f > t ? bull : f < t ? bear : neutral;
35220
+ };
34443
35221
  return {
34444
35222
  plots: [
34445
- { key: "fisher", title: "Fisher", values: out, color: SERIES_LINE, width: 2 },
34446
- { key: "trigger", title: "Trigger", values: shift(out, 1), color: WARNING }
35223
+ { key: "fisher", title: "Fisher", values: out, color: neutral, width: 2, colors: out.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) },
35224
+ { key: "trigger", title: "Trigger", values: trigger, color: str3(inputs, "triggerColor", NEUTRAL) }
34447
35225
  ],
34448
- levels: ZERO_LEVEL
35226
+ bands: [{ key: "spread", from: "fisher", to: "trigger", color: transp(neutral, 80), colors: out.map((x, i) => Number.isFinite(x) ? transp(inkOf(i), 80) : null) }],
35227
+ levels: [
35228
+ { key: "upper", price: 1.5, color: NEUTRAL, lineStyle: "dashed", title: "Upper Level" },
35229
+ { key: "lower", price: -1.5, color: NEUTRAL, lineStyle: "dashed", title: "Lower Level" },
35230
+ { key: "zero", price: 0, color: NEUTRAL, lineStyle: "dotted", title: "Zero Line" }
35231
+ ]
34449
35232
  };
34450
35233
  }
34451
35234
  };
@@ -34453,93 +35236,180 @@ var trix = {
34453
35236
  type: "trix",
34454
35237
  title: "TRIX",
34455
35238
  overlay: false,
34456
- inputs: [lengthInput(18), colorInput()],
35239
+ inputs: [
35240
+ lengthInput(18, "length", "Length", 5e3, "Length of each of the three EMA smoothing passes applied to the close price."),
35241
+ lengthInput(9, "signalLength", "Signal Length", 500, "Length of the EMA applied to TRIX to form the signal line."),
35242
+ colorInput(NEUTRAL, "TRIX Color", "color", "Color of the TRIX line."),
35243
+ colorInput(WARNING, "Signal Color", "signalColor", "Color of the signal line."),
35244
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the TRIX line and its gradient fill while TRIX holds above zero."),
35245
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the TRIX line and its gradient fill while TRIX holds below zero.")
35246
+ ],
34457
35247
  compute: (bars, inputs) => {
34458
35248
  const len = num3(inputs, "length", 18);
34459
- const logClose = map(closes(bars), Math.log);
34460
- const smooth = ema(ema(ema(logClose, len), len), len);
34461
- const values = map(change(smooth), (x) => x * 1e4);
34462
- return {
34463
- plots: [{ key: "trix", title: "TRIX", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34464
- levels: ZERO_LEVEL
34465
- };
35249
+ const smooth = ema(ema(ema(closes(bars), len), len), len);
35250
+ const values = roc(smooth, 1);
35251
+ return centeredOscillator({
35252
+ key: "trix",
35253
+ title: "TRIX",
35254
+ values,
35255
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
35256
+ zeroInk: NEUTRAL,
35257
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(values, num3(inputs, "signalLength", 9)), color: str3(inputs, "signalColor", WARNING) }]
35258
+ });
34466
35259
  }
34467
35260
  };
34468
- function tsiLine(src, long, short) {
35261
+ function tsiRatio(src, long, short) {
34469
35262
  const mom = change(src);
34470
35263
  const numer = ema(ema(mom, long), short);
34471
35264
  const denom = ema(ema(map(mom, Math.abs), long), short);
34472
- return zip(numer, denom, (a, b) => b === 0 ? 0 : 100 * a / b);
35265
+ return zip(numer, denom, (a, b) => b === 0 ? 0 : a / b);
34473
35266
  }
34474
35267
  var tsi = {
34475
35268
  type: "true-strength-index",
34476
35269
  title: "True Strength Index",
34477
35270
  shortTitle: "TSI",
34478
35271
  overlay: false,
34479
- inputs: [lengthInput(25, "longLength", "Long length"), lengthInput(13, "shortLength", "Short length"), lengthInput(13, "signalLength", "Signal length")],
35272
+ inputs: [
35273
+ lengthInput(25, "longLength", "Long Length", 5e3, "Length of the first (long) EMA applied to the 1-bar price change."),
35274
+ lengthInput(13, "shortLength", "Short Length", 5e3, "Length of the second (short) EMA applied to the long-smoothed price change."),
35275
+ lengthInput(13, "signalLength", "Signal Length", 500, "Length of the EMA of TSI used as the signal line."),
35276
+ colorInput(NEUTRAL, "TSI", "color", "Color of the TSI line."),
35277
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line."),
35278
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the TSI line and its gradient fill while TSI holds above zero."),
35279
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the TSI line and its gradient fill while TSI holds below zero.")
35280
+ ],
34480
35281
  compute: (bars, inputs) => {
34481
- const line = tsiLine(closes(bars), num3(inputs, "longLength", 25), num3(inputs, "shortLength", 13));
34482
- return {
34483
- plots: [
34484
- { key: "tsi", title: "TSI", values: line, color: SERIES_LINE, width: 2 },
34485
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: WARNING }
34486
- ],
34487
- levels: ZERO_LEVEL
34488
- };
35282
+ const values = map(tsiRatio(closes(bars), num3(inputs, "longLength", 25), num3(inputs, "shortLength", 13)), (x) => 100 * x);
35283
+ return centeredOscillator({
35284
+ key: "tsi",
35285
+ title: "TSI",
35286
+ values,
35287
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
35288
+ zeroInk: NEUTRAL,
35289
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(values, num3(inputs, "signalLength", 13)), color: str3(inputs, "signalColor", WARNING) }]
35290
+ });
34489
35291
  }
34490
35292
  };
35293
+ var SMI_INDICATOR = "Indicator";
35294
+ var SMI_OSCILLATOR = "Oscillator";
34491
35295
  var smiErgodic = {
34492
35296
  type: "smi-ergodic",
34493
35297
  title: "SMI Ergodic Oscillator",
34494
35298
  shortTitle: "SMIE",
34495
35299
  overlay: false,
34496
- inputs: [lengthInput(5, "shortLength", "Short length"), lengthInput(20, "longLength", "Long length"), lengthInput(5, "signalLength", "Signal length")],
35300
+ inputs: [
35301
+ lengthInput(20, "longLength", "Long Length", 5e3, "Length of the first EMA smoothing applied to the momentum of the closing price."),
35302
+ lengthInput(5, "shortLength", "Short Length", 5e3, "Length of the second EMA smoothing applied to the momentum of the closing price."),
35303
+ lengthInput(5, "signalLength", "Signal Length", 500, "Length of the EMA of the SMI that forms the signal line."),
35304
+ optionInput("display", "Display", SMI_INDICATOR, [SMI_INDICATOR, SMI_OSCILLATOR], "Indicator plots the SMI and its signal line. Oscillator plots the difference between them as a histogram."),
35305
+ colorInput(NEUTRAL, "SMI", "color", "Color of the SMI line while it sits exactly on its signal line."),
35306
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line."),
35307
+ colorInput(BULLISH, "Oscillator Grow", "growColor", "Color of the rising oscillator histogram, and of the SMI line while it leads its signal."),
35308
+ colorInput(BEARISH, "Oscillator Fall", "fallColor", "Color of the falling oscillator histogram, and of the SMI line while it lags its signal.")
35309
+ ],
34497
35310
  compute: (bars, inputs) => {
34498
- const line = tsiLine(closes(bars), num3(inputs, "longLength", 20), num3(inputs, "shortLength", 5));
34499
- const signal = ema(line, num3(inputs, "signalLength", 5));
34500
- const osc = sub(line, signal);
35311
+ const smi = tsiRatio(closes(bars), num3(inputs, "longLength", 20), num3(inputs, "shortLength", 5));
35312
+ const signal = ema(smi, num3(inputs, "signalLength", 5));
35313
+ const grow = str3(inputs, "growColor", BULLISH);
35314
+ const fall = str3(inputs, "fallColor", BEARISH);
35315
+ const zero = [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed", title: "Zero" }];
35316
+ if (str3(inputs, "display", SMI_INDICATOR) === SMI_OSCILLATOR) {
35317
+ const osc = sub(smi, signal);
35318
+ const colors = osc.map((x, i) => {
35319
+ if (!Number.isFinite(x)) return null;
35320
+ const prev2 = i > 0 && Number.isFinite(osc[i - 1]) ? osc[i - 1] : x;
35321
+ return x > prev2 ? grow : fall;
35322
+ });
35323
+ return { plots: [{ key: "osc", title: "Oscillator", values: osc, kind: "columns", color: grow, colors, base: 0 }], levels: zero };
35324
+ }
35325
+ const neutral = str3(inputs, "color", NEUTRAL);
35326
+ const inkOf = (i) => {
35327
+ const a = smi[i];
35328
+ const b = signal[i];
35329
+ if (!Number.isFinite(b)) return neutral;
35330
+ return a > b ? grow : a < b ? fall : neutral;
35331
+ };
34501
35332
  return {
34502
35333
  plots: [
34503
- { key: "osc", title: "Oscillator", values: osc, kind: "histogram", color: BULLISH, colors: histColors(osc), base: 0 },
34504
- { key: "smi", title: "SMI", values: line, color: SERIES_LINE, width: 2 },
34505
- { key: "signal", title: "Signal", values: signal, color: WARNING }
35334
+ { key: "smi", title: "SMI", values: smi, color: neutral, colors: smi.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) },
35335
+ { key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) }
34506
35336
  ],
34507
- levels: ZERO_LEVEL
35337
+ bands: [{ key: "spread", from: "smi", to: "signal", color: transp(neutral, 75), colors: smi.map((x, i) => Number.isFinite(x) ? transp(inkOf(i), 75) : null) }],
35338
+ levels: zero
34508
35339
  };
34509
35340
  }
34510
35341
  };
35342
+ var STC_MID = 50;
35343
+ var TRIGGERS = "Trigger Levels";
34511
35344
  var stc = {
34512
35345
  type: "schaff-trend-cycle",
34513
35346
  title: "Schaff Trend Cycle",
34514
35347
  shortTitle: "Schaff Trend Cycle",
34515
35348
  overlay: false,
34516
- inputs: [lengthInput(10, "cycleLength", "Cycle length"), lengthInput(23, "fastLength", "Fast length"), lengthInput(50, "slowLength", "Slow length")],
35349
+ inputs: [
35350
+ sourceInput("Close", "source", "Source", "Price series used to build the MACD that the trend cycle is measured on."),
35351
+ lengthInput(23, "fastLength", "Fast Length", 5e3, "Fast EMA length of the underlying MACD."),
35352
+ lengthInput(50, "slowLength", "Slow Length", 5e3, "Slow EMA length of the underlying MACD."),
35353
+ { ...lengthInput(10, "cycleLength", "Cycle Length", 5e3, "Window of the two stochastic passes. Each pass measures where its input sits inside the range of its own values over this many bars."), min: 2 },
35354
+ floatInput("factor", "Smoothing Factor", 0.5, 0.01, 1, 0.05, "%D style recursive smoothing applied after each stochastic pass. 0.5 is the published default."),
35355
+ { ...floatInput("upper", "Upper Trigger", 75, 0, 100, 1, "Upper trigger line. A cross down through it flags an emerging downtrend."), group: TRIGGERS },
35356
+ { ...floatInput("lower", "Lower Trigger", 25, 0, 100, 1, "Lower trigger line. A cross up through it flags an emerging uptrend."), group: TRIGGERS },
35357
+ colorInput(BULLISH, "Bullish Color", "bullColor", "Color of the trend cycle line and gradient while the cycle holds the upper half of its 0-100 scale."),
35358
+ colorInput(BEARISH, "Bearish Color", "bearColor", "Color of the trend cycle line and gradient while the cycle holds the lower half of its 0-100 scale."),
35359
+ colorInput(NEUTRAL, "Levels Color", "levelsColor", "Color of the dashed trigger lines and the dotted midline."),
35360
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Vertical gradient between the midline and the trend cycle, fading out toward the midline.")
35361
+ ],
34517
35362
  compute: (bars, inputs) => {
34518
35363
  const len = num3(inputs, "cycleLength", 10);
34519
- const src = closes(bars);
35364
+ const factor = num3(inputs, "factor", 0.5);
35365
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
34520
35366
  const macdLine = sub(ema(src, num3(inputs, "fastLength", 23)), ema(src, num3(inputs, "slowLength", 50)));
34521
- const stochOf = (v) => {
35367
+ const stochNorm = (v) => {
34522
35368
  const lo = lowest(v, len);
34523
35369
  const hi = highest(v, len);
34524
35370
  const out = new Array(v.length).fill(Number.NaN);
34525
- let prevRaw = Number.NaN;
34526
- let smoothed = Number.NaN;
35371
+ let pct = Number.NaN;
34527
35372
  for (let i = 0; i < v.length; i++) {
34528
35373
  const x = v[i];
34529
35374
  const l = lo[i];
34530
35375
  const h = hi[i];
34531
35376
  if (!Number.isFinite(x) || !Number.isFinite(l) || !Number.isFinite(h)) continue;
34532
- const raw = h > l ? (x - l) / (h - l) * 100 : Number.isFinite(prevRaw) ? prevRaw : 50;
34533
- prevRaw = raw;
34534
- smoothed = Number.isFinite(smoothed) ? smoothed + 0.5 * (raw - smoothed) : raw;
35377
+ if (h > l) pct = 100 * (x - l) / (h - l);
35378
+ out[i] = pct;
35379
+ }
35380
+ return out;
35381
+ };
35382
+ const smoothStep = (v) => {
35383
+ const out = new Array(v.length).fill(Number.NaN);
35384
+ let smoothed = Number.NaN;
35385
+ for (let i = 0; i < v.length; i++) {
35386
+ const x = v[i];
35387
+ if (!Number.isFinite(x)) continue;
35388
+ smoothed = Number.isFinite(smoothed) ? smoothed + factor * (x - smoothed) : x;
34535
35389
  out[i] = smoothed;
34536
35390
  }
34537
35391
  return out;
34538
35392
  };
34539
- const values = stochOf(stochOf(macdLine));
35393
+ const values = smoothStep(stochNorm(smoothStep(stochNorm(macdLine))));
35394
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
35395
+ const bull = str3(inputs, "bullColor", BULLISH);
35396
+ const bear = str3(inputs, "bearColor", BEARISH);
35397
+ const plots = [
35398
+ { key: "stc", title: "Schaff Trend Cycle", values, color: bear, width: 2, colors: values.map((x) => Number.isFinite(x) ? x > STC_MID ? bull : bear : null) }
35399
+ ];
35400
+ const bands = [];
35401
+ if (bool(inputs, "gradientFill", true)) {
35402
+ plots.push(anchorPlot("midAnchor", "Midline Anchor", values.length, STC_MID));
35403
+ bands.push({ key: "wash", from: "stc", to: "midAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, STC_MID, (x) => x > STC_MID ? bull : bear) });
35404
+ }
34540
35405
  return {
34541
- plots: [{ key: "stc", title: "STC", values, color: SERIES_LINE, width: 2 }],
34542
- levels: guideLevels(25, 75)
35406
+ plots,
35407
+ bands,
35408
+ levels: [
35409
+ { key: "upper", price: num3(inputs, "upper", 75), color: transp(levelInk, 25), lineStyle: "dashed", title: "Upper Trigger" },
35410
+ { key: "lower", price: num3(inputs, "lower", 25), color: transp(levelInk, 25), lineStyle: "dashed", title: "Lower Trigger" },
35411
+ { key: "midline", price: STC_MID, color: transp(levelInk, 60), lineStyle: "dotted", title: "Midline" }
35412
+ ]
34543
35413
  };
34544
35414
  }
34545
35415
  };
@@ -34548,22 +35418,37 @@ var kst = {
34548
35418
  title: "Know Sure Thing",
34549
35419
  shortTitle: "KST",
34550
35420
  overlay: false,
34551
- inputs: [sourceInput(), lengthInput(9, "signalLength", "Signal length")],
35421
+ inputs: [
35422
+ lengthInput(10, "rocLength1", "ROC Length #1", 5e3, "Lookback of the first (shortest) rate of change."),
35423
+ lengthInput(15, "rocLength2", "ROC Length #2", 5e3, "Lookback of the second rate of change."),
35424
+ lengthInput(20, "rocLength3", "ROC Length #3", 5e3, "Lookback of the third rate of change."),
35425
+ lengthInput(30, "rocLength4", "ROC Length #4", 5e3, "Lookback of the fourth (longest) rate of change."),
35426
+ lengthInput(10, "smaLength1", "SMA Length #1", 5e3, "Smoothing length applied to the first rate of change (RCMA #1)."),
35427
+ lengthInput(10, "smaLength2", "SMA Length #2", 5e3, "Smoothing length applied to the second rate of change (RCMA #2)."),
35428
+ lengthInput(10, "smaLength3", "SMA Length #3", 5e3, "Smoothing length applied to the third rate of change (RCMA #3)."),
35429
+ lengthInput(15, "smaLength4", "SMA Length #4", 5e3, "Smoothing length applied to the fourth rate of change (RCMA #4)."),
35430
+ lengthInput(9, "signalLength", "Signal Length", 500, "Simple moving average length of the signal line."),
35431
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the KST line while above zero."),
35432
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the KST line while below zero."),
35433
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line.")
35434
+ ],
34552
35435
  compute: (bars, inputs) => {
34553
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
34554
- const line = [
34555
- sma(roc(src, 10), 10),
34556
- sma(roc(src, 15), 10),
34557
- sma(roc(src, 20), 10),
34558
- sma(roc(src, 30), 15)
34559
- ].reduce((acc, r, i) => zip(acc, r, (a, b) => a + b * (i + 1)), new Array(src.length).fill(0));
34560
- return {
34561
- plots: [
34562
- { key: "kst", title: "KST", values: line, color: SERIES_LINE, width: 2 },
34563
- { key: "signal", title: "Signal", values: sma(line, num3(inputs, "signalLength", 9)), color: WARNING }
34564
- ],
34565
- levels: ZERO_LEVEL
34566
- };
35436
+ const c = closes(bars);
35437
+ const rcma = (rocKey, rocDefault, smaKey, smaDefault) => sma(roc(c, num3(inputs, rocKey, rocDefault)), num3(inputs, smaKey, smaDefault));
35438
+ const values = [rcma("rocLength1", 10, "smaLength1", 10), rcma("rocLength2", 15, "smaLength2", 10), rcma("rocLength3", 20, "smaLength3", 10), rcma("rocLength4", 30, "smaLength4", 15)].reduce(
35439
+ (acc, r, i) => zip(acc, r, (a, b) => a + b * (i + 1)),
35440
+ new Array(c.length).fill(0)
35441
+ );
35442
+ const bull = str3(inputs, "bullColor", BULLISH);
35443
+ return centeredOscillator({
35444
+ key: "kst",
35445
+ title: "KST",
35446
+ values,
35447
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
35448
+ zeroInk: NEUTRAL,
35449
+ valueTransparency: 60,
35450
+ extraPlots: [{ key: "signal", title: "Signal", values: sma(values, num3(inputs, "signalLength", 9)), color: str3(inputs, "signalColor", WARNING) }]
35451
+ });
34567
35452
  }
34568
35453
  };
34569
35454
  var coppock = {
@@ -34572,19 +35457,25 @@ var coppock = {
34572
35457
  shortTitle: "Coppock",
34573
35458
  overlay: false,
34574
35459
  inputs: [
34575
- lengthInput(10, "wmaLength", "WMA length"),
34576
- lengthInput(14, "longRoc", "Long RoC length"),
34577
- lengthInput(11, "shortRoc", "Short RoC length"),
34578
- sourceInput(),
34579
- colorInput()
35460
+ lengthInput(10, "wmaLength", "WMA Length", 5e3, "Length of the weighted moving average applied to the sum of the two rates of change."),
35461
+ lengthInput(14, "longRoc", "Long RoC Length", 5e3, "Lookback of the long rate of change, in bars. The original definition uses 14 monthly closes."),
35462
+ lengthInput(11, "shortRoc", "Short RoC Length", 5e3, "Lookback of the short rate of change, in bars. The original definition uses 11 monthly closes."),
35463
+ sourceInput("Close", "source", "Source", "Price series used for the rate of change calculations."),
35464
+ colorInput(NEUTRAL, "Coppock Curve", "color", "Color of the Coppock Curve line."),
35465
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the curve reads above zero."),
35466
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the curve reads below zero."),
35467
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
34580
35468
  ],
34581
35469
  compute: (bars, inputs) => {
34582
35470
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
34583
35471
  const values = wma(zip(roc(src, num3(inputs, "longRoc", 14)), roc(src, num3(inputs, "shortRoc", 11)), (a, b) => a + b), num3(inputs, "wmaLength", 10));
34584
- return {
34585
- plots: [{ key: "coppock", title: "Coppock", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34586
- levels: ZERO_LEVEL
34587
- };
35472
+ return centeredOscillator({
35473
+ key: "coppock",
35474
+ title: "Coppock Curve",
35475
+ values,
35476
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
35477
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
35478
+ });
34588
35479
  }
34589
35480
  };
34590
35481
  var dpo = {
@@ -34592,42 +35483,76 @@ var dpo = {
34592
35483
  title: "Detrended Price Oscillator",
34593
35484
  shortTitle: "DPO",
34594
35485
  overlay: false,
34595
- inputs: [lengthInput(21), colorInput()],
35486
+ inputs: [
35487
+ lengthInput(21, "length", "Length", 5e3, "Lookback of the simple moving average that price is detrended against. The comparison is displaced by Length / 2 + 1 bars."),
35488
+ sourceInput("Close", "source", "Source", "Price series used in the detrending calculation."),
35489
+ boolInput("centered", "Centered", false, void 0, "Shifts the oscillator back by Length / 2 + 1 bars so it is centered on the prices it detrends. The most recent bars then show no value."),
35490
+ colorInput(NEUTRAL, "DPO", "color", "Color of the line when it sits exactly at zero."),
35491
+ colorInput(BULLISH, "Above Zero", "bullColor", "Color of the line and fill while the oscillator is above zero."),
35492
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while the oscillator is below zero."),
35493
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
35494
+ ],
34596
35495
  compute: (bars, inputs) => {
34597
35496
  const len = num3(inputs, "length", 21);
34598
- const barsback = Math.floor(len / 2) + 1;
34599
- const values = sub(closes(bars), shift(sma(closes(bars), len), barsback));
34600
- return {
34601
- plots: [{ key: "dpo", title: "DPO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34602
- levels: ZERO_LEVEL
34603
- };
35497
+ const barsBack = Math.trunc(len / 2) + 1;
35498
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
35499
+ const basis = sma(src, len);
35500
+ const centered = bool(inputs, "centered", false);
35501
+ const values = centered ? shift(sub(shift(src, barsBack), basis), -barsBack) : sub(src, shift(basis, barsBack));
35502
+ return centeredOscillator({
35503
+ key: "dpo",
35504
+ title: "DPO",
35505
+ values,
35506
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
35507
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
35508
+ });
34604
35509
  }
34605
35510
  };
35511
+ var ROC_PERCENT = "ROC %";
35512
+ var ROC_MOMENTUM = "Momentum";
34606
35513
  var rocSpec = {
34607
35514
  type: "rate-of-change",
34608
35515
  title: "Rate of Change",
34609
35516
  shortTitle: "ROC",
34610
35517
  overlay: false,
34611
- inputs: [lengthInput(9), sourceInput(), colorInput()],
34612
- compute: (bars, inputs) => ({
34613
- plots: [
34614
- {
34615
- key: "roc",
34616
- title: "RoC",
34617
- values: roc(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 9)),
34618
- color: str3(inputs, "color", SERIES_LINE),
34619
- width: 2
34620
- }
34621
- ],
34622
- levels: ZERO_LEVEL
34623
- })
35518
+ inputs: [
35519
+ lengthInput(9, "length", "Length", 5e3, "Lookback period; the source is compared with its value this many bars ago."),
35520
+ sourceInput("Close", "source", "Source", "Series used as input for the calculation."),
35521
+ optionInput("mode", "Mode", ROC_PERCENT, [ROC_PERCENT, ROC_MOMENTUM], "ROC % plots the change as a percentage of the source value from the lookback; Momentum plots the raw difference."),
35522
+ colorInput(NEUTRAL, "Line", "color", "Color of the line when the reading sits exactly at zero."),
35523
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the line and fill while the reading is above zero."),
35524
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line and fill while the reading is below zero."),
35525
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the line and zero, following the line color.")
35526
+ ],
35527
+ compute: (bars, inputs) => {
35528
+ const len = num3(inputs, "length", 9);
35529
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
35530
+ const values = str3(inputs, "mode", ROC_PERCENT) === ROC_MOMENTUM ? change(src, len) : roc(src, len);
35531
+ return centeredOscillator({
35532
+ key: "roc",
35533
+ title: "ROC",
35534
+ values,
35535
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
35536
+ zeroInk: NEUTRAL,
35537
+ fill: bool(inputs, "gradientFill", true)
35538
+ });
35539
+ }
34624
35540
  };
34625
35541
  var ultimate = {
34626
35542
  type: "ultimate-oscillator",
34627
35543
  title: "Ultimate Oscillator",
34628
35544
  shortTitle: "UO",
34629
35545
  overlay: false,
34630
- inputs: [lengthInput(7, "fastLength", "Fast length"), lengthInput(14, "middleLength", "Middle length"), lengthInput(28, "slowLength", "Slow length"), colorInput()],
35546
+ inputs: [
35547
+ lengthInput(7, "fastLength", "Fast Length", 5e3, "Bars in the fastest averaging window. Weighted 4 in the final blend, so it dominates the oscillator."),
35548
+ lengthInput(14, "middleLength", "Middle Length", 5e3, "Bars in the intermediate averaging window. Weighted 2 in the final blend."),
35549
+ lengthInput(28, "slowLength", "Slow Length", 5e3, "Bars in the slowest averaging window. Weighted 1 in the final blend."),
35550
+ ...thresholdInputs(70, 30, 0, 100, "Overbought", "Oversold"),
35551
+ colorInput(NEUTRAL, "Oscillator", "color", "Color of the line midway between the levels. Toward either level it blends into that level\u2019s color."),
35552
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought and oversold levels."),
35553
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line at the oversold level and of the oversold zone shading."),
35554
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line at the overbought level and of the overbought zone shading.")
35555
+ ],
34631
35556
  compute: (bars, inputs) => {
34632
35557
  const n = bars.length;
34633
35558
  const bp = new Array(n).fill(Number.NaN);
@@ -34635,10 +35560,9 @@ var ultimate = {
34635
35560
  for (let i = 0; i < n; i++) {
34636
35561
  const b = bars[i];
34637
35562
  const pc = i > 0 ? bars[i - 1].close : b.close;
34638
- const lo = Math.min(b.low, pc);
34639
- const hi = Math.max(b.high, pc);
34640
- bp[i] = b.close - lo;
34641
- tr[i] = hi - lo;
35563
+ const trueLow = Math.min(b.low, pc);
35564
+ bp[i] = b.close - trueLow;
35565
+ tr[i] = Math.max(b.high, pc) - trueLow;
34642
35566
  }
34643
35567
  const avg = (len) => zip(sum(bp, len), sum(tr, len), (a, b) => b === 0 ? Number.NaN : a / b);
34644
35568
  const a7 = avg(num3(inputs, "fastLength", 7));
@@ -34649,10 +35573,17 @@ var ultimate = {
34649
35573
  const z = a28[i];
34650
35574
  return Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) ? 100 * (4 * x + 2 * y + z) / 7 : Number.NaN;
34651
35575
  });
34652
- return {
34653
- plots: [{ key: "uo", title: "UO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34654
- levels: guideLevels(30, 70, 50)
34655
- };
35576
+ return boundedOscillator({
35577
+ key: "uo",
35578
+ title: "UO",
35579
+ values,
35580
+ scale: { top: 100, bottom: 0 },
35581
+ overbought: num3(inputs, "overbought", 70),
35582
+ oversold: num3(inputs, "oversold", 30),
35583
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
35584
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
35585
+ zoneTransparency: 91
35586
+ });
34656
35587
  }
34657
35588
  };
34658
35589
  var rvgi = {
@@ -34660,7 +35591,11 @@ var rvgi = {
34660
35591
  title: "Relative Vigor Index",
34661
35592
  shortTitle: "RVGI",
34662
35593
  overlay: false,
34663
- inputs: [lengthInput(10)],
35594
+ inputs: [
35595
+ lengthInput(10, "length", "Length", 5e3, "Number of bars summed for the smoothed numerator (close \u2212 open) and denominator (high \u2212 low)."),
35596
+ colorInput(BULLISH, "RVGI", "color", "Color of the RVGI line."),
35597
+ colorInput(BEARISH, "Signal", "signalColor", "Color of the signal line.")
35598
+ ],
34664
35599
  compute: (bars, inputs) => {
34665
35600
  const len = num3(inputs, "length", 10);
34666
35601
  const co = swma(sub(closes(bars), opens(bars)));
@@ -34668,31 +35603,49 @@ var rvgi = {
34668
35603
  const line = zip(sum(co, len), sum(hl, len), (a, b) => b === 0 ? 0 : a / b);
34669
35604
  return {
34670
35605
  plots: [
34671
- { key: "rvgi", title: "RVGI", values: line, color: SERIES_LINE, width: 2 },
34672
- { key: "signal", title: "Signal", values: swma(line), color: WARNING }
35606
+ { key: "rvgi", title: "RVGI", values: line, color: str3(inputs, "color", BULLISH) },
35607
+ { key: "signal", title: "Signal", values: swma(line), color: str3(inputs, "signalColor", BEARISH) }
34673
35608
  ],
34674
- levels: ZERO_LEVEL
35609
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed", title: "Zero" }]
34675
35610
  };
34676
35611
  }
34677
35612
  };
35613
+ var RVI_EMA = "EMA";
35614
+ var RVI_RMA = "Wilder (RMA)";
34678
35615
  var relVolatility = {
34679
35616
  type: "relative-volatility-index",
34680
35617
  title: "Relative Volatility Index",
34681
35618
  shortTitle: "RVI",
34682
35619
  overlay: false,
34683
- inputs: [lengthInput(10, "stdevLength", "StdDev length"), lengthInput(14, "smoothLength", "Smoothing length"), sourceInput(), colorInput()],
35620
+ inputs: [
35621
+ lengthInput(10, "stdevLength", "Stdev Length", 5e3, "Number of bars used for the standard deviation of closing prices that serves as each bar's volatility reading."),
35622
+ lengthInput(14, "smoothLength", "Smoothing Length", 5e3, "Number of bars used to smooth the up and down volatility streams."),
35623
+ optionInput("smoothType", "Smoothing Type", RVI_EMA, [RVI_EMA, RVI_RMA], "Moving average applied to the volatility streams. EMA is Dorsey's classic calculation; Wilder (RMA) matches platforms that use Wilder smoothing."),
35624
+ ...thresholdInputs(80, 20),
35625
+ colorInput(NEUTRAL, "RVI Line", "color", "Color of the RVI line at the midline. Toward either threshold it blends into that threshold\u2019s color."),
35626
+ colorInput(BULLISH, "Up Volatility", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
35627
+ colorInput(BEARISH, "Down Volatility", "oversoldColor", "Color of the line and zone shading at and below the oversold level.")
35628
+ ],
34684
35629
  compute: (bars, inputs) => {
35630
+ const c = closes(bars);
35631
+ const sigma = stdev(c, num3(inputs, "stdevLength", 10));
35632
+ const d = change(c);
35633
+ const up = zip(sigma, d, (s, x) => x > 0 ? s : 0);
35634
+ const down = zip(sigma, d, (s, x) => x < 0 ? s : 0);
34685
35635
  const smoothLen = num3(inputs, "smoothLength", 14);
34686
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
34687
- const dev = stdev(src, num3(inputs, "stdevLength", 10));
34688
- const d = change(src);
34689
- const up = rma(zip(dev, d, (s, x) => x > 0 ? s : 0), smoothLen);
34690
- const dn = rma(zip(dev, d, (s, x) => x <= 0 ? s : 0), smoothLen);
34691
- const values = zip(up, dn, (u, w) => u + w === 0 ? 50 : 100 * u / (u + w));
34692
- return {
34693
- plots: [{ key: "rvi", title: "RVI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34694
- levels: guideLevels(20, 80, 50)
34695
- };
35636
+ const smooth = str3(inputs, "smoothType", RVI_EMA) === RVI_RMA ? rma : ema;
35637
+ const values = zip(smooth(up, smoothLen), smooth(down, smoothLen), (u, w) => u + w === 0 ? 0 : 100 * u / (u + w));
35638
+ return boundedOscillator({
35639
+ key: "rvi",
35640
+ title: "RVI",
35641
+ values,
35642
+ scale: { top: 100, bottom: 0 },
35643
+ overbought: num3(inputs, "overbought", 80),
35644
+ oversold: num3(inputs, "oversold", 20),
35645
+ ink: { low: str3(inputs, "oversoldColor", BEARISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BULLISH) },
35646
+ levelInk: NEUTRAL,
35647
+ width: 2
35648
+ });
34696
35649
  }
34697
35650
  };
34698
35651
  var bop = {
@@ -34700,28 +35653,43 @@ var bop = {
34700
35653
  title: "Balance of Power",
34701
35654
  shortTitle: "BOP",
34702
35655
  overlay: false,
34703
- inputs: [colorInput()],
35656
+ inputs: [
35657
+ boolInput("smoothing", "Smoothing", false, void 0, "Smooth the raw Balance of Power with a simple moving average instead of plotting the single-bar value."),
35658
+ lengthInput(14, "smoothLength", "Length", 5e3, "Simple moving average length applied when smoothing is enabled."),
35659
+ colorInput(NEUTRAL, "BOP Color", "color", "Color of the line at exactly zero."),
35660
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the Balance of Power is above zero \u2014 buyers in control of the bar."),
35661
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the Balance of Power is below zero \u2014 sellers in control of the bar.")
35662
+ ],
34704
35663
  compute: (bars, inputs) => {
34705
- const values = bars.map((b) => b.high === b.low ? 0 : (b.close - b.open) / (b.high - b.low));
34706
- return {
34707
- plots: [{ key: "bop", title: "BOP", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34708
- levels: ZERO_LEVEL
34709
- };
35664
+ const raw = bars.map((b) => b.high === b.low ? 0 : (b.close - b.open) / (b.high - b.low));
35665
+ const values = bool(inputs, "smoothing", false) ? sma(raw, num3(inputs, "smoothLength", 14)) : raw;
35666
+ return centeredOscillator({
35667
+ key: "bop",
35668
+ title: "BOP",
35669
+ values,
35670
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
35671
+ zeroInk: NEUTRAL,
35672
+ valueTransparency: 60
35673
+ });
34710
35674
  }
34711
35675
  };
34712
35676
  var elderRay = {
34713
35677
  type: "elder-ray",
34714
35678
  title: "Elder Ray",
34715
35679
  overlay: false,
34716
- inputs: [lengthInput(13)],
35680
+ inputs: [
35681
+ lengthInput(13, "length", "EMA Length", 5e3, "Number of bars in the exponential moving average of closing prices used as the reference level for Bull Power and Bear Power. Alexander Elder's classic setting is 13."),
35682
+ colorInput(BULLISH, "Bull Power", "bullColor", "Color of the Bull Power columns."),
35683
+ colorInput(BEARISH, "Bear Power", "bearColor", "Color of the Bear Power columns.")
35684
+ ],
34717
35685
  compute: (bars, inputs) => {
34718
35686
  const basis = ema(closes(bars), num3(inputs, "length", 13));
34719
35687
  return {
34720
35688
  plots: [
34721
- { key: "bull", title: "Bull power", values: sub(highs(bars), basis), kind: "histogram", color: BULLISH, base: 0 },
34722
- { key: "bear", title: "Bear power", values: sub(lows(bars), basis), kind: "histogram", color: BEARISH, base: 0 }
35689
+ { key: "bull", title: "Bull Power", values: sub(highs(bars), basis), kind: "columns", color: str3(inputs, "bullColor", BULLISH), base: 0 },
35690
+ { key: "bear", title: "Bear Power", values: sub(lows(bars), basis), kind: "columns", color: str3(inputs, "bearColor", BEARISH), base: 0 }
34723
35691
  ],
34724
- levels: ZERO_LEVEL
35692
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 30), lineStyle: "dashed", title: "Zero Line" }]
34725
35693
  };
34726
35694
  }
34727
35695
  };
@@ -34730,31 +35698,66 @@ var ttmSqueeze = {
34730
35698
  title: "TTM Squeeze",
34731
35699
  overlay: false,
34732
35700
  inputs: [
34733
- lengthInput(20),
34734
- { key: "bbMult", title: "Bollinger multiplier", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 },
34735
- { key: "kcMult", title: "Keltner multiplier", type: "float", defval: 1.5, min: 0.1, max: 50, step: 0.1 }
35701
+ { ...lengthInput(20, "length", "Length", 5e3, "Shared lookback for the Bollinger Bands, the Keltner Channels, and the momentum regression. John Carter's original setting is 20."), min: 2 },
35702
+ sourceInput("Close", "source", "Source", "Price series used for the shared moving average basis, the standard deviation, and the momentum displacement."),
35703
+ floatInput("bbMult", "Bollinger Bands Multiplier", 2, 0.1, 50, 0.1, "Standard deviation multiplier for the Bollinger Bands."),
35704
+ floatInput("kcMult", "Keltner Channels Multiplier", 1.5, 0.1, 50, 0.1, "ATR multiplier for the Keltner Channels. A smaller multiplier demands a tighter compression before the squeeze turns on."),
35705
+ colorInput(BULLISH, "Momentum Up", "bullColor", "Histogram color while momentum is positive. Bars building away from zero print solid, bars fading back print dimmed."),
35706
+ colorInput(BEARISH, "Momentum Down", "bearColor", "Histogram color while momentum is negative. Bars building away from zero print solid, bars fading back print dimmed."),
35707
+ colorInput(WARNING, "Squeeze On", "squeezeOnColor", "Zero-line dot color while both Bollinger Bands sit fully inside the Keltner Channels."),
35708
+ colorInput(NEUTRAL, "Squeeze Off", "squeezeOffColor", "Zero-line dot color while the Bollinger Bands trade outside the Keltner Channels."),
35709
+ boolInput("markFires", "Mark Squeeze Fires", true, STYLE, "Draws a cross on the zero line on the first bar where the bands re-emerge outside the channels, colored by the side of the momentum histogram.")
34736
35710
  ],
34737
35711
  compute: (bars, inputs) => {
34738
35712
  const len = num3(inputs, "length", 20);
34739
- const c = closes(bars);
34740
- const basis = sma(c, len);
34741
- const dev = map(stdev(c, len), (x) => x * num3(inputs, "bbMult", 2));
34742
- const kcRange = map(rma(trueRange(bars), len), (x) => x * num3(inputs, "kcMult", 1.5));
34743
- const donchianMid = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => (h + l) / 2);
34744
- const anchor = zip(donchianMid, basis, (d, b) => (d + b) / 2);
34745
- const mom = linreg(sub(c, anchor), len);
34746
- const squeezed = c.map((_, i) => {
35713
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
35714
+ const basis = sma(src, len);
35715
+ const dev = map(stdev(src, len), (x) => x * num3(inputs, "bbMult", 2));
35716
+ const kcRange = map(atr(bars, len), (x) => x * num3(inputs, "kcMult", 1.5));
35717
+ const rangeMid = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => (h + l) / 2);
35718
+ const anchor = zip(rangeMid, basis, (d, b) => (d + b) / 2);
35719
+ const mom = linreg(sub(src, anchor), len, 0);
35720
+ const squeezeOn = basis.map((b, i) => {
34747
35721
  const d = dev[i];
34748
35722
  const r = kcRange[i];
34749
- return Number.isFinite(d) && Number.isFinite(r) ? d < r : false;
35723
+ if (!Number.isFinite(b) || !Number.isFinite(d) || !Number.isFinite(r)) return null;
35724
+ return d < r;
34750
35725
  });
34751
- const stateColors = squeezed.map((s, i) => Number.isFinite(mom[i]) ? s ? BEARISH : BULLISH : null);
34752
- return {
34753
- plots: [
34754
- { key: "mom", title: "Momentum", values: mom, kind: "histogram", color: BULLISH, colors: histColors(mom), base: 0 },
34755
- { key: "squeeze", title: "Squeeze", values: mom.map((x) => Number.isFinite(x) ? 0 : Number.NaN), kind: "cross", color: INFO, colors: stateColors }
34756
- ]
34757
- };
35726
+ const bull = str3(inputs, "bullColor", BULLISH);
35727
+ const bear = str3(inputs, "bearColor", BEARISH);
35728
+ const onInk = str3(inputs, "squeezeOnColor", WARNING);
35729
+ const offInk = str3(inputs, "squeezeOffColor", NEUTRAL);
35730
+ const momColors = mom.map((x, i) => {
35731
+ if (!Number.isFinite(x)) return null;
35732
+ const prev2 = i > 0 && Number.isFinite(mom[i - 1]) ? mom[i - 1] : x;
35733
+ const rising = x > prev2;
35734
+ return x >= 0 ? rising ? bull : transp(bull, 60) : rising ? transp(bear, 60) : bear;
35735
+ });
35736
+ const plots = [
35737
+ { key: "mom", title: "Momentum", values: mom, kind: "columns", color: bull, colors: momColors, base: 0 },
35738
+ {
35739
+ key: "state",
35740
+ title: "Squeeze State",
35741
+ values: squeezeOn.map((s) => s == null ? Number.NaN : 0),
35742
+ kind: "circles",
35743
+ color: offInk,
35744
+ width: 2,
35745
+ colors: squeezeOn.map((s) => s == null ? null : s ? onInk : offInk)
35746
+ }
35747
+ ];
35748
+ if (bool(inputs, "markFires", true)) {
35749
+ const fires = squeezeOn.map((s, i) => s === false && squeezeOn[i - 1] === true ? 0 : Number.NaN);
35750
+ plots.push({
35751
+ key: "fire",
35752
+ title: "Squeeze Fire",
35753
+ values: fires,
35754
+ kind: "cross",
35755
+ color: offInk,
35756
+ width: 3,
35757
+ colors: fires.map((x, i) => Number.isFinite(x) ? Number.isFinite(mom[i]) ? mom[i] >= 0 ? bull : bear : offInk : null)
35758
+ });
35759
+ }
35760
+ return { plots };
34758
35761
  }
34759
35762
  };
34760
35763
  var oscillatorSpecs = [
@@ -34766,7 +35769,7 @@ var oscillatorSpecs = [
34766
35769
  awesome,
34767
35770
  cci,
34768
35771
  williamsR,
34769
- cmo,
35772
+ cmoSpec,
34770
35773
  connorsRsi,
34771
35774
  fisher,
34772
35775
  trix,
@@ -34790,15 +35793,19 @@ var aroon = {
34790
35793
  type: "aroon",
34791
35794
  title: "Aroon",
34792
35795
  overlay: false,
34793
- inputs: [lengthInput(14)],
35796
+ inputs: [
35797
+ lengthInput(14, "length", "Length", 5e3, "Lookback length used to locate the highest high and lowest low."),
35798
+ colorInput(BULLISH, "Aroon Up", "upColor", "Color of the Aroon Up line."),
35799
+ colorInput(BEARISH, "Aroon Down", "downColor", "Color of the Aroon Down line.")
35800
+ ],
34794
35801
  compute: (bars, inputs) => {
34795
35802
  const len = num3(inputs, "length", 14);
34796
- const up = map(barsSinceHighest(highs(bars), len + 1), (x) => 100 * (len - x) / len);
34797
- const down = map(barsSinceLowest(lows(bars), len + 1), (x) => 100 * (len - x) / len);
35803
+ const up = map(barsSinceHighest(highs(bars), len), (x) => 100 * (len - x) / len);
35804
+ const down = map(barsSinceLowest(lows(bars), len), (x) => 100 * (len - x) / len);
34798
35805
  return {
34799
35806
  plots: [
34800
- { key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
34801
- { key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
35807
+ { key: "up", title: "Aroon Up", values: up, color: str3(inputs, "upColor", BULLISH) },
35808
+ { key: "down", title: "Aroon Down", values: down, color: str3(inputs, "downColor", BEARISH) }
34802
35809
  ]
34803
35810
  };
34804
35811
  }
@@ -34808,10 +35815,16 @@ var adx = {
34808
35815
  title: "Average Directional Index",
34809
35816
  shortTitle: "ADX",
34810
35817
  overlay: false,
34811
- inputs: [lengthInput(14, "diLength", "DI length"), lengthInput(14, "adxLength", "ADX smoothing")],
35818
+ inputs: [
35819
+ lengthInput(14, "diLength", "DI Length", 5e3, "Wilder RMA smoothing length applied to directional movement and true range when computing +DI and \u2212DI."),
35820
+ lengthInput(14, "adxLength", "ADX Smoothing", 5e3, "Wilder RMA smoothing length applied to DX to obtain the ADX line."),
35821
+ floatInput("keyLevel", "Key Level", 25, 0, 100, 1, "Trend strength reference level; ADX above it is commonly read as a trending market."),
35822
+ colorInput(BULLISH, "+DI Color", "plusDiColor", "Color of the plus directional indicator, and of the ADX line above the key level while +DI leads."),
35823
+ colorInput(BEARISH, "-DI Color", "minusDiColor", "Color of the minus directional indicator, and of the ADX line above the key level while \u2212DI leads."),
35824
+ colorInput(NEUTRAL, "ADX Color", "adxColor", "Color of the ADX line while it sits below the key level \u2014 no trend strong enough to trade.")
35825
+ ],
34812
35826
  compute: (bars, inputs) => {
34813
35827
  const diLen = num3(inputs, "diLength", 14);
34814
- const adxLen = num3(inputs, "adxLength", 14);
34815
35828
  const n = bars.length;
34816
35829
  const plusDm = new Array(n).fill(Number.NaN);
34817
35830
  const minusDm = new Array(n).fill(Number.NaN);
@@ -34825,13 +35838,23 @@ var adx = {
34825
35838
  const plusDi = zip(rma(plusDm, diLen), atrLine, (d, a) => a === 0 ? 0 : 100 * d / a);
34826
35839
  const minusDi = zip(rma(minusDm, diLen), atrLine, (d, a) => a === 0 ? 0 : 100 * d / a);
34827
35840
  const dx = zip(plusDi, minusDi, (p, m) => p + m === 0 ? 0 : 100 * Math.abs(p - m) / (p + m));
35841
+ const adxLine = rma(dx, num3(inputs, "adxLength", 14));
35842
+ const plusInk = str3(inputs, "plusDiColor", BULLISH);
35843
+ const minusInk = str3(inputs, "minusDiColor", BEARISH);
35844
+ const neutralInk = str3(inputs, "adxColor", NEUTRAL);
35845
+ const keyLevel = num3(inputs, "keyLevel", 25);
35846
+ const adxColors = adxLine.map((x, i) => {
35847
+ if (!Number.isFinite(x)) return null;
35848
+ if (x <= keyLevel) return neutralInk;
35849
+ return (plusDi[i] ?? 0) > (minusDi[i] ?? 0) ? plusInk : minusInk;
35850
+ });
34828
35851
  return {
34829
35852
  plots: [
34830
- { key: "adx", title: "ADX", values: rma(dx, adxLen), color: SERIES_LINE, width: 2 },
34831
- { key: "plusDi", title: "+DI", values: plusDi, color: BULLISH },
34832
- { key: "minusDi", title: "-DI", values: minusDi, color: BEARISH }
35853
+ { key: "plusDi", title: "+DI", values: plusDi, color: transp(plusInk, 30) },
35854
+ { key: "minusDi", title: "-DI", values: minusDi, color: transp(minusInk, 30) },
35855
+ { key: "adx", title: "ADX", values: adxLine, color: neutralInk, width: 2, colors: adxColors }
34833
35856
  ],
34834
- levels: [{ key: "threshold", price: 25, color: NEUTRAL, lineStyle: "dotted" }]
35857
+ levels: [{ key: "keyLevel", price: keyLevel, color: NEUTRAL, lineStyle: "dashed", title: "Key Level" }]
34835
35858
  };
34836
35859
  }
34837
35860
  };
@@ -34840,7 +35863,12 @@ var vortex = {
34840
35863
  title: "Vortex Indicator",
34841
35864
  shortTitle: "VI",
34842
35865
  overlay: false,
34843
- inputs: [lengthInput(14)],
35866
+ inputs: [
35867
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to sum the vortex movements (VM+ and VM\u2212) and the true range."),
35868
+ colorInput(BULLISH, "VI+", "plusColor", "Color of the positive vortex line (VI+)."),
35869
+ colorInput(BEARISH, "VI-", "minusColor", "Color of the negative vortex line (VI\u2212)."),
35870
+ colorInput(NEUTRAL, "Baseline", "baselineColor", "Color of the dotted baseline drawn at 1.")
35871
+ ],
34844
35872
  compute: (bars, inputs) => {
34845
35873
  const len = num3(inputs, "length", 14);
34846
35874
  const n = bars.length;
@@ -34853,25 +35881,48 @@ var vortex = {
34853
35881
  const trSum = sum(trueRange(bars), len);
34854
35882
  return {
34855
35883
  plots: [
34856
- { key: "viPlus", title: "VI+", values: zip(sum(vmPlus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: BULLISH, width: 2 },
34857
- { key: "viMinus", title: "VI-", values: zip(sum(vmMinus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: BEARISH, width: 2 }
35884
+ { key: "viPlus", title: "VI+", values: zip(sum(vmPlus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: str3(inputs, "plusColor", BULLISH) },
35885
+ { key: "viMinus", title: "VI-", values: zip(sum(vmMinus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: str3(inputs, "minusColor", BEARISH) }
34858
35886
  ],
34859
- levels: [{ key: "one", price: 1, color: NEUTRAL, lineStyle: "dotted" }]
35887
+ levels: [{ key: "baseline", price: 1, color: str3(inputs, "baselineColor", NEUTRAL), lineStyle: "dotted", title: "Baseline" }]
34860
35888
  };
34861
35889
  }
34862
35890
  };
34863
35891
  var trendSpecs = [aroon, adx, vortex];
34864
35892
 
34865
35893
  // src/core/native-indicators/classics/volatility.ts
35894
+ function expansionColors(values, calm, expanding) {
35895
+ return values.map((x, i) => {
35896
+ if (!Number.isFinite(x)) return null;
35897
+ const prev2 = i > 0 && Number.isFinite(values[i - 1]) ? values[i - 1] : x;
35898
+ return x > prev2 ? expanding : calm;
35899
+ });
35900
+ }
35901
+ function washedToZero(key, title, values, colors, fallback, valueTransparency, width = 1) {
35902
+ return {
35903
+ plots: [{ key, title, values, color: fallback, width, colors }, anchorPlot("zeroAnchor", "Zero Anchor", values.length, 0)],
35904
+ bands: [{ key: "wash", from: key, to: "zeroAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, 0, (_x, i) => colors[i] ?? fallback, valueTransparency) }]
35905
+ };
35906
+ }
34866
35907
  var atrSpec = {
34867
35908
  type: "average-true-range",
34868
35909
  title: "Average True Range",
34869
35910
  shortTitle: "ATR",
34870
35911
  overlay: false,
34871
- inputs: [lengthInput(14), colorInput()],
34872
- compute: (bars, inputs) => ({
34873
- plots: [{ key: "atr", title: "ATR", values: atr(bars, num3(inputs, "length", 14)), color: str3(inputs, "color", SERIES_LINE), width: 2 }]
34874
- })
35912
+ inputs: [
35913
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to average the true range."),
35914
+ optionInput("smoothing", "Smoothing", "RMA", ["RMA", "SMA", "EMA", "WMA"], "Moving average applied to the true range. RMA is Wilder's original smoothing used by the classic ATR."),
35915
+ colorInput(NEUTRAL, "ATR Line", "color", "Color of the ATR line while volatility contracts or holds flat."),
35916
+ colorInput(WARNING, "Expanding", "expandColor", "Color of the ATR line while it rises \u2014 volatility expanding bar to bar.")
35917
+ ],
35918
+ compute: (bars, inputs) => {
35919
+ const len = num3(inputs, "length", 14);
35920
+ const tr = trueRange(bars);
35921
+ const kind = str3(inputs, "smoothing", "RMA");
35922
+ const values = kind === "SMA" ? sma(tr, len) : kind === "EMA" ? ema(tr, len) : kind === "WMA" ? wma(tr, len) : rma(tr, len);
35923
+ const calm = str3(inputs, "color", NEUTRAL);
35924
+ return washedToZero("atr", "ATR", values, expansionColors(values, calm, str3(inputs, "expandColor", WARNING)), calm, 75);
35925
+ }
34875
35926
  };
34876
35927
  var historicalVolatility = {
34877
35928
  type: "historical-volatility",
@@ -34879,14 +35930,23 @@ var historicalVolatility = {
34879
35930
  shortTitle: "HV",
34880
35931
  overlay: false,
34881
35932
  inputs: [
34882
- lengthInput(10),
34883
- { 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)" },
34884
- colorInput()
35933
+ { ...lengthInput(10, "length", "Length", 5e3, "Lookback window in bars for the standard deviation of close-to-close log returns."), min: 2 },
35934
+ boolInput("annualize", "Annualize", true, void 0, "Scale the per-bar volatility to an annual figure by multiplying by the square root of the periods per year."),
35935
+ intInput("periodsPerYear", "Periods Per Year", 252, 1, 1e5, "Number of periods in a year used for annualization: 252 for daily bars, 52 for weekly, 12 for monthly."),
35936
+ colorInput(WARNING, "Volatility Line", "color", "Color of the line at its recent highs; it fades toward grey as volatility contracts.")
34885
35937
  ],
34886
35938
  compute: (bars, inputs) => {
34887
- const logReturns = map(change(map(closes(bars), Math.log)), (x) => x);
34888
- const values = map(stdev(logReturns, num3(inputs, "length", 10)), (x) => x * Math.sqrt(num3(inputs, "annual", 365)) * 100);
34889
- return { plots: [{ key: "hv", title: "HV", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
35939
+ const len = num3(inputs, "length", 10);
35940
+ const logReturns = sub(map(closes(bars), Math.log), shift(map(closes(bars), Math.log), 1));
35941
+ const meanReturn = map(sum(logReturns, len), (s) => s / len);
35942
+ const squaredSum = sum(map(logReturns, (r) => r * r), len);
35943
+ const factor = bool(inputs, "annualize", true) ? Math.sqrt(num3(inputs, "periodsPerYear", 252)) : 1;
35944
+ const values = zip(squaredSum, meanReturn, (sq, m) => Math.sqrt(Math.max(sq - len * m * m, 0) / (len - 1)) * factor * 100);
35945
+ const low = lowest(values, 100);
35946
+ const high = highest(values, 100);
35947
+ const accent = str3(inputs, "color", WARNING);
35948
+ const colors = values.map((x, i) => Number.isFinite(x) ? gradient(x, low[i] ?? 0, high[i] ?? 0, NEUTRAL, accent) : null);
35949
+ return washedToZero("hv", "Historical Volatility", values, colors, accent, 75, 2);
34890
35950
  }
34891
35951
  };
34892
35952
  var chaikinVolatility = {
@@ -34894,13 +35954,26 @@ var chaikinVolatility = {
34894
35954
  title: "Chaikin Volatility",
34895
35955
  shortTitle: "CHV",
34896
35956
  overlay: false,
34897
- inputs: [lengthInput(10, "emaLength", "EMA length"), lengthInput(10, "rocLength", "RoC length"), colorInput()],
35957
+ inputs: [
35958
+ lengthInput(10, "emaLength", "EMA Length", 5e3, "Length of the exponential moving average applied to the bar high-low range."),
35959
+ lengthInput(10, "rocLength", "ROC Length", 5e3, "Number of bars used for the rate of change of the smoothed high-low range."),
35960
+ colorInput(NEUTRAL, "CHV Color", "color", "Color of the Chaikin Volatility line."),
35961
+ colorInput(WARNING, "Expansion", "expansionColor", "Line and fill color while the reading is above zero \u2014 the smoothed range is wider than it was ROC Length bars ago.")
35962
+ ],
34898
35963
  compute: (bars, inputs) => {
34899
35964
  const range = ema(sub(highs(bars), lows(bars)), num3(inputs, "emaLength", 10));
34900
- return {
34901
- plots: [{ key: "cv", title: "Chaikin Vol", values: roc(range, num3(inputs, "rocLength", 10)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34902
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
34903
- };
35965
+ const prior = shift(range, num3(inputs, "rocLength", 10));
35966
+ const values = zip(range, prior, (r, p) => p === 0 ? Number.NaN : 100 * (r - p) / p);
35967
+ const calm = str3(inputs, "color", NEUTRAL);
35968
+ const expansion = str3(inputs, "expansionColor", WARNING);
35969
+ return centeredOscillator({
35970
+ key: "chv",
35971
+ title: "Chaikin Volatility",
35972
+ values,
35973
+ // Only expansion is a state worth accenting; contraction stays structural grey.
35974
+ ink: { bull: expansion, bear: calm, neutral: calm },
35975
+ zeroInk: NEUTRAL
35976
+ });
34904
35977
  }
34905
35978
  };
34906
35979
  var massIndex = {
@@ -34908,14 +35981,42 @@ var massIndex = {
34908
35981
  title: "Mass Index",
34909
35982
  shortTitle: "MI",
34910
35983
  overlay: false,
34911
- inputs: [lengthInput(25), colorInput()],
35984
+ inputs: [
35985
+ lengthInput(25, "length", "Sum Length", 5e3, "Number of bars over which the single/double EMA ratio is summed."),
35986
+ lengthInput(9, "emaLength", "EMA Length", 5e3, "Length of the single EMA of the high-low range and of its double smoothing."),
35987
+ floatInput("setupLevel", "Setup Level", 27, 0, 1e3, 0.1, "Reversal bulge set-up level; a rise above it arms the pattern."),
35988
+ floatInput("triggerLevel", "Trigger Level", 26.5, 0, 1e3, 0.1, "Reversal bulge trigger level; a decline below it after a set-up completes the pattern."),
35989
+ colorInput(NEUTRAL, "Mass Index", "color", "Line color while no reversal bulge is armed."),
35990
+ colorInput(WARNING, "Armed", "armedColor", "Line color from the rise above the setup level until the decline below the trigger level."),
35991
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Reversal bulge levels color.")
35992
+ ],
34912
35993
  compute: (bars, inputs) => {
34913
- const range = sub(highs(bars), lows(bars));
34914
- const single = ema(range, 9);
34915
- const ratio = zip(single, ema(single, 9), (a, b) => b === 0 ? Number.NaN : a / b);
35994
+ const emaLen = num3(inputs, "emaLength", 9);
35995
+ const single = ema(sub(highs(bars), lows(bars)), emaLen);
35996
+ const ratio = zip(single, ema(single, emaLen), (a, b) => b === 0 ? 1 : a / b);
35997
+ const values = sum(ratio, num3(inputs, "length", 25));
35998
+ const setup = num3(inputs, "setupLevel", 27);
35999
+ const trigger = num3(inputs, "triggerLevel", 26.5);
36000
+ const calm = str3(inputs, "color", NEUTRAL);
36001
+ const armedInk = str3(inputs, "armedColor", WARNING);
36002
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
36003
+ let armed = false;
36004
+ const colors = values.map((x) => {
36005
+ if (!Number.isFinite(x)) return null;
36006
+ armed = x > setup ? true : x < trigger ? false : armed;
36007
+ return armed ? armedInk : calm;
36008
+ });
34916
36009
  return {
34917
- plots: [{ key: "mi", title: "Mass Index", values: sum(ratio, num3(inputs, "length", 25)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34918
- levels: [{ key: "reversal", price: 27, color: NEUTRAL, lineStyle: "dotted" }]
36010
+ plots: [
36011
+ { key: "mi", title: "Mass Index", values, color: calm, colors },
36012
+ anchorPlot("setupAnchor", "Setup Anchor", values.length, setup),
36013
+ anchorPlot("triggerAnchor", "Trigger Anchor", values.length, trigger)
36014
+ ],
36015
+ bands: [{ key: "bulge", from: "setupAnchor", to: "triggerAnchor", color: transp(armedInk, 90) }],
36016
+ levels: [
36017
+ { key: "setup", price: setup, color: levelInk, lineStyle: "dashed", title: "Setup Level" },
36018
+ { key: "trigger", price: trigger, color: levelInk, lineStyle: "dashed", title: "Trigger Level" }
36019
+ ]
34919
36020
  };
34920
36021
  }
34921
36022
  };
@@ -34924,30 +36025,83 @@ var standardDeviation = {
34924
36025
  title: "Standard Deviation",
34925
36026
  shortTitle: "StdDev",
34926
36027
  overlay: false,
34927
- inputs: [lengthInput(20), sourceInput(), colorInput()],
34928
- compute: (bars, inputs) => ({
34929
- plots: [
34930
- {
34931
- key: "stdev",
34932
- title: "StdDev",
34933
- values: stdev(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20)),
34934
- color: str3(inputs, "color", SERIES_LINE),
34935
- width: 2
34936
- }
34937
- ]
34938
- })
36028
+ inputs: [
36029
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used to compute the standard deviation."),
36030
+ sourceInput("Close", "source", "Source", "Price series the standard deviation is calculated on."),
36031
+ colorInput(NEUTRAL, "Standard Deviation", "color", "Line color while volatility contracts or holds flat."),
36032
+ colorInput(WARNING, "Expanding", "expandColor", "Line and fill color while volatility expands bar to bar.")
36033
+ ],
36034
+ compute: (bars, inputs) => {
36035
+ const values = stdev(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20));
36036
+ const calm = str3(inputs, "color", NEUTRAL);
36037
+ return washedToZero("stdev", "Standard Deviation", values, expansionColors(values, calm, str3(inputs, "expandColor", WARNING)), calm, 80);
36038
+ }
34939
36039
  };
34940
36040
  var ulcerIndex = {
34941
36041
  type: "ulcer-index",
34942
36042
  title: "Ulcer Index",
34943
36043
  overlay: false,
34944
- inputs: [lengthInput(14), colorInput()],
36044
+ inputs: [
36045
+ { ...lengthInput(14, "length", "Length", 500, "Lookback window. At each bar the percentage drawdown from the window's running maximum is squared; the squares are averaged and the root taken."), min: 2 },
36046
+ sourceInput("Close", "source", "Source", "Price series the Ulcer Index is computed on. Close is the standard choice."),
36047
+ floatInput("threshold", "Alert Threshold", 5, 0, 1e3, 0.25, "Reference level drawn as a dashed line. The index scales with the instrument's volatility and the window length, so set it relative to its own history."),
36048
+ colorInput(NEUTRAL, "Ulcer Index", "color", "Color of the index near zero; it blends toward the stress color as the reading approaches the threshold."),
36049
+ colorInput(BEARISH, "Stress", "stressColor", "Color of the index at and beyond the alert threshold."),
36050
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Vertical gradient between zero and the Ulcer Index, fully transparent at zero."),
36051
+ boolInput("showDrawdown", "Drawdown %", false, STYLE, "Plot the current percentage drawdown from the window high as columns below zero \u2014 the raw ingredient the index averages."),
36052
+ colorInput(transp(BEARISH, 50), "Drawdown Color", "drawdownColor", "Color of the drawdown columns."),
36053
+ colorInput(NEUTRAL, "Threshold", "thresholdColor", "Color of the alert threshold line.")
36054
+ ],
34945
36055
  compute: (bars, inputs) => {
34946
36056
  const len = num3(inputs, "length", 14);
34947
- const c = closes(bars);
34948
- const drawdown = zip(c, highest(c, len), (x, h) => h === 0 ? 0 : 100 * (x - h) / h);
34949
- const values = map(sma(map(drawdown, (d) => d * d), len), Math.sqrt);
34950
- return { plots: [{ key: "ui", title: "Ulcer", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
36057
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
36058
+ const n = src.length;
36059
+ const values = new Array(n).fill(Number.NaN);
36060
+ const drawdown = new Array(n).fill(Number.NaN);
36061
+ for (let i = len - 1; i < n; i++) {
36062
+ let runMax = Number.NaN;
36063
+ let sumSq = 0;
36064
+ let valid = true;
36065
+ let last = Number.NaN;
36066
+ for (let k = i - len + 1; k <= i; k++) {
36067
+ const price = src[k];
36068
+ if (!Number.isFinite(price)) {
36069
+ valid = false;
36070
+ break;
36071
+ }
36072
+ runMax = Number.isFinite(runMax) ? Math.max(runMax, price) : price;
36073
+ if (runMax <= 0) {
36074
+ valid = false;
36075
+ break;
36076
+ }
36077
+ last = 100 * (price - runMax) / runMax;
36078
+ sumSq += last * last;
36079
+ }
36080
+ if (!valid) continue;
36081
+ values[i] = Math.sqrt(sumSq / len);
36082
+ drawdown[i] = last;
36083
+ }
36084
+ const calm = str3(inputs, "color", NEUTRAL);
36085
+ const stress = str3(inputs, "stressColor", BEARISH);
36086
+ const threshold = num3(inputs, "threshold", 5);
36087
+ const colors = values.map((x) => Number.isFinite(x) ? threshold > 0 ? gradient(x, 0, threshold, calm, stress) : stress : null);
36088
+ const plots = [{ key: "ui", title: "Ulcer Index", values, color: calm, colors }];
36089
+ const bands = [];
36090
+ if (bool(inputs, "gradientFill", true)) {
36091
+ plots.push(anchorPlot("zeroAnchor", "Zero Base", n, 0));
36092
+ bands.push({ key: "wash", from: "ui", to: "zeroAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, 0, (_x, i) => colors[i] ?? calm) });
36093
+ }
36094
+ if (bool(inputs, "showDrawdown", false)) {
36095
+ plots.push({ key: "dd", title: "Drawdown %", values: drawdown, kind: "columns", color: str3(inputs, "drawdownColor", transp(BEARISH, 50)), base: 0 });
36096
+ }
36097
+ return {
36098
+ plots,
36099
+ bands,
36100
+ levels: [
36101
+ { key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dotted", title: "Zero Level" },
36102
+ { key: "threshold", price: threshold, color: str3(inputs, "thresholdColor", NEUTRAL), lineStyle: "dashed", title: "Alert Threshold" }
36103
+ ]
36104
+ };
34951
36105
  }
34952
36106
  };
34953
36107
  var choppiness = {
@@ -34955,19 +36109,33 @@ var choppiness = {
34955
36109
  title: "Choppiness Index",
34956
36110
  shortTitle: "CHOP",
34957
36111
  overlay: false,
34958
- inputs: [lengthInput(14), colorInput()],
36112
+ inputs: [
36113
+ { ...lengthInput(14, "length", "Length", 5e3, "Number of bars used to sum true ranges and to measure the highest high \u2212 lowest low range."), min: 2 },
36114
+ floatInput("upperBand", "Upper Band", 61.8, 0, 100, 0.1, "Readings above this level indicate a choppy, range-bound market."),
36115
+ floatInput("lowerBand", "Lower Band", 38.2, 0, 100, 0.1, "Readings below this level indicate a strong trend."),
36116
+ colorInput(NEUTRAL, "CHOP Line", "color", "Color of the Choppiness Index line."),
36117
+ colorInput(BULLISH, "Trending", "trendingColor", "Line, fill and zone color toward and below the lower band \u2014 a trending market."),
36118
+ colorInput(BEARISH, "Choppy", "choppyColor", "Line, fill and zone color toward and above the upper band \u2014 a choppy, range-bound market."),
36119
+ colorInput(transp(NEUTRAL, 90), "Band Fill", "fillColor", "Fill color of the zone between the upper and lower bands.")
36120
+ ],
34959
36121
  compute: (bars, inputs) => {
34960
36122
  const len = num3(inputs, "length", 14);
34961
36123
  const trSum = sum(trueRange(bars), len);
34962
36124
  const span = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => h - l);
34963
36125
  const values = zip(trSum, span, (t, s) => s > 0 && t > 0 ? 100 * Math.log10(t / s) / Math.log10(len) : Number.NaN);
34964
- return {
34965
- plots: [{ key: "chop", title: "CHOP", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
34966
- levels: [
34967
- { key: "upper", price: 61.8, color: NEUTRAL },
34968
- { key: "lower", price: 38.2, color: NEUTRAL }
34969
- ]
34970
- };
36126
+ return boundedOscillator({
36127
+ key: "chop",
36128
+ title: "CHOP",
36129
+ values,
36130
+ scale: { top: 100, bottom: 0 },
36131
+ overbought: num3(inputs, "upperBand", 61.8),
36132
+ oversold: num3(inputs, "lowerBand", 38.2),
36133
+ ink: { low: str3(inputs, "trendingColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "choppyColor", BEARISH) },
36134
+ levelInk: NEUTRAL,
36135
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 90)),
36136
+ midpointWash: true,
36137
+ levelTitles: { overbought: "Upper Band", oversold: "Lower Band" }
36138
+ });
34971
36139
  }
34972
36140
  };
34973
36141
  var volatilitySpecs = [atrSpec, historicalVolatility, chaikinVolatility, massIndex, standardDeviation, ulcerIndex, choppiness];
@@ -34980,17 +36148,85 @@ function moneyFlowVolume(bars) {
34980
36148
  return (2 * b.close - b.high - b.low) / (b.high - b.low) * b.volume;
34981
36149
  });
34982
36150
  }
36151
+ function slopeColors2(values, bull, bear, flat) {
36152
+ return values.map((x, i) => {
36153
+ if (!Number.isFinite(x)) return null;
36154
+ const prev2 = i > 0 ? values[i - 1] : Number.NaN;
36155
+ if (!Number.isFinite(prev2)) return flat;
36156
+ return x > prev2 ? bull : x < prev2 ? bear : flat;
36157
+ });
36158
+ }
36159
+ function volumeIndex(bars, src, base, when) {
36160
+ const out = new Array(bars.length).fill(Number.NaN);
36161
+ let index = base;
36162
+ for (let i = 0; i < bars.length; i++) {
36163
+ const v = bars[i].volume;
36164
+ const pv = i > 0 ? bars[i - 1].volume : null;
36165
+ const prev2 = i > 0 ? src[i - 1] : Number.NaN;
36166
+ const active = v != null && pv != null && Number.isFinite(v) && Number.isFinite(pv) && when(v, pv);
36167
+ if (active && Number.isFinite(prev2) && prev2 !== 0) index *= 1 + (src[i] - prev2) / prev2;
36168
+ out[i] = index;
36169
+ }
36170
+ return out;
36171
+ }
36172
+ function regimeIndex(key, title, values, signal, ink, showSignal, gradientFill, singleColor = false) {
36173
+ const inkOf = (i) => {
36174
+ const s = signal[i];
36175
+ const x = values[i];
36176
+ if (singleColor || !Number.isFinite(s)) return ink.neutral;
36177
+ return x > s ? ink.bull : x < s ? ink.bear : ink.neutral;
36178
+ };
36179
+ const plots = [
36180
+ {
36181
+ key: "signal",
36182
+ title: "Signal EMA",
36183
+ values: signal,
36184
+ color: ink.signal,
36185
+ ...showSignal ? {} : { display: { pane: false } }
36186
+ },
36187
+ { key, title, values, color: ink.neutral, width: 2, colors: values.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) }
36188
+ ];
36189
+ const bands = gradientFill ? [
36190
+ {
36191
+ key: "regime",
36192
+ from: key,
36193
+ to: "signal",
36194
+ color: transp(NEUTRAL, 100),
36195
+ gradient: values.map((x, i) => {
36196
+ const s = signal[i];
36197
+ if (!Number.isFinite(x) || !Number.isFinite(s)) return null;
36198
+ const above = x > s;
36199
+ const c = above ? ink.bull : ink.bear;
36200
+ return { topValue: Math.max(x, s), bottomValue: Math.min(x, s), topColor: transp(c, above ? 50 : 100), bottomColor: transp(c, above ? 100 : 50) };
36201
+ })
36202
+ }
36203
+ ] : [];
36204
+ return { plots, bands };
36205
+ }
34983
36206
  var obv = {
34984
36207
  type: "on-balance-volume",
34985
36208
  title: "On Balance Volume",
34986
36209
  shortTitle: "OBV",
34987
36210
  overlay: false,
34988
- inputs: [colorInput()],
36211
+ inputs: [
36212
+ boolInput("smoothing", "Smoothing MA", false, SETTINGS, "Display a moving average of OBV to act as a smoothing/signal line."),
36213
+ optionInput("maType", "MA Type", "SMA", ["SMA", "EMA", "WMA", "RMA (SMMA)"], "Moving average type used for the smoothing MA."),
36214
+ lengthInput(20, "maLength", "MA Length", 5e3, "Number of bars used for the smoothing MA."),
36215
+ colorInput(INFO, "OBV", "color", "Color of the OBV line."),
36216
+ colorInput(WARNING, "Smoothing MA", "maColor", "Color of the smoothing MA line.")
36217
+ ],
34989
36218
  compute: (bars, inputs) => {
34990
36219
  const vol = volumes(bars);
34991
36220
  const d = change(closes(bars));
34992
- const signed3 = zip(vol, d, (v, x) => x > 0 ? v : x < 0 ? -v : 0);
34993
- return { plots: [{ key: "obv", title: "OBV", values: cumSum(signed3), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
36221
+ const values = cumSum(zip(vol, d, (v, x) => x > 0 ? v : x < 0 ? -v : 0));
36222
+ const plots = [{ key: "obv", title: "OBV", values, color: str3(inputs, "color", INFO) }];
36223
+ if (bool(inputs, "smoothing", false)) {
36224
+ const len = num3(inputs, "maLength", 20);
36225
+ const kind = str3(inputs, "maType", "SMA");
36226
+ const ma = kind === "EMA" ? ema(values, len) : kind === "WMA" ? wma(values, len) : kind === "RMA (SMMA)" ? rma(values, len) : sma(values, len);
36227
+ plots.push({ key: "ma", title: "Smoothing MA", values: ma, color: str3(inputs, "maColor", WARNING) });
36228
+ }
36229
+ return { plots };
34994
36230
  }
34995
36231
  };
34996
36232
  var accDist = {
@@ -34998,24 +36234,50 @@ var accDist = {
34998
36234
  title: "Accumulation / Distribution",
34999
36235
  shortTitle: "A/D",
35000
36236
  overlay: false,
35001
- inputs: [colorInput()],
35002
- compute: (bars, inputs) => ({
35003
- plots: [{ key: "ad", title: "A/D", values: cumSum(moneyFlowVolume(bars)), color: str3(inputs, "color", SERIES_LINE), width: 2 }]
35004
- })
36237
+ inputs: [
36238
+ boolInput("slopeColoring", "Slope Coloring", true, STYLE, "Color the line by its slope \u2014 bullish while rising (accumulation), bearish while falling (distribution)."),
36239
+ colorInput(NEUTRAL, "A/D Line", "color", "Color of the line when slope coloring is disabled or the line is flat."),
36240
+ colorInput(BULLISH, "Rising", "bullColor", "Line color while the A/D Line rises."),
36241
+ colorInput(BEARISH, "Falling", "bearColor", "Line color while the A/D Line falls.")
36242
+ ],
36243
+ compute: (bars, inputs) => {
36244
+ const values = cumSum(moneyFlowVolume(bars));
36245
+ const flat = str3(inputs, "color", NEUTRAL);
36246
+ return {
36247
+ plots: [
36248
+ {
36249
+ key: "ad",
36250
+ title: "A/D",
36251
+ values,
36252
+ color: flat,
36253
+ ...bool(inputs, "slopeColoring", true) ? { colors: slopeColors2(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), flat) } : {}
36254
+ }
36255
+ ]
36256
+ };
36257
+ }
35005
36258
  };
35006
36259
  var cmf = {
35007
36260
  type: "chaikin-money-flow",
35008
36261
  title: "Chaikin Money Flow",
35009
36262
  shortTitle: "CMF",
35010
36263
  overlay: false,
35011
- inputs: [lengthInput(20), colorInput()],
36264
+ inputs: [
36265
+ lengthInput(20, "length", "Length", 5e3, "Lookback length over which money flow volume and volume are summed."),
36266
+ colorInput(NEUTRAL, "CMF Line", "color", "Color of the Chaikin Money Flow line."),
36267
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the CMF reads above zero (net money flowing in)."),
36268
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the CMF reads below zero (net money flowing out)."),
36269
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
36270
+ ],
35012
36271
  compute: (bars, inputs) => {
35013
36272
  const len = num3(inputs, "length", 20);
35014
36273
  const values = zip(sum(moneyFlowVolume(bars), len), sum(volumes(bars), len), (m, v) => v === 0 ? 0 : m / v);
35015
- return {
35016
- plots: [{ key: "cmf", title: "CMF", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35017
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35018
- };
36274
+ return centeredOscillator({
36275
+ key: "cmf",
36276
+ title: "CMF",
36277
+ values,
36278
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
36279
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
36280
+ });
35019
36281
  }
35020
36282
  };
35021
36283
  var chaikinOscillator = {
@@ -35023,14 +36285,23 @@ var chaikinOscillator = {
35023
36285
  title: "Chaikin Oscillator",
35024
36286
  shortTitle: "Chaikin Osc",
35025
36287
  overlay: false,
35026
- inputs: [lengthInput(3, "fastLength", "Fast length"), lengthInput(10, "slowLength", "Slow length"), colorInput()],
36288
+ inputs: [
36289
+ lengthInput(3, "fastLength", "Fast Length", 5e3, "Length of the fast EMA applied to the accumulation/distribution line."),
36290
+ lengthInput(10, "slowLength", "Slow Length", 5e3, "Length of the slow EMA applied to the accumulation/distribution line."),
36291
+ colorInput(NEUTRAL, "Oscillator", "color", "Color of the Chaikin Oscillator line."),
36292
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the oscillator reads above zero (accumulation)."),
36293
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the oscillator reads below zero (distribution).")
36294
+ ],
35027
36295
  compute: (bars, inputs) => {
35028
36296
  const ad = cumSum(moneyFlowVolume(bars));
35029
36297
  const values = sub(ema(ad, num3(inputs, "fastLength", 3)), ema(ad, num3(inputs, "slowLength", 10)));
35030
- return {
35031
- plots: [{ key: "co", title: "Chaikin Osc", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35032
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35033
- };
36298
+ return centeredOscillator({
36299
+ key: "co",
36300
+ title: "Chaikin Oscillator",
36301
+ values,
36302
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
36303
+ zeroInk: NEUTRAL
36304
+ });
35034
36305
  }
35035
36306
  };
35036
36307
  var eom = {
@@ -35038,20 +36309,28 @@ var eom = {
35038
36309
  title: "Ease of Movement",
35039
36310
  shortTitle: "EOM",
35040
36311
  overlay: false,
35041
- inputs: [lengthInput(14), { key: "divisor", title: "Divisor", type: "int", defval: 1e4, min: 1, max: 1e9, step: 1 }, colorInput()],
36312
+ inputs: [
36313
+ lengthInput(14, "length", "Length", 5e3, "Number of bars in the simple moving average smoothing the 1-bar Ease of Movement values."),
36314
+ intInput("divisor", "Divisor", 1e4, 1, 1e9, "Volume scaling constant of the box ratio; larger values scale the oscillator up. 10000 is the conventional default."),
36315
+ colorInput(NEUTRAL, "EOM Line", "color", "Color of the line when it sits exactly at zero."),
36316
+ colorInput(BULLISH, "Above Zero", "bullColor", "Color of the line and fill while Ease of Movement is positive."),
36317
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while Ease of Movement is negative.")
36318
+ ],
35042
36319
  compute: (bars, inputs) => {
35043
36320
  const div = num3(inputs, "divisor", 1e4);
35044
- const hl2 = sourceValues(bars, "HL2");
35045
- const move = change(hl2);
36321
+ const move = change(sourceValues(bars, "HL2"));
35046
36322
  const raw = bars.map((b, i) => {
35047
36323
  const m = move[i];
35048
- if (b.volume == null || !Number.isFinite(b.volume) || b.volume === 0 || !Number.isFinite(m)) return Number.NaN;
35049
- return div * m * (b.high - b.low) / b.volume;
36324
+ if (b.volume == null || !Number.isFinite(b.volume) || !Number.isFinite(m)) return Number.NaN;
36325
+ return b.volume === 0 ? 0 : div * m * (b.high - b.low) / b.volume;
36326
+ });
36327
+ return centeredOscillator({
36328
+ key: "eom",
36329
+ title: "EOM",
36330
+ values: sma(raw, num3(inputs, "length", 14)),
36331
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
36332
+ zeroInk: NEUTRAL
35050
36333
  });
35051
- return {
35052
- plots: [{ key: "eom", title: "EOM", values: sma(raw, num3(inputs, "length", 14)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35053
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35054
- };
35055
36334
  }
35056
36335
  };
35057
36336
  var forceIndex = {
@@ -35059,13 +36338,24 @@ var forceIndex = {
35059
36338
  title: "Force Index",
35060
36339
  shortTitle: "FI",
35061
36340
  overlay: false,
35062
- inputs: [lengthInput(13), colorInput()],
36341
+ inputs: [
36342
+ lengthInput(13, "length", "Length", 5e3, "EMA length applied to the raw 1-bar force index, (close \u2212 close[1]) \xD7 volume. Elder used 13 for the intermediate trend and 2 for short-term timing."),
36343
+ colorInput(BULLISH, "Force Index", "color", "Color of the line while above zero (buying pressure)."),
36344
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line while below zero (selling pressure).")
36345
+ ],
35063
36346
  compute: (bars, inputs) => {
35064
36347
  const raw = zip(change(closes(bars)), volumes(bars), (d, v) => d * v);
35065
- return {
35066
- plots: [{ key: "force", title: "Force", values: ema(raw, num3(inputs, "length", 13)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35067
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35068
- };
36348
+ const bull = str3(inputs, "color", BULLISH);
36349
+ return centeredOscillator({
36350
+ key: "force",
36351
+ title: "Force Index",
36352
+ values: ema(raw, num3(inputs, "length", 13)),
36353
+ // The reference reads an exact zero as buying pressure, so neutral shares the bull ink.
36354
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
36355
+ zeroInk: NEUTRAL,
36356
+ width: 2,
36357
+ valueTransparency: 60
36358
+ });
35069
36359
  }
35070
36360
  };
35071
36361
  var klinger = {
@@ -35073,19 +36363,44 @@ var klinger = {
35073
36363
  title: "Klinger Oscillator",
35074
36364
  shortTitle: "KVO",
35075
36365
  overlay: false,
35076
- inputs: [lengthInput(34, "fastLength", "Fast length"), lengthInput(55, "slowLength", "Slow length"), lengthInput(13, "signalLength", "Signal length")],
36366
+ inputs: [
36367
+ lengthInput(34, "fastLength", "Fast Length", 5e3, "Length of the fast EMA applied to the volume force."),
36368
+ lengthInput(55, "slowLength", "Slow Length", 5e3, "Length of the slow EMA applied to the volume force."),
36369
+ lengthInput(13, "signalLength", "Signal Length", 500, "Length of the EMA applied to the oscillator to obtain the signal line."),
36370
+ colorInput(BULLISH, "Bullish", "color", "Color of the Klinger oscillator while above zero."),
36371
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the Klinger oscillator while below zero."),
36372
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line.")
36373
+ ],
35077
36374
  compute: (bars, inputs) => {
35078
- const hlc3 = sourceValues(bars, "HLC3");
35079
- const trend = change(hlc3);
35080
- const signedVolume = zip(volumes(bars), trend, (v, t) => t >= 0 ? v : -v);
35081
- const line = sub(ema(signedVolume, num3(inputs, "fastLength", 34)), ema(signedVolume, num3(inputs, "slowLength", 55)));
35082
- return {
35083
- plots: [
35084
- { key: "kvo", title: "KVO", values: line, color: SERIES_LINE, width: 2 },
35085
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: WARNING }
35086
- ],
35087
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35088
- };
36375
+ const n = bars.length;
36376
+ const force = new Array(n).fill(Number.NaN);
36377
+ let trend = 1;
36378
+ let prevTrend = 1;
36379
+ let cm = 0;
36380
+ let prevDm = 0;
36381
+ for (let i = 0; i < n; i++) {
36382
+ const b = bars[i];
36383
+ const hlc = b.high + b.low + b.close;
36384
+ const prevHlc = i > 0 ? bars[i - 1].high + bars[i - 1].low + bars[i - 1].close : hlc;
36385
+ trend = hlc > prevHlc ? 1 : -1;
36386
+ const dm = b.high - b.low;
36387
+ cm = i > 0 && trend === prevTrend ? cm + dm : prevDm + dm;
36388
+ const v = b.volume;
36389
+ force[i] = v == null || !Number.isFinite(v) ? Number.NaN : cm === 0 ? 0 : v * Math.abs(2 * (dm / cm) - 1) * trend * 100;
36390
+ prevTrend = trend;
36391
+ prevDm = dm;
36392
+ }
36393
+ const line = sub(ema(force, num3(inputs, "fastLength", 34)), ema(force, num3(inputs, "slowLength", 55)));
36394
+ const bull = str3(inputs, "color", BULLISH);
36395
+ return centeredOscillator({
36396
+ key: "kvo",
36397
+ title: "KVO",
36398
+ values: line,
36399
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
36400
+ zeroInk: transp(NEUTRAL, 50),
36401
+ valueTransparency: 60,
36402
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: str3(inputs, "signalColor", WARNING) }]
36403
+ });
35089
36404
  }
35090
36405
  };
35091
36406
  var mfi = {
@@ -35093,7 +36408,15 @@ var mfi = {
35093
36408
  title: "Money Flow Index",
35094
36409
  shortTitle: "MFI",
35095
36410
  overlay: false,
35096
- inputs: [lengthInput(14), colorInput()],
36411
+ inputs: [
36412
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to sum positive and negative money flow."),
36413
+ floatInput("overbought", "Overbought Level", 80, 0, 100, 1, "Level above which the MFI is considered overbought."),
36414
+ floatInput("oversold", "Oversold Level", 20, 0, 100, 1, "Level below which the MFI is considered oversold."),
36415
+ colorInput(NEUTRAL, "MFI", "color", "Color of the MFI line between the levels. Toward either level it blends into that level\u2019s color."),
36416
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
36417
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level."),
36418
+ colorInput(transp(NEUTRAL, 94), "OB/OS Zone Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
36419
+ ],
35097
36420
  compute: (bars, inputs) => {
35098
36421
  const len = num3(inputs, "length", 14);
35099
36422
  const tp = sourceValues(bars, "HLC3");
@@ -35101,48 +36424,57 @@ var mfi = {
35101
36424
  const d = change(tp);
35102
36425
  const pos = sum(zip(rawFlow, d, (f, x) => x > 0 ? f : 0), len);
35103
36426
  const neg = sum(zip(rawFlow, d, (f, x) => x < 0 ? f : 0), len);
35104
- const values = zip(pos, neg, (p, n) => n === 0 ? 100 : 100 - 100 / (1 + p / n));
35105
- return {
35106
- plots: [{ key: "mfi", title: "MFI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35107
- levels: [
35108
- { key: "upper", price: 80, color: NEUTRAL },
35109
- { key: "lower", price: 20, color: NEUTRAL }
35110
- ]
35111
- };
36427
+ const values = zip(pos, neg, (p, w) => p + w > 0 ? 100 * p / (p + w) : Number.NaN);
36428
+ return boundedOscillator({
36429
+ key: "mfi",
36430
+ title: "MFI",
36431
+ values,
36432
+ scale: { top: 100, bottom: 0 },
36433
+ overbought: num3(inputs, "overbought", 80),
36434
+ oversold: num3(inputs, "oversold", 20),
36435
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
36436
+ levelInk: NEUTRAL,
36437
+ zoneTransparency: 91,
36438
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 94))
36439
+ });
35112
36440
  }
35113
36441
  };
35114
- function volumeIndex(bars, when) {
35115
- const out = new Array(bars.length).fill(Number.NaN);
35116
- let index = 1e3;
35117
- let started = false;
35118
- for (let i = 1; i < bars.length; i++) {
35119
- const v = bars[i].volume;
35120
- const pv = bars[i - 1].volume;
35121
- const pc = bars[i - 1].close;
35122
- if (v == null || pv == null || !Number.isFinite(v) || !Number.isFinite(pv) || pc === 0) {
35123
- if (started) out[i] = index;
35124
- continue;
35125
- }
35126
- started = true;
35127
- if (when(v, pv)) index += (bars[i].close - pc) / pc * index;
35128
- out[i] = index;
35129
- }
35130
- return out;
36442
+ function regimeStyleInputs(signalTitle) {
36443
+ return [
36444
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the line and gradient fill while it holds above its signal EMA."),
36445
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line and gradient fill while it holds below its signal EMA."),
36446
+ colorInput(NEUTRAL, "Neutral", "neutralColor", "Color used while the signal EMA is still forming, or the line sits exactly on it."),
36447
+ colorInput(NEUTRAL, signalTitle, "signalColor", "Color of the signal EMA overlay."),
36448
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the gap between the line and its signal EMA with a vertical gradient \u2014 transparent at the average, strongest at the line.")
36449
+ ];
35131
36450
  }
36451
+ var NVI_POSITION = "Position vs Average";
36452
+ var NVI_SINGLE = "Single Color";
35132
36453
  var nvi = {
35133
36454
  type: "negative-volume-index",
35134
36455
  title: "Negative Volume Index",
35135
36456
  shortTitle: "Negative Volume Index",
35136
36457
  overlay: false,
35137
- inputs: [lengthInput(255, "signalLength", "Signal length"), colorInput()],
36458
+ inputs: [
36459
+ floatInput("base", "Base Level", 1e3, 0.01, 1e6, 1, "Arbitrary seed of the cumulative line, conventionally 1000. The absolute level carries no information \u2014 the line is read by slope and by position against the signal EMA."),
36460
+ boolInput("showSignal", "Signal EMA", true, SETTINGS, "Displays the conventional signal overlay. Hiding it only removes the plotted average; the regime coloring and fill still use it."),
36461
+ lengthInput(255, "signalLength", "Signal Length", 5e3, "Length of the signal EMA. The convention is one trading year \u2014 255 sessions on daily charts."),
36462
+ optionInput("colorMode", "Line Coloring", NVI_POSITION, [NVI_POSITION, NVI_SINGLE], "'Position vs Average' colors the line by the classic regime read; 'Single Color' draws the whole line in the neutral color."),
36463
+ ...regimeStyleInputs("Signal EMA")
36464
+ ],
35138
36465
  compute: (bars, inputs) => {
35139
- const line = volumeIndex(bars, (v, pv) => v < pv);
35140
- return {
35141
- plots: [
35142
- { key: "nvi", title: "NVI", values: line, color: str3(inputs, "color", SERIES_LINE), width: 2 },
35143
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 255)), color: WARNING }
35144
- ]
35145
- };
36466
+ const values = volumeIndex(bars, closes(bars), num3(inputs, "base", 1e3), (v, pv) => v < pv);
36467
+ const signal = ema(values, num3(inputs, "signalLength", 255));
36468
+ return regimeIndex(
36469
+ "nvi",
36470
+ "NVI",
36471
+ values,
36472
+ signal,
36473
+ { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "neutralColor", NEUTRAL), signal: str3(inputs, "signalColor", NEUTRAL) },
36474
+ bool(inputs, "showSignal", true),
36475
+ bool(inputs, "gradientFill", true),
36476
+ str3(inputs, "colorMode", NVI_POSITION) === NVI_SINGLE
36477
+ );
35146
36478
  }
35147
36479
  };
35148
36480
  var pvi = {
@@ -35150,15 +36482,26 @@ var pvi = {
35150
36482
  title: "Positive Volume Index",
35151
36483
  shortTitle: "Positive Volume Index",
35152
36484
  overlay: false,
35153
- inputs: [lengthInput(255, "signalLength", "Signal length"), colorInput()],
36485
+ inputs: [
36486
+ sourceInput("Close", "source", "Source", "Price series whose one-bar percentage change moves PVI on its active sessions."),
36487
+ floatInput("base", "Starting Value", 1e3, 1, 1e6, 1, "Seed of the cumulative line. The absolute level carries no information, so the reads are the slope and the position against the signal EMA."),
36488
+ boolInput("showSignal", "Signal EMA", true, SETTINGS, "Plots the long EMA of PVI that anchors the classic regime read. Hiding it keeps the regime coloring and fill, which are always measured against it."),
36489
+ lengthInput(255, "signalLength", "Signal Length", 5e3, "Length of the signal EMA. The classic reference is a one-year average \u2014 255 trading days on daily charts."),
36490
+ ...regimeStyleInputs("Signal Line")
36491
+ ],
35154
36492
  compute: (bars, inputs) => {
35155
- const line = volumeIndex(bars, (v, pv) => v > pv);
35156
- return {
35157
- plots: [
35158
- { key: "pvi", title: "PVI", values: line, color: str3(inputs, "color", SERIES_LINE), width: 2 },
35159
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 255)), color: WARNING }
35160
- ]
35161
- };
36493
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
36494
+ const values = volumeIndex(bars, src, num3(inputs, "base", 1e3), (v, pv) => v > pv);
36495
+ const signal = ema(values, num3(inputs, "signalLength", 255));
36496
+ return regimeIndex(
36497
+ "pvi",
36498
+ "PVI",
36499
+ values,
36500
+ signal,
36501
+ { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "neutralColor", NEUTRAL), signal: str3(inputs, "signalColor", NEUTRAL) },
36502
+ bool(inputs, "showSignal", true),
36503
+ bool(inputs, "gradientFill", true)
36504
+ );
35162
36505
  }
35163
36506
  };
35164
36507
  var pvt = {
@@ -35166,7 +36509,15 @@ var pvt = {
35166
36509
  title: "Price Volume Trend",
35167
36510
  shortTitle: "PVT",
35168
36511
  overlay: false,
35169
- inputs: [colorInput()],
36512
+ inputs: [
36513
+ boolInput("showSignal", "Signal Line", false, SETTINGS, "Display a moving average of PVT acting as a signal line."),
36514
+ optionInput("signalType", "Signal Type", "SMA", ["SMA", "EMA"], "Moving average type used for the signal line."),
36515
+ lengthInput(21, "signalLength", "Signal Length", 5e3, "Number of bars used to compute the signal line."),
36516
+ colorInput(NEUTRAL, "PVT Color", "color", "Color of the PVT line while it is flat."),
36517
+ colorInput(BULLISH, "Rising", "bullColor", "Color of the PVT line while it rises bar to bar."),
36518
+ colorInput(BEARISH, "Falling", "bearColor", "Color of the PVT line while it falls bar to bar."),
36519
+ colorInput(WARNING, "Signal Color", "signalColor", "Color of the signal line.")
36520
+ ],
35170
36521
  compute: (bars, inputs) => {
35171
36522
  const c = closes(bars);
35172
36523
  const raw = bars.map((b, i) => {
@@ -35174,7 +36525,17 @@ var pvt = {
35174
36525
  const pc = c[i - 1];
35175
36526
  return pc === 0 ? 0 : (c[i] - pc) / pc * b.volume;
35176
36527
  });
35177
- return { plots: [{ key: "pvt", title: "PVT", values: cumSum(raw), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
36528
+ const values = cumSum(raw);
36529
+ const flat = str3(inputs, "color", NEUTRAL);
36530
+ const plots = [
36531
+ { key: "pvt", title: "PVT", values, color: flat, width: 2, colors: slopeColors2(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), flat) }
36532
+ ];
36533
+ if (bool(inputs, "showSignal", false)) {
36534
+ const len = num3(inputs, "signalLength", 21);
36535
+ const signal = str3(inputs, "signalType", "SMA") === "EMA" ? ema(values, len) : sma(values, len);
36536
+ plots.push({ key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) });
36537
+ }
36538
+ return { plots };
35178
36539
  }
35179
36540
  };
35180
36541
  var volumeOscillator = {
@@ -35182,39 +36543,75 @@ var volumeOscillator = {
35182
36543
  title: "Volume Oscillator",
35183
36544
  shortTitle: "Vol Osc",
35184
36545
  overlay: false,
35185
- inputs: [lengthInput(5, "fastLength", "Fast length"), lengthInput(10, "slowLength", "Slow length"), colorInput()],
36546
+ inputs: [
36547
+ lengthInput(5, "fastLength", "Fast Length", 5e3, "Length of the fast EMA of volume."),
36548
+ lengthInput(10, "slowLength", "Slow Length", 5e3, "Length of the slow EMA of volume."),
36549
+ colorInput(BULLISH, "Above Zero", "color", "Color of the line and fill while it holds above zero \u2014 volume expanding."),
36550
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while it holds below zero \u2014 volume contracting.")
36551
+ ],
35186
36552
  compute: (bars, inputs) => {
35187
36553
  const vol = volumes(bars);
35188
36554
  const fast = ema(vol, num3(inputs, "fastLength", 5));
35189
36555
  const slow = ema(vol, num3(inputs, "slowLength", 10));
35190
- const values = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : 100 * (f - s) / s);
35191
- return {
35192
- plots: [{ key: "vo", title: "Volume Osc", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35193
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35194
- };
36556
+ const values = zip(fast, slow, (f, s) => s === 0 ? 0 : 100 * (f - s) / s);
36557
+ const bull = str3(inputs, "color", BULLISH);
36558
+ return centeredOscillator({
36559
+ key: "vo",
36560
+ title: "Volume Oscillator",
36561
+ values,
36562
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
36563
+ zeroInk: NEUTRAL,
36564
+ valueTransparency: 60
36565
+ });
35195
36566
  }
35196
36567
  };
36568
+ var OSCILLATOR = "Oscillator";
35197
36569
  var pvo = {
35198
36570
  type: "pvo",
35199
36571
  title: "Percentage Volume Oscillator",
35200
36572
  shortTitle: "PVO",
35201
36573
  overlay: false,
35202
- inputs: [lengthInput(12, "fastLength", "Fast length"), lengthInput(26, "slowLength", "Slow length"), lengthInput(9, "signalLength", "Signal length")],
36574
+ inputs: [
36575
+ { ...lengthInput(12, "fastLength", "Fast Length", 5e3, "Period of the fast EMA of volume. The conventional setting is 12."), group: OSCILLATOR },
36576
+ { ...lengthInput(26, "slowLength", "Slow Length", 5e3, "Period of the slow EMA of volume. The PVO expresses the fast/slow spread as a percentage of this slow average."), group: OSCILLATOR },
36577
+ { ...lengthInput(9, "signalLength", "Signal Length", 500, "Period of the EMA applied to the PVO to form the signal line."), group: OSCILLATOR },
36578
+ colorInput(BULLISH, "Expansion Color", "color", "Color of the oscillator and fill while participation expands."),
36579
+ colorInput(BEARISH, "Contraction Color", "bearColor", "Color of the oscillator and fill while participation contracts."),
36580
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
36581
+ colorInput(transp(NEUTRAL, 50), "Zero Line", "zeroColor", "Color of the zero reference line."),
36582
+ boolInput("showSignal", "Show Signal Line", true, STYLE, "Plot the signal line, an EMA of the PVO."),
36583
+ boolInput("showHistogram", "Show Histogram", true, STYLE, "Plot the difference between the PVO and its signal line as columns behind the oscillator.")
36584
+ ],
35203
36585
  compute: (bars, inputs) => {
35204
36586
  const vol = volumes(bars);
35205
36587
  const fast = ema(vol, num3(inputs, "fastLength", 12));
35206
36588
  const slow = ema(vol, num3(inputs, "slowLength", 26));
35207
- const line = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : 100 * (f - s) / s);
35208
- const signal = ema(line, num3(inputs, "signalLength", 9));
35209
- const hist = sub(line, signal);
35210
- return {
35211
- plots: [
35212
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: hist.map((x) => Number.isFinite(x) ? x >= 0 ? BULLISH : BEARISH : null), base: 0 },
35213
- { key: "pvo", title: "PVO", values: line, color: SERIES_LINE, width: 2 },
35214
- { key: "signal", title: "Signal", values: signal, color: WARNING }
35215
- ],
35216
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35217
- };
36589
+ const values = zip(fast, slow, (f, s) => s > 0 ? 100 * (f - s) / s : Number.NaN);
36590
+ const signal = ema(values, num3(inputs, "signalLength", 9));
36591
+ const expansion = str3(inputs, "color", BULLISH);
36592
+ const contraction = str3(inputs, "bearColor", BEARISH);
36593
+ const extraPlots = [];
36594
+ if (bool(inputs, "showHistogram", true)) {
36595
+ const hist = sub(values, signal);
36596
+ extraPlots.push({
36597
+ key: "hist",
36598
+ title: "Histogram",
36599
+ values: hist,
36600
+ kind: "columns",
36601
+ color: transp(expansion, 60),
36602
+ colors: hist.map((x) => Number.isFinite(x) ? transp(x > 0 ? expansion : contraction, 60) : null),
36603
+ base: 0
36604
+ });
36605
+ }
36606
+ if (bool(inputs, "showSignal", true)) extraPlots.push({ key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) });
36607
+ return centeredOscillator({
36608
+ key: "pvo",
36609
+ title: "PVO",
36610
+ values,
36611
+ ink: { bull: expansion, bear: contraction, neutral: contraction },
36612
+ zeroInk: str3(inputs, "zeroColor", transp(NEUTRAL, 50)),
36613
+ extraPlots
36614
+ });
35218
36615
  }
35219
36616
  };
35220
36617
  var vfi = {
@@ -35223,55 +36620,86 @@ var vfi = {
35223
36620
  shortTitle: "Volume Flow Indicator",
35224
36621
  overlay: false,
35225
36622
  inputs: [
35226
- lengthInput(130),
35227
- { key: "coef", title: "Cutoff coefficient", type: "float", defval: 0.2, min: 0.01, max: 10, step: 0.01 },
35228
- { key: "volCoef", title: "Volume cap", type: "float", defval: 2.5, min: 0.1, max: 50, step: 0.1 },
35229
- lengthInput(3, "smoothLength", "Smoothing length")
36623
+ { ...lengthInput(130, "length", "Length", 5e3, "Rolling summation window, also used for the average volume that scales the result. The published default of 130 bars targets position-horizon work on daily charts."), min: 2 },
36624
+ floatInput("coef", "Cutoff Coefficient", 0.2, 0, 10, 0.05, "Scales the volatility-based minimum change in typical price. Moves smaller than the cutoff count as noise and contribute no volume."),
36625
+ floatInput("volCoef", "Volume Cap Multiplier", 2.5, 0.1, 50, 0.1, "Caps each bar's volume contribution at this multiple of the average volume, so a single climactic print stays bounded."),
36626
+ { ...lengthInput(30, "volLength", "Volatility Length", 5e3, "Window of the standard deviation of the log change in typical price used to build the adaptive cutoff."), min: 2 },
36627
+ boolInput("smooth", "Smooth VFI", true, SETTINGS, "Applies the published light smoothing \u2014 an EMA of the summed line \u2014 before plotting."),
36628
+ lengthInput(3, "smoothLength", "Smoothing Length", 500, "Length of the smoothing EMA. The published version uses 3 bars."),
36629
+ boolInput("showSignal", "Signal Line", true, SETTINGS, "Plots an EMA of VFI as a signal line."),
36630
+ lengthInput(5, "signalLength", "Signal Length", 500, "Length of the signal-line EMA."),
36631
+ colorInput(BULLISH, "Bullish", "color", "Color of the VFI line and gradient fill above the zero line \u2014 the net accumulation regime."),
36632
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the VFI line and gradient fill below the zero line \u2014 the net distribution regime."),
36633
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
36634
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the zero line and the VFI line with a vertical gradient.")
35230
36635
  ],
35231
36636
  compute: (bars, inputs) => {
35232
36637
  const len = num3(inputs, "length", 130);
36638
+ const typical = sourceValues(bars, "HLC3");
36639
+ const logChange = sub(map(typical, Math.log), shift(map(typical, Math.log), 1));
36640
+ const volatility = stdev(logChange, num3(inputs, "volLength", 30));
35233
36641
  const coef = num3(inputs, "coef", 0.2);
35234
36642
  const volCoef = num3(inputs, "volCoef", 2.5);
35235
- const typical = map(sourceValues(bars, "HLC3"), Math.log);
35236
- const inter = change(typical);
35237
- const vInter = stdev(inter, 30);
35238
36643
  const vol = volumes(bars);
35239
- const vAve = sma(vol, len);
35240
- const mf = change(sourceValues(bars, "HLC3"));
35241
- const raw = bars.map((b, i) => {
35242
- const cutoff = Number.isFinite(vInter[i]) ? coef * vInter[i] * b.close : Number.NaN;
36644
+ const avgVolume = shift(sma(vol, len), 1);
36645
+ const typicalChange = change(typical);
36646
+ const signed3 = bars.map((b, i) => {
36647
+ const sd = volatility[i];
36648
+ const m = typicalChange[i];
35243
36649
  const v = vol[i];
35244
- const ave = i > 0 ? vAve[i - 1] : Number.NaN;
35245
- const m = mf[i];
35246
- if (!Number.isFinite(cutoff) || !Number.isFinite(v) || !Number.isFinite(ave) || !Number.isFinite(m)) return Number.NaN;
35247
- const capped = Math.min(v, ave * volCoef);
36650
+ if (!Number.isFinite(sd) || !Number.isFinite(m) || !Number.isFinite(v)) return Number.NaN;
36651
+ const cutoff = coef * sd * b.close;
36652
+ const avg = avgVolume[i];
36653
+ const capped = Number.isFinite(avg) ? Math.min(v, avg * volCoef) : v;
35248
36654
  return m > cutoff ? capped : m < -cutoff ? -capped : 0;
35249
36655
  });
35250
- const values = bars.map((_, i) => {
35251
- const ave = i > 0 ? vAve[i - 1] : Number.NaN;
35252
- return Number.isFinite(ave) && ave !== 0 ? ave : Number.NaN;
36656
+ const raw = zip(sum(signed3, len), avgVolume, (f, a) => a > 0 ? f / a : Number.NaN);
36657
+ const values = bool(inputs, "smooth", true) ? ema(raw, num3(inputs, "smoothLength", 3)) : raw;
36658
+ const bull = str3(inputs, "color", BULLISH);
36659
+ const extraPlots = bool(inputs, "showSignal", true) ? [{ key: "signal", title: "Signal Line", values: ema(values, num3(inputs, "signalLength", 5)), color: str3(inputs, "signalColor", WARNING) }] : [];
36660
+ return centeredOscillator({
36661
+ key: "vfi",
36662
+ title: "VFI",
36663
+ values,
36664
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "bearColor", BEARISH) },
36665
+ zeroInk: transp(NEUTRAL, 90),
36666
+ fill: bool(inputs, "gradientFill", true),
36667
+ extraPlots
35253
36668
  });
35254
- const flowSum = sum(raw, len);
35255
- const line = zip(flowSum, values, (f, a) => f / a);
35256
- return {
35257
- plots: [{ key: "vfi", title: "VFI", values: sma(line, num3(inputs, "smoothLength", 3)), color: SERIES_LINE, width: 2 }],
35258
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35259
- };
35260
36669
  }
35261
36670
  };
36671
+ var II_OSCILLATOR = "Normalized Oscillator (II%)";
36672
+ var II_CUMULATIVE = "Cumulative Line";
35262
36673
  var intradayIntensity = {
35263
36674
  type: "intraday-intensity",
35264
36675
  title: "Intraday Intensity",
35265
36676
  shortTitle: "Intraday Intensity",
35266
36677
  overlay: false,
35267
- inputs: [lengthInput(21), colorInput()],
36678
+ inputs: [
36679
+ optionInput("mode", "Display", II_OSCILLATOR, [II_OSCILLATOR, II_CUMULATIVE], "The two published forms. The cumulative line is open-ended and read by slope against price; the normalized oscillator divides a rolling sum of intensity by the rolling sum of volume and oscillates around zero."),
36680
+ lengthInput(21, "length", "Oscillator Length", 5e3, "Window of the rolling sums in the normalized form. The common setting is 21 bars. It has no effect on the cumulative line."),
36681
+ colorInput(BULLISH, "Bullish", "color", "Color of positive readings, the rising cumulative line and the fill above zero \u2014 the accumulation side."),
36682
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of negative readings, the falling cumulative line and the fill below zero \u2014 the distribution side."),
36683
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the zero line and the oscillator with a vertical gradient. Applies to the normalized oscillator only.")
36684
+ ],
35268
36685
  compute: (bars, inputs) => {
36686
+ const intensity = moneyFlowVolume(bars);
36687
+ const bull = str3(inputs, "color", BULLISH);
36688
+ const bear = str3(inputs, "bearColor", BEARISH);
36689
+ if (str3(inputs, "mode", II_OSCILLATOR) === II_CUMULATIVE) {
36690
+ const values2 = cumSum(intensity);
36691
+ return { plots: [{ key: "ii", title: "Intraday Intensity", values: values2, color: bull, colors: slopeColors2(values2, bull, bear, bull) }] };
36692
+ }
35269
36693
  const len = num3(inputs, "length", 21);
35270
- const values = zip(sum(moneyFlowVolume(bars), len), sum(volumes(bars), len), (m, v) => v === 0 ? 0 : 100 * m / v);
35271
- return {
35272
- plots: [{ key: "ii", title: "II%", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
35273
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
35274
- };
36694
+ const values = zip(sum(intensity, len), sum(volumes(bars), len), (m, v) => v > 0 ? 100 * m / v : Number.NaN);
36695
+ return centeredOscillator({
36696
+ key: "ii",
36697
+ title: "Intraday Intensity",
36698
+ values,
36699
+ ink: { bull, bear, neutral: bear },
36700
+ zeroInk: transp(NEUTRAL, 90),
36701
+ fill: bool(inputs, "gradientFill", true)
36702
+ });
35275
36703
  }
35276
36704
  };
35277
36705
  var volumeSpecs = [
@@ -35304,15 +36732,24 @@ function periodKey(anchor, time) {
35304
36732
  }
35305
36733
  return Math.floor(time / DAY_MS2);
35306
36734
  }
36735
+ function barInterval(bars) {
36736
+ if (bars.length < 2) return 0;
36737
+ const gaps = [];
36738
+ for (let i = 1; i < bars.length; i++) gaps.push(bars[i].time - bars[i - 1].time);
36739
+ gaps.sort((a, b) => a - b);
36740
+ return gaps[Math.floor(gaps.length / 2)] ?? 0;
36741
+ }
35307
36742
  var parabolicSar = {
35308
36743
  type: "parabolic-sar",
35309
36744
  title: "Parabolic SAR",
35310
36745
  shortTitle: "SAR",
35311
36746
  overlay: true,
35312
36747
  inputs: [
35313
- { key: "start", title: "Start", type: "float", defval: 0.02, min: 1e-3, max: 1, step: 1e-3 },
35314
- { key: "increment", title: "Increment", type: "float", defval: 0.02, min: 1e-3, max: 1, step: 1e-3 },
35315
- { key: "maximum", title: "Max value", type: "float", defval: 0.2, min: 0.01, max: 1, step: 0.01 }
36748
+ floatInput("start", "Start", 0.02, 0, 1, 0.01, "Initial acceleration factor applied when a new trend begins."),
36749
+ floatInput("increment", "Increment", 0.02, 0, 1, 0.01, "Amount added to the acceleration factor each time the trend makes a new extreme point."),
36750
+ floatInput("maximum", "Maximum", 0.2, 0.01, 1, 0.01, "Upper cap on the acceleration factor."),
36751
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the SAR crosses while they trail below price \u2014 uptrend, the stop sits under the market."),
36752
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the SAR crosses while they sit above price \u2014 downtrend, the stop sits over the market.")
35316
36753
  ],
35317
36754
  compute: (bars, inputs) => {
35318
36755
  const start = num3(inputs, "start", 0.02);
@@ -35320,7 +36757,6 @@ var parabolicSar = {
35320
36757
  const max = num3(inputs, "maximum", 0.2);
35321
36758
  const n = bars.length;
35322
36759
  const values = new Array(n).fill(Number.NaN);
35323
- const colors = new Array(n).fill(null);
35324
36760
  if (n >= 2) {
35325
36761
  let long = bars[1].close >= bars[0].close;
35326
36762
  let sar = long ? bars[0].low : bars[0].high;
@@ -35345,18 +36781,21 @@ var parabolicSar = {
35345
36781
  af = Math.min(max, af + inc);
35346
36782
  }
35347
36783
  values[i] = sar;
35348
- colors[i] = long ? BULLISH : BEARISH;
35349
36784
  }
35350
36785
  }
35351
- return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
36786
+ const bull = str3(inputs, "bullColor", BULLISH);
36787
+ const bear = str3(inputs, "bearColor", BEARISH);
36788
+ const colors = values.map((x, i) => Number.isFinite(x) ? x < bars[i].close ? bull : bear : null);
36789
+ return { plots: [{ key: "sar", title: "SAR", values, kind: "cross", color: bull, colors }] };
35352
36790
  }
35353
36791
  };
35354
- var VWAP_BANDS_GROUP = "Bands";
35355
- var VWAP_STYLE = "Style";
36792
+ var VWAP_ANCHORS = ["Session", "Week", "Month", "Quarter", "Year"];
36793
+ var BANDS_STDEV = "Standard Deviation";
36794
+ var BANDS_PERCENT = "Percentage";
35356
36795
  var VWAP_BANDS = [
35357
- { on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
35358
- { on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
35359
- { on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
36796
+ { show: "band1", mult: "band1Mult", defMult: 1, defOn: true },
36797
+ { show: "band2", mult: "band2Mult", defMult: 2, defOn: false },
36798
+ { show: "band3", mult: "band3Mult", defMult: 3, defOn: false }
35360
36799
  ];
35361
36800
  var vwap = {
35362
36801
  type: "vwap",
@@ -35364,39 +36803,36 @@ var vwap = {
35364
36803
  shortTitle: "VWAP",
35365
36804
  overlay: true,
35366
36805
  inputs: [
35367
- { key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
35368
- sourceInput("HLC3"),
35369
- // Each band is one row: its toggle leads, the multiplier follows unlabeled.
36806
+ optionInput("anchor", "Anchor Period", "Session", VWAP_ANCHORS, "Period anchoring the average. VWAP and its bands reset on the first bar of each new session, week, month, quarter or year (UTC)."),
36807
+ sourceInput("HLC3", "source", "Source", "Price used in the volume weighted average. Typical price (HLC3) is the classic choice."),
36808
+ optionInput("bandsMode", "Bands Mode", BANDS_STDEV, [BANDS_STDEV, BANDS_PERCENT], "Bands offset method: multiples of the volume weighted standard deviation of price around VWAP, or a percentage of the VWAP value."),
35370
36809
  ...VWAP_BANDS.flatMap((b, i) => [
35371
- { key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
35372
- { key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
36810
+ boolInput(b.show, `Band #${i + 1}`, b.defOn, SETTINGS, `Show the ${["first", "second", "third"][i]} band pair.`),
36811
+ floatInput(b.mult, `Band #${i + 1} Multiplier`, b.defMult, 0, 50, 0.5, "Multiplier for this band pair, in standard deviations or percent depending on the bands mode.")
35373
36812
  ]),
35374
- { ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
35375
- // One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
35376
- ...VWAP_BANDS.flatMap((b, i) => [
35377
- { ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
35378
- { key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
35379
- { ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
35380
- ])
36813
+ boolInput("hideDwm", "Hide VWAP on 1D or Above", true, SETTINGS, "Hide the VWAP and its bands on daily and higher timeframes, where an intraday anchored VWAP is not meaningful."),
36814
+ colorInput(BULLISH, "Bullish", "bullColor", "VWAP line color while price closes above it, and the color of the lower (support) bands."),
36815
+ colorInput(BEARISH, "Bearish", "bearColor", "VWAP line color while price closes below it, and the color of the upper (resistance) bands."),
36816
+ colorInput(NEUTRAL, "Bands", "bandsColor", "Deviation bands fill color."),
36817
+ boolInput("fill", "Bands Fill", true, STYLE, "Fill the area between each visible band pair.")
35381
36818
  ],
35382
36819
  compute: (bars, inputs) => {
35383
- const anchor = str3(inputs, "anchor", "Day");
36820
+ const anchor = str3(inputs, "anchor", "Session");
35384
36821
  const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
35385
36822
  const n = bars.length;
36823
+ const hidden = bool(inputs, "hideDwm", true) && barInterval(bars) >= DAY_MS2;
35386
36824
  const values = new Array(n).fill(Number.NaN);
35387
- const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
36825
+ const bands = VWAP_BANDS.filter((b) => bool(inputs, b.show, b.defOn)).map((b) => ({
35388
36826
  mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
35389
- color: str3(inputs, b.color, b.defColor),
35390
- fill: bool(inputs, b.fill, true),
35391
- fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
35392
36827
  up: new Array(n).fill(Number.NaN),
35393
36828
  down: new Array(n).fill(Number.NaN)
35394
36829
  }));
36830
+ const percentMode = str3(inputs, "bandsMode", BANDS_STDEV) === BANDS_PERCENT;
35395
36831
  let period = Number.NaN;
35396
36832
  let cumPV = 0;
35397
36833
  let cumPV2 = 0;
35398
36834
  let cumV = 0;
35399
- for (let i = 0; i < n; i++) {
36835
+ for (let i = 0; i < n && !hidden; i++) {
35400
36836
  const b = bars[i];
35401
36837
  const key = periodKey(anchor, b.time);
35402
36838
  if (key !== period) {
@@ -35414,38 +36850,58 @@ var vwap = {
35414
36850
  }
35415
36851
  if (cumV <= 0) continue;
35416
36852
  const mean = cumPV / cumV;
35417
- const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
36853
+ const offset = percentMode ? mean / 100 : Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
35418
36854
  values[i] = mean;
35419
36855
  for (const band of bands) {
35420
- band.up[i] = mean + band.mult * sd;
35421
- band.down[i] = mean - band.mult * sd;
36856
+ band.up[i] = mean + band.mult * offset;
36857
+ band.down[i] = mean - band.mult * offset;
35422
36858
  }
35423
36859
  }
35424
- const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
36860
+ const bull = str3(inputs, "bullColor", BULLISH);
36861
+ const bear = str3(inputs, "bearColor", BEARISH);
36862
+ const fillColor = transp(str3(inputs, "bandsColor", NEUTRAL), 95);
36863
+ const plots = [
36864
+ { key: "vwap", title: "VWAP", values, color: bull, colors: values.map((x, i) => Number.isFinite(x) ? bars[i].close >= x ? bull : bear : null) }
36865
+ ];
35425
36866
  const fills = [];
35426
36867
  bands.forEach((band, i) => {
35427
36868
  plots.push(
35428
- { key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
35429
- { key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
36869
+ { key: `up${i}`, title: `Upper Band #${i + 1}`, values: band.up, color: transp(bear, 40) },
36870
+ { key: `down${i}`, title: `Lower Band #${i + 1}`, values: band.down, color: transp(bull, 40) }
35430
36871
  );
35431
- if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
36872
+ if (bool(inputs, "fill", true)) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: fillColor });
35432
36873
  });
35433
36874
  return { plots, bands: fills };
35434
36875
  }
35435
36876
  };
36877
+ var PIVOT_TRADITIONAL = "Traditional";
36878
+ var PIVOT_FIBONACCI = "Fibonacci";
36879
+ var PIVOT_CAMARILLA = "Camarilla";
36880
+ var PIVOT_WOODIE = "Woodie";
36881
+ var PIVOT_AUTO = "Auto";
35436
36882
  var pivotPoints = {
35437
36883
  type: "pivot-points",
35438
- title: "Pivot Points",
36884
+ title: "Pivot Points Standard",
35439
36885
  shortTitle: "Pivots",
35440
36886
  overlay: true,
35441
- inputs: [{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month"] }],
36887
+ inputs: [
36888
+ optionInput("kind", "Type", PIVOT_TRADITIONAL, [PIVOT_TRADITIONAL, PIVOT_FIBONACCI, PIVOT_CAMARILLA, PIVOT_WOODIE], "Formula used to calculate the levels. Traditional is the classic floor-trader method; Fibonacci projects 38.2/61.8/100% of the prior range around the pivot; Camarilla scales the prior range around the prior close; Woodie gives extra weight to the period open."),
36889
+ optionInput("anchor", "Pivots Timeframe", PIVOT_AUTO, [PIVOT_AUTO, "Daily", "Weekly", "Monthly", "Yearly"], "Period the pivots are anchored to. 'Auto' follows the chart's bar spacing: daily pivots up to 15-minute bars, weekly on other intraday bars, monthly on daily bars, yearly above."),
36890
+ colorInput(WARNING, "Pivot", "pivotColor", "Color of the central pivot (P) level."),
36891
+ colorInput(BEARISH, "Resistances", "resistanceColor", "Color of the resistance (R) levels."),
36892
+ colorInput(BULLISH, "Supports", "supportColor", "Color of the support (S) levels.")
36893
+ ],
35442
36894
  compute: (bars, inputs) => {
35443
- const anchor = str3(inputs, "anchor", "Day");
36895
+ const interval = barInterval(bars);
36896
+ const requested = str3(inputs, "anchor", PIVOT_AUTO);
36897
+ const auto = interval <= 15 * 6e4 ? "Session" : interval < DAY_MS2 ? "Week" : interval <= DAY_MS2 ? "Month" : "Year";
36898
+ const anchor = requested === "Daily" ? "Session" : requested === "Weekly" ? "Week" : requested === "Monthly" ? "Month" : requested === "Yearly" ? "Year" : auto;
36899
+ const kind = str3(inputs, "kind", PIVOT_TRADITIONAL);
35444
36900
  const aggs = /* @__PURE__ */ new Map();
35445
36901
  for (const b of bars) {
35446
36902
  const key = periodKey(anchor, b.time);
35447
36903
  const a = aggs.get(key);
35448
- if (!a) aggs.set(key, { high: b.high, low: b.low, close: b.close });
36904
+ if (!a) aggs.set(key, { open: b.open, high: b.high, low: b.low, close: b.close });
35449
36905
  else {
35450
36906
  a.high = Math.max(a.high, b.high);
35451
36907
  a.low = Math.min(a.low, b.low);
@@ -35454,31 +36910,61 @@ var pivotPoints = {
35454
36910
  }
35455
36911
  const n = bars.length;
35456
36912
  const mk = () => new Array(n).fill(Number.NaN);
35457
- const levels = { p: mk(), r1: mk(), s1: mk(), r2: mk(), s2: mk(), r3: mk(), s3: mk() };
36913
+ const levels = { p: mk(), r1: mk(), s1: mk(), r2: mk(), s2: mk(), r3: mk(), s3: mk(), r4: mk(), s4: mk() };
36914
+ let lastKey = Number.NaN;
35458
36915
  for (let i = 0; i < n; i++) {
35459
- const prev2 = aggs.get(periodKey(anchor, bars[i].time) - 1);
35460
- if (!prev2) continue;
35461
- const p = (prev2.high + prev2.low + prev2.close) / 3;
36916
+ const key = periodKey(anchor, bars[i].time);
36917
+ const fresh = key !== lastKey;
36918
+ lastKey = key;
36919
+ if (fresh) continue;
36920
+ const prev2 = aggs.get(key - 1);
36921
+ const curr = aggs.get(key);
36922
+ if (!prev2 || !curr) continue;
36923
+ const range = prev2.high - prev2.low;
36924
+ const traditional = (prev2.high + prev2.low + prev2.close) / 3;
36925
+ const p = kind === PIVOT_WOODIE ? (prev2.high + prev2.low + 2 * curr.open) / 4 : traditional;
35462
36926
  levels.p[i] = p;
35463
- levels.r1[i] = 2 * p - prev2.low;
35464
- levels.s1[i] = 2 * p - prev2.high;
35465
- levels.r2[i] = p + (prev2.high - prev2.low);
35466
- levels.s2[i] = p - (prev2.high - prev2.low);
35467
- levels.r3[i] = prev2.high + 2 * (p - prev2.low);
35468
- levels.s3[i] = prev2.low - 2 * (prev2.high - p);
35469
- }
35470
- const plot = (key, title, color) => ({ key, title, kind: "step", values: levels[key], color });
35471
- return {
35472
- plots: [
35473
- plot("p", "P", WARNING),
35474
- plot("r1", "R1", BEARISH),
35475
- plot("s1", "S1", BULLISH),
35476
- plot("r2", "R2", BEARISH),
35477
- plot("s2", "S2", BULLISH),
35478
- plot("r3", "R3", BEARISH),
35479
- plot("s3", "S3", BULLISH)
35480
- ]
35481
- };
36927
+ if (kind === PIVOT_FIBONACCI) {
36928
+ levels.r1[i] = traditional + 0.382 * range;
36929
+ levels.s1[i] = traditional - 0.382 * range;
36930
+ levels.r2[i] = traditional + 0.618 * range;
36931
+ levels.s2[i] = traditional - 0.618 * range;
36932
+ levels.r3[i] = traditional + range;
36933
+ levels.s3[i] = traditional - range;
36934
+ } else if (kind === PIVOT_CAMARILLA) {
36935
+ const cam = 1.1 * range;
36936
+ levels.r1[i] = prev2.close + cam / 12;
36937
+ levels.s1[i] = prev2.close - cam / 12;
36938
+ levels.r2[i] = prev2.close + cam / 6;
36939
+ levels.s2[i] = prev2.close - cam / 6;
36940
+ levels.r3[i] = prev2.close + cam / 4;
36941
+ levels.s3[i] = prev2.close - cam / 4;
36942
+ levels.r4[i] = prev2.close + cam / 2;
36943
+ levels.s4[i] = prev2.close - cam / 2;
36944
+ } else {
36945
+ levels.r1[i] = 2 * p - prev2.low;
36946
+ levels.s1[i] = 2 * p - prev2.high;
36947
+ levels.r2[i] = p + range;
36948
+ levels.s2[i] = p - range;
36949
+ if (kind !== PIVOT_WOODIE) {
36950
+ levels.r3[i] = prev2.high + 2 * (p - prev2.low);
36951
+ levels.s3[i] = prev2.low - 2 * (prev2.high - p);
36952
+ }
36953
+ }
36954
+ }
36955
+ const resistance = str3(inputs, "resistanceColor", BEARISH);
36956
+ const support = str3(inputs, "supportColor", BULLISH);
36957
+ const plot = (key, title, color) => ({ key, title, values: levels[key], color });
36958
+ const plots = [
36959
+ plot("p", "P", str3(inputs, "pivotColor", WARNING)),
36960
+ plot("r1", "R1", resistance),
36961
+ plot("s1", "S1", support),
36962
+ plot("r2", "R2", resistance),
36963
+ plot("s2", "S2", support)
36964
+ ];
36965
+ if (kind !== PIVOT_WOODIE) plots.push(plot("r3", "R3", resistance), plot("s3", "S3", support));
36966
+ if (kind === PIVOT_CAMARILLA) plots.push(plot("r4", "R4", resistance), plot("s4", "S4", support));
36967
+ return { plots };
35482
36968
  }
35483
36969
  };
35484
36970
  var fiftyTwoWeek = {
@@ -35486,15 +36972,25 @@ var fiftyTwoWeek = {
35486
36972
  title: "52 Week High/Low",
35487
36973
  shortTitle: "52W H/L",
35488
36974
  overlay: true,
35489
- inputs: [{ key: "weeks", title: "Weeks", type: "int", defval: 52, min: 1, max: 520, step: 1 }],
36975
+ inputs: [
36976
+ intInput("weeks", "Weeks", 52, 1, 520, "Length of the rolling window, in weeks."),
36977
+ boolInput("showAllTime", "Show All-Time High/Low", false, SETTINGS, "Additionally display the all-time high and low, tracked over the symbol's full loaded history."),
36978
+ colorInput(BULLISH, "52 Week High", "highColor", "Color of the 52 week high level."),
36979
+ colorInput(BEARISH, "52 Week Low", "lowColor", "Color of the 52 week low level."),
36980
+ colorInput(NEUTRAL, "All-Time High/Low", "allTimeColor", "Color of the all-time high and all-time low levels.")
36981
+ ],
35490
36982
  compute: (bars, inputs) => {
35491
36983
  const span = num3(inputs, "weeks", 52) * 7 * DAY_MS2;
35492
36984
  const n = bars.length;
35493
36985
  const hi = new Array(n).fill(Number.NaN);
35494
36986
  const lo = new Array(n).fill(Number.NaN);
36987
+ const ath = new Array(n).fill(Number.NaN);
36988
+ const atl = new Array(n).fill(Number.NaN);
35495
36989
  const maxIdx = [];
35496
36990
  const minIdx = [];
35497
36991
  let from = 0;
36992
+ let runHigh = -Infinity;
36993
+ let runLow = Infinity;
35498
36994
  for (let i = 0; i < n; i++) {
35499
36995
  const b = bars[i];
35500
36996
  while (maxIdx.length > 0 && bars[maxIdx[maxIdx.length - 1]].high <= b.high) maxIdx.pop();
@@ -35506,13 +37002,20 @@ var fiftyTwoWeek = {
35506
37002
  while (minIdx[0] < from) minIdx.shift();
35507
37003
  hi[i] = bars[maxIdx[0]].high;
35508
37004
  lo[i] = bars[minIdx[0]].low;
37005
+ runHigh = Math.max(runHigh, b.high);
37006
+ runLow = Math.min(runLow, b.low);
37007
+ ath[i] = runHigh;
37008
+ atl[i] = runLow;
37009
+ }
37010
+ const plots = [
37011
+ { key: "high", title: "52 Week High", values: hi, kind: "step", color: str3(inputs, "highColor", BULLISH), width: 2 },
37012
+ { key: "low", title: "52 Week Low", values: lo, kind: "step", color: str3(inputs, "lowColor", BEARISH), width: 2 }
37013
+ ];
37014
+ if (bool(inputs, "showAllTime", false)) {
37015
+ const ink = str3(inputs, "allTimeColor", NEUTRAL);
37016
+ plots.push({ key: "ath", title: "All-Time High", values: ath, kind: "step", color: ink }, { key: "atl", title: "All-Time Low", values: atl, kind: "step", color: ink });
35509
37017
  }
35510
- return {
35511
- plots: [
35512
- { key: "high", title: "52W high", values: hi, kind: "step", color: BULLISH },
35513
- { key: "low", title: "52W low", values: lo, kind: "step", color: BEARISH }
35514
- ]
35515
- };
37018
+ return { plots };
35516
37019
  }
35517
37020
  };
35518
37021
  var zigzag = {
@@ -35520,9 +37023,11 @@ var zigzag = {
35520
37023
  title: "ZigZag",
35521
37024
  overlay: true,
35522
37025
  inputs: [
35523
- { key: "deviation", title: "Deviation %", type: "float", defval: 5, min: 0.01, max: 100, step: 0.1 },
35524
- { key: "depth", title: "Depth", type: "int", defval: 10, min: 1, max: 500, step: 1 },
35525
- colorInput()
37026
+ floatInput("deviation", "Deviation (%)", 5, 0.01, 100, 0.1, "Minimum reversal from the leg extreme, as a percentage of that extreme, required to confirm a new pivot."),
37027
+ { ...intInput("depth", "Depth", 10, 2, 500, "Minimum number of bars required between two consecutive pivots.") },
37028
+ colorInput(BULLISH, "Up Leg", "bullColor", "Color of rising zigzag segments."),
37029
+ colorInput(BEARISH, "Down Leg", "bearColor", "Color of falling zigzag segments."),
37030
+ { key: "lineWidth", title: "Width", type: "int", defval: 2, min: 1, max: 10, step: 1, group: STYLE, tooltip: "Width of the zigzag segments." }
35526
37031
  ],
35527
37032
  compute: (bars, inputs) => {
35528
37033
  const dev = num3(inputs, "deviation", 5) / 100;
@@ -35535,14 +37040,14 @@ var zigzag = {
35535
37040
  const b = bars[i];
35536
37041
  if (up) {
35537
37042
  if (b.high >= ext.price) ext = { i, price: b.high };
35538
- else if (b.low <= ext.price * (1 - dev) && i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
37043
+ else if (b.low <= ext.price * (1 - dev) && ext.i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
35539
37044
  pivots.push(ext);
35540
37045
  up = false;
35541
37046
  ext = { i, price: b.low };
35542
37047
  }
35543
37048
  } else if (b.low <= ext.price) {
35544
37049
  ext = { i, price: b.low };
35545
- } else if (b.high >= ext.price * (1 + dev) && i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
37050
+ } else if (b.high >= ext.price * (1 + dev) && ext.i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
35546
37051
  pivots.push(ext);
35547
37052
  up = true;
35548
37053
  ext = { i, price: b.high };
@@ -35550,22 +37055,29 @@ var zigzag = {
35550
37055
  }
35551
37056
  pivots.push(ext);
35552
37057
  }
35553
- return {
35554
- plots: [],
35555
- polylines: [
35556
- {
35557
- key: "zigzag",
35558
- points: pivots.map((p) => ({ xloc: "bar_time", x: bars[p.i].time, price: p.price })),
35559
- curved: false,
35560
- closed: false,
35561
- lineColor: str3(inputs, "color", SERIES_LINE),
35562
- lineWidth: 2,
35563
- lineStyle: "solid",
35564
- arrowLeft: false,
35565
- arrowRight: false
35566
- }
35567
- ]
35568
- };
37058
+ const bull = str3(inputs, "bullColor", BULLISH);
37059
+ const bear = str3(inputs, "bearColor", BEARISH);
37060
+ const width = num3(inputs, "lineWidth", 2);
37061
+ const polylines = [];
37062
+ for (let k = 1; k < pivots.length; k++) {
37063
+ const a = pivots[k - 1];
37064
+ const b = pivots[k];
37065
+ polylines.push({
37066
+ key: `leg${k}`,
37067
+ points: [
37068
+ { xloc: "bar_time", x: bars[a.i].time, price: a.price },
37069
+ { xloc: "bar_time", x: bars[b.i].time, price: b.price }
37070
+ ],
37071
+ curved: false,
37072
+ closed: false,
37073
+ lineColor: b.price >= a.price ? bull : bear,
37074
+ lineWidth: width,
37075
+ lineStyle: "solid",
37076
+ arrowLeft: false,
37077
+ arrowRight: false
37078
+ });
37079
+ }
37080
+ return { plots: [], polylines };
35569
37081
  }
35570
37082
  };
35571
37083
  var williamsFractal = {
@@ -35573,10 +37085,16 @@ var williamsFractal = {
35573
37085
  title: "Williams Fractal",
35574
37086
  shortTitle: "Fractals",
35575
37087
  overlay: true,
35576
- inputs: [{ key: "periods", title: "Periods", type: "int", defval: 2, min: 1, max: 50, step: 1 }],
37088
+ inputs: [
37089
+ intInput("periods", "Periods", 2, 2, 50, "Bars required on each side of the candidate bar. 2 is Bill Williams' classic 5-bar fractal; a fractal is confirmed only once this many later bars have closed."),
37090
+ colorInput(BEARISH, "Up Fractal", "upColor", "Color of the up fractal marker (swing high), drawn above the fractal bar."),
37091
+ colorInput(BULLISH, "Down Fractal", "downColor", "Color of the down fractal marker (swing low), drawn below the fractal bar.")
37092
+ ],
35577
37093
  compute: (bars, inputs) => {
35578
37094
  const p = num3(inputs, "periods", 2);
35579
37095
  const markers = [];
37096
+ const up = str3(inputs, "upColor", BEARISH);
37097
+ const down = str3(inputs, "downColor", BULLISH);
35580
37098
  const isExtreme = (i, pick, better) => {
35581
37099
  const v = pick(bars[i]);
35582
37100
  for (let k = i - p; k <= i + p; k++) {
@@ -35586,12 +37104,8 @@ var williamsFractal = {
35586
37104
  return true;
35587
37105
  };
35588
37106
  for (let i = p; i < bars.length - p; i++) {
35589
- if (isExtreme(i, (b) => b.high, (a, b) => a >= b)) {
35590
- markers.push({ time: bars[i].time, position: "aboveBar", shape: "triangleup", color: BULLISH });
35591
- }
35592
- if (isExtreme(i, (b) => b.low, (a, b) => a <= b)) {
35593
- markers.push({ time: bars[i].time, position: "belowBar", shape: "triangledown", color: BEARISH });
35594
- }
37107
+ if (isExtreme(i, (b) => b.high, (a, b) => a > b)) markers.push({ time: bars[i].time, position: "aboveBar", shape: "triangleup", color: up });
37108
+ if (isExtreme(i, (b) => b.low, (a, b) => a < b)) markers.push({ time: bars[i].time, position: "belowBar", shape: "triangledown", color: down });
35595
37109
  }
35596
37110
  return { plots: [], markers };
35597
37111
  }