hyperprop-charting-library 0.1.81 → 0.1.83

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.
@@ -2444,6 +2444,40 @@ function createChart(element, options = {}) {
2444
2444
  ctx.fillStyle = lossFill;
2445
2445
  ctx.fillRect(boxX0, Math.min(entryY, stopY), boxW, Math.abs(stopY - entryY));
2446
2446
  ctx.restore();
2447
+ const isLongPosition = drawing.type === "long-position";
2448
+ const simStart = Math.max(0, Math.round(Math.min(entry.index, right.index)));
2449
+ const simEnd = Math.min(data.length - 1, Math.round(Math.max(entry.index, right.index)));
2450
+ let positionHit = null;
2451
+ for (let i = simStart; i <= simEnd; i += 1) {
2452
+ const bar = data[i];
2453
+ if (!bar) continue;
2454
+ const targetTouched = isLongPosition ? bar.h >= target.price : bar.l <= target.price;
2455
+ const stopTouched = isLongPosition ? bar.l <= stop.price : bar.h >= stop.price;
2456
+ if (stopTouched) {
2457
+ positionHit = { index: i, price: stop.price, profit: false };
2458
+ break;
2459
+ }
2460
+ if (targetTouched) {
2461
+ positionHit = { index: i, price: target.price, profit: true };
2462
+ break;
2463
+ }
2464
+ }
2465
+ if (positionHit) {
2466
+ const hitX = clamp(xFromDrawingPoint({ index: positionHit.index, price: positionHit.price }), boxX0, boxX1);
2467
+ const exitY = yFromPrice(positionHit.price);
2468
+ ctx.save();
2469
+ ctx.globalAlpha = draft ? 0.6 : 1;
2470
+ ctx.fillStyle = hexToRgba(positionHit.profit ? profitColor : lossColor, 0.2);
2471
+ ctx.fillRect(boxX0, Math.min(entryY, exitY), Math.max(0, hitX - boxX0), Math.abs(exitY - entryY));
2472
+ ctx.setLineDash([5, 4]);
2473
+ ctx.lineWidth = 1;
2474
+ ctx.strokeStyle = hexToRgba("#787b86", 0.9);
2475
+ ctx.beginPath();
2476
+ ctx.moveTo(crisp(boxX0), crisp(entryY));
2477
+ ctx.lineTo(crisp(hitX), crisp(exitY));
2478
+ ctx.stroke();
2479
+ ctx.restore();
2480
+ }
2447
2481
  if (isSelected) {
2448
2482
  ctx.save();
2449
2483
  ctx.setLineDash([]);
@@ -2469,11 +2503,12 @@ function createChart(element, options = {}) {
2469
2503
  handleAt(leftX, stopY, drawing.color);
2470
2504
  handleAt(rightX, entryY, drawing.color);
2471
2505
  const tick = getConfiguredTickSize();
2472
- const pctOf = (price) => {
2506
+ const pctOfDist = (dist) => {
2473
2507
  if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
2474
- 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)}%`;
2475
2510
  };
2476
- const ticksOf = (price) => tick > 0 ? ` ${Math.round(Math.abs(price - entry.price) / tick)}t` : "";
2511
+ const ticksOfDist = (dist) => tick > 0 ? ` ${Math.round(dist / tick)}` : "";
2477
2512
  const targetDist = Math.abs(target.price - entry.price);
2478
2513
  const stopDist = Math.abs(entry.price - stop.price);
2479
2514
  const rr = stopDist > 0 ? targetDist / stopDist : 0;
@@ -2483,16 +2518,18 @@ function createChart(element, options = {}) {
2483
2518
  const qtyRaw = riskAmount > 0 && stopDist > 0 ? riskAmount / (stopDist * effectivePointValue) : 0;
2484
2519
  const hasMoney = qtyRaw > 0 && Number.isFinite(qtyRaw);
2485
2520
  const qtyText = hasMoney ? qtyRaw.toFixed(Math.max(0, drawing.qtyPrecision)) : "";
2486
- const formatAmount = (value) => Math.abs(value) >= 1e3 ? value.toFixed(0) : value.toFixed(2);
2487
- const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * targetDist * effectivePointValue)}` : "";
2488
- 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)}` : "";
2489
2524
  const drawPositionPill = (text, centerX, centerY, bg) => {
2525
+ const lines = Array.isArray(text) ? text : [text];
2490
2526
  const prevFont = ctx.font;
2491
2527
  ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2492
2528
  const padding = 6;
2493
- const textW = ctx.measureText(text).width;
2529
+ const lineH = 14;
2530
+ const textW = Math.max(...lines.map((line) => ctx.measureText(line).width));
2494
2531
  const pillW = textW + padding * 2;
2495
- const pillH = 18;
2532
+ const pillH = lines.length === 1 ? 18 : lines.length * lineH + 6;
2496
2533
  const pillX = centerX - pillW / 2;
2497
2534
  const pillY = centerY - pillH / 2;
2498
2535
  ctx.fillStyle = bg;
@@ -2500,17 +2537,25 @@ function createChart(element, options = {}) {
2500
2537
  ctx.fillStyle = labelTextColor;
2501
2538
  ctx.textAlign = "center";
2502
2539
  ctx.textBaseline = "middle";
2503
- 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));
2504
2542
  ctx.font = prevFont;
2505
2543
  };
2506
- drawPositionPill(`Target ${formatPrice(target.price)} (${pctOf(target.price)})${ticksOf(target.price)}${targetAmountText}`, cx, targetY, profitLine);
2507
- drawPositionPill(`Stop ${formatPrice(stop.price)} (${pctOf(stop.price)})${ticksOf(stop.price)}${stopAmountText}`, cx, stopY, lossLine);
2508
- drawPositionPill(
2509
- hasMoney ? `Entry ${formatPrice(entry.price)} Qty ${qtyText} RR ${rr.toFixed(2)}` : `Entry ${formatPrice(entry.price)} RR ${rr.toFixed(2)}`,
2510
- cx,
2511
- entryY,
2512
- hexToRgba(drawing.color, 0.92)
2513
- );
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;
2547
+ let centerBg;
2548
+ if (positionHit) {
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)}`];
2552
+ centerBg = positionHit.profit ? profitLine : lossLine;
2553
+ } else {
2554
+ const line1 = hasMoney ? `Entry: ${formatPrice(entry.price)}, Qty: ${qtyText}` : `Entry: ${formatPrice(entry.price)}`;
2555
+ centerLines = [line1, `Risk/reward ratio: ${rr.toFixed(2)}`];
2556
+ centerBg = hexToRgba(drawing.color, 0.92);
2557
+ }
2558
+ drawPositionPill(centerLines, cx, entryY, centerBg);
2514
2559
  if (drawing.label) {
2515
2560
  drawDrawingLabel(drawing.label, cx, Math.min(targetY, stopY) - 4, drawing.color);
2516
2561
  }
