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.cjs
CHANGED
|
@@ -116,6 +116,33 @@ var _ScaleManager = class _ScaleManager {
|
|
|
116
116
|
};
|
|
117
117
|
_ScaleManager.VERTICAL_PADDING_PX = 20;
|
|
118
118
|
var ScaleManager = _ScaleManager;
|
|
119
|
+
function niceStep(rawStep) {
|
|
120
|
+
if (!isFinite(rawStep) || rawStep <= 0) return 1;
|
|
121
|
+
const exp = Math.floor(Math.log10(rawStep));
|
|
122
|
+
const base = Math.pow(10, exp);
|
|
123
|
+
const frac = rawStep / base;
|
|
124
|
+
const nice = frac < 1.5 ? 1 : frac < 3.5 ? 2 : frac < 7.5 ? 5 : 10;
|
|
125
|
+
return nice * base;
|
|
126
|
+
}
|
|
127
|
+
function generatePriceTicks(range, chartHeight, targetPxPerLabel = 48) {
|
|
128
|
+
const { min, max } = range;
|
|
129
|
+
if (!isFinite(min) || !isFinite(max) || max <= min) return [min];
|
|
130
|
+
const approxSteps = Math.max(2, Math.floor(chartHeight / targetPxPerLabel));
|
|
131
|
+
const rawStep = (max - min) / approxSteps;
|
|
132
|
+
const step = niceStep(rawStep);
|
|
133
|
+
const first = Math.ceil(min / step) * step;
|
|
134
|
+
const ticks = [];
|
|
135
|
+
const eps = step * 1e-6;
|
|
136
|
+
for (let v = first; v <= max + eps; v += step) {
|
|
137
|
+
ticks.push(v);
|
|
138
|
+
}
|
|
139
|
+
return ticks;
|
|
140
|
+
}
|
|
141
|
+
function precisionForStep(step, minPrecision = 0) {
|
|
142
|
+
if (!isFinite(step) || step <= 0) return minPrecision;
|
|
143
|
+
const exp = Math.floor(Math.log10(step));
|
|
144
|
+
return Math.max(minPrecision, -exp);
|
|
145
|
+
}
|
|
119
146
|
|
|
120
147
|
// src/core/EventEmitter.ts
|
|
121
148
|
var EventEmitter = class {
|
|
@@ -168,6 +195,11 @@ function resolveCssVarsInTheme(theme, container) {
|
|
|
168
195
|
container.appendChild(tmp);
|
|
169
196
|
const resolve = (value) => {
|
|
170
197
|
if (!value.includes("var(")) return value;
|
|
198
|
+
value = value.replace(
|
|
199
|
+
/rgba\(\s*(var\([^)]+\))\s*,\s*([0-9.]+)\s*\)/g,
|
|
200
|
+
"rgb($1 / $2)"
|
|
201
|
+
);
|
|
202
|
+
tmp.style.color = "";
|
|
171
203
|
tmp.style.color = value;
|
|
172
204
|
const resolved = getComputedStyle(tmp).color;
|
|
173
205
|
return resolved || value;
|
|
@@ -534,7 +566,8 @@ var DEFAULT_LABELS = {
|
|
|
534
566
|
toggleDrawingTitle: "Toggle Drawing Tools",
|
|
535
567
|
toggleFullscreenTitle: "Toggle Fullscreen",
|
|
536
568
|
exitFullscreenTitle: "Exit Fullscreen",
|
|
537
|
-
settingsTitle: "Settings"
|
|
569
|
+
settingsTitle: "Settings",
|
|
570
|
+
tfcToggleTitle: "Trade from Charts"
|
|
538
571
|
},
|
|
539
572
|
bottomBar: {
|
|
540
573
|
durations: {
|
|
@@ -725,8 +758,7 @@ function resolveLabels(override) {
|
|
|
725
758
|
|
|
726
759
|
// src/interaction/PanHandler.ts
|
|
727
760
|
var _PanHandler = class _PanHandler {
|
|
728
|
-
|
|
729
|
-
constructor(canvas, getViewport, setViewport, totalBars, onUpdate, isLocked = () => false, panY, onDragStart, onDragEnd) {
|
|
761
|
+
constructor(canvas, getViewport, setViewport, totalBars, onUpdate, isLocked = () => false, panY, onDragStart, onDragEnd, momentumOptions) {
|
|
730
762
|
this.canvas = canvas;
|
|
731
763
|
this.getViewport = getViewport;
|
|
732
764
|
this.setViewport = setViewport;
|
|
@@ -744,7 +776,12 @@ var _PanHandler = class _PanHandler {
|
|
|
744
776
|
this.dragStartY = 0;
|
|
745
777
|
this.hasPanned = false;
|
|
746
778
|
this.dragStartFired = false;
|
|
779
|
+
// ── Velocity sample buffer (rolling 100 ms window) ─────────────────────────
|
|
780
|
+
this._velBuffer = [];
|
|
781
|
+
// ── In-flight momentum RAF ─────────────────────────────────────────────────
|
|
782
|
+
this._momentumRafId = null;
|
|
747
783
|
this.onMouseDown = (e) => {
|
|
784
|
+
this._stopMomentum();
|
|
748
785
|
if (this.isLocked()) return;
|
|
749
786
|
this.isDragging = true;
|
|
750
787
|
this.isTouchDrag = false;
|
|
@@ -753,10 +790,15 @@ var _PanHandler = class _PanHandler {
|
|
|
753
790
|
this.dragStartX = e.clientX;
|
|
754
791
|
this.dragStartY = e.clientY;
|
|
755
792
|
this.hasPanned = false;
|
|
793
|
+
this._velBuffer.length = 0;
|
|
756
794
|
};
|
|
757
795
|
this.onTouchStart = (e) => {
|
|
796
|
+
if (e.touches.length !== 1) {
|
|
797
|
+
this._stopMomentum();
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
this._stopMomentum();
|
|
758
801
|
if (this.isLocked()) return;
|
|
759
|
-
if (e.touches.length !== 1) return;
|
|
760
802
|
this.isDragging = true;
|
|
761
803
|
this.isTouchDrag = true;
|
|
762
804
|
this.lastX = e.touches[0].clientX;
|
|
@@ -764,6 +806,7 @@ var _PanHandler = class _PanHandler {
|
|
|
764
806
|
this.dragStartX = e.touches[0].clientX;
|
|
765
807
|
this.dragStartY = e.touches[0].clientY;
|
|
766
808
|
this.dragStartFired = true;
|
|
809
|
+
this._velBuffer.length = 0;
|
|
767
810
|
this.onDragStart?.();
|
|
768
811
|
};
|
|
769
812
|
this.onMouseMove = (e) => {
|
|
@@ -786,6 +829,7 @@ var _PanHandler = class _PanHandler {
|
|
|
786
829
|
this.canvas.style.cursor = "grabbing";
|
|
787
830
|
}
|
|
788
831
|
if (this.hasPanned) {
|
|
832
|
+
this._recordVelocitySample(deltaX, performance.now());
|
|
789
833
|
this.pan(deltaX);
|
|
790
834
|
if (this.panY) this.panY(deltaY);
|
|
791
835
|
}
|
|
@@ -799,6 +843,7 @@ var _PanHandler = class _PanHandler {
|
|
|
799
843
|
e.preventDefault();
|
|
800
844
|
const deltaX = e.touches[0].clientX - this.lastX;
|
|
801
845
|
const deltaY = e.touches[0].clientY - this.lastY;
|
|
846
|
+
this._recordVelocitySample(deltaX, performance.now());
|
|
802
847
|
this.pan(deltaX);
|
|
803
848
|
if (this.panY) this.panY(deltaY);
|
|
804
849
|
this.lastX = e.touches[0].clientX;
|
|
@@ -810,9 +855,16 @@ var _PanHandler = class _PanHandler {
|
|
|
810
855
|
this.hasPanned = false;
|
|
811
856
|
if (this.dragStartFired) {
|
|
812
857
|
this.dragStartFired = false;
|
|
813
|
-
this.
|
|
858
|
+
const velocity = this._computeReleaseVelocity();
|
|
859
|
+
this._velBuffer.length = 0;
|
|
860
|
+
const launched = this._startMomentum(velocity);
|
|
861
|
+
if (!launched) this.onDragEnd?.();
|
|
814
862
|
}
|
|
815
863
|
};
|
|
864
|
+
this._momentumEnabled = momentumOptions?.enabled ?? true;
|
|
865
|
+
this._momentumDecay = Math.min(0.99, Math.max(0.8, momentumOptions?.decay ?? 0.95));
|
|
866
|
+
this._momentumThreshold = momentumOptions?.threshold ?? 0.3;
|
|
867
|
+
this._momentumMaxVelocity = momentumOptions?.maxVelocity ?? 6;
|
|
816
868
|
this.attach();
|
|
817
869
|
}
|
|
818
870
|
attach() {
|
|
@@ -823,12 +875,14 @@ var _PanHandler = class _PanHandler {
|
|
|
823
875
|
window.addEventListener("touchmove", this.onTouchMove, { passive: false });
|
|
824
876
|
window.addEventListener("touchend", this.onMouseUp);
|
|
825
877
|
}
|
|
826
|
-
/** Cancel any in-progress pan gesture (e.g. when a drawing drag takes over). */
|
|
878
|
+
/** Cancel any in-progress pan gesture or momentum (e.g. when a drawing drag takes over). */
|
|
827
879
|
cancel() {
|
|
880
|
+
this._stopMomentum();
|
|
828
881
|
this.isDragging = false;
|
|
829
882
|
this.hasPanned = false;
|
|
830
883
|
}
|
|
831
884
|
destroy() {
|
|
885
|
+
this._stopMomentum();
|
|
832
886
|
this.canvas.removeEventListener("mousedown", this.onMouseDown);
|
|
833
887
|
window.removeEventListener("mousemove", this.onMouseMove);
|
|
834
888
|
window.removeEventListener("mouseup", this.onMouseUp);
|
|
@@ -836,6 +890,73 @@ var _PanHandler = class _PanHandler {
|
|
|
836
890
|
window.removeEventListener("touchmove", this.onTouchMove);
|
|
837
891
|
window.removeEventListener("touchend", this.onMouseUp);
|
|
838
892
|
}
|
|
893
|
+
// ── Velocity tracking ────────────────────────────────────────────────────────
|
|
894
|
+
_recordVelocitySample(dx, t) {
|
|
895
|
+
this._velBuffer.push({ dx, t });
|
|
896
|
+
const cutoff = t - _PanHandler.VEL_WINDOW_MS;
|
|
897
|
+
let i = 0;
|
|
898
|
+
while (i < this._velBuffer.length && this._velBuffer[i].t < cutoff) i++;
|
|
899
|
+
if (i > 0) this._velBuffer.splice(0, i);
|
|
900
|
+
}
|
|
901
|
+
_computeReleaseVelocity() {
|
|
902
|
+
const buf = this._velBuffer;
|
|
903
|
+
if (buf.length < 2) return 0;
|
|
904
|
+
const totalDx = buf.reduce((acc, s) => acc + s.dx, 0);
|
|
905
|
+
const dt = buf[buf.length - 1].t - buf[0].t;
|
|
906
|
+
if (dt <= 0) return 0;
|
|
907
|
+
const raw = totalDx / dt;
|
|
908
|
+
return Math.sign(raw) * Math.min(Math.abs(raw), this._momentumMaxVelocity);
|
|
909
|
+
}
|
|
910
|
+
// ── Momentum animation ────────────────────────────────────────────────────────
|
|
911
|
+
/**
|
|
912
|
+
* Launch momentum scrolling with the given initial velocity (px/ms).
|
|
913
|
+
* Returns `true` if momentum was successfully launched, `false` otherwise.
|
|
914
|
+
*/
|
|
915
|
+
_startMomentum(velocity) {
|
|
916
|
+
if (!this._momentumEnabled) return false;
|
|
917
|
+
if (Math.abs(velocity) < this._momentumThreshold) return false;
|
|
918
|
+
let lastTime = null;
|
|
919
|
+
let vel = velocity;
|
|
920
|
+
const decay = this._momentumDecay;
|
|
921
|
+
const tick = (now) => {
|
|
922
|
+
if (lastTime === null) {
|
|
923
|
+
lastTime = now;
|
|
924
|
+
this._momentumRafId = requestAnimationFrame(tick);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
const dt = now - lastTime;
|
|
928
|
+
lastTime = now;
|
|
929
|
+
vel *= Math.pow(decay, dt / 16.67);
|
|
930
|
+
const pixelDelta = vel * dt;
|
|
931
|
+
if (Math.abs(pixelDelta) < 0.5) {
|
|
932
|
+
this._stopMomentum();
|
|
933
|
+
this.onDragEnd?.();
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (this.isLocked()) {
|
|
937
|
+
this._stopMomentum();
|
|
938
|
+
this.onDragEnd?.();
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const prevStart = this.getViewport().startIndex;
|
|
942
|
+
this.pan(pixelDelta);
|
|
943
|
+
if (this.getViewport().startIndex === prevStart) {
|
|
944
|
+
this._stopMomentum();
|
|
945
|
+
this.onDragEnd?.();
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
this._momentumRafId = requestAnimationFrame(tick);
|
|
949
|
+
};
|
|
950
|
+
this._momentumRafId = requestAnimationFrame(tick);
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
953
|
+
_stopMomentum() {
|
|
954
|
+
if (this._momentumRafId !== null) {
|
|
955
|
+
cancelAnimationFrame(this._momentumRafId);
|
|
956
|
+
this._momentumRafId = null;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
// ── Core pan logic ────────────────────────────────────────────────────────────
|
|
839
960
|
pan(deltaX) {
|
|
840
961
|
const vp = this.getViewport();
|
|
841
962
|
const visibleBars = vp.endIndex - vp.startIndex;
|
|
@@ -849,6 +970,7 @@ var _PanHandler = class _PanHandler {
|
|
|
849
970
|
}
|
|
850
971
|
};
|
|
851
972
|
_PanHandler.DRAG_THRESHOLD = 4;
|
|
973
|
+
_PanHandler.VEL_WINDOW_MS = 100;
|
|
852
974
|
var PanHandler = _PanHandler;
|
|
853
975
|
|
|
854
976
|
// src/interaction/ZoomHandler.ts
|
|
@@ -888,11 +1010,11 @@ var ZoomHandler = class {
|
|
|
888
1010
|
Math.max(MIN_VISIBLE_BARS, Math.min(MAX_VISIBLE_BARS, visibleBars * zoomFactor))
|
|
889
1011
|
);
|
|
890
1012
|
if (vp.endIndex >= this.totalBars()) {
|
|
891
|
-
const newEnd = vp.endIndex;
|
|
1013
|
+
const newEnd = Math.min(vp.endIndex, this.totalBars() + Math.floor(newVisible / 2));
|
|
892
1014
|
const newStart = Math.max(0, newEnd - newVisible);
|
|
893
|
-
this.setViewport({ ...vp, startIndex: newStart, endIndex:
|
|
1015
|
+
this.setViewport({ ...vp, startIndex: newStart, endIndex: newStart + newVisible });
|
|
894
1016
|
} else {
|
|
895
|
-
const mouseRatio = e.offsetX / this.
|
|
1017
|
+
const mouseRatio = e.offsetX / this.getChartWidth();
|
|
896
1018
|
const pivotBar = vp.startIndex + visibleBars * mouseRatio;
|
|
897
1019
|
const newStart = Math.max(0, pivotBar - newVisible * mouseRatio);
|
|
898
1020
|
this.setViewport({ ...vp, startIndex: newStart, endIndex: newStart + newVisible });
|
|
@@ -1429,18 +1551,18 @@ function renderHighLow(ctx, bars, viewport, priceRange, scale, theme, chartWidth
|
|
|
1429
1551
|
}
|
|
1430
1552
|
|
|
1431
1553
|
// src/overlays/GridOverlay.ts
|
|
1432
|
-
function renderGrid(ctx, priceRange, scale, theme, chartWidth, chartHeight, gridLines
|
|
1554
|
+
function renderGrid(ctx, priceRange, scale, theme, chartWidth, chartHeight, gridLines) {
|
|
1433
1555
|
ctx.save();
|
|
1434
1556
|
ctx.globalAlpha = 0.6;
|
|
1435
1557
|
ctx.strokeStyle = theme.grid;
|
|
1436
1558
|
ctx.lineWidth = 1;
|
|
1437
|
-
const step = (priceRange.max - priceRange.min) / gridLines;
|
|
1438
1559
|
ctx.beginPath();
|
|
1439
|
-
|
|
1440
|
-
const price
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1560
|
+
{
|
|
1561
|
+
for (const price of generatePriceTicks(priceRange, chartHeight)) {
|
|
1562
|
+
const y = Math.round(scale.yToPixel(price, priceRange, chartHeight)) + 0.5;
|
|
1563
|
+
ctx.moveTo(0, y);
|
|
1564
|
+
ctx.lineTo(chartWidth, y);
|
|
1565
|
+
}
|
|
1444
1566
|
}
|
|
1445
1567
|
ctx.stroke();
|
|
1446
1568
|
const vertCols = Math.max(1, Math.floor(chartWidth / 80));
|
|
@@ -1457,19 +1579,19 @@ function renderGrid(ctx, priceRange, scale, theme, chartWidth, chartHeight, grid
|
|
|
1457
1579
|
|
|
1458
1580
|
// src/overlays/CrosshairOverlay.ts
|
|
1459
1581
|
var SUB_DAILY = ["1m", "5m", "15m", "30m", "1h", "4h"];
|
|
1460
|
-
function formatCrosshairTime(ts, timeframe) {
|
|
1582
|
+
function formatCrosshairTime(ts, timeframe, timezone = "UTC") {
|
|
1461
1583
|
const d = new Date(ts);
|
|
1462
1584
|
if (SUB_DAILY.includes(timeframe)) {
|
|
1463
|
-
const date = d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone:
|
|
1464
|
-
const time = d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false, timeZone:
|
|
1585
|
+
const date = d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: timezone });
|
|
1586
|
+
const time = d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false, timeZone: timezone });
|
|
1465
1587
|
return `${date} ${time}`;
|
|
1466
1588
|
}
|
|
1467
1589
|
if (timeframe === "1M") {
|
|
1468
|
-
return d.toLocaleDateString("en-US", { month: "short", year: "numeric", timeZone:
|
|
1590
|
+
return d.toLocaleDateString("en-US", { month: "short", year: "numeric", timeZone: timezone });
|
|
1469
1591
|
}
|
|
1470
|
-
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone:
|
|
1592
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: timezone });
|
|
1471
1593
|
}
|
|
1472
|
-
function renderCrosshair(ctx, pos, priceRange, viewport, scale, theme, chartWidth, chartHeight, priceAxisWidth, axisY, timeframe, pricePrecision = 2, cfg = DEFAULT_CROSSHAIR_CONFIG) {
|
|
1594
|
+
function renderCrosshair(ctx, pos, priceRange, viewport, scale, theme, chartWidth, chartHeight, priceAxisWidth, axisY, timeframe, pricePrecision = 2, cfg = DEFAULT_CROSSHAIR_CONFIG, timezone = "UTC") {
|
|
1473
1595
|
const x = scale.xToPixel(pos.barIndex, viewport, chartWidth);
|
|
1474
1596
|
const y = scale.yToPixel(pos.price, priceRange, chartHeight);
|
|
1475
1597
|
ctx.setLineDash([4, 4]);
|
|
@@ -1497,7 +1619,7 @@ function renderCrosshair(ctx, pos, priceRange, viewport, scale, theme, chartWidt
|
|
|
1497
1619
|
ctx.textAlign = "left";
|
|
1498
1620
|
ctx.fillText(priceLabel, chartWidth + labelPad, y + 4);
|
|
1499
1621
|
if (pos.bar) {
|
|
1500
|
-
const timeLabel = formatCrosshairTime(pos.bar.time, timeframe);
|
|
1622
|
+
const timeLabel = formatCrosshairTime(pos.bar.time, timeframe, timezone);
|
|
1501
1623
|
const tw = ctx.measureText(timeLabel).width;
|
|
1502
1624
|
const boxW = tw + labelPad * 2;
|
|
1503
1625
|
const boxX = Math.max(0, Math.min(chartWidth - boxW, x - boxW / 2));
|
|
@@ -1512,8 +1634,70 @@ function renderCrosshair(ctx, pos, priceRange, viewport, scale, theme, chartWidt
|
|
|
1512
1634
|
}
|
|
1513
1635
|
}
|
|
1514
1636
|
|
|
1637
|
+
// src/overlays/TradeLevelCluster.ts
|
|
1638
|
+
function detectClusters(measured, thresholdPx, excludeLabels = /* @__PURE__ */ new Set()) {
|
|
1639
|
+
const eligible = measured.filter((m) => !excludeLabels.has(m.level.label)).sort((a, b) => a.y - b.y);
|
|
1640
|
+
if (eligible.length < 2) return [];
|
|
1641
|
+
const groups = [];
|
|
1642
|
+
let currentGroup = [eligible[0]];
|
|
1643
|
+
let groupStartY = eligible[0].y;
|
|
1644
|
+
for (let i = 1; i < eligible.length; i++) {
|
|
1645
|
+
const m = eligible[i];
|
|
1646
|
+
if (m.y - groupStartY <= thresholdPx) {
|
|
1647
|
+
currentGroup.push(m);
|
|
1648
|
+
} else {
|
|
1649
|
+
groups.push(currentGroup);
|
|
1650
|
+
currentGroup = [m];
|
|
1651
|
+
groupStartY = m.y;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
groups.push(currentGroup);
|
|
1655
|
+
return groups.filter((g) => g.length >= 2).map((members) => {
|
|
1656
|
+
const id = "cluster:" + members.map((m) => m.level.label).sort().join("|");
|
|
1657
|
+
const centerY = members.reduce((sum, m) => sum + m.y, 0) / members.length;
|
|
1658
|
+
const centerPrice = members.reduce((sum, m) => sum + m.price, 0) / members.length;
|
|
1659
|
+
return { id, members, centerY, centerPrice };
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
function computeFanOutLayout(cluster, boxH, gapPx = 4) {
|
|
1663
|
+
const n = cluster.members.length;
|
|
1664
|
+
const totalH = n * boxH + (n - 1) * gapPx;
|
|
1665
|
+
const startY = cluster.centerY - totalH / 2;
|
|
1666
|
+
const sorted = [...cluster.members].sort((a, b) => a.y - b.y);
|
|
1667
|
+
const result = /* @__PURE__ */ new Map();
|
|
1668
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
1669
|
+
result.set(sorted[i].level.label, startY + i * (boxH + gapPx));
|
|
1670
|
+
}
|
|
1671
|
+
return result;
|
|
1672
|
+
}
|
|
1673
|
+
function buildClusterBadgeLayout(cluster, chartW, ctx) {
|
|
1674
|
+
const n = cluster.members.length;
|
|
1675
|
+
const types = new Set(cluster.members.map((m) => m.level.type));
|
|
1676
|
+
let noun;
|
|
1677
|
+
if (types.size === 1) {
|
|
1678
|
+
const t = types.values().next().value;
|
|
1679
|
+
noun = t === "position" ? "Position" : t === "pending" ? "Order" : "Trade";
|
|
1680
|
+
} else {
|
|
1681
|
+
noun = "Level";
|
|
1682
|
+
}
|
|
1683
|
+
const text = `${n} ${noun}${n > 1 ? "s" : ""}`;
|
|
1684
|
+
const PAD_X = 12;
|
|
1685
|
+
const textW = ctx.measureText(text).width;
|
|
1686
|
+
const EXPAND_ICON_W = 16;
|
|
1687
|
+
const badgeW = PAD_X + textW + EXPAND_ICON_W + PAD_X;
|
|
1688
|
+
const badgeH = 24;
|
|
1689
|
+
const badgeX = (chartW - badgeW) / 2;
|
|
1690
|
+
const badgeY = cluster.centerY - badgeH / 2;
|
|
1691
|
+
return { badgeX, badgeY, badgeW, badgeH, text };
|
|
1692
|
+
}
|
|
1693
|
+
function hitTestClusterBadge(x, y, areas) {
|
|
1694
|
+
return areas.find((a) => x >= a.x && x <= a.x + a.w && y >= a.y && y <= a.y + a.h) ?? null;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1515
1697
|
// src/overlays/TradeLevelOverlay.ts
|
|
1516
1698
|
var BOX_PAD_X = 8;
|
|
1699
|
+
var PILL_PAD_X = 6;
|
|
1700
|
+
var PILL_GAP = 6;
|
|
1517
1701
|
var BOX_PAD_Y = 4;
|
|
1518
1702
|
var ROW_H = 16;
|
|
1519
1703
|
var BOX_RADIUS = 3;
|
|
@@ -1630,7 +1814,7 @@ function isDraggable(level) {
|
|
|
1630
1814
|
return !!level.entryPriceEditable;
|
|
1631
1815
|
return false;
|
|
1632
1816
|
}
|
|
1633
|
-
function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH, precision, hoveredLabel = null, draggingLabel = null, dragNewPrice = null, selectedLabel = null, pendingLabels = /* @__PURE__ */ new Set(), positionStyle = "line", hideConfirmCancel = false, draftOrderLabel = null, draftBracketPnl = {}) {
|
|
1817
|
+
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) {
|
|
1634
1818
|
if (levels.length === 0)
|
|
1635
1819
|
return {
|
|
1636
1820
|
hitAreas: [],
|
|
@@ -1638,7 +1822,9 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1638
1822
|
hoverAreas: [],
|
|
1639
1823
|
dragAreas: [],
|
|
1640
1824
|
confirmAreas: [],
|
|
1641
|
-
addBracketAreas: []
|
|
1825
|
+
addBracketAreas: [],
|
|
1826
|
+
clusterBadgeAreas: [],
|
|
1827
|
+
clusters: []
|
|
1642
1828
|
};
|
|
1643
1829
|
const colors = theme.tradeLevels;
|
|
1644
1830
|
const boxH = ROW_H + BOX_PAD_Y * 2;
|
|
@@ -1666,16 +1852,52 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1666
1852
|
const effectivePending = hasPending && !hideConfirmCancel;
|
|
1667
1853
|
const confirmW = effectivePending ? (CONFIRM_R * 2 + CONFIRM_GAP) * 2 : 0;
|
|
1668
1854
|
const closeW = effectivePending || hideConfirmCancel ? 0 : EDIT_R * 2 + EDIT_GAP + CLOSE_R * 2 + BOX_PAD_X / 2;
|
|
1669
|
-
const
|
|
1855
|
+
const isDraftLevel = level.data?._isDraft === true;
|
|
1856
|
+
const qtyPillText = showQuantityField && isDraftLevel && level.type === "pending" && level.lots !== void 0 ? String(level.lots) : null;
|
|
1857
|
+
const pillW = qtyPillText ? PILL_PAD_X + ctx.measureText(qtyPillText).width + PILL_PAD_X + PILL_GAP : 0;
|
|
1858
|
+
const bW = BOX_PAD_X + pillW + pnlW + labelW + BOX_PAD_X + confirmW + closeW;
|
|
1670
1859
|
measured.push({ level, price, y, boxW: bW, boxH });
|
|
1671
1860
|
}
|
|
1861
|
+
const excludeFromClustering = /* @__PURE__ */ new Set();
|
|
1862
|
+
if (draftOrderLabel) excludeFromClustering.add(draftOrderLabel);
|
|
1863
|
+
const clusters = clusterThresholdPx > 0 ? detectClusters(measured, clusterThresholdPx, excludeFromClustering) : [];
|
|
1864
|
+
const collapsedClusters = clusters.filter((c) => !expandedClusters.has(c.id));
|
|
1865
|
+
const expandedClusterList = clusters.filter((c) => expandedClusters.has(c.id));
|
|
1866
|
+
const collapsedLabels = /* @__PURE__ */ new Set();
|
|
1867
|
+
for (const c of collapsedClusters) {
|
|
1868
|
+
for (const m of c.members) collapsedLabels.add(m.level.label);
|
|
1869
|
+
}
|
|
1870
|
+
const expandedFanLayouts = /* @__PURE__ */ new Map();
|
|
1871
|
+
for (const ec of expandedClusterList) {
|
|
1872
|
+
expandedFanLayouts.set(ec.id, computeFanOutLayout(ec, boxH, 4));
|
|
1873
|
+
}
|
|
1874
|
+
const labelToExpandedCluster = /* @__PURE__ */ new Map();
|
|
1875
|
+
for (const ec of expandedClusterList) {
|
|
1876
|
+
for (const m of ec.members) labelToExpandedCluster.set(m.level.label, ec);
|
|
1877
|
+
}
|
|
1672
1878
|
measured.sort((a, b) => a.y - b.y);
|
|
1673
1879
|
const placed = [];
|
|
1674
1880
|
const layouts = /* @__PURE__ */ new Map();
|
|
1675
1881
|
for (const m of measured) {
|
|
1676
|
-
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1882
|
+
if (collapsedLabels.has(m.level.label)) continue;
|
|
1883
|
+
const ec = labelToExpandedCluster.get(m.level.label);
|
|
1884
|
+
if (ec) {
|
|
1885
|
+
const fanMap = expandedFanLayouts.get(ec.id);
|
|
1886
|
+
const fanY = fanMap?.get(m.level.label);
|
|
1887
|
+
if (fanY !== void 0) {
|
|
1888
|
+
placed.push({ label: m.level.label, boxY: fanY, boxH: m.boxH });
|
|
1889
|
+
layouts.set(m.level.label, { price: m.price, y: m.y, boxY: fanY, boxW: m.boxW, boxH: m.boxH });
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
let boxY = m.y - m.boxH / 2;
|
|
1894
|
+
let attempts = 0;
|
|
1895
|
+
while (attempts < placed.length + 1) {
|
|
1896
|
+
const overlaps = placed.some((p) => rectsOverlapY(boxY, m.boxH, p.boxY, p.boxH));
|
|
1897
|
+
if (!overlaps) break;
|
|
1898
|
+
boxY -= m.boxH + 2;
|
|
1899
|
+
attempts++;
|
|
1900
|
+
}
|
|
1679
1901
|
placed.push({ label: m.level.label, boxY, boxH: m.boxH });
|
|
1680
1902
|
layouts.set(m.level.label, { price: m.price, y: m.y, boxY, boxW: m.boxW, boxH: m.boxH });
|
|
1681
1903
|
}
|
|
@@ -1691,9 +1913,10 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1691
1913
|
selectedLabel
|
|
1692
1914
|
);
|
|
1693
1915
|
const activeLabel = hoveredLabel ?? selectedLabel;
|
|
1916
|
+
const visibleMeasured = measured.filter((m) => !collapsedLabels.has(m.level.label));
|
|
1694
1917
|
const drawOrder = [
|
|
1695
|
-
...
|
|
1696
|
-
...
|
|
1918
|
+
...visibleMeasured.filter((m) => m.level.label !== activeLabel),
|
|
1919
|
+
...visibleMeasured.filter((m) => m.level.label === activeLabel)
|
|
1697
1920
|
];
|
|
1698
1921
|
const hitAreas = [];
|
|
1699
1922
|
const editAreas = [];
|
|
@@ -1701,6 +1924,11 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1701
1924
|
const dragAreas = [];
|
|
1702
1925
|
const confirmAreas = [];
|
|
1703
1926
|
const addBracketAreas = [];
|
|
1927
|
+
const clusterBadgeAreas = [];
|
|
1928
|
+
const draftQtyPillAreas = [];
|
|
1929
|
+
for (const cluster of collapsedClusters) {
|
|
1930
|
+
drawClusterBadge(ctx, cluster, chartW, theme, clusterBadgeAreas);
|
|
1931
|
+
}
|
|
1704
1932
|
for (const { level } of drawOrder) {
|
|
1705
1933
|
const layout = layouts.get(level.label);
|
|
1706
1934
|
if (!layout) continue;
|
|
@@ -1729,11 +1957,148 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1729
1957
|
confirmAreas,
|
|
1730
1958
|
addBracketAreas,
|
|
1731
1959
|
hideConfirmCancel,
|
|
1732
|
-
level.label === draftOrderLabel ? draftBracketPnl : void 0
|
|
1960
|
+
level.label === draftOrderLabel ? draftBracketPnl : void 0,
|
|
1961
|
+
showQuantityField,
|
|
1962
|
+
draftQtyPillAreas
|
|
1733
1963
|
);
|
|
1964
|
+
const ec = labelToExpandedCluster.get(level.label);
|
|
1965
|
+
if (ec && layout.boxY !== layout.y - boxH / 2) {
|
|
1966
|
+
drawFanConnector(ctx, layout.boxY + boxH / 2, layout.y, chartW, theme);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
if (selectedLabel !== null && !layouts.has(selectedLabel)) {
|
|
1970
|
+
const offLevel = levels.find((l) => l.label === selectedLabel);
|
|
1971
|
+
if (offLevel) {
|
|
1972
|
+
const bh = offLevel;
|
|
1973
|
+
const slDragKey = `${offLevel.label}:stopLoss`;
|
|
1974
|
+
const tpDragKey = `${offLevel.label}:takeProfit`;
|
|
1975
|
+
const fallbackX = chartW * BRACKET_BASE_RATIO;
|
|
1976
|
+
if (bh.stopLossPrice !== void 0) {
|
|
1977
|
+
drawBracketLine(
|
|
1978
|
+
ctx,
|
|
1979
|
+
bh,
|
|
1980
|
+
"stopLoss",
|
|
1981
|
+
bh.stopLossPrice,
|
|
1982
|
+
scale,
|
|
1983
|
+
priceRange,
|
|
1984
|
+
priceH,
|
|
1985
|
+
bracketLineXMap.get(slDragKey) ?? fallbackX,
|
|
1986
|
+
chartW,
|
|
1987
|
+
theme,
|
|
1988
|
+
precision,
|
|
1989
|
+
draggingLabel,
|
|
1990
|
+
dragNewPrice,
|
|
1991
|
+
hitAreas,
|
|
1992
|
+
dragAreas,
|
|
1993
|
+
hideConfirmCancel,
|
|
1994
|
+
bh._slOrderLabel
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
if (bh.takeProfitPrice !== void 0) {
|
|
1998
|
+
drawBracketLine(
|
|
1999
|
+
ctx,
|
|
2000
|
+
bh,
|
|
2001
|
+
"takeProfit",
|
|
2002
|
+
bh.takeProfitPrice,
|
|
2003
|
+
scale,
|
|
2004
|
+
priceRange,
|
|
2005
|
+
priceH,
|
|
2006
|
+
bracketLineXMap.get(tpDragKey) ?? fallbackX,
|
|
2007
|
+
chartW,
|
|
2008
|
+
theme,
|
|
2009
|
+
precision,
|
|
2010
|
+
draggingLabel,
|
|
2011
|
+
dragNewPrice,
|
|
2012
|
+
hitAreas,
|
|
2013
|
+
dragAreas,
|
|
2014
|
+
hideConfirmCancel,
|
|
2015
|
+
bh._tpOrderLabel
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
ctx.restore();
|
|
2021
|
+
return {
|
|
2022
|
+
hitAreas,
|
|
2023
|
+
editAreas,
|
|
2024
|
+
hoverAreas,
|
|
2025
|
+
dragAreas,
|
|
2026
|
+
confirmAreas,
|
|
2027
|
+
addBracketAreas,
|
|
2028
|
+
clusterBadgeAreas,
|
|
2029
|
+
clusters,
|
|
2030
|
+
draftQtyPillArea: draftQtyPillAreas[0]
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
var BADGE_FONT = "bold 12px 'Inter', system-ui, sans-serif";
|
|
2034
|
+
var BADGE_PAD_X = 12;
|
|
2035
|
+
var BADGE_H = 24;
|
|
2036
|
+
var BADGE_RADIUS = 12;
|
|
2037
|
+
function drawClusterBadge(ctx, cluster, chartW, theme, clusterBadgeAreas) {
|
|
2038
|
+
const mono = monoColor(theme);
|
|
2039
|
+
const isDark = theme.background !== "#ffffff";
|
|
2040
|
+
ctx.save();
|
|
2041
|
+
ctx.strokeStyle = mono;
|
|
2042
|
+
ctx.globalAlpha = 0.12;
|
|
2043
|
+
ctx.lineWidth = 1;
|
|
2044
|
+
ctx.setLineDash([4, 4]);
|
|
2045
|
+
for (const m of cluster.members) {
|
|
2046
|
+
ctx.beginPath();
|
|
2047
|
+
ctx.moveTo(0, m.y);
|
|
2048
|
+
ctx.lineTo(chartW, m.y);
|
|
2049
|
+
ctx.stroke();
|
|
1734
2050
|
}
|
|
2051
|
+
ctx.setLineDash([]);
|
|
2052
|
+
ctx.restore();
|
|
2053
|
+
ctx.save();
|
|
2054
|
+
ctx.font = BADGE_FONT;
|
|
2055
|
+
const layout = buildClusterBadgeLayout(cluster, chartW, ctx);
|
|
2056
|
+
const { badgeX, badgeY, badgeW, text } = layout;
|
|
2057
|
+
const expandIcon = " \u25B8";
|
|
2058
|
+
const bgColor = isDark ? "rgba(40,40,40,0.92)" : "rgba(240,240,240,0.92)";
|
|
2059
|
+
ctx.fillStyle = bgColor;
|
|
2060
|
+
ctx.beginPath();
|
|
2061
|
+
ctx.roundRect(badgeX, badgeY, badgeW, BADGE_H, BADGE_RADIUS);
|
|
2062
|
+
ctx.fill();
|
|
2063
|
+
ctx.strokeStyle = mono;
|
|
2064
|
+
ctx.globalAlpha = 0.3;
|
|
2065
|
+
ctx.lineWidth = 1;
|
|
2066
|
+
ctx.stroke();
|
|
2067
|
+
ctx.globalAlpha = 1;
|
|
2068
|
+
ctx.fillStyle = mono;
|
|
2069
|
+
ctx.globalAlpha = 0.85;
|
|
2070
|
+
ctx.textBaseline = "middle";
|
|
2071
|
+
ctx.textAlign = "left";
|
|
2072
|
+
ctx.fillText(text + expandIcon, badgeX + BADGE_PAD_X, badgeY + BADGE_H / 2);
|
|
2073
|
+
ctx.globalAlpha = 1;
|
|
2074
|
+
ctx.restore();
|
|
2075
|
+
clusterBadgeAreas.push({
|
|
2076
|
+
clusterId: cluster.id,
|
|
2077
|
+
x: badgeX,
|
|
2078
|
+
y: badgeY,
|
|
2079
|
+
w: badgeW,
|
|
2080
|
+
h: BADGE_H
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
function drawFanConnector(ctx, fanCenterY, realY, chartW, theme) {
|
|
2084
|
+
if (Math.abs(fanCenterY - realY) < 2) return;
|
|
2085
|
+
const mono = monoColor(theme);
|
|
2086
|
+
ctx.save();
|
|
2087
|
+
ctx.strokeStyle = mono;
|
|
2088
|
+
ctx.globalAlpha = 0.25;
|
|
2089
|
+
ctx.lineWidth = 1;
|
|
2090
|
+
ctx.setLineDash([2, 3]);
|
|
2091
|
+
const connectorX = chartW - 8;
|
|
2092
|
+
ctx.beginPath();
|
|
2093
|
+
ctx.moveTo(connectorX, fanCenterY);
|
|
2094
|
+
ctx.lineTo(connectorX, realY);
|
|
2095
|
+
ctx.stroke();
|
|
2096
|
+
ctx.setLineDash([]);
|
|
2097
|
+
ctx.beginPath();
|
|
2098
|
+
ctx.moveTo(connectorX - 4, realY);
|
|
2099
|
+
ctx.lineTo(connectorX + 4, realY);
|
|
2100
|
+
ctx.stroke();
|
|
1735
2101
|
ctx.restore();
|
|
1736
|
-
return { hitAreas, editAreas, hoverAreas, dragAreas, confirmAreas, addBracketAreas };
|
|
1737
2102
|
}
|
|
1738
2103
|
function drawDotHoverBox(ctx, level, y, dotX, dotR, theme, precision) {
|
|
1739
2104
|
const pl = level;
|
|
@@ -1793,7 +2158,7 @@ function drawDotHoverBox(ctx, level, y, dotX, dotR, theme, precision) {
|
|
|
1793
2158
|
});
|
|
1794
2159
|
ctx.restore();
|
|
1795
2160
|
}
|
|
1796
|
-
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) {
|
|
2161
|
+
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) {
|
|
1797
2162
|
const { y, boxY, boxW, boxH } = layout;
|
|
1798
2163
|
const color = levelColor(level, colors, theme);
|
|
1799
2164
|
const active = hovered || selected;
|
|
@@ -1981,6 +2346,29 @@ function drawLevel(ctx, level, layout, chartW, colors, theme, precision, hovered
|
|
|
1981
2346
|
ctx.fill();
|
|
1982
2347
|
ctx.stroke();
|
|
1983
2348
|
let textX = boxX + BOX_PAD_X;
|
|
2349
|
+
const isDraftLevel = level.data?._isDraft === true;
|
|
2350
|
+
if (showQuantityField && isDraftLevel && level.type === "pending" && level.lots !== void 0) {
|
|
2351
|
+
const qtyText = String(level.lots);
|
|
2352
|
+
ctx.font = FONT_MAIN;
|
|
2353
|
+
const pillContentW = ctx.measureText(qtyText).width;
|
|
2354
|
+
const pillW = PILL_PAD_X + pillContentW + PILL_PAD_X;
|
|
2355
|
+
const pillX = textX;
|
|
2356
|
+
ctx.save();
|
|
2357
|
+
ctx.strokeStyle = monoColor(theme);
|
|
2358
|
+
ctx.globalAlpha = 0.35;
|
|
2359
|
+
ctx.lineWidth = 1;
|
|
2360
|
+
ctx.beginPath();
|
|
2361
|
+
ctx.moveTo(pillX + pillW, boxY + 3);
|
|
2362
|
+
ctx.lineTo(pillX + pillW, boxY + boxH - 3);
|
|
2363
|
+
ctx.stroke();
|
|
2364
|
+
ctx.restore();
|
|
2365
|
+
ctx.fillStyle = monoColor(theme);
|
|
2366
|
+
ctx.textAlign = "left";
|
|
2367
|
+
ctx.textBaseline = "middle";
|
|
2368
|
+
ctx.fillText(qtyText, pillX + PILL_PAD_X, midY);
|
|
2369
|
+
draftQtyPillAreas?.push({ label: level.label, pillX, pillY: boxY, pillW, pillH: boxH });
|
|
2370
|
+
textX += pillW + PILL_GAP;
|
|
2371
|
+
}
|
|
1984
2372
|
if (pnlText) {
|
|
1985
2373
|
const pnlColor = pnl !== void 0 ? pnl >= 0 ? theme.candle.up : theme.candle.down : monoColor(theme);
|
|
1986
2374
|
ctx.font = FONT_PNL;
|
|
@@ -2364,6 +2752,152 @@ function hitTestTradeLevelAddBracket(x, y, areas) {
|
|
|
2364
2752
|
return areas.find((a) => x >= a.x && x <= a.x + a.w && y >= a.y && y <= a.y + a.h) ?? null;
|
|
2365
2753
|
}
|
|
2366
2754
|
|
|
2755
|
+
// src/overlays/OffViewportLevelIndicator.ts
|
|
2756
|
+
function collectOffViewportMarkers(levels, priceRange, draggingLabel) {
|
|
2757
|
+
const above = [];
|
|
2758
|
+
const below = [];
|
|
2759
|
+
const { min, max } = priceRange;
|
|
2760
|
+
for (const level of levels) {
|
|
2761
|
+
if (level.label === draggingLabel) continue;
|
|
2762
|
+
const isDraftMarket = level.type === "pending" && level.orderType === "market" && level.data?._isDraft === true;
|
|
2763
|
+
if (isDraftMarket) continue;
|
|
2764
|
+
if (isFinite(level.price)) {
|
|
2765
|
+
if (level.price > max) above.push({ price: level.price, label: level.label, kind: "entry" });
|
|
2766
|
+
else if (level.price < min) below.push({ price: level.price, label: level.label, kind: "entry" });
|
|
2767
|
+
}
|
|
2768
|
+
const slPrice = level.stopLossPrice;
|
|
2769
|
+
const tpPrice = level.takeProfitPrice;
|
|
2770
|
+
if (slPrice !== void 0 && isFinite(slPrice)) {
|
|
2771
|
+
if (slPrice > max) above.push({ price: slPrice, label: level.label, kind: "sl" });
|
|
2772
|
+
else if (slPrice < min) below.push({ price: slPrice, label: level.label, kind: "sl" });
|
|
2773
|
+
}
|
|
2774
|
+
if (tpPrice !== void 0 && isFinite(tpPrice)) {
|
|
2775
|
+
if (tpPrice > max) above.push({ price: tpPrice, label: level.label, kind: "tp" });
|
|
2776
|
+
else if (tpPrice < min) below.push({ price: tpPrice, label: level.label, kind: "tp" });
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
return { above, below };
|
|
2780
|
+
}
|
|
2781
|
+
function entryCount(markers) {
|
|
2782
|
+
let n = 0;
|
|
2783
|
+
for (const m of markers) if (m.kind === "entry") n++;
|
|
2784
|
+
return n;
|
|
2785
|
+
}
|
|
2786
|
+
function nearestEdgeMarker(markers, edge) {
|
|
2787
|
+
let best = markers[0];
|
|
2788
|
+
let bestDist = Math.abs(best.price - edge);
|
|
2789
|
+
for (let i = 1; i < markers.length; i++) {
|
|
2790
|
+
const d = Math.abs(markers[i].price - edge);
|
|
2791
|
+
if (d < bestDist) {
|
|
2792
|
+
best = markers[i];
|
|
2793
|
+
bestDist = d;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
return best;
|
|
2797
|
+
}
|
|
2798
|
+
var PILL_FONT = "11px 'Inter', system-ui, sans-serif";
|
|
2799
|
+
var PILL_PAD_X2 = 8;
|
|
2800
|
+
var PILL_RIGHT_MARGIN = 8;
|
|
2801
|
+
var PILL_V_MARGIN = 10;
|
|
2802
|
+
var PILL_H = 20;
|
|
2803
|
+
var PILL_RADIUS = 10;
|
|
2804
|
+
var ARROW_UP = "\u25B2";
|
|
2805
|
+
var ARROW_DOWN = "\u25BC";
|
|
2806
|
+
function renderOffViewportIndicators(ctx, levels, priceRange, theme, chartWidth, chartHeight, draggingLabel, selectedLabel = null) {
|
|
2807
|
+
const hitAreas = [];
|
|
2808
|
+
const PADDING_PX = 20;
|
|
2809
|
+
const usable = chartHeight - 2 * PADDING_PX;
|
|
2810
|
+
const span = priceRange.max - priceRange.min;
|
|
2811
|
+
const padInPrice = usable > 0 ? span / usable * PADDING_PX : 0;
|
|
2812
|
+
const visualRange = {
|
|
2813
|
+
min: priceRange.min - padInPrice,
|
|
2814
|
+
max: priceRange.max + padInPrice
|
|
2815
|
+
};
|
|
2816
|
+
const { above, below } = collectOffViewportMarkers(levels, visualRange, draggingLabel);
|
|
2817
|
+
if (above.length === 0 && below.length === 0) return { hitAreas };
|
|
2818
|
+
ctx.save();
|
|
2819
|
+
ctx.font = PILL_FONT;
|
|
2820
|
+
ctx.textBaseline = "middle";
|
|
2821
|
+
ctx.textAlign = "center";
|
|
2822
|
+
const PILL_GAP2 = 4;
|
|
2823
|
+
const renderPill = (direction, text, y, target) => {
|
|
2824
|
+
const textW = ctx.measureText(text).width;
|
|
2825
|
+
const w = Math.ceil(textW) + PILL_PAD_X2 * 2;
|
|
2826
|
+
const x = chartWidth - PILL_RIGHT_MARGIN - w;
|
|
2827
|
+
ctx.fillStyle = theme.tooltip.background;
|
|
2828
|
+
ctx.strokeStyle = theme.tooltip.border;
|
|
2829
|
+
ctx.lineWidth = 1;
|
|
2830
|
+
roundedRect(ctx, x, y, w, PILL_H, PILL_RADIUS);
|
|
2831
|
+
ctx.fill();
|
|
2832
|
+
ctx.stroke();
|
|
2833
|
+
ctx.fillStyle = theme.tooltip.text;
|
|
2834
|
+
ctx.fillText(text, x + w / 2, y + PILL_H / 2 + 0.5);
|
|
2835
|
+
hitAreas.push({
|
|
2836
|
+
direction,
|
|
2837
|
+
x,
|
|
2838
|
+
y,
|
|
2839
|
+
w,
|
|
2840
|
+
h: PILL_H,
|
|
2841
|
+
targetPrice: target.price,
|
|
2842
|
+
targetLabel: target.label,
|
|
2843
|
+
targetKind: target.kind
|
|
2844
|
+
});
|
|
2845
|
+
return w;
|
|
2846
|
+
};
|
|
2847
|
+
const drawColumn = (direction, markers, baseY) => {
|
|
2848
|
+
if (markers.length === 0) return;
|
|
2849
|
+
const arrow = direction === "up" ? ARROW_UP : ARROW_DOWN;
|
|
2850
|
+
const edge = direction === "up" ? priceRange.max : priceRange.min;
|
|
2851
|
+
const stackDir = direction === "up" ? 1 : -1;
|
|
2852
|
+
let nextY = baseY;
|
|
2853
|
+
const entries = entryCount(markers);
|
|
2854
|
+
if (entries > 0) {
|
|
2855
|
+
const target = nearestEdgeMarker(
|
|
2856
|
+
markers.filter((m) => m.kind === "entry"),
|
|
2857
|
+
edge
|
|
2858
|
+
);
|
|
2859
|
+
renderPill(direction, `${arrow} ${entries}`, nextY, target);
|
|
2860
|
+
nextY += stackDir * (PILL_H + PILL_GAP2);
|
|
2861
|
+
}
|
|
2862
|
+
if (selectedLabel !== null) {
|
|
2863
|
+
const bracketMarkers = markers.filter(
|
|
2864
|
+
(m) => m.kind !== "entry" && m.label === selectedLabel
|
|
2865
|
+
);
|
|
2866
|
+
for (const bm of bracketMarkers) {
|
|
2867
|
+
const kindLabel = bm.kind.toUpperCase();
|
|
2868
|
+
renderPill(direction, `${arrow} ${kindLabel}`, nextY, bm);
|
|
2869
|
+
nextY += stackDir * (PILL_H + PILL_GAP2);
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
};
|
|
2873
|
+
drawColumn("up", above, PILL_V_MARGIN);
|
|
2874
|
+
drawColumn("down", below, chartHeight - PILL_V_MARGIN - PILL_H);
|
|
2875
|
+
ctx.restore();
|
|
2876
|
+
return { hitAreas };
|
|
2877
|
+
}
|
|
2878
|
+
function hitTestOffViewportIndicator(x, y, hitAreas) {
|
|
2879
|
+
return hitAreas.find((h) => x >= h.x && x <= h.x + h.w && y >= h.y && y <= h.y + h.h) ?? null;
|
|
2880
|
+
}
|
|
2881
|
+
function overlapsOffViewportPill(centerY, halfH, hitAreas) {
|
|
2882
|
+
const top = centerY - halfH;
|
|
2883
|
+
const bottom = centerY + halfH;
|
|
2884
|
+
return hitAreas.some((h) => top < h.y + h.h && bottom > h.y);
|
|
2885
|
+
}
|
|
2886
|
+
function roundedRect(ctx, x, y, w, h, r) {
|
|
2887
|
+
const rr = Math.min(r, w / 2, h / 2);
|
|
2888
|
+
ctx.beginPath();
|
|
2889
|
+
ctx.moveTo(x + rr, y);
|
|
2890
|
+
ctx.lineTo(x + w - rr, y);
|
|
2891
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + rr);
|
|
2892
|
+
ctx.lineTo(x + w, y + h - rr);
|
|
2893
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - rr, y + h);
|
|
2894
|
+
ctx.lineTo(x + rr, y + h);
|
|
2895
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - rr);
|
|
2896
|
+
ctx.lineTo(x, y + rr);
|
|
2897
|
+
ctx.quadraticCurveTo(x, y, x + rr, y);
|
|
2898
|
+
ctx.closePath();
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2367
2901
|
// src/axis/TimeAxis.ts
|
|
2368
2902
|
var COUNTDOWN_LABEL_HEIGHT = 18;
|
|
2369
2903
|
var COUNTDOWN_LABEL_PADDING_X = 6;
|
|
@@ -2390,45 +2924,45 @@ function formatRemaining(totalSec) {
|
|
|
2390
2924
|
const d = Math.floor(totalSec % 604800 / 86400);
|
|
2391
2925
|
return `${w}w ${d}d`;
|
|
2392
2926
|
}
|
|
2393
|
-
function formatTimestamp(ts, visibleBars, timeframe, spansMultipleDays) {
|
|
2927
|
+
function formatTimestamp(ts, visibleBars, timeframe, spansMultipleDays, timezone = "UTC") {
|
|
2394
2928
|
const d = new Date(ts);
|
|
2395
2929
|
if (timeframe && INTRADAY_TIMEFRAMES.includes(timeframe)) {
|
|
2396
2930
|
const time = d.toLocaleTimeString("en-US", {
|
|
2397
2931
|
hour: "2-digit",
|
|
2398
2932
|
minute: "2-digit",
|
|
2399
2933
|
hour12: false,
|
|
2400
|
-
timeZone:
|
|
2934
|
+
timeZone: timezone
|
|
2401
2935
|
});
|
|
2402
2936
|
if (spansMultipleDays) {
|
|
2403
|
-
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone:
|
|
2937
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: timezone }) + " " + time;
|
|
2404
2938
|
}
|
|
2405
2939
|
return time;
|
|
2406
2940
|
}
|
|
2407
2941
|
if (timeframe === "1Y") {
|
|
2408
|
-
return d.toLocaleDateString("en-US", { year: "numeric", timeZone:
|
|
2942
|
+
return d.toLocaleDateString("en-US", { year: "numeric", timeZone: timezone });
|
|
2409
2943
|
}
|
|
2410
2944
|
if (timeframe === "1M") {
|
|
2411
|
-
return d.toLocaleDateString("en-US", { month: "short", year: "numeric", timeZone:
|
|
2945
|
+
return d.toLocaleDateString("en-US", { month: "short", year: "numeric", timeZone: timezone });
|
|
2412
2946
|
}
|
|
2413
2947
|
if (visibleBars <= 365) {
|
|
2414
2948
|
return d.toLocaleDateString("en-US", {
|
|
2415
2949
|
month: "short",
|
|
2416
2950
|
day: "numeric",
|
|
2417
|
-
timeZone:
|
|
2951
|
+
timeZone: timezone
|
|
2418
2952
|
});
|
|
2419
2953
|
}
|
|
2420
|
-
return d.toLocaleDateString("en-US", { month: "short", year: "2-digit", timeZone:
|
|
2954
|
+
return d.toLocaleDateString("en-US", { month: "short", year: "2-digit", timeZone: timezone });
|
|
2421
2955
|
}
|
|
2422
|
-
function renderTimeAxis(ctx, bars, viewport, scale, theme, chartWidth, axisY, maxLabels = 8, cfg = DEFAULT_TIME_AXIS_CONFIG, timeframe, countdownBarIndex) {
|
|
2956
|
+
function renderTimeAxis(ctx, bars, viewport, scale, theme, chartWidth, axisY, maxLabels = 8, cfg = DEFAULT_TIME_AXIS_CONFIG, timeframe, countdownBarIndex, timezone = "UTC") {
|
|
2423
2957
|
const visibleBars = viewport.endIndex - viewport.startIndex;
|
|
2424
2958
|
const firstBar = bars[0];
|
|
2425
2959
|
const lastBar = bars[bars.length - 1];
|
|
2426
|
-
const spansMultipleDays = !!firstBar && !!lastBar && new Date(firstBar.time).toLocaleDateString("en-US", { timeZone:
|
|
2960
|
+
const spansMultipleDays = !!firstBar && !!lastBar && new Date(firstBar.time).toLocaleDateString("en-US", { timeZone: timezone }) !== new Date(lastBar.time).toLocaleDateString("en-US", { timeZone: timezone });
|
|
2427
2961
|
ctx.fillStyle = theme.axisText;
|
|
2428
2962
|
ctx.font = `${cfg.fontSize} ${cfg.fontFamily}`;
|
|
2429
2963
|
ctx.textAlign = "center";
|
|
2430
2964
|
const MIN_GAP = 12;
|
|
2431
|
-
const sampleLabel = formatTimestamp(bars[Math.floor(bars.length / 2)]?.time ?? bars[0].time, visibleBars, timeframe, spansMultipleDays);
|
|
2965
|
+
const sampleLabel = formatTimestamp(bars[Math.floor(bars.length / 2)]?.time ?? bars[0].time, visibleBars, timeframe, spansMultipleDays, timezone);
|
|
2432
2966
|
const labelW = ctx.measureText(sampleLabel).width;
|
|
2433
2967
|
const minStep = Math.max(1, Math.floor(bars.length / maxLabels));
|
|
2434
2968
|
let step = minStep;
|
|
@@ -2443,7 +2977,7 @@ function renderTimeAxis(ctx, bars, viewport, scale, theme, chartWidth, axisY, ma
|
|
|
2443
2977
|
const barIndex = viewport.startIndex + i;
|
|
2444
2978
|
if (countdownBarIndex !== void 0 && barIndex === countdownBarIndex) continue;
|
|
2445
2979
|
const x = scale.xToPixel(barIndex, viewport, chartWidth);
|
|
2446
|
-
const label = formatTimestamp(bars[i].time, visibleBars, timeframe, spansMultipleDays);
|
|
2980
|
+
const label = formatTimestamp(bars[i].time, visibleBars, timeframe, spansMultipleDays, timezone);
|
|
2447
2981
|
const halfW = ctx.measureText(label).width / 2;
|
|
2448
2982
|
const clampedX = Math.max(halfW, Math.min(chartWidth - halfW, x));
|
|
2449
2983
|
ctx.fillText(label, clampedX, axisY + 14);
|
|
@@ -2478,10 +3012,20 @@ function renderCandleCountdown(ctx, lastBarTime, intervalMs, lastBarIndex, viewp
|
|
|
2478
3012
|
}
|
|
2479
3013
|
|
|
2480
3014
|
// src/axis/PriceAxis.ts
|
|
2481
|
-
function renderPriceAxis(ctx, priceRange, scale, theme, chartWidth, chartHeight, axisWidth, gridLines
|
|
3015
|
+
function renderPriceAxis(ctx, priceRange, scale, theme, chartWidth, chartHeight, axisWidth, gridLines, pricePrecision = 2, cfg = DEFAULT_PRICE_AXIS_CONFIG) {
|
|
2482
3016
|
ctx.fillStyle = theme.axisText;
|
|
2483
3017
|
ctx.font = `${cfg.fontSize} ${cfg.fontFamily}`;
|
|
2484
3018
|
ctx.textAlign = "left";
|
|
3019
|
+
if (gridLines === void 0) {
|
|
3020
|
+
const ticks = generatePriceTicks(priceRange, chartHeight);
|
|
3021
|
+
const step2 = ticks.length >= 2 ? ticks[1] - ticks[0] : priceRange.max - priceRange.min;
|
|
3022
|
+
const precision = precisionForStep(step2, pricePrecision);
|
|
3023
|
+
for (const price of ticks) {
|
|
3024
|
+
const y = scale.yToPixel(price, priceRange, chartHeight);
|
|
3025
|
+
ctx.fillText(price.toFixed(precision), chartWidth + 4, y + 4);
|
|
3026
|
+
}
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
2485
3029
|
const step = (priceRange.max - priceRange.min) / gridLines;
|
|
2486
3030
|
for (let i = 0; i <= gridLines; i++) {
|
|
2487
3031
|
const price = priceRange.min + step * i;
|
|
@@ -3096,6 +3640,8 @@ var ICON_TOOL = {
|
|
|
3096
3640
|
14
|
|
3097
3641
|
)
|
|
3098
3642
|
};
|
|
3643
|
+
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>`;
|
|
3644
|
+
var ICON_TFC_OFF = `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><polyline points="1.5,12.5 5,9 7,11 14,4" stroke="#26a69a" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.3"/><polyline points="10,4 14,4 14,8" stroke="#26a69a" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.3"/><polyline points="1.5,3.5 5,7 7,5 14,12" stroke="#ef5350" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.3"/><polyline points="10,12 14,12 14,8" stroke="#ef5350" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.3"/><line x1="3" y1="3" x2="13" y2="13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`;
|
|
3099
3645
|
var iconScrollToEnd = (c, size = 16) => `<svg viewBox="0 0 16 16" width="${size}" height="${size}" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 4L6 8L1 12Z" fill="${c}"/><path d="M6 4L11 8L6 12Z" fill="${c}"/><line x1="13.5" y1="3.5" x2="13.5" y2="12.5" stroke="${c}" stroke-width="2" stroke-linecap="round"/></svg>`;
|
|
3100
3646
|
var iconResetView = (c, size = 16) => `<svg viewBox="0 0 16 16" width="${size}" height="${size}" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 2A6 6 0 1 1 2 8" stroke="${c}" stroke-width="1.6" stroke-linecap="round"/><path d="M4 10L2 8L1 10" stroke="${c}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
3101
3647
|
var ICON_TAB = {
|
|
@@ -3131,7 +3677,7 @@ function injectFlyoutScrollbarStyles() {
|
|
|
3131
3677
|
document.head.appendChild(style);
|
|
3132
3678
|
}
|
|
3133
3679
|
var TopBar = class {
|
|
3134
|
-
constructor(onSeriesChange, onTimeframeChange, onAddIndicator, onDurationChange, onSettingsClick, onFullscreenToggle, onDrawingToolsToggle, showDrawingToolsBtn, showSettingsBtn, initialSeries, initialTf, initialDuration, durationMap, theme, cfg = DEFAULT_TOP_BAR_CONFIG, labels = DEFAULT_LABELS.topBar) {
|
|
3680
|
+
constructor(onSeriesChange, onTimeframeChange, onAddIndicator, onDurationChange, onSettingsClick, onFullscreenToggle, onDrawingToolsToggle, showDrawingToolsBtn, showSettingsBtn, showTfcBtn, initialTfcActive, onTfcToggle, initialSeries, initialTf, initialDuration, durationMap, theme, cfg = DEFAULT_TOP_BAR_CONFIG, labels = DEFAULT_LABELS.topBar) {
|
|
3135
3681
|
this.onSeriesChange = onSeriesChange;
|
|
3136
3682
|
this.onTimeframeChange = onTimeframeChange;
|
|
3137
3683
|
this.onAddIndicator = onAddIndicator;
|
|
@@ -3139,11 +3685,14 @@ var TopBar = class {
|
|
|
3139
3685
|
this.onSettingsClick = onSettingsClick;
|
|
3140
3686
|
this.onFullscreenToggle = onFullscreenToggle;
|
|
3141
3687
|
this.onDrawingToolsToggle = onDrawingToolsToggle;
|
|
3688
|
+
this.onTfcToggle = onTfcToggle;
|
|
3142
3689
|
this.settingsBtn = null;
|
|
3143
3690
|
this.isFullscreen = false;
|
|
3144
3691
|
this.durationBtns = /* @__PURE__ */ new Map();
|
|
3145
3692
|
this.isMobileLayout = false;
|
|
3146
3693
|
this.drawingBtn = null;
|
|
3694
|
+
this.tfcBtn = null;
|
|
3695
|
+
this.tfcActive = true;
|
|
3147
3696
|
this.openDropdown = null;
|
|
3148
3697
|
this.indicatorOptions = [];
|
|
3149
3698
|
this.activeIndicators = /* @__PURE__ */ new Set();
|
|
@@ -3151,6 +3700,7 @@ var TopBar = class {
|
|
|
3151
3700
|
this.theme = theme;
|
|
3152
3701
|
this.cfg = cfg;
|
|
3153
3702
|
this.labels = labels;
|
|
3703
|
+
this.tfcActive = initialTfcActive;
|
|
3154
3704
|
this.currentSeries = initialSeries;
|
|
3155
3705
|
this.currentTf = initialTf;
|
|
3156
3706
|
this.activeDuration = initialDuration;
|
|
@@ -3227,6 +3777,21 @@ var TopBar = class {
|
|
|
3227
3777
|
this.addBtnHover(this.settingsBtn);
|
|
3228
3778
|
this.rightGroup.appendChild(this.settingsBtn);
|
|
3229
3779
|
}
|
|
3780
|
+
if (showTfcBtn) {
|
|
3781
|
+
this.tfcBtn = document.createElement("button");
|
|
3782
|
+
this.tfcBtn.title = this.labels.tfcToggleTitle;
|
|
3783
|
+
this.tfcBtn.innerHTML = this.tfcActive ? ICON_TFC : ICON_TFC_OFF;
|
|
3784
|
+
this.applyIconBtnStyles(this.tfcBtn, theme, this.tfcActive);
|
|
3785
|
+
this.tfcBtn.addEventListener("click", (e) => {
|
|
3786
|
+
e.stopPropagation();
|
|
3787
|
+
this.tfcActive = !this.tfcActive;
|
|
3788
|
+
this.tfcBtn.innerHTML = this.tfcActive ? ICON_TFC : ICON_TFC_OFF;
|
|
3789
|
+
this.applyIconBtnStyles(this.tfcBtn, this.theme, this.tfcActive);
|
|
3790
|
+
this.onTfcToggle?.(this.tfcActive);
|
|
3791
|
+
});
|
|
3792
|
+
this.addBtnHover(this.tfcBtn);
|
|
3793
|
+
this.rightGroup.appendChild(this.tfcBtn);
|
|
3794
|
+
}
|
|
3230
3795
|
this.el.append(this.leftGroup, this.rightGroup);
|
|
3231
3796
|
this.docClickHandler = (e) => {
|
|
3232
3797
|
if (!this.el.contains(e.target) && !this.flyout.contains(e.target)) {
|
|
@@ -3256,6 +3821,15 @@ var TopBar = class {
|
|
|
3256
3821
|
if (!this.drawingBtn) return;
|
|
3257
3822
|
this.applyIconBtnStyles(this.drawingBtn, this.theme, active);
|
|
3258
3823
|
}
|
|
3824
|
+
setTfcActive(active) {
|
|
3825
|
+
if (!this.tfcBtn) return;
|
|
3826
|
+
this.tfcActive = active;
|
|
3827
|
+
this.tfcBtn.innerHTML = active ? ICON_TFC : ICON_TFC_OFF;
|
|
3828
|
+
this.applyIconBtnStyles(this.tfcBtn, this.theme, active);
|
|
3829
|
+
if (this.isMobileLayout) {
|
|
3830
|
+
this.resizeSvg(this.tfcBtn, this.getSvgSize(this.tfcBtn) + 2);
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3259
3833
|
setTheme(theme) {
|
|
3260
3834
|
this.theme = theme;
|
|
3261
3835
|
this.closeDropdown();
|
|
@@ -3264,6 +3838,7 @@ var TopBar = class {
|
|
|
3264
3838
|
if (this.drawingBtn) this.applyIconBtnStyles(this.drawingBtn, theme, false);
|
|
3265
3839
|
this.applyIconBtnStyles(this.fullscreenBtn, theme, this.isFullscreen);
|
|
3266
3840
|
if (this.settingsBtn) this.applyIconBtnStyles(this.settingsBtn, theme, false);
|
|
3841
|
+
if (this.tfcBtn) this.applyIconBtnStyles(this.tfcBtn, theme, this.tfcActive);
|
|
3267
3842
|
this.applySeriesBtnStyles(this.seriesBtn, theme, false);
|
|
3268
3843
|
this.applyDropBtnStyles(this.tfBtn, theme, false);
|
|
3269
3844
|
this.applyDropBtnStyles(this.indBtn, theme, false);
|
|
@@ -3304,7 +3879,7 @@ var TopBar = class {
|
|
|
3304
3879
|
this.durationSep.style.display = btnDisplay;
|
|
3305
3880
|
this.durationDropBtn.style.display = isMobile ? "" : "none";
|
|
3306
3881
|
const containerSize = isMobile ? this.cfg.mobileIconBtnSize : this.cfg.iconBtnSize;
|
|
3307
|
-
for (const btn of [this.fullscreenBtn, this.settingsBtn, this.drawingBtn]) {
|
|
3882
|
+
for (const btn of [this.fullscreenBtn, this.settingsBtn, this.drawingBtn, this.tfcBtn]) {
|
|
3308
3883
|
if (!btn) continue;
|
|
3309
3884
|
btn.style.width = containerSize;
|
|
3310
3885
|
btn.style.height = containerSize;
|
|
@@ -3314,6 +3889,9 @@ var TopBar = class {
|
|
|
3314
3889
|
if (this.settingsBtn) {
|
|
3315
3890
|
this.resizeSvg(this.settingsBtn, this.getSvgSize(this.settingsBtn) + delta);
|
|
3316
3891
|
}
|
|
3892
|
+
if (this.tfcBtn) {
|
|
3893
|
+
this.resizeSvg(this.tfcBtn, this.getSvgSize(this.tfcBtn) + delta);
|
|
3894
|
+
}
|
|
3317
3895
|
if (this.drawingBtn) {
|
|
3318
3896
|
const drawIconSize = isMobile ? parseInt(this.cfg.mobileDrawBtnIconSize, 10) : parseInt(this.cfg.drawBtnIconFontSize, 10);
|
|
3319
3897
|
this.resizeSvg(this.drawingBtn, drawIconSize);
|
|
@@ -8142,7 +8720,7 @@ function pointToSegmentDist19(px, py, x1, y1, x2, y2) {
|
|
|
8142
8720
|
const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lenSq));
|
|
8143
8721
|
return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy));
|
|
8144
8722
|
}
|
|
8145
|
-
function
|
|
8723
|
+
function roundedRect2(ctx, x, y, w, h, r) {
|
|
8146
8724
|
ctx.beginPath();
|
|
8147
8725
|
ctx.moveTo(x + r, y);
|
|
8148
8726
|
ctx.lineTo(x + w - r, y);
|
|
@@ -8193,7 +8771,7 @@ var Callout = class {
|
|
|
8193
8771
|
ctx.beginPath();
|
|
8194
8772
|
ctx.arc(tipX, tipY, 3, 0, Math.PI * 2);
|
|
8195
8773
|
ctx.fill();
|
|
8196
|
-
|
|
8774
|
+
roundedRect2(ctx, bx, by, boxW, BOX_H, RADIUS);
|
|
8197
8775
|
ctx.fillStyle = "#37474f";
|
|
8198
8776
|
ctx.fill();
|
|
8199
8777
|
ctx.strokeStyle = "#546e7a";
|
|
@@ -8245,7 +8823,7 @@ var Callout = class {
|
|
|
8245
8823
|
// src/drawings/PriceLabel.ts
|
|
8246
8824
|
var HIT_TOLERANCE42 = 6;
|
|
8247
8825
|
var BADGE_W = 60;
|
|
8248
|
-
var
|
|
8826
|
+
var BADGE_H2 = 18;
|
|
8249
8827
|
var ARROW_W = 7;
|
|
8250
8828
|
var PriceLabel = class {
|
|
8251
8829
|
constructor(point) {
|
|
@@ -8262,21 +8840,21 @@ var PriceLabel = class {
|
|
|
8262
8840
|
ctx.fillStyle = "#26a69a";
|
|
8263
8841
|
ctx.beginPath();
|
|
8264
8842
|
ctx.moveTo(x, y);
|
|
8265
|
-
ctx.lineTo(x + ARROW_W, y -
|
|
8266
|
-
ctx.lineTo(x + ARROW_W, y +
|
|
8843
|
+
ctx.lineTo(x + ARROW_W, y - BADGE_H2 / 2);
|
|
8844
|
+
ctx.lineTo(x + ARROW_W, y + BADGE_H2 / 2);
|
|
8267
8845
|
ctx.closePath();
|
|
8268
8846
|
ctx.fill();
|
|
8269
8847
|
const bx = x + ARROW_W;
|
|
8270
|
-
const by = y -
|
|
8848
|
+
const by = y - BADGE_H2 / 2;
|
|
8271
8849
|
ctx.fillStyle = "#26a69a";
|
|
8272
8850
|
ctx.beginPath();
|
|
8273
|
-
ctx.roundRect(bx, by, BADGE_W,
|
|
8851
|
+
ctx.roundRect(bx, by, BADGE_W, BADGE_H2, BADGE_H2 / 2);
|
|
8274
8852
|
ctx.fill();
|
|
8275
8853
|
if (isSelected) {
|
|
8276
8854
|
ctx.strokeStyle = "#80cbc4";
|
|
8277
8855
|
ctx.lineWidth = 1.5;
|
|
8278
8856
|
ctx.beginPath();
|
|
8279
|
-
ctx.roundRect(bx - 2, by - 2, BADGE_W + 4,
|
|
8857
|
+
ctx.roundRect(bx - 2, by - 2, BADGE_W + 4, BADGE_H2 + 4, BADGE_H2 / 2 + 2);
|
|
8280
8858
|
ctx.stroke();
|
|
8281
8859
|
}
|
|
8282
8860
|
ctx.font = "11px 'Inter', system-ui, sans-serif";
|
|
@@ -8290,8 +8868,8 @@ var PriceLabel = class {
|
|
|
8290
8868
|
const ax = scale.xToPixel(this.points[0].barIndex, viewport, chartW);
|
|
8291
8869
|
const ay = scale.yToPixel(this.points[0].price, priceRange, chartH);
|
|
8292
8870
|
const bx = ax + ARROW_W;
|
|
8293
|
-
const by = ay -
|
|
8294
|
-
return x >= ax - HIT_TOLERANCE42 && x <= bx + BADGE_W + HIT_TOLERANCE42 && y >= by - HIT_TOLERANCE42 && y <= by +
|
|
8871
|
+
const by = ay - BADGE_H2 / 2;
|
|
8872
|
+
return x >= ax - HIT_TOLERANCE42 && x <= bx + BADGE_W + HIT_TOLERANCE42 && y >= by - HIT_TOLERANCE42 && y <= by + BADGE_H2 + HIT_TOLERANCE42;
|
|
8295
8873
|
}
|
|
8296
8874
|
getHandle() {
|
|
8297
8875
|
return null;
|
|
@@ -8411,7 +8989,7 @@ var Flag = class {
|
|
|
8411
8989
|
|
|
8412
8990
|
// src/drawings/PriceNote.ts
|
|
8413
8991
|
var HIT_TOLERANCE44 = 6;
|
|
8414
|
-
var
|
|
8992
|
+
var BADGE_H3 = 16;
|
|
8415
8993
|
var BADGE_PADDING_X = 6;
|
|
8416
8994
|
var TICK_HALF = 10;
|
|
8417
8995
|
var PriceNote = class {
|
|
@@ -8437,10 +9015,10 @@ var PriceNote = class {
|
|
|
8437
9015
|
const tw = ctx.measureText(label).width;
|
|
8438
9016
|
const badgeW = tw + BADGE_PADDING_X * 2;
|
|
8439
9017
|
const bx = x + TICK_HALF + 2;
|
|
8440
|
-
const by = y -
|
|
9018
|
+
const by = y - BADGE_H3 / 2;
|
|
8441
9019
|
ctx.fillStyle = "#ffa726";
|
|
8442
9020
|
ctx.beginPath();
|
|
8443
|
-
ctx.roundRect(bx, by, badgeW,
|
|
9021
|
+
ctx.roundRect(bx, by, badgeW, BADGE_H3, 3);
|
|
8444
9022
|
ctx.fill();
|
|
8445
9023
|
ctx.fillStyle = "#ffffff";
|
|
8446
9024
|
ctx.textBaseline = "middle";
|
|
@@ -8460,7 +9038,7 @@ var PriceNote = class {
|
|
|
8460
9038
|
|
|
8461
9039
|
// src/drawings/AnchoredNote.ts
|
|
8462
9040
|
var HIT_TOLERANCE45 = 6;
|
|
8463
|
-
var
|
|
9041
|
+
var BADGE_H4 = 20;
|
|
8464
9042
|
var PAD_H2 = 10;
|
|
8465
9043
|
function pointToSegmentDist21(px, py, x1, y1, x2, y2) {
|
|
8466
9044
|
const dx = x2 - x1, dy = y2 - y1;
|
|
@@ -8493,7 +9071,7 @@ var AnchoredNote = class {
|
|
|
8493
9071
|
ctx.lineWidth = isSelected ? 1.5 : 1;
|
|
8494
9072
|
const badgeW = this.getBadgeW(ctx);
|
|
8495
9073
|
const bx = x2 - badgeW / 2;
|
|
8496
|
-
const by = y2 -
|
|
9074
|
+
const by = y2 - BADGE_H4 / 2;
|
|
8497
9075
|
ctx.strokeStyle = "#42a5f5";
|
|
8498
9076
|
ctx.setLineDash([4, 3]);
|
|
8499
9077
|
ctx.beginPath();
|
|
@@ -8507,12 +9085,12 @@ var AnchoredNote = class {
|
|
|
8507
9085
|
ctx.fill();
|
|
8508
9086
|
ctx.fillStyle = "#37474f";
|
|
8509
9087
|
ctx.beginPath();
|
|
8510
|
-
ctx.roundRect(bx, by, badgeW,
|
|
9088
|
+
ctx.roundRect(bx, by, badgeW, BADGE_H4, 4);
|
|
8511
9089
|
ctx.fill();
|
|
8512
9090
|
ctx.strokeStyle = "#42a5f5";
|
|
8513
9091
|
ctx.lineWidth = isSelected ? 1.5 : 1;
|
|
8514
9092
|
ctx.beginPath();
|
|
8515
|
-
ctx.roundRect(bx, by, badgeW,
|
|
9093
|
+
ctx.roundRect(bx, by, badgeW, BADGE_H4, 4);
|
|
8516
9094
|
ctx.stroke();
|
|
8517
9095
|
const label = this.text || "Note";
|
|
8518
9096
|
const isPlaceholder = !this.text;
|
|
@@ -8531,8 +9109,8 @@ var AnchoredNote = class {
|
|
|
8531
9109
|
const y2 = scale.yToPixel(p2.price, priceRange, chartH);
|
|
8532
9110
|
const approxBadgeW = Math.max(50, (this.text?.length ?? 4) * 7 + 20);
|
|
8533
9111
|
const bx = x2 - approxBadgeW / 2;
|
|
8534
|
-
const by = y2 -
|
|
8535
|
-
if (x >= bx - HIT_TOLERANCE45 && x <= bx + approxBadgeW + HIT_TOLERANCE45 && y >= by - HIT_TOLERANCE45 && y <= by +
|
|
9112
|
+
const by = y2 - BADGE_H4 / 2;
|
|
9113
|
+
if (x >= bx - HIT_TOLERANCE45 && x <= bx + approxBadgeW + HIT_TOLERANCE45 && y >= by - HIT_TOLERANCE45 && y <= by + BADGE_H4 + HIT_TOLERANCE45) {
|
|
8536
9114
|
return true;
|
|
8537
9115
|
}
|
|
8538
9116
|
if (pointToSegmentDist21(x, y, x1, y1, x2, y2) <= HIT_TOLERANCE45) {
|
|
@@ -12483,6 +13061,158 @@ var TradePopover = class {
|
|
|
12483
13061
|
}
|
|
12484
13062
|
};
|
|
12485
13063
|
|
|
13064
|
+
// src/ui/DraftQtyFlyout.ts
|
|
13065
|
+
var DraftQtyFlyout = class {
|
|
13066
|
+
constructor(theme, onCommit) {
|
|
13067
|
+
this._min = 1;
|
|
13068
|
+
this._max = 100;
|
|
13069
|
+
this._step = 1;
|
|
13070
|
+
this.onDocMouseDown = (e) => {
|
|
13071
|
+
if (this.isVisible && !this.el.contains(e.target)) {
|
|
13072
|
+
this.syncIfValid();
|
|
13073
|
+
this.hide();
|
|
13074
|
+
}
|
|
13075
|
+
};
|
|
13076
|
+
this.theme = theme;
|
|
13077
|
+
this.onCommit = onCommit;
|
|
13078
|
+
this.el = document.createElement("div");
|
|
13079
|
+
Object.assign(this.el.style, {
|
|
13080
|
+
position: "fixed",
|
|
13081
|
+
display: "none",
|
|
13082
|
+
flexDirection: "row",
|
|
13083
|
+
alignItems: "center",
|
|
13084
|
+
gap: "4px",
|
|
13085
|
+
background: theme.tooltip.background,
|
|
13086
|
+
border: `1px solid ${theme.axisBorder}`,
|
|
13087
|
+
borderRadius: "5px",
|
|
13088
|
+
padding: "5px 7px",
|
|
13089
|
+
zIndex: "25",
|
|
13090
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.3)",
|
|
13091
|
+
fontFamily: "'Inter', system-ui, sans-serif",
|
|
13092
|
+
fontSize: "13px",
|
|
13093
|
+
color: theme.tooltip.text,
|
|
13094
|
+
pointerEvents: "auto"
|
|
13095
|
+
});
|
|
13096
|
+
const decBtn = this.makeStepBtn("\u2212", () => this.step(-1));
|
|
13097
|
+
this.input = this.makeInput();
|
|
13098
|
+
const incBtn = this.makeStepBtn("+", () => this.step(1));
|
|
13099
|
+
this.el.append(decBtn, this.input, incBtn);
|
|
13100
|
+
this.el.addEventListener("mousedown", (e) => e.stopPropagation());
|
|
13101
|
+
this.el.addEventListener("touchstart", (e) => e.stopPropagation());
|
|
13102
|
+
document.addEventListener("mousedown", this.onDocMouseDown);
|
|
13103
|
+
}
|
|
13104
|
+
// ── Public ────────────────────────────────────────────────────────────────
|
|
13105
|
+
show(pillX, boxBottom, currentQty, min, max) {
|
|
13106
|
+
this._min = min;
|
|
13107
|
+
this._max = max;
|
|
13108
|
+
this._step = min;
|
|
13109
|
+
this.input.min = String(min);
|
|
13110
|
+
this.input.max = String(max);
|
|
13111
|
+
this.input.step = String(min);
|
|
13112
|
+
this.input.value = String(currentQty);
|
|
13113
|
+
this.el.style.display = "flex";
|
|
13114
|
+
this.el.style.left = `${pillX}px`;
|
|
13115
|
+
this.el.style.top = `${boxBottom + 4}px`;
|
|
13116
|
+
requestAnimationFrame(() => {
|
|
13117
|
+
this.input.focus();
|
|
13118
|
+
this.input.select();
|
|
13119
|
+
});
|
|
13120
|
+
}
|
|
13121
|
+
hide() {
|
|
13122
|
+
this.el.style.display = "none";
|
|
13123
|
+
}
|
|
13124
|
+
get isVisible() {
|
|
13125
|
+
return this.el.style.display !== "none";
|
|
13126
|
+
}
|
|
13127
|
+
setTheme(theme) {
|
|
13128
|
+
this.theme = theme;
|
|
13129
|
+
Object.assign(this.el.style, {
|
|
13130
|
+
background: theme.tooltip.background,
|
|
13131
|
+
border: `1px solid ${theme.axisBorder}`,
|
|
13132
|
+
color: theme.tooltip.text
|
|
13133
|
+
});
|
|
13134
|
+
this.input.style.borderBottom = `1px solid ${theme.axisBorder}`;
|
|
13135
|
+
}
|
|
13136
|
+
destroy() {
|
|
13137
|
+
document.removeEventListener("mousedown", this.onDocMouseDown);
|
|
13138
|
+
}
|
|
13139
|
+
// ── Private ───────────────────────────────────────────────────────────────
|
|
13140
|
+
makeStepBtn(label, onClick) {
|
|
13141
|
+
const btn = document.createElement("button");
|
|
13142
|
+
btn.textContent = label;
|
|
13143
|
+
Object.assign(btn.style, {
|
|
13144
|
+
background: "transparent",
|
|
13145
|
+
border: "none",
|
|
13146
|
+
color: "inherit",
|
|
13147
|
+
cursor: "pointer",
|
|
13148
|
+
fontSize: "16px",
|
|
13149
|
+
lineHeight: "1",
|
|
13150
|
+
padding: "0 4px",
|
|
13151
|
+
borderRadius: "3px",
|
|
13152
|
+
opacity: "0.7"
|
|
13153
|
+
});
|
|
13154
|
+
btn.addEventListener("mouseenter", () => {
|
|
13155
|
+
btn.style.opacity = "1";
|
|
13156
|
+
});
|
|
13157
|
+
btn.addEventListener("mouseleave", () => {
|
|
13158
|
+
btn.style.opacity = "0.7";
|
|
13159
|
+
});
|
|
13160
|
+
btn.addEventListener("mousedown", (e) => {
|
|
13161
|
+
e.preventDefault();
|
|
13162
|
+
onClick();
|
|
13163
|
+
});
|
|
13164
|
+
return btn;
|
|
13165
|
+
}
|
|
13166
|
+
makeInput() {
|
|
13167
|
+
const input = document.createElement("input");
|
|
13168
|
+
input.type = "number";
|
|
13169
|
+
Object.assign(input.style, {
|
|
13170
|
+
width: "64px",
|
|
13171
|
+
background: "transparent",
|
|
13172
|
+
border: "none",
|
|
13173
|
+
borderBottom: `1px solid ${this.theme.axisBorder}`,
|
|
13174
|
+
color: "inherit",
|
|
13175
|
+
font: "13px 'Inter', system-ui, sans-serif",
|
|
13176
|
+
outline: "none",
|
|
13177
|
+
textAlign: "center",
|
|
13178
|
+
padding: "1px 2px"
|
|
13179
|
+
});
|
|
13180
|
+
input.addEventListener("input", () => {
|
|
13181
|
+
this.syncIfValid();
|
|
13182
|
+
});
|
|
13183
|
+
input.addEventListener("keydown", (e) => {
|
|
13184
|
+
if (e.key === "Enter") {
|
|
13185
|
+
e.preventDefault();
|
|
13186
|
+
this.syncIfValid();
|
|
13187
|
+
this.hide();
|
|
13188
|
+
} else if (e.key === "Escape") {
|
|
13189
|
+
e.preventDefault();
|
|
13190
|
+
this.hide();
|
|
13191
|
+
}
|
|
13192
|
+
});
|
|
13193
|
+
input.addEventListener("blur", () => {
|
|
13194
|
+
if (this.isVisible) {
|
|
13195
|
+
this.syncIfValid();
|
|
13196
|
+
this.hide();
|
|
13197
|
+
}
|
|
13198
|
+
});
|
|
13199
|
+
return input;
|
|
13200
|
+
}
|
|
13201
|
+
step(direction) {
|
|
13202
|
+
const current = parseFloat(this.input.value) || this._min;
|
|
13203
|
+
const next = Math.round((current + direction * this._step) * 1e10) / 1e10;
|
|
13204
|
+
this.input.value = String(Math.min(this._max, Math.max(this._min, next)));
|
|
13205
|
+
this.input.focus();
|
|
13206
|
+
this.syncIfValid();
|
|
13207
|
+
}
|
|
13208
|
+
syncIfValid() {
|
|
13209
|
+
const val = parseFloat(this.input.value);
|
|
13210
|
+
if (!isNaN(val) && val >= this._min && val <= this._max) {
|
|
13211
|
+
this.onCommit(val);
|
|
13212
|
+
}
|
|
13213
|
+
}
|
|
13214
|
+
};
|
|
13215
|
+
|
|
12486
13216
|
// src/ui/IsinPickerModal.ts
|
|
12487
13217
|
var INITIAL_LIMIT = 50;
|
|
12488
13218
|
var SEARCH_LIMIT = 20;
|
|
@@ -12693,9 +13423,14 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12693
13423
|
this._isDraggingBracket = false;
|
|
12694
13424
|
// Main line drag state
|
|
12695
13425
|
this._level = null;
|
|
12696
|
-
this._startY = 0;
|
|
12697
13426
|
this._startPrice = 0;
|
|
12698
13427
|
this._currentPrice = 0;
|
|
13428
|
+
// Pixel offset between click Y and the level's rendered Y at drag start.
|
|
13429
|
+
// Why: the drag hit-strip is 8 px tall; without this, the cursor-to-price
|
|
13430
|
+
// mapping would snap the level to wherever the cursor is instead of
|
|
13431
|
+
// preserving the grab point. Also immunises the drag from any priceRange
|
|
13432
|
+
// animation (lerp) that happens between drag-start and first mousemove.
|
|
13433
|
+
this._pixelOffset = 0;
|
|
12699
13434
|
// Entry-price constraint bounds (applied when SL/TP exist on the level)
|
|
12700
13435
|
this._mainMinPrice = -Infinity;
|
|
12701
13436
|
this._mainMaxPrice = Infinity;
|
|
@@ -12703,6 +13438,7 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12703
13438
|
this._bracketType = null;
|
|
12704
13439
|
this._bracketStartPrice = 0;
|
|
12705
13440
|
this._bracketCurrentPrice = 0;
|
|
13441
|
+
this._bracketPixelOffset = 0;
|
|
12706
13442
|
// Bracket constraint state (populated on startBracketDrag)
|
|
12707
13443
|
this._bracketParentPrice = 0;
|
|
12708
13444
|
this._bracketParentSide = "buy";
|
|
@@ -12714,11 +13450,11 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12714
13450
|
* Pass `slPrice` / `tpPrice` / `side` to constrain the drag between SL and TP
|
|
12715
13451
|
* (used for ES/EL entry orders where the entry price must stay between brackets).
|
|
12716
13452
|
*/
|
|
12717
|
-
startDrag(level,
|
|
13453
|
+
startDrag(level, clickY, levelPixelY, startPrice, slPrice, tpPrice, side, marketPrice, orderType) {
|
|
12718
13454
|
this._active = true;
|
|
12719
13455
|
this._isDraggingBracket = false;
|
|
12720
13456
|
this._level = level;
|
|
12721
|
-
this.
|
|
13457
|
+
this._pixelOffset = clickY - levelPixelY;
|
|
12722
13458
|
this._startPrice = startPrice;
|
|
12723
13459
|
this._currentPrice = startPrice;
|
|
12724
13460
|
this._bracketType = null;
|
|
@@ -12750,13 +13486,14 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12750
13486
|
* @param bracketType 'stopLoss' | 'takeProfit'
|
|
12751
13487
|
* @param startPrice Current bracket price
|
|
12752
13488
|
*/
|
|
12753
|
-
startBracketDrag(parentLevel, bracketType, startPrice, currentMarketPrice = 0, parentPriceOverride) {
|
|
13489
|
+
startBracketDrag(parentLevel, bracketType, startPrice, clickY, bracketPixelY, currentMarketPrice = 0, parentPriceOverride) {
|
|
12754
13490
|
this._active = true;
|
|
12755
13491
|
this._isDraggingBracket = true;
|
|
12756
13492
|
this._level = parentLevel;
|
|
12757
13493
|
this._bracketType = bracketType;
|
|
12758
13494
|
this._bracketStartPrice = startPrice;
|
|
12759
13495
|
this._bracketCurrentPrice = startPrice;
|
|
13496
|
+
this._bracketPixelOffset = clickY - bracketPixelY;
|
|
12760
13497
|
this._bracketParentPrice = parentPriceOverride ?? parentLevel.price;
|
|
12761
13498
|
this._bracketParentSide = parentLevel.side ?? "buy";
|
|
12762
13499
|
this._bracketMarketPrice = currentMarketPrice;
|
|
@@ -12767,9 +13504,10 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12767
13504
|
*/
|
|
12768
13505
|
onMouseMove(currentY, scale, priceRange, priceH) {
|
|
12769
13506
|
if (!this._active || !this._level) return null;
|
|
12770
|
-
const rawPrice = scale.pixelToPrice(currentY, priceRange, priceH);
|
|
12771
13507
|
if (this._isDraggingBracket && this._bracketType !== null) {
|
|
12772
|
-
const
|
|
13508
|
+
const effectiveY2 = currentY - this._bracketPixelOffset;
|
|
13509
|
+
const rawPrice2 = scale.pixelToPrice(effectiveY2, priceRange, priceH);
|
|
13510
|
+
const clampedPrice = this._clampBracketPrice(rawPrice2, this._bracketType, this._bracketParentPrice, this._bracketParentSide, this._bracketMarketPrice);
|
|
12773
13511
|
this._bracketCurrentPrice = clampedPrice;
|
|
12774
13512
|
return {
|
|
12775
13513
|
newPrice: clampedPrice,
|
|
@@ -12777,6 +13515,8 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12777
13515
|
isBracket: true
|
|
12778
13516
|
};
|
|
12779
13517
|
}
|
|
13518
|
+
const effectiveY = currentY - this._pixelOffset;
|
|
13519
|
+
const rawPrice = scale.pixelToPrice(effectiveY, priceRange, priceH);
|
|
12780
13520
|
const constrained = Math.max(this._mainMinPrice, Math.min(this._mainMaxPrice, rawPrice));
|
|
12781
13521
|
this._currentPrice = constrained;
|
|
12782
13522
|
return {
|
|
@@ -12875,10 +13615,12 @@ var TradeDragHandler = class _TradeDragHandler {
|
|
|
12875
13615
|
this._level = null;
|
|
12876
13616
|
this._bracketType = null;
|
|
12877
13617
|
this._currentPrice = 0;
|
|
13618
|
+
this._pixelOffset = 0;
|
|
12878
13619
|
this._mainMinPrice = -Infinity;
|
|
12879
13620
|
this._mainMaxPrice = Infinity;
|
|
12880
13621
|
this._bracketStartPrice = 0;
|
|
12881
13622
|
this._bracketCurrentPrice = 0;
|
|
13623
|
+
this._bracketPixelOffset = 0;
|
|
12882
13624
|
this._bracketParentPrice = 0;
|
|
12883
13625
|
this._bracketParentSide = "buy";
|
|
12884
13626
|
this._bracketMarketPrice = 0;
|
|
@@ -16405,6 +17147,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16405
17147
|
this._hoverBuyType = "limit";
|
|
16406
17148
|
this._hoverSellType = "limit";
|
|
16407
17149
|
this._minLots = 1;
|
|
17150
|
+
this._showQuantityField = false;
|
|
17151
|
+
this._qtyFieldMin = 1;
|
|
17152
|
+
this._qtyFieldMax = 100;
|
|
17153
|
+
this._draftQtyFlyout = null;
|
|
17154
|
+
this._draftQtyPillArea = void 0;
|
|
16408
17155
|
this._onOrderSubmit = void 0;
|
|
16409
17156
|
this.scrollToEndBtn = null;
|
|
16410
17157
|
this.scrollToEndVisible = false;
|
|
@@ -16420,6 +17167,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16420
17167
|
this.showBidAskLines = false;
|
|
16421
17168
|
this.showActLogo = false;
|
|
16422
17169
|
this.hideLevelConfirmCancel = false;
|
|
17170
|
+
this.levelClusteringEnabled = true;
|
|
17171
|
+
this.clusterThresholdDistance = 20;
|
|
16423
17172
|
this.enableThemeToggle = false;
|
|
16424
17173
|
this.activeIndicators = [];
|
|
16425
17174
|
this.indicatorResults = /* @__PURE__ */ new Map();
|
|
@@ -16485,19 +17234,28 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16485
17234
|
this._mobileDrawingFlyout = null;
|
|
16486
17235
|
// Fullscreen state
|
|
16487
17236
|
this._isCssFullscreen = false;
|
|
17237
|
+
this.timezone = "UTC";
|
|
16488
17238
|
this.settingsCanvasColors = {};
|
|
16489
17239
|
// Streaming
|
|
16490
17240
|
this.streamAdapter = null;
|
|
16491
17241
|
this.liveBidAsk = null;
|
|
16492
17242
|
this.tickClosePriceSource = "bid";
|
|
17243
|
+
/** True while an async data fetch is in-flight; suppresses tick processing to
|
|
17244
|
+
* prevent stale ticks from corrupting newly-loaded bars. */
|
|
17245
|
+
this._suppressTicks = false;
|
|
16493
17246
|
// Trade / position levels
|
|
16494
17247
|
this.levels = [];
|
|
16495
17248
|
this.tradeLevelHitAreas = [];
|
|
17249
|
+
this.offViewportHitAreas = [];
|
|
16496
17250
|
this.tradeLevelEditAreas = [];
|
|
16497
17251
|
this.tradeHoverAreas = [];
|
|
16498
17252
|
this.tradeLevelDragAreas = [];
|
|
16499
17253
|
this.tradeLevelConfirmAreas = [];
|
|
16500
17254
|
this.tradeAddBracketAreas = [];
|
|
17255
|
+
this.clusterBadgeAreas = [];
|
|
17256
|
+
this.lastClusters = [];
|
|
17257
|
+
/** Set of cluster IDs that are currently expanded (fanned out). */
|
|
17258
|
+
this.expandedClusters = /* @__PURE__ */ new Set();
|
|
16501
17259
|
this.hoveredTradeLabel = null;
|
|
16502
17260
|
this.selectedTradeLabel = null;
|
|
16503
17261
|
/** Estimated PNL text for draft order SL/TP bracket lines, set by setDraftBracketPnl(). */
|
|
@@ -16512,6 +17270,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16512
17270
|
// Inline drag confirmation — accumulates changes per level until user confirms
|
|
16513
17271
|
this.pendingLevelDrags = /* @__PURE__ */ new Map();
|
|
16514
17272
|
// Trade from Charts settings
|
|
17273
|
+
this.tfcEnabled = true;
|
|
17274
|
+
this.tfcActive = true;
|
|
16515
17275
|
this.tradeDisplayFilter = "all";
|
|
16516
17276
|
this.positionRenderStyle = "dot";
|
|
16517
17277
|
this.tradesThresholdForHorizontalLine = 2;
|
|
@@ -16522,7 +17282,9 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16522
17282
|
this.dataLoader = null;
|
|
16523
17283
|
this.isFetching = false;
|
|
16524
17284
|
this.noMoreData = false;
|
|
17285
|
+
this._loadGeneration = 0;
|
|
16525
17286
|
this.maxCandles = 200;
|
|
17287
|
+
this.prefetchThreshold = 80;
|
|
16526
17288
|
this.defaultVisibleBars = DEFAULT_VISIBLE_BARS;
|
|
16527
17289
|
this.defaultMobileVisibleBars = void 0;
|
|
16528
17290
|
this.mobileBarDivisor = 2;
|
|
@@ -16560,7 +17322,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16560
17322
|
if (this.tradeButton) {
|
|
16561
17323
|
if (isInChartArea && y >= 0 && y <= priceH && !this._isTradingActive && !hoveringDotLevel) {
|
|
16562
17324
|
this.tradeButton.setY(y);
|
|
16563
|
-
this.tradeButton.
|
|
17325
|
+
const btnHalfH = this.uiConfig.tradeButton.size / 2;
|
|
17326
|
+
if (overlapsOffViewportPill(y, btnHalfH, this.offViewportHitAreas)) {
|
|
17327
|
+
this.tradeButton.hide();
|
|
17328
|
+
} else if (this.tfcActive) {
|
|
17329
|
+
this.tradeButton.show();
|
|
17330
|
+
}
|
|
16564
17331
|
this._tradeButtonClickData = { price, y };
|
|
16565
17332
|
const allBars = this.dataStore.all;
|
|
16566
17333
|
const lastClose = allBars.length > 0 ? allBars[allBars.length - 1].close : price;
|
|
@@ -16662,7 +17429,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16662
17429
|
return;
|
|
16663
17430
|
}
|
|
16664
17431
|
if (!this.drawingManager.getActiveTool()) {
|
|
16665
|
-
if (hitTestTradeLevelClose(x, y, this.tradeLevelHitAreas) || hitTestTradeLevelEdit(x, y, this.tradeLevelEditAreas) || hitTestTradeLevelConfirm(x, y, this.tradeLevelConfirmAreas)) {
|
|
17432
|
+
if (hitTestClusterBadge(x, y, this.clusterBadgeAreas) || hitTestTradeLevelClose(x, y, this.tradeLevelHitAreas) || hitTestTradeLevelEdit(x, y, this.tradeLevelEditAreas) || hitTestTradeLevelConfirm(x, y, this.tradeLevelConfirmAreas) || hitTestOffViewportIndicator(x, y, this.offViewportHitAreas)) {
|
|
16666
17433
|
this.drawCanvas.style.cursor = "pointer";
|
|
16667
17434
|
this.scheduleRender();
|
|
16668
17435
|
return;
|
|
@@ -16874,6 +17641,25 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16874
17641
|
this.scheduleRender();
|
|
16875
17642
|
return;
|
|
16876
17643
|
}
|
|
17644
|
+
const clusterHitTouch = hitTestClusterBadge(x, y, this.clusterBadgeAreas);
|
|
17645
|
+
if (clusterHitTouch) {
|
|
17646
|
+
e.preventDefault();
|
|
17647
|
+
this.panHandler.cancel();
|
|
17648
|
+
if (this.expandedClusters.has(clusterHitTouch.clusterId)) {
|
|
17649
|
+
this.expandedClusters.delete(clusterHitTouch.clusterId);
|
|
17650
|
+
} else {
|
|
17651
|
+
this.expandedClusters.add(clusterHitTouch.clusterId);
|
|
17652
|
+
}
|
|
17653
|
+
this.renderTradeLayer();
|
|
17654
|
+
return;
|
|
17655
|
+
}
|
|
17656
|
+
const offVpHit = hitTestOffViewportIndicator(x, y, this.offViewportHitAreas);
|
|
17657
|
+
if (offVpHit) {
|
|
17658
|
+
e.preventDefault();
|
|
17659
|
+
this.panHandler.cancel();
|
|
17660
|
+
this.scrollToCenterPrice(offVpHit.targetPrice);
|
|
17661
|
+
return;
|
|
17662
|
+
}
|
|
16877
17663
|
const confirmHit = hitTestTradeLevelConfirm(x, y, this.tradeLevelConfirmAreas);
|
|
16878
17664
|
if (confirmHit) {
|
|
16879
17665
|
e.preventDefault();
|
|
@@ -16881,6 +17667,15 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16881
17667
|
else this.revertPendingChanges(confirmHit.label);
|
|
16882
17668
|
return;
|
|
16883
17669
|
}
|
|
17670
|
+
if (this._showQuantityField && this._draftQtyPillArea) {
|
|
17671
|
+
const p = this._draftQtyPillArea;
|
|
17672
|
+
if (x >= p.pillX && x <= p.pillX + p.pillW && y >= p.pillY && y <= p.pillY + p.pillH) {
|
|
17673
|
+
e.preventDefault();
|
|
17674
|
+
e.stopPropagation();
|
|
17675
|
+
this.openDraftQtyFlyout(p);
|
|
17676
|
+
return;
|
|
17677
|
+
}
|
|
17678
|
+
}
|
|
16884
17679
|
const closeHit = hitTestTradeLevelClose(x, y, this.tradeLevelHitAreas);
|
|
16885
17680
|
if (closeHit) {
|
|
16886
17681
|
e.preventDefault();
|
|
@@ -16930,6 +17725,10 @@ var _ChartEngine = class _ChartEngine {
|
|
|
16930
17725
|
}
|
|
16931
17726
|
this.drawingManager.selectDrawing(null);
|
|
16932
17727
|
this.hideMobileDrawingFlyout();
|
|
17728
|
+
if (this.expandedClusters.size > 0) {
|
|
17729
|
+
this.expandedClusters.clear();
|
|
17730
|
+
this.renderTradeLayer();
|
|
17731
|
+
}
|
|
16933
17732
|
const isInChartArea = x < chartW && y < timeAxisY;
|
|
16934
17733
|
if (isInChartArea) {
|
|
16935
17734
|
if (this._mobileCrosshairActive) {
|
|
@@ -17079,7 +17878,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17079
17878
|
if (this._isTradingActive) {
|
|
17080
17879
|
this.tradeButton.hide();
|
|
17081
17880
|
} else {
|
|
17082
|
-
this.tradeButton.
|
|
17881
|
+
const _btnHalfH2 = this.uiConfig.tradeButton.size / 2;
|
|
17882
|
+
if (overlapsOffViewportPill(y, _btnHalfH2, this.offViewportHitAreas)) {
|
|
17883
|
+
this.tradeButton.hide();
|
|
17884
|
+
} else if (this.tfcActive) {
|
|
17885
|
+
this.tradeButton.show();
|
|
17886
|
+
}
|
|
17083
17887
|
}
|
|
17084
17888
|
this._tradeButtonClickData = { price, y };
|
|
17085
17889
|
const _allBars = this.dataStore.all;
|
|
@@ -17161,12 +17965,36 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17161
17965
|
const rawBarIndex = this.scaleManager.pixelToBarIndex(x, this.viewport, chartW);
|
|
17162
17966
|
const price = this.scaleManager.pixelToPrice(y, priceRange, priceH);
|
|
17163
17967
|
const barIndex = Math.round(rawBarIndex);
|
|
17968
|
+
const clusterHit = hitTestClusterBadge(x, y, this.clusterBadgeAreas);
|
|
17969
|
+
if (clusterHit) {
|
|
17970
|
+
if (this.expandedClusters.has(clusterHit.clusterId)) {
|
|
17971
|
+
this.expandedClusters.delete(clusterHit.clusterId);
|
|
17972
|
+
} else {
|
|
17973
|
+
this.expandedClusters.add(clusterHit.clusterId);
|
|
17974
|
+
}
|
|
17975
|
+
this.renderTradeLayer();
|
|
17976
|
+
return;
|
|
17977
|
+
}
|
|
17978
|
+
const offVpHit = hitTestOffViewportIndicator(x, y, this.offViewportHitAreas);
|
|
17979
|
+
if (offVpHit) {
|
|
17980
|
+
this.scrollToCenterPrice(offVpHit.targetPrice);
|
|
17981
|
+
return;
|
|
17982
|
+
}
|
|
17164
17983
|
const confirmHit = hitTestTradeLevelConfirm(x, y, this.tradeLevelConfirmAreas);
|
|
17165
17984
|
if (confirmHit) {
|
|
17166
17985
|
if (confirmHit.action === "confirm") this.applyPendingChanges(confirmHit.label);
|
|
17167
17986
|
else this.revertPendingChanges(confirmHit.label);
|
|
17168
17987
|
return;
|
|
17169
17988
|
}
|
|
17989
|
+
if (this._showQuantityField && this._draftQtyPillArea) {
|
|
17990
|
+
const p = this._draftQtyPillArea;
|
|
17991
|
+
if (x >= p.pillX && x <= p.pillX + p.pillW && y >= p.pillY && y <= p.pillY + p.pillH) {
|
|
17992
|
+
e.preventDefault();
|
|
17993
|
+
e.stopPropagation();
|
|
17994
|
+
this.openDraftQtyFlyout(p);
|
|
17995
|
+
return;
|
|
17996
|
+
}
|
|
17997
|
+
}
|
|
17170
17998
|
const closedLevel = hitTestTradeLevelClose(x, y, this.tradeLevelHitAreas);
|
|
17171
17999
|
if (closedLevel) {
|
|
17172
18000
|
this._handleTradeLevelClose(closedLevel);
|
|
@@ -17245,6 +18073,10 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17245
18073
|
this.renderTradeLayer();
|
|
17246
18074
|
}
|
|
17247
18075
|
}
|
|
18076
|
+
if (this.expandedClusters.size > 0) {
|
|
18077
|
+
this.expandedClusters.clear();
|
|
18078
|
+
this.renderTradeLayer();
|
|
18079
|
+
}
|
|
17248
18080
|
const timeAxisY = rect.height - this.timeAxisHeight;
|
|
17249
18081
|
if (x >= chartW) {
|
|
17250
18082
|
if (y <= priceH) {
|
|
@@ -17360,6 +18192,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17360
18192
|
this.scheduleRender();
|
|
17361
18193
|
};
|
|
17362
18194
|
this.onKeyDown = (e) => {
|
|
18195
|
+
if (e.key === "Escape" && this.expandedClusters.size > 0) {
|
|
18196
|
+
this.expandedClusters.clear();
|
|
18197
|
+
this.renderTradeLayer();
|
|
18198
|
+
e.preventDefault();
|
|
18199
|
+
return;
|
|
18200
|
+
}
|
|
17363
18201
|
if (e.key === "Escape" && this.pendingLevelDrags.size > 0) {
|
|
17364
18202
|
this.revertPendingChanges();
|
|
17365
18203
|
e.preventDefault();
|
|
@@ -17379,6 +18217,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17379
18217
|
this.onTick = (tick) => {
|
|
17380
18218
|
const all = this.dataStore.all;
|
|
17381
18219
|
if (all.length === 0) return;
|
|
18220
|
+
if (this._suppressTicks) return;
|
|
17382
18221
|
this.liveBidAsk = { bid: tick.bid, ask: tick.ask };
|
|
17383
18222
|
this.setDotTickActive();
|
|
17384
18223
|
const last = all[all.length - 1];
|
|
@@ -17446,7 +18285,9 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17446
18285
|
durationTimeframeMap,
|
|
17447
18286
|
uiConfig,
|
|
17448
18287
|
labels,
|
|
17449
|
-
tickActivityMs = 3e4
|
|
18288
|
+
tickActivityMs = 3e4,
|
|
18289
|
+
hideSymbolAndTick = false,
|
|
18290
|
+
showBottomBar = false
|
|
17450
18291
|
} = config;
|
|
17451
18292
|
this.dataLoader = dataLoader ?? null;
|
|
17452
18293
|
this.drawingToolsEnabled = config.showDrawingTools !== false;
|
|
@@ -17455,6 +18296,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17455
18296
|
this.labels = resolveLabels(labels);
|
|
17456
18297
|
this.tickActivityMs = tickActivityMs;
|
|
17457
18298
|
this.maxCandles = config.maxCandles ?? 200;
|
|
18299
|
+
this.prefetchThreshold = config.prefetchThreshold ?? 80;
|
|
17458
18300
|
this.defaultVisibleBars = config.defaultVisibleBars ?? DEFAULT_VISIBLE_BARS;
|
|
17459
18301
|
this.defaultMobileVisibleBars = config.defaultMobileVisibleBars;
|
|
17460
18302
|
this.explicitMobileDivisor = config.mobileBarDivisor !== void 0;
|
|
@@ -17494,7 +18336,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17494
18336
|
this.showBidAskLines = config.showBidAskLines ?? false;
|
|
17495
18337
|
this.showActLogo = config.showActLogo ?? false;
|
|
17496
18338
|
this.hideLevelConfirmCancel = config.hideLevelConfirmCancel ?? false;
|
|
18339
|
+
this.levelClusteringEnabled = config.levelClusteringEnabled ?? true;
|
|
18340
|
+
this.clusterThresholdDistance = config.clusterThresholdDistance ?? 20;
|
|
17497
18341
|
this.enableThemeToggle = config.enableThemeToggle ?? false;
|
|
18342
|
+
this.tfcEnabled = config.tfcEnabled !== false;
|
|
18343
|
+
this.tfcActive = this.tfcEnabled;
|
|
17498
18344
|
this.showCandleCountdown = config.showCandleCountdown ?? true;
|
|
17499
18345
|
this.candleCountdownTimeframes = config.candleCountdownTimeframes;
|
|
17500
18346
|
this._onOrderSubmit = onOrderSubmit;
|
|
@@ -17502,6 +18348,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17502
18348
|
console.warn("[acttrader-charts] ChartConfig.minLots is deprecated. DraftOrderQtyInput has been removed. The value is stored and used as the submitted lot size.");
|
|
17503
18349
|
this._minLots = config.minLots;
|
|
17504
18350
|
}
|
|
18351
|
+
if (config.showQuantityField !== void 0) this._showQuantityField = config.showQuantityField;
|
|
18352
|
+
if (config.quantityFieldConfig) {
|
|
18353
|
+
if (config.quantityFieldConfig.minLots !== void 0) this._qtyFieldMin = config.quantityFieldConfig.minLots;
|
|
18354
|
+
if (config.quantityFieldConfig.maxLots !== void 0) this._qtyFieldMax = config.quantityFieldConfig.maxLots;
|
|
18355
|
+
}
|
|
17505
18356
|
if (config.onLotsChange !== void 0) {
|
|
17506
18357
|
console.warn("[acttrader-charts] ChartConfig.onLotsChange is deprecated. DraftOrderQtyInput has been removed; this callback will never fire.");
|
|
17507
18358
|
}
|
|
@@ -17510,6 +18361,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17510
18361
|
}
|
|
17511
18362
|
this.themeName = theme === "dark" ? "dark" : "light";
|
|
17512
18363
|
this.themeOverrides = config.themeOverrides;
|
|
18364
|
+
this.timezone = config.timezone === "local" ? Intl.DateTimeFormat().resolvedOptions().timeZone : config.timezone ?? "UTC";
|
|
17513
18365
|
this.aggregateFrom = config.aggregateFrom;
|
|
17514
18366
|
const _baseTheme = this.themeName === "dark" ? DARK_THEME : LIGHT_THEME;
|
|
17515
18367
|
this.theme = deepMergeTheme(_baseTheme, this.themeOverrides?.[this.themeName]);
|
|
@@ -17558,6 +18410,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17558
18410
|
this.drawingToolsEnabled ? () => this.toggleDrawingTools() : null,
|
|
17559
18411
|
this.drawingToolsEnabled,
|
|
17560
18412
|
this.settingsEnabled,
|
|
18413
|
+
this.tfcEnabled,
|
|
18414
|
+
this.tfcActive,
|
|
18415
|
+
this.tfcEnabled ? (enabled) => {
|
|
18416
|
+
this.setTfcActive(enabled);
|
|
18417
|
+
} : null,
|
|
17561
18418
|
this.series,
|
|
17562
18419
|
this.timeframe,
|
|
17563
18420
|
null,
|
|
@@ -17709,7 +18566,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17709
18566
|
this.loadingOverlayEl.appendChild(loadingLabel);
|
|
17710
18567
|
this.canvasWrap.appendChild(this.loadingOverlayEl);
|
|
17711
18568
|
}
|
|
17712
|
-
if (showUI) {
|
|
18569
|
+
if (showUI && !hideSymbolAndTick) {
|
|
17713
18570
|
this.siEl = document.createElement("div");
|
|
17714
18571
|
Object.assign(this.siEl.style, {
|
|
17715
18572
|
position: "absolute",
|
|
@@ -17853,6 +18710,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17853
18710
|
const rect = this._cachedDrawRect ?? this.drawCanvas.getBoundingClientRect();
|
|
17854
18711
|
const y = e.clientY - rect.top;
|
|
17855
18712
|
const { priceH } = this.getChartLayout();
|
|
18713
|
+
const btnHalfH = this.uiConfig.tradeButton.size / 2;
|
|
18714
|
+
if (overlapsOffViewportPill(y, btnHalfH, this.offViewportHitAreas)) {
|
|
18715
|
+
this.tradeButton?.hide();
|
|
18716
|
+
return;
|
|
18717
|
+
}
|
|
17856
18718
|
if (y >= 0 && y <= priceH) {
|
|
17857
18719
|
this.tradeButton?.setY(y);
|
|
17858
18720
|
const priceRange = this._displayedPriceRange ?? this.getScaledPriceRange(this.viewport);
|
|
@@ -17867,7 +18729,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17867
18729
|
}
|
|
17868
18730
|
middleRow.appendChild(this.canvasWrap);
|
|
17869
18731
|
this.wrapper.appendChild(middleRow);
|
|
17870
|
-
if (showUI) {
|
|
18732
|
+
if (showUI && showBottomBar) {
|
|
17871
18733
|
const resolvedDurationMap = { ...DURATION_TIMEFRAME_MAP, ...durationTimeframeMap };
|
|
17872
18734
|
this.bottomBar = new BottomBar(
|
|
17873
18735
|
(d, tf) => {
|
|
@@ -17942,6 +18804,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17942
18804
|
},
|
|
17943
18805
|
() => {
|
|
17944
18806
|
this._isPanning = false;
|
|
18807
|
+
},
|
|
18808
|
+
{
|
|
18809
|
+
enabled: config.momentumScrollEnabled,
|
|
18810
|
+
decay: config.momentumDecay,
|
|
18811
|
+
threshold: config.momentumThreshold,
|
|
18812
|
+
maxVelocity: config.momentumMaxVelocity
|
|
17945
18813
|
}
|
|
17946
18814
|
);
|
|
17947
18815
|
this.zoomHandler = new ZoomHandler(
|
|
@@ -18017,9 +18885,31 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18017
18885
|
return (this.tradePopover?.isVisible ?? false) || this.draftOrderLabel !== null || (this.tradeDragHandler?.active ?? false) || this.pendingLevelDrags.size > 0;
|
|
18018
18886
|
}
|
|
18019
18887
|
// ── Public API ────────────────────────────────────────────────────────────
|
|
18888
|
+
/**
|
|
18889
|
+
* Completely resets the chart to a clean, empty state.
|
|
18890
|
+
*
|
|
18891
|
+
* Cancels any in-flight data fetch, clears all bars, resets the price/time
|
|
18892
|
+
* axes, and discards the live bid/ask price line. The chart will render as
|
|
18893
|
+
* blank until the next `loadData()` call.
|
|
18894
|
+
*
|
|
18895
|
+
* Call this before switching to a new symbol so that no previous symbol's
|
|
18896
|
+
* data, price lines, or stale ticks can bleed into the new chart:
|
|
18897
|
+
* ```
|
|
18898
|
+
* chart.setSymbol('GBPUSD').resetData();
|
|
18899
|
+
* // … fetch new bars, then:
|
|
18900
|
+
* chart.loadData(newBars);
|
|
18901
|
+
* ```
|
|
18902
|
+
*/
|
|
18903
|
+
resetData() {
|
|
18904
|
+
++this._loadGeneration;
|
|
18905
|
+
this.setLoading(false);
|
|
18906
|
+
return this.loadData([]);
|
|
18907
|
+
}
|
|
18020
18908
|
loadData(bars, fitAll = false) {
|
|
18021
18909
|
this.isFetching = false;
|
|
18022
18910
|
this.noMoreData = false;
|
|
18911
|
+
this._suppressTicks = false;
|
|
18912
|
+
this.liveBidAsk = null;
|
|
18023
18913
|
this.pricePanOffset = 0;
|
|
18024
18914
|
this.priceScaleFactor = 1;
|
|
18025
18915
|
this._displayedPriceRange = null;
|
|
@@ -18166,6 +19056,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18166
19056
|
}
|
|
18167
19057
|
triggerLoad(reason) {
|
|
18168
19058
|
if (!this.dataLoader) return;
|
|
19059
|
+
const gen = ++this._loadGeneration;
|
|
19060
|
+
this._suppressTicks = true;
|
|
18169
19061
|
this.isFetching = true;
|
|
18170
19062
|
this.noMoreData = false;
|
|
18171
19063
|
const end = /* @__PURE__ */ new Date();
|
|
@@ -18177,6 +19069,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18177
19069
|
const interval = this.timeframeToInterval(this.getBaseInterval());
|
|
18178
19070
|
this.setLoading(true);
|
|
18179
19071
|
this.dataLoader({ start, end, interval }).then((bars) => {
|
|
19072
|
+
if (gen !== this._loadGeneration) return;
|
|
18180
19073
|
this.loadData(this.maybeAggregate(bars), reason === "duration");
|
|
18181
19074
|
this.setLoading(false);
|
|
18182
19075
|
this.emitter.emit("dataLoaded", {
|
|
@@ -18186,25 +19079,17 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18186
19079
|
end
|
|
18187
19080
|
});
|
|
18188
19081
|
}).catch(() => {
|
|
19082
|
+
if (gen !== this._loadGeneration) return;
|
|
18189
19083
|
this.isFetching = false;
|
|
18190
19084
|
this.setLoading(false);
|
|
18191
19085
|
});
|
|
18192
19086
|
}
|
|
18193
19087
|
checkNeedMoreData() {
|
|
18194
19088
|
if (!this.dataLoader || this.isFetching || this.noMoreData) return;
|
|
18195
|
-
const threshold = Math.max(
|
|
18196
|
-
20,
|
|
18197
|
-
Math.round(
|
|
18198
|
-
initialVisibleBars(
|
|
18199
|
-
this.mobileBarDivisor,
|
|
18200
|
-
this.explicitMobileDivisor,
|
|
18201
|
-
this.defaultVisibleBars,
|
|
18202
|
-
this.defaultMobileVisibleBars
|
|
18203
|
-
) * 0.25
|
|
18204
|
-
)
|
|
18205
|
-
);
|
|
19089
|
+
const threshold = Math.max(20, this.prefetchThreshold);
|
|
18206
19090
|
if (this._targetViewport.startIndex > threshold) return;
|
|
18207
19091
|
if (this.dataStore.length === 0) return;
|
|
19092
|
+
const gen = this._loadGeneration;
|
|
18208
19093
|
this.isFetching = true;
|
|
18209
19094
|
const before = this.dataStore.all[0].time;
|
|
18210
19095
|
const end = new Date(before - 1);
|
|
@@ -18216,9 +19101,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18216
19101
|
const interval = this.timeframeToInterval(this.getBaseInterval());
|
|
18217
19102
|
this.setLoading(true);
|
|
18218
19103
|
this.dataLoader({ start, end, interval }).then((bars) => {
|
|
19104
|
+
if (gen !== this._loadGeneration) return;
|
|
18219
19105
|
this.prependData(this.maybeAggregate(bars));
|
|
18220
19106
|
this.setLoading(false);
|
|
18221
19107
|
}).catch(() => {
|
|
19108
|
+
if (gen !== this._loadGeneration) return;
|
|
18222
19109
|
this.isFetching = false;
|
|
18223
19110
|
this.setLoading(false);
|
|
18224
19111
|
});
|
|
@@ -18430,6 +19317,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18430
19317
|
this.leftBar?.setTheme(this.theme);
|
|
18431
19318
|
this.tradeButton?.setTheme(this.theme, this.priceAxisWidth);
|
|
18432
19319
|
this.tradePopover?.setTheme(this.theme);
|
|
19320
|
+
this._draftQtyFlyout?.setTheme(this.theme);
|
|
18433
19321
|
this.indicatorSettingsDialog?.setTheme(this.theme);
|
|
18434
19322
|
this.chartSettingsDialog?.setTheme(this.theme);
|
|
18435
19323
|
this.applyScrollToEndBtnStyles();
|
|
@@ -18460,11 +19348,21 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18460
19348
|
this.leftBar?.setTheme(this.theme);
|
|
18461
19349
|
this.tradeButton?.setTheme(this.theme, this.priceAxisWidth);
|
|
18462
19350
|
this.tradePopover?.setTheme(this.theme);
|
|
19351
|
+
this._draftQtyFlyout?.setTheme(this.theme);
|
|
18463
19352
|
this.indicatorSettingsDialog?.setTheme(this.theme);
|
|
18464
19353
|
this.chartSettingsDialog?.setTheme(this.theme);
|
|
18465
19354
|
this.scheduleRender();
|
|
18466
19355
|
return this;
|
|
18467
19356
|
}
|
|
19357
|
+
/**
|
|
19358
|
+
* Changes the display timezone for all time-axis and crosshair labels.
|
|
19359
|
+
* Accepts any IANA string (e.g. `"America/New_York"`), `"UTC"`, or `"local"`.
|
|
19360
|
+
*/
|
|
19361
|
+
setTimezone(tz) {
|
|
19362
|
+
this.timezone = tz === "local" ? Intl.DateTimeFormat().resolvedOptions().timeZone : tz;
|
|
19363
|
+
this.scheduleRender();
|
|
19364
|
+
return this;
|
|
19365
|
+
}
|
|
18468
19366
|
setSeries(series) {
|
|
18469
19367
|
this.series = series;
|
|
18470
19368
|
this.topBar?.setSeries(series);
|
|
@@ -18473,6 +19371,31 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18473
19371
|
this.emitStateChange();
|
|
18474
19372
|
return this;
|
|
18475
19373
|
}
|
|
19374
|
+
/**
|
|
19375
|
+
* Toggle TFC (Trade from Charts) on or off at runtime.
|
|
19376
|
+
* When disabled, all trade levels, the trade button, and trade popover are hidden.
|
|
19377
|
+
* Only functional when `tfcEnabled` config option is `true` (default).
|
|
19378
|
+
*/
|
|
19379
|
+
setTfcActive(enabled) {
|
|
19380
|
+
if (!this.tfcEnabled) return this;
|
|
19381
|
+
this.tfcActive = enabled;
|
|
19382
|
+
this.topBar?.setTfcActive(enabled);
|
|
19383
|
+
if (!enabled) {
|
|
19384
|
+
this.tradeButton?.hide();
|
|
19385
|
+
this.tradePopover?.hide();
|
|
19386
|
+
if (this.draftOrderLabel !== null) {
|
|
19387
|
+
this.removeDraftOrder();
|
|
19388
|
+
}
|
|
19389
|
+
}
|
|
19390
|
+
if (this.chartSettingsDialog) {
|
|
19391
|
+
this.chartSettingsDialog.destroy();
|
|
19392
|
+
this.chartSettingsDialog = null;
|
|
19393
|
+
}
|
|
19394
|
+
this.renderTradeLayer();
|
|
19395
|
+
this.emitter.emit("tfcToggle", { enabled });
|
|
19396
|
+
this.emitStateChange();
|
|
19397
|
+
return this;
|
|
19398
|
+
}
|
|
18476
19399
|
clearDuration() {
|
|
18477
19400
|
if (this.duration === void 0) return;
|
|
18478
19401
|
this.duration = void 0;
|
|
@@ -18516,7 +19439,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18516
19439
|
})),
|
|
18517
19440
|
positionRenderStyle: this.positionRenderStyle,
|
|
18518
19441
|
tradeDisplayFilter: this.tradeDisplayFilter,
|
|
18519
|
-
canvasColors: Object.keys(this.settingsCanvasColors).length > 0 ? { ...this.settingsCanvasColors } : void 0
|
|
19442
|
+
canvasColors: Object.keys(this.settingsCanvasColors).length > 0 ? { ...this.settingsCanvasColors } : void 0,
|
|
19443
|
+
tfcEnabled: this.tfcActive
|
|
18520
19444
|
};
|
|
18521
19445
|
}
|
|
18522
19446
|
/**
|
|
@@ -18588,9 +19512,14 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18588
19512
|
if (state.canvasColors) {
|
|
18589
19513
|
this.settingsCanvasColors = state.canvasColors;
|
|
18590
19514
|
}
|
|
19515
|
+
if (state.tfcEnabled !== void 0 && this.tfcEnabled) {
|
|
19516
|
+
this.tfcActive = state.tfcEnabled;
|
|
19517
|
+
this.topBar?.setTfcActive(state.tfcEnabled);
|
|
19518
|
+
}
|
|
18591
19519
|
} finally {
|
|
18592
19520
|
this.isRestoringState = false;
|
|
18593
19521
|
}
|
|
19522
|
+
this.emitStateChange();
|
|
18594
19523
|
this.scheduleRender();
|
|
18595
19524
|
return this;
|
|
18596
19525
|
}
|
|
@@ -18652,6 +19581,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18652
19581
|
}
|
|
18653
19582
|
if (this.siStreamDot)
|
|
18654
19583
|
this.siStreamDot.style.background = this.theme.ui.stream[this.streamStatus];
|
|
19584
|
+
this.liveBidAsk = null;
|
|
18655
19585
|
return this;
|
|
18656
19586
|
}
|
|
18657
19587
|
/**
|
|
@@ -18715,6 +19645,38 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18715
19645
|
this.tradePopover?.setLots(lots);
|
|
18716
19646
|
return this;
|
|
18717
19647
|
}
|
|
19648
|
+
/**
|
|
19649
|
+
* Updates the quantity (lots) shown in the draft order QTY pill and
|
|
19650
|
+
* submitted when the order is confirmed.
|
|
19651
|
+
* @param qty Lot size to display and submit.
|
|
19652
|
+
*/
|
|
19653
|
+
setDraftQty(qty) {
|
|
19654
|
+
if (this.draftOrderLabel) {
|
|
19655
|
+
const idx = this.levels.findIndex((l) => l.label === this.draftOrderLabel);
|
|
19656
|
+
if (idx !== -1) {
|
|
19657
|
+
const lvl = this.levels[idx];
|
|
19658
|
+
this.levels = [
|
|
19659
|
+
...this.levels.slice(0, idx),
|
|
19660
|
+
{ ...lvl, lots: qty },
|
|
19661
|
+
...this.levels.slice(idx + 1)
|
|
19662
|
+
];
|
|
19663
|
+
this.renderTradeLayer();
|
|
19664
|
+
}
|
|
19665
|
+
}
|
|
19666
|
+
return this;
|
|
19667
|
+
}
|
|
19668
|
+
openDraftQtyFlyout(pill) {
|
|
19669
|
+
if (!this._draftQtyFlyout) {
|
|
19670
|
+
this._draftQtyFlyout = new DraftQtyFlyout(this.theme, (qty) => this.setDraftQty(qty));
|
|
19671
|
+
this.getFloatingRoot().appendChild(this._draftQtyFlyout.el);
|
|
19672
|
+
}
|
|
19673
|
+
const draft = this.levels.find((l) => l.label === this.draftOrderLabel);
|
|
19674
|
+
const currentQty = draft?.lots ?? this._qtyFieldMin;
|
|
19675
|
+
const rect = this.canvasWrap.getBoundingClientRect();
|
|
19676
|
+
const vpX = rect.left + pill.pillX;
|
|
19677
|
+
const vpY = rect.top + pill.pillY + pill.pillH;
|
|
19678
|
+
this._draftQtyFlyout.show(vpX, vpY, currentQty, this._qtyFieldMin, this._qtyFieldMax);
|
|
19679
|
+
}
|
|
18718
19680
|
/**
|
|
18719
19681
|
* @deprecated DraftOrderQtyInput has been removed. This method is a no-op
|
|
18720
19682
|
* kept for backward compatibility. It will be removed in a future version.
|
|
@@ -19190,7 +20152,27 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19190
20152
|
*/
|
|
19191
20153
|
selectLevel(label) {
|
|
19192
20154
|
this.selectedTradeLabel = label;
|
|
19193
|
-
if (label !== null)
|
|
20155
|
+
if (label !== null) {
|
|
20156
|
+
this.crosshair = null;
|
|
20157
|
+
const cluster = this.lastClusters.find(
|
|
20158
|
+
(c) => c.members.some((m) => m.level.label === label)
|
|
20159
|
+
);
|
|
20160
|
+
if (cluster) this.expandedClusters.add(cluster.id);
|
|
20161
|
+
}
|
|
20162
|
+
this.renderTradeLayer();
|
|
20163
|
+
return this;
|
|
20164
|
+
}
|
|
20165
|
+
/** Enable or disable trade-level fan-out clustering at runtime. */
|
|
20166
|
+
setLevelClusteringEnabled(enabled) {
|
|
20167
|
+
this.levelClusteringEnabled = enabled;
|
|
20168
|
+
this.expandedClusters.clear();
|
|
20169
|
+
this.renderTradeLayer();
|
|
20170
|
+
return this;
|
|
20171
|
+
}
|
|
20172
|
+
/** Update the cluster proximity threshold (in pixels) at runtime. Only effective when levelClusteringEnabled is true. */
|
|
20173
|
+
setClusterThresholdDistance(px) {
|
|
20174
|
+
this.clusterThresholdDistance = px;
|
|
20175
|
+
this.expandedClusters.clear();
|
|
19194
20176
|
this.renderTradeLayer();
|
|
19195
20177
|
return this;
|
|
19196
20178
|
}
|
|
@@ -19205,6 +20187,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19205
20187
|
this.chartSettingsDialog?.destroy();
|
|
19206
20188
|
this.isinPickerModal?.destroy();
|
|
19207
20189
|
this.tradePopover?.destroy();
|
|
20190
|
+
this._draftQtyFlyout?.destroy();
|
|
19208
20191
|
this.leftBar?.destroy();
|
|
19209
20192
|
this.drawCanvas.removeEventListener("mousemove", this.onMouseMove);
|
|
19210
20193
|
this.drawCanvas.removeEventListener("mouseleave", this.onMouseLeave);
|
|
@@ -19373,7 +20356,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19373
20356
|
theme: this.themeName
|
|
19374
20357
|
},
|
|
19375
20358
|
this.labels.dialogs,
|
|
19376
|
-
!!this._onOrderSubmit,
|
|
20359
|
+
!!this._onOrderSubmit && this.tfcActive,
|
|
19377
20360
|
this.enableThemeToggle
|
|
19378
20361
|
);
|
|
19379
20362
|
this.getFloatingRoot().appendChild(this.chartSettingsDialog.el);
|
|
@@ -19417,7 +20400,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19417
20400
|
const modals = [
|
|
19418
20401
|
this.isinPickerModal?.el,
|
|
19419
20402
|
this.indicatorSettingsDialog?.el,
|
|
19420
|
-
this.chartSettingsDialog?.el
|
|
20403
|
+
this.chartSettingsDialog?.el,
|
|
20404
|
+
this._draftQtyFlyout?.el
|
|
19421
20405
|
];
|
|
19422
20406
|
for (const el of modals) {
|
|
19423
20407
|
if (el && el.parentElement !== root) root.appendChild(el);
|
|
@@ -19484,11 +20468,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19484
20468
|
const dpr = window.devicePixelRatio || 1;
|
|
19485
20469
|
const { tradeCtx: ctx, tradeCanvas: canvas, theme, viewport } = this;
|
|
19486
20470
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
19487
|
-
if (this.levels.length === 0 || this.dataStore.length === 0) {
|
|
20471
|
+
if (!this.tfcActive || this.levels.length === 0 || this.dataStore.length === 0) {
|
|
19488
20472
|
this.tradeLevelHitAreas = [];
|
|
19489
20473
|
this.tradeLevelEditAreas = [];
|
|
19490
20474
|
this.tradeHoverAreas = [];
|
|
19491
20475
|
this.tradeAddBracketAreas = [];
|
|
20476
|
+
this.offViewportHitAreas = [];
|
|
19492
20477
|
return;
|
|
19493
20478
|
}
|
|
19494
20479
|
const { chartW, priceH } = this.getChartLayout();
|
|
@@ -19543,6 +20528,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19543
20528
|
// Include true pending orders AND entry-order positions (ES/EL)
|
|
19544
20529
|
processedLevels.filter((l) => l.type === "pending" || isEntryOrder(l))
|
|
19545
20530
|
) : processedLevels;
|
|
20531
|
+
const clusterThreshold = this.levelClusteringEnabled ? this.clusterThresholdDistance : 0;
|
|
19546
20532
|
const result = renderTradeLevels(
|
|
19547
20533
|
ctx,
|
|
19548
20534
|
filteredLevels,
|
|
@@ -19560,9 +20546,23 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19560
20546
|
this.positionRenderStyle,
|
|
19561
20547
|
this.hideLevelConfirmCancel,
|
|
19562
20548
|
this.draftOrderLabel,
|
|
19563
|
-
this.draftBracketPnl
|
|
20549
|
+
this.draftBracketPnl,
|
|
20550
|
+
this.expandedClusters,
|
|
20551
|
+
clusterThreshold,
|
|
20552
|
+
this._showQuantityField
|
|
19564
20553
|
);
|
|
19565
20554
|
ctx.restore();
|
|
20555
|
+
const offVpResult = renderOffViewportIndicators(
|
|
20556
|
+
ctx,
|
|
20557
|
+
filteredLevels,
|
|
20558
|
+
priceRange,
|
|
20559
|
+
theme,
|
|
20560
|
+
chartW,
|
|
20561
|
+
priceH,
|
|
20562
|
+
this.tradeDragHandler?.dragLabel ?? null,
|
|
20563
|
+
this.selectedTradeLabel
|
|
20564
|
+
);
|
|
20565
|
+
this.offViewportHitAreas = offVpResult.hitAreas;
|
|
19566
20566
|
ctx.restore();
|
|
19567
20567
|
this.tradeLevelHitAreas = result.hitAreas;
|
|
19568
20568
|
this.tradeLevelEditAreas = result.editAreas;
|
|
@@ -19570,6 +20570,15 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19570
20570
|
this.tradeLevelDragAreas = result.dragAreas;
|
|
19571
20571
|
this.tradeLevelConfirmAreas = result.confirmAreas;
|
|
19572
20572
|
this.tradeAddBracketAreas = result.addBracketAreas;
|
|
20573
|
+
this.clusterBadgeAreas = result.clusterBadgeAreas;
|
|
20574
|
+
this.lastClusters = result.clusters;
|
|
20575
|
+
this._draftQtyPillArea = result.draftQtyPillArea;
|
|
20576
|
+
if (this.expandedClusters.size > 0) {
|
|
20577
|
+
const currentIds = new Set(result.clusters.map((c) => c.id));
|
|
20578
|
+
for (const id of this.expandedClusters) {
|
|
20579
|
+
if (!currentIds.has(id)) this.expandedClusters.delete(id);
|
|
20580
|
+
}
|
|
20581
|
+
}
|
|
19573
20582
|
}
|
|
19574
20583
|
// ── Inline pending-change confirmation ────────────────────────────────────
|
|
19575
20584
|
/** Returns the effective entry price for a level, preferring any staged main drag. */
|
|
@@ -19683,6 +20692,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19683
20692
|
side,
|
|
19684
20693
|
price,
|
|
19685
20694
|
orderType,
|
|
20695
|
+
lots: this._qtyFieldMin,
|
|
19686
20696
|
data: { _isDraft: true }
|
|
19687
20697
|
};
|
|
19688
20698
|
this.levels = [...this.levels, draftLevel];
|
|
@@ -19706,6 +20716,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19706
20716
|
this.selectedTradeLabel = null;
|
|
19707
20717
|
this.draftOrderLabel = null;
|
|
19708
20718
|
this.draftBracketPnl = {};
|
|
20719
|
+
this._draftQtyFlyout?.hide();
|
|
20720
|
+
this._draftQtyPillArea = void 0;
|
|
19709
20721
|
this.renderTradeLayer();
|
|
19710
20722
|
this.emitter.emit("draftCancelled", { label, isFullscreen: this.isFullscreen });
|
|
19711
20723
|
}
|
|
@@ -19719,7 +20731,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
19719
20731
|
const draft = level;
|
|
19720
20732
|
this._onOrderSubmit?.({
|
|
19721
20733
|
price: mainEntry?.newPrice ?? draft.price,
|
|
19722
|
-
lots: this.
|
|
20734
|
+
lots: draft.lots ?? this._qtyFieldMin,
|
|
19723
20735
|
side: draft.side,
|
|
19724
20736
|
orderType: draft.orderType,
|
|
19725
20737
|
...draft.stopLossPrice !== void 0 ? { stopLoss: draft.stopLossPrice } : {},
|
|
@@ -20197,7 +21209,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20197
21209
|
isMobileDevice() ? 3 : void 0,
|
|
20198
21210
|
this.uiConfig.timeAxis,
|
|
20199
21211
|
this.timeframe,
|
|
20200
|
-
countdownBarIndex
|
|
21212
|
+
countdownBarIndex,
|
|
21213
|
+
this.timezone
|
|
20201
21214
|
);
|
|
20202
21215
|
if (showCountdown) {
|
|
20203
21216
|
const lastBar2 = this.dataStore.all[this.dataStore.length - 1];
|
|
@@ -20230,7 +21243,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20230
21243
|
subPaneY,
|
|
20231
21244
|
this.timeframe,
|
|
20232
21245
|
this.dataStore.getPricePrecision(),
|
|
20233
|
-
this.uiConfig.crosshair
|
|
21246
|
+
this.uiConfig.crosshair,
|
|
21247
|
+
this.timezone
|
|
20234
21248
|
);
|
|
20235
21249
|
}
|
|
20236
21250
|
if (this.logoImage) {
|
|
@@ -20464,7 +21478,12 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20464
21478
|
if (this._isTradingActive) {
|
|
20465
21479
|
this.tradeButton.hide();
|
|
20466
21480
|
} else {
|
|
20467
|
-
this.tradeButton.
|
|
21481
|
+
const _btnHalfH1 = this.uiConfig.tradeButton.size / 2;
|
|
21482
|
+
if (overlapsOffViewportPill(y, _btnHalfH1, this.offViewportHitAreas)) {
|
|
21483
|
+
this.tradeButton.hide();
|
|
21484
|
+
} else if (this.tfcActive) {
|
|
21485
|
+
this.tradeButton.show();
|
|
21486
|
+
}
|
|
20468
21487
|
}
|
|
20469
21488
|
this._tradeButtonClickData = { price, y };
|
|
20470
21489
|
const _allBars = this.dataStore.all;
|
|
@@ -20477,6 +21496,9 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20477
21496
|
/** Shared helper: initiate a trade level main-line drag from a hit area. */
|
|
20478
21497
|
_startTradeDragFromHit(dragHit, level, hitY) {
|
|
20479
21498
|
if (!this.tradeDragHandler) return;
|
|
21499
|
+
const { priceH } = this.getChartLayout();
|
|
21500
|
+
const priceRange = this._displayedPriceRange ?? this.getScaledPriceRange(this.viewport);
|
|
21501
|
+
const levelPixelY = this.scaleManager.yToPixel(dragHit.price, priceRange, priceH);
|
|
20480
21502
|
if (dragHit.isBracket) {
|
|
20481
21503
|
this._pendingBracketOrderLabel = dragHit.bracketOrderLabel ?? null;
|
|
20482
21504
|
this._isBracketNew = false;
|
|
@@ -20489,6 +21511,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20489
21511
|
level,
|
|
20490
21512
|
dragHit.bracketType,
|
|
20491
21513
|
dragHit.price,
|
|
21514
|
+
hitY,
|
|
21515
|
+
levelPixelY,
|
|
20492
21516
|
marketPrice,
|
|
20493
21517
|
this._effectiveEntryPrice(level.label, level.price)
|
|
20494
21518
|
);
|
|
@@ -20508,6 +21532,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20508
21532
|
this.tradeDragHandler.startDrag(
|
|
20509
21533
|
level,
|
|
20510
21534
|
hitY,
|
|
21535
|
+
levelPixelY,
|
|
20511
21536
|
dragHit.price,
|
|
20512
21537
|
posLevel.stopLossPrice,
|
|
20513
21538
|
posLevel.takeProfitPrice,
|
|
@@ -20522,6 +21547,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20522
21547
|
this.tradeDragHandler.startDrag(
|
|
20523
21548
|
level,
|
|
20524
21549
|
hitY,
|
|
21550
|
+
levelPixelY,
|
|
20525
21551
|
dragHit.price,
|
|
20526
21552
|
void 0,
|
|
20527
21553
|
void 0,
|
|
@@ -20551,6 +21577,8 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20551
21577
|
level,
|
|
20552
21578
|
addHit.bracketType,
|
|
20553
21579
|
initialPrice,
|
|
21580
|
+
hitY,
|
|
21581
|
+
btnCenterY,
|
|
20554
21582
|
marketPrice,
|
|
20555
21583
|
this._effectiveEntryPrice(level.label, level.price)
|
|
20556
21584
|
);
|
|
@@ -20637,6 +21665,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20637
21665
|
* This happens when the system clock was behind feed time during the initial data load. */
|
|
20638
21666
|
fillTickGap(fromTime, toTime) {
|
|
20639
21667
|
if (!this.dataLoader) return;
|
|
21668
|
+
if (this.isFetching) return;
|
|
20640
21669
|
const interval = this.timeframeToInterval(this.getBaseInterval());
|
|
20641
21670
|
this.dataLoader({ start: new Date(fromTime), end: new Date(toTime), interval }).then((rawBars) => {
|
|
20642
21671
|
const bars = this.maybeAggregate(rawBars);
|
|
@@ -20671,6 +21700,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20671
21700
|
* tick-reconstructed OHLCV with authoritative exchange data. No-op if no loader. */
|
|
20672
21701
|
correctCompletedBar(barTime, intervalMs) {
|
|
20673
21702
|
if (!this.dataLoader) return;
|
|
21703
|
+
if (this.isFetching) return;
|
|
20674
21704
|
const start = new Date(barTime);
|
|
20675
21705
|
const end = new Date(barTime + intervalMs * 3);
|
|
20676
21706
|
const interval = this.timeframeToInterval(this.getBaseInterval());
|
|
@@ -20710,6 +21740,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20710
21740
|
this.scheduleRender();
|
|
20711
21741
|
}
|
|
20712
21742
|
scrollToEnd() {
|
|
21743
|
+
this.panHandler.cancel();
|
|
20713
21744
|
if (this.scrollAnimId !== null) {
|
|
20714
21745
|
cancelAnimationFrame(this.scrollAnimId);
|
|
20715
21746
|
this.scrollAnimId = null;
|
|
@@ -20741,6 +21772,38 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20741
21772
|
};
|
|
20742
21773
|
this.scrollAnimId = requestAnimationFrame(step);
|
|
20743
21774
|
}
|
|
21775
|
+
/**
|
|
21776
|
+
* Smooth-scroll the Y axis so `price` lands at the vertical center of the
|
|
21777
|
+
* chart. Animates `pricePanOffset` only — horizontal viewport is preserved.
|
|
21778
|
+
* Used by off-viewport level indicator clicks.
|
|
21779
|
+
*/
|
|
21780
|
+
scrollToCenterPrice(price) {
|
|
21781
|
+
if (!isFinite(price) || this.dataStore.length === 0) return;
|
|
21782
|
+
if (this.scrollAnimId !== null) {
|
|
21783
|
+
cancelAnimationFrame(this.scrollAnimId);
|
|
21784
|
+
this.scrollAnimId = null;
|
|
21785
|
+
}
|
|
21786
|
+
const base = this.dataStore.getPriceRange(this.viewport);
|
|
21787
|
+
const naturalMid = (base.min + base.max) / 2;
|
|
21788
|
+
const startPan = this.pricePanOffset;
|
|
21789
|
+
const targetPan = price - naturalMid;
|
|
21790
|
+
if (Math.abs(targetPan - startPan) < 1e-9) return;
|
|
21791
|
+
const DURATION = 350;
|
|
21792
|
+
const t0 = performance.now();
|
|
21793
|
+
const ease = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
|
21794
|
+
const step = (now) => {
|
|
21795
|
+
const raw = Math.min((now - t0) / DURATION, 1);
|
|
21796
|
+
const prog = ease(raw);
|
|
21797
|
+
this.pricePanOffset = startPan + (targetPan - startPan) * prog;
|
|
21798
|
+
this.render();
|
|
21799
|
+
if (raw < 1) {
|
|
21800
|
+
this.scrollAnimId = requestAnimationFrame(step);
|
|
21801
|
+
} else {
|
|
21802
|
+
this.scrollAnimId = null;
|
|
21803
|
+
}
|
|
21804
|
+
};
|
|
21805
|
+
this.scrollAnimId = requestAnimationFrame(step);
|
|
21806
|
+
}
|
|
20744
21807
|
resetPriceAxis() {
|
|
20745
21808
|
this.priceScaleFactor = 1;
|
|
20746
21809
|
this.pricePanOffset = 0;
|
|
@@ -20748,6 +21811,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20748
21811
|
this.scheduleRender();
|
|
20749
21812
|
}
|
|
20750
21813
|
resetTimeAxis() {
|
|
21814
|
+
this.panHandler.cancel();
|
|
20751
21815
|
const total = this.dataStore.length;
|
|
20752
21816
|
if (total === 0) return;
|
|
20753
21817
|
const newStart = Math.max(
|