hyperprop-charting-library 0.1.71 → 0.1.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2391,6 +2391,88 @@ function createChart(element, options = {}) {
2391
2391
  }
2392
2392
  }
2393
2393
  }
2394
+ } else if (drawing.type === "long-position" || drawing.type === "short-position") {
2395
+ const entry = drawing.points[0];
2396
+ const target = drawing.points[1];
2397
+ const stop = drawing.points[2];
2398
+ const right = drawing.points[3];
2399
+ if (entry && target && stop && right) {
2400
+ const leftX = xFromDrawingPoint(entry);
2401
+ const rightX = xFromDrawingPoint(right);
2402
+ const boxX0 = Math.min(leftX, rightX);
2403
+ const boxX1 = Math.max(leftX, rightX);
2404
+ const boxW = Math.max(1, boxX1 - boxX0);
2405
+ const entryY = yFromPrice(entry.price);
2406
+ const targetY = yFromPrice(target.price);
2407
+ const stopY = yFromPrice(stop.price);
2408
+ const profitFill = "rgba(38,166,154,0.16)";
2409
+ const lossFill = "rgba(239,83,80,0.16)";
2410
+ const profitLine = "rgba(38,166,154,0.9)";
2411
+ const lossLine = "rgba(239,83,80,0.9)";
2412
+ ctx.save();
2413
+ ctx.globalAlpha = draft ? 0.6 : 1;
2414
+ ctx.fillStyle = profitFill;
2415
+ ctx.fillRect(boxX0, Math.min(entryY, targetY), boxW, Math.abs(targetY - entryY));
2416
+ ctx.fillStyle = lossFill;
2417
+ ctx.fillRect(boxX0, Math.min(entryY, stopY), boxW, Math.abs(stopY - entryY));
2418
+ ctx.restore();
2419
+ ctx.save();
2420
+ ctx.setLineDash([]);
2421
+ ctx.lineWidth = Math.max(1, drawing.width);
2422
+ ctx.strokeStyle = profitLine;
2423
+ ctx.beginPath();
2424
+ ctx.moveTo(crisp(boxX0), crisp(targetY));
2425
+ ctx.lineTo(crisp(boxX1), crisp(targetY));
2426
+ ctx.stroke();
2427
+ ctx.strokeStyle = lossLine;
2428
+ ctx.beginPath();
2429
+ ctx.moveTo(crisp(boxX0), crisp(stopY));
2430
+ ctx.lineTo(crisp(boxX1), crisp(stopY));
2431
+ ctx.stroke();
2432
+ ctx.strokeStyle = drawing.color;
2433
+ ctx.beginPath();
2434
+ ctx.moveTo(crisp(boxX0), crisp(entryY));
2435
+ ctx.lineTo(crisp(boxX1), crisp(entryY));
2436
+ ctx.stroke();
2437
+ ctx.restore();
2438
+ drawDrawingHandle(leftX, targetY, drawing.color);
2439
+ drawDrawingHandle(leftX, entryY, drawing.color);
2440
+ drawDrawingHandle(leftX, stopY, drawing.color);
2441
+ drawDrawingHandle(rightX, entryY, drawing.color);
2442
+ const tick = getConfiguredTickSize();
2443
+ const pctOf = (price) => {
2444
+ if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
2445
+ return `${((price - entry.price) / Math.abs(entry.price) * 100).toFixed(2)}%`;
2446
+ };
2447
+ const ticksOf = (price) => tick > 0 ? ` ${Math.round(Math.abs(price - entry.price) / tick)}t` : "";
2448
+ const targetDist = Math.abs(target.price - entry.price);
2449
+ const stopDist = Math.abs(entry.price - stop.price);
2450
+ const rr = stopDist > 0 ? targetDist / stopDist : 0;
2451
+ const cx = (boxX0 + boxX1) / 2;
2452
+ const drawPositionPill = (text, centerX, centerY, bg) => {
2453
+ const prevFont = ctx.font;
2454
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2455
+ const padding = 6;
2456
+ const textW = ctx.measureText(text).width;
2457
+ const pillW = textW + padding * 2;
2458
+ const pillH = 18;
2459
+ const pillX = centerX - pillW / 2;
2460
+ const pillY = centerY - pillH / 2;
2461
+ ctx.fillStyle = bg;
2462
+ fillRoundedRect(pillX, pillY, pillW, pillH, 4);
2463
+ ctx.fillStyle = "#ffffff";
2464
+ ctx.textAlign = "center";
2465
+ ctx.textBaseline = "middle";
2466
+ ctx.fillText(text, centerX, pillY + pillH / 2);
2467
+ ctx.font = prevFont;
2468
+ };
2469
+ drawPositionPill(`Target ${formatPrice(target.price)} (${pctOf(target.price)})${ticksOf(target.price)}`, cx, targetY, profitLine);
2470
+ drawPositionPill(`Stop ${formatPrice(stop.price)} (${pctOf(stop.price)})${ticksOf(stop.price)}`, cx, stopY, lossLine);
2471
+ drawPositionPill(`Entry ${formatPrice(entry.price)} RR ${rr.toFixed(2)}`, cx, entryY, hexToRgba(drawing.color, 0.92));
2472
+ if (drawing.label) {
2473
+ drawDrawingLabel(drawing.label, cx, Math.min(targetY, stopY) - 4, drawing.color);
2474
+ }
2475
+ }
2394
2476
  }
