hyperprop-charting-library 0.1.70 → 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.
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";
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";
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
@@ -2088,6 +2088,7 @@ function createChart(element, options = {}) {
2088
2088
  };
2089
2089
  const xFromDrawingPoint = (point) => chartLeft + (point.index + 0.5 - xStart) / xSpan * chartWidth;
2090
2090
  const FIB_RATIOS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618];
2091
+ const FIB_EXT_RATIOS = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
2091
2092
  const hexToRgba = (hex, alpha) => {
2092
2093
  const cleaned = hex.replace("#", "");
2093
2094
  if (cleaned.length !== 6) {
@@ -2288,6 +2289,165 @@ function createChart(element, options = {}) {
2288
2289
  });
2289
2290
  ctx.font = prevFont;
2290
2291
  }
2292
+ } else if (drawing.type === "fib-extension") {
2293
+ const p0 = drawing.points[0];
2294
+ const p1 = drawing.points[1];
2295
+ const p2 = drawing.points[2];
2296
+ if (p0 && p1) {
2297
+ const x0 = xFromDrawingPoint(p0);
2298
+ const y0 = yFromPrice(p0.price);
2299
+ const x1 = xFromDrawingPoint(p1);
2300
+ const y1 = yFromPrice(p1.price);
2301
+ ctx.save();
2302
+ ctx.strokeStyle = hexToRgba(drawing.color, 0.6);
2303
+ ctx.setLineDash([4, 4]);
2304
+ ctx.lineWidth = 1;
2305
+ ctx.beginPath();
2306
+ ctx.moveTo(x0, y0);
2307
+ ctx.lineTo(x1, y1);
2308
+ if (p2) {
2309
+ ctx.lineTo(xFromDrawingPoint(p2), yFromPrice(p2.price));
2310
+ }
2311
+ ctx.stroke();
2312
+ ctx.restore();
2313
+ drawDrawingHandle(x0, y0, drawing.color);
2314
+ drawDrawingHandle(x1, y1, drawing.color);
2315
+ if (p2) {
2316
+ const x2 = xFromDrawingPoint(p2);
2317
+ const y2 = yFromPrice(p2.price);
2318
+ drawDrawingHandle(x2, y2, drawing.color);
2319
+ const palette = drawing.colors.length > 0 ? drawing.colors : null;
2320
+ const levelColorAt = (index) => palette ? palette[index % palette.length] : drawing.color;
2321
+ const move = p1.price - p0.price;
2322
+ const levelLines = FIB_EXT_RATIOS.map((ratio) => {
2323
+ const price = p2.price + move * ratio;
2324
+ return { ratio, price, y: yFromPrice(price) };
2325
+ });
2326
+ for (let index = 0; index < levelLines.length - 1; index += 1) {
2327
+ const top = levelLines[index];
2328
+ const bottom = levelLines[index + 1];
2329
+ const bandTop = Math.min(top.y, bottom.y);
2330
+ const bandBottom = Math.max(top.y, bottom.y);
2331
+ ctx.save();
2332
+ ctx.fillStyle = hexToRgba(levelColorAt(index), 0.1);
2333
+ ctx.globalAlpha = draft ? 0.5 : 1;
2334
+ ctx.fillRect(chartLeft, bandTop, chartRight - chartLeft, bandBottom - bandTop);
2335
+ ctx.restore();
2336
+ }
2337
+ ctx.save();
2338
+ ctx.lineWidth = drawing.width;
2339
+ applyDashPattern(drawing.style, dashPatterns.dotted, dashPatterns.dashed);
2340
+ levelLines.forEach((level, index) => {
2341
+ ctx.strokeStyle = levelColorAt(index);
2342
+ ctx.beginPath();
2343
+ ctx.moveTo(crisp(chartLeft), crisp(level.y));
2344
+ ctx.lineTo(crisp(chartRight), crisp(level.y));
2345
+ ctx.stroke();
2346
+ });
2347
+ ctx.restore();
2348
+ const prevFont = ctx.font;
2349
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2350
+ levelLines.forEach((level, index) => {
2351
+ const labelText = `${level.ratio} (${formatPrice(level.price)})`;
2352
+ const textWidth = ctx.measureText(labelText).width;
2353
+ const padding = 4;
2354
+ const bgX = chartLeft + 4;
2355
+ const bgY = level.y - 9;
2356
+ ctx.fillStyle = mergedOptions.backgroundColor;
2357
+ fillRoundedRect(bgX, bgY, textWidth + padding * 2, 16, 3);
2358
+ ctx.fillStyle = levelColorAt(index);
2359
+ ctx.textAlign = "left";
2360
+ ctx.textBaseline = "middle";
2361
+ ctx.fillText(labelText, bgX + padding, bgY + 8);
2362
+ });
2363
+ ctx.font = prevFont;
2364
+ if (drawing.label) {
2365
+ drawDrawingLabel(drawing.label, (x0 + x2) / 2, Math.min(y0, y2), drawing.color);
2366
+ }
2367
+ }
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
+ }
2291
2451
  }
