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.
- package/dist/hyperprop-charting-library.cjs +62 -17
- package/dist/hyperprop-charting-library.js +62 -17
- package/dist/index.cjs +62 -17
- package/dist/index.js +62 -17
- package/docs/API.md +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
2506
|
+
const pctOfDist = (dist) => {
|
|
2473
2507
|
if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
|
|
2474
|
-
|
|
2508
|
+
const pct = dist / Math.abs(entry.price) * 100;
|
|
2509
|
+
return `${pct < 1 ? pct.toFixed(3) : pct.toFixed(2)}%`;
|
|
2475
2510
|
};
|
|
2476
|
-
const
|
|
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) =>
|
|
2487
|
-
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2488
|
-
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2521
|
+
const formatAmount = (value) => value.toFixed(2);
|
|
2522
|
+
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * target.price * effectivePointValue)}` : "";
|
|
2523
|
+
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * stop.price * effectivePointValue)}` : "";
|
|
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
|
|
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
|
-
|
|
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(
|
|
2507
|
-
drawPositionPill(`Stop ${formatPrice(
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
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
|
|
2480
|
+
const pctOfDist = (dist) => {
|
|
2447
2481
|
if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
|
|
2448
|
-
|
|
2482
|
+
const pct = dist / Math.abs(entry.price) * 100;
|
|
2483
|
+
return `${pct < 1 ? pct.toFixed(3) : pct.toFixed(2)}%`;
|
|
2449
2484
|
};
|
|
2450
|
-
const
|
|
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) =>
|
|
2461
|
-
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2462
|
-
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2495
|
+
const formatAmount = (value) => value.toFixed(2);
|
|
2496
|
+
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * target.price * effectivePointValue)}` : "";
|
|
2497
|
+
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * stop.price * effectivePointValue)}` : "";
|
|
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
|
|
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
|
-
|
|
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(
|
|
2481
|
-
drawPositionPill(`Stop ${formatPrice(
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
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
|
|
2506
|
+
const pctOfDist = (dist) => {
|
|
2473
2507
|
if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
|
|
2474
|
-
|
|
2508
|
+
const pct = dist / Math.abs(entry.price) * 100;
|
|
2509
|
+
return `${pct < 1 ? pct.toFixed(3) : pct.toFixed(2)}%`;
|
|
2475
2510
|
};
|
|
2476
|
-
const
|
|
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) =>
|
|
2487
|
-
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2488
|
-
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2521
|
+
const formatAmount = (value) => value.toFixed(2);
|
|
2522
|
+
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * target.price * effectivePointValue)}` : "";
|
|
2523
|
+
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * stop.price * effectivePointValue)}` : "";
|
|
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
|
|
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
|
-
|
|
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(
|
|
2507
|
-
drawPositionPill(`Stop ${formatPrice(
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
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
|
|
2480
|
+
const pctOfDist = (dist) => {
|
|
2447
2481
|
if (!Number.isFinite(entry.price) || entry.price === 0) return "0.00%";
|
|
2448
|
-
|
|
2482
|
+
const pct = dist / Math.abs(entry.price) * 100;
|
|
2483
|
+
return `${pct < 1 ? pct.toFixed(3) : pct.toFixed(2)}%`;
|
|
2449
2484
|
};
|
|
2450
|
-
const
|
|
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) =>
|
|
2461
|
-
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2462
|
-
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw *
|
|
2495
|
+
const formatAmount = (value) => value.toFixed(2);
|
|
2496
|
+
const targetAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * target.price * effectivePointValue)}` : "";
|
|
2497
|
+
const stopAmountText = hasMoney ? `, Amount: ${formatAmount(qtyRaw * stop.price * effectivePointValue)}` : "";
|
|
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
|
|
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
|
-
|
|
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(
|
|
2481
|
-
drawPositionPill(`Stop ${formatPrice(
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
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`
|