2395
2477
  ctx.restore();
2396
2478
  };
@@ -3587,6 +3669,28 @@ function createChart(element, options = {}) {
3587
3669
  } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
3588
3670
  return { drawing, target: "line" };
3589
3671
  }
3672
+ } else if (drawing.type === "long-position" || drawing.type === "short-position") {
3673
+ const entry = drawing.points[0];
3674
+ const target = drawing.points[1];
3675
+ const stop = drawing.points[2];
3676
+ const right = drawing.points[3];
3677
+ if (!entry || !target || !stop || !right) continue;
3678
+ const leftX = canvasXFromDrawingPoint(entry);
3679
+ const rightX = canvasXFromDrawingPoint(right);
3680
+ const entryY = canvasYFromDrawingPrice(entry.price);
3681
+ const targetY = canvasYFromDrawingPrice(target.price);
3682
+ const stopY = canvasYFromDrawingPrice(stop.price);
3683
+ if (Math.hypot(x - leftX, y - targetY) <= 9) return { drawing, target: "handle", pointIndex: 1 };
3684
+ if (Math.hypot(x - leftX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 0 };
3685
+ if (Math.hypot(x - leftX, y - stopY) <= 9) return { drawing, target: "handle", pointIndex: 2 };
3686
+ if (Math.hypot(x - rightX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 3 };
3687
+ const x0 = Math.min(leftX, rightX);
3688
+ const x1 = Math.max(leftX, rightX);
3689
+ const yTop = Math.min(targetY, stopY, entryY);
3690
+ const yBot = Math.max(targetY, stopY, entryY);
3691
+ if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
3692
+ return { drawing, target: "line" };
3693
+ }
3590
3694
  }
3591
3695
  }
3592
3696
  return null;
@@ -3787,6 +3891,33 @@ function createChart(element, options = {}) {
3787
3891
  draw();
3788
3892
  return true;
3789
3893
  }
3894
+ if (activeDrawingTool === "long-position" || activeDrawingTool === "short-position") {
3895
+ const isLong = activeDrawingTool === "long-position";
3896
+ const tick = getConfiguredTickSize();
3897
+ const priceOffset = tick > 0 ? tick * 20 : Math.abs(point.price) * 0.01 || 1;
3898
+ const width2 = Math.max(5, Math.round(xSpan * 0.15));
3899
+ const entryPrice = point.price;
3900
+ const targetPrice = isLong ? entryPrice + priceOffset : entryPrice - priceOffset;
3901
+ const stopPrice = isLong ? entryPrice - priceOffset : entryPrice + priceOffset;
3902
+ const defaults = getDrawingToolDefaults(activeDrawingTool);
3903
+ drawings.push(
3904
+ normalizeDrawingState({
3905
+ type: activeDrawingTool,
3906
+ points: [
3907
+ point,
3908
+ normalizeDrawingPoint(point.index, targetPrice),
3909
+ normalizeDrawingPoint(point.index, stopPrice),
3910
+ normalizeDrawingPoint(point.index + width2, entryPrice)
3911
+ ],
3912
+ color: defaults.color ?? "#2563eb",
3913
+ style: defaults.style ?? "solid",
3914
+ width: defaults.width ?? 1
3915
+ })
3916
+ );
3917
+ emitDrawingsChange();
3918
+ draw();
3919
+ return true;
3920
+ }
3790
3921
  return false;
3791
3922
  };
