@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.
@@ -1,6 +1,6 @@
1
1
  import { deserializeDrawing, chartTypes, rendererLayers, isLineLikeSeries, seriesShownOn, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, nativeInstanceChannel, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, DEFAULT_DRAWING_COLOR, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, MAGNIFIER_TIMEFRAME_OPTIONS, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, seriesInScale, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS, stableSeriesId } from './chunk-BFA32GOU.js';
2
- import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, CALLOUT_STYLE_ID, CALLOUT_CSS, Popover, blendOver, splitColor, CalloutBubble, Menu, TextArea, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-BKHSQ4YM.js';
3
- import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BULLISH, CATEGORICAL, SERIES_LINE, BEARISH, isDarkColor, INFO, injectStyles, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, iconMarkup, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-CAFCLMPF.js';
2
+ import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, CALLOUT_STYLE_ID, CALLOUT_CSS, Popover, blendOver, splitColor, CalloutBubble, Menu, TextArea, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-WTWGEB7K.js';
3
+ import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, INFO, BULLISH, BEARISH, isDarkColor, injectStyles, SERIES_LINE, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, iconMarkup, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-CAFCLMPF.js';
4
4
 
5
5
  // src/core/events/EventBus.ts
6
6
  var TypedEventBus = class {
@@ -1179,6 +1179,8 @@ var MultiProviderFeed = class {
1179
1179
  this.liveBars = [];
1180
1180
  /** Sync-accessible symbol metadata, warmed by load()/symbolInfoFor (the engine reads it synchronously). */
1181
1181
  this.symInfoCache = /* @__PURE__ */ new Map();
1182
+ /** Requests in flight, keyed like the cache — every concurrent asker shares one round trip. */
1183
+ this.symInfoInflight = /* @__PURE__ */ new Map();
1182
1184
  this.cache = new CachingDataFeed(new RegistryFetchFeed(this.registry), store);
1183
1185
  }
1184
1186
  // ── Registry surface (driven by chart.data / DataControl) ──────────────
@@ -1239,9 +1241,36 @@ var MultiProviderFeed = class {
1239
1241
  async symbolInfoFor(raw) {
1240
1242
  const resolved = this.registry.resolve(raw, { default: this.primaryProvider });
1241
1243
  if (!resolved) return void 0;
1242
- const info = await this.registry.get(resolved.provider)?.getSymbolInfo?.(resolved.ticker);
1243
- if (info) this.symInfoCache.set(symKey(resolved), info);
1244
- return info;
1244
+ return this.fetchSymbolInfo(resolved);
1245
+ }
1246
+ /**
1247
+ * One round trip per symbol, shared. A market switch probes this metadata from five
1248
+ * places at once (the bar load's prefetch, the renderer's tick size, the market-status
1249
+ * badge, the session shading, the session toggle) — without the in-flight map each
1250
+ * would open its own request for the same answer, and the last one to land decided how
1251
+ * late the chrome settled.
1252
+ *
1253
+ * Cached for {@link SYMBOL_INFO_TTL_MS}: this metadata is stable within a session
1254
+ * (tick size, session vocabulary, listing prefix), but not immutable — a continuous
1255
+ * futures row rolls its current contract — so the entry expires rather than pinning the
1256
+ * session's first answer forever. A failed probe is NOT remembered: the next asker retries.
1257
+ */
1258
+ fetchSymbolInfo(resolved) {
1259
+ const key = symKey(resolved);
1260
+ const hit = this.symInfoCache.get(key);
1261
+ if (hit && Date.now() - hit.at < SYMBOL_INFO_TTL_MS) return Promise.resolve(hit.info);
1262
+ const inflight = this.symInfoInflight.get(key);
1263
+ if (inflight) return inflight;
1264
+ const getSymbolInfo = this.registry.get(resolved.provider)?.getSymbolInfo;
1265
+ if (!getSymbolInfo) return Promise.resolve(void 0);
1266
+ const p = Promise.resolve(getSymbolInfo.call(this.registry.get(resolved.provider), resolved.ticker)).then((info) => {
1267
+ if (info) this.symInfoCache.set(key, { info, at: Date.now() });
1268
+ return info;
1269
+ }).finally(() => {
1270
+ this.symInfoInflight.delete(key);
1271
+ });
1272
+ this.symInfoInflight.set(key, p);
1273
+ return p;
1245
1274
  }
1246
1275
  /**
1247
1276
  * Per-symbol capabilities, resolved through the owning provider's `capabilitiesFor`
@@ -1285,16 +1314,10 @@ var MultiProviderFeed = class {
1285
1314
  symbolInfo(cfg) {
1286
1315
  if (cfg.data && cfg.data.length > 0) return void 0;
1287
1316
  const resolved = this.registry.resolve(rawSymbol(cfg), { default: this.primaryProvider });
1288
- return resolved ? this.symInfoCache.get(symKey(resolved)) : void 0;
1317
+ return resolved ? this.symInfoCache.get(symKey(resolved))?.info : void 0;
1289
1318
  }
1290
1319
  prefetchSymbolInfo(resolved) {
1291
- const key = symKey(resolved);
1292
- if (this.symInfoCache.has(key)) return;
1293
- const provider = this.registry.get(resolved.provider);
1294
- if (!provider?.getSymbolInfo) return;
1295
- void provider.getSymbolInfo(resolved.ticker).then((info) => {
1296
- if (info) this.symInfoCache.set(key, info);
1297
- }).catch(() => {
1320
+ void this.fetchSymbolInfo(resolved).catch(() => {
1298
1321
  });
1299
1322
  }
1300
1323
  async loadRange(cfg, range) {
@@ -1344,8 +1367,9 @@ function canonical(cfg, resolved) {
1344
1367
  return { ...cfg, symbol: `${resolved.provider}:${resolved.ticker}` };
1345
1368
  }
1346
1369
  function symKey(resolved) {
1347
- return `${resolved.provider}|${resolved.ticker}`;
1370
+ return `${resolved.provider}|${resolved.ticker.trim().toUpperCase()}`;
1348
1371
  }
1372
+ var SYMBOL_INFO_TTL_MS = 10 * 6e4;
1349
1373
  async function safeBars(provider, ticker, tf, range) {
1350
1374
  try {
1351
1375
  return await provider.getBars(ticker, tf, range);
@@ -1845,9 +1869,22 @@ var TIMEZONES = [
1845
1869
  { value: "Pacific/Apia", label: "Apia" },
1846
1870
  { value: "Pacific/Kiritimati", label: "Kiritimati" }
1847
1871
  ];
1872
+ var EXCHANGE_TIMEZONE = "exchange";
1873
+ function isExchangeTimezone(zone) {
1874
+ return zone === EXCHANGE_TIMEZONE;
1875
+ }
1876
+ function resolveTimezone(zone, exchangeZone) {
1877
+ if (!isExchangeTimezone(zone)) return zone;
1878
+ return exchangeZone && exchangeZone !== "" ? exchangeZone : "Etc/UTC";
1879
+ }
1848
1880
  function normalizeTimezone(zone) {
1849
1881
  return zone === "UTC" || zone === "Etc/UTC" || zone === "Etc/GMT" ? "Etc/UTC" : zone;
1850
1882
  }
1883
+ function timezoneMenuRows(current) {
1884
+ const active = normalizeTimezone(current);
1885
+ const [utc, ...zones] = TIMEZONES.map((t) => ({ value: t.value, label: tzMenuLabel(t.value, t.label), checked: t.value === active }));
1886
+ return [utc, { value: EXCHANGE_TIMEZONE, label: "Exchange", checked: isExchangeTimezone(current) }, ...zones];
1887
+ }
1851
1888
  function tzOffset(zone, date = /* @__PURE__ */ new Date()) {
1852
1889
  try {
1853
1890
  const parts = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: "shortOffset" }).formatToParts(date);
@@ -3503,7 +3540,10 @@ var IndicatorInputsDialog = class {
3503
3540
  id,
3504
3541
  theme: this.host.theme(),
3505
3542
  get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
3506
- onChange: (v) => emit(v)
3543
+ onChange: (v) => emit(v),
3544
+ // An input change re-executes the script over its whole history: commit the
3545
+ // opacity drag once on release, not once per pointer move.
3546
+ commit: "release"
3507
3547
  }).el;
3508
3548
  }
3509
3549
  if (inp.type === "symbol") return this.buildSymbol(id, String(current), emit);
@@ -3750,10 +3790,6 @@ var IndicatorInputsDialog = class {
3750
3790
  }
3751
3791
  /** Open a themed month calendar under `anchor` — same surface + shadow as the choice list. */
3752
3792
  openCalendar(anchor, current, onPick) {
3753
- const parsed = parseIsoDate(current);
3754
- let year = parsed?.getFullYear() ?? (/* @__PURE__ */ new Date()).getFullYear();
3755
- let month = parsed?.getMonth() ?? (/* @__PURE__ */ new Date()).getMonth();
3756
- const selected = parsed ? isoDate(parsed) : current;
3757
3793
  const pop = new Popover({
3758
3794
  trigger: anchor,
3759
3795
  theme: this.host.theme(),
@@ -3765,80 +3801,10 @@ var IndicatorInputsDialog = class {
3765
3801
  this.calendarPop = null;
3766
3802
  this.calendarAnchor = null;
3767
3803
  },
3768
- content: (el) => {
3769
- const title = document.createElement("div");
3770
- title.className = "vela-ind-cal-title";
3771
- const prev2 = document.createElement("button");
3772
- prev2.type = "button";
3773
- prev2.className = "vela-ind-cal-nav";
3774
- prev2.setAttribute("aria-label", "Previous month");
3775
- prev2.innerHTML = iconAt("chevron-left", 14);
3776
- const next = document.createElement("button");
3777
- next.type = "button";
3778
- next.className = "vela-ind-cal-nav";
3779
- next.setAttribute("aria-label", "Next month");
3780
- next.innerHTML = iconAt("chevron-right", 14);
3781
- const head = document.createElement("div");
3782
- head.className = "vela-ind-cal-head";
3783
- head.append(prev2, title, next);
3784
- const week = document.createElement("div");
3785
- week.className = "vela-ind-cal-week";
3786
- for (const d of WEEKDAY_LABELS) {
3787
- const cell = document.createElement("span");
3788
- cell.textContent = d;
3789
- week.appendChild(cell);
3790
- }
3791
- const grid = document.createElement("div");
3792
- grid.className = "vela-ind-cal-grid";
3793
- const paint = () => {
3794
- title.textContent = `${MONTH_LABELS[month]} ${year}`;
3795
- grid.replaceChildren();
3796
- const first = new Date(year, month, 1);
3797
- const startPad = first.getDay();
3798
- const days = new Date(year, month + 1, 0).getDate();
3799
- const today = isoDate(/* @__PURE__ */ new Date());
3800
- for (let i = 0; i < startPad; i++) {
3801
- const blank = document.createElement("span");
3802
- blank.className = "vela-ind-cal-blank";
3803
- grid.appendChild(blank);
3804
- }
3805
- for (let day = 1; day <= days; day++) {
3806
- const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
3807
- const b = document.createElement("button");
3808
- b.type = "button";
3809
- b.className = "vela-ind-cal-day";
3810
- b.textContent = String(day);
3811
- if (iso === selected) b.dataset.checked = "1";
3812
- if (iso === today) b.dataset.today = "1";
3813
- b.addEventListener("click", (e) => {
3814
- e.stopPropagation();
3815
- pop.hide();
3816
- onPick(iso);
3817
- });
3818
- grid.appendChild(b);
3819
- }
3820
- };
3821
- prev2.addEventListener("click", (e) => {
3822
- e.stopPropagation();
3823
- month -= 1;
3824
- if (month < 0) {
3825
- month = 11;
3826
- year -= 1;
3827
- }
3828
- paint();
3829
- });
3830
- next.addEventListener("click", (e) => {
3831
- e.stopPropagation();
3832
- month += 1;
3833
- if (month > 11) {
3834
- month = 0;
3835
- year += 1;
3836
- }
3837
- paint();
3838
- });
3839
- paint();
3840
- el.append(head, week, grid);
3841
- }
3804
+ content: (el) => fillCalendar(el, current, (iso) => {
3805
+ pop.hide();
3806
+ onPick(iso);
3807
+ })
3842
3808
  });
3843
3809
  this.calendarPop = pop;
3844
3810
  this.calendarAnchor = anchor;
@@ -3947,7 +3913,7 @@ function groupInputs(inputs) {
3947
3913
  var CALENDAR_SVG = iconAt("calendar", 14);
3948
3914
  var CLOCK_SVG = iconAt("clock", 14);
3949
3915
  var DIALOG_STYLE_ID = "vela-ind-dialog-styles";
3950
- var DIALOG_STYLE_REV = "29";
3916
+ var DIALOG_STYLE_REV = "30";
3951
3917
  var LEGEND_ICON_PX = 16;
3952
3918
  function ensureDialogStyles() {
3953
3919
  if (typeof document === "undefined") return;
@@ -3963,8 +3929,12 @@ function ensureDialogStyles() {
3963
3929
  .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;}
3964
3930
  .vela-ind-combo-chevron:hover{opacity:0.9;}
3965
3931
  .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;}
3932
+ .vela-ind-cal [hidden]{display:none !important;}
3966
3933
  .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
3967
- .vela-ind-cal-title{flex:1;text-align:center;font-weight:600;font-size:14px;color:var(--vela-fg-bright);}
3934
+ .vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
3935
+ .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;}
3936
+ .vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
3937
+ .vela-ind-cal-switch:disabled{cursor:default;}
3968
3938
  .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;}
3969
3939
  .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
3970
3940
  .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
@@ -3975,6 +3945,12 @@ function ensureDialogStyles() {
3975
3945
  .vela-ind-cal-day:hover{background:var(--vela-hover);}
3976
3946
  .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
3977
3947
  .vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
3948
+ .vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
3949
+ .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;}
3950
+ .vela-ind-cal-cell:hover{background:var(--vela-hover);}
3951
+ .vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
3952
+ .vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
3953
+ .vela-ind-cal-cell[data-outside]{opacity:0.45;}
3978
3954
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
3979
3955
  .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;}
3980
3956
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
@@ -4009,6 +3985,7 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
4009
3985
  return { value: v, label: v };
4010
3986
  });
4011
3987
  var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
3988
+ var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
4012
3989
  var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
