hyperprop-charting-library 0.1.82 → 0.1.84
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 +128 -17
- package/dist/hyperprop-charting-library.d.ts +1 -1
- package/dist/hyperprop-charting-library.js +128 -17
- package/dist/index.cjs +128 -17
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +128 -17
- package/docs/API.md +1 -1
- package/package.json +1 -1
|
@@ -2503,11 +2503,12 @@ function createChart(element, options = {}) {
|
|
|
2503
2503
|
handleAt(leftX, stopY, drawing.color);
|
|
2504
2504
|
handleAt(rightX, entryY, drawing.color);
|
|
2505
2505
|
const tick = getConfiguredTickSize();
|
|
2506
|
-
const
|
|
2506
|
+
const pctOfDist = (dist) => {
|
|
2507
2507
|
if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
|
|
2508
|
-
|
|
2508
|
+
const pct = dist / Math.abs(entry.price) * 100;
|
|
2509
|
+
return `${pct < 1 ? pct.toFixed(3) : pct.toFixed(2)}%`;
|
|
2509
2510
|
};
|
|
2510
|
-
const
|
|
2511
|
+
const ticksOfDist = (dist) => tick > 0 ? ` ${Math.round(dist / tick)}` : "";
|
|
2511
2512
|
const targetDist = Math.abs(target.price - entry.price);
|
|
2512
2513
|
const stopDist = Math.abs(entry.price - stop.price);
|
|
2513
2514
|
const rr = stopDist > 0 ? targetDist / stopDist : 0;
|
|
@@ -2517,16 +2518,18 @@ function createChart(element, options = {}) {
|
|
|
2517
2518
|
const qtyRaw = riskAmount > 0 && stopDist > 0 ? riskAmount / (stopDist * effectivePointValue) : 0;
|
|
2518
2519
|
const hasMoney = qtyRaw > 0 && Number.isFinite(qtyRaw);
|
|
2519
2520
|
const qtyText = hasMoney ? qtyRaw.toFixed(Math.max(0, drawing.qtyPrecision)) : "";
|
|
2520
|
-
const formatAmount = (value) =>
|
|
2521
|
-
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2522
|
-
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2521
|
+
const formatAmount = (value) => value.toFixed(2);
|
|
2522
|
+
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * target.price * effectivePointValue)}` : "";
|
|
2523
|
+
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * stop.price * effectivePointValue)}` : "";
|
|
2523
2524
|
const drawPositionPill = (text, centerX, centerY, bg) => {
|
|
2525
|
+
const lines = Array.isArray(text) ? text : [text];
|
|
2524
2526
|
const prevFont = ctx.font;
|
|
2525
2527
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2526
2528
|
const padding = 6;
|
|
2527
|
-
const
|
|
2529
|
+
const lineH = 14;
|
|
2530
|
+
const textW = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
2528
2531
|
const pillW = textW + padding * 2;
|
|
2529
|
-
const pillH = 18;
|
|
2532
|
+
const pillH = lines.length === 1 ? 18 : lines.length * lineH + 6;
|
|
2530
2533
|
const pillX = centerX - pillW / 2;
|
|
2531
2534
|
const pillY = centerY - pillH / 2;
|
|
2532
2535
|
ctx.fillStyle = bg;
|
|
@@ -2534,28 +2537,104 @@ function createChart(element, options = {}) {
|
|
|
2534
2537
|
ctx.fillStyle = labelTextColor;
|
|
2535
2538
|
ctx.textAlign = "center";
|
|
2536
2539
|
ctx.textBaseline = "middle";
|
|
2537
|
-
|
|
2540
|
+
const startY = pillY + pillH / 2 - (lines.length - 1) * lineH / 2;
|
|
2541
|
+
lines.forEach((line, lineIndex) => ctx.fillText(line, centerX, startY + lineIndex * lineH));
|
|
2538
2542
|
ctx.font = prevFont;
|
|
2539
2543
|
};
|
|
2540
|
-
drawPositionPill(`Target ${formatPrice(
|
|
2541
|
-
drawPositionPill(`Stop ${formatPrice(
|
|
2542
|
-
let
|
|
2544
|
+
drawPositionPill(`Target: ${formatPrice(targetDist)} (${pctOfDist(targetDist)})${ticksOfDist(targetDist)}${targetAmountText}`, cx, targetY, profitLine);
|
|
2545
|
+
drawPositionPill(`Stop: ${formatPrice(stopDist)} (${pctOfDist(stopDist)})${ticksOfDist(stopDist)}${stopAmountText}`, cx, stopY, lossLine);
|
|
2546
|
+
let centerLines;
|
|
2543
2547
|
let centerBg;
|
|
2544
2548
|
if (positionHit) {
|
|
2545
|
-
const
|
|
2546
|
-
const
|
|
2547
|
-
|
|
2549
|
+
const pnlPoints = positionHit.profit ? targetDist : -stopDist;
|
|
2550
|
+
const line1 = hasMoney ? `Closed P&L: ${formatPrice(pnlPoints)}, Qty: ${qtyText}` : `Closed P&L: ${formatPrice(pnlPoints)}`;
|
|
2551
|
+
centerLines = [line1, `Risk/reward ratio: ${rr.toFixed(2)}`];
|
|
2548
2552
|
centerBg = positionHit.profit ? profitLine : lossLine;
|
|
2549
2553
|
} else {
|
|
2550
|
-
|
|
2554
|
+
const line1 = hasMoney ? `Entry: ${formatPrice(entry.price)}, Qty: ${qtyText}` : `Entry: ${formatPrice(entry.price)}`;
|
|
2555
|
+
centerLines = [line1, `Risk/reward ratio: ${rr.toFixed(2)}`];
|
|
2551
2556
|
centerBg = hexToRgba(drawing.color, 0.92);
|
|
2552
2557
|
}
|
|
2553
|
-
drawPositionPill(
|
|
2558
|
+
drawPositionPill(centerLines, cx, entryY, centerBg);
|
|
2554
2559
|
if (drawing.label) {
|
|
2555
2560
|
drawDrawingLabel(drawing.label, cx, Math.min(targetY, stopY) - 4, drawing.color);
|
|
2556
2561
|
}
|
|
2557
2562
|
}
|
|
2558
2563
|
}
|
|
2564
|
+
} else if (drawing.type === "price-range") {
|
|
2565
|
+
const p0 = drawing.points[0];
|
|
2566
|
+
const p1 = drawing.points[1];
|
|
2567
|
+
if (p0 && p1) {
|
|
2568
|
+
const px0 = xFromDrawingPoint(p0);
|
|
2569
|
+
const px1 = xFromDrawingPoint(p1);
|
|
2570
|
+
const leftX = Math.min(px0, px1);
|
|
2571
|
+
const rightX = Math.max(px0, px1);
|
|
2572
|
+
const boxW = Math.max(1, rightX - leftX);
|
|
2573
|
+
const y0 = yFromPrice(p0.price);
|
|
2574
|
+
const y1 = yFromPrice(p1.price);
|
|
2575
|
+
const topY = Math.min(y0, y1);
|
|
2576
|
+
const botY = Math.max(y0, y1);
|
|
2577
|
+
ctx.save();
|
|
2578
|
+
ctx.globalAlpha = draft ? 0.6 : 1;
|
|
2579
|
+
ctx.fillStyle = hexToRgba(drawing.color, 0.14);
|
|
2580
|
+
ctx.fillRect(leftX, topY, boxW, Math.max(0, botY - topY));
|
|
2581
|
+
ctx.setLineDash([]);
|
|
2582
|
+
ctx.lineWidth = Math.max(1, drawing.width);
|
|
2583
|
+
ctx.strokeStyle = drawing.color;
|
|
2584
|
+
ctx.beginPath();
|
|
2585
|
+
ctx.moveTo(crisp(leftX), crisp(topY));
|
|
2586
|
+
ctx.lineTo(crisp(rightX), crisp(topY));
|
|
2587
|
+
ctx.moveTo(crisp(leftX), crisp(botY));
|
|
2588
|
+
ctx.lineTo(crisp(rightX), crisp(botY));
|
|
2589
|
+
ctx.stroke();
|
|
2590
|
+
const midX = (leftX + rightX) / 2;
|
|
2591
|
+
const down = p1.price <= p0.price;
|
|
2592
|
+
const arrowToY = down ? botY : topY;
|
|
2593
|
+
ctx.beginPath();
|
|
2594
|
+
ctx.moveTo(crisp(midX), crisp(down ? topY : botY));
|
|
2595
|
+
ctx.lineTo(crisp(midX), crisp(arrowToY));
|
|
2596
|
+
ctx.stroke();
|
|
2597
|
+
const head = 6;
|
|
2598
|
+
ctx.beginPath();
|
|
2599
|
+
ctx.moveTo(midX, arrowToY);
|
|
2600
|
+
ctx.lineTo(midX - head, arrowToY + (down ? -head : head));
|
|
2601
|
+
ctx.moveTo(midX, arrowToY);
|
|
2602
|
+
ctx.lineTo(midX + head, arrowToY + (down ? -head : head));
|
|
2603
|
+
ctx.stroke();
|
|
2604
|
+
ctx.restore();
|
|
2605
|
+
handleAt(px0, y0, drawing.color);
|
|
2606
|
+
handleAt(px1, y1, drawing.color);
|
|
2607
|
+
const tick = getConfiguredTickSize();
|
|
2608
|
+
const diff = p1.price - p0.price;
|
|
2609
|
+
const base = Math.abs(p0.price) > 0 ? Math.abs(p0.price) : 1;
|
|
2610
|
+
const pct = diff / base * 100;
|
|
2611
|
+
const ticks = tick > 0 ? Math.round(diff / tick) : 0;
|
|
2612
|
+
const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
|
|
2613
|
+
const labelText = 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)}%`)})`;
|
|
2614
|
+
const prevFont = ctx.font;
|
|
2615
|
+
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2616
|
+
const padding = 6;
|
|
2617
|
+
const textW = ctx.measureText(labelText).width;
|
|
2618
|
+
const pillW = textW + padding * 2;
|
|
2619
|
+
const pillH = 18;
|
|
2620
|
+
const pillX = midX - pillW / 2;
|
|
2621
|
+
const pillY = botY + 6;
|
|
2622
|
+
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2623
|
+
fillRoundedRect(pillX, pillY, pillW, pillH, 4);
|
|
2624
|
+
ctx.save();
|
|
2625
|
+
ctx.strokeStyle = hexToRgba(drawing.color, 0.5);
|
|
2626
|
+
ctx.lineWidth = 1;
|
|
2627
|
+
ctx.strokeRect(crisp(pillX), crisp(pillY), pillW, pillH);
|
|
2628
|
+
ctx.restore();
|
|
2629
|
+
ctx.fillStyle = drawing.color;
|
|
2630
|
+
ctx.textAlign = "center";
|
|
2631
|
+
ctx.textBaseline = "middle";
|
|
2632
|
+
ctx.fillText(labelText, midX, pillY + pillH / 2);
|
|
2633
|
+
ctx.font = prevFont;
|
|
2634
|
+
if (drawing.label) {
|
|
2635
|
+
drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2559
2638
|
}
|
|
2560
2639
|
ctx.restore();
|
|
2561
2640
|
};
|
|
@@ -3791,6 +3870,19 @@ function createChart(element, options = {}) {
|
|
|
3791
3870
|
if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
|
|
3792
3871
|
return { drawing, target: "line" };
|
|
3793
3872
|
}
|
|
3873
|
+
} else if (drawing.type === "price-range") {
|
|
3874
|
+
const p0 = drawing.points[0];
|
|
3875
|
+
const p1 = drawing.points[1];
|
|
3876
|
+
if (!p0 || !p1) continue;
|
|
3877
|
+
const px0 = canvasXFromDrawingPoint(p0);
|
|
3878
|
+
const px1 = canvasXFromDrawingPoint(p1);
|
|
3879
|
+
const y0 = canvasYFromDrawingPrice(p0.price);
|
|
3880
|
+
const y1 = canvasYFromDrawingPrice(p1.price);
|
|
3881
|
+
if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
|
|
3882
|
+
if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
|
|
3883
|
+
if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
|
|
3884
|
+
return { drawing, target: "line" };
|
|
3885
|
+
}
|
|
3794
3886
|
}
|
|
3795
3887
|
}
|
|
3796
3888
|
return null;
|
|
@@ -3991,6 +4083,25 @@ function createChart(element, options = {}) {
|
|
|
3991
4083
|
draw();
|
|
3992
4084
|
return true;
|
|
3993
4085
|
}
|
|
4086
|
+
if (activeDrawingTool === "price-range") {
|
|
4087
|
+
const tick = getConfiguredTickSize();
|
|
4088
|
+
const visibleRange = drawState.yMax - drawState.yMin;
|
|
4089
|
+
const priceOffset = visibleRange > 0 ? visibleRange * 0.18 : tick > 0 ? tick * 80 : Math.abs(point.price) * 0.03 || 1;
|
|
4090
|
+
const width2 = Math.max(8, Math.round(xSpan * 0.2));
|
|
4091
|
+
const defaults = getDrawingToolDefaults(activeDrawingTool);
|
|
4092
|
+
drawings.push(
|
|
4093
|
+
normalizeDrawingState({
|
|
4094
|
+
type: activeDrawingTool,
|
|
4095
|
+
points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
|
|
4096
|
+
color: defaults.color ?? "#2962ff",
|
|
4097
|
+
style: defaults.style ?? "solid",
|
|
4098
|
+
width: defaults.width ?? 1
|
|
4099
|
+
})
|
|
4100
|
+
);
|
|
4101
|
+
emitDrawingsChange();
|
|
4102
|
+
draw();
|
|
4103
|
+
return true;
|
|
4104
|
+
}
|
|
3994
4105
|
if (activeDrawingTool === "long-position" || activeDrawingTool === "short-position") {
|
|
3995
4106
|
const isLong = activeDrawingTool === "long-position";
|
|
3996
4107
|
const tick = getConfiguredTickSize();
|
|
@@ -44,7 +44,7 @@ interface ChartOptions {
|
|
|
44
44
|
drawings?: DrawingObjectOptions[];
|
|
45
45
|
}
|
|
46
46
|
type IndicatorPane = "overlay" | "separate";
|
|
47
|
-
type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position";
|
|
47
|
+
type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range";
|
|
48
48
|
interface DrawingPoint {
|
|
49
49
|
index: number;
|
|
50
50
|
price: number;
|
|
@@ -2477,11 +2477,12 @@ function createChart(element, options = {}) {
|
|
|
2477
2477
|
handleAt(leftX, stopY, drawing.color);
|
|
2478
2478
|
handleAt(rightX, entryY, drawing.color);
|
|
2479
2479
|
const tick = getConfiguredTickSize();
|
|
2480
|
-
const
|
|
2480
|
+
const pctOfDist = (dist) => {
|
|
2481
2481
|
if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
|
|
2482
|
-
|
|
2482
|
+
const pct = dist / Math.abs(entry.price) * 100;
|
|
2483
|
+
return `${pct < 1 ? pct.toFixed(3) : pct.toFixed(2)}%`;
|
|
2483
2484
|
};
|
|
2484
|
-
const
|
|
2485
|
+
const ticksOfDist = (dist) => tick > 0 ? ` ${Math.round(dist / tick)}` : "";
|
|
2485
2486
|
const targetDist = Math.abs(target.price - entry.price);
|
|
2486
2487
|
const stopDist = Math.abs(entry.price - stop.price);
|
|
2487
2488
|
const rr = stopDist > 0 ? targetDist / stopDist : 0;
|
|
@@ -2491,16 +2492,18 @@ function createChart(element, options = {}) {
|
|
|
2491
2492
|
const qtyRaw = riskAmount > 0 && stopDist > 0 ? riskAmount / (stopDist * effectivePointValue) : 0;
|
|
2492
2493
|
const hasMoney = qtyRaw > 0 && Number.isFinite(qtyRaw);
|
|
2493
2494
|
const qtyText = hasMoney ? qtyRaw.toFixed(Math.max(0, drawing.qtyPrecision)) : "";
|
|
2494
|
-
const formatAmount = (value) =>
|
|
2495
|
-
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2496
|
-
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2495
|
+
const formatAmount = (value) => value.toFixed(2);
|
|
2496
|
+
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * target.price * effectivePointValue)}` : "";
|
|
2497
|
+
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * stop.price * effectivePointValue)}` : "";
|
|
2497
2498
|
const drawPositionPill = (text, centerX, centerY, bg) => {
|
|
2499
|
+
const lines = Array.isArray(text) ? text : [text];
|
|
2498
2500
|
const prevFont = ctx.font;
|
|
2499
2501
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2500
2502
|
const padding = 6;
|
|
2501
|
-
const
|
|
2503
|
+
const lineH = 14;
|
|
2504
|
+
const textW = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
2502
2505
|
const pillW = textW + padding * 2;
|
|
2503
|
-
const pillH = 18;
|
|
2506
|
+
const pillH = lines.length === 1 ? 18 : lines.length * lineH + 6;
|
|
2504
2507
|
const pillX = centerX - pillW / 2;
|
|
2505
2508
|
const pillY = centerY - pillH / 2;
|
|
2506
2509
|
ctx.fillStyle = bg;
|
|
@@ -2508,28 +2511,104 @@ function createChart(element, options = {}) {
|
|
|
2508
2511
|
ctx.fillStyle = labelTextColor;
|
|
2509
2512
|
ctx.textAlign = "center";
|
|
2510
2513
|
ctx.textBaseline = "middle";
|
|
2511
|
-
|
|
2514
|
+
const startY = pillY + pillH / 2 - (lines.length - 1) * lineH / 2;
|
|
2515
|
+
lines.forEach((line, lineIndex) => ctx.fillText(line, centerX, startY + lineIndex * lineH));
|
|
2512
2516
|
ctx.font = prevFont;
|
|
2513
2517
|
};
|
|
2514
|
-
drawPositionPill(`Target ${formatPrice(
|
|
2515
|
-
drawPositionPill(`Stop ${formatPrice(
|
|
2516
|
-
let
|
|
2518
|
+
drawPositionPill(`Target: ${formatPrice(targetDist)} (${pctOfDist(targetDist)})${ticksOfDist(targetDist)}${targetAmountText}`, cx, targetY, profitLine);
|
|
2519
|
+
drawPositionPill(`Stop: ${formatPrice(stopDist)} (${pctOfDist(stopDist)})${ticksOfDist(stopDist)}${stopAmountText}`, cx, stopY, lossLine);
|
|
2520
|
+
let centerLines;
|
|
2517
2521
|
let centerBg;
|
|
2518
2522
|
if (positionHit) {
|
|
2519
|
-
const
|
|
2520
|
-
const
|
|
2521
|
-
|
|
2523
|
+
const pnlPoints = positionHit.profit ? targetDist : -stopDist;
|
|
2524
|
+
const line1 = hasMoney ? `Closed P&L: ${formatPrice(pnlPoints)}, Qty: ${qtyText}` : `Closed P&L: ${formatPrice(pnlPoints)}`;
|
|
2525
|
+
centerLines = [line1, `Risk/reward ratio: ${rr.toFixed(2)}`];
|
|
2522
2526
|
centerBg = positionHit.profit ? profitLine : lossLine;
|
|
2523
2527
|
} else {
|
|
2524
|
-
|
|
2528
|
+
const line1 = hasMoney ? `Entry: ${formatPrice(entry.price)}, Qty: ${qtyText}` : `Entry: ${formatPrice(entry.price)}`;
|
|
2529
|
+
centerLines = [line1, `Risk/reward ratio: ${rr.toFixed(2)}`];
|
|
2525
2530
|
centerBg = hexToRgba(drawing.color, 0.92);
|
|
2526
2531
|
}
|
|
2527
|
-
drawPositionPill(
|
|
2532
|
+
drawPositionPill(centerLines, cx, entryY, centerBg);
|
|
2528
2533
|
if (drawing.label) {
|
|
2529
2534
|
drawDrawingLabel(drawing.label, cx, Math.min(targetY, stopY) - 4, drawing.color);
|
|
2530
2535
|
}
|
|
2531
2536
|
}
|
|
2532
2537
|
}
|
|
2538
|
+
} else if (drawing.type === "price-range") {
|
|
2539
|
+
const p0 = drawing.points[0];
|
|
2540
|
+
const p1 = drawing.points[1];
|
|
2541
|
+
if (p0 && p1) {
|
|
2542
|
+
const px0 = xFromDrawingPoint(p0);
|
|
2543
|
+
const px1 = xFromDrawingPoint(p1);
|
|
2544
|
+
const leftX = Math.min(px0, px1);
|
|
2545
|
+
const rightX = Math.max(px0, px1);
|
|
2546
|
+
const boxW = Math.max(1, rightX - leftX);
|
|
2547
|
+
const y0 = yFromPrice(p0.price);
|
|
2548
|
+
const y1 = yFromPrice(p1.price);
|
|
2549
|
+
const topY = Math.min(y0, y1);
|
|
2550
|
+
const botY = Math.max(y0, y1);
|
|
2551
|
+
ctx.save();
|
|
2552
|
+
ctx.globalAlpha = draft ? 0.6 : 1;
|
|
2553
|
+
ctx.fillStyle = hexToRgba(drawing.color, 0.14);
|
|
2554
|
+
ctx.fillRect(leftX, topY, boxW, Math.max(0, botY - topY));
|
|
2555
|
+
ctx.setLineDash([]);
|
|
2556
|
+
ctx.lineWidth = Math.max(1, drawing.width);
|
|
2557
|
+
ctx.strokeStyle = drawing.color;
|
|
2558
|
+
ctx.beginPath();
|
|
2559
|
+
ctx.moveTo(crisp(leftX), crisp(topY));
|
|
2560
|
+
ctx.lineTo(crisp(rightX), crisp(topY));
|
|
2561
|
+
ctx.moveTo(crisp(leftX), crisp(botY));
|
|
2562
|
+
ctx.lineTo(crisp(rightX), crisp(botY));
|
|
2563
|
+
ctx.stroke();
|
|
2564
|
+
const midX = (leftX + rightX) / 2;
|
|
2565
|
+
const down = p1.price <= p0.price;
|
|
2566
|
+
const arrowToY = down ? botY : topY;
|
|
2567
|
+
ctx.beginPath();
|
|
2568
|
+
ctx.moveTo(crisp(midX), crisp(down ? topY : botY));
|
|
2569
|
+
ctx.lineTo(crisp(midX), crisp(arrowToY));
|
|
2570
|
+
ctx.stroke();
|
|
2571
|
+
const head = 6;
|
|
2572
|
+
ctx.beginPath();
|
|
2573
|
+
ctx.moveTo(midX, arrowToY);
|
|
2574
|
+
ctx.lineTo(midX - head, arrowToY + (down ? -head : head));
|
|
2575
|
+
ctx.moveTo(midX, arrowToY);
|
|
2576
|
+
ctx.lineTo(midX + head, arrowToY + (down ? -head : head));
|
|
2577
|
+
ctx.stroke();
|
|
2578
|
+
ctx.restore();
|
|
2579
|
+
handleAt(px0, y0, drawing.color);
|
|
2580
|
+
handleAt(px1, y1, drawing.color);
|
|
2581
|
+
const tick = getConfiguredTickSize();
|
|
2582
|
+
const diff = p1.price - p0.price;
|
|
2583
|
+
const base = Math.abs(p0.price) > 0 ? Math.abs(p0.price) : 1;
|
|
2584
|
+
const pct = diff / base * 100;
|
|
2585
|
+
const ticks = tick > 0 ? Math.round(diff / tick) : 0;
|
|
2586
|
+
const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
|
|
2587
|
+
const labelText = 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)}%`)})`;
|
|
2588
|
+
const prevFont = ctx.font;
|
|
2589
|
+
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2590
|
+
const padding = 6;
|
|
2591
|
+
const textW = ctx.measureText(labelText).width;
|
|
2592
|
+
const pillW = textW + padding * 2;
|
|
2593
|
+
const pillH = 18;
|
|
2594
|
+
const pillX = midX - pillW / 2;
|
|
2595
|
+
const pillY = botY + 6;
|
|
2596
|
+
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2597
|
+
fillRoundedRect(pillX, pillY, pillW, pillH, 4);
|
|
2598
|
+
ctx.save();
|
|
2599
|
+
ctx.strokeStyle = hexToRgba(drawing.color, 0.5);
|
|
2600
|
+
ctx.lineWidth = 1;
|
|
2601
|
+
ctx.strokeRect(crisp(pillX), crisp(pillY), pillW, pillH);
|
|
2602
|
+
ctx.restore();
|
|
2603
|
+
ctx.fillStyle = drawing.color;
|
|
2604
|
+
ctx.textAlign = "center";
|
|
2605
|
+
ctx.textBaseline = "middle";
|
|
2606
|
+
ctx.fillText(labelText, midX, pillY + pillH / 2);
|
|
2607
|
+
ctx.font = prevFont;
|
|
2608
|
+
if (drawing.label) {
|
|
2609
|
+
drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2533
2612
|
}
|
|
2534
2613
|
ctx.restore();
|
|
2535
2614
|
};
|
|
@@ -3765,6 +3844,19 @@ function createChart(element, options = {}) {
|
|
|
3765
3844
|
if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
|
|
3766
3845
|
return { drawing, target: "line" };
|
|
3767
3846
|
}
|
|
3847
|
+
} else if (drawing.type === "price-range") {
|
|
3848
|
+
const p0 = drawing.points[0];
|
|
3849
|
+
const p1 = drawing.points[1];
|
|
3850
|
+
if (!p0 || !p1) continue;
|
|
3851
|
+
const px0 = canvasXFromDrawingPoint(p0);
|
|
3852
|
+
const px1 = canvasXFromDrawingPoint(p1);
|
|
3853
|
+
const y0 = canvasYFromDrawingPrice(p0.price);
|
|
3854
|
+
const y1 = canvasYFromDrawingPrice(p1.price);
|
|
3855
|
+
if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
|
|
3856
|
+
if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
|
|
3857
|
+
if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
|
|
3858
|
+
return { drawing, target: "line" };
|
|
3859
|
+
}
|
|
3768
3860
|
}
|
|
3769
3861
|
}
|
|
3770
3862
|
return null;
|
|
@@ -3965,6 +4057,25 @@ function createChart(element, options = {}) {
|
|
|
3965
4057
|
draw();
|
|
3966
4058
|
return true;
|
|
3967
4059
|
}
|
|
4060
|
+
if (activeDrawingTool === "price-range") {
|
|
4061
|
+
const tick = getConfiguredTickSize();
|
|
4062
|
+
const visibleRange = drawState.yMax - drawState.yMin;
|
|
4063
|
+
const priceOffset = visibleRange > 0 ? visibleRange * 0.18 : tick > 0 ? tick * 80 : Math.abs(point.price) * 0.03 || 1;
|
|
4064
|
+
const width2 = Math.max(8, Math.round(xSpan * 0.2));
|
|
4065
|
+
const defaults = getDrawingToolDefaults(activeDrawingTool);
|
|
4066
|
+
drawings.push(
|
|
4067
|
+
normalizeDrawingState({
|
|
4068
|
+
type: activeDrawingTool,
|
|
4069
|
+
points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
|
|
4070
|
+
color: defaults.color ?? "#2962ff",
|
|
4071
|
+
style: defaults.style ?? "solid",
|
|
4072
|
+
width: defaults.width ?? 1
|
|
4073
|
+
})
|
|
4074
|
+
);
|
|
4075
|
+
emitDrawingsChange();
|
|
4076
|
+
draw();
|
|
4077
|
+
return true;
|
|
4078
|
+
}
|
|
3968
4079
|
if (activeDrawingTool === "long-position" || activeDrawingTool === "short-position") {
|
|
3969
4080
|
const isLong = activeDrawingTool === "long-position";
|
|
3970
4081
|
const tick = getConfiguredTickSize();
|
package/dist/index.cjs
CHANGED
|
@@ -2503,11 +2503,12 @@ function createChart(element, options = {}) {
|
|
|
2503
2503
|
handleAt(leftX, stopY, drawing.color);
|
|
2504
2504
|
handleAt(rightX, entryY, drawing.color);
|
|
2505
2505
|
const tick = getConfiguredTickSize();
|
|
2506
|
-
const
|
|
2506
|
+
const pctOfDist = (dist) => {
|
|
2507
2507
|
if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
|
|
2508
|
-
|
|
2508
|
+
const pct = dist / Math.abs(entry.price) * 100;
|
|
2509
|
+
return `${pct < 1 ? pct.toFixed(3) : pct.toFixed(2)}%`;
|
|
2509
2510
|
};
|
|
2510
|
-
const
|
|
2511
|
+
const ticksOfDist = (dist) => tick > 0 ? ` ${Math.round(dist / tick)}` : "";
|
|
2511
2512
|
const targetDist = Math.abs(target.price - entry.price);
|
|
2512
2513
|
const stopDist = Math.abs(entry.price - stop.price);
|
|
2513
2514
|
const rr = stopDist > 0 ? targetDist / stopDist : 0;
|
|
@@ -2517,16 +2518,18 @@ function createChart(element, options = {}) {
|
|
|
2517
2518
|
const qtyRaw = riskAmount > 0 && stopDist > 0 ? riskAmount / (stopDist * effectivePointValue) : 0;
|
|
2518
2519
|
const hasMoney = qtyRaw > 0 && Number.isFinite(qtyRaw);
|
|
2519
2520
|
const qtyText = hasMoney ? qtyRaw.toFixed(Math.max(0, drawing.qtyPrecision)) : "";
|
|
2520
|
-
const formatAmount = (value) =>
|
|
2521
|
-
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2522
|
-
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2521
|
+
const formatAmount = (value) => value.toFixed(2);
|
|
2522
|
+
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * target.price * effectivePointValue)}` : "";
|
|
2523
|
+
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * stop.price * effectivePointValue)}` : "";
|
|
2523
2524
|
const drawPositionPill = (text, centerX, centerY, bg) => {
|
|
2525
|
+
const lines = Array.isArray(text) ? text : [text];
|
|
2524
2526
|
const prevFont = ctx.font;
|
|
2525
2527
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2526
2528
|
const padding = 6;
|
|
2527
|
-
const
|
|
2529
|
+
const lineH = 14;
|
|
2530
|
+
const textW = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
2528
2531
|
const pillW = textW + padding * 2;
|
|
2529
|
-
const pillH = 18;
|
|
2532
|
+
const pillH = lines.length === 1 ? 18 : lines.length * lineH + 6;
|
|
2530
2533
|
const pillX = centerX - pillW / 2;
|
|
2531
2534
|
const pillY = centerY - pillH / 2;
|
|
2532
2535
|
ctx.fillStyle = bg;
|
|
@@ -2534,28 +2537,104 @@ function createChart(element, options = {}) {
|
|
|
2534
2537
|
ctx.fillStyle = labelTextColor;
|
|
2535
2538
|
ctx.textAlign = "center";
|
|
2536
2539
|
ctx.textBaseline = "middle";
|
|
2537
|
-
|
|
2540
|
+
const startY = pillY + pillH / 2 - (lines.length - 1) * lineH / 2;
|
|
2541
|
+
lines.forEach((line, lineIndex) => ctx.fillText(line, centerX, startY + lineIndex * lineH));
|
|
2538
2542
|
ctx.font = prevFont;
|
|
2539
2543
|
};
|
|
2540
|
-
drawPositionPill(`Target ${formatPrice(
|
|
2541
|
-
drawPositionPill(`Stop ${formatPrice(
|
|
2542
|
-
let
|
|
2544
|
+
drawPositionPill(`Target: ${formatPrice(targetDist)} (${pctOfDist(targetDist)})${ticksOfDist(targetDist)}${targetAmountText}`, cx, targetY, profitLine);
|
|
2545
|
+
drawPositionPill(`Stop: ${formatPrice(stopDist)} (${pctOfDist(stopDist)})${ticksOfDist(stopDist)}${stopAmountText}`, cx, stopY, lossLine);
|
|
2546
|
+
let centerLines;
|
|
2543
2547
|
let centerBg;
|
|
2544
2548
|
if (positionHit) {
|
|
2545
|
-
const
|
|
2546
|
-
const
|
|
2547
|
-
|
|
2549
|
+
const pnlPoints = positionHit.profit ? targetDist : -stopDist;
|
|
2550
|
+
const line1 = hasMoney ? `Closed P&L: ${formatPrice(pnlPoints)}, Qty: ${qtyText}` : `Closed P&L: ${formatPrice(pnlPoints)}`;
|
|
2551
|
+
centerLines = [line1, `Risk/reward ratio: ${rr.toFixed(2)}`];
|
|
2548
2552
|
centerBg = positionHit.profit ? profitLine : lossLine;
|
|
2549
2553
|
} else {
|
|
2550
|
-
|
|
2554
|
+
const line1 = hasMoney ? `Entry: ${formatPrice(entry.price)}, Qty: ${qtyText}` : `Entry: ${formatPrice(entry.price)}`;
|
|
2555
|
+
centerLines = [line1, `Risk/reward ratio: ${rr.toFixed(2)}`];
|
|
2551
2556
|
centerBg = hexToRgba(drawing.color, 0.92);
|
|
2552
2557
|
}
|
|
2553
|
-
drawPositionPill(
|
|
2558
|
+
drawPositionPill(centerLines, cx, entryY, centerBg);
|
|
2554
2559
|
if (drawing.label) {
|
|
2555
2560
|
drawDrawingLabel(drawing.label, cx, Math.min(targetY, stopY) - 4, drawing.color);
|
|
2556
2561
|
}
|
|
2557
2562
|
}
|
|
2558
2563
|
}
|
|
2564
|
+
} else if (drawing.type === "price-range") {
|
|
2565
|
+
const p0 = drawing.points[0];
|
|
2566
|
+
const p1 = drawing.points[1];
|
|
2567
|
+
if (p0 && p1) {
|
|
2568
|
+
const px0 = xFromDrawingPoint(p0);
|
|
2569
|
+
const px1 = xFromDrawingPoint(p1);
|
|
2570
|
+
const leftX = Math.min(px0, px1);
|
|
2571
|
+
const rightX = Math.max(px0, px1);
|
|
2572
|
+
const boxW = Math.max(1, rightX - leftX);
|
|
2573
|
+
const y0 = yFromPrice(p0.price);
|
|
2574
|
+
const y1 = yFromPrice(p1.price);
|
|
2575
|
+
const topY = Math.min(y0, y1);
|
|
2576
|
+
const botY = Math.max(y0, y1);
|
|
2577
|
+
ctx.save();
|
|
2578
|
+
ctx.globalAlpha = draft ? 0.6 : 1;
|
|
2579
|
+
ctx.fillStyle = hexToRgba(drawing.color, 0.14);
|
|
2580
|
+
ctx.fillRect(leftX, topY, boxW, Math.max(0, botY - topY));
|
|
2581
|
+
ctx.setLineDash([]);
|
|
2582
|
+
ctx.lineWidth = Math.max(1, drawing.width);
|
|
2583
|
+
ctx.strokeStyle = drawing.color;
|
|
2584
|
+
ctx.beginPath();
|
|
2585
|
+
ctx.moveTo(crisp(leftX), crisp(topY));
|
|
2586
|
+
ctx.lineTo(crisp(rightX), crisp(topY));
|
|
2587
|
+
ctx.moveTo(crisp(leftX), crisp(botY));
|
|
2588
|
+
ctx.lineTo(crisp(rightX), crisp(botY));
|
|
2589
|
+
ctx.stroke();
|
|
2590
|
+
const midX = (leftX + rightX) / 2;
|
|
2591
|
+
const down = p1.price <= p0.price;
|
|
2592
|
+
const arrowToY = down ? botY : topY;
|
|
2593
|
+
ctx.beginPath();
|
|
2594
|
+
ctx.moveTo(crisp(midX), crisp(down ? topY : botY));
|
|
2595
|
+
ctx.lineTo(crisp(midX), crisp(arrowToY));
|
|
2596
|
+
ctx.stroke();
|
|
2597
|
+
const head = 6;
|
|
2598
|
+
ctx.beginPath();
|
|
2599
|
+
ctx.moveTo(midX, arrowToY);
|
|
2600
|
+
ctx.lineTo(midX - head, arrowToY + (down ? -head : head));
|
|
2601
|
+
ctx.moveTo(midX, arrowToY);
|
|
2602
|
+
ctx.lineTo(midX + head, arrowToY + (down ? -head : head));
|
|
2603
|
+
ctx.stroke();
|
|
2604
|
+
ctx.restore();
|
|
2605
|
+
handleAt(px0, y0, drawing.color);
|
|
2606
|
+
handleAt(px1, y1, drawing.color);
|
|
2607
|
+
const tick = getConfiguredTickSize();
|
|
2608
|
+
const diff = p1.price - p0.price;
|
|
2609
|
+
const base = Math.abs(p0.price) > 0 ? Math.abs(p0.price) : 1;
|
|
2610
|
+
const pct = diff / base * 100;
|
|
2611
|
+
const ticks = tick > 0 ? Math.round(diff / tick) : 0;
|
|
2612
|
+
const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
|
|
2613
|
+
const labelText = 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)}%`)})`;
|
|
2614
|
+
const prevFont = ctx.font;
|
|
2615
|
+
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2616
|
+
const padding = 6;
|
|
2617
|
+
const textW = ctx.measureText(labelText).width;
|
|
2618
|
+
const pillW = textW + padding * 2;
|
|
2619
|
+
const pillH = 18;
|
|
2620
|
+
const pillX = midX - pillW / 2;
|
|
2621
|
+
const pillY = botY + 6;
|
|
2622
|
+
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2623
|
+
fillRoundedRect(pillX, pillY, pillW, pillH, 4);
|
|
2624
|
+
ctx.save();
|
|
2625
|
+
ctx.strokeStyle = hexToRgba(drawing.color, 0.5);
|
|
2626
|
+
ctx.lineWidth = 1;
|
|
2627
|
+
ctx.strokeRect(crisp(pillX), crisp(pillY), pillW, pillH);
|
|
2628
|
+
ctx.restore();
|
|
2629
|
+
ctx.fillStyle = drawing.color;
|
|
2630
|
+
ctx.textAlign = "center";
|
|
2631
|
+
ctx.textBaseline = "middle";
|
|
2632
|
+
ctx.fillText(labelText, midX, pillY + pillH / 2);
|
|
2633
|
+
ctx.font = prevFont;
|
|
2634
|
+
if (drawing.label) {
|
|
2635
|
+
drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2559
2638
|
}
|
|
2560
2639
|
ctx.restore();
|
|
2561
2640
|
};
|
|
@@ -3791,6 +3870,19 @@ function createChart(element, options = {}) {
|
|
|
3791
3870
|
if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
|
|
3792
3871
|
return { drawing, target: "line" };
|
|
3793
3872
|
}
|
|
3873
|
+
} else if (drawing.type === "price-range") {
|
|
3874
|
+
const p0 = drawing.points[0];
|
|
3875
|
+
const p1 = drawing.points[1];
|
|
3876
|
+
if (!p0 || !p1) continue;
|
|
3877
|
+
const px0 = canvasXFromDrawingPoint(p0);
|
|
3878
|
+
const px1 = canvasXFromDrawingPoint(p1);
|
|
3879
|
+
const y0 = canvasYFromDrawingPrice(p0.price);
|
|
3880
|
+
const y1 = canvasYFromDrawingPrice(p1.price);
|
|
3881
|
+
if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
|
|
3882
|
+
if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
|
|
3883
|
+
if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
|
|
3884
|
+
return { drawing, target: "line" };
|
|
3885
|
+
}
|
|
3794
3886
|
}
|
|
3795
3887
|
}
|
|
3796
3888
|
return null;
|
|
@@ -3991,6 +4083,25 @@ function createChart(element, options = {}) {
|
|
|
3991
4083
|
draw();
|
|
3992
4084
|
return true;
|
|
3993
4085
|
}
|
|
4086
|
+
if (activeDrawingTool === "price-range") {
|
|
4087
|
+
const tick = getConfiguredTickSize();
|
|
4088
|
+
const visibleRange = drawState.yMax - drawState.yMin;
|
|
4089
|
+
const priceOffset = visibleRange > 0 ? visibleRange * 0.18 : tick > 0 ? tick * 80 : Math.abs(point.price) * 0.03 || 1;
|
|
4090
|
+
const width2 = Math.max(8, Math.round(xSpan * 0.2));
|
|
4091
|
+
const defaults = getDrawingToolDefaults(activeDrawingTool);
|
|
4092
|
+
drawings.push(
|
|
4093
|
+
normalizeDrawingState({
|
|
4094
|
+
type: activeDrawingTool,
|
|
4095
|
+
points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
|
|
4096
|
+
color: defaults.color ?? "#2962ff",
|
|
4097
|
+
style: defaults.style ?? "solid",
|
|
4098
|
+
width: defaults.width ?? 1
|
|
4099
|
+
})
|
|
4100
|
+
);
|
|
4101
|
+
emitDrawingsChange();
|
|
4102
|
+
draw();
|
|
4103
|
+
return true;
|
|
4104
|
+
}
|
|
3994
4105
|
if (activeDrawingTool === "long-position" || activeDrawingTool === "short-position") {
|
|
3995
4106
|
const isLong = activeDrawingTool === "long-position";
|
|
3996
4107
|
const tick = getConfiguredTickSize();
|
package/dist/index.d.cts
CHANGED
|
@@ -44,7 +44,7 @@ interface ChartOptions {
|
|
|
44
44
|
drawings?: DrawingObjectOptions[];
|
|
45
45
|
}
|
|
46
46
|
type IndicatorPane = "overlay" | "separate";
|
|
47
|
-
type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position";
|
|
47
|
+
type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range";
|
|
48
48
|
interface DrawingPoint {
|
|
49
49
|
index: number;
|
|
50
50
|
price: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ interface ChartOptions {
|
|
|
44
44
|
drawings?: DrawingObjectOptions[];
|
|
45
45
|
}
|
|
46
46
|
type IndicatorPane = "overlay" | "separate";
|
|
47
|
-
type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position";
|
|
47
|
+
type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range";
|
|
48
48
|
interface DrawingPoint {
|
|
49
49
|
index: number;
|
|
50
50
|
price: number;
|
package/dist/index.js
CHANGED
|
@@ -2477,11 +2477,12 @@ function createChart(element, options = {}) {
|
|
|
2477
2477
|
handleAt(leftX, stopY, drawing.color);
|
|
2478
2478
|
handleAt(rightX, entryY, drawing.color);
|
|
2479
2479
|
const tick = getConfiguredTickSize();
|
|
2480
|
-
const
|
|
2480
|
+
const pctOfDist = (dist) => {
|
|
2481
2481
|
if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
|
|
2482
|
-
|
|
2482
|
+
const pct = dist / Math.abs(entry.price) * 100;
|
|
2483
|
+
return `${pct < 1 ? pct.toFixed(3) : pct.toFixed(2)}%`;
|
|
2483
2484
|
};
|
|
2484
|
-
const
|
|
2485
|
+
const ticksOfDist = (dist) => tick > 0 ? ` ${Math.round(dist / tick)}` : "";
|
|
2485
2486
|
const targetDist = Math.abs(target.price - entry.price);
|
|
2486
2487
|
const stopDist = Math.abs(entry.price - stop.price);
|
|
2487
2488
|
const rr = stopDist > 0 ? targetDist / stopDist : 0;
|
|
@@ -2491,16 +2492,18 @@ function createChart(element, options = {}) {
|
|
|
2491
2492
|
const qtyRaw = riskAmount > 0 && stopDist > 0 ? riskAmount / (stopDist * effectivePointValue) : 0;
|
|
2492
2493
|
const hasMoney = qtyRaw > 0 && Number.isFinite(qtyRaw);
|
|
2493
2494
|
const qtyText = hasMoney ? qtyRaw.toFixed(Math.max(0, drawing.qtyPrecision)) : "";
|
|
2494
|
-
const formatAmount = (value) =>
|
|
2495
|
-
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2496
|
-
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2495
|
+
const formatAmount = (value) => value.toFixed(2);
|
|
2496
|
+
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * target.price * effectivePointValue)}` : "";
|
|
2497
|
+
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * stop.price * effectivePointValue)}` : "";
|
|
2497
2498
|
const drawPositionPill = (text, centerX, centerY, bg) => {
|
|
2499
|
+
const lines = Array.isArray(text) ? text : [text];
|
|
2498
2500
|
const prevFont = ctx.font;
|
|
2499
2501
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2500
2502
|
const padding = 6;
|
|
2501
|
-
const
|
|
2503
|
+
const lineH = 14;
|
|
2504
|
+
const textW = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
2502
2505
|
const pillW = textW + padding * 2;
|
|
2503
|
-
const pillH = 18;
|
|
2506
|
+
const pillH = lines.length === 1 ? 18 : lines.length * lineH + 6;
|
|
2504
2507
|
const pillX = centerX - pillW / 2;
|
|
2505
2508
|
const pillY = centerY - pillH / 2;
|
|
2506
2509
|
ctx.fillStyle = bg;
|
|
@@ -2508,28 +2511,104 @@ function createChart(element, options = {}) {
|
|
|
2508
2511
|
ctx.fillStyle = labelTextColor;
|
|
2509
2512
|
ctx.textAlign = "center";
|
|
2510
2513
|
ctx.textBaseline = "middle";
|
|
2511
|
-
|
|
2514
|
+
const startY = pillY + pillH / 2 - (lines.length - 1) * lineH / 2;
|
|
2515
|
+
lines.forEach((line, lineIndex) => ctx.fillText(line, centerX, startY + lineIndex * lineH));
|
|
2512
2516
|
ctx.font = prevFont;
|
|
2513
2517
|
};
|
|
2514
|
-
drawPositionPill(`Target ${formatPrice(
|
|
2515
|
-
drawPositionPill(`Stop ${formatPrice(
|
|
2516
|
-
let
|
|
2518
|
+
drawPositionPill(`Target: ${formatPrice(targetDist)} (${pctOfDist(targetDist)})${ticksOfDist(targetDist)}${targetAmountText}`, cx, targetY, profitLine);
|
|
2519
|
+
drawPositionPill(`Stop: ${formatPrice(stopDist)} (${pctOfDist(stopDist)})${ticksOfDist(stopDist)}${stopAmountText}`, cx, stopY, lossLine);
|
|
2520
|
+
let centerLines;
|
|
2517
2521
|
let centerBg;
|
|
2518
2522
|
if (positionHit) {
|
|
2519
|
-
const
|
|
2520
|
-
const
|
|
2521
|
-
|
|
2523
|
+
const pnlPoints = positionHit.profit ? targetDist : -stopDist;
|
|
2524
|
+
const line1 = hasMoney ? `Closed P&L: ${formatPrice(pnlPoints)}, Qty: ${qtyText}` : `Closed P&L: ${formatPrice(pnlPoints)}`;
|
|
2525
|
+
centerLines = [line1, `Risk/reward ratio: ${rr.toFixed(2)}`];
|
|
2522
2526
|
centerBg = positionHit.profit ? profitLine : lossLine;
|
|
2523
2527
|
} else {
|
|
2524
|
-
|
|
2528
|
+
const line1 = hasMoney ? `Entry: ${formatPrice(entry.price)}, Qty: ${qtyText}` : `Entry: ${formatPrice(entry.price)}`;
|
|
2529
|
+
centerLines = [line1, `Risk/reward ratio: ${rr.toFixed(2)}`];
|
|
2525
2530
|
centerBg = hexToRgba(drawing.color, 0.92);
|
|
2526
2531
|
}
|
|
2527
|
-
drawPositionPill(
|
|
2532
|
+
drawPositionPill(centerLines, cx, entryY, centerBg);
|
|
2528
2533
|
if (drawing.label) {
|
|
2529
2534
|
drawDrawingLabel(drawing.label, cx, Math.min(targetY, stopY) - 4, drawing.color);
|
|
2530
2535
|
}
|
|
2531
2536
|
}
|
|
2532
2537
|
}
|
|
2538
|
+
} else if (drawing.type === "price-range") {
|
|
2539
|
+
const p0 = drawing.points[0];
|
|
2540
|
+
const p1 = drawing.points[1];
|
|
2541
|
+
if (p0 && p1) {
|
|
2542
|
+
const px0 = xFromDrawingPoint(p0);
|
|
2543
|
+
const px1 = xFromDrawingPoint(p1);
|
|
2544
|
+
const leftX = Math.min(px0, px1);
|
|
2545
|
+
const rightX = Math.max(px0, px1);
|
|
2546
|
+
const boxW = Math.max(1, rightX - leftX);
|
|
2547
|
+
const y0 = yFromPrice(p0.price);
|
|
2548
|
+
const y1 = yFromPrice(p1.price);
|
|
2549
|
+
const topY = Math.min(y0, y1);
|
|
2550
|
+
const botY = Math.max(y0, y1);
|
|
2551
|
+
ctx.save();
|
|
2552
|
+
ctx.globalAlpha = draft ? 0.6 : 1;
|
|
2553
|
+
ctx.fillStyle = hexToRgba(drawing.color, 0.14);
|
|
2554
|
+
ctx.fillRect(leftX, topY, boxW, Math.max(0, botY - topY));
|
|
2555
|
+
ctx.setLineDash([]);
|
|
2556
|
+
ctx.lineWidth = Math.max(1, drawing.width);
|
|
2557
|
+
ctx.strokeStyle = drawing.color;
|
|
2558
|
+
ctx.beginPath();
|
|
2559
|
+
ctx.moveTo(crisp(leftX), crisp(topY));
|
|
2560
|
+
ctx.lineTo(crisp(rightX), crisp(topY));
|
|
2561
|
+
ctx.moveTo(crisp(leftX), crisp(botY));
|
|
2562
|
+
ctx.lineTo(crisp(rightX), crisp(botY));
|
|
2563
|
+
ctx.stroke();
|
|
2564
|
+
const midX = (leftX + rightX) / 2;
|
|
2565
|
+
const down = p1.price <= p0.price;
|
|
2566
|
+
const arrowToY = down ? botY : topY;
|
|
2567
|
+
ctx.beginPath();
|
|
2568
|
+
ctx.moveTo(crisp(midX), crisp(down ? topY : botY));
|
|
2569
|
+
ctx.lineTo(crisp(midX), crisp(arrowToY));
|
|
2570
|
+
ctx.stroke();
|
|
2571
|
+
const head = 6;
|
|
2572
|
+
ctx.beginPath();
|
|
2573
|
+
ctx.moveTo(midX, arrowToY);
|
|
2574
|
+
ctx.lineTo(midX - head, arrowToY + (down ? -head : head));
|
|
2575
|
+
ctx.moveTo(midX, arrowToY);
|
|
2576
|
+
ctx.lineTo(midX + head, arrowToY + (down ? -head : head));
|
|
2577
|
+
ctx.stroke();
|
|
2578
|
+
ctx.restore();
|
|
2579
|
+
handleAt(px0, y0, drawing.color);
|
|
2580
|
+
handleAt(px1, y1, drawing.color);
|
|
2581
|
+
const tick = getConfiguredTickSize();
|
|
2582
|
+
const diff = p1.price - p0.price;
|
|
2583
|
+
const base = Math.abs(p0.price) > 0 ? Math.abs(p0.price) : 1;
|
|
2584
|
+
const pct = diff / base * 100;
|
|
2585
|
+
const ticks = tick > 0 ? Math.round(diff / tick) : 0;
|
|
2586
|
+
const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
|
|
2587
|
+
const labelText = 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)}%`)})`;
|
|
2588
|
+
const prevFont = ctx.font;
|
|
2589
|
+
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2590
|
+
const padding = 6;
|
|
2591
|
+
const textW = ctx.measureText(labelText).width;
|
|
2592
|
+
const pillW = textW + padding * 2;
|
|
2593
|
+
const pillH = 18;
|
|
2594
|
+
const pillX = midX - pillW / 2;
|
|
2595
|
+
const pillY = botY + 6;
|
|
2596
|
+
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2597
|
+
fillRoundedRect(pillX, pillY, pillW, pillH, 4);
|
|
2598
|
+
ctx.save();
|
|
2599
|
+
ctx.strokeStyle = hexToRgba(drawing.color, 0.5);
|
|
2600
|
+
ctx.lineWidth = 1;
|
|
2601
|
+
ctx.strokeRect(crisp(pillX), crisp(pillY), pillW, pillH);
|
|
2602
|
+
ctx.restore();
|
|
2603
|
+
ctx.fillStyle = drawing.color;
|
|
2604
|
+
ctx.textAlign = "center";
|
|
2605
|
+
ctx.textBaseline = "middle";
|
|
2606
|
+
ctx.fillText(labelText, midX, pillY + pillH / 2);
|
|
2607
|
+
ctx.font = prevFont;
|
|
2608
|
+
if (drawing.label) {
|
|
2609
|
+
drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2533
2612
|
}
|
|
2534
2613
|
ctx.restore();
|
|
2535
2614
|
};
|
|
@@ -3765,6 +3844,19 @@ function createChart(element, options = {}) {
|
|
|
3765
3844
|
if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
|
|
3766
3845
|
return { drawing, target: "line" };
|
|
3767
3846
|
}
|
|
3847
|
+
} else if (drawing.type === "price-range") {
|
|
3848
|
+
const p0 = drawing.points[0];
|
|
3849
|
+
const p1 = drawing.points[1];
|
|
3850
|
+
if (!p0 || !p1) continue;
|
|
3851
|
+
const px0 = canvasXFromDrawingPoint(p0);
|
|
3852
|
+
const px1 = canvasXFromDrawingPoint(p1);
|
|
3853
|
+
const y0 = canvasYFromDrawingPrice(p0.price);
|
|
3854
|
+
const y1 = canvasYFromDrawingPrice(p1.price);
|
|
3855
|
+
if (Math.hypot(x - px0, y - y0) <= 9) return { drawing, target: "handle", pointIndex: 0 };
|
|
3856
|
+
if (Math.hypot(x - px1, y - y1) <= 9) return { drawing, target: "handle", pointIndex: 1 };
|
|
3857
|
+
if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1) && y >= Math.min(y0, y1) && y <= Math.max(y0, y1)) {
|
|
3858
|
+
return { drawing, target: "line" };
|
|
3859
|
+
}
|
|
3768
3860
|
}
|
|
3769
3861
|
}
|
|
3770
3862
|
return null;
|
|
@@ -3965,6 +4057,25 @@ function createChart(element, options = {}) {
|
|
|
3965
4057
|
draw();
|
|
3966
4058
|
return true;
|
|
3967
4059
|
}
|
|
4060
|
+
if (activeDrawingTool === "price-range") {
|
|
4061
|
+
const tick = getConfiguredTickSize();
|
|
4062
|
+
const visibleRange = drawState.yMax - drawState.yMin;
|
|
4063
|
+
const priceOffset = visibleRange > 0 ? visibleRange * 0.18 : tick > 0 ? tick * 80 : Math.abs(point.price) * 0.03 || 1;
|
|
4064
|
+
const width2 = Math.max(8, Math.round(xSpan * 0.2));
|
|
4065
|
+
const defaults = getDrawingToolDefaults(activeDrawingTool);
|
|
4066
|
+
drawings.push(
|
|
4067
|
+
normalizeDrawingState({
|
|
4068
|
+
type: activeDrawingTool,
|
|
4069
|
+
points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
|
|
4070
|
+
color: defaults.color ?? "#2962ff",
|
|
4071
|
+
style: defaults.style ?? "solid",
|
|
4072
|
+
width: defaults.width ?? 1
|
|
4073
|
+
})
|
|
4074
|
+
);
|
|
4075
|
+
emitDrawingsChange();
|
|
4076
|
+
draw();
|
|
4077
|
+
return true;
|
|
4078
|
+
}
|
|
3968
4079
|
if (activeDrawingTool === "long-position" || activeDrawingTool === "short-position") {
|
|
3969
4080
|
const isLong = activeDrawingTool === "long-position";
|
|
3970
4081
|
const tick = getConfiguredTickSize();
|
package/docs/API.md
CHANGED
|
@@ -422,7 +422,7 @@ Drawings are user-created chart tools, separate from indicators. They are intera
|
|
|
422
422
|
- `ray`: two-point line that extends infinitely past the second point
|
|
423
423
|
- `fib-retracement`: two-point retracement with levels/bands
|
|
424
424
|
- `fib-extension`: trend-based three-point extension (click trend start, trend end, then the projection origin); levels project from the third point by the first→second move
|
|
425
|
-
- `long-position` / `short-position`: risk/reward forecasting box (single click drops a default box). Points are `[entry, target, stop, rightEdge]`; anchors are target/entry/stop on the left and a time-width handle on the right. Labels show price, % move, ticks, and risk/reward ratio. `color` sets the entry line; `colors` is `[profitColor, lossColor, labelTextColor]` (defaults `POSITION_DEFAULT_COLORS`). Sizing inputs `accountSize`, `lotSize`, `risk`, `riskMode` (`"percent"|"amount"`), `leverage`, `pointValue`, `qtyPrecision` drive the Qty/Amount labels — set `pointValue` (contract $/point) from your app for real money values. The box also runs a trade simulation across the bars it covers: it shades the traversed region and draws a diagonal to the first bar that touches the target or stop, and the center label switches to "Closed P&L" (green if the target was hit first, red if the stop was hit first).
|
|
425
|
+
- `long-position` / `short-position`: risk/reward forecasting box (single click drops a default box). Points are `[entry, target, stop, rightEdge]`; anchors are target/entry/stop on the left and a time-width handle on the right. Labels show price, % move, ticks, and risk/reward ratio. `color` sets the entry line; `colors` is `[profitColor, lossColor, labelTextColor]` (defaults `POSITION_DEFAULT_COLORS`). Sizing inputs `accountSize`, `lotSize`, `risk`, `riskMode` (`"percent"|"amount"`), `leverage`, `pointValue`, `qtyPrecision` drive the Qty/Amount labels — set `pointValue` (contract $/point) from your app for real money values. A `price-range` tool (single click drops a default box; two diagonal anchors) measures a region and labels the signed price change, % move and ticks with a vertical arrow. The box also runs a trade simulation across the bars it covers: it shades the traversed region and draws a diagonal to the first bar that touches the target or stop, and the center label switches to "Closed P&L" (green if the target was hit first, red if the stop was hit first).
|
|
426
426
|
- `points: DrawingPoint[]`
|
|
427
427
|
- `visible?: boolean`
|
|
428
428
|
- `color?: string`
|