3792
3923
  const setDrawingDefaults = (tool, defaults) => {
@@ -3810,6 +3941,22 @@ function createChart(element, options = {}) {
3810
3941
  if (drawing.id !== drawingDragState?.drawingId || drawing.locked) {
3811
3942
  return drawing;
3812
3943
  }
3944
+ if (drawingDragState.target === "handle" && (drawing.type === "long-position" || drawing.type === "short-position")) {
3945
+ const pts = drawing.points.map((point) => ({ ...point }));
3946
+ const entry = pts[0];
3947
+ const pointIndex = drawingDragState.pointIndex ?? 0;
3948
+ if (pointIndex === 0) {
3949
+ pts[0] = normalizeDrawingPoint(entry.index, currentPoint.price);
3950
+ if (pts[3]) pts[3] = normalizeDrawingPoint(pts[3].index, currentPoint.price);
3951
+ } else if (pointIndex === 1 && pts[1]) {
3952
+ pts[1] = normalizeDrawingPoint(entry.index, currentPoint.price);
3953
+ } else if (pointIndex === 2 && pts[2]) {
3954
+ pts[2] = normalizeDrawingPoint(entry.index, currentPoint.price);
3955
+ } else if (pointIndex === 3 && pts[3]) {
3956
+ pts[3] = normalizeDrawingPoint(currentPoint.index, entry.price);
3957
+ }
3958
+ return { ...drawing, points: pts };
3959
+ }
3813
3960
  if (drawingDragState.target === "handle") {
3814
3961
  return {
3815
3962
  ...drawing,
@@ -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";
47
+ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position";
48
48
  interface DrawingPoint {
49
49
  index: number;
50
50
  price: number;
@@ -2366,6 +2366,88 @@ function createChart(element, options = {}) {
2366
2366
  }
2367
2367
  }
2368
2368
  }
2369
+ } else if (drawing.type === "long-position" || drawing.type === "short-position") {
2370
+ const entry = drawing.points[0];
2371
+ const target = drawing.points[1];
2372
+ const stop = drawing.points[2];
2373
+ const right = drawing.points[3];
2374
+ if (entry && target && stop && right) {
2375
+ const leftX = xFromDrawingPoint(entry);
2376
+ const rightX = xFromDrawingPoint(right);
2377
+ const boxX0 = Math.min(leftX, rightX);
2378
+ const boxX1 = Math.max(leftX, rightX);
2379
+ const boxW = Math.max(1, boxX1 - boxX0);
2380
+ const entryY = yFromPrice(entry.price);
2381
+ const targetY = yFromPrice(target.price);
2382
+ const stopY = yFromPrice(stop.price);
2383
+ const profitFill = "rgba(38,166,154,0.16)";
2384
+ const lossFill = "rgba(239,83,80,0.16)";
2385
+ const profitLine = "rgba(38,166,154,0.9)";
2386
+ const lossLine = "rgba(239,83,80,0.9)";
2387
+ ctx.save();
2388
+ ctx.globalAlpha = draft ? 0.6 : 1;
2389
+ ctx.fillStyle = profitFill;
2390
+ ctx.fillRect(boxX0, Math.min(entryY, targetY), boxW, Math.abs(targetY - entryY));
2391
+ ctx.fillStyle = lossFill;
2392
+ ctx.fillRect(boxX0, Math.min(entryY, stopY), boxW, Math.abs(stopY - entryY));
2393
+ ctx.restore();
2394
+ ctx.save();
2395
+ ctx.setLineDash([]);
2396
+ ctx.lineWidth = Math.max(1, drawing.width);
2397
+ ctx.strokeStyle = profitLine;
2398
+ ctx.beginPath();
2399
+ ctx.moveTo(crisp(boxX0), crisp(targetY));
2400
+ ctx.lineTo(crisp(boxX1), crisp(targetY));
2401
+ ctx.stroke();
2402
+ ctx.strokeStyle = lossLine;
2403
+ ctx.beginPath();
2404
+ ctx.moveTo(crisp(boxX0), crisp(stopY));
2405
+ ctx.lineTo(crisp(boxX1), crisp(stopY));
2406
+ ctx.stroke();
2407
+ ctx.strokeStyle = drawing.color;
2408
+ ctx.beginPath();
2409
+ ctx.moveTo(crisp(boxX0), crisp(entryY));
2410
+ ctx.lineTo(crisp(boxX1), crisp(entryY));
2411
+ ctx.stroke();
2412
+ ctx.restore();
2413
+ drawDrawingHandle(leftX, targetY, drawing.color);
2414
+ drawDrawingHandle(leftX, entryY, drawing.color);
2415
+ drawDrawingHandle(leftX, stopY, drawing.color);
2416
+ drawDrawingHandle(rightX, entryY, drawing.color);
2417
+ const tick = getConfiguredTickSize();
2418
+ const pctOf = (price) => {
2419
+ if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
2420
+ return `${((price - entry.price) / Math.abs(entry.price) * 100).toFixed(2)}%`;
2421
+ };
2422
+ const ticksOf = (price) => tick > 0 ? ` ${Math.round(Math.abs(price - entry.price) / tick)}t` : "";
2423
+ const targetDist = Math.abs(target.price - entry.price);
2424
+ const stopDist = Math.abs(entry.price - stop.price);
2425
+ const rr = stopDist > 0 ? targetDist / stopDist : 0;
2426
+ const cx = (boxX0 + boxX1) / 2;
2427
+ const drawPositionPill = (text, centerX, centerY, bg) => {
2428
+ const prevFont = ctx.font;
2429
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2430
+ const padding = 6;
2431
+ const textW = ctx.measureText(text).width;
2432
+ const pillW = textW + padding * 2;
2433
+ const pillH = 18;
2434
+ const pillX = centerX - pillW / 2;
2435
+ const pillY = centerY - pillH / 2;
2436
+ ctx.fillStyle = bg;
2437
+ fillRoundedRect(pillX, pillY, pillW, pillH, 4);
2438
+ ctx.fillStyle = "#ffffff";
2439
+ ctx.textAlign = "center";
2440
+ ctx.textBaseline = "middle";
2441
+ ctx.fillText(text, centerX, pillY + pillH / 2);
2442
+ ctx.font = prevFont;
2443
+ };
2444
+ drawPositionPill(`Target ${formatPrice(target.price)} (${pctOf(target.price)})${ticksOf(target.price)}`, cx, targetY, profitLine);
2445
+ drawPositionPill(`Stop ${formatPrice(stop.price)} (${pctOf(stop.price)})${ticksOf(stop.price)}`, cx, stopY, lossLine);
2446
+ drawPositionPill(`Entry ${formatPrice(entry.price)} RR ${rr.toFixed(2)}`, cx, entryY, hexToRgba(drawing.color, 0.92));
2447
+ if (drawing.label) {
2448
+ drawDrawingLabel(drawing.label, cx, Math.min(targetY, stopY) - 4, drawing.color);
2449
+ }
2450
+ }
2369
2451
  }
2370
2452
  ctx.restore();
2371
2453
  };
@@ -3562,6 +3644,28 @@ function createChart(element, options = {}) {
3562
3644
  } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
3563
3645
  return { drawing, target: "line" };
3564
3646
  }
