hyperprop-charting-library 0.1.119 → 0.1.120

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.
@@ -435,6 +435,60 @@ var computeStdDevSeries = (data, length, source) => {
435
435
  }
436
436
  return result;
437
437
  };
438
+ var VWAP_SESSION_ANCHOR_MS = 22 * 36e5;
439
+ var vwapSessionDayOf = (ms) => Math.floor((ms - VWAP_SESSION_ANCHOR_MS) / 864e5);
440
+ var computeVwapSeries = (data, band) => {
441
+ const result = new Array(data.length).fill(null);
442
+ let session = Number.NaN;
443
+ let cumPv = 0;
444
+ let cumPv2 = 0;
445
+ let cumV = 0;
446
+ for (let i = 0; i < data.length; i += 1) {
447
+ const point = data[i];
448
+ if (!point) continue;
449
+ const day = vwapSessionDayOf(point.time.getTime());
450
+ if (day !== session) {
451
+ session = day;
452
+ cumPv = 0;
453
+ cumPv2 = 0;
454
+ cumV = 0;
455
+ }
456
+ const typical = (point.h + point.l + point.c) / 3;
457
+ const volume = Math.max(0, point.v ?? 0);
458
+ cumPv += typical * volume;
459
+ cumPv2 += typical * typical * volume;
460
+ cumV += volume;
461
+ if (cumV <= 0) continue;
462
+ const vwap = cumPv / cumV;
463
+ if (band === "vwap") {
464
+ result[i] = vwap;
465
+ } else {
466
+ const variance = Math.max(0, cumPv2 / cumV - vwap * vwap);
467
+ result[i] = Math.sqrt(variance);
468
+ }
469
+ }
470
+ for (let i = 1; i < data.length; i += 1) {
471
+ const point = data[i];
472
+ const prev = data[i - 1];
473
+ const next = data[i + 1];
474
+ if (!point || !prev || !next) continue;
475
+ const day = vwapSessionDayOf(point.time.getTime());
476
+ if (day !== vwapSessionDayOf(prev.time.getTime()) && day === vwapSessionDayOf(next.time.getTime())) {
477
+ result[i] = null;
478
+ }
479
+ }
480
+ return result;
481
+ };
482
+ var computeBollingerSeries = (data, length, multiplier, source, band) => {
483
+ const basis = computeSmaSeries(data, length, source);
484
+ if (band === "basis") return basis;
485
+ const dev = computeStdDevSeries(data, length, source);
486
+ return basis.map((value, idx) => {
487
+ const sigma = dev[idx];
488
+ if (value == null || sigma == null) return null;
489
+ return band === "upper" ? value + multiplier * sigma : value - multiplier * sigma;
490
+ });
491
+ };
438
492
  var computeRsiSeries = (data, length) => {
439
493
  const result = new Array(data.length).fill(null);
440
494
  if (data.length < 2) return result;
@@ -561,6 +615,39 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
561
615
  }
562
616
  ctx.restore();
563
617
  };
618
+ var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
619
+ if (!renderContext.yFromPrice || opacity <= 0) return;
620
+ const yFromPrice = renderContext.yFromPrice;
621
+ ctx.save();
622
+ ctx.globalAlpha = Math.min(1, Math.max(0, opacity));
623
+ ctx.fillStyle = color;
624
+ let runStart = -1;
625
+ const flushRun = (endIndexExclusive) => {
626
+ if (runStart < 0) return;
627
+ ctx.beginPath();
628
+ for (let i = runStart; i < endIndexExclusive; i += 1) {
629
+ const x = renderContext.xFromIndex(i);
630
+ const y = yFromPrice(upper[i]);
631
+ if (i === runStart) ctx.moveTo(x, y);
632
+ else ctx.lineTo(x, y);
633
+ }
634
+ for (let i = endIndexExclusive - 1; i >= runStart; i -= 1) {
635
+ ctx.lineTo(renderContext.xFromIndex(i), yFromPrice(lower[i]));
636
+ }
637
+ ctx.closePath();
638
+ ctx.fill();
639
+ runStart = -1;
640
+ };
641
+ for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
642
+ const up = upper[index];
643
+ const lo = lower[index];
644
+ const valid = Number.isFinite(up ?? Number.NaN) && Number.isFinite(lo ?? Number.NaN);
645
+ if (valid && runStart < 0) runStart = index;
646
+ if (!valid) flushRun(index);
647
+ }
648
+ flushRun(renderContext.endIndex + 1);
649
+ ctx.restore();
650
+ };
564
651
  var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
565
652
  const visible = [];
566
653
  for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
@@ -822,6 +909,89 @@ var BUILTIN_HMA_INDICATOR = {
822
909
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
823
910
  }
824
911
  };