@@ -2418,6 +2418,40 @@ function createChart(element, options = {}) {
2418
2418
  ctx.fillStyle = lossFill;
2419
2419
  ctx.fillRect(boxX0, Math.min(entryY, stopY), boxW, Math.abs(stopY - entryY));
2420
2420
  ctx.restore();
2421
+ const isLongPosition = drawing.type === "long-position";
2422
+ const simStart = Math.max(0, Math.round(Math.min(entry.index, right.index)));
2423
+ const simEnd = Math.min(data.length - 1, Math.round(Math.max(entry.index, right.index)));
2424
+ let positionHit = null;
2425
+ for (let i = simStart; i <= simEnd; i += 1) {
2426
+ const bar = data[i];
2427
+ if (!bar) continue;
2428
+ const targetTouched = isLongPosition ? bar.h >= target.price : bar.l <= target.price;
2429
+ const stopTouched = isLongPosition ? bar.l <= stop.price : bar.h >= stop.price;
2430
+ if (stopTouched) {
2431
+ positionHit = { index: i, price: stop.price, profit: false };
2432
+ break;
2433
+ }
2434
+ if (targetTouched) {
2435
+ positionHit = { index: i, price: target.price, profit: true };
2436
+ break;
2437
+ }
2438
+ }
2439
+ if (positionHit) {
2440
+ const hitX = clamp(xFromDrawingPoint({ index: positionHit.index, price: positionHit.price }), boxX0, boxX1);
2441
+ const exitY = yFromPrice(positionHit.price);
2442
+ ctx.save();
2443
+ ctx.globalAlpha = draft ? 0.6 : 1;
2444
+ ctx.fillStyle = hexToRgba(positionHit.profit ? profitColor : lossColor, 0.2);
2445
+ ctx.fillRect(boxX0, Math.min(entryY, exitY), Math.max(0, hitX - boxX0), Math.abs(exitY - entryY));
2446
+ ctx.setLineDash([5, 4]);
2447
+ ctx.lineWidth = 1;
2448
+ ctx.strokeStyle = hexToRgba("#787b86", 0.9);
2449
+ ctx.beginPath();
2450
+ ctx.moveTo(crisp(boxX0), crisp(entryY));
2451
+ ctx.lineTo(crisp(hitX), crisp(exitY));
2452
+ ctx.stroke();
2453
+ ctx.restore();
2454
+ }
2421
2455
  if (isSelected) {
2422
2456
  ctx.save();
2423
2457
  ctx.setLineDash([]);
@@ -2443,11 +2477,12 @@ function createChart(element, options = {}) {
2443
2477
  handleAt(leftX, stopY, drawing.color);
2444
2478
  handleAt(rightX, entryY, drawing.color);
2445
2479
  const tick = getConfiguredTickSize();
2446
- const pctOf = (price) => {
2480
+ const pctOfDist = (dist) => {
2447
2481
  if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
2448
- 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)}%`;
2449
2484
  };
2450
- const ticksOf = (price) => tick > 0 ? ` ${Math.round(Math.abs(price - entry.price) / tick)}t` : "";
2485
+ const ticksOfDist = (dist) => tick > 0 ? ` ${Math.round(dist / tick)}` : "";
2451
2486
  const targetDist = Math.abs(target.price - entry.price);
2452
2487
  const stopDist = Math.abs(entry.price - stop.price);
2453
2488
  const rr = stopDist > 0 ? targetDist / stopDist : 0;
@@ -2457,16 +2492,18 @@ function createChart(element, options = {}) {
2457
2492
  const qtyRaw = riskAmount > 0 && stopDist > 0 ? riskAmount / (stopDist * effectivePointValue) : 0;
2458
2493
  const hasMoney = qtyRaw > 0 && Number.isFinite(qtyRaw);
2459
2494
  const qtyText = hasMoney ? qtyRaw.toFixed(Math.max(0, drawing.qtyPrecision)) : "";
2460
- const formatAmount = (value) => Math.abs(value) >= 1e3 ? value.toFixed(0) : value.toFixed(2);
2461
- const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * targetDist * effectivePointValue)}` : "";
2462
- 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)}` : "";
2463
2498
  const drawPositionPill = (text, centerX, centerY, bg) => {
2499
+ const lines = Array.isArray(text) ? text : [text];
2464
2500
  const prevFont = ctx.font;
2465
2501
  ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2466
2502
  const padding = 6;
2467
- const textW = ctx.measureText(text).width;
2503
+ const lineH = 14;
2504
+ const textW = Math.max(...lines.map((line) => ctx.measureText(line).width));
2468
2505
  const pillW = textW + padding * 2;
2469
- const pillH = 18;
2506
+ const pillH = lines.length === 1 ? 18 : lines.length * lineH + 6;
2470
2507
  const pillX = centerX - pillW / 2;
2471
2508
  const pillY = centerY - pillH / 2;
2472
2509
  ctx.fillStyle = bg;
@@ -2474,17 +2511,25 @@ function createChart(element, options = {}) {
2474
2511
  ctx.fillStyle = labelTextColor;
2475
2512
  ctx.textAlign = "center";
2476
2513
  ctx.textBaseline = "middle";
2477
- 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));
2478
2516
  ctx.font = prevFont;
2479
2517
  };
2480
- drawPositionPill(`Target ${formatPrice(target.price)} (${pctOf(target.price)})${ticksOf(target.price)}${targetAmountText}`, cx, targetY, profitLine);
2481
- drawPositionPill(`Stop ${formatPrice(stop.price)} (${pctOf(stop.price)})${ticksOf(stop.price)}${stopAmountText}`, cx, stopY, lossLine);
2482
- drawPositionPill(
2483
- hasMoney ? `Entry ${formatPrice(entry.price)} Qty ${qtyText} RR ${rr.toFixed(2)}` : `Entry ${formatPrice(entry.price)} RR ${rr.toFixed(2)}`,
2484
- cx,
2485
- entryY,
2486
- hexToRgba(drawing.color, 0.92)
2487
- );
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;
2521
+ let centerBg;
2522
+ if (positionHit) {
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)}`];
2526
+ centerBg = positionHit.profit ? profitLine : lossLine;
2527
+ } else {
2528
+ const line1 = hasMoney ? `Entry: ${formatPrice(entry.price)}, Qty: ${qtyText}` : `Entry: ${formatPrice(entry.price)}`;
2529
+ centerLines = [line1, `Risk/reward ratio: ${rr.toFixed(2)}`];
2530
+ centerBg = hexToRgba(drawing.color, 0.92);
2531
+ }
2532
+ drawPositionPill(centerLines, cx, entryY, centerBg);
2488
2533
  if (drawing.label) {
2489
2534
  drawDrawingLabel(drawing.label, cx, Math.min(targetY, stopY) - 4, drawing.color);
2490
2535
  }