3647
+ } else if (drawing.type === "long-position" || drawing.type === "short-position") {
3648
+ const entry = drawing.points[0];
3649
+ const target = drawing.points[1];
3650
+ const stop = drawing.points[2];
3651
+ const right = drawing.points[3];
3652
+ if (!entry || !target || !stop || !right) continue;
3653
+ const leftX = canvasXFromDrawingPoint(entry);
3654
+ const rightX = canvasXFromDrawingPoint(right);
3655
+ const entryY = canvasYFromDrawingPrice(entry.price);
3656
+ const targetY = canvasYFromDrawingPrice(target.price);
3657
+ const stopY = canvasYFromDrawingPrice(stop.price);
3658
+ if (Math.hypot(x - leftX, y - targetY) <= 9) return { drawing, target: "handle", pointIndex: 1 };
3659
+ if (Math.hypot(x - leftX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 0 };
3660
+ if (Math.hypot(x - leftX, y - stopY) <= 9) return { drawing, target: "handle", pointIndex: 2 };
3661
+ if (Math.hypot(x - rightX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 3 };
3662
+ const x0 = Math.min(leftX, rightX);
3663
+ const x1 = Math.max(leftX, rightX);
3664
+ const yTop = Math.min(targetY, stopY, entryY);
3665
+ const yBot = Math.max(targetY, stopY, entryY);
3666
+ if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
3667
+ return { drawing, target: "line" };
3668
+ }
3565
3669
  }
3566
3670
  }
3567
3671
  return null;
@@ -3762,6 +3866,33 @@ function createChart(element, options = {}) {
3762
3866
  draw();
3763
3867
  return true;
3764
3868
  }
3869
+ if (activeDrawingTool === "long-position" || activeDrawingTool === "short-position") {
3870
+ const isLong = activeDrawingTool === "long-position";
3871
+ const tick = getConfiguredTickSize();
3872
+ const priceOffset = tick > 0 ? tick * 20 : Math.abs(point.price) * 0.01 || 1;
3873
+ const width2 = Math.max(5, Math.round(xSpan * 0.15));
3874
+ const entryPrice = point.price;
3875
+ const targetPrice = isLong ? entryPrice + priceOffset : entryPrice - priceOffset;
3876
+ const stopPrice = isLong ? entryPrice - priceOffset : entryPrice + priceOffset;
3877
+ const defaults = getDrawingToolDefaults(activeDrawingTool);
3878
+ drawings.push(
3879
+ normalizeDrawingState({
3880
+ type: activeDrawingTool,
3881
+ points: [
3882
+ point,
3883
+ normalizeDrawingPoint(point.index, targetPrice),
3884
+ normalizeDrawingPoint(point.index, stopPrice),
3885
+ normalizeDrawingPoint(point.index + width2, entryPrice)
3886
+ ],
3887
+ color: defaults.color ?? "#2563eb",
3888
+ style: defaults.style ?? "solid",
3889
+ width: defaults.width ?? 1
3890
+ })
3891
+ );
3892
+ emitDrawingsChange();
3893
+ draw();
3894
+ return true;
3895
+ }
3765
3896
  return false;
3766
3897
  };
