hyperprop-charting-library 0.1.70 → 0.1.72
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 +295 -2
- package/dist/hyperprop-charting-library.d.ts +1 -1
- package/dist/hyperprop-charting-library.js +295 -2
- package/dist/index.cjs +295 -2
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +295 -2
- package/docs/API.md +6 -1
- package/package.json +1 -1
|
@@ -2088,6 +2088,7 @@ function createChart(element, options = {}) {
|
|
|
2088
2088
|
};
|
|
2089
2089
|
const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
|
|
2090
2090
|
const FIB_RATIOS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
|
|
2091
|
+
const FIB_EXT_RATIOS = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
|
|
2091
2092
|
const hexToRgba = (hex, alpha) => {
|
|
2092
2093
|
const cleaned = hex.replace("#", "");
|
|
2093
2094
|
if (cleaned.length !== 6) {
|
|
@@ -2288,6 +2289,165 @@ function createChart(element, options = {}) {
|
|
|
2288
2289
|
});
|
|
2289
2290
|
ctx.font = prevFont;
|
|
2290
2291
|
}
|
|
2292
|
+
} else if (drawing.type === "fib-extension") {
|
|
2293
|
+
const p0 = drawing.points[0];
|
|
2294
|
+
const p1 = drawing.points[1];
|
|
2295
|
+
const p2 = drawing.points[2];
|
|
2296
|
+
if (p0 && p1) {
|
|
2297
|
+
const x0 = xFromDrawingPoint(p0);
|
|
2298
|
+
const y0 = yFromPrice(p0.price);
|
|
2299
|
+
const x1 = xFromDrawingPoint(p1);
|
|
2300
|
+
const y1 = yFromPrice(p1.price);
|
|
2301
|
+
ctx.save();
|
|
2302
|
+
ctx.strokeStyle = hexToRgba(drawing.color, 0.6);
|
|
2303
|
+
ctx.setLineDash([4, 4]);
|
|
2304
|
+
ctx.lineWidth = 1;
|
|
2305
|
+
ctx.beginPath();
|
|
2306
|
+
ctx.moveTo(x0, y0);
|
|
2307
|
+
ctx.lineTo(x1, y1);
|
|
2308
|
+
if (p2) {
|
|
2309
|
+
ctx.lineTo(xFromDrawingPoint(p2), yFromPrice(p2.price));
|
|
2310
|
+
}
|
|
2311
|
+
ctx.stroke();
|
|
2312
|
+
ctx.restore();
|
|
2313
|
+
drawDrawingHandle(x0, y0, drawing.color);
|
|
2314
|
+
drawDrawingHandle(x1, y1, drawing.color);
|
|
2315
|
+
if (p2) {
|
|
2316
|
+
const x2 = xFromDrawingPoint(p2);
|
|
2317
|
+
const y2 = yFromPrice(p2.price);
|
|
2318
|
+
drawDrawingHandle(x2, y2, drawing.color);
|
|
2319
|
+
const palette = drawing.colors.length > 0 ? drawing.colors : null;
|
|
2320
|
+
const levelColorAt = (index) => palette ? palette[index % palette.length] : drawing.color;
|
|
2321
|
+
const move = p1.price - p0.price;
|
|
2322
|
+
const levelLines = FIB_EXT_RATIOS.map((ratio) => {
|
|
2323
|
+
const price = p2.price + move * ratio;
|
|
2324
|
+
return { ratio, price, y: yFromPrice(price) };
|
|
2325
|
+
});
|
|
2326
|
+
for (let index = 0; index < levelLines.length - 1; index += 1) {
|
|
2327
|
+
const top = levelLines[index];
|
|
2328
|
+
const bottom = levelLines[index + 1];
|
|
2329
|
+
const bandTop = Math.min(top.y, bottom.y);
|
|
2330
|
+
const bandBottom = Math.max(top.y, bottom.y);
|
|
2331
|
+
ctx.save();
|
|
2332
|
+
ctx.fillStyle = hexToRgba(levelColorAt(index), 0.1);
|
|
2333
|
+
ctx.globalAlpha = draft ? 0.5 : 1;
|
|
2334
|
+
ctx.fillRect(chartLeft, bandTop, chartRight - chartLeft, bandBottom - bandTop);
|
|
2335
|
+
ctx.restore();
|
|
2336
|
+
}
|
|
2337
|
+
ctx.save();
|
|
2338
|
+
ctx.lineWidth = drawing.width;
|
|
2339
|
+
applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
2340
|
+
levelLines.forEach((level, index) => {
|
|
2341
|
+
ctx.strokeStyle = levelColorAt(index);
|
|
2342
|
+
ctx.beginPath();
|
|
2343
|
+
ctx.moveTo(crisp(chartLeft), crisp(level.y));
|
|
2344
|
+
ctx.lineTo(crisp(chartRight), crisp(level.y));
|
|
2345
|
+
ctx.stroke();
|
|
2346
|
+
});
|
|
2347
|
+
ctx.restore();
|
|
2348
|
+
const prevFont = ctx.font;
|
|
2349
|
+
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2350
|
+
levelLines.forEach((level, index) => {
|
|
2351
|
+
const labelText = `${level.ratio} (${formatPrice(level.price)})`;
|
|
2352
|
+
const textWidth = ctx.measureText(labelText).width;
|
|
2353
|
+
const padding = 4;
|
|
2354
|
+
const bgX = chartLeft + 4;
|
|
2355
|
+
const bgY = level.y - 9;
|
|
2356
|
+
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2357
|
+
fillRoundedRect(bgX, bgY, textWidth + padding * 2, 16, 3);
|
|
2358
|
+
ctx.fillStyle = levelColorAt(index);
|
|
2359
|
+
ctx.textAlign = "left";
|
|
2360
|
+
ctx.textBaseline = "middle";
|
|
2361
|
+
ctx.fillText(labelText, bgX + padding, bgY + 8);
|
|
2362
|
+
});
|
|
2363
|
+
ctx.font = prevFont;
|
|
2364
|
+
if (drawing.label) {
|
|
2365
|
+
drawDrawingLabel(drawing.label, (x0 + x2) / 2, Math.min(y0, y2), drawing.color);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
} else if (drawing.type === "long-position" || drawing.type === "short-position") {
|
|
2370
|
+
const entry = drawing.points[0];
|
|
2371
|
+
const target = drawing.points[1];
|
|
2372
|
+
const stop = drawing.points[2];
|
|
2373
|
+
const right = drawing.points[3];
|
|
2374
|
+
if (entry && target && stop && right) {
|
|
2375
|
+
const leftX = xFromDrawingPoint(entry);
|
|
2376
|
+
const rightX = xFromDrawingPoint(right);
|
|
2377
|
+
const boxX0 = Math.min(leftX, rightX);
|
|
2378
|
+
const boxX1 = Math.max(leftX, rightX);
|
|
2379
|
+
const boxW = Math.max(1, boxX1 - boxX0);
|
|
2380
|
+
const entryY = yFromPrice(entry.price);
|
|
2381
|
+
const targetY = yFromPrice(target.price);
|
|
2382
|
+
const stopY = yFromPrice(stop.price);
|
|
2383
|
+
const profitFill = "rgba(38,166,154,0.16)";
|
|
2384
|
+
const lossFill = "rgba(239,83,80,0.16)";
|
|
2385
|
+
const profitLine = "rgba(38,166,154,0.9)";
|
|
2386
|
+
const lossLine = "rgba(239,83,80,0.9)";
|
|
2387
|
+
ctx.save();
|
|
2388
|
+
ctx.globalAlpha = draft ? 0.6 : 1;
|
|
2389
|
+
ctx.fillStyle = profitFill;
|
|
2390
|
+
ctx.fillRect(boxX0, Math.min(entryY, targetY), boxW, Math.abs(targetY - entryY));
|
|
2391
|
+
ctx.fillStyle = lossFill;
|
|
2392
|
+
ctx.fillRect(boxX0, Math.min(entryY, stopY), boxW, Math.abs(stopY - entryY));
|
|
2393
|
+
ctx.restore();
|
|
2394
|
+
ctx.save();
|
|
2395
|
+
ctx.setLineDash([]);
|
|
2396
|
+
ctx.lineWidth = Math.max(1, drawing.width);
|
|
2397
|
+
ctx.strokeStyle = profitLine;
|
|
2398
|
+
ctx.beginPath();
|
|
2399
|
+
ctx.moveTo(crisp(boxX0), crisp(targetY));
|
|
2400
|
+
ctx.lineTo(crisp(boxX1), crisp(targetY));
|
|
2401
|
+
ctx.stroke();
|
|
2402
|
+
ctx.strokeStyle = lossLine;
|
|
2403
|
+
ctx.beginPath();
|
|
2404
|
+
ctx.moveTo(crisp(boxX0), crisp(stopY));
|
|
2405
|
+
ctx.lineTo(crisp(boxX1), crisp(stopY));
|
|
2406
|
+
ctx.stroke();
|
|
2407
|
+
ctx.strokeStyle = drawing.color;
|
|
2408
|
+
ctx.beginPath();
|
|
2409
|
+
ctx.moveTo(crisp(boxX0), crisp(entryY));
|
|
2410
|
+
ctx.lineTo(crisp(boxX1), crisp(entryY));
|
|
2411
|
+
ctx.stroke();
|
|
2412
|
+
ctx.restore();
|
|
2413
|
+
drawDrawingHandle(leftX, targetY, drawing.color);
|
|
2414
|
+
drawDrawingHandle(leftX, entryY, drawing.color);
|
|
2415
|
+
drawDrawingHandle(leftX, stopY, drawing.color);
|
|
2416
|
+
drawDrawingHandle(rightX, entryY, drawing.color);
|
|
2417
|
+
const tick = getConfiguredTickSize();
|
|
2418
|
+
const pctOf = (price) => {
|
|
2419
|
+
if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
|
|
2420
|
+
return `${((price - entry.price) / Math.abs(entry.price) * 100).toFixed(2)}%`;
|
|
2421
|
+
};
|
|
2422
|
+
const ticksOf = (price) => tick > 0 ? ` ${Math.round(Math.abs(price - entry.price) / tick)}t` : "";
|
|
2423
|
+
const targetDist = Math.abs(target.price - entry.price);
|
|
2424
|
+
const stopDist = Math.abs(entry.price - stop.price);
|
|
2425
|
+
const rr = stopDist > 0 ? targetDist / stopDist : 0;
|
|
2426
|
+
const cx = (boxX0 + boxX1) / 2;
|
|
2427
|
+
const drawPositionPill = (text, centerX, centerY, bg) => {
|
|
2428
|
+
const prevFont = ctx.font;
|
|
2429
|
+
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2430
|
+
const padding = 6;
|
|
2431
|
+
const textW = ctx.measureText(text).width;
|
|
2432
|
+
const pillW = textW + padding * 2;
|
|
2433
|
+
const pillH = 18;
|
|
2434
|
+
const pillX = centerX - pillW / 2;
|
|
2435
|
+
const pillY = centerY - pillH / 2;
|
|
2436
|
+
ctx.fillStyle = bg;
|
|
2437
|
+
fillRoundedRect(pillX, pillY, pillW, pillH, 4);
|
|
2438
|
+
ctx.fillStyle = "#ffffff";
|
|
2439
|
+
ctx.textAlign = "center";
|
|
2440
|
+
ctx.textBaseline = "middle";
|
|
2441
|
+
ctx.fillText(text, centerX, pillY + pillH / 2);
|
|
2442
|
+
ctx.font = prevFont;
|
|
2443
|
+
};
|
|
2444
|
+
drawPositionPill(`Target ${formatPrice(target.price)} (${pctOf(target.price)})${ticksOf(target.price)}`, cx, targetY, profitLine);
|
|
2445
|
+
drawPositionPill(`Stop ${formatPrice(stop.price)} (${pctOf(stop.price)})${ticksOf(stop.price)}`, cx, stopY, lossLine);
|
|
2446
|
+
drawPositionPill(`Entry ${formatPrice(entry.price)} RR ${rr.toFixed(2)}`, cx, entryY, hexToRgba(drawing.color, 0.92));
|
|
2447
|
+
if (drawing.label) {
|
|
2448
|
+
drawDrawingLabel(drawing.label, cx, Math.min(targetY, stopY) - 4, drawing.color);
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2291
2451
|
}
|
|
2292
2452
|
ctx.restore();
|
|
2293
2453
|
};
|
|
@@ -3449,6 +3609,63 @@ function createChart(element, options = {}) {
|
|
|
3449
3609
|
return { drawing, target: "line" };
|
|
3450
3610
|
}
|
|
3451
3611
|
}
|
|
3612
|
+
} else if (drawing.type === "fib-extension") {
|
|
3613
|
+
const p0 = drawing.points[0];
|
|
3614
|
+
const p1 = drawing.points[1];
|
|
3615
|
+
const p2 = drawing.points[2];
|
|
3616
|
+
if (!p0 || !p1) continue;
|
|
3617
|
+
const x0 = canvasXFromDrawingPoint(p0);
|
|
3618
|
+
const y0 = canvasYFromDrawingPrice(p0.price);
|
|
3619
|
+
const x1 = canvasXFromDrawingPoint(p1);
|
|
3620
|
+
const y1 = canvasYFromDrawingPrice(p1.price);
|
|
3621
|
+
if (Math.hypot(x - x0, y - y0) <= 8) {
|
|
3622
|
+
return { drawing, target: "handle", pointIndex: 0 };
|
|
3623
|
+
}
|
|
3624
|
+
if (Math.hypot(x - x1, y - y1) <= 8) {
|
|
3625
|
+
return { drawing, target: "handle", pointIndex: 1 };
|
|
3626
|
+
}
|
|
3627
|
+
if (p2) {
|
|
3628
|
+
const x2 = canvasXFromDrawingPoint(p2);
|
|
3629
|
+
const y2 = canvasYFromDrawingPrice(p2.price);
|
|
3630
|
+
if (Math.hypot(x - x2, y - y2) <= 8) {
|
|
3631
|
+
return { drawing, target: "handle", pointIndex: 2 };
|
|
3632
|
+
}
|
|
3633
|
+
const move = p1.price - p0.price;
|
|
3634
|
+
const fibExtRatios = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
|
|
3635
|
+
for (const ratio of fibExtRatios) {
|
|
3636
|
+
const lineY = canvasYFromDrawingPrice(p2.price + move * ratio);
|
|
3637
|
+
if (Math.abs(y - lineY) <= 5) {
|
|
3638
|
+
return { drawing, target: "line" };
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
|
|
3642
|
+
return { drawing, target: "line" };
|
|
3643
|
+
}
|
|
3644
|
+
} else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
|
|
3645
|
+
return { drawing, target: "line" };
|
|
3646
|
+
}
|
|
3647
|
+
} else if (drawing.type === "long-position" || drawing.type === "short-position") {
|
|
3648
|
+
const entry = drawing.points[0];
|
|
3649
|
+
const target = drawing.points[1];
|
|
3650
|
+
const stop = drawing.points[2];
|
|
3651
|
+
const right = drawing.points[3];
|
|
3652
|
+
if (!entry || !target || !stop || !right) continue;
|
|
3653
|
+
const leftX = canvasXFromDrawingPoint(entry);
|
|
3654
|
+
const rightX = canvasXFromDrawingPoint(right);
|
|
3655
|
+
const entryY = canvasYFromDrawingPrice(entry.price);
|
|
3656
|
+
const targetY = canvasYFromDrawingPrice(target.price);
|
|
3657
|
+
const stopY = canvasYFromDrawingPrice(stop.price);
|
|
3658
|
+
if (Math.hypot(x - leftX, y - targetY) <= 9) return { drawing, target: "handle", pointIndex: 1 };
|
|
3659
|
+
if (Math.hypot(x - leftX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 0 };
|
|
3660
|
+
if (Math.hypot(x - leftX, y - stopY) <= 9) return { drawing, target: "handle", pointIndex: 2 };
|
|
3661
|
+
if (Math.hypot(x - rightX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 3 };
|
|
3662
|
+
const x0 = Math.min(leftX, rightX);
|
|
3663
|
+
const x1 = Math.max(leftX, rightX);
|
|
3664
|
+
const yTop = Math.min(targetY, stopY, entryY);
|
|
3665
|
+
const yBot = Math.max(targetY, stopY, entryY);
|
|
3666
|
+
if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
|
|
3667
|
+
return { drawing, target: "line" };
|
|
3668
|
+
}
|
|
3452
3669
|
}
|
|
3453
3670
|
}
|
|
3454
3671
|
return null;
|
|
@@ -3616,6 +3833,66 @@ function createChart(element, options = {}) {
|
|
|
3616
3833
|
draw();
|
|
3617
3834
|
return true;
|
|
3618
3835
|
}
|
|
3836
|
+
if (activeDrawingTool === "fib-extension") {
|
|
3837
|
+
if (draftDrawing?.type === "fib-extension") {
|
|
3838
|
+
if (draftDrawing.points.length < 3) {
|
|
3839
|
+
draftDrawing = normalizeDrawingState({
|
|
3840
|
+
...serializeDrawing(draftDrawing),
|
|
3841
|
+
points: [...draftDrawing.points.slice(0, -1), point, point]
|
|
3842
|
+
});
|
|
3843
|
+
draw();
|
|
3844
|
+
return true;
|
|
3845
|
+
}
|
|
3846
|
+
const completed = normalizeDrawingState({
|
|
3847
|
+
...serializeDrawing(draftDrawing),
|
|
3848
|
+
points: [draftDrawing.points[0], draftDrawing.points[1], point]
|
|
3849
|
+
});
|
|
3850
|
+
drawings.push(completed);
|
|
3851
|
+
draftDrawing = null;
|
|
3852
|
+
activeDrawingTool = null;
|
|
3853
|
+
emitDrawingsChange();
|
|
3854
|
+
draw();
|
|
3855
|
+
return true;
|
|
3856
|
+
}
|
|
3857
|
+
const defaults = getDrawingToolDefaults("fib-extension");
|
|
3858
|
+
draftDrawing = normalizeDrawingState({
|
|
3859
|
+
type: "fib-extension",
|
|
3860
|
+
points: [point, point],
|
|
3861
|
+
color: defaults.color ?? "#2563eb",
|
|
3862
|
+
colors: defaults.colors ?? FIB_DEFAULT_PALETTE,
|
|
3863
|
+
style: defaults.style ?? "solid",
|
|
3864
|
+
width: defaults.width ?? 1
|
|
3865
|
+
});
|
|
3866
|
+
draw();
|
|
3867
|
+
return true;
|
|
3868
|
+
}
|
|
3869
|
+
if (activeDrawingTool === "long-position" || activeDrawingTool === "short-position") {
|
|
3870
|
+
const isLong = activeDrawingTool === "long-position";
|
|
3871
|
+
const tick = getConfiguredTickSize();
|
|
3872
|
+
const priceOffset = tick > 0 ? tick * 20 : Math.abs(point.price) * 0.01 || 1;
|
|
3873
|
+
const width2 = Math.max(5, Math.round(xSpan * 0.15));
|
|
3874
|
+
const entryPrice = point.price;
|
|
3875
|
+
const targetPrice = isLong ? entryPrice + priceOffset : entryPrice - priceOffset;
|
|
3876
|
+
const stopPrice = isLong ? entryPrice - priceOffset : entryPrice + priceOffset;
|
|
3877
|
+
const defaults = getDrawingToolDefaults(activeDrawingTool);
|
|
3878
|
+
drawings.push(
|
|
3879
|
+
normalizeDrawingState({
|
|
3880
|
+
type: activeDrawingTool,
|
|
3881
|
+
points: [
|
|
3882
|
+
point,
|
|
3883
|
+
normalizeDrawingPoint(point.index, targetPrice),
|
|
3884
|
+
normalizeDrawingPoint(point.index, stopPrice),
|
|
3885
|
+
normalizeDrawingPoint(point.index + width2, entryPrice)
|
|
3886
|
+
],
|
|
3887
|
+
color: defaults.color ?? "#2563eb",
|
|
3888
|
+
style: defaults.style ?? "solid",
|
|
3889
|
+
width: defaults.width ?? 1
|
|
3890
|
+
})
|
|
3891
|
+
);
|
|
3892
|
+
emitDrawingsChange();
|
|
3893
|
+
draw();
|
|
3894
|
+
return true;
|
|
3895
|
+
}
|
|
3619
3896
|
return false;
|
|
3620
3897
|
};
|
|
3621
3898
|
const setDrawingDefaults = (tool, defaults) => {
|
|
@@ -3639,6 +3916,22 @@ function createChart(element, options = {}) {
|
|
|
3639
3916
|
if (drawing.id !== drawingDragState?.drawingId || drawing.locked) {
|
|
3640
3917
|
return drawing;
|
|
3641
3918
|
}
|
|
3919
|
+
if (drawingDragState.target === "handle" && (drawing.type === "long-position" || drawing.type === "short-position")) {
|
|
3920
|
+
const pts = drawing.points.map((point) => ({ ...point }));
|
|
3921
|
+
const entry = pts[0];
|
|
3922
|
+
const pointIndex = drawingDragState.pointIndex ?? 0;
|
|
3923
|
+
if (pointIndex === 0) {
|
|
3924
|
+
pts[0] = normalizeDrawingPoint(entry.index, currentPoint.price);
|
|
3925
|
+
if (pts[3]) pts[3] = normalizeDrawingPoint(pts[3].index, currentPoint.price);
|
|
3926
|
+
} else if (pointIndex === 1 && pts[1]) {
|
|
3927
|
+
pts[1] = normalizeDrawingPoint(entry.index, currentPoint.price);
|
|
3928
|
+
} else if (pointIndex === 2 && pts[2]) {
|
|
3929
|
+
pts[2] = normalizeDrawingPoint(entry.index, currentPoint.price);
|
|
3930
|
+
} else if (pointIndex === 3 && pts[3]) {
|
|
3931
|
+
pts[3] = normalizeDrawingPoint(currentPoint.index, entry.price);
|
|
3932
|
+
}
|
|
3933
|
+
return { ...drawing, points: pts };
|
|
3934
|
+
}
|
|
3642
3935
|
if (drawingDragState.target === "handle") {
|
|
3643
3936
|
return {
|
|
3644
3937
|
...drawing,
|
|
@@ -3888,12 +4181,12 @@ function createChart(element, options = {}) {
|
|
|
3888
4181
|
setCrosshairPoint(null);
|
|
3889
4182
|
return;
|
|
3890
4183
|
}
|
|
3891
|
-
if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement")) {
|
|
4184
|
+
if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
|
|
3892
4185
|
const nextPoint = drawingPointFromCanvas(point.x, point.y);
|
|
3893
4186
|
if (nextPoint) {
|
|
3894
4187
|
draftDrawing = {
|
|
3895
4188
|
...draftDrawing,
|
|
3896
|
-
points: [draftDrawing.points
|
|
4189
|
+
points: [...draftDrawing.points.slice(0, -1), nextPoint]
|
|
3897
4190
|
};
|
|
3898
4191
|
canvas.style.cursor = "crosshair";
|
|
3899
4192
|
draw();
|
package/dist/index.cjs
CHANGED
|
@@ -2113,6 +2113,7 @@ function createChart(element, options = {}) {
|
|
|
2113
2113
|
};
|
|
2114
2114
|
const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
|
|
2115
2115
|
const FIB_RATIOS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
|
|
2116
|
+
const FIB_EXT_RATIOS = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
|
|
2116
2117
|
const hexToRgba = (hex, alpha) => {
|
|
2117
2118
|
const cleaned = hex.replace("#", "");
|
|
2118
2119
|
if (cleaned.length !== 6) {
|
|
@@ -2313,6 +2314,165 @@ function createChart(element, options = {}) {
|
|
|
2313
2314
|
});
|
|
2314
2315
|
ctx.font = prevFont;
|
|
2315
2316
|
}
|
|
2317
|
+
} else if (drawing.type === "fib-extension") {
|
|
2318
|
+
const p0 = drawing.points[0];
|
|
2319
|
+
const p1 = drawing.points[1];
|
|
2320
|
+
const p2 = drawing.points[2];
|
|
2321
|
+
if (p0 && p1) {
|
|
2322
|
+
const x0 = xFromDrawingPoint(p0);
|
|
2323
|
+
const y0 = yFromPrice(p0.price);
|
|
2324
|
+
const x1 = xFromDrawingPoint(p1);
|
|
2325
|
+
const y1 = yFromPrice(p1.price);
|
|
2326
|
+
ctx.save();
|
|
2327
|
+
ctx.strokeStyle = hexToRgba(drawing.color, 0.6);
|
|
2328
|
+
ctx.setLineDash([4, 4]);
|
|
2329
|
+
ctx.lineWidth = 1;
|
|
2330
|
+
ctx.beginPath();
|
|
2331
|
+
ctx.moveTo(x0, y0);
|
|
2332
|
+
ctx.lineTo(x1, y1);
|
|
2333
|
+
if (p2) {
|
|
2334
|
+
ctx.lineTo(xFromDrawingPoint(p2), yFromPrice(p2.price));
|
|
2335
|
+
}
|
|
2336
|
+
ctx.stroke();
|
|
2337
|
+
ctx.restore();
|
|
2338
|
+
drawDrawingHandle(x0, y0, drawing.color);
|
|
2339
|
+
drawDrawingHandle(x1, y1, drawing.color);
|
|
2340
|
+
if (p2) {
|
|
2341
|
+
const x2 = xFromDrawingPoint(p2);
|
|
2342
|
+
const y2 = yFromPrice(p2.price);
|
|
2343
|
+
drawDrawingHandle(x2, y2, drawing.color);
|
|
2344
|
+
const palette = drawing.colors.length > 0 ? drawing.colors : null;
|
|
2345
|
+
const levelColorAt = (index) => palette ? palette[index % palette.length] : drawing.color;
|
|
2346
|
+
const move = p1.price - p0.price;
|
|
2347
|
+
const levelLines = FIB_EXT_RATIOS.map((ratio) => {
|
|
2348
|
+
const price = p2.price + move * ratio;
|
|
2349
|
+
return { ratio, price, y: yFromPrice(price) };
|
|
2350
|
+
});
|
|
2351
|
+
for (let index = 0; index < levelLines.length - 1; index += 1) {
|
|
2352
|
+
const top = levelLines[index];
|
|
2353
|
+
const bottom = levelLines[index + 1];
|
|
2354
|
+
const bandTop = Math.min(top.y, bottom.y);
|
|
2355
|
+
const bandBottom = Math.max(top.y, bottom.y);
|
|
2356
|
+
ctx.save();
|
|
2357
|
+
ctx.fillStyle = hexToRgba(levelColorAt(index), 0.1);
|
|
2358
|
+
ctx.globalAlpha = draft ? 0.5 : 1;
|
|
2359
|
+
ctx.fillRect(chartLeft, bandTop, chartRight - chartLeft, bandBottom - bandTop);
|
|
2360
|
+
ctx.restore();
|
|
2361
|
+
}
|
|
2362
|
+
ctx.save();
|
|
2363
|
+
ctx.lineWidth = drawing.width;
|
|
2364
|
+
applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
2365
|
+
levelLines.forEach((level, index) => {
|
|
2366
|
+
ctx.strokeStyle = levelColorAt(index);
|
|
2367
|
+
ctx.beginPath();
|
|
2368
|
+
ctx.moveTo(crisp(chartLeft), crisp(level.y));
|
|
2369
|
+
ctx.lineTo(crisp(chartRight), crisp(level.y));
|
|
2370
|
+
ctx.stroke();
|
|
2371
|
+
});
|
|
2372
|
+
ctx.restore();
|
|
2373
|
+
const prevFont = ctx.font;
|
|
2374
|
+
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2375
|
+
levelLines.forEach((level, index) => {
|
|
2376
|
+
const labelText = `${level.ratio} (${formatPrice(level.price)})`;
|
|
2377
|
+
const textWidth = ctx.measureText(labelText).width;
|
|
2378
|
+
const padding = 4;
|
|
2379
|
+
const bgX = chartLeft + 4;
|
|
2380
|
+
const bgY = level.y - 9;
|
|
2381
|
+
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2382
|
+
fillRoundedRect(bgX, bgY, textWidth + padding * 2, 16, 3);
|
|
2383
|
+
ctx.fillStyle = levelColorAt(index);
|
|
2384
|
+
ctx.textAlign = "left";
|
|
2385
|
+
ctx.textBaseline = "middle";
|
|
2386
|
+
ctx.fillText(labelText, bgX + padding, bgY + 8);
|
|
2387
|
+
});
|
|
2388
|
+
ctx.font = prevFont;
|
|
2389
|
+
if (drawing.label) {
|
|
2390
|
+
drawDrawingLabel(drawing.label, (x0 + x2) / 2, Math.min(y0, y2), drawing.color);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
} else if (drawing.type === "long-position" || drawing.type === "short-position") {
|
|
2395
|
+
const entry = drawing.points[0];
|
|
2396
|
+
const target = drawing.points[1];
|
|
2397
|
+
const stop = drawing.points[2];
|
|
2398
|
+
const right = drawing.points[3];
|
|
2399
|
+
if (entry && target && stop && right) {
|
|
2400
|
+
const leftX = xFromDrawingPoint(entry);
|
|
2401
|
+
const rightX = xFromDrawingPoint(right);
|
|
2402
|
+
const boxX0 = Math.min(leftX, rightX);
|
|
2403
|
+
const boxX1 = Math.max(leftX, rightX);
|
|
2404
|
+
const boxW = Math.max(1, boxX1 - boxX0);
|
|
2405
|
+
const entryY = yFromPrice(entry.price);
|
|
2406
|
+
const targetY = yFromPrice(target.price);
|
|
2407
|
+
const stopY = yFromPrice(stop.price);
|
|
2408
|
+
const profitFill = "rgba(38,166,154,0.16)";
|
|
2409
|
+
const lossFill = "rgba(239,83,80,0.16)";
|
|
2410
|
+
const profitLine = "rgba(38,166,154,0.9)";
|
|
2411
|
+
const lossLine = "rgba(239,83,80,0.9)";
|
|
2412
|
+
ctx.save();
|
|
2413
|
+
ctx.globalAlpha = draft ? 0.6 : 1;
|
|
2414
|
+
ctx.fillStyle = profitFill;
|
|
2415
|
+
ctx.fillRect(boxX0, Math.min(entryY, targetY), boxW, Math.abs(targetY - entryY));
|
|
2416
|
+
ctx.fillStyle = lossFill;
|
|
2417
|
+
ctx.fillRect(boxX0, Math.min(entryY, stopY), boxW, Math.abs(stopY - entryY));
|
|
2418
|
+
ctx.restore();
|
|
2419
|
+
ctx.save();
|
|
2420
|
+
ctx.setLineDash([]);
|
|
2421
|
+
ctx.lineWidth = Math.max(1, drawing.width);
|
|
2422
|
+
ctx.strokeStyle = profitLine;
|
|
2423
|
+
ctx.beginPath();
|
|
2424
|
+
ctx.moveTo(crisp(boxX0), crisp(targetY));
|
|
2425
|
+
ctx.lineTo(crisp(boxX1), crisp(targetY));
|
|
2426
|
+
ctx.stroke();
|
|
2427
|
+
ctx.strokeStyle = lossLine;
|
|
2428
|
+
ctx.beginPath();
|
|
2429
|
+
ctx.moveTo(crisp(boxX0), crisp(stopY));
|
|
2430
|
+
ctx.lineTo(crisp(boxX1), crisp(stopY));
|
|
2431
|
+
ctx.stroke();
|
|
2432
|
+
ctx.strokeStyle = drawing.color;
|
|
2433
|
+
ctx.beginPath();
|
|
2434
|
+
ctx.moveTo(crisp(boxX0), crisp(entryY));
|
|
2435
|
+
ctx.lineTo(crisp(boxX1), crisp(entryY));
|
|
2436
|
+
ctx.stroke();
|
|
2437
|
+
ctx.restore();
|
|
2438
|
+
drawDrawingHandle(leftX, targetY, drawing.color);
|
|
2439
|
+
drawDrawingHandle(leftX, entryY, drawing.color);
|
|
2440
|
+
drawDrawingHandle(leftX, stopY, drawing.color);
|
|
2441
|
+
drawDrawingHandle(rightX, entryY, drawing.color);
|
|
2442
|
+
const tick = getConfiguredTickSize();
|
|
2443
|
+
const pctOf = (price) => {
|
|
2444
|
+
if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
|
|
2445
|
+
return `${((price - entry.price) / Math.abs(entry.price) * 100).toFixed(2)}%`;
|
|
2446
|
+
};
|
|
2447
|
+
const ticksOf = (price) => tick > 0 ? ` ${Math.round(Math.abs(price - entry.price) / tick)}t` : "";
|
|
2448
|
+
const targetDist = Math.abs(target.price - entry.price);
|
|
2449
|
+
const stopDist = Math.abs(entry.price - stop.price);
|
|
2450
|
+
const rr = stopDist > 0 ? targetDist / stopDist : 0;
|
|
2451
|
+
const cx = (boxX0 + boxX1) / 2;
|
|
2452
|
+
const drawPositionPill = (text, centerX, centerY, bg) => {
|
|
2453
|
+
const prevFont = ctx.font;
|
|
2454
|
+
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2455
|
+
const padding = 6;
|
|
2456
|
+
const textW = ctx.measureText(text).width;
|
|
2457
|
+
const pillW = textW + padding * 2;
|
|
2458
|
+
const pillH = 18;
|
|
2459
|
+
const pillX = centerX - pillW / 2;
|
|
2460
|
+
const pillY = centerY - pillH / 2;
|
|
2461
|
+
ctx.fillStyle = bg;
|
|
2462
|
+
fillRoundedRect(pillX, pillY, pillW, pillH, 4);
|
|
2463
|
+
ctx.fillStyle = "#ffffff";
|
|
2464
|
+
ctx.textAlign = "center";
|
|
2465
|
+
ctx.textBaseline = "middle";
|
|
2466
|
+
ctx.fillText(text, centerX, pillY + pillH / 2);
|
|
2467
|
+
ctx.font = prevFont;
|
|
2468
|
+
};
|
|
2469
|
+
drawPositionPill(`Target ${formatPrice(target.price)} (${pctOf(target.price)})${ticksOf(target.price)}`, cx, targetY, profitLine);
|
|
2470
|
+
drawPositionPill(`Stop ${formatPrice(stop.price)} (${pctOf(stop.price)})${ticksOf(stop.price)}`, cx, stopY, lossLine);
|
|
2471
|
+
drawPositionPill(`Entry ${formatPrice(entry.price)} RR ${rr.toFixed(2)}`, cx, entryY, hexToRgba(drawing.color, 0.92));
|
|
2472
|
+
if (drawing.label) {
|
|
2473
|
+
drawDrawingLabel(drawing.label, cx, Math.min(targetY, stopY) - 4, drawing.color);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2316
2476
|
}
|
|
2317
2477
|
ctx.restore();
|
|
2318
2478
|
};
|
|
@@ -3474,6 +3634,63 @@ function createChart(element, options = {}) {
|
|
|
3474
3634
|
return { drawing, target: "line" };
|
|
3475
3635
|
}
|
|
3476
3636
|
}
|
|
3637
|
+
} else if (drawing.type === "fib-extension") {
|
|
3638
|
+
const p0 = drawing.points[0];
|
|
3639
|
+
const p1 = drawing.points[1];
|
|
3640
|
+
const p2 = drawing.points[2];
|
|
3641
|
+
if (!p0 || !p1) continue;
|
|
3642
|
+
const x0 = canvasXFromDrawingPoint(p0);
|
|
3643
|
+
const y0 = canvasYFromDrawingPrice(p0.price);
|
|
3644
|
+
const x1 = canvasXFromDrawingPoint(p1);
|
|
3645
|
+
const y1 = canvasYFromDrawingPrice(p1.price);
|
|
3646
|
+
if (Math.hypot(x - x0, y - y0) <= 8) {
|
|
3647
|
+
return { drawing, target: "handle", pointIndex: 0 };
|
|
3648
|
+
}
|
|
3649
|
+
if (Math.hypot(x - x1, y - y1) <= 8) {
|
|
3650
|
+
return { drawing, target: "handle", pointIndex: 1 };
|
|
3651
|
+
}
|
|
3652
|
+
if (p2) {
|
|
3653
|
+
const x2 = canvasXFromDrawingPoint(p2);
|
|
3654
|
+
const y2 = canvasYFromDrawingPrice(p2.price);
|
|
3655
|
+
if (Math.hypot(x - x2, y - y2) <= 8) {
|
|
3656
|
+
return { drawing, target: "handle", pointIndex: 2 };
|
|
3657
|
+
}
|
|
3658
|
+
const move = p1.price - p0.price;
|
|
3659
|
+
const fibExtRatios = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
|
|
3660
|
+
for (const ratio of fibExtRatios) {
|
|
3661
|
+
const lineY = canvasYFromDrawingPrice(p2.price + move * ratio);
|
|
3662
|
+
if (Math.abs(y - lineY) <= 5) {
|
|
3663
|
+
return { drawing, target: "line" };
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
|
|
3667
|
+
return { drawing, target: "line" };
|
|
3668
|
+
}
|
|
3669
|
+
} else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
|
|
3670
|
+
return { drawing, target: "line" };
|
|
3671
|
+
}
|
|
3672
|
+
} else if (drawing.type === "long-position" || drawing.type === "short-position") {
|
|
3673
|
+
const entry = drawing.points[0];
|
|
3674
|
+
const target = drawing.points[1];
|
|
3675
|
+
const stop = drawing.points[2];
|
|
3676
|
+
const right = drawing.points[3];
|
|
3677
|
+
if (!entry || !target || !stop || !right) continue;
|
|
3678
|
+
const leftX = canvasXFromDrawingPoint(entry);
|
|
3679
|
+
const rightX = canvasXFromDrawingPoint(right);
|
|
3680
|
+
const entryY = canvasYFromDrawingPrice(entry.price);
|
|
3681
|
+
const targetY = canvasYFromDrawingPrice(target.price);
|
|
3682
|
+
const stopY = canvasYFromDrawingPrice(stop.price);
|
|
3683
|
+
if (Math.hypot(x - leftX, y - targetY) <= 9) return { drawing, target: "handle", pointIndex: 1 };
|
|
3684
|
+
if (Math.hypot(x - leftX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 0 };
|
|
3685
|
+
if (Math.hypot(x - leftX, y - stopY) <= 9) return { drawing, target: "handle", pointIndex: 2 };
|
|
3686
|
+
if (Math.hypot(x - rightX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 3 };
|
|
3687
|
+
const x0 = Math.min(leftX, rightX);
|
|
3688
|
+
const x1 = Math.max(leftX, rightX);
|
|
3689
|
+
const yTop = Math.min(targetY, stopY, entryY);
|
|
3690
|
+
const yBot = Math.max(targetY, stopY, entryY);
|
|
3691
|
+
if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
|
|
3692
|
+
return { drawing, target: "line" };
|
|
3693
|
+
}
|
|
3477
3694
|
}
|
|
3478
3695
|
}
|
|
3479
3696
|
return null;
|
|
@@ -3641,6 +3858,66 @@ function createChart(element, options = {}) {
|
|
|
3641
3858
|
draw();
|
|
3642
3859
|
return true;
|
|
3643
3860
|
}
|
|
3861
|
+
if (activeDrawingTool === "fib-extension") {
|
|
3862
|
+
if (draftDrawing?.type === "fib-extension") {
|
|
3863
|
+
if (draftDrawing.points.length < 3) {
|
|
3864
|
+
draftDrawing = normalizeDrawingState({
|
|
3865
|
+
...serializeDrawing(draftDrawing),
|
|
3866
|
+
points: [...draftDrawing.points.slice(0, -1), point, point]
|
|
3867
|
+
});
|
|
3868
|
+
draw();
|
|
3869
|
+
return true;
|
|
3870
|
+
}
|
|
3871
|
+
const completed = normalizeDrawingState({
|
|
3872
|
+
...serializeDrawing(draftDrawing),
|
|
3873
|
+
points: [draftDrawing.points[0], draftDrawing.points[1], point]
|
|
3874
|
+
});
|
|
3875
|
+
drawings.push(completed);
|
|
3876
|
+
draftDrawing = null;
|
|
3877
|
+
activeDrawingTool = null;
|
|
3878
|
+
emitDrawingsChange();
|
|
3879
|
+
draw();
|
|
3880
|
+
return true;
|
|
3881
|
+
}
|
|
3882
|
+
const defaults = getDrawingToolDefaults("fib-extension");
|
|
3883
|
+
draftDrawing = normalizeDrawingState({
|
|
3884
|
+
type: "fib-extension",
|
|
3885
|
+
points: [point, point],
|
|
3886
|
+
color: defaults.color ?? "#2563eb",
|
|
3887
|
+
colors: defaults.colors ?? FIB_DEFAULT_PALETTE,
|
|
3888
|
+
style: defaults.style ?? "solid",
|
|
3889
|
+
width: defaults.width ?? 1
|
|
3890
|
+
});
|
|
3891
|
+
draw();
|
|
3892
|
+
return true;
|
|
3893
|
+
}
|
|
3894
|
+
if (activeDrawingTool === "long-position" || activeDrawingTool === "short-position") {
|
|
3895
|
+
const isLong = activeDrawingTool === "long-position";
|
|
3896
|
+
const tick = getConfiguredTickSize();
|
|
3897
|
+
const priceOffset = tick > 0 ? tick * 20 : Math.abs(point.price) * 0.01 || 1;
|
|
3898
|
+
const width2 = Math.max(5, Math.round(xSpan * 0.15));
|
|
3899
|
+
const entryPrice = point.price;
|
|
3900
|
+
const targetPrice = isLong ? entryPrice + priceOffset : entryPrice - priceOffset;
|
|
3901
|
+
const stopPrice = isLong ? entryPrice - priceOffset : entryPrice + priceOffset;
|
|
3902
|
+
const defaults = getDrawingToolDefaults(activeDrawingTool);
|
|
3903
|
+
drawings.push(
|
|
3904
|
+
normalizeDrawingState({
|
|
3905
|
+
type: activeDrawingTool,
|
|
3906
|
+
points: [
|
|
3907
|
+
point,
|
|
3908
|
+
normalizeDrawingPoint(point.index, targetPrice),
|
|
3909
|
+
normalizeDrawingPoint(point.index, stopPrice),
|
|
3910
|
+
normalizeDrawingPoint(point.index + width2, entryPrice)
|
|
3911
|
+
],
|
|
3912
|
+
color: defaults.color ?? "#2563eb",
|
|
3913
|
+
style: defaults.style ?? "solid",
|
|
3914
|
+
width: defaults.width ?? 1
|
|
3915
|
+
})
|
|
3916
|
+
);
|
|
3917
|
+
emitDrawingsChange();
|
|
3918
|
+
draw();
|
|
3919
|
+
return true;
|
|
3920
|
+
}
|
|
3644
3921
|
return false;
|
|
3645
3922
|
};
|
|
3646
3923
|
const setDrawingDefaults = (tool, defaults) => {
|
|
@@ -3664,6 +3941,22 @@ function createChart(element, options = {}) {
|
|
|
3664
3941
|
if (drawing.id !== drawingDragState?.drawingId || drawing.locked) {
|
|
3665
3942
|
return drawing;
|
|
3666
3943
|
}
|
|
3944
|
+
if (drawingDragState.target === "handle" && (drawing.type === "long-position" || drawing.type === "short-position")) {
|
|
3945
|
+
const pts = drawing.points.map((point) => ({ ...point }));
|
|
3946
|
+
const entry = pts[0];
|
|
3947
|
+
const pointIndex = drawingDragState.pointIndex ?? 0;
|
|
3948
|
+
if (pointIndex === 0) {
|
|
3949
|
+
pts[0] = normalizeDrawingPoint(entry.index, currentPoint.price);
|
|
3950
|
+
if (pts[3]) pts[3] = normalizeDrawingPoint(pts[3].index, currentPoint.price);
|
|
3951
|
+
} else if (pointIndex === 1 && pts[1]) {
|
|
3952
|
+
pts[1] = normalizeDrawingPoint(entry.index, currentPoint.price);
|
|
3953
|
+
} else if (pointIndex === 2 && pts[2]) {
|
|
3954
|
+
pts[2] = normalizeDrawingPoint(entry.index, currentPoint.price);
|
|
3955
|
+
} else if (pointIndex === 3 && pts[3]) {
|
|
3956
|
+
pts[3] = normalizeDrawingPoint(currentPoint.index, entry.price);
|
|
3957
|
+
}
|
|
3958
|
+
return { ...drawing, points: pts };
|
|
3959
|
+
}
|
|
3667
3960
|
if (drawingDragState.target === "handle") {
|
|
3668
3961
|
return {
|
|
3669
3962
|
...drawing,
|
|
@@ -3913,12 +4206,12 @@ function createChart(element, options = {}) {
|
|
|
3913
4206
|
setCrosshairPoint(null);
|
|
3914
4207
|
return;
|
|
3915
4208
|
}
|
|
3916
|
-
if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement")) {
|
|
4209
|
+
if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
|
|
3917
4210
|
const nextPoint = drawingPointFromCanvas(point.x, point.y);
|
|
3918
4211
|
if (nextPoint) {
|
|
3919
4212
|
draftDrawing = {
|
|
3920
4213
|
...draftDrawing,
|
|
3921
|
-
points: [draftDrawing.points
|
|
4214
|
+
points: [...draftDrawing.points.slice(0, -1), nextPoint]
|
|
3922
4215
|
};
|
|
3923
4216
|
canvas.style.cursor = "crosshair";
|
|
3924
4217
|
draw();
|