4013
3990
  function normalizeDateInput(raw) {
4014
3991
  const t = raw.trim();
@@ -4030,6 +4007,151 @@ function parseIsoDate(raw) {
4030
4007
  function isoDate(d) {
4031
4008
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
4032
4009
  }
4010
+ function fillCalendar(el, current, onPick) {
4011
+ const today = /* @__PURE__ */ new Date();
4012
+ const selected = parseIsoDate(current);
4013
+ let year = selected?.getFullYear() ?? today.getFullYear();
4014
+ let month = selected?.getMonth() ?? today.getMonth();
4015
+ let mode = "date";
4016
+ const prev2 = document.createElement("button");
4017
+ prev2.type = "button";
4018
+ prev2.className = "vela-ind-cal-nav";
4019
+ prev2.innerHTML = iconAt("chevron-left", 14);
4020
+ const next = document.createElement("button");
4021
+ next.type = "button";
4022
+ next.className = "vela-ind-cal-nav";
4023
+ next.innerHTML = iconAt("chevron-right", 14);
4024
+ const monthBtn = document.createElement("button");
4025
+ monthBtn.type = "button";
4026
+ monthBtn.className = "vela-ind-cal-switch";
4027
+ monthBtn.setAttribute("aria-label", "Choose month");
4028
+ const yearBtn = document.createElement("button");
4029
+ yearBtn.type = "button";
4030
+ yearBtn.className = "vela-ind-cal-switch";
4031
+ yearBtn.setAttribute("aria-label", "Choose year");
4032
+ const title = document.createElement("div");
4033
+ title.className = "vela-ind-cal-title";
4034
+ title.append(monthBtn, yearBtn);
4035
+ const head = document.createElement("div");
4036
+ head.className = "vela-ind-cal-head";
4037
+ head.append(prev2, title, next);
4038
+ const week = document.createElement("div");
4039
+ week.className = "vela-ind-cal-week";
4040
+ for (const d of WEEKDAY_LABELS) {
4041
+ const cell = document.createElement("span");
4042
+ cell.textContent = d;
4043
+ week.appendChild(cell);
4044
+ }
4045
+ const grid = document.createElement("div");
4046
+ const paint = () => {
4047
+ const decade = Math.floor(year / 10) * 10;
4048
+ monthBtn.hidden = mode !== "date";
4049
+ monthBtn.textContent = MONTH_LABELS[month] ?? "";
4050
+ yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
4051
+ yearBtn.disabled = mode === "year";
4052
+ week.hidden = mode !== "date";
4053
+ prev2.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
4054
+ next.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
4055
+ grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
4056
+ grid.replaceChildren();
4057
+ if (mode === "date") {
4058
+ const startPad = new Date(year, month, 1).getDay();
4059
+ const days = new Date(year, month + 1, 0).getDate();
4060
+ const todayIso = isoDate(today);
4061
+ const selectedIso = selected ? isoDate(selected) : "";
4062
+ for (let i = 0; i < startPad; i++) {
4063
+ const blank = document.createElement("span");
4064
+ blank.className = "vela-ind-cal-blank";
4065
+ grid.appendChild(blank);
4066
+ }
4067
+ for (let day = 1; day <= days; day++) {
4068
+ const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
4069
+ const b = document.createElement("button");
4070
+ b.type = "button";
4071
+ b.className = "vela-ind-cal-day";
4072
+ b.textContent = String(day);
4073
+ if (iso === selectedIso) b.dataset.checked = "1";
4074
+ if (iso === todayIso) b.dataset.today = "1";
4075
+ b.addEventListener("click", (e) => {
4076
+ e.stopPropagation();
4077
+ onPick(iso);
4078
+ });
4079
+ grid.appendChild(b);
4080
+ }
4081
+ return;
4082
+ }
4083
+ if (mode === "month") {
4084
+ for (let m = 0; m < 12; m++) {
4085
+ const b = document.createElement("button");
4086
+ b.type = "button";
4087
+ b.className = "vela-ind-cal-cell";
4088
+ b.textContent = MONTH_SHORT[m] ?? "";
4089
+ if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
4090
+ if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
4091
+ b.addEventListener("click", (e) => {
4092
+ e.stopPropagation();
4093
+ month = m;
4094
+ mode = "date";
4095
+ paint();
4096
+ });
4097
+ grid.appendChild(b);
4098
+ }
4099
+ return;
4100
+ }
4101
+ for (let y = decade - 1; y <= decade + 10; y++) {
4102
+ const b = document.createElement("button");
4103
+ b.type = "button";
4104
+ b.className = "vela-ind-cal-cell";
4105
+ b.textContent = String(y);
4106
+ if (y < decade || y > decade + 9) b.dataset.outside = "1";
4107
+ if (selected?.getFullYear() === y) b.dataset.checked = "1";
4108
+ if (today.getFullYear() === y) b.dataset.today = "1";
4109
+ b.addEventListener("click", (e) => {
4110
+ e.stopPropagation();
4111
+ year = y;
4112
+ mode = "month";
4113
+ paint();
4114
+ });
4115
+ grid.appendChild(b);
4116
+ }
4117
+ };
4118
+ const step = (dir) => {
4119
+ if (mode === "date") {
4120
+ month += dir;
4121
+ if (month < 0) {
4122
+ month = 11;
4123
+ year -= 1;
4124
+ }
4125
+ if (month > 11) {
4126
+ month = 0;
4127
+ year += 1;
4128
+ }
4129
+ } else {
4130
+ year += mode === "month" ? dir : dir * 10;
4131
+ }
4132
+ paint();
4133
+ };
4134
+ prev2.addEventListener("click", (e) => {
4135
+ e.stopPropagation();
4136
+ step(-1);
4137
+ });
4138
+ next.addEventListener("click", (e) => {
4139
+ e.stopPropagation();
4140
+ step(1);
4141
+ });
4142
+ monthBtn.addEventListener("click", (e) => {
4143
+ e.stopPropagation();
4144
+ mode = "month";
4145
+ paint();
4146
+ });
4147
+ yearBtn.addEventListener("click", (e) => {
4148
+ e.stopPropagation();
4149
+ mode = "year";
4150
+ paint();
4151
+ });
4152
+ paint();
4153
+ el.append(head, week, grid);
4154
+ }
4033
4155
  function normalizeTimeInput(raw) {
4034
4156
  const t = raw.trim();
4035
4157
  const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
@@ -5311,10 +5433,12 @@ var BASELINE_LEVEL_DEFAULT = 50;
5311
5433
  var PREMARKET_SHADE = withAlpha(WARNING, 0.08);
5312
5434
  var POSTMARKET_SHADE = withAlpha(ACCENT, 0.08);
5313
5435
  var EXTENDED_SHADE = POSTMARKET_SHADE;
5436
+ var DEFAULT_MARGINS = { top: 10, bottom: 10, right: 10 };
5314
5437
  function defaultChartStyle() {
5315
5438
  return {
5316
5439
  chartTypes: {},
5317
5440
  fontSize: 11,
5441
+ margins: { ...DEFAULT_MARGINS },
5318
5442
  gridVert: { visible: true, color: null },
5319
5443
  gridHorz: { visible: true, color: null },
5320
5444
  borderColor: null,
@@ -5438,6 +5562,12 @@ function clampLevel(v) {
5438
5562
  function clampSpacing(v) {
5439
5563
  return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
5440
5564
  }
5565
+ function clampMarginPct(v) {
5566
+ return v < 0 ? 0 : v > 40 ? 40 : v;
5567
+ }
5568
+ function clampMarginBars(v) {
5569
+ return Math.round(v < 0 ? 0 : v > 200 ? 200 : v);
5570
+ }
5441
5571
  function factoryResetConfig(factory, priceStyle = factory.series.style) {
5442
5572
  const bag = {};
5443
5573
  for (const t of chartTypes()) {
@@ -5486,6 +5616,7 @@ function mergeConfig(base, patch) {
5486
5616
  const ps = asObject(p.priceScale);
5487
5617
  const anim = asObject(p.animations);
5488
5618
  const panes = asObject(p.panes);
5619
+ const margins = asObject(p.margins);
5489
5620
  const trades = asObject(p.trades);
5490
5621
  const ts = asObject(p.timeScale);
5491
5622
  const marks = asObject(p.marks);
@@ -5545,6 +5676,11 @@ function mergeConfig(base, patch) {
5545
5676
  panes: {
5546
5677
  separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
5547
5678
  },
5679
+ margins: {
5680
+ top: isNum(margins.top) ? clampMarginPct(margins.top) : base.margins.top,
5681
+ bottom: isNum(margins.bottom) ? clampMarginPct(margins.bottom) : base.margins.bottom,
5682
+ right: isNum(margins.right) ? clampMarginBars(margins.right) : base.margins.right
5683
+ },
5548
5684
  trades: {
5549
5685
  visible: isBool(trades.visible) ? trades.visible : base.trades.visible,
5550
5686
  labels: isBool(trades.labels) ? trades.labels : base.trades.labels,
@@ -10874,6 +11010,10 @@ var BUILTIN_SETTINGS_IDS = [
10874
11010
  "canvas.grid",
10875
11011
  "canvas.grid.vertical",
10876
11012
  "canvas.grid.horizontal",
11013
+ "canvas.margins",
11014
+ "canvas.margins.top",
11015
+ "canvas.margins.bottom",
11016
+ "canvas.margins.right",
10877
11017
  "canvas.theme"
10878
11018
  ];
10879
11019
  function settingsIdCatalog(hostSections, markGroups = []) {
@@ -11354,7 +11494,7 @@ var SettingsDialog = class {
11354
11494
  if (hr.kind === "heading") body.append(this.sectionTitle(hr.label));
11355
11495
  else if (hr.kind === "toggle") body.append(this.boolRow(hr.label, hr.get(), (v) => hr.set(v)));
11356
11496
  else if (hr.kind === "color") body.append(this.colorRow(hr.label, hr.get(), (v) => hr.set(v)));
11357
- else body.append(this.selectRowLabeled(hr.label, hr.get(), hr.options.map((o) => [o, o]), (v) => hr.set(v)));
11497
+ else body.append(this.selectRowLabeled(hr.label, hr.get(), normalizeSelectOptions(hr.options), (v) => hr.set(v)));
11358
11498
  }
11359
11499
  }
11360
11500
  };
@@ -11404,6 +11544,10 @@ var SettingsDialog = class {
11404
11544
  body.append(sid(this.toggleRow("Horizontal", config.grid.horzLines.visible, (v) => this.emit({ grid: { horzLines: { visible: v } } }), [
11405
11545
  this.swatch(config.grid.horzLines.color, (v) => this.emit({ grid: { horzLines: { color: v } } }))
11406
11546
  ]), "canvas.grid.horizontal"));
11547
+ body.append(sid(this.sectionTitle("Margins"), "canvas.margins"));
11548
+ body.append(sid(this.numberRow("Top", config.margins.top, 0, 40, 1, (v) => this.emit({ margins: { top: v } }), "%"), "canvas.margins.top"));
11549
+ body.append(sid(this.numberRow("Bottom", config.margins.bottom, 0, 40, 1, (v) => this.emit({ margins: { bottom: v } }), "%"), "canvas.margins.bottom"));
11550
+ body.append(sid(this.numberRow("Right", config.margins.right, 0, 200, 1, (v) => this.emit({ margins: { right: v } }), "bars"), "canvas.margins.right"));
11407
11551
  if (this.themeControl) {
11408
11552
  const tc = this.themeControl;
11409
11553
  body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
@@ -11975,23 +12119,16 @@ var SettingsDialog = class {
11975
12119
  this.hintTips.push(dispose);
11976
12120
  return el;
11977
12121
  }
11978
- numberRow(label, value, min, max, step, onChange) {
11979
- return fieldRow({
11980
- label,
11981
- labelSize: "sm",
11982
- className: "vela-sd-row",
11983
- control: buildFieldControl({
11984
- kind: "number",
11985
- value,
11986
- min,
11987
- max,
11988
- step,
11989
- fill: false,
11990
- commit: "live",
11991
- steppers: true,
11992
- onChange
11993
- }).el
11994
- });
12122
+ /** `unit` (optional) trails the input as muted text — "%", "bars". */
12123
+ numberRow(label, value, min, max, step, onChange, unit) {
12124
+ const controls = [buildFieldControl({ kind: "number", value, min, max, step, fill: false, commit: "live", steppers: true, onChange }).el];
12125
+ if (unit) {
12126
+ const u = document.createElement("span");
12127
+ u.textContent = unit;
12128
+ u.style.cssText = "color:var(--vela-fg-muted);";
12129
+ controls.push(u);
12130
+ }
12131
+ return this.rowWith(label, controls);
11995
12132
  }
11996
12133
  /** A dropdown whose option values differ from their display labels. */
11997
12134
  selectRowLabeled(label, value, options, onChange) {
@@ -16877,9 +17014,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange)
16877
17014
  }
16878
17015
 
16879
17016
  // src/renderers/native/core/autoscale.ts
16880
- var MARGIN_TOP = 2 / 7;
16881
- var MARGIN_BOTTOM = 1 / 7;
16882
- function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log = false, offsetOf = () => 0) {
17017
+ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log = false, offsetOf = () => 0, margins = DEFAULT_MARGINS) {
16883
17018
  let min = Infinity;
16884
17019
  let max = -Infinity;
16885
17020
  const consider = (v) => {
@@ -16914,14 +17049,17 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
16914
17049
  const pad = Math.abs(min) * 0.1 || 1;
16915
17050
  return { min: min - pad, max: max + pad, log: log && min - pad > 0 };
16916
17051
  }
17052
+ const content = Math.max(0.1, 1 - (margins.top + margins.bottom) / 100);
17053
+ const above = margins.top / 100 / content;
17054
+ const below = margins.bottom / 100 / content;
16917
17055
  if (log && min > 0) {
16918
17056
  const lmin = Math.log(min);
16919
17057
  const lmax = Math.log(max);
16920
17058
  const lspan = lmax - lmin;
16921
- return { min: Math.exp(lmin - lspan * MARGIN_BOTTOM), max: Math.exp(lmax + lspan * MARGIN_TOP), log: true };
17059
+ return { min: Math.exp(lmin - lspan * below), max: Math.exp(lmax + lspan * above), log: true };
16922
17060
  }
16923
17061
  const span = max - min;
16924
- return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
17062
+ return { min: min - span * below, max: max + span * above };
16925
17063
  }
16926
17064
  function considerSeries(s, i0, i1, off, consider) {
16927
17065
  if (!seriesInScale(s)) return;
@@ -18722,6 +18860,7 @@ var NativeRenderer = class {
18722
18860
  intro: this.intro.style !== false
18723
18861
  },
18724
18862
  panes: { separatorColor: s.separatorColor ?? t.borderColor },
18863
+ margins: { ...s.margins },
18725
18864
  trades: {
18726
18865
  visible: this.scene.tradeMarkers.visible,
18727
18866
  labels: this.scene.tradeMarkers.labels,
@@ -18834,6 +18973,10 @@ var NativeRenderer = class {
18834
18973
  this.animAutoscale.toggle(next.animations.autoscale);
18835
18974
  this.intro = { style: next.animations.intro ? this.introOnStyle : false, duration: this.intro.duration || INTRO_DURATION_DEFAULT_MS };
18836
18975
  s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
18976
+ if (next.margins.right !== s.margins.right && this.coords.barCount > 0) {
18977
+ this.applyViewport({ barSpacing: this.coords.getViewport().barSpacing, rightOffset: next.margins.right });
18978
+ }
18979
+ s.margins = { ...next.margins };
18837
18980
  this.scene.tradeMarkers = {
18838
18981
  visible: next.trades.visible,
18839
18982
  labels: next.trades.labels,
@@ -19077,7 +19220,7 @@ var NativeRenderer = class {
19077
19220
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
19078
19221
  scrollToRealtime() {
19079
19222
  if (this.coords.barCount === 0) return;
19080
- this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
19223
+ this.glideRightOffset(this.scene.style.margins.right);
19081
19224
  }
19082
19225
  /** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
19083
19226
  * instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
@@ -21019,6 +21162,7 @@ var NativeRenderer = class {
21019
21162
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
21020
21163
  this.chrome.prepare(this.scene, this.coords, this.theme);
21021
21164
  const animating = this.animator.active;
21165
+ const margins = this.scene.style.margins;
21022
21166
  for (const pane of panes) {
21023
21167
  if (pane.manualScale) {
21024
21168
  pane.scaleTarget = pane.manualScale;
@@ -21034,7 +21178,7 @@ var NativeRenderer = class {
21034
21178
  if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
21035
21179
  }
21036
21180
  const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels) || !this.paneHasMeasurableContent(masterModels, dr));
21037
- pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
21181
+ pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
21038
21182
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
21039
21183
  pane.axisFormat = void 0;
21040
21184
  pane.axisBands = void 0;
@@ -21045,7 +21189,7 @@ var NativeRenderer = class {
21045
21189
  pane.axisFormat = "volume";
21046
21190
  }
21047
21191
  } else if (this.layerNativesOwnPane(pane, masterModels)) {
21048
- pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
21192
+ pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
21049
21193
  pane.percentBaseline = this.bars[i0]?.close ?? 0;
21050
21194
  if (masterModels.every((m) => m.paneAxis != null)) {
21051
21195
  pane.axisFormat = "none";
@@ -21074,7 +21218,7 @@ var NativeRenderer = class {
21074
21218
  continue;
21075
21219
  }
21076
21220
  const mdr = this.chrome.paneDrawingsRange([model], this.scene, false, vr);
21077
- sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id));
21221
+ sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id), margins);
21078
21222
  if (!animating || !sl.initialized) {
21079
21223
  sl.scale = { ...sl.scaleTarget };
21080
21224
  sl.initialized = true;
@@ -21115,13 +21259,13 @@ var NativeRenderer = class {
21115
21259
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
21116
21260
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
21117
21261
  const visibleBars = Math.min(n, 200);
21118
- const rightOffset = 6;
21262
+ const rightOffset = this.scene.style.margins.right;
21119
21263
  const v = this.clampViewport(w / ((visibleBars + rightOffset) * this.coords.spacingScale), rightOffset);
21120
21264
  this.coords.setViewport(v);
21121
21265
  this.targetBarSpacing = v.barSpacing;
21122
21266
  }
21123
21267
  /** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
21124
- * zoom (bar spacing), re-anchor the newest bars at the default right offset.
21268
+ * zoom (bar spacing), re-anchor the newest bars at the configured right margin.
21125
21269
  * `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
21126
21270
  * head may still be backfilling toward the previous depth, and raising the spacing
21127
21271
  * to its temporary bar count would lose the zoom this exists to keep. */
@@ -21130,7 +21274,7 @@ var NativeRenderer = class {
21130
21274
  this.panVelocity = 0;
21131
21275
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
21132
21276
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
21133
- const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
21277
+ const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: this.scene.style.margins.right };
21134
21278
  this.coords.setViewport(v);
21135
21279
  this.targetBarSpacing = v.barSpacing;
21136
21280
  }
@@ -24277,7 +24421,8 @@ var ClassicIndicator = class {
24277
24421
  lineStyle: plot.lineStyle ?? "solid",
24278
24422
  ...plot.base != null ? { base: plot.base } : {}
24279
24423
  },
24280
- ...plot.overlay ? { overlay: true } : {}
24424
+ ...plot.overlay ? { overlay: true } : {},
24425
+ ...plot.display ? { display: plot.display } : {}
24281
24426
  });
24282
24427
  });
24283
24428
  if (out.markers && out.markers.length > 0) {
@@ -24299,7 +24444,9 @@ var ClassicIndicator = class {
24299
24444
  paneId: "",
24300
24445
  fromSeriesId: from,
24301
24446
  toSeriesId: to,
24302
- color: band.color
24447
+ color: band.color,
24448
+ ...band.colors ? { colors: [...band.colors] } : {},
24449
+ ...band.gradient ? { gradient: [...band.gradient] } : {}
24303
24450
  }
24304
24451
  ];
24305
24452
  });
@@ -24457,6 +24604,16 @@ function stdev(v, len) {
24457
24604
  return Math.sqrt(sq / len);
24458
24605
  });
24459
24606
  }
24607
+ function meanDev(v, len) {
24608
+ return windowed(v, len, (w, from, to) => {
24609
+ let s = 0;
24610
+ for (let k = from; k <= to; k++) s += w[k];
24611
+ const mean = s / len;
24612
+ let dev = 0;
24613
+ for (let k = from; k <= to; k++) dev += Math.abs(w[k] - mean);
24614
+ return dev / len;
24615
+ });
24616
+ }
24460
24617
  function highest(v, len) {
24461
24618
  return windowed(v, len, (w, from, to) => {
24462
24619
  let m = -Infinity;
@@ -24588,7 +24745,7 @@ function stoch(v, hi, lo, len) {
24588
24745
  }
24589
24746
  return out;
24590
24747
  }
24591
- function linreg(v, len) {
24748
+ function linreg(v, len, offset = 0) {
24592
24749
  const sx = (len - 1) * len / 2;
24593
24750
  const sxx = (len - 1) * len * (2 * len - 1) / 6;
24594
24751
  const denom = len * sxx - sx * sx;
@@ -24602,9 +24759,101 @@ function linreg(v, len) {
24602
24759
  }
24603
24760
  const slope = denom === 0 ? 0 : (len * sxy - sx * sy) / denom;
24604
24761
  const intercept = (sy - slope * sx) / len;
24605
- return intercept + slope * (len - 1);
24762
+ return intercept + slope * (len - 1 - offset);
24763
+ });
24764
+ }
24765
+ function hma(v, len) {
24766
+ const n = Math.max(2, Math.trunc(len));
24767
+ const raw = sub(map(wma(v, Math.max(1, Math.trunc(n / 2))), (x) => 2 * x), wma(v, n));
24768
+ return wma(raw, Math.max(1, Math.round(Math.sqrt(n))));
24769
+ }
24770
+ function alma(v, len, offset, sigma) {
24771
+ const m = offset * (len - 1);
24772
+ const s = len / Math.max(sigma, 1e-9);
24773
+ const weights = new Array(len);
24774
+ let norm = 0;
24775
+ for (let i = 0; i < len; i++) {
24776
+ const d = i - m;
24777
+ const w = Math.exp(-(d * d) / (2 * s * s));
24778
+ weights[i] = w;
24779
+ norm += w;
24780
+ }
24781
+ return windowed(v, len, (win, from, to) => {
24782
+ let acc = 0;
24783
+ for (let k = from; k <= to; k++) acc += win[k] * weights[k - from];
24784
+ return norm === 0 ? Number.NaN : acc / norm;
24785
+ });
24786
+ }
24787
+ function dema(v, len) {
24788
+ const e1 = ema(v, len);
24789
+ return sub(map(e1, (x) => 2 * x), ema(e1, len));
24790
+ }
24791
+ function tema(v, len) {
24792
+ const e1 = ema(v, len);
24793
+ const e2 = ema(e1, len);
24794
+ const e3 = ema(e2, len);
24795
+ return zip(zip(map(e1, (x) => 3 * x), map(e2, (x) => 3 * x), (a, b) => a - b), e3, (a, b) => a + b);
24796
+ }
24797
+ function kama(v, len, fastAlpha = 2 / 3, slowAlpha = 2 / 31) {
24798
+ const direction = map(change(v, len), Math.abs);
24799
+ const volatility = sum(map(change(v), Math.abs), len);
24800
+ const out = new Array(v.length).fill(Number.NaN);
24801
+ let prev2 = Number.NaN;
24802
+ for (let i = 0; i < v.length; i++) {
24803
+ const x = v[i];
24804
+ if (!Number.isFinite(x)) continue;
24805
+ const d = direction[i];
24806
+ const w = volatility[i];
24807
+ if (!Number.isFinite(d) || !Number.isFinite(w)) {
24808
+ prev2 = Number.isFinite(prev2) ? prev2 : x;
24809
+ out[i] = prev2;
24810
+ continue;
24811
+ }
24812
+ const er = w === 0 ? 0 : d / w;
24813
+ const sc = (er * (fastAlpha - slowAlpha) + slowAlpha) ** 2;
24814
+ prev2 = Number.isFinite(prev2) ? prev2 + sc * (x - prev2) : x;
24815
+ out[i] = prev2;
24816
+ }
24817
+ return out;
24818
+ }
24819
+ function mcginley(v, len) {
24820
+ const out = new Array(v.length).fill(Number.NaN);
24821
+ let prev2 = Number.NaN;
24822
+ for (let i = 0; i < v.length; i++) {
24823
+ const x = v[i];
24824
+ if (!Number.isFinite(x)) continue;
24825
+ if (!Number.isFinite(prev2)) {
24826
+ prev2 = x;
24827
+ } else {
24828
+ const ratio = prev2 === 0 ? 0 : x / prev2;
24829
+ const denom = len * ratio ** 4;
24830
+ prev2 += denom === 0 ? 0 : (x - prev2) / denom;
24831
+ }
24832
+ out[i] = prev2;
24833
+ }
24834
+ return out;
24835
+ }
24836
+ function hamming(v, len, a0 = 0.54, a1 = 0.46) {
24837
+ if (len <= 1) return v.map((x) => Number.isFinite(x) ? x : Number.NaN);
24838
+ const weights = new Array(len);
24839
+ let norm = 0;
24840
+ for (let i = 0; i < len; i++) {
24841
+ const w = a0 - a1 * Math.cos(2 * Math.PI * i / (len - 1));
24842
+ weights[i] = w;
24843
+ norm += w;
24844
+ }
24845
+ return windowed(v, len, (win, from, to) => {
24846
+ let acc = 0;
24847
+ for (let k = from; k <= to; k++) acc += win[k] * weights[to - k];
24848
+ return norm === 0 ? Number.NaN : acc / norm;
24606
24849
  });
24607
24850
  }
24851
+ function cmo(v, len) {
24852
+ const d = change(v);
24853
+ const up = sum(map(d, (x) => Math.max(x, 0)), len);
24854
+ const dn = sum(map(d, (x) => Math.max(-x, 0)), len);
24855
+ return zip(up, dn, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
24856
+ }
24608
24857
  function swma(v) {
24609
24858
  const out = new Array(v.length).fill(Number.NaN);
24610
24859
  for (let i = 3; i < v.length; i++) {
@@ -24654,38 +24903,253 @@ function cumSum(v) {
24654
24903
  }
24655
24904
 
24656
24905
  // src/core/native-indicators/classics/shared.ts
24657
- function lengthInput(defval, key = "length", title = "Length", max = 5e3) {
24658
- return { key, title, type: "int", defval, min: 1, max, step: 1 };
24906
+ var SETTINGS = "Settings";
24907
+ var STYLE = "Style";
24908
+ function lengthInput(defval, key = "length", title = "Length", max = 5e3, tooltip) {
24909
+ return { key, title, type: "int", defval, min: 1, max, step: 1, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
24910
+ }
24911
+ function intInput(key, title, defval, min = 1, max = 5e3, tooltip) {
24912
+ return { key, title, type: "int", defval, min, max, step: 1, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
24913
+ }
24914
+ function floatInput(key, title, defval, min = 0, max = 1e3, step = 0.1, tooltip) {
24915
+ return { key, title, type: "float", defval, min, max, step, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
24916
+ }
24917
+ function boolInput(key, title, defval, group = SETTINGS, tooltip) {
24918
+ return { key, title, type: "bool", defval, group, ...tooltip != null ? { tooltip } : {} };
24659
24919
  }
24660
- function sourceInput(defval = "Close") {
24661
- return { key: "source", title: "Source", type: "string", defval, options: SOURCES2 };
24920
+ function optionInput(key, title, defval, options, tooltip) {
24921
+ return { key, title, type: "string", defval, options, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
24662
24922
  }
24663
- function colorInput(defval = SERIES_LINE, title = "Color", key = "color") {
24664
- return { key, title, type: "color", defval };
24923
+ function sourceInput(defval = "Close", key = "source", title = "Source", tooltip) {
24924
+ return { key, title, type: "string", defval, options: SOURCES2, group: SETTINGS, ...tooltip != null ? { tooltip } : {} };
24925
+ }
24926
+ function colorInput(defval = INFO, title = "Color", key = "color", tooltip) {
24927
+ return { key, title, type: "color", defval, group: STYLE, ...tooltip != null ? { tooltip } : {} };
24928
+ }
24929
+ function widthInput(defval = 2, key = "lineWidth", title = "Line Width") {
24930
+ return { key, title, type: "int", defval, min: 1, max: 5, step: 1, group: STYLE, tooltip: "Width of the plotted line, in pixels." };
24931
+ }
24932
+ function transp(hex, transparency) {
24933
+ return withAlpha2(hex, (100 - transparency) / 100);
24665
24934
  }
24666
24935
  function withAlpha2(hex, alpha = 0.08) {
24667
24936
  const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
24668
24937
  if (!m) return hex;
24669
- const a = Math.round(alpha * 255).toString(16).padStart(2, "0");
24938
+ const a = Math.round(Math.max(0, Math.min(1, alpha)) * 255).toString(16).padStart(2, "0");
24670
24939
  return `#${m[1]}${a}`;
24671
24940
  }
24941
+ function channelStyleInputs(basisTitle = "Basis") {
24942
+ return [
24943
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the lower band and of the basis while price holds above it."),
24944
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the upper band and of the basis while price sits below it."),
24945
+ colorInput(NEUTRAL, basisTitle, "basisColor", "Color of the basis line while price sits exactly on it.")
24946
+ ];
24947
+ }
24948
+ function directionalChannel(bars, basis, upper, lower, ink, key) {
24949
+ const basisColors = basis.map((b, i) => {
24950
+ if (!Number.isFinite(b)) return null;
24951
+ const c = bars[i].close;
24952
+ return c > b ? ink.bull : c < b ? ink.bear : ink.neutral;
24953
+ });
24954
+ const wash = upper.map((u, i) => {
24955
+ const l = lower[i];
24956
+ if (!Number.isFinite(u) || !Number.isFinite(l)) return null;
24957
+ return { topValue: u, bottomValue: l, topColor: transp(ink.bear, 90), bottomColor: transp(ink.bull, 90) };
24958
+ });
24959
+ return {
24960
+ plots: [
24961
+ { key: "basis", title: "Basis", values: [...basis], color: ink.neutral, colors: basisColors },
24962
+ { key: "upper", title: "Upper", values: [...upper], color: transp(ink.bear, 30) },
24963
+ { key: "lower", title: "Lower", values: [...lower], color: transp(ink.bull, 30) }
24964
+ ],
24965
+ bands: [{ key, from: "upper", to: "lower", color: transp(NEUTRAL, 90), gradient: wash }]
24966
+ };
24967
+ }
24968
+ function anchorPlot(key, title, length, level) {
24969
+ return {
24970
+ key,
24971
+ title,
24972
+ values: new Array(length).fill(level),
24973
+ color: transp(NEUTRAL, 100),
24974
+ display: { pane: false, legend: false, dataWindow: false }
24975
+ };
24976
+ }
24977
+ function thresholdZones(length, levels, ink, transparency = 92) {
24978
+ return {
24979
+ plots: [
24980
+ anchorPlot("obAnchor", "Overbought Anchor", length, levels.overbought),
24981
+ anchorPlot("osAnchor", "Oversold Anchor", length, levels.oversold),
24982
+ anchorPlot("topAnchor", "Top Anchor", length, levels.top),
24983
+ anchorPlot("bottomAnchor", "Bottom Anchor", length, levels.bottom)
24984
+ ],
24985
+ bands: [
24986
+ { key: "obZone", from: "obAnchor", to: "topAnchor", color: transp(ink.overbought, transparency) },
24987
+ { key: "osZone", from: "osAnchor", to: "bottomAnchor", color: transp(ink.oversold, transparency) }
24988
+ ]
24989
+ };
24990
+ }
24991
+ function rampColors(values, levels, ink) {
24992
+ return values.map((x) => {
24993
+ if (!Number.isFinite(x)) return null;
24994
+ return x <= levels.mid ? gradient(x, levels.low, levels.mid, ink.low, ink.mid) : gradient(x, levels.mid, levels.high, ink.mid, ink.high);
24995
+ });
24996
+ }
24997
+ function baselineGradient(values, baseline, inkOf, valueTransparency = 50, baseTransparency = 100) {
24998
+ return values.map((x, i) => {
24999
+ if (!Number.isFinite(x)) return null;
25000
+ const ink = inkOf(x, i);
25001
+ const above = x > baseline;
25002
+ return {
25003
+ topValue: Math.max(x, baseline),
25004
+ bottomValue: Math.min(x, baseline),
25005
+ topColor: transp(ink, above ? valueTransparency : baseTransparency),
25006
+ bottomColor: transp(ink, above ? baseTransparency : valueTransparency)
25007
+ };
25008
+ });
25009
+ }
25010
+ function momentumColumnColors(values, ink) {
25011
+ return values.map((x, i) => {
25012
+ if (!Number.isFinite(x)) return null;
25013
+ const prev2 = i > 0 && Number.isFinite(values[i - 1]) ? values[i - 1] : x;
25014
+ const rising = x > prev2;
25015
+ if (x >= 0) return rising ? ink.growAbove : ink.fallAbove;
25016
+ return rising ? ink.growBelow : ink.fallBelow;
25017
+ });
25018
+ }
25019
+ function boundedOscillator(opts) {
25020
+ const mid = (opts.overbought + opts.oversold) / 2;
25021
+ const ramp = rampColors(opts.values, { low: opts.oversold, mid, high: opts.overbought }, opts.ink);
25022
+ const zones = thresholdZones(
25023
+ opts.values.length,
25024
+ { overbought: opts.overbought, oversold: opts.oversold, top: opts.scale.top, bottom: opts.scale.bottom },
25025
+ { overbought: opts.ink.high, oversold: opts.ink.low },
25026
+ opts.zoneTransparency
25027
+ );
25028
+ const plots = [{ key: opts.key, title: opts.title, values: opts.values, color: opts.ink.mid, width: opts.width ?? 1, colors: ramp }, ...zones.plots];
25029
+ const bands = [...zones.bands];
25030
+ if (opts.bandFill != null) bands.push({ key: "band", from: "obAnchor", to: "osAnchor", color: opts.bandFill });
25031
+ if (opts.midpointWash) {
25032
+ plots.push(anchorPlot("midAnchor", "Midpoint Anchor", opts.values.length, mid));
25033
+ bands.push({ key: "wash", from: opts.key, to: "midAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(opts.values, mid, (_x, i) => ramp[i] ?? opts.ink.mid) });
25034
+ }
25035
+ const levels = [
25036
+ { key: "overbought", price: opts.overbought, color: opts.levelInk, lineStyle: "dashed", title: opts.levelTitles?.overbought ?? "Overbought" },
25037
+ { key: "oversold", price: opts.oversold, color: opts.levelInk, lineStyle: "dashed", title: opts.levelTitles?.oversold ?? "Oversold" }
25038
+ ];
25039
+ if (opts.midline != null) levels.splice(1, 0, { key: "midline", price: opts.midline, color: opts.levelInk, lineStyle: "dotted", title: "Midline" });
25040
+ return { plots, bands, levels };
25041
+ }
25042
+ function centeredOscillator(opts) {
25043
+ const baseline = opts.baseline ?? 0;
25044
+ const inkOf = (x) => x > baseline ? opts.ink.bull : x < baseline ? opts.ink.bear : opts.ink.neutral;
25045
+ const plots = [
25046
+ { key: opts.key, title: opts.title, values: opts.values, color: opts.ink.neutral, width: opts.width ?? 1, colors: opts.values.map((x) => Number.isFinite(x) ? inkOf(x) : null) },
25047
+ ...opts.extraPlots ?? []
25048
+ ];
25049
+ const bands = [];
25050
+ if (opts.fill !== false) {
25051
+ plots.push(anchorPlot("baseline", "Baseline Anchor", opts.values.length, baseline));
25052
+ bands.push({ key: "wash", from: opts.key, to: "baseline", color: transp(NEUTRAL, 100), gradient: baselineGradient(opts.values, baseline, inkOf, opts.valueTransparency) });
25053
+ }
25054
+ return {
25055
+ plots,
25056
+ bands,
25057
+ levels: [{ key: "zero", price: baseline, color: opts.zeroInk, lineStyle: opts.zeroLineStyle ?? "dashed", title: opts.zeroTitle ?? "Zero Line" }, ...opts.extraLevels ?? []]
25058
+ };
25059
+ }
25060
+ function channels(hex) {
25061
+ const m = /^#([0-9a-f]{6})/i.exec(hex.trim());
25062
+ if (!m) return null;
25063
+ const n = parseInt(m[1], 16);
25064
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
25065
+ }
25066
+ function gradient(value, low, high, from, to) {
25067
+ const a = channels(from);
25068
+ const b = channels(to);
25069
+ if (!a || !b || !Number.isFinite(value)) return from;
25070
+ const span = high - low;
25071
+ const t = span === 0 ? 0 : Math.max(0, Math.min(1, (value - low) / span));
25072
+ const mix = (i) => Math.round(a[i] + (b[i] - a[i]) * t).toString(16).padStart(2, "0");
25073
+ return `#${mix(0)}${mix(1)}${mix(2)}`;
25074
+ }
24672
25075
 
24673
25076
  // src/core/native-indicators/classics/averages.ts
24674
- var MA_KINDS = ["SMA", "EMA", "WMA", "RMA", "VWMA"];
24675
- var offsetInput = { key: "offset", title: "Offset", type: "int", defval: 0, min: -500, max: 500, step: 1 };
25077
+ var MA_TYPES = ["SMA", "EMA", "WMA", "RMA (SMMA)", "HMA", "ALMA", "VWMA", "DEMA", "TEMA", "KAMA", "LSMA", "McGinley", "Hamming"];
25078
+ var ENVELOPE_MA_TYPES = ["SMA", "EMA", "WMA", "RMA (SMMA)", "HMA", "VWMA"];
25079
+ function movingAverageOf(kind, src, bars, len, opts = {}) {
25080
+ switch (kind) {
25081
+ case "EMA":
25082
+ return ema(src, len);
25083
+ case "WMA":
25084
+ return wma(src, len);
25085
+ case "RMA":
25086
+ case "RMA (SMMA)":
25087
+ return rma(src, len);
25088
+ case "HMA":
25089
+ return hma(src, len);
25090
+ case "ALMA":
25091
+ return alma(src, len, opts.almaOffset ?? 0.85, opts.almaSigma ?? 6);
25092
+ case "VWMA":
25093
+ return vwma(src, volumes(bars), len);
25094
+ case "DEMA":
25095
+ return dema(src, len);
25096
+ case "TEMA":
25097
+ return tema(src, len);
25098
+ case "KAMA":
25099
+ return kama(src, len);
25100
+ case "LSMA":
25101
+ return linreg(src, len, opts.lsmaOffset ?? 0);
25102
+ case "McGinley":
25103
+ return mcginley(src, len);
25104
+ case "Hamming":
25105
+ return hamming(src, len);
25106
+ default:
25107
+ return sma(src, len);
25108
+ }
25109
+ }
25110
+ function slopeColors(values, bull, bear, flat) {
25111
+ return values.map((x, i) => {
25112
+ if (!Number.isFinite(x)) return null;
25113
+ const prev2 = i > 0 ? values[i - 1] : Number.NaN;
25114
+ if (!Number.isFinite(prev2)) return flat;
25115
+ return x > prev2 ? bull : x < prev2 ? bear : flat;
25116
+ });
25117
+ }
25118
+ function directionInputs() {
25119
+ return [
25120
+ boolInput("trendColor", "Slope Trend Coloring", true, STYLE, "Colors the line by its slope \u2014 bullish while it rises, bearish while it falls. Disable to draw it in the neutral color."),
25121
+ colorInput(BULLISH, "Bullish", "bullColor"),
25122
+ colorInput(BEARISH, "Bearish", "bearColor"),
25123
+ colorInput(NEUTRAL, "Neutral", "neutralColor"),
25124
+ widthInput()
25125
+ ];
25126
+ }
25127
+ function slopeColoredPlot(key, title, values, inputs) {
25128
+ const neutral = str3(inputs, "neutralColor", NEUTRAL);
25129
+ const colored = bool(inputs, "trendColor", true);
25130
+ return {
25131
+ key,
25132
+ title,
25133
+ values,
25134
+ color: neutral,
25135
+ width: num3(inputs, "lineWidth", 2),
25136
+ ...colored ? { colors: slopeColors(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), neutral) } : {}
25137
+ };
25138
+ }
25139
+ var offsetInput = { key: "offset", title: "Offset", type: "int", defval: 0, min: -500, max: 500, step: 1, group: SETTINGS, tooltip: "Bars the plotted line is displaced by. Positive values push it to the right." };
24676
25140
  function fixedAverage(type, title, shortTitle, smooth) {
24677
25141
  return {
24678
25142
  type,
24679
25143
  title,
24680
25144
  shortTitle,
24681
25145
  overlay: true,
24682
- inputs: [lengthInput(20), sourceInput(), offsetInput, colorInput()],
25146
+ inputs: [lengthInput(20, "length", "Length", 5e3, `Number of bars used in the ${shortTitle} calculation.`), sourceInput("Close", "source", "Source", `Price series the ${shortTitle} is calculated on.`), offsetInput, colorInput()],
24683
25147
  compute: (bars, inputs) => {
24684
25148
  const len = num3(inputs, "length", 20);
24685
25149
  let values = smooth(sourceValues(bars, str3(inputs, "source", "Close")), len);
24686
25150
  const offset = Math.trunc(num3(inputs, "offset", 0));
24687
25151
  if (offset !== 0) values = shift(values, offset);
24688
- return { plots: [{ key: shortTitle.toLowerCase(), title: shortTitle, values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
25152
+ return { plots: [{ key: shortTitle.toLowerCase(), title: shortTitle, values, color: str3(inputs, "color", INFO), width: 2 }] };
24689
25153
  }
24690
25154
  };
24691
25155
  }
@@ -24697,36 +25161,37 @@ var movingAverage = {
24697
25161
  shortTitle: "MA",
24698
25162
  overlay: true,
24699
25163
  inputs: [
24700
- { key: "maType", title: "Type", type: "string", defval: "SMA", options: MA_KINDS },
24701
- lengthInput(20),
24702
- sourceInput(),
25164
+ optionInput("maType", "MA 1 Type", "SMA", MA_TYPES, "Moving average calculation used for MA 1."),
25165
+ lengthInput(20, "length", "MA 1 Length", 5e3, "Number of bars used in the MA 1 calculation."),
25166
+ sourceInput("Close", "source", "MA 1 Source", "Price series MA 1 is calculated on."),
25167
+ boolInput("showMa2", "Show MA 2", false, SETTINGS, "Displays the optional second moving average."),
25168
+ optionInput("ma2Type", "MA 2 Type", "SMA", MA_TYPES, "Moving average calculation used for MA 2."),
25169
+ lengthInput(50, "ma2Length", "MA 2 Length", 5e3, "Number of bars used in the MA 2 calculation."),
25170
+ sourceInput("Close", "ma2Source", "MA 2 Source", "Price series MA 2 is calculated on."),
25171
+ floatInput("almaOffset", "ALMA Offset", 0.85, 0, 1, 0.05, "ALMA type only. Values near 1 make the average more responsive, values near 0 make it smoother."),
25172
+ floatInput("almaSigma", "ALMA Sigma", 6, 0.1, 100, 0.5, "ALMA type only. Larger values widen the Gaussian window for a smoother average."),
25173
+ intInput("lsmaOffset", "LSMA Offset", 0, 0, 500, "LSMA type only. Bar offset applied to the linear regression value."),
24703
25174
  offsetInput,
24704
- colorInput()
25175
+ colorInput(INFO, "MA 1", "color", "MA 1 line color."),
25176
+ colorInput(WARNING, "MA 2", "ma2Color", "MA 2 line color.")
24705
25177
  ],
24706
25178
  compute: (bars, inputs) => {
24707
- const len = num3(inputs, "length", 20);
24708
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
25179
+ const opts = {
25180
+ almaOffset: num3(inputs, "almaOffset", 0.85),
25181
+ almaSigma: num3(inputs, "almaSigma", 6),
25182
+ lsmaOffset: Math.trunc(num3(inputs, "lsmaOffset", 0))
25183
+ };
25184
+ const offset = Math.trunc(num3(inputs, "offset", 0));
25185
+ const displace = (v) => offset === 0 ? v : shift(v, offset);
24709
25186
  const kind = str3(inputs, "maType", "SMA");
24710
- let values;
24711
- switch (kind) {
24712
- case "EMA":
24713
- values = ema(src, len);
24714
- break;
24715
- case "WMA":
24716
- values = wma(src, len);
24717
- break;
24718
- case "RMA":
24719
- values = rma(src, len);
24720
- break;
24721
- case "VWMA":
24722
- values = vwma(src, volumes(bars), len);
24723
- break;
24724
- default:
24725
- values = sma(src, len);
25187
+ const ma1 = displace(movingAverageOf(kind, sourceValues(bars, str3(inputs, "source", "Close")), bars, num3(inputs, "length", 20), opts));
25188
+ const plots = [{ key: "ma", title: kind, values: ma1, color: str3(inputs, "color", INFO), width: 2 }];
25189
+ if (bool(inputs, "showMa2", false)) {
25190
+ const kind2 = str3(inputs, "ma2Type", "SMA");
25191
+ const ma2 = displace(movingAverageOf(kind2, sourceValues(bars, str3(inputs, "ma2Source", "Close")), bars, num3(inputs, "ma2Length", 50), opts));
25192
+ plots.push({ key: "ma2", title: `${kind2} 2`, values: ma2, color: str3(inputs, "ma2Color", WARNING), width: 2 });
24726
25193
  }
24727
- const offset = Math.trunc(num3(inputs, "offset", 0));
24728
- if (offset !== 0) values = shift(values, offset);
24729
- return { plots: [{ key: "ma", title: kind, values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
25194
+ return { plots };
24730
25195
  }
24731
25196
  };
24732
25197
  var smoothedMa = {
@@ -24734,17 +25199,13 @@ var smoothedMa = {
24734
25199
  title: "Smoothed Moving Average",
24735
25200
  shortTitle: "RMA",
24736
25201
  overlay: true,
24737
- inputs: [lengthInput(14), sourceInput(), colorInput()],
25202
+ inputs: [
25203
+ sourceInput("Close", "source", "Source", "Price series the average smooths. Wilder computed his 1978 indicators on the close, but the recursion accepts any source."),
25204
+ lengthInput(14, "length", "RMA Length", 5e3, "Number of bars N in Wilder's smoothing: the line keeps (N \u2212 1)/N of its previous value and blends in 1/N of the new price. That alpha is smaller than an EMA's, so an N-period RMA responds like a (2N \u2212 1)-period EMA."),
25205
+ ...directionInputs()
25206
+ ],
24738
25207
  compute: (bars, inputs) => ({
24739
- plots: [
24740
- {
24741
- key: "rma",
24742
- title: "RMA",
24743
- values: rma(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
24744
- color: str3(inputs, "color", SERIES_LINE),
24745
- width: 2
24746
- }
24747
- ]
25208
+ plots: [slopeColoredPlot("rma", "RMA", rma(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)), inputs)]
24748
25209
  })
24749
25210
  };
24750
25211
  var zlema = {
@@ -24752,61 +25213,86 @@ var zlema = {
24752
25213
  title: "Zero-Lag Exponential Moving Average",
24753
25214
  shortTitle: "ZLEMA",
24754
25215
  overlay: true,
24755
- inputs: [lengthInput(14), sourceInput(), colorInput()],
25216
+ inputs: [
25217
+ sourceInput("Close", "source", "Source", "Price series the average de-lags and then smooths."),
25218
+ lengthInput(21, "length", "ZLEMA Length", 5e3, "EMA length N. It sets both the smoothing (alpha = 2 / (N + 1)) and the correction: an EMA of length N lags by about (N \u2212 1)/2 bars, and the de-lagging step is sized to cancel exactly that."),
25219
+ ...directionInputs()
25220
+ ],
24756
25221
  compute: (bars, inputs) => {
24757
- const len = num3(inputs, "length", 14);
25222
+ const len = num3(inputs, "length", 21);
24758
25223
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
24759
- const lag = Math.floor((len - 1) / 2);
24760
- const lagged = shift(src, lag);
24761
- const delagged = zip(src, lagged, (a, b) => 2 * a - b);
24762
- return { plots: [{ key: "zlema", title: "ZLEMA", values: ema(delagged, len), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
25224
+ const lag = Math.round((len - 1) / 2);
25225
+ const delagged = zip(src, shift(src, lag), (a, b) => 2 * a - b);
25226
+ return { plots: [slopeColoredPlot("zlema", "ZLEMA", ema(delagged, len), inputs)] };
24763
25227
  }
24764
25228
  };
25229
+ var MOMENTUM_SCALER = "Momentum (|CMO|)";
25230
+ var VOLATILITY_SCALER = "Volatility (StDev Ratio)";
25231
+ var REGIME = "Trend Regime";
24765
25232
  var vidya = {
24766
25233
  type: "vidya",
24767
25234
  title: "Variable Index Dynamic Average",
24768
25235
  shortTitle: "VIDYA",
24769
25236
  overlay: true,
24770
- inputs: [lengthInput(14), { key: "cmoLength", title: "Momentum length", type: "int", defval: 9, min: 1, max: 500, step: 1 }, sourceInput(), colorInput()],
25237
+ inputs: [
25238
+ sourceInput("Close", "source", "Source", "Price series the average smooths. VIDYA is classically computed on the close."),
25239
+ lengthInput(12, "length", "VIDYA Length", 5e3, "EMA-equivalent length N of the base smoothing constant, alpha = 2 / (N + 1). The adaptive scaler rescales this base speed bar by bar."),
25240
+ optionInput("scaler", "Adaptive Scaler", MOMENTUM_SCALER, [MOMENTUM_SCALER, VOLATILITY_SCALER], "Bar-by-bar measure that rescales the smoothing constant. Momentum divides the absolute Chande Momentum Oscillator by 100; Volatility is Chande's original short-term over reference standard deviation."),
25241
+ intInput("scalerLength", "Scaler Lookback", 9, 1, 5e3, "Lookback of the adaptive scaler \u2014 the CMO period in momentum mode, or the short-term standard deviation window in volatility mode."),
25242
+ intInput("refLength", "Reference StDev Length", 30, 2, 5e3, "Volatility mode only \u2014 window of the reference standard deviation the short-term reading is divided by."),
25243
+ { ...floatInput("flatThreshold", "Flat Threshold", 0.05, 0, 10, 0.01, "One-bar change of VIDYA, as a fraction of ATR, below which the line reads as flat."), group: REGIME },
25244
+ { ...intInput("atrLength", "ATR Normalization Length", 14, 1, 5e3, "ATR window used to normalize VIDYA's one-bar slope, so the flat threshold behaves consistently across symbols and timeframes."), group: REGIME },
25245
+ boolInput("trendColor", "Trend Regime Coloring", true, STYLE, "Colors VIDYA by its regime read \u2014 bullish while sloping up beyond the flat threshold, bearish while sloping down, flat inside congestion."),
25246
+ boolInput("adaptiveGradient", "Adaptive Gradient", true, STYLE, "Blends the trend color toward the flat color according to the adaptive scaler, so the line pales as momentum dies."),
25247
+ colorInput(BULLISH, "Bullish", "bullColor"),
25248
+ colorInput(BEARISH, "Bearish", "bearColor"),
25249
+ colorInput(NEUTRAL, "Flat", "flatColor"),
25250
+ colorInput(NEUTRAL, "Neutral", "neutralColor"),
25251
+ widthInput()
25252
+ ],
24771
25253
  compute: (bars, inputs) => {
24772
- const len = num3(inputs, "length", 14);
24773
- const cmoLen = num3(inputs, "cmoLength", 9);
25254
+ const len = num3(inputs, "length", 12);
24774
25255
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
24775
- const d = change(src);
24776
- const su = new Array(src.length).fill(Number.NaN);
24777
- const sd = new Array(src.length).fill(Number.NaN);
24778
- for (let i = cmoLen; i < src.length; i++) {
24779
- let u = 0;
24780
- let w = 0;
24781
- let ok = true;
24782
- for (let k = i - cmoLen + 1; k <= i; k++) {
24783
- const x = d[k];
24784
- if (!Number.isFinite(x)) {
24785
- ok = false;
24786
- break;
24787
- }
24788
- if (x > 0) u += x;
24789
- else w -= x;
24790
- }
24791
- if (ok) {
24792
- su[i] = u;
24793
- sd[i] = w;
24794
- }
24795
- }
24796
- const alpha = 2 / (len + 1);
25256
+ const scalerLen = num3(inputs, "scalerLength", 9);
25257
+ const momentum = map(cmo(src, scalerLen), (x) => Math.abs(x) / 100);
25258
+ const shortDev = stdev(src, scalerLen);
25259
+ const refDev = stdev(src, num3(inputs, "refLength", 30));
25260
+ const volatility = zip(shortDev, refDev, (s, r) => r > 0 ? s / r : 0);
25261
+ const scale = str3(inputs, "scaler", MOMENTUM_SCALER) === VOLATILITY_SCALER ? volatility : momentum;
25262
+ const alphaBase = 2 / (len + 1);
24797
25263
  const out = new Array(src.length).fill(Number.NaN);
24798
25264
  let prev2 = Number.NaN;
24799
25265
  for (let i = 0; i < src.length; i++) {
24800
25266
  const x = src[i];
24801
- const u = su[i];
24802
- const w = sd[i];
24803
- if (!Number.isFinite(x) || !Number.isFinite(u) || !Number.isFinite(w)) continue;
24804
- const total = u + w;
24805
- const k = total === 0 ? 0 : Math.abs((u - w) / total);
24806
- prev2 = Number.isFinite(prev2) ? alpha * k * x + (1 - alpha * k) * prev2 : x;
25267
+ if (!Number.isFinite(x)) continue;
25268
+ const k = scale[i];
25269
+ if (!Number.isFinite(prev2) || !Number.isFinite(k)) prev2 = x;
25270
+ else {
25271
+ const alpha = Math.min(alphaBase * k, 1);
25272
+ prev2 = alpha * x + (1 - alpha) * prev2;
25273
+ }
24807
25274
  out[i] = prev2;
24808
25275
  }
24809
- return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
25276
+ const neutral = str3(inputs, "neutralColor", NEUTRAL);
25277
+ if (!bool(inputs, "trendColor", true)) {
25278
+ return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: neutral, width: num3(inputs, "lineWidth", 2) }] };
25279
+ }
25280
+ const bull = str3(inputs, "bullColor", BULLISH);
25281
+ const bear = str3(inputs, "bearColor", BEARISH);
25282
+ const flat = str3(inputs, "flatColor", NEUTRAL);
25283
+ const threshold = num3(inputs, "flatThreshold", 0.05);
25284
+ const range = atr(bars, num3(inputs, "atrLength", 14));
25285
+ const fade = bool(inputs, "adaptiveGradient", true);
25286
+ const colors = out.map((x, i) => {
25287
+ if (!Number.isFinite(x)) return null;
25288
+ const r = range[i];
25289
+ const slope = i > 0 && Number.isFinite(out[i - 1]) ? x - out[i - 1] : 0;
25290
+ const normalized = Number.isFinite(r) && r !== 0 ? slope / r : 0;
25291
+ const ink = normalized > threshold ? bull : normalized < -threshold ? bear : null;
25292
+ if (ink == null) return flat;
25293
+ return fade ? gradient(Math.min(scale[i] ?? 0, 1), 0, 1, flat, ink) : ink;
25294
+ });
25295
+ return { plots: [{ key: "vidya", title: "VIDYA", values: out, color: flat, width: num3(inputs, "lineWidth", 2), colors }] };
24810
25296
  }
24811
25297
  };
24812
25298
  var maEnvelope = {
@@ -24815,115 +25301,218 @@ var maEnvelope = {
24815
25301
  shortTitle: "MA Env",
24816
25302
  overlay: true,
24817
25303
  inputs: [
24818
- { key: "maType", title: "Type", type: "string", defval: "SMA", options: ["SMA", "EMA"] },
24819
- lengthInput(20),
24820
- sourceInput(),
24821
- { key: "percent", title: "Percent", type: "float", defval: 2.5, min: 0.01, max: 50, step: 0.1 },
24822
- colorInput()
25304
+ optionInput("maType", "MA Type", "SMA", ENVELOPE_MA_TYPES, "Moving average type used as the envelope basis."),
25305
+ { ...lengthInput(20, "length", "Length", 5e3, "Number of bars in the basis moving average."), min: 2 },
25306
+ sourceInput("Close", "source", "Source", "Price series the basis moving average is calculated on."),
25307
+ floatInput("percent", "Percent", 2.5, 0, 50, 0.1, "Envelope offset as a percentage of the basis. Upper = basis \xD7 (1 + percent / 100), lower = basis \xD7 (1 \u2212 percent / 100)."),
25308
+ ...channelStyleInputs()
24823
25309
  ],
24824
25310
  compute: (bars, inputs) => {
24825
- const len = num3(inputs, "length", 20);
24826
25311
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
24827
- const basis = str3(inputs, "maType", "SMA") === "EMA" ? ema(src, len) : sma(src, len);
25312
+ const basis = movingAverageOf(str3(inputs, "maType", "SMA"), src, bars, num3(inputs, "length", 20));
24828
25313
  const k = num3(inputs, "percent", 2.5) / 100;
24829
- const ink = str3(inputs, "color", SERIES_LINE);
24830
- return {
24831
- plots: [
24832
- { key: "basis", title: "Basis", values: basis, color: ink, width: 2 },
24833
- { key: "upper", title: "Upper", values: map(basis, (x) => x * (1 + k)), color: ink },
24834
- { key: "lower", title: "Lower", values: map(basis, (x) => x * (1 - k)), color: ink }
24835
- ],
24836
- bands: [{ key: "envelope", from: "upper", to: "lower", color: withAlpha2(ink) }]
24837
- };
25314
+ const ink = { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "basisColor", NEUTRAL) };
25315
+ return directionalChannel(bars, basis, map(basis, (x) => x * (1 + k)), map(basis, (x) => x * (1 - k)), ink, "envelope");
24838
25316
  }
24839
25317
  };
25318
+ var LINREG_CURVE = "Curve";
25319
+ var LINREG_SLOPE = "Slope";
24840
25320
  var linearRegression = {
24841
25321
  type: "linear-regression",
24842
25322
  title: "Linear Regression Curve",
24843
25323
  shortTitle: "LinReg",
24844
- overlay: true,
24845
- inputs: [lengthInput(14), sourceInput(), colorInput()],
24846
- compute: (bars, inputs) => ({
24847
- plots: [
24848
- {
24849
- key: "linreg",
24850
- title: "LinReg",
24851
- values: linreg(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
24852
- color: str3(inputs, "color", SERIES_LINE),
24853
- width: 2
24854
- }
24855
- ]
24856
- })
25324
+ // Slope lives in its own pane; Curve force-overlays itself onto the price candles.
25325
+ overlay: false,
25326
+ inputs: [
25327
+ optionInput("mode", "Mode", LINREG_CURVE, [LINREG_CURVE, LINREG_SLOPE], "Curve plots the regression value on the price chart; Slope plots the per-bar slope of the fit in the pane."),
25328
+ { ...lengthInput(100, "length", "Length", 5e3, "Number of bars used to fit the linear regression."), min: 2 },
25329
+ sourceInput("Close", "source", "Source", "Price series the regression is fitted to."),
25330
+ colorInput(BULLISH, "Rising", "bullColor", "Color of the regression curve and slope line while the fit slopes upward."),
25331
+ colorInput(BEARISH, "Falling", "bearColor", "Color of the regression curve and slope line while the fit slopes downward."),
25332
+ colorInput(NEUTRAL, "Regression Line", "lineColor", "Color of the regression curve and slope line while the fit is flat.")
25333
+ ],
25334
+ compute: (bars, inputs) => {
25335
+ const len = num3(inputs, "length", 100);
25336
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
25337
+ const curve = linreg(src, len, 0);
25338
+ const slope = zip(curve, linreg(src, len, 1), (a, b) => a - b);
25339
+ const bull = str3(inputs, "bullColor", BULLISH);
25340
+ const bear = str3(inputs, "bearColor", BEARISH);
25341
+ const flat = str3(inputs, "lineColor", NEUTRAL);
25342
+ const colors = slope.map((s) => Number.isFinite(s) ? s > 0 ? bull : s < 0 ? bear : flat : null);
25343
+ if (str3(inputs, "mode", LINREG_CURVE) === LINREG_SLOPE) {
25344
+ return {
25345
+ plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
25346
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
25347
+ };
25348
+ }
25349
+ return { plots: [{ key: "curve", title: "Curve", values: curve, color: flat, width: 2, colors, overlay: true }] };
25350
+ }
24857
25351
  };
24858
25352
  var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope, linearRegression];
24859
25353
 
24860
25354
  // src/core/native-indicators/classics/bands.ts
25355
+ var JAW_COLOR = "#ab47bc";
25356
+ var TEETH_COLOR = WARNING;
25357
+ var LIPS_COLOR = "#ffd700";
24861
25358
  function bollinger(bars, inputs) {
24862
25359
  const len = num3(inputs, "length", 20);
24863
25360
  const mult = num3(inputs, "mult", 2);
24864
25361
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
24865
25362
  const basis = sma(src, len);
24866
25363
  const dev = map(stdev(src, len), (x) => x * mult);
24867
- return { basis, upper: zip(basis, dev, (b, d) => b + d), lower: zip(basis, dev, (b, d) => b - d) };
25364
+ return { src, basis, upper: zip(basis, dev, (b, d) => b + d), lower: zip(basis, dev, (b, d) => b - d) };
25365
+ }
25366
+ function bollingerInputs(multMin, multStep) {
25367
+ return [
25368
+ { ...lengthInput(20, "length", "Length", 5e3, "Number of bars used for the Bollinger Bands basis (a simple moving average) and for the standard deviation."), min: 2 },
25369
+ sourceInput("Close", "source", "Source", "Price series the bands are computed from."),
25370
+ floatInput("mult", "Multiplier", 2, multMin, 50, multStep, "Standard deviation multiplier for the upper and lower bands.")
25371
+ ];
24868
25372
  }
24869
- var BB_INPUTS = [lengthInput(20), sourceInput(), { key: "mult", title: "StdDev", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 }];
24870
25373
  var bollingerBands = {
24871
25374
  type: "bollinger-bands",
24872
25375
  title: "Bollinger Bands",
24873
25376
  shortTitle: "BB",
24874
25377
  overlay: true,
24875
- inputs: [...BB_INPUTS, colorInput()],
25378
+ inputs: [
25379
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used to calculate the basis SMA and the standard deviation."),
25380
+ sourceInput("Close", "source", "Source", "Price series the bands are calculated on."),
25381
+ floatInput("mult", "Multiplier", 2, 0, 50, 0.1, "Number of standard deviations added above and below the basis to form the bands."),
25382
+ colorInput(WARNING, "Basis", "basisColor", "Color of the basis (middle band) moving average."),
25383
+ colorInput(INFO, "Bands", "color", "Color of the upper and lower bands."),
25384
+ colorInput(transp(INFO, 92), "Fill", "fillColor", "Fill color of the area between the upper and lower bands.")
25385
+ ],
24876
25386
  compute: (bars, inputs) => {
24877
25387
  const { basis, upper, lower } = bollinger(bars, inputs);
24878
- const ink = str3(inputs, "color", SERIES_LINE);
25388
+ const ink = str3(inputs, "color", INFO);
24879
25389
  return {
24880
25390
  plots: [
24881
- { key: "basis", title: "Basis", values: basis, color: WARNING },
25391
+ { key: "basis", title: "Basis", values: basis, color: str3(inputs, "basisColor", WARNING) },
24882
25392
  { key: "upper", title: "Upper", values: upper, color: ink },
24883
25393
  { key: "lower", title: "Lower", values: lower, color: ink }
24884
25394
  ],
24885
- bands: [{ key: "bb", from: "upper", to: "lower", color: withAlpha2(ink) }]
25395
+ bands: [{ key: "bb", from: "upper", to: "lower", color: str3(inputs, "fillColor", transp(INFO, 92)) }]
24886
25396
  };
24887
25397
  }
24888
25398
  };
25399
+ var PERCENT_B_MID = 0.5;
25400
+ var THRESHOLDS = "Thresholds";
24889
25401
  var percentB = {
24890
25402
  type: "percent-b",
24891
25403
  title: "Bollinger Bands %B",
24892
25404
  shortTitle: "%B",
24893
25405
  overlay: false,
24894
- inputs: [...BB_INPUTS, colorInput()],
25406
+ inputs: [
25407
+ ...bollingerInputs(1e-3, 0.25),
25408
+ { ...floatInput("overbought", "Overbought", 1, -10, 10, 0.05, "Level treated as overbought. 1 is the upper band itself, so readings above the default mean price sits outside the bands."), group: THRESHOLDS },
25409
+ { ...floatInput("oversold", "Oversold", 0, -10, 10, 0.05, "Level treated as oversold. 0 is the lower band itself, so readings below the default mean price sits outside the bands."), group: THRESHOLDS },
25410
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of %B above the 0.5 midline, where price trades in the upper half of the bands."),
25411
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of %B below the 0.5 midline, where price trades in the lower half of the bands."),
25412
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Vertical gradient between %B and the 0.5 midline, fully transparent at the midline and more opaque at the oscillator value."),
25413
+ colorInput(NEUTRAL, "Levels Color", "levelsColor", "Color of the dashed overbought/oversold levels and of the dotted midline.")
25414
+ ],
24895
25415
  compute: (bars, inputs) => {
24896
- const { upper, lower } = bollinger(bars, inputs);
24897
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
25416
+ const { src, upper, lower } = bollinger(bars, inputs);
24898
25417
  const values = src.map((x, i) => {
24899
25418
  const u = upper[i];
24900
25419
  const l = lower[i];
24901
25420
  return Number.isFinite(u) && Number.isFinite(l) && u !== l ? (x - l) / (u - l) : Number.NaN;
24902
25421
  });
25422
+ const bull = str3(inputs, "bullColor", BULLISH);
25423
+ const bear = str3(inputs, "bearColor", BEARISH);
25424
+ const levels = str3(inputs, "levelsColor", NEUTRAL);
25425
+ const colors = values.map((x) => Number.isFinite(x) ? x > PERCENT_B_MID ? bull : bear : null);
25426
+ const plots = [
25427
+ { key: "pb", title: "%B", values, color: bear, colors },
25428
+ { key: "mid", title: "Midline", values: values.map((x) => Number.isFinite(x) ? PERCENT_B_MID : Number.NaN), color: levels, display: { pane: false, legend: false, dataWindow: false } }
25429
+ ];
25430
+ const bands = bool(inputs, "gradientFill", true) ? [
25431
+ {
25432
+ key: "pbFill",
25433
+ from: "pb",
25434
+ to: "mid",
25435
+ color: transp(NEUTRAL, 100),
25436
+ // Opaque at the oscillator, invisible where it meets the midline.
25437
+ gradient: values.map((x) => {
25438
+ if (!Number.isFinite(x)) return null;
25439
+ const up = x > PERCENT_B_MID;
25440
+ const ink = up ? bull : bear;
25441
+ return {
25442
+ topValue: Math.max(x, PERCENT_B_MID),
25443
+ bottomValue: Math.min(x, PERCENT_B_MID),
25444
+ topColor: up ? transp(ink, 50) : transp(ink, 100),
25445
+ bottomColor: up ? transp(ink, 100) : transp(ink, 50)
25446
+ };
25447
+ })
25448
+ }
25449
+ ] : [];
24903
25450
  return {
24904
- plots: [{ key: "pb", title: "%B", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25451
+ plots,
25452
+ bands,
24905
25453
  levels: [
24906
- { key: "upper", price: 1, color: NEUTRAL },
24907
- { key: "middle", price: 0.5, color: NEUTRAL, lineStyle: "dotted" },
24908
- { key: "lower", price: 0, color: NEUTRAL }
25454
+ { key: "overbought", price: num3(inputs, "overbought", 1), color: levels, lineStyle: "dashed", title: "Overbought" },
25455
+ { key: "oversold", price: num3(inputs, "oversold", 0), color: levels, lineStyle: "dashed", title: "Oversold" },
25456
+ { key: "midline", price: PERCENT_B_MID, color: transp(levels, 50), lineStyle: "dotted", title: "Midline" }
24909
25457
  ]
24910
25458
  };
24911
25459
  }
24912
25460
  };
25461
+ var SQUEEZE_BULGE = "Squeeze & Bulge";
24913
25462
  var bandwidth = {
24914
25463
  type: "bandwidth",
24915
25464
  title: "Bollinger Bands Width",
24916
25465
  shortTitle: "BandWidth",
24917
25466
  overlay: false,
24918
- inputs: [...BB_INPUTS, colorInput()],
25467
+ inputs: [
25468
+ ...bollingerInputs(1e-3, 0.25),
25469
+ { ...intInput("lookback", "Reference Lookback", 125, 2, 5e3, "Window used to locate the lowest (Squeeze) and highest (Bulge) BandWidth references."), group: SQUEEZE_BULGE },
25470
+ boolInput("showSqueeze", "Show Squeeze Level", true, SQUEEZE_BULGE, "Plots the lowest BandWidth of the reference lookback \u2014 unusually compressed volatility, a condition rather than a directional signal."),
25471
+ boolInput("showBulge", "Show Bulge Level", true, SQUEEZE_BULGE, "Plots the highest BandWidth of the reference lookback \u2014 extreme width that is unlikely to be sustained."),
25472
+ colorInput(NEUTRAL, "BandWidth", "color", "Color of the BandWidth line. At its Squeeze reference the line takes the Squeeze color, at its Bulge reference the Bulge color."),
25473
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between zero and BandWidth with a vertical gradient, fully transparent at zero."),
25474
+ colorInput(WARNING, "Squeeze Level", "squeezeColor", "Color of the lowest-BandWidth reference line."),
25475
+ colorInput(BEARISH, "Bulge Level", "bulgeColor", "Color of the highest-BandWidth reference line.")
25476
+ ],
24919
25477
  compute: (bars, inputs) => {
24920
25478
  const { basis, upper, lower } = bollinger(bars, inputs);
24921
25479
  const values = basis.map((b, i) => {
24922
25480
  const u = upper[i];
24923
25481
  const l = lower[i];
24924
- return Number.isFinite(b) && b !== 0 && Number.isFinite(u) && Number.isFinite(l) ? (u - l) / b * 100 : Number.NaN;
25482
+ return Number.isFinite(b) && b !== 0 && Number.isFinite(u) && Number.isFinite(l) ? (u - l) / b : Number.NaN;
25483
+ });
25484
+ const lookback = num3(inputs, "lookback", 125);
25485
+ const squeezeLevel = lowest(values, lookback);
25486
+ const bulgeLevel = highest(values, lookback);
25487
+ const base = str3(inputs, "color", NEUTRAL);
25488
+ const squeezeInk = str3(inputs, "squeezeColor", WARNING);
25489
+ const bulgeInk = str3(inputs, "bulgeColor", BEARISH);
25490
+ const colors = values.map((x, i) => {
25491
+ if (!Number.isFinite(x)) return null;
25492
+ if (x <= squeezeLevel[i]) return squeezeInk;
25493
+ if (x >= bulgeLevel[i]) return bulgeInk;
25494
+ return base;
24925
25495
  });
24926
- return { plots: [{ key: "bbw", title: "BBW", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
25496
+ const plots = [
25497
+ { key: "bbw", title: "BandWidth", values, color: base, colors },
25498
+ { key: "zero", title: "Zero Line", values: values.map((x) => Number.isFinite(x) ? 0 : Number.NaN), color: transp(NEUTRAL, 90) }
25499
+ ];
25500
+ if (bool(inputs, "showSqueeze", true)) plots.push({ key: "squeeze", title: "Squeeze Level", values: squeezeLevel, color: squeezeInk });
25501
+ if (bool(inputs, "showBulge", true)) plots.push({ key: "bulge", title: "Bulge Level", values: bulgeLevel, color: bulgeInk });
25502
+ const bands = bool(inputs, "gradientFill", true) ? [
25503
+ {
25504
+ key: "bbwFill",
25505
+ from: "bbw",
25506
+ to: "zero",
25507
+ color: transp(NEUTRAL, 100),
25508
+ gradient: values.map((x, i) => {
25509
+ if (!Number.isFinite(x)) return null;
25510
+ const ink = colors[i] ?? base;
25511
+ return { topValue: x, bottomValue: 0, topColor: transp(ink, 50), bottomColor: transp(ink, 100) };
25512
+ })
25513
+ }
25514
+ ] : [];
25515
+ return { plots, bands };
24927
25516
  }
24928
25517
  };
24929
25518
  var keltner = {
@@ -24932,26 +25521,17 @@ var keltner = {
24932
25521
  shortTitle: "KC",
24933
25522
  overlay: true,
24934
25523
  inputs: [
24935
- lengthInput(20),
24936
- { key: "mult", title: "Multiplier", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 },
24937
- { key: "atrLength", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
24938
- sourceInput(),
24939
- colorInput()
25524
+ lengthInput(20, "length", "EMA Length", 5e3, "Number of bars used to compute the exponential moving average basis line."),
25525
+ intInput("atrLength", "ATR Length", 10, 1, 5e3, "Number of bars used to compute the Average True Range (Wilder smoothing)."),
25526
+ floatInput("mult", "Multiplier", 2, 0, 50, 0.1, "ATR multiple used to offset the upper and lower bands from the basis line."),
25527
+ sourceInput("Close", "source", "Source", "Price series used to compute the basis line."),
25528
+ ...channelStyleInputs("Basis Color")
24940
25529
  ],
24941
25530
  compute: (bars, inputs) => {
24942
- const len = num3(inputs, "length", 20);
24943
- const mult = num3(inputs, "mult", 2);
24944
- const basis = ema(sourceValues(bars, str3(inputs, "source", "Close")), len);
24945
- const range = map(atr(bars, num3(inputs, "atrLength", 10)), (x) => x * mult);
24946
- const ink = str3(inputs, "color", SERIES_LINE);
24947
- return {
24948
- plots: [
24949
- { key: "basis", title: "Basis", values: basis, color: WARNING },
24950
- { key: "upper", title: "Upper", values: zip(basis, range, (b, r) => b + r), color: ink },
24951
- { key: "lower", title: "Lower", values: zip(basis, range, (b, r) => b - r), color: ink }
24952
- ],
24953
- bands: [{ key: "kc", from: "upper", to: "lower", color: withAlpha2(ink) }]
24954
- };
25531
+ const basis = ema(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20));
25532
+ const range = map(atr(bars, num3(inputs, "atrLength", 10)), (x) => x * num3(inputs, "mult", 2));
25533
+ const ink = { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "basisColor", NEUTRAL) };
25534
+ return directionalChannel(bars, basis, zip(basis, range, (b, r) => b + r), zip(basis, range, (b, r) => b - r), ink, "kc");
24955
25535
  }
24956
25536
  };
24957
25537
  var donchian = {
@@ -24959,19 +25539,24 @@ var donchian = {
24959
25539
  title: "Donchian Channels",
24960
25540
  shortTitle: "DC",
24961
25541
  overlay: true,
24962
- inputs: [lengthInput(20), colorInput()],
25542
+ inputs: [
25543
+ lengthInput(20, "length", "Length", 5e3, "Lookback length. The upper and lower channels are the highest high and lowest low over this many bars."),
25544
+ colorInput(BEARISH, "Upper", "upperColor", "Color of the upper channel line."),
25545
+ colorInput(NEUTRAL, "Middle", "middleColor", "Color of the channel midline."),
25546
+ colorInput(BULLISH, "Lower", "lowerColor", "Color of the lower channel line."),
25547
+ colorInput(transp(NEUTRAL, 92), "Fill", "fillColor", "Color of the channel interior fill.")
25548
+ ],
24963
25549
  compute: (bars, inputs) => {
24964
25550
  const len = num3(inputs, "length", 20);
24965
25551
  const upper = highest(highs(bars), len);
24966
25552
  const lower = lowest(lows(bars), len);
24967
- const ink = str3(inputs, "color", SERIES_LINE);
24968
25553
  return {
24969
25554
  plots: [
24970
- { key: "basis", title: "Basis", values: zip(upper, lower, (u, l) => (u + l) / 2), color: WARNING },
24971
- { key: "upper", title: "Upper", values: upper, color: ink },
24972
- { key: "lower", title: "Lower", values: lower, color: ink }
25555
+ { key: "upper", title: "Upper", values: upper, color: str3(inputs, "upperColor", BEARISH) },
25556
+ { key: "middle", title: "Middle", kind: "circles", values: zip(upper, lower, (u, l) => (u + l) / 2), color: str3(inputs, "middleColor", NEUTRAL) },
25557
+ { key: "lower", title: "Lower", values: lower, color: str3(inputs, "lowerColor", BULLISH) }
24973
25558
  ],
24974
- bands: [{ key: "dc", from: "upper", to: "lower", color: withAlpha2(ink) }]
25559
+ bands: [{ key: "dc", from: "upper", to: "lower", color: str3(inputs, "fillColor", transp(NEUTRAL, 92)) }]
24975
25560
  };
24976
25561
  }
24977
25562
  };
@@ -24980,16 +25565,37 @@ var chandelier = {
24980
25565
  title: "Chandelier Exit",
24981
25566
  shortTitle: "CE",
24982
25567
  overlay: true,
24983
- inputs: [lengthInput(22), { key: "mult", title: "ATR multiplier", type: "float", defval: 3, min: 0.1, max: 50, step: 0.1 }],
25568
+ inputs: [
25569
+ lengthInput(22, "length", "Length", 5e3, "Lookback used for both the highest high / lowest low extremes and the ATR."),
25570
+ floatInput("mult", "ATR Multiplier", 3, 0, 50, 0.1, "Multiple of ATR subtracted from the highest high (long stop) and added to the lowest low (short stop)."),
25571
+ boolInput("ratchet", "Ratchet Stops", false, void 0, "Prevents the long stop from falling and the short stop from rising, resetting once close crosses through the stop."),
25572
+ colorInput(BULLISH, "Long Stop", "longColor", "Color of the long trailing stop line."),
25573
+ colorInput(BEARISH, "Short Stop", "shortColor", "Color of the short trailing stop line.")
25574
+ ],
24984
25575
  compute: (bars, inputs) => {
24985
25576
  const len = num3(inputs, "length", 22);
24986
25577
  const range = map(atr(bars, len), (x) => x * num3(inputs, "mult", 3));
24987
- const long = zip(highest(highs(bars), len), range, (h, r) => h - r);
24988
- const short = zip(lowest(lows(bars), len), range, (l, r) => l + r);
25578
+ const basicLong = zip(highest(highs(bars), len), range, (h, r) => h - r);
25579
+ const basicShort = zip(lowest(lows(bars), len), range, (l, r) => l + r);
25580
+ let long = basicLong;
25581
+ let short = basicShort;
25582
+ if (bool(inputs, "ratchet", false)) {
25583
+ long = new Array(bars.length).fill(Number.NaN);
25584
+ short = new Array(bars.length).fill(Number.NaN);
25585
+ for (let i = 0; i < bars.length; i++) {
25586
+ const bl = basicLong[i];
25587
+ const bs = basicShort[i];
25588
+ const prevClose = i > 0 ? bars[i - 1].close : bars[i].close;
25589
+ const prevLong = i > 0 && Number.isFinite(long[i - 1]) ? long[i - 1] : bl;
25590
+ const prevShort = i > 0 && Number.isFinite(short[i - 1]) ? short[i - 1] : bs;
25591
+ long[i] = Number.isFinite(bl) ? prevClose > prevLong ? Math.max(bl, prevLong) : bl : Number.NaN;
25592
+ short[i] = Number.isFinite(bs) ? prevClose < prevShort ? Math.min(bs, prevShort) : bs : Number.NaN;
25593
+ }
25594
+ }
24989
25595
  return {
24990
25596
  plots: [
24991
- { key: "long", title: "Long stop", values: long, color: BULLISH },
24992
- { key: "short", title: "Short stop", values: short, color: BEARISH }
25597
+ { key: "long", title: "Long Stop", values: long, color: str3(inputs, "longColor", BULLISH) },
25598
+ { key: "short", title: "Short Stop", values: short, color: str3(inputs, "shortColor", BEARISH) }
24993
25599
  ]
24994
25600
  };
24995
25601
  }
@@ -25000,21 +25606,22 @@ var chandeKroll = {
25000
25606
  shortTitle: "CKS",
25001
25607
  overlay: true,
25002
25608
  inputs: [
25003
- { key: "p", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
25004
- { key: "x", title: "ATR multiplier", type: "float", defval: 1, min: 0.1, max: 50, step: 0.1 },
25005
- { key: "q", title: "Stop length", type: "int", defval: 9, min: 1, max: 500, step: 1 }
25609
+ intInput("p", "ATR Length (P)", 10, 1, 5e3, "Lookback used for both the ATR and the highest high / lowest low price extremes."),
25610
+ floatInput("x", "ATR Multiplier (X)", 1, 0, 50, 0.1, "Multiplier applied to the ATR to offset the preliminary stops from the price extremes."),
25611
+ intInput("q", "Stop Length (Q)", 9, 1, 5e3, "Lookback over which the preliminary stops are extended into the final stop levels."),
25612
+ colorInput(BEARISH, "Stop Short", "shortColor", "Color of the protective stop line for short positions."),
25613
+ colorInput(BULLISH, "Stop Long", "longColor", "Color of the protective stop line for long positions.")
25006
25614
  ],
25007
25615
  compute: (bars, inputs) => {
25008
25616
  const p = num3(inputs, "p", 10);
25009
- const x = num3(inputs, "x", 1);
25010
25617
  const q = num3(inputs, "q", 9);
25011
- const range = map(atr(bars, p), (v) => v * x);
25618
+ const range = map(atr(bars, p), (v) => v * num3(inputs, "x", 1));
25012
25619
  const firstHigh = zip(highest(highs(bars), p), range, (h, r) => h - r);
25013
25620
  const firstLow = zip(lowest(lows(bars), p), range, (l, r) => l + r);
25014
25621
  return {
25015
25622
  plots: [
25016
- { key: "stopShort", title: "Stop short", values: highest(firstHigh, q), color: BEARISH },
25017
- { key: "stopLong", title: "Stop long", values: lowest(firstLow, q), color: BULLISH }
25623
+ { key: "stopShort", title: "Stop Short", values: highest(firstHigh, q), color: str3(inputs, "shortColor", BEARISH) },
25624
+ { key: "stopLong", title: "Stop Long", values: lowest(firstLow, q), color: str3(inputs, "longColor", BULLISH) }
25018
25625
  ]
25019
25626
  };
25020
25627
  }
@@ -25025,36 +25632,42 @@ var supertrend = {
25025
25632
  shortTitle: "SuperTrend",
25026
25633
  overlay: true,
25027
25634
  inputs: [
25028
- { key: "atrLength", title: "ATR length", type: "int", defval: 10, min: 1, max: 500, step: 1 },
25029
- { key: "mult", title: "Factor", type: "float", defval: 3, min: 0.1, max: 50, step: 0.1 }
25635
+ intInput("atrLength", "ATR Length", 10, 1, 5e3, "Number of bars used to compute the Wilder ATR."),
25636
+ floatInput("mult", "Factor", 3, 0, 50, 0.1, "ATR multiplier setting how far the trailing stop sits from the bar midpoint."),
25637
+ colorInput(BULLISH, "Uptrend Color", "upColor", "Line color while the trend is up."),
25638
+ colorInput(BEARISH, "Downtrend Color", "downColor", "Line color while the trend is down.")
25030
25639
  ],
25031
25640
  compute: (bars, inputs) => {
25032
- const len = num3(inputs, "atrLength", 10);
25033
25641
  const mult = num3(inputs, "mult", 3);
25034
- const range = atr(bars, len);
25642
+ const range = atr(bars, num3(inputs, "atrLength", 10));
25035
25643
  const n = bars.length;
25036
- const st = new Array(n).fill(Number.NaN);
25037
- const dir = new Array(n).fill(1);
25038
- let up = Number.NaN;
25039
- let dn = Number.NaN;
25040
- let trend = 1;
25644
+ const up = new Array(n).fill(Number.NaN);
25645
+ const down = new Array(n).fill(Number.NaN);
25646
+ let finalUpper = Number.NaN;
25647
+ let finalLower = Number.NaN;
25648
+ let trend = -1;
25041
25649
  for (let i = 0; i < n; i++) {
25042
25650
  const r = range[i];
25043
25651
  if (!Number.isFinite(r)) continue;
25044
25652
  const b = bars[i];
25045
25653
  const mid = (b.high + b.low) / 2;
25046
- const basicUp = mid - mult * r;
25047
- const basicDn = mid + mult * r;
25654
+ const basicUpper = mid + mult * r;
25655
+ const basicLower = mid - mult * r;
25656
+ const prevUpper = Number.isFinite(finalUpper) ? finalUpper : basicUpper;
25657
+ const prevLower = Number.isFinite(finalLower) ? finalLower : basicLower;
25048
25658
  const prevClose = i > 0 ? bars[i - 1].close : b.close;
25049
- up = Number.isFinite(up) && prevClose > up ? Math.max(basicUp, up) : basicUp;
25050
- dn = Number.isFinite(dn) && prevClose < dn ? Math.min(basicDn, dn) : basicDn;
25051
- if (trend === 1 && b.close < up) trend = -1;
25052
- else if (trend === -1 && b.close > dn) trend = 1;
25053
- dir[i] = trend;
25054
- st[i] = trend === 1 ? up : dn;
25659
+ finalUpper = prevClose <= prevUpper ? Math.min(basicUpper, prevUpper) : basicUpper;
25660
+ finalLower = prevClose >= prevLower ? Math.max(basicLower, prevLower) : basicLower;
25661
+ trend = b.close > finalUpper ? 1 : b.close < finalLower ? -1 : trend;
25662
+ if (trend === 1) up[i] = finalLower;
25663
+ else down[i] = finalUpper;
25055
25664
  }
25056
- const colors = dir.map((d, i) => Number.isFinite(st[i]) ? d === 1 ? BULLISH : BEARISH : null);
25057
- return { plots: [{ key: "st", title: "SuperTrend", values: st, color: BULLISH, width: 2, colors }] };
25665
+ return {
25666
+ plots: [
25667
+ { key: "up", title: "Uptrend", values: up, color: str3(inputs, "upColor", BULLISH) },
25668
+ { key: "down", title: "Downtrend", values: down, color: str3(inputs, "downColor", BEARISH) }
25669
+ ]
25670
+ };
25058
25671
  }
25059
25672
  };
25060
25673
  var alligator = {
@@ -25063,12 +25676,15 @@ var alligator = {
25063
25676
  shortTitle: "Alligator",
25064
25677
  overlay: true,
25065
25678
  inputs: [
25066
- { key: "jawLength", title: "Jaw length", type: "int", defval: 13, min: 1, max: 500, step: 1 },
25067
- { key: "teethLength", title: "Teeth length", type: "int", defval: 8, min: 1, max: 500, step: 1 },
25068
- { key: "lipsLength", title: "Lips length", type: "int", defval: 5, min: 1, max: 500, step: 1 },
25069
- { key: "jawOffset", title: "Jaw offset", type: "int", defval: 8, min: 0, max: 100, step: 1 },
25070
- { key: "teethOffset", title: "Teeth offset", type: "int", defval: 5, min: 0, max: 100, step: 1 },
25071
- { key: "lipsOffset", title: "Lips offset", type: "int", defval: 3, min: 0, max: 100, step: 1 }
25679
+ intInput("jawLength", "Jaw Length", 13, 1, 5e3, "Smoothing length of the Jaw SMMA of median price (hl2), 13 in the classic setup."),
25680
+ intInput("teethLength", "Teeth Length", 8, 1, 5e3, "Smoothing length of the Teeth SMMA of median price (hl2), 8 in the classic setup."),
25681
+ intInput("lipsLength", "Lips Length", 5, 1, 5e3, "Smoothing length of the Lips SMMA of median price (hl2), 5 in the classic setup."),
25682
+ intInput("jawOffset", "Jaw Offset", 8, 0, 100, "Number of bars the Jaw plot is shifted into the future, 8 in the classic setup."),
25683
+ intInput("teethOffset", "Teeth Offset", 5, 0, 100, "Number of bars the Teeth plot is shifted into the future, 5 in the classic setup."),
25684
+ intInput("lipsOffset", "Lips Offset", 3, 0, 100, "Number of bars the Lips plot is shifted into the future, 3 in the classic setup."),
25685
+ colorInput(JAW_COLOR, "Jaw", "jawColor", "Color of the Jaw line."),
25686
+ colorInput(TEETH_COLOR, "Teeth", "teethColor", "Color of the Teeth line."),
25687
+ colorInput(LIPS_COLOR, "Lips", "lipsColor", "Color of the Lips line.")
25072
25688
  ],
25073
25689
  compute: (bars, inputs) => {
25074
25690
  const hl2 = sourceValues(bars, "HL2");
@@ -25077,99 +25693,134 @@ var alligator = {
25077
25693
  const lips = shift(rma(hl2, num3(inputs, "lipsLength", 5)), num3(inputs, "lipsOffset", 3));
25078
25694
  return {
25079
25695
  plots: [
25080
- { key: "jaw", title: "Jaw", values: jaw, color: SERIES_LINE },
25081
- { key: "teeth", title: "Teeth", values: teeth, color: BEARISH },
25082
- { key: "lips", title: "Lips", values: lips, color: BULLISH }
25696
+ { key: "jaw", title: "Jaw", values: jaw, color: str3(inputs, "jawColor", JAW_COLOR) },
25697
+ { key: "teeth", title: "Teeth", values: teeth, color: str3(inputs, "teethColor", TEETH_COLOR) },
25698
+ { key: "lips", title: "Lips", values: lips, color: str3(inputs, "lipsColor", LIPS_COLOR) }
25083
25699
  ]
25084
25700
  };
25085
25701
  }
25086
25702
  };
25703
+ var GATOR_LINES = "Alligator Lines";
25704
+ var SMMA_TYPE = "SMMA (Wilder)";
25087
25705
  var gator = {
25088
25706
  type: "gator-oscillator",
25089
25707
  title: "Gator Oscillator",
25090
25708
  shortTitle: "Gator Oscillator",
25091
25709
  overlay: false,
25092
- inputs: [],
25093
- compute: (bars) => {
25094
- const hl2 = sourceValues(bars, "HL2");
25095
- const jaw = shift(rma(hl2, 13), 8);
25096
- const teeth = shift(rma(hl2, 8), 5);
25097
- const lips = shift(rma(hl2, 5), 3);
25710
+ inputs: [
25711
+ { ...sourceInput("HL2", "source", "Source", "Price series fed to the three Alligator averages. Bill Williams' original recipe uses the median price (high + low) / 2."), group: GATOR_LINES },
25712
+ { ...optionInput("maType", "Smoothing", SMMA_TYPE, [SMMA_TYPE, "SMA", "EMA"], "Moving average applied to the source for the jaw, teeth and lips. The original uses the smoothed (Wilder-style) moving average."), group: GATOR_LINES },
25713
+ { ...intInput("jawLength", "Jaw Length", 13, 1, 500, "Averaging length of the jaw line."), group: GATOR_LINES },
25714
+ { ...intInput("jawOffset", "Jaw Offset", 8, 0, 100, "Bars the jaw is displaced forward, 8 in the original recipe. The upper histogram measures the absolute spread between the displaced jaw and teeth."), group: GATOR_LINES },
25715
+ { ...intInput("teethLength", "Teeth Length", 8, 1, 500, "Averaging length of the teeth line."), group: GATOR_LINES },
25716
+ { ...intInput("teethOffset", "Teeth Offset", 5, 0, 100, "Bars the teeth are displaced forward, 5 in the original recipe. The teeth line enters both spreads."), group: GATOR_LINES },
25717
+ { ...intInput("lipsLength", "Lips Length", 5, 1, 500, "Averaging length of the lips line."), group: GATOR_LINES },
25718
+ { ...intInput("lipsOffset", "Lips Offset", 3, 0, 100, "Bars the lips are displaced forward, 3 in the original recipe. The lower histogram measures the negative absolute spread between the displaced teeth and lips."), group: GATOR_LINES },
25719
+ colorInput(BULLISH, "Expanding", "growColor", "Column color while a spread grew against its own previous bar \u2014 the averages are spreading apart."),
25720
+ colorInput(BEARISH, "Contracting", "shrinkColor", "Column color while a spread shrank \u2014 the averages are converging. Color encodes expansion, not trade direction."),
25721
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero line separating the two histograms.")
25722
+ ],
25723
+ compute: (bars, inputs) => {
25724
+ const src = sourceValues(bars, str3(inputs, "source", "HL2"));
25725
+ const kind = str3(inputs, "maType", SMMA_TYPE);
25726
+ const smooth = (len) => kind === "SMA" ? sma(src, len) : kind === "EMA" ? ema(src, len) : rma(src, len);
25727
+ const jaw = shift(smooth(num3(inputs, "jawLength", 13)), num3(inputs, "jawOffset", 8));
25728
+ const teeth = shift(smooth(num3(inputs, "teethLength", 8)), num3(inputs, "teethOffset", 5));
25729
+ const lips = shift(smooth(num3(inputs, "lipsLength", 5)), num3(inputs, "lipsOffset", 3));
25098
25730
  const upper = map(sub(jaw, teeth), Math.abs);
25099
- const lower = map(sub(teeth, lips), (x) => -Math.abs(x));
25100
- const colorByExpansion = (v) => v.map((x, i) => {
25101
- if (!Number.isFinite(x)) return null;
25102
- const prev2 = i > 0 ? v[i - 1] : Number.NaN;
25103
- return Number.isFinite(prev2) && Math.abs(x) > Math.abs(prev2) ? BULLISH : BEARISH;
25104
- });
25731
+ const lower = map(sub(teeth, lips), Math.abs);
25732
+ const grow = str3(inputs, "growColor", BULLISH);
25733
+ const shrink = str3(inputs, "shrinkColor", BEARISH);
25734
+ const expansionColors2 = (v) => {
25735
+ let growing = false;
25736
+ return v.map((x, i) => {
25737
+ if (!Number.isFinite(x)) return null;
25738
+ const prev2 = i > 0 ? v[i - 1] : Number.NaN;
25739
+ if (Number.isFinite(prev2)) growing = x > prev2 ? true : x < prev2 ? false : growing;
25740
+ return growing ? grow : shrink;
25741
+ });
25742
+ };
25105
25743
  return {
25106
25744
  plots: [
25107
- { key: "upper", title: "Upper", values: upper, kind: "histogram", color: BULLISH, colors: colorByExpansion(upper), base: 0 },
25108
- { key: "lower", title: "Lower", values: lower, kind: "histogram", color: BEARISH, colors: colorByExpansion(lower), base: 0 }
25109
- ]
25745
+ { key: "upper", title: "Upper Histogram (Jaw - Teeth)", values: upper, kind: "columns", color: grow, colors: expansionColors2(upper), base: 0 },
25746
+ { key: "lower", title: "Lower Histogram (Teeth - Lips)", values: map(lower, (x) => -x), kind: "columns", color: shrink, colors: expansionColors2(lower), base: 0 }
25747
+ ],
25748
+ levels: [{ key: "zero", price: 0, color: str3(inputs, "zeroColor", NEUTRAL), lineStyle: "solid", title: "Zero Line" }]
25110
25749
  };
25111
25750
  }
25112
25751
  };
25113
25752
  var bandSpecs = [bollingerBands, percentB, bandwidth, keltner, donchian, chandelier, chandeKroll, supertrend, alligator, gator];
25114
25753
 
25115
25754
  // src/core/native-indicators/classics/oscillators.ts
25116
- function guideLevels(low, high, mid) {
25117
- const levels = [
25118
- { key: "upper", price: high, color: NEUTRAL },
25119
- { key: "lower", price: low, color: NEUTRAL }
25755
+ function thresholdInputs(obDefault, osDefault, min = 0, max = 100, obTitle = "Overbought Level", osTitle = "Oversold Level") {
25756
+ return [
25757
+ floatInput("overbought", obTitle, obDefault, min, max, 1, "Level above which the oscillator is considered overbought."),
25758
+ floatInput("oversold", osTitle, osDefault, min, max, 1, "Level below which the oscillator is considered oversold.")
25120
25759
  ];
25121
- if (mid != null) levels.splice(1, 0, { key: "middle", price: mid, color: NEUTRAL, lineStyle: "dotted" });
25122
- return levels;
25123
- }
25124
- var ZERO_LEVEL = [{ key: "zero", price: 0, color: NEUTRAL }];
25125
- function histColors(values) {
25126
- return values.map((x, i) => {
25127
- if (!Number.isFinite(x)) return null;
25128
- const prev2 = i > 0 ? values[i - 1] : Number.NaN;
25129
- const rising = Number.isFinite(prev2) ? x >= prev2 : true;
25130
- if (x >= 0) return rising ? BULLISH : `${BULLISH}80`;
25131
- return rising ? `${BEARISH}80` : BEARISH;
25132
- });
25133
25760
  }
25134
25761
  var rsiSpec = {
25135
25762
  type: "rsi",
25136
25763
  title: "Relative Strength Index",
25137
25764
  shortTitle: "RSI",
25138
25765
  overlay: false,
25139
- inputs: [lengthInput(14), sourceInput(), colorInput()],
25140
- compute: (bars, inputs) => ({
25141
- plots: [
25142
- {
25143
- key: "rsi",
25144
- title: "RSI",
25145
- values: rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
25146
- color: str3(inputs, "color", SERIES_LINE),
25147
- width: 2
25148
- }
25149
- ],
25150
- levels: guideLevels(30, 70, 50)
25766
+ inputs: [
25767
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used in the Wilder smoothing of average gains and losses."),
25768
+ sourceInput("Close", "source", "Source", "Price series the RSI is calculated on."),
25769
+ ...thresholdInputs(70, 30, 0, 100, "Overbought", "Oversold"),
25770
+ colorInput(NEUTRAL, "RSI", "color", "Color of the RSI line at the midline. Toward either threshold it blends into that threshold\u2019s color."),
25771
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level."),
25772
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
25773
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought, midline and oversold levels."),
25774
+ colorInput(transp(NEUTRAL, 90), "Band Fill", "fillColor", "Color of the fill between the overbought and oversold levels.")
25775
+ ],
25776
+ compute: (bars, inputs) => boundedOscillator({
25777
+ key: "rsi",
25778
+ title: "RSI",
25779
+ values: rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 14)),
25780
+ scale: { top: 100, bottom: 0 },
25781
+ overbought: num3(inputs, "overbought", 70),
25782
+ oversold: num3(inputs, "oversold", 30),
25783
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
25784
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
25785
+ width: 2,
25786
+ midline: 50,
25787
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 90))
25151
25788
  })
25152
25789
  };
25790
+ function stochasticOutput(k, d, inputs) {
25791
+ const overbought = num3(inputs, "overbought", 80);
25792
+ const oversold = num3(inputs, "oversold", 20);
25793
+ return {
25794
+ plots: [
25795
+ { key: "k", title: "%K", values: k, color: str3(inputs, "kColor", INFO) },
25796
+ { key: "d", title: "%D", values: d, color: str3(inputs, "dColor", WARNING) },
25797
+ anchorPlot("obAnchor", "Overbought Anchor", k.length, overbought),
25798
+ anchorPlot("osAnchor", "Oversold Anchor", k.length, oversold)
25799
+ ],
25800
+ bands: [{ key: "band", from: "obAnchor", to: "osAnchor", color: str3(inputs, "fillColor", transp(INFO, 90)) }],
25801
+ levels: [
25802
+ { key: "overbought", price: overbought, color: NEUTRAL, lineStyle: "dashed", title: "Overbought" },
25803
+ { key: "oversold", price: oversold, color: NEUTRAL, lineStyle: "dashed", title: "Oversold" }
25804
+ ]
25805
+ };
25806
+ }
25153
25807
  var stochastic = {
25154
25808
  type: "stochastic",
25155
25809
  title: "Stochastic",
25156
25810
  shortTitle: "Stoch",
25157
25811
  overlay: false,
25158
25812
  inputs: [
25159
- lengthInput(14, "kLength", "%K length"),
25160
- lengthInput(1, "kSmoothing", "%K smoothing", 500),
25161
- lengthInput(3, "dLength", "%D smoothing", 500)
25813
+ lengthInput(14, "kLength", "%K Length", 5e3, "Number of bars used for the highest high / lowest low range of the raw %K."),
25814
+ lengthInput(3, "kSmoothing", "%K Smoothing", 500, "Simple moving average length applied to the raw %K. 1 gives the fast stochastic, 3 the classic slow stochastic."),
25815
+ lengthInput(3, "dLength", "%D Smoothing", 500, "Simple moving average length of the %D signal line, applied to the smoothed %K."),
25816
+ ...thresholdInputs(80, 20),
25817
+ colorInput(INFO, "%K", "kColor", "Color of the %K line."),
25818
+ colorInput(WARNING, "%D", "dColor", "Color of the %D signal line."),
25819
+ colorInput(transp(INFO, 90), "Band Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
25162
25820
  ],
25163
25821
  compute: (bars, inputs) => {
25164
- const k = sma(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "kLength", 14)), num3(inputs, "kSmoothing", 1));
25165
- const d = sma(k, num3(inputs, "dLength", 3));
25166
- return {
25167
- plots: [
25168
- { key: "k", title: "%K", values: k, color: SERIES_LINE, width: 2 },
25169
- { key: "d", title: "%D", values: d, color: WARNING }
25170
- ],
25171
- levels: guideLevels(20, 80)
25172
- };
25822
+ const k = sma(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "kLength", 14)), num3(inputs, "kSmoothing", 3));
25823
+ return stochasticOutput(k, sma(k, num3(inputs, "dLength", 3)), inputs);
25173
25824
  }
25174
25825
  };
25175
25826
  var stochasticRsi = {
@@ -25178,23 +25829,20 @@ var stochasticRsi = {
25178
25829
  shortTitle: "Stoch RSI",
25179
25830
  overlay: false,
25180
25831
  inputs: [
25181
- lengthInput(14, "rsiLength", "RSI length"),
25182
- lengthInput(14, "stochLength", "Stochastic length"),
25183
- lengthInput(3, "kSmoothing", "%K smoothing", 500),
25184
- lengthInput(3, "dLength", "%D smoothing", 500),
25185
- sourceInput()
25832
+ sourceInput("Close", "source", "RSI Source", "Price series the RSI is computed on."),
25833
+ lengthInput(14, "rsiLength", "RSI Length", 5e3, "Lookback length of the Wilder RSI."),
25834
+ lengthInput(14, "stochLength", "Stochastic Length", 5e3, "Lookback over which the highest and lowest RSI values are taken."),
25835
+ lengthInput(3, "kSmoothing", "%K Smoothing", 500, "Simple moving average length applied to the stochastic of RSI to form %K."),
25836
+ lengthInput(3, "dLength", "%D Smoothing", 500, "Simple moving average length applied to %K to form the %D signal line."),
25837
+ ...thresholdInputs(80, 20),
25838
+ colorInput(INFO, "%K Color", "kColor", "Color of the %K line."),
25839
+ colorInput(WARNING, "%D Color", "dColor", "Color of the %D signal line."),
25840
+ colorInput(transp(INFO, 90), "Zone Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
25186
25841
  ],
25187
25842
  compute: (bars, inputs) => {
25188
25843
  const r = rsi(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "rsiLength", 14));
25189
25844
  const k = sma(stoch(r, r, r, num3(inputs, "stochLength", 14)), num3(inputs, "kSmoothing", 3));
25190
- const d = sma(k, num3(inputs, "dLength", 3));
25191
- return {
25192
- plots: [
25193
- { key: "k", title: "%K", values: k, color: SERIES_LINE, width: 2 },
25194
- { key: "d", title: "%D", values: d, color: WARNING }
25195
- ],
25196
- levels: guideLevels(20, 80)
25197
- };
25845
+ return stochasticOutput(k, sma(k, num3(inputs, "dLength", 3)), inputs);
25198
25846
  }
25199
25847
  };
25200
25848
  var macd = {
@@ -25202,23 +25850,35 @@ var macd = {
25202
25850
  title: "MACD",
25203
25851
  overlay: false,
25204
25852
  inputs: [
25205
- lengthInput(12, "fastLength", "Fast length"),
25206
- lengthInput(26, "slowLength", "Slow length"),
25207
- lengthInput(9, "signalLength", "Signal smoothing", 500),
25208
- sourceInput()
25853
+ lengthInput(12, "fastLength", "Fast Length", 5e3, "Length of the fast EMA."),
25854
+ lengthInput(26, "slowLength", "Slow Length", 5e3, "Length of the slow EMA."),
25855
+ lengthInput(9, "signalLength", "Signal Smoothing", 500, "Length of the EMA applied to the MACD line to obtain the signal line."),
25856
+ sourceInput("Close", "source", "Source", "Price series used to compute the fast and slow EMAs."),
25857
+ colorInput(INFO, "MACD Line", "color", "Color of the MACD line."),
25858
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
25859
+ colorInput(BULLISH, "Grow Above", "growAboveColor", "Histogram color when above zero and rising."),
25860
+ colorInput(transp(BULLISH, 50), "Fall Above", "fallAboveColor", "Histogram color when above zero and falling."),
25861
+ colorInput(transp(BEARISH, 50), "Grow Below", "growBelowColor", "Histogram color when below zero and rising."),
25862
+ colorInput(BEARISH, "Fall Below", "fallBelowColor", "Histogram color when below zero and falling.")
25209
25863
  ],
25210
25864
  compute: (bars, inputs) => {
25211
25865
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
25212
25866
  const line = sub(ema(src, num3(inputs, "fastLength", 12)), ema(src, num3(inputs, "slowLength", 26)));
25213
25867
  const signal = ema(line, num3(inputs, "signalLength", 9));
25214
25868
  const hist = sub(line, signal);
25869
+ const colors = momentumColumnColors(hist, {
25870
+ growAbove: str3(inputs, "growAboveColor", BULLISH),
25871
+ fallAbove: str3(inputs, "fallAboveColor", transp(BULLISH, 50)),
25872
+ growBelow: str3(inputs, "growBelowColor", transp(BEARISH, 50)),
25873
+ fallBelow: str3(inputs, "fallBelowColor", BEARISH)
25874
+ });
25215
25875
  return {
25216
25876
  plots: [
25217
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: histColors(hist), base: 0 },
25218
- { key: "macd", title: "MACD", values: line, color: SERIES_LINE, width: 2 },
25219
- { key: "signal", title: "Signal", values: signal, color: WARNING }
25877
+ { key: "hist", title: "Histogram", values: hist, kind: "columns", color: BULLISH, colors, base: 0 },
25878
+ { key: "macd", title: "MACD", values: line, color: str3(inputs, "color", INFO) },
25879
+ { key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) }
25220
25880
  ],
25221
- levels: ZERO_LEVEL
25881
+ levels: [{ key: "zero", price: 0, color: NEUTRAL, lineStyle: "dashed", title: "Zero Line" }]
25222
25882
  };
25223
25883
  }
25224
25884
  };
@@ -25228,26 +25888,49 @@ var ppo = {
25228
25888
  shortTitle: "PPO",
25229
25889
  overlay: false,
25230
25890
  inputs: [
25231
- lengthInput(12, "fastLength", "Fast length"),
25232
- lengthInput(26, "slowLength", "Slow length"),
25233
- lengthInput(9, "signalLength", "Signal smoothing", 500),
25234
- sourceInput()
25891
+ sourceInput("Close", "source", "Source", "Price series fed into the fast and slow averages."),
25892
+ lengthInput(12, "fastLength", "Fast Length", 5e3, "Length of the fast moving average."),
25893
+ lengthInput(26, "slowLength", "Slow Length", 5e3, "Length of the slow moving average. The spread is divided by it and scaled to a percentage of price."),
25894
+ optionInput("maType", "Average Type", "EMA", ["EMA", "SMA"], "Moving average type used for the fast and slow averages."),
25895
+ lengthInput(9, "signalLength", "Signal Smoothing", 500, "Length of the signal line that smooths the PPO line."),
25896
+ optionInput("signalType", "Signal Type", "EMA", ["EMA", "SMA"], "Moving average type used for the signal line."),
25897
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the PPO line, gradient fill and histogram above the zero line."),
25898
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the PPO line, gradient fill and histogram below the zero line."),
25899
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
25900
+ boolInput("showHistogram", "Show Histogram", true, STYLE, "Displays the histogram (PPO minus signal line) as columns. Bright columns mark a spread still widening, faded ones a spread narrowing back."),
25901
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the zero line and the PPO line with a vertical gradient.")
25235
25902
  ],
25236
25903
  compute: (bars, inputs) => {
25237
25904
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
25238
- const fast = ema(src, num3(inputs, "fastLength", 12));
25239
- const slow = ema(src, num3(inputs, "slowLength", 26));
25905
+ const average = (kind2, v, len) => kind2 === "SMA" ? sma(v, len) : ema(v, len);
25906
+ const kind = str3(inputs, "maType", "EMA");
25907
+ const fast = average(kind, src, num3(inputs, "fastLength", 12));
25908
+ const slow = average(kind, src, num3(inputs, "slowLength", 26));
25240
25909
  const line = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : (f - s) / s * 100);
25241
- const signal = ema(line, num3(inputs, "signalLength", 9));
25910
+ const signal = average(str3(inputs, "signalType", "EMA"), line, num3(inputs, "signalLength", 9));
25242
25911
  const hist = sub(line, signal);
25243
- return {
25244
- plots: [
25245
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: histColors(hist), base: 0 },
25246
- { key: "ppo", title: "PPO", values: line, color: SERIES_LINE, width: 2 },
25247
- { key: "signal", title: "Signal", values: signal, color: WARNING }
25248
- ],
25249
- levels: ZERO_LEVEL
25250
- };
25912
+ const bull = str3(inputs, "bullColor", BULLISH);
25913
+ const bear = str3(inputs, "bearColor", BEARISH);
25914
+ const out = centeredOscillator({
25915
+ key: "ppo",
25916
+ title: "PPO Line",
25917
+ values: line,
25918
+ ink: { bull, bear, neutral: bear },
25919
+ zeroInk: transp(NEUTRAL, 90),
25920
+ fill: bool(inputs, "gradientFill", true),
25921
+ extraPlots: [{ key: "signal", title: "Signal Line", values: signal, color: str3(inputs, "signalColor", WARNING) }]
25922
+ });
25923
+ if (bool(inputs, "showHistogram", true)) {
25924
+ const colors = hist.map((x, i) => {
25925
+ if (!Number.isFinite(x)) return null;
25926
+ const prev2 = i > 0 && Number.isFinite(hist[i - 1]) ? hist[i - 1] : x;
25927
+ const ink = x >= 0 ? bull : bear;
25928
+ const expanding = x >= 0 ? x > prev2 : x < prev2;
25929
+ return transp(ink, expanding ? 40 : 70);
25930
+ });
25931
+ out.plots.unshift({ key: "hist", title: "Histogram", values: hist, kind: "columns", color: bull, colors, base: 0 });
25932
+ }
25933
+ return out;
25251
25934
  }
25252
25935
  };
25253
25936
  var awesome = {
@@ -25255,48 +25938,67 @@ var awesome = {
25255
25938
  title: "Awesome Oscillator",
25256
25939
  shortTitle: "AO",
25257
25940
  overlay: false,
25258
- inputs: [],
25259
- compute: (bars) => {
25941
+ inputs: [
25942
+ colorInput(BULLISH, "Rising", "risingColor", "Histogram color when the oscillator is higher than on the previous bar."),
25943
+ colorInput(BEARISH, "Falling", "fallingColor", "Histogram color when the oscillator is lower than or equal to the previous bar."),
25944
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
25945
+ ],
25946
+ compute: (bars, inputs) => {
25260
25947
  const hl2 = sourceValues(bars, "HL2");
25261
25948
  const ao = sub(sma(hl2, 5), sma(hl2, 34));
25949
+ const rising = str3(inputs, "risingColor", BULLISH);
25950
+ const falling = str3(inputs, "fallingColor", BEARISH);
25262
25951
  const colors = ao.map((x, i) => {
25263
25952
  if (!Number.isFinite(x)) return null;
25264
25953
  const prev2 = i > 0 ? ao[i - 1] : Number.NaN;
25265
- return Number.isFinite(prev2) && x < prev2 ? BEARISH : BULLISH;
25954
+ return Number.isFinite(prev2) && x > prev2 ? rising : falling;
25266
25955
  });
25267
25956
  return {
25268
- plots: [{ key: "ao", title: "AO", values: ao, kind: "histogram", color: BULLISH, colors, base: 0 }],
25269
- levels: ZERO_LEVEL
25957
+ plots: [{ key: "ao", title: "AO", values: ao, kind: "columns", color: rising, colors, base: 0 }],
25958
+ levels: [{ key: "zero", price: 0, color: str3(inputs, "zeroColor", NEUTRAL), lineStyle: "dashed", title: "Zero Line" }]
25270
25959
  };
25271
25960
  }
25272
25961
  };
25962
+ var CCI_SCALING = 0.015;
25273
25963
  var cci = {
25274
25964
  type: "commodity-channel-index",
25275
25965
  title: "Commodity Channel Index",
25276
25966
  shortTitle: "CCI",
25277
25967
  overlay: false,
25278
- inputs: [lengthInput(20), sourceInput("HLC3"), colorInput()],
25968
+ inputs: [
25969
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used for the moving average and the mean absolute deviation of the source."),
25970
+ sourceInput("HLC3", "source", "Source", "Price series measured by the CCI; the typical price hlc3 is the classic choice."),
25971
+ floatInput("upper", "Upper Level", 100, -1e3, 1e3, 10, "Overbought reference level, +100 classically."),
25972
+ floatInput("lower", "Lower Level", -100, -1e3, 1e3, 10, "Oversold reference level, \u2212100 classically."),
25973
+ colorInput(NEUTRAL, "CCI Line", "color", "Color of the CCI line."),
25974
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the CCI reads above zero."),
25975
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the CCI reads below zero."),
25976
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the upper, lower and zero reference lines.")
25977
+ ],
25279
25978
  compute: (bars, inputs) => {
25280
25979
  const len = num3(inputs, "length", 20);
25281
25980
  const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
25282
25981
  const basis = sma(src, len);
25283
- const dev = new Array(src.length).fill(Number.NaN);
25284
- for (let i = len - 1; i < src.length; i++) {
25285
- const mean = basis[i];
25286
- if (!Number.isFinite(mean)) continue;
25287
- let d = 0;
25288
- for (let k = i - len + 1; k <= i; k++) d += Math.abs(src[k] - mean);
25289
- dev[i] = d / len;
25290
- }
25982
+ const dev = meanDev(src, len);
25291
25983
  const values = src.map((x, i) => {
25292
25984
  const b = basis[i];
25293
25985
  const d = dev[i];
25294
- return Number.isFinite(b) && Number.isFinite(d) && d !== 0 ? (x - b) / (0.015 * d) : Number.NaN;
25986
+ if (!Number.isFinite(b) || !Number.isFinite(d)) return Number.NaN;
25987
+ return d === 0 ? 0 : (x - b) / (CCI_SCALING * d);
25988
+ });
25989
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
25990
+ return centeredOscillator({
25991
+ key: "cci",
25992
+ title: "CCI",
25993
+ values,
25994
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
25995
+ zeroInk: levelInk,
25996
+ zeroLineStyle: "dotted",
25997
+ extraLevels: [
25998
+ { key: "upper", price: num3(inputs, "upper", 100), color: levelInk, lineStyle: "dashed", title: "Upper Level" },
25999
+ { key: "lower", price: num3(inputs, "lower", -100), color: levelInk, lineStyle: "dashed", title: "Lower Level" }
26000
+ ]
25295
26001
  });
25296
- return {
25297
- plots: [{ key: "cci", title: "CCI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25298
- levels: guideLevels(-100, 100, 0)
25299
- };
25300
26002
  }
25301
26003
  };
25302
26004
  var williamsR = {
@@ -25304,31 +26006,78 @@ var williamsR = {
25304
26006
  title: "Williams %R",
25305
26007
  shortTitle: "%R",
25306
26008
  overlay: false,
25307
- inputs: [lengthInput(14), colorInput()],
26009
+ inputs: [
26010
+ lengthInput(14, "length", "Length", 5e3, "Lookback length of the highest high / lowest low range. 14 is the classic setting."),
26011
+ sourceInput("Close", "source", "Source", "Series located within the lookback range. Close is the classic choice."),
26012
+ ...thresholdInputs(-20, -80, -100, 0),
26013
+ colorInput(NEUTRAL, "%R Line", "color", "Color of the %R line between the thresholds. Toward either level it blends into that level\u2019s color."),
26014
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
26015
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level.")
26016
+ ],
25308
26017
  compute: (bars, inputs) => {
25309
- const values = map(stoch(closes(bars), highs(bars), lows(bars), num3(inputs, "length", 14)), (x) => x - 100);
25310
- return {
25311
- plots: [{ key: "wr", title: "%R", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25312
- levels: guideLevels(-80, -20)
25313
- };
26018
+ const len = num3(inputs, "length", 14);
26019
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
26020
+ const hh = highest(highs(bars), len);
26021
+ const ll = lowest(lows(bars), len);
26022
+ const values = src.map((x, i) => {
26023
+ const h = hh[i];
26024
+ const l = ll[i];
26025
+ return Number.isFinite(h) && Number.isFinite(l) && h > l ? -100 * (h - x) / (h - l) : Number.NaN;
26026
+ });
26027
+ return boundedOscillator({
26028
+ key: "wr",
26029
+ title: "%R",
26030
+ values,
26031
+ scale: { top: 0, bottom: -100 },
26032
+ overbought: num3(inputs, "overbought", -20),
26033
+ oversold: num3(inputs, "oversold", -80),
26034
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
26035
+ levelInk: NEUTRAL
26036
+ });
25314
26037
  }
25315
26038
  };
25316
- var cmo = {
26039
+ var cmoSpec = {
25317
26040
  type: "chande-momentum-oscillator",
25318
26041
  title: "Chande Momentum Oscillator",
25319
26042
  shortTitle: "CMO",
25320
26043
  overlay: false,
25321
- inputs: [lengthInput(9), sourceInput(), colorInput()],
26044
+ inputs: [
26045
+ lengthInput(9, "length", "Length", 5e3, "Number of bars over which up moves and down moves are summed."),
26046
+ sourceInput("Close", "source", "Source", "Price series used to measure bar-to-bar changes."),
26047
+ floatInput("overbought", "Overbought Level", 50, 0, 100, 1, "Level above which the oscillator is considered overbought."),
26048
+ floatInput("oversold", "Oversold Level", -50, -100, 0, 1, "Level below which the oscillator is considered oversold."),
26049
+ colorInput(NEUTRAL, "CMO Line", "color", "Color of the CMO line."),
26050
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while momentum is positive, and shading of the oversold zone."),
26051
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while momentum is negative, and shading of the overbought zone."),
26052
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought, oversold, and zero lines.")
26053
+ ],
25322
26054
  compute: (bars, inputs) => {
25323
26055
  const len = num3(inputs, "length", 9);
25324
26056
  const d = change(sourceValues(bars, str3(inputs, "source", "Close")));
25325
- const su = sum(map(d, (x) => Math.max(x, 0)), len);
25326
- const sd = sum(map(d, (x) => Math.max(-x, 0)), len);
25327
- const values = zip(su, sd, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
25328
- return {
25329
- plots: [{ key: "cmo", title: "CMO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25330
- levels: guideLevels(-50, 50, 0)
25331
- };
26057
+ const up = sum(map(d, (x) => Math.max(x, 0)), len);
26058
+ const down = sum(map(d, (x) => Math.max(-x, 0)), len);
26059
+ const values = zip(up, down, (u, w) => u + w === 0 ? 0 : 100 * (u - w) / (u + w));
26060
+ const bull = str3(inputs, "bullColor", BULLISH);
26061
+ const bear = str3(inputs, "bearColor", BEARISH);
26062
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
26063
+ const overbought = num3(inputs, "overbought", 50);
26064
+ const oversold = num3(inputs, "oversold", -50);
26065
+ const out = centeredOscillator({
26066
+ key: "cmo",
26067
+ title: "CMO",
26068
+ values,
26069
+ ink: { bull, bear, neutral: str3(inputs, "color", NEUTRAL) },
26070
+ zeroInk: levelInk,
26071
+ zeroLineStyle: "dotted",
26072
+ extraLevels: [
26073
+ { key: "overbought", price: overbought, color: levelInk, lineStyle: "dashed", title: "Overbought" },
26074
+ { key: "oversold", price: oversold, color: levelInk, lineStyle: "dashed", title: "Oversold" }
26075
+ ]
26076
+ });
26077
+ const zones = thresholdZones(values.length, { overbought, oversold, top: 100, bottom: -100 }, { overbought: bear, oversold: bull });
26078
+ out.plots.push(...zones.plots);
26079
+ out.bands = [...out.bands ?? [], ...zones.bands];
26080
+ return out;
25332
26081
  }
25333
26082
  };
25334
26083
  var connorsRsi = {
@@ -25337,10 +26086,14 @@ var connorsRsi = {
25337
26086
  shortTitle: "CRSI",
25338
26087
  overlay: false,
25339
26088
  inputs: [
25340
- lengthInput(3, "rsiLength", "RSI length"),
25341
- lengthInput(2, "streakLength", "Streak RSI length"),
25342
- lengthInput(100, "rankLength", "Percent-rank length"),
25343
- colorInput()
26089
+ lengthInput(3, "rsiLength", "RSI Length", 5e3, "Length of the Wilder RSI applied to the closing price."),
26090
+ lengthInput(2, "streakLength", "Streak RSI Length", 5e3, "Length of the Wilder RSI applied to the up/down close streak."),
26091
+ lengthInput(100, "rankLength", "Percent Rank Length", 5e3, "Lookback of the percent rank applied to the 1-bar rate of change of the closing price."),
26092
+ ...thresholdInputs(90, 10),
26093
+ colorInput(NEUTRAL, "CRSI Line", "color", "Color of the Connors RSI line."),
26094
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Line, fill and zone color toward and below the oversold level."),
26095
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Line, fill and zone color toward and above the overbought level."),
26096
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought and oversold levels.")
25344
26097
  ],
25345
26098
  compute: (bars, inputs) => {
25346
26099
  const c = closes(bars);
@@ -25358,10 +26111,17 @@ var connorsRsi = {
25358
26111
  const z = r[i];
25359
26112
  return Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) ? (x + y + z) / 3 : Number.NaN;
25360
26113
  });
25361
- return {
25362
- plots: [{ key: "crsi", title: "CRSI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25363
- levels: guideLevels(20, 80, 50)
25364
- };
26114
+ return boundedOscillator({
26115
+ key: "crsi",
26116
+ title: "Connors RSI",
26117
+ values,
26118
+ scale: { top: 100, bottom: 0 },
26119
+ overbought: num3(inputs, "overbought", 90),
26120
+ oversold: num3(inputs, "oversold", 10),
26121
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
26122
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
26123
+ midpointWash: true
26124
+ });
25365
26125
  }
25366
26126
  };
25367
26127
  var fisher = {
@@ -25369,7 +26129,13 @@ var fisher = {
25369
26129
  title: "Fisher Transform",
25370
26130
  shortTitle: "Fisher",
25371
26131
  overlay: false,
25372
- inputs: [lengthInput(9)],
26132
+ inputs: [
26133
+ lengthInput(9, "length", "Length", 5e3, "Lookback period used to locate the median price hl2 within its highest-lowest range. Ehlers' original length is 10; 9 is the common platform default."),
26134
+ colorInput(NEUTRAL, "Fisher Line", "color", "Color of the Fisher line when it sits exactly on its trigger."),
26135
+ colorInput(NEUTRAL, "Trigger Line", "triggerColor", "Color of the trigger line, the Fisher value delayed one bar."),
26136
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the Fisher line and fill while it is above the trigger."),
26137
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the Fisher line and fill while it is below the trigger.")
26138
+ ],
25373
26139
  compute: (bars, inputs) => {
25374
26140
  const len = num3(inputs, "length", 9);
25375
26141
  const hl2 = sourceValues(bars, "HL2");
@@ -25377,23 +26143,39 @@ var fisher = {
25377
26143
  const lo = lowest(hl2, len);
25378
26144
  const n = bars.length;
25379
26145
  const out = new Array(n).fill(Number.NaN);
26146
+ let position = 0;
25380
26147
  let value = 0;
25381
- let fish = 0;
25382
26148
  for (let i = 0; i < n; i++) {
25383
26149
  const h = hi[i];
25384
26150
  const l = lo[i];
25385
26151
  if (!Number.isFinite(h) || !Number.isFinite(l)) continue;
25386
- const norm = h === l ? 0 : (hl2[i] - l) / (h - l) - 0.5;
25387
- value = Math.max(-0.999, Math.min(0.999, 0.66 * norm + 0.67 * value));
25388
- fish = 0.5 * Math.log((1 + value) / (1 - value)) + 0.5 * fish;
25389
- out[i] = fish;
25390
- }
26152
+ const raw = h > l ? (hl2[i] - l) / (h - l) - 0.5 : 0;
26153
+ position = 0.66 * raw + 0.67 * position;
26154
+ position = position > 0.99 ? 0.999 : position < -0.99 ? -0.999 : position;
26155
+ value = 0.5 * Math.log((1 + position) / (1 - position)) + 0.5 * value;
26156
+ out[i] = value;
26157
+ }
26158
+ const trigger = shift(out, 1);
26159
+ const neutral = str3(inputs, "color", NEUTRAL);
26160
+ const bull = str3(inputs, "bullColor", BULLISH);
26161
+ const bear = str3(inputs, "bearColor", BEARISH);
26162
+ const inkOf = (i) => {
26163
+ const f = out[i];
26164
+ const t = trigger[i];
26165
+ if (!Number.isFinite(t)) return neutral;
26166
+ return f > t ? bull : f < t ? bear : neutral;
26167
+ };
25391
26168
  return {
25392
26169
  plots: [
25393
- { key: "fisher", title: "Fisher", values: out, color: SERIES_LINE, width: 2 },
25394
- { key: "trigger", title: "Trigger", values: shift(out, 1), color: WARNING }
26170
+ { key: "fisher", title: "Fisher", values: out, color: neutral, width: 2, colors: out.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) },
26171
+ { key: "trigger", title: "Trigger", values: trigger, color: str3(inputs, "triggerColor", NEUTRAL) }
25395
26172
  ],
25396
- levels: ZERO_LEVEL
26173
+ bands: [{ key: "spread", from: "fisher", to: "trigger", color: transp(neutral, 80), colors: out.map((x, i) => Number.isFinite(x) ? transp(inkOf(i), 80) : null) }],
26174
+ levels: [
26175
+ { key: "upper", price: 1.5, color: NEUTRAL, lineStyle: "dashed", title: "Upper Level" },
26176
+ { key: "lower", price: -1.5, color: NEUTRAL, lineStyle: "dashed", title: "Lower Level" },
26177
+ { key: "zero", price: 0, color: NEUTRAL, lineStyle: "dotted", title: "Zero Line" }
26178
+ ]
25397
26179
  };
25398
26180
  }
25399
26181
  };
@@ -25401,93 +26183,180 @@ var trix = {
25401
26183
  type: "trix",
25402
26184
  title: "TRIX",
25403
26185
  overlay: false,
25404
- inputs: [lengthInput(18), colorInput()],
26186
+ inputs: [
26187
+ lengthInput(18, "length", "Length", 5e3, "Length of each of the three EMA smoothing passes applied to the close price."),
26188
+ lengthInput(9, "signalLength", "Signal Length", 500, "Length of the EMA applied to TRIX to form the signal line."),
26189
+ colorInput(NEUTRAL, "TRIX Color", "color", "Color of the TRIX line."),
26190
+ colorInput(WARNING, "Signal Color", "signalColor", "Color of the signal line."),
26191
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the TRIX line and its gradient fill while TRIX holds above zero."),
26192
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the TRIX line and its gradient fill while TRIX holds below zero.")
26193
+ ],
25405
26194
  compute: (bars, inputs) => {
25406
26195
  const len = num3(inputs, "length", 18);
25407
- const logClose = map(closes(bars), Math.log);
25408
- const smooth = ema(ema(ema(logClose, len), len), len);
25409
- const values = map(change(smooth), (x) => x * 1e4);
25410
- return {
25411
- plots: [{ key: "trix", title: "TRIX", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25412
- levels: ZERO_LEVEL
25413
- };
26196
+ const smooth = ema(ema(ema(closes(bars), len), len), len);
26197
+ const values = roc(smooth, 1);
26198
+ return centeredOscillator({
26199
+ key: "trix",
26200
+ title: "TRIX",
26201
+ values,
26202
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
26203
+ zeroInk: NEUTRAL,
26204
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(values, num3(inputs, "signalLength", 9)), color: str3(inputs, "signalColor", WARNING) }]
26205
+ });
25414
26206
  }
25415
26207
  };
25416
- function tsiLine(src, long, short) {
26208
+ function tsiRatio(src, long, short) {
25417
26209
  const mom = change(src);
25418
26210
  const numer = ema(ema(mom, long), short);
25419
26211
  const denom = ema(ema(map(mom, Math.abs), long), short);
25420
- return zip(numer, denom, (a, b) => b === 0 ? 0 : 100 * a / b);
26212
+ return zip(numer, denom, (a, b) => b === 0 ? 0 : a / b);
25421
26213
  }
25422
26214
  var tsi = {
25423
26215
  type: "true-strength-index",
25424
26216
  title: "True Strength Index",
25425
26217
  shortTitle: "TSI",
25426
26218
  overlay: false,
25427
- inputs: [lengthInput(25, "longLength", "Long length"), lengthInput(13, "shortLength", "Short length"), lengthInput(13, "signalLength", "Signal length")],
26219
+ inputs: [
26220
+ lengthInput(25, "longLength", "Long Length", 5e3, "Length of the first (long) EMA applied to the 1-bar price change."),
26221
+ lengthInput(13, "shortLength", "Short Length", 5e3, "Length of the second (short) EMA applied to the long-smoothed price change."),
26222
+ lengthInput(13, "signalLength", "Signal Length", 500, "Length of the EMA of TSI used as the signal line."),
26223
+ colorInput(NEUTRAL, "TSI", "color", "Color of the TSI line."),
26224
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line."),
26225
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the TSI line and its gradient fill while TSI holds above zero."),
26226
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the TSI line and its gradient fill while TSI holds below zero.")
26227
+ ],
25428
26228
  compute: (bars, inputs) => {
25429
- const line = tsiLine(closes(bars), num3(inputs, "longLength", 25), num3(inputs, "shortLength", 13));
25430
- return {
25431
- plots: [
25432
- { key: "tsi", title: "TSI", values: line, color: SERIES_LINE, width: 2 },
25433
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: WARNING }
25434
- ],
25435
- levels: ZERO_LEVEL
25436
- };
26229
+ const values = map(tsiRatio(closes(bars), num3(inputs, "longLength", 25), num3(inputs, "shortLength", 13)), (x) => 100 * x);
26230
+ return centeredOscillator({
26231
+ key: "tsi",
26232
+ title: "TSI",
26233
+ values,
26234
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
26235
+ zeroInk: NEUTRAL,
26236
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(values, num3(inputs, "signalLength", 13)), color: str3(inputs, "signalColor", WARNING) }]
26237
+ });
25437
26238
  }
25438
26239
  };
26240
+ var SMI_INDICATOR = "Indicator";
26241
+ var SMI_OSCILLATOR = "Oscillator";
25439
26242
  var smiErgodic = {
25440
26243
  type: "smi-ergodic",
25441
26244
  title: "SMI Ergodic Oscillator",
25442
26245
  shortTitle: "SMIE",
25443
26246
  overlay: false,
25444
- inputs: [lengthInput(5, "shortLength", "Short length"), lengthInput(20, "longLength", "Long length"), lengthInput(5, "signalLength", "Signal length")],
26247
+ inputs: [
26248
+ lengthInput(20, "longLength", "Long Length", 5e3, "Length of the first EMA smoothing applied to the momentum of the closing price."),
26249
+ lengthInput(5, "shortLength", "Short Length", 5e3, "Length of the second EMA smoothing applied to the momentum of the closing price."),
26250
+ lengthInput(5, "signalLength", "Signal Length", 500, "Length of the EMA of the SMI that forms the signal line."),
26251
+ optionInput("display", "Display", SMI_INDICATOR, [SMI_INDICATOR, SMI_OSCILLATOR], "Indicator plots the SMI and its signal line. Oscillator plots the difference between them as a histogram."),
26252
+ colorInput(NEUTRAL, "SMI", "color", "Color of the SMI line while it sits exactly on its signal line."),
26253
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line."),
26254
+ colorInput(BULLISH, "Oscillator Grow", "growColor", "Color of the rising oscillator histogram, and of the SMI line while it leads its signal."),
26255
+ colorInput(BEARISH, "Oscillator Fall", "fallColor", "Color of the falling oscillator histogram, and of the SMI line while it lags its signal.")
26256
+ ],
25445
26257
  compute: (bars, inputs) => {
25446
- const line = tsiLine(closes(bars), num3(inputs, "longLength", 20), num3(inputs, "shortLength", 5));
25447
- const signal = ema(line, num3(inputs, "signalLength", 5));
25448
- const osc = sub(line, signal);
26258
+ const smi = tsiRatio(closes(bars), num3(inputs, "longLength", 20), num3(inputs, "shortLength", 5));
26259
+ const signal = ema(smi, num3(inputs, "signalLength", 5));
26260
+ const grow = str3(inputs, "growColor", BULLISH);
26261
+ const fall = str3(inputs, "fallColor", BEARISH);
26262
+ const zero = [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed", title: "Zero" }];
26263
+ if (str3(inputs, "display", SMI_INDICATOR) === SMI_OSCILLATOR) {
26264
+ const osc = sub(smi, signal);
26265
+ const colors = osc.map((x, i) => {
26266
+ if (!Number.isFinite(x)) return null;
26267
+ const prev2 = i > 0 && Number.isFinite(osc[i - 1]) ? osc[i - 1] : x;
26268
+ return x > prev2 ? grow : fall;
26269
+ });
26270
+ return { plots: [{ key: "osc", title: "Oscillator", values: osc, kind: "columns", color: grow, colors, base: 0 }], levels: zero };
26271
+ }
26272
+ const neutral = str3(inputs, "color", NEUTRAL);
26273
+ const inkOf = (i) => {
26274
+ const a = smi[i];
26275
+ const b = signal[i];
26276
+ if (!Number.isFinite(b)) return neutral;
26277
+ return a > b ? grow : a < b ? fall : neutral;
26278
+ };
25449
26279
  return {
25450
26280
  plots: [
25451
- { key: "osc", title: "Oscillator", values: osc, kind: "histogram", color: BULLISH, colors: histColors(osc), base: 0 },
25452
- { key: "smi", title: "SMI", values: line, color: SERIES_LINE, width: 2 },
25453
- { key: "signal", title: "Signal", values: signal, color: WARNING }
26281
+ { key: "smi", title: "SMI", values: smi, color: neutral, colors: smi.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) },
26282
+ { key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) }
25454
26283
  ],
25455
- levels: ZERO_LEVEL
26284
+ bands: [{ key: "spread", from: "smi", to: "signal", color: transp(neutral, 75), colors: smi.map((x, i) => Number.isFinite(x) ? transp(inkOf(i), 75) : null) }],
26285
+ levels: zero
25456
26286
  };
25457
26287
  }
25458
26288
  };
26289
+ var STC_MID = 50;
26290
+ var TRIGGERS = "Trigger Levels";
25459
26291
  var stc = {
25460
26292
  type: "schaff-trend-cycle",
25461
26293
  title: "Schaff Trend Cycle",
25462
26294
  shortTitle: "Schaff Trend Cycle",
25463
26295
  overlay: false,
25464
- inputs: [lengthInput(10, "cycleLength", "Cycle length"), lengthInput(23, "fastLength", "Fast length"), lengthInput(50, "slowLength", "Slow length")],
26296
+ inputs: [
26297
+ sourceInput("Close", "source", "Source", "Price series used to build the MACD that the trend cycle is measured on."),
26298
+ lengthInput(23, "fastLength", "Fast Length", 5e3, "Fast EMA length of the underlying MACD."),
26299
+ lengthInput(50, "slowLength", "Slow Length", 5e3, "Slow EMA length of the underlying MACD."),
26300
+ { ...lengthInput(10, "cycleLength", "Cycle Length", 5e3, "Window of the two stochastic passes. Each pass measures where its input sits inside the range of its own values over this many bars."), min: 2 },
26301
+ floatInput("factor", "Smoothing Factor", 0.5, 0.01, 1, 0.05, "%D style recursive smoothing applied after each stochastic pass. 0.5 is the published default."),
26302
+ { ...floatInput("upper", "Upper Trigger", 75, 0, 100, 1, "Upper trigger line. A cross down through it flags an emerging downtrend."), group: TRIGGERS },
26303
+ { ...floatInput("lower", "Lower Trigger", 25, 0, 100, 1, "Lower trigger line. A cross up through it flags an emerging uptrend."), group: TRIGGERS },
26304
+ colorInput(BULLISH, "Bullish Color", "bullColor", "Color of the trend cycle line and gradient while the cycle holds the upper half of its 0-100 scale."),
26305
+ colorInput(BEARISH, "Bearish Color", "bearColor", "Color of the trend cycle line and gradient while the cycle holds the lower half of its 0-100 scale."),
26306
+ colorInput(NEUTRAL, "Levels Color", "levelsColor", "Color of the dashed trigger lines and the dotted midline."),
26307
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Vertical gradient between the midline and the trend cycle, fading out toward the midline.")
26308
+ ],
25465
26309
  compute: (bars, inputs) => {
25466
26310
  const len = num3(inputs, "cycleLength", 10);
25467
- const src = closes(bars);
26311
+ const factor = num3(inputs, "factor", 0.5);
26312
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
25468
26313
  const macdLine = sub(ema(src, num3(inputs, "fastLength", 23)), ema(src, num3(inputs, "slowLength", 50)));
25469
- const stochOf = (v) => {
26314
+ const stochNorm = (v) => {
25470
26315
  const lo = lowest(v, len);
25471
26316
  const hi = highest(v, len);
25472
26317
  const out = new Array(v.length).fill(Number.NaN);
25473
- let prevRaw = Number.NaN;
25474
- let smoothed = Number.NaN;
26318
+ let pct = Number.NaN;
25475
26319
  for (let i = 0; i < v.length; i++) {
25476
26320
  const x = v[i];
25477
26321
  const l = lo[i];
25478
26322
  const h = hi[i];
25479
26323
  if (!Number.isFinite(x) || !Number.isFinite(l) || !Number.isFinite(h)) continue;
25480
- const raw = h > l ? (x - l) / (h - l) * 100 : Number.isFinite(prevRaw) ? prevRaw : 50;
25481
- prevRaw = raw;
25482
- smoothed = Number.isFinite(smoothed) ? smoothed + 0.5 * (raw - smoothed) : raw;
26324
+ if (h > l) pct = 100 * (x - l) / (h - l);
26325
+ out[i] = pct;
26326
+ }
26327
+ return out;
26328
+ };
26329
+ const smoothStep = (v) => {
26330
+ const out = new Array(v.length).fill(Number.NaN);
26331
+ let smoothed = Number.NaN;
26332
+ for (let i = 0; i < v.length; i++) {
26333
+ const x = v[i];
26334
+ if (!Number.isFinite(x)) continue;
26335
+ smoothed = Number.isFinite(smoothed) ? smoothed + factor * (x - smoothed) : x;
25483
26336
  out[i] = smoothed;
25484
26337
  }
25485
26338
  return out;
25486
26339
  };
25487
- const values = stochOf(stochOf(macdLine));
26340
+ const values = smoothStep(stochNorm(smoothStep(stochNorm(macdLine))));
26341
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
26342
+ const bull = str3(inputs, "bullColor", BULLISH);
26343
+ const bear = str3(inputs, "bearColor", BEARISH);
26344
+ const plots = [
26345
+ { key: "stc", title: "Schaff Trend Cycle", values, color: bear, width: 2, colors: values.map((x) => Number.isFinite(x) ? x > STC_MID ? bull : bear : null) }
26346
+ ];
26347
+ const bands = [];
26348
+ if (bool(inputs, "gradientFill", true)) {
26349
+ plots.push(anchorPlot("midAnchor", "Midline Anchor", values.length, STC_MID));
26350
+ bands.push({ key: "wash", from: "stc", to: "midAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, STC_MID, (x) => x > STC_MID ? bull : bear) });
26351
+ }
25488
26352
  return {
25489
- plots: [{ key: "stc", title: "STC", values, color: SERIES_LINE, width: 2 }],
25490
- levels: guideLevels(25, 75)
26353
+ plots,
26354
+ bands,
26355
+ levels: [
26356
+ { key: "upper", price: num3(inputs, "upper", 75), color: transp(levelInk, 25), lineStyle: "dashed", title: "Upper Trigger" },
26357
+ { key: "lower", price: num3(inputs, "lower", 25), color: transp(levelInk, 25), lineStyle: "dashed", title: "Lower Trigger" },
26358
+ { key: "midline", price: STC_MID, color: transp(levelInk, 60), lineStyle: "dotted", title: "Midline" }
26359
+ ]
25491
26360
  };
25492
26361
  }
25493
26362
  };
@@ -25496,22 +26365,37 @@ var kst = {
25496
26365
  title: "Know Sure Thing",
25497
26366
  shortTitle: "KST",
25498
26367
  overlay: false,
25499
- inputs: [sourceInput(), lengthInput(9, "signalLength", "Signal length")],
26368
+ inputs: [
26369
+ lengthInput(10, "rocLength1", "ROC Length #1", 5e3, "Lookback of the first (shortest) rate of change."),
26370
+ lengthInput(15, "rocLength2", "ROC Length #2", 5e3, "Lookback of the second rate of change."),
26371
+ lengthInput(20, "rocLength3", "ROC Length #3", 5e3, "Lookback of the third rate of change."),
26372
+ lengthInput(30, "rocLength4", "ROC Length #4", 5e3, "Lookback of the fourth (longest) rate of change."),
26373
+ lengthInput(10, "smaLength1", "SMA Length #1", 5e3, "Smoothing length applied to the first rate of change (RCMA #1)."),
26374
+ lengthInput(10, "smaLength2", "SMA Length #2", 5e3, "Smoothing length applied to the second rate of change (RCMA #2)."),
26375
+ lengthInput(10, "smaLength3", "SMA Length #3", 5e3, "Smoothing length applied to the third rate of change (RCMA #3)."),
26376
+ lengthInput(15, "smaLength4", "SMA Length #4", 5e3, "Smoothing length applied to the fourth rate of change (RCMA #4)."),
26377
+ lengthInput(9, "signalLength", "Signal Length", 500, "Simple moving average length of the signal line."),
26378
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the KST line while above zero."),
26379
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the KST line while below zero."),
26380
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line.")
26381
+ ],
25500
26382
  compute: (bars, inputs) => {
25501
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
25502
- const line = [
25503
- sma(roc(src, 10), 10),
25504
- sma(roc(src, 15), 10),
25505
- sma(roc(src, 20), 10),
25506
- sma(roc(src, 30), 15)
25507
- ].reduce((acc, r, i) => zip(acc, r, (a, b) => a + b * (i + 1)), new Array(src.length).fill(0));
25508
- return {
25509
- plots: [
25510
- { key: "kst", title: "KST", values: line, color: SERIES_LINE, width: 2 },
25511
- { key: "signal", title: "Signal", values: sma(line, num3(inputs, "signalLength", 9)), color: WARNING }
25512
- ],
25513
- levels: ZERO_LEVEL
25514
- };
26383
+ const c = closes(bars);
26384
+ const rcma = (rocKey, rocDefault, smaKey, smaDefault) => sma(roc(c, num3(inputs, rocKey, rocDefault)), num3(inputs, smaKey, smaDefault));
26385
+ const values = [rcma("rocLength1", 10, "smaLength1", 10), rcma("rocLength2", 15, "smaLength2", 10), rcma("rocLength3", 20, "smaLength3", 10), rcma("rocLength4", 30, "smaLength4", 15)].reduce(
26386
+ (acc, r, i) => zip(acc, r, (a, b) => a + b * (i + 1)),
26387
+ new Array(c.length).fill(0)
26388
+ );
26389
+ const bull = str3(inputs, "bullColor", BULLISH);
26390
+ return centeredOscillator({
26391
+ key: "kst",
26392
+ title: "KST",
26393
+ values,
26394
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
26395
+ zeroInk: NEUTRAL,
26396
+ valueTransparency: 60,
26397
+ extraPlots: [{ key: "signal", title: "Signal", values: sma(values, num3(inputs, "signalLength", 9)), color: str3(inputs, "signalColor", WARNING) }]
26398
+ });
25515
26399
  }
25516
26400
  };
25517
26401
  var coppock = {
@@ -25520,19 +26404,25 @@ var coppock = {
25520
26404
  shortTitle: "Coppock",
25521
26405
  overlay: false,
25522
26406
  inputs: [
25523
- lengthInput(10, "wmaLength", "WMA length"),
25524
- lengthInput(14, "longRoc", "Long RoC length"),
25525
- lengthInput(11, "shortRoc", "Short RoC length"),
25526
- sourceInput(),
25527
- colorInput()
26407
+ lengthInput(10, "wmaLength", "WMA Length", 5e3, "Length of the weighted moving average applied to the sum of the two rates of change."),
26408
+ lengthInput(14, "longRoc", "Long RoC Length", 5e3, "Lookback of the long rate of change, in bars. The original definition uses 14 monthly closes."),
26409
+ lengthInput(11, "shortRoc", "Short RoC Length", 5e3, "Lookback of the short rate of change, in bars. The original definition uses 11 monthly closes."),
26410
+ sourceInput("Close", "source", "Source", "Price series used for the rate of change calculations."),
26411
+ colorInput(NEUTRAL, "Coppock Curve", "color", "Color of the Coppock Curve line."),
26412
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the curve reads above zero."),
26413
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the curve reads below zero."),
26414
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
25528
26415
  ],
25529
26416
  compute: (bars, inputs) => {
25530
26417
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
25531
26418
  const values = wma(zip(roc(src, num3(inputs, "longRoc", 14)), roc(src, num3(inputs, "shortRoc", 11)), (a, b) => a + b), num3(inputs, "wmaLength", 10));
25532
- return {
25533
- plots: [{ key: "coppock", title: "Coppock", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25534
- levels: ZERO_LEVEL
25535
- };
26419
+ return centeredOscillator({
26420
+ key: "coppock",
26421
+ title: "Coppock Curve",
26422
+ values,
26423
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
26424
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
26425
+ });
25536
26426
  }
25537
26427
  };
25538
26428
  var dpo = {
@@ -25540,42 +26430,76 @@ var dpo = {
25540
26430
  title: "Detrended Price Oscillator",
25541
26431
  shortTitle: "DPO",
25542
26432
  overlay: false,
25543
- inputs: [lengthInput(21), colorInput()],
26433
+ inputs: [
26434
+ lengthInput(21, "length", "Length", 5e3, "Lookback of the simple moving average that price is detrended against. The comparison is displaced by Length / 2 + 1 bars."),
26435
+ sourceInput("Close", "source", "Source", "Price series used in the detrending calculation."),
26436
+ boolInput("centered", "Centered", false, void 0, "Shifts the oscillator back by Length / 2 + 1 bars so it is centered on the prices it detrends. The most recent bars then show no value."),
26437
+ colorInput(NEUTRAL, "DPO", "color", "Color of the line when it sits exactly at zero."),
26438
+ colorInput(BULLISH, "Above Zero", "bullColor", "Color of the line and fill while the oscillator is above zero."),
26439
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while the oscillator is below zero."),
26440
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
26441
+ ],
25544
26442
  compute: (bars, inputs) => {
25545
26443
  const len = num3(inputs, "length", 21);
25546
- const barsback = Math.floor(len / 2) + 1;
25547
- const values = sub(closes(bars), shift(sma(closes(bars), len), barsback));
25548
- return {
25549
- plots: [{ key: "dpo", title: "DPO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25550
- levels: ZERO_LEVEL
25551
- };
26444
+ const barsBack = Math.trunc(len / 2) + 1;
26445
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
26446
+ const basis = sma(src, len);
26447
+ const centered = bool(inputs, "centered", false);
26448
+ const values = centered ? shift(sub(shift(src, barsBack), basis), -barsBack) : sub(src, shift(basis, barsBack));
26449
+ return centeredOscillator({
26450
+ key: "dpo",
26451
+ title: "DPO",
26452
+ values,
26453
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
26454
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
26455
+ });
25552
26456
  }
25553
26457
  };
26458
+ var ROC_PERCENT = "ROC %";
26459
+ var ROC_MOMENTUM = "Momentum";
25554
26460
  var rocSpec = {
25555
26461
  type: "rate-of-change",
25556
26462
  title: "Rate of Change",
25557
26463
  shortTitle: "ROC",
25558
26464
  overlay: false,
25559
- inputs: [lengthInput(9), sourceInput(), colorInput()],
25560
- compute: (bars, inputs) => ({
25561
- plots: [
25562
- {
25563
- key: "roc",
25564
- title: "RoC",
25565
- values: roc(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 9)),
25566
- color: str3(inputs, "color", SERIES_LINE),
25567
- width: 2
25568
- }
25569
- ],
25570
- levels: ZERO_LEVEL
25571
- })
26465
+ inputs: [
26466
+ lengthInput(9, "length", "Length", 5e3, "Lookback period; the source is compared with its value this many bars ago."),
26467
+ sourceInput("Close", "source", "Source", "Series used as input for the calculation."),
26468
+ optionInput("mode", "Mode", ROC_PERCENT, [ROC_PERCENT, ROC_MOMENTUM], "ROC % plots the change as a percentage of the source value from the lookback; Momentum plots the raw difference."),
26469
+ colorInput(NEUTRAL, "Line", "color", "Color of the line when the reading sits exactly at zero."),
26470
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the line and fill while the reading is above zero."),
26471
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line and fill while the reading is below zero."),
26472
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the line and zero, following the line color.")
26473
+ ],
26474
+ compute: (bars, inputs) => {
26475
+ const len = num3(inputs, "length", 9);
26476
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
26477
+ const values = str3(inputs, "mode", ROC_PERCENT) === ROC_MOMENTUM ? change(src, len) : roc(src, len);
26478
+ return centeredOscillator({
26479
+ key: "roc",
26480
+ title: "ROC",
26481
+ values,
26482
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
26483
+ zeroInk: NEUTRAL,
26484
+ fill: bool(inputs, "gradientFill", true)
26485
+ });
26486
+ }
25572
26487
  };
25573
26488
  var ultimate = {
25574
26489
  type: "ultimate-oscillator",
25575
26490
  title: "Ultimate Oscillator",
25576
26491
  shortTitle: "UO",
25577
26492
  overlay: false,
25578
- inputs: [lengthInput(7, "fastLength", "Fast length"), lengthInput(14, "middleLength", "Middle length"), lengthInput(28, "slowLength", "Slow length"), colorInput()],
26493
+ inputs: [
26494
+ lengthInput(7, "fastLength", "Fast Length", 5e3, "Bars in the fastest averaging window. Weighted 4 in the final blend, so it dominates the oscillator."),
26495
+ lengthInput(14, "middleLength", "Middle Length", 5e3, "Bars in the intermediate averaging window. Weighted 2 in the final blend."),
26496
+ lengthInput(28, "slowLength", "Slow Length", 5e3, "Bars in the slowest averaging window. Weighted 1 in the final blend."),
26497
+ ...thresholdInputs(70, 30, 0, 100, "Overbought", "Oversold"),
26498
+ colorInput(NEUTRAL, "Oscillator", "color", "Color of the line midway between the levels. Toward either level it blends into that level\u2019s color."),
26499
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Color of the overbought and oversold levels."),
26500
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line at the oversold level and of the oversold zone shading."),
26501
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line at the overbought level and of the overbought zone shading.")
26502
+ ],
25579
26503
  compute: (bars, inputs) => {
25580
26504
  const n = bars.length;
25581
26505
  const bp = new Array(n).fill(Number.NaN);
@@ -25583,10 +26507,9 @@ var ultimate = {
25583
26507
  for (let i = 0; i < n; i++) {
25584
26508
  const b = bars[i];
25585
26509
  const pc = i > 0 ? bars[i - 1].close : b.close;
25586
- const lo = Math.min(b.low, pc);
25587
- const hi = Math.max(b.high, pc);
25588
- bp[i] = b.close - lo;
25589
- tr[i] = hi - lo;
26510
+ const trueLow = Math.min(b.low, pc);
26511
+ bp[i] = b.close - trueLow;
26512
+ tr[i] = Math.max(b.high, pc) - trueLow;
25590
26513
  }
25591
26514
  const avg = (len) => zip(sum(bp, len), sum(tr, len), (a, b) => b === 0 ? Number.NaN : a / b);
25592
26515
  const a7 = avg(num3(inputs, "fastLength", 7));
@@ -25597,10 +26520,17 @@ var ultimate = {
25597
26520
  const z = a28[i];
25598
26521
  return Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) ? 100 * (4 * x + 2 * y + z) / 7 : Number.NaN;
25599
26522
  });
25600
- return {
25601
- plots: [{ key: "uo", title: "UO", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25602
- levels: guideLevels(30, 70, 50)
25603
- };
26523
+ return boundedOscillator({
26524
+ key: "uo",
26525
+ title: "UO",
26526
+ values,
26527
+ scale: { top: 100, bottom: 0 },
26528
+ overbought: num3(inputs, "overbought", 70),
26529
+ oversold: num3(inputs, "oversold", 30),
26530
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
26531
+ levelInk: str3(inputs, "levelsColor", NEUTRAL),
26532
+ zoneTransparency: 91
26533
+ });
25604
26534
  }
25605
26535
  };
25606
26536
  var rvgi = {
@@ -25608,7 +26538,11 @@ var rvgi = {
25608
26538
  title: "Relative Vigor Index",
25609
26539
  shortTitle: "RVGI",
25610
26540
  overlay: false,
25611
- inputs: [lengthInput(10)],
26541
+ inputs: [
26542
+ lengthInput(10, "length", "Length", 5e3, "Number of bars summed for the smoothed numerator (close \u2212 open) and denominator (high \u2212 low)."),
26543
+ colorInput(BULLISH, "RVGI", "color", "Color of the RVGI line."),
26544
+ colorInput(BEARISH, "Signal", "signalColor", "Color of the signal line.")
26545
+ ],
25612
26546
  compute: (bars, inputs) => {
25613
26547
  const len = num3(inputs, "length", 10);
25614
26548
  const co = swma(sub(closes(bars), opens(bars)));
@@ -25616,31 +26550,49 @@ var rvgi = {
25616
26550
  const line = zip(sum(co, len), sum(hl, len), (a, b) => b === 0 ? 0 : a / b);
25617
26551
  return {
25618
26552
  plots: [
25619
- { key: "rvgi", title: "RVGI", values: line, color: SERIES_LINE, width: 2 },
25620
- { key: "signal", title: "Signal", values: swma(line), color: WARNING }
26553
+ { key: "rvgi", title: "RVGI", values: line, color: str3(inputs, "color", BULLISH) },
26554
+ { key: "signal", title: "Signal", values: swma(line), color: str3(inputs, "signalColor", BEARISH) }
25621
26555
  ],
25622
- levels: ZERO_LEVEL
26556
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed", title: "Zero" }]
25623
26557
  };
25624
26558
  }
25625
26559
  };
26560
+ var RVI_EMA = "EMA";
26561
+ var RVI_RMA = "Wilder (RMA)";
25626
26562
  var relVolatility = {
25627
26563
  type: "relative-volatility-index",
25628
26564
  title: "Relative Volatility Index",
25629
26565
  shortTitle: "RVI",
25630
26566
  overlay: false,
25631
- inputs: [lengthInput(10, "stdevLength", "StdDev length"), lengthInput(14, "smoothLength", "Smoothing length"), sourceInput(), colorInput()],
26567
+ inputs: [
26568
+ lengthInput(10, "stdevLength", "Stdev Length", 5e3, "Number of bars used for the standard deviation of closing prices that serves as each bar's volatility reading."),
26569
+ lengthInput(14, "smoothLength", "Smoothing Length", 5e3, "Number of bars used to smooth the up and down volatility streams."),
26570
+ optionInput("smoothType", "Smoothing Type", RVI_EMA, [RVI_EMA, RVI_RMA], "Moving average applied to the volatility streams. EMA is Dorsey's classic calculation; Wilder (RMA) matches platforms that use Wilder smoothing."),
26571
+ ...thresholdInputs(80, 20),
26572
+ colorInput(NEUTRAL, "RVI Line", "color", "Color of the RVI line at the midline. Toward either threshold it blends into that threshold\u2019s color."),
26573
+ colorInput(BULLISH, "Up Volatility", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
26574
+ colorInput(BEARISH, "Down Volatility", "oversoldColor", "Color of the line and zone shading at and below the oversold level.")
26575
+ ],
25632
26576
  compute: (bars, inputs) => {
26577
+ const c = closes(bars);
26578
+ const sigma = stdev(c, num3(inputs, "stdevLength", 10));
26579
+ const d = change(c);
26580
+ const up = zip(sigma, d, (s, x) => x > 0 ? s : 0);
26581
+ const down = zip(sigma, d, (s, x) => x < 0 ? s : 0);
25633
26582
  const smoothLen = num3(inputs, "smoothLength", 14);
25634
- const src = sourceValues(bars, str3(inputs, "source", "Close"));
25635
- const dev = stdev(src, num3(inputs, "stdevLength", 10));
25636
- const d = change(src);
25637
- const up = rma(zip(dev, d, (s, x) => x > 0 ? s : 0), smoothLen);
25638
- const dn = rma(zip(dev, d, (s, x) => x <= 0 ? s : 0), smoothLen);
25639
- const values = zip(up, dn, (u, w) => u + w === 0 ? 50 : 100 * u / (u + w));
25640
- return {
25641
- plots: [{ key: "rvi", title: "RVI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25642
- levels: guideLevels(20, 80, 50)
25643
- };
26583
+ const smooth = str3(inputs, "smoothType", RVI_EMA) === RVI_RMA ? rma : ema;
26584
+ const values = zip(smooth(up, smoothLen), smooth(down, smoothLen), (u, w) => u + w === 0 ? 0 : 100 * u / (u + w));
26585
+ return boundedOscillator({
26586
+ key: "rvi",
26587
+ title: "RVI",
26588
+ values,
26589
+ scale: { top: 100, bottom: 0 },
26590
+ overbought: num3(inputs, "overbought", 80),
26591
+ oversold: num3(inputs, "oversold", 20),
26592
+ ink: { low: str3(inputs, "oversoldColor", BEARISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BULLISH) },
26593
+ levelInk: NEUTRAL,
26594
+ width: 2
26595
+ });
25644
26596
  }
25645
26597
  };
25646
26598
  var bop = {
@@ -25648,28 +26600,43 @@ var bop = {
25648
26600
  title: "Balance of Power",
25649
26601
  shortTitle: "BOP",
25650
26602
  overlay: false,
25651
- inputs: [colorInput()],
26603
+ inputs: [
26604
+ boolInput("smoothing", "Smoothing", false, void 0, "Smooth the raw Balance of Power with a simple moving average instead of plotting the single-bar value."),
26605
+ lengthInput(14, "smoothLength", "Length", 5e3, "Simple moving average length applied when smoothing is enabled."),
26606
+ colorInput(NEUTRAL, "BOP Color", "color", "Color of the line at exactly zero."),
26607
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the Balance of Power is above zero \u2014 buyers in control of the bar."),
26608
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the Balance of Power is below zero \u2014 sellers in control of the bar.")
26609
+ ],
25652
26610
  compute: (bars, inputs) => {
25653
- const values = bars.map((b) => b.high === b.low ? 0 : (b.close - b.open) / (b.high - b.low));
25654
- return {
25655
- plots: [{ key: "bop", title: "BOP", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25656
- levels: ZERO_LEVEL
25657
- };
26611
+ const raw = bars.map((b) => b.high === b.low ? 0 : (b.close - b.open) / (b.high - b.low));
26612
+ const values = bool(inputs, "smoothing", false) ? sma(raw, num3(inputs, "smoothLength", 14)) : raw;
26613
+ return centeredOscillator({
26614
+ key: "bop",
26615
+ title: "BOP",
26616
+ values,
26617
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
26618
+ zeroInk: NEUTRAL,
26619
+ valueTransparency: 60
26620
+ });
25658
26621
  }
25659
26622
  };
25660
26623
  var elderRay = {
25661
26624
  type: "elder-ray",
25662
26625
  title: "Elder Ray",
25663
26626
  overlay: false,
25664
- inputs: [lengthInput(13)],
26627
+ inputs: [
26628
+ lengthInput(13, "length", "EMA Length", 5e3, "Number of bars in the exponential moving average of closing prices used as the reference level for Bull Power and Bear Power. Alexander Elder's classic setting is 13."),
26629
+ colorInput(BULLISH, "Bull Power", "bullColor", "Color of the Bull Power columns."),
26630
+ colorInput(BEARISH, "Bear Power", "bearColor", "Color of the Bear Power columns.")
26631
+ ],
25665
26632
  compute: (bars, inputs) => {
25666
26633
  const basis = ema(closes(bars), num3(inputs, "length", 13));
25667
26634
  return {
25668
26635
  plots: [
25669
- { key: "bull", title: "Bull power", values: sub(highs(bars), basis), kind: "histogram", color: BULLISH, base: 0 },
25670
- { key: "bear", title: "Bear power", values: sub(lows(bars), basis), kind: "histogram", color: BEARISH, base: 0 }
26636
+ { key: "bull", title: "Bull Power", values: sub(highs(bars), basis), kind: "columns", color: str3(inputs, "bullColor", BULLISH), base: 0 },
26637
+ { key: "bear", title: "Bear Power", values: sub(lows(bars), basis), kind: "columns", color: str3(inputs, "bearColor", BEARISH), base: 0 }
25671
26638
  ],
25672
- levels: ZERO_LEVEL
26639
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 30), lineStyle: "dashed", title: "Zero Line" }]
25673
26640
  };
25674
26641
  }
25675
26642
  };
@@ -25678,31 +26645,66 @@ var ttmSqueeze = {
25678
26645
  title: "TTM Squeeze",
25679
26646
  overlay: false,
25680
26647
  inputs: [
25681
- lengthInput(20),
25682
- { key: "bbMult", title: "Bollinger multiplier", type: "float", defval: 2, min: 0.1, max: 50, step: 0.1 },
25683
- { key: "kcMult", title: "Keltner multiplier", type: "float", defval: 1.5, min: 0.1, max: 50, step: 0.1 }
26648
+ { ...lengthInput(20, "length", "Length", 5e3, "Shared lookback for the Bollinger Bands, the Keltner Channels, and the momentum regression. John Carter's original setting is 20."), min: 2 },
26649
+ sourceInput("Close", "source", "Source", "Price series used for the shared moving average basis, the standard deviation, and the momentum displacement."),
26650
+ floatInput("bbMult", "Bollinger Bands Multiplier", 2, 0.1, 50, 0.1, "Standard deviation multiplier for the Bollinger Bands."),
26651
+ floatInput("kcMult", "Keltner Channels Multiplier", 1.5, 0.1, 50, 0.1, "ATR multiplier for the Keltner Channels. A smaller multiplier demands a tighter compression before the squeeze turns on."),
26652
+ colorInput(BULLISH, "Momentum Up", "bullColor", "Histogram color while momentum is positive. Bars building away from zero print solid, bars fading back print dimmed."),
26653
+ colorInput(BEARISH, "Momentum Down", "bearColor", "Histogram color while momentum is negative. Bars building away from zero print solid, bars fading back print dimmed."),
26654
+ colorInput(WARNING, "Squeeze On", "squeezeOnColor", "Zero-line dot color while both Bollinger Bands sit fully inside the Keltner Channels."),
26655
+ colorInput(NEUTRAL, "Squeeze Off", "squeezeOffColor", "Zero-line dot color while the Bollinger Bands trade outside the Keltner Channels."),
26656
+ boolInput("markFires", "Mark Squeeze Fires", true, STYLE, "Draws a cross on the zero line on the first bar where the bands re-emerge outside the channels, colored by the side of the momentum histogram.")
25684
26657
  ],
25685
26658
  compute: (bars, inputs) => {
25686
26659
  const len = num3(inputs, "length", 20);
25687
- const c = closes(bars);
25688
- const basis = sma(c, len);
25689
- const dev = map(stdev(c, len), (x) => x * num3(inputs, "bbMult", 2));
25690
- const kcRange = map(rma(trueRange(bars), len), (x) => x * num3(inputs, "kcMult", 1.5));
25691
- const donchianMid = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => (h + l) / 2);
25692
- const anchor = zip(donchianMid, basis, (d, b) => (d + b) / 2);
25693
- const mom = linreg(sub(c, anchor), len);
25694
- const squeezed = c.map((_, i) => {
26660
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
26661
+ const basis = sma(src, len);
26662
+ const dev = map(stdev(src, len), (x) => x * num3(inputs, "bbMult", 2));
26663
+ const kcRange = map(atr(bars, len), (x) => x * num3(inputs, "kcMult", 1.5));
26664
+ const rangeMid = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => (h + l) / 2);
26665
+ const anchor = zip(rangeMid, basis, (d, b) => (d + b) / 2);
26666
+ const mom = linreg(sub(src, anchor), len, 0);
26667
+ const squeezeOn = basis.map((b, i) => {
25695
26668
  const d = dev[i];
25696
26669
  const r = kcRange[i];
25697
- return Number.isFinite(d) && Number.isFinite(r) ? d < r : false;
26670
+ if (!Number.isFinite(b) || !Number.isFinite(d) || !Number.isFinite(r)) return null;
26671
+ return d < r;
25698
26672
  });
25699
- const stateColors = squeezed.map((s, i) => Number.isFinite(mom[i]) ? s ? BEARISH : BULLISH : null);
25700
- return {
25701
- plots: [
25702
- { key: "mom", title: "Momentum", values: mom, kind: "histogram", color: BULLISH, colors: histColors(mom), base: 0 },
25703
- { key: "squeeze", title: "Squeeze", values: mom.map((x) => Number.isFinite(x) ? 0 : Number.NaN), kind: "cross", color: INFO, colors: stateColors }
25704
- ]
25705
- };
26673
+ const bull = str3(inputs, "bullColor", BULLISH);
26674
+ const bear = str3(inputs, "bearColor", BEARISH);
26675
+ const onInk = str3(inputs, "squeezeOnColor", WARNING);
26676
+ const offInk = str3(inputs, "squeezeOffColor", NEUTRAL);
26677
+ const momColors = mom.map((x, i) => {
26678
+ if (!Number.isFinite(x)) return null;
26679
+ const prev2 = i > 0 && Number.isFinite(mom[i - 1]) ? mom[i - 1] : x;
26680
+ const rising = x > prev2;
26681
+ return x >= 0 ? rising ? bull : transp(bull, 60) : rising ? transp(bear, 60) : bear;
26682
+ });
26683
+ const plots = [
26684
+ { key: "mom", title: "Momentum", values: mom, kind: "columns", color: bull, colors: momColors, base: 0 },
26685
+ {
26686
+ key: "state",
26687
+ title: "Squeeze State",
26688
+ values: squeezeOn.map((s) => s == null ? Number.NaN : 0),
26689
+ kind: "circles",
26690
+ color: offInk,
26691
+ width: 2,
26692
+ colors: squeezeOn.map((s) => s == null ? null : s ? onInk : offInk)
26693
+ }
26694
+ ];
26695
+ if (bool(inputs, "markFires", true)) {
26696
+ const fires = squeezeOn.map((s, i) => s === false && squeezeOn[i - 1] === true ? 0 : Number.NaN);
26697
+ plots.push({
26698
+ key: "fire",
26699
+ title: "Squeeze Fire",
26700
+ values: fires,
26701
+ kind: "cross",
26702
+ color: offInk,
26703
+ width: 3,
26704
+ colors: fires.map((x, i) => Number.isFinite(x) ? Number.isFinite(mom[i]) ? mom[i] >= 0 ? bull : bear : offInk : null)
26705
+ });
26706
+ }
26707
+ return { plots };
25706
26708
  }
25707
26709
  };
25708
26710
  var oscillatorSpecs = [
@@ -25714,7 +26716,7 @@ var oscillatorSpecs = [
25714
26716
  awesome,
25715
26717
  cci,
25716
26718
  williamsR,
25717
- cmo,
26719
+ cmoSpec,
25718
26720
  connorsRsi,
25719
26721
  fisher,
25720
26722
  trix,
@@ -25738,15 +26740,19 @@ var aroon = {
25738
26740
  type: "aroon",
25739
26741
  title: "Aroon",
25740
26742
  overlay: false,
25741
- inputs: [lengthInput(14)],
26743
+ inputs: [
26744
+ lengthInput(14, "length", "Length", 5e3, "Lookback length used to locate the highest high and lowest low."),
26745
+ colorInput(BULLISH, "Aroon Up", "upColor", "Color of the Aroon Up line."),
26746
+ colorInput(BEARISH, "Aroon Down", "downColor", "Color of the Aroon Down line.")
26747
+ ],
25742
26748
  compute: (bars, inputs) => {
25743
26749
  const len = num3(inputs, "length", 14);
25744
- const up = map(barsSinceHighest(highs(bars), len + 1), (x) => 100 * (len - x) / len);
25745
- const down = map(barsSinceLowest(lows(bars), len + 1), (x) => 100 * (len - x) / len);
26750
+ const up = map(barsSinceHighest(highs(bars), len), (x) => 100 * (len - x) / len);
26751
+ const down = map(barsSinceLowest(lows(bars), len), (x) => 100 * (len - x) / len);
25746
26752
  return {
25747
26753
  plots: [
25748
- { key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
25749
- { key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
26754
+ { key: "up", title: "Aroon Up", values: up, color: str3(inputs, "upColor", BULLISH) },
26755
+ { key: "down", title: "Aroon Down", values: down, color: str3(inputs, "downColor", BEARISH) }
25750
26756
  ]
25751
26757
  };
25752
26758
  }
@@ -25756,10 +26762,16 @@ var adx = {
25756
26762
  title: "Average Directional Index",
25757
26763
  shortTitle: "ADX",
25758
26764
  overlay: false,
25759
- inputs: [lengthInput(14, "diLength", "DI length"), lengthInput(14, "adxLength", "ADX smoothing")],
26765
+ inputs: [
26766
+ lengthInput(14, "diLength", "DI Length", 5e3, "Wilder RMA smoothing length applied to directional movement and true range when computing +DI and \u2212DI."),
26767
+ lengthInput(14, "adxLength", "ADX Smoothing", 5e3, "Wilder RMA smoothing length applied to DX to obtain the ADX line."),
26768
+ floatInput("keyLevel", "Key Level", 25, 0, 100, 1, "Trend strength reference level; ADX above it is commonly read as a trending market."),
26769
+ colorInput(BULLISH, "+DI Color", "plusDiColor", "Color of the plus directional indicator, and of the ADX line above the key level while +DI leads."),
26770
+ colorInput(BEARISH, "-DI Color", "minusDiColor", "Color of the minus directional indicator, and of the ADX line above the key level while \u2212DI leads."),
26771
+ colorInput(NEUTRAL, "ADX Color", "adxColor", "Color of the ADX line while it sits below the key level \u2014 no trend strong enough to trade.")
26772
+ ],
25760
26773
  compute: (bars, inputs) => {
25761
26774
  const diLen = num3(inputs, "diLength", 14);
25762
- const adxLen = num3(inputs, "adxLength", 14);
25763
26775
  const n = bars.length;
25764
26776
  const plusDm = new Array(n).fill(Number.NaN);
25765
26777
  const minusDm = new Array(n).fill(Number.NaN);
@@ -25773,13 +26785,23 @@ var adx = {
25773
26785
  const plusDi = zip(rma(plusDm, diLen), atrLine, (d, a) => a === 0 ? 0 : 100 * d / a);
25774
26786
  const minusDi = zip(rma(minusDm, diLen), atrLine, (d, a) => a === 0 ? 0 : 100 * d / a);
25775
26787
  const dx = zip(plusDi, minusDi, (p, m) => p + m === 0 ? 0 : 100 * Math.abs(p - m) / (p + m));
26788
+ const adxLine = rma(dx, num3(inputs, "adxLength", 14));
26789
+ const plusInk = str3(inputs, "plusDiColor", BULLISH);
26790
+ const minusInk = str3(inputs, "minusDiColor", BEARISH);
26791
+ const neutralInk = str3(inputs, "adxColor", NEUTRAL);
26792
+ const keyLevel = num3(inputs, "keyLevel", 25);
26793
+ const adxColors = adxLine.map((x, i) => {
26794
+ if (!Number.isFinite(x)) return null;
26795
+ if (x <= keyLevel) return neutralInk;
26796
+ return (plusDi[i] ?? 0) > (minusDi[i] ?? 0) ? plusInk : minusInk;
26797
+ });
25776
26798
  return {
25777
26799
  plots: [
25778
- { key: "adx", title: "ADX", values: rma(dx, adxLen), color: SERIES_LINE, width: 2 },
25779
- { key: "plusDi", title: "+DI", values: plusDi, color: BULLISH },
25780
- { key: "minusDi", title: "-DI", values: minusDi, color: BEARISH }
26800
+ { key: "plusDi", title: "+DI", values: plusDi, color: transp(plusInk, 30) },
26801
+ { key: "minusDi", title: "-DI", values: minusDi, color: transp(minusInk, 30) },
26802
+ { key: "adx", title: "ADX", values: adxLine, color: neutralInk, width: 2, colors: adxColors }
25781
26803
  ],
25782
- levels: [{ key: "threshold", price: 25, color: NEUTRAL, lineStyle: "dotted" }]
26804
+ levels: [{ key: "keyLevel", price: keyLevel, color: NEUTRAL, lineStyle: "dashed", title: "Key Level" }]
25783
26805
  };
25784
26806
  }
25785
26807
  };
@@ -25788,7 +26810,12 @@ var vortex = {
25788
26810
  title: "Vortex Indicator",
25789
26811
  shortTitle: "VI",
25790
26812
  overlay: false,
25791
- inputs: [lengthInput(14)],
26813
+ inputs: [
26814
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to sum the vortex movements (VM+ and VM\u2212) and the true range."),
26815
+ colorInput(BULLISH, "VI+", "plusColor", "Color of the positive vortex line (VI+)."),
26816
+ colorInput(BEARISH, "VI-", "minusColor", "Color of the negative vortex line (VI\u2212)."),
26817
+ colorInput(NEUTRAL, "Baseline", "baselineColor", "Color of the dotted baseline drawn at 1.")
26818
+ ],
25792
26819
  compute: (bars, inputs) => {
25793
26820
  const len = num3(inputs, "length", 14);
25794
26821
  const n = bars.length;
@@ -25801,25 +26828,48 @@ var vortex = {
25801
26828
  const trSum = sum(trueRange(bars), len);
25802
26829
  return {
25803
26830
  plots: [
25804
- { key: "viPlus", title: "VI+", values: zip(sum(vmPlus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: BULLISH, width: 2 },
25805
- { key: "viMinus", title: "VI-", values: zip(sum(vmMinus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: BEARISH, width: 2 }
26831
+ { key: "viPlus", title: "VI+", values: zip(sum(vmPlus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: str3(inputs, "plusColor", BULLISH) },
26832
+ { key: "viMinus", title: "VI-", values: zip(sum(vmMinus, len), trSum, (v, t) => t === 0 ? Number.NaN : v / t), color: str3(inputs, "minusColor", BEARISH) }
25806
26833
  ],
25807
- levels: [{ key: "one", price: 1, color: NEUTRAL, lineStyle: "dotted" }]
26834
+ levels: [{ key: "baseline", price: 1, color: str3(inputs, "baselineColor", NEUTRAL), lineStyle: "dotted", title: "Baseline" }]
25808
26835
  };
25809
26836
  }
25810
26837
  };
25811
26838
  var trendSpecs = [aroon, adx, vortex];
25812
26839
 
25813
26840
  // src/core/native-indicators/classics/volatility.ts
26841
+ function expansionColors(values, calm, expanding) {
26842
+ return values.map((x, i) => {
26843
+ if (!Number.isFinite(x)) return null;
26844
+ const prev2 = i > 0 && Number.isFinite(values[i - 1]) ? values[i - 1] : x;
26845
+ return x > prev2 ? expanding : calm;
26846
+ });
26847
+ }
26848
+ function washedToZero(key, title, values, colors, fallback, valueTransparency, width = 1) {
26849
+ return {
26850
+ plots: [{ key, title, values, color: fallback, width, colors }, anchorPlot("zeroAnchor", "Zero Anchor", values.length, 0)],
26851
+ bands: [{ key: "wash", from: key, to: "zeroAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, 0, (_x, i) => colors[i] ?? fallback, valueTransparency) }]
26852
+ };
26853
+ }
25814
26854
  var atrSpec = {
25815
26855
  type: "average-true-range",
25816
26856
  title: "Average True Range",
25817
26857
  shortTitle: "ATR",
25818
26858
  overlay: false,
25819
- inputs: [lengthInput(14), colorInput()],
25820
- compute: (bars, inputs) => ({
25821
- plots: [{ key: "atr", title: "ATR", values: atr(bars, num3(inputs, "length", 14)), color: str3(inputs, "color", SERIES_LINE), width: 2 }]
25822
- })
26859
+ inputs: [
26860
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to average the true range."),
26861
+ optionInput("smoothing", "Smoothing", "RMA", ["RMA", "SMA", "EMA", "WMA"], "Moving average applied to the true range. RMA is Wilder's original smoothing used by the classic ATR."),
26862
+ colorInput(NEUTRAL, "ATR Line", "color", "Color of the ATR line while volatility contracts or holds flat."),
26863
+ colorInput(WARNING, "Expanding", "expandColor", "Color of the ATR line while it rises \u2014 volatility expanding bar to bar.")
26864
+ ],
26865
+ compute: (bars, inputs) => {
26866
+ const len = num3(inputs, "length", 14);
26867
+ const tr = trueRange(bars);
26868
+ const kind = str3(inputs, "smoothing", "RMA");
26869
+ const values = kind === "SMA" ? sma(tr, len) : kind === "EMA" ? ema(tr, len) : kind === "WMA" ? wma(tr, len) : rma(tr, len);
26870
+ const calm = str3(inputs, "color", NEUTRAL);
26871
+ return washedToZero("atr", "ATR", values, expansionColors(values, calm, str3(inputs, "expandColor", WARNING)), calm, 75);
26872
+ }
25823
26873
  };
25824
26874
  var historicalVolatility = {
25825
26875
  type: "historical-volatility",
@@ -25827,14 +26877,23 @@ var historicalVolatility = {
25827
26877
  shortTitle: "HV",
25828
26878
  overlay: false,
25829
26879
  inputs: [
25830
- lengthInput(10),
25831
- { 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)" },
25832
- colorInput()
26880
+ { ...lengthInput(10, "length", "Length", 5e3, "Lookback window in bars for the standard deviation of close-to-close log returns."), min: 2 },
26881
+ boolInput("annualize", "Annualize", true, void 0, "Scale the per-bar volatility to an annual figure by multiplying by the square root of the periods per year."),
26882
+ intInput("periodsPerYear", "Periods Per Year", 252, 1, 1e5, "Number of periods in a year used for annualization: 252 for daily bars, 52 for weekly, 12 for monthly."),
26883
+ colorInput(WARNING, "Volatility Line", "color", "Color of the line at its recent highs; it fades toward grey as volatility contracts.")
25833
26884
  ],
25834
26885
  compute: (bars, inputs) => {
25835
- const logReturns = map(change(map(closes(bars), Math.log)), (x) => x);
25836
- const values = map(stdev(logReturns, num3(inputs, "length", 10)), (x) => x * Math.sqrt(num3(inputs, "annual", 365)) * 100);
25837
- return { plots: [{ key: "hv", title: "HV", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
26886
+ const len = num3(inputs, "length", 10);
26887
+ const logReturns = sub(map(closes(bars), Math.log), shift(map(closes(bars), Math.log), 1));
26888
+ const meanReturn = map(sum(logReturns, len), (s) => s / len);
26889
+ const squaredSum = sum(map(logReturns, (r) => r * r), len);
26890
+ const factor = bool(inputs, "annualize", true) ? Math.sqrt(num3(inputs, "periodsPerYear", 252)) : 1;
26891
+ const values = zip(squaredSum, meanReturn, (sq, m) => Math.sqrt(Math.max(sq - len * m * m, 0) / (len - 1)) * factor * 100);
26892
+ const low = lowest(values, 100);
26893
+ const high = highest(values, 100);
26894
+ const accent = str3(inputs, "color", WARNING);
26895
+ const colors = values.map((x, i) => Number.isFinite(x) ? gradient(x, low[i] ?? 0, high[i] ?? 0, NEUTRAL, accent) : null);
26896
+ return washedToZero("hv", "Historical Volatility", values, colors, accent, 75, 2);
25838
26897
  }
25839
26898
  };
25840
26899
  var chaikinVolatility = {
@@ -25842,13 +26901,26 @@ var chaikinVolatility = {
25842
26901
  title: "Chaikin Volatility",
25843
26902
  shortTitle: "CHV",
25844
26903
  overlay: false,
25845
- inputs: [lengthInput(10, "emaLength", "EMA length"), lengthInput(10, "rocLength", "RoC length"), colorInput()],
26904
+ inputs: [
26905
+ lengthInput(10, "emaLength", "EMA Length", 5e3, "Length of the exponential moving average applied to the bar high-low range."),
26906
+ lengthInput(10, "rocLength", "ROC Length", 5e3, "Number of bars used for the rate of change of the smoothed high-low range."),
26907
+ colorInput(NEUTRAL, "CHV Color", "color", "Color of the Chaikin Volatility line."),
26908
+ colorInput(WARNING, "Expansion", "expansionColor", "Line and fill color while the reading is above zero \u2014 the smoothed range is wider than it was ROC Length bars ago.")
26909
+ ],
25846
26910
  compute: (bars, inputs) => {
25847
26911
  const range = ema(sub(highs(bars), lows(bars)), num3(inputs, "emaLength", 10));
25848
- return {
25849
- plots: [{ key: "cv", title: "Chaikin Vol", values: roc(range, num3(inputs, "rocLength", 10)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25850
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
25851
- };
26912
+ const prior = shift(range, num3(inputs, "rocLength", 10));
26913
+ const values = zip(range, prior, (r, p) => p === 0 ? Number.NaN : 100 * (r - p) / p);
26914
+ const calm = str3(inputs, "color", NEUTRAL);
26915
+ const expansion = str3(inputs, "expansionColor", WARNING);
26916
+ return centeredOscillator({
26917
+ key: "chv",
26918
+ title: "Chaikin Volatility",
26919
+ values,
26920
+ // Only expansion is a state worth accenting; contraction stays structural grey.
26921
+ ink: { bull: expansion, bear: calm, neutral: calm },
26922
+ zeroInk: NEUTRAL
26923
+ });
25852
26924
  }
25853
26925
  };
25854
26926
  var massIndex = {
@@ -25856,14 +26928,42 @@ var massIndex = {
25856
26928
  title: "Mass Index",
25857
26929
  shortTitle: "MI",
25858
26930
  overlay: false,
25859
- inputs: [lengthInput(25), colorInput()],
26931
+ inputs: [
26932
+ lengthInput(25, "length", "Sum Length", 5e3, "Number of bars over which the single/double EMA ratio is summed."),
26933
+ lengthInput(9, "emaLength", "EMA Length", 5e3, "Length of the single EMA of the high-low range and of its double smoothing."),
26934
+ floatInput("setupLevel", "Setup Level", 27, 0, 1e3, 0.1, "Reversal bulge set-up level; a rise above it arms the pattern."),
26935
+ floatInput("triggerLevel", "Trigger Level", 26.5, 0, 1e3, 0.1, "Reversal bulge trigger level; a decline below it after a set-up completes the pattern."),
26936
+ colorInput(NEUTRAL, "Mass Index", "color", "Line color while no reversal bulge is armed."),
26937
+ colorInput(WARNING, "Armed", "armedColor", "Line color from the rise above the setup level until the decline below the trigger level."),
26938
+ colorInput(NEUTRAL, "Levels", "levelsColor", "Reversal bulge levels color.")
26939
+ ],
25860
26940
  compute: (bars, inputs) => {
25861
- const range = sub(highs(bars), lows(bars));
25862
- const single = ema(range, 9);
25863
- const ratio = zip(single, ema(single, 9), (a, b) => b === 0 ? Number.NaN : a / b);
26941
+ const emaLen = num3(inputs, "emaLength", 9);
26942
+ const single = ema(sub(highs(bars), lows(bars)), emaLen);
26943
+ const ratio = zip(single, ema(single, emaLen), (a, b) => b === 0 ? 1 : a / b);
26944
+ const values = sum(ratio, num3(inputs, "length", 25));
26945
+ const setup = num3(inputs, "setupLevel", 27);
26946
+ const trigger = num3(inputs, "triggerLevel", 26.5);
26947
+ const calm = str3(inputs, "color", NEUTRAL);
26948
+ const armedInk = str3(inputs, "armedColor", WARNING);
26949
+ const levelInk = str3(inputs, "levelsColor", NEUTRAL);
26950
+ let armed = false;
26951
+ const colors = values.map((x) => {
26952
+ if (!Number.isFinite(x)) return null;
26953
+ armed = x > setup ? true : x < trigger ? false : armed;
26954
+ return armed ? armedInk : calm;
26955
+ });
25864
26956
  return {
25865
- plots: [{ key: "mi", title: "Mass Index", values: sum(ratio, num3(inputs, "length", 25)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25866
- levels: [{ key: "reversal", price: 27, color: NEUTRAL, lineStyle: "dotted" }]
26957
+ plots: [
26958
+ { key: "mi", title: "Mass Index", values, color: calm, colors },
26959
+ anchorPlot("setupAnchor", "Setup Anchor", values.length, setup),
26960
+ anchorPlot("triggerAnchor", "Trigger Anchor", values.length, trigger)
26961
+ ],
26962
+ bands: [{ key: "bulge", from: "setupAnchor", to: "triggerAnchor", color: transp(armedInk, 90) }],
26963
+ levels: [
26964
+ { key: "setup", price: setup, color: levelInk, lineStyle: "dashed", title: "Setup Level" },
26965
+ { key: "trigger", price: trigger, color: levelInk, lineStyle: "dashed", title: "Trigger Level" }
26966
+ ]
25867
26967
  };
25868
26968
  }
25869
26969
  };
@@ -25872,30 +26972,83 @@ var standardDeviation = {
25872
26972
  title: "Standard Deviation",
25873
26973
  shortTitle: "StdDev",
25874
26974
  overlay: false,
25875
- inputs: [lengthInput(20), sourceInput(), colorInput()],
25876
- compute: (bars, inputs) => ({
25877
- plots: [
25878
- {
25879
- key: "stdev",
25880
- title: "StdDev",
25881
- values: stdev(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20)),
25882
- color: str3(inputs, "color", SERIES_LINE),
25883
- width: 2
25884
- }
25885
- ]
25886
- })
26975
+ inputs: [
26976
+ lengthInput(20, "length", "Length", 5e3, "Number of bars used to compute the standard deviation."),
26977
+ sourceInput("Close", "source", "Source", "Price series the standard deviation is calculated on."),
26978
+ colorInput(NEUTRAL, "Standard Deviation", "color", "Line color while volatility contracts or holds flat."),
26979
+ colorInput(WARNING, "Expanding", "expandColor", "Line and fill color while volatility expands bar to bar.")
26980
+ ],
26981
+ compute: (bars, inputs) => {
26982
+ const values = stdev(sourceValues(bars, str3(inputs, "source", "Close")), num3(inputs, "length", 20));
26983
+ const calm = str3(inputs, "color", NEUTRAL);
26984
+ return washedToZero("stdev", "Standard Deviation", values, expansionColors(values, calm, str3(inputs, "expandColor", WARNING)), calm, 80);
26985
+ }
25887
26986
  };
25888
26987
  var ulcerIndex = {
25889
26988
  type: "ulcer-index",
25890
26989
  title: "Ulcer Index",
25891
26990
  overlay: false,
25892
- inputs: [lengthInput(14), colorInput()],
26991
+ inputs: [
26992
+ { ...lengthInput(14, "length", "Length", 500, "Lookback window. At each bar the percentage drawdown from the window's running maximum is squared; the squares are averaged and the root taken."), min: 2 },
26993
+ sourceInput("Close", "source", "Source", "Price series the Ulcer Index is computed on. Close is the standard choice."),
26994
+ floatInput("threshold", "Alert Threshold", 5, 0, 1e3, 0.25, "Reference level drawn as a dashed line. The index scales with the instrument's volatility and the window length, so set it relative to its own history."),
26995
+ colorInput(NEUTRAL, "Ulcer Index", "color", "Color of the index near zero; it blends toward the stress color as the reading approaches the threshold."),
26996
+ colorInput(BEARISH, "Stress", "stressColor", "Color of the index at and beyond the alert threshold."),
26997
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Vertical gradient between zero and the Ulcer Index, fully transparent at zero."),
26998
+ boolInput("showDrawdown", "Drawdown %", false, STYLE, "Plot the current percentage drawdown from the window high as columns below zero \u2014 the raw ingredient the index averages."),
26999
+ colorInput(transp(BEARISH, 50), "Drawdown Color", "drawdownColor", "Color of the drawdown columns."),
27000
+ colorInput(NEUTRAL, "Threshold", "thresholdColor", "Color of the alert threshold line.")
27001
+ ],
25893
27002
  compute: (bars, inputs) => {
25894
27003
  const len = num3(inputs, "length", 14);
25895
- const c = closes(bars);
25896
- const drawdown = zip(c, highest(c, len), (x, h) => h === 0 ? 0 : 100 * (x - h) / h);
25897
- const values = map(sma(map(drawdown, (d) => d * d), len), Math.sqrt);
25898
- return { plots: [{ key: "ui", title: "Ulcer", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
27004
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
27005
+ const n = src.length;
27006
+ const values = new Array(n).fill(Number.NaN);
27007
+ const drawdown = new Array(n).fill(Number.NaN);
27008
+ for (let i = len - 1; i < n; i++) {
27009
+ let runMax = Number.NaN;
27010
+ let sumSq = 0;
27011
+ let valid = true;
27012
+ let last = Number.NaN;
27013
+ for (let k = i - len + 1; k <= i; k++) {
27014
+ const price = src[k];
27015
+ if (!Number.isFinite(price)) {
27016
+ valid = false;
27017
+ break;
27018
+ }
27019
+ runMax = Number.isFinite(runMax) ? Math.max(runMax, price) : price;
27020
+ if (runMax <= 0) {
27021
+ valid = false;
27022
+ break;
27023
+ }
27024
+ last = 100 * (price - runMax) / runMax;
27025
+ sumSq += last * last;
27026
+ }
27027
+ if (!valid) continue;
27028
+ values[i] = Math.sqrt(sumSq / len);
27029
+ drawdown[i] = last;
27030
+ }
27031
+ const calm = str3(inputs, "color", NEUTRAL);
27032
+ const stress = str3(inputs, "stressColor", BEARISH);
27033
+ const threshold = num3(inputs, "threshold", 5);
27034
+ const colors = values.map((x) => Number.isFinite(x) ? threshold > 0 ? gradient(x, 0, threshold, calm, stress) : stress : null);
27035
+ const plots = [{ key: "ui", title: "Ulcer Index", values, color: calm, colors }];
27036
+ const bands = [];
27037
+ if (bool(inputs, "gradientFill", true)) {
27038
+ plots.push(anchorPlot("zeroAnchor", "Zero Base", n, 0));
27039
+ bands.push({ key: "wash", from: "ui", to: "zeroAnchor", color: transp(NEUTRAL, 100), gradient: baselineGradient(values, 0, (_x, i) => colors[i] ?? calm) });
27040
+ }
27041
+ if (bool(inputs, "showDrawdown", false)) {
27042
+ plots.push({ key: "dd", title: "Drawdown %", values: drawdown, kind: "columns", color: str3(inputs, "drawdownColor", transp(BEARISH, 50)), base: 0 });
27043
+ }
27044
+ return {
27045
+ plots,
27046
+ bands,
27047
+ levels: [
27048
+ { key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dotted", title: "Zero Level" },
27049
+ { key: "threshold", price: threshold, color: str3(inputs, "thresholdColor", NEUTRAL), lineStyle: "dashed", title: "Alert Threshold" }
27050
+ ]
27051
+ };
25899
27052
  }
25900
27053
  };
25901
27054
  var choppiness = {
@@ -25903,19 +27056,33 @@ var choppiness = {
25903
27056
  title: "Choppiness Index",
25904
27057
  shortTitle: "CHOP",
25905
27058
  overlay: false,
25906
- inputs: [lengthInput(14), colorInput()],
27059
+ inputs: [
27060
+ { ...lengthInput(14, "length", "Length", 5e3, "Number of bars used to sum true ranges and to measure the highest high \u2212 lowest low range."), min: 2 },
27061
+ floatInput("upperBand", "Upper Band", 61.8, 0, 100, 0.1, "Readings above this level indicate a choppy, range-bound market."),
27062
+ floatInput("lowerBand", "Lower Band", 38.2, 0, 100, 0.1, "Readings below this level indicate a strong trend."),
27063
+ colorInput(NEUTRAL, "CHOP Line", "color", "Color of the Choppiness Index line."),
27064
+ colorInput(BULLISH, "Trending", "trendingColor", "Line, fill and zone color toward and below the lower band \u2014 a trending market."),
27065
+ colorInput(BEARISH, "Choppy", "choppyColor", "Line, fill and zone color toward and above the upper band \u2014 a choppy, range-bound market."),
27066
+ colorInput(transp(NEUTRAL, 90), "Band Fill", "fillColor", "Fill color of the zone between the upper and lower bands.")
27067
+ ],
25907
27068
  compute: (bars, inputs) => {
25908
27069
  const len = num3(inputs, "length", 14);
25909
27070
  const trSum = sum(trueRange(bars), len);
25910
27071
  const span = zip(highest(highs(bars), len), lowest(lows(bars), len), (h, l) => h - l);
25911
27072
  const values = zip(trSum, span, (t, s) => s > 0 && t > 0 ? 100 * Math.log10(t / s) / Math.log10(len) : Number.NaN);
25912
- return {
25913
- plots: [{ key: "chop", title: "CHOP", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25914
- levels: [
25915
- { key: "upper", price: 61.8, color: NEUTRAL },
25916
- { key: "lower", price: 38.2, color: NEUTRAL }
25917
- ]
25918
- };
27073
+ return boundedOscillator({
27074
+ key: "chop",
27075
+ title: "CHOP",
27076
+ values,
27077
+ scale: { top: 100, bottom: 0 },
27078
+ overbought: num3(inputs, "upperBand", 61.8),
27079
+ oversold: num3(inputs, "lowerBand", 38.2),
27080
+ ink: { low: str3(inputs, "trendingColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "choppyColor", BEARISH) },
27081
+ levelInk: NEUTRAL,
27082
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 90)),
27083
+ midpointWash: true,
27084
+ levelTitles: { overbought: "Upper Band", oversold: "Lower Band" }
27085
+ });
25919
27086
  }
25920
27087
  };
25921
27088
  var volatilitySpecs = [atrSpec, historicalVolatility, chaikinVolatility, massIndex, standardDeviation, ulcerIndex, choppiness];
@@ -25928,17 +27095,85 @@ function moneyFlowVolume(bars) {
25928
27095
  return (2 * b.close - b.high - b.low) / (b.high - b.low) * b.volume;
25929
27096
  });
25930
27097
  }
27098
+ function slopeColors2(values, bull, bear, flat) {
27099
+ return values.map((x, i) => {
27100
+ if (!Number.isFinite(x)) return null;
27101
+ const prev2 = i > 0 ? values[i - 1] : Number.NaN;
27102
+ if (!Number.isFinite(prev2)) return flat;
27103
+ return x > prev2 ? bull : x < prev2 ? bear : flat;
27104
+ });
27105
+ }
27106
+ function volumeIndex(bars, src, base, when) {
27107
+ const out = new Array(bars.length).fill(Number.NaN);
27108
+ let index = base;
27109
+ for (let i = 0; i < bars.length; i++) {
27110
+ const v = bars[i].volume;
27111
+ const pv = i > 0 ? bars[i - 1].volume : null;
27112
+ const prev2 = i > 0 ? src[i - 1] : Number.NaN;
27113
+ const active = v != null && pv != null && Number.isFinite(v) && Number.isFinite(pv) && when(v, pv);
27114
+ if (active && Number.isFinite(prev2) && prev2 !== 0) index *= 1 + (src[i] - prev2) / prev2;
27115
+ out[i] = index;
27116
+ }
27117
+ return out;
27118
+ }
27119
+ function regimeIndex(key, title, values, signal, ink, showSignal, gradientFill, singleColor = false) {
27120
+ const inkOf = (i) => {
27121
+ const s = signal[i];
27122
+ const x = values[i];
27123
+ if (singleColor || !Number.isFinite(s)) return ink.neutral;
27124
+ return x > s ? ink.bull : x < s ? ink.bear : ink.neutral;
27125
+ };
27126
+ const plots = [
27127
+ {
27128
+ key: "signal",
27129
+ title: "Signal EMA",
27130
+ values: signal,
27131
+ color: ink.signal,
27132
+ ...showSignal ? {} : { display: { pane: false } }
27133
+ },
27134
+ { key, title, values, color: ink.neutral, width: 2, colors: values.map((x, i) => Number.isFinite(x) ? inkOf(i) : null) }
27135
+ ];
27136
+ const bands = gradientFill ? [
27137
+ {
27138
+ key: "regime",
27139
+ from: key,
27140
+ to: "signal",
27141
+ color: transp(NEUTRAL, 100),
27142
+ gradient: values.map((x, i) => {
27143
+ const s = signal[i];
27144
+ if (!Number.isFinite(x) || !Number.isFinite(s)) return null;
27145
+ const above = x > s;
27146
+ const c = above ? ink.bull : ink.bear;
27147
+ return { topValue: Math.max(x, s), bottomValue: Math.min(x, s), topColor: transp(c, above ? 50 : 100), bottomColor: transp(c, above ? 100 : 50) };
27148
+ })
27149
+ }
27150
+ ] : [];
27151
+ return { plots, bands };
27152
+ }
25931
27153
  var obv = {
25932
27154
  type: "on-balance-volume",
25933
27155
  title: "On Balance Volume",
25934
27156
  shortTitle: "OBV",
25935
27157
  overlay: false,
25936
- inputs: [colorInput()],
27158
+ inputs: [
27159
+ boolInput("smoothing", "Smoothing MA", false, SETTINGS, "Display a moving average of OBV to act as a smoothing/signal line."),
27160
+ optionInput("maType", "MA Type", "SMA", ["SMA", "EMA", "WMA", "RMA (SMMA)"], "Moving average type used for the smoothing MA."),
27161
+ lengthInput(20, "maLength", "MA Length", 5e3, "Number of bars used for the smoothing MA."),
27162
+ colorInput(INFO, "OBV", "color", "Color of the OBV line."),
27163
+ colorInput(WARNING, "Smoothing MA", "maColor", "Color of the smoothing MA line.")
27164
+ ],
25937
27165
  compute: (bars, inputs) => {
25938
27166
  const vol = volumes(bars);
25939
27167
  const d = change(closes(bars));
25940
- const signed2 = zip(vol, d, (v, x) => x > 0 ? v : x < 0 ? -v : 0);
25941
- return { plots: [{ key: "obv", title: "OBV", values: cumSum(signed2), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
27168
+ const values = cumSum(zip(vol, d, (v, x) => x > 0 ? v : x < 0 ? -v : 0));
27169
+ const plots = [{ key: "obv", title: "OBV", values, color: str3(inputs, "color", INFO) }];
27170
+ if (bool(inputs, "smoothing", false)) {
27171
+ const len = num3(inputs, "maLength", 20);
27172
+ const kind = str3(inputs, "maType", "SMA");
27173
+ const ma = kind === "EMA" ? ema(values, len) : kind === "WMA" ? wma(values, len) : kind === "RMA (SMMA)" ? rma(values, len) : sma(values, len);
27174
+ plots.push({ key: "ma", title: "Smoothing MA", values: ma, color: str3(inputs, "maColor", WARNING) });
27175
+ }
27176
+ return { plots };
25942
27177
  }
25943
27178
  };
25944
27179
  var accDist = {
@@ -25946,24 +27181,50 @@ var accDist = {
25946
27181
  title: "Accumulation / Distribution",
25947
27182
  shortTitle: "A/D",
25948
27183
  overlay: false,
25949
- inputs: [colorInput()],
25950
- compute: (bars, inputs) => ({
25951
- plots: [{ key: "ad", title: "A/D", values: cumSum(moneyFlowVolume(bars)), color: str3(inputs, "color", SERIES_LINE), width: 2 }]
25952
- })
27184
+ inputs: [
27185
+ boolInput("slopeColoring", "Slope Coloring", true, STYLE, "Color the line by its slope \u2014 bullish while rising (accumulation), bearish while falling (distribution)."),
27186
+ colorInput(NEUTRAL, "A/D Line", "color", "Color of the line when slope coloring is disabled or the line is flat."),
27187
+ colorInput(BULLISH, "Rising", "bullColor", "Line color while the A/D Line rises."),
27188
+ colorInput(BEARISH, "Falling", "bearColor", "Line color while the A/D Line falls.")
27189
+ ],
27190
+ compute: (bars, inputs) => {
27191
+ const values = cumSum(moneyFlowVolume(bars));
27192
+ const flat = str3(inputs, "color", NEUTRAL);
27193
+ return {
27194
+ plots: [
27195
+ {
27196
+ key: "ad",
27197
+ title: "A/D",
27198
+ values,
27199
+ color: flat,
27200
+ ...bool(inputs, "slopeColoring", true) ? { colors: slopeColors2(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), flat) } : {}
27201
+ }
27202
+ ]
27203
+ };
27204
+ }
25953
27205
  };
25954
27206
  var cmf = {
25955
27207
  type: "chaikin-money-flow",
25956
27208
  title: "Chaikin Money Flow",
25957
27209
  shortTitle: "CMF",
25958
27210
  overlay: false,
25959
- inputs: [lengthInput(20), colorInput()],
27211
+ inputs: [
27212
+ lengthInput(20, "length", "Length", 5e3, "Lookback length over which money flow volume and volume are summed."),
27213
+ colorInput(NEUTRAL, "CMF Line", "color", "Color of the Chaikin Money Flow line."),
27214
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the CMF reads above zero (net money flowing in)."),
27215
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the CMF reads below zero (net money flowing out)."),
27216
+ colorInput(NEUTRAL, "Zero Line", "zeroColor", "Color of the zero reference line.")
27217
+ ],
25960
27218
  compute: (bars, inputs) => {
25961
27219
  const len = num3(inputs, "length", 20);
25962
27220
  const values = zip(sum(moneyFlowVolume(bars), len), sum(volumes(bars), len), (m, v) => v === 0 ? 0 : m / v);
25963
- return {
25964
- plots: [{ key: "cmf", title: "CMF", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25965
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
25966
- };
27221
+ return centeredOscillator({
27222
+ key: "cmf",
27223
+ title: "CMF",
27224
+ values,
27225
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
27226
+ zeroInk: str3(inputs, "zeroColor", NEUTRAL)
27227
+ });
25967
27228
  }
25968
27229
  };
25969
27230
  var chaikinOscillator = {
@@ -25971,14 +27232,23 @@ var chaikinOscillator = {
25971
27232
  title: "Chaikin Oscillator",
25972
27233
  shortTitle: "Chaikin Osc",
25973
27234
  overlay: false,
25974
- inputs: [lengthInput(3, "fastLength", "Fast length"), lengthInput(10, "slowLength", "Slow length"), colorInput()],
27235
+ inputs: [
27236
+ lengthInput(3, "fastLength", "Fast Length", 5e3, "Length of the fast EMA applied to the accumulation/distribution line."),
27237
+ lengthInput(10, "slowLength", "Slow Length", 5e3, "Length of the slow EMA applied to the accumulation/distribution line."),
27238
+ colorInput(NEUTRAL, "Oscillator", "color", "Color of the Chaikin Oscillator line."),
27239
+ colorInput(BULLISH, "Bullish", "bullColor", "Line and fill color while the oscillator reads above zero (accumulation)."),
27240
+ colorInput(BEARISH, "Bearish", "bearColor", "Line and fill color while the oscillator reads below zero (distribution).")
27241
+ ],
25975
27242
  compute: (bars, inputs) => {
25976
27243
  const ad = cumSum(moneyFlowVolume(bars));
25977
27244
  const values = sub(ema(ad, num3(inputs, "fastLength", 3)), ema(ad, num3(inputs, "slowLength", 10)));
25978
- return {
25979
- plots: [{ key: "co", title: "Chaikin Osc", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
25980
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
25981
- };
27245
+ return centeredOscillator({
27246
+ key: "co",
27247
+ title: "Chaikin Oscillator",
27248
+ values,
27249
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
27250
+ zeroInk: NEUTRAL
27251
+ });
25982
27252
  }
25983
27253
  };
25984
27254
  var eom = {
@@ -25986,20 +27256,28 @@ var eom = {
25986
27256
  title: "Ease of Movement",
25987
27257
  shortTitle: "EOM",
25988
27258
  overlay: false,
25989
- inputs: [lengthInput(14), { key: "divisor", title: "Divisor", type: "int", defval: 1e4, min: 1, max: 1e9, step: 1 }, colorInput()],
27259
+ inputs: [
27260
+ lengthInput(14, "length", "Length", 5e3, "Number of bars in the simple moving average smoothing the 1-bar Ease of Movement values."),
27261
+ intInput("divisor", "Divisor", 1e4, 1, 1e9, "Volume scaling constant of the box ratio; larger values scale the oscillator up. 10000 is the conventional default."),
27262
+ colorInput(NEUTRAL, "EOM Line", "color", "Color of the line when it sits exactly at zero."),
27263
+ colorInput(BULLISH, "Above Zero", "bullColor", "Color of the line and fill while Ease of Movement is positive."),
27264
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while Ease of Movement is negative.")
27265
+ ],
25990
27266
  compute: (bars, inputs) => {
25991
27267
  const div = num3(inputs, "divisor", 1e4);
25992
- const hl2 = sourceValues(bars, "HL2");
25993
- const move = change(hl2);
27268
+ const move = change(sourceValues(bars, "HL2"));
25994
27269
  const raw = bars.map((b, i) => {
25995
27270
  const m = move[i];
25996
- if (b.volume == null || !Number.isFinite(b.volume) || b.volume === 0 || !Number.isFinite(m)) return Number.NaN;
25997
- return div * m * (b.high - b.low) / b.volume;
27271
+ if (b.volume == null || !Number.isFinite(b.volume) || !Number.isFinite(m)) return Number.NaN;
27272
+ return b.volume === 0 ? 0 : div * m * (b.high - b.low) / b.volume;
27273
+ });
27274
+ return centeredOscillator({
27275
+ key: "eom",
27276
+ title: "EOM",
27277
+ values: sma(raw, num3(inputs, "length", 14)),
27278
+ ink: { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "color", NEUTRAL) },
27279
+ zeroInk: NEUTRAL
25998
27280
  });
25999
- return {
26000
- plots: [{ key: "eom", title: "EOM", values: sma(raw, num3(inputs, "length", 14)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
26001
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26002
- };
26003
27281
  }
26004
27282
  };
26005
27283
  var forceIndex = {
@@ -26007,13 +27285,24 @@ var forceIndex = {
26007
27285
  title: "Force Index",
26008
27286
  shortTitle: "FI",
26009
27287
  overlay: false,
26010
- inputs: [lengthInput(13), colorInput()],
27288
+ inputs: [
27289
+ lengthInput(13, "length", "Length", 5e3, "EMA length applied to the raw 1-bar force index, (close \u2212 close[1]) \xD7 volume. Elder used 13 for the intermediate trend and 2 for short-term timing."),
27290
+ colorInput(BULLISH, "Force Index", "color", "Color of the line while above zero (buying pressure)."),
27291
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line while below zero (selling pressure).")
27292
+ ],
26011
27293
  compute: (bars, inputs) => {
26012
27294
  const raw = zip(change(closes(bars)), volumes(bars), (d, v) => d * v);
26013
- return {
26014
- plots: [{ key: "force", title: "Force", values: ema(raw, num3(inputs, "length", 13)), color: str3(inputs, "color", SERIES_LINE), width: 2 }],
26015
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26016
- };
27295
+ const bull = str3(inputs, "color", BULLISH);
27296
+ return centeredOscillator({
27297
+ key: "force",
27298
+ title: "Force Index",
27299
+ values: ema(raw, num3(inputs, "length", 13)),
27300
+ // The reference reads an exact zero as buying pressure, so neutral shares the bull ink.
27301
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
27302
+ zeroInk: NEUTRAL,
27303
+ width: 2,
27304
+ valueTransparency: 60
27305
+ });
26017
27306
  }
26018
27307
  };
26019
27308
  var klinger = {
@@ -26021,19 +27310,44 @@ var klinger = {
26021
27310
  title: "Klinger Oscillator",
26022
27311
  shortTitle: "KVO",
26023
27312
  overlay: false,
26024
- inputs: [lengthInput(34, "fastLength", "Fast length"), lengthInput(55, "slowLength", "Slow length"), lengthInput(13, "signalLength", "Signal length")],
27313
+ inputs: [
27314
+ lengthInput(34, "fastLength", "Fast Length", 5e3, "Length of the fast EMA applied to the volume force."),
27315
+ lengthInput(55, "slowLength", "Slow Length", 5e3, "Length of the slow EMA applied to the volume force."),
27316
+ lengthInput(13, "signalLength", "Signal Length", 500, "Length of the EMA applied to the oscillator to obtain the signal line."),
27317
+ colorInput(BULLISH, "Bullish", "color", "Color of the Klinger oscillator while above zero."),
27318
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the Klinger oscillator while below zero."),
27319
+ colorInput(WARNING, "Signal", "signalColor", "Color of the signal line.")
27320
+ ],
26025
27321
  compute: (bars, inputs) => {
26026
- const hlc3 = sourceValues(bars, "HLC3");
26027
- const trend = change(hlc3);
26028
- const signedVolume = zip(volumes(bars), trend, (v, t) => t >= 0 ? v : -v);
26029
- const line = sub(ema(signedVolume, num3(inputs, "fastLength", 34)), ema(signedVolume, num3(inputs, "slowLength", 55)));
26030
- return {
26031
- plots: [
26032
- { key: "kvo", title: "KVO", values: line, color: SERIES_LINE, width: 2 },
26033
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: WARNING }
26034
- ],
26035
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26036
- };
27322
+ const n = bars.length;
27323
+ const force = new Array(n).fill(Number.NaN);
27324
+ let trend = 1;
27325
+ let prevTrend = 1;
27326
+ let cm = 0;
27327
+ let prevDm = 0;
27328
+ for (let i = 0; i < n; i++) {
27329
+ const b = bars[i];
27330
+ const hlc = b.high + b.low + b.close;
27331
+ const prevHlc = i > 0 ? bars[i - 1].high + bars[i - 1].low + bars[i - 1].close : hlc;
27332
+ trend = hlc > prevHlc ? 1 : -1;
27333
+ const dm = b.high - b.low;
27334
+ cm = i > 0 && trend === prevTrend ? cm + dm : prevDm + dm;
27335
+ const v = b.volume;
27336
+ force[i] = v == null || !Number.isFinite(v) ? Number.NaN : cm === 0 ? 0 : v * Math.abs(2 * (dm / cm) - 1) * trend * 100;
27337
+ prevTrend = trend;
27338
+ prevDm = dm;
27339
+ }
27340
+ const line = sub(ema(force, num3(inputs, "fastLength", 34)), ema(force, num3(inputs, "slowLength", 55)));
27341
+ const bull = str3(inputs, "color", BULLISH);
27342
+ return centeredOscillator({
27343
+ key: "kvo",
27344
+ title: "KVO",
27345
+ values: line,
27346
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
27347
+ zeroInk: transp(NEUTRAL, 50),
27348
+ valueTransparency: 60,
27349
+ extraPlots: [{ key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 13)), color: str3(inputs, "signalColor", WARNING) }]
27350
+ });
26037
27351
  }
26038
27352
  };
26039
27353
  var mfi = {
@@ -26041,7 +27355,15 @@ var mfi = {
26041
27355
  title: "Money Flow Index",
26042
27356
  shortTitle: "MFI",
26043
27357
  overlay: false,
26044
- inputs: [lengthInput(14), colorInput()],
27358
+ inputs: [
27359
+ lengthInput(14, "length", "Length", 5e3, "Number of bars used to sum positive and negative money flow."),
27360
+ floatInput("overbought", "Overbought Level", 80, 0, 100, 1, "Level above which the MFI is considered overbought."),
27361
+ floatInput("oversold", "Oversold Level", 20, 0, 100, 1, "Level below which the MFI is considered oversold."),
27362
+ colorInput(NEUTRAL, "MFI", "color", "Color of the MFI line between the levels. Toward either level it blends into that level\u2019s color."),
27363
+ colorInput(BEARISH, "Overbought", "overboughtColor", "Color of the line and zone shading at and above the overbought level."),
27364
+ colorInput(BULLISH, "Oversold", "oversoldColor", "Color of the line and zone shading at and below the oversold level."),
27365
+ colorInput(transp(NEUTRAL, 94), "OB/OS Zone Fill", "fillColor", "Fill color of the area between the overbought and oversold levels.")
27366
+ ],
26045
27367
  compute: (bars, inputs) => {
26046
27368
  const len = num3(inputs, "length", 14);
26047
27369
  const tp = sourceValues(bars, "HLC3");
@@ -26049,48 +27371,57 @@ var mfi = {
26049
27371
  const d = change(tp);
26050
27372
  const pos = sum(zip(rawFlow, d, (f, x) => x > 0 ? f : 0), len);
26051
27373
  const neg = sum(zip(rawFlow, d, (f, x) => x < 0 ? f : 0), len);
26052
- const values = zip(pos, neg, (p, n) => n === 0 ? 100 : 100 - 100 / (1 + p / n));
26053
- return {
26054
- plots: [{ key: "mfi", title: "MFI", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
26055
- levels: [
26056
- { key: "upper", price: 80, color: NEUTRAL },
26057
- { key: "lower", price: 20, color: NEUTRAL }
26058
- ]
26059
- };
27374
+ const values = zip(pos, neg, (p, w) => p + w > 0 ? 100 * p / (p + w) : Number.NaN);
27375
+ return boundedOscillator({
27376
+ key: "mfi",
27377
+ title: "MFI",
27378
+ values,
27379
+ scale: { top: 100, bottom: 0 },
27380
+ overbought: num3(inputs, "overbought", 80),
27381
+ oversold: num3(inputs, "oversold", 20),
27382
+ ink: { low: str3(inputs, "oversoldColor", BULLISH), mid: str3(inputs, "color", NEUTRAL), high: str3(inputs, "overboughtColor", BEARISH) },
27383
+ levelInk: NEUTRAL,
27384
+ zoneTransparency: 91,
27385
+ bandFill: str3(inputs, "fillColor", transp(NEUTRAL, 94))
27386
+ });
26060
27387
  }
26061
27388
  };
26062
- function volumeIndex(bars, when) {
26063
- const out = new Array(bars.length).fill(Number.NaN);
26064
- let index = 1e3;
26065
- let started = false;
26066
- for (let i = 1; i < bars.length; i++) {
26067
- const v = bars[i].volume;
26068
- const pv = bars[i - 1].volume;
26069
- const pc = bars[i - 1].close;
26070
- if (v == null || pv == null || !Number.isFinite(v) || !Number.isFinite(pv) || pc === 0) {
26071
- if (started) out[i] = index;
26072
- continue;
26073
- }
26074
- started = true;
26075
- if (when(v, pv)) index += (bars[i].close - pc) / pc * index;
26076
- out[i] = index;
26077
- }
26078
- return out;
27389
+ function regimeStyleInputs(signalTitle) {
27390
+ return [
27391
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the line and gradient fill while it holds above its signal EMA."),
27392
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the line and gradient fill while it holds below its signal EMA."),
27393
+ colorInput(NEUTRAL, "Neutral", "neutralColor", "Color used while the signal EMA is still forming, or the line sits exactly on it."),
27394
+ colorInput(NEUTRAL, signalTitle, "signalColor", "Color of the signal EMA overlay."),
27395
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the gap between the line and its signal EMA with a vertical gradient \u2014 transparent at the average, strongest at the line.")
27396
+ ];
26079
27397
  }
27398
+ var NVI_POSITION = "Position vs Average";
27399
+ var NVI_SINGLE = "Single Color";
26080
27400
  var nvi = {
26081
27401
  type: "negative-volume-index",
26082
27402
  title: "Negative Volume Index",
26083
27403
  shortTitle: "Negative Volume Index",
26084
27404
  overlay: false,
26085
- inputs: [lengthInput(255, "signalLength", "Signal length"), colorInput()],
27405
+ inputs: [
27406
+ floatInput("base", "Base Level", 1e3, 0.01, 1e6, 1, "Arbitrary seed of the cumulative line, conventionally 1000. The absolute level carries no information \u2014 the line is read by slope and by position against the signal EMA."),
27407
+ boolInput("showSignal", "Signal EMA", true, SETTINGS, "Displays the conventional signal overlay. Hiding it only removes the plotted average; the regime coloring and fill still use it."),
27408
+ lengthInput(255, "signalLength", "Signal Length", 5e3, "Length of the signal EMA. The convention is one trading year \u2014 255 sessions on daily charts."),
27409
+ optionInput("colorMode", "Line Coloring", NVI_POSITION, [NVI_POSITION, NVI_SINGLE], "'Position vs Average' colors the line by the classic regime read; 'Single Color' draws the whole line in the neutral color."),
27410
+ ...regimeStyleInputs("Signal EMA")
27411
+ ],
26086
27412
  compute: (bars, inputs) => {
26087
- const line = volumeIndex(bars, (v, pv) => v < pv);
26088
- return {
26089
- plots: [
26090
- { key: "nvi", title: "NVI", values: line, color: str3(inputs, "color", SERIES_LINE), width: 2 },
26091
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 255)), color: WARNING }
26092
- ]
26093
- };
27413
+ const values = volumeIndex(bars, closes(bars), num3(inputs, "base", 1e3), (v, pv) => v < pv);
27414
+ const signal = ema(values, num3(inputs, "signalLength", 255));
27415
+ return regimeIndex(
27416
+ "nvi",
27417
+ "NVI",
27418
+ values,
27419
+ signal,
27420
+ { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "neutralColor", NEUTRAL), signal: str3(inputs, "signalColor", NEUTRAL) },
27421
+ bool(inputs, "showSignal", true),
27422
+ bool(inputs, "gradientFill", true),
27423
+ str3(inputs, "colorMode", NVI_POSITION) === NVI_SINGLE
27424
+ );
26094
27425
  }
26095
27426
  };
26096
27427
  var pvi = {
@@ -26098,15 +27429,26 @@ var pvi = {
26098
27429
  title: "Positive Volume Index",
26099
27430
  shortTitle: "Positive Volume Index",
26100
27431
  overlay: false,
26101
- inputs: [lengthInput(255, "signalLength", "Signal length"), colorInput()],
27432
+ inputs: [
27433
+ sourceInput("Close", "source", "Source", "Price series whose one-bar percentage change moves PVI on its active sessions."),
27434
+ floatInput("base", "Starting Value", 1e3, 1, 1e6, 1, "Seed of the cumulative line. The absolute level carries no information, so the reads are the slope and the position against the signal EMA."),
27435
+ boolInput("showSignal", "Signal EMA", true, SETTINGS, "Plots the long EMA of PVI that anchors the classic regime read. Hiding it keeps the regime coloring and fill, which are always measured against it."),
27436
+ lengthInput(255, "signalLength", "Signal Length", 5e3, "Length of the signal EMA. The classic reference is a one-year average \u2014 255 trading days on daily charts."),
27437
+ ...regimeStyleInputs("Signal Line")
27438
+ ],
26102
27439
  compute: (bars, inputs) => {
26103
- const line = volumeIndex(bars, (v, pv) => v > pv);
26104
- return {
26105
- plots: [
26106
- { key: "pvi", title: "PVI", values: line, color: str3(inputs, "color", SERIES_LINE), width: 2 },
26107
- { key: "signal", title: "Signal", values: ema(line, num3(inputs, "signalLength", 255)), color: WARNING }
26108
- ]
26109
- };
27440
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
27441
+ const values = volumeIndex(bars, src, num3(inputs, "base", 1e3), (v, pv) => v > pv);
27442
+ const signal = ema(values, num3(inputs, "signalLength", 255));
27443
+ return regimeIndex(
27444
+ "pvi",
27445
+ "PVI",
27446
+ values,
27447
+ signal,
27448
+ { bull: str3(inputs, "bullColor", BULLISH), bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "neutralColor", NEUTRAL), signal: str3(inputs, "signalColor", NEUTRAL) },
27449
+ bool(inputs, "showSignal", true),
27450
+ bool(inputs, "gradientFill", true)
27451
+ );
26110
27452
  }
26111
27453
  };
26112
27454
  var pvt = {
@@ -26114,7 +27456,15 @@ var pvt = {
26114
27456
  title: "Price Volume Trend",
26115
27457
  shortTitle: "PVT",
26116
27458
  overlay: false,
26117
- inputs: [colorInput()],
27459
+ inputs: [
27460
+ boolInput("showSignal", "Signal Line", false, SETTINGS, "Display a moving average of PVT acting as a signal line."),
27461
+ optionInput("signalType", "Signal Type", "SMA", ["SMA", "EMA"], "Moving average type used for the signal line."),
27462
+ lengthInput(21, "signalLength", "Signal Length", 5e3, "Number of bars used to compute the signal line."),
27463
+ colorInput(NEUTRAL, "PVT Color", "color", "Color of the PVT line while it is flat."),
27464
+ colorInput(BULLISH, "Rising", "bullColor", "Color of the PVT line while it rises bar to bar."),
27465
+ colorInput(BEARISH, "Falling", "bearColor", "Color of the PVT line while it falls bar to bar."),
27466
+ colorInput(WARNING, "Signal Color", "signalColor", "Color of the signal line.")
27467
+ ],
26118
27468
  compute: (bars, inputs) => {
26119
27469
  const c = closes(bars);
26120
27470
  const raw = bars.map((b, i) => {
@@ -26122,7 +27472,17 @@ var pvt = {
26122
27472
  const pc = c[i - 1];
26123
27473
  return pc === 0 ? 0 : (c[i] - pc) / pc * b.volume;
26124
27474
  });
26125
- return { plots: [{ key: "pvt", title: "PVT", values: cumSum(raw), color: str3(inputs, "color", SERIES_LINE), width: 2 }] };
27475
+ const values = cumSum(raw);
27476
+ const flat = str3(inputs, "color", NEUTRAL);
27477
+ const plots = [
27478
+ { key: "pvt", title: "PVT", values, color: flat, width: 2, colors: slopeColors2(values, str3(inputs, "bullColor", BULLISH), str3(inputs, "bearColor", BEARISH), flat) }
27479
+ ];
27480
+ if (bool(inputs, "showSignal", false)) {
27481
+ const len = num3(inputs, "signalLength", 21);
27482
+ const signal = str3(inputs, "signalType", "SMA") === "EMA" ? ema(values, len) : sma(values, len);
27483
+ plots.push({ key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) });
27484
+ }
27485
+ return { plots };
26126
27486
  }
26127
27487
  };
26128
27488
  var volumeOscillator = {
@@ -26130,39 +27490,75 @@ var volumeOscillator = {
26130
27490
  title: "Volume Oscillator",
26131
27491
  shortTitle: "Vol Osc",
26132
27492
  overlay: false,
26133
- inputs: [lengthInput(5, "fastLength", "Fast length"), lengthInput(10, "slowLength", "Slow length"), colorInput()],
27493
+ inputs: [
27494
+ lengthInput(5, "fastLength", "Fast Length", 5e3, "Length of the fast EMA of volume."),
27495
+ lengthInput(10, "slowLength", "Slow Length", 5e3, "Length of the slow EMA of volume."),
27496
+ colorInput(BULLISH, "Above Zero", "color", "Color of the line and fill while it holds above zero \u2014 volume expanding."),
27497
+ colorInput(BEARISH, "Below Zero", "bearColor", "Color of the line and fill while it holds below zero \u2014 volume contracting.")
27498
+ ],
26134
27499
  compute: (bars, inputs) => {
26135
27500
  const vol = volumes(bars);
26136
27501
  const fast = ema(vol, num3(inputs, "fastLength", 5));
26137
27502
  const slow = ema(vol, num3(inputs, "slowLength", 10));
26138
- const values = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : 100 * (f - s) / s);
26139
- return {
26140
- plots: [{ key: "vo", title: "Volume Osc", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
26141
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26142
- };
27503
+ const values = zip(fast, slow, (f, s) => s === 0 ? 0 : 100 * (f - s) / s);
27504
+ const bull = str3(inputs, "color", BULLISH);
27505
+ return centeredOscillator({
27506
+ key: "vo",
27507
+ title: "Volume Oscillator",
27508
+ values,
27509
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: bull },
27510
+ zeroInk: NEUTRAL,
27511
+ valueTransparency: 60
27512
+ });
26143
27513
  }
26144
27514
  };
27515
+ var OSCILLATOR = "Oscillator";
26145
27516
  var pvo = {
26146
27517
  type: "pvo",
26147
27518
  title: "Percentage Volume Oscillator",
26148
27519
  shortTitle: "PVO",
26149
27520
  overlay: false,
26150
- inputs: [lengthInput(12, "fastLength", "Fast length"), lengthInput(26, "slowLength", "Slow length"), lengthInput(9, "signalLength", "Signal length")],
27521
+ inputs: [
27522
+ { ...lengthInput(12, "fastLength", "Fast Length", 5e3, "Period of the fast EMA of volume. The conventional setting is 12."), group: OSCILLATOR },
27523
+ { ...lengthInput(26, "slowLength", "Slow Length", 5e3, "Period of the slow EMA of volume. The PVO expresses the fast/slow spread as a percentage of this slow average."), group: OSCILLATOR },
27524
+ { ...lengthInput(9, "signalLength", "Signal Length", 500, "Period of the EMA applied to the PVO to form the signal line."), group: OSCILLATOR },
27525
+ colorInput(BULLISH, "Expansion Color", "color", "Color of the oscillator and fill while participation expands."),
27526
+ colorInput(BEARISH, "Contraction Color", "bearColor", "Color of the oscillator and fill while participation contracts."),
27527
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
27528
+ colorInput(transp(NEUTRAL, 50), "Zero Line", "zeroColor", "Color of the zero reference line."),
27529
+ boolInput("showSignal", "Show Signal Line", true, STYLE, "Plot the signal line, an EMA of the PVO."),
27530
+ boolInput("showHistogram", "Show Histogram", true, STYLE, "Plot the difference between the PVO and its signal line as columns behind the oscillator.")
27531
+ ],
26151
27532
  compute: (bars, inputs) => {
26152
27533
  const vol = volumes(bars);
26153
27534
  const fast = ema(vol, num3(inputs, "fastLength", 12));
26154
27535
  const slow = ema(vol, num3(inputs, "slowLength", 26));
26155
- const line = zip(fast, slow, (f, s) => s === 0 ? Number.NaN : 100 * (f - s) / s);
26156
- const signal = ema(line, num3(inputs, "signalLength", 9));
26157
- const hist = sub(line, signal);
26158
- return {
26159
- plots: [
26160
- { key: "hist", title: "Histogram", values: hist, kind: "histogram", color: BULLISH, colors: hist.map((x) => Number.isFinite(x) ? x >= 0 ? BULLISH : BEARISH : null), base: 0 },
26161
- { key: "pvo", title: "PVO", values: line, color: SERIES_LINE, width: 2 },
26162
- { key: "signal", title: "Signal", values: signal, color: WARNING }
26163
- ],
26164
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26165
- };
27536
+ const values = zip(fast, slow, (f, s) => s > 0 ? 100 * (f - s) / s : Number.NaN);
27537
+ const signal = ema(values, num3(inputs, "signalLength", 9));
27538
+ const expansion = str3(inputs, "color", BULLISH);
27539
+ const contraction = str3(inputs, "bearColor", BEARISH);
27540
+ const extraPlots = [];
27541
+ if (bool(inputs, "showHistogram", true)) {
27542
+ const hist = sub(values, signal);
27543
+ extraPlots.push({
27544
+ key: "hist",
27545
+ title: "Histogram",
27546
+ values: hist,
27547
+ kind: "columns",
27548
+ color: transp(expansion, 60),
27549
+ colors: hist.map((x) => Number.isFinite(x) ? transp(x > 0 ? expansion : contraction, 60) : null),
27550
+ base: 0
27551
+ });
27552
+ }
27553
+ if (bool(inputs, "showSignal", true)) extraPlots.push({ key: "signal", title: "Signal", values: signal, color: str3(inputs, "signalColor", WARNING) });
27554
+ return centeredOscillator({
27555
+ key: "pvo",
27556
+ title: "PVO",
27557
+ values,
27558
+ ink: { bull: expansion, bear: contraction, neutral: contraction },
27559
+ zeroInk: str3(inputs, "zeroColor", transp(NEUTRAL, 50)),
27560
+ extraPlots
27561
+ });
26166
27562
  }
26167
27563
  };
26168
27564
  var vfi = {
@@ -26171,55 +27567,86 @@ var vfi = {
26171
27567
  shortTitle: "Volume Flow Indicator",
26172
27568
  overlay: false,
26173
27569
  inputs: [
26174
- lengthInput(130),
26175
- { key: "coef", title: "Cutoff coefficient", type: "float", defval: 0.2, min: 0.01, max: 10, step: 0.01 },
26176
- { key: "volCoef", title: "Volume cap", type: "float", defval: 2.5, min: 0.1, max: 50, step: 0.1 },
26177
- lengthInput(3, "smoothLength", "Smoothing length")
27570
+ { ...lengthInput(130, "length", "Length", 5e3, "Rolling summation window, also used for the average volume that scales the result. The published default of 130 bars targets position-horizon work on daily charts."), min: 2 },
27571
+ floatInput("coef", "Cutoff Coefficient", 0.2, 0, 10, 0.05, "Scales the volatility-based minimum change in typical price. Moves smaller than the cutoff count as noise and contribute no volume."),
27572
+ floatInput("volCoef", "Volume Cap Multiplier", 2.5, 0.1, 50, 0.1, "Caps each bar's volume contribution at this multiple of the average volume, so a single climactic print stays bounded."),
27573
+ { ...lengthInput(30, "volLength", "Volatility Length", 5e3, "Window of the standard deviation of the log change in typical price used to build the adaptive cutoff."), min: 2 },
27574
+ boolInput("smooth", "Smooth VFI", true, SETTINGS, "Applies the published light smoothing \u2014 an EMA of the summed line \u2014 before plotting."),
27575
+ lengthInput(3, "smoothLength", "Smoothing Length", 500, "Length of the smoothing EMA. The published version uses 3 bars."),
27576
+ boolInput("showSignal", "Signal Line", true, SETTINGS, "Plots an EMA of VFI as a signal line."),
27577
+ lengthInput(5, "signalLength", "Signal Length", 500, "Length of the signal-line EMA."),
27578
+ colorInput(BULLISH, "Bullish", "color", "Color of the VFI line and gradient fill above the zero line \u2014 the net accumulation regime."),
27579
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the VFI line and gradient fill below the zero line \u2014 the net distribution regime."),
27580
+ colorInput(WARNING, "Signal Line", "signalColor", "Color of the signal line."),
27581
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the zero line and the VFI line with a vertical gradient.")
26178
27582
  ],
26179
27583
  compute: (bars, inputs) => {
26180
27584
  const len = num3(inputs, "length", 130);
27585
+ const typical = sourceValues(bars, "HLC3");
27586
+ const logChange = sub(map(typical, Math.log), shift(map(typical, Math.log), 1));
27587
+ const volatility = stdev(logChange, num3(inputs, "volLength", 30));
26181
27588
  const coef = num3(inputs, "coef", 0.2);
26182
27589
  const volCoef = num3(inputs, "volCoef", 2.5);
26183
- const typical = map(sourceValues(bars, "HLC3"), Math.log);
26184
- const inter = change(typical);
26185
- const vInter = stdev(inter, 30);
26186
27590
  const vol = volumes(bars);
26187
- const vAve = sma(vol, len);
26188
- const mf = change(sourceValues(bars, "HLC3"));
26189
- const raw = bars.map((b, i) => {
26190
- const cutoff = Number.isFinite(vInter[i]) ? coef * vInter[i] * b.close : Number.NaN;
27591
+ const avgVolume = shift(sma(vol, len), 1);
27592
+ const typicalChange = change(typical);
27593
+ const signed2 = bars.map((b, i) => {
27594
+ const sd = volatility[i];
27595
+ const m = typicalChange[i];
26191
27596
  const v = vol[i];
26192
- const ave = i > 0 ? vAve[i - 1] : Number.NaN;
26193
- const m = mf[i];
26194
- if (!Number.isFinite(cutoff) || !Number.isFinite(v) || !Number.isFinite(ave) || !Number.isFinite(m)) return Number.NaN;
26195
- const capped = Math.min(v, ave * volCoef);
27597
+ if (!Number.isFinite(sd) || !Number.isFinite(m) || !Number.isFinite(v)) return Number.NaN;
27598
+ const cutoff = coef * sd * b.close;
27599
+ const avg = avgVolume[i];
27600
+ const capped = Number.isFinite(avg) ? Math.min(v, avg * volCoef) : v;
26196
27601
  return m > cutoff ? capped : m < -cutoff ? -capped : 0;
26197
27602
  });
26198
- const values = bars.map((_, i) => {
26199
- const ave = i > 0 ? vAve[i - 1] : Number.NaN;
26200
- return Number.isFinite(ave) && ave !== 0 ? ave : Number.NaN;
27603
+ const raw = zip(sum(signed2, len), avgVolume, (f, a) => a > 0 ? f / a : Number.NaN);
27604
+ const values = bool(inputs, "smooth", true) ? ema(raw, num3(inputs, "smoothLength", 3)) : raw;
27605
+ const bull = str3(inputs, "color", BULLISH);
27606
+ const extraPlots = bool(inputs, "showSignal", true) ? [{ key: "signal", title: "Signal Line", values: ema(values, num3(inputs, "signalLength", 5)), color: str3(inputs, "signalColor", WARNING) }] : [];
27607
+ return centeredOscillator({
27608
+ key: "vfi",
27609
+ title: "VFI",
27610
+ values,
27611
+ ink: { bull, bear: str3(inputs, "bearColor", BEARISH), neutral: str3(inputs, "bearColor", BEARISH) },
27612
+ zeroInk: transp(NEUTRAL, 90),
27613
+ fill: bool(inputs, "gradientFill", true),
27614
+ extraPlots
26201
27615
  });
26202
- const flowSum = sum(raw, len);
26203
- const line = zip(flowSum, values, (f, a) => f / a);
26204
- return {
26205
- plots: [{ key: "vfi", title: "VFI", values: sma(line, num3(inputs, "smoothLength", 3)), color: SERIES_LINE, width: 2 }],
26206
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26207
- };
26208
27616
  }
26209
27617
  };
27618
+ var II_OSCILLATOR = "Normalized Oscillator (II%)";
27619
+ var II_CUMULATIVE = "Cumulative Line";
26210
27620
  var intradayIntensity = {
26211
27621
  type: "intraday-intensity",
26212
27622
  title: "Intraday Intensity",
26213
27623
  shortTitle: "Intraday Intensity",
26214
27624
  overlay: false,
26215
- inputs: [lengthInput(21), colorInput()],
27625
+ inputs: [
27626
+ optionInput("mode", "Display", II_OSCILLATOR, [II_OSCILLATOR, II_CUMULATIVE], "The two published forms. The cumulative line is open-ended and read by slope against price; the normalized oscillator divides a rolling sum of intensity by the rolling sum of volume and oscillates around zero."),
27627
+ lengthInput(21, "length", "Oscillator Length", 5e3, "Window of the rolling sums in the normalized form. The common setting is 21 bars. It has no effect on the cumulative line."),
27628
+ colorInput(BULLISH, "Bullish", "color", "Color of positive readings, the rising cumulative line and the fill above zero \u2014 the accumulation side."),
27629
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of negative readings, the falling cumulative line and the fill below zero \u2014 the distribution side."),
27630
+ boolInput("gradientFill", "Gradient Fill", true, STYLE, "Fills the area between the zero line and the oscillator with a vertical gradient. Applies to the normalized oscillator only.")
27631
+ ],
26216
27632
  compute: (bars, inputs) => {
27633
+ const intensity = moneyFlowVolume(bars);
27634
+ const bull = str3(inputs, "color", BULLISH);
27635
+ const bear = str3(inputs, "bearColor", BEARISH);
27636
+ if (str3(inputs, "mode", II_OSCILLATOR) === II_CUMULATIVE) {
27637
+ const values2 = cumSum(intensity);
27638
+ return { plots: [{ key: "ii", title: "Intraday Intensity", values: values2, color: bull, colors: slopeColors2(values2, bull, bear, bull) }] };
27639
+ }
26217
27640
  const len = num3(inputs, "length", 21);
26218
- const values = zip(sum(moneyFlowVolume(bars), len), sum(volumes(bars), len), (m, v) => v === 0 ? 0 : 100 * m / v);
26219
- return {
26220
- plots: [{ key: "ii", title: "II%", values, color: str3(inputs, "color", SERIES_LINE), width: 2 }],
26221
- levels: [{ key: "zero", price: 0, color: NEUTRAL }]
26222
- };
27641
+ const values = zip(sum(intensity, len), sum(volumes(bars), len), (m, v) => v > 0 ? 100 * m / v : Number.NaN);
27642
+ return centeredOscillator({
27643
+ key: "ii",
27644
+ title: "Intraday Intensity",
27645
+ values,
27646
+ ink: { bull, bear, neutral: bear },
27647
+ zeroInk: transp(NEUTRAL, 90),
27648
+ fill: bool(inputs, "gradientFill", true)
27649
+ });
26223
27650
  }
26224
27651
  };
26225
27652
  var volumeSpecs = [
@@ -26252,15 +27679,24 @@ function periodKey(anchor, time) {
26252
27679
  }
26253
27680
  return Math.floor(time / DAY_MS2);
26254
27681
  }
27682
+ function barInterval(bars) {
27683
+ if (bars.length < 2) return 0;
27684
+ const gaps = [];
27685
+ for (let i = 1; i < bars.length; i++) gaps.push(bars[i].time - bars[i - 1].time);
27686
+ gaps.sort((a, b) => a - b);
27687
+ return gaps[Math.floor(gaps.length / 2)] ?? 0;
27688
+ }
26255
27689
  var parabolicSar = {
26256
27690
  type: "parabolic-sar",
26257
27691
  title: "Parabolic SAR",
26258
27692
  shortTitle: "SAR",
26259
27693
  overlay: true,
26260
27694
  inputs: [
26261
- { key: "start", title: "Start", type: "float", defval: 0.02, min: 1e-3, max: 1, step: 1e-3 },
26262
- { key: "increment", title: "Increment", type: "float", defval: 0.02, min: 1e-3, max: 1, step: 1e-3 },
26263
- { key: "maximum", title: "Max value", type: "float", defval: 0.2, min: 0.01, max: 1, step: 0.01 }
27695
+ floatInput("start", "Start", 0.02, 0, 1, 0.01, "Initial acceleration factor applied when a new trend begins."),
27696
+ floatInput("increment", "Increment", 0.02, 0, 1, 0.01, "Amount added to the acceleration factor each time the trend makes a new extreme point."),
27697
+ floatInput("maximum", "Maximum", 0.2, 0.01, 1, 0.01, "Upper cap on the acceleration factor."),
27698
+ colorInput(BULLISH, "Bullish", "bullColor", "Color of the SAR crosses while they trail below price \u2014 uptrend, the stop sits under the market."),
27699
+ colorInput(BEARISH, "Bearish", "bearColor", "Color of the SAR crosses while they sit above price \u2014 downtrend, the stop sits over the market.")
26264
27700
  ],
26265
27701
  compute: (bars, inputs) => {
26266
27702
  const start = num3(inputs, "start", 0.02);
@@ -26268,7 +27704,6 @@ var parabolicSar = {
26268
27704
  const max = num3(inputs, "maximum", 0.2);
26269
27705
  const n = bars.length;
26270
27706
  const values = new Array(n).fill(Number.NaN);
26271
- const colors = new Array(n).fill(null);
26272
27707
  if (n >= 2) {
26273
27708
  let long = bars[1].close >= bars[0].close;
26274
27709
  let sar = long ? bars[0].low : bars[0].high;
@@ -26293,18 +27728,21 @@ var parabolicSar = {
26293
27728
  af = Math.min(max, af + inc);
26294
27729
  }
26295
27730
  values[i] = sar;
26296
- colors[i] = long ? BULLISH : BEARISH;
26297
27731
  }
26298
27732
  }
26299
- return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
27733
+ const bull = str3(inputs, "bullColor", BULLISH);
27734
+ const bear = str3(inputs, "bearColor", BEARISH);
27735
+ const colors = values.map((x, i) => Number.isFinite(x) ? x < bars[i].close ? bull : bear : null);
27736
+ return { plots: [{ key: "sar", title: "SAR", values, kind: "cross", color: bull, colors }] };
26300
27737
  }
26301
27738
  };
26302
- var VWAP_BANDS_GROUP = "Bands";
26303
- var VWAP_STYLE = "Style";
27739
+ var VWAP_ANCHORS = ["Session", "Week", "Month", "Quarter", "Year"];
27740
+ var BANDS_STDEV = "Standard Deviation";
27741
+ var BANDS_PERCENT = "Percentage";
26304
27742
  var VWAP_BANDS = [
26305
- { on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
26306
- { on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
26307
- { on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
27743
+ { show: "band1", mult: "band1Mult", defMult: 1, defOn: true },
27744
+ { show: "band2", mult: "band2Mult", defMult: 2, defOn: false },
27745
+ { show: "band3", mult: "band3Mult", defMult: 3, defOn: false }
26308
27746
  ];
26309
27747
  var vwap = {
26310
27748
  type: "vwap",
@@ -26312,39 +27750,36 @@ var vwap = {
26312
27750
  shortTitle: "VWAP",
26313
27751
  overlay: true,
26314
27752
  inputs: [
26315
- { key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
26316
- sourceInput("HLC3"),
26317
- // Each band is one row: its toggle leads, the multiplier follows unlabeled.
27753
+ optionInput("anchor", "Anchor Period", "Session", VWAP_ANCHORS, "Period anchoring the average. VWAP and its bands reset on the first bar of each new session, week, month, quarter or year (UTC)."),
27754
+ sourceInput("HLC3", "source", "Source", "Price used in the volume weighted average. Typical price (HLC3) is the classic choice."),
27755
+ optionInput("bandsMode", "Bands Mode", BANDS_STDEV, [BANDS_STDEV, BANDS_PERCENT], "Bands offset method: multiples of the volume weighted standard deviation of price around VWAP, or a percentage of the VWAP value."),
26318
27756
  ...VWAP_BANDS.flatMap((b, i) => [
26319
- { key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
26320
- { key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
27757
+ boolInput(b.show, `Band #${i + 1}`, b.defOn, SETTINGS, `Show the ${["first", "second", "third"][i]} band pair.`),
27758
+ floatInput(b.mult, `Band #${i + 1} Multiplier`, b.defMult, 0, 50, 0.5, "Multiplier for this band pair, in standard deviations or percent depending on the bands mode.")
26321
27759
  ]),
26322
- { ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
26323
- // One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
26324
- ...VWAP_BANDS.flatMap((b, i) => [
26325
- { ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
26326
- { key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
26327
- { ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
26328
- ])
27760
+ boolInput("hideDwm", "Hide VWAP on 1D or Above", true, SETTINGS, "Hide the VWAP and its bands on daily and higher timeframes, where an intraday anchored VWAP is not meaningful."),
27761
+ colorInput(BULLISH, "Bullish", "bullColor", "VWAP line color while price closes above it, and the color of the lower (support) bands."),
27762
+ colorInput(BEARISH, "Bearish", "bearColor", "VWAP line color while price closes below it, and the color of the upper (resistance) bands."),
27763
+ colorInput(NEUTRAL, "Bands", "bandsColor", "Deviation bands fill color."),
27764
+ boolInput("fill", "Bands Fill", true, STYLE, "Fill the area between each visible band pair.")
26329
27765
  ],
26330
27766
  compute: (bars, inputs) => {
26331
- const anchor = str3(inputs, "anchor", "Day");
27767
+ const anchor = str3(inputs, "anchor", "Session");
26332
27768
  const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
26333
27769
  const n = bars.length;
27770
+ const hidden = bool(inputs, "hideDwm", true) && barInterval(bars) >= DAY_MS2;
26334
27771
  const values = new Array(n).fill(Number.NaN);
26335
- const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
27772
+ const bands = VWAP_BANDS.filter((b) => bool(inputs, b.show, b.defOn)).map((b) => ({
26336
27773
  mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
26337
- color: str3(inputs, b.color, b.defColor),
26338
- fill: bool(inputs, b.fill, true),
26339
- fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
26340
27774
  up: new Array(n).fill(Number.NaN),
26341
27775
  down: new Array(n).fill(Number.NaN)
26342
27776
  }));
27777
+ const percentMode = str3(inputs, "bandsMode", BANDS_STDEV) === BANDS_PERCENT;
26343
27778
  let period = Number.NaN;
26344
27779
  let cumPV = 0;
26345
27780
  let cumPV2 = 0;
26346
27781
  let cumV = 0;
26347
- for (let i = 0; i < n; i++) {
27782
+ for (let i = 0; i < n && !hidden; i++) {
26348
27783
  const b = bars[i];
26349
27784
  const key = periodKey(anchor, b.time);
26350
27785
  if (key !== period) {
@@ -26362,38 +27797,58 @@ var vwap = {
26362
27797
  }
26363
27798
  if (cumV <= 0) continue;
26364
27799
  const mean = cumPV / cumV;
26365
- const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
27800
+ const offset = percentMode ? mean / 100 : Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
26366
27801
  values[i] = mean;
26367
27802
  for (const band of bands) {
26368
- band.up[i] = mean + band.mult * sd;
26369
- band.down[i] = mean - band.mult * sd;
27803
+ band.up[i] = mean + band.mult * offset;
27804
+ band.down[i] = mean - band.mult * offset;
26370
27805
  }
26371
27806
  }
26372
- const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
27807
+ const bull = str3(inputs, "bullColor", BULLISH);
27808
+ const bear = str3(inputs, "bearColor", BEARISH);
27809
+ const fillColor = transp(str3(inputs, "bandsColor", NEUTRAL), 95);
27810
+ const plots = [
27811
+ { key: "vwap", title: "VWAP", values, color: bull, colors: values.map((x, i) => Number.isFinite(x) ? bars[i].close >= x ? bull : bear : null) }
27812
+ ];
26373
27813
  const fills = [];
26374
27814
  bands.forEach((band, i) => {
26375
27815
  plots.push(
26376
- { key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
26377
- { key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
27816
+ { key: `up${i}`, title: `Upper Band #${i + 1}`, values: band.up, color: transp(bear, 40) },
27817
+ { key: `down${i}`, title: `Lower Band #${i + 1}`, values: band.down, color: transp(bull, 40) }
26378
27818
  );
26379
- if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
27819
+ if (bool(inputs, "fill", true)) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: fillColor });
26380
27820
  });
26381
27821
  return { plots, bands: fills };
26382
27822
  }
26383
27823
  };
27824
+ var PIVOT_TRADITIONAL = "Traditional";
27825
+ var PIVOT_FIBONACCI = "Fibonacci";
27826
+ var PIVOT_CAMARILLA = "Camarilla";
27827
+ var PIVOT_WOODIE = "Woodie";
27828
+ var PIVOT_AUTO = "Auto";
26384
27829
  var pivotPoints = {
26385
27830
  type: "pivot-points",
26386
- title: "Pivot Points",
27831
+ title: "Pivot Points Standard",
26387
27832
  shortTitle: "Pivots",
26388
27833
  overlay: true,
26389
- inputs: [{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month"] }],
27834
+ inputs: [
27835
+ optionInput("kind", "Type", PIVOT_TRADITIONAL, [PIVOT_TRADITIONAL, PIVOT_FIBONACCI, PIVOT_CAMARILLA, PIVOT_WOODIE], "Formula used to calculate the levels. Traditional is the classic floor-trader method; Fibonacci projects 38.2/61.8/100% of the prior range around the pivot; Camarilla scales the prior range around the prior close; Woodie gives extra weight to the period open."),
27836
+ optionInput("anchor", "Pivots Timeframe", PIVOT_AUTO, [PIVOT_AUTO, "Daily", "Weekly", "Monthly", "Yearly"], "Period the pivots are anchored to. 'Auto' follows the chart's bar spacing: daily pivots up to 15-minute bars, weekly on other intraday bars, monthly on daily bars, yearly above."),
27837
+ colorInput(WARNING, "Pivot", "pivotColor", "Color of the central pivot (P) level."),
27838
+ colorInput(BEARISH, "Resistances", "resistanceColor", "Color of the resistance (R) levels."),
27839
+ colorInput(BULLISH, "Supports", "supportColor", "Color of the support (S) levels.")
27840
+ ],
26390
27841
  compute: (bars, inputs) => {
26391
- const anchor = str3(inputs, "anchor", "Day");
27842
+ const interval = barInterval(bars);
27843
+ const requested = str3(inputs, "anchor", PIVOT_AUTO);
27844
+ const auto = interval <= 15 * 6e4 ? "Session" : interval < DAY_MS2 ? "Week" : interval <= DAY_MS2 ? "Month" : "Year";
27845
+ const anchor = requested === "Daily" ? "Session" : requested === "Weekly" ? "Week" : requested === "Monthly" ? "Month" : requested === "Yearly" ? "Year" : auto;
27846
+ const kind = str3(inputs, "kind", PIVOT_TRADITIONAL);
26392
27847
  const aggs = /* @__PURE__ */ new Map();
26393
27848
  for (const b of bars) {
26394
27849
  const key = periodKey(anchor, b.time);
26395
27850
  const a = aggs.get(key);
26396
- if (!a) aggs.set(key, { high: b.high, low: b.low, close: b.close });
27851
+ if (!a) aggs.set(key, { open: b.open, high: b.high, low: b.low, close: b.close });
26397
27852
  else {
26398
27853
  a.high = Math.max(a.high, b.high);
26399
27854
  a.low = Math.min(a.low, b.low);
@@ -26402,31 +27857,61 @@ var pivotPoints = {
26402
27857
  }
26403
27858
  const n = bars.length;
26404
27859
  const mk = () => new Array(n).fill(Number.NaN);
26405
- const levels = { p: mk(), r1: mk(), s1: mk(), r2: mk(), s2: mk(), r3: mk(), s3: mk() };
27860
+ const levels = { p: mk(), r1: mk(), s1: mk(), r2: mk(), s2: mk(), r3: mk(), s3: mk(), r4: mk(), s4: mk() };
27861
+ let lastKey = Number.NaN;
26406
27862
  for (let i = 0; i < n; i++) {
26407
- const prev2 = aggs.get(periodKey(anchor, bars[i].time) - 1);
26408
- if (!prev2) continue;
26409
- const p = (prev2.high + prev2.low + prev2.close) / 3;
27863
+ const key = periodKey(anchor, bars[i].time);
27864
+ const fresh = key !== lastKey;
27865
+ lastKey = key;
27866
+ if (fresh) continue;
27867
+ const prev2 = aggs.get(key - 1);
27868
+ const curr = aggs.get(key);
27869
+ if (!prev2 || !curr) continue;
27870
+ const range = prev2.high - prev2.low;
27871
+ const traditional = (prev2.high + prev2.low + prev2.close) / 3;
27872
+ const p = kind === PIVOT_WOODIE ? (prev2.high + prev2.low + 2 * curr.open) / 4 : traditional;
26410
27873
  levels.p[i] = p;
26411
- levels.r1[i] = 2 * p - prev2.low;
26412
- levels.s1[i] = 2 * p - prev2.high;
26413
- levels.r2[i] = p + (prev2.high - prev2.low);
26414
- levels.s2[i] = p - (prev2.high - prev2.low);
26415
- levels.r3[i] = prev2.high + 2 * (p - prev2.low);
26416
- levels.s3[i] = prev2.low - 2 * (prev2.high - p);
26417
- }
26418
- const plot = (key, title, color) => ({ key, title, kind: "step", values: levels[key], color });
26419
- return {
26420
- plots: [
26421
- plot("p", "P", WARNING),
26422
- plot("r1", "R1", BEARISH),
26423
- plot("s1", "S1", BULLISH),
26424
- plot("r2", "R2", BEARISH),
26425
- plot("s2", "S2", BULLISH),
26426
- plot("r3", "R3", BEARISH),
26427
- plot("s3", "S3", BULLISH)
26428
- ]
26429
- };
27874
+ if (kind === PIVOT_FIBONACCI) {
27875
+ levels.r1[i] = traditional + 0.382 * range;
27876
+ levels.s1[i] = traditional - 0.382 * range;
27877
+ levels.r2[i] = traditional + 0.618 * range;
27878
+ levels.s2[i] = traditional - 0.618 * range;
27879
+ levels.r3[i] = traditional + range;
27880
+ levels.s3[i] = traditional - range;
27881
+ } else if (kind === PIVOT_CAMARILLA) {
27882
+ const cam = 1.1 * range;
27883
+ levels.r1[i] = prev2.close + cam / 12;
27884
+ levels.s1[i] = prev2.close - cam / 12;
27885
+ levels.r2[i] = prev2.close + cam / 6;
27886
+ levels.s2[i] = prev2.close - cam / 6;
27887
+ levels.r3[i] = prev2.close + cam / 4;
27888
+ levels.s3[i] = prev2.close - cam / 4;
27889
+ levels.r4[i] = prev2.close + cam / 2;
27890
+ levels.s4[i] = prev2.close - cam / 2;
27891
+ } else {
27892
+ levels.r1[i] = 2 * p - prev2.low;
27893
+ levels.s1[i] = 2 * p - prev2.high;
27894
+ levels.r2[i] = p + range;
27895
+ levels.s2[i] = p - range;
27896
+ if (kind !== PIVOT_WOODIE) {
27897
+ levels.r3[i] = prev2.high + 2 * (p - prev2.low);
27898
+ levels.s3[i] = prev2.low - 2 * (prev2.high - p);
27899
+ }
27900
+ }
27901
+ }
27902
+ const resistance = str3(inputs, "resistanceColor", BEARISH);
27903
+ const support = str3(inputs, "supportColor", BULLISH);
27904
+ const plot = (key, title, color) => ({ key, title, values: levels[key], color });
27905
+ const plots = [
27906
+ plot("p", "P", str3(inputs, "pivotColor", WARNING)),
27907
+ plot("r1", "R1", resistance),
27908
+ plot("s1", "S1", support),
27909
+ plot("r2", "R2", resistance),
27910
+ plot("s2", "S2", support)
27911
+ ];
27912
+ if (kind !== PIVOT_WOODIE) plots.push(plot("r3", "R3", resistance), plot("s3", "S3", support));
27913
+ if (kind === PIVOT_CAMARILLA) plots.push(plot("r4", "R4", resistance), plot("s4", "S4", support));
27914
+ return { plots };
26430
27915
  }
26431
27916
  };
26432
27917
  var fiftyTwoWeek = {
@@ -26434,15 +27919,25 @@ var fiftyTwoWeek = {
26434
27919
  title: "52 Week High/Low",
26435
27920
  shortTitle: "52W H/L",
26436
27921
  overlay: true,
26437
- inputs: [{ key: "weeks", title: "Weeks", type: "int", defval: 52, min: 1, max: 520, step: 1 }],
27922
+ inputs: [
27923
+ intInput("weeks", "Weeks", 52, 1, 520, "Length of the rolling window, in weeks."),
27924
+ boolInput("showAllTime", "Show All-Time High/Low", false, SETTINGS, "Additionally display the all-time high and low, tracked over the symbol's full loaded history."),
27925
+ colorInput(BULLISH, "52 Week High", "highColor", "Color of the 52 week high level."),
27926
+ colorInput(BEARISH, "52 Week Low", "lowColor", "Color of the 52 week low level."),
27927
+ colorInput(NEUTRAL, "All-Time High/Low", "allTimeColor", "Color of the all-time high and all-time low levels.")
27928
+ ],
26438
27929
  compute: (bars, inputs) => {
26439
27930
  const span = num3(inputs, "weeks", 52) * 7 * DAY_MS2;
26440
27931
  const n = bars.length;
26441
27932
  const hi = new Array(n).fill(Number.NaN);
26442
27933
  const lo = new Array(n).fill(Number.NaN);
27934
+ const ath = new Array(n).fill(Number.NaN);
27935
+ const atl = new Array(n).fill(Number.NaN);
26443
27936
  const maxIdx = [];
26444
27937
  const minIdx = [];
26445
27938
  let from = 0;
27939
+ let runHigh = -Infinity;
27940
+ let runLow = Infinity;
26446
27941
  for (let i = 0; i < n; i++) {
26447
27942
  const b = bars[i];
26448
27943
  while (maxIdx.length > 0 && bars[maxIdx[maxIdx.length - 1]].high <= b.high) maxIdx.pop();
@@ -26454,13 +27949,20 @@ var fiftyTwoWeek = {
26454
27949
  while (minIdx[0] < from) minIdx.shift();
26455
27950
  hi[i] = bars[maxIdx[0]].high;
26456
27951
  lo[i] = bars[minIdx[0]].low;
27952
+ runHigh = Math.max(runHigh, b.high);
27953
+ runLow = Math.min(runLow, b.low);
27954
+ ath[i] = runHigh;
27955
+ atl[i] = runLow;
27956
+ }
27957
+ const plots = [
27958
+ { key: "high", title: "52 Week High", values: hi, kind: "step", color: str3(inputs, "highColor", BULLISH), width: 2 },
27959
+ { key: "low", title: "52 Week Low", values: lo, kind: "step", color: str3(inputs, "lowColor", BEARISH), width: 2 }
27960
+ ];
27961
+ if (bool(inputs, "showAllTime", false)) {
27962
+ const ink = str3(inputs, "allTimeColor", NEUTRAL);
27963
+ plots.push({ key: "ath", title: "All-Time High", values: ath, kind: "step", color: ink }, { key: "atl", title: "All-Time Low", values: atl, kind: "step", color: ink });
26457
27964
  }
26458
- return {
26459
- plots: [
26460
- { key: "high", title: "52W high", values: hi, kind: "step", color: BULLISH },
26461
- { key: "low", title: "52W low", values: lo, kind: "step", color: BEARISH }
26462
- ]
26463
- };
27965
+ return { plots };
26464
27966
  }
26465
27967
  };
26466
27968
  var zigzag = {
@@ -26468,9 +27970,11 @@ var zigzag = {
26468
27970
  title: "ZigZag",
26469
27971
  overlay: true,
26470
27972
  inputs: [
26471
- { key: "deviation", title: "Deviation %", type: "float", defval: 5, min: 0.01, max: 100, step: 0.1 },
26472
- { key: "depth", title: "Depth", type: "int", defval: 10, min: 1, max: 500, step: 1 },
26473
- colorInput()
27973
+ floatInput("deviation", "Deviation (%)", 5, 0.01, 100, 0.1, "Minimum reversal from the leg extreme, as a percentage of that extreme, required to confirm a new pivot."),
27974
+ { ...intInput("depth", "Depth", 10, 2, 500, "Minimum number of bars required between two consecutive pivots.") },
27975
+ colorInput(BULLISH, "Up Leg", "bullColor", "Color of rising zigzag segments."),
27976
+ colorInput(BEARISH, "Down Leg", "bearColor", "Color of falling zigzag segments."),
27977
+ { key: "lineWidth", title: "Width", type: "int", defval: 2, min: 1, max: 10, step: 1, group: STYLE, tooltip: "Width of the zigzag segments." }
26474
27978
  ],
26475
27979
  compute: (bars, inputs) => {
26476
27980
  const dev = num3(inputs, "deviation", 5) / 100;
@@ -26483,14 +27987,14 @@ var zigzag = {
26483
27987
  const b = bars[i];
26484
27988
  if (up) {
26485
27989
  if (b.high >= ext.price) ext = { i, price: b.high };
26486
- else if (b.low <= ext.price * (1 - dev) && i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
27990
+ else if (b.low <= ext.price * (1 - dev) && ext.i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
26487
27991
  pivots.push(ext);
26488
27992
  up = false;
26489
27993
  ext = { i, price: b.low };
26490
27994
  }
26491
27995
  } else if (b.low <= ext.price) {
26492
27996
  ext = { i, price: b.low };
26493
- } else if (b.high >= ext.price * (1 + dev) && i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
27997
+ } else if (b.high >= ext.price * (1 + dev) && ext.i - (pivots[pivots.length - 1]?.i ?? -depth) >= depth) {
26494
27998
  pivots.push(ext);
26495
27999
  up = true;
26496
28000
  ext = { i, price: b.high };
@@ -26498,22 +28002,29 @@ var zigzag = {
26498
28002
  }
26499
28003
  pivots.push(ext);
26500
28004
  }
26501
- return {
26502
- plots: [],
26503
- polylines: [
26504
- {
26505
- key: "zigzag",
26506
- points: pivots.map((p) => ({ xloc: "bar_time", x: bars[p.i].time, price: p.price })),
26507
- curved: false,
26508
- closed: false,
26509
- lineColor: str3(inputs, "color", SERIES_LINE),
26510
- lineWidth: 2,
26511
- lineStyle: "solid",
26512
- arrowLeft: false,
26513
- arrowRight: false
26514
- }
26515
- ]
26516
- };
28005
+ const bull = str3(inputs, "bullColor", BULLISH);
28006
+ const bear = str3(inputs, "bearColor", BEARISH);
28007
+ const width = num3(inputs, "lineWidth", 2);
28008
+ const polylines = [];
28009
+ for (let k = 1; k < pivots.length; k++) {
28010
+ const a = pivots[k - 1];
28011
+ const b = pivots[k];
28012
+ polylines.push({
28013
+ key: `leg${k}`,
28014
+ points: [
28015
+ { xloc: "bar_time", x: bars[a.i].time, price: a.price },
28016
+ { xloc: "bar_time", x: bars[b.i].time, price: b.price }
28017
+ ],
28018
+ curved: false,
28019
+ closed: false,
28020
+ lineColor: b.price >= a.price ? bull : bear,
28021
+ lineWidth: width,
28022
+ lineStyle: "solid",
28023
+ arrowLeft: false,
28024
+ arrowRight: false
28025
+ });
28026
+ }
28027
+ return { plots: [], polylines };
26517
28028
  }
26518
28029
  };
26519
28030
  var williamsFractal = {
@@ -26521,10 +28032,16 @@ var williamsFractal = {
26521
28032
  title: "Williams Fractal",
26522
28033
  shortTitle: "Fractals",
26523
28034
  overlay: true,
26524
- inputs: [{ key: "periods", title: "Periods", type: "int", defval: 2, min: 1, max: 50, step: 1 }],
28035
+ inputs: [
28036
+ intInput("periods", "Periods", 2, 2, 50, "Bars required on each side of the candidate bar. 2 is Bill Williams' classic 5-bar fractal; a fractal is confirmed only once this many later bars have closed."),
28037
+ colorInput(BEARISH, "Up Fractal", "upColor", "Color of the up fractal marker (swing high), drawn above the fractal bar."),
28038
+ colorInput(BULLISH, "Down Fractal", "downColor", "Color of the down fractal marker (swing low), drawn below the fractal bar.")
28039
+ ],
26525
28040
  compute: (bars, inputs) => {
26526
28041
  const p = num3(inputs, "periods", 2);
26527
28042
  const markers = [];
28043
+ const up = str3(inputs, "upColor", BEARISH);
28044
+ const down = str3(inputs, "downColor", BULLISH);
26528
28045
  const isExtreme = (i, pick, better) => {
26529
28046
  const v = pick(bars[i]);
26530
28047
  for (let k = i - p; k <= i + p; k++) {
@@ -26534,12 +28051,8 @@ var williamsFractal = {
26534
28051
  return true;
26535
28052
  };
26536
28053
  for (let i = p; i < bars.length - p; i++) {
26537
- if (isExtreme(i, (b) => b.high, (a, b) => a >= b)) {
26538
- markers.push({ time: bars[i].time, position: "aboveBar", shape: "triangleup", color: BULLISH });
26539
- }
26540
- if (isExtreme(i, (b) => b.low, (a, b) => a <= b)) {
26541
- markers.push({ time: bars[i].time, position: "belowBar", shape: "triangledown", color: BEARISH });
26542
- }
28054
+ if (isExtreme(i, (b) => b.high, (a, b) => a > b)) markers.push({ time: bars[i].time, position: "aboveBar", shape: "triangleup", color: up });
28055
+ if (isExtreme(i, (b) => b.low, (a, b) => a < b)) markers.push({ time: bars[i].time, position: "belowBar", shape: "triangledown", color: down });
26543
28056
  }
26544
28057
  return { plots: [], markers };
26545
28058
  }
@@ -26911,4 +28424,4 @@ function resolveElement(container) {
26911
28424
  return element;
26912
28425
  }
26913
28426
 
26914
- export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MarksControl, MultiProviderFeed, NativeRenderer, RendererControl, SecondClock, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isGroupRow, normalizeSession, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
28427
+ export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MarksControl, MultiProviderFeed, NativeRenderer, RendererControl, SecondClock, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isExchangeTimezone, isGroupRow, normalizeSession, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, resolveTimezone, sharedBarStore, timeframeToMs, timezoneMenuRows, tzButtonLabel, tzMenuLabel, tzOffset };