3767
3898
  const setDrawingDefaults = (tool, defaults) => {
@@ -3785,6 +3916,22 @@ function createChart(element, options = {}) {
3785
3916
  if (drawing.id !== drawingDragState?.drawingId || drawing.locked) {
3786
3917
  return drawing;
3787
3918
  }
3919
+ if (drawingDragState.target === "handle" && (drawing.type === "long-position" || drawing.type === "short-position")) {
3920
+ const pts = drawing.points.map((point) => ({ ...point }));
3921
+ const entry = pts[0];
3922
+ const pointIndex = drawingDragState.pointIndex ?? 0;
3923
+ if (pointIndex === 0) {
3924
+ pts[0] = normalizeDrawingPoint(entry.index, currentPoint.price);
3925
+ if (pts[3]) pts[3] = normalizeDrawingPoint(pts[3].index, currentPoint.price);
3926
+ } else if (pointIndex === 1 && pts[1]) {
3927
+ pts[1] = normalizeDrawingPoint(entry.index, currentPoint.price);
3928
+ } else if (pointIndex === 2 && pts[2]) {
3929
+ pts[2] = normalizeDrawingPoint(entry.index, currentPoint.price);
3930
+ } else if (pointIndex === 3 && pts[3]) {
3931
+ pts[3] = normalizeDrawingPoint(currentPoint.index, entry.price);
3932
+ }
3933
+ return { ...drawing, points: pts };
3934
+ }
3788
3935
  if (drawingDragState.target === "handle") {
3789
3936
  return {
3790
3937
  ...drawing,
package/dist/index.cjs CHANGED
@@ -2391,6 +2391,88 @@ function createChart(element, options = {}) {
2391
2391
  }
2392
2392
  }
2393
2393
  }
2394
+ } else if (drawing.type === "long-position" || drawing.type === "short-position") {
2395
+ const entry = drawing.points[0];
2396
+ const target = drawing.points[1];
2397
+ const stop = drawing.points[2];
2398
+ const right = drawing.points[3];
2399
+ if (entry && target && stop && right) {
2400
+ const leftX = xFromDrawingPoint(entry);
2401
+ const rightX = xFromDrawingPoint(right);
2402
+ const boxX0 = Math.min(leftX, rightX);
2403
+ const boxX1 = Math.max(leftX, rightX);
2404
+ const boxW = Math.max(1, boxX1 - boxX0);
2405
+ const entryY = yFromPrice(entry.price);
2406
+ const targetY = yFromPrice(target.price);
2407
+ const stopY = yFromPrice(stop.price);
2408
+ const profitFill = "rgba(38,166,154,0.16)";
2409
+ const lossFill = "rgba(239,83,80,0.16)";
2410
+ const profitLine = "rgba(38,166,154,0.9)";
2411
+ const lossLine = "rgba(239,83,80,0.9)";
2412
+ ctx.save();
2413
+ ctx.globalAlpha = draft ? 0.6 : 1;
2414
+ ctx.fillStyle = profitFill;
2415
+ ctx.fillRect(boxX0, Math.min(entryY, targetY), boxW, Math.abs(targetY - entryY));
2416
+ ctx.fillStyle = lossFill;
2417
+ ctx.fillRect(boxX0, Math.min(entryY, stopY), boxW, Math.abs(stopY - entryY));
2418
+ ctx.restore();
2419
+ ctx.save();
2420
+ ctx.setLineDash([]);
2421
+ ctx.lineWidth = Math.max(1, drawing.width);
2422
+ ctx.strokeStyle = profitLine;
2423
+ ctx.beginPath();
2424
+ ctx.moveTo(crisp(boxX0), crisp(targetY));
2425
+ ctx.lineTo(crisp(boxX1), crisp(targetY));
2426
+ ctx.stroke();
2427
+ ctx.strokeStyle = lossLine;
2428
+ ctx.beginPath();
2429
+ ctx.moveTo(crisp(boxX0), crisp(stopY));
2430
+ ctx.lineTo(crisp(boxX1), crisp(stopY));
2431
+ ctx.stroke();
2432
+ ctx.strokeStyle = drawing.color;
2433
+ ctx.beginPath();
2434
+ ctx.moveTo(crisp(boxX0), crisp(entryY));
2435
+ ctx.lineTo(crisp(boxX1), crisp(entryY));
2436
+ ctx.stroke();
2437
+ ctx.restore();
2438
+ drawDrawingHandle(leftX, targetY, drawing.color);
2439
+ drawDrawingHandle(leftX, entryY, drawing.color);
2440
+ drawDrawingHandle(leftX, stopY, drawing.color);
2441
+ drawDrawingHandle(rightX, entryY, drawing.color);
2442
+ const tick = getConfiguredTickSize();
2443
+ const pctOf = (price) => {
2444
+ if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
2445
+ return `${((price - entry.price) / Math.abs(entry.price) * 100).toFixed(2)}%`;
2446
+ };
2447
+ const ticksOf = (price) => tick > 0 ? ` ${Math.round(Math.abs(price - entry.price) / tick)}t` : "";
2448
+ const targetDist = Math.abs(target.price - entry.price);
2449
+ const stopDist = Math.abs(entry.price - stop.price);
2450
+ const rr = stopDist > 0 ? targetDist / stopDist : 0;
2451
+ const cx = (boxX0 + boxX1) / 2;
2452
+ const drawPositionPill = (text, centerX, centerY, bg) => {
2453
+ const prevFont = ctx.font;
2454
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2455
+ const padding = 6;
2456
+ const textW = ctx.measureText(text).width;
2457
+ const pillW = textW + padding * 2;
2458
+ const pillH = 18;
2459
+ const pillX = centerX - pillW / 2;
2460
+ const pillY = centerY - pillH / 2;
2461
+ ctx.fillStyle = bg;
2462
+ fillRoundedRect(pillX, pillY, pillW, pillH, 4);
2463
+ ctx.fillStyle = "#ffffff";
2464
+ ctx.textAlign = "center";
2465
+ ctx.textBaseline = "middle";
2466
+ ctx.fillText(text, centerX, pillY + pillH / 2);
2467
+ ctx.font = prevFont;
2468
+ };
2469
+ drawPositionPill(`Target ${formatPrice(target.price)} (${pctOf(target.price)})${ticksOf(target.price)}`, cx, targetY, profitLine);
2470
+ drawPositionPill(`Stop ${formatPrice(stop.price)} (${pctOf(stop.price)})${ticksOf(stop.price)}`, cx, stopY, lossLine);
2471
+ drawPositionPill(`Entry ${formatPrice(entry.price)} RR ${rr.toFixed(2)}`, cx, entryY, hexToRgba(drawing.color, 0.92));
2472
+ if (drawing.label) {
2473
+ drawDrawingLabel(drawing.label, cx, Math.min(targetY, stopY) - 4, drawing.color);
2474
+ }
2475
+ }
2394
2476
  }
2395
2477
  ctx.restore();
2396
2478
  };
@@ -3587,6 +3669,28 @@ function createChart(element, options = {}) {
3587
3669
  } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
3588
3670
  return { drawing, target: "line" };
3589
3671
  }