912
+ var BUILTIN_VWAP_INDICATOR = {
913
+ id: "vwap",
914
+ name: "VWAP",
915
+ pane: "overlay",
916
+ defaultInputs: {
917
+ color: "#f59e0b",
918
+ width: 2,
919
+ showBands: false,
920
+ bandMultiplier: 1,
921
+ bandColor: "#94a3b8",
922
+ bandFillOpacity: 0.06
923
+ },
924
+ draw: (ctx, renderContext, inputs) => {
925
+ const vwap = withCachedSeries(
926
+ "vwap|line",
927
+ renderContext.data,
928
+ () => computeVwapSeries(renderContext.data, "vwap")
929
+ );
930
+ if (inputs.showBands) {
931
+ const sigma = withCachedSeries(
932
+ "vwap|sigma",
933
+ renderContext.data,
934
+ () => computeVwapSeries(renderContext.data, "sigma")
935
+ );
936
+ const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
937
+ const upper = vwap.map((value, idx) => {
938
+ const s = sigma[idx];
939
+ return value == null || s == null ? null : value + multiplier * s;
940
+ });
941
+ const lower = vwap.map((value, idx) => {
942
+ const s = sigma[idx];
943
+ return value == null || s == null ? null : value - multiplier * s;
944
+ });
945
+ const bandColor = inputs.bandColor ?? "#94a3b8";
946
+ fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.bandFillOpacity) || 0.06);
947
+ drawOverlaySeries(ctx, renderContext, upper, bandColor, 1);
948
+ drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
949
+ }
950
+ drawOverlaySeries(ctx, renderContext, vwap, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
951
+ }
952
+ };
953
+ var BUILTIN_BOLLINGER_INDICATOR = {
954
+ id: "bollinger",
955
+ name: "BB",
956
+ pane: "overlay",
957
+ // Basis is pink so it stays distinguishable from the (orange) VWAP line
958
+ // when both overlays are active.
959
+ defaultInputs: {
960
+ length: 20,
961
+ multiplier: 2,
962
+ source: "close",
963
+ basisColor: "#ec4899",
964
+ bandColor: "#3b82f6",
965
+ width: 1.5,
966
+ fillOpacity: 0.05
967
+ },
968
+ draw: (ctx, renderContext, inputs) => {
969
+ const length = clampIndicatorLength(inputs.length, 20);
970
+ const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
971
+ const source = inputs.source ?? "close";
972
+ const basis = withCachedSeries(
973
+ `bb|basis|${length}|${source}`,
974
+ renderContext.data,
975
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "basis")
976
+ );
977
+ const upper = withCachedSeries(
978
+ `bb|upper|${length}|${multiplier}|${source}`,
979
+ renderContext.data,
980
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "upper")
981
+ );
982
+ const lower = withCachedSeries(
983
+ `bb|lower|${length}|${multiplier}|${source}`,
984
+ renderContext.data,
985
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "lower")
986
+ );
987
+ const bandColor = inputs.bandColor ?? "#3b82f6";
988
+ const width = Number(inputs.width) || 1.5;
989
+ fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.05);
990
+ drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
991
+ drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
992
+ drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#f59e0b", width);
993
+ }
994
+ };
825
995
  var BUILTIN_STDDEV_INDICATOR = {
826
996
  id: "stddev",
827
997
  name: "StdDev",
@@ -905,6 +1075,8 @@ var BUILTIN_INDICATORS = [
905
1075
  BUILTIN_VWMA_INDICATOR,
906
1076
  BUILTIN_RMA_INDICATOR,
907
1077
  BUILTIN_HMA_INDICATOR,
1078
+ BUILTIN_VWAP_INDICATOR,
1079
+ BUILTIN_BOLLINGER_INDICATOR,
908
1080
  BUILTIN_STDDEV_INDICATOR,
909
1081
  BUILTIN_ATR_INDICATOR
910
1082
  ];
@@ -2757,15 +2929,31 @@ function createChart(element, options = {}) {
2757
2929
  const pct = diff / base * 100;
2758
2930
  const ticks = tick > 0 ? Math.round(diff / tick) : 0;
2759
2931
  const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
2760
- const labelText = tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`;
2932
+ const labelLines = [
2933
+ tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`
2934
+ ];
2935
+ const barSpan = Math.abs(Math.round(p1.index) - Math.round(p0.index));
2936
+ if (barSpan > 0) {
2937
+ const t0 = getTimeForIndex(Math.round(p0.index));
2938
+ const t1 = getTimeForIndex(Math.round(p1.index));
2939
+ const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
2940
+ labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatDuration(spanMs)}` : `${barSpan} bars`);
2941
+ }
2942
+ const pointValue = Number(drawing.pointValue);
2943
+ if (Number.isFinite(pointValue) && pointValue > 0) {
2944
+ const dollars = diff * pointValue;
2945
+ const money = Math.abs(dollars).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
2946
+ labelLines.push(`${signed(dollars, `$${money}`)} / contract`);
2947
+ }
2761
2948
  const prevFont = ctx.font;
2762
2949
  ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2763
2950
  const padding = 6;
2764
- const textW = ctx.measureText(labelText).width;
2951
+ const lineHeight = 15;
2952
+ const textW = Math.max(...labelLines.map((line) => ctx.measureText(line).width));
2765
2953
  const pillW = textW + padding * 2;
2766
- const pillH = 18;
2954
+ const pillH = labelLines.length * lineHeight + padding;
2767
2955
  const pillX = midX - pillW / 2;
2768
- const pillY = botY + 6;
2956
+ const pillY = botY + 6 + pillH <= fullChartBottom - 2 ? botY + 6 : Math.max(chartTop + 2, topY - 6 - pillH);
2769
2957
  ctx.fillStyle = mergedOptions.backgroundColor;
2770
2958
  fillRoundedRect(pillX, pillY, pillW, pillH, 4);
2771
2959
  ctx.save();
@@ -2776,7 +2964,9 @@ function createChart(element, options = {}) {
2776
2964
  ctx.fillStyle = drawing.color;
2777
2965
  ctx.textAlign = "center";
2778
2966
  ctx.textBaseline = "middle";
2779
- ctx.fillText(labelText, midX, pillY + pillH / 2);
2967
+ labelLines.forEach((line, lineIndex) => {
2968
+ ctx.fillText(line, midX, pillY + padding / 2 + lineIndex * lineHeight + lineHeight / 2);
2969
+ });
2780
2970
  ctx.font = prevFont;
2781
2971
  if (drawing.label) {
2782
2972
  drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
@@ -3552,8 +3742,9 @@ function createChart(element, options = {}) {
3552
3742
  }
3553
3743
  return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
3554
3744
  };
3745
+ const LEGEND_EXCLUDED_INPUT_KEYS = /* @__PURE__ */ new Set(["width", "bandMultiplier", "bandFillOpacity", "fillOpacity"]);
3555
3746
  const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
3556
- const inputValues = Object.entries(indicator.inputs).filter(([, value]) => isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
3747
+ const inputValues = Object.entries(indicator.inputs).filter(([key, value]) => !LEGEND_EXCLUDED_INPUT_KEYS.has(key) && typeof value !== "boolean" && isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
3557
3748
  if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
3558
3749
  return `${plugin.name} ${inputValues.join(" ")}`;
3559
3750
  }
@@ -4531,7 +4722,9 @@ function createChart(element, options = {}) {
4531
4722
  points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
4532
4723
  color: defaults.color ?? "#2962ff",
4533
4724
  style: defaults.style ?? "solid",
4534
- width: defaults.width ?? 1
4725
+ width: defaults.width ?? 1,
4726
+ // pointValue enables the dollar-P&L line in the measure label.
4727
+ ...defaults.pointValue === void 0 ? {} : { pointValue: defaults.pointValue }
4535
4728
  })
4536
4729
  );
4537
4730
  emitDrawingsChange();
@@ -409,6 +409,60 @@ var computeStdDevSeries = (data, length, source) => {
409
409
  }
410
410
  return result;
411
411
  };
412
+ var VWAP_SESSION_ANCHOR_MS = 22 * 36e5;
413
+ var vwapSessionDayOf = (ms) => Math.floor((ms - VWAP_SESSION_ANCHOR_MS) / 864e5);
414
+ var computeVwapSeries = (data, band) => {
415
+ const result = new Array(data.length).fill(null);
416
+ let session = Number.NaN;
417
+ let cumPv = 0;
418
+ let cumPv2 = 0;
419
+ let cumV = 0;
420
+ for (let i = 0; i < data.length; i += 1) {
421
+ const point = data[i];
422
+ if (!point) continue;
423
+ const day = vwapSessionDayOf(point.time.getTime());
424
+ if (day !== session) {
425
+ session = day;
426
+ cumPv = 0;
427
+ cumPv2 = 0;
428
+ cumV = 0;
429
+ }
430
+ const typical = (point.h + point.l + point.c) / 3;
431
+ const volume = Math.max(0, point.v ?? 0);
432
+ cumPv += typical * volume;
433
+ cumPv2 += typical * typical * volume;
434
+ cumV += volume;
435
+ if (cumV <= 0) continue;
436
+ const vwap = cumPv / cumV;
437
+ if (band === "vwap") {
438
+ result[i] = vwap;
439
+ } else {
440
+ const variance = Math.max(0, cumPv2 / cumV - vwap * vwap);
441
+ result[i] = Math.sqrt(variance);
442
+ }
443
+ }
444
+ for (let i = 1; i < data.length; i += 1) {
445
+ const point = data[i];
446
+ const prev = data[i - 1];
447
+ const next = data[i + 1];
448
+ if (!point || !prev || !next) continue;
449
+ const day = vwapSessionDayOf(point.time.getTime());
450
+ if (day !== vwapSessionDayOf(prev.time.getTime()) && day === vwapSessionDayOf(next.time.getTime())) {
451
+ result[i] = null;
452
+ }
453
+ }
454
+ return result;
455
+ };
456
+ var computeBollingerSeries = (data, length, multiplier, source, band) => {
457
+ const basis = computeSmaSeries(data, length, source);
458
+ if (band === "basis") return basis;
459
+ const dev = computeStdDevSeries(data, length, source);
460
+ return basis.map((value, idx) => {
461
+ const sigma = dev[idx];
462
+ if (value == null || sigma == null) return null;
463
+ return band === "upper" ? value + multiplier * sigma : value - multiplier * sigma;
464
+ });
465
+ };
412
466
  var computeRsiSeries = (data, length) => {
413
467
  const result = new Array(data.length).fill(null);
414
468
  if (data.length < 2) return result;
@@ -535,6 +589,39 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
535
589
  }
536
590
  ctx.restore();
537
591
  };
592
+ var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
593
+ if (!renderContext.yFromPrice || opacity <= 0) return;
594
+ const yFromPrice = renderContext.yFromPrice;
595
+ ctx.save();
596
+ ctx.globalAlpha = Math.min(1, Math.max(0, opacity));
597
+ ctx.fillStyle = color;
598
+ let runStart = -1;
599
+ const flushRun = (endIndexExclusive) => {
600
+ if (runStart < 0) return;
601
+ ctx.beginPath();
602
+ for (let i = runStart; i < endIndexExclusive; i += 1) {
603
+ const x = renderContext.xFromIndex(i);
604
+ const y = yFromPrice(upper[i]);
605
+ if (i === runStart) ctx.moveTo(x, y);
606
+ else ctx.lineTo(x, y);
607
+ }
608
+ for (let i = endIndexExclusive - 1; i >= runStart; i -= 1) {
609
+ ctx.lineTo(renderContext.xFromIndex(i), yFromPrice(lower[i]));
610
+ }
611
+ ctx.closePath();
612
+ ctx.fill();
613
+ runStart = -1;
614
+ };
615
+ for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
616
+ const up = upper[index];
617
+ const lo = lower[index];
618
+ const valid = Number.isFinite(up ?? Number.NaN) && Number.isFinite(lo ?? Number.NaN);
619
+ if (valid && runStart < 0) runStart = index;
620
+ if (!valid) flushRun(index);
621
+ }
622
+ flushRun(renderContext.endIndex + 1);
623
+ ctx.restore();
624
+ };
538
625
  var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
539
626
  const visible = [];
540
627
  for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
@@ -796,6 +883,89 @@ var BUILTIN_HMA_INDICATOR = {
796
883
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
797
884
  }
798
885
  };
886
+ var BUILTIN_VWAP_INDICATOR = {
887
+ id: "vwap",
888
+ name: "VWAP",
889
+ pane: "overlay",
890
+ defaultInputs: {
891
+ color: "#f59e0b",
892
+ width: 2,
893
+ showBands: false,
894
+ bandMultiplier: 1,
895
+ bandColor: "#94a3b8",
896
+ bandFillOpacity: 0.06
897
+ },
898
+ draw: (ctx, renderContext, inputs) => {
899
+ const vwap = withCachedSeries(
900
+ "vwap|line",
901
+ renderContext.data,
902
+ () => computeVwapSeries(renderContext.data, "vwap")
903
+ );
904
+ if (inputs.showBands) {
905
+ const sigma = withCachedSeries(
906
+ "vwap|sigma",
907
+ renderContext.data,
908
+ () => computeVwapSeries(renderContext.data, "sigma")
909
+ );
910
+ const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
911
+ const upper = vwap.map((value, idx) => {
912
+ const s = sigma[idx];
913
+ return value == null || s == null ? null : value + multiplier * s;
914
+ });
915
+ const lower = vwap.map((value, idx) => {
916
+ const s = sigma[idx];
917
+ return value == null || s == null ? null : value - multiplier * s;
918
+ });
919
+ const bandColor = inputs.bandColor ?? "#94a3b8";
920
+ fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.bandFillOpacity) || 0.06);
921
+ drawOverlaySeries(ctx, renderContext, upper, bandColor, 1);
922
+ drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
923
+ }
924
+ drawOverlaySeries(ctx, renderContext, vwap, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
925
+ }
926
+ };
927
+ var BUILTIN_BOLLINGER_INDICATOR = {
928
+ id: "bollinger",
929
+ name: "BB",
930
+ pane: "overlay",
931
+ // Basis is pink so it stays distinguishable from the (orange) VWAP line
932
+ // when both overlays are active.
933
+ defaultInputs: {
934
+ length: 20,
935
+ multiplier: 2,
936
+ source: "close",
937
+ basisColor: "#ec4899",
938
+ bandColor: "#3b82f6",
939
+ width: 1.5,
940
+ fillOpacity: 0.05
941
+ },
942
+ draw: (ctx, renderContext, inputs) => {
943
+ const length = clampIndicatorLength(inputs.length, 20);
944
+ const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
945
+ const source = inputs.source ?? "close";
946
+ const basis = withCachedSeries(
947
+ `bb|basis|${length}|${source}`,
948
+ renderContext.data,
949
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "basis")
950
+ );
951
+ const upper = withCachedSeries(
952
+ `bb|upper|${length}|${multiplier}|${source}`,
953
+ renderContext.data,
954
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "upper")
955
+ );
956
+ const lower = withCachedSeries(
957
+ `bb|lower|${length}|${multiplier}|${source}`,
958
+ renderContext.data,
959
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "lower")
960
+ );
961
+ const bandColor = inputs.bandColor ?? "#3b82f6";
962
+ const width = Number(inputs.width) || 1.5;
963
+ fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.05);
964
+ drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
965
+ drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
966
+ drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#f59e0b", width);
967
+ }
968
+ };
799
969
  var BUILTIN_STDDEV_INDICATOR = {
800
970
  id: "stddev",
801
971
  name: "StdDev",
@@ -879,6 +1049,8 @@ var BUILTIN_INDICATORS = [
879
1049
  BUILTIN_VWMA_INDICATOR,
880
1050
  BUILTIN_RMA_INDICATOR,
881
1051
  BUILTIN_HMA_INDICATOR,
1052
+ BUILTIN_VWAP_INDICATOR,
1053
+ BUILTIN_BOLLINGER_INDICATOR,
882
1054
  BUILTIN_STDDEV_INDICATOR,
883
1055
  BUILTIN_ATR_INDICATOR
884
1056
  ];
@@ -2731,15 +2903,31 @@ function createChart(element, options = {}) {
2731
2903
  const pct = diff / base * 100;
2732
2904
  const ticks = tick > 0 ? Math.round(diff / tick) : 0;
2733
2905
  const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
2734
- const labelText = tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`;
2906
+ const labelLines = [
2907
+ tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`
2908
+ ];
2909
+ const barSpan = Math.abs(Math.round(p1.index) - Math.round(p0.index));
2910
+ if (barSpan > 0) {
2911
+ const t0 = getTimeForIndex(Math.round(p0.index));
2912
+ const t1 = getTimeForIndex(Math.round(p1.index));
2913
+ const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
2914
+ labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatDuration(spanMs)}` : `${barSpan} bars`);
2915
+ }
2916
+ const pointValue = Number(drawing.pointValue);
2917
+ if (Number.isFinite(pointValue) && pointValue > 0) {
2918
+ const dollars = diff * pointValue;
2919
+ const money = Math.abs(dollars).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
2920
+ labelLines.push(`${signed(dollars, `$${money}`)} / contract`);
2921
+ }
2735
2922
  const prevFont = ctx.font;
