acttrader-charts 1.0.13 → 1.0.15
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 +32 -1
- package/dist/{MACD-CEmdbwGz.d.cts → MACD-DVxcNurb.d.cts} +108 -5
- package/dist/{MACD-CEmdbwGz.d.ts → MACD-DVxcNurb.d.ts} +108 -5
- package/dist/index.cjs +1164 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -2
- package/dist/index.d.ts +66 -2
- package/dist/index.js +1164 -100
- 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 +68 -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: {
|
|
@@ -726,8 +759,7 @@ function resolveLabels(override) {
|
|
|
726
759
|
|
|
727
760
|
// src/interaction/PanHandler.ts
|
|
728
761
|
var _PanHandler = class _PanHandler {
|
|
729
|
-
|
|
730
|
-
constructor(canvas, getViewport, setViewport, totalBars, onUpdate, isLocked = () => false, panY, onDragStart, onDragEnd) {
|
|
762
|
+
constructor(canvas, getViewport, setViewport, totalBars, onUpdate, isLocked = () => false, panY, onDragStart, onDragEnd, momentumOptions) {
|
|
731
763
|
this.canvas = canvas;
|
|
732
764
|
this.getViewport = getViewport;
|
|
733
765
|
this.setViewport = setViewport;
|
|
@@ -745,7 +777,12 @@ var _PanHandler = class _PanHandler {
|
|
|
745
777
|
this.dragStartY = 0;
|
|
746
778
|
this.hasPanned = false;
|
|
747
779
|
this.dragStartFired = false;
|
|
780
|
+
// ── Velocity sample buffer (rolling 100 ms window) ─────────────────────────
|
|
781
|
+
this._velBuffer = [];
|
|
782
|
+
// ── In-flight momentum RAF ─────────────────────────────────────────────────
|
|
783
|
+
this._momentumRafId = null;
|
|
748
784
|
this.onMouseDown = (e) => {
|
|
785
|
+
this._stopMomentum();
|
|
749
786
|
if (this.isLocked()) return;
|
|
750
787
|
this.isDragging = true;
|
|
751
788
|
this.isTouchDrag = false;
|
|
@@ -754,10 +791,15 @@ var _PanHandler = class _PanHandler {
|
|
|
754
791
|
this.dragStartX = e.clientX;
|
|
755
792
|
this.dragStartY = e.clientY;
|
|
756
793
|
this.hasPanned = false;
|
|
794
|
+
this._velBuffer.length = 0;
|
|
757
795
|
};
|
|
758
796
|
this.onTouchStart = (e) => {
|
|
797
|
+
if (e.touches.length !== 1) {
|
|
798
|
+
this._stopMomentum();
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
this._stopMomentum();
|
|
759
802
|
if (this.isLocked()) return;
|
|
760
|
-
if (e.touches.length !== 1) return;
|
|
761
803
|
this.isDragging = true;
|
|
762
804
|
this.isTouchDrag = true;
|
|
763
805
|
this.lastX = e.touches[0].clientX;
|
|
@@ -765,6 +807,7 @@ var _PanHandler = class _PanHandler {
|
|
|
765
807
|
this.dragStartX = e.touches[0].clientX;
|
|
766
808
|
this.dragStartY = e.touches[0].clientY;
|
|
767
809
|
this.dragStartFired = true;
|
|
810
|
+
this._velBuffer.length = 0;
|
|
768
811
|
this.onDragStart?.();
|
|
769
812
|
};
|
|
770
813
|
this.onMouseMove = (e) => {
|
|
@@ -787,6 +830,7 @@ var _PanHandler = class _PanHandler {
|
|
|
787
830
|
this.canvas.style.cursor = "grabbing";
|
|
788
831
|
}
|
|
789
832
|
if (this.hasPanned) {
|
|
833
|
+
this._recordVelocitySample(deltaX, performance.now());
|
|
790
834
|
this.pan(deltaX);
|
|
791
835
|
if (this.panY) this.panY(deltaY);
|
|
792
836
|
}
|
|
@@ -800,6 +844,7 @@ var _PanHandler = class _PanHandler {
|
|
|
800
844
|
e.preventDefault();
|
|
801
845
|
const deltaX = e.touches[0].clientX - this.lastX;
|
|
802
846
|
const deltaY = e.touches[0].clientY - this.lastY;
|
|
847
|
+
this._recordVelocitySample(deltaX, performance.now());
|
|
803
848
|
this.pan(deltaX);
|
|
804
849
|
if (this.panY) this.panY(deltaY);
|
|
805
850
|
this.lastX = e.touches[0].clientX;
|
|
@@ -811,9 +856,16 @@ var _PanHandler = class _PanHandler {
|
|
|
811
856
|
this.hasPanned = false;
|
|
812
857
|
if (this.dragStartFired) {
|
|
813
858
|
this.dragStartFired = false;
|
|
814
|
-
this.
|
|
859
|
+
const velocity = this._computeReleaseVelocity();
|
|
860
|
+
this._velBuffer.length = 0;
|
|
861
|
+
const launched = this._startMomentum(velocity);
|
|
862
|
+
if (!launched) this.onDragEnd?.();
|
|
815
863
|
}
|
|
816
864
|
};
|
|
865
|
+
this._momentumEnabled = momentumOptions?.enabled ?? true;
|
|
866
|
+
this._momentumDecay = Math.min(0.99, Math.max(0.8, momentumOptions?.decay ?? 0.95));
|
|
867
|
+
this._momentumThreshold = momentumOptions?.threshold ?? 0.3;
|
|
868
|
+
this._momentumMaxVelocity = momentumOptions?.maxVelocity ?? 6;
|
|
817
869
|
this.attach();
|
|
818
870
|
}
|
|
819
871
|
attach() {
|
|
@@ -824,12 +876,14 @@ var _PanHandler = class _PanHandler {
|
|
|
824
876
|
window.addEventListener("touchmove", this.onTouchMove, { passive: false });
|
|
825
877
|
window.addEventListener("touchend", this.onMouseUp);
|
|
826
878
|
}
|
|
827
|
-
/** Cancel any in-progress pan gesture (e.g. when a drawing drag takes over). */
|
|
879
|
+
/** Cancel any in-progress pan gesture or momentum (e.g. when a drawing drag takes over). */
|
|
828
880
|
cancel() {
|
|
881
|
+
this._stopMomentum();
|
|
829
882
|
this.isDragging = false;
|
|
830
883
|
this.hasPanned = false;
|
|
831
884
|
}
|
|
832
885
|
destroy() {
|
|
886
|
+
this._stopMomentum();
|
|
833
887
|
this.canvas.removeEventListener("mousedown", this.onMouseDown);
|
|
834
888
|
window.removeEventListener("mousemove", this.onMouseMove);
|
|
835
889
|
window.removeEventListener("mouseup", this.onMouseUp);
|
|
@@ -837,6 +891,73 @@ var _PanHandler = class _PanHandler {
|
|
|
837
891
|
window.removeEventListener("touchmove", this.onTouchMove);
|
|
838
892
|
window.removeEventListener("touchend", this.onMouseUp);
|
|
839
893
|
}
|
|
894
|
+
// ── Velocity tracking ────────────────────────────────────────────────────────
|
|
895
|
+
_recordVelocitySample(dx, t) {
|
|
896
|
+
this._velBuffer.push({ dx, t });
|
|
897
|
+
const cutoff = t - _PanHandler.VEL_WINDOW_MS;
|
|
898
|
+
let i = 0;
|
|
899
|
+
while (i < this._velBuffer.length && this._velBuffer[i].t < cutoff) i++;
|
|
900
|
+
if (i > 0) this._velBuffer.splice(0, i);
|
|
901
|
+
}
|
|
902
|
+
_computeReleaseVelocity() {
|
|
903
|
+
const buf = this._velBuffer;
|
|
904
|
+
if (buf.length < 2) return 0;
|
|
905
|
+
const totalDx = buf.reduce((acc, s) => acc + s.dx, 0);
|
|
906
|
+
const dt = buf[buf.length - 1].t - buf[0].t;
|
|
907
|
+
if (dt <= 0) return 0;
|
|
908
|
+
const raw = totalDx / dt;
|
|
909
|
+
return Math.sign(raw) * Math.min(Math.abs(raw), this._momentumMaxVelocity);
|
|
910
|
+
}
|
|
911
|
+
// ── Momentum animation ────────────────────────────────────────────────────────
|
|
912
|
+
/**
|
|
913
|
+
* Launch momentum scrolling with the given initial velocity (px/ms).
|
|
914
|
+
* Returns `true` if momentum was successfully launched, `false` otherwise.
|
|
915
|
+
*/
|
|
916
|
+
_startMomentum(velocity) {
|
|
917
|
+
if (!this._momentumEnabled) return false;
|
|
918
|
+
if (Math.abs(velocity) < this._momentumThreshold) return false;
|
|
919
|
+
let lastTime = null;
|
|
920
|
+
let vel = velocity;
|
|
921
|
+
const decay = this._momentumDecay;
|
|
922
|
+
const tick = (now) => {
|
|
923
|
+
if (lastTime === null) {
|
|
924
|
+
lastTime = now;
|
|
925
|
+
this._momentumRafId = requestAnimationFrame(tick);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const dt = now - lastTime;
|
|
929
|
+
lastTime = now;
|
|
930
|
+
vel *= Math.pow(decay, dt / 16.67);
|
|
931
|
+
const pixelDelta = vel * dt;
|
|
932
|
+
if (Math.abs(pixelDelta) < 0.5) {
|
|
933
|
+
this._stopMomentum();
|
|
934
|
+
this.onDragEnd?.();
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (this.isLocked()) {
|
|
938
|
+
this._stopMomentum();
|
|
939
|
+
this.onDragEnd?.();
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const prevStart = this.getViewport().startIndex;
|
|
943
|
+
this.pan(pixelDelta);
|
|
944
|
+
if (this.getViewport().startIndex === prevStart) {
|
|
945
|
+
this._stopMomentum();
|
|
946
|
+
this.onDragEnd?.();
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
this._momentumRafId = requestAnimationFrame(tick);
|
|
950
|
+
};
|
|
951
|
+
this._momentumRafId = requestAnimationFrame(tick);
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
_stopMomentum() {
|
|
955
|
+
if (this._momentumRafId !== null) {
|
|
956
|
+
cancelAnimationFrame(this._momentumRafId);
|
|
957
|
+
this._momentumRafId = null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
// ── Core pan logic ────────────────────────────────────────────────────────────
|
|
840
961
|
pan(deltaX) {
|
|
841
962
|
const vp = this.getViewport();
|
|
842
963
|
const visibleBars = vp.endIndex - vp.startIndex;
|
|
@@ -850,6 +971,7 @@ var _PanHandler = class _PanHandler {
|
|
|
850
971
|
}
|
|
851
972
|
};
|
|
852
973
|
_PanHandler.DRAG_THRESHOLD = 4;
|
|
974
|
+
_PanHandler.VEL_WINDOW_MS = 100;
|
|
853
975
|
var PanHandler = _PanHandler;
|
|
854
976
|
|
|
855
977
|
// src/interaction/ZoomHandler.ts
|
|
@@ -889,11 +1011,11 @@ var ZoomHandler = class {
|
|
|
889
1011
|
Math.max(MIN_VISIBLE_BARS, Math.min(MAX_VISIBLE_BARS, visibleBars * zoomFactor))
|
|
890
1012
|
);
|
|
891
1013
|
if (vp.endIndex >= this.totalBars()) {
|
|
892
|
-
const newEnd = vp.endIndex;
|
|
1014
|
+
const newEnd = Math.min(vp.endIndex, this.totalBars() + Math.floor(newVisible / 2));
|
|
893
1015
|
const newStart = Math.max(0, newEnd - newVisible);
|
|
894
|
-
this.setViewport({ ...vp, startIndex: newStart, endIndex:
|
|
1016
|
+
this.setViewport({ ...vp, startIndex: newStart, endIndex: newStart + newVisible });
|
|
895
1017
|
} else {
|
|
896
|
-
const mouseRatio = e.offsetX / this.
|
|
1018
|
+
const mouseRatio = e.offsetX / this.getChartWidth();
|
|
897
1019
|
const pivotBar = vp.startIndex + visibleBars * mouseRatio;
|
|
898
1020
|
const newStart = Math.max(0, pivotBar - newVisible * mouseRatio);
|
|
899
1021
|
this.setViewport({ ...vp, startIndex: newStart, endIndex: newStart + newVisible });
|
|
@@ -1430,18 +1552,18 @@ function renderHighLow(ctx, bars, viewport, priceRange, scale, theme, chartWidth
|
|
|
1430
1552
|
}
|
|
1431
1553
|
|
|
1432
1554
|
// src/overlays/GridOverlay.ts
|
|
1433
|
-
function renderGrid(ctx, priceRange, scale, theme, chartWidth, chartHeight, gridLines
|
|
1555
|
+
function renderGrid(ctx, priceRange, scale, theme, chartWidth, chartHeight, gridLines) {
|
|
1434
1556
|
ctx.save();
|
|
1435
1557
|
ctx.globalAlpha = 0.6;
|
|
1436
1558
|
ctx.strokeStyle = theme.grid;
|
|
1437
1559
|
ctx.lineWidth = 1;
|
|
1438
|
-
const step = (priceRange.max - priceRange.min) / gridLines;
|
|
1439
1560
|
ctx.beginPath();
|
|
1440
|
-
|
|
1441
|
-
const price
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1561
|
+
{
|
|
1562
|
+
for (const price of generatePriceTicks(priceRange, chartHeight)) {
|
|
1563
|
+
const y = Math.round(scale.yToPixel(price, priceRange, chartHeight)) + 0.5;
|
|
1564
|
+
ctx.moveTo(0, y);
|
|
1565
|
+
ctx.lineTo(chartWidth, y);
|
|
1566
|
+
}
|
|
1445
1567
|
}
|
|
1446
1568
|
ctx.stroke();
|
|
1447
1569
|
const vertCols = Math.max(1, Math.floor(chartWidth / 80));
|
|
@@ -1458,19 +1580,19 @@ function renderGrid(ctx, priceRange, scale, theme, chartWidth, chartHeight, grid
|
|
|
1458
1580
|
|
|
1459
1581
|
// src/overlays/CrosshairOverlay.ts
|
|
1460
1582
|
var SUB_DAILY = ["1m", "5m", "15m", "30m", "1h", "4h"];
|
|
1461
|
-
function formatCrosshairTime(ts, timeframe) {
|
|
1583
|
+
function formatCrosshairTime(ts, timeframe, timezone = "UTC") {
|
|
1462
1584
|
const d = new Date(ts);
|
|
1463
1585
|
if (SUB_DAILY.includes(timeframe)) {
|
|
1464
|
-
const date = d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone:
|
|
1465
|
-
const time = d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false, timeZone:
|
|
1586
|
+
const date = d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: timezone });
|
|
1587
|
+
const time = d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false, timeZone: timezone });
|
|
1466
1588
|
return `${date} ${time}`;
|
|
1467
1589
|
}
|
|
1468
1590
|
if (timeframe === "1M") {
|
|
1469
|
-
return d.toLocaleDateString("en-US", { month: "short", year: "numeric", timeZone:
|
|
1591
|
+
return d.toLocaleDateString("en-US", { month: "short", year: "numeric", timeZone: timezone });
|
|
1470
1592
|
}
|
|
1471
|
-
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone:
|
|
1593
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: timezone });
|
|
1472
1594
|
}
|
|
1473
|
-
function renderCrosshair(ctx, pos, priceRange, viewport, scale, theme, chartWidth, chartHeight, priceAxisWidth, axisY, timeframe, pricePrecision = 2, cfg = DEFAULT_CROSSHAIR_CONFIG) {
|
|
1595
|
+
function renderCrosshair(ctx, pos, priceRange, viewport, scale, theme, chartWidth, chartHeight, priceAxisWidth, axisY, timeframe, pricePrecision = 2, cfg = DEFAULT_CROSSHAIR_CONFIG, timezone = "UTC") {
|
|
1474
1596
|
const x = scale.xToPixel(pos.barIndex, viewport, chartWidth);
|
|
1475
1597
|
const y = scale.yToPixel(pos.price, priceRange, chartHeight);
|
|
1476
1598
|
ctx.setLineDash([4, 4]);
|
|
@@ -1498,7 +1620,7 @@ function renderCrosshair(ctx, pos, priceRange, viewport, scale, theme, chartWidt
|
|
|
1498
1620
|
ctx.textAlign = "left";
|
|
1499
1621
|
ctx.fillText(priceLabel, chartWidth + labelPad, y + 4);
|
|
1500
1622
|
if (pos.bar) {
|
|
1501
|
-
const timeLabel = formatCrosshairTime(pos.bar.time, timeframe);
|
|
1623
|
+
const timeLabel = formatCrosshairTime(pos.bar.time, timeframe, timezone);
|
|
1502
1624
|
const tw = ctx.measureText(timeLabel).width;
|
|
1503
1625
|
const boxW = tw + labelPad * 2;
|
|
1504
1626
|
const boxX = Math.max(0, Math.min(chartWidth - boxW, x - boxW / 2));
|
|
@@ -1513,8 +1635,70 @@ function renderCrosshair(ctx, pos, priceRange, viewport, scale, theme, chartWidt
|
|
|
1513
1635
|
}
|
|
1514
1636
|
}
|
|
1515
1637
|
|
|
1638
|
+
// src/overlays/TradeLevelCluster.ts
|
|
1639
|
+
function detectClusters(measured, thresholdPx, excludeLabels = /* @__PURE__ */ new Set()) {
|
|
1640
|
+
const eligible = measured.filter((m) => !excludeLabels.has(m.level.label)).sort((a, b) => a.y - b.y);
|
|
1641
|
+
if (eligible.length < 2) return [];
|
|
1642
|
+
const groups = [];
|
|
1643
|
+
let currentGroup = [eligible[0]];
|
|
1644
|
+
let groupStartY = eligible[0].y;
|
|
1645
|
+
for (let i = 1; i < eligible.length; i++) {
|
|
1646
|
+
const m = eligible[i];
|
|
1647
|
+
if (m.y - groupStartY <= thresholdPx) {
|
|
1648
|
+
currentGroup.push(m);
|
|
1649
|
+
} else {
|
|
1650
|
+
groups.push(currentGroup);
|
|
1651
|
+
currentGroup = [m];
|
|
1652
|
+
groupStartY = m.y;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
groups.push(currentGroup);
|
|
1656
|
+
return groups.filter((g) => g.length >= 2).map((members) => {
|
|
1657
|
+
const id = "cluster:" + members.map((m) => m.level.label).sort().join("|");
|
|
1658
|
+
const centerY = members.reduce((sum, m) => sum + m.y, 0) / members.length;
|
|
1659
|
+
const centerPrice = members.reduce((sum, m) => sum + m.price, 0) / members.length;
|
|
1660
|
+
return { id, members, centerY, centerPrice };
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
function computeFanOutLayout(cluster, boxH, gapPx = 4) {
|
|
1664
|
+
const n = cluster.members.length;
|
|
1665
|
+
const totalH = n * boxH + (n - 1) * gapPx;
|
|
1666
|
+
const startY = cluster.centerY - totalH / 2;
|
|
1667
|
+
const sorted = [...cluster.members].sort((a, b) => a.y - b.y);
|
|
1668
|
+
const result = /* @__PURE__ */ new Map();
|
|
1669
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
1670
|
+
result.set(sorted[i].level.label, startY + i * (boxH + gapPx));
|
|
1671
|
+
}
|
|
1672
|
+
return result;
|
|
1673
|
+
}
|
|
1674
|
+
function buildClusterBadgeLayout(cluster, chartW, ctx) {
|
|
1675
|
+
const n = cluster.members.length;
|
|
1676
|
+
const types = new Set(cluster.members.map((m) => m.level.type));
|
|
1677
|
+
let noun;
|
|
1678
|
+
if (types.size === 1) {
|
|
1679
|
+
const t = types.values().next().value;
|
|
1680
|
+
noun = t === "position" ? "Position" : t === "pending" ? "Order" : "Trade";
|
|
1681
|
+
} else {
|
|
1682
|
+
noun = "Level";
|
|
1683
|
+
}
|
|
1684
|
+
const text = `${n} ${noun}${n > 1 ? "s" : ""}`;
|
|
1685
|
+
const PAD_X = 12;
|
|
1686
|
+
const textW = ctx.measureText(text).width;
|
|
1687
|
+
const EXPAND_ICON_W = 16;
|
|
1688
|
+
const badgeW = PAD_X + textW + EXPAND_ICON_W + PAD_X;
|
|
1689
|
+
const badgeH = 24;
|
|
1690
|
+
const badgeX = (chartW - badgeW) / 2;
|
|
1691
|
+
const badgeY = cluster.centerY - badgeH / 2;
|
|
1692
|
+
return { badgeX, badgeY, badgeW, badgeH, text };
|
|
1693
|
+
}
|
|
1694
|
+
function hitTestClusterBadge(x, y, areas) {
|
|
1695
|
+
return areas.find((a) => x >= a.x && x <= a.x + a.w && y >= a.y && y <= a.y + a.h) ?? null;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1516
1698
|
// src/overlays/TradeLevelOverlay.ts
|
|
1517
1699
|
var BOX_PAD_X = 8;
|
|
1700
|
+
var PILL_PAD_X = 6;
|
|
1701
|
+
var PILL_GAP = 6;
|
|
1518
1702
|
var BOX_PAD_Y = 4;
|
|
1519
1703
|
var ROW_H = 16;
|
|
1520
1704
|
var BOX_RADIUS = 3;
|
|
@@ -1631,7 +1815,7 @@ function isDraggable(level) {
|
|
|
1631
1815
|
return !!level.entryPriceEditable;
|
|
1632
1816
|
return false;
|
|
1633
1817
|
}
|
|
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 = {}) {
|
|
1818
|
+
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, showQuantityField = false) {
|
|
1635
1819
|
if (levels.length === 0)
|
|
1636
1820
|
return {
|
|
1637
1821
|
hitAreas: [],
|
|
@@ -1639,7 +1823,9 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1639
1823
|
hoverAreas: [],
|
|
1640
1824
|
dragAreas: [],
|
|
1641
1825
|
confirmAreas: [],
|
|
1642
|
-
addBracketAreas: []
|
|
1826
|
+
addBracketAreas: [],
|
|
1827
|
+
clusterBadgeAreas: [],
|
|
1828
|
+
clusters: []
|
|
1643
1829
|
};
|
|
1644
1830
|
const colors = theme.tradeLevels;
|
|
1645
1831
|
const boxH = ROW_H + BOX_PAD_Y * 2;
|
|
@@ -1667,16 +1853,52 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1667
1853
|
const effectivePending = hasPending && !hideConfirmCancel;
|
|
1668
1854
|
const confirmW = effectivePending ? (CONFIRM_R * 2 + CONFIRM_GAP) * 2 : 0;
|
|
1669
1855
|
const closeW = effectivePending || hideConfirmCancel ? 0 : EDIT_R * 2 + EDIT_GAP + CLOSE_R * 2 + BOX_PAD_X / 2;
|
|
1670
|
-
const
|
|
1856
|
+
const isDraftLevel = level.data?._isDraft === true;
|
|
1857
|
+
const qtyPillText = showQuantityField && isDraftLevel && level.type === "pending" && level.lots !== void 0 ? String(level.lots) : null;
|
|
1858
|
+
const pillW = qtyPillText ? PILL_PAD_X + ctx.measureText(qtyPillText).width + PILL_PAD_X + PILL_GAP : 0;
|
|
1859
|
+
const bW = BOX_PAD_X + pillW + pnlW + labelW + BOX_PAD_X + confirmW + closeW;
|
|
1671
1860
|
measured.push({ level, price, y, boxW: bW, boxH });
|
|
1672
1861
|
}
|
|
1862
|
+
const excludeFromClustering = /* @__PURE__ */ new Set();
|
|
1863
|
+
if (draftOrderLabel) excludeFromClustering.add(draftOrderLabel);
|
|
1864
|
+
const clusters = clusterThresholdPx > 0 ? detectClusters(measured, clusterThresholdPx, excludeFromClustering) : [];
|
|
1865
|
+
const collapsedClusters = clusters.filter((c) => !expandedClusters.has(c.id));
|
|
1866
|
+
const expandedClusterList = clusters.filter((c) => expandedClusters.has(c.id));
|
|
1867
|
+
const collapsedLabels = /* @__PURE__ */ new Set();
|
|
1868
|
+
for (const c of collapsedClusters) {
|
|
1869
|
+
for (const m of c.members) collapsedLabels.add(m.level.label);
|
|
1870
|
+
}
|
|
1871
|
+
const expandedFanLayouts = /* @__PURE__ */ new Map();
|
|
1872
|
+
for (const ec of expandedClusterList) {
|
|
1873
|
+
expandedFanLayouts.set(ec.id, computeFanOutLayout(ec, boxH, 4));
|
|
1874
|
+
}
|
|
1875
|
+
const labelToExpandedCluster = /* @__PURE__ */ new Map();
|
|
1876
|
+
for (const ec of expandedClusterList) {
|
|
1877
|
+
for (const m of ec.members) labelToExpandedCluster.set(m.level.label, ec);
|
|
1878
|
+
}
|
|
1673
1879
|
measured.sort((a, b) => a.y - b.y);
|
|
1674
1880
|
const placed = [];
|
|
1675
1881
|
const layouts = /* @__PURE__ */ new Map();
|
|
1676
1882
|
for (const m of measured) {
|
|
1677
|
-
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1883
|
+
if (collapsedLabels.has(m.level.label)) continue;
|
|
1884
|
+
const ec = labelToExpandedCluster.get(m.level.label);
|
|
1885
|
+
if (ec) {
|
|
1886
|
+
const fanMap = expandedFanLayouts.get(ec.id);
|
|
1887
|
+
const fanY = fanMap?.get(m.level.label);
|
|
1888
|
+
if (fanY !== void 0) {
|
|
1889
|
+
placed.push({ label: m.level.label, boxY: fanY, boxH: m.boxH });
|
|
1890
|
+
layouts.set(m.level.label, { price: m.price, y: m.y, boxY: fanY, boxW: m.boxW, boxH: m.boxH });
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
let boxY = m.y - m.boxH / 2;
|
|
1895
|
+
let attempts = 0;
|
|
1896
|
+
while (attempts < placed.length + 1) {
|
|
1897
|
+
const overlaps = placed.some((p) => rectsOverlapY(boxY, m.boxH, p.boxY, p.boxH));
|
|
1898
|
+
if (!overlaps) break;
|
|
1899
|
+
boxY -= m.boxH + 2;
|
|
1900
|
+
attempts++;
|
|
1901
|
+
}
|
|
1680
1902
|
placed.push({ label: m.level.label, boxY, boxH: m.boxH });
|
|
1681
1903
|
layouts.set(m.level.label, { price: m.price, y: m.y, boxY, boxW: m.boxW, boxH: m.boxH });
|
|
1682
1904
|
}
|
|
@@ -1692,9 +1914,10 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1692
1914
|
selectedLabel
|
|
1693
1915
|
);
|
|
1694
1916
|
const activeLabel = hoveredLabel ?? selectedLabel;
|
|
1917
|
+
const visibleMeasured = measured.filter((m) => !collapsedLabels.has(m.level.label));
|
|
1695
1918
|
const drawOrder = [
|
|
1696
|
-
...
|
|
1697
|
-
...
|
|
1919
|
+
...visibleMeasured.filter((m) => m.level.label !== activeLabel),
|
|
1920
|
+
...visibleMeasured.filter((m) => m.level.label === activeLabel)
|
|
1698
1921
|
];
|
|
1699
1922
|
const hitAreas = [];
|
|
1700
1923
|
const editAreas = [];
|
|
@@ -1702,6 +1925,11 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1702
1925
|
const dragAreas = [];
|
|
1703
1926
|
const confirmAreas = [];
|
|
1704
1927
|
const addBracketAreas = [];
|
|
1928
|
+
const clusterBadgeAreas = [];
|
|
1929
|
+
const draftQtyPillAreas = [];
|
|
1930
|
+
for (const cluster of collapsedClusters) {
|
|
1931
|
+
drawClusterBadge(ctx, cluster, chartW, theme, clusterBadgeAreas);
|
|
1932
|
+
}
|
|
1705
1933
|
for (const { level } of drawOrder) {
|
|
1706
1934
|
const layout = layouts.get(level.label);
|
|
1707
1935
|
if (!layout) continue;
|
|
@@ -1730,11 +1958,148 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1730
1958
|
confirmAreas,
|
|
1731
1959
|
addBracketAreas,
|
|
1732
1960
|
hideConfirmCancel,
|
|
1733
|
-
level.label === draftOrderLabel ? draftBracketPnl : void 0
|
|
1961
|
+
level.label === draftOrderLabel ? draftBracketPnl : void 0,
|
|
1962
|
+
showQuantityField,
|
|
1963
|
+
draftQtyPillAreas
|
|
1734
1964
|
);
|
|
1965
|
+
const ec = labelToExpandedCluster.get(level.label);
|
|
1966
|
+
if (ec && layout.boxY !== layout.y - boxH / 2) {
|
|
1967
|
+
drawFanConnector(ctx, layout.boxY + boxH / 2, layout.y, chartW, theme);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
if (selectedLabel !== null && !layouts.has(selectedLabel)) {
|
|
1971
|
+
const offLevel = levels.find((l) => l.label === selectedLabel);
|
|
1972
|
+
if (offLevel) {
|
|
1973
|
+
const bh = offLevel;
|
|
1974
|
+
const slDragKey = `${offLevel.label}:stopLoss`;
|
|
1975
|
+
const tpDragKey = `${offLevel.label}:takeProfit`;
|
|
1976
|
+
const fallbackX = chartW * BRACKET_BASE_RATIO;
|
|
1977
|
+
if (bh.stopLossPrice !== void 0) {
|
|
1978
|
+
drawBracketLine(
|
|
1979
|
+
ctx,
|
|
1980
|
+
bh,
|
|
1981
|
+
"stopLoss",
|
|
1982
|
+
bh.stopLossPrice,
|
|
1983
|
+
scale,
|
|
1984
|
+
priceRange,
|
|
1985
|
+
priceH,
|
|
1986
|
+
bracketLineXMap.get(slDragKey) ?? fallbackX,
|
|
1987
|
+
chartW,
|
|
1988
|
+
theme,
|
|
1989
|
+
precision,
|
|
1990
|
+
draggingLabel,
|
|
1991
|
+
dragNewPrice,
|
|
1992
|
+
hitAreas,
|
|
1993
|
+
dragAreas,
|
|
1994
|
+
hideConfirmCancel,
|
|
1995
|
+
bh._slOrderLabel
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
if (bh.takeProfitPrice !== void 0) {
|
|
1999
|
+
drawBracketLine(
|
|
2000
|
+
ctx,
|
|
2001
|
+
bh,
|
|
2002
|
+
"takeProfit",
|
|
2003
|
+
bh.takeProfitPrice,
|
|
2004
|
+
scale,
|
|
2005
|
+
priceRange,
|
|
2006
|
+
priceH,
|
|
2007
|
+
bracketLineXMap.get(tpDragKey) ?? fallbackX,
|
|
2008
|
+
chartW,
|
|
2009
|
+
theme,
|
|
2010
|
+
precision,
|
|
2011
|
+
draggingLabel,
|
|
2012
|
+
dragNewPrice,
|
|
2013
|
+
hitAreas,
|
|
2014
|
+
dragAreas,
|
|
2015
|
+
hideConfirmCancel,
|
|
2016
|
+
bh._tpOrderLabel
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
1735
2020
|
}
|
|
1736
2021
|
ctx.restore();
|
|
1737
|
-
return {
|
|
2022
|
+
return {
|
|
2023
|
+
hitAreas,
|
|
2024
|
+
editAreas,
|
|
2025
|
+
hoverAreas,
|
|
2026
|
+
dragAreas,
|
|
2027
|
+
confirmAreas,
|
|
2028
|
+
addBracketAreas,
|
|
2029
|
+
clusterBadgeAreas,
|
|
2030
|
+
clusters,
|
|
2031
|
+
draftQtyPillArea: draftQtyPillAreas[0]
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
var BADGE_FONT = "bold 12px 'Inter', system-ui, sans-serif";
|
|
2035
|
+
var BADGE_PAD_X = 12;
|
|
2036
|
+
var BADGE_H = 24;
|
|
2037
|
+
var BADGE_RADIUS = 12;
|
|
2038
|
+
function drawClusterBadge(ctx, cluster, chartW, theme, clusterBadgeAreas) {
|
|
2039
|
+
const mono = monoColor(theme);
|
|
2040
|
+
const isDark = theme.background !== "#ffffff";
|
|
2041
|
+
ctx.save();
|
|
2042
|
+
ctx.strokeStyle = mono;
|
|
2043
|
+
ctx.globalAlpha = 0.12;
|
|
2044
|
+
ctx.lineWidth = 1;
|
|
2045
|
+
ctx.setLineDash([4, 4]);
|
|
2046
|
+
for (const m of cluster.members) {
|
|
2047
|
+
ctx.beginPath();
|
|
2048
|
+
ctx.moveTo(0, m.y);
|
|
2049
|
+
ctx.lineTo(chartW, m.y);
|
|
2050
|
+
ctx.stroke();
|
|
2051
|
+
}
|
|
2052
|
+
ctx.setLineDash([]);
|
|
2053
|
+
ctx.restore();
|
|
2054
|
+
ctx.save();
|
|
2055
|
+
ctx.font = BADGE_FONT;
|
|
2056
|
+
const layout = buildClusterBadgeLayout(cluster, chartW, ctx);
|
|
2057
|
+
const { badgeX, badgeY, badgeW, text } = layout;
|
|
2058
|
+
const expandIcon = " \u25B8";
|
|
2059
|
+
const bgColor = isDark ? "rgba(40,40,40,0.92)" : "rgba(240,240,240,0.92)";
|
|
2060
|
+
ctx.fillStyle = bgColor;
|
|
2061
|
+
ctx.beginPath();
|
|
2062
|
+
ctx.roundRect(badgeX, badgeY, badgeW, BADGE_H, BADGE_RADIUS);
|
|
2063
|
+
ctx.fill();
|
|
2064
|
+
ctx.strokeStyle = mono;
|
|
2065
|
+
ctx.globalAlpha = 0.3;
|
|
2066
|
+
ctx.lineWidth = 1;
|
|
2067
|
+
ctx.stroke();
|
|
2068
|
+
ctx.globalAlpha = 1;
|
|
2069
|
+
ctx.fillStyle = mono;
|
|
2070
|
+
ctx.globalAlpha = 0.85;
|
|
2071
|
+
ctx.textBaseline = "middle";
|
|
2072
|
+
ctx.textAlign = "left";
|
|
2073
|
+
ctx.fillText(text + expandIcon, badgeX + BADGE_PAD_X, badgeY + BADGE_H / 2);
|
|
2074
|
+
ctx.globalAlpha = 1;
|
|
2075
|
+
ctx.restore();
|
|
2076
|
+
clusterBadgeAreas.push({
|
|
2077
|
+
clusterId: cluster.id,
|
|
2078
|
+
x: badgeX,
|
|
2079
|
+
y: badgeY,
|
|
2080
|
+
w: badgeW,
|
|
2081
|
+
h: BADGE_H
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
function drawFanConnector(ctx, fanCenterY, realY, chartW, theme) {
|
|
2085
|
+
if (Math.abs(fanCenterY - realY) < 2) return;
|
|
2086
|
+
const mono = monoColor(theme);
|
|
2087
|
+
ctx.save();
|
|
2088
|
+
ctx.strokeStyle = mono;
|
|
2089
|
+
ctx.globalAlpha = 0.25;
|
|
2090
|
+
ctx.lineWidth = 1;
|
|
2091
|
+
ctx.setLineDash([2, 3]);
|
|
2092
|
+
const connectorX = chartW - 8;
|
|
2093
|
+
ctx.beginPath();
|
|
2094
|
+
ctx.moveTo(connectorX, fanCenterY);
|
|
2095
|
+
ctx.lineTo(connectorX, realY);
|
|
2096
|
+
ctx.stroke();
|
|
2097
|
+
ctx.setLineDash([]);
|
|
2098
|
+
ctx.beginPath();
|
|
2099
|
+
ctx.moveTo(connectorX - 4, realY);
|
|
2100
|
+
ctx.lineTo(connectorX + 4, realY);
|
|
2101
|
+
ctx.stroke();
|
|
2102
|
+
ctx.restore();
|
|
1738
2103
|
}
|
|
1739
2104
|
function drawDotHoverBox(ctx, level, y, dotX, dotR, theme, precision) {
|
|
1740
2105
|
const pl = level;
|
|
@@ -1794,7 +2159,7 @@ function drawDotHoverBox(ctx, level, y, dotX, dotR, theme, precision) {
|
|
|
1794
2159
|
});
|
|
1795
2160
|
ctx.restore();
|
|
1796
2161
|
}
|
|
1797
|
-
function drawLevel(ctx, level, layout, chartW, colors, theme, precision, hovered, selected, scale, priceRange, priceH, draggingLabel, dragNewPrice, bracketLineXMap, hasPendingChanges, positionStyle, hitAreas, editAreas, hoverAreas, dragAreas, confirmAreas, addBracketAreas, hideConfirmCancel = false, draftBracketPnl) {
|
|
2162
|
+
function drawLevel(ctx, level, layout, chartW, colors, theme, precision, hovered, selected, scale, priceRange, priceH, draggingLabel, dragNewPrice, bracketLineXMap, hasPendingChanges, positionStyle, hitAreas, editAreas, hoverAreas, dragAreas, confirmAreas, addBracketAreas, hideConfirmCancel = false, draftBracketPnl, showQuantityField = false, draftQtyPillAreas) {
|
|
1798
2163
|
const { y, boxY, boxW, boxH } = layout;
|
|
1799
2164
|
const color = levelColor(level, colors, theme);
|
|
1800
2165
|
const active = hovered || selected;
|
|
@@ -1982,6 +2347,29 @@ function drawLevel(ctx, level, layout, chartW, colors, theme, precision, hovered
|
|
|
1982
2347
|
ctx.fill();
|
|
1983
2348
|
ctx.stroke();
|
|
1984
2349
|
let textX = boxX + BOX_PAD_X;
|
|
2350
|
+
const isDraftLevel = level.data?._isDraft === true;
|
|
2351
|
+
if (showQuantityField && isDraftLevel && level.type === "pending" && level.lots !== void 0) {
|
|
2352
|
+
const qtyText = String(level.lots);
|
|
2353
|
+
ctx.font = FONT_MAIN;
|
|
2354
|
+
const pillContentW = ctx.measureText(qtyText).width;
|
|
2355
|
+
const pillW = PILL_PAD_X + pillContentW + PILL_PAD_X;
|
|
2356
|
+
const pillX = textX;
|
|
2357
|
+
ctx.save();
|
|
2358
|
+
ctx.strokeStyle = monoColor(theme);
|
|
2359
|
+
ctx.globalAlpha = 0.35;
|
|
2360
|
+
ctx.lineWidth = 1;
|
|
2361
|
+
ctx.beginPath();
|
|
2362
|
+
ctx.moveTo(pillX + pillW, boxY + 3);
|
|
2363
|
+
ctx.lineTo(pillX + pillW, boxY + boxH - 3);
|
|
2364
|
+
ctx.stroke();
|
|
2365
|
+
ctx.restore();
|
|
2366
|
+
ctx.fillStyle = monoColor(theme);
|
|
2367
|
+
ctx.textAlign = "left";
|
|
2368
|
+
ctx.textBaseline = "middle";
|
|
2369
|
+
ctx.fillText(qtyText, pillX + PILL_PAD_X, midY);
|
|
2370
|
+
draftQtyPillAreas?.push({ label: level.label, pillX, pillY: boxY, pillW, pillH: boxH });
|
|
2371
|
+
textX += pillW + PILL_GAP;
|
|
2372
|
+
}
|
|
1985
2373
|
if (pnlText) {
|
|
1986
2374
|
const pnlColor = pnl !== void 0 ? pnl >= 0 ? theme.candle.up : theme.candle.down : monoColor(theme);
|
|
1987
2375
|
ctx.font = FONT_PNL;
|
|
@@ -2365,6 +2753,152 @@ function hitTestTradeLevelAddBracket(x, y, areas) {
|
|
|
2365
2753
|
return areas.find((a) => x >= a.x && x <= a.x + a.w && y >= a.y && y <= a.y + a.h) ?? null;
|
|
2366
2754
|
}
|
|
2367
2755
|
|
|
2756
|
+
// src/overlays/OffViewportLevelIndicator.ts
|
|
2757
|
+
function collectOffViewportMarkers(levels, priceRange, draggingLabel) {
|
|
2758
|
+
const above = [];
|
|
2759
|
+
const below = [];
|
|
2760
|
+
const { min, max } = priceRange;
|
|
2761
|
+
for (const level of levels) {
|
|
2762
|
+
if (level.label === draggingLabel) continue;
|
|
2763
|
+
const isDraftMarket = level.type === "pending" && level.orderType === "market" && level.data?._isDraft === true;
|
|
2764
|
+
if (isDraftMarket) continue;
|
|
2765
|
+
if (isFinite(level.price)) {
|
|
2766
|
+
if (level.price > max) above.push({ price: level.price, label: level.label, kind: "entry" });
|
|
2767
|
+
else if (level.price < min) below.push({ price: level.price, label: level.label, kind: "entry" });
|
|
2768
|
+
}
|
|
2769
|
+
const slPrice = level.stopLossPrice;
|
|
2770
|
+
const tpPrice = level.takeProfitPrice;
|
|
2771
|
+
if (slPrice !== void 0 && isFinite(slPrice)) {
|
|
2772
|
+
if (slPrice > max) above.push({ price: slPrice, label: level.label, kind: "sl" });
|
|
2773
|
+
else if (slPrice < min) below.push({ price: slPrice, label: level.label, kind: "sl" });
|
|
2774
|
+
}
|
|
2775
|
+
if (tpPrice !== void 0 && isFinite(tpPrice)) {
|
|
2776
|
+
if (tpPrice > max) above.push({ price: tpPrice, label: level.label, kind: "tp" });
|
|
2777
|
+
else if (tpPrice < min) below.push({ price: tpPrice, label: level.label, kind: "tp" });
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
return { above, below };
|
|
2781
|
+
}
|
|
2782
|
+
function entryCount(markers) {
|
|
2783
|
+
let n = 0;
|
|
2784
|
+
for (const m of markers) if (m.kind === "entry") n++;
|
|
2785
|
+
return n;
|
|
2786
|
+
}
|
|
2787
|
+
function nearestEdgeMarker(markers, edge) {
|
|
2788
|
+
let best = markers[0];
|
|
2789
|
+
let bestDist = Math.abs(best.price - edge);
|
|
2790
|
+
for (let i = 1; i < markers.length; i++) {
|
|
2791
|
+
const d = Math.abs(markers[i].price - edge);
|
|
2792
|
+
if (d < bestDist) {
|
|
2793
|
+
best = markers[i];
|
|
2794
|
+
bestDist = d;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
return best;
|
|
2798
|
+
}
|
|
2799
|
+
var PILL_FONT = "11px 'Inter', system-ui, sans-serif";
|
|
2800
|
+
var PILL_PAD_X2 = 8;
|
|
2801
|
+
var PILL_RIGHT_MARGIN = 8;
|
|
2802
|
+
var PILL_V_MARGIN = 10;
|
|
2803
|
+
var PILL_H = 20;
|
|
2804
|
+
var PILL_RADIUS = 10;
|
|
2805
|
+
var ARROW_UP = "\u25B2";
|
|
2806
|
+
var ARROW_DOWN = "\u25BC";
|
|
2807
|
+
function renderOffViewportIndicators(ctx, levels, priceRange, theme, chartWidth, chartHeight, draggingLabel, selectedLabel = null) {
|
|
2808
|
+
const hitAreas = [];
|
|
2809
|
+
const PADDING_PX = 20;
|
|
2810
|
+
const usable = chartHeight - 2 * PADDING_PX;
|
|
2811
|
+
const span = priceRange.max - priceRange.min;
|
|
2812
|
+
const padInPrice = usable > 0 ? span / usable * PADDING_PX : 0;
|
|
2813
|
+
const visualRange = {
|
|
2814
|
+
min: priceRange.min - padInPrice,
|
|
2815
|
+
max: priceRange.max + padInPrice
|
|
2816
|
+
};
|
|
2817
|
+
const { above, below } = collectOffViewportMarkers(levels, visualRange, draggingLabel);
|
|
2818
|
+
if (above.length === 0 && below.length === 0) return { hitAreas };
|
|
2819
|
+
ctx.save();
|
|
2820
|
+
ctx.font = PILL_FONT;
|
|
2821
|
+
ctx.textBaseline = "middle";
|
|
2822
|
+
ctx.textAlign = "center";
|
|
2823
|
+
const PILL_GAP2 = 4;
|
|
2824
|
+
const renderPill = (direction, text, y, target) => {
|
|
2825
|
+
const textW = ctx.measureText(text).width;
|
|
2826
|
+
const w = Math.ceil(textW) + PILL_PAD_X2 * 2;
|
|
2827
|
+
const x = chartWidth - PILL_RIGHT_MARGIN - w;
|
|
2828
|
+
ctx.fillStyle = theme.tooltip.background;
|
|
2829
|
+
ctx.strokeStyle = theme.tooltip.border;
|
|
2830
|
+
ctx.lineWidth = 1;
|
|
2831
|
+
roundedRect(ctx, x, y, w, PILL_H, PILL_RADIUS);
|
|
2832
|
+
ctx.fill();
|
|
2833
|
+
ctx.stroke();
|
|
2834
|
+
ctx.fillStyle = theme.tooltip.text;
|
|
2835
|
+
ctx.fillText(text, x + w / 2, y + PILL_H / 2 + 0.5);
|
|
2836
|
+
hitAreas.push({
|
|
2837
|
+
direction,
|
|
2838
|
+
x,
|
|
2839
|
+
y,
|
|
2840
|
+
w,
|
|
2841
|
+
h: PILL_H,
|
|
2842
|
+
targetPrice: target.price,
|
|
2843
|
+
targetLabel: target.label,
|
|
2844
|
+
targetKind: target.kind
|
|
2845
|
+
});
|
|
2846
|
+
return w;
|
|
2847
|
+
};
|
|
2848
|
+
const drawColumn = (direction, markers, baseY) => {
|
|
2849
|
+
if (markers.length === 0) return;
|
|
2850
|
+
const arrow = direction === "up" ? ARROW_UP : ARROW_DOWN;
|
|
2851
|
+
const edge = direction === "up" ? priceRange.max : priceRange.min;
|
|
2852
|
+
const stackDir = direction === "up" ? 1 : -1;
|
|
2853
|
+
let nextY = baseY;
|
|
2854
|
+
const entries = entryCount(markers);
|
|
2855
|
+
if (entries > 0) {
|
|
2856
|
+
const target = nearestEdgeMarker(
|
|
2857
|
+
markers.filter((m) => m.kind === "entry"),
|
|
2858
|
+
edge
|
|
2859
|
+
);
|
|
2860
|
+
renderPill(direction, `${arrow} ${entries}`, nextY, target);
|
|
2861
|
+
nextY += stackDir * (PILL_H + PILL_GAP2);
|
|
2862
|
+
}
|
|
2863
|
+
if (selectedLabel !== null) {
|
|
2864
|
+
const bracketMarkers = markers.filter(
|
|
2865
|
+
(m) => m.kind !== "entry" && m.label === selectedLabel
|
|
2866
|
+
);
|
|
2867
|
+
for (const bm of bracketMarkers) {
|
|
2868
|
+
const kindLabel = bm.kind.toUpperCase();
|
|
2869
|
+
renderPill(direction, `${arrow} ${kindLabel}`, nextY, bm);
|
|
2870
|
+
nextY += stackDir * (PILL_H + PILL_GAP2);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
};
|
|
2874
|
+
drawColumn("up", above, PILL_V_MARGIN);
|
|
2875
|
+
drawColumn("down", below, chartHeight - PILL_V_MARGIN - PILL_H);
|
|
2876
|
+
ctx.restore();
|
|
2877
|
+
return { hitAreas };
|
|
2878
|
+
}
|
|
2879
|
+
function hitTestOffViewportIndicator(x, y, hitAreas) {
|
|
2880
|
+
return hitAreas.find((h) => x >= h.x && x <= h.x + h.w && y >= h.y && y <= h.y + h.h) ?? null;
|
|
2881
|
+
}
|
|
2882
|
+
function overlapsOffViewportPill(centerY, halfH, hitAreas) {
|
|
2883
|
+
const top = centerY - halfH;
|
|
2884
|
+
const bottom = centerY + halfH;
|
|
2885
|
+
return hitAreas.some((h) => top < h.y + h.h && bottom > h.y);
|
|
2886
|
+
}
|
|
2887
|
+
function roundedRect(ctx, x, y, w, h, r) {
|
|
2888
|
+
const rr = Math.min(r, w / 2, h / 2);
|
|
2889
|
+
ctx.beginPath();
|
|
2890
|
+
ctx.moveTo(x + rr, y);
|
|
2891
|
+
ctx.lineTo(x + w - rr, y);
|
|
2892
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + rr);
|
|
2893
|
+
ctx.lineTo(x + w, y + h - rr);
|
|
2894
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - rr, y + h);
|
|
2895
|
+
ctx.lineTo(x + rr, y + h);
|
|
2896
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - rr);
|
|
2897
|
+
ctx.lineTo(x, y + rr);
|
|
2898
|
+
ctx.quadraticCurveTo(x, y, x + rr, y);
|
|
2899
|
+
ctx.closePath();
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2368
2902
|
// src/axis/TimeAxis.ts
|
|
2369
2903
|
var COUNTDOWN_LABEL_HEIGHT = 18;
|
|
2370
2904
|
var COUNTDOWN_LABEL_PADDING_X = 6;
|
|
@@ -2391,45 +2925,45 @@ function formatRemaining(totalSec) {
|
|
|
2391
2925
|
const d = Math.floor(totalSec % 604800 / 86400);
|
|
2392
2926
|
return `${w}w ${d}d`;
|
|
2393
2927
|
}
|
|
2394
|
-
function formatTimestamp(ts, visibleBars, timeframe, spansMultipleDays) {
|
|
2928
|
+
function formatTimestamp(ts, visibleBars, timeframe, spansMultipleDays, timezone = "UTC") {
|
|
2395
2929
|
const d = new Date(ts);
|
|
2396
2930
|
if (timeframe && INTRADAY_TIMEFRAMES.includes(timeframe)) {
|
|
2397
2931
|
const time = d.toLocaleTimeString("en-US", {
|
|
2398
2932
|
hour: "2-digit",
|
|
2399
2933
|
minute: "2-digit",
|
|
2400
2934
|
hour12: false,
|
|
2401
|
-
timeZone:
|
|
2935
|
+
timeZone: timezone
|
|
2402
2936
|
});
|
|
2403
2937
|
if (spansMultipleDays) {
|
|
2404
|
-
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone:
|
|
2938
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: timezone }) + " " + time;
|
|
2405
2939
|
}
|
|
2406
2940
|
return time;
|
|
2407
2941
|
}
|
|
2408
2942
|
if (timeframe === "1Y") {
|
|
2409
|
-
return d.toLocaleDateString("en-US", { year: "numeric", timeZone:
|
|
2943
|
+
return d.toLocaleDateString("en-US", { year: "numeric", timeZone: timezone });
|
|
2410
2944
|
}
|
|
2411
2945
|
if (timeframe === "1M") {
|
|
2412
|
-
return d.toLocaleDateString("en-US", { month: "short", year: "numeric", timeZone:
|
|
2946
|
+
return d.toLocaleDateString("en-US", { month: "short", year: "numeric", timeZone: timezone });
|
|
2413
2947
|
}
|
|
2414
2948
|
if (visibleBars <= 365) {
|
|
2415
2949
|
return d.toLocaleDateString("en-US", {
|
|
2416
2950
|
month: "short",
|
|
2417
2951
|
day: "numeric",
|
|
2418
|
-
timeZone:
|
|
2952
|
+
timeZone: timezone
|
|
2419
2953
|
});
|
|
2420
2954
|
}
|
|
2421
|
-
return d.toLocaleDateString("en-US", { month: "short", year: "2-digit", timeZone:
|
|
2955
|
+
return d.toLocaleDateString("en-US", { month: "short", year: "2-digit", timeZone: timezone });
|
|
2422
2956
|
}
|
|
2423
|
-
function renderTimeAxis(ctx, bars, viewport, scale, theme, chartWidth, axisY, maxLabels = 8, cfg = DEFAULT_TIME_AXIS_CONFIG, timeframe, countdownBarIndex) {
|
|
2957
|
+
function renderTimeAxis(ctx, bars, viewport, scale, theme, chartWidth, axisY, maxLabels = 8, cfg = DEFAULT_TIME_AXIS_CONFIG, timeframe, countdownBarIndex, timezone = "UTC") {
|
|
2424
2958
|
const visibleBars = viewport.endIndex - viewport.startIndex;
|
|
2425
2959
|
const firstBar = bars[0];
|
|
2426
2960
|
const lastBar = bars[bars.length - 1];
|
|
2427
|
-
const spansMultipleDays = !!firstBar && !!lastBar && new Date(firstBar.time).toLocaleDateString("en-US", { timeZone:
|
|
2961
|
+
const spansMultipleDays = !!firstBar && !!lastBar && new Date(firstBar.time).toLocaleDateString("en-US", { timeZone: timezone }) !== new Date(lastBar.time).toLocaleDateString("en-US", { timeZone: timezone });
|
|
2428
2962
|
ctx.fillStyle = theme.axisText;
|
|
2429
2963
|
ctx.font = `${cfg.fontSize} ${cfg.fontFamily}`;
|
|
2430
2964
|
ctx.textAlign = "center";
|
|
2431
2965
|
const MIN_GAP = 12;
|
|
2432
|
-
const sampleLabel = formatTimestamp(bars[Math.floor(bars.length / 2)]?.time ?? bars[0].time, visibleBars, timeframe, spansMultipleDays);
|
|
2966
|
+
const sampleLabel = formatTimestamp(bars[Math.floor(bars.length / 2)]?.time ?? bars[0].time, visibleBars, timeframe, spansMultipleDays, timezone);
|
|
2433
2967
|
const labelW = ctx.measureText(sampleLabel).width;
|
|
2434
2968
|
const minStep = Math.max(1, Math.floor(bars.length / maxLabels));
|
|
2435
2969
|
let step = minStep;
|
|
@@ -2444,7 +2978,7 @@ function renderTimeAxis(ctx, bars, viewport, scale, theme, chartWidth, axisY, ma
|
|
|
2444
2978
|
const barIndex = viewport.startIndex + i;
|
|
2445
2979
|
if (countdownBarIndex !== void 0 && barIndex === countdownBarIndex) continue;
|
|
2446
2980
|
const x = scale.xToPixel(barIndex, viewport, chartWidth);
|
|
2447
|
-
const label = formatTimestamp(bars[i].time, visibleBars, timeframe, spansMultipleDays);
|
|
2981
|
+
const label = formatTimestamp(bars[i].time, visibleBars, timeframe, spansMultipleDays, timezone);
|
|
2448
2982
|
const halfW = ctx.measureText(label).width / 2;
|
|
2449
2983
|
const clampedX = Math.max(halfW, Math.min(chartWidth - halfW, x));
|
|
2450
2984
|
ctx.fillText(label, clampedX, axisY + 14);
|
|
@@ -2479,10 +3013,20 @@ function renderCandleCountdown(ctx, lastBarTime, intervalMs, lastBarIndex, viewp
|
|
|
2479
3013
|
}
|
|
2480
3014
|
|
|
2481
3015
|
// src/axis/PriceAxis.ts
|
|
2482
|
-
function renderPriceAxis(ctx, priceRange, scale, theme, chartWidth, chartHeight, axisWidth, gridLines
|
|
3016
|
+
function renderPriceAxis(ctx, priceRange, scale, theme, chartWidth, chartHeight, axisWidth, gridLines, pricePrecision = 2, cfg = DEFAULT_PRICE_AXIS_CONFIG) {
|
|
2483
3017
|
ctx.fillStyle = theme.axisText;
|
|
2484
3018
|
ctx.font = `${cfg.fontSize} ${cfg.fontFamily}`;
|
|
2485
3019
|
ctx.textAlign = "left";
|
|
3020
|
+
if (gridLines === void 0) {
|
|
3021
|
+
const ticks = generatePriceTicks(priceRange, chartHeight);
|
|
3022
|
+
const step2 = ticks.length >= 2 ? ticks[1] - ticks[0] : priceRange.max - priceRange.min;
|
|
3023
|
+
const precision = precisionForStep(step2, pricePrecision);
|
|
3024
|
+
for (const price of ticks) {
|
|
3025
|
+
const y = scale.yToPixel(price, priceRange, chartHeight);
|
|
3026
|
+
ctx.fillText(price.toFixed(precision), chartWidth + 4, y + 4);
|
|
3027
|
+
}
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
2486
3030
|
const step = (priceRange.max - priceRange.min) / gridLines;
|
|
2487
3031
|
for (let i = 0; i <= gridLines; i++) {
|
|
2488
3032
|
const price = priceRange.min + step * i;
|
|
@@ -3097,6 +3641,8 @@ var ICON_TOOL = {
|
|
|
3097
3641
|
14
|
|
3098
3642
|
)
|
|
3099
3643
|
};
|
|
3644
|
+
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>`;
|
|
3645
|
+
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
3646
|
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
3647
|
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
3648
|
var ICON_TAB = {
|
|
@@ -3132,7 +3678,7 @@ function injectFlyoutScrollbarStyles() {
|
|
|
3132
3678
|
document.head.appendChild(style);
|
|
3133
3679
|
}
|
|
3134
3680
|
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) {
|
|
3681
|
+
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
3682
|
this.onSeriesChange = onSeriesChange;
|
|
3137
3683
|
this.onTimeframeChange = onTimeframeChange;
|
|
3138
3684
|
this.onAddIndicator = onAddIndicator;
|
|
@@ -3140,11 +3686,14 @@ var TopBar = class {
|
|
|
3140
3686
|
this.onSettingsClick = onSettingsClick;
|
|
3141
3687
|
this.onFullscreenToggle = onFullscreenToggle;
|
|
3142
3688
|
this.onDrawingToolsToggle = onDrawingToolsToggle;
|
|
3689
|
+
this.onTfcToggle = onTfcToggle;
|
|
3143
3690
|
this.settingsBtn = null;
|
|
3144
3691
|
this.isFullscreen = false;
|
|
3145
3692
|
this.durationBtns = /* @__PURE__ */ new Map();
|
|
3146
3693
|
this.isMobileLayout = false;
|
|
3147
3694
|
this.drawingBtn = null;
|
|
3695
|
+
this.tfcBtn = null;
|
|
3696
|
+
this.tfcActive = true;
|
|
3148
3697
|
this.openDropdown = null;
|
|
3149
3698
|
this.indicatorOptions = [];
|
|
3150
3699
|
this.activeIndicators = /* @__PURE__ */ new Set();
|
|
@@ -3152,6 +3701,7 @@ var TopBar = class {
|
|
|
3152
3701
|
this.theme = theme;
|
|
3153
3702
|
this.cfg = cfg;
|
|
3154
3703
|
this.labels = labels;
|
|
3704
|
+
this.tfcActive = initialTfcActive;
|
|
3155
3705
|
this.currentSeries = initialSeries;
|
|
3156
3706
|
this.currentTf = initialTf;
|
|
3157
3707
|
this.activeDuration = initialDuration;
|
|
@@ -3228,6 +3778,21 @@ var TopBar = class {
|
|
|
3228
3778
|
this.addBtnHover(this.settingsBtn);
|
|
3229
3779
|
this.rightGroup.appendChild(this.settingsBtn);
|
|
3230
3780
|
}
|
|
3781
|
+
if (showTfcBtn) {
|
|
3782
|
+
this.tfcBtn = document.createElement("button");
|
|
3783
|
+
this.tfcBtn.title = this.labels.tfcToggleTitle;
|
|
3784
|
+
this.tfcBtn.innerHTML = this.tfcActive ? ICON_TFC : ICON_TFC_OFF;
|
|
3785
|
+
this.applyIconBtnStyles(this.tfcBtn, theme, this.tfcActive);
|
|
3786
|
+
this.tfcBtn.addEventListener("click", (e) => {
|
|
3787
|
+
e.stopPropagation();
|
|
3788
|
+
this.tfcActive = !this.tfcActive;
|
|
3789
|
+
this.tfcBtn.innerHTML = this.tfcActive ? ICON_TFC : ICON_TFC_OFF;
|
|
3790
|
+
this.applyIconBtnStyles(this.tfcBtn, this.theme, this.tfcActive);
|
|
3791
|
+
this.onTfcToggle?.(this.tfcActive);
|
|
3792
|
+
});
|
|
3793
|
+
this.addBtnHover(this.tfcBtn);
|
|
3794
|
+
this.rightGroup.appendChild(this.tfcBtn);
|
|
3795
|
+
}
|
|
3231
3796
|
this.el.append(this.leftGroup, this.rightGroup);
|
|
3232
3797
|
this.docClickHandler = (e) => {
|
|
3233
3798
|
if (!this.el.contains(e.target) && !this.flyout.contains(e.target)) {
|
|
@@ -3257,6 +3822,15 @@ var TopBar = class {
|
|
|
3257
3822
|
if (!this.drawingBtn) return;
|
|
3258
3823
|
this.applyIconBtnStyles(this.drawingBtn, this.theme, active);
|
|
3259
3824
|
}
|
|
3825
|
+
setTfcActive(active) {
|
|
3826
|
+
if (!this.tfcBtn) return;
|
|
3827
|
+
this.tfcActive = active;
|
|
3828
|
+
this.tfcBtn.innerHTML = active ? ICON_TFC : ICON_TFC_OFF;
|
|
3829
|
+
this.applyIconBtnStyles(this.tfcBtn, this.theme, active);
|
|
3830
|
+
if (this.isMobileLayout) {
|
|
3831
|
+
this.resizeSvg(this.tfcBtn, this.getSvgSize(this.tfcBtn) + 2);
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3260
3834
|
setTheme(theme) {
|
|
3261
3835
|
this.theme = theme;
|
|
3262
3836
|
this.closeDropdown();
|
|
@@ -3265,6 +3839,7 @@ var TopBar = class {
|
|
|
3265
3839
|
if (this.drawingBtn) this.applyIconBtnStyles(this.drawingBtn, theme, false);
|
|
3266
3840
|
this.applyIconBtnStyles(this.fullscreenBtn, theme, this.isFullscreen);
|
|
3267
3841
|
if (this.settingsBtn) this.applyIconBtnStyles(this.settingsBtn, theme, false);
|
|
3842
|
+
if (this.tfcBtn) this.applyIconBtnStyles(this.tfcBtn, theme, this.tfcActive);
|
|
3268
3843
|
this.applySeriesBtnStyles(this.seriesBtn, theme, false);
|
|
3269
3844
|
this.applyDropBtnStyles(this.tfBtn, theme, false);
|
|
3270
3845
|
this.applyDropBtnStyles(this.indBtn, theme, false);
|
|
@@ -3305,7 +3880,7 @@ var TopBar = class {
|
|
|
3305
3880
|
this.durationSep.style.display = btnDisplay;
|
|
3306
3881
|
this.durationDropBtn.style.display = isMobile ? "" : "none";
|
|
3307
3882
|
const containerSize = isMobile ? this.cfg.mobileIconBtnSize : this.cfg.iconBtnSize;
|
|
3308
|
-
for (const btn of [this.fullscreenBtn, this.settingsBtn, this.drawingBtn]) {
|
|
3883
|
+
for (const btn of [this.fullscreenBtn, this.settingsBtn, this.drawingBtn, this.tfcBtn]) {
|
|
3309
3884
|
if (!btn) continue;
|
|
3310
3885
|
btn.style.width = containerSize;
|
|
3311
3886
|
btn.style.height = containerSize;
|
|
@@ -3315,6 +3890,9 @@ var TopBar = class {
|
|
|
3315
3890
|
if (this.settingsBtn) {
|
|
3316
3891
|
this.resizeSvg(this.settingsBtn, this.getSvgSize(this.settingsBtn) + delta);
|
|
3317
3892
|
}
|
|
3893
|
+
if (this.tfcBtn) {
|
|
3894
|
+
this.resizeSvg(this.tfcBtn, this.getSvgSize(this.tfcBtn) + delta);
|
|
3895
|
+
}
|
|
3318
3896
|
if (this.drawingBtn) {
|
|
3319
3897
|
const drawIconSize = isMobile ? parseInt(this.cfg.mobileDrawBtnIconSize, 10) : parseInt(this.cfg.drawBtnIconFontSize, 10);
|
|
3320
3898
|
this.resizeSvg(this.drawingBtn, drawIconSize);
|
|
@@ -8143,7 +8721,7 @@ function pointToSegmentDist19(px, py, x1, y1, x2, y2) {
|
|
|
8143
8721
|
const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lenSq));
|
|
8144
8722
|
return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy));
|
|
8145
8723
|
}
|
|
8146
|
-
function
|
|
8724
|
+
function roundedRect2(ctx, x, y, w, h, r) {
|
|
8147
8725
|
ctx.beginPath();
|
|
8148
8726
|
ctx.moveTo(x + r, y);
|
|
8149
8727
|
ctx.lineTo(x + w - r, y);
|
|
@@ -8194,7 +8772,7 @@ var Callout = class {
|
|
|
8194
8772
|
ctx.beginPath();
|
|
8195
8773
|
ctx.arc(tipX, tipY, 3, 0, Math.PI * 2);
|
|
8196
8774
|
ctx.fill();
|
|
8197
|
-
|
|
8775
|
+
roundedRect2(ctx, bx, by, boxW, BOX_H, RADIUS);
|
|
8198
8776
|
ctx.fillStyle = "#37474f";
|
|
8199
8777
|
ctx.fill();
|
|
8200
8778
|
ctx.strokeStyle = "#546e7a";
|
|
@@ -8246,7 +8824,7 @@ var Callout = class {
|
|
|
8246
8824
|
// src/drawings/PriceLabel.ts
|
|
8247
8825
|
var HIT_TOLERANCE42 = 6;
|
|
8248
8826
|
var BADGE_W = 60;
|
|
8249
|
-
var
|
|
8827
|
+
var BADGE_H2 = 18;
|
|
8250
8828
|
var ARROW_W = 7;
|
|
8251
8829
|
var PriceLabel = class {
|
|
8252
8830
|
constructor(point) {
|
|
@@ -8263,21 +8841,21 @@ var PriceLabel = class {
|
|
|
8263
8841
|
ctx.fillStyle = "#26a69a";
|
|
8264
8842
|
ctx.beginPath();
|
|
8265
8843
|
ctx.moveTo(x, y);
|
|
8266
|
-
ctx.lineTo(x + ARROW_W, y -
|
|
8267
|
-
ctx.lineTo(x + ARROW_W, y +
|
|
8844
|
+
ctx.lineTo(x + ARROW_W, y - BADGE_H2 / 2);
|
|
8845
|
+
ctx.lineTo(x + ARROW_W, y + BADGE_H2 / 2);
|
|
8268
8846
|
ctx.closePath();
|
|
8269
8847
|
ctx.fill();
|
|
8270
8848
|
const bx = x + ARROW_W;
|
|
8271
|
-
const by = y -
|
|
8849
|
+
const by = y - BADGE_H2 / 2;
|
|
8272
8850
|
ctx.fillStyle = "#26a69a";
|
|
8273
8851
|
ctx.beginPath();
|
|
8274
|
-
ctx.roundRect(bx, by, BADGE_W,
|
|
8852
|
+
ctx.roundRect(bx, by, BADGE_W, BADGE_H2, BADGE_H2 / 2);
|
|
8275
8853
|
ctx.fill();
|
|
8276
8854
|
if (isSelected) {
|
|
8277
8855
|
ctx.strokeStyle = "#80cbc4";
|
|
8278
8856
|
ctx.lineWidth = 1.5;
|
|
8279
8857
|
ctx.beginPath();
|
|
8280
|
-
ctx.roundRect(bx - 2, by - 2, BADGE_W + 4,
|
|
8858
|
+
ctx.roundRect(bx - 2, by - 2, BADGE_W + 4, BADGE_H2 + 4, BADGE_H2 / 2 + 2);
|
|
8281
8859
|
ctx.stroke();
|
|
8282
8860
|
}
|
|
8283
8861
|
ctx.font = "11px 'Inter', system-ui, sans-serif";
|
|
@@ -8291,8 +8869,8 @@ var PriceLabel = class {
|
|
|
8291
8869
|
const ax = scale.xToPixel(this.points[0].barIndex, viewport, chartW);
|
|
8292
8870
|
const ay = scale.yToPixel(this.points[0].price, priceRange, chartH);
|
|
8293
8871
|
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 +
|
|
8872
|
+
const by = ay - BADGE_H2 / 2;
|
|
8873
|
+
return x >= ax - HIT_TOLERANCE42 && x <= bx + BADGE_W + HIT_TOLERANCE42 && y >= by - HIT_TOLERANCE42 && y <= by + BADGE_H2 + HIT_TOLERANCE42;
|
|
8296
8874
|
}
|
|
8297
8875
|
getHandle() {
|
|
8298
8876
|
return null;
|
|
@@ -8412,7 +8990,7 @@ var Flag = class {
|
|
|
8412
8990
|
|
|
8413
8991
|
// src/drawings/PriceNote.ts
|
|
8414
8992
|
var HIT_TOLERANCE44 = 6;
|
|
8415
|
-
var
|
|
8993
|
+
var BADGE_H3 = 16;
|
|
8416
8994
|
var BADGE_PADDING_X = 6;
|
|
8417
8995
|
var TICK_HALF = 10;
|
|
8418
8996
|
var PriceNote = class {
|
|
@@ -8438,10 +9016,10 @@ var PriceNote = class {
|
|
|
8438
9016
|
const tw = ctx.measureText(label).width;
|
|
8439
9017
|
const badgeW = tw + BADGE_PADDING_X * 2;
|
|
8440
9018
|
const bx = x + TICK_HALF + 2;
|
|
8441
|
-
const by = y -
|
|
9019
|
+
const by = y - BADGE_H3 / 2;
|
|
8442
9020
|
ctx.fillStyle = "#ffa726";
|
|
8443
9021
|
ctx.beginPath();
|
|
8444
|
-
ctx.roundRect(bx, by, badgeW,
|
|
9022
|
+
ctx.roundRect(bx, by, badgeW, BADGE_H3, 3);
|
|
8445
9023
|
ctx.fill();
|
|
8446
9024
|
ctx.fillStyle = "#ffffff";
|
|
8447
9025
|
ctx.textBaseline = "middle";
|
|
@@ -8461,7 +9039,7 @@ var PriceNote = class {
|
|
|
8461
9039
|
|
|
8462
9040
|
// src/drawings/AnchoredNote.ts
|
|
8463
9041
|
var HIT_TOLERANCE45 = 6;
|
|
8464
|
-
var
|
|
9042
|
+
var BADGE_H4 = 20;
|
|
8465
9043
|
var PAD_H2 = 10;
|
|
8466
9044
|
function pointToSegmentDist21(px, py, x1, y1, x2, y2) {
|
|
8467
9045
|
const dx = x2 - x1, dy = y2 - y1;
|
|
@@ -8494,7 +9072,7 @@ var AnchoredNote = class {
|
|
|
8494
9072
|
ctx.lineWidth = isSelected ? 1.5 : 1;
|
|
8495
9073
|
const badgeW = this.getBadgeW(ctx);
|
|
8496
9074
|
const bx = x2 - badgeW / 2;
|
|
8497
|
-
const by = y2 -
|
|
9075
|
+
const by = y2 - BADGE_H4 / 2;
|
|
8498
9076
|
ctx.strokeStyle = "#42a5f5";
|
|
8499
9077
|
ctx.setLineDash([4, 3]);
|
|
8500
9078
|
ctx.beginPath();
|
|
@@ -8508,12 +9086,12 @@ var AnchoredNote = class {
|
|
|
8508
9086
|
ctx.fill();
|
|
8509
9087
|
ctx.fillStyle = "#37474f";
|
|
8510
9088
|
ctx.beginPath();
|
|
8511
|
-
ctx.roundRect(bx, by, badgeW,
|
|
9089
|
+
ctx.roundRect(bx, by, badgeW, BADGE_H4, 4);
|
|
8512
9090
|
ctx.fill();
|
|
8513
9091
|
ctx.strokeStyle = "#42a5f5";
|
|
8514
9092
|
ctx.lineWidth = isSelected ? 1.5 : 1;
|
|
8515
9093
|
ctx.beginPath();
|
|
8516
|
-
ctx.roundRect(bx, by, badgeW,
|
|
9094
|
+
ctx.roundRect(bx, by, badgeW, BADGE_H4, 4);
|
|
8517
9095
|
ctx.stroke();
|
|
8518
9096
|
const label = this.text || "Note";
|
|
8519
9097
|
const isPlaceholder = !this.text;
|
|
@@ -8532,8 +9110,8 @@ var AnchoredNote = class {
|
|
|
8532
9110
|
const y2 = scale.yToPixel(p2.price, priceRange, chartH);
|
|
8533
9111
|
const approxBadgeW = Math.max(50, (this.text?.length ?? 4) * 7 + 20);
|
|
8534
9112
|
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 +
|
|
9113
|
+
const by = y2 - BADGE_H4 / 2;
|
|
9114
|
+
if (x >= bx - HIT_TOLERANCE45 && x <= bx + approxBadgeW + HIT_TOLERANCE45 && y >= by - HIT_TOLERANCE45 && y <= by + BADGE_H4 + HIT_TOLERANCE45) {
|
|
8537
9115
|
return true;
|
|
8538
9116
|
}
|
|
8539
9117
|
if (pointToSegmentDist21(x, y, x1, y1, x2, y2) <= HIT_TOLERANCE45) {
|
|
@@ -12484,6 +13062,158 @@ var TradePopover = class {
|
|
|
12484
13062
|
}
|
|
12485
13063
|
};
|
|
12486
13064
|
|
|
13065
|
+
// src/ui/DraftQtyFlyout.ts
|
|
13066
|
+
var DraftQtyFlyout = class {
|
|
13067
|
+
constructor(theme, onCommit) {
|
|
13068
|
+
this._min = 1;
|
|
13069
|
+
this._max = 100;
|
|
13070
|
+
this._step = 1;
|
|
13071
|
+
this.onDocMouseDown = (e) => {
|
|
13072
|
+
if (this.isVisible && !this.el.contains(e.target)) {
|
|
13073
|
+
this.syncIfValid();
|
|
13074
|
+
this.hide();
|
|
13075
|
+
}
|
|
13076
|
+
};
|
|
13077
|
+
this.theme = theme;
|
|
13078
|
+
this.onCommit = onCommit;
|
|
13079
|
+
this.el = document.createElement("div");
|
|
13080
|
+
Object.assign(this.el.style, {
|
|
13081
|
+
position: "fixed",
|
|
13082
|
+
display: "none",
|
|
13083
|
+
flexDirection: "row",
|
|
13084
|
+
alignItems: "center",
|
|
13085
|
+
gap: "4px",
|
|
13086
|
+
background: theme.tooltip.background,
|
|
13087
|
+
border: `1px solid ${theme.axisBorder}`,
|
|
13088
|
+
borderRadius: "5px",
|
|
13089
|
+
padding: "5px 7px",
|
|
13090
|
+
zIndex: "25",
|
|
13091
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.3)",
|
|
13092
|
+
fontFamily: "'Inter', system-ui, sans-serif",
|
|
13093
|
+
fontSize: "13px",
|
|
13094
|
+
color: theme.tooltip.text,
|
|
13095
|
+
pointerEvents: "auto"
|
|
13096
|
+
});
|
|
13097
|
+
const decBtn = this.makeStepBtn("\u2212", () => this.step(-1));
|
|
13098
|
+
this.input = this.makeInput();
|
|
13099
|
+
const incBtn = this.makeStepBtn("+", () => this.step(1));
|
|
13100
|
+
this.el.append(decBtn, this.input, incBtn);
|
|
13101
|
+
this.el.addEventListener("mousedown", (e) => e.stopPropagation());
|
|
13102
|
+
this.el.addEventListener("touchstart", (e) => e.stopPropagation());
|
|
13103
|
+
document.addEventListener("mousedown", this.onDocMouseDown);
|
|
13104
|
+
}
|
|
13105
|
+
// ── Public ────────────────────────────────────────────────────────────────
|
|
13106
|
+
show(pillX, boxBottom, currentQty, min, max) {
|
|
13107
|
+
this._min = min;
|
|
13108
|
+
this._max = max;
|
|
13109
|
+
this._step = min;
|
|
13110
|
+
this.input.min = String(min);
|
|
13111
|
+
this.input.max = String(max);
|
|
13112
|
+
this.input.step = String(min);
|
|
13113
|
+
this.input.value = String(currentQty);
|
|
13114
|
+
this.el.style.display = "flex";
|
|
13115
|
+
this.el.style.left = `${pillX}px`;
|
|
13116
|
+
this.el.style.top = `${boxBottom + 4}px`;
|
|
13117
|
+
requestAnimationFrame(() => {
|
|
13118
|
+
this.input.focus();
|
|
13119
|
+
this.input.select();
|
|
13120
|
+
});
|
|
13121
|
+
}
|
|
13122
|
+
hide() {
|
|
13123
|
+
this.el.style.display = "none";
|
|
13124
|
+
}
|
|
13125
|
+
get isVisible() {
|
|
13126
|
+
return this.el.style.display !== "none";
|
|
13127
|
+
}
|
|
13128
|
+
setTheme(theme) {
|
|
13129
|
+
this.theme = theme;
|
|
13130
|
+
Object.assign(this.el.style, {
|
|
13131
|
+
background: theme.tooltip.background,
|
|
13132
|
+
border: `1px solid ${theme.axisBorder}`,
|
|
13133
|
+
color: theme.tooltip.text
|
|
13134
|
+
});
|
|
13135
|
+
this.input.style.borderBottom = `1px solid ${theme.axisBorder}`;
|
|
13136
|
+
}
|
|
13137
|
+
destroy() {
|
|
13138
|
+
document.removeEventListener("mousedown", this.onDocMouseDown);
|
|
13139
|
+
}
|
|
13140
|
+
// ── Private ───────────────────────────────────────────────────────────────
|
|
13141
|
+
makeStepBtn(label, onClick) {
|
|
13142
|
+
const btn = document.createElement("button");
|
|
13143
|
+
btn.textContent = label;
|
|
13144
|
+
Object.assign(btn.style, {
|
|
13145
|
+
background: "transparent",
|
|
13146
|
+
border: "none",
|
|
13147
|
+
color: "inherit",
|
|
13148
|
+
cursor: "pointer",
|
|
13149
|
+
fontSize: "16px",
|
|
13150
|
+
lineHeight: "1",
|
|
13151
|
+
padding: "0 4px",
|
|
13152
|
+
borderRadius: "3px",
|
|
13153
|
+
opacity: "0.7"
|
|
13154
|
+
});
|
|
13155
|
+
btn.addEventListener("mouseenter", () => {
|
|
13156
|
+
btn.style.opacity = "1";
|
|
13157
|
+
});
|
|
13158
|
+
btn.addEventListener("mouseleave", () => {
|
|
13159
|
+
btn.style.opacity = "0.7";
|
|
13160
|
+
});
|
|
13161
|
+
btn.addEventListener("mousedown", (e) => {
|
|
13162
|
+
e.preventDefault();
|
|
13163
|
+
onClick();
|
|
13164
|
+
});
|
|
13165
|
+
return btn;
|
|
13166
|
+
}
|
|
13167
|
+
makeInput() {
|
|
13168
|
+
const input = document.createElement("input");
|
|
13169
|
+
input.type = "number";
|
|
13170
|
+
Object.assign(input.style, {
|
|
13171
|
+
width: "64px",
|
|
13172
|
+
background: "transparent",
|
|
13173
|
+
border: "none",
|
|
13174
|
+
borderBottom: `1px solid ${this.theme.axisBorder}`,
|
|
13175
|
+
color: "inherit",
|
|
13176
|
+
font: "13px 'Inter', system-ui, sans-serif",
|
|
13177
|
+
outline: "none",
|
|
13178
|
+
textAlign: "center",
|
|
13179
|
+
padding: "1px 2px"
|
|
13180
|
+
});
|
|
13181
|
+
input.addEventListener("input", () => {
|
|
13182
|
+
this.syncIfValid();
|
|
13183
|
+
});
|
|
13184
|
+
input.addEventListener("keydown", (e) => {
|
|
13185
|
+
if (e.key === "Enter") {
|
|
13186
|
+
e.preventDefault();
|
|
13187
|
+
this.syncIfValid();
|
|
13188
|
+
this.hide();
|
|
13189
|
+
} else if (e.key === "Escape") {
|
|
13190
|
+
e.preventDefault();
|
|
13191
|
+
this.hide();
|
|
13192
|
+
}
|
|
13193
|
+
});
|
|
13194
|
+
input.addEventListener("blur", () => {
|
|
13195
|
+
if (this.isVisible) {
|
|
13196
|
+
this.syncIfValid();
|
|
13197
|
+
this.hide();
|
|
13198
|
+
}
|
|
13199
|
+
});
|
|
13200
|
+
return input;
|
|
13201
|
+
}
|
|
13202
|
+
step(direction) {
|
|
13203
|
+
const current = parseFloat(this.input.value) || this._min;
|
|
13204
|
+
const next = Math.round((current + direction * this._step) * 1e10) / 1e10;
|
|
13205
|
+
this.input.value = String(Math.min(this._max, Math.max(this._min, next)));
|
|
13206
|
+
this.input.focus();
|
|
13207
|
+
this.syncIfValid();
|
|
13208
|
+
}
|
|
13209
|
+
syncIfValid() {
|
|
13210
|
+
const val = parseFloat(this.input.value);
|
|
13211
|
+
if (!isNaN(val) && val >= this._min && val <= this._max) {
|
|
13212
|
+
this.onCommit(val);
|
|
13213
|
+
}
|
|
13214
|
+
}
|
|
13215
|
+
};
|
|
13216
|
+
|
|
12487
13217
|
// src/ui/IsinPickerModal.ts
|
|
12488
13218
|
var INITIAL_LIMIT = 50;
|
|
12489
13219
|
var SEARCH_LIMIT = 20;
|
|
@@ -12694,9 +13424,14 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12694
13424
|
this._isDraggingBracket = false;
|
|
12695
13425
|
// Main line drag state
|
|
12696
13426
|
this._level = null;
|
|
12697
|
-
this._startY = 0;
|
|
12698
13427
|
this._startPrice = 0;
|
|
12699
13428
|
this._currentPrice = 0;
|
|
13429
|
+
// Pixel offset between click Y and the level's rendered Y at drag start.
|
|
13430
|
+
// Why: the drag hit-strip is 8 px tall; without this, the cursor-to-price
|
|
13431
|
+
// mapping would snap the level to wherever the cursor is instead of
|
|
13432
|
+
// preserving the grab point. Also immunises the drag from any priceRange
|
|
13433
|
+
// animation (lerp) that happens between drag-start and first mousemove.
|
|
13434
|
+
this._pixelOffset = 0;
|
|
12700
13435
|
// Entry-price constraint bounds (applied when SL/TP exist on the level)
|
|
12701
13436
|
this._mainMinPrice = -Infinity;
|
|
12702
13437
|
this._mainMaxPrice = Infinity;
|
|
@@ -12704,6 +13439,7 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12704
13439
|
this._bracketType = null;
|
|
12705
13440
|
this._bracketStartPrice = 0;
|
|
12706
13441
|
this._bracketCurrentPrice = 0;
|
|
13442
|
+
this._bracketPixelOffset = 0;
|
|
12707
13443
|
// Bracket constraint state (populated on startBracketDrag)
|
|
12708
13444
|
this._bracketParentPrice = 0;
|
|
12709
13445
|
this._bracketParentSide = "buy";
|
|
@@ -12715,11 +13451,11 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12715
13451
|
* Pass `slPrice` / `tpPrice` / `side` to constrain the drag between SL and TP
|
|
12716
13452
|
* (used for ES/EL entry orders where the entry price must stay between brackets).
|
|
12717
13453
|
*/
|
|
12718
|
-
startDrag(level,
|
|
13454
|
+
startDrag(level, clickY, levelPixelY, startPrice, slPrice, tpPrice, side, marketPrice, orderType) {
|
|
12719
13455
|
this._active = true;
|
|
12720
13456
|
this._isDraggingBracket = false;
|
|
12721
13457
|
this._level = level;
|
|
12722
|
-
this.
|
|
13458
|
+
this._pixelOffset = clickY - levelPixelY;
|
|
12723
13459
|
this._startPrice = startPrice;
|
|
12724
13460
|
this._currentPrice = startPrice;
|
|
12725
13461
|
this._bracketType = null;
|
|
@@ -12751,13 +13487,14 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12751
13487
|
* @param bracketType 'stopLoss' | 'takeProfit'
|
|
12752
13488
|
* @param startPrice Current bracket price
|
|
12753
13489
|
*/
|
|
12754
|
-
startBracketDrag(parentLevel, bracketType, startPrice, currentMarketPrice = 0, parentPriceOverride) {
|
|
13490
|
+
startBracketDrag(parentLevel, bracketType, startPrice, clickY, bracketPixelY, currentMarketPrice = 0, parentPriceOverride) {
|
|
12755
13491
|
this._active = true;
|
|
12756
13492
|
this._isDraggingBracket = true;
|
|
12757
13493
|
this._level = parentLevel;
|
|
12758
13494
|
this._bracketType = bracketType;
|
|
12759
13495
|
this._bracketStartPrice = startPrice;
|
|
12760
13496
|
this._bracketCurrentPrice = startPrice;
|
|
13497
|
+
this._bracketPixelOffset = clickY - bracketPixelY;
|
|
12761
13498
|
this._bracketParentPrice = parentPriceOverride ?? parentLevel.price;
|
|
12762
13499
|
this._bracketParentSide = parentLevel.side ?? "buy";
|
|
12763
13500
|
this._bracketMarketPrice = currentMarketPrice;
|
|
@@ -12768,9 +13505,10 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12768
13505
|
*/
|
|
12769
13506
|
onMouseMove(currentY, scale, priceRange, priceH) {
|
|
12770
13507
|
if (!this._active || !this._level) return null;
|
|
12771
|
-
const rawPrice = scale.pixelToPrice(currentY, priceRange, priceH);
|
|
12772
13508
|
if (this._isDraggingBracket && this._bracketType !== null) {
|
|
12773
|
-
const
|
|
13509
|
+
const effectiveY2 = currentY - this._bracketPixelOffset;
|
|
13510
|
+
const rawPrice2 = scale.pixelToPrice(effectiveY2, priceRange, priceH);
|
|
13511
|
+
const clampedPrice = this._clampBracketPrice(rawPrice2, this._bracketType, this._bracketParentPrice, this._bracketParentSide, this._bracketMarketPrice);
|
|
12774
13512
|
this._bracketCurrentPrice = clampedPrice;
|
|
12775
13513
|
return {
|
|
12776
13514
|
newPrice: clampedPrice,
|
|
@@ -12778,6 +13516,8 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12778
13516
|
isBracket: true
|
|
12779
13517
|
};
|
|
12780
13518
|
}
|
|
13519
|
+
const effectiveY = currentY - this._pixelOffset;
|
|
13520
|
+
const rawPrice = scale.pixelToPrice(effectiveY, priceRange, priceH);
|
|
12781
13521
|
const constrained = Math.max(this._mainMinPrice, Math.min(this._mainMaxPrice, rawPrice));
|
|
12782
13522
|
this._currentPrice = constrained;
|
|
12783
13523
|
return {
|
|
@@ -12876,10 +13616,12 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12876
13616
|
this._level = null;
|
|
12877
13617
|
this._bracketType = null;
|
|
12878
13618
|
this._currentPrice = 0;
|
|
13619
|
+
this._pixelOffset = 0;
|
|
12879
13620
|
this._mainMinPrice = -Infinity;
|
|
12880
13621
|
this._mainMaxPrice = Infinity;
|
|
12881
13622
|
this._bracketStartPrice = 0;
|
|
12882
13623
|
this._bracketCurrentPrice = 0;
|
|
13624
|
+
this._bracketPixelOffset = 0;
|
|
12883
13625
|
this._bracketParentPrice = 0;
|
|
12884
13626
|
this._bracketParentSide = "buy";
|
|
12885
13627
|
this._bracketMarketPrice = 0;
|
|
@@ -13897,6 +14639,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
13897
14639
|
this._hoverBuyType = "limit";
|
|
13898
14640
|
this._hoverSellType = "limit";
|
|
13899
14641
|
this._minLots = 1;
|
|
14642
|
+
this._showQuantityField = false;
|
|
14643
|
+
this._qtyFieldMin = 1;
|
|
14644
|
+
this._qtyFieldMax = 100;
|
|
14645
|
+
this._draftQtyFlyout = null;
|
|
14646
|
+
this._draftQtyPillArea = void 0;
|
|
13900
14647
|
this._onOrderSubmit = void 0;
|
|
13901
14648
|
this.scrollToEndBtn = null;
|
|
13902
14649
|
this.scrollToEndVisible = false;
|
|
@@ -13912,6 +14659,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
13912
14659
|
this.showBidAskLines = false;
|
|
13913
14660
|
this.showActLogo = false;
|
|
13914
14661
|
this.hideLevelConfirmCancel = false;
|
|
14662
|
+
this.levelClusteringEnabled = true;
|
|
14663
|
+
this.clusterThresholdDistance = 20;
|
|
13915
14664
|
this.enableThemeToggle = false;
|
|
13916
14665
|
this.activeIndicators = [];
|
|
13917
14666
|
this.indicatorResults = /* @__PURE__ */ new Map();
|
|
@@ -13977,19 +14726,28 @@ var _ChartEngine = class _ChartEngine {
|
|
|
13977
14726
|
this._mobileDrawingFlyout = null;
|
|
13978
14727
|
// Fullscreen state
|
|
13979
14728
|
this._isCssFullscreen = false;
|
|
14729
|
+
this.timezone = "UTC";
|
|
13980
14730
|
this.settingsCanvasColors = {};
|
|
13981
14731
|
// Streaming
|
|
13982
14732
|
this.streamAdapter = null;
|
|
13983
14733
|
this.liveBidAsk = null;
|
|
13984
14734
|
this.tickClosePriceSource = "bid";
|
|
14735
|
+
/** True while an async data fetch is in-flight; suppresses tick processing to
|
|
14736
|
+
* prevent stale ticks from corrupting newly-loaded bars. */
|
|
14737
|
+
this._suppressTicks = false;
|
|
13985
14738
|
// Trade / position levels
|
|
13986
14739
|
this.levels = [];
|
|
13987
14740
|
this.tradeLevelHitAreas = [];
|
|
14741
|
+
this.offViewportHitAreas = [];
|
|
13988
14742
|
this.tradeLevelEditAreas = [];
|
|
13989
14743
|
this.tradeHoverAreas = [];
|
|
13990
14744
|
this.tradeLevelDragAreas = [];
|
|
13991
14745
|
this.tradeLevelConfirmAreas = [];
|
|
13992
14746
|
this.tradeAddBracketAreas = [];
|
|
14747
|
+
this.clusterBadgeAreas = [];
|
|
14748
|
+
this.lastClusters = [];
|
|
14749
|
+
/** Set of cluster IDs that are currently expanded (fanned out). */
|
|
14750
|
+
this.expandedClusters = /* @__PURE__ */ new Set();
|
|
13993
14751
|
this.hoveredTradeLabel = null;
|
|
13994
14752
|
this.selectedTradeLabel = null;
|
|
13995
14753
|
/** Estimated PNL text for draft order SL/TP bracket lines, set by setDraftBracketPnl(). */
|
|
@@ -14004,6 +14762,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14004
14762
|
// Inline drag confirmation — accumulates changes per level until user confirms
|
|
14005
14763
|
this.pendingLevelDrags = /* @__PURE__ */ new Map();
|
|
14006
14764
|
// Trade from Charts settings
|
|
14765
|
+
this.tfcEnabled = true;
|
|
14766
|
+
this.tfcActive = true;
|
|
14007
14767
|
this.tradeDisplayFilter = "all";
|
|
14008
14768
|
this.positionRenderStyle = "dot";
|
|
14009
14769
|
this.tradesThresholdForHorizontalLine = 2;
|
|
@@ -14014,7 +14774,9 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14014
14774
|
this.dataLoader = null;
|
|
14015
14775
|
this.isFetching = false;
|
|
14016
14776
|
this.noMoreData = false;
|
|
14777
|
+
this._loadGeneration = 0;
|
|
14017
14778
|
this.maxCandles = 200;
|
|
14779
|
+
this.prefetchThreshold = 80;
|
|
14018
14780
|
this.defaultVisibleBars = DEFAULT_VISIBLE_BARS;
|
|
14019
14781
|
this.defaultMobileVisibleBars = void 0;
|
|
14020
14782
|
this.mobileBarDivisor = 2;
|
|
@@ -14052,7 +14814,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14052
14814
|
if (this.tradeButton) {
|
|
14053
14815
|
if (isInChartArea && y >= 0 && y <= priceH && !this._isTradingActive && !hoveringDotLevel) {
|
|
14054
14816
|
this.tradeButton.setY(y);
|
|
14055
|
-
this.tradeButton.
|
|
14817
|
+
const btnHalfH = this.uiConfig.tradeButton.size / 2;
|
|
14818
|
+
if (overlapsOffViewportPill(y, btnHalfH, this.offViewportHitAreas)) {
|
|
14819
|
+
this.tradeButton.hide();
|
|
14820
|
+
} else if (this.tfcActive) {
|
|
14821
|
+
this.tradeButton.show();
|
|
14822
|
+
}
|
|
14056
14823
|
this._tradeButtonClickData = { price, y };
|
|
14057
14824
|
const allBars = this.dataStore.all;
|
|
14058
14825
|
const lastClose = allBars.length > 0 ? allBars[allBars.length - 1].close : price;
|
|
@@ -14154,7 +14921,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14154
14921
|
return;
|
|
14155
14922
|
}
|
|
14156
14923
|
if (!this.drawingManager.getActiveTool()) {
|
|
14157
|
-
if (hitTestTradeLevelClose(x, y, this.tradeLevelHitAreas) || hitTestTradeLevelEdit(x, y, this.tradeLevelEditAreas) || hitTestTradeLevelConfirm(x, y, this.tradeLevelConfirmAreas)) {
|
|
14924
|
+
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
14925
|
this.drawCanvas.style.cursor = "pointer";
|
|
14159
14926
|
this.scheduleRender();
|
|
14160
14927
|
return;
|
|
@@ -14366,6 +15133,25 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14366
15133
|
this.scheduleRender();
|
|
14367
15134
|
return;
|
|
14368
15135
|
}
|
|
15136
|
+
const clusterHitTouch = hitTestClusterBadge(x, y, this.clusterBadgeAreas);
|
|
15137
|
+
if (clusterHitTouch) {
|
|
15138
|
+
e.preventDefault();
|
|
15139
|
+
this.panHandler.cancel();
|
|
15140
|
+
if (this.expandedClusters.has(clusterHitTouch.clusterId)) {
|
|
15141
|
+
this.expandedClusters.delete(clusterHitTouch.clusterId);
|
|
15142
|
+
} else {
|
|
15143
|
+
this.expandedClusters.add(clusterHitTouch.clusterId);
|
|
15144
|
+
}
|
|
15145
|
+
this.renderTradeLayer();
|
|
15146
|
+
return;
|
|
15147
|
+
}
|
|
15148
|
+
const offVpHit = hitTestOffViewportIndicator(x, y, this.offViewportHitAreas);
|
|
15149
|
+
if (offVpHit) {
|
|
15150
|
+
e.preventDefault();
|
|
15151
|
+
this.panHandler.cancel();
|
|
15152
|
+
this.scrollToCenterPrice(offVpHit.targetPrice);
|
|
15153
|
+
return;
|
|
15154
|
+
}
|
|
14369
15155
|
const confirmHit = hitTestTradeLevelConfirm(x, y, this.tradeLevelConfirmAreas);
|
|
14370
15156
|
if (confirmHit) {
|
|
14371
15157
|
e.preventDefault();
|
|
@@ -14373,6 +15159,15 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14373
15159
|
else this.revertPendingChanges(confirmHit.label);
|
|
14374
15160
|
return;
|
|
14375
15161
|
}
|
|
15162
|
+
if (this._showQuantityField && this._draftQtyPillArea) {
|
|
15163
|
+
const p = this._draftQtyPillArea;
|
|
15164
|
+
if (x >= p.pillX && x <= p.pillX + p.pillW && y >= p.pillY && y <= p.pillY + p.pillH) {
|
|
15165
|
+
e.preventDefault();
|
|
15166
|
+
e.stopPropagation();
|
|
15167
|
+
this.openDraftQtyFlyout(p);
|
|
15168
|
+
return;
|
|
15169
|
+
}
|
|
15170
|
+
}
|
|
14376
15171
|
const closeHit = hitTestTradeLevelClose(x, y, this.tradeLevelHitAreas);
|
|
14377
15172
|
if (closeHit) {
|
|
14378
15173
|
e.preventDefault();
|
|
@@ -14422,6 +15217,10 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14422
15217
|
}
|
|
14423
15218
|
this.drawingManager.selectDrawing(null);
|
|
14424
15219
|
this.hideMobileDrawingFlyout();
|
|
15220
|
+
if (this.expandedClusters.size > 0) {
|
|
15221
|
+
this.expandedClusters.clear();
|
|
15222
|
+
this.renderTradeLayer();
|
|
15223
|
+
}
|
|
14425
15224
|
const isInChartArea = x < chartW && y < timeAxisY;
|
|
14426
15225
|
if (isInChartArea) {
|
|
14427
15226
|
if (this._mobileCrosshairActive) {
|
|
@@ -14571,7 +15370,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14571
15370
|
if (this._isTradingActive) {
|
|
14572
15371
|
this.tradeButton.hide();
|
|
14573
15372
|
} else {
|
|
14574
|
-
this.tradeButton.
|
|
15373
|
+
const _btnHalfH2 = this.uiConfig.tradeButton.size / 2;
|
|
15374
|
+
if (overlapsOffViewportPill(y, _btnHalfH2, this.offViewportHitAreas)) {
|
|
15375
|
+
this.tradeButton.hide();
|
|
15376
|
+
} else if (this.tfcActive) {
|
|
15377
|
+
this.tradeButton.show();
|
|
15378
|
+
}
|
|
14575
15379
|
}
|
|
14576
15380
|
this._tradeButtonClickData = { price, y };
|
|
14577
15381
|
const _allBars = this.dataStore.all;
|
|
@@ -14653,12 +15457,36 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14653
15457
|
const rawBarIndex = this.scaleManager.pixelToBarIndex(x, this.viewport, chartW);
|
|
14654
15458
|
const price = this.scaleManager.pixelToPrice(y, priceRange, priceH);
|
|
14655
15459
|
const barIndex = Math.round(rawBarIndex);
|
|
15460
|
+
const clusterHit = hitTestClusterBadge(x, y, this.clusterBadgeAreas);
|
|
15461
|
+
if (clusterHit) {
|
|
15462
|
+
if (this.expandedClusters.has(clusterHit.clusterId)) {
|
|
15463
|
+
this.expandedClusters.delete(clusterHit.clusterId);
|
|
15464
|
+
} else {
|
|
15465
|
+
this.expandedClusters.add(clusterHit.clusterId);
|
|
15466
|
+
}
|
|
15467
|
+
this.renderTradeLayer();
|
|
15468
|
+
return;
|
|
15469
|
+
}
|
|
15470
|
+
const offVpHit = hitTestOffViewportIndicator(x, y, this.offViewportHitAreas);
|
|
15471
|
+
if (offVpHit) {
|
|
15472
|
+
this.scrollToCenterPrice(offVpHit.targetPrice);
|
|
15473
|
+
return;
|
|
15474
|
+
}
|
|
14656
15475
|
const confirmHit = hitTestTradeLevelConfirm(x, y, this.tradeLevelConfirmAreas);
|
|
14657
15476
|
if (confirmHit) {
|
|
14658
15477
|
if (confirmHit.action === "confirm") this.applyPendingChanges(confirmHit.label);
|
|
14659
15478
|
else this.revertPendingChanges(confirmHit.label);
|
|
14660
15479
|
return;
|
|
14661
15480
|
}
|
|
15481
|
+
if (this._showQuantityField && this._draftQtyPillArea) {
|
|
15482
|
+
const p = this._draftQtyPillArea;
|
|
15483
|
+
if (x >= p.pillX && x <= p.pillX + p.pillW && y >= p.pillY && y <= p.pillY + p.pillH) {
|
|
15484
|
+
e.preventDefault();
|
|
15485
|
+
e.stopPropagation();
|
|
15486
|
+
this.openDraftQtyFlyout(p);
|
|
15487
|
+
return;
|
|
15488
|
+
}
|
|
15489
|
+
}
|
|
14662
15490
|
const closedLevel = hitTestTradeLevelClose(x, y, this.tradeLevelHitAreas);
|
|
14663
15491
|
if (closedLevel) {
|
|
14664
15492
|
this._handleTradeLevelClose(closedLevel);
|
|
@@ -14737,6 +15565,10 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14737
15565
|
this.renderTradeLayer();
|
|
14738
15566
|
}
|
|
14739
15567
|
}
|
|
15568
|
+
if (this.expandedClusters.size > 0) {
|
|
15569
|
+
this.expandedClusters.clear();
|
|
15570
|
+
this.renderTradeLayer();
|
|
15571
|
+
}
|
|
14740
15572
|
const timeAxisY = rect.height - this.timeAxisHeight;
|
|
14741
15573
|
if (x >= chartW) {
|
|
14742
15574
|
if (y <= priceH) {
|
|
@@ -14852,6 +15684,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14852
15684
|
this.scheduleRender();
|
|
14853
15685
|
};
|
|
14854
15686
|
this.onKeyDown = (e) => {
|
|
15687
|
+
if (e.key === "Escape" && this.expandedClusters.size > 0) {
|
|
15688
|
+
this.expandedClusters.clear();
|
|
15689
|
+
this.renderTradeLayer();
|
|
15690
|
+
e.preventDefault();
|
|
15691
|
+
return;
|
|
15692
|
+
}
|
|
14855
15693
|
if (e.key === "Escape" && this.pendingLevelDrags.size > 0) {
|
|
14856
15694
|
this.revertPendingChanges();
|
|
14857
15695
|
e.preventDefault();
|
|
@@ -14871,6 +15709,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14871
15709
|
this.onTick = (tick) => {
|
|
14872
15710
|
const all = this.dataStore.all;
|
|
14873
15711
|
if (all.length === 0) return;
|
|
15712
|
+
if (this._suppressTicks) return;
|
|
14874
15713
|
this.liveBidAsk = { bid: tick.bid, ask: tick.ask };
|
|
14875
15714
|
this.setDotTickActive();
|
|
14876
15715
|
const last = all[all.length - 1];
|
|
@@ -14938,7 +15777,9 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14938
15777
|
durationTimeframeMap,
|
|
14939
15778
|
uiConfig,
|
|
14940
15779
|
labels,
|
|
14941
|
-
tickActivityMs = 3e4
|
|
15780
|
+
tickActivityMs = 3e4,
|
|
15781
|
+
hideSymbolAndTick = false,
|
|
15782
|
+
showBottomBar = false
|
|
14942
15783
|
} = config;
|
|
14943
15784
|
this.dataLoader = dataLoader ?? null;
|
|
14944
15785
|
this.drawingToolsEnabled = config.showDrawingTools !== false;
|
|
@@ -14947,6 +15788,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14947
15788
|
this.labels = resolveLabels(labels);
|
|
14948
15789
|
this.tickActivityMs = tickActivityMs;
|
|
14949
15790
|
this.maxCandles = config.maxCandles ?? 200;
|
|
15791
|
+
this.prefetchThreshold = config.prefetchThreshold ?? 80;
|
|
14950
15792
|
this.defaultVisibleBars = config.defaultVisibleBars ?? DEFAULT_VISIBLE_BARS;
|
|
14951
15793
|
this.defaultMobileVisibleBars = config.defaultMobileVisibleBars;
|
|
14952
15794
|
this.explicitMobileDivisor = config.mobileBarDivisor !== void 0;
|
|
@@ -14986,7 +15828,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14986
15828
|
this.showBidAskLines = config.showBidAskLines ?? false;
|
|
14987
15829
|
this.showActLogo = config.showActLogo ?? false;
|
|
14988
15830
|
this.hideLevelConfirmCancel = config.hideLevelConfirmCancel ?? false;
|
|
15831
|
+
this.levelClusteringEnabled = config.levelClusteringEnabled ?? true;
|
|
15832
|
+
this.clusterThresholdDistance = config.clusterThresholdDistance ?? 20;
|
|
14989
15833
|
this.enableThemeToggle = config.enableThemeToggle ?? false;
|
|
15834
|
+
this.tfcEnabled = config.tfcEnabled !== false;
|
|
15835
|
+
this.tfcActive = this.tfcEnabled;
|
|
14990
15836
|
this.showCandleCountdown = config.showCandleCountdown ?? true;
|
|
14991
15837
|
this.candleCountdownTimeframes = config.candleCountdownTimeframes;
|
|
14992
15838
|
this._onOrderSubmit = onOrderSubmit;
|
|
@@ -14994,6 +15840,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
14994
15840
|
console.warn("[acttrader-charts] ChartConfig.minLots is deprecated. DraftOrderQtyInput has been removed. The value is stored and used as the submitted lot size.");
|
|
14995
15841
|
this._minLots = config.minLots;
|
|
14996
15842
|
}
|
|
15843
|
+
if (config.showQuantityField !== void 0) this._showQuantityField = config.showQuantityField;
|
|
15844
|
+
if (config.quantityFieldConfig) {
|
|
15845
|
+
if (config.quantityFieldConfig.minLots !== void 0) this._qtyFieldMin = config.quantityFieldConfig.minLots;
|
|
15846
|
+
if (config.quantityFieldConfig.maxLots !== void 0) this._qtyFieldMax = config.quantityFieldConfig.maxLots;
|
|
15847
|
+
}
|
|
14997
15848
|
if (config.onLotsChange !== void 0) {
|
|
14998
15849
|
console.warn("[acttrader-charts] ChartConfig.onLotsChange is deprecated. DraftOrderQtyInput has been removed; this callback will never fire.");
|
|
14999
15850
|
}
|
|
@@ -15002,6 +15853,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15002
15853
|
}
|
|
15003
15854
|
this.themeName = theme === "dark" ? "dark" : "light";
|
|
15004
15855
|
this.themeOverrides = config.themeOverrides;
|
|
15856
|
+
this.timezone = config.timezone === "local" ? Intl.DateTimeFormat().resolvedOptions().timeZone : config.timezone ?? "UTC";
|
|
15005
15857
|
this.aggregateFrom = config.aggregateFrom;
|
|
15006
15858
|
const _baseTheme = this.themeName === "dark" ? DARK_THEME : LIGHT_THEME;
|
|
15007
15859
|
this.theme = deepMergeTheme(_baseTheme, this.themeOverrides?.[this.themeName]);
|
|
@@ -15050,6 +15902,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15050
15902
|
this.drawingToolsEnabled ? () => this.toggleDrawingTools() : null,
|
|
15051
15903
|
this.drawingToolsEnabled,
|
|
15052
15904
|
this.settingsEnabled,
|
|
15905
|
+
this.tfcEnabled,
|
|
15906
|
+
this.tfcActive,
|
|
15907
|
+
this.tfcEnabled ? (enabled) => {
|
|
15908
|
+
this.setTfcActive(enabled);
|
|
15909
|
+
} : null,
|
|
15053
15910
|
this.series,
|
|
15054
15911
|
this.timeframe,
|
|
15055
15912
|
null,
|
|
@@ -15201,7 +16058,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15201
16058
|
this.loadingOverlayEl.appendChild(loadingLabel);
|
|
15202
16059
|
this.canvasWrap.appendChild(this.loadingOverlayEl);
|
|
15203
16060
|
}
|
|
15204
|
-
if (showUI) {
|
|
16061
|
+
if (showUI && !hideSymbolAndTick) {
|
|
15205
16062
|
this.siEl = document.createElement("div");
|
|
15206
16063
|
Object.assign(this.siEl.style, {
|
|
15207
16064
|
position: "absolute",
|
|
@@ -15345,6 +16202,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15345
16202
|
const rect = this._cachedDrawRect ?? this.drawCanvas.getBoundingClientRect();
|
|
15346
16203
|
const y = e.clientY - rect.top;
|
|
15347
16204
|
const { priceH } = this.getChartLayout();
|
|
16205
|
+
const btnHalfH = this.uiConfig.tradeButton.size / 2;
|
|
16206
|
+
if (overlapsOffViewportPill(y, btnHalfH, this.offViewportHitAreas)) {
|
|
16207
|
+
this.tradeButton?.hide();
|
|
16208
|
+
return;
|
|
16209
|
+
}
|
|
15348
16210
|
if (y >= 0 && y <= priceH) {
|
|
15349
16211
|
this.tradeButton?.setY(y);
|
|
15350
16212
|
const priceRange = this._displayedPriceRange ?? this.getScaledPriceRange(this.viewport);
|
|
@@ -15359,7 +16221,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15359
16221
|
}
|
|
15360
16222
|
middleRow.appendChild(this.canvasWrap);
|
|
15361
16223
|
this.wrapper.appendChild(middleRow);
|
|
15362
|
-
if (showUI) {
|
|
16224
|
+
if (showUI && showBottomBar) {
|
|
15363
16225
|
const resolvedDurationMap = { ...DURATION_TIMEFRAME_MAP, ...durationTimeframeMap };
|
|
15364
16226
|
this.bottomBar = new BottomBar(
|
|
15365
16227
|
(d, tf) => {
|
|
@@ -15434,6 +16296,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15434
16296
|
},
|
|
15435
16297
|
() => {
|
|
15436
16298
|
this._isPanning = false;
|
|
16299
|
+
},
|
|
16300
|
+
{
|
|
16301
|
+
enabled: config.momentumScrollEnabled,
|
|
16302
|
+
decay: config.momentumDecay,
|
|
16303
|
+
threshold: config.momentumThreshold,
|
|
16304
|
+
maxVelocity: config.momentumMaxVelocity
|
|
15437
16305
|
}
|
|
15438
16306
|
);
|
|
15439
16307
|
this.zoomHandler = new ZoomHandler(
|
|
@@ -15509,9 +16377,31 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15509
16377
|
return (this.tradePopover?.isVisible ?? false) || this.draftOrderLabel !== null || (this.tradeDragHandler?.active ?? false) || this.pendingLevelDrags.size > 0;
|
|
15510
16378
|
}
|
|
15511
16379
|
// ── Public API ────────────────────────────────────────────────────────────
|
|
16380
|
+
/**
|
|
16381
|
+
* Completely resets the chart to a clean, empty state.
|
|
16382
|
+
*
|
|
16383
|
+
* Cancels any in-flight data fetch, clears all bars, resets the price/time
|
|
16384
|
+
* axes, and discards the live bid/ask price line. The chart will render as
|
|
16385
|
+
* blank until the next `loadData()` call.
|
|
16386
|
+
*
|
|
16387
|
+
* Call this before switching to a new symbol so that no previous symbol's
|
|
16388
|
+
* data, price lines, or stale ticks can bleed into the new chart:
|
|
16389
|
+
* ```
|
|
16390
|
+
* chart.setSymbol('GBPUSD').resetData();
|
|
16391
|
+
* // … fetch new bars, then:
|
|
16392
|
+
* chart.loadData(newBars);
|
|
16393
|
+
* ```
|
|
16394
|
+
*/
|
|
16395
|
+
resetData() {
|
|
16396
|
+
++this._loadGeneration;
|
|
16397
|
+
this.setLoading(false);
|
|
16398
|
+
return this.loadData([]);
|
|
16399
|
+
}
|
|
15512
16400
|
loadData(bars, fitAll = false) {
|
|
15513
16401
|
this.isFetching = false;
|
|
15514
16402
|
this.noMoreData = false;
|
|
16403
|
+
this._suppressTicks = false;
|
|
16404
|
+
this.liveBidAsk = null;
|
|
15515
16405
|
this.pricePanOffset = 0;
|
|
15516
16406
|
this.priceScaleFactor = 1;
|
|
15517
16407
|
this._displayedPriceRange = null;
|
|
@@ -15658,6 +16548,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15658
16548
|
}
|
|
15659
16549
|
triggerLoad(reason) {
|
|
15660
16550
|
if (!this.dataLoader) return;
|
|
16551
|
+
const gen = ++this._loadGeneration;
|
|
16552
|
+
this._suppressTicks = true;
|
|
15661
16553
|
this.isFetching = true;
|
|
15662
16554
|
this.noMoreData = false;
|
|
15663
16555
|
const end = /* @__PURE__ */ new Date();
|
|
@@ -15669,6 +16561,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15669
16561
|
const interval = this.timeframeToInterval(this.getBaseInterval());
|
|
15670
16562
|
this.setLoading(true);
|
|
15671
16563
|
this.dataLoader({ start, end, interval }).then((bars) => {
|
|
16564
|
+
if (gen !== this._loadGeneration) return;
|
|
15672
16565
|
this.loadData(this.maybeAggregate(bars), reason === "duration");
|
|
15673
16566
|
this.setLoading(false);
|
|
15674
16567
|
this.emitter.emit("dataLoaded", {
|
|
@@ -15678,25 +16571,17 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15678
16571
|
end
|
|
15679
16572
|
});
|
|
15680
16573
|
}).catch(() => {
|
|
16574
|
+
if (gen !== this._loadGeneration) return;
|
|
15681
16575
|
this.isFetching = false;
|
|
15682
16576
|
this.setLoading(false);
|
|
15683
16577
|
});
|
|
15684
16578
|
}
|
|
15685
16579
|
checkNeedMoreData() {
|
|
15686
16580
|
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
|
-
);
|
|
16581
|
+
const threshold = Math.max(20, this.prefetchThreshold);
|
|
15698
16582
|
if (this._targetViewport.startIndex > threshold) return;
|
|
15699
16583
|
if (this.dataStore.length === 0) return;
|
|
16584
|
+
const gen = this._loadGeneration;
|
|
15700
16585
|
this.isFetching = true;
|
|
15701
16586
|
const before = this.dataStore.all[0].time;
|
|
15702
16587
|
const end = new Date(before - 1);
|
|
@@ -15708,9 +16593,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15708
16593
|
const interval = this.timeframeToInterval(this.getBaseInterval());
|
|
15709
16594
|
this.setLoading(true);
|
|
15710
16595
|
this.dataLoader({ start, end, interval }).then((bars) => {
|
|
16596
|
+
if (gen !== this._loadGeneration) return;
|
|
15711
16597
|
this.prependData(this.maybeAggregate(bars));
|
|
15712
16598
|
this.setLoading(false);
|
|
15713
16599
|
}).catch(() => {
|
|
16600
|
+
if (gen !== this._loadGeneration) return;
|
|
15714
16601
|
this.isFetching = false;
|
|
15715
16602
|
this.setLoading(false);
|
|
15716
16603
|
});
|
|
@@ -15922,6 +16809,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15922
16809
|
this.leftBar?.setTheme(this.theme);
|
|
15923
16810
|
this.tradeButton?.setTheme(this.theme, this.priceAxisWidth);
|
|
15924
16811
|
this.tradePopover?.setTheme(this.theme);
|
|
16812
|
+
this._draftQtyFlyout?.setTheme(this.theme);
|
|
15925
16813
|
this.indicatorSettingsDialog?.setTheme(this.theme);
|
|
15926
16814
|
this.chartSettingsDialog?.setTheme(this.theme);
|
|
15927
16815
|
this.applyScrollToEndBtnStyles();
|
|
@@ -15952,11 +16840,21 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15952
16840
|
this.leftBar?.setTheme(this.theme);
|
|
15953
16841
|
this.tradeButton?.setTheme(this.theme, this.priceAxisWidth);
|
|
15954
16842
|
this.tradePopover?.setTheme(this.theme);
|
|
16843
|
+
this._draftQtyFlyout?.setTheme(this.theme);
|
|
15955
16844
|
this.indicatorSettingsDialog?.setTheme(this.theme);
|
|
15956
16845
|
this.chartSettingsDialog?.setTheme(this.theme);
|
|
15957
16846
|
this.scheduleRender();
|
|
15958
16847
|
return this;
|
|
15959
16848
|
}
|
|
16849
|
+
/**
|
|
16850
|
+
* Changes the display timezone for all time-axis and crosshair labels.
|
|
16851
|
+
* Accepts any IANA string (e.g. `"America/New_York"`), `"UTC"`, or `"local"`.
|
|
16852
|
+
*/
|
|
16853
|
+
setTimezone(tz) {
|
|
16854
|
+
this.timezone = tz === "local" ? Intl.DateTimeFormat().resolvedOptions().timeZone : tz;
|
|
16855
|
+
this.scheduleRender();
|
|
16856
|
+
return this;
|
|
16857
|
+
}
|
|
15960
16858
|
setSeries(series) {
|
|
15961
16859
|
this.series = series;
|
|
15962
16860
|
this.topBar?.setSeries(series);
|
|
@@ -15965,6 +16863,31 @@ var _ChartEngine = class _ChartEngine {
|
|
|
15965
16863
|
this.emitStateChange();
|
|
15966
16864
|
return this;
|
|
15967
16865
|
}
|
|
16866
|
+
/**
|
|
16867
|
+
* Toggle TFC (Trade from Charts) on or off at runtime.
|
|
16868
|
+
* When disabled, all trade levels, the trade button, and trade popover are hidden.
|
|
16869
|
+
* Only functional when `tfcEnabled` config option is `true` (default).
|
|
16870
|
+
*/
|
|
16871
|
+
setTfcActive(enabled) {
|
|
16872
|
+
if (!this.tfcEnabled) return this;
|
|
16873
|
+
this.tfcActive = enabled;
|
|
16874
|
+
this.topBar?.setTfcActive(enabled);
|
|
16875
|
+
if (!enabled) {
|
|
16876
|
+
this.tradeButton?.hide();
|
|
16877
|
+
this.tradePopover?.hide();
|
|
16878
|
+
if (this.draftOrderLabel !== null) {
|
|
16879
|
+
this.removeDraftOrder();
|
|
16880
|
+
}
|
|
16881
|
+
}
|
|
16882
|
+
if (this.chartSettingsDialog) {
|
|
16883
|
+
this.chartSettingsDialog.destroy();
|
|
16884
|
+
this.chartSettingsDialog = null;
|
|
16885
|
+
}
|
|
16886
|
+
this.renderTradeLayer();
|
|
16887
|
+
this.emitter.emit("tfcToggle", { enabled });
|
|
16888
|
+
this.emitStateChange();
|
|
16889
|
+
return this;
|
|
16890
|
+
}
|
|
15968
16891
|
clearDuration() {
|
|
15969
16892
|
if (this.duration === void 0) return;
|
|
15970
16893
|
this.duration = void 0;
|
|
@@ -16008,7 +16931,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16008
16931
|
})),
|
|
16009
16932
|
positionRenderStyle: this.positionRenderStyle,
|
|
16010
16933
|
tradeDisplayFilter: this.tradeDisplayFilter,
|
|
16011
|
-
canvasColors: Object.keys(this.settingsCanvasColors).length > 0 ? { ...this.settingsCanvasColors } : void 0
|
|
16934
|
+
canvasColors: Object.keys(this.settingsCanvasColors).length > 0 ? { ...this.settingsCanvasColors } : void 0,
|
|
16935
|
+
tfcEnabled: this.tfcActive
|
|
16012
16936
|
};
|
|
16013
16937
|
}
|
|
16014
16938
|
/**
|
|
@@ -16080,9 +17004,14 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16080
17004
|
if (state.canvasColors) {
|
|
16081
17005
|
this.settingsCanvasColors = state.canvasColors;
|
|
16082
17006
|
}
|
|
17007
|
+
if (state.tfcEnabled !== void 0 && this.tfcEnabled) {
|
|
17008
|
+
this.tfcActive = state.tfcEnabled;
|
|
17009
|
+
this.topBar?.setTfcActive(state.tfcEnabled);
|
|
17010
|
+
}
|
|
16083
17011
|
} finally {
|
|
16084
17012
|
this.isRestoringState = false;
|
|
16085
17013
|
}
|
|
17014
|
+
this.emitStateChange();
|
|
16086
17015
|
this.scheduleRender();
|
|
16087
17016
|
return this;
|
|
16088
17017
|
}
|
|
@@ -16144,6 +17073,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16144
17073
|
}
|
|
16145
17074
|
if (this.siStreamDot)
|
|
16146
17075
|
this.siStreamDot.style.background = this.theme.ui.stream[this.streamStatus];
|
|
17076
|
+
this.liveBidAsk = null;
|
|
16147
17077
|
return this;
|
|
16148
17078
|
}
|
|
16149
17079
|
/**
|
|
@@ -16207,6 +17137,38 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16207
17137
|
this.tradePopover?.setLots(lots);
|
|
16208
17138
|
return this;
|
|
16209
17139
|
}
|
|
17140
|
+
/**
|
|
17141
|
+
* Updates the quantity (lots) shown in the draft order QTY pill and
|
|
17142
|
+
* submitted when the order is confirmed.
|
|
17143
|
+
* @param qty Lot size to display and submit.
|
|
17144
|
+
*/
|
|
17145
|
+
setDraftQty(qty) {
|
|
17146
|
+
if (this.draftOrderLabel) {
|
|
17147
|
+
const idx = this.levels.findIndex((l) => l.label === this.draftOrderLabel);
|
|
17148
|
+
if (idx !== -1) {
|
|
17149
|
+
const lvl = this.levels[idx];
|
|
17150
|
+
this.levels = [
|
|
17151
|
+
...this.levels.slice(0, idx),
|
|
17152
|
+
{ ...lvl, lots: qty },
|
|
17153
|
+
...this.levels.slice(idx + 1)
|
|
17154
|
+
];
|
|
17155
|
+
this.renderTradeLayer();
|
|
17156
|
+
}
|
|
17157
|
+
}
|
|
17158
|
+
return this;
|
|
17159
|
+
}
|
|
17160
|
+
openDraftQtyFlyout(pill) {
|
|
17161
|
+
if (!this._draftQtyFlyout) {
|
|
17162
|
+
this._draftQtyFlyout = new DraftQtyFlyout(this.theme, (qty) => this.setDraftQty(qty));
|
|
17163
|
+
this.getFloatingRoot().appendChild(this._draftQtyFlyout.el);
|
|
17164
|
+
}
|
|
17165
|
+
const draft = this.levels.find((l) => l.label === this.draftOrderLabel);
|
|
17166
|
+
const currentQty = draft?.lots ?? this._qtyFieldMin;
|
|
17167
|
+
const rect = this.canvasWrap.getBoundingClientRect();
|
|
17168
|
+
const vpX = rect.left + pill.pillX;
|
|
17169
|
+
const vpY = rect.top + pill.pillY + pill.pillH;
|
|
17170
|
+
this._draftQtyFlyout.show(vpX, vpY, currentQty, this._qtyFieldMin, this._qtyFieldMax);
|
|
17171
|
+
}
|
|
16210
17172
|
/**
|
|
16211
17173
|
* @deprecated DraftOrderQtyInput has been removed. This method is a no-op
|
|
16212
17174
|
* kept for backward compatibility. It will be removed in a future version.
|
|
@@ -16682,7 +17644,27 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16682
17644
|
*/
|
|
16683
17645
|
selectLevel(label) {
|
|
16684
17646
|
this.selectedTradeLabel = label;
|
|
16685
|
-
if (label !== null)
|
|
17647
|
+
if (label !== null) {
|
|
17648
|
+
this.crosshair = null;
|
|
17649
|
+
const cluster = this.lastClusters.find(
|
|
17650
|
+
(c) => c.members.some((m) => m.level.label === label)
|
|
17651
|
+
);
|
|
17652
|
+
if (cluster) this.expandedClusters.add(cluster.id);
|
|
17653
|
+
}
|
|
17654
|
+
this.renderTradeLayer();
|
|
17655
|
+
return this;
|
|
17656
|
+
}
|
|
17657
|
+
/** Enable or disable trade-level fan-out clustering at runtime. */
|
|
17658
|
+
setLevelClusteringEnabled(enabled) {
|
|
17659
|
+
this.levelClusteringEnabled = enabled;
|
|
17660
|
+
this.expandedClusters.clear();
|
|
17661
|
+
this.renderTradeLayer();
|
|
17662
|
+
return this;
|
|
17663
|
+
}
|
|
17664
|
+
/** Update the cluster proximity threshold (in pixels) at runtime. Only effective when levelClusteringEnabled is true. */
|
|
17665
|
+
setClusterThresholdDistance(px) {
|
|
17666
|
+
this.clusterThresholdDistance = px;
|
|
17667
|
+
this.expandedClusters.clear();
|
|
16686
17668
|
this.renderTradeLayer();
|
|
16687
17669
|
return this;
|
|
16688
17670
|
}
|
|
@@ -16697,6 +17679,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16697
17679
|
this.chartSettingsDialog?.destroy();
|
|
16698
17680
|
this.isinPickerModal?.destroy();
|
|
16699
17681
|
this.tradePopover?.destroy();
|
|
17682
|
+
this._draftQtyFlyout?.destroy();
|
|
16700
17683
|
this.leftBar?.destroy();
|
|
16701
17684
|
this.drawCanvas.removeEventListener("mousemove", this.onMouseMove);
|
|
16702
17685
|
this.drawCanvas.removeEventListener("mouseleave", this.onMouseLeave);
|
|
@@ -16865,7 +17848,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16865
17848
|
theme: this.themeName
|
|
16866
17849
|
},
|
|
16867
17850
|
this.labels.dialogs,
|
|
16868
|
-
!!this._onOrderSubmit,
|
|
17851
|
+
!!this._onOrderSubmit && this.tfcActive,
|
|
16869
17852
|
this.enableThemeToggle
|
|
16870
17853
|
);
|
|
16871
17854
|
this.getFloatingRoot().appendChild(this.chartSettingsDialog.el);
|
|
@@ -16909,7 +17892,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16909
17892
|
const modals = [
|
|
16910
17893
|
this.isinPickerModal?.el,
|
|
16911
17894
|
this.indicatorSettingsDialog?.el,
|
|
16912
|
-
this.chartSettingsDialog?.el
|
|
17895
|
+
this.chartSettingsDialog?.el,
|
|
17896
|
+
this._draftQtyFlyout?.el
|
|
16913
17897
|
];
|
|
16914
17898
|
for (const el of modals) {
|
|
16915
17899
|
if (el && el.parentElement !== root) root.appendChild(el);
|
|
@@ -16976,11 +17960,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16976
17960
|
const dpr = window.devicePixelRatio || 1;
|
|
16977
17961
|
const { tradeCtx: ctx, tradeCanvas: canvas, theme, viewport } = this;
|
|
16978
17962
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
16979
|
-
if (this.levels.length === 0 || this.dataStore.length === 0) {
|
|
17963
|
+
if (!this.tfcActive || this.levels.length === 0 || this.dataStore.length === 0) {
|
|
16980
17964
|
this.tradeLevelHitAreas = [];
|
|
16981
17965
|
this.tradeLevelEditAreas = [];
|
|
16982
17966
|
this.tradeHoverAreas = [];
|
|
16983
17967
|
this.tradeAddBracketAreas = [];
|
|
17968
|
+
this.offViewportHitAreas = [];
|
|
16984
17969
|
return;
|
|
16985
17970
|
}
|
|
16986
17971
|
const { chartW, priceH } = this.getChartLayout();
|
|
@@ -17035,6 +18020,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17035
18020
|
// Include true pending orders AND entry-order positions (ES/EL)
|
|
17036
18021
|
processedLevels.filter((l) => l.type === "pending" || isEntryOrder(l))
|
|
17037
18022
|
) : processedLevels;
|
|
18023
|
+
const clusterThreshold = this.levelClusteringEnabled ? this.clusterThresholdDistance : 0;
|
|
17038
18024
|
const result = renderTradeLevels(
|
|
17039
18025
|
ctx,
|
|
17040
18026
|
filteredLevels,
|
|
@@ -17052,9 +18038,23 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17052
18038
|
this.positionRenderStyle,
|
|
17053
18039
|
this.hideLevelConfirmCancel,
|
|
17054
18040
|
this.draftOrderLabel,
|
|
17055
|
-
this.draftBracketPnl
|
|
18041
|
+
this.draftBracketPnl,
|
|
18042
|
+
this.expandedClusters,
|
|
18043
|
+
clusterThreshold,
|
|
18044
|
+
this._showQuantityField
|
|
17056
18045
|
);
|
|
17057
18046
|
ctx.restore();
|
|
18047
|
+
const offVpResult = renderOffViewportIndicators(
|
|
18048
|
+
ctx,
|
|
18049
|
+
filteredLevels,
|
|
18050
|
+
priceRange,
|
|
18051
|
+
theme,
|
|
18052
|
+
chartW,
|
|
18053
|
+
priceH,
|
|
18054
|
+
this.tradeDragHandler?.dragLabel ?? null,
|
|
18055
|
+
this.selectedTradeLabel
|
|
18056
|
+
);
|
|
18057
|
+
this.offViewportHitAreas = offVpResult.hitAreas;
|
|
17058
18058
|
ctx.restore();
|
|
17059
18059
|
this.tradeLevelHitAreas = result.hitAreas;
|
|
17060
18060
|
this.tradeLevelEditAreas = result.editAreas;
|
|
@@ -17062,6 +18062,15 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17062
18062
|
this.tradeLevelDragAreas = result.dragAreas;
|
|
17063
18063
|
this.tradeLevelConfirmAreas = result.confirmAreas;
|
|
17064
18064
|
this.tradeAddBracketAreas = result.addBracketAreas;
|
|
18065
|
+
this.clusterBadgeAreas = result.clusterBadgeAreas;
|
|
18066
|
+
this.lastClusters = result.clusters;
|
|
18067
|
+
this._draftQtyPillArea = result.draftQtyPillArea;
|
|
18068
|
+
if (this.expandedClusters.size > 0) {
|
|
18069
|
+
const currentIds = new Set(result.clusters.map((c) => c.id));
|
|
18070
|
+
for (const id of this.expandedClusters) {
|
|
18071
|
+
if (!currentIds.has(id)) this.expandedClusters.delete(id);
|
|
18072
|
+
}
|
|
18073
|
+
}
|
|
17065
18074
|
}
|
|
17066
18075
|
// ── Inline pending-change confirmation ────────────────────────────────────
|
|
17067
18076
|
/** Returns the effective entry price for a level, preferring any staged main drag. */
|
|
@@ -17175,6 +18184,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17175
18184
|
side,
|
|
17176
18185
|
price,
|
|
17177
18186
|
orderType,
|
|
18187
|
+
lots: this._qtyFieldMin,
|
|
17178
18188
|
data: { _isDraft: true }
|
|
17179
18189
|
};
|
|
17180
18190
|
this.levels = [...this.levels, draftLevel];
|
|
@@ -17198,6 +18208,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17198
18208
|
this.selectedTradeLabel = null;
|
|
17199
18209
|
this.draftOrderLabel = null;
|
|
17200
18210
|
this.draftBracketPnl = {};
|
|
18211
|
+
this._draftQtyFlyout?.hide();
|
|
18212
|
+
this._draftQtyPillArea = void 0;
|
|
17201
18213
|
this.renderTradeLayer();
|
|
17202
18214
|
this.emitter.emit("draftCancelled", { label, isFullscreen: this.isFullscreen });
|
|
17203
18215
|
}
|
|
@@ -17211,7 +18223,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17211
18223
|
const draft = level;
|
|
17212
18224
|
this._onOrderSubmit?.({
|
|
17213
18225
|
price: mainEntry?.newPrice ?? draft.price,
|
|
17214
|
-
lots: this.
|
|
18226
|
+
lots: draft.lots ?? this._qtyFieldMin,
|
|
17215
18227
|
side: draft.side,
|
|
17216
18228
|
orderType: draft.orderType,
|
|
17217
18229
|
...draft.stopLossPrice !== void 0 ? { stopLoss: draft.stopLossPrice } : {},
|
|
@@ -17689,7 +18701,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17689
18701
|
isMobileDevice() ? 3 : void 0,
|
|
17690
18702
|
this.uiConfig.timeAxis,
|
|
17691
18703
|
this.timeframe,
|
|
17692
|
-
countdownBarIndex
|
|
18704
|
+
countdownBarIndex,
|
|
18705
|
+
this.timezone
|
|
17693
18706
|
);
|
|
17694
18707
|
if (showCountdown) {
|
|
17695
18708
|
const lastBar2 = this.dataStore.all[this.dataStore.length - 1];
|
|
@@ -17722,7 +18735,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17722
18735
|
subPaneY,
|
|
17723
18736
|
this.timeframe,
|
|
17724
18737
|
this.dataStore.getPricePrecision(),
|
|
17725
|
-
this.uiConfig.crosshair
|
|
18738
|
+
this.uiConfig.crosshair,
|
|
18739
|
+
this.timezone
|
|
17726
18740
|
);
|
|
17727
18741
|
}
|
|
17728
18742
|
if (this.logoImage) {
|
|
@@ -17956,7 +18970,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17956
18970
|
if (this._isTradingActive) {
|
|
17957
18971
|
this.tradeButton.hide();
|
|
17958
18972
|
} else {
|
|
17959
|
-
this.tradeButton.
|
|
18973
|
+
const _btnHalfH1 = this.uiConfig.tradeButton.size / 2;
|
|
18974
|
+
if (overlapsOffViewportPill(y, _btnHalfH1, this.offViewportHitAreas)) {
|
|
18975
|
+
this.tradeButton.hide();
|
|
18976
|
+
} else if (this.tfcActive) {
|
|
18977
|
+
this.tradeButton.show();
|
|
18978
|
+
}
|
|
17960
18979
|
}
|
|
17961
18980
|
this._tradeButtonClickData = { price, y };
|
|
17962
18981
|
const _allBars = this.dataStore.all;
|
|
@@ -17969,6 +18988,9 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17969
18988
|
/** Shared helper: initiate a trade level main-line drag from a hit area. */
|
|
17970
18989
|
_startTradeDragFromHit(dragHit, level, hitY) {
|
|
17971
18990
|
if (!this.tradeDragHandler) return;
|
|
18991
|
+
const { priceH } = this.getChartLayout();
|
|
18992
|
+
const priceRange = this._displayedPriceRange ?? this.getScaledPriceRange(this.viewport);
|
|
18993
|
+
const levelPixelY = this.scaleManager.yToPixel(dragHit.price, priceRange, priceH);
|
|
17972
18994
|
if (dragHit.isBracket) {
|
|
17973
18995
|
this._pendingBracketOrderLabel = dragHit.bracketOrderLabel ?? null;
|
|
17974
18996
|
this._isBracketNew = false;
|
|
@@ -17981,6 +19003,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17981
19003
|
level,
|
|
17982
19004
|
dragHit.bracketType,
|
|
17983
19005
|
dragHit.price,
|
|
19006
|
+
hitY,
|
|
19007
|
+
levelPixelY,
|
|
17984
19008
|
marketPrice,
|
|
17985
19009
|
this._effectiveEntryPrice(level.label, level.price)
|
|
17986
19010
|
);
|
|
@@ -18000,6 +19024,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18000
19024
|
this.tradeDragHandler.startDrag(
|
|
18001
19025
|
level,
|
|
18002
19026
|
hitY,
|
|
19027
|
+
levelPixelY,
|
|
18003
19028
|
dragHit.price,
|
|
18004
19029
|
posLevel.stopLossPrice,
|
|
18005
19030
|
posLevel.takeProfitPrice,
|
|
@@ -18014,6 +19039,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18014
19039
|
this.tradeDragHandler.startDrag(
|
|
18015
19040
|
level,
|
|
18016
19041
|
hitY,
|
|
19042
|
+
levelPixelY,
|
|
18017
19043
|
dragHit.price,
|
|
18018
19044
|
void 0,
|
|
18019
19045
|
void 0,
|
|
@@ -18043,6 +19069,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18043
19069
|
level,
|
|
18044
19070
|
addHit.bracketType,
|
|
18045
19071
|
initialPrice,
|
|
19072
|
+
hitY,
|
|
19073
|
+
btnCenterY,
|
|
18046
19074
|
marketPrice,
|
|
18047
19075
|
this._effectiveEntryPrice(level.label, level.price)
|
|
18048
19076
|
);
|
|
@@ -18129,6 +19157,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18129
19157
|
* This happens when the system clock was behind feed time during the initial data load. */
|
|
18130
19158
|
fillTickGap(fromTime, toTime) {
|
|
18131
19159
|
if (!this.dataLoader) return;
|
|
19160
|
+
if (this.isFetching) return;
|
|
18132
19161
|
const interval = this.timeframeToInterval(this.getBaseInterval());
|
|
18133
19162
|
this.dataLoader({ start: new Date(fromTime), end: new Date(toTime), interval }).then((rawBars) => {
|
|
18134
19163
|
const bars = this.maybeAggregate(rawBars);
|
|
@@ -18163,6 +19192,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18163
19192
|
* tick-reconstructed OHLCV with authoritative exchange data. No-op if no loader. */
|
|
18164
19193
|
correctCompletedBar(barTime, intervalMs) {
|
|
18165
19194
|
if (!this.dataLoader) return;
|
|
19195
|
+
if (this.isFetching) return;
|
|
18166
19196
|
const start = new Date(barTime);
|
|
18167
19197
|
const end = new Date(barTime + intervalMs * 3);
|
|
18168
19198
|
const interval = this.timeframeToInterval(this.getBaseInterval());
|
|
@@ -18202,6 +19232,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18202
19232
|
this.scheduleRender();
|
|
18203
19233
|
}
|
|
18204
19234
|
scrollToEnd() {
|
|
19235
|
+
this.panHandler.cancel();
|
|
18205
19236
|
if (this.scrollAnimId !== null) {
|
|
18206
19237
|
cancelAnimationFrame(this.scrollAnimId);
|
|
18207
19238
|
this.scrollAnimId = null;
|
|
@@ -18233,6 +19264,38 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18233
19264
|
};
|
|
18234
19265
|
this.scrollAnimId = requestAnimationFrame(step);
|
|
18235
19266
|
}
|
|
19267
|
+
/**
|
|
19268
|
+
* Smooth-scroll the Y axis so `price` lands at the vertical center of the
|
|
19269
|
+
* chart. Animates `pricePanOffset` only — horizontal viewport is preserved.
|
|
19270
|
+
* Used by off-viewport level indicator clicks.
|
|
19271
|
+
*/
|
|
19272
|
+
scrollToCenterPrice(price) {
|
|
19273
|
+
if (!isFinite(price) || this.dataStore.length === 0) return;
|
|
19274
|
+
if (this.scrollAnimId !== null) {
|
|
19275
|
+
cancelAnimationFrame(this.scrollAnimId);
|
|
19276
|
+
this.scrollAnimId = null;
|
|
19277
|
+
}
|
|
19278
|
+
const base = this.dataStore.getPriceRange(this.viewport);
|
|
19279
|
+
const naturalMid = (base.min + base.max) / 2;
|
|
19280
|
+
const startPan = this.pricePanOffset;
|
|
19281
|
+
const targetPan = price - naturalMid;
|
|
19282
|
+
if (Math.abs(targetPan - startPan) < 1e-9) return;
|
|
19283
|
+
const DURATION = 350;
|
|
19284
|
+
const t0 = performance.now();
|
|
19285
|
+
const ease = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
|
19286
|
+
const step = (now) => {
|
|
19287
|
+
const raw = Math.min((now - t0) / DURATION, 1);
|
|
19288
|
+
const prog = ease(raw);
|
|
19289
|
+
this.pricePanOffset = startPan + (targetPan - startPan) * prog;
|
|
19290
|
+
this.render();
|
|
19291
|
+
if (raw < 1) {
|
|
19292
|
+
this.scrollAnimId = requestAnimationFrame(step);
|
|
19293
|
+
} else {
|
|
19294
|
+
this.scrollAnimId = null;
|
|
19295
|
+
}
|
|
19296
|
+
};
|
|
19297
|
+
this.scrollAnimId = requestAnimationFrame(step);
|
|
19298
|
+
}
|
|
18236
19299
|
resetPriceAxis() {
|
|
18237
19300
|
this.priceScaleFactor = 1;
|
|
18238
19301
|
this.pricePanOffset = 0;
|
|
@@ -18240,6 +19303,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18240
19303
|
this.scheduleRender();
|
|
18241
19304
|
}
|
|
18242
19305
|
resetTimeAxis() {
|
|
19306
|
+
this.panHandler.cancel();
|
|
18243
19307
|
const total = this.dataStore.length;
|
|
18244
19308
|
if (total === 0) return;
|
|
18245
19309
|
const newStart = Math.max(
|