hyperprop-charting-library 0.1.145 → 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 +663 -3
- package/dist/hyperprop-charting-library.d.ts +29 -1
- package/dist/hyperprop-charting-library.js +663 -3
- package/dist/index.cjs +663 -3
- package/dist/index.d.cts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +663 -3
- package/docs/API.md +76 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2035,6 +2035,493 @@ var BUILTIN_DONCHIAN_INDICATOR = {
|
|
|
2035
2035
|
return rangeOfSeries([upper, lower], startIndex, endIndex, skipIndex);
|
|
2036
2036
|
}
|
|
2037
2037
|
};
|
|
2038
|
+
var computeVolumeProfile = (data, fromIndex, toIndex, rowCount, valueAreaPercent) => {
|
|
2039
|
+
const from = Math.max(0, Math.round(fromIndex));
|
|
2040
|
+
const to = Math.min(data.length - 1, Math.round(toIndex));
|
|
2041
|
+
if (to < from) return null;
|
|
2042
|
+
let low = Number.POSITIVE_INFINITY;
|
|
2043
|
+
let high = Number.NEGATIVE_INFINITY;
|
|
2044
|
+
for (let i = from; i <= to; i += 1) {
|
|
2045
|
+
const bar = data[i];
|
|
2046
|
+
if (!bar) continue;
|
|
2047
|
+
if (bar.l < low) low = bar.l;
|
|
2048
|
+
if (bar.h > high) high = bar.h;
|
|
2049
|
+
}
|
|
2050
|
+
if (!Number.isFinite(low) || !Number.isFinite(high)) return null;
|
|
2051
|
+
if (high - low < Number.EPSILON) {
|
|
2052
|
+
const pad = Math.max(Math.abs(high) * 1e-6, 1e-6);
|
|
2053
|
+
low -= pad;
|
|
2054
|
+
high += pad;
|
|
2055
|
+
}
|
|
2056
|
+
const rows = Math.max(4, Math.min(500, Math.floor(rowCount) || 24));
|
|
2057
|
+
const rowSize = (high - low) / rows;
|
|
2058
|
+
const buckets = Array.from({ length: rows }, (_, index) => ({
|
|
2059
|
+
low: low + index * rowSize,
|
|
2060
|
+
high: low + (index + 1) * rowSize,
|
|
2061
|
+
up: 0,
|
|
2062
|
+
down: 0,
|
|
2063
|
+
total: 0
|
|
2064
|
+
}));
|
|
2065
|
+
let totalVolume = 0;
|
|
2066
|
+
for (let i = from; i <= to; i += 1) {
|
|
2067
|
+
const bar = data[i];
|
|
2068
|
+
if (!bar) continue;
|
|
2069
|
+
const volume = Math.max(0, bar.v ?? 0);
|
|
2070
|
+
if (volume <= 0) continue;
|
|
2071
|
+
const firstRow = Math.max(0, Math.min(rows - 1, Math.floor((bar.l - low) / rowSize)));
|
|
2072
|
+
const lastRow = Math.max(0, Math.min(rows - 1, Math.floor((bar.h - low) / rowSize)));
|
|
2073
|
+
const share = volume / (lastRow - firstRow + 1);
|
|
2074
|
+
const up = bar.c >= bar.o;
|
|
2075
|
+
for (let row = firstRow; row <= lastRow; row += 1) {
|
|
2076
|
+
const bucket = buckets[row];
|
|
2077
|
+
if (up) bucket.up += share;
|
|
2078
|
+
else bucket.down += share;
|
|
2079
|
+
bucket.total += share;
|
|
2080
|
+
}
|
|
2081
|
+
totalVolume += volume;
|
|
2082
|
+
}
|
|
2083
|
+
if (totalVolume <= 0) return null;
|
|
2084
|
+
let pocIndex = 0;
|
|
2085
|
+
let maxRowVolume = 0;
|
|
2086
|
+
for (let row = 0; row < rows; row += 1) {
|
|
2087
|
+
const total = buckets[row].total;
|
|
2088
|
+
if (total > maxRowVolume) {
|
|
2089
|
+
maxRowVolume = total;
|
|
2090
|
+
pocIndex = row;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
const target = totalVolume * Math.min(0.99, Math.max(0.1, valueAreaPercent / 100));
|
|
2094
|
+
let valueAreaFrom = pocIndex;
|
|
2095
|
+
let valueAreaTo = pocIndex;
|
|
2096
|
+
let accumulated = buckets[pocIndex].total;
|
|
2097
|
+
while (accumulated < target && (valueAreaFrom > 0 || valueAreaTo < rows - 1)) {
|
|
2098
|
+
const above = valueAreaTo < rows - 1 ? buckets[valueAreaTo + 1].total : -1;
|
|
2099
|
+
const below = valueAreaFrom > 0 ? buckets[valueAreaFrom - 1].total : -1;
|
|
2100
|
+
if (above < 0 && below < 0) break;
|
|
2101
|
+
if (above >= below) {
|
|
2102
|
+
valueAreaTo += 1;
|
|
2103
|
+
accumulated += above;
|
|
2104
|
+
} else {
|
|
2105
|
+
valueAreaFrom -= 1;
|
|
2106
|
+
accumulated += below;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
return { rows: buckets, rowSize, low, high, maxRowVolume, totalVolume, pocIndex, valueAreaFrom, valueAreaTo };
|
|
2110
|
+
};
|
|
2111
|
+
var sessionSegments = (renderContext, fromIndex, toIndex) => {
|
|
2112
|
+
const session = renderContext.session;
|
|
2113
|
+
if (!session) return [{ from: fromIndex, to: toIndex }];
|
|
2114
|
+
const segments = [];
|
|
2115
|
+
let start = fromIndex;
|
|
2116
|
+
let day = session.dayAt(fromIndex);
|
|
2117
|
+
for (let index = fromIndex + 1; index <= toIndex; index += 1) {
|
|
2118
|
+
const current = session.dayAt(index);
|
|
2119
|
+
if (current !== day) {
|
|
2120
|
+
segments.push({ from: start, to: index - 1 });
|
|
2121
|
+
start = index;
|
|
2122
|
+
day = current;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
segments.push({ from: start, to: toIndex });
|
|
2126
|
+
return segments;
|
|
2127
|
+
};
|
|
2128
|
+
var withAlpha = (color, alpha) => {
|
|
2129
|
+
const clamped = Math.max(0, Math.min(1, alpha));
|
|
2130
|
+
if (color.startsWith("#") && (color.length === 7 || color.length === 4)) {
|
|
2131
|
+
const hex = color.length === 4 ? `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}` : color;
|
|
2132
|
+
const r = Number.parseInt(hex.slice(1, 3), 16);
|
|
2133
|
+
const g = Number.parseInt(hex.slice(3, 5), 16);
|
|
2134
|
+
const b = Number.parseInt(hex.slice(5, 7), 16);
|
|
2135
|
+
return `rgba(${r},${g},${b},${clamped})`;
|
|
2136
|
+
}
|
|
2137
|
+
if (color.startsWith("rgba(")) return color;
|
|
2138
|
+
if (color.startsWith("rgb(")) return color.replace("rgb(", "rgba(").replace(")", `,${clamped})`);
|
|
2139
|
+
return color;
|
|
2140
|
+
};
|
|
2141
|
+
var BUILTIN_VOLUME_PROFILE_INDICATOR = {
|
|
2142
|
+
id: "volume-profile",
|
|
2143
|
+
name: "Volume Profile",
|
|
2144
|
+
pane: "overlay",
|
|
2145
|
+
defaultInputs: {
|
|
2146
|
+
// "visible" profiles everything on screen and follows pan/zoom; "session"
|
|
2147
|
+
// draws one profile per trading day inside that day's bars.
|
|
2148
|
+
scope: "visible",
|
|
2149
|
+
rows: 40,
|
|
2150
|
+
widthPercent: 28,
|
|
2151
|
+
placement: "right",
|
|
2152
|
+
valueAreaPercent: 70,
|
|
2153
|
+
upColor: "#26a69a",
|
|
2154
|
+
downColor: "#ef5350",
|
|
2155
|
+
valueAreaOpacity: 0.55,
|
|
2156
|
+
outsideOpacity: 0.22,
|
|
2157
|
+
showPoc: true,
|
|
2158
|
+
pocColor: "#f7a600",
|
|
2159
|
+
showValueArea: true,
|
|
2160
|
+
valueAreaColor: "#7e8aa3",
|
|
2161
|
+
showLabels: true
|
|
2162
|
+
},
|
|
2163
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2164
|
+
const { yFromPrice, chartLeft, chartRight, chartTop, chartBottom, data } = renderContext;
|
|
2165
|
+
if (!yFromPrice || data.length === 0) return;
|
|
2166
|
+
const from = Math.max(0, renderContext.startIndex);
|
|
2167
|
+
const to = Math.min(data.length - 1, renderContext.endIndex);
|
|
2168
|
+
if (to < from) return;
|
|
2169
|
+
const rows = Math.max(4, Math.min(500, Math.floor(Number(inputs.rows)) || 40));
|
|
2170
|
+
const valueAreaPercent = Math.max(10, Math.min(99, Number(inputs.valueAreaPercent) || 70));
|
|
2171
|
+
const perSession = inputs.scope === "session" && renderContext.session !== void 0;
|
|
2172
|
+
const segments = perSession ? sessionSegments(renderContext, from, to) : [{ from, to }];
|
|
2173
|
+
const widthRatio = Math.max(0.02, Math.min(0.9, (Number(inputs.widthPercent) || 28) / 100));
|
|
2174
|
+
const placeLeft = inputs.placement === "left";
|
|
2175
|
+
ctx.save();
|
|
2176
|
+
ctx.beginPath();
|
|
2177
|
+
ctx.rect(chartLeft, chartTop, chartRight - chartLeft, chartBottom - chartTop);
|
|
2178
|
+
ctx.clip();
|
|
2179
|
+
for (const segment of segments) {
|
|
2180
|
+
const profile = withCachedComputation(
|
|
2181
|
+
`volume-profile|${perSession ? "s" : "v"}|${segment.from}|${segment.to}|${rows}|${valueAreaPercent}`,
|
|
2182
|
+
data,
|
|
2183
|
+
() => computeVolumeProfile(data, segment.from, segment.to, rows, valueAreaPercent)
|
|
2184
|
+
);
|
|
2185
|
+
if (!profile || profile.maxRowVolume <= 0) continue;
|
|
2186
|
+
const segmentLeft = renderContext.xFromIndex(segment.from) - renderContext.candleSpacing / 2;
|
|
2187
|
+
const segmentRight = renderContext.xFromIndex(segment.to) + renderContext.candleSpacing / 2;
|
|
2188
|
+
const available = perSession ? Math.max(0, Math.min(segmentRight, chartRight) - Math.max(segmentLeft, chartLeft)) : chartRight - chartLeft;
|
|
2189
|
+
const maxWidth = available * (perSession ? Math.min(1, widthRatio * 2.2) : widthRatio);
|
|
2190
|
+
if (maxWidth < (perSession ? 10 : 2)) continue;
|
|
2191
|
+
const anchorX = perSession ? Math.max(segmentLeft, chartLeft) : placeLeft ? chartLeft : chartRight;
|
|
2192
|
+
const direction = perSession ? 1 : placeLeft ? 1 : -1;
|
|
2193
|
+
for (let row = 0; row < profile.rows.length; row += 1) {
|
|
2194
|
+
const bucket = profile.rows[row];
|
|
2195
|
+
if (bucket.total <= 0) continue;
|
|
2196
|
+
const yTop = yFromPrice(bucket.high);
|
|
2197
|
+
const yBottom = yFromPrice(bucket.low);
|
|
2198
|
+
const height = Math.max(1, Math.abs(yBottom - yTop) - 1);
|
|
2199
|
+
const top = Math.min(yTop, yBottom);
|
|
2200
|
+
if (top > chartBottom || top + height < chartTop) continue;
|
|
2201
|
+
const inValueArea = row >= profile.valueAreaFrom && row <= profile.valueAreaTo;
|
|
2202
|
+
const alpha = inValueArea ? Math.max(0, Math.min(1, Number(inputs.valueAreaOpacity) || 0.55)) : Math.max(0, Math.min(1, Number(inputs.outsideOpacity) || 0.22));
|
|
2203
|
+
const rowWidth = bucket.total / profile.maxRowVolume * maxWidth;
|
|
2204
|
+
const upWidth = bucket.total > 0 ? rowWidth * (bucket.up / bucket.total) : 0;
|
|
2205
|
+
ctx.fillStyle = withAlpha(inputs.upColor ?? "#26a69a", alpha);
|
|
2206
|
+
ctx.fillRect(direction > 0 ? anchorX : anchorX - upWidth, top, upWidth, height);
|
|
2207
|
+
ctx.fillStyle = withAlpha(inputs.downColor ?? "#ef5350", alpha);
|
|
2208
|
+
const downWidth = rowWidth - upWidth;
|
|
2209
|
+
ctx.fillRect(
|
|
2210
|
+
direction > 0 ? anchorX + upWidth : anchorX - rowWidth,
|
|
2211
|
+
top,
|
|
2212
|
+
downWidth,
|
|
2213
|
+
height
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
const lineFrom = direction > 0 ? anchorX : anchorX - maxWidth;
|
|
2217
|
+
const lineTo = direction > 0 ? anchorX + maxWidth : anchorX;
|
|
2218
|
+
const drawLevel = (price, color, label, dashed) => {
|
|
2219
|
+
const y = yFromPrice(price);
|
|
2220
|
+
if (y < chartTop || y > chartBottom) return;
|
|
2221
|
+
ctx.save();
|
|
2222
|
+
ctx.strokeStyle = color;
|
|
2223
|
+
ctx.lineWidth = 1;
|
|
2224
|
+
ctx.setLineDash(dashed ? [4, 3] : []);
|
|
2225
|
+
ctx.beginPath();
|
|
2226
|
+
ctx.moveTo(lineFrom, Math.round(y) + 0.5);
|
|
2227
|
+
ctx.lineTo(lineTo, Math.round(y) + 0.5);
|
|
2228
|
+
ctx.stroke();
|
|
2229
|
+
ctx.restore();
|
|
2230
|
+
if (inputs.showLabels && !perSession) {
|
|
2231
|
+
ctx.save();
|
|
2232
|
+
ctx.fillStyle = color;
|
|
2233
|
+
ctx.font = "10px system-ui, -apple-system, Segoe UI, Roboto, sans-serif";
|
|
2234
|
+
ctx.textAlign = direction > 0 ? "left" : "right";
|
|
2235
|
+
ctx.textBaseline = "bottom";
|
|
2236
|
+
ctx.fillText(label, direction > 0 ? lineFrom + 3 : lineTo - 3, y - 1);
|
|
2237
|
+
ctx.restore();
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
const poc = profile.rows[profile.pocIndex];
|
|
2241
|
+
if (inputs.showValueArea) {
|
|
2242
|
+
const areaHigh = profile.rows[profile.valueAreaTo];
|
|
2243
|
+
const areaLow = profile.rows[profile.valueAreaFrom];
|
|
2244
|
+
const areaColor = inputs.valueAreaColor ?? "#7e8aa3";
|
|
2245
|
+
drawLevel(areaHigh.high, areaColor, "VAH", true);
|
|
2246
|
+
drawLevel(areaLow.low, areaColor, "VAL", true);
|
|
2247
|
+
}
|
|
2248
|
+
if (inputs.showPoc) {
|
|
2249
|
+
drawLevel((poc.low + poc.high) / 2, inputs.pocColor ?? "#f7a600", "POC", false);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
ctx.restore();
|
|
2253
|
+
}
|
|
2254
|
+
};
|
|
2255
|
+
var parseMinutes = (value, fallback) => {
|
|
2256
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2257
|
+
const match = /^(\d{1,2}):?(\d{2})$/.exec(String(value ?? "").trim());
|
|
2258
|
+
if (!match) return fallback;
|
|
2259
|
+
const hour = Number(match[1]);
|
|
2260
|
+
const minute = Number(match[2]);
|
|
2261
|
+
if (!Number.isFinite(hour) || !Number.isFinite(minute)) return fallback;
|
|
2262
|
+
return hour * 60 + minute;
|
|
2263
|
+
};
|
|
2264
|
+
var computeSessionLevels = (data, session, rthStart, rthEnd, orMinutes) => {
|
|
2265
|
+
const lastIndex = data.length - 1;
|
|
2266
|
+
if (lastIndex < 0) return [];
|
|
2267
|
+
const currentDay = session.dayAt(lastIndex);
|
|
2268
|
+
if (!Number.isFinite(currentDay)) return [];
|
|
2269
|
+
let currentFrom = lastIndex;
|
|
2270
|
+
while (currentFrom > 0 && session.dayAt(currentFrom - 1) === currentDay) currentFrom -= 1;
|
|
2271
|
+
const priorTo = currentFrom - 1;
|
|
2272
|
+
const result = [];
|
|
2273
|
+
if (priorTo >= 0) {
|
|
2274
|
+
const priorDay = session.dayAt(priorTo);
|
|
2275
|
+
let priorFrom = priorTo;
|
|
2276
|
+
while (priorFrom > 0 && session.dayAt(priorFrom - 1) === priorDay) priorFrom -= 1;
|
|
2277
|
+
let high = Number.NEGATIVE_INFINITY;
|
|
2278
|
+
let low = Number.POSITIVE_INFINITY;
|
|
2279
|
+
for (let i = priorFrom; i <= priorTo; i += 1) {
|
|
2280
|
+
const bar = data[i];
|
|
2281
|
+
if (!bar) continue;
|
|
2282
|
+
if (bar.h > high) high = bar.h;
|
|
2283
|
+
if (bar.l < low) low = bar.l;
|
|
2284
|
+
}
|
|
2285
|
+
const close = data[priorTo]?.c;
|
|
2286
|
+
if (Number.isFinite(high)) result.push({ price: high, label: "PDH", color: "prior", fromIndex: currentFrom });
|
|
2287
|
+
if (Number.isFinite(low)) result.push({ price: low, label: "PDL", color: "prior", fromIndex: currentFrom });
|
|
2288
|
+
if (close !== void 0) result.push({ price: close, label: "PDC", color: "prior", fromIndex: currentFrom });
|
|
2289
|
+
}
|
|
2290
|
+
let onHigh = Number.NEGATIVE_INFINITY;
|
|
2291
|
+
let onLow = Number.POSITIVE_INFINITY;
|
|
2292
|
+
let orHigh = Number.NEGATIVE_INFINITY;
|
|
2293
|
+
let orLow = Number.POSITIVE_INFINITY;
|
|
2294
|
+
let ibHigh = Number.NEGATIVE_INFINITY;
|
|
2295
|
+
let ibLow = Number.POSITIVE_INFINITY;
|
|
2296
|
+
let orFromIndex = lastIndex;
|
|
2297
|
+
let ibFromIndex = lastIndex;
|
|
2298
|
+
for (let i = currentFrom; i <= lastIndex; i += 1) {
|
|
2299
|
+
const bar = data[i];
|
|
2300
|
+
if (!bar) continue;
|
|
2301
|
+
const minutes = session.minutesOfDayAt(i);
|
|
2302
|
+
if (!Number.isFinite(minutes)) continue;
|
|
2303
|
+
if (minutes < rthStart || minutes >= rthEnd) {
|
|
2304
|
+
if (bar.h > onHigh) onHigh = bar.h;
|
|
2305
|
+
if (bar.l < onLow) onLow = bar.l;
|
|
2306
|
+
continue;
|
|
2307
|
+
}
|
|
2308
|
+
if (minutes < rthStart + orMinutes) {
|
|
2309
|
+
if (bar.h > orHigh) orHigh = bar.h;
|
|
2310
|
+
if (bar.l < orLow) orLow = bar.l;
|
|
2311
|
+
orFromIndex = Math.min(orFromIndex, i);
|
|
2312
|
+
}
|
|
2313
|
+
if (minutes < rthStart + 60) {
|
|
2314
|
+
if (bar.h > ibHigh) ibHigh = bar.h;
|
|
2315
|
+
if (bar.l < ibLow) ibLow = bar.l;
|
|
2316
|
+
ibFromIndex = Math.min(ibFromIndex, i);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
if (Number.isFinite(onHigh)) result.push({ price: onHigh, label: "ONH", color: "overnight", fromIndex: currentFrom });
|
|
2320
|
+
if (Number.isFinite(onLow)) result.push({ price: onLow, label: "ONL", color: "overnight", fromIndex: currentFrom });
|
|
2321
|
+
if (Number.isFinite(orHigh)) result.push({ price: orHigh, label: "ORH", color: "openingRange", fromIndex: orFromIndex });
|
|
2322
|
+
if (Number.isFinite(orLow)) result.push({ price: orLow, label: "ORL", color: "openingRange", fromIndex: orFromIndex });
|
|
2323
|
+
if (Number.isFinite(ibHigh)) result.push({ price: ibHigh, label: "IBH", color: "initialBalance", fromIndex: ibFromIndex });
|
|
2324
|
+
if (Number.isFinite(ibLow)) result.push({ price: ibLow, label: "IBL", color: "initialBalance", fromIndex: ibFromIndex });
|
|
2325
|
+
return result;
|
|
2326
|
+
};
|
|
2327
|
+
var BUILTIN_SESSION_LEVELS_INDICATOR = {
|
|
2328
|
+
id: "session-levels",
|
|
2329
|
+
name: "Session levels",
|
|
2330
|
+
pane: "overlay",
|
|
2331
|
+
defaultInputs: {
|
|
2332
|
+
showPriorDay: true,
|
|
2333
|
+
showOvernight: true,
|
|
2334
|
+
showOpeningRange: true,
|
|
2335
|
+
showInitialBalance: false,
|
|
2336
|
+
// Cash-session bounds in the session's own zone; everything before the
|
|
2337
|
+
// open on the same trading day counts as overnight.
|
|
2338
|
+
rthStart: "09:30",
|
|
2339
|
+
rthEnd: "16:00",
|
|
2340
|
+
openingRangeMinutes: 30,
|
|
2341
|
+
priorDayColor: "#8b93a7",
|
|
2342
|
+
overnightColor: "#7c5cff",
|
|
2343
|
+
openingRangeColor: "#26a69a",
|
|
2344
|
+
initialBalanceColor: "#f7a600",
|
|
2345
|
+
lineWidth: 1,
|
|
2346
|
+
showLabels: true
|
|
2347
|
+
},
|
|
2348
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2349
|
+
const { yFromPrice, chartLeft, chartRight, chartTop, chartBottom, data, session } = renderContext;
|
|
2350
|
+
if (!yFromPrice || !session || data.length === 0) return;
|
|
2351
|
+
const rthStart = parseMinutes(inputs.rthStart, 570);
|
|
2352
|
+
const rthEnd = parseMinutes(inputs.rthEnd, 960);
|
|
2353
|
+
const orMinutes = Math.max(1, Math.floor(Number(inputs.openingRangeMinutes) || 30));
|
|
2354
|
+
const levels = withCachedComputation(
|
|
2355
|
+
`session-levels|${rthStart}|${rthEnd}|${orMinutes}|${data.length}`,
|
|
2356
|
+
data,
|
|
2357
|
+
() => computeSessionLevels(data, session, rthStart, rthEnd, orMinutes)
|
|
2358
|
+
);
|
|
2359
|
+
const groupEnabled = {
|
|
2360
|
+
prior: inputs.showPriorDay !== false,
|
|
2361
|
+
overnight: inputs.showOvernight !== false,
|
|
2362
|
+
openingRange: inputs.showOpeningRange !== false,
|
|
2363
|
+
initialBalance: inputs.showInitialBalance === true
|
|
2364
|
+
};
|
|
2365
|
+
const groupColor = {
|
|
2366
|
+
prior: inputs.priorDayColor ?? "#8b93a7",
|
|
2367
|
+
overnight: inputs.overnightColor ?? "#7c5cff",
|
|
2368
|
+
openingRange: inputs.openingRangeColor ?? "#26a69a",
|
|
2369
|
+
initialBalance: inputs.initialBalanceColor ?? "#f7a600"
|
|
2370
|
+
};
|
|
2371
|
+
ctx.save();
|
|
2372
|
+
ctx.lineWidth = Math.max(1, Number(inputs.lineWidth) || 1);
|
|
2373
|
+
ctx.font = "10px system-ui, -apple-system, Segoe UI, Roboto, sans-serif";
|
|
2374
|
+
ctx.textAlign = "right";
|
|
2375
|
+
ctx.textBaseline = "bottom";
|
|
2376
|
+
for (const level of levels) {
|
|
2377
|
+
if (!groupEnabled[level.color]) continue;
|
|
2378
|
+
const y = yFromPrice(level.price);
|
|
2379
|
+
if (!Number.isFinite(y) || y < chartTop || y > chartBottom) continue;
|
|
2380
|
+
const startX = Math.max(chartLeft, renderContext.xFromIndex(level.fromIndex));
|
|
2381
|
+
const color = groupColor[level.color] ?? "#8b93a7";
|
|
2382
|
+
ctx.strokeStyle = color;
|
|
2383
|
+
ctx.setLineDash(level.color === "prior" ? [] : [5, 4]);
|
|
2384
|
+
ctx.beginPath();
|
|
2385
|
+
ctx.moveTo(startX, Math.round(y) + 0.5);
|
|
2386
|
+
ctx.lineTo(chartRight, Math.round(y) + 0.5);
|
|
2387
|
+
ctx.stroke();
|
|
2388
|
+
if (inputs.showLabels !== false) {
|
|
2389
|
+
ctx.fillStyle = color;
|
|
2390
|
+
ctx.fillText(level.label, chartRight - 4, y - 2);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
ctx.restore();
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
var BUILTIN_ANCHORED_VWAP_INDICATOR = {
|
|
2397
|
+
id: "anchored-vwap",
|
|
2398
|
+
name: "Anchored VWAP",
|
|
2399
|
+
pane: "overlay",
|
|
2400
|
+
defaultInputs: {
|
|
2401
|
+
anchorTime: "",
|
|
2402
|
+
color: "#22d3ee",
|
|
2403
|
+
width: 2,
|
|
2404
|
+
showBands: false,
|
|
2405
|
+
bandMultiplier: 1,
|
|
2406
|
+
bandColor: "#94a3b8",
|
|
2407
|
+
bandFillOpacity: 0.06,
|
|
2408
|
+
showAnchorMarker: true
|
|
2409
|
+
},
|
|
2410
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2411
|
+
const { data, yFromPrice } = renderContext;
|
|
2412
|
+
if (!yFromPrice || data.length === 0) return;
|
|
2413
|
+
const anchorIndex = resolveAnchorIndex(renderContext, inputs.anchorTime);
|
|
2414
|
+
if (anchorIndex === null) return;
|
|
2415
|
+
const series = withCachedComputation(
|
|
2416
|
+
`anchored-vwap|${anchorIndex}|${inputs.showBands ? "b" : "n"}`,
|
|
2417
|
+
data,
|
|
2418
|
+
() => computeAnchoredVwap(data, anchorIndex)
|
|
2419
|
+
);
|
|
2420
|
+
if (inputs.showBands) {
|
|
2421
|
+
const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
|
|
2422
|
+
const upper = series.vwap.map((value, idx) => {
|
|
2423
|
+
const sigma = series.sigma[idx];
|
|
2424
|
+
return value == null || sigma == null ? null : value + multiplier * sigma;
|
|
2425
|
+
});
|
|
2426
|
+
const lower = series.vwap.map((value, idx) => {
|
|
2427
|
+
const sigma = series.sigma[idx];
|
|
2428
|
+
return value == null || sigma == null ? null : value - multiplier * sigma;
|
|
2429
|
+
});
|
|
2430
|
+
const bandColor = inputs.bandColor ?? "#94a3b8";
|
|
2431
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.bandFillOpacity) || 0.06);
|
|
2432
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, 1);
|
|
2433
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
|
|
2434
|
+
}
|
|
2435
|
+
drawOverlaySeries(ctx, renderContext, series.vwap, inputs.color ?? "#22d3ee", Number(inputs.width) || 2);
|
|
2436
|
+
if (inputs.showAnchorMarker !== false) {
|
|
2437
|
+
const anchorBar = data[anchorIndex];
|
|
2438
|
+
const anchorValue = series.vwap[anchorIndex];
|
|
2439
|
+
if (anchorBar && anchorValue != null) {
|
|
2440
|
+
const x = renderContext.xFromIndex(anchorIndex);
|
|
2441
|
+
const y = yFromPrice(anchorValue);
|
|
2442
|
+
if (x >= renderContext.chartLeft - 8 && x <= renderContext.chartRight + 8) {
|
|
2443
|
+
ctx.save();
|
|
2444
|
+
ctx.fillStyle = inputs.color ?? "#22d3ee";
|
|
2445
|
+
ctx.beginPath();
|
|
2446
|
+
ctx.arc(x, y, 3.5, 0, Math.PI * 2);
|
|
2447
|
+
ctx.fill();
|
|
2448
|
+
ctx.restore();
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
},
|
|
2453
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
2454
|
+
const anchorIndex = resolveAnchorIndexFromData(data, inputs.anchorTime);
|
|
2455
|
+
if (anchorIndex === null) return null;
|
|
2456
|
+
const series = withCachedComputation(
|
|
2457
|
+
`anchored-vwap|${anchorIndex}|${inputs.showBands ? "b" : "n"}`,
|
|
2458
|
+
data,
|
|
2459
|
+
() => computeAnchoredVwap(data, anchorIndex)
|
|
2460
|
+
);
|
|
2461
|
+
if (!inputs.showBands) return rangeOfSeries([series.vwap], startIndex, endIndex, skipIndex);
|
|
2462
|
+
const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
|
|
2463
|
+
let min = Number.POSITIVE_INFINITY;
|
|
2464
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
2465
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
2466
|
+
if (idx === skipIndex) continue;
|
|
2467
|
+
const value = series.vwap[idx];
|
|
2468
|
+
const sigma = series.sigma[idx];
|
|
2469
|
+
if (value == null || sigma == null) continue;
|
|
2470
|
+
const lo = value - multiplier * sigma;
|
|
2471
|
+
const hi = value + multiplier * sigma;
|
|
2472
|
+
if (lo < min) min = lo;
|
|
2473
|
+
if (hi > max) max = hi;
|
|
2474
|
+
}
|
|
2475
|
+
return min <= max ? { min, max } : null;
|
|
2476
|
+
}
|
|
2477
|
+
};
|
|
2478
|
+
var resolveAnchorIndexFromData = (data, anchorTime) => {
|
|
2479
|
+
if (data.length === 0) return null;
|
|
2480
|
+
const raw = typeof anchorTime === "number" ? anchorTime : Date.parse(String(anchorTime ?? ""));
|
|
2481
|
+
if (!Number.isFinite(raw)) return null;
|
|
2482
|
+
let lo = 0;
|
|
2483
|
+
let hi = data.length - 1;
|
|
2484
|
+
if (raw <= data[0].time.getTime()) return 0;
|
|
2485
|
+
if (raw >= data[hi].time.getTime()) return hi;
|
|
2486
|
+
while (lo < hi) {
|
|
2487
|
+
const mid = lo + hi + 1 >> 1;
|
|
2488
|
+
if (data[mid].time.getTime() <= raw) lo = mid;
|
|
2489
|
+
else hi = mid - 1;
|
|
2490
|
+
}
|
|
2491
|
+
return lo;
|
|
2492
|
+
};
|
|
2493
|
+
var resolveAnchorIndex = (renderContext, anchorTime) => {
|
|
2494
|
+
const explicit = resolveAnchorIndexFromData(renderContext.data, anchorTime);
|
|
2495
|
+
if (explicit !== null) return explicit;
|
|
2496
|
+
const session = renderContext.session;
|
|
2497
|
+
const lastIndex = renderContext.data.length - 1;
|
|
2498
|
+
if (!session || lastIndex < 0) return lastIndex >= 0 ? 0 : null;
|
|
2499
|
+
const day = session.dayAt(lastIndex);
|
|
2500
|
+
let index = lastIndex;
|
|
2501
|
+
while (index > 0 && session.dayAt(index - 1) === day) index -= 1;
|
|
2502
|
+
return index;
|
|
2503
|
+
};
|
|
2504
|
+
var computeAnchoredVwap = (data, anchorIndex) => {
|
|
2505
|
+
const vwap = new Array(data.length).fill(null);
|
|
2506
|
+
const sigma = new Array(data.length).fill(null);
|
|
2507
|
+
let cumPv = 0;
|
|
2508
|
+
let cumPv2 = 0;
|
|
2509
|
+
let cumV = 0;
|
|
2510
|
+
for (let i = Math.max(0, anchorIndex); i < data.length; i += 1) {
|
|
2511
|
+
const bar = data[i];
|
|
2512
|
+
if (!bar) continue;
|
|
2513
|
+
const typical = (bar.h + bar.l + bar.c) / 3;
|
|
2514
|
+
const volume = Math.max(0, bar.v ?? 0) || 1;
|
|
2515
|
+
cumPv += typical * volume;
|
|
2516
|
+
cumPv2 += typical * typical * volume;
|
|
2517
|
+
cumV += volume;
|
|
2518
|
+
if (cumV <= 0) continue;
|
|
2519
|
+
const value = cumPv / cumV;
|
|
2520
|
+
vwap[i] = value;
|
|
2521
|
+
sigma[i] = Math.sqrt(Math.max(0, cumPv2 / cumV - value * value));
|
|
2522
|
+
}
|
|
2523
|
+
return { vwap, sigma };
|
|
2524
|
+
};
|
|
2038
2525
|
var BUILTIN_INDICATORS = [
|
|
2039
2526
|
BUILTIN_VOLUME_INDICATOR,
|
|
2040
2527
|
BUILTIN_SMA_INDICATOR,
|
|
@@ -2062,7 +2549,10 @@ var BUILTIN_INDICATORS = [
|
|
|
2062
2549
|
BUILTIN_SUPERTREND_INDICATOR,
|
|
2063
2550
|
BUILTIN_ICHIMOKU_INDICATOR,
|
|
2064
2551
|
BUILTIN_KELTNER_INDICATOR,
|
|
2065
|
-
BUILTIN_DONCHIAN_INDICATOR
|
|
2552
|
+
BUILTIN_DONCHIAN_INDICATOR,
|
|
2553
|
+
BUILTIN_VOLUME_PROFILE_INDICATOR,
|
|
2554
|
+
BUILTIN_SESSION_LEVELS_INDICATOR,
|
|
2555
|
+
BUILTIN_ANCHORED_VWAP_INDICATOR
|
|
2066
2556
|
];
|
|
2067
2557
|
|
|
2068
2558
|
// src/scripts.ts
|
|
@@ -3406,6 +3896,9 @@ function createChart(element, options = {}) {
|
|
|
3406
3896
|
pointValue: Number(drawing.pointValue) || 1,
|
|
3407
3897
|
qtyPrecision: Number.isFinite(drawing.qtyPrecision) ? Math.max(0, Math.floor(Number(drawing.qtyPrecision))) : 0,
|
|
3408
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)),
|
|
3409
3902
|
...drawing.label === void 0 ? {} : { label: drawing.label }
|
|
3410
3903
|
};
|
|
3411
3904
|
};
|
|
@@ -3439,6 +3932,9 @@ function createChart(element, options = {}) {
|
|
|
3439
3932
|
pointValue: drawing.pointValue,
|
|
3440
3933
|
qtyPrecision: drawing.qtyPrecision,
|
|
3441
3934
|
fontSize: drawing.fontSize,
|
|
3935
|
+
profileRows: drawing.profileRows,
|
|
3936
|
+
profileValueAreaPercent: drawing.profileValueAreaPercent,
|
|
3937
|
+
profileWidthRatio: drawing.profileWidthRatio,
|
|
3442
3938
|
...drawing.label === void 0 ? {} : { label: drawing.label }
|
|
3443
3939
|
});
|
|
3444
3940
|
let tradeMarkers = [];
|
|
@@ -3458,6 +3954,44 @@ function createChart(element, options = {}) {
|
|
|
3458
3954
|
let compiledSession = compileSession(sessionOptions.spec);
|
|
3459
3955
|
const isHour12 = () => timeFormat === "12h";
|
|
3460
3956
|
const formatClock = (time) => zoneClock.formatTime(time.getTime(), isHour12());
|
|
3957
|
+
let sessionInfoCache = [];
|
|
3958
|
+
let sessionInfoCacheKey = "";
|
|
3959
|
+
const sessionInfoForIndex = (index) => {
|
|
3960
|
+
if (!compiledSession) return null;
|
|
3961
|
+
const point = data[index];
|
|
3962
|
+
if (!point) return null;
|
|
3963
|
+
const key = `${data.length}|${data[0]?.time.getTime() ?? 0}|${data[data.length - 1]?.time.getTime() ?? 0}|${sessionOptions.spec === null ? "none" : JSON.stringify(sessionOptions.spec)}`;
|
|
3964
|
+
if (key !== sessionInfoCacheKey) {
|
|
3965
|
+
sessionInfoCacheKey = key;
|
|
3966
|
+
sessionInfoCache = new Array(data.length);
|
|
3967
|
+
}
|
|
3968
|
+
const cached = sessionInfoCache[index];
|
|
3969
|
+
if (cached) return cached;
|
|
3970
|
+
const ms = point.time.getTime();
|
|
3971
|
+
const info = sessionInfoAt(compiledSession, ms);
|
|
3972
|
+
const resolved = {
|
|
3973
|
+
day: info.sessionDay,
|
|
3974
|
+
inSession: info.inSession,
|
|
3975
|
+
minutes: compiledSession.clock.minutesOfDay(ms)
|
|
3976
|
+
};
|
|
3977
|
+
sessionInfoCache[index] = resolved;
|
|
3978
|
+
return resolved;
|
|
3979
|
+
};
|
|
3980
|
+
const buildIndicatorSessionContext = () => {
|
|
3981
|
+
if (!compiledSession) return void 0;
|
|
3982
|
+
const dayAt = (index) => sessionInfoForIndex(index)?.day ?? Number.NaN;
|
|
3983
|
+
return {
|
|
3984
|
+
dayAt,
|
|
3985
|
+
inSessionAt: (index) => sessionInfoForIndex(index)?.inSession ?? false,
|
|
3986
|
+
isSessionStart: (index) => {
|
|
3987
|
+
if (index <= 0) return index === 0;
|
|
3988
|
+
const current = dayAt(index);
|
|
3989
|
+
const previous = dayAt(index - 1);
|
|
3990
|
+
return Number.isFinite(current) && Number.isFinite(previous) && current !== previous;
|
|
3991
|
+
},
|
|
3992
|
+
minutesOfDayAt: (index) => sessionInfoForIndex(index)?.minutes ?? Number.NaN
|
|
3993
|
+
};
|
|
3994
|
+
};
|
|
3461
3995
|
let barMarks = [];
|
|
3462
3996
|
let timescaleMarks = [];
|
|
3463
3997
|
let markRegions = [];
|
|
@@ -6107,6 +6641,81 @@ function createChart(element, options = {}) {
|
|
|
6107
6641
|
handleAt(ax, ay, drawing.color);
|
|
6108
6642
|
handleAt(bx, by, drawing.color);
|
|
6109
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
|
+
}
|
|
6110
6719
|
} else if (drawing.type === "price-range") {
|
|
6111
6720
|
const p0 = drawing.points[0];
|
|
6112
6721
|
const p1 = drawing.points[1];
|
|
@@ -6864,6 +7473,7 @@ function createChart(element, options = {}) {
|
|
|
6864
7473
|
).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
|
|
6865
7474
|
if (activeOverlayIndicators.length > 0) {
|
|
6866
7475
|
const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
7476
|
+
const indicatorSession = buildIndicatorSessionContext();
|
|
6867
7477
|
activeOverlayIndicators.forEach(({ indicator, plugin }) => {
|
|
6868
7478
|
plugin.draw(
|
|
6869
7479
|
ctx,
|
|
@@ -6885,7 +7495,8 @@ function createChart(element, options = {}) {
|
|
|
6885
7495
|
getVolumeByIndex,
|
|
6886
7496
|
candleSpacing,
|
|
6887
7497
|
upColor: mergedOptions.upColor,
|
|
6888
|
-
downColor: mergedOptions.downColor
|
|
7498
|
+
downColor: mergedOptions.downColor,
|
|
7499
|
+
...indicatorSession ? { session: indicatorSession } : {}
|
|
6889
7500
|
},
|
|
6890
7501
|
indicator.inputs
|
|
6891
7502
|
);
|
|
@@ -7685,8 +8296,17 @@ function createChart(element, options = {}) {
|
|
|
7685
8296
|
return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
|
|
7686
8297
|
};
|
|
7687
8298
|
const LEGEND_EXCLUDED_INPUT_KEYS = /* @__PURE__ */ new Set(["width", "bandMultiplier", "bandFillOpacity", "fillOpacity"]);
|
|
8299
|
+
const formatLegendValue = (value) => {
|
|
8300
|
+
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
|
|
8301
|
+
const ms = Date.parse(value);
|
|
8302
|
+
if (Number.isFinite(ms)) {
|
|
8303
|
+
return getTimeStepMs() < 864e5 ? `${zoneClock.formatDayMonth(ms)} ${zoneClock.formatTime(ms, isHour12())}` : zoneClock.formatDayMonth(ms);
|
|
8304
|
+
}
|
|
8305
|
+
}
|
|
8306
|
+
return String(value);
|
|
8307
|
+
};
|
|
7688
8308
|
const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
|
|
7689
|
-
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]) =>
|
|
8309
|
+
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]) => formatLegendValue(value));
|
|
7690
8310
|
if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
|
|
7691
8311
|
return `${plugin.name} ${inputValues.join(" ")}`;
|
|
7692
8312
|
}
|
|
@@ -8868,6 +9488,19 @@ function createChart(element, options = {}) {
|
|
|
8868
9488
|
if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5 * hitTolerance) {
|
|
8869
9489
|
return { drawing, target: "line" };
|
|
8870
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
|
+
}
|
|
8871
9504
|
} else if (drawing.type === "price-range") {
|
|
8872
9505
|
const p0 = drawing.points[0];
|
|
8873
9506
|
const p1 = drawing.points[1];
|
|
@@ -9319,6 +9952,33 @@ function createChart(element, options = {}) {
|
|
|
9319
9952
|
scheduleDraw();
|
|
9320
9953
|
return true;
|
|
9321
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
|
+
}
|
|
9322
9982
|
if (activeDrawingTool === "price-range") {
|
|
9323
9983
|
const tick = getConfiguredTickSize();
|
|
9324
9984
|
const visibleRange = drawState.yMax - drawState.yMin;
|