2736
2923
  ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2737
2924
  const padding = 6;
2738
- const textW = ctx.measureText(labelText).width;
2925
+ const lineHeight = 15;
2926
+ const textW = Math.max(...labelLines.map((line) => ctx.measureText(line).width));
2739
2927
  const pillW = textW + padding * 2;
2740
- const pillH = 18;
2928
+ const pillH = labelLines.length * lineHeight + padding;
2741
2929
  const pillX = midX - pillW / 2;
2742
- const pillY = botY + 6;
2930
+ const pillY = botY + 6 + pillH <= fullChartBottom - 2 ? botY + 6 : Math.max(chartTop + 2, topY - 6 - pillH);
2743
2931
  ctx.fillStyle = mergedOptions.backgroundColor;
2744
2932
  fillRoundedRect(pillX, pillY, pillW, pillH, 4);
2745
2933
  ctx.save();
@@ -2750,7 +2938,9 @@ function createChart(element, options = {}) {
2750
2938
  ctx.fillStyle = drawing.color;
2751
2939
  ctx.textAlign = "center";
2752
2940
  ctx.textBaseline = "middle";
2753
- ctx.fillText(labelText, midX, pillY + pillH / 2);
2941
+ labelLines.forEach((line, lineIndex) => {
2942
+ ctx.fillText(line, midX, pillY + padding / 2 + lineIndex * lineHeight + lineHeight / 2);
2943
+ });
2754
2944
  ctx.font = prevFont;
2755
2945
  if (drawing.label) {
2756
2946
  drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
@@ -3526,8 +3716,9 @@ function createChart(element, options = {}) {
3526
3716
  }
3527
3717
  return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
3528
3718
  };
3719
+ const LEGEND_EXCLUDED_INPUT_KEYS = /* @__PURE__ */ new Set(["width", "bandMultiplier", "bandFillOpacity", "fillOpacity"]);
3529
3720
  const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
3530
- const inputValues = Object.entries(indicator.inputs).filter(([, value]) => isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
3721
+ const inputValues = Object.entries(indicator.inputs).filter(([key, value]) => !LEGEND_EXCLUDED_INPUT_KEYS.has(key) && typeof value !== "boolean" && isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
3531
3722
  if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
3532
3723
  return `${plugin.name} ${inputValues.join(" ")}`;
3533
3724
  }
@@ -4505,7 +4696,9 @@ function createChart(element, options = {}) {
4505
4696
  points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
4506
4697
  color: defaults.color ?? "#2962ff",
4507
4698
  style: defaults.style ?? "solid",
4508
- width: defaults.width ?? 1
4699
+ width: defaults.width ?? 1,
4700
+ // pointValue enables the dollar-P&L line in the measure label.
4701
+ ...defaults.pointValue === void 0 ? {} : { pointValue: defaults.pointValue }
4509
4702
  })
4510
4703
  );