3672
+ } else if (drawing.type === "long-position" || drawing.type === "short-position") {
3673
+ const entry = drawing.points[0];
3674
+ const target = drawing.points[1];
3675
+ const stop = drawing.points[2];
3676
+ const right = drawing.points[3];
3677
+ if (!entry || !target || !stop || !right) continue;
3678
+ const leftX = canvasXFromDrawingPoint(entry);
3679
+ const rightX = canvasXFromDrawingPoint(right);
3680
+ const entryY = canvasYFromDrawingPrice(entry.price);
3681
+ const targetY = canvasYFromDrawingPrice(target.price);
3682
+ const stopY = canvasYFromDrawingPrice(stop.price);
3683
+ if (Math.hypot(x - leftX, y - targetY) <= 9) return { drawing, target: "handle", pointIndex: 1 };
3684
+ if (Math.hypot(x - leftX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 0 };
3685
+ if (Math.hypot(x - leftX, y - stopY) <= 9) return { drawing, target: "handle", pointIndex: 2 };
3686
+ if (Math.hypot(x - rightX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 3 };
3687
+ const x0 = Math.min(leftX, rightX);
3688
+ const x1 = Math.max(leftX, rightX);
3689
+ const yTop = Math.min(targetY, stopY, entryY);
3690
+ const yBot = Math.max(targetY, stopY, entryY);
3691
+ if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
3692
+ return { drawing, target: "line" };
3693
+ }
3590
3694
  }
3591
3695
  }
3592
3696
  return null;
@@ -3787,6 +3891,33 @@ function createChart(element, options = {}) {
3787
3891
  draw();
3788
3892
  return true;
3789
3893
  }
3894
+ if (activeDrawingTool === "long-position" || activeDrawingTool === "short-position") {
3895
+ const isLong = activeDrawingTool === "long-position";
3896
+ const tick = getConfiguredTickSize();
3897
+ const priceOffset = tick > 0 ? tick * 20 : Math.abs(point.price) * 0.01 || 1;
3898
+ const width2 = Math.max(5, Math.round(xSpan * 0.15));
3899
+ const entryPrice = point.price;
3900
+ const targetPrice = isLong ? entryPrice + priceOffset : entryPrice - priceOffset;
3901
+ const stopPrice = isLong ? entryPrice - priceOffset : entryPrice + priceOffset;
3902
+ const defaults = getDrawingToolDefaults(activeDrawingTool);
3903
+ drawings.push(
3904
+ normalizeDrawingState({
3905
+ type: activeDrawingTool,
3906
+ points: [
3907
+ point,
3908
+ normalizeDrawingPoint(point.index, targetPrice),
3909
+ normalizeDrawingPoint(point.index, stopPrice),
3910
+ normalizeDrawingPoint(point.index + width2, entryPrice)
3911
+ ],
3912
+ color: defaults.color ?? "#2563eb",
3913
+ style: defaults.style ?? "solid",
3914
+ width: defaults.width ?? 1
3915
+ })
3916
+ );
3917
+ emitDrawingsChange();
3918
+ draw();
3919
+ return true;
3920
+ }
3790
3921
  return false;
3791
3922
  };