2292
2452
  ctx.restore();
2293
2453
  };
@@ -3449,6 +3609,63 @@ function createChart(element, options = {}) {
3449
3609
  return { drawing, target: "line" };
3450
3610
  }
3451
3611
  }
3612
+ } else if (drawing.type === "fib-extension") {
3613
+ const p0 = drawing.points[0];
3614
+ const p1 = drawing.points[1];
3615
+ const p2 = drawing.points[2];
3616
+ if (!p0 || !p1) continue;
3617
+ const x0 = canvasXFromDrawingPoint(p0);
3618
+ const y0 = canvasYFromDrawingPrice(p0.price);
3619
+ const x1 = canvasXFromDrawingPoint(p1);
3620
+ const y1 = canvasYFromDrawingPrice(p1.price);
3621
+ if (Math.hypot(x - x0, y - y0) <= 8) {
3622
+ return { drawing, target: "handle", pointIndex: 0 };
3623
+ }
3624
+ if (Math.hypot(x - x1, y - y1) <= 8) {
3625
+ return { drawing, target: "handle", pointIndex: 1 };
3626
+ }
3627
+ if (p2) {
3628
+ const x2 = canvasXFromDrawingPoint(p2);
3629
+ const y2 = canvasYFromDrawingPrice(p2.price);
3630
+ if (Math.hypot(x - x2, y - y2) <= 8) {
3631
+ return { drawing, target: "handle", pointIndex: 2 };
3632
+ }
3633
+ const move = p1.price - p0.price;
3634
+ const fibExtRatios = [0, 0.382, 0.5, 0.618, 1, 1.272, 1.618, 2.618];
3635
+ for (const ratio of fibExtRatios) {
3636
+ const lineY = canvasYFromDrawingPrice(p2.price + move * ratio);
3637
+ if (Math.abs(y - lineY) <= 5) {
3638
+ return { drawing, target: "line" };
3639
+ }
3640
+ }
3641
+ if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6 || distanceToSegment(x, y, x1, y1, x2, y2) <= 6) {
3642
+ return { drawing, target: "line" };
3643
+ }
3644
+ } else if (distanceToSegment(x, y, x0, y0, x1, y1) <= 6) {
3645
+ return { drawing, target: "line" };
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
+ }
3452
3669
  }
3453
3670
  }
3454
3671
  return null;
@@ -3616,6 +3833,66 @@ function createChart(element, options = {}) {
3616
3833
  draw();
3617
3834
  return true;
3618
3835
  }