4511
4704
  emitDrawingsChange();
package/dist/index.cjs CHANGED
@@ -435,6 +435,60 @@ var computeStdDevSeries = (data, length, source) => {
435
435
  }
436
436
  return result;
437
437
  };
438
+ var VWAP_SESSION_ANCHOR_MS = 22 * 36e5;
439
+ var vwapSessionDayOf = (ms) => Math.floor((ms - VWAP_SESSION_ANCHOR_MS) / 864e5);
440
+ var computeVwapSeries = (data, band) => {
441
+ const result = new Array(data.length).fill(null);
442
+ let session = Number.NaN;
443
+ let cumPv = 0;
444
+ let cumPv2 = 0;
445
+ let cumV = 0;
446
+ for (let i = 0; i < data.length; i += 1) {
447
+ const point = data[i];
448
+ if (!point) continue;
449
+ const day = vwapSessionDayOf(point.time.getTime());
450
+ if (day !== session) {
451
+ session = day;
452
+ cumPv = 0;
453
+ cumPv2 = 0;
454
+ cumV = 0;
455
+ }
456
+ const typical = (point.h + point.l + point.c) / 3;
457
+ const volume = Math.max(0, point.v ?? 0);
458
+ cumPv += typical * volume;
459
+ cumPv2 += typical * typical * volume;
460
+ cumV += volume;
461
+ if (cumV <= 0) continue;
462
+ const vwap = cumPv / cumV;
463
+ if (band === "vwap") {
464
+ result[i] = vwap;
465
+ } else {
466
+ const variance = Math.max(0, cumPv2 / cumV - vwap * vwap);
467
+ result[i] = Math.sqrt(variance);
468
+ }
469
+ }
470
+ for (let i = 1; i < data.length; i += 1) {
471
+ const point = data[i];
472
+ const prev = data[i - 1];
473
+ const next = data[i + 1];
474
+ if (!point || !prev || !next) continue;
475
+ const day = vwapSessionDayOf(point.time.getTime());
476
+ if (day !== vwapSessionDayOf(prev.time.getTime()) && day === vwapSessionDayOf(next.time.getTime())) {
477
+ result[i] = null;
478
+ }
479
+ }
480
+ return result;
481
+ };
482
+ var computeBollingerSeries = (data, length, multiplier, source, band) => {
483
+ const basis = computeSmaSeries(data, length, source);
484
+ if (band === "basis") return basis;
485
+ const dev = computeStdDevSeries(data, length, source);
486
+ return basis.map((value, idx) => {
487
+ const sigma = dev[idx];
488
+ if (value == null || sigma == null) return null;
489
+ return band === "upper" ? value + multiplier * sigma : value - multiplier * sigma;
490
+ });
491
+ };
438
492
  var computeRsiSeries = (data, length) => {
439
493
  const result = new Array(data.length).fill(null);
440
494
  if (data.length < 2) return result;
@@ -561,6 +615,39 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
561
615
  }