3792
3923
  const setDrawingDefaults = (tool, defaults) => {
@@ -3810,6 +3941,22 @@ function createChart(element, options = {}) {
3810
3941
  if (drawing.id !== drawingDragState?.drawingId || drawing.locked) {
3811
3942
  return drawing;
3812
3943
  }
3944
+ if (drawingDragState.target === "handle" && (drawing.type === "long-position" || drawing.type === "short-position")) {
3945
+ const pts = drawing.points.map((point) => ({ ...point }));
3946
+ const entry = pts[0];
3947
+ const pointIndex = drawingDragState.pointIndex ?? 0;
3948
+ if (pointIndex === 0) {
3949
+ pts[0] = normalizeDrawingPoint(entry.index, currentPoint.price);
3950
+ if (pts[3]) pts[3] = normalizeDrawingPoint(pts[3].index, currentPoint.price);
3951
+ } else if (pointIndex === 1 && pts[1]) {
3952
+ pts[1] = normalizeDrawingPoint(entry.index, currentPoint.price);
3953
+ } else if (pointIndex === 2 && pts[2]) {
3954
+ pts[2] = normalizeDrawingPoint(entry.index, currentPoint.price);
3955
+ } else if (pointIndex === 3 && pts[3]) {
3956
+ pts[3] = normalizeDrawingPoint(currentPoint.index, entry.price);
3957
+ }
3958
+ return { ...drawing, points: pts };
3959
+ }
3813
3960
  if (drawingDragState.target === "handle") {
3814
3961
  return {
3815
3962
  ...drawing,
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";
47
+ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position";
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";
47
+ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position";
48
48
  interface DrawingPoint {
49
49
  index: number;
50
50
  price: number;
package/dist/index.js CHANGED
@@ -2366,6 +2366,88 @@ function createChart(element, options = {}) {
2366
2366
  }
2367
2367
  }
2368
2368
  }
2369
+ } else if (drawing.type === "long-position" || drawing.type === "short-position") {
2370
+ const entry = drawing.points[0];
2371
+ const target = drawing.points[1];
2372
+ const stop = drawing.points[2];
2373
+ const right = drawing.points[3];
2374
+ if (entry && target && stop && right) {
2375
+ const leftX = xFromDrawingPoint(entry);
2376
+ const rightX = xFromDrawingPoint(right);
2377
+ const boxX0 = Math.min(leftX, rightX);
2378
+ const boxX1 = Math.max(leftX, rightX);
2379
+ const boxW = Math.max(1, boxX1 - boxX0);
2380
+ const entryY = yFromPrice(entry.price);
2381
+ const targetY = yFromPrice(target.price);
2382
+ const stopY = yFromPrice(stop.price);
2383
+ const profitFill = "rgba(38,166,154,0.16)";
2384
+ const lossFill = "rgba(239,83,80,0.16)";
2385
+ const profitLine = "rgba(38,166,154,0.9)";
2386
+ const lossLine = "rgba(239,83,80,0.9)";
2387
+ ctx.save();
2388
+ ctx.globalAlpha = draft ? 0.6 : 1;
2389
+ ctx.fillStyle = profitFill;
2390
+ ctx.fillRect(boxX0, Math.min(entryY, targetY), boxW, Math.abs(targetY - entryY));
2391
+ ctx.fillStyle = lossFill;
2392
+ ctx.fillRect(boxX0, Math.min(entryY, stopY), boxW, Math.abs(stopY - entryY));
2393
+ ctx.restore();
2394
+ ctx.save();
2395
+ ctx.setLineDash([]);
2396
+ ctx.lineWidth = Math.max(1, drawing.width);
2397
+ ctx.strokeStyle = profitLine;
2398
+ ctx.beginPath();
2399
+ ctx.moveTo(crisp(boxX0), crisp(targetY));
2400
+ ctx.lineTo(crisp(boxX1), crisp(targetY));
2401
+ ctx.stroke();
2402
+ ctx.strokeStyle = lossLine;
2403
+ ctx.beginPath();
2404
+ ctx.moveTo(crisp(boxX0), crisp(stopY));
2405
+ ctx.lineTo(crisp(boxX1), crisp(stopY));
2406
+ ctx.stroke();
2407
+ ctx.strokeStyle = drawing.color;
2408
+ ctx.beginPath();
2409
+ ctx.moveTo(crisp(boxX0), crisp(entryY));
2410
+ ctx.lineTo(crisp(boxX1), crisp(entryY));
2411
+ ctx.stroke();
2412
+ ctx.restore();
2413
+ drawDrawingHandle(leftX, targetY, drawing.color);
2414
+ drawDrawingHandle(leftX, entryY, drawing.color);
2415
+ drawDrawingHandle(leftX, stopY, drawing.color);
2416
+ drawDrawingHandle(rightX, entryY, drawing.color);
2417
+ const tick = getConfiguredTickSize();
2418
+ const pctOf = (price) => {
2419
+ if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
2420
+ return `${((price - entry.price) / Math.abs(entry.price) * 100).toFixed(2)}%`;
2421
+ };
2422
+ const ticksOf = (price) => tick > 0 ? ` ${Math.round(Math.abs(price - entry.price) / tick)}t` : "";
2423
+ const targetDist = Math.abs(target.price - entry.price);
2424
+ const stopDist = Math.abs(entry.price - stop.price);
2425
+ const rr = stopDist > 0 ? targetDist / stopDist : 0;
2426
+ const cx = (boxX0 + boxX1) / 2;
2427
+ const drawPositionPill = (text, centerX, centerY, bg) => {
2428
+ const prevFont = ctx.font;
2429
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2430
+ const padding = 6;
2431
+ const textW = ctx.measureText(text).width;
2432
+ const pillW = textW + padding * 2;
2433
+ const pillH = 18;
2434
+ const pillX = centerX - pillW / 2;
2435
+ const pillY = centerY - pillH / 2;
2436
+ ctx.fillStyle = bg;
2437
+ fillRoundedRect(pillX, pillY, pillW, pillH, 4);
2438
+ ctx.fillStyle = "#ffffff";
2439
+ ctx.textAlign = "center";
2440
+ ctx.textBaseline = "middle";
2441
+ ctx.fillText(text, centerX, pillY + pillH / 2);
2442
+ ctx.font = prevFont;
2443
+ };
2444
+ drawPositionPill(`Target ${formatPrice(target.price)} (${pctOf(target.price)})${ticksOf(target.price)}`, cx, targetY, profitLine);
2445
+ drawPositionPill(`Stop ${formatPrice(stop.price)} (${pctOf(stop.price)})${ticksOf(stop.price)}`, cx, stopY, lossLine);
2446
+ drawPositionPill(`Entry ${formatPrice(entry.price)} RR ${rr.toFixed(2)}`, cx, entryY, hexToRgba(drawing.color, 0.92));
2447
+ if (drawing.label) {
2448
+ drawDrawingLabel(drawing.label, cx, Math.min(targetY, stopY) - 4, drawing.color);
2449
+ }
2450
+ }
2369
2451
  }
2370
2452
  ctx.restore();
2371
2453
  };
@@ -3562,6 +3644,28 @@ function createChart(element, options = {}) {
3562
3644
  } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
3563
3645
  return { drawing, target: "line" };
3564
3646
  }