package/dist/index.cjs CHANGED
@@ -2444,6 +2444,40 @@ function createChart(element, options = {}) {
2444
2444
  ctx.fillStyle = lossFill;
2445
2445
  ctx.fillRect(boxX0, Math.min(entryY, stopY), boxW, Math.abs(stopY - entryY));
2446
2446
  ctx.restore();
2447
+ const isLongPosition = drawing.type === "long-position";
2448
+ const simStart = Math.max(0, Math.round(Math.min(entry.index, right.index)));
2449
+ const simEnd = Math.min(data.length - 1, Math.round(Math.max(entry.index, right.index)));
2450
+ let positionHit = null;
2451
+ for (let i = simStart; i <= simEnd; i += 1) {
2452
+ const bar = data[i];
2453
+ if (!bar) continue;
2454
+ const targetTouched = isLongPosition ? bar.h >= target.price : bar.l <= target.price;
2455
+ const stopTouched = isLongPosition ? bar.l <= stop.price : bar.h >= stop.price;
2456
+ if (stopTouched) {
2457
+ positionHit = { index: i, price: stop.price, profit: false };
2458
+ break;
2459
+ }
2460
+ if (targetTouched) {
2461
+ positionHit = { index: i, price: target.price, profit: true };
2462
+ break;
2463
+ }
2464
+ }
2465
+ if (positionHit) {
2466
+ const hitX = clamp(xFromDrawingPoint({ index: positionHit.index, price: positionHit.price }), boxX0, boxX1);
2467
+ const exitY = yFromPrice(positionHit.price);
2468
+ ctx.save();
2469
+ ctx.globalAlpha = draft ? 0.6 : 1;
2470
+ ctx.fillStyle = hexToRgba(positionHit.profit ? profitColor : lossColor, 0.2);
2471
+ ctx.fillRect(boxX0, Math.min(entryY, exitY), Math.max(0, hitX - boxX0), Math.abs(exitY - entryY));
2472
+ ctx.setLineDash([5, 4]);
2473
+ ctx.lineWidth = 1;
2474
+ ctx.strokeStyle = hexToRgba("#787b86", 0.9);
2475
+ ctx.beginPath();
2476
+ ctx.moveTo(crisp(boxX0), crisp(entryY));
2477
+ ctx.lineTo(crisp(hitX), crisp(exitY));
2478
+ ctx.stroke();
2479
+ ctx.restore();
2480
+ }
2447
2481
  if (isSelected) {
2448
2482
  ctx.save();
2449
2483
  ctx.setLineDash([]);
@@ -2469,11 +2503,12 @@ function createChart(element, options = {}) {
2469
2503
  handleAt(leftX, stopY, drawing.color);
2470
2504
  handleAt(rightX, entryY, drawing.color);
2471
2505
  const tick = getConfiguredTickSize();
2472
- const pctOf = (price) => {
2506
+ const pctOfDist = (dist) => {
2473
2507
  if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
2474
- 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)}%`;
2475
2510
  };
2476
- const ticksOf = (price) => tick > 0 ? ` ${Math.round(Math.abs(price - entry.price) / tick)}t` : "";
2511
+ const ticksOfDist = (dist) => tick > 0 ? ` ${Math.round(dist / tick)}` : "";
2477
2512
  const targetDist = Math.abs(target.price - entry.price);
2478
2513
  const stopDist = Math.abs(entry.price - stop.price);
2479
2514
  const rr = stopDist > 0 ? targetDist / stopDist : 0;
@@ -2483,16 +2518,18 @@ function createChart(element, options = {}) {
2483
2518
  const qtyRaw = riskAmount > 0 && stopDist > 0 ? riskAmount / (stopDist * effectivePointValue) : 0;
2484
2519
  const hasMoney = qtyRaw > 0 && Number.isFinite(qtyRaw);
2485
2520
  const qtyText = hasMoney ? qtyRaw.toFixed(Math.max(0, drawing.qtyPrecision)) : "";
2486
- const formatAmount = (value) => Math.abs(value) >= 1e3 ? value.toFixed(0) : value.toFixed(2);
2487
- const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * targetDist * effectivePointValue)}` : "";
2488
- 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)}` : "";
2489
2524
  const drawPositionPill = (text, centerX, centerY, bg) => {
2525
+ const lines = Array.isArray(text) ? text : [text];
2490
2526
  const prevFont = ctx.font;
2491
2527
  ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2492
2528
  const padding = 6;
2493
- const textW = ctx.measureText(text).width;
2529
+ const lineH = 14;
2530
+ const textW = Math.max(...lines.map((line) => ctx.measureText(line).width));
2494
2531
  const pillW = textW + padding * 2;
2495
- const pillH = 18;
2532
+ const pillH = lines.length === 1 ? 18 : lines.length * lineH + 6;
2496
2533
  const pillX = centerX - pillW / 2;
2497
2534
  const pillY = centerY - pillH / 2;
2498
2535
  ctx.fillStyle = bg;
@@ -2500,17 +2537,25 @@ function createChart(element, options = {}) {
2500
2537
  ctx.fillStyle = labelTextColor;
2501
2538
  ctx.textAlign = "center";
2502
2539
  ctx.textBaseline = "middle";
2503
- 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));
2504
2542
  ctx.font = prevFont;
2505
2543
  };
2506
- drawPositionPill(`Target ${formatPrice(target.price)} (${pctOf(target.price)})${ticksOf(target.price)}${targetAmountText}`, cx, targetY, profitLine);
2507
- drawPositionPill(`Stop ${formatPrice(stop.price)} (${pctOf(stop.price)})${ticksOf(stop.price)}${stopAmountText}`, cx, stopY, lossLine);
2508
- drawPositionPill(
2509
- hasMoney ? `Entry ${formatPrice(entry.price)} Qty ${qtyText} RR ${rr.toFixed(2)}` : `Entry ${formatPrice(entry.price)} RR ${rr.toFixed(2)}`,
2510
- cx,
2511
- entryY,
2512
- hexToRgba(drawing.color, 0.92)
2513
- );
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;
2547
+ let centerBg;
2548
+ if (positionHit) {
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)}`];
2552
+ centerBg = positionHit.profit ? profitLine : lossLine;
2553
+ } else {
2554
+ const line1 = hasMoney ? `Entry: ${formatPrice(entry.price)}, Qty: ${qtyText}` : `Entry: ${formatPrice(entry.price)}`;
2555
+ centerLines = [line1, `Risk/reward ratio: ${rr.toFixed(2)}`];
2556
+ centerBg = hexToRgba(drawing.color, 0.92);
2557
+ }
2558
+ drawPositionPill(centerLines, cx, entryY, centerBg);
2514
2559
  if (drawing.label) {
2515
2560
  drawDrawingLabel(drawing.label, cx, Math.min(targetY, stopY) - 4, drawing.color);
2516
2561
  }