562
616
  ctx.restore();
563
617
  };
618
+ var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
619
+ if (!renderContext.yFromPrice || opacity <= 0) return;
620
+ const yFromPrice = renderContext.yFromPrice;
621
+ ctx.save();
622
+ ctx.globalAlpha = Math.min(1, Math.max(0, opacity));
623
+ ctx.fillStyle = color;
624
+ let runStart = -1;
625
+ const flushRun = (endIndexExclusive) => {
626
+ if (runStart < 0) return;
627
+ ctx.beginPath();
628
+ for (let i = runStart; i < endIndexExclusive; i += 1) {
629
+ const x = renderContext.xFromIndex(i);
630
+ const y = yFromPrice(upper[i]);
631
+ if (i === runStart) ctx.moveTo(x, y);
632
+ else ctx.lineTo(x, y);
633
+ }
634
+ for (let i = endIndexExclusive - 1; i >= runStart; i -= 1) {
635
+ ctx.lineTo(renderContext.xFromIndex(i), yFromPrice(lower[i]));
636
+ }
637
+ ctx.closePath();
638
+ ctx.fill();
639
+ runStart = -1;
640
+ };
641
+ for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
642
+ const up = upper[index];
643
+ const lo = lower[index];
644
+ const valid = Number.isFinite(up ?? Number.NaN) && Number.isFinite(lo ?? Number.NaN);
645
+ if (valid && runStart < 0) runStart = index;
646
+ if (!valid) flushRun(index);
647
+ }
648
+ flushRun(renderContext.endIndex + 1);
649
+ ctx.restore();
650
+ };
564
651
  var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
565
652
  const visible = [];
566
653
  for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
@@ -822,6 +909,89 @@ var BUILTIN_HMA_INDICATOR = {
822
909
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
823
910
  }
824
911
  };