3836
+ if (activeDrawingTool === "fib-extension") {
3837
+ if (draftDrawing?.type === "fib-extension") {
3838
+ if (draftDrawing.points.length < 3) {
3839
+ draftDrawing = normalizeDrawingState({
3840
+ ...serializeDrawing(draftDrawing),
3841
+ points: [...draftDrawing.points.slice(0, -1), point, point]
3842
+ });
3843
+ draw();
3844
+ return true;
3845
+ }
3846
+ const completed = normalizeDrawingState({
3847
+ ...serializeDrawing(draftDrawing),
3848
+ points: [draftDrawing.points[0], draftDrawing.points[1], point]
3849
+ });
3850
+ drawings.push(completed);
3851
+ draftDrawing = null;
3852
+ activeDrawingTool = null;
3853
+ emitDrawingsChange();
3854
+ draw();
3855
+ return true;
3856
+ }
3857
+ const defaults = getDrawingToolDefaults("fib-extension");
3858
+ draftDrawing = normalizeDrawingState({
3859
+ type: "fib-extension",
3860
+ points: [point, point],
3861
+ color: defaults.color ?? "#2563eb",
3862
+ colors: defaults.colors ?? FIB_DEFAULT_PALETTE,
3863
+ style: defaults.style ?? "solid",
3864
+ width: defaults.width ?? 1
3865
+ });
3866
+ draw();
3867
+ return true;
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
+ }
3619
3896
  return false;
3620
3897
  };
3621
3898
  const setDrawingDefaults = (tool, defaults) => {
@@ -3639,6 +3916,22 @@ function createChart(element, options = {}) {
3639
3916
  if (drawing.id !== drawingDragState?.drawingId || drawing.locked) {
3640
3917
  return drawing;
3641
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
+ }
3642
3935
  if (drawingDragState.target === "handle") {
3643
3936
  return {
3644
3937
  ...drawing,
@@ -3888,12 +4181,12 @@ function createChart(element, options = {}) {
3888
4181
  setCrosshairPoint(null);
3889
4182
  return;
3890
4183
  }
3891
- if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement")) {
4184
+ if (draftDrawing && (activeDrawingTool === "trendline" || activeDrawingTool === "ray" || activeDrawingTool === "fib-retracement" || activeDrawingTool === "fib-extension")) {
3892
4185
  const nextPoint = drawingPointFromCanvas(point.x, point.y);
3893
4186
  if (nextPoint) {
3894
4187
  draftDrawing = {
3895
4188
  ...draftDrawing,
3896
- points: [draftDrawing.points[0], nextPoint]
4189
+ points: [...draftDrawing.points.slice(0, -1), nextPoint]
3897
4190
  };
3898
4191
  canvas.style.cursor = "crosshair";
3899
4192
  draw();
package/docs/API.md CHANGED
@@ -416,11 +416,13 @@ Volume style inputs:
416
416
  Drawings are user-created chart tools, separate from indicators. They are interactive chart objects like horizontal lines and trendlines.
417
417
 
418
418
  - `id?: string`
419
- - `type: "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement"`
419
+ - `type: "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension"`
420
420
  - `horizontal-line` / `vertical-line`: single-point, full-width/full-height line (one click to place)
421
421
  - `trendline`: two-point segment (click start, click end)
422
422
  - `ray`: two-point line that extends infinitely past the second point
423
423
  - `fib-retracement`: two-point retracement with levels/bands
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.
424
426
  - `points: DrawingPoint[]`
425
427
  - `visible?: boolean`
426
428
  - `color?: string`
@@ -447,6 +449,9 @@ chart.setActiveDrawingTool("vertical-line"); // next plot click creates a vert
447
449
  chart.setActiveDrawingTool("trendline"); // first click starts, second click commits
448
450
  chart.setActiveDrawingTool("ray"); // first click starts, second click commits (extends past p2)
449
451
  chart.setActiveDrawingTool("fib-retracement"); // first click starts, second click commits
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
450
455
  chart.setActiveDrawingTool(null); // back to normal cursor/pan
451
456
  ```
452
457
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.70",
3
+ "version": "0.1.72",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",