hyperprop-charting-library 0.1.23 → 0.1.24

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/README.md CHANGED
@@ -60,6 +60,19 @@ const chart = createChart(root, {
60
60
  });
61
61
  ```
62
62
 
63
+ ## Candle Color Behavior
64
+
65
+ You can control how up/down candle color is decided:
66
+
67
+ ```ts
68
+ const chart = createChart(root, {
69
+ // "openClose" (default) or "prevClose"
70
+ candleColorMode: "prevClose",
71
+ // -1 = auto epsilon from priceDecimals, 0 = strict compare
72
+ candleColorEpsilon: -1
73
+ });
74
+ ```
75
+
63
76
  ## Crosshair "+" Button
64
77
 
65
78
  ```ts
@@ -151,6 +151,8 @@ var DEFAULT_OPTIONS = {
151
151
  candleBodyWidthRatio: 0.7,
152
152
  candleMinWidth: 0.5,
153
153
  candleWickWidth: 1,
154
+ candleColorMode: "openClose",
155
+ candleColorEpsilon: -1,
154
156
  autoScaleSmoothing: 0.16,
155
157
  autoScaleIgnoreLatestCandle: true,
156
158
  doubleClickEnabled: true,
@@ -511,11 +513,11 @@ var BUILTIN_VOLUME_INDICATOR = {
511
513
  const xCenter = xFromIndex(index);
512
514
  const barX = Math.round(xCenter - barWidth / 2);
513
515
  const barY = Math.round(paneBottom - volumeHeight);
514
- const isUp = point.c >= point.o;
515
- const opacity = isUp ? upOpacity : downOpacity;
516
+ const direction = renderContext.getCandleDirectionByIndex(index);
517
+ const opacity = direction === "up" ? upOpacity : downOpacity;
516
518
  ctx.save();
517
519
  ctx.globalAlpha = opacity;
518
- ctx.fillStyle = isUp ? upColor : downColor;
520
+ ctx.fillStyle = direction === "up" ? upColor : downColor;
519
521
  ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
520
522
  ctx.restore();
521
523
  }
@@ -867,6 +869,24 @@ function createChart(element, options = {}) {
867
869
  const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
868
870
  return price.toFixed(decimals);
869
871
  };
872
+ const roundToPricePrecision = (price) => {
873
+ const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
874
+ return Number(price.toFixed(decimals));
875
+ };
876
+ const getResolvedCandleColorEpsilon = () => {
877
+ const configured = mergedOptions.candleColorEpsilon;
878
+ if (configured >= 0) {
879
+ return configured;
880
+ }
881
+ const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
882
+ return decimals > 0 ? 0.5 / 10 ** decimals : 0;
883
+ };
884
+ const getDirectionFromDelta = (delta) => {
885
+ const epsilon = Math.max(0, getResolvedCandleColorEpsilon());
886
+ if (delta > epsilon) return 1;
887
+ if (delta < -epsilon) return -1;
888
+ return 0;
889
+ };
870
890
  const getStabilizedPriceTemplate = () => {
871
891
  const explicitTemplate = mergedOptions.priceLabelWidthTemplate.trim();
872
892
  if (explicitTemplate.length > 0) {
@@ -893,14 +913,33 @@ function createChart(element, options = {}) {
893
913
  return Math.max(measured, templateWidth);
894
914
  };
895
915
  const parseData = (nextData) => {
896
- return nextData.map((point) => ({
897
- time: new Date(point.t),
898
- o: point.o,
899
- h: point.h,
900
- l: point.l,
901
- c: point.c,
902
- ...point.v === void 0 ? {} : { v: point.v }
903
- })).filter((point) => Number.isFinite(point.time.getTime())).sort((a, b) => a.time.getTime() - b.time.getTime());
916
+ const dedupedByTime = /* @__PURE__ */ new Map();
917
+ for (const point of nextData) {
918
+ const time = new Date(point.t);
919
+ const timeMs = time.getTime();
920
+ if (!Number.isFinite(timeMs)) {
921
+ continue;
922
+ }
923
+ const open = Number(point.o);
924
+ const close = Number(point.c);
925
+ const highInput = Number(point.h);
926
+ const lowInput = Number(point.l);
927
+ if (!Number.isFinite(open) || !Number.isFinite(close) || !Number.isFinite(highInput) || !Number.isFinite(lowInput)) {
928
+ continue;
929
+ }
930
+ const normalizedHigh = Math.max(highInput, open, close);
931
+ const normalizedLow = Math.min(lowInput, open, close);
932
+ const volumeValue = point.v === void 0 ? void 0 : Number(point.v);
933
+ dedupedByTime.set(timeMs, {
934
+ time,
935
+ o: open,
936
+ h: normalizedHigh,
937
+ l: normalizedLow,
938
+ c: close,
939
+ ...volumeValue === void 0 || !Number.isFinite(volumeValue) ? {} : { v: volumeValue }
940
+ });
941
+ }
942
+ return Array.from(dedupedByTime.values()).sort((a, b) => a.time.getTime() - b.time.getTime());
904
943
  };
905
944
  const getTimeStepMs = () => {
906
945
  if (data.length < 2) {
@@ -977,6 +1016,26 @@ function createChart(element, options = {}) {
977
1016
  const upperDelta = Math.abs(upperPoint.time.getTime() - timeMs);
978
1017
  return lowerDelta <= upperDelta ? lower : upper;
979
1018
  };
1019
+ const getCandleDirectionByIndex = (index) => {
1020
+ const point = data[index];
1021
+ if (!point) {
1022
+ return "up";
1023
+ }
1024
+ const prevPoint = index > 0 ? data[index - 1] : void 0;
1025
+ const mode = mergedOptions.candleColorMode;
1026
+ const baseForMode = mode === "prevClose" && prevPoint ? prevPoint.c : point.o;
1027
+ let direction = getDirectionFromDelta(point.c - baseForMode);
1028
+ if (direction === 0 && mode === "prevClose") {
1029
+ direction = getDirectionFromDelta(point.c - point.o);
1030
+ }
1031
+ if (direction === 0 && prevPoint) {
1032
+ direction = getDirectionFromDelta(point.c - prevPoint.c);
1033
+ }
1034
+ if (direction === 0) {
1035
+ return point.c >= point.o ? "up" : "down";
1036
+ }
1037
+ return direction > 0 ? "up" : "down";
1038
+ };
980
1039
  const formatHoverTimeLabel = (time, mode) => {
981
1040
  if (mode === "time") {
982
1041
  return time.toLocaleTimeString(void 0, {
@@ -1562,8 +1621,8 @@ function createChart(element, options = {}) {
1562
1621
  const closeY = yFromPrice(point.c);
1563
1622
  const highY = yFromPrice(point.h);
1564
1623
  const lowY = yFromPrice(point.l);
1565
- const isUp = point.c >= point.o;
1566
- const candleColor = isUp ? mergedOptions.upColor : mergedOptions.downColor;
1624
+ const direction = getCandleDirectionByIndex(index);
1625
+ const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
1567
1626
  ctx.strokeStyle = candleColor;
1568
1627
  ctx.lineWidth = candleWickWidth;
1569
1628
  ctx.beginPath();
@@ -1597,6 +1656,7 @@ function createChart(element, options = {}) {
1597
1656
  chartHeight,
1598
1657
  xFromIndex,
1599
1658
  yFromPrice,
1659
+ getCandleDirectionByIndex,
1600
1660
  candleSpacing,
1601
1661
  upColor: mergedOptions.upColor,
1602
1662
  downColor: mergedOptions.downColor
@@ -1656,6 +1716,7 @@ function createChart(element, options = {}) {
1656
1716
  chartHeight: paneHeight,
1657
1717
  xFromIndex,
1658
1718
  yFromPrice: null,
1719
+ getCandleDirectionByIndex,
1659
1720
  candleSpacing,
1660
1721
  upColor: mergedOptions.upColor,
1661
1722
  downColor: mergedOptions.downColor
@@ -1693,7 +1754,8 @@ function createChart(element, options = {}) {
1693
1754
  const tickerPrice = lastPoint.c;
1694
1755
  const tickerY = yFromPrice(tickerPrice);
1695
1756
  const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
1696
- const tickerColor = ticker.color ?? (lastPoint.c >= lastPoint.o ? mergedOptions.upColor : mergedOptions.downColor);
1757
+ const lastDirection = getCandleDirectionByIndex(data.length - 1);
1758
+ const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
1697
1759
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
1698
1760
  const tickerStyle = ticker.style ?? "solid";
1699
1761
  ctx.save();
@@ -2061,7 +2123,7 @@ function createChart(element, options = {}) {
2061
2123
  x,
2062
2124
  y,
2063
2125
  region,
2064
- ...region === "plot" ? { price: Number(priceFromCanvasY(y).toFixed(mergedOptions.priceDecimals)) } : {},
2126
+ ...region === "plot" ? { price: roundToPricePrecision(priceFromCanvasY(y)) } : {},
2065
2127
  ...index === null ? {} : { index },
2066
2128
  ...hoverTime ? { time: hoverTime.toISOString() } : {},
2067
2129
  ...point ? { point } : {}
@@ -2107,7 +2169,7 @@ function createChart(element, options = {}) {
2107
2169
  if (orderRegion) {
2108
2170
  if (orderRegion.draggable) {
2109
2171
  activePointerId = event.pointerId;
2110
- const startPrice = Number(orderRegion.line.price.toFixed(2));
2172
+ const startPrice = roundToPricePrecision(orderRegion.line.price);
2111
2173
  actionDragState = {
2112
2174
  orderId: orderRegion.orderId,
2113
2175
  action: orderRegion.action,
@@ -2166,7 +2228,7 @@ function createChart(element, options = {}) {
2166
2228
  if (activePointerId !== null && event.pointerId !== activePointerId) {
2167
2229
  return;
2168
2230
  }
2169
- const nextPrice = Number(priceFromCanvasY(point.y).toFixed(2));
2231
+ const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
2170
2232
  if (nextPrice !== orderDragState.lastPrice) {
2171
2233
  orderDragState.lastPrice = nextPrice;
2172
2234
  orderLines = orderLines.map((line) => {
@@ -2196,7 +2258,7 @@ function createChart(element, options = {}) {
2196
2258
  if (activePointerId !== null && event.pointerId !== activePointerId) {
2197
2259
  return;
2198
2260
  }
2199
- const nextPrice = Number(priceFromCanvasY(point.y).toFixed(2));
2261
+ const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
2200
2262
  if (nextPrice !== actionDragState.lastPrice) {
2201
2263
  actionDragState.lastPrice = nextPrice;
2202
2264
  actionDragState.moved = true;
@@ -2313,7 +2375,7 @@ function createChart(element, options = {}) {
2313
2375
  canvas.style.cursor = "default";
2314
2376
  if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
2315
2377
  if (!pointerDownInfo.moved) {
2316
- const clickPrice = pointerDownInfo.region === "plot" ? Number(priceFromCanvasY(pointerDownInfo.y).toFixed(2)) : void 0;
2378
+ const clickPrice = pointerDownInfo.region === "plot" ? roundToPricePrecision(priceFromCanvasY(pointerDownInfo.y)) : void 0;
2317
2379
  chartClickHandler?.({
2318
2380
  x: pointerDownInfo.x,
2319
2381
  y: pointerDownInfo.y,
@@ -2373,7 +2435,7 @@ function createChart(element, options = {}) {
2373
2435
  }
2374
2436
  orderActionHandler?.({
2375
2437
  action: "createLimit",
2376
- price: Number(priceFromCanvasY(point.y).toFixed(2))
2438
+ price: roundToPricePrecision(priceFromCanvasY(point.y))
2377
2439
  });
2378
2440
  return;
2379
2441
  }
@@ -22,6 +22,8 @@ interface ChartOptions {
22
22
  candleBodyWidthRatio?: number;
23
23
  candleMinWidth?: number;
24
24
  candleWickWidth?: number;
25
+ candleColorMode?: "openClose" | "prevClose";
26
+ candleColorEpsilon?: number;
25
27
  autoScaleSmoothing?: number;
26
28
  autoScaleIgnoreLatestCandle?: boolean;
27
29
  doubleClickEnabled?: boolean;
@@ -65,6 +67,7 @@ interface IndicatorRenderContext {
65
67
  chartHeight: number;
66
68
  xFromIndex: (index: number) => number;
67
69
  yFromPrice: ((price: number) => number) | null;
70
+ getCandleDirectionByIndex: (index: number) => "up" | "down";
68
71
  candleSpacing: number;
69
72
  upColor: string;
70
73
  downColor: string;
@@ -127,6 +127,8 @@ var DEFAULT_OPTIONS = {
127
127
  candleBodyWidthRatio: 0.7,
128
128
  candleMinWidth: 0.5,
129
129
  candleWickWidth: 1,
130
+ candleColorMode: "openClose",
131
+ candleColorEpsilon: -1,
130
132
  autoScaleSmoothing: 0.16,
131
133
  autoScaleIgnoreLatestCandle: true,
132
134
  doubleClickEnabled: true,
@@ -487,11 +489,11 @@ var BUILTIN_VOLUME_INDICATOR = {
487
489
  const xCenter = xFromIndex(index);
488
490
  const barX = Math.round(xCenter - barWidth / 2);
489
491
  const barY = Math.round(paneBottom - volumeHeight);
490
- const isUp = point.c >= point.o;
491
- const opacity = isUp ? upOpacity : downOpacity;
492
+ const direction = renderContext.getCandleDirectionByIndex(index);
493
+ const opacity = direction === "up" ? upOpacity : downOpacity;
492
494
  ctx.save();
493
495
  ctx.globalAlpha = opacity;
494
- ctx.fillStyle = isUp ? upColor : downColor;
496
+ ctx.fillStyle = direction === "up" ? upColor : downColor;
495
497
  ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
496
498
  ctx.restore();
497
499
  }
@@ -843,6 +845,24 @@ function createChart(element, options = {}) {
843
845
  const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
844
846
  return price.toFixed(decimals);
845
847
  };
848
+ const roundToPricePrecision = (price) => {
849
+ const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
850
+ return Number(price.toFixed(decimals));
851
+ };
852
+ const getResolvedCandleColorEpsilon = () => {
853
+ const configured = mergedOptions.candleColorEpsilon;
854
+ if (configured >= 0) {
855
+ return configured;
856
+ }
857
+ const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
858
+ return decimals > 0 ? 0.5 / 10 ** decimals : 0;
859
+ };
860
+ const getDirectionFromDelta = (delta) => {
861
+ const epsilon = Math.max(0, getResolvedCandleColorEpsilon());
862
+ if (delta > epsilon) return 1;
863
+ if (delta < -epsilon) return -1;
864
+ return 0;
865
+ };
846
866
  const getStabilizedPriceTemplate = () => {
847
867
  const explicitTemplate = mergedOptions.priceLabelWidthTemplate.trim();
848
868
  if (explicitTemplate.length > 0) {
@@ -869,14 +889,33 @@ function createChart(element, options = {}) {
869
889
  return Math.max(measured, templateWidth);
870
890
  };
871
891
  const parseData = (nextData) => {
872
- return nextData.map((point) => ({
873
- time: new Date(point.t),
874
- o: point.o,
875
- h: point.h,
876
- l: point.l,
877
- c: point.c,
878
- ...point.v === void 0 ? {} : { v: point.v }
879
- })).filter((point) => Number.isFinite(point.time.getTime())).sort((a, b) => a.time.getTime() - b.time.getTime());
892
+ const dedupedByTime = /* @__PURE__ */ new Map();
893
+ for (const point of nextData) {
894
+ const time = new Date(point.t);
895
+ const timeMs = time.getTime();
896
+ if (!Number.isFinite(timeMs)) {
897
+ continue;
898
+ }
899
+ const open = Number(point.o);
900
+ const close = Number(point.c);
901
+ const highInput = Number(point.h);
902
+ const lowInput = Number(point.l);
903
+ if (!Number.isFinite(open) || !Number.isFinite(close) || !Number.isFinite(highInput) || !Number.isFinite(lowInput)) {
904
+ continue;
905
+ }
906
+ const normalizedHigh = Math.max(highInput, open, close);
907
+ const normalizedLow = Math.min(lowInput, open, close);
908
+ const volumeValue = point.v === void 0 ? void 0 : Number(point.v);
909
+ dedupedByTime.set(timeMs, {
910
+ time,
911
+ o: open,
912
+ h: normalizedHigh,
913
+ l: normalizedLow,
914
+ c: close,
915
+ ...volumeValue === void 0 || !Number.isFinite(volumeValue) ? {} : { v: volumeValue }
916
+ });
917
+ }
918
+ return Array.from(dedupedByTime.values()).sort((a, b) => a.time.getTime() - b.time.getTime());
880
919
  };
881
920
  const getTimeStepMs = () => {
882
921
  if (data.length < 2) {
@@ -953,6 +992,26 @@ function createChart(element, options = {}) {
953
992
  const upperDelta = Math.abs(upperPoint.time.getTime() - timeMs);
954
993
  return lowerDelta <= upperDelta ? lower : upper;
955
994
  };
995
+ const getCandleDirectionByIndex = (index) => {
996
+ const point = data[index];
997
+ if (!point) {
998
+ return "up";
999
+ }
1000
+ const prevPoint = index > 0 ? data[index - 1] : void 0;
1001
+ const mode = mergedOptions.candleColorMode;
1002
+ const baseForMode = mode === "prevClose" && prevPoint ? prevPoint.c : point.o;
1003
+ let direction = getDirectionFromDelta(point.c - baseForMode);
1004
+ if (direction === 0 && mode === "prevClose") {
1005
+ direction = getDirectionFromDelta(point.c - point.o);
1006
+ }
1007
+ if (direction === 0 && prevPoint) {
1008
+ direction = getDirectionFromDelta(point.c - prevPoint.c);
1009
+ }
1010
+ if (direction === 0) {
1011
+ return point.c >= point.o ? "up" : "down";
1012
+ }
1013
+ return direction > 0 ? "up" : "down";
1014
+ };
956
1015
  const formatHoverTimeLabel = (time, mode) => {
957
1016
  if (mode === "time") {
958
1017
  return time.toLocaleTimeString(void 0, {
@@ -1538,8 +1597,8 @@ function createChart(element, options = {}) {
1538
1597
  const closeY = yFromPrice(point.c);
1539
1598
  const highY = yFromPrice(point.h);
1540
1599
  const lowY = yFromPrice(point.l);
1541
- const isUp = point.c >= point.o;
1542
- const candleColor = isUp ? mergedOptions.upColor : mergedOptions.downColor;
1600
+ const direction = getCandleDirectionByIndex(index);
1601
+ const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
1543
1602
  ctx.strokeStyle = candleColor;
1544
1603
  ctx.lineWidth = candleWickWidth;
1545
1604
  ctx.beginPath();
@@ -1573,6 +1632,7 @@ function createChart(element, options = {}) {
1573
1632
  chartHeight,
1574
1633
  xFromIndex,
1575
1634
  yFromPrice,
1635
+ getCandleDirectionByIndex,
1576
1636
  candleSpacing,
1577
1637
  upColor: mergedOptions.upColor,
1578
1638
  downColor: mergedOptions.downColor
@@ -1632,6 +1692,7 @@ function createChart(element, options = {}) {
1632
1692
  chartHeight: paneHeight,
1633
1693
  xFromIndex,
1634
1694
  yFromPrice: null,
1695
+ getCandleDirectionByIndex,
1635
1696
  candleSpacing,
1636
1697
  upColor: mergedOptions.upColor,
1637
1698
  downColor: mergedOptions.downColor
@@ -1669,7 +1730,8 @@ function createChart(element, options = {}) {
1669
1730
  const tickerPrice = lastPoint.c;
1670
1731
  const tickerY = yFromPrice(tickerPrice);
1671
1732
  const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
1672
- const tickerColor = ticker.color ?? (lastPoint.c >= lastPoint.o ? mergedOptions.upColor : mergedOptions.downColor);
1733
+ const lastDirection = getCandleDirectionByIndex(data.length - 1);
1734
+ const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
1673
1735
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
1674
1736
  const tickerStyle = ticker.style ?? "solid";
1675
1737
  ctx.save();
@@ -2037,7 +2099,7 @@ function createChart(element, options = {}) {
2037
2099
  x,
2038
2100
  y,
2039
2101
  region,
2040
- ...region === "plot" ? { price: Number(priceFromCanvasY(y).toFixed(mergedOptions.priceDecimals)) } : {},
2102
+ ...region === "plot" ? { price: roundToPricePrecision(priceFromCanvasY(y)) } : {},
2041
2103
  ...index === null ? {} : { index },
2042
2104
  ...hoverTime ? { time: hoverTime.toISOString() } : {},
2043
2105
  ...point ? { point } : {}
@@ -2083,7 +2145,7 @@ function createChart(element, options = {}) {
2083
2145
  if (orderRegion) {
2084
2146
  if (orderRegion.draggable) {
2085
2147
  activePointerId = event.pointerId;
2086
- const startPrice = Number(orderRegion.line.price.toFixed(2));
2148
+ const startPrice = roundToPricePrecision(orderRegion.line.price);
2087
2149
  actionDragState = {
2088
2150
  orderId: orderRegion.orderId,
2089
2151
  action: orderRegion.action,
@@ -2142,7 +2204,7 @@ function createChart(element, options = {}) {
2142
2204
  if (activePointerId !== null && event.pointerId !== activePointerId) {
2143
2205
  return;
2144
2206
  }
2145
- const nextPrice = Number(priceFromCanvasY(point.y).toFixed(2));
2207
+ const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
2146
2208
  if (nextPrice !== orderDragState.lastPrice) {
2147
2209
  orderDragState.lastPrice = nextPrice;
2148
2210
  orderLines = orderLines.map((line) => {
@@ -2172,7 +2234,7 @@ function createChart(element, options = {}) {
2172
2234
  if (activePointerId !== null && event.pointerId !== activePointerId) {
2173
2235
  return;
2174
2236
  }
2175
- const nextPrice = Number(priceFromCanvasY(point.y).toFixed(2));
2237
+ const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
2176
2238
  if (nextPrice !== actionDragState.lastPrice) {
2177
2239
  actionDragState.lastPrice = nextPrice;
2178
2240
  actionDragState.moved = true;
@@ -2289,7 +2351,7 @@ function createChart(element, options = {}) {
2289
2351
  canvas.style.cursor = "default";
2290
2352
  if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
2291
2353
  if (!pointerDownInfo.moved) {
2292
- const clickPrice = pointerDownInfo.region === "plot" ? Number(priceFromCanvasY(pointerDownInfo.y).toFixed(2)) : void 0;
2354
+ const clickPrice = pointerDownInfo.region === "plot" ? roundToPricePrecision(priceFromCanvasY(pointerDownInfo.y)) : void 0;
2293
2355
  chartClickHandler?.({
2294
2356
  x: pointerDownInfo.x,
2295
2357
  y: pointerDownInfo.y,
@@ -2349,7 +2411,7 @@ function createChart(element, options = {}) {
2349
2411
  }
2350
2412
  orderActionHandler?.({
2351
2413
  action: "createLimit",
2352
- price: Number(priceFromCanvasY(point.y).toFixed(2))
2414
+ price: roundToPricePrecision(priceFromCanvasY(point.y))
2353
2415
  });
2354
2416
  return;
2355
2417
  }
package/dist/index.cjs CHANGED
@@ -151,6 +151,8 @@ var DEFAULT_OPTIONS = {
151
151
  candleBodyWidthRatio: 0.7,
152
152
  candleMinWidth: 0.5,
153
153
  candleWickWidth: 1,
154
+ candleColorMode: "openClose",
155
+ candleColorEpsilon: -1,
154
156
  autoScaleSmoothing: 0.16,
155
157
  autoScaleIgnoreLatestCandle: true,
156
158
  doubleClickEnabled: true,
@@ -511,11 +513,11 @@ var BUILTIN_VOLUME_INDICATOR = {
511
513
  const xCenter = xFromIndex(index);
512
514
  const barX = Math.round(xCenter - barWidth / 2);
513
515
  const barY = Math.round(paneBottom - volumeHeight);
514
- const isUp = point.c >= point.o;
515
- const opacity = isUp ? upOpacity : downOpacity;
516
+ const direction = renderContext.getCandleDirectionByIndex(index);
517
+ const opacity = direction === "up" ? upOpacity : downOpacity;
516
518
  ctx.save();
517
519
  ctx.globalAlpha = opacity;
518
- ctx.fillStyle = isUp ? upColor : downColor;
520
+ ctx.fillStyle = direction === "up" ? upColor : downColor;
519
521
  ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
520
522
  ctx.restore();
521
523
  }
@@ -867,6 +869,24 @@ function createChart(element, options = {}) {
867
869
  const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
868
870
  return price.toFixed(decimals);
869
871
  };
872
+ const roundToPricePrecision = (price) => {
873
+ const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
874
+ return Number(price.toFixed(decimals));
875
+ };
876
+ const getResolvedCandleColorEpsilon = () => {
877
+ const configured = mergedOptions.candleColorEpsilon;
878
+ if (configured >= 0) {
879
+ return configured;
880
+ }
881
+ const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
882
+ return decimals > 0 ? 0.5 / 10 ** decimals : 0;
883
+ };
884
+ const getDirectionFromDelta = (delta) => {
885
+ const epsilon = Math.max(0, getResolvedCandleColorEpsilon());
886
+ if (delta > epsilon) return 1;
887
+ if (delta < -epsilon) return -1;
888
+ return 0;
889
+ };
870
890
  const getStabilizedPriceTemplate = () => {
871
891
  const explicitTemplate = mergedOptions.priceLabelWidthTemplate.trim();
872
892
  if (explicitTemplate.length > 0) {
@@ -893,14 +913,33 @@ function createChart(element, options = {}) {
893
913
  return Math.max(measured, templateWidth);
894
914
  };
895
915
  const parseData = (nextData) => {
896
- return nextData.map((point) => ({
897
- time: new Date(point.t),
898
- o: point.o,
899
- h: point.h,
900
- l: point.l,
901
- c: point.c,
902
- ...point.v === void 0 ? {} : { v: point.v }
903
- })).filter((point) => Number.isFinite(point.time.getTime())).sort((a, b) => a.time.getTime() - b.time.getTime());
916
+ const dedupedByTime = /* @__PURE__ */ new Map();
917
+ for (const point of nextData) {
918
+ const time = new Date(point.t);
919
+ const timeMs = time.getTime();
920
+ if (!Number.isFinite(timeMs)) {
921
+ continue;
922
+ }
923
+ const open = Number(point.o);
924
+ const close = Number(point.c);
925
+ const highInput = Number(point.h);
926
+ const lowInput = Number(point.l);
927
+ if (!Number.isFinite(open) || !Number.isFinite(close) || !Number.isFinite(highInput) || !Number.isFinite(lowInput)) {
928
+ continue;
929
+ }
930
+ const normalizedHigh = Math.max(highInput, open, close);
931
+ const normalizedLow = Math.min(lowInput, open, close);
932
+ const volumeValue = point.v === void 0 ? void 0 : Number(point.v);
933
+ dedupedByTime.set(timeMs, {
934
+ time,
935
+ o: open,
936
+ h: normalizedHigh,
937
+ l: normalizedLow,
938
+ c: close,
939
+ ...volumeValue === void 0 || !Number.isFinite(volumeValue) ? {} : { v: volumeValue }
940
+ });
941
+ }
942
+ return Array.from(dedupedByTime.values()).sort((a, b) => a.time.getTime() - b.time.getTime());
904
943
  };
905
944
  const getTimeStepMs = () => {
906
945
  if (data.length < 2) {
@@ -977,6 +1016,26 @@ function createChart(element, options = {}) {
977
1016
  const upperDelta = Math.abs(upperPoint.time.getTime() - timeMs);
978
1017
  return lowerDelta <= upperDelta ? lower : upper;
979
1018
  };
1019
+ const getCandleDirectionByIndex = (index) => {
1020
+ const point = data[index];
1021
+ if (!point) {
1022
+ return "up";
1023
+ }
1024
+ const prevPoint = index > 0 ? data[index - 1] : void 0;
1025
+ const mode = mergedOptions.candleColorMode;
1026
+ const baseForMode = mode === "prevClose" && prevPoint ? prevPoint.c : point.o;
1027
+ let direction = getDirectionFromDelta(point.c - baseForMode);
1028
+ if (direction === 0 && mode === "prevClose") {
1029
+ direction = getDirectionFromDelta(point.c - point.o);
1030
+ }
1031
+ if (direction === 0 && prevPoint) {
1032
+ direction = getDirectionFromDelta(point.c - prevPoint.c);
1033
+ }
1034
+ if (direction === 0) {
1035
+ return point.c >= point.o ? "up" : "down";
1036
+ }
1037
+ return direction > 0 ? "up" : "down";
1038
+ };
980
1039
  const formatHoverTimeLabel = (time, mode) => {
981
1040
  if (mode === "time") {
982
1041
  return time.toLocaleTimeString(void 0, {
@@ -1562,8 +1621,8 @@ function createChart(element, options = {}) {
1562
1621
  const closeY = yFromPrice(point.c);
1563
1622
  const highY = yFromPrice(point.h);
1564
1623
  const lowY = yFromPrice(point.l);
1565
- const isUp = point.c >= point.o;
1566
- const candleColor = isUp ? mergedOptions.upColor : mergedOptions.downColor;
1624
+ const direction = getCandleDirectionByIndex(index);
1625
+ const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
1567
1626
  ctx.strokeStyle = candleColor;
1568
1627
  ctx.lineWidth = candleWickWidth;
1569
1628
  ctx.beginPath();
@@ -1597,6 +1656,7 @@ function createChart(element, options = {}) {
1597
1656
  chartHeight,
1598
1657
  xFromIndex,
1599
1658
  yFromPrice,
1659
+ getCandleDirectionByIndex,
1600
1660
  candleSpacing,
1601
1661
  upColor: mergedOptions.upColor,
1602
1662
  downColor: mergedOptions.downColor
@@ -1656,6 +1716,7 @@ function createChart(element, options = {}) {
1656
1716
  chartHeight: paneHeight,
1657
1717
  xFromIndex,
1658
1718
  yFromPrice: null,
1719
+ getCandleDirectionByIndex,
1659
1720
  candleSpacing,
1660
1721
  upColor: mergedOptions.upColor,
1661
1722
  downColor: mergedOptions.downColor
@@ -1693,7 +1754,8 @@ function createChart(element, options = {}) {
1693
1754
  const tickerPrice = lastPoint.c;
1694
1755
  const tickerY = yFromPrice(tickerPrice);
1695
1756
  const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
1696
- const tickerColor = ticker.color ?? (lastPoint.c >= lastPoint.o ? mergedOptions.upColor : mergedOptions.downColor);
1757
+ const lastDirection = getCandleDirectionByIndex(data.length - 1);
1758
+ const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
1697
1759
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
1698
1760
  const tickerStyle = ticker.style ?? "solid";
1699
1761
  ctx.save();
@@ -2061,7 +2123,7 @@ function createChart(element, options = {}) {
2061
2123
  x,
2062
2124
  y,
2063
2125
  region,
2064
- ...region === "plot" ? { price: Number(priceFromCanvasY(y).toFixed(mergedOptions.priceDecimals)) } : {},
2126
+ ...region === "plot" ? { price: roundToPricePrecision(priceFromCanvasY(y)) } : {},
2065
2127
  ...index === null ? {} : { index },
2066
2128
  ...hoverTime ? { time: hoverTime.toISOString() } : {},
2067
2129
  ...point ? { point } : {}
@@ -2107,7 +2169,7 @@ function createChart(element, options = {}) {
2107
2169
  if (orderRegion) {
2108
2170
  if (orderRegion.draggable) {
2109
2171
  activePointerId = event.pointerId;
2110
- const startPrice = Number(orderRegion.line.price.toFixed(2));
2172
+ const startPrice = roundToPricePrecision(orderRegion.line.price);
2111
2173
  actionDragState = {
2112
2174
  orderId: orderRegion.orderId,
2113
2175
  action: orderRegion.action,
@@ -2166,7 +2228,7 @@ function createChart(element, options = {}) {
2166
2228
  if (activePointerId !== null && event.pointerId !== activePointerId) {
2167
2229
  return;
2168
2230
  }
2169
- const nextPrice = Number(priceFromCanvasY(point.y).toFixed(2));
2231
+ const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
2170
2232
  if (nextPrice !== orderDragState.lastPrice) {
2171
2233
  orderDragState.lastPrice = nextPrice;
2172
2234
  orderLines = orderLines.map((line) => {
@@ -2196,7 +2258,7 @@ function createChart(element, options = {}) {
2196
2258
  if (activePointerId !== null && event.pointerId !== activePointerId) {
2197
2259
  return;
2198
2260
  }
2199
- const nextPrice = Number(priceFromCanvasY(point.y).toFixed(2));
2261
+ const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
2200
2262
  if (nextPrice !== actionDragState.lastPrice) {
2201
2263
  actionDragState.lastPrice = nextPrice;
2202
2264
  actionDragState.moved = true;
@@ -2313,7 +2375,7 @@ function createChart(element, options = {}) {
2313
2375
  canvas.style.cursor = "default";
2314
2376
  if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
2315
2377
  if (!pointerDownInfo.moved) {
2316
- const clickPrice = pointerDownInfo.region === "plot" ? Number(priceFromCanvasY(pointerDownInfo.y).toFixed(2)) : void 0;
2378
+ const clickPrice = pointerDownInfo.region === "plot" ? roundToPricePrecision(priceFromCanvasY(pointerDownInfo.y)) : void 0;
2317
2379
  chartClickHandler?.({
2318
2380
  x: pointerDownInfo.x,
2319
2381
  y: pointerDownInfo.y,
@@ -2373,7 +2435,7 @@ function createChart(element, options = {}) {
2373
2435
  }
2374
2436
  orderActionHandler?.({
2375
2437
  action: "createLimit",
2376
- price: Number(priceFromCanvasY(point.y).toFixed(2))
2438
+ price: roundToPricePrecision(priceFromCanvasY(point.y))
2377
2439
  });
2378
2440
  return;
2379
2441
  }
package/dist/index.d.cts CHANGED
@@ -22,6 +22,8 @@ interface ChartOptions {
22
22
  candleBodyWidthRatio?: number;
23
23
  candleMinWidth?: number;
24
24
  candleWickWidth?: number;
25
+ candleColorMode?: "openClose" | "prevClose";
26
+ candleColorEpsilon?: number;
25
27
  autoScaleSmoothing?: number;
26
28
  autoScaleIgnoreLatestCandle?: boolean;
27
29
  doubleClickEnabled?: boolean;
@@ -65,6 +67,7 @@ interface IndicatorRenderContext {
65
67
  chartHeight: number;
66
68
  xFromIndex: (index: number) => number;
67
69
  yFromPrice: ((price: number) => number) | null;
70
+ getCandleDirectionByIndex: (index: number) => "up" | "down";
68
71
  candleSpacing: number;
69
72
  upColor: string;
70
73
  downColor: string;
package/dist/index.d.ts CHANGED
@@ -22,6 +22,8 @@ interface ChartOptions {
22
22
  candleBodyWidthRatio?: number;
23
23
  candleMinWidth?: number;
24
24
  candleWickWidth?: number;
25
+ candleColorMode?: "openClose" | "prevClose";
26
+ candleColorEpsilon?: number;
25
27
  autoScaleSmoothing?: number;
26
28
  autoScaleIgnoreLatestCandle?: boolean;
27
29
  doubleClickEnabled?: boolean;
@@ -65,6 +67,7 @@ interface IndicatorRenderContext {
65
67
  chartHeight: number;
66
68
  xFromIndex: (index: number) => number;
67
69
  yFromPrice: ((price: number) => number) | null;
70
+ getCandleDirectionByIndex: (index: number) => "up" | "down";
68
71
  candleSpacing: number;
69
72
  upColor: string;
70
73
  downColor: string;
package/dist/index.js CHANGED
@@ -127,6 +127,8 @@ var DEFAULT_OPTIONS = {
127
127
  candleBodyWidthRatio: 0.7,
128
128
  candleMinWidth: 0.5,
129
129
  candleWickWidth: 1,
130
+ candleColorMode: "openClose",
131
+ candleColorEpsilon: -1,
130
132
  autoScaleSmoothing: 0.16,
131
133
  autoScaleIgnoreLatestCandle: true,
132
134
  doubleClickEnabled: true,
@@ -487,11 +489,11 @@ var BUILTIN_VOLUME_INDICATOR = {
487
489
  const xCenter = xFromIndex(index);
488
490
  const barX = Math.round(xCenter - barWidth / 2);
489
491
  const barY = Math.round(paneBottom - volumeHeight);
490
- const isUp = point.c >= point.o;
491
- const opacity = isUp ? upOpacity : downOpacity;
492
+ const direction = renderContext.getCandleDirectionByIndex(index);
493
+ const opacity = direction === "up" ? upOpacity : downOpacity;
492
494
  ctx.save();
493
495
  ctx.globalAlpha = opacity;
494
- ctx.fillStyle = isUp ? upColor : downColor;
496
+ ctx.fillStyle = direction === "up" ? upColor : downColor;
495
497
  ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
496
498
  ctx.restore();
497
499
  }
@@ -843,6 +845,24 @@ function createChart(element, options = {}) {
843
845
  const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
844
846
  return price.toFixed(decimals);
845
847
  };
848
+ const roundToPricePrecision = (price) => {
849
+ const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
850
+ return Number(price.toFixed(decimals));
851
+ };
852
+ const getResolvedCandleColorEpsilon = () => {
853
+ const configured = mergedOptions.candleColorEpsilon;
854
+ if (configured >= 0) {
855
+ return configured;
856
+ }
857
+ const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
858
+ return decimals > 0 ? 0.5 / 10 ** decimals : 0;
859
+ };
860
+ const getDirectionFromDelta = (delta) => {
861
+ const epsilon = Math.max(0, getResolvedCandleColorEpsilon());
862
+ if (delta > epsilon) return 1;
863
+ if (delta < -epsilon) return -1;
864
+ return 0;
865
+ };
846
866
  const getStabilizedPriceTemplate = () => {
847
867
  const explicitTemplate = mergedOptions.priceLabelWidthTemplate.trim();
848
868
  if (explicitTemplate.length > 0) {
@@ -869,14 +889,33 @@ function createChart(element, options = {}) {
869
889
  return Math.max(measured, templateWidth);
870
890
  };
871
891
  const parseData = (nextData) => {
872
- return nextData.map((point) => ({
873
- time: new Date(point.t),
874
- o: point.o,
875
- h: point.h,
876
- l: point.l,
877
- c: point.c,
878
- ...point.v === void 0 ? {} : { v: point.v }
879
- })).filter((point) => Number.isFinite(point.time.getTime())).sort((a, b) => a.time.getTime() - b.time.getTime());
892
+ const dedupedByTime = /* @__PURE__ */ new Map();
893
+ for (const point of nextData) {
894
+ const time = new Date(point.t);
895
+ const timeMs = time.getTime();
896
+ if (!Number.isFinite(timeMs)) {
897
+ continue;
898
+ }
899
+ const open = Number(point.o);
900
+ const close = Number(point.c);
901
+ const highInput = Number(point.h);
902
+ const lowInput = Number(point.l);
903
+ if (!Number.isFinite(open) || !Number.isFinite(close) || !Number.isFinite(highInput) || !Number.isFinite(lowInput)) {
904
+ continue;
905
+ }
906
+ const normalizedHigh = Math.max(highInput, open, close);
907
+ const normalizedLow = Math.min(lowInput, open, close);
908
+ const volumeValue = point.v === void 0 ? void 0 : Number(point.v);
909
+ dedupedByTime.set(timeMs, {
910
+ time,
911
+ o: open,
912
+ h: normalizedHigh,
913
+ l: normalizedLow,
914
+ c: close,
915
+ ...volumeValue === void 0 || !Number.isFinite(volumeValue) ? {} : { v: volumeValue }
916
+ });
917
+ }
918
+ return Array.from(dedupedByTime.values()).sort((a, b) => a.time.getTime() - b.time.getTime());
880
919
  };
881
920
  const getTimeStepMs = () => {
882
921
  if (data.length < 2) {
@@ -953,6 +992,26 @@ function createChart(element, options = {}) {
953
992
  const upperDelta = Math.abs(upperPoint.time.getTime() - timeMs);
954
993
  return lowerDelta <= upperDelta ? lower : upper;
955
994
  };
995
+ const getCandleDirectionByIndex = (index) => {
996
+ const point = data[index];
997
+ if (!point) {
998
+ return "up";
999
+ }
1000
+ const prevPoint = index > 0 ? data[index - 1] : void 0;
1001
+ const mode = mergedOptions.candleColorMode;
1002
+ const baseForMode = mode === "prevClose" && prevPoint ? prevPoint.c : point.o;
1003
+ let direction = getDirectionFromDelta(point.c - baseForMode);
1004
+ if (direction === 0 && mode === "prevClose") {
1005
+ direction = getDirectionFromDelta(point.c - point.o);
1006
+ }
1007
+ if (direction === 0 && prevPoint) {
1008
+ direction = getDirectionFromDelta(point.c - prevPoint.c);
1009
+ }
1010
+ if (direction === 0) {
1011
+ return point.c >= point.o ? "up" : "down";
1012
+ }
1013
+ return direction > 0 ? "up" : "down";
1014
+ };
956
1015
  const formatHoverTimeLabel = (time, mode) => {
957
1016
  if (mode === "time") {
958
1017
  return time.toLocaleTimeString(void 0, {
@@ -1538,8 +1597,8 @@ function createChart(element, options = {}) {
1538
1597
  const closeY = yFromPrice(point.c);
1539
1598
  const highY = yFromPrice(point.h);
1540
1599
  const lowY = yFromPrice(point.l);
1541
- const isUp = point.c >= point.o;
1542
- const candleColor = isUp ? mergedOptions.upColor : mergedOptions.downColor;
1600
+ const direction = getCandleDirectionByIndex(index);
1601
+ const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
1543
1602
  ctx.strokeStyle = candleColor;
1544
1603
  ctx.lineWidth = candleWickWidth;
1545
1604
  ctx.beginPath();
@@ -1573,6 +1632,7 @@ function createChart(element, options = {}) {
1573
1632
  chartHeight,
1574
1633
  xFromIndex,
1575
1634
  yFromPrice,
1635
+ getCandleDirectionByIndex,
1576
1636
  candleSpacing,
1577
1637
  upColor: mergedOptions.upColor,
1578
1638
  downColor: mergedOptions.downColor
@@ -1632,6 +1692,7 @@ function createChart(element, options = {}) {
1632
1692
  chartHeight: paneHeight,
1633
1693
  xFromIndex,
1634
1694
  yFromPrice: null,
1695
+ getCandleDirectionByIndex,
1635
1696
  candleSpacing,
1636
1697
  upColor: mergedOptions.upColor,
1637
1698
  downColor: mergedOptions.downColor
@@ -1669,7 +1730,8 @@ function createChart(element, options = {}) {
1669
1730
  const tickerPrice = lastPoint.c;
1670
1731
  const tickerY = yFromPrice(tickerPrice);
1671
1732
  const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
1672
- const tickerColor = ticker.color ?? (lastPoint.c >= lastPoint.o ? mergedOptions.upColor : mergedOptions.downColor);
1733
+ const lastDirection = getCandleDirectionByIndex(data.length - 1);
1734
+ const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
1673
1735
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
1674
1736
  const tickerStyle = ticker.style ?? "solid";
1675
1737
  ctx.save();
@@ -2037,7 +2099,7 @@ function createChart(element, options = {}) {
2037
2099
  x,
2038
2100
  y,
2039
2101
  region,
2040
- ...region === "plot" ? { price: Number(priceFromCanvasY(y).toFixed(mergedOptions.priceDecimals)) } : {},
2102
+ ...region === "plot" ? { price: roundToPricePrecision(priceFromCanvasY(y)) } : {},
2041
2103
  ...index === null ? {} : { index },
2042
2104
  ...hoverTime ? { time: hoverTime.toISOString() } : {},
2043
2105
  ...point ? { point } : {}
@@ -2083,7 +2145,7 @@ function createChart(element, options = {}) {
2083
2145
  if (orderRegion) {
2084
2146
  if (orderRegion.draggable) {
2085
2147
  activePointerId = event.pointerId;
2086
- const startPrice = Number(orderRegion.line.price.toFixed(2));
2148
+ const startPrice = roundToPricePrecision(orderRegion.line.price);
2087
2149
  actionDragState = {
2088
2150
  orderId: orderRegion.orderId,
2089
2151
  action: orderRegion.action,
@@ -2142,7 +2204,7 @@ function createChart(element, options = {}) {
2142
2204
  if (activePointerId !== null && event.pointerId !== activePointerId) {
2143
2205
  return;
2144
2206
  }
2145
- const nextPrice = Number(priceFromCanvasY(point.y).toFixed(2));
2207
+ const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
2146
2208
  if (nextPrice !== orderDragState.lastPrice) {
2147
2209
  orderDragState.lastPrice = nextPrice;
2148
2210
  orderLines = orderLines.map((line) => {
@@ -2172,7 +2234,7 @@ function createChart(element, options = {}) {
2172
2234
  if (activePointerId !== null && event.pointerId !== activePointerId) {
2173
2235
  return;
2174
2236
  }
2175
- const nextPrice = Number(priceFromCanvasY(point.y).toFixed(2));
2237
+ const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
2176
2238
  if (nextPrice !== actionDragState.lastPrice) {
2177
2239
  actionDragState.lastPrice = nextPrice;
2178
2240
  actionDragState.moved = true;
@@ -2289,7 +2351,7 @@ function createChart(element, options = {}) {
2289
2351
  canvas.style.cursor = "default";
2290
2352
  if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
2291
2353
  if (!pointerDownInfo.moved) {
2292
- const clickPrice = pointerDownInfo.region === "plot" ? Number(priceFromCanvasY(pointerDownInfo.y).toFixed(2)) : void 0;
2354
+ const clickPrice = pointerDownInfo.region === "plot" ? roundToPricePrecision(priceFromCanvasY(pointerDownInfo.y)) : void 0;
2293
2355
  chartClickHandler?.({
2294
2356
  x: pointerDownInfo.x,
2295
2357
  y: pointerDownInfo.y,
@@ -2349,7 +2411,7 @@ function createChart(element, options = {}) {
2349
2411
  }
2350
2412
  orderActionHandler?.({
2351
2413
  action: "createLimit",
2352
- price: Number(priceFromCanvasY(point.y).toFixed(2))
2414
+ price: roundToPricePrecision(priceFromCanvasY(point.y))
2353
2415
  });
2354
2416
  return;
2355
2417
  }
package/docs/API.md CHANGED
@@ -50,6 +50,8 @@ Top-level options:
50
50
  - `candleBodyWidthRatio` (default `0.7`)
51
51
  - `candleMinWidth` (default `0.5`)
52
52
  - `candleWickWidth` (default `1`)
53
+ - `candleColorMode` (`"openClose" | "prevClose"`, default `"openClose"`)
54
+ - `candleColorEpsilon` (default `-1` = auto from `priceDecimals`; set `0` to disable tolerance)
53
55
  - `autoScaleSmoothing` (default `0.16`)
54
56
  - `autoScaleIgnoreLatestCandle` (default `true`)
55
57
  - `doubleClickEnabled` (default `true`)
package/docs/RECIPES.md CHANGED
@@ -26,6 +26,26 @@ Use:
26
26
  - `autoScaleSmoothing` for smoother scale transitions
27
27
  - `autoScaleIgnoreLatestCandle` to reduce live-candle jitter
28
28
 
29
+ ## Stabilize candle up/down coloring on tiny deltas
30
+
31
+ ```ts
32
+ const chart = createChart(rootEl, {
33
+ // default behavior
34
+ candleColorMode: "openClose",
35
+ // auto epsilon from priceDecimals (recommended)
36
+ candleColorEpsilon: -1
37
+ });
38
+ ```
39
+
40
+ Use previous-close mode if your UX expects green/red by last-close change:
41
+
42
+ ```ts
43
+ const chart = createChart(rootEl, {
44
+ candleColorMode: "prevClose",
45
+ candleColorEpsilon: -1
46
+ });
47
+ ```
48
+
29
49
  ## Add a static alert/level line
30
50
 
31
51
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",