hyperprop-charting-library 0.1.118 → 0.1.120
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hyperprop-charting-library.cjs +230 -8
- package/dist/hyperprop-charting-library.d.ts +8 -0
- package/dist/hyperprop-charting-library.js +230 -8
- package/dist/index.cjs +230 -8
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +230 -8
- package/package.json +1 -1
|
@@ -43,7 +43,10 @@ var DEFAULT_GRID_OPTIONS = {
|
|
|
43
43
|
verticalLines: true,
|
|
44
44
|
xTickCount: 8,
|
|
45
45
|
yTickCount: 6,
|
|
46
|
-
horizontalTickCount: 6
|
|
46
|
+
horizontalTickCount: 6,
|
|
47
|
+
sessionSeparators: false,
|
|
48
|
+
sessionSeparatorColor: "#3b4150",
|
|
49
|
+
sessionSeparatorOpacity: 0.55
|
|
47
50
|
};
|
|
48
51
|
var DEFAULT_AXIS_OPTIONS = {
|
|
49
52
|
lineColor: "#3b3f47",
|
|
@@ -432,6 +435,60 @@ var computeStdDevSeries = (data, length, source) => {
|
|
|
432
435
|
}
|
|
433
436
|
return result;
|
|
434
437
|
};
|
|
438
|
+
var VWAP_SESSION_ANCHOR_MS = 22 * 36e5;
|
|
439
|
+
var vwapSessionDayOf = (ms) => Math.floor((ms - VWAP_SESSION_ANCHOR_MS) / 864e5);
|
|
440
|
+
var computeVwapSeries = (data, band) => {
|
|
441
|
+
const result = new Array(data.length).fill(null);
|
|
442
|
+
let session = Number.NaN;
|
|
443
|
+
let cumPv = 0;
|
|
444
|
+
let cumPv2 = 0;
|
|
445
|
+
let cumV = 0;
|
|
446
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
447
|
+
const point = data[i];
|
|
448
|
+
if (!point) continue;
|
|
449
|
+
const day = vwapSessionDayOf(point.time.getTime());
|
|
450
|
+
if (day !== session) {
|
|
451
|
+
session = day;
|
|
452
|
+
cumPv = 0;
|
|
453
|
+
cumPv2 = 0;
|
|
454
|
+
cumV = 0;
|
|
455
|
+
}
|
|
456
|
+
const typical = (point.h + point.l + point.c) / 3;
|
|
457
|
+
const volume = Math.max(0, point.v ?? 0);
|
|
458
|
+
cumPv += typical * volume;
|
|
459
|
+
cumPv2 += typical * typical * volume;
|
|
460
|
+
cumV += volume;
|
|
461
|
+
if (cumV <= 0) continue;
|
|
462
|
+
const vwap = cumPv / cumV;
|
|
463
|
+
if (band === "vwap") {
|
|
464
|
+
result[i] = vwap;
|
|
465
|
+
} else {
|
|
466
|
+
const variance = Math.max(0, cumPv2 / cumV - vwap * vwap);
|
|
467
|
+
result[i] = Math.sqrt(variance);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
for (let i = 1; i < data.length; i += 1) {
|
|
471
|
+
const point = data[i];
|
|
472
|
+
const prev = data[i - 1];
|
|
473
|
+
const next = data[i + 1];
|
|
474
|
+
if (!point || !prev || !next) continue;
|
|
475
|
+
const day = vwapSessionDayOf(point.time.getTime());
|
|
476
|
+
if (day !== vwapSessionDayOf(prev.time.getTime()) && day === vwapSessionDayOf(next.time.getTime())) {
|
|
477
|
+
result[i] = null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
};
|
|
482
|
+
var computeBollingerSeries = (data, length, multiplier, source, band) => {
|
|
483
|
+
const basis = computeSmaSeries(data, length, source);
|
|
484
|
+
if (band === "basis") return basis;
|
|
485
|
+
const dev = computeStdDevSeries(data, length, source);
|
|
486
|
+
return basis.map((value, idx) => {
|
|
487
|
+
const sigma = dev[idx];
|
|
488
|
+
if (value == null || sigma == null) return null;
|
|
489
|
+
return band === "upper" ? value + multiplier * sigma : value - multiplier * sigma;
|
|
490
|
+
});
|
|
491
|
+
};
|
|
435
492
|
var computeRsiSeries = (data, length) => {
|
|
436
493
|
const result = new Array(data.length).fill(null);
|
|
437
494
|
if (data.length < 2) return result;
|
|
@@ -558,6 +615,39 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
|
558
615
|
}
|
|
559
616
|
ctx.restore();
|
|
560
617
|
};
|
|
618
|
+
var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
|
|
619
|
+
if (!renderContext.yFromPrice || opacity <= 0) return;
|
|
620
|
+
const yFromPrice = renderContext.yFromPrice;
|
|
621
|
+
ctx.save();
|
|
622
|
+
ctx.globalAlpha = Math.min(1, Math.max(0, opacity));
|
|
623
|
+
ctx.fillStyle = color;
|
|
624
|
+
let runStart = -1;
|
|
625
|
+
const flushRun = (endIndexExclusive) => {
|
|
626
|
+
if (runStart < 0) return;
|
|
627
|
+
ctx.beginPath();
|
|
628
|
+
for (let i = runStart; i < endIndexExclusive; i += 1) {
|
|
629
|
+
const x = renderContext.xFromIndex(i);
|
|
630
|
+
const y = yFromPrice(upper[i]);
|
|
631
|
+
if (i === runStart) ctx.moveTo(x, y);
|
|
632
|
+
else ctx.lineTo(x, y);
|
|
633
|
+
}
|
|
634
|
+
for (let i = endIndexExclusive - 1; i >= runStart; i -= 1) {
|
|
635
|
+
ctx.lineTo(renderContext.xFromIndex(i), yFromPrice(lower[i]));
|
|
636
|
+
}
|
|
637
|
+
ctx.closePath();
|
|
638
|
+
ctx.fill();
|
|
639
|
+
runStart = -1;
|
|
640
|
+
};
|
|
641
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
642
|
+
const up = upper[index];
|
|
643
|
+
const lo = lower[index];
|
|
644
|
+
const valid = Number.isFinite(up ?? Number.NaN) && Number.isFinite(lo ?? Number.NaN);
|
|
645
|
+
if (valid && runStart < 0) runStart = index;
|
|
646
|
+
if (!valid) flushRun(index);
|
|
647
|
+
}
|
|
648
|
+
flushRun(renderContext.endIndex + 1);
|
|
649
|
+
ctx.restore();
|
|
650
|
+
};
|
|
561
651
|
var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
|
|
562
652
|
const visible = [];
|
|
563
653
|
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
@@ -819,6 +909,89 @@ var BUILTIN_HMA_INDICATOR = {
|
|
|
819
909
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
|
|
820
910
|
}
|
|
821
911
|
};
|
|
912
|
+
var BUILTIN_VWAP_INDICATOR = {
|
|
913
|
+
id: "vwap",
|
|
914
|
+
name: "VWAP",
|
|
915
|
+
pane: "overlay",
|
|
916
|
+
defaultInputs: {
|
|
917
|
+
color: "#f59e0b",
|
|
918
|
+
width: 2,
|
|
919
|
+
showBands: false,
|
|
920
|
+
bandMultiplier: 1,
|
|
921
|
+
bandColor: "#94a3b8",
|
|
922
|
+
bandFillOpacity: 0.06
|
|
923
|
+
},
|
|
924
|
+
draw: (ctx, renderContext, inputs) => {
|
|
925
|
+
const vwap = withCachedSeries(
|
|
926
|
+
"vwap|line",
|
|
927
|
+
renderContext.data,
|
|
928
|
+
() => computeVwapSeries(renderContext.data, "vwap")
|
|
929
|
+
);
|
|
930
|
+
if (inputs.showBands) {
|
|
931
|
+
const sigma = withCachedSeries(
|
|
932
|
+
"vwap|sigma",
|
|
933
|
+
renderContext.data,
|
|
934
|
+
() => computeVwapSeries(renderContext.data, "sigma")
|
|
935
|
+
);
|
|
936
|
+
const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
|
|
937
|
+
const upper = vwap.map((value, idx) => {
|
|
938
|
+
const s = sigma[idx];
|
|
939
|
+
return value == null || s == null ? null : value + multiplier * s;
|
|
940
|
+
});
|
|
941
|
+
const lower = vwap.map((value, idx) => {
|
|
942
|
+
const s = sigma[idx];
|
|
943
|
+
return value == null || s == null ? null : value - multiplier * s;
|
|
944
|
+
});
|
|
945
|
+
const bandColor = inputs.bandColor ?? "#94a3b8";
|
|
946
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.bandFillOpacity) || 0.06);
|
|
947
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, 1);
|
|
948
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
|
|
949
|
+
}
|
|
950
|
+
drawOverlaySeries(ctx, renderContext, vwap, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
var BUILTIN_BOLLINGER_INDICATOR = {
|
|
954
|
+
id: "bollinger",
|
|
955
|
+
name: "BB",
|
|
956
|
+
pane: "overlay",
|
|
957
|
+
// Basis is pink so it stays distinguishable from the (orange) VWAP line
|
|
958
|
+
// when both overlays are active.
|
|
959
|
+
defaultInputs: {
|
|
960
|
+
length: 20,
|
|
961
|
+
multiplier: 2,
|
|
962
|
+
source: "close",
|
|
963
|
+
basisColor: "#ec4899",
|
|
964
|
+
bandColor: "#3b82f6",
|
|
965
|
+
width: 1.5,
|
|
966
|
+
fillOpacity: 0.05
|
|
967
|
+
},
|
|
968
|
+
draw: (ctx, renderContext, inputs) => {
|
|
969
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
970
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
|
|
971
|
+
const source = inputs.source ?? "close";
|
|
972
|
+
const basis = withCachedSeries(
|
|
973
|
+
`bb|basis|${length}|${source}`,
|
|
974
|
+
renderContext.data,
|
|
975
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "basis")
|
|
976
|
+
);
|
|
977
|
+
const upper = withCachedSeries(
|
|
978
|
+
`bb|upper|${length}|${multiplier}|${source}`,
|
|
979
|
+
renderContext.data,
|
|
980
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "upper")
|
|
981
|
+
);
|
|
982
|
+
const lower = withCachedSeries(
|
|
983
|
+
`bb|lower|${length}|${multiplier}|${source}`,
|
|
984
|
+
renderContext.data,
|
|
985
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "lower")
|
|
986
|
+
);
|
|
987
|
+
const bandColor = inputs.bandColor ?? "#3b82f6";
|
|
988
|
+
const width = Number(inputs.width) || 1.5;
|
|
989
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.05);
|
|
990
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
|
|
991
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
|
|
992
|
+
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#f59e0b", width);
|
|
993
|
+
}
|
|
994
|
+
};
|
|
822
995
|
var BUILTIN_STDDEV_INDICATOR = {
|
|
823
996
|
id: "stddev",
|
|
824
997
|
name: "StdDev",
|
|
@@ -902,6 +1075,8 @@ var BUILTIN_INDICATORS = [
|
|
|
902
1075
|
BUILTIN_VWMA_INDICATOR,
|
|
903
1076
|
BUILTIN_RMA_INDICATOR,
|
|
904
1077
|
BUILTIN_HMA_INDICATOR,
|
|
1078
|
+
BUILTIN_VWAP_INDICATOR,
|
|
1079
|
+
BUILTIN_BOLLINGER_INDICATOR,
|
|
905
1080
|
BUILTIN_STDDEV_INDICATOR,
|
|
906
1081
|
BUILTIN_ATR_INDICATOR
|
|
907
1082
|
];
|
|
@@ -2754,15 +2929,31 @@ function createChart(element, options = {}) {
|
|
|
2754
2929
|
const pct = diff / base * 100;
|
|
2755
2930
|
const ticks = tick > 0 ? Math.round(diff / tick) : 0;
|
|
2756
2931
|
const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
|
|
2757
|
-
const
|
|
2932
|
+
const labelLines = [
|
|
2933
|
+
tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`
|
|
2934
|
+
];
|
|
2935
|
+
const barSpan = Math.abs(Math.round(p1.index) - Math.round(p0.index));
|
|
2936
|
+
if (barSpan > 0) {
|
|
2937
|
+
const t0 = getTimeForIndex(Math.round(p0.index));
|
|
2938
|
+
const t1 = getTimeForIndex(Math.round(p1.index));
|
|
2939
|
+
const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
|
|
2940
|
+
labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatDuration(spanMs)}` : `${barSpan} bars`);
|
|
2941
|
+
}
|
|
2942
|
+
const pointValue = Number(drawing.pointValue);
|
|
2943
|
+
if (Number.isFinite(pointValue) && pointValue > 0) {
|
|
2944
|
+
const dollars = diff * pointValue;
|
|
2945
|
+
const money = Math.abs(dollars).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
2946
|
+
labelLines.push(`${signed(dollars, `$${money}`)} / contract`);
|
|
2947
|
+
}
|
|
2758
2948
|
const prevFont = ctx.font;
|
|
2759
2949
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2760
2950
|
const padding = 6;
|
|
2761
|
-
const
|
|
2951
|
+
const lineHeight = 15;
|
|
2952
|
+
const textW = Math.max(...labelLines.map((line) => ctx.measureText(line).width));
|
|
2762
2953
|
const pillW = textW + padding * 2;
|
|
2763
|
-
const pillH =
|
|
2954
|
+
const pillH = labelLines.length * lineHeight + padding;
|
|
2764
2955
|
const pillX = midX - pillW / 2;
|
|
2765
|
-
const pillY = botY + 6;
|
|
2956
|
+
const pillY = botY + 6 + pillH <= fullChartBottom - 2 ? botY + 6 : Math.max(chartTop + 2, topY - 6 - pillH);
|
|
2766
2957
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2767
2958
|
fillRoundedRect(pillX, pillY, pillW, pillH, 4);
|
|
2768
2959
|
ctx.save();
|
|
@@ -2773,7 +2964,9 @@ function createChart(element, options = {}) {
|
|
|
2773
2964
|
ctx.fillStyle = drawing.color;
|
|
2774
2965
|
ctx.textAlign = "center";
|
|
2775
2966
|
ctx.textBaseline = "middle";
|
|
2776
|
-
|
|
2967
|
+
labelLines.forEach((line, lineIndex) => {
|
|
2968
|
+
ctx.fillText(line, midX, pillY + padding / 2 + lineIndex * lineHeight + lineHeight / 2);
|
|
2969
|
+
});
|
|
2777
2970
|
ctx.font = prevFont;
|
|
2778
2971
|
if (drawing.label) {
|
|
2779
2972
|
drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
|
|
@@ -2917,6 +3110,32 @@ function createChart(element, options = {}) {
|
|
|
2917
3110
|
}
|
|
2918
3111
|
ctx.restore();
|
|
2919
3112
|
}
|
|
3113
|
+
if (grid.sessionSeparators && data.length > 1) {
|
|
3114
|
+
const first = Math.max(1, startIndex);
|
|
3115
|
+
const barDeltaMs = data.length > 1 ? Math.abs(data[Math.min(first, data.length - 1)].time.getTime() - data[Math.min(first, data.length - 1) - 1].time.getTime()) : 0;
|
|
3116
|
+
if (barDeltaMs > 0 && barDeltaMs < 0.9 * 864e5) {
|
|
3117
|
+
const SESSION_ANCHOR_MS = 22 * 36e5;
|
|
3118
|
+
const sessionDayOf = (ms) => Math.floor((ms - SESSION_ANCHOR_MS) / 864e5);
|
|
3119
|
+
ctx.save();
|
|
3120
|
+
ctx.globalAlpha = clamp(grid.sessionSeparatorOpacity, 0, 1);
|
|
3121
|
+
ctx.strokeStyle = grid.sessionSeparatorColor;
|
|
3122
|
+
ctx.lineWidth = 1;
|
|
3123
|
+
for (let index = first; index <= endIndex; index += 1) {
|
|
3124
|
+
const point = data[index];
|
|
3125
|
+
const prev = data[index - 1];
|
|
3126
|
+
if (!point || !prev) continue;
|
|
3127
|
+
if (sessionDayOf(point.time.getTime()) === sessionDayOf(prev.time.getTime())) {
|
|
3128
|
+
continue;
|
|
3129
|
+
}
|
|
3130
|
+
const x = chartLeft + (index - xStart) / xSpan * chartWidth;
|
|
3131
|
+
ctx.beginPath();
|
|
3132
|
+
ctx.moveTo(crisp(x), crisp(chartTop));
|
|
3133
|
+
ctx.lineTo(crisp(x), crisp(fullChartBottom));
|
|
3134
|
+
ctx.stroke();
|
|
3135
|
+
}
|
|
3136
|
+
ctx.restore();
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
2920
3139
|
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2921
3140
|
const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
|
|
2922
3141
|
const lastDataIndex = data.length - 1;
|
|
@@ -3523,8 +3742,9 @@ function createChart(element, options = {}) {
|
|
|
3523
3742
|
}
|
|
3524
3743
|
return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
|
|
3525
3744
|
};
|
|
3745
|
+
const LEGEND_EXCLUDED_INPUT_KEYS = /* @__PURE__ */ new Set(["width", "bandMultiplier", "bandFillOpacity", "fillOpacity"]);
|
|
3526
3746
|
const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
|
|
3527
|
-
const inputValues = Object.entries(indicator.inputs).filter(([, value]) => isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
|
|
3747
|
+
const inputValues = Object.entries(indicator.inputs).filter(([key, value]) => !LEGEND_EXCLUDED_INPUT_KEYS.has(key) && typeof value !== "boolean" && isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
|
|
3528
3748
|
if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
|
|
3529
3749
|
return `${plugin.name} ${inputValues.join(" ")}`;
|
|
3530
3750
|
}
|
|
@@ -4502,7 +4722,9 @@ function createChart(element, options = {}) {
|
|
|
4502
4722
|
points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
|
|
4503
4723
|
color: defaults.color ?? "#2962ff",
|
|
4504
4724
|
style: defaults.style ?? "solid",
|
|
4505
|
-
width: defaults.width ?? 1
|
|
4725
|
+
width: defaults.width ?? 1,
|
|
4726
|
+
// pointValue enables the dollar-P&L line in the measure label.
|
|
4727
|
+
...defaults.pointValue === void 0 ? {} : { pointValue: defaults.pointValue }
|
|
4506
4728
|
})
|
|
4507
4729
|
);
|
|
4508
4730
|
emitDrawingsChange();
|
|
@@ -215,6 +215,14 @@ interface GridOptions {
|
|
|
215
215
|
xTickCount?: number;
|
|
216
216
|
yTickCount?: number;
|
|
217
217
|
horizontalTickCount?: number;
|
|
218
|
+
/**
|
|
219
|
+
* Vertical separator at every trading-day boundary (a bar whose UTC
|
|
220
|
+
* calendar day differs from the previous bar's). Drawn stronger than the
|
|
221
|
+
* regular grid so intraday charts stay oriented while scrolling history.
|
|
222
|
+
*/
|
|
223
|
+
sessionSeparators?: boolean;
|
|
224
|
+
sessionSeparatorColor?: string;
|
|
225
|
+
sessionSeparatorOpacity?: number;
|
|
218
226
|
}
|
|
219
227
|
interface CrosshairOptions {
|
|
220
228
|
visible?: boolean;
|
|
@@ -17,7 +17,10 @@ var DEFAULT_GRID_OPTIONS = {
|
|
|
17
17
|
verticalLines: true,
|
|
18
18
|
xTickCount: 8,
|
|
19
19
|
yTickCount: 6,
|
|
20
|
-
horizontalTickCount: 6
|
|
20
|
+
horizontalTickCount: 6,
|
|
21
|
+
sessionSeparators: false,
|
|
22
|
+
sessionSeparatorColor: "#3b4150",
|
|
23
|
+
sessionSeparatorOpacity: 0.55
|
|
21
24
|
};
|
|
22
25
|
var DEFAULT_AXIS_OPTIONS = {
|
|
23
26
|
lineColor: "#3b3f47",
|
|
@@ -406,6 +409,60 @@ var computeStdDevSeries = (data, length, source) => {
|
|
|
406
409
|
}
|
|
407
410
|
return result;
|
|
408
411
|
};
|
|
412
|
+
var VWAP_SESSION_ANCHOR_MS = 22 * 36e5;
|
|
413
|
+
var vwapSessionDayOf = (ms) => Math.floor((ms - VWAP_SESSION_ANCHOR_MS) / 864e5);
|
|
414
|
+
var computeVwapSeries = (data, band) => {
|
|
415
|
+
const result = new Array(data.length).fill(null);
|
|
416
|
+
let session = Number.NaN;
|
|
417
|
+
let cumPv = 0;
|
|
418
|
+
let cumPv2 = 0;
|
|
419
|
+
let cumV = 0;
|
|
420
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
421
|
+
const point = data[i];
|
|
422
|
+
if (!point) continue;
|
|
423
|
+
const day = vwapSessionDayOf(point.time.getTime());
|
|
424
|
+
if (day !== session) {
|
|
425
|
+
session = day;
|
|
426
|
+
cumPv = 0;
|
|
427
|
+
cumPv2 = 0;
|
|
428
|
+
cumV = 0;
|
|
429
|
+
}
|
|
430
|
+
const typical = (point.h + point.l + point.c) / 3;
|
|
431
|
+
const volume = Math.max(0, point.v ?? 0);
|
|
432
|
+
cumPv += typical * volume;
|
|
433
|
+
cumPv2 += typical * typical * volume;
|
|
434
|
+
cumV += volume;
|
|
435
|
+
if (cumV <= 0) continue;
|
|
436
|
+
const vwap = cumPv / cumV;
|
|
437
|
+
if (band === "vwap") {
|
|
438
|
+
result[i] = vwap;
|
|
439
|
+
} else {
|
|
440
|
+
const variance = Math.max(0, cumPv2 / cumV - vwap * vwap);
|
|
441
|
+
result[i] = Math.sqrt(variance);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (let i = 1; i < data.length; i += 1) {
|
|
445
|
+
const point = data[i];
|
|
446
|
+
const prev = data[i - 1];
|
|
447
|
+
const next = data[i + 1];
|
|
448
|
+
if (!point || !prev || !next) continue;
|
|
449
|
+
const day = vwapSessionDayOf(point.time.getTime());
|
|
450
|
+
if (day !== vwapSessionDayOf(prev.time.getTime()) && day === vwapSessionDayOf(next.time.getTime())) {
|
|
451
|
+
result[i] = null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return result;
|
|
455
|
+
};
|
|
456
|
+
var computeBollingerSeries = (data, length, multiplier, source, band) => {
|
|
457
|
+
const basis = computeSmaSeries(data, length, source);
|
|
458
|
+
if (band === "basis") return basis;
|
|
459
|
+
const dev = computeStdDevSeries(data, length, source);
|
|
460
|
+
return basis.map((value, idx) => {
|
|
461
|
+
const sigma = dev[idx];
|
|
462
|
+
if (value == null || sigma == null) return null;
|
|
463
|
+
return band === "upper" ? value + multiplier * sigma : value - multiplier * sigma;
|
|
464
|
+
});
|
|
465
|
+
};
|
|
409
466
|
var computeRsiSeries = (data, length) => {
|
|
410
467
|
const result = new Array(data.length).fill(null);
|
|
411
468
|
if (data.length < 2) return result;
|
|
@@ -532,6 +589,39 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
|
532
589
|
}
|
|
533
590
|
ctx.restore();
|
|
534
591
|
};
|
|
592
|
+
var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
|
|
593
|
+
if (!renderContext.yFromPrice || opacity <= 0) return;
|
|
594
|
+
const yFromPrice = renderContext.yFromPrice;
|
|
595
|
+
ctx.save();
|
|
596
|
+
ctx.globalAlpha = Math.min(1, Math.max(0, opacity));
|
|
597
|
+
ctx.fillStyle = color;
|
|
598
|
+
let runStart = -1;
|
|
599
|
+
const flushRun = (endIndexExclusive) => {
|
|
600
|
+
if (runStart < 0) return;
|
|
601
|
+
ctx.beginPath();
|
|
602
|
+
for (let i = runStart; i < endIndexExclusive; i += 1) {
|
|
603
|
+
const x = renderContext.xFromIndex(i);
|
|
604
|
+
const y = yFromPrice(upper[i]);
|
|
605
|
+
if (i === runStart) ctx.moveTo(x, y);
|
|
606
|
+
else ctx.lineTo(x, y);
|
|
607
|
+
}
|
|
608
|
+
for (let i = endIndexExclusive - 1; i >= runStart; i -= 1) {
|
|
609
|
+
ctx.lineTo(renderContext.xFromIndex(i), yFromPrice(lower[i]));
|
|
610
|
+
}
|
|
611
|
+
ctx.closePath();
|
|
612
|
+
ctx.fill();
|
|
613
|
+
runStart = -1;
|
|
614
|
+
};
|
|
615
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
616
|
+
const up = upper[index];
|
|
617
|
+
const lo = lower[index];
|
|
618
|
+
const valid = Number.isFinite(up ?? Number.NaN) && Number.isFinite(lo ?? Number.NaN);
|
|
619
|
+
if (valid && runStart < 0) runStart = index;
|
|
620
|
+
if (!valid) flushRun(index);
|
|
621
|
+
}
|
|
622
|
+
flushRun(renderContext.endIndex + 1);
|
|
623
|
+
ctx.restore();
|
|
624
|
+
};
|
|
535
625
|
var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
|
|
536
626
|
const visible = [];
|
|
537
627
|
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
@@ -793,6 +883,89 @@ var BUILTIN_HMA_INDICATOR = {
|
|
|
793
883
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
|
|
794
884
|
}
|
|
795
885
|
};
|
|
886
|
+
var BUILTIN_VWAP_INDICATOR = {
|
|
887
|
+
id: "vwap",
|
|
888
|
+
name: "VWAP",
|
|
889
|
+
pane: "overlay",
|
|
890
|
+
defaultInputs: {
|
|
891
|
+
color: "#f59e0b",
|
|
892
|
+
width: 2,
|
|
893
|
+
showBands: false,
|
|
894
|
+
bandMultiplier: 1,
|
|
895
|
+
bandColor: "#94a3b8",
|
|
896
|
+
bandFillOpacity: 0.06
|
|
897
|
+
},
|
|
898
|
+
draw: (ctx, renderContext, inputs) => {
|
|
899
|
+
const vwap = withCachedSeries(
|
|
900
|
+
"vwap|line",
|
|
901
|
+
renderContext.data,
|
|
902
|
+
() => computeVwapSeries(renderContext.data, "vwap")
|
|
903
|
+
);
|
|
904
|
+
if (inputs.showBands) {
|
|
905
|
+
const sigma = withCachedSeries(
|
|
906
|
+
"vwap|sigma",
|
|
907
|
+
renderContext.data,
|
|
908
|
+
() => computeVwapSeries(renderContext.data, "sigma")
|
|
909
|
+
);
|
|
910
|
+
const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
|
|
911
|
+
const upper = vwap.map((value, idx) => {
|
|
912
|
+
const s = sigma[idx];
|
|
913
|
+
return value == null || s == null ? null : value + multiplier * s;
|
|
914
|
+
});
|
|
915
|
+
const lower = vwap.map((value, idx) => {
|
|
916
|
+
const s = sigma[idx];
|
|
917
|
+
return value == null || s == null ? null : value - multiplier * s;
|
|
918
|
+
});
|
|
919
|
+
const bandColor = inputs.bandColor ?? "#94a3b8";
|
|
920
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.bandFillOpacity) || 0.06);
|
|
921
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, 1);
|
|
922
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
|
|
923
|
+
}
|
|
924
|
+
drawOverlaySeries(ctx, renderContext, vwap, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
var BUILTIN_BOLLINGER_INDICATOR = {
|
|
928
|
+
id: "bollinger",
|
|
929
|
+
name: "BB",
|
|
930
|
+
pane: "overlay",
|
|
931
|
+
// Basis is pink so it stays distinguishable from the (orange) VWAP line
|
|
932
|
+
// when both overlays are active.
|
|
933
|
+
defaultInputs: {
|
|
934
|
+
length: 20,
|
|
935
|
+
multiplier: 2,
|
|
936
|
+
source: "close",
|
|
937
|
+
basisColor: "#ec4899",
|
|
938
|
+
bandColor: "#3b82f6",
|
|
939
|
+
width: 1.5,
|
|
940
|
+
fillOpacity: 0.05
|
|
941
|
+
},
|
|
942
|
+
draw: (ctx, renderContext, inputs) => {
|
|
943
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
944
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
|
|
945
|
+
const source = inputs.source ?? "close";
|
|
946
|
+
const basis = withCachedSeries(
|
|
947
|
+
`bb|basis|${length}|${source}`,
|
|
948
|
+
renderContext.data,
|
|
949
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "basis")
|
|
950
|
+
);
|
|
951
|
+
const upper = withCachedSeries(
|
|
952
|
+
`bb|upper|${length}|${multiplier}|${source}`,
|
|
953
|
+
renderContext.data,
|
|
954
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "upper")
|
|
955
|
+
);
|
|
956
|
+
const lower = withCachedSeries(
|
|
957
|
+
`bb|lower|${length}|${multiplier}|${source}`,
|
|
958
|
+
renderContext.data,
|
|
959
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "lower")
|
|
960
|
+
);
|
|
961
|
+
const bandColor = inputs.bandColor ?? "#3b82f6";
|
|
962
|
+
const width = Number(inputs.width) || 1.5;
|
|
963
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.05);
|
|
964
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
|
|
965
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
|
|
966
|
+
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#f59e0b", width);
|
|
967
|
+
}
|
|
968
|
+
};
|
|
796
969
|
var BUILTIN_STDDEV_INDICATOR = {
|
|
797
970
|
id: "stddev",
|
|
798
971
|
name: "StdDev",
|
|
@@ -876,6 +1049,8 @@ var BUILTIN_INDICATORS = [
|
|
|
876
1049
|
BUILTIN_VWMA_INDICATOR,
|
|
877
1050
|
BUILTIN_RMA_INDICATOR,
|
|
878
1051
|
BUILTIN_HMA_INDICATOR,
|
|
1052
|
+
BUILTIN_VWAP_INDICATOR,
|
|
1053
|
+
BUILTIN_BOLLINGER_INDICATOR,
|
|
879
1054
|
BUILTIN_STDDEV_INDICATOR,
|
|
880
1055
|
BUILTIN_ATR_INDICATOR
|
|
881
1056
|
];
|
|
@@ -2728,15 +2903,31 @@ function createChart(element, options = {}) {
|
|
|
2728
2903
|
const pct = diff / base * 100;
|
|
2729
2904
|
const ticks = tick > 0 ? Math.round(diff / tick) : 0;
|
|
2730
2905
|
const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
|
|
2731
|
-
const
|
|
2906
|
+
const labelLines = [
|
|
2907
|
+
tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`
|
|
2908
|
+
];
|
|
2909
|
+
const barSpan = Math.abs(Math.round(p1.index) - Math.round(p0.index));
|
|
2910
|
+
if (barSpan > 0) {
|
|
2911
|
+
const t0 = getTimeForIndex(Math.round(p0.index));
|
|
2912
|
+
const t1 = getTimeForIndex(Math.round(p1.index));
|
|
2913
|
+
const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
|
|
2914
|
+
labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatDuration(spanMs)}` : `${barSpan} bars`);
|
|
2915
|
+
}
|
|
2916
|
+
const pointValue = Number(drawing.pointValue);
|
|
2917
|
+
if (Number.isFinite(pointValue) && pointValue > 0) {
|
|
2918
|
+
const dollars = diff * pointValue;
|
|
2919
|
+
const money = Math.abs(dollars).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
2920
|
+
labelLines.push(`${signed(dollars, `$${money}`)} / contract`);
|
|
2921
|
+
}
|
|
2732
2922
|
const prevFont = ctx.font;
|
|
2733
2923
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2734
2924
|
const padding = 6;
|
|
2735
|
-
const
|
|
2925
|
+
const lineHeight = 15;
|
|
2926
|
+
const textW = Math.max(...labelLines.map((line) => ctx.measureText(line).width));
|
|
2736
2927
|
const pillW = textW + padding * 2;
|
|
2737
|
-
const pillH =
|
|
2928
|
+
const pillH = labelLines.length * lineHeight + padding;
|
|
2738
2929
|
const pillX = midX - pillW / 2;
|
|
2739
|
-
const pillY = botY + 6;
|
|
2930
|
+
const pillY = botY + 6 + pillH <= fullChartBottom - 2 ? botY + 6 : Math.max(chartTop + 2, topY - 6 - pillH);
|
|
2740
2931
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2741
2932
|
fillRoundedRect(pillX, pillY, pillW, pillH, 4);
|
|
2742
2933
|
ctx.save();
|
|
@@ -2747,7 +2938,9 @@ function createChart(element, options = {}) {
|
|
|
2747
2938
|
ctx.fillStyle = drawing.color;
|
|
2748
2939
|
ctx.textAlign = "center";
|
|
2749
2940
|
ctx.textBaseline = "middle";
|
|
2750
|
-
|
|
2941
|
+
labelLines.forEach((line, lineIndex) => {
|
|
2942
|
+
ctx.fillText(line, midX, pillY + padding / 2 + lineIndex * lineHeight + lineHeight / 2);
|
|
2943
|
+
});
|
|
2751
2944
|
ctx.font = prevFont;
|
|
2752
2945
|
if (drawing.label) {
|
|
2753
2946
|
drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
|
|
@@ -2891,6 +3084,32 @@ function createChart(element, options = {}) {
|
|
|
2891
3084
|
}
|
|
2892
3085
|
ctx.restore();
|
|
2893
3086
|
}
|
|
3087
|
+
if (grid.sessionSeparators && data.length > 1) {
|
|
3088
|
+
const first = Math.max(1, startIndex);
|
|
3089
|
+
const barDeltaMs = data.length > 1 ? Math.abs(data[Math.min(first, data.length - 1)].time.getTime() - data[Math.min(first, data.length - 1) - 1].time.getTime()) : 0;
|
|
3090
|
+
if (barDeltaMs > 0 && barDeltaMs < 0.9 * 864e5) {
|
|
3091
|
+
const SESSION_ANCHOR_MS = 22 * 36e5;
|
|
3092
|
+
const sessionDayOf = (ms) => Math.floor((ms - SESSION_ANCHOR_MS) / 864e5);
|
|
3093
|
+
ctx.save();
|
|
3094
|
+
ctx.globalAlpha = clamp(grid.sessionSeparatorOpacity, 0, 1);
|
|
3095
|
+
ctx.strokeStyle = grid.sessionSeparatorColor;
|
|
3096
|
+
ctx.lineWidth = 1;
|
|
3097
|
+
for (let index = first; index <= endIndex; index += 1) {
|
|
3098
|
+
const point = data[index];
|
|
3099
|
+
const prev = data[index - 1];
|
|
3100
|
+
if (!point || !prev) continue;
|
|
3101
|
+
if (sessionDayOf(point.time.getTime()) === sessionDayOf(prev.time.getTime())) {
|
|
3102
|
+
continue;
|
|
3103
|
+
}
|
|
3104
|
+
const x = chartLeft + (index - xStart) / xSpan * chartWidth;
|
|
3105
|
+
ctx.beginPath();
|
|
3106
|
+
ctx.moveTo(crisp(x), crisp(chartTop));
|
|
3107
|
+
ctx.lineTo(crisp(x), crisp(fullChartBottom));
|
|
3108
|
+
ctx.stroke();
|
|
3109
|
+
}
|
|
3110
|
+
ctx.restore();
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
2894
3113
|
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2895
3114
|
const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
|
|
2896
3115
|
const lastDataIndex = data.length - 1;
|
|
@@ -3497,8 +3716,9 @@ function createChart(element, options = {}) {
|
|
|
3497
3716
|
}
|
|
3498
3717
|
return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
|
|
3499
3718
|
};
|
|
3719
|
+
const LEGEND_EXCLUDED_INPUT_KEYS = /* @__PURE__ */ new Set(["width", "bandMultiplier", "bandFillOpacity", "fillOpacity"]);
|
|
3500
3720
|
const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
|
|
3501
|
-
const inputValues = Object.entries(indicator.inputs).filter(([, value]) => isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
|
|
3721
|
+
const inputValues = Object.entries(indicator.inputs).filter(([key, value]) => !LEGEND_EXCLUDED_INPUT_KEYS.has(key) && typeof value !== "boolean" && isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
|
|
3502
3722
|
if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
|
|
3503
3723
|
return `${plugin.name} ${inputValues.join(" ")}`;
|
|
3504
3724
|
}
|
|
@@ -4476,7 +4696,9 @@ function createChart(element, options = {}) {
|
|
|
4476
4696
|
points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
|
|
4477
4697
|
color: defaults.color ?? "#2962ff",
|
|
4478
4698
|
style: defaults.style ?? "solid",
|
|
4479
|
-
width: defaults.width ?? 1
|
|
4699
|
+
width: defaults.width ?? 1,
|
|
4700
|
+
// pointValue enables the dollar-P&L line in the measure label.
|
|
4701
|
+
...defaults.pointValue === void 0 ? {} : { pointValue: defaults.pointValue }
|
|
4480
4702
|
})
|
|
4481
4703
|
);
|
|
4482
4704
|
emitDrawingsChange();
|
package/dist/index.cjs
CHANGED
|
@@ -43,7 +43,10 @@ var DEFAULT_GRID_OPTIONS = {
|
|
|
43
43
|
verticalLines: true,
|
|
44
44
|
xTickCount: 8,
|
|
45
45
|
yTickCount: 6,
|
|
46
|
-
horizontalTickCount: 6
|
|
46
|
+
horizontalTickCount: 6,
|
|
47
|
+
sessionSeparators: false,
|
|
48
|
+
sessionSeparatorColor: "#3b4150",
|
|
49
|
+
sessionSeparatorOpacity: 0.55
|
|
47
50
|
};
|
|
48
51
|
var DEFAULT_AXIS_OPTIONS = {
|
|
49
52
|
lineColor: "#3b3f47",
|
|
@@ -432,6 +435,60 @@ var computeStdDevSeries = (data, length, source) => {
|
|
|
432
435
|
}
|
|
433
436
|
return result;
|
|
434
437
|
};
|
|
438
|
+
var VWAP_SESSION_ANCHOR_MS = 22 * 36e5;
|
|
439
|
+
var vwapSessionDayOf = (ms) => Math.floor((ms - VWAP_SESSION_ANCHOR_MS) / 864e5);
|
|
440
|
+
var computeVwapSeries = (data, band) => {
|
|
441
|
+
const result = new Array(data.length).fill(null);
|
|
442
|
+
let session = Number.NaN;
|
|
443
|
+
let cumPv = 0;
|
|
444
|
+
let cumPv2 = 0;
|
|
445
|
+
let cumV = 0;
|
|
446
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
447
|
+
const point = data[i];
|
|
448
|
+
if (!point) continue;
|
|
449
|
+
const day = vwapSessionDayOf(point.time.getTime());
|
|
450
|
+
if (day !== session) {
|
|
451
|
+
session = day;
|
|
452
|
+
cumPv = 0;
|
|
453
|
+
cumPv2 = 0;
|
|
454
|
+
cumV = 0;
|
|
455
|
+
}
|
|
456
|
+
const typical = (point.h + point.l + point.c) / 3;
|
|
457
|
+
const volume = Math.max(0, point.v ?? 0);
|
|
458
|
+
cumPv += typical * volume;
|
|
459
|
+
cumPv2 += typical * typical * volume;
|
|
460
|
+
cumV += volume;
|
|
461
|
+
if (cumV <= 0) continue;
|
|
462
|
+
const vwap = cumPv / cumV;
|
|
463
|
+
if (band === "vwap") {
|
|
464
|
+
result[i] = vwap;
|
|
465
|
+
} else {
|
|
466
|
+
const variance = Math.max(0, cumPv2 / cumV - vwap * vwap);
|
|
467
|
+
result[i] = Math.sqrt(variance);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
for (let i = 1; i < data.length; i += 1) {
|
|
471
|
+
const point = data[i];
|
|
472
|
+
const prev = data[i - 1];
|
|
473
|
+
const next = data[i + 1];
|
|
474
|
+
if (!point || !prev || !next) continue;
|
|
475
|
+
const day = vwapSessionDayOf(point.time.getTime());
|
|
476
|
+
if (day !== vwapSessionDayOf(prev.time.getTime()) && day === vwapSessionDayOf(next.time.getTime())) {
|
|
477
|
+
result[i] = null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
};
|
|
482
|
+
var computeBollingerSeries = (data, length, multiplier, source, band) => {
|
|
483
|
+
const basis = computeSmaSeries(data, length, source);
|
|
484
|
+
if (band === "basis") return basis;
|
|
485
|
+
const dev = computeStdDevSeries(data, length, source);
|
|
486
|
+
return basis.map((value, idx) => {
|
|
487
|
+
const sigma = dev[idx];
|
|
488
|
+
if (value == null || sigma == null) return null;
|
|
489
|
+
return band === "upper" ? value + multiplier * sigma : value - multiplier * sigma;
|
|
490
|
+
});
|
|
491
|
+
};
|
|
435
492
|
var computeRsiSeries = (data, length) => {
|
|
436
493
|
const result = new Array(data.length).fill(null);
|
|
437
494
|
if (data.length < 2) return result;
|
|
@@ -558,6 +615,39 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
|
558
615
|
}
|
|
559
616
|
ctx.restore();
|
|
560
617
|
};
|
|
618
|
+
var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
|
|
619
|
+
if (!renderContext.yFromPrice || opacity <= 0) return;
|
|
620
|
+
const yFromPrice = renderContext.yFromPrice;
|
|
621
|
+
ctx.save();
|
|
622
|
+
ctx.globalAlpha = Math.min(1, Math.max(0, opacity));
|
|
623
|
+
ctx.fillStyle = color;
|
|
624
|
+
let runStart = -1;
|
|
625
|
+
const flushRun = (endIndexExclusive) => {
|
|
626
|
+
if (runStart < 0) return;
|
|
627
|
+
ctx.beginPath();
|
|
628
|
+
for (let i = runStart; i < endIndexExclusive; i += 1) {
|
|
629
|
+
const x = renderContext.xFromIndex(i);
|
|
630
|
+
const y = yFromPrice(upper[i]);
|
|
631
|
+
if (i === runStart) ctx.moveTo(x, y);
|
|
632
|
+
else ctx.lineTo(x, y);
|
|
633
|
+
}
|
|
634
|
+
for (let i = endIndexExclusive - 1; i >= runStart; i -= 1) {
|
|
635
|
+
ctx.lineTo(renderContext.xFromIndex(i), yFromPrice(lower[i]));
|
|
636
|
+
}
|
|
637
|
+
ctx.closePath();
|
|
638
|
+
ctx.fill();
|
|
639
|
+
runStart = -1;
|
|
640
|
+
};
|
|
641
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
642
|
+
const up = upper[index];
|
|
643
|
+
const lo = lower[index];
|
|
644
|
+
const valid = Number.isFinite(up ?? Number.NaN) && Number.isFinite(lo ?? Number.NaN);
|
|
645
|
+
if (valid && runStart < 0) runStart = index;
|
|
646
|
+
if (!valid) flushRun(index);
|
|
647
|
+
}
|
|
648
|
+
flushRun(renderContext.endIndex + 1);
|
|
649
|
+
ctx.restore();
|
|
650
|
+
};
|
|
561
651
|
var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
|
|
562
652
|
const visible = [];
|
|
563
653
|
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
@@ -819,6 +909,89 @@ var BUILTIN_HMA_INDICATOR = {
|
|
|
819
909
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
|
|
820
910
|
}
|
|
821
911
|
};
|
|
912
|
+
var BUILTIN_VWAP_INDICATOR = {
|
|
913
|
+
id: "vwap",
|
|
914
|
+
name: "VWAP",
|
|
915
|
+
pane: "overlay",
|
|
916
|
+
defaultInputs: {
|
|
917
|
+
color: "#f59e0b",
|
|
918
|
+
width: 2,
|
|
919
|
+
showBands: false,
|
|
920
|
+
bandMultiplier: 1,
|
|
921
|
+
bandColor: "#94a3b8",
|
|
922
|
+
bandFillOpacity: 0.06
|
|
923
|
+
},
|
|
924
|
+
draw: (ctx, renderContext, inputs) => {
|
|
925
|
+
const vwap = withCachedSeries(
|
|
926
|
+
"vwap|line",
|
|
927
|
+
renderContext.data,
|
|
928
|
+
() => computeVwapSeries(renderContext.data, "vwap")
|
|
929
|
+
);
|
|
930
|
+
if (inputs.showBands) {
|
|
931
|
+
const sigma = withCachedSeries(
|
|
932
|
+
"vwap|sigma",
|
|
933
|
+
renderContext.data,
|
|
934
|
+
() => computeVwapSeries(renderContext.data, "sigma")
|
|
935
|
+
);
|
|
936
|
+
const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
|
|
937
|
+
const upper = vwap.map((value, idx) => {
|
|
938
|
+
const s = sigma[idx];
|
|
939
|
+
return value == null || s == null ? null : value + multiplier * s;
|
|
940
|
+
});
|
|
941
|
+
const lower = vwap.map((value, idx) => {
|
|
942
|
+
const s = sigma[idx];
|
|
943
|
+
return value == null || s == null ? null : value - multiplier * s;
|
|
944
|
+
});
|
|
945
|
+
const bandColor = inputs.bandColor ?? "#94a3b8";
|
|
946
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.bandFillOpacity) || 0.06);
|
|
947
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, 1);
|
|
948
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
|
|
949
|
+
}
|
|
950
|
+
drawOverlaySeries(ctx, renderContext, vwap, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
var BUILTIN_BOLLINGER_INDICATOR = {
|
|
954
|
+
id: "bollinger",
|
|
955
|
+
name: "BB",
|
|
956
|
+
pane: "overlay",
|
|
957
|
+
// Basis is pink so it stays distinguishable from the (orange) VWAP line
|
|
958
|
+
// when both overlays are active.
|
|
959
|
+
defaultInputs: {
|
|
960
|
+
length: 20,
|
|
961
|
+
multiplier: 2,
|
|
962
|
+
source: "close",
|
|
963
|
+
basisColor: "#ec4899",
|
|
964
|
+
bandColor: "#3b82f6",
|
|
965
|
+
width: 1.5,
|
|
966
|
+
fillOpacity: 0.05
|
|
967
|
+
},
|
|
968
|
+
draw: (ctx, renderContext, inputs) => {
|
|
969
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
970
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
|
|
971
|
+
const source = inputs.source ?? "close";
|
|
972
|
+
const basis = withCachedSeries(
|
|
973
|
+
`bb|basis|${length}|${source}`,
|
|
974
|
+
renderContext.data,
|
|
975
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "basis")
|
|
976
|
+
);
|
|
977
|
+
const upper = withCachedSeries(
|
|
978
|
+
`bb|upper|${length}|${multiplier}|${source}`,
|
|
979
|
+
renderContext.data,
|
|
980
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "upper")
|
|
981
|
+
);
|
|
982
|
+
const lower = withCachedSeries(
|
|
983
|
+
`bb|lower|${length}|${multiplier}|${source}`,
|
|
984
|
+
renderContext.data,
|
|
985
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "lower")
|
|
986
|
+
);
|
|
987
|
+
const bandColor = inputs.bandColor ?? "#3b82f6";
|
|
988
|
+
const width = Number(inputs.width) || 1.5;
|
|
989
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.05);
|
|
990
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
|
|
991
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
|
|
992
|
+
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#f59e0b", width);
|
|
993
|
+
}
|
|
994
|
+
};
|
|
822
995
|
var BUILTIN_STDDEV_INDICATOR = {
|
|
823
996
|
id: "stddev",
|
|
824
997
|
name: "StdDev",
|
|
@@ -902,6 +1075,8 @@ var BUILTIN_INDICATORS = [
|
|
|
902
1075
|
BUILTIN_VWMA_INDICATOR,
|
|
903
1076
|
BUILTIN_RMA_INDICATOR,
|
|
904
1077
|
BUILTIN_HMA_INDICATOR,
|
|
1078
|
+
BUILTIN_VWAP_INDICATOR,
|
|
1079
|
+
BUILTIN_BOLLINGER_INDICATOR,
|
|
905
1080
|
BUILTIN_STDDEV_INDICATOR,
|
|
906
1081
|
BUILTIN_ATR_INDICATOR
|
|
907
1082
|
];
|
|
@@ -2754,15 +2929,31 @@ function createChart(element, options = {}) {
|
|
|
2754
2929
|
const pct = diff / base * 100;
|
|
2755
2930
|
const ticks = tick > 0 ? Math.round(diff / tick) : 0;
|
|
2756
2931
|
const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
|
|
2757
|
-
const
|
|
2932
|
+
const labelLines = [
|
|
2933
|
+
tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`
|
|
2934
|
+
];
|
|
2935
|
+
const barSpan = Math.abs(Math.round(p1.index) - Math.round(p0.index));
|
|
2936
|
+
if (barSpan > 0) {
|
|
2937
|
+
const t0 = getTimeForIndex(Math.round(p0.index));
|
|
2938
|
+
const t1 = getTimeForIndex(Math.round(p1.index));
|
|
2939
|
+
const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
|
|
2940
|
+
labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatDuration(spanMs)}` : `${barSpan} bars`);
|
|
2941
|
+
}
|
|
2942
|
+
const pointValue = Number(drawing.pointValue);
|
|
2943
|
+
if (Number.isFinite(pointValue) && pointValue > 0) {
|
|
2944
|
+
const dollars = diff * pointValue;
|
|
2945
|
+
const money = Math.abs(dollars).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
2946
|
+
labelLines.push(`${signed(dollars, `$${money}`)} / contract`);
|
|
2947
|
+
}
|
|
2758
2948
|
const prevFont = ctx.font;
|
|
2759
2949
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2760
2950
|
const padding = 6;
|
|
2761
|
-
const
|
|
2951
|
+
const lineHeight = 15;
|
|
2952
|
+
const textW = Math.max(...labelLines.map((line) => ctx.measureText(line).width));
|
|
2762
2953
|
const pillW = textW + padding * 2;
|
|
2763
|
-
const pillH =
|
|
2954
|
+
const pillH = labelLines.length * lineHeight + padding;
|
|
2764
2955
|
const pillX = midX - pillW / 2;
|
|
2765
|
-
const pillY = botY + 6;
|
|
2956
|
+
const pillY = botY + 6 + pillH <= fullChartBottom - 2 ? botY + 6 : Math.max(chartTop + 2, topY - 6 - pillH);
|
|
2766
2957
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2767
2958
|
fillRoundedRect(pillX, pillY, pillW, pillH, 4);
|
|
2768
2959
|
ctx.save();
|
|
@@ -2773,7 +2964,9 @@ function createChart(element, options = {}) {
|
|
|
2773
2964
|
ctx.fillStyle = drawing.color;
|
|
2774
2965
|
ctx.textAlign = "center";
|
|
2775
2966
|
ctx.textBaseline = "middle";
|
|
2776
|
-
|
|
2967
|
+
labelLines.forEach((line, lineIndex) => {
|
|
2968
|
+
ctx.fillText(line, midX, pillY + padding / 2 + lineIndex * lineHeight + lineHeight / 2);
|
|
2969
|
+
});
|
|
2777
2970
|
ctx.font = prevFont;
|
|
2778
2971
|
if (drawing.label) {
|
|
2779
2972
|
drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
|
|
@@ -2917,6 +3110,32 @@ function createChart(element, options = {}) {
|
|
|
2917
3110
|
}
|
|
2918
3111
|
ctx.restore();
|
|
2919
3112
|
}
|
|
3113
|
+
if (grid.sessionSeparators && data.length > 1) {
|
|
3114
|
+
const first = Math.max(1, startIndex);
|
|
3115
|
+
const barDeltaMs = data.length > 1 ? Math.abs(data[Math.min(first, data.length - 1)].time.getTime() - data[Math.min(first, data.length - 1) - 1].time.getTime()) : 0;
|
|
3116
|
+
if (barDeltaMs > 0 && barDeltaMs < 0.9 * 864e5) {
|
|
3117
|
+
const SESSION_ANCHOR_MS = 22 * 36e5;
|
|
3118
|
+
const sessionDayOf = (ms) => Math.floor((ms - SESSION_ANCHOR_MS) / 864e5);
|
|
3119
|
+
ctx.save();
|
|
3120
|
+
ctx.globalAlpha = clamp(grid.sessionSeparatorOpacity, 0, 1);
|
|
3121
|
+
ctx.strokeStyle = grid.sessionSeparatorColor;
|
|
3122
|
+
ctx.lineWidth = 1;
|
|
3123
|
+
for (let index = first; index <= endIndex; index += 1) {
|
|
3124
|
+
const point = data[index];
|
|
3125
|
+
const prev = data[index - 1];
|
|
3126
|
+
if (!point || !prev) continue;
|
|
3127
|
+
if (sessionDayOf(point.time.getTime()) === sessionDayOf(prev.time.getTime())) {
|
|
3128
|
+
continue;
|
|
3129
|
+
}
|
|
3130
|
+
const x = chartLeft + (index - xStart) / xSpan * chartWidth;
|
|
3131
|
+
ctx.beginPath();
|
|
3132
|
+
ctx.moveTo(crisp(x), crisp(chartTop));
|
|
3133
|
+
ctx.lineTo(crisp(x), crisp(fullChartBottom));
|
|
3134
|
+
ctx.stroke();
|
|
3135
|
+
}
|
|
3136
|
+
ctx.restore();
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
2920
3139
|
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2921
3140
|
const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
|
|
2922
3141
|
const lastDataIndex = data.length - 1;
|
|
@@ -3523,8 +3742,9 @@ function createChart(element, options = {}) {
|
|
|
3523
3742
|
}
|
|
3524
3743
|
return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
|
|
3525
3744
|
};
|
|
3745
|
+
const LEGEND_EXCLUDED_INPUT_KEYS = /* @__PURE__ */ new Set(["width", "bandMultiplier", "bandFillOpacity", "fillOpacity"]);
|
|
3526
3746
|
const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
|
|
3527
|
-
const inputValues = Object.entries(indicator.inputs).filter(([, value]) => isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
|
|
3747
|
+
const inputValues = Object.entries(indicator.inputs).filter(([key, value]) => !LEGEND_EXCLUDED_INPUT_KEYS.has(key) && typeof value !== "boolean" && isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
|
|
3528
3748
|
if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
|
|
3529
3749
|
return `${plugin.name} ${inputValues.join(" ")}`;
|
|
3530
3750
|
}
|
|
@@ -4502,7 +4722,9 @@ function createChart(element, options = {}) {
|
|
|
4502
4722
|
points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
|
|
4503
4723
|
color: defaults.color ?? "#2962ff",
|
|
4504
4724
|
style: defaults.style ?? "solid",
|
|
4505
|
-
width: defaults.width ?? 1
|
|
4725
|
+
width: defaults.width ?? 1,
|
|
4726
|
+
// pointValue enables the dollar-P&L line in the measure label.
|
|
4727
|
+
...defaults.pointValue === void 0 ? {} : { pointValue: defaults.pointValue }
|
|
4506
4728
|
})
|
|
4507
4729
|
);
|
|
4508
4730
|
emitDrawingsChange();
|
package/dist/index.d.cts
CHANGED
|
@@ -215,6 +215,14 @@ interface GridOptions {
|
|
|
215
215
|
xTickCount?: number;
|
|
216
216
|
yTickCount?: number;
|
|
217
217
|
horizontalTickCount?: number;
|
|
218
|
+
/**
|
|
219
|
+
* Vertical separator at every trading-day boundary (a bar whose UTC
|
|
220
|
+
* calendar day differs from the previous bar's). Drawn stronger than the
|
|
221
|
+
* regular grid so intraday charts stay oriented while scrolling history.
|
|
222
|
+
*/
|
|
223
|
+
sessionSeparators?: boolean;
|
|
224
|
+
sessionSeparatorColor?: string;
|
|
225
|
+
sessionSeparatorOpacity?: number;
|
|
218
226
|
}
|
|
219
227
|
interface CrosshairOptions {
|
|
220
228
|
visible?: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -215,6 +215,14 @@ interface GridOptions {
|
|
|
215
215
|
xTickCount?: number;
|
|
216
216
|
yTickCount?: number;
|
|
217
217
|
horizontalTickCount?: number;
|
|
218
|
+
/**
|
|
219
|
+
* Vertical separator at every trading-day boundary (a bar whose UTC
|
|
220
|
+
* calendar day differs from the previous bar's). Drawn stronger than the
|
|
221
|
+
* regular grid so intraday charts stay oriented while scrolling history.
|
|
222
|
+
*/
|
|
223
|
+
sessionSeparators?: boolean;
|
|
224
|
+
sessionSeparatorColor?: string;
|
|
225
|
+
sessionSeparatorOpacity?: number;
|
|
218
226
|
}
|
|
219
227
|
interface CrosshairOptions {
|
|
220
228
|
visible?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,10 @@ var DEFAULT_GRID_OPTIONS = {
|
|
|
17
17
|
verticalLines: true,
|
|
18
18
|
xTickCount: 8,
|
|
19
19
|
yTickCount: 6,
|
|
20
|
-
horizontalTickCount: 6
|
|
20
|
+
horizontalTickCount: 6,
|
|
21
|
+
sessionSeparators: false,
|
|
22
|
+
sessionSeparatorColor: "#3b4150",
|
|
23
|
+
sessionSeparatorOpacity: 0.55
|
|
21
24
|
};
|
|
22
25
|
var DEFAULT_AXIS_OPTIONS = {
|
|
23
26
|
lineColor: "#3b3f47",
|
|
@@ -406,6 +409,60 @@ var computeStdDevSeries = (data, length, source) => {
|
|
|
406
409
|
}
|
|
407
410
|
return result;
|
|
408
411
|
};
|
|
412
|
+
var VWAP_SESSION_ANCHOR_MS = 22 * 36e5;
|
|
413
|
+
var vwapSessionDayOf = (ms) => Math.floor((ms - VWAP_SESSION_ANCHOR_MS) / 864e5);
|
|
414
|
+
var computeVwapSeries = (data, band) => {
|
|
415
|
+
const result = new Array(data.length).fill(null);
|
|
416
|
+
let session = Number.NaN;
|
|
417
|
+
let cumPv = 0;
|
|
418
|
+
let cumPv2 = 0;
|
|
419
|
+
let cumV = 0;
|
|
420
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
421
|
+
const point = data[i];
|
|
422
|
+
if (!point) continue;
|
|
423
|
+
const day = vwapSessionDayOf(point.time.getTime());
|
|
424
|
+
if (day !== session) {
|
|
425
|
+
session = day;
|
|
426
|
+
cumPv = 0;
|
|
427
|
+
cumPv2 = 0;
|
|
428
|
+
cumV = 0;
|
|
429
|
+
}
|
|
430
|
+
const typical = (point.h + point.l + point.c) / 3;
|
|
431
|
+
const volume = Math.max(0, point.v ?? 0);
|
|
432
|
+
cumPv += typical * volume;
|
|
433
|
+
cumPv2 += typical * typical * volume;
|
|
434
|
+
cumV += volume;
|
|
435
|
+
if (cumV <= 0) continue;
|
|
436
|
+
const vwap = cumPv / cumV;
|
|
437
|
+
if (band === "vwap") {
|
|
438
|
+
result[i] = vwap;
|
|
439
|
+
} else {
|
|
440
|
+
const variance = Math.max(0, cumPv2 / cumV - vwap * vwap);
|
|
441
|
+
result[i] = Math.sqrt(variance);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (let i = 1; i < data.length; i += 1) {
|
|
445
|
+
const point = data[i];
|
|
446
|
+
const prev = data[i - 1];
|
|
447
|
+
const next = data[i + 1];
|
|
448
|
+
if (!point || !prev || !next) continue;
|
|
449
|
+
const day = vwapSessionDayOf(point.time.getTime());
|
|
450
|
+
if (day !== vwapSessionDayOf(prev.time.getTime()) && day === vwapSessionDayOf(next.time.getTime())) {
|
|
451
|
+
result[i] = null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return result;
|
|
455
|
+
};
|
|
456
|
+
var computeBollingerSeries = (data, length, multiplier, source, band) => {
|
|
457
|
+
const basis = computeSmaSeries(data, length, source);
|
|
458
|
+
if (band === "basis") return basis;
|
|
459
|
+
const dev = computeStdDevSeries(data, length, source);
|
|
460
|
+
return basis.map((value, idx) => {
|
|
461
|
+
const sigma = dev[idx];
|
|
462
|
+
if (value == null || sigma == null) return null;
|
|
463
|
+
return band === "upper" ? value + multiplier * sigma : value - multiplier * sigma;
|
|
464
|
+
});
|
|
465
|
+
};
|
|
409
466
|
var computeRsiSeries = (data, length) => {
|
|
410
467
|
const result = new Array(data.length).fill(null);
|
|
411
468
|
if (data.length < 2) return result;
|
|
@@ -532,6 +589,39 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
|
532
589
|
}
|
|
533
590
|
ctx.restore();
|
|
534
591
|
};
|
|
592
|
+
var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
|
|
593
|
+
if (!renderContext.yFromPrice || opacity <= 0) return;
|
|
594
|
+
const yFromPrice = renderContext.yFromPrice;
|
|
595
|
+
ctx.save();
|
|
596
|
+
ctx.globalAlpha = Math.min(1, Math.max(0, opacity));
|
|
597
|
+
ctx.fillStyle = color;
|
|
598
|
+
let runStart = -1;
|
|
599
|
+
const flushRun = (endIndexExclusive) => {
|
|
600
|
+
if (runStart < 0) return;
|
|
601
|
+
ctx.beginPath();
|
|
602
|
+
for (let i = runStart; i < endIndexExclusive; i += 1) {
|
|
603
|
+
const x = renderContext.xFromIndex(i);
|
|
604
|
+
const y = yFromPrice(upper[i]);
|
|
605
|
+
if (i === runStart) ctx.moveTo(x, y);
|
|
606
|
+
else ctx.lineTo(x, y);
|
|
607
|
+
}
|
|
608
|
+
for (let i = endIndexExclusive - 1; i >= runStart; i -= 1) {
|
|
609
|
+
ctx.lineTo(renderContext.xFromIndex(i), yFromPrice(lower[i]));
|
|
610
|
+
}
|
|
611
|
+
ctx.closePath();
|
|
612
|
+
ctx.fill();
|
|
613
|
+
runStart = -1;
|
|
614
|
+
};
|
|
615
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
616
|
+
const up = upper[index];
|
|
617
|
+
const lo = lower[index];
|
|
618
|
+
const valid = Number.isFinite(up ?? Number.NaN) && Number.isFinite(lo ?? Number.NaN);
|
|
619
|
+
if (valid && runStart < 0) runStart = index;
|
|
620
|
+
if (!valid) flushRun(index);
|
|
621
|
+
}
|
|
622
|
+
flushRun(renderContext.endIndex + 1);
|
|
623
|
+
ctx.restore();
|
|
624
|
+
};
|
|
535
625
|
var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
|
|
536
626
|
const visible = [];
|
|
537
627
|
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
@@ -793,6 +883,89 @@ var BUILTIN_HMA_INDICATOR = {
|
|
|
793
883
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
|
|
794
884
|
}
|
|
795
885
|
};
|
|
886
|
+
var BUILTIN_VWAP_INDICATOR = {
|
|
887
|
+
id: "vwap",
|
|
888
|
+
name: "VWAP",
|
|
889
|
+
pane: "overlay",
|
|
890
|
+
defaultInputs: {
|
|
891
|
+
color: "#f59e0b",
|
|
892
|
+
width: 2,
|
|
893
|
+
showBands: false,
|
|
894
|
+
bandMultiplier: 1,
|
|
895
|
+
bandColor: "#94a3b8",
|
|
896
|
+
bandFillOpacity: 0.06
|
|
897
|
+
},
|
|
898
|
+
draw: (ctx, renderContext, inputs) => {
|
|
899
|
+
const vwap = withCachedSeries(
|
|
900
|
+
"vwap|line",
|
|
901
|
+
renderContext.data,
|
|
902
|
+
() => computeVwapSeries(renderContext.data, "vwap")
|
|
903
|
+
);
|
|
904
|
+
if (inputs.showBands) {
|
|
905
|
+
const sigma = withCachedSeries(
|
|
906
|
+
"vwap|sigma",
|
|
907
|
+
renderContext.data,
|
|
908
|
+
() => computeVwapSeries(renderContext.data, "sigma")
|
|
909
|
+
);
|
|
910
|
+
const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
|
|
911
|
+
const upper = vwap.map((value, idx) => {
|
|
912
|
+
const s = sigma[idx];
|
|
913
|
+
return value == null || s == null ? null : value + multiplier * s;
|
|
914
|
+
});
|
|
915
|
+
const lower = vwap.map((value, idx) => {
|
|
916
|
+
const s = sigma[idx];
|
|
917
|
+
return value == null || s == null ? null : value - multiplier * s;
|
|
918
|
+
});
|
|
919
|
+
const bandColor = inputs.bandColor ?? "#94a3b8";
|
|
920
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.bandFillOpacity) || 0.06);
|
|
921
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, 1);
|
|
922
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
|
|
923
|
+
}
|
|
924
|
+
drawOverlaySeries(ctx, renderContext, vwap, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
var BUILTIN_BOLLINGER_INDICATOR = {
|
|
928
|
+
id: "bollinger",
|
|
929
|
+
name: "BB",
|
|
930
|
+
pane: "overlay",
|
|
931
|
+
// Basis is pink so it stays distinguishable from the (orange) VWAP line
|
|
932
|
+
// when both overlays are active.
|
|
933
|
+
defaultInputs: {
|
|
934
|
+
length: 20,
|
|
935
|
+
multiplier: 2,
|
|
936
|
+
source: "close",
|
|
937
|
+
basisColor: "#ec4899",
|
|
938
|
+
bandColor: "#3b82f6",
|
|
939
|
+
width: 1.5,
|
|
940
|
+
fillOpacity: 0.05
|
|
941
|
+
},
|
|
942
|
+
draw: (ctx, renderContext, inputs) => {
|
|
943
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
944
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
|
|
945
|
+
const source = inputs.source ?? "close";
|
|
946
|
+
const basis = withCachedSeries(
|
|
947
|
+
`bb|basis|${length}|${source}`,
|
|
948
|
+
renderContext.data,
|
|
949
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "basis")
|
|
950
|
+
);
|
|
951
|
+
const upper = withCachedSeries(
|
|
952
|
+
`bb|upper|${length}|${multiplier}|${source}`,
|
|
953
|
+
renderContext.data,
|
|
954
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "upper")
|
|
955
|
+
);
|
|
956
|
+
const lower = withCachedSeries(
|
|
957
|
+
`bb|lower|${length}|${multiplier}|${source}`,
|
|
958
|
+
renderContext.data,
|
|
959
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "lower")
|
|
960
|
+
);
|
|
961
|
+
const bandColor = inputs.bandColor ?? "#3b82f6";
|
|
962
|
+
const width = Number(inputs.width) || 1.5;
|
|
963
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.05);
|
|
964
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
|
|
965
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
|
|
966
|
+
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#f59e0b", width);
|
|
967
|
+
}
|
|
968
|
+
};
|
|
796
969
|
var BUILTIN_STDDEV_INDICATOR = {
|
|
797
970
|
id: "stddev",
|
|
798
971
|
name: "StdDev",
|
|
@@ -876,6 +1049,8 @@ var BUILTIN_INDICATORS = [
|
|
|
876
1049
|
BUILTIN_VWMA_INDICATOR,
|
|
877
1050
|
BUILTIN_RMA_INDICATOR,
|
|
878
1051
|
BUILTIN_HMA_INDICATOR,
|
|
1052
|
+
BUILTIN_VWAP_INDICATOR,
|
|
1053
|
+
BUILTIN_BOLLINGER_INDICATOR,
|
|
879
1054
|
BUILTIN_STDDEV_INDICATOR,
|
|
880
1055
|
BUILTIN_ATR_INDICATOR
|
|
881
1056
|
];
|
|
@@ -2728,15 +2903,31 @@ function createChart(element, options = {}) {
|
|
|
2728
2903
|
const pct = diff / base * 100;
|
|
2729
2904
|
const ticks = tick > 0 ? Math.round(diff / tick) : 0;
|
|
2730
2905
|
const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
|
|
2731
|
-
const
|
|
2906
|
+
const labelLines = [
|
|
2907
|
+
tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`
|
|
2908
|
+
];
|
|
2909
|
+
const barSpan = Math.abs(Math.round(p1.index) - Math.round(p0.index));
|
|
2910
|
+
if (barSpan > 0) {
|
|
2911
|
+
const t0 = getTimeForIndex(Math.round(p0.index));
|
|
2912
|
+
const t1 = getTimeForIndex(Math.round(p1.index));
|
|
2913
|
+
const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
|
|
2914
|
+
labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatDuration(spanMs)}` : `${barSpan} bars`);
|
|
2915
|
+
}
|
|
2916
|
+
const pointValue = Number(drawing.pointValue);
|
|
2917
|
+
if (Number.isFinite(pointValue) && pointValue > 0) {
|
|
2918
|
+
const dollars = diff * pointValue;
|
|
2919
|
+
const money = Math.abs(dollars).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
2920
|
+
labelLines.push(`${signed(dollars, `$${money}`)} / contract`);
|
|
2921
|
+
}
|
|
2732
2922
|
const prevFont = ctx.font;
|
|
2733
2923
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2734
2924
|
const padding = 6;
|
|
2735
|
-
const
|
|
2925
|
+
const lineHeight = 15;
|
|
2926
|
+
const textW = Math.max(...labelLines.map((line) => ctx.measureText(line).width));
|
|
2736
2927
|
const pillW = textW + padding * 2;
|
|
2737
|
-
const pillH =
|
|
2928
|
+
const pillH = labelLines.length * lineHeight + padding;
|
|
2738
2929
|
const pillX = midX - pillW / 2;
|
|
2739
|
-
const pillY = botY + 6;
|
|
2930
|
+
const pillY = botY + 6 + pillH <= fullChartBottom - 2 ? botY + 6 : Math.max(chartTop + 2, topY - 6 - pillH);
|
|
2740
2931
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2741
2932
|
fillRoundedRect(pillX, pillY, pillW, pillH, 4);
|
|
2742
2933
|
ctx.save();
|
|
@@ -2747,7 +2938,9 @@ function createChart(element, options = {}) {
|
|
|
2747
2938
|
ctx.fillStyle = drawing.color;
|
|
2748
2939
|
ctx.textAlign = "center";
|
|
2749
2940
|
ctx.textBaseline = "middle";
|
|
2750
|
-
|
|
2941
|
+
labelLines.forEach((line, lineIndex) => {
|
|
2942
|
+
ctx.fillText(line, midX, pillY + padding / 2 + lineIndex * lineHeight + lineHeight / 2);
|
|
2943
|
+
});
|
|
2751
2944
|
ctx.font = prevFont;
|
|
2752
2945
|
if (drawing.label) {
|
|
2753
2946
|
drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
|
|
@@ -2891,6 +3084,32 @@ function createChart(element, options = {}) {
|
|
|
2891
3084
|
}
|
|
2892
3085
|
ctx.restore();
|
|
2893
3086
|
}
|
|
3087
|
+
if (grid.sessionSeparators && data.length > 1) {
|
|
3088
|
+
const first = Math.max(1, startIndex);
|
|
3089
|
+
const barDeltaMs = data.length > 1 ? Math.abs(data[Math.min(first, data.length - 1)].time.getTime() - data[Math.min(first, data.length - 1) - 1].time.getTime()) : 0;
|
|
3090
|
+
if (barDeltaMs > 0 && barDeltaMs < 0.9 * 864e5) {
|
|
3091
|
+
const SESSION_ANCHOR_MS = 22 * 36e5;
|
|
3092
|
+
const sessionDayOf = (ms) => Math.floor((ms - SESSION_ANCHOR_MS) / 864e5);
|
|
3093
|
+
ctx.save();
|
|
3094
|
+
ctx.globalAlpha = clamp(grid.sessionSeparatorOpacity, 0, 1);
|
|
3095
|
+
ctx.strokeStyle = grid.sessionSeparatorColor;
|
|
3096
|
+
ctx.lineWidth = 1;
|
|
3097
|
+
for (let index = first; index <= endIndex; index += 1) {
|
|
3098
|
+
const point = data[index];
|
|
3099
|
+
const prev = data[index - 1];
|
|
3100
|
+
if (!point || !prev) continue;
|
|
3101
|
+
if (sessionDayOf(point.time.getTime()) === sessionDayOf(prev.time.getTime())) {
|
|
3102
|
+
continue;
|
|
3103
|
+
}
|
|
3104
|
+
const x = chartLeft + (index - xStart) / xSpan * chartWidth;
|
|
3105
|
+
ctx.beginPath();
|
|
3106
|
+
ctx.moveTo(crisp(x), crisp(chartTop));
|
|
3107
|
+
ctx.lineTo(crisp(x), crisp(fullChartBottom));
|
|
3108
|
+
ctx.stroke();
|
|
3109
|
+
}
|
|
3110
|
+
ctx.restore();
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
2894
3113
|
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2895
3114
|
const useSmoothedCandle = tickerOpts.smoothing && smoothedTickerPrice !== null;
|
|
2896
3115
|
const lastDataIndex = data.length - 1;
|
|
@@ -3497,8 +3716,9 @@ function createChart(element, options = {}) {
|
|
|
3497
3716
|
}
|
|
3498
3717
|
return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
|
|
3499
3718
|
};
|
|
3719
|
+
const LEGEND_EXCLUDED_INPUT_KEYS = /* @__PURE__ */ new Set(["width", "bandMultiplier", "bandFillOpacity", "fillOpacity"]);
|
|
3500
3720
|
const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
|
|
3501
|
-
const inputValues = Object.entries(indicator.inputs).filter(([, value]) => isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
|
|
3721
|
+
const inputValues = Object.entries(indicator.inputs).filter(([key, value]) => !LEGEND_EXCLUDED_INPUT_KEYS.has(key) && typeof value !== "boolean" && isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
|
|
3502
3722
|
if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
|
|
3503
3723
|
return `${plugin.name} ${inputValues.join(" ")}`;
|
|
3504
3724
|
}
|
|
@@ -4476,7 +4696,9 @@ function createChart(element, options = {}) {
|
|
|
4476
4696
|
points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
|
|
4477
4697
|
color: defaults.color ?? "#2962ff",
|
|
4478
4698
|
style: defaults.style ?? "solid",
|
|
4479
|
-
width: defaults.width ?? 1
|
|
4699
|
+
width: defaults.width ?? 1,
|
|
4700
|
+
// pointValue enables the dollar-P&L line in the measure label.
|
|
4701
|
+
...defaults.pointValue === void 0 ? {} : { pointValue: defaults.pointValue }
|
|
4480
4702
|
})
|
|
4481
4703
|
);
|
|
4482
4704
|
emitDrawingsChange();
|