acttrader-charts 1.0.13 → 1.0.14
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 +13 -1
- package/dist/{MACD-CEmdbwGz.d.cts → MACD-CxJLGBBd.d.cts} +58 -5
- package/dist/{MACD-CEmdbwGz.d.ts → MACD-CxJLGBBd.d.ts} +58 -5
- package/dist/index.cjs +729 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -2
- package/dist/index.d.ts +29 -2
- package/dist/index.js +729 -67
- package/dist/index.js.map +1 -1
- package/dist/indicators/index.d.cts +2 -2
- package/dist/indicators/index.d.ts +2 -2
- package/dist/webview/chart.html +67 -67
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -117,6 +117,33 @@ var _ScaleManager = class _ScaleManager {
|
|
|
117
117
|
};
|
|
118
118
|
_ScaleManager.VERTICAL_PADDING_PX = 20;
|
|
119
119
|
var ScaleManager = _ScaleManager;
|
|
120
|
+
function niceStep(rawStep) {
|
|
121
|
+
if (!isFinite(rawStep) || rawStep <= 0) return 1;
|
|
122
|
+
const exp = Math.floor(Math.log10(rawStep));
|
|
123
|
+
const base = Math.pow(10, exp);
|
|
124
|
+
const frac = rawStep / base;
|
|
125
|
+
const nice = frac < 1.5 ? 1 : frac < 3.5 ? 2 : frac < 7.5 ? 5 : 10;
|
|
126
|
+
return nice * base;
|
|
127
|
+
}
|
|
128
|
+
function generatePriceTicks(range, chartHeight, targetPxPerLabel = 48) {
|
|
129
|
+
const { min, max } = range;
|
|
130
|
+
if (!isFinite(min) || !isFinite(max) || max <= min) return [min];
|
|
131
|
+
const approxSteps = Math.max(2, Math.floor(chartHeight / targetPxPerLabel));
|
|
132
|
+
const rawStep = (max - min) / approxSteps;
|
|
133
|
+
const step = niceStep(rawStep);
|
|
134
|
+
const first = Math.ceil(min / step) * step;
|
|
135
|
+
const ticks = [];
|
|
136
|
+
const eps = step * 1e-6;
|
|
137
|
+
for (let v = first; v <= max + eps; v += step) {
|
|
138
|
+
ticks.push(v);
|
|
139
|
+
}
|
|
140
|
+
return ticks;
|
|
141
|
+
}
|
|
142
|
+
function precisionForStep(step, minPrecision = 0) {
|
|
143
|
+
if (!isFinite(step) || step <= 0) return minPrecision;
|
|
144
|
+
const exp = Math.floor(Math.log10(step));
|
|
145
|
+
return Math.max(minPrecision, -exp);
|
|
146
|
+
}
|
|
120
147
|
|
|
121
148
|
// src/core/EventEmitter.ts
|
|
122
149
|
var EventEmitter = class {
|
|
@@ -169,6 +196,11 @@ function resolveCssVarsInTheme(theme, container) {
|
|
|
169
196
|
container.appendChild(tmp);
|
|
170
197
|
const resolve = (value) => {
|
|
171
198
|
if (!value.includes("var(")) return value;
|
|
199
|
+
value = value.replace(
|
|
200
|
+
/rgba\(\s*(var\([^)]+\))\s*,\s*([0-9.]+)\s*\)/g,
|
|
201
|
+
"rgb($1 / $2)"
|
|
202
|
+
);
|
|
203
|
+
tmp.style.color = "";
|
|
172
204
|
tmp.style.color = value;
|
|
173
205
|
const resolved = getComputedStyle(tmp).color;
|
|
174
206
|
return resolved || value;
|
|
@@ -535,7 +567,8 @@ var DEFAULT_LABELS = {
|
|
|
535
567
|
toggleDrawingTitle: "Toggle Drawing Tools",
|
|
536
568
|
toggleFullscreenTitle: "Toggle Fullscreen",
|
|
537
569
|
exitFullscreenTitle: "Exit Fullscreen",
|
|
538
|
-
settingsTitle: "Settings"
|
|
570
|
+
settingsTitle: "Settings",
|
|
571
|
+
tfcToggleTitle: "Trade from Charts"
|
|
539
572
|
},
|
|
540
573
|
bottomBar: {
|
|
541
574
|
durations: {
|
|
@@ -1430,18 +1463,18 @@ function renderHighLow(ctx, bars, viewport, priceRange, scale, theme, chartWidth
|
|
|
1430
1463
|
}
|
|
1431
1464
|
|
|
1432
1465
|
// src/overlays/GridOverlay.ts
|
|
1433
|
-
function renderGrid(ctx, priceRange, scale, theme, chartWidth, chartHeight, gridLines
|
|
1466
|
+
function renderGrid(ctx, priceRange, scale, theme, chartWidth, chartHeight, gridLines) {
|
|
1434
1467
|
ctx.save();
|
|
1435
1468
|
ctx.globalAlpha = 0.6;
|
|
1436
1469
|
ctx.strokeStyle = theme.grid;
|
|
1437
1470
|
ctx.lineWidth = 1;
|
|
1438
|
-
const step = (priceRange.max - priceRange.min) / gridLines;
|
|
1439
1471
|
ctx.beginPath();
|
|
1440
|
-
|
|
1441
|
-
const price
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1472
|
+
{
|
|
1473
|
+
for (const price of generatePriceTicks(priceRange, chartHeight)) {
|
|
1474
|
+
const y = Math.round(scale.yToPixel(price, priceRange, chartHeight)) + 0.5;
|
|
1475
|
+
ctx.moveTo(0, y);
|
|
1476
|
+
ctx.lineTo(chartWidth, y);
|
|
1477
|
+
}
|
|
1445
1478
|
}
|
|
1446
1479
|
ctx.stroke();
|
|
1447
1480
|
const vertCols = Math.max(1, Math.floor(chartWidth / 80));
|
|
@@ -1513,6 +1546,66 @@ function renderCrosshair(ctx, pos, priceRange, viewport, scale, theme, chartWidt
|
|
|
1513
1546
|
}
|
|
1514
1547
|
}
|
|
1515
1548
|
|
|
1549
|
+
// src/overlays/TradeLevelCluster.ts
|
|
1550
|
+
function detectClusters(measured, thresholdPx, excludeLabels = /* @__PURE__ */ new Set()) {
|
|
1551
|
+
const eligible = measured.filter((m) => !excludeLabels.has(m.level.label)).sort((a, b) => a.y - b.y);
|
|
1552
|
+
if (eligible.length < 2) return [];
|
|
1553
|
+
const groups = [];
|
|
1554
|
+
let currentGroup = [eligible[0]];
|
|
1555
|
+
let groupStartY = eligible[0].y;
|
|
1556
|
+
for (let i = 1; i < eligible.length; i++) {
|
|
1557
|
+
const m = eligible[i];
|
|
1558
|
+
if (m.y - groupStartY <= thresholdPx) {
|
|
1559
|
+
currentGroup.push(m);
|
|
1560
|
+
} else {
|
|
1561
|
+
groups.push(currentGroup);
|
|
1562
|
+
currentGroup = [m];
|
|
1563
|
+
groupStartY = m.y;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
groups.push(currentGroup);
|
|
1567
|
+
return groups.filter((g) => g.length >= 2).map((members) => {
|
|
1568
|
+
const id = "cluster:" + members.map((m) => m.level.label).sort().join("|");
|
|
1569
|
+
const centerY = members.reduce((sum, m) => sum + m.y, 0) / members.length;
|
|
1570
|
+
const centerPrice = members.reduce((sum, m) => sum + m.price, 0) / members.length;
|
|
1571
|
+
return { id, members, centerY, centerPrice };
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
function computeFanOutLayout(cluster, boxH, gapPx = 4) {
|
|
1575
|
+
const n = cluster.members.length;
|
|
1576
|
+
const totalH = n * boxH + (n - 1) * gapPx;
|
|
1577
|
+
const startY = cluster.centerY - totalH / 2;
|
|
1578
|
+
const sorted = [...cluster.members].sort((a, b) => a.y - b.y);
|
|
1579
|
+
const result = /* @__PURE__ */ new Map();
|
|
1580
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
1581
|
+
result.set(sorted[i].level.label, startY + i * (boxH + gapPx));
|
|
1582
|
+
}
|
|
1583
|
+
return result;
|
|
1584
|
+
}
|
|
1585
|
+
function buildClusterBadgeLayout(cluster, chartW, ctx) {
|
|
1586
|
+
const n = cluster.members.length;
|
|
1587
|
+
const types = new Set(cluster.members.map((m) => m.level.type));
|
|
1588
|
+
let noun;
|
|
1589
|
+
if (types.size === 1) {
|
|
1590
|
+
const t = types.values().next().value;
|
|
1591
|
+
noun = t === "position" ? "Position" : t === "pending" ? "Order" : "Trade";
|
|
1592
|
+
} else {
|
|
1593
|
+
noun = "Level";
|
|
1594
|
+
}
|
|
1595
|
+
const text = `${n} ${noun}${n > 1 ? "s" : ""}`;
|
|
1596
|
+
const PAD_X = 12;
|
|
1597
|
+
const textW = ctx.measureText(text).width;
|
|
1598
|
+
const EXPAND_ICON_W = 16;
|
|
1599
|
+
const badgeW = PAD_X + textW + EXPAND_ICON_W + PAD_X;
|
|
1600
|
+
const badgeH = 24;
|
|
1601
|
+
const badgeX = (chartW - badgeW) / 2;
|
|
1602
|
+
const badgeY = cluster.centerY - badgeH / 2;
|
|
1603
|
+
return { badgeX, badgeY, badgeW, badgeH, text };
|
|
1604
|
+
}
|
|
1605
|
+
function hitTestClusterBadge(x, y, areas) {
|
|
1606
|
+
return areas.find((a) => x >= a.x && x <= a.x + a.w && y >= a.y && y <= a.y + a.h) ?? null;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1516
1609
|
// src/overlays/TradeLevelOverlay.ts
|
|
1517
1610
|
var BOX_PAD_X = 8;
|
|
1518
1611
|
var BOX_PAD_Y = 4;
|
|
@@ -1631,7 +1724,7 @@ function isDraggable(level) {
|
|
|
1631
1724
|
return !!level.entryPriceEditable;
|
|
1632
1725
|
return false;
|
|
1633
1726
|
}
|
|
1634
|
-
function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH, precision, hoveredLabel = null, draggingLabel = null, dragNewPrice = null, selectedLabel = null, pendingLabels = /* @__PURE__ */ new Set(), positionStyle = "line", hideConfirmCancel = false, draftOrderLabel = null, draftBracketPnl = {}) {
|
|
1727
|
+
function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH, precision, hoveredLabel = null, draggingLabel = null, dragNewPrice = null, selectedLabel = null, pendingLabels = /* @__PURE__ */ new Set(), positionStyle = "line", hideConfirmCancel = false, draftOrderLabel = null, draftBracketPnl = {}, expandedClusters = /* @__PURE__ */ new Set(), clusterThresholdPx = 0) {
|
|
1635
1728
|
if (levels.length === 0)
|
|
1636
1729
|
return {
|
|
1637
1730
|
hitAreas: [],
|
|
@@ -1639,7 +1732,9 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1639
1732
|
hoverAreas: [],
|
|
1640
1733
|
dragAreas: [],
|
|
1641
1734
|
confirmAreas: [],
|
|
1642
|
-
addBracketAreas: []
|
|
1735
|
+
addBracketAreas: [],
|
|
1736
|
+
clusterBadgeAreas: [],
|
|
1737
|
+
clusters: []
|
|
1643
1738
|
};
|
|
1644
1739
|
const colors = theme.tradeLevels;
|
|
1645
1740
|
const boxH = ROW_H + BOX_PAD_Y * 2;
|
|
@@ -1670,13 +1765,46 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1670
1765
|
const bW = BOX_PAD_X + pnlW + labelW + BOX_PAD_X + confirmW + closeW;
|
|
1671
1766
|
measured.push({ level, price, y, boxW: bW, boxH });
|
|
1672
1767
|
}
|
|
1768
|
+
const excludeFromClustering = /* @__PURE__ */ new Set();
|
|
1769
|
+
if (draftOrderLabel) excludeFromClustering.add(draftOrderLabel);
|
|
1770
|
+
const clusters = clusterThresholdPx > 0 ? detectClusters(measured, clusterThresholdPx, excludeFromClustering) : [];
|
|
1771
|
+
const collapsedClusters = clusters.filter((c) => !expandedClusters.has(c.id));
|
|
1772
|
+
const expandedClusterList = clusters.filter((c) => expandedClusters.has(c.id));
|
|
1773
|
+
const collapsedLabels = /* @__PURE__ */ new Set();
|
|
1774
|
+
for (const c of collapsedClusters) {
|
|
1775
|
+
for (const m of c.members) collapsedLabels.add(m.level.label);
|
|
1776
|
+
}
|
|
1777
|
+
const expandedFanLayouts = /* @__PURE__ */ new Map();
|
|
1778
|
+
for (const ec of expandedClusterList) {
|
|
1779
|
+
expandedFanLayouts.set(ec.id, computeFanOutLayout(ec, boxH, 4));
|
|
1780
|
+
}
|
|
1781
|
+
const labelToExpandedCluster = /* @__PURE__ */ new Map();
|
|
1782
|
+
for (const ec of expandedClusterList) {
|
|
1783
|
+
for (const m of ec.members) labelToExpandedCluster.set(m.level.label, ec);
|
|
1784
|
+
}
|
|
1673
1785
|
measured.sort((a, b) => a.y - b.y);
|
|
1674
1786
|
const placed = [];
|
|
1675
1787
|
const layouts = /* @__PURE__ */ new Map();
|
|
1676
1788
|
for (const m of measured) {
|
|
1677
|
-
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1789
|
+
if (collapsedLabels.has(m.level.label)) continue;
|
|
1790
|
+
const ec = labelToExpandedCluster.get(m.level.label);
|
|
1791
|
+
if (ec) {
|
|
1792
|
+
const fanMap = expandedFanLayouts.get(ec.id);
|
|
1793
|
+
const fanY = fanMap?.get(m.level.label);
|
|
1794
|
+
if (fanY !== void 0) {
|
|
1795
|
+
placed.push({ label: m.level.label, boxY: fanY, boxH: m.boxH });
|
|
1796
|
+
layouts.set(m.level.label, { price: m.price, y: m.y, boxY: fanY, boxW: m.boxW, boxH: m.boxH });
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
let boxY = m.y - m.boxH / 2;
|
|
1801
|
+
let attempts = 0;
|
|
1802
|
+
while (attempts < placed.length + 1) {
|
|
1803
|
+
const overlaps = placed.some((p) => rectsOverlapY(boxY, m.boxH, p.boxY, p.boxH));
|
|
1804
|
+
if (!overlaps) break;
|
|
1805
|
+
boxY -= m.boxH + 2;
|
|
1806
|
+
attempts++;
|
|
1807
|
+
}
|
|
1680
1808
|
placed.push({ label: m.level.label, boxY, boxH: m.boxH });
|
|
1681
1809
|
layouts.set(m.level.label, { price: m.price, y: m.y, boxY, boxW: m.boxW, boxH: m.boxH });
|
|
1682
1810
|
}
|
|
@@ -1692,9 +1820,10 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1692
1820
|
selectedLabel
|
|
1693
1821
|
);
|
|
1694
1822
|
const activeLabel = hoveredLabel ?? selectedLabel;
|
|
1823
|
+
const visibleMeasured = measured.filter((m) => !collapsedLabels.has(m.level.label));
|
|
1695
1824
|
const drawOrder = [
|
|
1696
|
-
...
|
|
1697
|
-
...
|
|
1825
|
+
...visibleMeasured.filter((m) => m.level.label !== activeLabel),
|
|
1826
|
+
...visibleMeasured.filter((m) => m.level.label === activeLabel)
|
|
1698
1827
|
];
|
|
1699
1828
|
const hitAreas = [];
|
|
1700
1829
|
const editAreas = [];
|
|
@@ -1702,6 +1831,10 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1702
1831
|
const dragAreas = [];
|
|
1703
1832
|
const confirmAreas = [];
|
|
1704
1833
|
const addBracketAreas = [];
|
|
1834
|
+
const clusterBadgeAreas = [];
|
|
1835
|
+
for (const cluster of collapsedClusters) {
|
|
1836
|
+
drawClusterBadge(ctx, cluster, chartW, theme, clusterBadgeAreas);
|
|
1837
|
+
}
|
|
1705
1838
|
for (const { level } of drawOrder) {
|
|
1706
1839
|
const layout = layouts.get(level.label);
|
|
1707
1840
|
if (!layout) continue;
|
|
@@ -1732,9 +1865,134 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1732
1865
|
hideConfirmCancel,
|
|
1733
1866
|
level.label === draftOrderLabel ? draftBracketPnl : void 0
|
|
1734
1867
|
);
|
|
1868
|
+
const ec = labelToExpandedCluster.get(level.label);
|
|
1869
|
+
if (ec && layout.boxY !== layout.y - boxH / 2) {
|
|
1870
|
+
drawFanConnector(ctx, layout.boxY + boxH / 2, layout.y, chartW, theme);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
if (selectedLabel !== null && !layouts.has(selectedLabel)) {
|
|
1874
|
+
const offLevel = levels.find((l) => l.label === selectedLabel);
|
|
1875
|
+
if (offLevel) {
|
|
1876
|
+
const bh = offLevel;
|
|
1877
|
+
const slDragKey = `${offLevel.label}:stopLoss`;
|
|
1878
|
+
const tpDragKey = `${offLevel.label}:takeProfit`;
|
|
1879
|
+
const fallbackX = chartW * BRACKET_BASE_RATIO;
|
|
1880
|
+
if (bh.stopLossPrice !== void 0) {
|
|
1881
|
+
drawBracketLine(
|
|
1882
|
+
ctx,
|
|
1883
|
+
bh,
|
|
1884
|
+
"stopLoss",
|
|
1885
|
+
bh.stopLossPrice,
|
|
1886
|
+
scale,
|
|
1887
|
+
priceRange,
|
|
1888
|
+
priceH,
|
|
1889
|
+
bracketLineXMap.get(slDragKey) ?? fallbackX,
|
|
1890
|
+
chartW,
|
|
1891
|
+
theme,
|
|
1892
|
+
precision,
|
|
1893
|
+
draggingLabel,
|
|
1894
|
+
dragNewPrice,
|
|
1895
|
+
hitAreas,
|
|
1896
|
+
dragAreas,
|
|
1897
|
+
hideConfirmCancel,
|
|
1898
|
+
bh._slOrderLabel
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
if (bh.takeProfitPrice !== void 0) {
|
|
1902
|
+
drawBracketLine(
|
|
1903
|
+
ctx,
|
|
1904
|
+
bh,
|
|
1905
|
+
"takeProfit",
|
|
1906
|
+
bh.takeProfitPrice,
|
|
1907
|
+
scale,
|
|
1908
|
+
priceRange,
|
|
1909
|
+
priceH,
|
|
1910
|
+
bracketLineXMap.get(tpDragKey) ?? fallbackX,
|
|
1911
|
+
chartW,
|
|
1912
|
+
theme,
|
|
1913
|
+
precision,
|
|
1914
|
+
draggingLabel,
|
|
1915
|
+
dragNewPrice,
|
|
1916
|
+
hitAreas,
|
|
1917
|
+
dragAreas,
|
|
1918
|
+
hideConfirmCancel,
|
|
1919
|
+
bh._tpOrderLabel
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1735
1923
|
}
|
|
1736
1924
|
ctx.restore();
|
|
1737
|
-
return { hitAreas, editAreas, hoverAreas, dragAreas, confirmAreas, addBracketAreas };
|
|
1925
|
+
return { hitAreas, editAreas, hoverAreas, dragAreas, confirmAreas, addBracketAreas, clusterBadgeAreas, clusters };
|
|
1926
|
+
}
|
|
1927
|
+
var BADGE_FONT = "bold 12px 'Inter', system-ui, sans-serif";
|
|
1928
|
+
var BADGE_PAD_X = 12;
|
|
1929
|
+
var BADGE_H = 24;
|
|
1930
|
+
var BADGE_RADIUS = 12;
|
|
1931
|
+
function drawClusterBadge(ctx, cluster, chartW, theme, clusterBadgeAreas) {
|
|
1932
|
+
const mono = monoColor(theme);
|
|
1933
|
+
const isDark = theme.background !== "#ffffff";
|
|
1934
|
+
ctx.save();
|
|
1935
|
+
ctx.strokeStyle = mono;
|
|
1936
|
+
ctx.globalAlpha = 0.12;
|
|
1937
|
+
ctx.lineWidth = 1;
|
|
1938
|
+
ctx.setLineDash([4, 4]);
|
|
1939
|
+
for (const m of cluster.members) {
|
|
1940
|
+
ctx.beginPath();
|
|
1941
|
+
ctx.moveTo(0, m.y);
|
|
1942
|
+
ctx.lineTo(chartW, m.y);
|
|
1943
|
+
ctx.stroke();
|
|
1944
|
+
}
|
|
1945
|
+
ctx.setLineDash([]);
|
|
1946
|
+
ctx.restore();
|
|
1947
|
+
ctx.save();
|
|
1948
|
+
ctx.font = BADGE_FONT;
|
|
1949
|
+
const layout = buildClusterBadgeLayout(cluster, chartW, ctx);
|
|
1950
|
+
const { badgeX, badgeY, badgeW, text } = layout;
|
|
1951
|
+
const expandIcon = " \u25B8";
|
|
1952
|
+
const bgColor = isDark ? "rgba(40,40,40,0.92)" : "rgba(240,240,240,0.92)";
|
|
1953
|
+
ctx.fillStyle = bgColor;
|
|
1954
|
+
ctx.beginPath();
|
|
1955
|
+
ctx.roundRect(badgeX, badgeY, badgeW, BADGE_H, BADGE_RADIUS);
|
|
1956
|
+
ctx.fill();
|
|
1957
|
+
ctx.strokeStyle = mono;
|
|
1958
|
+
ctx.globalAlpha = 0.3;
|
|
1959
|
+
ctx.lineWidth = 1;
|
|
1960
|
+
ctx.stroke();
|
|
1961
|
+
ctx.globalAlpha = 1;
|
|
1962
|
+
ctx.fillStyle = mono;
|
|
1963
|
+
ctx.globalAlpha = 0.85;
|
|
1964
|
+
ctx.textBaseline = "middle";
|
|
1965
|
+
ctx.textAlign = "left";
|
|
1966
|
+
ctx.fillText(text + expandIcon, badgeX + BADGE_PAD_X, badgeY + BADGE_H / 2);
|
|
1967
|
+
ctx.globalAlpha = 1;
|
|
1968
|
+
ctx.restore();
|
|
1969
|
+
clusterBadgeAreas.push({
|
|
1970
|
+
clusterId: cluster.id,
|
|
1971
|
+
x: badgeX,
|
|
1972
|
+
y: badgeY,
|
|
1973
|
+
w: badgeW,
|
|
1974
|
+
h: BADGE_H
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
function drawFanConnector(ctx, fanCenterY, realY, chartW, theme) {
|
|
1978
|
+
if (Math.abs(fanCenterY - realY) < 2) return;
|
|
1979
|
+
const mono = monoColor(theme);
|
|
1980
|
+
ctx.save();
|
|
1981
|
+
ctx.strokeStyle = mono;
|
|
1982
|
+
ctx.globalAlpha = 0.25;
|
|
1983
|
+
ctx.lineWidth = 1;
|
|
1984
|
+
ctx.setLineDash([2, 3]);
|
|
1985
|
+
const connectorX = chartW - 8;
|
|
1986
|
+
ctx.beginPath();
|
|
1987
|
+
ctx.moveTo(connectorX, fanCenterY);
|
|
1988
|
+
ctx.lineTo(connectorX, realY);
|
|
1989
|
+
ctx.stroke();
|
|
1990
|
+
ctx.setLineDash([]);
|
|
1991
|
+
ctx.beginPath();
|
|
1992
|
+
ctx.moveTo(connectorX - 4, realY);
|
|
1993
|
+
ctx.lineTo(connectorX + 4, realY);
|
|
1994
|
+
ctx.stroke();
|
|
1995
|
+
ctx.restore();
|
|
1738
1996
|
}
|
|
1739
1997
|
function drawDotHoverBox(ctx, level, y, dotX, dotR, theme, precision) {
|
|
1740
1998
|
const pl = level;
|
|
@@ -2365,6 +2623,152 @@ function hitTestTradeLevelAddBracket(x, y, areas) {
|
|
|
2365
2623
|
return areas.find((a) => x >= a.x && x <= a.x + a.w && y >= a.y && y <= a.y + a.h) ?? null;
|
|
2366
2624
|
}
|
|
2367
2625
|
|
|
2626
|
+
// src/overlays/OffViewportLevelIndicator.ts
|
|
2627
|
+
function collectOffViewportMarkers(levels, priceRange, draggingLabel) {
|
|
2628
|
+
const above = [];
|
|
2629
|
+
const below = [];
|
|
2630
|
+
const { min, max } = priceRange;
|
|
2631
|
+
for (const level of levels) {
|
|
2632
|
+
if (level.label === draggingLabel) continue;
|
|
2633
|
+
const isDraftMarket = level.type === "pending" && level.orderType === "market" && level.data?._isDraft === true;
|
|
2634
|
+
if (isDraftMarket) continue;
|
|
2635
|
+
if (isFinite(level.price)) {
|
|
2636
|
+
if (level.price > max) above.push({ price: level.price, label: level.label, kind: "entry" });
|
|
2637
|
+
else if (level.price < min) below.push({ price: level.price, label: level.label, kind: "entry" });
|
|
2638
|
+
}
|
|
2639
|
+
const slPrice = level.stopLossPrice;
|
|
2640
|
+
const tpPrice = level.takeProfitPrice;
|
|
2641
|
+
if (slPrice !== void 0 && isFinite(slPrice)) {
|
|
2642
|
+
if (slPrice > max) above.push({ price: slPrice, label: level.label, kind: "sl" });
|
|
2643
|
+
else if (slPrice < min) below.push({ price: slPrice, label: level.label, kind: "sl" });
|
|
2644
|
+
}
|
|
2645
|
+
if (tpPrice !== void 0 && isFinite(tpPrice)) {
|
|
2646
|
+
if (tpPrice > max) above.push({ price: tpPrice, label: level.label, kind: "tp" });
|
|
2647
|
+
else if (tpPrice < min) below.push({ price: tpPrice, label: level.label, kind: "tp" });
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
return { above, below };
|
|
2651
|
+
}
|
|
2652
|
+
function entryCount(markers) {
|
|
2653
|
+
let n = 0;
|
|
2654
|
+
for (const m of markers) if (m.kind === "entry") n++;
|
|
2655
|
+
return n;
|
|
2656
|
+
}
|
|
2657
|
+
function nearestEdgeMarker(markers, edge) {
|
|
2658
|
+
let best = markers[0];
|
|
2659
|
+
let bestDist = Math.abs(best.price - edge);
|
|
2660
|
+
for (let i = 1; i < markers.length; i++) {
|
|
2661
|
+
const d = Math.abs(markers[i].price - edge);
|
|
2662
|
+
if (d < bestDist) {
|
|
2663
|
+
best = markers[i];
|
|
2664
|
+
bestDist = d;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
return best;
|
|
2668
|
+
}
|
|
2669
|
+
var PILL_FONT = "11px 'Inter', system-ui, sans-serif";
|
|
2670
|
+
var PILL_PAD_X = 8;
|
|
2671
|
+
var PILL_RIGHT_MARGIN = 8;
|
|
2672
|
+
var PILL_V_MARGIN = 10;
|
|
2673
|
+
var PILL_H = 20;
|
|
2674
|
+
var PILL_RADIUS = 10;
|
|
2675
|
+
var ARROW_UP = "\u25B2";
|
|
2676
|
+
var ARROW_DOWN = "\u25BC";
|
|
2677
|
+
function renderOffViewportIndicators(ctx, levels, priceRange, theme, chartWidth, chartHeight, draggingLabel, selectedLabel = null) {
|
|
2678
|
+
const hitAreas = [];
|
|
2679
|
+
const PADDING_PX = 20;
|
|
2680
|
+
const usable = chartHeight - 2 * PADDING_PX;
|
|
2681
|
+
const span = priceRange.max - priceRange.min;
|
|
2682
|
+
const padInPrice = usable > 0 ? span / usable * PADDING_PX : 0;
|
|
2683
|
+
const visualRange = {
|
|
2684
|
+
min: priceRange.min - padInPrice,
|
|
2685
|
+
max: priceRange.max + padInPrice
|
|
2686
|
+
};
|
|
2687
|
+
const { above, below } = collectOffViewportMarkers(levels, visualRange, draggingLabel);
|
|
2688
|
+
if (above.length === 0 && below.length === 0) return { hitAreas };
|
|
2689
|
+
ctx.save();
|
|
2690
|
+
ctx.font = PILL_FONT;
|
|
2691
|
+
ctx.textBaseline = "middle";
|
|
2692
|
+
ctx.textAlign = "center";
|
|
2693
|
+
const PILL_GAP = 4;
|
|
2694
|
+
const renderPill = (direction, text, y, target) => {
|
|
2695
|
+
const textW = ctx.measureText(text).width;
|
|
2696
|
+
const w = Math.ceil(textW) + PILL_PAD_X * 2;
|
|
2697
|
+
const x = chartWidth - PILL_RIGHT_MARGIN - w;
|
|
2698
|
+
ctx.fillStyle = theme.tooltip.background;
|
|
2699
|
+
ctx.strokeStyle = theme.tooltip.border;
|
|
2700
|
+
ctx.lineWidth = 1;
|
|
2701
|
+
roundedRect(ctx, x, y, w, PILL_H, PILL_RADIUS);
|
|
2702
|
+
ctx.fill();
|
|
2703
|
+
ctx.stroke();
|
|
2704
|
+
ctx.fillStyle = theme.tooltip.text;
|
|
2705
|
+
ctx.fillText(text, x + w / 2, y + PILL_H / 2 + 0.5);
|
|
2706
|
+
hitAreas.push({
|
|
2707
|
+
direction,
|
|
2708
|
+
x,
|
|
2709
|
+
y,
|
|
2710
|
+
w,
|
|
2711
|
+
h: PILL_H,
|
|
2712
|
+
targetPrice: target.price,
|
|
2713
|
+
targetLabel: target.label,
|
|
2714
|
+
targetKind: target.kind
|
|
2715
|
+
});
|
|
2716
|
+
return w;
|
|
2717
|
+
};
|
|
2718
|
+
const drawColumn = (direction, markers, baseY) => {
|
|
2719
|
+
if (markers.length === 0) return;
|
|
2720
|
+
const arrow = direction === "up" ? ARROW_UP : ARROW_DOWN;
|
|
2721
|
+
const edge = direction === "up" ? priceRange.max : priceRange.min;
|
|
2722
|
+
const stackDir = direction === "up" ? 1 : -1;
|
|
2723
|
+
let nextY = baseY;
|
|
2724
|
+
const entries = entryCount(markers);
|
|
2725
|
+
if (entries > 0) {
|
|
2726
|
+
const target = nearestEdgeMarker(
|
|
2727
|
+
markers.filter((m) => m.kind === "entry"),
|
|
2728
|
+
edge
|
|
2729
|
+
);
|
|
2730
|
+
renderPill(direction, `${arrow} ${entries}`, nextY, target);
|
|
2731
|
+
nextY += stackDir * (PILL_H + PILL_GAP);
|
|
2732
|
+
}
|
|
2733
|
+
if (selectedLabel !== null) {
|
|
2734
|
+
const bracketMarkers = markers.filter(
|
|
2735
|
+
(m) => m.kind !== "entry" && m.label === selectedLabel
|
|
2736
|
+
);
|
|
2737
|
+
for (const bm of bracketMarkers) {
|
|
2738
|
+
const kindLabel = bm.kind.toUpperCase();
|
|
2739
|
+
renderPill(direction, `${arrow} ${kindLabel}`, nextY, bm);
|
|
2740
|
+
nextY += stackDir * (PILL_H + PILL_GAP);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
};
|
|
2744
|
+
drawColumn("up", above, PILL_V_MARGIN);
|
|
2745
|
+
drawColumn("down", below, chartHeight - PILL_V_MARGIN - PILL_H);
|
|
2746
|
+
ctx.restore();
|
|
2747
|
+
return { hitAreas };
|
|
2748
|
+
}
|
|
2749
|
+
function hitTestOffViewportIndicator(x, y, hitAreas) {
|
|
2750
|
+
return hitAreas.find((h) => x >= h.x && x <= h.x + h.w && y >= h.y && y <= h.y + h.h) ?? null;
|
|
2751
|
+
}
|
|
2752
|
+
function overlapsOffViewportPill(centerY, halfH, hitAreas) {
|
|
2753
|
+
const top = centerY - halfH;
|
|
2754
|
+
const bottom = centerY + halfH;
|
|
2755
|
+
return hitAreas.some((h) => top < h.y + h.h && bottom > h.y);
|
|
2756
|
+
}
|
|
2757
|
+
function roundedRect(ctx, x, y, w, h, r) {
|
|
2758
|
+
const rr = Math.min(r, w / 2, h / 2);
|
|
2759
|
+
ctx.beginPath();
|
|
2760
|
+
ctx.moveTo(x + rr, y);
|
|
2761
|
+
ctx.lineTo(x + w - rr, y);
|
|
2762
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + rr);
|
|
2763
|
+
ctx.lineTo(x + w, y + h - rr);
|
|
2764
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - rr, y + h);
|
|
2765
|
+
ctx.lineTo(x + rr, y + h);
|
|
2766
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - rr);
|
|
2767
|
+
ctx.lineTo(x, y + rr);
|
|
2768
|
+
ctx.quadraticCurveTo(x, y, x + rr, y);
|
|
2769
|
+
ctx.closePath();
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2368
2772
|
// src/axis/TimeAxis.ts
|
|
2369
2773
|
var COUNTDOWN_LABEL_HEIGHT = 18;
|
|
2370
2774
|
var COUNTDOWN_LABEL_PADDING_X = 6;
|
|
@@ -2479,10 +2883,20 @@ function renderCandleCountdown(ctx, lastBarTime, intervalMs, lastBarIndex, viewp
|
|
|
2479
2883
|
}
|
|
2480
2884
|
|
|
2481
2885
|
// src/axis/PriceAxis.ts
|
|
2482
|
-
function renderPriceAxis(ctx, priceRange, scale, theme, chartWidth, chartHeight, axisWidth, gridLines
|
|
2886
|
+
function renderPriceAxis(ctx, priceRange, scale, theme, chartWidth, chartHeight, axisWidth, gridLines, pricePrecision = 2, cfg = DEFAULT_PRICE_AXIS_CONFIG) {
|
|
2483
2887
|
ctx.fillStyle = theme.axisText;
|
|
2484
2888
|
ctx.font = `${cfg.fontSize} ${cfg.fontFamily}`;
|
|
2485
2889
|
ctx.textAlign = "left";
|
|
2890
|
+
if (gridLines === void 0) {
|
|
2891
|
+
const ticks = generatePriceTicks(priceRange, chartHeight);
|
|
2892
|
+
const step2 = ticks.length >= 2 ? ticks[1] - ticks[0] : priceRange.max - priceRange.min;
|
|
2893
|
+
const precision = precisionForStep(step2, pricePrecision);
|
|
2894
|
+
for (const price of ticks) {
|
|
2895
|
+
const y = scale.yToPixel(price, priceRange, chartHeight);
|
|
2896
|
+
ctx.fillText(price.toFixed(precision), chartWidth + 4, y + 4);
|
|
2897
|
+
}
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2486
2900
|
const step = (priceRange.max - priceRange.min) / gridLines;
|
|
2487
2901
|
for (let i = 0; i <= gridLines; i++) {
|
|
2488
2902
|
const price = priceRange.min + step * i;
|
|
@@ -3097,6 +3511,8 @@ var ICON_TOOL = {
|
|
|
3097
3511
|
14
|
|
3098
3512
|
)
|
|
3099
3513
|
};
|
|
3514
|
+
var ICON_TFC = `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><polyline points="1.5,12.5 5,9 7,11 14,4" stroke="#26a69a" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><polyline points="10,4 14,4 14,8" stroke="#26a69a" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><polyline points="1.5,3.5 5,7 7,5 14,12" stroke="#ef5350" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><polyline points="10,12 14,12 14,8" stroke="#ef5350" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
3515
|
+
var ICON_TFC_OFF = `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><polyline points="1.5,12.5 5,9 7,11 14,4" stroke="#26a69a" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.3"/><polyline points="10,4 14,4 14,8" stroke="#26a69a" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.3"/><polyline points="1.5,3.5 5,7 7,5 14,12" stroke="#ef5350" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.3"/><polyline points="10,12 14,12 14,8" stroke="#ef5350" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.3"/><line x1="3" y1="3" x2="13" y2="13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`;
|
|
3100
3516
|
var iconScrollToEnd = (c, size = 16) => `<svg viewBox="0 0 16 16" width="${size}" height="${size}" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 4L6 8L1 12Z" fill="${c}"/><path d="M6 4L11 8L6 12Z" fill="${c}"/><line x1="13.5" y1="3.5" x2="13.5" y2="12.5" stroke="${c}" stroke-width="2" stroke-linecap="round"/></svg>`;
|
|
3101
3517
|
var iconResetView = (c, size = 16) => `<svg viewBox="0 0 16 16" width="${size}" height="${size}" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 2A6 6 0 1 1 2 8" stroke="${c}" stroke-width="1.6" stroke-linecap="round"/><path d="M4 10L2 8L1 10" stroke="${c}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
3102
3518
|
var ICON_TAB = {
|
|
@@ -3132,7 +3548,7 @@ function injectFlyoutScrollbarStyles() {
|
|
|
3132
3548
|
document.head.appendChild(style);
|
|
3133
3549
|
}
|
|
3134
3550
|
var TopBar = class {
|
|
3135
|
-
constructor(onSeriesChange, onTimeframeChange, onAddIndicator, onDurationChange, onSettingsClick, onFullscreenToggle, onDrawingToolsToggle, showDrawingToolsBtn, showSettingsBtn, initialSeries, initialTf, initialDuration, durationMap, theme, cfg = DEFAULT_TOP_BAR_CONFIG, labels = DEFAULT_LABELS.topBar) {
|
|
3551
|
+
constructor(onSeriesChange, onTimeframeChange, onAddIndicator, onDurationChange, onSettingsClick, onFullscreenToggle, onDrawingToolsToggle, showDrawingToolsBtn, showSettingsBtn, showTfcBtn, initialTfcActive, onTfcToggle, initialSeries, initialTf, initialDuration, durationMap, theme, cfg = DEFAULT_TOP_BAR_CONFIG, labels = DEFAULT_LABELS.topBar) {
|
|
3136
3552
|
this.onSeriesChange = onSeriesChange;
|
|
3137
3553
|
this.onTimeframeChange = onTimeframeChange;
|
|
3138
3554
|
this.onAddIndicator = onAddIndicator;
|
|
@@ -3140,11 +3556,14 @@ var TopBar = class {
|
|
|
3140
3556
|
this.onSettingsClick = onSettingsClick;
|
|
3141
3557
|
this.onFullscreenToggle = onFullscreenToggle;
|
|
3142
3558
|
this.onDrawingToolsToggle = onDrawingToolsToggle;
|
|
3559
|
+
this.onTfcToggle = onTfcToggle;
|
|
3143
3560
|
this.settingsBtn = null;
|
|
3144
3561
|
this.isFullscreen = false;
|
|
3145
3562
|
this.durationBtns = /* @__PURE__ */ new Map();
|
|
3146
3563
|
this.isMobileLayout = false;
|
|
3147
3564
|
this.drawingBtn = null;
|
|
3565
|
+
this.tfcBtn = null;
|
|
3566
|
+
this.tfcActive = true;
|
|
3148
3567
|
this.openDropdown = null;
|
|
3149
3568
|
this.indicatorOptions = [];
|
|
3150
3569
|
this.activeIndicators = /* @__PURE__ */ new Set();
|
|
@@ -3152,6 +3571,7 @@ var TopBar = class {
|
|
|
3152
3571
|
this.theme = theme;
|
|
3153
3572
|
this.cfg = cfg;
|
|
3154
3573
|
this.labels = labels;
|
|
3574
|
+
this.tfcActive = initialTfcActive;
|
|
3155
3575
|
this.currentSeries = initialSeries;
|
|
3156
3576
|
this.currentTf = initialTf;
|
|
3157
3577
|
this.activeDuration = initialDuration;
|
|
@@ -3228,6 +3648,21 @@ var TopBar = class {
|
|
|
3228
3648
|
this.addBtnHover(this.settingsBtn);
|
|
3229
3649
|
this.rightGroup.appendChild(this.settingsBtn);
|
|
3230
3650
|
}
|
|
3651
|
+
if (showTfcBtn) {
|
|
3652
|
+
this.tfcBtn = document.createElement("button");
|
|
3653
|
+
this.tfcBtn.title = this.labels.tfcToggleTitle;
|
|
3654
|
+
this.tfcBtn.innerHTML = this.tfcActive ? ICON_TFC : ICON_TFC_OFF;
|
|
3655
|
+
this.applyIconBtnStyles(this.tfcBtn, theme, this.tfcActive);
|
|
3656
|
+
this.tfcBtn.addEventListener("click", (e) => {
|
|
3657
|
+
e.stopPropagation();
|
|
3658
|
+
this.tfcActive = !this.tfcActive;
|
|
3659
|
+
this.tfcBtn.innerHTML = this.tfcActive ? ICON_TFC : ICON_TFC_OFF;
|
|
3660
|
+
this.applyIconBtnStyles(this.tfcBtn, this.theme, this.tfcActive);
|
|
3661
|
+
this.onTfcToggle?.(this.tfcActive);
|
|
3662
|
+
});
|
|
3663
|
+
this.addBtnHover(this.tfcBtn);
|
|
3664
|
+
this.rightGroup.appendChild(this.tfcBtn);
|
|
3665
|
+
}
|
|
3231
3666
|
this.el.append(this.leftGroup, this.rightGroup);
|
|
3232
3667
|
this.docClickHandler = (e) => {
|
|
3233
3668
|
if (!this.el.contains(e.target) && !this.flyout.contains(e.target)) {
|
|
@@ -3257,6 +3692,15 @@ var TopBar = class {
|
|
|
3257
3692
|
if (!this.drawingBtn) return;
|
|
3258
3693
|
this.applyIconBtnStyles(this.drawingBtn, this.theme, active);
|
|
3259
3694
|
}
|
|
3695
|
+
setTfcActive(active) {
|
|
3696
|
+
if (!this.tfcBtn) return;
|
|
3697
|
+
this.tfcActive = active;
|
|
3698
|
+
this.tfcBtn.innerHTML = active ? ICON_TFC : ICON_TFC_OFF;
|
|
3699
|
+
this.applyIconBtnStyles(this.tfcBtn, this.theme, active);
|
|
3700
|
+
if (this.isMobileLayout) {
|
|
3701
|
+
this.resizeSvg(this.tfcBtn, this.getSvgSize(this.tfcBtn) + 2);
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3260
3704
|
setTheme(theme) {
|
|
3261
3705
|
this.theme = theme;
|
|
3262
3706
|
this.closeDropdown();
|
|
@@ -3265,6 +3709,7 @@ var TopBar = class {
|
|
|
3265
3709
|
if (this.drawingBtn) this.applyIconBtnStyles(this.drawingBtn, theme, false);
|
|
3266
3710
|
this.applyIconBtnStyles(this.fullscreenBtn, theme, this.isFullscreen);
|
|
3267
3711
|
if (this.settingsBtn) this.applyIconBtnStyles(this.settingsBtn, theme, false);
|
|
3712
|
+
if (this.tfcBtn) this.applyIconBtnStyles(this.tfcBtn, theme, this.tfcActive);
|
|
3268
3713
|
this.applySeriesBtnStyles(this.seriesBtn, theme, false);
|
|
3269
3714
|
this.applyDropBtnStyles(this.tfBtn, theme, false);
|
|
3270
3715
|
this.applyDropBtnStyles(this.indBtn, theme, false);
|
|
@@ -3305,7 +3750,7 @@ var TopBar = class {
|
|
|
3305
3750
|
this.durationSep.style.display = btnDisplay;
|
|
3306
3751
|
this.durationDropBtn.style.display = isMobile ? "" : "none";
|
|
3307
3752
|
const containerSize = isMobile ? this.cfg.mobileIconBtnSize : this.cfg.iconBtnSize;
|
|
3308
|
-
for (const btn of [this.fullscreenBtn, this.settingsBtn, this.drawingBtn]) {
|
|
3753
|
+
for (const btn of [this.fullscreenBtn, this.settingsBtn, this.drawingBtn, this.tfcBtn]) {
|
|
3309
3754
|
if (!btn) continue;
|
|
3310
3755
|
btn.style.width = containerSize;
|
|
3311
3756
|
btn.style.height = containerSize;
|
|
@@ -3315,6 +3760,9 @@ var TopBar = class {
|
|
|
3315
3760
|
if (this.settingsBtn) {
|
|
3316
3761
|
this.resizeSvg(this.settingsBtn, this.getSvgSize(this.settingsBtn) + delta);
|
|
3317
3762
|
}
|
|
3763
|
+
if (this.tfcBtn) {
|
|
3764
|
+
this.resizeSvg(this.tfcBtn, this.getSvgSize(this.tfcBtn) + delta);
|
|
3765
|
+
}
|
|
3318
3766
|
if (this.drawingBtn) {
|
|
3319
3767
|
const drawIconSize = isMobile ? parseInt(this.cfg.mobileDrawBtnIconSize, 10) : parseInt(this.cfg.drawBtnIconFontSize, 10);
|
|
3320
3768
|
this.resizeSvg(this.drawingBtn, drawIconSize);
|
|
@@ -8143,7 +8591,7 @@ function pointToSegmentDist19(px, py, x1, y1, x2, y2) {
|
|
|
8143
8591
|
const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lenSq));
|
|
8144
8592
|
return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy));
|
|
8145
8593
|
}
|
|
8146
|
-
function
|
|
8594
|
+
function roundedRect2(ctx, x, y, w, h, r) {
|
|
8147
8595
|
ctx.beginPath();
|
|
8148
8596
|
ctx.moveTo(x + r, y);
|
|
8149
8597
|
ctx.lineTo(x + w - r, y);
|
|
@@ -8194,7 +8642,7 @@ var Callout = class {
|
|
|
8194
8642
|
ctx.beginPath();
|
|
8195
8643
|
ctx.arc(tipX, tipY, 3, 0, Math.PI * 2);
|
|
8196
8644
|
ctx.fill();
|
|
8197
|
-
|
|
8645
|
+
roundedRect2(ctx, bx, by, boxW, BOX_H, RADIUS);
|
|
8198
8646
|
ctx.fillStyle = "#37474f";
|
|
8199
8647
|
ctx.fill();
|
|
8200
8648
|
ctx.strokeStyle = "#546e7a";
|
|
@@ -8246,7 +8694,7 @@ var Callout = class {
|
|
|
8246
8694
|
// src/drawings/PriceLabel.ts
|
|
8247
8695
|
var HIT_TOLERANCE42 = 6;
|
|
8248
8696
|
var BADGE_W = 60;
|
|
8249
|
-
var
|
|
8697
|
+
var BADGE_H2 = 18;
|
|
8250
8698
|
var ARROW_W = 7;
|
|
8251
8699
|
var PriceLabel = class {
|
|
8252
8700
|
constructor(point) {
|
|
@@ -8263,21 +8711,21 @@ var PriceLabel = class {
|
|
|
8263
8711
|
ctx.fillStyle = "#26a69a";
|
|
8264
8712
|
ctx.beginPath();
|
|
8265
8713
|
ctx.moveTo(x, y);
|
|
8266
|
-
ctx.lineTo(x + ARROW_W, y -
|
|
8267
|
-
ctx.lineTo(x + ARROW_W, y +
|
|
8714
|
+
ctx.lineTo(x + ARROW_W, y - BADGE_H2 / 2);
|
|
8715
|
+
ctx.lineTo(x + ARROW_W, y + BADGE_H2 / 2);
|
|
8268
8716
|
ctx.closePath();
|
|
8269
8717
|
ctx.fill();
|
|
8270
8718
|
const bx = x + ARROW_W;
|
|
8271
|
-
const by = y -
|
|
8719
|
+
const by = y - BADGE_H2 / 2;
|
|
8272
8720
|
ctx.fillStyle = "#26a69a";
|
|
8273
8721
|
ctx.beginPath();
|
|
8274
|
-
ctx.roundRect(bx, by, BADGE_W,
|
|
8722
|
+
ctx.roundRect(bx, by, BADGE_W, BADGE_H2, BADGE_H2 / 2);
|
|
8275
8723
|
ctx.fill();
|
|
8276
8724
|
if (isSelected) {
|
|
8277
8725
|
ctx.strokeStyle = "#80cbc4";
|
|
8278
8726
|
ctx.lineWidth = 1.5;
|
|
8279
8727
|
ctx.beginPath();
|
|
8280
|
-
ctx.roundRect(bx - 2, by - 2, BADGE_W + 4,
|
|
8728
|
+
ctx.roundRect(bx - 2, by - 2, BADGE_W + 4, BADGE_H2 + 4, BADGE_H2 / 2 + 2);
|
|
8281
8729
|
ctx.stroke();
|
|
8282
8730
|
}
|
|
8283
8731
|
ctx.font = "11px 'Inter', system-ui, sans-serif";
|
|
@@ -8291,8 +8739,8 @@ var PriceLabel = class {
|
|
|
8291
8739
|
const ax = scale.xToPixel(this.points[0].barIndex, viewport, chartW);
|
|
8292
8740
|
const ay = scale.yToPixel(this.points[0].price, priceRange, chartH);
|
|
8293
8741
|
const bx = ax + ARROW_W;
|
|
8294
|
-
const by = ay -
|
|
8295
|
-
return x >= ax - HIT_TOLERANCE42 && x <= bx + BADGE_W + HIT_TOLERANCE42 && y >= by - HIT_TOLERANCE42 && y <= by +
|
|
8742
|
+
const by = ay - BADGE_H2 / 2;
|
|
8743
|
+
return x >= ax - HIT_TOLERANCE42 && x <= bx + BADGE_W + HIT_TOLERANCE42 && y >= by - HIT_TOLERANCE42 && y <= by + BADGE_H2 + HIT_TOLERANCE42;
|
|
8296
8744
|
}
|
|
8297
8745
|
getHandle() {
|
|
8298
8746
|
return null;
|
|
@@ -8412,7 +8860,7 @@ var Flag = class {
|
|
|
8412
8860
|
|
|
8413
8861
|
// src/drawings/PriceNote.ts
|
|
8414
8862
|
var HIT_TOLERANCE44 = 6;
|
|
8415
|
-
var
|
|
8863
|
+
var BADGE_H3 = 16;
|
|
8416
8864
|
var BADGE_PADDING_X = 6;
|
|
8417
8865
|
var TICK_HALF = 10;
|
|
8418
8866
|
var PriceNote = class {
|
|
@@ -8438,10 +8886,10 @@ var PriceNote = class {
|
|
|
8438
8886
|
const tw = ctx.measureText(label).width;
|
|
8439
8887
|
const badgeW = tw + BADGE_PADDING_X * 2;
|
|
8440
8888
|
const bx = x + TICK_HALF + 2;
|
|
8441
|
-
const by = y -
|
|
8889
|
+
const by = y - BADGE_H3 / 2;
|
|
8442
8890
|
ctx.fillStyle = "#ffa726";
|
|
8443
8891
|
ctx.beginPath();
|
|
8444
|
-
ctx.roundRect(bx, by, badgeW,
|
|
8892
|
+
ctx.roundRect(bx, by, badgeW, BADGE_H3, 3);
|
|
8445
8893
|
ctx.fill();
|
|
8446
8894
|
ctx.fillStyle = "#ffffff";
|
|
8447
8895
|
ctx.textBaseline = "middle";
|
|
@@ -8461,7 +8909,7 @@ var PriceNote = class {
|
|
|
8461
8909
|
|
|
8462
8910
|
// src/drawings/AnchoredNote.ts
|
|
8463
8911
|
var HIT_TOLERANCE45 = 6;
|
|
8464
|
-
var
|
|
8912
|
+
var BADGE_H4 = 20;
|
|
8465
8913
|
var PAD_H2 = 10;
|
|
8466
8914
|
function pointToSegmentDist21(px, py, x1, y1, x2, y2) {
|
|
8467
8915
|
const dx = x2 - x1, dy = y2 - y1;
|
|
@@ -8494,7 +8942,7 @@ var AnchoredNote = class {
|
|
|
8494
8942
|
ctx.lineWidth = isSelected ? 1.5 : 1;
|
|
8495
8943
|
const badgeW = this.getBadgeW(ctx);
|
|
8496
8944
|
const bx = x2 - badgeW / 2;
|
|
8497
|
-
const by = y2 -
|
|
8945
|
+
const by = y2 - BADGE_H4 / 2;
|
|
8498
8946
|
ctx.strokeStyle = "#42a5f5";
|
|
8499
8947
|
ctx.setLineDash([4, 3]);
|
|
8500
8948
|
ctx.beginPath();
|
|
@@ -8508,12 +8956,12 @@ var AnchoredNote = class {
|
|
|
8508
8956
|
ctx.fill();
|
|
8509
8957
|
ctx.fillStyle = "#37474f";
|
|
8510
8958
|
ctx.beginPath();
|
|
8511
|
-
ctx.roundRect(bx, by, badgeW,
|
|
8959
|
+
ctx.roundRect(bx, by, badgeW, BADGE_H4, 4);
|
|
8512
8960
|
ctx.fill();
|
|
8513
8961
|
ctx.strokeStyle = "#42a5f5";
|
|
8514
8962
|
ctx.lineWidth = isSelected ? 1.5 : 1;
|
|
8515
8963
|
ctx.beginPath();
|
|
8516
|
-
ctx.roundRect(bx, by, badgeW,
|
|
8964
|
+
ctx.roundRect(bx, by, badgeW, BADGE_H4, 4);
|
|
8517
8965
|
ctx.stroke();
|
|
8518
8966
|
const label = this.text || "Note";
|
|
8519
8967
|
const isPlaceholder = !this.text;
|
|
@@ -8532,8 +8980,8 @@ var AnchoredNote = class {
|
|
|
8532
8980
|
const y2 = scale.yToPixel(p2.price, priceRange, chartH);
|
|
8533
8981
|
const approxBadgeW = Math.max(50, (this.text?.length ?? 4) * 7 + 20);
|
|
8534
8982
|
const bx = x2 - approxBadgeW / 2;
|
|
8535
|
-
const by = y2 -
|
|
8536
|
-
if (x >= bx - HIT_TOLERANCE45 && x <= bx + approxBadgeW + HIT_TOLERANCE45 && y >= by - HIT_TOLERANCE45 && y <= by +
|
|
8983
|
+
const by = y2 - BADGE_H4 / 2;
|
|
8984
|
+
if (x >= bx - HIT_TOLERANCE45 && x <= bx + approxBadgeW + HIT_TOLERANCE45 && y >= by - HIT_TOLERANCE45 && y <= by + BADGE_H4 + HIT_TOLERANCE45) {
|
|
8537
8985
|
return true;
|
|
8538
8986
|
}
|
|
8539
8987
|
if (pointToSegmentDist21(x, y, x1, y1, x2, y2) <= HIT_TOLERANCE45) {
|
|
@@ -12694,9 +13142,14 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12694
13142
|
this._isDraggingBracket = false;
|
|
12695
13143
|
// Main line drag state
|
|
12696
13144
|
this._level = null;
|
|
12697
|
-
this._startY = 0;
|
|
12698
13145
|
this._startPrice = 0;
|
|
12699
13146
|
this._currentPrice = 0;
|
|
13147
|
+
// Pixel offset between click Y and the level's rendered Y at drag start.
|
|
13148
|
+
// Why: the drag hit-strip is 8 px tall; without this, the cursor-to-price
|
|
13149
|
+
// mapping would snap the level to wherever the cursor is instead of
|
|
13150
|
+
// preserving the grab point. Also immunises the drag from any priceRange
|
|
13151
|
+
// animation (lerp) that happens between drag-start and first mousemove.
|
|
13152
|
+
this._pixelOffset = 0;
|
|
12700
13153
|
// Entry-price constraint bounds (applied when SL/TP exist on the level)
|
|
12701
13154
|
this._mainMinPrice = -Infinity;
|
|
12702
13155
|
this._mainMaxPrice = Infinity;
|
|
@@ -12704,6 +13157,7 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12704
13157
|
this._bracketType = null;
|
|
12705
13158
|
this._bracketStartPrice = 0;
|
|
12706
13159
|
this._bracketCurrentPrice = 0;
|
|
13160
|
+
this._bracketPixelOffset = 0;
|
|
12707
13161
|
// Bracket constraint state (populated on startBracketDrag)
|
|
12708
13162
|
this._bracketParentPrice = 0;
|
|
12709
13163
|
this._bracketParentSide = "buy";
|
|
@@ -12715,11 +13169,11 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12715
13169
|
* Pass `slPrice` / `tpPrice` / `side` to constrain the drag between SL and TP
|
|
12716
13170
|
* (used for ES/EL entry orders where the entry price must stay between brackets).
|
|
12717
13171
|
*/
|
|
12718
|
-
startDrag(level,
|
|
13172
|
+
startDrag(level, clickY, levelPixelY, startPrice, slPrice, tpPrice, side, marketPrice, orderType) {
|
|
12719
13173
|
this._active = true;
|
|
12720
13174
|
this._isDraggingBracket = false;
|
|
12721
13175
|
this._level = level;
|
|
12722
|
-
this.
|
|
13176
|
+
this._pixelOffset = clickY - levelPixelY;
|
|
12723
13177
|
this._startPrice = startPrice;
|
|
12724
13178
|
this._currentPrice = startPrice;
|
|
12725
13179
|
this._bracketType = null;
|
|
@@ -12751,13 +13205,14 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12751
13205
|
* @param bracketType 'stopLoss' | 'takeProfit'
|
|
12752
13206
|
* @param startPrice Current bracket price
|
|
12753
13207
|
*/
|
|
12754
|
-
startBracketDrag(parentLevel, bracketType, startPrice, currentMarketPrice = 0, parentPriceOverride) {
|
|
13208
|
+
startBracketDrag(parentLevel, bracketType, startPrice, clickY, bracketPixelY, currentMarketPrice = 0, parentPriceOverride) {
|
|
12755
13209
|
this._active = true;
|
|
12756
13210
|
this._isDraggingBracket = true;
|
|
12757
13211
|
this._level = parentLevel;
|
|
12758
13212
|
this._bracketType = bracketType;
|
|
12759
13213
|
this._bracketStartPrice = startPrice;
|
|
12760
13214
|
this._bracketCurrentPrice = startPrice;
|
|
13215
|
+
this._bracketPixelOffset = clickY - bracketPixelY;
|
|
12761
13216
|
this._bracketParentPrice = parentPriceOverride ?? parentLevel.price;
|
|
12762
13217
|
this._bracketParentSide = parentLevel.side ?? "buy";
|
|
12763
13218
|
this._bracketMarketPrice = currentMarketPrice;
|
|
@@ -12768,9 +13223,10 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12768
13223
|
*/
|
|
12769
13224
|
onMouseMove(currentY, scale, priceRange, priceH) {
|
|
12770
13225
|
if (!this._active || !this._level) return null;
|
|
12771
|
-
const rawPrice = scale.pixelToPrice(currentY, priceRange, priceH);
|
|
12772
13226
|
if (this._isDraggingBracket && this._bracketType !== null) {
|
|
12773
|
-
const
|
|
13227
|
+
const effectiveY2 = currentY - this._bracketPixelOffset;
|
|
13228
|
+
const rawPrice2 = scale.pixelToPrice(effectiveY2, priceRange, priceH);
|
|
13229
|
+
const clampedPrice = this._clampBracketPrice(rawPrice2, this._bracketType, this._bracketParentPrice, this._bracketParentSide, this._bracketMarketPrice);
|
|
12774
13230
|
this._bracketCurrentPrice = clampedPrice;
|
|
12775
13231
|
return {
|
|
12776
13232
|
newPrice: clampedPrice,
|
|
@@ -12778,6 +13234,8 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12778
13234
|
isBracket: true
|
|
12779
13235
|
};
|
|
12780
13236
|
}
|
|
13237
|
+
const effectiveY = currentY - this._pixelOffset;
|
|
13238
|
+
const rawPrice = scale.pixelToPrice(effectiveY, priceRange, priceH);
|
|
12781
13239
|
const constrained = Math.max(this._mainMinPrice, Math.min(this._mainMaxPrice, rawPrice));
|
|
12782
13240
|
this._currentPrice = constrained;
|
|
12783
13241
|
return {
|
|
@@ -12876,10 +13334,12 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12876
13334
|
this._level = null;
|
|
12877
13335
|
this._bracketType = null;
|
|
12878
13336
|
this._currentPrice = 0;
|
|
13337
|
+
this._pixelOffset = 0;
|
|
12879
13338
|
this._mainMinPrice = -Infinity;
|
|
12880
13339
|
this._mainMaxPrice = Infinity;
|
|
12881
13340
|
this._bracketStartPrice = 0;
|
|
12882
13341
|
this._bracketCurrentPrice = 0;
|
|
13342
|
+
this._bracketPixelOffset = 0;
|
|
12883
13343
|
this._bracketParentPrice = 0;
|
|
12884
13344
|
this._bracketParentSide = "buy";
|
|
12885
13345
|
this._bracketMarketPrice = 0;
|
|
@@ -13912,6 +14372,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
13912
14372
|
this.showBidAskLines = false;
|
|
13913
14373
|
this.showActLogo = false;
|
|
13914
14374
|
this.hideLevelConfirmCancel = false;
|
|
14375
|
+
this.levelClusteringEnabled = true;
|
|
14376
|
+
this.clusterThresholdDistance = 20;
|
|
13915
14377
|
this.enableThemeToggle = false;
|
|
13916
14378
|
this.activeIndicators = [];
|
|
13917
14379
|
this.indicatorResults = /* @__PURE__ */ new Map();
|
|
@@ -13985,11 +14447,16 @@ var _ChartEngine = class _ChartEngine {
|
|
|
13985
14447
|
// Trade / position levels
|
|
13986
14448
|
this.levels = [];
|
|
13987
14449
|
this.tradeLevelHitAreas = [];
|
|
14450
|
+
this.offViewportHitAreas = [];
|
|
13988
14451
|
this.tradeLevelEditAreas = [];
|
|
13989
14452
|
this.tradeHoverAreas = [];
|
|
13990
14453
|
this.tradeLevelDragAreas = [];
|
|
13991
14454
|
this.tradeLevelConfirmAreas = [];
|
|
13992
14455
|
this.tradeAddBracketAreas = [];
|
|
14456
|
+
this.clusterBadgeAreas = [];
|
|
14457
|
+
this.lastClusters = [];
|
|
14458
|
+
/** Set of cluster IDs that are currently expanded (fanned out). */
|
|
14459
|
+
this.expandedClusters = /* @__PURE__ */ new Set();
|
|
13993
14460
|
this.hoveredTradeLabel = null;
|
|
13994
14461
|
this.selectedTradeLabel = null;
|
|
13995
14462
|
/** Estimated PNL text for draft order SL/TP bracket lines, set by setDraftBracketPnl(). */
|
|
@@ -14004,6 +14471,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14004
14471
|
// Inline drag confirmation — accumulates changes per level until user confirms
|
|
14005
14472
|
this.pendingLevelDrags = /* @__PURE__ */ new Map();
|
|
14006
14473
|
// Trade from Charts settings
|
|
14474
|
+
this.tfcEnabled = true;
|
|
14475
|
+
this.tfcActive = true;
|
|
14007
14476
|
this.tradeDisplayFilter = "all";
|
|
14008
14477
|
this.positionRenderStyle = "dot";
|
|
14009
14478
|
this.tradesThresholdForHorizontalLine = 2;
|
|
@@ -14014,7 +14483,9 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14014
14483
|
this.dataLoader = null;
|
|
14015
14484
|
this.isFetching = false;
|
|
14016
14485
|
this.noMoreData = false;
|
|
14486
|
+
this._loadGeneration = 0;
|
|
14017
14487
|
this.maxCandles = 200;
|
|
14488
|
+
this.prefetchThreshold = 80;
|
|
14018
14489
|
this.defaultVisibleBars = DEFAULT_VISIBLE_BARS;
|
|
14019
14490
|
this.defaultMobileVisibleBars = void 0;
|
|
14020
14491
|
this.mobileBarDivisor = 2;
|
|
@@ -14052,7 +14523,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14052
14523
|
if (this.tradeButton) {
|
|
14053
14524
|
if (isInChartArea && y >= 0 && y <= priceH && !this._isTradingActive && !hoveringDotLevel) {
|
|
14054
14525
|
this.tradeButton.setY(y);
|
|
14055
|
-
this.tradeButton.
|
|
14526
|
+
const btnHalfH = this.uiConfig.tradeButton.size / 2;
|
|
14527
|
+
if (overlapsOffViewportPill(y, btnHalfH, this.offViewportHitAreas)) {
|
|
14528
|
+
this.tradeButton.hide();
|
|
14529
|
+
} else if (this.tfcActive) {
|
|
14530
|
+
this.tradeButton.show();
|
|
14531
|
+
}
|
|
14056
14532
|
this._tradeButtonClickData = { price, y };
|
|
14057
14533
|
const allBars = this.dataStore.all;
|
|
14058
14534
|
const lastClose = allBars.length > 0 ? allBars[allBars.length - 1].close : price;
|
|
@@ -14154,7 +14630,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14154
14630
|
return;
|
|
14155
14631
|
}
|
|
14156
14632
|
if (!this.drawingManager.getActiveTool()) {
|
|
14157
|
-
if (hitTestTradeLevelClose(x, y, this.tradeLevelHitAreas) || hitTestTradeLevelEdit(x, y, this.tradeLevelEditAreas) || hitTestTradeLevelConfirm(x, y, this.tradeLevelConfirmAreas)) {
|
|
14633
|
+
if (hitTestClusterBadge(x, y, this.clusterBadgeAreas) || hitTestTradeLevelClose(x, y, this.tradeLevelHitAreas) || hitTestTradeLevelEdit(x, y, this.tradeLevelEditAreas) || hitTestTradeLevelConfirm(x, y, this.tradeLevelConfirmAreas) || hitTestOffViewportIndicator(x, y, this.offViewportHitAreas)) {
|
|
14158
14634
|
this.drawCanvas.style.cursor = "pointer";
|
|
14159
14635
|
this.scheduleRender();
|
|
14160
14636
|
return;
|
|
@@ -14366,6 +14842,25 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14366
14842
|
this.scheduleRender();
|
|
14367
14843
|
return;
|
|
14368
14844
|
}
|
|
14845
|
+
const clusterHitTouch = hitTestClusterBadge(x, y, this.clusterBadgeAreas);
|
|
14846
|
+
if (clusterHitTouch) {
|
|
14847
|
+
e.preventDefault();
|
|
14848
|
+
this.panHandler.cancel();
|
|
14849
|
+
if (this.expandedClusters.has(clusterHitTouch.clusterId)) {
|
|
14850
|
+
this.expandedClusters.delete(clusterHitTouch.clusterId);
|
|
14851
|
+
} else {
|
|
14852
|
+
this.expandedClusters.add(clusterHitTouch.clusterId);
|
|
14853
|
+
}
|
|
14854
|
+
this.renderTradeLayer();
|
|
14855
|
+
return;
|
|
14856
|
+
}
|
|
14857
|
+
const offVpHit = hitTestOffViewportIndicator(x, y, this.offViewportHitAreas);
|
|
14858
|
+
if (offVpHit) {
|
|
14859
|
+
e.preventDefault();
|
|
14860
|
+
this.panHandler.cancel();
|
|
14861
|
+
this.scrollToCenterPrice(offVpHit.targetPrice);
|
|
14862
|
+
return;
|
|
14863
|
+
}
|
|
14369
14864
|
const confirmHit = hitTestTradeLevelConfirm(x, y, this.tradeLevelConfirmAreas);
|
|
14370
14865
|
if (confirmHit) {
|
|
14371
14866
|
e.preventDefault();
|
|
@@ -14422,6 +14917,10 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14422
14917
|
}
|
|
14423
14918
|
this.drawingManager.selectDrawing(null);
|
|
14424
14919
|
this.hideMobileDrawingFlyout();
|
|
14920
|
+
if (this.expandedClusters.size > 0) {
|
|
14921
|
+
this.expandedClusters.clear();
|
|
14922
|
+
this.renderTradeLayer();
|
|
14923
|
+
}
|
|
14425
14924
|
const isInChartArea = x < chartW && y < timeAxisY;
|
|
14426
14925
|
if (isInChartArea) {
|
|
14427
14926
|
if (this._mobileCrosshairActive) {
|
|
@@ -14571,7 +15070,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14571
15070
|
if (this._isTradingActive) {
|
|
14572
15071
|
this.tradeButton.hide();
|
|
14573
15072
|
} else {
|
|
14574
|
-
this.tradeButton.
|
|
15073
|
+
const _btnHalfH2 = this.uiConfig.tradeButton.size / 2;
|
|
15074
|
+
if (overlapsOffViewportPill(y, _btnHalfH2, this.offViewportHitAreas)) {
|
|
15075
|
+
this.tradeButton.hide();
|
|
15076
|
+
} else if (this.tfcActive) {
|
|
15077
|
+
this.tradeButton.show();
|
|
15078
|
+
}
|
|
14575
15079
|
}
|
|
14576
15080
|
this._tradeButtonClickData = { price, y };
|
|
14577
15081
|
const _allBars = this.dataStore.all;
|
|
@@ -14653,6 +15157,21 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14653
15157
|
const rawBarIndex = this.scaleManager.pixelToBarIndex(x, this.viewport, chartW);
|
|
14654
15158
|
const price = this.scaleManager.pixelToPrice(y, priceRange, priceH);
|
|
14655
15159
|
const barIndex = Math.round(rawBarIndex);
|
|
15160
|
+
const clusterHit = hitTestClusterBadge(x, y, this.clusterBadgeAreas);
|
|
15161
|
+
if (clusterHit) {
|
|
15162
|
+
if (this.expandedClusters.has(clusterHit.clusterId)) {
|
|
15163
|
+
this.expandedClusters.delete(clusterHit.clusterId);
|
|
15164
|
+
} else {
|
|
15165
|
+
this.expandedClusters.add(clusterHit.clusterId);
|
|
15166
|
+
}
|
|
15167
|
+
this.renderTradeLayer();
|
|
15168
|
+
return;
|
|
15169
|
+
}
|
|
15170
|
+
const offVpHit = hitTestOffViewportIndicator(x, y, this.offViewportHitAreas);
|
|
15171
|
+
if (offVpHit) {
|
|
15172
|
+
this.scrollToCenterPrice(offVpHit.targetPrice);
|
|
15173
|
+
return;
|
|
15174
|
+
}
|
|
14656
15175
|
const confirmHit = hitTestTradeLevelConfirm(x, y, this.tradeLevelConfirmAreas);
|
|
14657
15176
|
if (confirmHit) {
|
|
14658
15177
|
if (confirmHit.action === "confirm") this.applyPendingChanges(confirmHit.label);
|
|
@@ -14737,6 +15256,10 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14737
15256
|
this.renderTradeLayer();
|
|
14738
15257
|
}
|
|
14739
15258
|
}
|
|
15259
|
+
if (this.expandedClusters.size > 0) {
|
|
15260
|
+
this.expandedClusters.clear();
|
|
15261
|
+
this.renderTradeLayer();
|
|
15262
|
+
}
|
|
14740
15263
|
const timeAxisY = rect.height - this.timeAxisHeight;
|
|
14741
15264
|
if (x >= chartW) {
|
|
14742
15265
|
if (y <= priceH) {
|
|
@@ -14852,6 +15375,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14852
15375
|
this.scheduleRender();
|
|
14853
15376
|
};
|
|
14854
15377
|
this.onKeyDown = (e) => {
|
|
15378
|
+
if (e.key === "Escape" && this.expandedClusters.size > 0) {
|
|
15379
|
+
this.expandedClusters.clear();
|
|
15380
|
+
this.renderTradeLayer();
|
|
15381
|
+
e.preventDefault();
|
|
15382
|
+
return;
|
|
15383
|
+
}
|
|
14855
15384
|
if (e.key === "Escape" && this.pendingLevelDrags.size > 0) {
|
|
14856
15385
|
this.revertPendingChanges();
|
|
14857
15386
|
e.preventDefault();
|
|
@@ -14938,7 +15467,9 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14938
15467
|
durationTimeframeMap,
|
|
14939
15468
|
uiConfig,
|
|
14940
15469
|
labels,
|
|
14941
|
-
tickActivityMs = 3e4
|
|
15470
|
+
tickActivityMs = 3e4,
|
|
15471
|
+
hideSymbolAndTick = false,
|
|
15472
|
+
showBottomBar = false
|
|
14942
15473
|
} = config;
|
|
14943
15474
|
this.dataLoader = dataLoader ?? null;
|
|
14944
15475
|
this.drawingToolsEnabled = config.showDrawingTools !== false;
|
|
@@ -14947,6 +15478,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14947
15478
|
this.labels = resolveLabels(labels);
|
|
14948
15479
|
this.tickActivityMs = tickActivityMs;
|
|
14949
15480
|
this.maxCandles = config.maxCandles ?? 200;
|
|
15481
|
+
this.prefetchThreshold = config.prefetchThreshold ?? 80;
|
|
14950
15482
|
this.defaultVisibleBars = config.defaultVisibleBars ?? DEFAULT_VISIBLE_BARS;
|
|
14951
15483
|
this.defaultMobileVisibleBars = config.defaultMobileVisibleBars;
|
|
14952
15484
|
this.explicitMobileDivisor = config.mobileBarDivisor !== void 0;
|
|
@@ -14986,7 +15518,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14986
15518
|
this.showBidAskLines = config.showBidAskLines ?? false;
|
|
14987
15519
|
this.showActLogo = config.showActLogo ?? false;
|
|
14988
15520
|
this.hideLevelConfirmCancel = config.hideLevelConfirmCancel ?? false;
|
|
15521
|
+
this.levelClusteringEnabled = config.levelClusteringEnabled ?? true;
|
|
15522
|
+
this.clusterThresholdDistance = config.clusterThresholdDistance ?? 20;
|
|
14989
15523
|
this.enableThemeToggle = config.enableThemeToggle ?? false;
|
|
15524
|
+
this.tfcEnabled = config.tfcEnabled !== false;
|
|
15525
|
+
this.tfcActive = this.tfcEnabled;
|
|
14990
15526
|
this.showCandleCountdown = config.showCandleCountdown ?? true;
|
|
14991
15527
|
this.candleCountdownTimeframes = config.candleCountdownTimeframes;
|
|
14992
15528
|
this._onOrderSubmit = onOrderSubmit;
|
|
@@ -15050,6 +15586,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15050
15586
|
this.drawingToolsEnabled ? () => this.toggleDrawingTools() : null,
|
|
15051
15587
|
this.drawingToolsEnabled,
|
|
15052
15588
|
this.settingsEnabled,
|
|
15589
|
+
this.tfcEnabled,
|
|
15590
|
+
this.tfcActive,
|
|
15591
|
+
this.tfcEnabled ? (enabled) => {
|
|
15592
|
+
this.setTfcActive(enabled);
|
|
15593
|
+
} : null,
|
|
15053
15594
|
this.series,
|
|
15054
15595
|
this.timeframe,
|
|
15055
15596
|
null,
|
|
@@ -15201,7 +15742,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15201
15742
|
this.loadingOverlayEl.appendChild(loadingLabel);
|
|
15202
15743
|
this.canvasWrap.appendChild(this.loadingOverlayEl);
|
|
15203
15744
|
}
|
|
15204
|
-
if (showUI) {
|
|
15745
|
+
if (showUI && !hideSymbolAndTick) {
|
|
15205
15746
|
this.siEl = document.createElement("div");
|
|
15206
15747
|
Object.assign(this.siEl.style, {
|
|
15207
15748
|
position: "absolute",
|
|
@@ -15345,6 +15886,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15345
15886
|
const rect = this._cachedDrawRect ?? this.drawCanvas.getBoundingClientRect();
|
|
15346
15887
|
const y = e.clientY - rect.top;
|
|
15347
15888
|
const { priceH } = this.getChartLayout();
|
|
15889
|
+
const btnHalfH = this.uiConfig.tradeButton.size / 2;
|
|
15890
|
+
if (overlapsOffViewportPill(y, btnHalfH, this.offViewportHitAreas)) {
|
|
15891
|
+
this.tradeButton?.hide();
|
|
15892
|
+
return;
|
|
15893
|
+
}
|
|
15348
15894
|
if (y >= 0 && y <= priceH) {
|
|
15349
15895
|
this.tradeButton?.setY(y);
|
|
15350
15896
|
const priceRange = this._displayedPriceRange ?? this.getScaledPriceRange(this.viewport);
|
|
@@ -15359,7 +15905,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15359
15905
|
}
|
|
15360
15906
|
middleRow.appendChild(this.canvasWrap);
|
|
15361
15907
|
this.wrapper.appendChild(middleRow);
|
|
15362
|
-
if (showUI) {
|
|
15908
|
+
if (showUI && showBottomBar) {
|
|
15363
15909
|
const resolvedDurationMap = { ...DURATION_TIMEFRAME_MAP, ...durationTimeframeMap };
|
|
15364
15910
|
this.bottomBar = new BottomBar(
|
|
15365
15911
|
(d, tf) => {
|
|
@@ -15658,6 +16204,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15658
16204
|
}
|
|
15659
16205
|
triggerLoad(reason) {
|
|
15660
16206
|
if (!this.dataLoader) return;
|
|
16207
|
+
const gen = ++this._loadGeneration;
|
|
15661
16208
|
this.isFetching = true;
|
|
15662
16209
|
this.noMoreData = false;
|
|
15663
16210
|
const end = /* @__PURE__ */ new Date();
|
|
@@ -15669,6 +16216,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15669
16216
|
const interval = this.timeframeToInterval(this.getBaseInterval());
|
|
15670
16217
|
this.setLoading(true);
|
|
15671
16218
|
this.dataLoader({ start, end, interval }).then((bars) => {
|
|
16219
|
+
if (gen !== this._loadGeneration) return;
|
|
15672
16220
|
this.loadData(this.maybeAggregate(bars), reason === "duration");
|
|
15673
16221
|
this.setLoading(false);
|
|
15674
16222
|
this.emitter.emit("dataLoaded", {
|
|
@@ -15678,25 +16226,17 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15678
16226
|
end
|
|
15679
16227
|
});
|
|
15680
16228
|
}).catch(() => {
|
|
16229
|
+
if (gen !== this._loadGeneration) return;
|
|
15681
16230
|
this.isFetching = false;
|
|
15682
16231
|
this.setLoading(false);
|
|
15683
16232
|
});
|
|
15684
16233
|
}
|
|
15685
16234
|
checkNeedMoreData() {
|
|
15686
16235
|
if (!this.dataLoader || this.isFetching || this.noMoreData) return;
|
|
15687
|
-
const threshold = Math.max(
|
|
15688
|
-
20,
|
|
15689
|
-
Math.round(
|
|
15690
|
-
initialVisibleBars(
|
|
15691
|
-
this.mobileBarDivisor,
|
|
15692
|
-
this.explicitMobileDivisor,
|
|
15693
|
-
this.defaultVisibleBars,
|
|
15694
|
-
this.defaultMobileVisibleBars
|
|
15695
|
-
) * 0.25
|
|
15696
|
-
)
|
|
15697
|
-
);
|
|
16236
|
+
const threshold = Math.max(20, this.prefetchThreshold);
|
|
15698
16237
|
if (this._targetViewport.startIndex > threshold) return;
|
|
15699
16238
|
if (this.dataStore.length === 0) return;
|
|
16239
|
+
const gen = this._loadGeneration;
|
|
15700
16240
|
this.isFetching = true;
|
|
15701
16241
|
const before = this.dataStore.all[0].time;
|
|
15702
16242
|
const end = new Date(before - 1);
|
|
@@ -15708,9 +16248,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15708
16248
|
const interval = this.timeframeToInterval(this.getBaseInterval());
|
|
15709
16249
|
this.setLoading(true);
|
|
15710
16250
|
this.dataLoader({ start, end, interval }).then((bars) => {
|
|
16251
|
+
if (gen !== this._loadGeneration) return;
|
|
15711
16252
|
this.prependData(this.maybeAggregate(bars));
|
|
15712
16253
|
this.setLoading(false);
|
|
15713
16254
|
}).catch(() => {
|
|
16255
|
+
if (gen !== this._loadGeneration) return;
|
|
15714
16256
|
this.isFetching = false;
|
|
15715
16257
|
this.setLoading(false);
|
|
15716
16258
|
});
|
|
@@ -15965,6 +16507,31 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15965
16507
|
this.emitStateChange();
|
|
15966
16508
|
return this;
|
|
15967
16509
|
}
|
|
16510
|
+
/**
|
|
16511
|
+
* Toggle TFC (Trade from Charts) on or off at runtime.
|
|
16512
|
+
* When disabled, all trade levels, the trade button, and trade popover are hidden.
|
|
16513
|
+
* Only functional when `tfcEnabled` config option is `true` (default).
|
|
16514
|
+
*/
|
|
16515
|
+
setTfcActive(enabled) {
|
|
16516
|
+
if (!this.tfcEnabled) return this;
|
|
16517
|
+
this.tfcActive = enabled;
|
|
16518
|
+
this.topBar?.setTfcActive(enabled);
|
|
16519
|
+
if (!enabled) {
|
|
16520
|
+
this.tradeButton?.hide();
|
|
16521
|
+
this.tradePopover?.hide();
|
|
16522
|
+
if (this.draftOrderLabel !== null) {
|
|
16523
|
+
this.removeDraftOrder();
|
|
16524
|
+
}
|
|
16525
|
+
}
|
|
16526
|
+
if (this.chartSettingsDialog) {
|
|
16527
|
+
this.chartSettingsDialog.destroy();
|
|
16528
|
+
this.chartSettingsDialog = null;
|
|
16529
|
+
}
|
|
16530
|
+
this.renderTradeLayer();
|
|
16531
|
+
this.emitter.emit("tfcToggle", { enabled });
|
|
16532
|
+
this.emitStateChange();
|
|
16533
|
+
return this;
|
|
16534
|
+
}
|
|
15968
16535
|
clearDuration() {
|
|
15969
16536
|
if (this.duration === void 0) return;
|
|
15970
16537
|
this.duration = void 0;
|
|
@@ -16008,7 +16575,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16008
16575
|
})),
|
|
16009
16576
|
positionRenderStyle: this.positionRenderStyle,
|
|
16010
16577
|
tradeDisplayFilter: this.tradeDisplayFilter,
|
|
16011
|
-
canvasColors: Object.keys(this.settingsCanvasColors).length > 0 ? { ...this.settingsCanvasColors } : void 0
|
|
16578
|
+
canvasColors: Object.keys(this.settingsCanvasColors).length > 0 ? { ...this.settingsCanvasColors } : void 0,
|
|
16579
|
+
tfcEnabled: this.tfcActive
|
|
16012
16580
|
};
|
|
16013
16581
|
}
|
|
16014
16582
|
/**
|
|
@@ -16080,9 +16648,14 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16080
16648
|
if (state.canvasColors) {
|
|
16081
16649
|
this.settingsCanvasColors = state.canvasColors;
|
|
16082
16650
|
}
|
|
16651
|
+
if (state.tfcEnabled !== void 0 && this.tfcEnabled) {
|
|
16652
|
+
this.tfcActive = state.tfcEnabled;
|
|
16653
|
+
this.topBar?.setTfcActive(state.tfcEnabled);
|
|
16654
|
+
}
|
|
16083
16655
|
} finally {
|
|
16084
16656
|
this.isRestoringState = false;
|
|
16085
16657
|
}
|
|
16658
|
+
this.emitStateChange();
|
|
16086
16659
|
this.scheduleRender();
|
|
16087
16660
|
return this;
|
|
16088
16661
|
}
|
|
@@ -16682,7 +17255,27 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16682
17255
|
*/
|
|
16683
17256
|
selectLevel(label) {
|
|
16684
17257
|
this.selectedTradeLabel = label;
|
|
16685
|
-
if (label !== null)
|
|
17258
|
+
if (label !== null) {
|
|
17259
|
+
this.crosshair = null;
|
|
17260
|
+
const cluster = this.lastClusters.find(
|
|
17261
|
+
(c) => c.members.some((m) => m.level.label === label)
|
|
17262
|
+
);
|
|
17263
|
+
if (cluster) this.expandedClusters.add(cluster.id);
|
|
17264
|
+
}
|
|
17265
|
+
this.renderTradeLayer();
|
|
17266
|
+
return this;
|
|
17267
|
+
}
|
|
17268
|
+
/** Enable or disable trade-level fan-out clustering at runtime. */
|
|
17269
|
+
setLevelClusteringEnabled(enabled) {
|
|
17270
|
+
this.levelClusteringEnabled = enabled;
|
|
17271
|
+
this.expandedClusters.clear();
|
|
17272
|
+
this.renderTradeLayer();
|
|
17273
|
+
return this;
|
|
17274
|
+
}
|
|
17275
|
+
/** Update the cluster proximity threshold (in pixels) at runtime. Only effective when levelClusteringEnabled is true. */
|
|
17276
|
+
setClusterThresholdDistance(px) {
|
|
17277
|
+
this.clusterThresholdDistance = px;
|
|
17278
|
+
this.expandedClusters.clear();
|
|
16686
17279
|
this.renderTradeLayer();
|
|
16687
17280
|
return this;
|
|
16688
17281
|
}
|
|
@@ -16865,7 +17458,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16865
17458
|
theme: this.themeName
|
|
16866
17459
|
},
|
|
16867
17460
|
this.labels.dialogs,
|
|
16868
|
-
!!this._onOrderSubmit,
|
|
17461
|
+
!!this._onOrderSubmit && this.tfcActive,
|
|
16869
17462
|
this.enableThemeToggle
|
|
16870
17463
|
);
|
|
16871
17464
|
this.getFloatingRoot().appendChild(this.chartSettingsDialog.el);
|
|
@@ -16976,11 +17569,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16976
17569
|
const dpr = window.devicePixelRatio || 1;
|
|
16977
17570
|
const { tradeCtx: ctx, tradeCanvas: canvas, theme, viewport } = this;
|
|
16978
17571
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
16979
|
-
if (this.levels.length === 0 || this.dataStore.length === 0) {
|
|
17572
|
+
if (!this.tfcActive || this.levels.length === 0 || this.dataStore.length === 0) {
|
|
16980
17573
|
this.tradeLevelHitAreas = [];
|
|
16981
17574
|
this.tradeLevelEditAreas = [];
|
|
16982
17575
|
this.tradeHoverAreas = [];
|
|
16983
17576
|
this.tradeAddBracketAreas = [];
|
|
17577
|
+
this.offViewportHitAreas = [];
|
|
16984
17578
|
return;
|
|
16985
17579
|
}
|
|
16986
17580
|
const { chartW, priceH } = this.getChartLayout();
|
|
@@ -17035,6 +17629,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17035
17629
|
// Include true pending orders AND entry-order positions (ES/EL)
|
|
17036
17630
|
processedLevels.filter((l) => l.type === "pending" || isEntryOrder(l))
|
|
17037
17631
|
) : processedLevels;
|
|
17632
|
+
const clusterThreshold = this.levelClusteringEnabled ? this.clusterThresholdDistance : 0;
|
|
17038
17633
|
const result = renderTradeLevels(
|
|
17039
17634
|
ctx,
|
|
17040
17635
|
filteredLevels,
|
|
@@ -17052,9 +17647,22 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17052
17647
|
this.positionRenderStyle,
|
|
17053
17648
|
this.hideLevelConfirmCancel,
|
|
17054
17649
|
this.draftOrderLabel,
|
|
17055
|
-
this.draftBracketPnl
|
|
17650
|
+
this.draftBracketPnl,
|
|
17651
|
+
this.expandedClusters,
|
|
17652
|
+
clusterThreshold
|
|
17056
17653
|
);
|
|
17057
17654
|
ctx.restore();
|
|
17655
|
+
const offVpResult = renderOffViewportIndicators(
|
|
17656
|
+
ctx,
|
|
17657
|
+
filteredLevels,
|
|
17658
|
+
priceRange,
|
|
17659
|
+
theme,
|
|
17660
|
+
chartW,
|
|
17661
|
+
priceH,
|
|
17662
|
+
this.tradeDragHandler?.dragLabel ?? null,
|
|
17663
|
+
this.selectedTradeLabel
|
|
17664
|
+
);
|
|
17665
|
+
this.offViewportHitAreas = offVpResult.hitAreas;
|
|
17058
17666
|
ctx.restore();
|
|
17059
17667
|
this.tradeLevelHitAreas = result.hitAreas;
|
|
17060
17668
|
this.tradeLevelEditAreas = result.editAreas;
|
|
@@ -17062,6 +17670,14 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17062
17670
|
this.tradeLevelDragAreas = result.dragAreas;
|
|
17063
17671
|
this.tradeLevelConfirmAreas = result.confirmAreas;
|
|
17064
17672
|
this.tradeAddBracketAreas = result.addBracketAreas;
|
|
17673
|
+
this.clusterBadgeAreas = result.clusterBadgeAreas;
|
|
17674
|
+
this.lastClusters = result.clusters;
|
|
17675
|
+
if (this.expandedClusters.size > 0) {
|
|
17676
|
+
const currentIds = new Set(result.clusters.map((c) => c.id));
|
|
17677
|
+
for (const id of this.expandedClusters) {
|
|
17678
|
+
if (!currentIds.has(id)) this.expandedClusters.delete(id);
|
|
17679
|
+
}
|
|
17680
|
+
}
|
|
17065
17681
|
}
|
|
17066
17682
|
// ── Inline pending-change confirmation ────────────────────────────────────
|
|
17067
17683
|
/** Returns the effective entry price for a level, preferring any staged main drag. */
|
|
@@ -17956,7 +18572,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17956
18572
|
if (this._isTradingActive) {
|
|
17957
18573
|
this.tradeButton.hide();
|
|
17958
18574
|
} else {
|
|
17959
|
-
this.tradeButton.
|
|
18575
|
+
const _btnHalfH1 = this.uiConfig.tradeButton.size / 2;
|
|
18576
|
+
if (overlapsOffViewportPill(y, _btnHalfH1, this.offViewportHitAreas)) {
|
|
18577
|
+
this.tradeButton.hide();
|
|
18578
|
+
} else if (this.tfcActive) {
|
|
18579
|
+
this.tradeButton.show();
|
|
18580
|
+
}
|
|
17960
18581
|
}
|
|
17961
18582
|
this._tradeButtonClickData = { price, y };
|
|
17962
18583
|
const _allBars = this.dataStore.all;
|
|
@@ -17969,6 +18590,9 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17969
18590
|
/** Shared helper: initiate a trade level main-line drag from a hit area. */
|
|
17970
18591
|
_startTradeDragFromHit(dragHit, level, hitY) {
|
|
17971
18592
|
if (!this.tradeDragHandler) return;
|
|
18593
|
+
const { priceH } = this.getChartLayout();
|
|
18594
|
+
const priceRange = this._displayedPriceRange ?? this.getScaledPriceRange(this.viewport);
|
|
18595
|
+
const levelPixelY = this.scaleManager.yToPixel(dragHit.price, priceRange, priceH);
|
|
17972
18596
|
if (dragHit.isBracket) {
|
|
17973
18597
|
this._pendingBracketOrderLabel = dragHit.bracketOrderLabel ?? null;
|
|
17974
18598
|
this._isBracketNew = false;
|
|
@@ -17981,6 +18605,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17981
18605
|
level,
|
|
17982
18606
|
dragHit.bracketType,
|
|
17983
18607
|
dragHit.price,
|
|
18608
|
+
hitY,
|
|
18609
|
+
levelPixelY,
|
|
17984
18610
|
marketPrice,
|
|
17985
18611
|
this._effectiveEntryPrice(level.label, level.price)
|
|
17986
18612
|
);
|
|
@@ -18000,6 +18626,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18000
18626
|
this.tradeDragHandler.startDrag(
|
|
18001
18627
|
level,
|
|
18002
18628
|
hitY,
|
|
18629
|
+
levelPixelY,
|
|
18003
18630
|
dragHit.price,
|
|
18004
18631
|
posLevel.stopLossPrice,
|
|
18005
18632
|
posLevel.takeProfitPrice,
|
|
@@ -18014,6 +18641,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18014
18641
|
this.tradeDragHandler.startDrag(
|
|
18015
18642
|
level,
|
|
18016
18643
|
hitY,
|
|
18644
|
+
levelPixelY,
|
|
18017
18645
|
dragHit.price,
|
|
18018
18646
|
void 0,
|
|
18019
18647
|
void 0,
|
|
@@ -18043,6 +18671,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18043
18671
|
level,
|
|
18044
18672
|
addHit.bracketType,
|
|
18045
18673
|
initialPrice,
|
|
18674
|
+
hitY,
|
|
18675
|
+
btnCenterY,
|
|
18046
18676
|
marketPrice,
|
|
18047
18677
|
this._effectiveEntryPrice(level.label, level.price)
|
|
18048
18678
|
);
|
|
@@ -18233,6 +18863,38 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18233
18863
|
};
|
|
18234
18864
|
this.scrollAnimId = requestAnimationFrame(step);
|
|
18235
18865
|
}
|
|
18866
|
+
/**
|
|
18867
|
+
* Smooth-scroll the Y axis so `price` lands at the vertical center of the
|
|
18868
|
+
* chart. Animates `pricePanOffset` only — horizontal viewport is preserved.
|
|
18869
|
+
* Used by off-viewport level indicator clicks.
|
|
18870
|
+
*/
|
|
18871
|
+
scrollToCenterPrice(price) {
|
|
18872
|
+
if (!isFinite(price) || this.dataStore.length === 0) return;
|
|
18873
|
+
if (this.scrollAnimId !== null) {
|
|
18874
|
+
cancelAnimationFrame(this.scrollAnimId);
|
|
18875
|
+
this.scrollAnimId = null;
|
|
18876
|
+
}
|
|
18877
|
+
const base = this.dataStore.getPriceRange(this.viewport);
|
|
18878
|
+
const naturalMid = (base.min + base.max) / 2;
|
|
18879
|
+
const startPan = this.pricePanOffset;
|
|
18880
|
+
const targetPan = price - naturalMid;
|
|
18881
|
+
if (Math.abs(targetPan - startPan) < 1e-9) return;
|
|
18882
|
+
const DURATION = 350;
|
|
18883
|
+
const t0 = performance.now();
|
|
18884
|
+
const ease = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
|
18885
|
+
const step = (now) => {
|
|
18886
|
+
const raw = Math.min((now - t0) / DURATION, 1);
|
|
18887
|
+
const prog = ease(raw);
|
|
18888
|
+
this.pricePanOffset = startPan + (targetPan - startPan) * prog;
|
|
18889
|
+
this.render();
|
|
18890
|
+
if (raw < 1) {
|
|
18891
|
+
this.scrollAnimId = requestAnimationFrame(step);
|
|
18892
|
+
} else {
|
|
18893
|
+
this.scrollAnimId = null;
|
|
18894
|
+
}
|
|
18895
|
+
};
|
|
18896
|
+
this.scrollAnimId = requestAnimationFrame(step);
|
|
18897
|
+
}
|
|
18236
18898
|
resetPriceAxis() {
|
|
18237
18899
|
this.priceScaleFactor = 1;
|
|
18238
18900
|
this.pricePanOffset = 0;
|