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.
- package/dist/hyperprop-charting-library.cjs +123 -2
- package/dist/hyperprop-charting-library.d.ts +13 -1
- package/dist/hyperprop-charting-library.js +123 -2
- package/dist/index.cjs +123 -2
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +123 -2
- package/docs/API.md +64 -0
- package/package.json +1 -1
|
@@ -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
|