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.
@@ -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 pctOf = (price) => {
2506
+ const pctOfDist = (dist) => {
2507
2507
  if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
2508
- return `${((price - entry.price) / Math.abs(entry.price) * 100).toFixed(2)}%`;
2508
+ const pct = dist / Math.abs(entry.price) * 100;
2509
+ return `${pct < 1 ? pct.toFixed(3) : pct.toFixed(2)}%`;
2509
2510
  };
2510
- const ticksOf = (price) => tick > 0 ? ` ${Math.round(Math.abs(price - entry.price) / tick)}t` : "";
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) => Math.abs(value) >= 1e3 ? value.toFixed(0) : value.toFixed(2);
2521
- const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * targetDist * effectivePointValue)}` : "";
2522
- const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * stopDist * effectivePointValue)}` : "";
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 textW = ctx.measureText(text).width;
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
- ctx.fillText(text, centerX, pillY + pillH / 2);
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(target.price)} (${pctOf(target.price)})${ticksOf(target.price)}${targetAmountText}`, cx, targetY, profitLine);
2541
- drawPositionPill(`Stop ${formatPrice(stop.price)} (${pctOf(stop.price)})${ticksOf(stop.price)}${stopAmountText}`, cx, stopY, lossLine);
2542
- let centerText;
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 pnl = positionHit.profit ? qtyRaw * targetDist * effectivePointValue : -(qtyRaw * stopDist * effectivePointValue);
2546
- const pnlText = hasMoney ? `Closed P&L: ${formatAmount(pnl)}` : `Closed ${positionHit.profit ? "+" : "\u2212"}${formatPrice(Math.abs(positionHit.price - entry.price))}`;
2547
- centerText = hasMoney ? `${pnlText}, Qty: ${qtyText} RR ${rr.toFixed(2)}` : `${pnlText} RR ${rr.toFixed(2)}`;
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
- centerText = hasMoney ? `Entry ${formatPrice(entry.price)} Qty ${qtyText} RR ${rr.toFixed(2)}` : `Entry ${formatPrice(entry.price)} RR ${rr.toFixed(2)}`;
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(centerText, cx, entryY, centerBg);
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 pctOf = (price) => {
2480
+ const pctOfDist = (dist) => {
2481
2481
  if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
2482
- return `${((price - entry.price) / Math.abs(entry.price) * 100).toFixed(2)}%`;
2482
+ const pct = dist / Math.abs(entry.price) * 100;
2483
+ return `${pct < 1 ? pct.toFixed(3) : pct.toFixed(2)}%`;
2483
2484
  };
2484
- const ticksOf = (price) => tick > 0 ? ` ${Math.round(Math.abs(price - entry.price) / tick)}t` : "";
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) => Math.abs(value) >= 1e3 ? value.toFixed(0) : value.toFixed(2);
2495
- const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * targetDist * effectivePointValue)}` : "";
2496
- const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * stopDist * effectivePointValue)}` : "";
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 textW = ctx.measureText(text).width;
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
- ctx.fillText(text, centerX, pillY + pillH / 2);
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(target.price)} (${pctOf(target.price)})${ticksOf(target.price)}${targetAmountText}`, cx, targetY, profitLine);
2515
- drawPositionPill(`Stop ${formatPrice(stop.price)} (${pctOf(stop.price)})${ticksOf(stop.price)}${stopAmountText}`, cx, stopY, lossLine);
2516
- let centerText;
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 pnl = positionHit.profit ? qtyRaw * targetDist * effectivePointValue : -(qtyRaw * stopDist * effectivePointValue);
2520
- const pnlText = hasMoney ? `Closed P&L: ${formatAmount(pnl)}` : `Closed ${positionHit.profit ? "+" : "\u2212"}${formatPrice(Math.abs(positionHit.price - entry.price))}`;
2521
- centerText = hasMoney ? `${pnlText}, Qty: ${qtyText} RR ${rr.toFixed(2)}` : `${pnlText} RR ${rr.toFixed(2)}`;
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
- centerText = hasMoney ? `Entry ${formatPrice(entry.price)} Qty ${qtyText} RR ${rr.toFixed(2)}` : `Entry ${formatPrice(entry.price)} RR ${rr.toFixed(2)}`;
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(centerText, cx, entryY, centerBg);
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 pctOf = (price) => {
2506
+ const pctOfDist = (dist) => {
2507
2507
  if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
2508
- return `${((price - entry.price) / Math.abs(entry.price) * 100).toFixed(2)}%`;
2508
+ const pct = dist / Math.abs(entry.price) * 100;
2509
+ return `${pct < 1 ? pct.toFixed(3) : pct.toFixed(2)}%`;
2509
2510
  };
2510
- const ticksOf = (price) => tick > 0 ? ` ${Math.round(Math.abs(price - entry.price) / tick)}t` : "";
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) => Math.abs(value) >= 1e3 ? value.toFixed(0) : value.toFixed(2);
2521
- const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * targetDist * effectivePointValue)}` : "";
2522
- const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * stopDist * effectivePointValue)}` : "";
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 textW = ctx.measureText(text).width;
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
- ctx.fillText(text, centerX, pillY + pillH / 2);
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(target.price)} (${pctOf(target.price)})${ticksOf(target.price)}${targetAmountText}`, cx, targetY, profitLine);
2541
- drawPositionPill(`Stop ${formatPrice(stop.price)} (${pctOf(stop.price)})${ticksOf(stop.price)}${stopAmountText}`, cx, stopY, lossLine);
2542
- let centerText;
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 pnl = positionHit.profit ? qtyRaw * targetDist * effectivePointValue : -(qtyRaw * stopDist * effectivePointValue);
2546
- const pnlText = hasMoney ? `Closed P&L: ${formatAmount(pnl)}` : `Closed ${positionHit.profit ? "+" : "\u2212"}${formatPrice(Math.abs(positionHit.price - entry.price))}`;
2547
- centerText = hasMoney ? `${pnlText}, Qty: ${qtyText} RR ${rr.toFixed(2)}` : `${pnlText} RR ${rr.toFixed(2)}`;
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
- centerText = hasMoney ? `Entry ${formatPrice(entry.price)} Qty ${qtyText} RR ${rr.toFixed(2)}` : `Entry ${formatPrice(entry.price)} RR ${rr.toFixed(2)}`;
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(centerText, cx, entryY, centerBg);
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 pctOf = (price) => {
2480
+ const pctOfDist = (dist) => {
2481
2481
  if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
2482
- return `${((price - entry.price) / Math.abs(entry.price) * 100).toFixed(2)}%`;
2482
+ const pct = dist / Math.abs(entry.price) * 100;
2483
+ return `${pct < 1 ? pct.toFixed(3) : pct.toFixed(2)}%`;
2483
2484
  };
2484
- const ticksOf = (price) => tick > 0 ? ` ${Math.round(Math.abs(price - entry.price) / tick)}t` : "";
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) => Math.abs(value) >= 1e3 ? value.toFixed(0) : value.toFixed(2);
2495
- const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * targetDist * effectivePointValue)}` : "";
2496
- const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * stopDist * effectivePointValue)}` : "";
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 textW = ctx.measureText(text).width;
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
- ctx.fillText(text, centerX, pillY + pillH / 2);
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(target.price)} (${pctOf(target.price)})${ticksOf(target.price)}${targetAmountText}`, cx, targetY, profitLine);
2515
- drawPositionPill(`Stop ${formatPrice(stop.price)} (${pctOf(stop.price)})${ticksOf(stop.price)}${stopAmountText}`, cx, stopY, lossLine);
2516
- let centerText;
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 pnl = positionHit.profit ? qtyRaw * targetDist * effectivePointValue : -(qtyRaw * stopDist * effectivePointValue);
2520
- const pnlText = hasMoney ? `Closed P&L: ${formatAmount(pnl)}` : `Closed ${positionHit.profit ? "+" : "\u2212"}${formatPrice(Math.abs(positionHit.price - entry.price))}`;
2521
- centerText = hasMoney ? `${pnlText}, Qty: ${qtyText} RR ${rr.toFixed(2)}` : `${pnlText} RR ${rr.toFixed(2)}`;
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
- centerText = hasMoney ? `Entry ${formatPrice(entry.price)} Qty ${qtyText} RR ${rr.toFixed(2)}` : `Entry ${formatPrice(entry.price)} RR ${rr.toFixed(2)}`;
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(centerText, cx, entryY, centerBg);
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`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.82",
3
+ "version": "0.1.84",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",