package/dist/index.js CHANGED
@@ -2418,6 +2418,40 @@ function createChart(element, options = {}) {
2418
2418
  ctx.fillStyle = lossFill;
2419
2419
  ctx.fillRect(boxX0, Math.min(entryY, stopY), boxW, Math.abs(stopY - entryY));
2420
2420
  ctx.restore();
2421
+ const isLongPosition = drawing.type === "long-position";
2422
+ const simStart = Math.max(0, Math.round(Math.min(entry.index, right.index)));
2423
+ const simEnd = Math.min(data.length - 1, Math.round(Math.max(entry.index, right.index)));
2424
+ let positionHit = null;
2425
+ for (let i = simStart; i <= simEnd; i += 1) {
2426
+ const bar = data[i];
2427
+ if (!bar) continue;
2428
+ const targetTouched = isLongPosition ? bar.h >= target.price : bar.l <= target.price;
2429
+ const stopTouched = isLongPosition ? bar.l <= stop.price : bar.h >= stop.price;
2430
+ if (stopTouched) {
2431
+ positionHit = { index: i, price: stop.price, profit: false };
2432
+ break;
2433
+ }
2434
+ if (targetTouched) {
2435
+ positionHit = { index: i, price: target.price, profit: true };
2436
+ break;
2437
+ }
2438
+ }
2439
+ if (positionHit) {
2440
+ const hitX = clamp(xFromDrawingPoint({ index: positionHit.index, price: positionHit.price }), boxX0, boxX1);
2441
+ const exitY = yFromPrice(positionHit.price);
2442
+ ctx.save();
2443
+ ctx.globalAlpha = draft ? 0.6 : 1;
2444
+ ctx.fillStyle = hexToRgba(positionHit.profit ? profitColor : lossColor, 0.2);
2445
+ ctx.fillRect(boxX0, Math.min(entryY, exitY), Math.max(0, hitX - boxX0), Math.abs(exitY - entryY));
2446
+ ctx.setLineDash([5, 4]);
2447
+ ctx.lineWidth = 1;
2448
+ ctx.strokeStyle = hexToRgba("#787b86", 0.9);
2449
+ ctx.beginPath();
2450
+ ctx.moveTo(crisp(boxX0), crisp(entryY));
2451
+ ctx.lineTo(crisp(hitX), crisp(exitY));
2452
+ ctx.stroke();
2453
+ ctx.restore();
2454
+ }
2421
2455
  if (isSelected) {
2422
2456
  ctx.save();
2423
2457
  ctx.setLineDash([]);
@@ -2443,11 +2477,12 @@ function createChart(element, options = {}) {
2443
2477
  handleAt(leftX, stopY, drawing.color);
2444
2478
  handleAt(rightX, entryY, drawing.color);
2445
2479
  const tick = getConfiguredTickSize();
2446
- const pctOf = (price) => {
2480
+ const pctOfDist = (dist) => {
2447
2481
  if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
2448
- 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)}%`;
2449
2484
  };
2450
- const ticksOf = (price) => tick > 0 ? ` ${Math.round(Math.abs(price - entry.price) / tick)}t` : "";
2485
+ const ticksOfDist = (dist) => tick > 0 ? ` ${Math.round(dist / tick)}` : "";
2451
2486
  const targetDist = Math.abs(target.price - entry.price);
2452
2487
  const stopDist = Math.abs(entry.price - stop.price);
2453
2488
  const rr = stopDist > 0 ? targetDist / stopDist : 0;
@@ -2457,16 +2492,18 @@ function createChart(element, options = {}) {
2457
2492
  const qtyRaw = riskAmount > 0 && stopDist > 0 ? riskAmount / (stopDist * effectivePointValue) : 0;
2458
2493
  const hasMoney = qtyRaw > 0 && Number.isFinite(qtyRaw);
2459
2494
  const qtyText = hasMoney ? qtyRaw.toFixed(Math.max(0, drawing.qtyPrecision)) : "";
2460
- const formatAmount = (value) => Math.abs(value) >= 1e3 ? value.toFixed(0) : value.toFixed(2);
2461
- const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * targetDist * effectivePointValue)}` : "";
2462
- 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)}` : "";
2463
2498
  const drawPositionPill = (text, centerX, centerY, bg) => {
2499
+ const lines = Array.isArray(text) ? text : [text];
2464
2500
  const prevFont = ctx.font;
2465
2501
  ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2466
2502
  const padding = 6;
2467
- const textW = ctx.measureText(text).width;
2503
+ const lineH = 14;
2504
+ const textW = Math.max(...lines.map((line) => ctx.measureText(line).width));
2468
2505
  const pillW = textW + padding * 2;
2469
- const pillH = 18;
2506
+ const pillH = lines.length === 1 ? 18 : lines.length * lineH + 6;
2470
2507
  const pillX = centerX - pillW / 2;
2471
2508
  const pillY = centerY - pillH / 2;
2472
2509
  ctx.fillStyle = bg;
@@ -2474,17 +2511,25 @@ function createChart(element, options = {}) {
2474
2511
  ctx.fillStyle = labelTextColor;
2475
2512
  ctx.textAlign = "center";
2476
2513
  ctx.textBaseline = "middle";
2477
- 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));
2478
2516
  ctx.font = prevFont;
2479
2517
  };
2480
- drawPositionPill(`Target ${formatPrice(target.price)} (${pctOf(target.price)})${ticksOf(target.price)}${targetAmountText}`, cx, targetY, profitLine);
2481
- drawPositionPill(`Stop ${formatPrice(stop.price)} (${pctOf(stop.price)})${ticksOf(stop.price)}${stopAmountText}`, cx, stopY, lossLine);
2482
- drawPositionPill(
2483
- hasMoney ? `Entry ${formatPrice(entry.price)} Qty ${qtyText} RR ${rr.toFixed(2)}` : `Entry ${formatPrice(entry.price)} RR ${rr.toFixed(2)}`,
2484
- cx,
2485
- entryY,
2486
- hexToRgba(drawing.color, 0.92)
2487
- );
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;
2521
+ let centerBg;
2522
+ if (positionHit) {
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)}`];
2526
+ centerBg = positionHit.profit ? profitLine : lossLine;
2527
+ } else {
2528
+ const line1 = hasMoney ? `Entry: ${formatPrice(entry.price)}, Qty: ${qtyText}` : `Entry: ${formatPrice(entry.price)}`;
2529
+ centerLines = [line1, `Risk/reward ratio: ${rr.toFixed(2)}`];
2530
+ centerBg = hexToRgba(drawing.color, 0.92);
2531
+ }
2532
+ drawPositionPill(centerLines, cx, entryY, centerBg);
2488
2533
  if (drawing.label) {
2489
2534
  drawDrawingLabel(drawing.label, cx, Math.min(targetY, stopY) - 4, drawing.color);
2490
2535
  }
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.
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).
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.81",
3
+ "version": "0.1.83",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",