912
+ var BUILTIN_VWAP_INDICATOR = {
913
+ id: "vwap",
914
+ name: "VWAP",
915
+ pane: "overlay",
916
+ defaultInputs: {
917
+ color: "#f59e0b",
918
+ width: 2,
919
+ showBands: false,
920
+ bandMultiplier: 1,
921
+ bandColor: "#94a3b8",
922
+ bandFillOpacity: 0.06
923
+ },
924
+ draw: (ctx, renderContext, inputs) => {
925
+ const vwap = withCachedSeries(
926
+ "vwap|line",
927
+ renderContext.data,
928
+ () => computeVwapSeries(renderContext.data, "vwap")
929
+ );
930
+ if (inputs.showBands) {
931
+ const sigma = withCachedSeries(
932
+ "vwap|sigma",
933
+ renderContext.data,
934
+ () => computeVwapSeries(renderContext.data, "sigma")
935
+ );
936
+ const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
937
+ const upper = vwap.map((value, idx) => {
938
+ const s = sigma[idx];
939
+ return value == null || s == null ? null : value + multiplier * s;
940
+ });
941
+ const lower = vwap.map((value, idx) => {
942
+ const s = sigma[idx];
943
+ return value == null || s == null ? null : value - multiplier * s;
944
+ });
945
+ const bandColor = inputs.bandColor ?? "#94a3b8";
946
+ fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.bandFillOpacity) || 0.06);
947
+ drawOverlaySeries(ctx, renderContext, upper, bandColor, 1);
948
+ drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
949
+ }
950
+ drawOverlaySeries(ctx, renderContext, vwap, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
951
+ }
952
+ };
953
+ var BUILTIN_BOLLINGER_INDICATOR = {
954
+ id: "bollinger",
955
+ name: "BB",
956
+ pane: "overlay",
957
+ // Basis is pink so it stays distinguishable from the (orange) VWAP line
958
+ // when both overlays are active.
959
+ defaultInputs: {
960
+ length: 20,
961
+ multiplier: 2,
962
+ source: "close",
963
+ basisColor: "#ec4899",
964
+ bandColor: "#3b82f6",
965
+ width: 1.5,
966
+ fillOpacity: 0.05
967
+ },
968
+ draw: (ctx, renderContext, inputs) => {
969
+ const length = clampIndicatorLength(inputs.length, 20);
970
+ const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
971
+ const source = inputs.source ?? "close";
972
+ const basis = withCachedSeries(
973
+ `bb|basis|${length}|${source}`,
974
+ renderContext.data,
975
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "basis")
976
+ );
977
+ const upper = withCachedSeries(
978
+ `bb|upper|${length}|${multiplier}|${source}`,
979
+ renderContext.data,
980
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "upper")
981
+ );
982
+ const lower = withCachedSeries(
983
+ `bb|lower|${length}|${multiplier}|${source}`,
984
+ renderContext.data,
985
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "lower")
986
+ );
987
+ const bandColor = inputs.bandColor ?? "#3b82f6";
988
+ const width = Number(inputs.width) || 1.5;
989
+ fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.05);
990
+ drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
991
+ drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
992
+ drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#f59e0b", width);
993
+ }
994
+ };
825
995
  var BUILTIN_STDDEV_INDICATOR = {
826
996
  id: "stddev",
827
997
  name: "StdDev",
@@ -905,6 +1075,8 @@ var BUILTIN_INDICATORS = [
905
1075
  BUILTIN_VWMA_INDICATOR,
906
1076
  BUILTIN_RMA_INDICATOR,
907
1077
  BUILTIN_HMA_INDICATOR,
1078
+ BUILTIN_VWAP_INDICATOR,
1079
+ BUILTIN_BOLLINGER_INDICATOR,
908
1080
  BUILTIN_STDDEV_INDICATOR,
909
1081
  BUILTIN_ATR_INDICATOR
910
1082
  ];
@@ -2757,15 +2929,31 @@ function createChart(element, options = {}) {
2757
2929
  const pct = diff / base * 100;
2758
2930
  const ticks = tick > 0 ? Math.round(diff / tick) : 0;
2759
2931
  const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
2760
- const labelText = tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`;
2932
+ const labelLines = [
2933
+ tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`
2934
+ ];
2935
+ const barSpan = Math.abs(Math.round(p1.index) - Math.round(p0.index));
2936
+ if (barSpan > 0) {
2937
+ const t0 = getTimeForIndex(Math.round(p0.index));
2938
+ const t1 = getTimeForIndex(Math.round(p1.index));
2939
+ const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
2940
+ labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatDuration(spanMs)}` : `${barSpan} bars`);
2941
+ }
2942
+ const pointValue = Number(drawing.pointValue);
2943
+ if (Number.isFinite(pointValue) && pointValue > 0) {
2944
+ const dollars = diff * pointValue;
2945
+ const money = Math.abs(dollars).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
2946
+ labelLines.push(`${signed(dollars, `$${money}`)} / contract`);
2947
+ }
2761
2948
  const prevFont = ctx.font;
2762
2949
  ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2763
2950
  const padding = 6;
2764
- const textW = ctx.measureText(labelText).width;
2951
+ const lineHeight = 15;
2952
+ const textW = Math.max(...labelLines.map((line) => ctx.measureText(line).width));
2765
2953
  const pillW = textW + padding * 2;
2766
- const pillH = 18;
2954
+ const pillH = labelLines.length * lineHeight + padding;
2767
2955
  const pillX = midX - pillW / 2;
2768
- const pillY = botY + 6;
2956
+ const pillY = botY + 6 + pillH <= fullChartBottom - 2 ? botY + 6 : Math.max(chartTop + 2, topY - 6 - pillH);
2769
2957
  ctx.fillStyle = mergedOptions.backgroundColor;
2770
2958
  fillRoundedRect(pillX, pillY, pillW, pillH, 4);
2771
2959
  ctx.save();
@@ -2776,7 +2964,9 @@ function createChart(element, options = {}) {
2776
2964
  ctx.fillStyle = drawing.color;
2777
2965
  ctx.textAlign = "center";
2778
2966
  ctx.textBaseline = "middle";
2779
- ctx.fillText(labelText, midX, pillY + pillH / 2);
2967
+ labelLines.forEach((line, lineIndex) => {
2968
+ ctx.fillText(line, midX, pillY + padding / 2 + lineIndex * lineHeight + lineHeight / 2);
2969
+ });
2780
2970
  ctx.font = prevFont;
2781
2971
  if (drawing.label) {
2782
2972
  drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
@@ -3552,8 +3742,9 @@ function createChart(element, options = {}) {
3552
3742
  }
3553
3743
  return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
3554
3744
  };
3745
+ const LEGEND_EXCLUDED_INPUT_KEYS = /* @__PURE__ */ new Set(["width", "bandMultiplier", "bandFillOpacity", "fillOpacity"]);
3555
3746
  const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
3556
- const inputValues = Object.entries(indicator.inputs).filter(([, value]) => isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
3747
+ const inputValues = Object.entries(indicator.inputs).filter(([key, value]) => !LEGEND_EXCLUDED_INPUT_KEYS.has(key) && typeof value !== "boolean" && isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
3557
3748
  if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
3558
3749
  return `${plugin.name} ${inputValues.join(" ")}`;
3559
3750
  }
@@ -4531,7 +4722,9 @@ function createChart(element, options = {}) {
4531
4722
  points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
4532
4723
  color: defaults.color ?? "#2962ff",
4533
4724
  style: defaults.style ?? "solid",
4534
- width: defaults.width ?? 1
4725
+ width: defaults.width ?? 1,
4726
+ // pointValue enables the dollar-P&L line in the measure label.
4727
+ ...defaults.pointValue === void 0 ? {} : { pointValue: defaults.pointValue }
4535
4728
  })
4536
4729
  );
4537
4730
  emitDrawingsChange();
package/dist/index.js CHANGED
@@ -409,6 +409,60 @@ var computeStdDevSeries = (data, length, source) => {
409
409
  }
410
410
  return result;
411
411
  };
412
+ var VWAP_SESSION_ANCHOR_MS = 22 * 36e5;
413
+ var vwapSessionDayOf = (ms) => Math.floor((ms - VWAP_SESSION_ANCHOR_MS) / 864e5);
414
+ var computeVwapSeries = (data, band) => {
415
+ const result = new Array(data.length).fill(null);
416
+ let session = Number.NaN;
417
+ let cumPv = 0;
418
+ let cumPv2 = 0;
419
+ let cumV = 0;
420
+ for (let i = 0; i < data.length; i += 1) {
421
+ const point = data[i];
422
+ if (!point) continue;
423
+ const day = vwapSessionDayOf(point.time.getTime());
424
+ if (day !== session) {
425
+ session = day;
426
+ cumPv = 0;
427
+ cumPv2 = 0;
428
+ cumV = 0;
429
+ }
430
+ const typical = (point.h + point.l + point.c) / 3;
431
+ const volume = Math.max(0, point.v ?? 0);
432
+ cumPv += typical * volume;
433
+ cumPv2 += typical * typical * volume;
434
+ cumV += volume;
435
+ if (cumV <= 0) continue;
436
+ const vwap = cumPv / cumV;
437
+ if (band === "vwap") {
438
+ result[i] = vwap;
439
+ } else {
440
+ const variance = Math.max(0, cumPv2 / cumV - vwap * vwap);
441
+ result[i] = Math.sqrt(variance);
442
+ }
443
+ }
444
+ for (let i = 1; i < data.length; i += 1) {
445
+ const point = data[i];
446
+ const prev = data[i - 1];
447
+ const next = data[i + 1];
448
+ if (!point || !prev || !next) continue;
449
+ const day = vwapSessionDayOf(point.time.getTime());
450
+ if (day !== vwapSessionDayOf(prev.time.getTime()) && day === vwapSessionDayOf(next.time.getTime())) {
451
+ result[i] = null;
452
+ }
453
+ }
454
+ return result;
455
+ };
456
+ var computeBollingerSeries = (data, length, multiplier, source, band) => {
457
+ const basis = computeSmaSeries(data, length, source);
458
+ if (band === "basis") return basis;
459
+ const dev = computeStdDevSeries(data, length, source);
460
+ return basis.map((value, idx) => {
461
+ const sigma = dev[idx];
462
+ if (value == null || sigma == null) return null;
463
+ return band === "upper" ? value + multiplier * sigma : value - multiplier * sigma;
464
+ });
465
+ };
412
466
  var computeRsiSeries = (data, length) => {
413
467
  const result = new Array(data.length).fill(null);
414
468
  if (data.length < 2) return result;
@@ -535,6 +589,39 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
535
589
  }
536
590
  ctx.restore();
537
591
  };
592
+ var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
593
+ if (!renderContext.yFromPrice || opacity <= 0) return;
594
+ const yFromPrice = renderContext.yFromPrice;
595
+ ctx.save();
596
+ ctx.globalAlpha = Math.min(1, Math.max(0, opacity));
597
+ ctx.fillStyle = color;
598
+ let runStart = -1;
599
+ const flushRun = (endIndexExclusive) => {
600
+ if (runStart < 0) return;
601
+ ctx.beginPath();
602
+ for (let i = runStart; i < endIndexExclusive; i += 1) {
603
+ const x = renderContext.xFromIndex(i);
604
+ const y = yFromPrice(upper[i]);
605
+ if (i === runStart) ctx.moveTo(x, y);
606
+ else ctx.lineTo(x, y);
607
+ }
608
+ for (let i = endIndexExclusive - 1; i >= runStart; i -= 1) {
609
+ ctx.lineTo(renderContext.xFromIndex(i), yFromPrice(lower[i]));
610
+ }
611
+ ctx.closePath();
612
+ ctx.fill();
613
+ runStart = -1;
614
+ };
615
+ for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
616
+ const up = upper[index];
617
+ const lo = lower[index];
618
+ const valid = Number.isFinite(up ?? Number.NaN) && Number.isFinite(lo ?? Number.NaN);
619
+ if (valid && runStart < 0) runStart = index;
620
+ if (!valid) flushRun(index);
621
+ }
622
+ flushRun(renderContext.endIndex + 1);
623
+ ctx.restore();
624
+ };
538
625
  var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
539
626
  const visible = [];
540
627
  for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
@@ -796,6 +883,89 @@ var BUILTIN_HMA_INDICATOR = {
796
883
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
797
884
  }
798
885
  };
886
+ var BUILTIN_VWAP_INDICATOR = {
887
+ id: "vwap",
888
+ name: "VWAP",
889
+ pane: "overlay",
890
+ defaultInputs: {
891
+ color: "#f59e0b",
892
+ width: 2,
893
+ showBands: false,
894
+ bandMultiplier: 1,
895
+ bandColor: "#94a3b8",
896
+ bandFillOpacity: 0.06
897
+ },
898
+ draw: (ctx, renderContext, inputs) => {
899
+ const vwap = withCachedSeries(
900
+ "vwap|line",
901
+ renderContext.data,
902
+ () => computeVwapSeries(renderContext.data, "vwap")
903
+ );
904
+ if (inputs.showBands) {
905
+ const sigma = withCachedSeries(
906
+ "vwap|sigma",
907
+ renderContext.data,
908
+ () => computeVwapSeries(renderContext.data, "sigma")
909
+ );
910
+ const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
911
+ const upper = vwap.map((value, idx) => {
912
+ const s = sigma[idx];
913
+ return value == null || s == null ? null : value + multiplier * s;
914
+ });
915
+ const lower = vwap.map((value, idx) => {
916
+ const s = sigma[idx];
917
+ return value == null || s == null ? null : value - multiplier * s;
918
+ });
919
+ const bandColor = inputs.bandColor ?? "#94a3b8";
920
+ fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.bandFillOpacity) || 0.06);
921
+ drawOverlaySeries(ctx, renderContext, upper, bandColor, 1);
922
+ drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
923
+ }
924
+ drawOverlaySeries(ctx, renderContext, vwap, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
925
+ }
926
+ };
927
+ var BUILTIN_BOLLINGER_INDICATOR = {
928
+ id: "bollinger",
929
+ name: "BB",
930
+ pane: "overlay",
931
+ // Basis is pink so it stays distinguishable from the (orange) VWAP line
932
+ // when both overlays are active.
933
+ defaultInputs: {
934
+ length: 20,
935
+ multiplier: 2,
936
+ source: "close",
937
+ basisColor: "#ec4899",
938
+ bandColor: "#3b82f6",
939
+ width: 1.5,
940
+ fillOpacity: 0.05
941
+ },
942
+ draw: (ctx, renderContext, inputs) => {
943
+ const length = clampIndicatorLength(inputs.length, 20);
944
+ const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
945
+ const source = inputs.source ?? "close";
946
+ const basis = withCachedSeries(
947
+ `bb|basis|${length}|${source}`,
948
+ renderContext.data,
949
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "basis")
950
+ );
951
+ const upper = withCachedSeries(
952
+ `bb|upper|${length}|${multiplier}|${source}`,
953
+ renderContext.data,
954
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "upper")
955
+ );
956
+ const lower = withCachedSeries(
957
+ `bb|lower|${length}|${multiplier}|${source}`,
958
+ renderContext.data,
959
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "lower")
960
+ );
961
+ const bandColor = inputs.bandColor ?? "#3b82f6";
962
+ const width = Number(inputs.width) || 1.5;
963
+ fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.05);
964
+ drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
965
+ drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
966
+ drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#f59e0b", width);
967
+ }
968
+ };
799
969
  var BUILTIN_STDDEV_INDICATOR = {
800
970
  id: "stddev",
801
971
  name: "StdDev",
@@ -879,6 +1049,8 @@ var BUILTIN_INDICATORS = [
879
1049
  BUILTIN_VWMA_INDICATOR,
880
1050
  BUILTIN_RMA_INDICATOR,
881
1051
  BUILTIN_HMA_INDICATOR,
1052
+ BUILTIN_VWAP_INDICATOR,
1053
+ BUILTIN_BOLLINGER_INDICATOR,
882
1054
  BUILTIN_STDDEV_INDICATOR,
883
1055
  BUILTIN_ATR_INDICATOR
884
1056
  ];
@@ -2731,15 +2903,31 @@ function createChart(element, options = {}) {
2731
2903
  const pct = diff / base * 100;
2732
2904
  const ticks = tick > 0 ? Math.round(diff / tick) : 0;
2733
2905
  const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
2734
- const labelText = tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`;
2906
+ const labelLines = [
2907
+ tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`
2908
+ ];
2909
+ const barSpan = Math.abs(Math.round(p1.index) - Math.round(p0.index));
2910
+ if (barSpan > 0) {
2911
+ const t0 = getTimeForIndex(Math.round(p0.index));
2912
+ const t1 = getTimeForIndex(Math.round(p1.index));
2913
+ const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
2914
+ labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatDuration(spanMs)}` : `${barSpan} bars`);
2915
+ }
2916
+ const pointValue = Number(drawing.pointValue);
2917
+ if (Number.isFinite(pointValue) && pointValue > 0) {
2918
+ const dollars = diff * pointValue;
2919
+ const money = Math.abs(dollars).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
2920
+ labelLines.push(`${signed(dollars, `$${money}`)} / contract`);
2921
+ }
2735
2922
  const prevFont = ctx.font;
