@funnest/charts 0.6.5 → 0.6.7

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/chart.d.ts CHANGED
@@ -3,7 +3,7 @@ import { TimeScale, type IndexRange, type TimeScaleOptions } from './model/time-
3
3
  import type { IndicatorDefinition, IndicatorParams, IndicatorType } from './indicators/definitions';
4
4
  import { type IndicatorConfig, type IndicatorPatch } from './indicators/manager';
5
5
  import { type Drawing, type DrawingDefinition, type DrawingType } from './objects/drawings';
6
- import { type AlertLineOptions, type ExecutionMarker, type LineHandle, type OrderLineOptions, type PositionLineOptions } from './objects/trading';
6
+ import { type AlertLineOptions, type ExecutionMarker, type LineHandle, type OrderLineOptions, type PositionLineOptions, type TradingLabelOptions } from './objects/trading';
7
7
  import { type ChartSettings, type ChartState } from './settings';
8
8
  import { type TradingAdapter } from './objects/trading-sync';
9
9
  import { type ChartType, type CrosshairInfo, type PaneId } from './render/renderer';
@@ -411,8 +411,11 @@ export declare class Chart {
411
411
  addOrderLine(options: OrderLineOptions): LineHandle<OrderLineOptions>;
412
412
  /** Price alert line (dotted, gray, "알림" label). Pass `onMove` to make it draggable and `onCancel` for an ×. */
413
413
  addAlertLine(options: AlertLineOptions): LineHandle<AlertLineOptions>;
414
- /** Fill markers (▲ buy under the bar, ▼ sell above). Replaces the previous set. */
414
+ /** Fill markers (▲ buy under the bar, ▼ sell above; ⚡ at the price for liquidations). Replaces the previous set. */
415
415
  setExecutions(markers: readonly ExecutionMarker[]): void;
416
+ /** What position/order/alert line labels show and where (direction, price, size, P&L, ×, alignment, line width). */
417
+ setTradingLabels(patch: Partial<TradingLabelOptions>): void;
418
+ get tradingLabels(): TradingLabelOptions;
416
419
  setTheme(theme: Partial<ThemeTokens>): void;
417
420
  /** Re-reads CSS variables used by the theme (call after changing them outside of class/style attributes). */
418
421
  refreshTheme(): void;
package/dist/index.d.ts CHANGED
@@ -19,7 +19,8 @@ export type { PaneCoords } from './objects/types';
19
19
  export type { Painter } from './render/painter';
20
20
  export type { Palette } from './render/renderer';
21
21
  export { snapToOhlc } from './objects/magnet';
22
- export type { AlertLineOptions, ExecutionGroup, ExecutionMarker, LineHandle, OrderLineOptions, PositionLineOptions } from './objects/trading';
22
+ export { DEFAULT_TRADING_LABEL_OPTIONS } from './objects/trading';
23
+ export type { AlertLineOptions, ExecutionGroup, ExecutionMarker, LineHandle, OrderLineOptions, PositionLineOptions, TradingLabelOptions } from './objects/trading';
23
24
  export type { Execution, OrderInfo, PositionInfo, TradingAdapter, TradingCallbacks } from './objects/trading-sync';
24
25
  export { bucketStart, TickAggregator, timeZoneOffset } from './realtime';
25
26
  export type { Tick } from './realtime';
package/dist/index.js CHANGED
@@ -1554,6 +1554,10 @@ var Painter = class {
1554
1554
  * A row of joined label segments ("pill") centered vertically on `y`.
1555
1555
  * Returns each segment's rect for hit testing (e.g. an order's cancel button).
1556
1556
  */
1557
+ /** Width `pill` will take for these segments. */
1558
+ pillWidth(segments) {
1559
+ return segments.reduce((w, seg) => w + Math.ceil(this.measure(seg.text)) + (seg.padding ?? 8) * 2, 0);
1560
+ }
1557
1561
  pill(segments, x, y, height = 20, radius = 4) {
1558
1562
  const { ctx } = this;
1559
1563
  const rects = [];
@@ -1579,8 +1583,18 @@ var Painter = class {
1579
1583
  var inRect = (r, x, y, pad = 0) => x >= r.x - pad && x <= r.x + r.w + pad && y >= r.y - pad && y <= r.y + r.h + pad;
1580
1584
 
1581
1585
  // src/objects/trading.ts
1586
+ var DEFAULT_TRADING_LABEL_OPTIONS = {
1587
+ side: true,
1588
+ price: false,
1589
+ quantity: true,
1590
+ pnl: true,
1591
+ buttons: true,
1592
+ align: "left",
1593
+ fullWidth: true
1594
+ };
1582
1595
  var LABEL_X = 12;
1583
1596
  var LABEL_HEIGHT = 20;
1597
+ var LABEL_GAP = 6;
1584
1598
  var ORDER_DASH = [6, 4];
1585
1599
  var ALERT_DASH = [2, 3];
1586
1600
  var TradingLayer = class {
@@ -1595,6 +1609,14 @@ var TradingLayer = class {
1595
1609
  labelRects = /* @__PURE__ */ new Map();
1596
1610
  drag = null;
1597
1611
  groups = [];
1612
+ labelOptions = { ...DEFAULT_TRADING_LABEL_OPTIONS };
1613
+ get labelSettings() {
1614
+ return { ...this.labelOptions };
1615
+ }
1616
+ setLabelOptions(patch) {
1617
+ this.labelOptions = { ...this.labelOptions, ...patch };
1618
+ this.onChange();
1619
+ }
1598
1620
  addPosition(options) {
1599
1621
  const id = newId("pos");
1600
1622
  this.positions.set(id, { ...options });
@@ -1699,19 +1721,21 @@ var TradingLayer = class {
1699
1721
  const index = barIndexAt(e.time);
1700
1722
  const bar = bars[index];
1701
1723
  if (!bar) continue;
1702
- const key = `${bar.time}:${e.side}`;
1724
+ const liquidation = e.kind === "liquidation";
1725
+ const key = `${bar.time}:${liquidation ? "liq" : e.side}`;
1703
1726
  let g = byKey.get(key);
1704
1727
  if (!g) {
1705
1728
  const buy = e.side === "buy";
1706
1729
  g = {
1707
1730
  key,
1708
1731
  side: e.side,
1732
+ kind: liquidation ? "liquidation" : "fill",
1709
1733
  count: 0,
1710
1734
  quantity: 0,
1711
1735
  averagePrice: 0,
1712
1736
  time: e.time,
1713
1737
  x: indexToX(index),
1714
- y: buy ? c.priceToY(bar.low) + 10 : c.priceToY(bar.high) - 10,
1738
+ y: liquidation ? c.priceToY(e.price) : buy ? c.priceToY(bar.low) + 10 : c.priceToY(bar.high) - 10,
1715
1739
  weighted: 0,
1716
1740
  weight: 0
1717
1741
  };
@@ -1728,6 +1752,23 @@ var TradingLayer = class {
1728
1752
  const { ctx } = p;
1729
1753
  for (const g of this.groups) {
1730
1754
  if (g.x < -10 || g.x > c.plotWidth + 10) continue;
1755
+ if (g.kind === "liquidation") {
1756
+ ctx.beginPath();
1757
+ ctx.arc(g.x, g.y, 8, 0, Math.PI * 2);
1758
+ ctx.fillStyle = palette.down;
1759
+ ctx.fill();
1760
+ ctx.beginPath();
1761
+ ctx.moveTo(g.x + 1.5, g.y - 5.5);
1762
+ ctx.lineTo(g.x - 3.5, g.y + 1);
1763
+ ctx.lineTo(g.x - 0.2, g.y + 1);
1764
+ ctx.lineTo(g.x - 1.5, g.y + 5.5);
1765
+ ctx.lineTo(g.x + 3.5, g.y - 1);
1766
+ ctx.lineTo(g.x + 0.2, g.y - 1);
1767
+ ctx.closePath();
1768
+ ctx.fillStyle = "#FFFFFF";
1769
+ ctx.fill();
1770
+ continue;
1771
+ }
1731
1772
  const buy = g.side === "buy";
1732
1773
  ctx.beginPath();
1733
1774
  ctx.moveTo(g.x, buy ? g.y - 5 : g.y + 5);
@@ -1738,15 +1779,41 @@ var TradingLayer = class {
1738
1779
  ctx.fill();
1739
1780
  if (g.count > 1) p.text(String(g.count), g.x + 7, g.y, buy ? palette.up : palette.down, "left", "middle");
1740
1781
  }
1782
+ const opts = this.labelOptions;
1783
+ const placed = [];
1784
+ const placeX = (width, y) => {
1785
+ const toLeft = opts.align === "right";
1786
+ let x = opts.align === "left" ? LABEL_X : toLeft ? c.plotWidth - width - LABEL_X : (c.plotWidth - width) / 2;
1787
+ for (let moved = true; moved; ) {
1788
+ moved = false;
1789
+ for (const r of placed) {
1790
+ const overlapsY = Math.abs(r.y + LABEL_HEIGHT / 2 - y) < LABEL_HEIGHT + 2;
1791
+ if (overlapsY && x < r.x + r.w + LABEL_GAP && x + width > r.x - LABEL_GAP) {
1792
+ x = toLeft ? r.x - width - LABEL_GAP : r.x + r.w + LABEL_GAP;
1793
+ moved = true;
1794
+ }
1795
+ }
1796
+ }
1797
+ placed.push({ x, y: y - LABEL_HEIGHT / 2, w: width, h: LABEL_HEIGHT });
1798
+ return x;
1799
+ };
1800
+ const drawLine = (y, color, segments, dash) => {
1801
+ if (segments.length === 0) {
1802
+ p.hLine(y, 0, c.plotWidth, color, dash);
1803
+ return null;
1804
+ }
1805
+ const x = placeX(p.pillWidth(segments), y);
1806
+ const from = opts.fullWidth ? 0 : Math.min(x + p.pillWidth(segments), c.plotWidth);
1807
+ p.hLine(y, from, c.plotWidth, color, dash);
1808
+ return p.pill(segments, x, y, LABEL_HEIGHT);
1809
+ };
1741
1810
  for (const pos of this.positions.values()) {
1742
1811
  const y = c.priceToY(pos.price);
1743
1812
  badges.push({ y, text: c.formatPrice(pos.price), bg: palette.text, fg: palette.background });
1744
1813
  if (!visible(y)) continue;
1745
- p.hLine(y, 0, c.plotWidth, palette.text);
1746
- const segments = [
1747
- { text: `${pos.label ?? labels.position} \xB7 ${pos.quantity}`, bg: palette.text, fg: palette.background }
1748
- ];
1749
- if (last) {
1814
+ const main = [opts.side ? pos.label ?? labels.position : "", opts.price ? c.formatPrice(pos.price) : "", opts.quantity ? String(pos.quantity) : ""].filter(Boolean).join(" \xB7 ");
1815
+ const segments = main ? [{ text: main, bg: palette.text, fg: palette.background }] : [];
1816
+ if (last && opts.pnl) {
1750
1817
  const pnl = (last.close - pos.price) * pos.quantity * (pos.side === "long" ? 1 : -1);
1751
1818
  const pct = pos.price ? (last.close - pos.price) / pos.price * 100 * (pos.side === "long" ? 1 : -1) : 0;
1752
1819
  const sign = pnl > 0 ? "+" : "";
@@ -1756,7 +1823,7 @@ var TradingLayer = class {
1756
1823
  fg: "#FFFFFF"
1757
1824
  });
1758
1825
  }
1759
- p.pill(segments, LABEL_X, y, LABEL_HEIGHT);
1826
+ drawLine(y, palette.text, segments);
1760
1827
  }
1761
1828
  for (const [id, order] of this.orders) {
1762
1829
  const price = this.drag?.id === id ? this.drag.price : order.price;
@@ -1764,15 +1831,15 @@ var TradingLayer = class {
1764
1831
  const color = order.alert ? palette.textSecondary : order.side === "buy" ? palette.up : palette.down;
1765
1832
  badges.push({ y, text: c.formatPrice(price), bg: color, fg: "#FFFFFF" });
1766
1833
  if (!visible(y)) continue;
1767
- p.hLine(y, 0, c.plotWidth, color, order.alert ? ALERT_DASH : ORDER_DASH);
1768
- const main = order.alert ? order.label ?? labels.alert : [order.side === "buy" ? labels.buy : labels.sell, order.label, String(order.quantity)].filter(Boolean).join(" ");
1769
- const segments = [{ text: main, bg: color, fg: "#FFFFFF" }];
1770
- if (order.onCancel) segments.push({ text: "\xD7", bg: color, fg: "#FFFFFF", padding: 7 });
1771
- const rects = p.pill(segments, LABEL_X, y, LABEL_HEIGHT);
1772
- if (order.onCancel && rects[1]) p.vLine(rects[1].x, y - 6, y + 6, "rgba(255,255,255,0.4)");
1834
+ const main = (order.alert ? [order.label ?? labels.alert, opts.price ? c.formatPrice(price) : ""] : [opts.side ? order.side === "buy" ? labels.buy : labels.sell : "", order.label, opts.price ? c.formatPrice(price) : "", opts.quantity ? String(order.quantity) : ""]).filter(Boolean).join(" ");
1835
+ const cancellable = !!order.onCancel && opts.buttons;
1836
+ const segments = [{ text: main || c.formatPrice(price), bg: color, fg: "#FFFFFF" }];
1837
+ if (cancellable) segments.push({ text: "\xD7", bg: color, fg: "#FFFFFF", padding: 7 });
1838
+ const rects = drawLine(y, color, segments, order.alert ? ALERT_DASH : ORDER_DASH);
1839
+ if (cancellable && rects[1]) p.vLine(rects[1].x, y - 6, y + 6, "rgba(255,255,255,0.4)");
1773
1840
  this.labelRects.set(id, {
1774
1841
  label: { x: rects[0].x, y: rects[0].y, w: rects.reduce((w, r) => w + r.w, 0), h: LABEL_HEIGHT },
1775
- cancel: order.onCancel ? rects[1] : null
1842
+ cancel: cancellable ? rects[1] : null
1776
1843
  });
1777
1844
  }
1778
1845
  }
@@ -2652,8 +2719,8 @@ function firstFinite(values, from) {
2652
2719
  return Number.NaN;
2653
2720
  }
2654
2721
  var TOOLTIP_LABELS = {
2655
- ko: { buy: "\uB9E4\uC218", sell: "\uB9E4\uB3C4", fills: (n) => `${n}\uAC74`, price: "\uCCB4\uACB0\uAC00", avg: "\uD3C9\uADE0\uAC00", qty: "\uC218\uB7C9" },
2656
- en: { buy: "Buy", sell: "Sell", fills: (n) => `${n} fills`, price: "Price", avg: "Avg", qty: "Qty" }
2722
+ ko: { buy: "\uB9E4\uC218", sell: "\uB9E4\uB3C4", liquidation: "\uCCAD\uC0B0", fills: (n) => `${n}\uAC74`, price: "\uCCB4\uACB0\uAC00", avg: "\uD3C9\uADE0\uAC00", qty: "\uC218\uB7C9" },
2723
+ en: { buy: "Buy", sell: "Sell", liquidation: "Liquidation", fills: (n) => `${n} fills`, price: "Price", avg: "Avg", qty: "Qty" }
2657
2724
  };
2658
2725
  function fallbackResolution(requested, supported) {
2659
2726
  if (!supported?.length || supported.includes(requested)) return requested;
@@ -3655,10 +3722,17 @@ var Chart = class {
3655
3722
  addAlertLine(options) {
3656
3723
  return this.trading.addAlert(options);
3657
3724
  }
3658
- /** Fill markers (▲ buy under the bar, ▼ sell above). Replaces the previous set. */
3725
+ /** Fill markers (▲ buy under the bar, ▼ sell above; ⚡ at the price for liquidations). Replaces the previous set. */
3659
3726
  setExecutions(markers) {
3660
3727
  this.trading.setExecutions(markers);
3661
3728
  }
3729
+ /** What position/order/alert line labels show and where (direction, price, size, P&L, ×, alignment, line width). */
3730
+ setTradingLabels(patch) {
3731
+ this.trading.setLabelOptions(patch);
3732
+ }
3733
+ get tradingLabels() {
3734
+ return this.trading.labelSettings;
3735
+ }
3662
3736
  setTheme(theme) {
3663
3737
  this.theme = { ...this.theme, ...theme };
3664
3738
  this.colors.clear();
@@ -4156,14 +4230,14 @@ var Chart = class {
4156
4230
  const g = this.tooltipKey ? this.trading.executionGroup(this.tooltipKey) : void 0;
4157
4231
  if (!g) return null;
4158
4232
  const t = TOOLTIP_LABELS[this.locale];
4159
- const side = g.side === "buy" ? t.buy : t.sell;
4233
+ const side = g.kind === "liquidation" ? t.liquidation : g.side === "buy" ? t.buy : t.sell;
4160
4234
  const lines = [
4161
4235
  g.count > 1 ? `${side} ${t.fills(g.count)}` : side,
4162
4236
  `${g.count > 1 ? t.avg : t.price} ${formatPrice(roundToTick(g.averagePrice, this.tickSize), this.pricePrecision)}`
4163
4237
  ];
4164
4238
  if (g.quantity > 0) lines.push(`${t.qty} ${Number(g.quantity.toFixed(8))}`);
4165
4239
  lines.push(this.formatter.crosshairLabel(g.time, true));
4166
- return { x: g.x, y: g.y, side: g.side, lines };
4240
+ return { x: g.x, y: g.y, side: g.kind === "liquidation" ? "sell" : g.side, lines };
4167
4241
  }
4168
4242
  emitCrosshair(info) {
4169
4243
  const key = info ? `${info.index}|${info.value}|${info.bar.close}|${info.bar.volume ?? ""}` : "";
@@ -4557,6 +4631,7 @@ export {
4557
4631
  CHART_THEMES,
4558
4632
  Chart,
4559
4633
  DEFAULT_CHART_SETTINGS,
4634
+ DEFAULT_TRADING_LABEL_OPTIONS,
4560
4635
  FIB_LEVELS,
4561
4636
  INDICATORS,
4562
4637
  PRICE_SOURCES,
@@ -37,11 +37,33 @@ export interface ExecutionMarker {
37
37
  price: number;
38
38
  side: 'buy' | 'sell';
39
39
  quantity?: number;
40
+ /** 'liquidation': a forced close, drawn as a ⚡ badge at its price instead of ▲/▼. Default 'fill'. */
41
+ kind?: 'fill' | 'liquidation';
40
42
  }
43
+ /**
44
+ * What the labels of position, order and alert lines show, and where (chart settings like
45
+ * "라벨" in trading terminals). Defaults keep the original look.
46
+ */
47
+ export interface TradingLabelOptions {
48
+ /** 매수/매도 and the position name. */
49
+ side: boolean;
50
+ /** The line's price inside the label (the price axis always shows it). */
51
+ price: boolean;
52
+ quantity: boolean;
53
+ /** Positions: P&L against the last price. */
54
+ pnl: boolean;
55
+ /** The × on cancellable lines. */
56
+ buttons: boolean;
57
+ align: 'left' | 'center' | 'right';
58
+ /** Draw lines across the whole chart; off, only from the label to the price axis. */
59
+ fullWidth: boolean;
60
+ }
61
+ export declare const DEFAULT_TRADING_LABEL_OPTIONS: TradingLabelOptions;
41
62
  /** Fills on the same bar and side, shown as one marker. */
42
63
  export interface ExecutionGroup {
43
64
  key: string;
44
65
  side: 'buy' | 'sell';
66
+ kind: 'fill' | 'liquidation';
45
67
  count: number;
46
68
  quantity: number;
47
69
  /** Quantity-weighted average price (plain average when quantities are missing). */
@@ -72,7 +94,10 @@ export declare class TradingLayer {
72
94
  private labelRects;
73
95
  private drag;
74
96
  private groups;
97
+ private labelOptions;
75
98
  constructor(onChange: () => void);
99
+ get labelSettings(): TradingLabelOptions;
100
+ setLabelOptions(patch: Partial<TradingLabelOptions>): void;
76
101
  addPosition(options: PositionLineOptions): LineHandle<PositionLineOptions>;
77
102
  addOrder(options: OrderLineOptions): LineHandle<OrderLineOptions>;
78
103
  addAlert(options: AlertLineOptions): LineHandle<AlertLineOptions>;
@@ -21,6 +21,8 @@ export declare class Painter {
21
21
  * A row of joined label segments ("pill") centered vertically on `y`.
22
22
  * Returns each segment's rect for hit testing (e.g. an order's cancel button).
23
23
  */
24
+ /** Width `pill` will take for these segments. */
25
+ pillWidth(segments: readonly PillSegment[]): number;
24
26
  pill(segments: readonly PillSegment[], x: number, y: number, height?: number, radius?: number): Rect[];
25
27
  }
26
28
  export interface Rect {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnest/charts",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "Canvas charting engine for financial charts",
5
5
  "keywords": [
6
6
  "chart",