hyperprop-charting-library 0.1.146 → 0.1.147

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.
@@ -2072,8 +2072,8 @@ var BUILTIN_DONCHIAN_INDICATOR = {
2072
2072
  }
2073
2073
  };
2074
2074
  var computeVolumeProfile = (data, fromIndex, toIndex, rowCount, valueAreaPercent) => {
2075
- const from = Math.max(0, fromIndex);
2076
- const to = Math.min(data.length - 1, toIndex);
2075
+ const from = Math.max(0, Math.round(fromIndex));
2076
+ const to = Math.min(data.length - 1, Math.round(toIndex));
2077
2077
  if (to < from) return null;
2078
2078
  let low = Number.POSITIVE_INFINITY;
2079
2079
  let high = Number.NEGATIVE_INFINITY;
@@ -3932,6 +3932,9 @@ function createChart(element, options = {}) {
3932
3932
  pointValue: Number(drawing.pointValue) || 1,
3933
3933
  qtyPrecision: Number.isFinite(drawing.qtyPrecision) ? Math.max(0, Math.floor(Number(drawing.qtyPrecision))) : 0,
3934
3934
  fontSize: Math.max(6, Number(drawing.fontSize) || 14),
3935
+ profileRows: Math.max(4, Math.min(200, Math.round(Number(drawing.profileRows)) || 24)),
3936
+ profileValueAreaPercent: Math.max(10, Math.min(99, Number(drawing.profileValueAreaPercent) || 70)),
3937
+ profileWidthRatio: Math.max(0.1, Math.min(1, Number(drawing.profileWidthRatio) || 0.5)),
3935
3938
  ...drawing.label === void 0 ? {} : { label: drawing.label }
3936
3939
  };
3937
3940
  };
@@ -3965,6 +3968,9 @@ function createChart(element, options = {}) {
3965
3968
  pointValue: drawing.pointValue,
3966
3969
  qtyPrecision: drawing.qtyPrecision,
3967
3970
  fontSize: drawing.fontSize,
3971
+ profileRows: drawing.profileRows,
3972
+ profileValueAreaPercent: drawing.profileValueAreaPercent,
3973
+ profileWidthRatio: drawing.profileWidthRatio,
3968
3974
  ...drawing.label === void 0 ? {} : { label: drawing.label }
3969
3975
  });
3970
3976
  let tradeMarkers = [];
@@ -6671,6 +6677,81 @@ function createChart(element, options = {}) {
6671
6677
  handleAt(ax, ay, drawing.color);
6672
6678
  handleAt(bx, by, drawing.color);
6673
6679
  }