2736
2923
  ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2737
2924
  const padding = 6;
2738
- const textW = ctx.measureText(labelText).width;
2925
+ const lineHeight = 15;
2926
+ const textW = Math.max(...labelLines.map((line) => ctx.measureText(line).width));
2739
2927
  const pillW = textW + padding * 2;
2740
- const pillH = 18;
2928
+ const pillH = labelLines.length * lineHeight + padding;
2741
2929
  const pillX = midX - pillW / 2;
2742
- const pillY = botY + 6;
2930
+ const pillY = botY + 6 + pillH <= fullChartBottom - 2 ? botY + 6 : Math.max(chartTop + 2, topY - 6 - pillH);
2743
2931
  ctx.fillStyle = mergedOptions.backgroundColor;
2744
2932
  fillRoundedRect(pillX, pillY, pillW, pillH, 4);
2745
2933
  ctx.save();
@@ -2750,7 +2938,9 @@ function createChart(element, options = {}) {
2750
2938
  ctx.fillStyle = drawing.color;
2751
2939
  ctx.textAlign = "center";
2752
2940
  ctx.textBaseline = "middle";
2753
- ctx.fillText(labelText, midX, pillY + pillH / 2);
2941
+ labelLines.forEach((line, lineIndex) => {
2942
+ ctx.fillText(line, midX, pillY + padding / 2 + lineIndex * lineHeight + lineHeight / 2);
2943
+ });
2754
2944
  ctx.font = prevFont;
2755
2945
  if (drawing.label) {
2756
2946
  drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
@@ -3526,8 +3716,9 @@ function createChart(element, options = {}) {
3526
3716
  }
3527
3717
  return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
3528
3718
  };
3719
+ const LEGEND_EXCLUDED_INPUT_KEYS = /* @__PURE__ */ new Set(["width", "bandMultiplier", "bandFillOpacity", "fillOpacity"]);
3529
3720
  const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
3530
- const inputValues = Object.entries(indicator.inputs).filter(([, value]) => isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
3721
+ const inputValues = Object.entries(indicator.inputs).filter(([key, value]) => !LEGEND_EXCLUDED_INPUT_KEYS.has(key) && typeof value !== "boolean" && isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
3531
3722
  if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
3532
3723
  return `${plugin.name} ${inputValues.join(" ")}`;
3533
3724
  }
@@ -4505,7 +4696,9 @@ function createChart(element, options = {}) {
4505
4696
  points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
4506
4697
  color: defaults.color ?? "#2962ff",
4507
4698
  style: defaults.style ?? "solid",
4508
- width: defaults.width ?? 1
4699
+ width: defaults.width ?? 1,
4700
+ // pointValue enables the dollar-P&L line in the measure label.
4701
+ ...defaults.pointValue === void 0 ? {} : { pointValue: defaults.pointValue }
4509
4702
  })
4510
4703
  );
4511
4704
  emitDrawingsChange();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.119",
3
+ "version": "0.1.120",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",