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.cjs
CHANGED
|
@@ -116,6 +116,33 @@ var _ScaleManager = class _ScaleManager {
|
|
|
116
116
|
};
|
|
117
117
|
_ScaleManager.VERTICAL_PADDING_PX = 20;
|
|
118
118
|
var ScaleManager = _ScaleManager;
|
|
119
|
+
function niceStep(rawStep) {
|
|
120
|
+
if (!isFinite(rawStep) || rawStep <= 0) return 1;
|
|
121
|
+
const exp = Math.floor(Math.log10(rawStep));
|
|
122
|
+
const base = Math.pow(10, exp);
|
|
123
|
+
const frac = rawStep / base;
|
|
124
|
+
const nice = frac < 1.5 ? 1 : frac < 3.5 ? 2 : frac < 7.5 ? 5 : 10;
|
|
125
|
+
return nice * base;
|
|
126
|
+
}
|
|
127
|
+
function generatePriceTicks(range, chartHeight, targetPxPerLabel = 48) {
|
|
128
|
+
const { min, max } = range;
|
|
129
|
+
if (!isFinite(min) || !isFinite(max) || max <= min) return [min];
|
|
130
|
+
const approxSteps = Math.max(2, Math.floor(chartHeight / targetPxPerLabel));
|
|
131
|
+
const rawStep = (max - min) / approxSteps;
|
|
132
|
+
const step = niceStep(rawStep);
|
|
133
|
+
const first = Math.ceil(min / step) * step;
|
|
134
|
+
const ticks = [];
|
|
135
|
+
const eps = step * 1e-6;
|
|
136
|
+
for (let v = first; v <= max + eps; v += step) {
|
|
137
|
+
ticks.push(v);
|
|
138
|
+
}
|
|
139
|
+
return ticks;
|
|
140
|
+
}
|
|
141
|
+
function precisionForStep(step, minPrecision = 0) {
|
|
142
|
+
if (!isFinite(step) || step <= 0) return minPrecision;
|
|
143
|
+
const exp = Math.floor(Math.log10(step));
|
|
144
|
+
return Math.max(minPrecision, -exp);
|
|
145
|
+
}
|
|
119
146
|
|
|
120
147
|
// src/core/EventEmitter.ts
|
|
121
148
|
var EventEmitter = class {
|
|
@@ -168,6 +195,11 @@ function resolveCssVarsInTheme(theme, container) {
|
|
|
168
195
|
container.appendChild(tmp);
|
|
169
196
|
const resolve = (value) => {
|
|
170
197
|
if (!value.includes("var(")) return value;
|
|
198
|
+
value = value.replace(
|
|
199
|
+
/rgba\(\s*(var\([^)]+\))\s*,\s*([0-9.]+)\s*\)/g,
|
|
200
|
+
"rgb($1 / $2)"
|
|
201
|
+
);
|
|
202
|
+
tmp.style.color = "";
|
|
171
203
|
tmp.style.color = value;
|
|
172
204
|
const resolved = getComputedStyle(tmp).color;
|
|
173
205
|
return resolved || value;
|
|
@@ -534,7 +566,8 @@ var DEFAULT_LABELS = {
|
|
|
534
566
|
toggleDrawingTitle: "Toggle Drawing Tools",
|
|
535
567
|
toggleFullscreenTitle: "Toggle Fullscreen",
|
|
536
568
|
exitFullscreenTitle: "Exit Fullscreen",
|
|
537
|
-
settingsTitle: "Settings"
|
|
569
|
+
settingsTitle: "Settings",
|
|
570
|
+
tfcToggleTitle: "Trade from Charts"
|
|
538
571
|
},
|
|
539
572
|
bottomBar: {
|
|
540
573
|
durations: {
|
|
@@ -1429,18 +1462,18 @@ function renderHighLow(ctx, bars, viewport, priceRange, scale, theme, chartWidth
|
|
|
1429
1462
|
}
|
|
1430
1463
|
|
|
1431
1464
|
// src/overlays/GridOverlay.ts
|
|
1432
|
-
function renderGrid(ctx, priceRange, scale, theme, chartWidth, chartHeight, gridLines
|
|
1465
|
+
function renderGrid(ctx, priceRange, scale, theme, chartWidth, chartHeight, gridLines) {
|
|
1433
1466
|
ctx.save();
|
|
1434
1467
|
ctx.globalAlpha = 0.6;
|
|
1435
1468
|
ctx.strokeStyle = theme.grid;
|
|
1436
1469
|
ctx.lineWidth = 1;
|
|
1437
|
-
const step = (priceRange.max - priceRange.min) / gridLines;
|
|
1438
1470
|
ctx.beginPath();
|
|
1439
|
-
|
|
1440
|
-
const price
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1471
|
+
{
|
|
1472
|
+
for (const price of generatePriceTicks(priceRange, chartHeight)) {
|
|
1473
|
+
const y = Math.round(scale.yToPixel(price, priceRange, chartHeight)) + 0.5;
|
|
1474
|
+
ctx.moveTo(0, y);
|
|
1475
|
+
ctx.lineTo(chartWidth, y);
|
|
1476
|
+
}
|
|
1444
1477
|
}
|
|
1445
1478
|
ctx.stroke();
|
|
1446
1479
|
const vertCols = Math.max(1, Math.floor(chartWidth / 80));
|
|
@@ -1512,6 +1545,66 @@ function renderCrosshair(ctx, pos, priceRange, viewport, scale, theme, chartWidt
|
|
|
1512
1545
|
}
|
|
1513
1546
|
}
|
|
1514
1547
|
|
|
1548
|
+
// src/overlays/TradeLevelCluster.ts
|
|
1549
|
+
function detectClusters(measured, thresholdPx, excludeLabels = /* @__PURE__ */ new Set()) {
|
|
1550
|
+
const eligible = measured.filter((m) => !excludeLabels.has(m.level.label)).sort((a, b) => a.y - b.y);
|
|
1551
|
+
if (eligible.length < 2) return [];
|
|
1552
|
+
const groups = [];
|
|
1553
|
+
let currentGroup = [eligible[0]];
|
|
1554
|
+
let groupStartY = eligible[0].y;
|
|
1555
|
+
for (let i = 1; i < eligible.length; i++) {
|
|
1556
|
+
const m = eligible[i];
|
|
1557
|
+
if (m.y - groupStartY <= thresholdPx) {
|
|
1558
|
+
currentGroup.push(m);
|
|
1559
|
+
} else {
|
|
1560
|
+
groups.push(currentGroup);
|
|
1561
|
+
currentGroup = [m];
|
|
1562
|
+
groupStartY = m.y;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
groups.push(currentGroup);
|
|
1566
|
+
return groups.filter((g) => g.length >= 2).map((members) => {
|
|
1567
|
+
const id = "cluster:" + members.map((m) => m.level.label).sort().join("|");
|
|
1568
|
+
const centerY = members.reduce((sum, m) => sum + m.y, 0) / members.length;
|
|
1569
|
+
const centerPrice = members.reduce((sum, m) => sum + m.price, 0) / members.length;
|
|
1570
|
+
return { id, members, centerY, centerPrice };
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
function computeFanOutLayout(cluster, boxH, gapPx = 4) {
|
|
1574
|
+
const n = cluster.members.length;
|
|
1575
|
+
const totalH = n * boxH + (n - 1) * gapPx;
|
|
1576
|
+
const startY = cluster.centerY - totalH / 2;
|
|
1577
|
+
const sorted = [...cluster.members].sort((a, b) => a.y - b.y);
|
|
1578
|
+
const result = /* @__PURE__ */ new Map();
|
|
1579
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
1580
|
+
result.set(sorted[i].level.label, startY + i * (boxH + gapPx));
|
|
1581
|
+
}
|
|
1582
|
+
return result;
|
|
1583
|
+
}
|
|
1584
|
+
function buildClusterBadgeLayout(cluster, chartW, ctx) {
|
|
1585
|
+
const n = cluster.members.length;
|
|
1586
|
+
const types = new Set(cluster.members.map((m) => m.level.type));
|
|
1587
|
+
let noun;
|
|
1588
|
+
if (types.size === 1) {
|
|
1589
|
+
const t = types.values().next().value;
|
|
1590
|
+
noun = t === "position" ? "Position" : t === "pending" ? "Order" : "Trade";
|
|
1591
|
+
} else {
|
|
1592
|
+
noun = "Level";
|
|
1593
|
+
}
|
|
1594
|
+
const text = `${n} ${noun}${n > 1 ? "s" : ""}`;
|
|
1595
|
+
const PAD_X = 12;
|
|
1596
|
+
const textW = ctx.measureText(text).width;
|
|
1597
|
+
const EXPAND_ICON_W = 16;
|
|
1598
|
+
const badgeW = PAD_X + textW + EXPAND_ICON_W + PAD_X;
|
|
1599
|
+
const badgeH = 24;
|
|
1600
|
+
const badgeX = (chartW - badgeW) / 2;
|
|
1601
|
+
const badgeY = cluster.centerY - badgeH / 2;
|
|
1602
|
+
return { badgeX, badgeY, badgeW, badgeH, text };
|
|
1603
|
+
}
|
|
1604
|
+
function hitTestClusterBadge(x, y, areas) {
|
|
1605
|
+
return areas.find((a) => x >= a.x && x <= a.x + a.w && y >= a.y && y <= a.y + a.h) ?? null;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1515
1608
|
// src/overlays/TradeLevelOverlay.ts
|
|
1516
1609
|
var BOX_PAD_X = 8;
|
|
1517
1610
|
var BOX_PAD_Y = 4;
|
|
@@ -1630,7 +1723,7 @@ function isDraggable(level) {
|
|
|
1630
1723
|
return !!level.entryPriceEditable;
|
|
1631
1724
|
return false;
|
|
1632
1725
|
}
|
|
1633
|
-
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 = {}) {
|
|
1726
|
+
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) {
|
|
1634
1727
|
if (levels.length === 0)
|
|
1635
1728
|
return {
|
|
1636
1729
|
hitAreas: [],
|
|
@@ -1638,7 +1731,9 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1638
1731
|
hoverAreas: [],
|
|
1639
1732
|
dragAreas: [],
|
|
1640
1733
|
confirmAreas: [],
|
|
1641
|
-
addBracketAreas: []
|
|
1734
|
+
addBracketAreas: [],
|
|
1735
|
+
clusterBadgeAreas: [],
|
|
1736
|
+
clusters: []
|
|
1642
1737
|
};
|
|
1643
1738
|
const colors = theme.tradeLevels;
|
|
1644
1739
|
const boxH = ROW_H + BOX_PAD_Y * 2;
|
|
@@ -1669,13 +1764,46 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1669
1764
|
const bW = BOX_PAD_X + pnlW + labelW + BOX_PAD_X + confirmW + closeW;
|
|
1670
1765
|
measured.push({ level, price, y, boxW: bW, boxH });
|
|
1671
1766
|
}
|
|
1767
|
+
const excludeFromClustering = /* @__PURE__ */ new Set();
|
|
1768
|
+
if (draftOrderLabel) excludeFromClustering.add(draftOrderLabel);
|
|
1769
|
+
const clusters = clusterThresholdPx > 0 ? detectClusters(measured, clusterThresholdPx, excludeFromClustering) : [];
|
|
1770
|
+
const collapsedClusters = clusters.filter((c) => !expandedClusters.has(c.id));
|
|
1771
|
+
const expandedClusterList = clusters.filter((c) => expandedClusters.has(c.id));
|
|
1772
|
+
const collapsedLabels = /* @__PURE__ */ new Set();
|
|
1773
|
+
for (const c of collapsedClusters) {
|
|
1774
|
+
for (const m of c.members) collapsedLabels.add(m.level.label);
|
|
1775
|
+
}
|
|
1776
|
+
const expandedFanLayouts = /* @__PURE__ */ new Map();
|
|
1777
|
+
for (const ec of expandedClusterList) {
|
|
1778
|
+
expandedFanLayouts.set(ec.id, computeFanOutLayout(ec, boxH, 4));
|
|
1779
|
+
}
|
|
1780
|
+
const labelToExpandedCluster = /* @__PURE__ */ new Map();
|
|
1781
|
+
for (const ec of expandedClusterList) {
|
|
1782
|
+
for (const m of ec.members) labelToExpandedCluster.set(m.level.label, ec);
|
|
1783
|
+
}
|
|
1672
1784
|
measured.sort((a, b) => a.y - b.y);
|
|
1673
1785
|
const placed = [];
|
|
1674
1786
|
const layouts = /* @__PURE__ */ new Map();
|
|
1675
1787
|
for (const m of measured) {
|
|
1676
|
-
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1788
|
+
if (collapsedLabels.has(m.level.label)) continue;
|
|
1789
|
+
const ec = labelToExpandedCluster.get(m.level.label);
|
|
1790
|
+
if (ec) {
|
|
1791
|
+
const fanMap = expandedFanLayouts.get(ec.id);
|
|
1792
|
+
const fanY = fanMap?.get(m.level.label);
|
|
1793
|
+
if (fanY !== void 0) {
|
|
1794
|
+
placed.push({ label: m.level.label, boxY: fanY, boxH: m.boxH });
|
|
1795
|
+
layouts.set(m.level.label, { price: m.price, y: m.y, boxY: fanY, boxW: m.boxW, boxH: m.boxH });
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
let boxY = m.y - m.boxH / 2;
|
|
1800
|
+
let attempts = 0;
|
|
1801
|
+
while (attempts < placed.length + 1) {
|
|
1802
|
+
const overlaps = placed.some((p) => rectsOverlapY(boxY, m.boxH, p.boxY, p.boxH));
|
|
1803
|
+
if (!overlaps) break;
|
|
1804
|
+
boxY -= m.boxH + 2;
|
|
1805
|
+
attempts++;
|
|
1806
|
+
}
|
|
1679
1807
|
placed.push({ label: m.level.label, boxY, boxH: m.boxH });
|
|
1680
1808
|
layouts.set(m.level.label, { price: m.price, y: m.y, boxY, boxW: m.boxW, boxH: m.boxH });
|
|
1681
1809
|
}
|
|
@@ -1691,9 +1819,10 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1691
1819
|
selectedLabel
|
|
1692
1820
|
);
|
|
1693
1821
|
const activeLabel = hoveredLabel ?? selectedLabel;
|
|
1822
|
+
const visibleMeasured = measured.filter((m) => !collapsedLabels.has(m.level.label));
|
|
1694
1823
|
const drawOrder = [
|
|
1695
|
-
...
|
|
1696
|
-
...
|
|
1824
|
+
...visibleMeasured.filter((m) => m.level.label !== activeLabel),
|
|
1825
|
+
...visibleMeasured.filter((m) => m.level.label === activeLabel)
|
|
1697
1826
|
];
|
|
1698
1827
|
const hitAreas = [];
|
|
1699
1828
|
const editAreas = [];
|
|
@@ -1701,6 +1830,10 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1701
1830
|
const dragAreas = [];
|
|
1702
1831
|
const confirmAreas = [];
|
|
1703
1832
|
const addBracketAreas = [];
|
|
1833
|
+
const clusterBadgeAreas = [];
|
|
1834
|
+
for (const cluster of collapsedClusters) {
|
|
1835
|
+
drawClusterBadge(ctx, cluster, chartW, theme, clusterBadgeAreas);
|
|
1836
|
+
}
|
|
1704
1837
|
for (const { level } of drawOrder) {
|
|
1705
1838
|
const layout = layouts.get(level.label);
|
|
1706
1839
|
if (!layout) continue;
|
|
@@ -1731,9 +1864,134 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1731
1864
|
hideConfirmCancel,
|
|
1732
1865
|
level.label === draftOrderLabel ? draftBracketPnl : void 0
|
|
1733
1866
|
);
|
|
1867
|
+
const ec = labelToExpandedCluster.get(level.label);
|
|
1868
|
+
if (ec && layout.boxY !== layout.y - boxH / 2) {
|
|
1869
|
+
drawFanConnector(ctx, layout.boxY + boxH / 2, layout.y, chartW, theme);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
if (selectedLabel !== null && !layouts.has(selectedLabel)) {
|
|
1873
|
+
const offLevel = levels.find((l) => l.label === selectedLabel);
|
|
1874
|
+
if (offLevel) {
|
|
1875
|
+
const bh = offLevel;
|
|
1876
|
+
const slDragKey = `${offLevel.label}:stopLoss`;
|
|
1877
|
+
const tpDragKey = `${offLevel.label}:takeProfit`;
|
|
1878
|
+
const fallbackX = chartW * BRACKET_BASE_RATIO;
|
|
1879
|
+
if (bh.stopLossPrice !== void 0) {
|
|
1880
|
+
drawBracketLine(
|
|
1881
|
+
ctx,
|
|
1882
|
+
bh,
|
|
1883
|
+
"stopLoss",
|
|
1884
|
+
bh.stopLossPrice,
|
|
1885
|
+
scale,
|
|
1886
|
+
priceRange,
|
|
1887
|
+
priceH,
|
|
1888
|
+
bracketLineXMap.get(slDragKey) ?? fallbackX,
|
|
1889
|
+
chartW,
|
|
1890
|
+
theme,
|
|
1891
|
+
precision,
|
|
1892
|
+
draggingLabel,
|
|
1893
|
+
dragNewPrice,
|
|
1894
|
+
hitAreas,
|
|
1895
|
+
dragAreas,
|
|
1896
|
+
hideConfirmCancel,
|
|
1897
|
+
bh._slOrderLabel
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
if (bh.takeProfitPrice !== void 0) {
|
|
1901
|
+
drawBracketLine(
|
|
1902
|
+
ctx,
|
|
1903
|
+
bh,
|
|
1904
|
+
"takeProfit",
|
|
1905
|
+
bh.takeProfitPrice,
|
|
1906
|
+
scale,
|
|
1907
|
+
priceRange,
|
|
1908
|
+
priceH,
|
|
1909
|
+
bracketLineXMap.get(tpDragKey) ?? fallbackX,
|
|
1910
|
+
chartW,
|
|
1911
|
+
theme,
|
|
1912
|
+
precision,
|
|
1913
|
+
draggingLabel,
|
|
1914
|
+
dragNewPrice,
|
|
1915
|
+
hitAreas,
|
|
1916
|
+
dragAreas,
|
|
1917
|
+
hideConfirmCancel,
|
|
1918
|
+
bh._tpOrderLabel
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1734
1922
|
}
|
|
1735
1923
|
ctx.restore();
|
|
1736
|
-
return { hitAreas, editAreas, hoverAreas, dragAreas, confirmAreas, addBracketAreas };
|
|
1924
|
+
return { hitAreas, editAreas, hoverAreas, dragAreas, confirmAreas, addBracketAreas, clusterBadgeAreas, clusters };
|
|
1925
|
+
}
|
|
1926
|
+
var BADGE_FONT = "bold 12px 'Inter', system-ui, sans-serif";
|
|
1927
|
+
var BADGE_PAD_X = 12;
|
|
1928
|
+
var BADGE_H = 24;
|
|
1929
|
+
var BADGE_RADIUS = 12;
|
|
1930
|
+
function drawClusterBadge(ctx, cluster, chartW, theme, clusterBadgeAreas) {
|
|
1931
|
+
const mono = monoColor(theme);
|
|
1932
|
+
const isDark = theme.background !== "#ffffff";
|
|
1933
|
+
ctx.save();
|
|
1934
|
+
ctx.strokeStyle = mono;
|
|
1935
|
+
ctx.globalAlpha = 0.12;
|
|
1936
|
+
ctx.lineWidth = 1;
|
|
1937
|
+
ctx.setLineDash([4, 4]);
|
|
1938
|
+
for (const m of cluster.members) {
|
|
1939
|
+
ctx.beginPath();
|
|
1940
|
+
ctx.moveTo(0, m.y);
|
|
1941
|
+
ctx.lineTo(chartW, m.y);
|
|
1942
|
+
ctx.stroke();
|
|
1943
|
+
}
|
|
1944
|
+
ctx.setLineDash([]);
|
|
1945
|
+
ctx.restore();
|
|
1946
|
+
ctx.save();
|
|
1947
|
+
ctx.font = BADGE_FONT;
|
|
1948
|
+
const layout = buildClusterBadgeLayout(cluster, chartW, ctx);
|
|
1949
|
+
const { badgeX, badgeY, badgeW, text } = layout;
|
|
1950
|
+
const expandIcon = " \u25B8";
|
|
1951
|
+
const bgColor = isDark ? "rgba(40,40,40,0.92)" : "rgba(240,240,240,0.92)";
|
|
1952
|
+
ctx.fillStyle = bgColor;
|
|
1953
|
+
ctx.beginPath();
|
|
1954
|
+
ctx.roundRect(badgeX, badgeY, badgeW, BADGE_H, BADGE_RADIUS);
|
|
1955
|
+
ctx.fill();
|
|
1956
|
+
ctx.strokeStyle = mono;
|
|
1957
|
+
ctx.globalAlpha = 0.3;
|
|
1958
|
+
ctx.lineWidth = 1;
|
|
1959
|
+
ctx.stroke();
|
|
1960
|
+
ctx.globalAlpha = 1;
|
|
1961
|
+
ctx.fillStyle = mono;
|
|
1962
|
+
ctx.globalAlpha = 0.85;
|
|
1963
|
+
ctx.textBaseline = "middle";
|
|
1964
|
+
ctx.textAlign = "left";
|
|
1965
|
+
ctx.fillText(text + expandIcon, badgeX + BADGE_PAD_X, badgeY + BADGE_H / 2);
|
|
1966
|
+
ctx.globalAlpha = 1;
|
|
1967
|
+
ctx.restore();
|
|
1968
|
+
clusterBadgeAreas.push({
|
|
1969
|
+
clusterId: cluster.id,
|
|
1970
|
+
x: badgeX,
|
|
1971
|
+
y: badgeY,
|
|
1972
|
+
w: badgeW,
|
|
1973
|
+
h: BADGE_H
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
function drawFanConnector(ctx, fanCenterY, realY, chartW, theme) {
|
|
1977
|
+
if (Math.abs(fanCenterY - realY) < 2) return;
|
|
1978
|
+
const mono = monoColor(theme);
|
|
1979
|
+
ctx.save();
|
|
1980
|
+
ctx.strokeStyle = mono;
|
|
1981
|
+
ctx.globalAlpha = 0.25;
|
|
1982
|
+
ctx.lineWidth = 1;
|
|
1983
|
+
ctx.setLineDash([2, 3]);
|
|
1984
|
+
const connectorX = chartW - 8;
|
|
1985
|
+
ctx.beginPath();
|
|
1986
|
+
ctx.moveTo(connectorX, fanCenterY);
|
|
1987
|
+
ctx.lineTo(connectorX, realY);
|
|
1988
|
+
ctx.stroke();
|
|
1989
|
+
ctx.setLineDash([]);
|
|
1990
|
+
ctx.beginPath();
|
|
1991
|
+
ctx.moveTo(connectorX - 4, realY);
|
|
1992
|
+
ctx.lineTo(connectorX + 4, realY);
|
|
1993
|
+
ctx.stroke();
|
|
1994
|
+
ctx.restore();
|
|
1737
1995
|
}
|
|
1738
1996
|
function drawDotHoverBox(ctx, level, y, dotX, dotR, theme, precision) {
|
|
1739
1997
|
const pl = level;
|
|
@@ -2364,6 +2622,152 @@ function hitTestTradeLevelAddBracket(x, y, areas) {
|
|
|
2364
2622
|
return areas.find((a) => x >= a.x && x <= a.x + a.w && y >= a.y && y <= a.y + a.h) ?? null;
|
|
2365
2623
|
}
|
|
2366
2624
|
|
|
2625
|
+
// src/overlays/OffViewportLevelIndicator.ts
|
|
2626
|
+
function collectOffViewportMarkers(levels, priceRange, draggingLabel) {
|
|
2627
|
+
const above = [];
|
|
2628
|
+
const below = [];
|
|
2629
|
+
const { min, max } = priceRange;
|
|
2630
|
+
for (const level of levels) {
|
|
2631
|
+
if (level.label === draggingLabel) continue;
|
|
2632
|
+
const isDraftMarket = level.type === "pending" && level.orderType === "market" && level.data?._isDraft === true;
|
|
2633
|
+
if (isDraftMarket) continue;
|
|
2634
|
+
if (isFinite(level.price)) {
|
|
2635
|
+
if (level.price > max) above.push({ price: level.price, label: level.label, kind: "entry" });
|
|
2636
|
+
else if (level.price < min) below.push({ price: level.price, label: level.label, kind: "entry" });
|
|
2637
|
+
}
|
|
2638
|
+
const slPrice = level.stopLossPrice;
|
|
2639
|
+
const tpPrice = level.takeProfitPrice;
|
|
2640
|
+
if (slPrice !== void 0 && isFinite(slPrice)) {
|
|
2641
|
+
if (slPrice > max) above.push({ price: slPrice, label: level.label, kind: "sl" });
|
|
2642
|
+
else if (slPrice < min) below.push({ price: slPrice, label: level.label, kind: "sl" });
|
|
2643
|
+
}
|
|
2644
|
+
if (tpPrice !== void 0 && isFinite(tpPrice)) {
|
|
2645
|
+
if (tpPrice > max) above.push({ price: tpPrice, label: level.label, kind: "tp" });
|
|
2646
|
+
else if (tpPrice < min) below.push({ price: tpPrice, label: level.label, kind: "tp" });
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
return { above, below };
|
|
2650
|
+
}
|
|
2651
|
+
function entryCount(markers) {
|
|
2652
|
+
let n = 0;
|
|
2653
|
+
for (const m of markers) if (m.kind === "entry") n++;
|
|
2654
|
+
return n;
|
|
2655
|
+
}
|
|
2656
|
+
function nearestEdgeMarker(markers, edge) {
|
|
2657
|
+
let best = markers[0];
|
|
2658
|
+
let bestDist = Math.abs(best.price - edge);
|
|
2659
|
+
for (let i = 1; i < markers.length; i++) {
|
|
2660
|
+
const d = Math.abs(markers[i].price - edge);
|
|
2661
|
+
if (d < bestDist) {
|
|
2662
|
+
best = markers[i];
|
|
2663
|
+
bestDist = d;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
return best;
|
|
2667
|
+
}
|
|
2668
|
+
var PILL_FONT = "11px 'Inter', system-ui, sans-serif";
|
|
2669
|
+
var PILL_PAD_X = 8;
|
|
2670
|
+
var PILL_RIGHT_MARGIN = 8;
|
|
2671
|
+
var PILL_V_MARGIN = 10;
|
|
2672
|
+
var PILL_H = 20;
|
|
2673
|
+
var PILL_RADIUS = 10;
|
|
2674
|
+
var ARROW_UP = "\u25B2";
|
|
2675
|
+
var ARROW_DOWN = "\u25BC";
|
|
2676
|
+
function renderOffViewportIndicators(ctx, levels, priceRange, theme, chartWidth, chartHeight, draggingLabel, selectedLabel = null) {
|
|
2677
|
+
const hitAreas = [];
|
|
2678
|
+
const PADDING_PX = 20;
|
|
2679
|
+
const usable = chartHeight - 2 * PADDING_PX;
|
|
2680
|
+
const span = priceRange.max - priceRange.min;
|
|
2681
|
+
const padInPrice = usable > 0 ? span / usable * PADDING_PX : 0;
|
|
2682
|
+
const visualRange = {
|
|
2683
|
+
min: priceRange.min - padInPrice,
|
|
2684
|
+
max: priceRange.max + padInPrice
|
|
2685
|
+
};
|
|
2686
|
+
const { above, below } = collectOffViewportMarkers(levels, visualRange, draggingLabel);
|
|
2687
|
+
if (above.length === 0 && below.length === 0) return { hitAreas };
|
|
2688
|
+
ctx.save();
|
|
2689
|
+
ctx.font = PILL_FONT;
|
|
2690
|
+
ctx.textBaseline = "middle";
|
|
2691
|
+
ctx.textAlign = "center";
|
|
2692
|
+
const PILL_GAP = 4;
|
|
2693
|
+
const renderPill = (direction, text, y, target) => {
|
|
2694
|
+
const textW = ctx.measureText(text).width;
|
|
2695
|
+
const w = Math.ceil(textW) + PILL_PAD_X * 2;
|
|
2696
|
+
const x = chartWidth - PILL_RIGHT_MARGIN - w;
|
|
2697
|
+
ctx.fillStyle = theme.tooltip.background;
|
|
2698
|
+
ctx.strokeStyle = theme.tooltip.border;
|
|
2699
|
+
ctx.lineWidth = 1;
|
|
2700
|
+
roundedRect(ctx, x, y, w, PILL_H, PILL_RADIUS);
|
|
2701
|
+
ctx.fill();
|
|
2702
|
+
ctx.stroke();
|
|
2703
|
+
ctx.fillStyle = theme.tooltip.text;
|
|
2704
|
+
ctx.fillText(text, x + w / 2, y + PILL_H / 2 + 0.5);
|
|
2705
|
+
hitAreas.push({
|
|
2706
|
+
direction,
|
|
2707
|
+
x,
|
|
2708
|
+
y,
|
|
2709
|
+
w,
|
|
2710
|
+
h: PILL_H,
|
|
2711
|
+
targetPrice: target.price,
|
|
2712
|
+
targetLabel: target.label,
|
|
2713
|
+
targetKind: target.kind
|
|
2714
|
+
});
|
|
2715
|
+
return w;
|
|
2716
|
+
};
|
|
2717
|
+
const drawColumn = (direction, markers, baseY) => {
|
|
2718
|
+
if (markers.length === 0) return;
|
|
2719
|
+
const arrow = direction === "up" ? ARROW_UP : ARROW_DOWN;
|
|
2720
|
+
const edge = direction === "up" ? priceRange.max : priceRange.min;
|
|
2721
|
+
const stackDir = direction === "up" ? 1 : -1;
|
|
2722
|
+
let nextY = baseY;
|
|
2723
|
+
const entries = entryCount(markers);
|
|
2724
|
+
if (entries > 0) {
|
|
2725
|
+
const target = nearestEdgeMarker(
|
|
2726
|
+
markers.filter((m) => m.kind === "entry"),
|
|
2727
|
+
edge
|
|
2728
|
+
);
|
|
2729
|
+
renderPill(direction, `${arrow} ${entries}`, nextY, target);
|
|
2730
|
+
nextY += stackDir * (PILL_H + PILL_GAP);
|
|
2731
|
+
}
|
|
2732
|
+
if (selectedLabel !== null) {
|
|
2733
|
+
const bracketMarkers = markers.filter(
|
|
2734
|
+
(m) => m.kind !== "entry" && m.label === selectedLabel
|
|
2735
|
+
);
|
|
2736
|
+
for (const bm of bracketMarkers) {
|
|
2737
|
+
const kindLabel = bm.kind.toUpperCase();
|
|
2738
|
+
renderPill(direction, `${arrow} ${kindLabel}`, nextY, bm);
|
|
2739
|
+
nextY += stackDir * (PILL_H + PILL_GAP);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
};
|
|
2743
|
+
drawColumn("up", above, PILL_V_MARGIN);
|
|
2744
|
+
drawColumn("down", below, chartHeight - PILL_V_MARGIN - PILL_H);
|
|
2745
|
+
ctx.restore();
|
|
2746
|
+
return { hitAreas };
|
|
2747
|
+
}
|
|
2748
|
+
function hitTestOffViewportIndicator(x, y, hitAreas) {
|
|
2749
|
+
return hitAreas.find((h) => x >= h.x && x <= h.x + h.w && y >= h.y && y <= h.y + h.h) ?? null;
|
|
2750
|
+
}
|
|
2751
|
+
function overlapsOffViewportPill(centerY, halfH, hitAreas) {
|
|
2752
|
+
const top = centerY - halfH;
|
|
2753
|
+
const bottom = centerY + halfH;
|
|
2754
|
+
return hitAreas.some((h) => top < h.y + h.h && bottom > h.y);
|
|
2755
|
+
}
|
|
2756
|
+
function roundedRect(ctx, x, y, w, h, r) {
|
|
2757
|
+
const rr = Math.min(r, w / 2, h / 2);
|
|
2758
|
+
ctx.beginPath();
|
|
2759
|
+
ctx.moveTo(x + rr, y);
|
|
2760
|
+
ctx.lineTo(x + w - rr, y);
|
|
2761
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + rr);
|
|
2762
|
+
ctx.lineTo(x + w, y + h - rr);
|
|
2763
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - rr, y + h);
|
|
2764
|
+
ctx.lineTo(x + rr, y + h);
|
|
2765
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - rr);
|
|
2766
|
+
ctx.lineTo(x, y + rr);
|
|
2767
|
+
ctx.quadraticCurveTo(x, y, x + rr, y);
|
|
2768
|
+
ctx.closePath();
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2367
2771
|
// src/axis/TimeAxis.ts
|
|
2368
2772
|
var COUNTDOWN_LABEL_HEIGHT = 18;
|
|
2369
2773
|
var COUNTDOWN_LABEL_PADDING_X = 6;
|
|
@@ -2478,10 +2882,20 @@ function renderCandleCountdown(ctx, lastBarTime, intervalMs, lastBarIndex, viewp
|
|
|
2478
2882
|
}
|
|
2479
2883
|
|
|
2480
2884
|
// src/axis/PriceAxis.ts
|
|
2481
|
-
function renderPriceAxis(ctx, priceRange, scale, theme, chartWidth, chartHeight, axisWidth, gridLines
|
|
2885
|
+
function renderPriceAxis(ctx, priceRange, scale, theme, chartWidth, chartHeight, axisWidth, gridLines, pricePrecision = 2, cfg = DEFAULT_PRICE_AXIS_CONFIG) {
|
|
2482
2886
|
ctx.fillStyle = theme.axisText;
|
|
2483
2887
|
ctx.font = `${cfg.fontSize} ${cfg.fontFamily}`;
|
|
2484
2888
|
ctx.textAlign = "left";
|
|
2889
|
+
if (gridLines === void 0) {
|
|
2890
|
+
const ticks = generatePriceTicks(priceRange, chartHeight);
|
|
2891
|
+
const step2 = ticks.length >= 2 ? ticks[1] - ticks[0] : priceRange.max - priceRange.min;
|
|
2892
|
+
const precision = precisionForStep(step2, pricePrecision);
|
|
2893
|
+
for (const price of ticks) {
|
|
2894
|
+
const y = scale.yToPixel(price, priceRange, chartHeight);
|
|
2895
|
+
ctx.fillText(price.toFixed(precision), chartWidth + 4, y + 4);
|
|
2896
|
+
}
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2485
2899
|
const step = (priceRange.max - priceRange.min) / gridLines;
|
|
2486
2900
|
for (let i = 0; i <= gridLines; i++) {
|
|
2487
2901
|
const price = priceRange.min + step * i;
|
|
@@ -3096,6 +3510,8 @@ var ICON_TOOL = {
|
|
|
3096
3510
|
14
|
|
3097
3511
|
)
|
|
3098
3512
|
};
|
|
3513
|
+
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>`;
|
|
3514
|
+
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>`;
|
|
3099
3515
|
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>`;
|
|
3100
3516
|
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>`;
|
|
3101
3517
|
var ICON_TAB = {
|
|
@@ -3131,7 +3547,7 @@ function injectFlyoutScrollbarStyles() {
|
|
|
3131
3547
|
document.head.appendChild(style);
|
|
3132
3548
|
}
|
|
3133
3549
|
var TopBar = class {
|
|
3134
|
-
constructor(onSeriesChange, onTimeframeChange, onAddIndicator, onDurationChange, onSettingsClick, onFullscreenToggle, onDrawingToolsToggle, showDrawingToolsBtn, showSettingsBtn, initialSeries, initialTf, initialDuration, durationMap, theme, cfg = DEFAULT_TOP_BAR_CONFIG, labels = DEFAULT_LABELS.topBar) {
|
|
3550
|
+
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) {
|
|
3135
3551
|
this.onSeriesChange = onSeriesChange;
|
|
3136
3552
|
this.onTimeframeChange = onTimeframeChange;
|
|
3137
3553
|
this.onAddIndicator = onAddIndicator;
|
|
@@ -3139,11 +3555,14 @@ var TopBar = class {
|
|
|
3139
3555
|
this.onSettingsClick = onSettingsClick;
|
|
3140
3556
|
this.onFullscreenToggle = onFullscreenToggle;
|
|
3141
3557
|
this.onDrawingToolsToggle = onDrawingToolsToggle;
|
|
3558
|
+
this.onTfcToggle = onTfcToggle;
|
|
3142
3559
|
this.settingsBtn = null;
|
|
3143
3560
|
this.isFullscreen = false;
|
|
3144
3561
|
this.durationBtns = /* @__PURE__ */ new Map();
|
|
3145
3562
|
this.isMobileLayout = false;
|
|
3146
3563
|
this.drawingBtn = null;
|
|
3564
|
+
this.tfcBtn = null;
|
|
3565
|
+
this.tfcActive = true;
|
|
3147
3566
|
this.openDropdown = null;
|
|
3148
3567
|
this.indicatorOptions = [];
|
|
3149
3568
|
this.activeIndicators = /* @__PURE__ */ new Set();
|
|
@@ -3151,6 +3570,7 @@ var TopBar = class {
|
|
|
3151
3570
|
this.theme = theme;
|
|
3152
3571
|
this.cfg = cfg;
|
|
3153
3572
|
this.labels = labels;
|
|
3573
|
+
this.tfcActive = initialTfcActive;
|
|
3154
3574
|
this.currentSeries = initialSeries;
|
|
3155
3575
|
this.currentTf = initialTf;
|
|
3156
3576
|
this.activeDuration = initialDuration;
|
|
@@ -3227,6 +3647,21 @@ var TopBar = class {
|
|
|
3227
3647
|
this.addBtnHover(this.settingsBtn);
|
|
3228
3648
|
this.rightGroup.appendChild(this.settingsBtn);
|
|
3229
3649
|
}
|
|
3650
|
+
if (showTfcBtn) {
|
|
3651
|
+
this.tfcBtn = document.createElement("button");
|
|
3652
|
+
this.tfcBtn.title = this.labels.tfcToggleTitle;
|
|
3653
|
+
this.tfcBtn.innerHTML = this.tfcActive ? ICON_TFC : ICON_TFC_OFF;
|
|
3654
|
+
this.applyIconBtnStyles(this.tfcBtn, theme, this.tfcActive);
|
|
3655
|
+
this.tfcBtn.addEventListener("click", (e) => {
|
|
3656
|
+
e.stopPropagation();
|
|
3657
|
+
this.tfcActive = !this.tfcActive;
|
|
3658
|
+
this.tfcBtn.innerHTML = this.tfcActive ? ICON_TFC : ICON_TFC_OFF;
|
|
3659
|
+
this.applyIconBtnStyles(this.tfcBtn, this.theme, this.tfcActive);
|
|
3660
|
+
this.onTfcToggle?.(this.tfcActive);
|
|
3661
|
+
});
|
|
3662
|
+
this.addBtnHover(this.tfcBtn);
|
|
3663
|
+
this.rightGroup.appendChild(this.tfcBtn);
|
|
3664
|
+
}
|
|
3230
3665
|
this.el.append(this.leftGroup, this.rightGroup);
|
|
3231
3666
|
this.docClickHandler = (e) => {
|
|
3232
3667
|
if (!this.el.contains(e.target) && !this.flyout.contains(e.target)) {
|
|
@@ -3256,6 +3691,15 @@ var TopBar = class {
|
|
|
3256
3691
|
if (!this.drawingBtn) return;
|
|
3257
3692
|
this.applyIconBtnStyles(this.drawingBtn, this.theme, active);
|
|
3258
3693
|
}
|
|
3694
|
+
setTfcActive(active) {
|
|
3695
|
+
if (!this.tfcBtn) return;
|
|
3696
|
+
this.tfcActive = active;
|
|
3697
|
+
this.tfcBtn.innerHTML = active ? ICON_TFC : ICON_TFC_OFF;
|
|
3698
|
+
this.applyIconBtnStyles(this.tfcBtn, this.theme, active);
|
|
3699
|
+
if (this.isMobileLayout) {
|
|
3700
|
+
this.resizeSvg(this.tfcBtn, this.getSvgSize(this.tfcBtn) + 2);
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3259
3703
|
setTheme(theme) {
|
|
3260
3704
|
this.theme = theme;
|
|
3261
3705
|
this.closeDropdown();
|
|
@@ -3264,6 +3708,7 @@ var TopBar = class {
|
|
|
3264
3708
|
if (this.drawingBtn) this.applyIconBtnStyles(this.drawingBtn, theme, false);
|
|
3265
3709
|
this.applyIconBtnStyles(this.fullscreenBtn, theme, this.isFullscreen);
|
|
3266
3710
|
if (this.settingsBtn) this.applyIconBtnStyles(this.settingsBtn, theme, false);
|
|
3711
|
+
if (this.tfcBtn) this.applyIconBtnStyles(this.tfcBtn, theme, this.tfcActive);
|
|
3267
3712
|
this.applySeriesBtnStyles(this.seriesBtn, theme, false);
|
|
3268
3713
|
this.applyDropBtnStyles(this.tfBtn, theme, false);
|
|
3269
3714
|
this.applyDropBtnStyles(this.indBtn, theme, false);
|
|
@@ -3304,7 +3749,7 @@ var TopBar = class {
|
|
|
3304
3749
|
this.durationSep.style.display = btnDisplay;
|
|
3305
3750
|
this.durationDropBtn.style.display = isMobile ? "" : "none";
|
|
3306
3751
|
const containerSize = isMobile ? this.cfg.mobileIconBtnSize : this.cfg.iconBtnSize;
|
|
3307
|
-
for (const btn of [this.fullscreenBtn, this.settingsBtn, this.drawingBtn]) {
|
|
3752
|
+
for (const btn of [this.fullscreenBtn, this.settingsBtn, this.drawingBtn, this.tfcBtn]) {
|
|
3308
3753
|
if (!btn) continue;
|
|
3309
3754
|
btn.style.width = containerSize;
|
|
3310
3755
|
btn.style.height = containerSize;
|
|
@@ -3314,6 +3759,9 @@ var TopBar = class {
|
|
|
3314
3759
|
if (this.settingsBtn) {
|
|
3315
3760
|
this.resizeSvg(this.settingsBtn, this.getSvgSize(this.settingsBtn) + delta);
|
|
3316
3761
|
}
|
|
3762
|
+
if (this.tfcBtn) {
|
|
3763
|
+
this.resizeSvg(this.tfcBtn, this.getSvgSize(this.tfcBtn) + delta);
|
|
3764
|
+
}
|
|
3317
3765
|
if (this.drawingBtn) {
|
|
3318
3766
|
const drawIconSize = isMobile ? parseInt(this.cfg.mobileDrawBtnIconSize, 10) : parseInt(this.cfg.drawBtnIconFontSize, 10);
|
|
3319
3767
|
this.resizeSvg(this.drawingBtn, drawIconSize);
|
|
@@ -8142,7 +8590,7 @@ function pointToSegmentDist19(px, py, x1, y1, x2, y2) {
|
|
|
8142
8590
|
const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lenSq));
|
|
8143
8591
|
return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy));
|
|
8144
8592
|
}
|
|
8145
|
-
function
|
|
8593
|
+
function roundedRect2(ctx, x, y, w, h, r) {
|
|
8146
8594
|
ctx.beginPath();
|
|
8147
8595
|
ctx.moveTo(x + r, y);
|
|
8148
8596
|
ctx.lineTo(x + w - r, y);
|
|
@@ -8193,7 +8641,7 @@ var Callout = class {
|
|
|
8193
8641
|
ctx.beginPath();
|
|
8194
8642
|
ctx.arc(tipX, tipY, 3, 0, Math.PI * 2);
|
|
8195
8643
|
ctx.fill();
|
|
8196
|
-
|
|
8644
|
+
roundedRect2(ctx, bx, by, boxW, BOX_H, RADIUS);
|
|
8197
8645
|
ctx.fillStyle = "#37474f";
|
|
8198
8646
|
ctx.fill();
|
|
8199
8647
|
ctx.strokeStyle = "#546e7a";
|
|
@@ -8245,7 +8693,7 @@ var Callout = class {
|
|
|
8245
8693
|
// src/drawings/PriceLabel.ts
|
|
8246
8694
|
var HIT_TOLERANCE42 = 6;
|
|
8247
8695
|
var BADGE_W = 60;
|
|
8248
|
-
var
|
|
8696
|
+
var BADGE_H2 = 18;
|
|
8249
8697
|
var ARROW_W = 7;
|
|
8250
8698
|
var PriceLabel = class {
|
|
8251
8699
|
constructor(point) {
|
|
@@ -8262,21 +8710,21 @@ var PriceLabel = class {
|
|
|
8262
8710
|
ctx.fillStyle = "#26a69a";
|
|
8263
8711
|
ctx.beginPath();
|
|
8264
8712
|
ctx.moveTo(x, y);
|
|
8265
|
-
ctx.lineTo(x + ARROW_W, y -
|
|
8266
|
-
ctx.lineTo(x + ARROW_W, y +
|
|
8713
|
+
ctx.lineTo(x + ARROW_W, y - BADGE_H2 / 2);
|
|
8714
|
+
ctx.lineTo(x + ARROW_W, y + BADGE_H2 / 2);
|
|
8267
8715
|
ctx.closePath();
|
|
8268
8716
|
ctx.fill();
|
|
8269
8717
|
const bx = x + ARROW_W;
|
|
8270
|
-
const by = y -
|
|
8718
|
+
const by = y - BADGE_H2 / 2;
|
|
8271
8719
|
ctx.fillStyle = "#26a69a";
|
|
8272
8720
|
ctx.beginPath();
|
|
8273
|
-
ctx.roundRect(bx, by, BADGE_W,
|
|
8721
|
+
ctx.roundRect(bx, by, BADGE_W, BADGE_H2, BADGE_H2 / 2);
|
|
8274
8722
|
ctx.fill();
|
|
8275
8723
|
if (isSelected) {
|
|
8276
8724
|
ctx.strokeStyle = "#80cbc4";
|
|
8277
8725
|
ctx.lineWidth = 1.5;
|
|
8278
8726
|
ctx.beginPath();
|
|
8279
|
-
ctx.roundRect(bx - 2, by - 2, BADGE_W + 4,
|
|
8727
|
+
ctx.roundRect(bx - 2, by - 2, BADGE_W + 4, BADGE_H2 + 4, BADGE_H2 / 2 + 2);
|
|
8280
8728
|
ctx.stroke();
|
|
8281
8729
|
}
|
|
8282
8730
|
ctx.font = "11px 'Inter', system-ui, sans-serif";
|
|
@@ -8290,8 +8738,8 @@ var PriceLabel = class {
|
|
|
8290
8738
|
const ax = scale.xToPixel(this.points[0].barIndex, viewport, chartW);
|
|
8291
8739
|
const ay = scale.yToPixel(this.points[0].price, priceRange, chartH);
|
|
8292
8740
|
const bx = ax + ARROW_W;
|
|
8293
|
-
const by = ay -
|
|
8294
|
-
return x >= ax - HIT_TOLERANCE42 && x <= bx + BADGE_W + HIT_TOLERANCE42 && y >= by - HIT_TOLERANCE42 && y <= by +
|
|
8741
|
+
const by = ay - BADGE_H2 / 2;
|
|
8742
|
+
return x >= ax - HIT_TOLERANCE42 && x <= bx + BADGE_W + HIT_TOLERANCE42 && y >= by - HIT_TOLERANCE42 && y <= by + BADGE_H2 + HIT_TOLERANCE42;
|
|
8295
8743
|
}
|
|
8296
8744
|
getHandle() {
|
|
8297
8745
|
return null;
|
|
@@ -8411,7 +8859,7 @@ var Flag = class {
|
|
|
8411
8859
|
|
|
8412
8860
|
// src/drawings/PriceNote.ts
|
|
8413
8861
|
var HIT_TOLERANCE44 = 6;
|
|
8414
|
-
var
|
|
8862
|
+
var BADGE_H3 = 16;
|
|
8415
8863
|
var BADGE_PADDING_X = 6;
|
|
8416
8864
|
var TICK_HALF = 10;
|
|
8417
8865
|
var PriceNote = class {
|
|
@@ -8437,10 +8885,10 @@ var PriceNote = class {
|
|
|
8437
8885
|
const tw = ctx.measureText(label).width;
|
|
8438
8886
|
const badgeW = tw + BADGE_PADDING_X * 2;
|
|
8439
8887
|
const bx = x + TICK_HALF + 2;
|
|
8440
|
-
const by = y -
|
|
8888
|
+
const by = y - BADGE_H3 / 2;
|
|
8441
8889
|
ctx.fillStyle = "#ffa726";
|
|
8442
8890
|
ctx.beginPath();
|
|
8443
|
-
ctx.roundRect(bx, by, badgeW,
|
|
8891
|
+
ctx.roundRect(bx, by, badgeW, BADGE_H3, 3);
|
|
8444
8892
|
ctx.fill();
|
|
8445
8893
|
ctx.fillStyle = "#ffffff";
|
|
8446
8894
|
ctx.textBaseline = "middle";
|
|
@@ -8460,7 +8908,7 @@ var PriceNote = class {
|
|
|
8460
8908
|
|
|
8461
8909
|
// src/drawings/AnchoredNote.ts
|
|
8462
8910
|
var HIT_TOLERANCE45 = 6;
|
|
8463
|
-
var
|
|
8911
|
+
var BADGE_H4 = 20;
|
|
8464
8912
|
var PAD_H2 = 10;
|
|
8465
8913
|
function pointToSegmentDist21(px, py, x1, y1, x2, y2) {
|
|
8466
8914
|
const dx = x2 - x1, dy = y2 - y1;
|
|
@@ -8493,7 +8941,7 @@ var AnchoredNote = class {
|
|
|
8493
8941
|
ctx.lineWidth = isSelected ? 1.5 : 1;
|
|
8494
8942
|
const badgeW = this.getBadgeW(ctx);
|
|
8495
8943
|
const bx = x2 - badgeW / 2;
|
|
8496
|
-
const by = y2 -
|
|
8944
|
+
const by = y2 - BADGE_H4 / 2;
|
|
8497
8945
|
ctx.strokeStyle = "#42a5f5";
|
|
8498
8946
|
ctx.setLineDash([4, 3]);
|
|
8499
8947
|
ctx.beginPath();
|
|
@@ -8507,12 +8955,12 @@ var AnchoredNote = class {
|
|
|
8507
8955
|
ctx.fill();
|
|
8508
8956
|
ctx.fillStyle = "#37474f";
|
|
8509
8957
|
ctx.beginPath();
|
|
8510
|
-
ctx.roundRect(bx, by, badgeW,
|
|
8958
|
+
ctx.roundRect(bx, by, badgeW, BADGE_H4, 4);
|
|
8511
8959
|
ctx.fill();
|
|
8512
8960
|
ctx.strokeStyle = "#42a5f5";
|
|
8513
8961
|
ctx.lineWidth = isSelected ? 1.5 : 1;
|
|
8514
8962
|
ctx.beginPath();
|
|
8515
|
-
ctx.roundRect(bx, by, badgeW,
|
|
8963
|
+
ctx.roundRect(bx, by, badgeW, BADGE_H4, 4);
|
|
8516
8964
|
ctx.stroke();
|
|
8517
8965
|
const label = this.text || "Note";
|
|
8518
8966
|
const isPlaceholder = !this.text;
|
|
@@ -8531,8 +8979,8 @@ var AnchoredNote = class {
|
|
|
8531
8979
|
const y2 = scale.yToPixel(p2.price, priceRange, chartH);
|
|
8532
8980
|
const approxBadgeW = Math.max(50, (this.text?.length ?? 4) * 7 + 20);
|
|
8533
8981
|
const bx = x2 - approxBadgeW / 2;
|
|
8534
|
-
const by = y2 -
|
|
8535
|
-
if (x >= bx - HIT_TOLERANCE45 && x <= bx + approxBadgeW + HIT_TOLERANCE45 && y >= by - HIT_TOLERANCE45 && y <= by +
|
|
8982
|
+
const by = y2 - BADGE_H4 / 2;
|
|
8983
|
+
if (x >= bx - HIT_TOLERANCE45 && x <= bx + approxBadgeW + HIT_TOLERANCE45 && y >= by - HIT_TOLERANCE45 && y <= by + BADGE_H4 + HIT_TOLERANCE45) {
|
|
8536
8984
|
return true;
|
|
8537
8985
|
}
|
|
8538
8986
|
if (pointToSegmentDist21(x, y, x1, y1, x2, y2) <= HIT_TOLERANCE45) {
|
|
@@ -12693,9 +13141,14 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12693
13141
|
this._isDraggingBracket = false;
|
|
12694
13142
|
// Main line drag state
|
|
12695
13143
|
this._level = null;
|
|
12696
|
-
this._startY = 0;
|
|
12697
13144
|
this._startPrice = 0;
|
|
12698
13145
|
this._currentPrice = 0;
|
|
13146
|
+
// Pixel offset between click Y and the level's rendered Y at drag start.
|
|
13147
|
+
// Why: the drag hit-strip is 8 px tall; without this, the cursor-to-price
|
|
13148
|
+
// mapping would snap the level to wherever the cursor is instead of
|
|
13149
|
+
// preserving the grab point. Also immunises the drag from any priceRange
|
|
13150
|
+
// animation (lerp) that happens between drag-start and first mousemove.
|
|
13151
|
+
this._pixelOffset = 0;
|
|
12699
13152
|
// Entry-price constraint bounds (applied when SL/TP exist on the level)
|
|
12700
13153
|
this._mainMinPrice = -Infinity;
|
|
12701
13154
|
this._mainMaxPrice = Infinity;
|
|
@@ -12703,6 +13156,7 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12703
13156
|
this._bracketType = null;
|
|
12704
13157
|
this._bracketStartPrice = 0;
|
|
12705
13158
|
this._bracketCurrentPrice = 0;
|
|
13159
|
+
this._bracketPixelOffset = 0;
|
|
12706
13160
|
// Bracket constraint state (populated on startBracketDrag)
|
|
12707
13161
|
this._bracketParentPrice = 0;
|
|
12708
13162
|
this._bracketParentSide = "buy";
|
|
@@ -12714,11 +13168,11 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12714
13168
|
* Pass `slPrice` / `tpPrice` / `side` to constrain the drag between SL and TP
|
|
12715
13169
|
* (used for ES/EL entry orders where the entry price must stay between brackets).
|
|
12716
13170
|
*/
|
|
12717
|
-
startDrag(level,
|
|
13171
|
+
startDrag(level, clickY, levelPixelY, startPrice, slPrice, tpPrice, side, marketPrice, orderType) {
|
|
12718
13172
|
this._active = true;
|
|
12719
13173
|
this._isDraggingBracket = false;
|
|
12720
13174
|
this._level = level;
|
|
12721
|
-
this.
|
|
13175
|
+
this._pixelOffset = clickY - levelPixelY;
|
|
12722
13176
|
this._startPrice = startPrice;
|
|
12723
13177
|
this._currentPrice = startPrice;
|
|
12724
13178
|
this._bracketType = null;
|
|
@@ -12750,13 +13204,14 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12750
13204
|
* @param bracketType 'stopLoss' | 'takeProfit'
|
|
12751
13205
|
* @param startPrice Current bracket price
|
|
12752
13206
|
*/
|
|
12753
|
-
startBracketDrag(parentLevel, bracketType, startPrice, currentMarketPrice = 0, parentPriceOverride) {
|
|
13207
|
+
startBracketDrag(parentLevel, bracketType, startPrice, clickY, bracketPixelY, currentMarketPrice = 0, parentPriceOverride) {
|
|
12754
13208
|
this._active = true;
|
|
12755
13209
|
this._isDraggingBracket = true;
|
|
12756
13210
|
this._level = parentLevel;
|
|
12757
13211
|
this._bracketType = bracketType;
|
|
12758
13212
|
this._bracketStartPrice = startPrice;
|
|
12759
13213
|
this._bracketCurrentPrice = startPrice;
|
|
13214
|
+
this._bracketPixelOffset = clickY - bracketPixelY;
|
|
12760
13215
|
this._bracketParentPrice = parentPriceOverride ?? parentLevel.price;
|
|
12761
13216
|
this._bracketParentSide = parentLevel.side ?? "buy";
|
|
12762
13217
|
this._bracketMarketPrice = currentMarketPrice;
|
|
@@ -12767,9 +13222,10 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12767
13222
|
*/
|
|
12768
13223
|
onMouseMove(currentY, scale, priceRange, priceH) {
|
|
12769
13224
|
if (!this._active || !this._level) return null;
|
|
12770
|
-
const rawPrice = scale.pixelToPrice(currentY, priceRange, priceH);
|
|
12771
13225
|
if (this._isDraggingBracket && this._bracketType !== null) {
|
|
12772
|
-
const
|
|
13226
|
+
const effectiveY2 = currentY - this._bracketPixelOffset;
|
|
13227
|
+
const rawPrice2 = scale.pixelToPrice(effectiveY2, priceRange, priceH);
|
|
13228
|
+
const clampedPrice = this._clampBracketPrice(rawPrice2, this._bracketType, this._bracketParentPrice, this._bracketParentSide, this._bracketMarketPrice);
|
|
12773
13229
|
this._bracketCurrentPrice = clampedPrice;
|
|
12774
13230
|
return {
|
|
12775
13231
|
newPrice: clampedPrice,
|
|
@@ -12777,6 +13233,8 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12777
13233
|
isBracket: true
|
|
12778
13234
|
};
|
|
12779
13235
|
}
|
|
13236
|
+
const effectiveY = currentY - this._pixelOffset;
|
|
13237
|
+
const rawPrice = scale.pixelToPrice(effectiveY, priceRange, priceH);
|
|
12780
13238
|
const constrained = Math.max(this._mainMinPrice, Math.min(this._mainMaxPrice, rawPrice));
|
|
12781
13239
|
this._currentPrice = constrained;
|
|
12782
13240
|
return {
|
|
@@ -12875,10 +13333,12 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12875
13333
|
this._level = null;
|
|
12876
13334
|
this._bracketType = null;
|
|
12877
13335
|
this._currentPrice = 0;
|
|
13336
|
+
this._pixelOffset = 0;
|
|
12878
13337
|
this._mainMinPrice = -Infinity;
|
|
12879
13338
|
this._mainMaxPrice = Infinity;
|
|
12880
13339
|
this._bracketStartPrice = 0;
|
|
12881
13340
|
this._bracketCurrentPrice = 0;
|
|
13341
|
+
this._bracketPixelOffset = 0;
|
|
12882
13342
|
this._bracketParentPrice = 0;
|
|
12883
13343
|
this._bracketParentSide = "buy";
|
|
12884
13344
|
this._bracketMarketPrice = 0;
|
|
@@ -16420,6 +16880,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16420
16880
|
this.showBidAskLines = false;
|
|
16421
16881
|
this.showActLogo = false;
|
|
16422
16882
|
this.hideLevelConfirmCancel = false;
|
|
16883
|
+
this.levelClusteringEnabled = true;
|
|
16884
|
+
this.clusterThresholdDistance = 20;
|
|
16423
16885
|
this.enableThemeToggle = false;
|
|
16424
16886
|
this.activeIndicators = [];
|
|
16425
16887
|
this.indicatorResults = /* @__PURE__ */ new Map();
|
|
@@ -16493,11 +16955,16 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16493
16955
|
// Trade / position levels
|
|
16494
16956
|
this.levels = [];
|
|
16495
16957
|
this.tradeLevelHitAreas = [];
|
|
16958
|
+
this.offViewportHitAreas = [];
|
|
16496
16959
|
this.tradeLevelEditAreas = [];
|
|
16497
16960
|
this.tradeHoverAreas = [];
|
|
16498
16961
|
this.tradeLevelDragAreas = [];
|
|
16499
16962
|
this.tradeLevelConfirmAreas = [];
|
|
16500
16963
|
this.tradeAddBracketAreas = [];
|
|
16964
|
+
this.clusterBadgeAreas = [];
|
|
16965
|
+
this.lastClusters = [];
|
|
16966
|
+
/** Set of cluster IDs that are currently expanded (fanned out). */
|
|
16967
|
+
this.expandedClusters = /* @__PURE__ */ new Set();
|
|
16501
16968
|
this.hoveredTradeLabel = null;
|
|
16502
16969
|
this.selectedTradeLabel = null;
|
|
16503
16970
|
/** Estimated PNL text for draft order SL/TP bracket lines, set by setDraftBracketPnl(). */
|
|
@@ -16512,6 +16979,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16512
16979
|
// Inline drag confirmation — accumulates changes per level until user confirms
|
|
16513
16980
|
this.pendingLevelDrags = /* @__PURE__ */ new Map();
|
|
16514
16981
|
// Trade from Charts settings
|
|
16982
|
+
this.tfcEnabled = true;
|
|
16983
|
+
this.tfcActive = true;
|
|
16515
16984
|
this.tradeDisplayFilter = "all";
|
|
16516
16985
|
this.positionRenderStyle = "dot";
|
|
16517
16986
|
this.tradesThresholdForHorizontalLine = 2;
|
|
@@ -16522,7 +16991,9 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16522
16991
|
this.dataLoader = null;
|
|
16523
16992
|
this.isFetching = false;
|
|
16524
16993
|
this.noMoreData = false;
|
|
16994
|
+
this._loadGeneration = 0;
|
|
16525
16995
|
this.maxCandles = 200;
|
|
16996
|
+
this.prefetchThreshold = 80;
|
|
16526
16997
|
this.defaultVisibleBars = DEFAULT_VISIBLE_BARS;
|
|
16527
16998
|
this.defaultMobileVisibleBars = void 0;
|
|
16528
16999
|
this.mobileBarDivisor = 2;
|
|
@@ -16560,7 +17031,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16560
17031
|
if (this.tradeButton) {
|
|
16561
17032
|
if (isInChartArea && y >= 0 && y <= priceH && !this._isTradingActive && !hoveringDotLevel) {
|
|
16562
17033
|
this.tradeButton.setY(y);
|
|
16563
|
-
this.tradeButton.
|
|
17034
|
+
const btnHalfH = this.uiConfig.tradeButton.size / 2;
|
|
17035
|
+
if (overlapsOffViewportPill(y, btnHalfH, this.offViewportHitAreas)) {
|
|
17036
|
+
this.tradeButton.hide();
|
|
17037
|
+
} else if (this.tfcActive) {
|
|
17038
|
+
this.tradeButton.show();
|
|
17039
|
+
}
|
|
16564
17040
|
this._tradeButtonClickData = { price, y };
|
|
16565
17041
|
const allBars = this.dataStore.all;
|
|
16566
17042
|
const lastClose = allBars.length > 0 ? allBars[allBars.length - 1].close : price;
|
|
@@ -16662,7 +17138,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16662
17138
|
return;
|
|
16663
17139
|
}
|
|
16664
17140
|
if (!this.drawingManager.getActiveTool()) {
|
|
16665
|
-
if (hitTestTradeLevelClose(x, y, this.tradeLevelHitAreas) || hitTestTradeLevelEdit(x, y, this.tradeLevelEditAreas) || hitTestTradeLevelConfirm(x, y, this.tradeLevelConfirmAreas)) {
|
|
17141
|
+
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)) {
|
|
16666
17142
|
this.drawCanvas.style.cursor = "pointer";
|
|
16667
17143
|
this.scheduleRender();
|
|
16668
17144
|
return;
|
|
@@ -16874,6 +17350,25 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16874
17350
|
this.scheduleRender();
|
|
16875
17351
|
return;
|
|
16876
17352
|
}
|
|
17353
|
+
const clusterHitTouch = hitTestClusterBadge(x, y, this.clusterBadgeAreas);
|
|
17354
|
+
if (clusterHitTouch) {
|
|
17355
|
+
e.preventDefault();
|
|
17356
|
+
this.panHandler.cancel();
|
|
17357
|
+
if (this.expandedClusters.has(clusterHitTouch.clusterId)) {
|
|
17358
|
+
this.expandedClusters.delete(clusterHitTouch.clusterId);
|
|
17359
|
+
} else {
|
|
17360
|
+
this.expandedClusters.add(clusterHitTouch.clusterId);
|
|
17361
|
+
}
|
|
17362
|
+
this.renderTradeLayer();
|
|
17363
|
+
return;
|
|
17364
|
+
}
|
|
17365
|
+
const offVpHit = hitTestOffViewportIndicator(x, y, this.offViewportHitAreas);
|
|
17366
|
+
if (offVpHit) {
|
|
17367
|
+
e.preventDefault();
|
|
17368
|
+
this.panHandler.cancel();
|
|
17369
|
+
this.scrollToCenterPrice(offVpHit.targetPrice);
|
|
17370
|
+
return;
|
|
17371
|
+
}
|
|
16877
17372
|
const confirmHit = hitTestTradeLevelConfirm(x, y, this.tradeLevelConfirmAreas);
|
|
16878
17373
|
if (confirmHit) {
|
|
16879
17374
|
e.preventDefault();
|
|
@@ -16930,6 +17425,10 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16930
17425
|
}
|
|
16931
17426
|
this.drawingManager.selectDrawing(null);
|
|
16932
17427
|
this.hideMobileDrawingFlyout();
|
|
17428
|
+
if (this.expandedClusters.size > 0) {
|
|
17429
|
+
this.expandedClusters.clear();
|
|
17430
|
+
this.renderTradeLayer();
|
|
17431
|
+
}
|
|
16933
17432
|
const isInChartArea = x < chartW && y < timeAxisY;
|
|
16934
17433
|
if (isInChartArea) {
|
|
16935
17434
|
if (this._mobileCrosshairActive) {
|
|
@@ -17079,7 +17578,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17079
17578
|
if (this._isTradingActive) {
|
|
17080
17579
|
this.tradeButton.hide();
|
|
17081
17580
|
} else {
|
|
17082
|
-
this.tradeButton.
|
|
17581
|
+
const _btnHalfH2 = this.uiConfig.tradeButton.size / 2;
|
|
17582
|
+
if (overlapsOffViewportPill(y, _btnHalfH2, this.offViewportHitAreas)) {
|
|
17583
|
+
this.tradeButton.hide();
|
|
17584
|
+
} else if (this.tfcActive) {
|
|
17585
|
+
this.tradeButton.show();
|
|
17586
|
+
}
|
|
17083
17587
|
}
|
|
17084
17588
|
this._tradeButtonClickData = { price, y };
|
|
17085
17589
|
const _allBars = this.dataStore.all;
|
|
@@ -17161,6 +17665,21 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17161
17665
|
const rawBarIndex = this.scaleManager.pixelToBarIndex(x, this.viewport, chartW);
|
|
17162
17666
|
const price = this.scaleManager.pixelToPrice(y, priceRange, priceH);
|
|
17163
17667
|
const barIndex = Math.round(rawBarIndex);
|
|
17668
|
+
const clusterHit = hitTestClusterBadge(x, y, this.clusterBadgeAreas);
|
|
17669
|
+
if (clusterHit) {
|
|
17670
|
+
if (this.expandedClusters.has(clusterHit.clusterId)) {
|
|
17671
|
+
this.expandedClusters.delete(clusterHit.clusterId);
|
|
17672
|
+
} else {
|
|
17673
|
+
this.expandedClusters.add(clusterHit.clusterId);
|
|
17674
|
+
}
|
|
17675
|
+
this.renderTradeLayer();
|
|
17676
|
+
return;
|
|
17677
|
+
}
|
|
17678
|
+
const offVpHit = hitTestOffViewportIndicator(x, y, this.offViewportHitAreas);
|
|
17679
|
+
if (offVpHit) {
|
|
17680
|
+
this.scrollToCenterPrice(offVpHit.targetPrice);
|
|
17681
|
+
return;
|
|
17682
|
+
}
|
|
17164
17683
|
const confirmHit = hitTestTradeLevelConfirm(x, y, this.tradeLevelConfirmAreas);
|
|
17165
17684
|
if (confirmHit) {
|
|
17166
17685
|
if (confirmHit.action === "confirm") this.applyPendingChanges(confirmHit.label);
|
|
@@ -17245,6 +17764,10 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17245
17764
|
this.renderTradeLayer();
|
|
17246
17765
|
}
|
|
17247
17766
|
}
|
|
17767
|
+
if (this.expandedClusters.size > 0) {
|
|
17768
|
+
this.expandedClusters.clear();
|
|
17769
|
+
this.renderTradeLayer();
|
|
17770
|
+
}
|
|
17248
17771
|
const timeAxisY = rect.height - this.timeAxisHeight;
|
|
17249
17772
|
if (x >= chartW) {
|
|
17250
17773
|
if (y <= priceH) {
|
|
@@ -17360,6 +17883,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17360
17883
|
this.scheduleRender();
|
|
17361
17884
|
};
|
|
17362
17885
|
this.onKeyDown = (e) => {
|
|
17886
|
+
if (e.key === "Escape" && this.expandedClusters.size > 0) {
|
|
17887
|
+
this.expandedClusters.clear();
|
|
17888
|
+
this.renderTradeLayer();
|
|
17889
|
+
e.preventDefault();
|
|
17890
|
+
return;
|
|
17891
|
+
}
|
|
17363
17892
|
if (e.key === "Escape" && this.pendingLevelDrags.size > 0) {
|
|
17364
17893
|
this.revertPendingChanges();
|
|
17365
17894
|
e.preventDefault();
|
|
@@ -17446,7 +17975,9 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17446
17975
|
durationTimeframeMap,
|
|
17447
17976
|
uiConfig,
|
|
17448
17977
|
labels,
|
|
17449
|
-
tickActivityMs = 3e4
|
|
17978
|
+
tickActivityMs = 3e4,
|
|
17979
|
+
hideSymbolAndTick = false,
|
|
17980
|
+
showBottomBar = false
|
|
17450
17981
|
} = config;
|
|
17451
17982
|
this.dataLoader = dataLoader ?? null;
|
|
17452
17983
|
this.drawingToolsEnabled = config.showDrawingTools !== false;
|
|
@@ -17455,6 +17986,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17455
17986
|
this.labels = resolveLabels(labels);
|
|
17456
17987
|
this.tickActivityMs = tickActivityMs;
|
|
17457
17988
|
this.maxCandles = config.maxCandles ?? 200;
|
|
17989
|
+
this.prefetchThreshold = config.prefetchThreshold ?? 80;
|
|
17458
17990
|
this.defaultVisibleBars = config.defaultVisibleBars ?? DEFAULT_VISIBLE_BARS;
|
|
17459
17991
|
this.defaultMobileVisibleBars = config.defaultMobileVisibleBars;
|
|
17460
17992
|
this.explicitMobileDivisor = config.mobileBarDivisor !== void 0;
|
|
@@ -17494,7 +18026,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17494
18026
|
this.showBidAskLines = config.showBidAskLines ?? false;
|
|
17495
18027
|
this.showActLogo = config.showActLogo ?? false;
|
|
17496
18028
|
this.hideLevelConfirmCancel = config.hideLevelConfirmCancel ?? false;
|
|
18029
|
+
this.levelClusteringEnabled = config.levelClusteringEnabled ?? true;
|
|
18030
|
+
this.clusterThresholdDistance = config.clusterThresholdDistance ?? 20;
|
|
17497
18031
|
this.enableThemeToggle = config.enableThemeToggle ?? false;
|
|
18032
|
+
this.tfcEnabled = config.tfcEnabled !== false;
|
|
18033
|
+
this.tfcActive = this.tfcEnabled;
|
|
17498
18034
|
this.showCandleCountdown = config.showCandleCountdown ?? true;
|
|
17499
18035
|
this.candleCountdownTimeframes = config.candleCountdownTimeframes;
|
|
17500
18036
|
this._onOrderSubmit = onOrderSubmit;
|
|
@@ -17558,6 +18094,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17558
18094
|
this.drawingToolsEnabled ? () => this.toggleDrawingTools() : null,
|
|
17559
18095
|
this.drawingToolsEnabled,
|
|
17560
18096
|
this.settingsEnabled,
|
|
18097
|
+
this.tfcEnabled,
|
|
18098
|
+
this.tfcActive,
|
|
18099
|
+
this.tfcEnabled ? (enabled) => {
|
|
18100
|
+
this.setTfcActive(enabled);
|
|
18101
|
+
} : null,
|
|
17561
18102
|
this.series,
|
|
17562
18103
|
this.timeframe,
|
|
17563
18104
|
null,
|
|
@@ -17709,7 +18250,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17709
18250
|
this.loadingOverlayEl.appendChild(loadingLabel);
|
|
17710
18251
|
this.canvasWrap.appendChild(this.loadingOverlayEl);
|
|
17711
18252
|
}
|
|
17712
|
-
if (showUI) {
|
|
18253
|
+
if (showUI && !hideSymbolAndTick) {
|
|
17713
18254
|
this.siEl = document.createElement("div");
|
|
17714
18255
|
Object.assign(this.siEl.style, {
|
|
17715
18256
|
position: "absolute",
|
|
@@ -17853,6 +18394,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17853
18394
|
const rect = this._cachedDrawRect ?? this.drawCanvas.getBoundingClientRect();
|
|
17854
18395
|
const y = e.clientY - rect.top;
|
|
17855
18396
|
const { priceH } = this.getChartLayout();
|
|
18397
|
+
const btnHalfH = this.uiConfig.tradeButton.size / 2;
|
|
18398
|
+
if (overlapsOffViewportPill(y, btnHalfH, this.offViewportHitAreas)) {
|
|
18399
|
+
this.tradeButton?.hide();
|
|
18400
|
+
return;
|
|
18401
|
+
}
|
|
17856
18402
|
if (y >= 0 && y <= priceH) {
|
|
17857
18403
|
this.tradeButton?.setY(y);
|
|
17858
18404
|
const priceRange = this._displayedPriceRange ?? this.getScaledPriceRange(this.viewport);
|
|
@@ -17867,7 +18413,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17867
18413
|
}
|
|
17868
18414
|
middleRow.appendChild(this.canvasWrap);
|
|
17869
18415
|
this.wrapper.appendChild(middleRow);
|
|
17870
|
-
if (showUI) {
|
|
18416
|
+
if (showUI && showBottomBar) {
|
|
17871
18417
|
const resolvedDurationMap = { ...DURATION_TIMEFRAME_MAP, ...durationTimeframeMap };
|
|
17872
18418
|
this.bottomBar = new BottomBar(
|
|
17873
18419
|
(d, tf) => {
|
|
@@ -18166,6 +18712,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18166
18712
|
}
|
|
18167
18713
|
triggerLoad(reason) {
|
|
18168
18714
|
if (!this.dataLoader) return;
|
|
18715
|
+
const gen = ++this._loadGeneration;
|
|
18169
18716
|
this.isFetching = true;
|
|
18170
18717
|
this.noMoreData = false;
|
|
18171
18718
|
const end = /* @__PURE__ */ new Date();
|
|
@@ -18177,6 +18724,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18177
18724
|
const interval = this.timeframeToInterval(this.getBaseInterval());
|
|
18178
18725
|
this.setLoading(true);
|
|
18179
18726
|
this.dataLoader({ start, end, interval }).then((bars) => {
|
|
18727
|
+
if (gen !== this._loadGeneration) return;
|
|
18180
18728
|
this.loadData(this.maybeAggregate(bars), reason === "duration");
|
|
18181
18729
|
this.setLoading(false);
|
|
18182
18730
|
this.emitter.emit("dataLoaded", {
|
|
@@ -18186,25 +18734,17 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18186
18734
|
end
|
|
18187
18735
|
});
|
|
18188
18736
|
}).catch(() => {
|
|
18737
|
+
if (gen !== this._loadGeneration) return;
|
|
18189
18738
|
this.isFetching = false;
|
|
18190
18739
|
this.setLoading(false);
|
|
18191
18740
|
});
|
|
18192
18741
|
}
|
|
18193
18742
|
checkNeedMoreData() {
|
|
18194
18743
|
if (!this.dataLoader || this.isFetching || this.noMoreData) return;
|
|
18195
|
-
const threshold = Math.max(
|
|
18196
|
-
20,
|
|
18197
|
-
Math.round(
|
|
18198
|
-
initialVisibleBars(
|
|
18199
|
-
this.mobileBarDivisor,
|
|
18200
|
-
this.explicitMobileDivisor,
|
|
18201
|
-
this.defaultVisibleBars,
|
|
18202
|
-
this.defaultMobileVisibleBars
|
|
18203
|
-
) * 0.25
|
|
18204
|
-
)
|
|
18205
|
-
);
|
|
18744
|
+
const threshold = Math.max(20, this.prefetchThreshold);
|
|
18206
18745
|
if (this._targetViewport.startIndex > threshold) return;
|
|
18207
18746
|
if (this.dataStore.length === 0) return;
|
|
18747
|
+
const gen = this._loadGeneration;
|
|
18208
18748
|
this.isFetching = true;
|
|
18209
18749
|
const before = this.dataStore.all[0].time;
|
|
18210
18750
|
const end = new Date(before - 1);
|
|
@@ -18216,9 +18756,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18216
18756
|
const interval = this.timeframeToInterval(this.getBaseInterval());
|
|
18217
18757
|
this.setLoading(true);
|
|
18218
18758
|
this.dataLoader({ start, end, interval }).then((bars) => {
|
|
18759
|
+
if (gen !== this._loadGeneration) return;
|
|
18219
18760
|
this.prependData(this.maybeAggregate(bars));
|
|
18220
18761
|
this.setLoading(false);
|
|
18221
18762
|
}).catch(() => {
|
|
18763
|
+
if (gen !== this._loadGeneration) return;
|
|
18222
18764
|
this.isFetching = false;
|
|
18223
18765
|
this.setLoading(false);
|
|
18224
18766
|
});
|
|
@@ -18473,6 +19015,31 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18473
19015
|
this.emitStateChange();
|
|
18474
19016
|
return this;
|
|
18475
19017
|
}
|
|
19018
|
+
/**
|
|
19019
|
+
* Toggle TFC (Trade from Charts) on or off at runtime.
|
|
19020
|
+
* When disabled, all trade levels, the trade button, and trade popover are hidden.
|
|
19021
|
+
* Only functional when `tfcEnabled` config option is `true` (default).
|
|
19022
|
+
*/
|
|
19023
|
+
setTfcActive(enabled) {
|
|
19024
|
+
if (!this.tfcEnabled) return this;
|
|
19025
|
+
this.tfcActive = enabled;
|
|
19026
|
+
this.topBar?.setTfcActive(enabled);
|
|
19027
|
+
if (!enabled) {
|
|
19028
|
+
this.tradeButton?.hide();
|
|
19029
|
+
this.tradePopover?.hide();
|
|
19030
|
+
if (this.draftOrderLabel !== null) {
|
|
19031
|
+
this.removeDraftOrder();
|
|
19032
|
+
}
|
|
19033
|
+
}
|
|
19034
|
+
if (this.chartSettingsDialog) {
|
|
19035
|
+
this.chartSettingsDialog.destroy();
|
|
19036
|
+
this.chartSettingsDialog = null;
|
|
19037
|
+
}
|
|
19038
|
+
this.renderTradeLayer();
|
|
19039
|
+
this.emitter.emit("tfcToggle", { enabled });
|
|
19040
|
+
this.emitStateChange();
|
|
19041
|
+
return this;
|
|
19042
|
+
}
|
|
18476
19043
|
clearDuration() {
|
|
18477
19044
|
if (this.duration === void 0) return;
|
|
18478
19045
|
this.duration = void 0;
|
|
@@ -18516,7 +19083,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18516
19083
|
})),
|
|
18517
19084
|
positionRenderStyle: this.positionRenderStyle,
|
|
18518
19085
|
tradeDisplayFilter: this.tradeDisplayFilter,
|
|
18519
|
-
canvasColors: Object.keys(this.settingsCanvasColors).length > 0 ? { ...this.settingsCanvasColors } : void 0
|
|
19086
|
+
canvasColors: Object.keys(this.settingsCanvasColors).length > 0 ? { ...this.settingsCanvasColors } : void 0,
|
|
19087
|
+
tfcEnabled: this.tfcActive
|
|
18520
19088
|
};
|
|
18521
19089
|
}
|
|
18522
19090
|
/**
|
|
@@ -18588,9 +19156,14 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18588
19156
|
if (state.canvasColors) {
|
|
18589
19157
|
this.settingsCanvasColors = state.canvasColors;
|
|
18590
19158
|
}
|
|
19159
|
+
if (state.tfcEnabled !== void 0 && this.tfcEnabled) {
|
|
19160
|
+
this.tfcActive = state.tfcEnabled;
|
|
19161
|
+
this.topBar?.setTfcActive(state.tfcEnabled);
|
|
19162
|
+
}
|
|
18591
19163
|
} finally {
|
|
18592
19164
|
this.isRestoringState = false;
|
|
18593
19165
|
}
|
|
19166
|
+
this.emitStateChange();
|
|
18594
19167
|
this.scheduleRender();
|
|
18595
19168
|
return this;
|
|
18596
19169
|
}
|
|
@@ -19190,7 +19763,27 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19190
19763
|
*/
|
|
19191
19764
|
selectLevel(label) {
|
|
19192
19765
|
this.selectedTradeLabel = label;
|
|
19193
|
-
if (label !== null)
|
|
19766
|
+
if (label !== null) {
|
|
19767
|
+
this.crosshair = null;
|
|
19768
|
+
const cluster = this.lastClusters.find(
|
|
19769
|
+
(c) => c.members.some((m) => m.level.label === label)
|
|
19770
|
+
);
|
|
19771
|
+
if (cluster) this.expandedClusters.add(cluster.id);
|
|
19772
|
+
}
|
|
19773
|
+
this.renderTradeLayer();
|
|
19774
|
+
return this;
|
|
19775
|
+
}
|
|
19776
|
+
/** Enable or disable trade-level fan-out clustering at runtime. */
|
|
19777
|
+
setLevelClusteringEnabled(enabled) {
|
|
19778
|
+
this.levelClusteringEnabled = enabled;
|
|
19779
|
+
this.expandedClusters.clear();
|
|
19780
|
+
this.renderTradeLayer();
|
|
19781
|
+
return this;
|
|
19782
|
+
}
|
|
19783
|
+
/** Update the cluster proximity threshold (in pixels) at runtime. Only effective when levelClusteringEnabled is true. */
|
|
19784
|
+
setClusterThresholdDistance(px) {
|
|
19785
|
+
this.clusterThresholdDistance = px;
|
|
19786
|
+
this.expandedClusters.clear();
|
|
19194
19787
|
this.renderTradeLayer();
|
|
19195
19788
|
return this;
|
|
19196
19789
|
}
|
|
@@ -19373,7 +19966,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19373
19966
|
theme: this.themeName
|
|
19374
19967
|
},
|
|
19375
19968
|
this.labels.dialogs,
|
|
19376
|
-
!!this._onOrderSubmit,
|
|
19969
|
+
!!this._onOrderSubmit && this.tfcActive,
|
|
19377
19970
|
this.enableThemeToggle
|
|
19378
19971
|
);
|
|
19379
19972
|
this.getFloatingRoot().appendChild(this.chartSettingsDialog.el);
|
|
@@ -19484,11 +20077,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19484
20077
|
const dpr = window.devicePixelRatio || 1;
|
|
19485
20078
|
const { tradeCtx: ctx, tradeCanvas: canvas, theme, viewport } = this;
|
|
19486
20079
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
19487
|
-
if (this.levels.length === 0 || this.dataStore.length === 0) {
|
|
20080
|
+
if (!this.tfcActive || this.levels.length === 0 || this.dataStore.length === 0) {
|
|
19488
20081
|
this.tradeLevelHitAreas = [];
|
|
19489
20082
|
this.tradeLevelEditAreas = [];
|
|
19490
20083
|
this.tradeHoverAreas = [];
|
|
19491
20084
|
this.tradeAddBracketAreas = [];
|
|
20085
|
+
this.offViewportHitAreas = [];
|
|
19492
20086
|
return;
|
|
19493
20087
|
}
|
|
19494
20088
|
const { chartW, priceH } = this.getChartLayout();
|
|
@@ -19543,6 +20137,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19543
20137
|
// Include true pending orders AND entry-order positions (ES/EL)
|
|
19544
20138
|
processedLevels.filter((l) => l.type === "pending" || isEntryOrder(l))
|
|
19545
20139
|
) : processedLevels;
|
|
20140
|
+
const clusterThreshold = this.levelClusteringEnabled ? this.clusterThresholdDistance : 0;
|
|
19546
20141
|
const result = renderTradeLevels(
|
|
19547
20142
|
ctx,
|
|
19548
20143
|
filteredLevels,
|
|
@@ -19560,9 +20155,22 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19560
20155
|
this.positionRenderStyle,
|
|
19561
20156
|
this.hideLevelConfirmCancel,
|
|
19562
20157
|
this.draftOrderLabel,
|
|
19563
|
-
this.draftBracketPnl
|
|
20158
|
+
this.draftBracketPnl,
|
|
20159
|
+
this.expandedClusters,
|
|
20160
|
+
clusterThreshold
|
|
19564
20161
|
);
|
|
19565
20162
|
ctx.restore();
|
|
20163
|
+
const offVpResult = renderOffViewportIndicators(
|
|
20164
|
+
ctx,
|
|
20165
|
+
filteredLevels,
|
|
20166
|
+
priceRange,
|
|
20167
|
+
theme,
|
|
20168
|
+
chartW,
|
|
20169
|
+
priceH,
|
|
20170
|
+
this.tradeDragHandler?.dragLabel ?? null,
|
|
20171
|
+
this.selectedTradeLabel
|
|
20172
|
+
);
|
|
20173
|
+
this.offViewportHitAreas = offVpResult.hitAreas;
|
|
19566
20174
|
ctx.restore();
|
|
19567
20175
|
this.tradeLevelHitAreas = result.hitAreas;
|
|
19568
20176
|
this.tradeLevelEditAreas = result.editAreas;
|
|
@@ -19570,6 +20178,14 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19570
20178
|
this.tradeLevelDragAreas = result.dragAreas;
|
|
19571
20179
|
this.tradeLevelConfirmAreas = result.confirmAreas;
|
|
19572
20180
|
this.tradeAddBracketAreas = result.addBracketAreas;
|
|
20181
|
+
this.clusterBadgeAreas = result.clusterBadgeAreas;
|
|
20182
|
+
this.lastClusters = result.clusters;
|
|
20183
|
+
if (this.expandedClusters.size > 0) {
|
|
20184
|
+
const currentIds = new Set(result.clusters.map((c) => c.id));
|
|
20185
|
+
for (const id of this.expandedClusters) {
|
|
20186
|
+
if (!currentIds.has(id)) this.expandedClusters.delete(id);
|
|
20187
|
+
}
|
|
20188
|
+
}
|
|
19573
20189
|
}
|
|
19574
20190
|
// ── Inline pending-change confirmation ────────────────────────────────────
|
|
19575
20191
|
/** Returns the effective entry price for a level, preferring any staged main drag. */
|
|
@@ -20464,7 +21080,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20464
21080
|
if (this._isTradingActive) {
|
|
20465
21081
|
this.tradeButton.hide();
|
|
20466
21082
|
} else {
|
|
20467
|
-
this.tradeButton.
|
|
21083
|
+
const _btnHalfH1 = this.uiConfig.tradeButton.size / 2;
|
|
21084
|
+
if (overlapsOffViewportPill(y, _btnHalfH1, this.offViewportHitAreas)) {
|
|
21085
|
+
this.tradeButton.hide();
|
|
21086
|
+
} else if (this.tfcActive) {
|
|
21087
|
+
this.tradeButton.show();
|
|
21088
|
+
}
|
|
20468
21089
|
}
|
|
20469
21090
|
this._tradeButtonClickData = { price, y };
|
|
20470
21091
|
const _allBars = this.dataStore.all;
|
|
@@ -20477,6 +21098,9 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20477
21098
|
/** Shared helper: initiate a trade level main-line drag from a hit area. */
|
|
20478
21099
|
_startTradeDragFromHit(dragHit, level, hitY) {
|
|
20479
21100
|
if (!this.tradeDragHandler) return;
|
|
21101
|
+
const { priceH } = this.getChartLayout();
|
|
21102
|
+
const priceRange = this._displayedPriceRange ?? this.getScaledPriceRange(this.viewport);
|
|
21103
|
+
const levelPixelY = this.scaleManager.yToPixel(dragHit.price, priceRange, priceH);
|
|
20480
21104
|
if (dragHit.isBracket) {
|
|
20481
21105
|
this._pendingBracketOrderLabel = dragHit.bracketOrderLabel ?? null;
|
|
20482
21106
|
this._isBracketNew = false;
|
|
@@ -20489,6 +21113,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20489
21113
|
level,
|
|
20490
21114
|
dragHit.bracketType,
|
|
20491
21115
|
dragHit.price,
|
|
21116
|
+
hitY,
|
|
21117
|
+
levelPixelY,
|
|
20492
21118
|
marketPrice,
|
|
20493
21119
|
this._effectiveEntryPrice(level.label, level.price)
|
|
20494
21120
|
);
|
|
@@ -20508,6 +21134,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20508
21134
|
this.tradeDragHandler.startDrag(
|
|
20509
21135
|
level,
|
|
20510
21136
|
hitY,
|
|
21137
|
+
levelPixelY,
|
|
20511
21138
|
dragHit.price,
|
|
20512
21139
|
posLevel.stopLossPrice,
|
|
20513
21140
|
posLevel.takeProfitPrice,
|
|
@@ -20522,6 +21149,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20522
21149
|
this.tradeDragHandler.startDrag(
|
|
20523
21150
|
level,
|
|
20524
21151
|
hitY,
|
|
21152
|
+
levelPixelY,
|
|
20525
21153
|
dragHit.price,
|
|
20526
21154
|
void 0,
|
|
20527
21155
|
void 0,
|
|
@@ -20551,6 +21179,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20551
21179
|
level,
|
|
20552
21180
|
addHit.bracketType,
|
|
20553
21181
|
initialPrice,
|
|
21182
|
+
hitY,
|
|
21183
|
+
btnCenterY,
|
|
20554
21184
|
marketPrice,
|
|
20555
21185
|
this._effectiveEntryPrice(level.label, level.price)
|
|
20556
21186
|
);
|
|
@@ -20741,6 +21371,38 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20741
21371
|
};
|
|
20742
21372
|
this.scrollAnimId = requestAnimationFrame(step);
|
|
20743
21373
|
}
|
|
21374
|
+
/**
|
|
21375
|
+
* Smooth-scroll the Y axis so `price` lands at the vertical center of the
|
|
21376
|
+
* chart. Animates `pricePanOffset` only — horizontal viewport is preserved.
|
|
21377
|
+
* Used by off-viewport level indicator clicks.
|
|
21378
|
+
*/
|
|
21379
|
+
scrollToCenterPrice(price) {
|
|
21380
|
+
if (!isFinite(price) || this.dataStore.length === 0) return;
|
|
21381
|
+
if (this.scrollAnimId !== null) {
|
|
21382
|
+
cancelAnimationFrame(this.scrollAnimId);
|
|
21383
|
+
this.scrollAnimId = null;
|
|
21384
|
+
}
|
|
21385
|
+
const base = this.dataStore.getPriceRange(this.viewport);
|
|
21386
|
+
const naturalMid = (base.min + base.max) / 2;
|
|
21387
|
+
const startPan = this.pricePanOffset;
|
|
21388
|
+
const targetPan = price - naturalMid;
|
|
21389
|
+
if (Math.abs(targetPan - startPan) < 1e-9) return;
|
|
21390
|
+
const DURATION = 350;
|
|
21391
|
+
const t0 = performance.now();
|
|
21392
|
+
const ease = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
|
21393
|
+
const step = (now) => {
|
|
21394
|
+
const raw = Math.min((now - t0) / DURATION, 1);
|
|
21395
|
+
const prog = ease(raw);
|
|
21396
|
+
this.pricePanOffset = startPan + (targetPan - startPan) * prog;
|
|
21397
|
+
this.render();
|
|
21398
|
+
if (raw < 1) {
|
|
21399
|
+
this.scrollAnimId = requestAnimationFrame(step);
|
|
21400
|
+
} else {
|
|
21401
|
+
this.scrollAnimId = null;
|
|
21402
|
+
}
|
|
21403
|
+
};
|
|
21404
|
+
this.scrollAnimId = requestAnimationFrame(step);
|
|
21405
|
+
}
|
|
20744
21406
|
resetPriceAxis() {
|
|
20745
21407
|
this.priceScaleFactor = 1;
|
|
20746
21408
|
this.pricePanOffset = 0;
|