3647
+ } else if (drawing.type === "long-position" || drawing.type === "short-position") {
3648
+ const entry = drawing.points[0];
3649
+ const target = drawing.points[1];
3650
+ const stop = drawing.points[2];
3651
+ const right = drawing.points[3];
3652
+ if (!entry || !target || !stop || !right) continue;
3653
+ const leftX = canvasXFromDrawingPoint(entry);
3654
+ const rightX = canvasXFromDrawingPoint(right);
3655
+ const entryY = canvasYFromDrawingPrice(entry.price);
3656
+ const targetY = canvasYFromDrawingPrice(target.price);
3657
+ const stopY = canvasYFromDrawingPrice(stop.price);
3658
+ if (Math.hypot(x - leftX, y - targetY) <= 9) return { drawing, target: "handle", pointIndex: 1 };
3659
+ if (Math.hypot(x - leftX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 0 };
3660
+ if (Math.hypot(x - leftX, y - stopY) <= 9) return { drawing, target: "handle", pointIndex: 2 };
3661
+ if (Math.hypot(x - rightX, y - entryY) <= 9) return { drawing, target: "handle", pointIndex: 3 };
3662
+ const x0 = Math.min(leftX, rightX);
3663
+ const x1 = Math.max(leftX, rightX);
3664
+ const yTop = Math.min(targetY, stopY, entryY);
3665
+ const yBot = Math.max(targetY, stopY, entryY);
3666
+ if (x >= x0 && x <= x1 && y >= yTop && y <= yBot) {
3667
+ return { drawing, target: "line" };
3668
+ }
3565
3669
  }
3566
3670
  }
3567
3671
  return null;
@@ -3762,6 +3866,33 @@ function createChart(element, options = {}) {
3762
3866
  draw();
3763
3867
  return true;
3764
3868
  }
3869
+ if (activeDrawingTool === "long-position" || activeDrawingTool === "short-position") {
3870
+ const isLong = activeDrawingTool === "long-position";
3871
+ const tick = getConfiguredTickSize();
3872
+ const priceOffset = tick > 0 ? tick * 20 : Math.abs(point.price) * 0.01 || 1;
3873
+ const width2 = Math.max(5, Math.round(xSpan * 0.15));
3874
+ const entryPrice = point.price;
3875
+ const targetPrice = isLong ? entryPrice + priceOffset : entryPrice - priceOffset;
3876
+ const stopPrice = isLong ? entryPrice - priceOffset : entryPrice + priceOffset;
3877
+ const defaults = getDrawingToolDefaults(activeDrawingTool);
3878
+ drawings.push(
3879
+ normalizeDrawingState({
3880
+ type: activeDrawingTool,
3881
+ points: [
3882
+ point,
3883
+ normalizeDrawingPoint(point.index, targetPrice),
3884
+ normalizeDrawingPoint(point.index, stopPrice),
3885
+ normalizeDrawingPoint(point.index + width2, entryPrice)
3886
+ ],
3887
+ color: defaults.color ?? "#2563eb",
3888
+ style: defaults.style ?? "solid",
3889
+ width: defaults.width ?? 1
3890
+ })
3891
+ );
3892
+ emitDrawingsChange();
3893
+ draw();
3894
+ return true;
3895
+ }
3765
3896
  return false;
3766
3897
  };
3767
3898
  const setDrawingDefaults = (tool, defaults) => {
@@ -3785,6 +3916,22 @@ function createChart(element, options = {}) {
3785
3916
  if (drawing.id !== drawingDragState?.drawingId || drawing.locked) {
3786
3917
  return drawing;
3787
3918
  }
3919
+ if (drawingDragState.target === "handle" && (drawing.type === "long-position" || drawing.type === "short-position")) {
3920
+ const pts = drawing.points.map((point) => ({ ...point }));
3921
+ const entry = pts[0];
3922
+ const pointIndex = drawingDragState.pointIndex ?? 0;
3923
+ if (pointIndex === 0) {
3924
+ pts[0] = normalizeDrawingPoint(entry.index, currentPoint.price);
3925
+ if (pts[3]) pts[3] = normalizeDrawingPoint(pts[3].index, currentPoint.price);
3926
+ } else if (pointIndex === 1 && pts[1]) {
3927
+ pts[1] = normalizeDrawingPoint(entry.index, currentPoint.price);
3928
+ } else if (pointIndex === 2 && pts[2]) {
3929
+ pts[2] = normalizeDrawingPoint(entry.index, currentPoint.price);
3930
+ } else if (pointIndex === 3 && pts[3]) {
3931
+ pts[3] = normalizeDrawingPoint(currentPoint.index, entry.price);
3932
+ }
3933
+ return { ...drawing, points: pts };
3934
+ }
3788
3935
  if (drawingDragState.target === "handle") {
3789
3936
  return {
3790
3937
  ...drawing,
package/docs/API.md CHANGED
@@ -422,6 +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.
425
426
  - `points: DrawingPoint[]`
426
427
  - `visible?: boolean`
427
428
  - `color?: string`
@@ -449,6 +450,8 @@ chart.setActiveDrawingTool("trendline"); // first click starts, second cli
449
450
  chart.setActiveDrawingTool("ray"); // first click starts, second click commits (extends past p2)
450
451
  chart.setActiveDrawingTool("fib-retracement"); // first click starts, second click commits
451
452
  chart.setActiveDrawingTool("fib-extension"); // three clicks: trend start, trend end, projection origin
453
+ chart.setActiveDrawingTool("long-position"); // single click drops a long risk/reward box
454
+ chart.setActiveDrawingTool("short-position"); // single click drops a short risk/reward box
452
455
  chart.setActiveDrawingTool(null); // back to normal cursor/pan
453
456
  ```
454
457
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.71",
3
+ "version": "0.1.72",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",