6680
+ } else if (drawing.type === "fixed-range-volume-profile") {
6681
+ const p0 = drawing.points[0];
6682
+ const p1 = drawing.points[1];
6683
+ if (p0 && p1 && data.length > 0) {
6684
+ const fromIndex = Math.max(0, Math.round(Math.min(p0.index, p1.index)));
6685
+ const toIndex = Math.min(data.length - 1, Math.round(Math.max(p0.index, p1.index)));
6686
+ const leftX = xFromDrawingPoint(p0.index <= p1.index ? p0 : p1) - candleSpacing / 2;
6687
+ const rightX = xFromDrawingPoint(p0.index <= p1.index ? p1 : p0) + candleSpacing / 2;
6688
+ const rangeWidth = Math.max(1, rightX - leftX);
6689
+ const rows = Math.max(4, Math.min(200, Math.round(drawing.profileRows) || 24));
6690
+ const valueAreaPercent = Math.max(10, Math.min(99, Number(drawing.profileValueAreaPercent) || 70));
6691
+ const profile = toIndex >= fromIndex ? computeVolumeProfile(data, fromIndex, toIndex, rows, valueAreaPercent) : null;
6692
+ ctx.save();
6693
+ ctx.globalAlpha = draft ? 0.5 : 1;
6694
+ ctx.fillStyle = hexToRgba(drawing.color, 0.06);
6695
+ ctx.fillRect(leftX, chartTop, rangeWidth, chartBottom - chartTop);
6696
+ ctx.strokeStyle = hexToRgba(drawing.color, 0.5);
6697
+ ctx.lineWidth = 1;
6698
+ ctx.setLineDash([4, 3]);
6699
+ ctx.beginPath();
6700
+ ctx.moveTo(crisp(leftX), chartTop);
6701
+ ctx.lineTo(crisp(leftX), chartBottom);
6702
+ ctx.moveTo(crisp(rightX), chartTop);
6703
+ ctx.lineTo(crisp(rightX), chartBottom);
6704
+ ctx.stroke();
6705
+ ctx.setLineDash([]);
6706
+ if (profile && profile.maxRowVolume > 0) {
6707
+ const widthRatio = Math.max(0.1, Math.min(1, Number(drawing.profileWidthRatio) || 0.5));
6708
+ const maxWidth = rangeWidth * widthRatio;
6709
+ for (let row = 0; row < profile.rows.length; row += 1) {
6710
+ const bucket = profile.rows[row];
6711
+ if (bucket.total <= 0) continue;
6712
+ const yTop = yFromPrice(bucket.high);
6713
+ const yBottom = yFromPrice(bucket.low);
6714
+ const top = Math.min(yTop, yBottom);
6715
+ const height2 = Math.max(1, Math.abs(yBottom - yTop) - 1);
6716
+ if (top > chartBottom || top + height2 < chartTop) continue;
6717
+ const inValueArea = row >= profile.valueAreaFrom && row <= profile.valueAreaTo;
6718
+ ctx.globalAlpha = (draft ? 0.5 : 1) * (inValueArea ? 0.5 : 0.2);
6719
+ const rowWidth = bucket.total / profile.maxRowVolume * maxWidth;
6720
+ const upWidth = rowWidth * (bucket.up / bucket.total);
6721
+ ctx.fillStyle = mergedOptions.upColor;
6722
+ ctx.fillRect(leftX, top, upWidth, height2);
6723
+ ctx.fillStyle = mergedOptions.downColor;
6724
+ ctx.fillRect(leftX + upWidth, top, rowWidth - upWidth, height2);
6725
+ }
6726
+ ctx.globalAlpha = draft ? 0.6 : 1;
6727
+ const poc = profile.rows[profile.pocIndex];
6728
+ const drawProfileLevel = (price, color, label, dashed) => {
6729
+ const y = yFromPrice(price);
6730
+ if (y < chartTop || y > chartBottom) return;
6731
+ ctx.strokeStyle = color;
6732
+ ctx.lineWidth = 1;
6733
+ ctx.setLineDash(dashed ? [4, 3] : []);
6734
+ ctx.beginPath();
6735
+ ctx.moveTo(leftX, crisp(y));
6736
+ ctx.lineTo(leftX + maxWidth, crisp(y));
6737
+ ctx.stroke();
6738
+ ctx.setLineDash([]);
6739
+ const prevFont = ctx.font;
6740
+ ctx.font = `10px ${mergedOptions.fontFamily}`;
6741
+ ctx.fillStyle = color;
6742
+ ctx.textAlign = "left";
6743
+ ctx.textBaseline = "bottom";
6744
+ ctx.fillText(label, leftX + 3, y - 1);
6745
+ ctx.font = prevFont;
6746
+ };
6747
+ drawProfileLevel(profile.rows[profile.valueAreaTo].high, "#7e8aa3", "VAH", true);
6748
+ drawProfileLevel(profile.rows[profile.valueAreaFrom].low, "#7e8aa3", "VAL", true);
6749
+ drawProfileLevel((poc.low + poc.high) / 2, "#f7a600", "POC", false);
6750
+ }
6751
+ ctx.restore();
6752
+ handleAt(leftX, yFromPrice(p0.index <= p1.index ? p0.price : p1.price), drawing.color);
6753
+ handleAt(rightX, yFromPrice(p0.index <= p1.index ? p1.price : p0.price), drawing.color);
6754
+ }
6674
6755
  } else if (drawing.type === "price-range") {
6675
6756
  const p0 = drawing.points[0];
6676
6757
  const p1 = drawing.points[1];
@@ -9443,6 +9524,19 @@ function createChart(element, options = {}) {
9443
9524
  if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5 * hitTolerance) {
9444
9525
  return { drawing, target: "line" };
9445
9526
  }
9527
+ } else if (drawing.type === "fixed-range-volume-profile") {
9528
+ const p0 = drawing.points[0];
9529
+ const p1 = drawing.points[1];
9530
+ if (!p0 || !p1) continue;
9531
+ const px0 = canvasXFromDrawingPoint(p0);
9532
+ const px1 = canvasXFromDrawingPoint(p1);
9533
+ const y0 = canvasYFromDrawingPrice(p0.price);
9534
+ const y1 = canvasYFromDrawingPrice(p1.price);
9535
+ if (Math.hypot(x - px0, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
9536
+ if (Math.hypot(x - px1, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
9537
+ if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1)) {
9538
+ return { drawing, target: "line" };
9539
+ }
9446
9540
  } else if (drawing.type === "price-range") {
9447
9541
  const p0 = drawing.points[0];
9448
9542
  const p1 = drawing.points[1];
@@ -9894,6 +9988,33 @@ function createChart(element, options = {}) {
9894
9988
  scheduleDraw();
9895
9989
  return true;
9896
9990
  }
9991
+ if (activeDrawingTool === "fixed-range-volume-profile") {
9992
+ if (draftDrawing?.type === "fixed-range-volume-profile") {
9993
+ const completed = normalizeDrawingState({
9994
+ ...serializeDrawing(draftDrawing),
9995
+ points: [draftDrawing.points[0], point]
9996
+ });
9997
+ drawings.push(completed);
9998
+ draftDrawing = null;
9999
+ activeDrawingTool = null;
10000
+ emitDrawingsChange();
10001
+ scheduleDraw();
10002
+ return true;
10003
+ }
10004
+ const defaults = getDrawingToolDefaults("fixed-range-volume-profile");
10005
+ draftDrawing = normalizeDrawingState({
10006
+ type: "fixed-range-volume-profile",
10007
+ points: [point, point],
10008
+ color: defaults.color ?? "#5b8def",
10009
+ style: defaults.style ?? "solid",
10010
+ width: defaults.width ?? 1,
10011
+ ...defaults.profileRows === void 0 ? {} : { profileRows: defaults.profileRows },
10012
+ ...defaults.profileValueAreaPercent === void 0 ? {} : { profileValueAreaPercent: defaults.profileValueAreaPercent },
10013
+ ...defaults.profileWidthRatio === void 0 ? {} : { profileWidthRatio: defaults.profileWidthRatio }
10014
+ });
10015
+ scheduleDraw();
10016
+ return true;
10017
+ }
9897
10018
  if (activeDrawingTool === "price-range") {
9898
10019
  const tick = getConfiguredTickSize();
9899
10020
  const visibleRange = drawState.yMax - drawState.yMin;
@@ -120,6 +120,12 @@ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray"
120
120
  | "brush"
121
121
  /** Text box with a connector line pointing at an anchored bar/price. */
122
122
  | "callout"
123
+ /**
124
+ * Volume profile over a bar range picked by two clicks (TradingView's
125
+ * fixed-range volume profile). Only the two anchors' bar indexes matter —
126
+ * the price extent comes from the bars they enclose.
127
+ */
128
+ | "fixed-range-volume-profile"
123
129
  /**
124
130
  * TradingView-style ruler. Unlike every other tool it is transient: the
125
131
  * measurement overlay is never added to the drawings list (no
@@ -157,6 +163,12 @@ interface DrawingObjectOptions {
157
163
  pointValue?: number;
158
164
  qtyPrecision?: number;
159
165
  fontSize?: number;
166
+ /** Price buckets across the range. Default 24. */
167
+ profileRows?: number;
168
+ /** Share of volume enclosed by the value area. Default 70. */
169
+ profileValueAreaPercent?: number;
170
+ /** Profile width as a share of the selected range. Default 0.5. */
171
+ profileWidthRatio?: number;
160
172
  }
161
173
  /** Default colors for position tools: [profit, loss, label text]. */
162
174
  declare const POSITION_DEFAULT_COLORS: string[];
@@ -220,7 +232,7 @@ interface DrawingHoverEvent {
220
232
  x: number;
221
233
  y: number;
222
234
  }
223
- type DrawingDefaults = Partial<Pick<DrawingObjectOptions, "color" | "colors" | "style" | "width" | "accountSize" | "lotSize" | "risk" | "riskMode" | "leverage" | "pointValue" | "qtyPrecision" | "fontSize">>;
235
+ type DrawingDefaults = Partial<Pick<DrawingObjectOptions, "color" | "colors" | "style" | "width" | "accountSize" | "lotSize" | "risk" | "riskMode" | "leverage" | "pointValue" | "qtyPrecision" | "fontSize" | "profileRows" | "profileValueAreaPercent" | "profileWidthRatio">>;
224
236
  interface IndicatorInstanceOptions<TInputs extends Record<string, unknown> = Record<string, unknown>> {
225
237
  id?: string;
226
238
  type: string;
@@ -2036,8 +2036,8 @@ var BUILTIN_DONCHIAN_INDICATOR = {
2036
2036
  }
2037
2037
  };
2038
2038
  var computeVolumeProfile = (data, fromIndex, toIndex, rowCount, valueAreaPercent) => {
2039
- const from = Math.max(0, fromIndex);
2040
- const to = Math.min(data.length - 1, toIndex);
2039
+ const from = Math.max(0, Math.round(fromIndex));
2040
+ const to = Math.min(data.length - 1, Math.round(toIndex));
2041
2041
  if (to < from) return null;
2042
2042
  let low = Number.POSITIVE_INFINITY;
2043
2043
  let high = Number.NEGATIVE_INFINITY;
@@ -3896,6 +3896,9 @@ function createChart(element, options = {}) {
3896
3896
  pointValue: Number(drawing.pointValue) || 1,
3897
3897
  qtyPrecision: Number.isFinite(drawing.qtyPrecision) ? Math.max(0, Math.floor(Number(drawing.qtyPrecision))) : 0,
3898
3898
  fontSize: Math.max(6, Number(drawing.fontSize) || 14),
3899
+ profileRows: Math.max(4, Math.min(200, Math.round(Number(drawing.profileRows)) || 24)),
3900
+ profileValueAreaPercent: Math.max(10, Math.min(99, Number(drawing.profileValueAreaPercent) || 70)),
3901
+ profileWidthRatio: Math.max(0.1, Math.min(1, Number(drawing.profileWidthRatio) || 0.5)),
3899
3902
  ...drawing.label === void 0 ? {} : { label: drawing.label }
3900
3903
  };
3901
3904
  };
@@ -3929,6 +3932,9 @@ function createChart(element, options = {}) {
3929
3932
  pointValue: drawing.pointValue,
3930
3933
  qtyPrecision: drawing.qtyPrecision,
3931
3934
  fontSize: drawing.fontSize,
3935
+ profileRows: drawing.profileRows,
3936
+ profileValueAreaPercent: drawing.profileValueAreaPercent,
3937
+ profileWidthRatio: drawing.profileWidthRatio,
3932
3938
  ...drawing.label === void 0 ? {} : { label: drawing.label }
3933
3939
  });
3934
3940
  let tradeMarkers = [];
@@ -6635,6 +6641,81 @@ function createChart(element, options = {}) {
6635
6641
  handleAt(ax, ay, drawing.color);
6636
6642
  handleAt(bx, by, drawing.color);
6637
6643
  }
6644
+ } else if (drawing.type === "fixed-range-volume-profile") {
6645
+ const p0 = drawing.points[0];
6646
+ const p1 = drawing.points[1];
6647
+ if (p0 && p1 && data.length > 0) {
6648
+ const fromIndex = Math.max(0, Math.round(Math.min(p0.index, p1.index)));
6649
+ const toIndex = Math.min(data.length - 1, Math.round(Math.max(p0.index, p1.index)));
6650
+ const leftX = xFromDrawingPoint(p0.index <= p1.index ? p0 : p1) - candleSpacing / 2;
6651
+ const rightX = xFromDrawingPoint(p0.index <= p1.index ? p1 : p0) + candleSpacing / 2;
6652
+ const rangeWidth = Math.max(1, rightX - leftX);
6653
+ const rows = Math.max(4, Math.min(200, Math.round(drawing.profileRows) || 24));
6654
+ const valueAreaPercent = Math.max(10, Math.min(99, Number(drawing.profileValueAreaPercent) || 70));
6655
+ const profile = toIndex >= fromIndex ? computeVolumeProfile(data, fromIndex, toIndex, rows, valueAreaPercent) : null;
6656
+ ctx.save();
6657
+ ctx.globalAlpha = draft ? 0.5 : 1;
6658
+ ctx.fillStyle = hexToRgba(drawing.color, 0.06);
6659
+ ctx.fillRect(leftX, chartTop, rangeWidth, chartBottom - chartTop);
6660
+ ctx.strokeStyle = hexToRgba(drawing.color, 0.5);
6661
+ ctx.lineWidth = 1;
6662
+ ctx.setLineDash([4, 3]);
6663
+ ctx.beginPath();
6664
+ ctx.moveTo(crisp(leftX), chartTop);
6665
+ ctx.lineTo(crisp(leftX), chartBottom);
6666
+ ctx.moveTo(crisp(rightX), chartTop);
6667
+ ctx.lineTo(crisp(rightX), chartBottom);
6668
+ ctx.stroke();
6669
+ ctx.setLineDash([]);
6670
+ if (profile && profile.maxRowVolume > 0) {
6671
+ const widthRatio = Math.max(0.1, Math.min(1, Number(drawing.profileWidthRatio) || 0.5));
6672
+ const maxWidth = rangeWidth * widthRatio;
6673
+ for (let row = 0; row < profile.rows.length; row += 1) {
6674
+ const bucket = profile.rows[row];
6675
+ if (bucket.total <= 0) continue;
6676
+ const yTop = yFromPrice(bucket.high);
6677
+ const yBottom = yFromPrice(bucket.low);
6678
+ const top = Math.min(yTop, yBottom);
6679
+ const height2 = Math.max(1, Math.abs(yBottom - yTop) - 1);
6680
+ if (top > chartBottom || top + height2 < chartTop) continue;
6681
+ const inValueArea = row >= profile.valueAreaFrom && row <= profile.valueAreaTo;
6682
+ ctx.globalAlpha = (draft ? 0.5 : 1) * (inValueArea ? 0.5 : 0.2);
6683
+ const rowWidth = bucket.total / profile.maxRowVolume * maxWidth;
6684
+ const upWidth = rowWidth * (bucket.up / bucket.total);
6685
+ ctx.fillStyle = mergedOptions.upColor;
6686
+ ctx.fillRect(leftX, top, upWidth, height2);
6687
+ ctx.fillStyle = mergedOptions.downColor;
6688
+ ctx.fillRect(leftX + upWidth, top, rowWidth - upWidth, height2);
6689
+ }
6690
+ ctx.globalAlpha = draft ? 0.6 : 1;
6691
+ const poc = profile.rows[profile.pocIndex];
6692
+ const drawProfileLevel = (price, color, label, dashed) => {
6693
+ const y = yFromPrice(price);
6694
+ if (y < chartTop || y > chartBottom) return;
6695
+ ctx.strokeStyle = color;
6696
+ ctx.lineWidth = 1;
6697
+ ctx.setLineDash(dashed ? [4, 3] : []);
6698
+ ctx.beginPath();
6699
+ ctx.moveTo(leftX, crisp(y));
6700
+ ctx.lineTo(leftX + maxWidth, crisp(y));
6701
+ ctx.stroke();
6702
+ ctx.setLineDash([]);
6703
+ const prevFont = ctx.font;
6704
+ ctx.font = `10px ${mergedOptions.fontFamily}`;
6705
+ ctx.fillStyle = color;
6706
+ ctx.textAlign = "left";
6707
+ ctx.textBaseline = "bottom";
6708
+ ctx.fillText(label, leftX + 3, y - 1);
6709
+ ctx.font = prevFont;
6710
+ };
6711
+ drawProfileLevel(profile.rows[profile.valueAreaTo].high, "#7e8aa3", "VAH", true);
6712
+ drawProfileLevel(profile.rows[profile.valueAreaFrom].low, "#7e8aa3", "VAL", true);
6713
+ drawProfileLevel((poc.low + poc.high) / 2, "#f7a600", "POC", false);
6714
+ }
6715
+ ctx.restore();
6716
+ handleAt(leftX, yFromPrice(p0.index <= p1.index ? p0.price : p1.price), drawing.color);
6717
+ handleAt(rightX, yFromPrice(p0.index <= p1.index ? p1.price : p0.price), drawing.color);
6718
+ }
6638
6719
  } else if (drawing.type === "price-range") {
6639
6720
  const p0 = drawing.points[0];
6640
6721
  const p1 = drawing.points[1];
@@ -9407,6 +9488,19 @@ function createChart(element, options = {}) {
9407
9488
  if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5 * hitTolerance) {
9408
9489
  return { drawing, target: "line" };
9409
9490
  }
9491
+ } else if (drawing.type === "fixed-range-volume-profile") {
9492
+ const p0 = drawing.points[0];
9493
+ const p1 = drawing.points[1];
9494
+ if (!p0 || !p1) continue;
9495
+ const px0 = canvasXFromDrawingPoint(p0);
9496
+ const px1 = canvasXFromDrawingPoint(p1);
9497
+ const y0 = canvasYFromDrawingPrice(p0.price);
9498
+ const y1 = canvasYFromDrawingPrice(p1.price);
9499
+ if (Math.hypot(x - px0, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
9500
+ if (Math.hypot(x - px1, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
9501
+ if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1)) {
9502
+ return { drawing, target: "line" };
9503
+ }
9410
9504
  } else if (drawing.type === "price-range") {
9411
9505
  const p0 = drawing.points[0];
9412
9506
  const p1 = drawing.points[1];
@@ -9858,6 +9952,33 @@ function createChart(element, options = {}) {
9858
9952
  scheduleDraw();
9859
9953
  return true;
9860
9954
  }
9955
+ if (activeDrawingTool === "fixed-range-volume-profile") {
9956
+ if (draftDrawing?.type === "fixed-range-volume-profile") {
9957
+ const completed = normalizeDrawingState({
9958
+ ...serializeDrawing(draftDrawing),
9959
+ points: [draftDrawing.points[0], point]
9960
+ });
9961
+ drawings.push(completed);
9962
+ draftDrawing = null;
9963
+ activeDrawingTool = null;
9964
+ emitDrawingsChange();
9965
+ scheduleDraw();
9966
+ return true;
9967
+ }
9968
+ const defaults = getDrawingToolDefaults("fixed-range-volume-profile");
9969
+ draftDrawing = normalizeDrawingState({
9970
+ type: "fixed-range-volume-profile",
9971
+ points: [point, point],
9972
+ color: defaults.color ?? "#5b8def",
9973
+ style: defaults.style ?? "solid",
9974
+ width: defaults.width ?? 1,
9975
+ ...defaults.profileRows === void 0 ? {} : { profileRows: defaults.profileRows },
9976
+ ...defaults.profileValueAreaPercent === void 0 ? {} : { profileValueAreaPercent: defaults.profileValueAreaPercent },
9977
+ ...defaults.profileWidthRatio === void 0 ? {} : { profileWidthRatio: defaults.profileWidthRatio }
9978
+ });
9979
+ scheduleDraw();
9980
+ return true;
9981
+ }
9861
9982
  if (activeDrawingTool === "price-range") {
9862
9983
  const tick = getConfiguredTickSize();
9863
9984
  const visibleRange = drawState.yMax - drawState.yMin;
package/dist/index.cjs CHANGED
@@ -2072,8 +2072,8 @@ var BUILTIN_DONCHIAN_INDICATOR = {
2072
2072
  }
2073
2073
  };
2074
2074
  var computeVolumeProfile = (data, fromIndex, toIndex, rowCount, valueAreaPercent) => {
2075
- const from = Math.max(0, fromIndex);
2076
- const to = Math.min(data.length - 1, toIndex);
2075
+ const from = Math.max(0, Math.round(fromIndex));
2076
+ const to = Math.min(data.length - 1, Math.round(toIndex));
2077
2077
  if (to < from) return null;
2078
2078
  let low = Number.POSITIVE_INFINITY;
2079
2079
  let high = Number.NEGATIVE_INFINITY;
@@ -3932,6 +3932,9 @@ function createChart(element, options = {}) {
3932
3932
  pointValue: Number(drawing.pointValue) || 1,
3933
3933
  qtyPrecision: Number.isFinite(drawing.qtyPrecision) ? Math.max(0, Math.floor(Number(drawing.qtyPrecision))) : 0,
3934
3934
  fontSize: Math.max(6, Number(drawing.fontSize) || 14),
3935
+ profileRows: Math.max(4, Math.min(200, Math.round(Number(drawing.profileRows)) || 24)),
3936
+ profileValueAreaPercent: Math.max(10, Math.min(99, Number(drawing.profileValueAreaPercent) || 70)),
3937
+ profileWidthRatio: Math.max(0.1, Math.min(1, Number(drawing.profileWidthRatio) || 0.5)),
3935
3938
  ...drawing.label === void 0 ? {} : { label: drawing.label }
3936
3939
  };
3937
3940
  };
@@ -3965,6 +3968,9 @@ function createChart(element, options = {}) {
3965
3968
  pointValue: drawing.pointValue,
3966
3969
  qtyPrecision: drawing.qtyPrecision,
3967
3970
  fontSize: drawing.fontSize,
3971
+ profileRows: drawing.profileRows,
3972
+ profileValueAreaPercent: drawing.profileValueAreaPercent,
3973
+ profileWidthRatio: drawing.profileWidthRatio,
3968
3974
  ...drawing.label === void 0 ? {} : { label: drawing.label }
3969
3975
  });
3970
3976
  let tradeMarkers = [];
@@ -6671,6 +6677,81 @@ function createChart(element, options = {}) {
6671
6677
  handleAt(ax, ay, drawing.color);
6672
6678
  handleAt(bx, by, drawing.color);
6673
6679
  }
6680
+ } else if (drawing.type === "fixed-range-volume-profile") {
6681
+ const p0 = drawing.points[0];
6682
+ const p1 = drawing.points[1];
6683
+ if (p0 && p1 && data.length > 0) {
6684
+ const fromIndex = Math.max(0, Math.round(Math.min(p0.index, p1.index)));
6685
+ const toIndex = Math.min(data.length - 1, Math.round(Math.max(p0.index, p1.index)));
6686
+ const leftX = xFromDrawingPoint(p0.index <= p1.index ? p0 : p1) - candleSpacing / 2;
6687
+ const rightX = xFromDrawingPoint(p0.index <= p1.index ? p1 : p0) + candleSpacing / 2;
6688
+ const rangeWidth = Math.max(1, rightX - leftX);
6689
+ const rows = Math.max(4, Math.min(200, Math.round(drawing.profileRows) || 24));
6690
+ const valueAreaPercent = Math.max(10, Math.min(99, Number(drawing.profileValueAreaPercent) || 70));
6691
+ const profile = toIndex >= fromIndex ? computeVolumeProfile(data, fromIndex, toIndex, rows, valueAreaPercent) : null;
6692
+ ctx.save();
6693
+ ctx.globalAlpha = draft ? 0.5 : 1;
6694
+ ctx.fillStyle = hexToRgba(drawing.color, 0.06);
6695
+ ctx.fillRect(leftX, chartTop, rangeWidth, chartBottom - chartTop);
6696
+ ctx.strokeStyle = hexToRgba(drawing.color, 0.5);
6697
+ ctx.lineWidth = 1;
6698
+ ctx.setLineDash([4, 3]);
6699
+ ctx.beginPath();
6700
+ ctx.moveTo(crisp(leftX), chartTop);
6701
+ ctx.lineTo(crisp(leftX), chartBottom);
6702
+ ctx.moveTo(crisp(rightX), chartTop);
6703
+ ctx.lineTo(crisp(rightX), chartBottom);
6704
+ ctx.stroke();
6705
+ ctx.setLineDash([]);
6706
+ if (profile && profile.maxRowVolume > 0) {
6707
+ const widthRatio = Math.max(0.1, Math.min(1, Number(drawing.profileWidthRatio) || 0.5));
6708
+ const maxWidth = rangeWidth * widthRatio;
6709
+ for (let row = 0; row < profile.rows.length; row += 1) {
6710
+ const bucket = profile.rows[row];
6711
+ if (bucket.total <= 0) continue;
6712
+ const yTop = yFromPrice(bucket.high);
6713
+ const yBottom = yFromPrice(bucket.low);
6714
+ const top = Math.min(yTop, yBottom);
6715
+ const height2 = Math.max(1, Math.abs(yBottom - yTop) - 1);
6716
+ if (top > chartBottom || top + height2 < chartTop) continue;
6717
+ const inValueArea = row >= profile.valueAreaFrom && row <= profile.valueAreaTo;
6718
+ ctx.globalAlpha = (draft ? 0.5 : 1) * (inValueArea ? 0.5 : 0.2);
6719
+ const rowWidth = bucket.total / profile.maxRowVolume * maxWidth;
6720
+ const upWidth = rowWidth * (bucket.up / bucket.total);
6721
+ ctx.fillStyle = mergedOptions.upColor;
6722
+ ctx.fillRect(leftX, top, upWidth, height2);
6723
+ ctx.fillStyle = mergedOptions.downColor;
6724
+ ctx.fillRect(leftX + upWidth, top, rowWidth - upWidth, height2);
6725
+ }
6726
+ ctx.globalAlpha = draft ? 0.6 : 1;
6727
+ const poc = profile.rows[profile.pocIndex];
6728
+ const drawProfileLevel = (price, color, label, dashed) => {
6729
+ const y = yFromPrice(price);
6730
+ if (y < chartTop || y > chartBottom) return;
6731
+ ctx.strokeStyle = color;
6732
+ ctx.lineWidth = 1;
6733
+ ctx.setLineDash(dashed ? [4, 3] : []);
6734
+ ctx.beginPath();
6735
+ ctx.moveTo(leftX, crisp(y));
6736
+ ctx.lineTo(leftX + maxWidth, crisp(y));
6737
+ ctx.stroke();
6738
+ ctx.setLineDash([]);
6739
+ const prevFont = ctx.font;
6740
+ ctx.font = `10px ${mergedOptions.fontFamily}`;
6741
+ ctx.fillStyle = color;
6742
+ ctx.textAlign = "left";
6743
+ ctx.textBaseline = "bottom";
6744
+ ctx.fillText(label, leftX + 3, y - 1);
6745
+ ctx.font = prevFont;
6746
+ };
6747
+ drawProfileLevel(profile.rows[profile.valueAreaTo].high, "#7e8aa3", "VAH", true);
6748
+ drawProfileLevel(profile.rows[profile.valueAreaFrom].low, "#7e8aa3", "VAL", true);
6749
+ drawProfileLevel((poc.low + poc.high) / 2, "#f7a600", "POC", false);
6750
+ }
6751
+ ctx.restore();
6752
+ handleAt(leftX, yFromPrice(p0.index <= p1.index ? p0.price : p1.price), drawing.color);
6753
+ handleAt(rightX, yFromPrice(p0.index <= p1.index ? p1.price : p0.price), drawing.color);
6754
+ }
6674
6755
  } else if (drawing.type === "price-range") {
6675
6756
  const p0 = drawing.points[0];
6676
6757
  const p1 = drawing.points[1];
@@ -9443,6 +9524,19 @@ function createChart(element, options = {}) {
9443
9524
  if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5 * hitTolerance) {
9444
9525
  return { drawing, target: "line" };
9445
9526
  }
9527
+ } else if (drawing.type === "fixed-range-volume-profile") {
9528
+ const p0 = drawing.points[0];
9529
+ const p1 = drawing.points[1];
9530
+ if (!p0 || !p1) continue;
9531
+ const px0 = canvasXFromDrawingPoint(p0);
9532
+ const px1 = canvasXFromDrawingPoint(p1);
9533
+ const y0 = canvasYFromDrawingPrice(p0.price);
9534
+ const y1 = canvasYFromDrawingPrice(p1.price);
9535
+ if (Math.hypot(x - px0, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
9536
+ if (Math.hypot(x - px1, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
9537
+ if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1)) {
9538
+ return { drawing, target: "line" };
9539
+ }
9446
9540
  } else if (drawing.type === "price-range") {
9447
9541
  const p0 = drawing.points[0];
9448
9542
  const p1 = drawing.points[1];
@@ -9894,6 +9988,33 @@ function createChart(element, options = {}) {
9894
9988
  scheduleDraw();
9895
9989
  return true;
9896
9990
  }
9991
+ if (activeDrawingTool === "fixed-range-volume-profile") {
9992
+ if (draftDrawing?.type === "fixed-range-volume-profile") {
9993
+ const completed = normalizeDrawingState({
9994
+ ...serializeDrawing(draftDrawing),
9995
+ points: [draftDrawing.points[0], point]
9996
+ });
9997
+ drawings.push(completed);
9998
+ draftDrawing = null;
9999
+ activeDrawingTool = null;
10000
+ emitDrawingsChange();
10001
+ scheduleDraw();
10002
+ return true;
10003
+ }
10004
+ const defaults = getDrawingToolDefaults("fixed-range-volume-profile");
10005
+ draftDrawing = normalizeDrawingState({
10006
+ type: "fixed-range-volume-profile",
10007
+ points: [point, point],
10008
+ color: defaults.color ?? "#5b8def",
10009
+ style: defaults.style ?? "solid",
10010
+ width: defaults.width ?? 1,
10011
+ ...defaults.profileRows === void 0 ? {} : { profileRows: defaults.profileRows },
10012
+ ...defaults.profileValueAreaPercent === void 0 ? {} : { profileValueAreaPercent: defaults.profileValueAreaPercent },
10013
+ ...defaults.profileWidthRatio === void 0 ? {} : { profileWidthRatio: defaults.profileWidthRatio }
10014
+ });
10015
+ scheduleDraw();
10016
+ return true;
10017
+ }
9897
10018
  if (activeDrawingTool === "price-range") {
9898
10019
  const tick = getConfiguredTickSize();
9899
10020
  const visibleRange = drawState.yMax - drawState.yMin;
package/dist/index.d.cts CHANGED
@@ -120,6 +120,12 @@ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray"
120
120
  | "brush"
121
121
  /** Text box with a connector line pointing at an anchored bar/price. */
122
122
  | "callout"
123
+ /**
124
+ * Volume profile over a bar range picked by two clicks (TradingView's
125
+ * fixed-range volume profile). Only the two anchors' bar indexes matter —
126
+ * the price extent comes from the bars they enclose.
127
+ */
128
+ | "fixed-range-volume-profile"
123
129
  /**
124
130
  * TradingView-style ruler. Unlike every other tool it is transient: the
125
131
  * measurement overlay is never added to the drawings list (no
@@ -157,6 +163,12 @@ interface DrawingObjectOptions {
157
163
  pointValue?: number;
158
164
  qtyPrecision?: number;
159
165
  fontSize?: number;
166
+ /** Price buckets across the range. Default 24. */
167
+ profileRows?: number;
168
+ /** Share of volume enclosed by the value area. Default 70. */
169
+ profileValueAreaPercent?: number;
170
+ /** Profile width as a share of the selected range. Default 0.5. */
171
+ profileWidthRatio?: number;
160
172
  }
161
173
  /** Default colors for position tools: [profit, loss, label text]. */
162
174
  declare const POSITION_DEFAULT_COLORS: string[];
@@ -220,7 +232,7 @@ interface DrawingHoverEvent {
220
232
  x: number;
221
233
  y: number;
222
234
  }
223
- type DrawingDefaults = Partial<Pick<DrawingObjectOptions, "color" | "colors" | "style" | "width" | "accountSize" | "lotSize" | "risk" | "riskMode" | "leverage" | "pointValue" | "qtyPrecision" | "fontSize">>;
235
+ type DrawingDefaults = Partial<Pick<DrawingObjectOptions, "color" | "colors" | "style" | "width" | "accountSize" | "lotSize" | "risk" | "riskMode" | "leverage" | "pointValue" | "qtyPrecision" | "fontSize" | "profileRows" | "profileValueAreaPercent" | "profileWidthRatio">>;
224
236
  interface IndicatorInstanceOptions<TInputs extends Record<string, unknown> = Record<string, unknown>> {
225
237
  id?: string;
226
238
  type: string;
package/dist/index.d.ts CHANGED
@@ -120,6 +120,12 @@ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray"
120
120
  | "brush"
121
121
  /** Text box with a connector line pointing at an anchored bar/price. */
122
122
  | "callout"
123
+ /**
124
+ * Volume profile over a bar range picked by two clicks (TradingView's
125
+ * fixed-range volume profile). Only the two anchors' bar indexes matter —
126
+ * the price extent comes from the bars they enclose.
127
+ */
128
+ | "fixed-range-volume-profile"
123
129
  /**
124
130
  * TradingView-style ruler. Unlike every other tool it is transient: the
125
131
  * measurement overlay is never added to the drawings list (no
@@ -157,6 +163,12 @@ interface DrawingObjectOptions {
157
163
  pointValue?: number;
158
164
  qtyPrecision?: number;
159
165
  fontSize?: number;
166
+ /** Price buckets across the range. Default 24. */
167
+ profileRows?: number;
168
+ /** Share of volume enclosed by the value area. Default 70. */
169
+ profileValueAreaPercent?: number;
170
+ /** Profile width as a share of the selected range. Default 0.5. */
171
+ profileWidthRatio?: number;
160
172
  }
161
173
  /** Default colors for position tools: [profit, loss, label text]. */
162
174
  declare const POSITION_DEFAULT_COLORS: string[];
@@ -220,7 +232,7 @@ interface DrawingHoverEvent {
220
232
  x: number;
221
233
  y: number;
222
234
  }
223
- type DrawingDefaults = Partial<Pick<DrawingObjectOptions, "color" | "colors" | "style" | "width" | "accountSize" | "lotSize" | "risk" | "riskMode" | "leverage" | "pointValue" | "qtyPrecision" | "fontSize">>;
235
+ type DrawingDefaults = Partial<Pick<DrawingObjectOptions, "color" | "colors" | "style" | "width" | "accountSize" | "lotSize" | "risk" | "riskMode" | "leverage" | "pointValue" | "qtyPrecision" | "fontSize" | "profileRows" | "profileValueAreaPercent" | "profileWidthRatio">>;
224
236
  interface IndicatorInstanceOptions<TInputs extends Record<string, unknown> = Record<string, unknown>> {
225
237
  id?: string;
226
238
  type: string;
package/dist/index.js CHANGED
@@ -2036,8 +2036,8 @@ var BUILTIN_DONCHIAN_INDICATOR = {
2036
2036
  }
2037
2037
  };
2038
2038
  var computeVolumeProfile = (data, fromIndex, toIndex, rowCount, valueAreaPercent) => {
2039
- const from = Math.max(0, fromIndex);
2040
- const to = Math.min(data.length - 1, toIndex);
2039
+ const from = Math.max(0, Math.round(fromIndex));
2040
+ const to = Math.min(data.length - 1, Math.round(toIndex));
2041
2041
  if (to < from) return null;
2042
2042
  let low = Number.POSITIVE_INFINITY;
2043
2043
  let high = Number.NEGATIVE_INFINITY;
@@ -3896,6 +3896,9 @@ function createChart(element, options = {}) {
3896
3896
  pointValue: Number(drawing.pointValue) || 1,
3897
3897
  qtyPrecision: Number.isFinite(drawing.qtyPrecision) ? Math.max(0, Math.floor(Number(drawing.qtyPrecision))) : 0,
3898
3898
  fontSize: Math.max(6, Number(drawing.fontSize) || 14),
3899
+ profileRows: Math.max(4, Math.min(200, Math.round(Number(drawing.profileRows)) || 24)),
3900
+ profileValueAreaPercent: Math.max(10, Math.min(99, Number(drawing.profileValueAreaPercent) || 70)),
3901
+ profileWidthRatio: Math.max(0.1, Math.min(1, Number(drawing.profileWidthRatio) || 0.5)),
3899
3902
  ...drawing.label === void 0 ? {} : { label: drawing.label }
3900
3903
  };
3901
3904
  };
@@ -3929,6 +3932,9 @@ function createChart(element, options = {}) {
3929
3932
  pointValue: drawing.pointValue,
3930
3933
  qtyPrecision: drawing.qtyPrecision,
3931
3934
  fontSize: drawing.fontSize,
3935
+ profileRows: drawing.profileRows,
3936
+ profileValueAreaPercent: drawing.profileValueAreaPercent,
3937
+ profileWidthRatio: drawing.profileWidthRatio,
3932
3938
  ...drawing.label === void 0 ? {} : { label: drawing.label }
3933
3939
  });
3934
3940
  let tradeMarkers = [];
@@ -6635,6 +6641,81 @@ function createChart(element, options = {}) {
6635
6641
  handleAt(ax, ay, drawing.color);
6636
6642
  handleAt(bx, by, drawing.color);
6637
6643
  }
6644
+ } else if (drawing.type === "fixed-range-volume-profile") {
6645
+ const p0 = drawing.points[0];
6646
+ const p1 = drawing.points[1];
6647
+ if (p0 && p1 && data.length > 0) {
6648
+ const fromIndex = Math.max(0, Math.round(Math.min(p0.index, p1.index)));
6649
+ const toIndex = Math.min(data.length - 1, Math.round(Math.max(p0.index, p1.index)));
6650
+ const leftX = xFromDrawingPoint(p0.index <= p1.index ? p0 : p1) - candleSpacing / 2;
6651
+ const rightX = xFromDrawingPoint(p0.index <= p1.index ? p1 : p0) + candleSpacing / 2;
6652
+ const rangeWidth = Math.max(1, rightX - leftX);
6653
+ const rows = Math.max(4, Math.min(200, Math.round(drawing.profileRows) || 24));
6654
+ const valueAreaPercent = Math.max(10, Math.min(99, Number(drawing.profileValueAreaPercent) || 70));
6655
+ const profile = toIndex >= fromIndex ? computeVolumeProfile(data, fromIndex, toIndex, rows, valueAreaPercent) : null;
6656
+ ctx.save();
6657
+ ctx.globalAlpha = draft ? 0.5 : 1;
6658
+ ctx.fillStyle = hexToRgba(drawing.color, 0.06);
6659
+ ctx.fillRect(leftX, chartTop, rangeWidth, chartBottom - chartTop);
6660
+ ctx.strokeStyle = hexToRgba(drawing.color, 0.5);
6661
+ ctx.lineWidth = 1;
6662
+ ctx.setLineDash([4, 3]);
6663
+ ctx.beginPath();
6664
+ ctx.moveTo(crisp(leftX), chartTop);
6665
+ ctx.lineTo(crisp(leftX), chartBottom);
6666
+ ctx.moveTo(crisp(rightX), chartTop);
6667
+ ctx.lineTo(crisp(rightX), chartBottom);
6668
+ ctx.stroke();
6669
+ ctx.setLineDash([]);
6670
+ if (profile && profile.maxRowVolume > 0) {
6671
+ const widthRatio = Math.max(0.1, Math.min(1, Number(drawing.profileWidthRatio) || 0.5));
6672
+ const maxWidth = rangeWidth * widthRatio;
6673
+ for (let row = 0; row < profile.rows.length; row += 1) {
6674
+ const bucket = profile.rows[row];
6675
+ if (bucket.total <= 0) continue;
6676
+ const yTop = yFromPrice(bucket.high);
6677
+ const yBottom = yFromPrice(bucket.low);
6678
+ const top = Math.min(yTop, yBottom);
6679
+ const height2 = Math.max(1, Math.abs(yBottom - yTop) - 1);
6680
+ if (top > chartBottom || top + height2 < chartTop) continue;
6681
+ const inValueArea = row >= profile.valueAreaFrom && row <= profile.valueAreaTo;
6682
+ ctx.globalAlpha = (draft ? 0.5 : 1) * (inValueArea ? 0.5 : 0.2);
6683
+ const rowWidth = bucket.total / profile.maxRowVolume * maxWidth;
6684
+ const upWidth = rowWidth * (bucket.up / bucket.total);
6685
+ ctx.fillStyle = mergedOptions.upColor;
6686
+ ctx.fillRect(leftX, top, upWidth, height2);
6687
+ ctx.fillStyle = mergedOptions.downColor;
6688
+ ctx.fillRect(leftX + upWidth, top, rowWidth - upWidth, height2);
6689
+ }
6690
+ ctx.globalAlpha = draft ? 0.6 : 1;
6691
+ const poc = profile.rows[profile.pocIndex];
6692
+ const drawProfileLevel = (price, color, label, dashed) => {
6693
+ const y = yFromPrice(price);
6694
+ if (y < chartTop || y > chartBottom) return;
6695
+ ctx.strokeStyle = color;
6696
+ ctx.lineWidth = 1;
6697
+ ctx.setLineDash(dashed ? [4, 3] : []);
6698
+ ctx.beginPath();
6699
+ ctx.moveTo(leftX, crisp(y));
6700
+ ctx.lineTo(leftX + maxWidth, crisp(y));
6701
+ ctx.stroke();
6702
+ ctx.setLineDash([]);
6703
+ const prevFont = ctx.font;
6704
+ ctx.font = `10px ${mergedOptions.fontFamily}`;
6705
+ ctx.fillStyle = color;
6706
+ ctx.textAlign = "left";
6707
+ ctx.textBaseline = "bottom";
6708
+ ctx.fillText(label, leftX + 3, y - 1);
6709
+ ctx.font = prevFont;
6710
+ };
6711
+ drawProfileLevel(profile.rows[profile.valueAreaTo].high, "#7e8aa3", "VAH", true);
6712
+ drawProfileLevel(profile.rows[profile.valueAreaFrom].low, "#7e8aa3", "VAL", true);
6713
+ drawProfileLevel((poc.low + poc.high) / 2, "#f7a600", "POC", false);
6714
+ }
6715
+ ctx.restore();
6716
+ handleAt(leftX, yFromPrice(p0.index <= p1.index ? p0.price : p1.price), drawing.color);
6717
+ handleAt(rightX, yFromPrice(p0.index <= p1.index ? p1.price : p0.price), drawing.color);
6718
+ }
6638
6719
  } else if (drawing.type === "price-range") {
6639
6720
  const p0 = drawing.points[0];
6640
6721
  const p1 = drawing.points[1];
@@ -9407,6 +9488,19 @@ function createChart(element, options = {}) {
9407
9488
  if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5 * hitTolerance) {
9408
9489
  return { drawing, target: "line" };
9409
9490
  }
9491
+ } else if (drawing.type === "fixed-range-volume-profile") {
9492
+ const p0 = drawing.points[0];
9493
+ const p1 = drawing.points[1];
9494
+ if (!p0 || !p1) continue;
9495
+ const px0 = canvasXFromDrawingPoint(p0);
9496
+ const px1 = canvasXFromDrawingPoint(p1);
9497
+ const y0 = canvasYFromDrawingPrice(p0.price);
9498
+ const y1 = canvasYFromDrawingPrice(p1.price);
9499
+ if (Math.hypot(x - px0, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
9500
+ if (Math.hypot(x - px1, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
9501
+ if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1)) {
9502
+ return { drawing, target: "line" };
9503
+ }
9410
9504
  } else if (drawing.type === "price-range") {
9411
9505
  const p0 = drawing.points[0];
9412
9506
  const p1 = drawing.points[1];
@@ -9858,6 +9952,33 @@ function createChart(element, options = {}) {
9858
9952
  scheduleDraw();
9859
9953
  return true;
9860
9954
  }
9955
+ if (activeDrawingTool === "fixed-range-volume-profile") {
9956
+ if (draftDrawing?.type === "fixed-range-volume-profile") {
9957
+ const completed = normalizeDrawingState({
9958
+ ...serializeDrawing(draftDrawing),
9959
+ points: [draftDrawing.points[0], point]
9960
+ });
9961
+ drawings.push(completed);
9962
+ draftDrawing = null;
9963
+ activeDrawingTool = null;
9964
+ emitDrawingsChange();
9965
+ scheduleDraw();
9966
+ return true;
9967
+ }
9968
+ const defaults = getDrawingToolDefaults("fixed-range-volume-profile");
9969
+ draftDrawing = normalizeDrawingState({
9970
+ type: "fixed-range-volume-profile",
9971
+ points: [point, point],
9972
+ color: defaults.color ?? "#5b8def",
9973
+ style: defaults.style ?? "solid",
9974
+ width: defaults.width ?? 1,
9975
+ ...defaults.profileRows === void 0 ? {} : { profileRows: defaults.profileRows },
9976
+ ...defaults.profileValueAreaPercent === void 0 ? {} : { profileValueAreaPercent: defaults.profileValueAreaPercent },
9977
+ ...defaults.profileWidthRatio === void 0 ? {} : { profileWidthRatio: defaults.profileWidthRatio }
9978
+ });
9979
+ scheduleDraw();
9980
+ return true;
9981
+ }
9861
9982
  if (activeDrawingTool === "price-range") {
9862
9983
  const tick = getConfiguredTickSize();
9863
9984
  const visibleRange = drawState.yMax - drawState.yMin;
package/docs/API.md CHANGED
@@ -645,6 +645,70 @@ link.click();
645
645
 
646
646
  ---
647
647
 
648
+ ## Futures desk indicators
649
+
650
+ Three built-ins aimed at intraday futures work. All three are overlays on the
651
+ main pane, and the last two need a session spec (`setSession`) — without one
652
+ they have no notion of a trading day and render nothing.
653
+
654
+ ### `volume-profile`
655
+
656
+ Volume distribution across price, with point of control and value area.
657
+
658
+ ```ts
659
+ chart.addIndicator("volume-profile", { scope: "visible", rows: 40, widthPercent: 28 });
660
+ chart.addIndicator("volume-profile", { scope: "session" }); // one per trading day
661
+ ```
662
+
663
+ `scope: "visible"` profiles whatever is on screen and follows pan/zoom, drawn
664
+ against the price axis; `scope: "session"` draws a separate profile inside each
665
+ trading day's bars, skipping any session too narrow to read. Rows are split
666
+ into buy and sell volume by each bar's own direction, shaded brighter inside
667
+ the value area. Inputs: `rows`, `widthPercent`, `placement`
668
+ (`"right" | "left"`, visible scope only), `valueAreaPercent`, `upColor`,
669
+ `downColor`, `valueAreaOpacity`, `outsideOpacity`, `showPoc`, `pocColor`,
670
+ `showValueArea`, `valueAreaColor`, `showLabels`.
671
+
672
+ OHLCV bars carry no intrabar detail, so each bar's volume is spread evenly over
673
+ the rows its high–low range touches — the same approximation TradingView makes
674
+ when building a profile from candles rather than ticks.
675
+
676
+ ### `session-levels`
677
+
678
+ The reference prices marked up before the open: prior-day high/low/close
679
+ (PDH/PDL/PDC), overnight high/low (ONH/ONL), the opening range (ORH/ORL) and
680
+ optionally the initial balance (IBH/IBL, first hour).
681
+
682
+ ```ts
683
+ chart.setSession({ spec: "cme-futures" });
684
+ chart.addIndicator("session-levels", { openingRangeMinutes: 30, showInitialBalance: true });
685
+ ```
686
+
687
+ Cash-session bounds are inputs (`rthStart` / `rthEnd`, default 09:30–16:00)
688
+ rather than being read from the chart's session spec: a CME futures session
689
+ runs 18:00–17:00, but the levels traders mark are relative to the 09:30 equity
690
+ open inside it. Everything in the current session outside those hours counts as
691
+ overnight.
692
+
693
+ ### `anchored-vwap`
694
+
695
+ VWAP measured from a bar you choose rather than the session open.
696
+
697
+ ```ts
698
+ chart.addIndicator("anchored-vwap", {
699
+ anchorTime: "2026-07-14T13:30:00Z", // epoch ms or ISO
700
+ showBands: true,
701
+ bandMultiplier: 2
702
+ });
703
+ ```
704
+
705
+ Without an `anchorTime` it anchors to the most recent session start, which is
706
+ just the session VWAP. Add one instance per anchor; hosts typically expose this
707
+ as a "start here" action on the bar under the cursor. Volume-less feeds fall
708
+ back to an unweighted running average rather than flatlining.
709
+
710
+ ---
711
+
648
712
  ## Time zones and trading sessions
649
713
 
650
714
  Timestamps are rendered in whatever zone you choose, and the chart can be told
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.146",
3
+ "version": "0.1.147",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",