acttrader-charts 1.0.18 → 1.0.19
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 +12 -0
- package/dist/{MACD-CjgSMwdG.d.cts → MACD-DZxpf0eQ.d.cts} +21 -0
- package/dist/{MACD-CjgSMwdG.d.ts → MACD-DZxpf0eQ.d.ts} +21 -0
- package/dist/index.cjs +258 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -2
- package/dist/index.d.ts +12 -2
- package/dist/index.js +258 -37
- 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 +7 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -177,6 +177,8 @@ chart.setLevels([], 'label', 'price', 'pending'); // clear all of one type
|
|
|
177
177
|
|
|
178
178
|
**Brackets follow entry on drag:** when the entry line of a pending order, draft order, or an open position with `entryPriceEditable: true` is dragged, any existing `stopLossPrice` / `takeProfitPrice` brackets translate along with it by the same price delta. The distance between entry and each bracket is whatever the user currently sees — if they manually drag SL or TP to a new price, that new distance becomes the anchor for the next entry drag. Missing brackets aren't auto-created. On confirm, `tradeLevelEdit` carries all translated fields together in one `changes[]` array; on mobile (`hideLevelConfirmCancel`) the three changes are applied as one atomic event.
|
|
179
179
|
|
|
180
|
+
**Bracket pill auto-offset:** when an SL or TP price sits within about one pill-height of the entry price, the bracket's label pill is pushed vertically away from the entry pill and connected back to its real price line by a dashed leader. The horizontal bracket line itself stays at the true price; only the pill and its `×` button move, and drag targets follow the displaced pill — so grabbing the pill always drags the bracket, grabbing the entry pill always drags the entry, and grabbing the real bracket line still drags the bracket. Works for both buy and sell orders without configuration.
|
|
181
|
+
|
|
180
182
|
**TFC events:**
|
|
181
183
|
|
|
182
184
|
```ts
|
|
@@ -204,6 +206,14 @@ chart.on('tradeLevelClose', ({ label, type, action, isFullscreen }) => {
|
|
|
204
206
|
chart.on('tradeLevelDrag', ({ label, newPrice, bracketType, isFullscreen }) => {
|
|
205
207
|
updatePricePreview(label, newPrice);
|
|
206
208
|
});
|
|
209
|
+
|
|
210
|
+
// Live qty while the user edits the QTY pill flyout — fires before the level edit is
|
|
211
|
+
// confirmed, so host-side Estimated PNL for SL/TP brackets can recompute immediately
|
|
212
|
+
// instead of waiting for `tradeLevelEdit` at ✓ confirm. `type` is `'draft'` for draft
|
|
213
|
+
// orders, otherwise the parent level's type.
|
|
214
|
+
chart.on('tradeLevelQtyChange', ({ label, type, newLots, previousLots }) => {
|
|
215
|
+
updateEstimatedPnl(label, newLots);
|
|
216
|
+
});
|
|
207
217
|
```
|
|
208
218
|
|
|
209
219
|
**Off-viewport level indicators:**
|
|
@@ -784,8 +794,10 @@ chart.on('stateChange', ({ symbol, state }) => {
|
|
|
784
794
|
chart.on('tradeLevelEdit', ({ label, type, data, changes, isFullscreen }) => {});
|
|
785
795
|
chart.on('tradeLevelClose', ({ label, type, action, data, isFullscreen }) => {});
|
|
786
796
|
chart.on('tradeLevelDrag', ({ label, newPrice, data, bracketType, isFullscreen }) => {}); // fires on every mouse-move during drag
|
|
797
|
+
chart.on('tradeLevelQtyChange', ({ label, type, newLots, previousLots, isFullscreen }) => {}); // live qty edit via QTY pill — use to refresh Estimated PNL on SL/TP brackets
|
|
787
798
|
chart.on('tradeLevelEditOpen', ({ label, type, price, side, stopLossPrice, takeProfitPrice, data, isFullscreen }) => {});
|
|
788
799
|
chart.on('tradeLevelConfirmed',({ label, type, isFullscreen }) => {});
|
|
800
|
+
chart.on('tradeLevelEditCancelled', ({ label, type, isFullscreen }) => {}); // ESC / inline ✕ aborted an edit — reset external modify-order panel
|
|
789
801
|
chart.on('draftInitiated', ({ side, price, orderType, isFullscreen }) => {}); // new draft order shown — open buy/sell form
|
|
790
802
|
chart.on('draftCancelled', ({ label, isFullscreen }) => {}); // draft order dismissed without confirming
|
|
791
803
|
chart.on('tfcToggle', ({ enabled }) => {}); // TFC toggled on or off via top bar button or setTfcActive()
|
|
@@ -1126,6 +1126,14 @@ type ChartEventMap = {
|
|
|
1126
1126
|
type: TradeLevelType | 'draft';
|
|
1127
1127
|
isFullscreen: boolean;
|
|
1128
1128
|
};
|
|
1129
|
+
/** Emitted when an in-progress level edit is cancelled from the chart (ESC key or inline ✕ cancel button).
|
|
1130
|
+
* Mirrors `tradeLevelConfirmed` but for the revert path. Not fired for draft orders (those emit `draftCancelled`).
|
|
1131
|
+
* External panels listen to reset their modify-order form when the user aborts the edit from the chart side. */
|
|
1132
|
+
tradeLevelEditCancelled: {
|
|
1133
|
+
label: string;
|
|
1134
|
+
type: TradeLevelType;
|
|
1135
|
+
isFullscreen: boolean;
|
|
1136
|
+
};
|
|
1129
1137
|
/** Emitted when a draft order is cancelled (Escape, ✕ button, or external revert). */
|
|
1130
1138
|
draftCancelled: {
|
|
1131
1139
|
label: string;
|
|
@@ -1173,6 +1181,19 @@ type ChartEventMap = {
|
|
|
1173
1181
|
uiStateChange: {
|
|
1174
1182
|
hasOpenUI: boolean;
|
|
1175
1183
|
};
|
|
1184
|
+
/** Emitted live while the user edits lot qty via the QTY pill flyout, before the
|
|
1185
|
+
* level edit is confirmed. Use this to recompute qty-derived UI (e.g. Estimated
|
|
1186
|
+
* PNL on SL/TP brackets) in real time. Final committed qty still arrives on
|
|
1187
|
+
* `tradeLevelEdit.newLots` at ✓ confirm (or immediately for draft orders).
|
|
1188
|
+
* `type` is `'draft'` for the in-progress draft order, otherwise the parent level's type.
|
|
1189
|
+
* `previousLots` is the qty at edit-session start (useful for revert-aware previews). */
|
|
1190
|
+
tradeLevelQtyChange: {
|
|
1191
|
+
label: string;
|
|
1192
|
+
type: TradeLevelType | 'draft';
|
|
1193
|
+
newLots: number;
|
|
1194
|
+
previousLots: number;
|
|
1195
|
+
isFullscreen: boolean;
|
|
1196
|
+
};
|
|
1176
1197
|
/** Emitted when the user confirms all edits to a level — replaces separate tradeLevelDragEnd / tradeLevelBracketDrag events. */
|
|
1177
1198
|
tradeLevelEdit: {
|
|
1178
1199
|
label: string;
|
|
@@ -1126,6 +1126,14 @@ type ChartEventMap = {
|
|
|
1126
1126
|
type: TradeLevelType | 'draft';
|
|
1127
1127
|
isFullscreen: boolean;
|
|
1128
1128
|
};
|
|
1129
|
+
/** Emitted when an in-progress level edit is cancelled from the chart (ESC key or inline ✕ cancel button).
|
|
1130
|
+
* Mirrors `tradeLevelConfirmed` but for the revert path. Not fired for draft orders (those emit `draftCancelled`).
|
|
1131
|
+
* External panels listen to reset their modify-order form when the user aborts the edit from the chart side. */
|
|
1132
|
+
tradeLevelEditCancelled: {
|
|
1133
|
+
label: string;
|
|
1134
|
+
type: TradeLevelType;
|
|
1135
|
+
isFullscreen: boolean;
|
|
1136
|
+
};
|
|
1129
1137
|
/** Emitted when a draft order is cancelled (Escape, ✕ button, or external revert). */
|
|
1130
1138
|
draftCancelled: {
|
|
1131
1139
|
label: string;
|
|
@@ -1173,6 +1181,19 @@ type ChartEventMap = {
|
|
|
1173
1181
|
uiStateChange: {
|
|
1174
1182
|
hasOpenUI: boolean;
|
|
1175
1183
|
};
|
|
1184
|
+
/** Emitted live while the user edits lot qty via the QTY pill flyout, before the
|
|
1185
|
+
* level edit is confirmed. Use this to recompute qty-derived UI (e.g. Estimated
|
|
1186
|
+
* PNL on SL/TP brackets) in real time. Final committed qty still arrives on
|
|
1187
|
+
* `tradeLevelEdit.newLots` at ✓ confirm (or immediately for draft orders).
|
|
1188
|
+
* `type` is `'draft'` for the in-progress draft order, otherwise the parent level's type.
|
|
1189
|
+
* `previousLots` is the qty at edit-session start (useful for revert-aware previews). */
|
|
1190
|
+
tradeLevelQtyChange: {
|
|
1191
|
+
label: string;
|
|
1192
|
+
type: TradeLevelType | 'draft';
|
|
1193
|
+
newLots: number;
|
|
1194
|
+
previousLots: number;
|
|
1195
|
+
isFullscreen: boolean;
|
|
1196
|
+
};
|
|
1176
1197
|
/** Emitted when the user confirms all edits to a level — replaces separate tradeLevelDragEnd / tradeLevelBracketDrag events. */
|
|
1177
1198
|
tradeLevelEdit: {
|
|
1178
1199
|
label: string;
|
package/dist/index.cjs
CHANGED
|
@@ -1727,6 +1727,21 @@ function hasPnl(level) {
|
|
|
1727
1727
|
function rectsOverlapY(ay, ah, by, bh) {
|
|
1728
1728
|
return ay < by + bh && ay + ah > by;
|
|
1729
1729
|
}
|
|
1730
|
+
function cascadeBracketAwayFromEntry(entry, preferredBoxY, boxH, direction, sameColumnObstacles) {
|
|
1731
|
+
const obstacles = [];
|
|
1732
|
+
if (entry) obstacles.push(entry);
|
|
1733
|
+
for (const o of sameColumnObstacles) obstacles.push(o);
|
|
1734
|
+
let boxY = preferredBoxY;
|
|
1735
|
+
let attempts = 0;
|
|
1736
|
+
const maxAttempts = obstacles.length + 2;
|
|
1737
|
+
while (attempts < maxAttempts) {
|
|
1738
|
+
const hit = obstacles.some((o) => rectsOverlapY(boxY, boxH, o.boxY, o.boxH));
|
|
1739
|
+
if (!hit) break;
|
|
1740
|
+
boxY += direction * (boxH + 2);
|
|
1741
|
+
attempts++;
|
|
1742
|
+
}
|
|
1743
|
+
return boxY;
|
|
1744
|
+
}
|
|
1730
1745
|
function getLevelDisplayText(level, price, precision) {
|
|
1731
1746
|
if (level.data?._isDraft) {
|
|
1732
1747
|
const p = level;
|
|
@@ -1958,6 +1973,54 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
1958
1973
|
selectedLabel,
|
|
1959
1974
|
bracketBoxH
|
|
1960
1975
|
);
|
|
1976
|
+
const bracketItems = [];
|
|
1977
|
+
const fallbackX = chartW * BRACKET_BASE_RATIO;
|
|
1978
|
+
for (const level of levels) {
|
|
1979
|
+
if (level.label !== hoveredLabel && level.label !== selectedLabel) continue;
|
|
1980
|
+
const bh = level;
|
|
1981
|
+
const mainDrag = draggingLabel === level.label;
|
|
1982
|
+
if (bh.stopLossPrice !== void 0) {
|
|
1983
|
+
const key = `${level.label}:stopLoss`;
|
|
1984
|
+
const ownDrag = draggingLabel === key;
|
|
1985
|
+
const price = ownDrag && dragNewPrice !== null ? dragNewPrice : mainDrag && dragNewSLPrice !== null ? dragNewSLPrice : bh.stopLossPrice;
|
|
1986
|
+
bracketItems.push({
|
|
1987
|
+
key,
|
|
1988
|
+
label: level.label,
|
|
1989
|
+
y: scale.yToPixel(price, priceRange, priceH),
|
|
1990
|
+
columnX: bracketLineXMap.get(key) ?? fallbackX
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
if (bh.takeProfitPrice !== void 0) {
|
|
1994
|
+
const key = `${level.label}:takeProfit`;
|
|
1995
|
+
const ownDrag = draggingLabel === key;
|
|
1996
|
+
const price = ownDrag && dragNewPrice !== null ? dragNewPrice : mainDrag && dragNewTPPrice !== null ? dragNewTPPrice : bh.takeProfitPrice;
|
|
1997
|
+
bracketItems.push({
|
|
1998
|
+
key,
|
|
1999
|
+
label: level.label,
|
|
2000
|
+
y: scale.yToPixel(price, priceRange, priceH),
|
|
2001
|
+
columnX: bracketLineXMap.get(key) ?? fallbackX
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
bracketItems.sort((a, b) => a.y - b.y);
|
|
2006
|
+
const bracketBoxYMap = /* @__PURE__ */ new Map();
|
|
2007
|
+
const placedBrackets = [];
|
|
2008
|
+
for (const item of bracketItems) {
|
|
2009
|
+
const entry = layouts.get(item.label);
|
|
2010
|
+
const preferredBoxY = item.y - bracketBoxH / 2;
|
|
2011
|
+
const entryRect = entry ? { boxY: entry.boxY, boxH: entry.boxH } : null;
|
|
2012
|
+
const dir = entry ? Math.sign(item.y - entry.y) || 1 : 1;
|
|
2013
|
+
const sameColObstacles = placedBrackets.filter((p) => p.columnX === item.columnX);
|
|
2014
|
+
const newBoxY = cascadeBracketAwayFromEntry(
|
|
2015
|
+
entryRect,
|
|
2016
|
+
preferredBoxY,
|
|
2017
|
+
bracketBoxH,
|
|
2018
|
+
dir,
|
|
2019
|
+
sameColObstacles
|
|
2020
|
+
);
|
|
2021
|
+
bracketBoxYMap.set(item.key, newBoxY);
|
|
2022
|
+
placedBrackets.push({ columnX: item.columnX, boxY: newBoxY, boxH: bracketBoxH });
|
|
2023
|
+
}
|
|
1961
2024
|
const activeLabel = hoveredLabel ?? selectedLabel;
|
|
1962
2025
|
const bracketPnlHostLabel = draftOrderLabel ?? selectedLabel;
|
|
1963
2026
|
const visibleMeasured = measured.filter((m) => !collapsedLabels.has(m.level.label));
|
|
@@ -2009,7 +2072,8 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
2009
2072
|
draftQtyPillAreas,
|
|
2010
2073
|
dragNewSLPrice,
|
|
2011
2074
|
dragNewTPPrice,
|
|
2012
|
-
buttonScale
|
|
2075
|
+
buttonScale,
|
|
2076
|
+
bracketBoxYMap
|
|
2013
2077
|
);
|
|
2014
2078
|
const ec = labelToExpandedCluster.get(level.label);
|
|
2015
2079
|
if (ec && layout.boxY !== layout.y - boxH / 2) {
|
|
@@ -2022,7 +2086,6 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
2022
2086
|
const bh = offLevel;
|
|
2023
2087
|
const slDragKey = `${offLevel.label}:stopLoss`;
|
|
2024
2088
|
const tpDragKey = `${offLevel.label}:takeProfit`;
|
|
2025
|
-
const fallbackX = chartW * BRACKET_BASE_RATIO;
|
|
2026
2089
|
const offMainDragActive = draggingLabel === offLevel.label;
|
|
2027
2090
|
const offSlPrice = offMainDragActive && dragNewSLPrice !== null ? dragNewSLPrice : bh.stopLossPrice;
|
|
2028
2091
|
const offTpPrice = offMainDragActive && dragNewTPPrice !== null ? dragNewTPPrice : bh.takeProfitPrice;
|
|
@@ -2046,7 +2109,8 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
2046
2109
|
hideConfirmCancel,
|
|
2047
2110
|
bh._slOrderLabel,
|
|
2048
2111
|
void 0,
|
|
2049
|
-
buttonScale
|
|
2112
|
+
buttonScale,
|
|
2113
|
+
bracketBoxYMap.get(slDragKey)
|
|
2050
2114
|
);
|
|
2051
2115
|
}
|
|
2052
2116
|
if (offTpPrice !== void 0) {
|
|
@@ -2069,7 +2133,8 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
|
|
|
2069
2133
|
hideConfirmCancel,
|
|
2070
2134
|
bh._tpOrderLabel,
|
|
2071
2135
|
void 0,
|
|
2072
|
-
buttonScale
|
|
2136
|
+
buttonScale,
|
|
2137
|
+
bracketBoxYMap.get(tpDragKey)
|
|
2073
2138
|
);
|
|
2074
2139
|
}
|
|
2075
2140
|
}
|
|
@@ -2215,7 +2280,7 @@ function drawDotHoverBox(ctx, level, y, dotX, dotR, theme, precision) {
|
|
|
2215
2280
|
});
|
|
2216
2281
|
ctx.restore();
|
|
2217
2282
|
}
|
|
2218
|
-
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, dragNewSLPrice = null, dragNewTPPrice = null, buttonScale = 1) {
|
|
2283
|
+
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, dragNewSLPrice = null, dragNewTPPrice = null, buttonScale = 1, bracketBoxYMap) {
|
|
2219
2284
|
const { y, boxY, boxW, boxH } = layout;
|
|
2220
2285
|
const color = levelColor(level, colors, theme);
|
|
2221
2286
|
const active = hovered || selected;
|
|
@@ -2289,7 +2354,8 @@ function drawLevel(ctx, level, layout, chartW, colors, theme, precision, hovered
|
|
|
2289
2354
|
hideConfirmCancel,
|
|
2290
2355
|
bracketHost._slOrderLabel,
|
|
2291
2356
|
draftBracketPnl?.sl,
|
|
2292
|
-
buttonScale
|
|
2357
|
+
buttonScale,
|
|
2358
|
+
bracketBoxYMap?.get(slDragKey)
|
|
2293
2359
|
);
|
|
2294
2360
|
} else if (draggingLabel === slDragKey && dragNewPrice !== null) {
|
|
2295
2361
|
const bLineX = chartW * BRACKET_BASE_RATIO;
|
|
@@ -2336,7 +2402,8 @@ function drawLevel(ctx, level, layout, chartW, colors, theme, precision, hovered
|
|
|
2336
2402
|
hideConfirmCancel,
|
|
2337
2403
|
bracketHost._tpOrderLabel,
|
|
2338
2404
|
draftBracketPnl?.tp,
|
|
2339
|
-
buttonScale
|
|
2405
|
+
buttonScale,
|
|
2406
|
+
bracketBoxYMap?.get(tpDragKey)
|
|
2340
2407
|
);
|
|
2341
2408
|
} else if (draggingLabel === tpDragKey && dragNewPrice !== null) {
|
|
2342
2409
|
const bLineX = chartW * BRACKET_BASE_RATIO;
|
|
@@ -2670,7 +2737,7 @@ function drawAddBracketButton(ctx, level, bracketType, entryY, bracketLineX, cha
|
|
|
2670
2737
|
h: bH
|
|
2671
2738
|
});
|
|
2672
2739
|
}
|
|
2673
|
-
function drawBracketLine(ctx, level, bracketType, bracketPrice, scale, priceRange, priceH, bracketLineX, chartW, theme, precision, draggingLabel, dragNewPrice, hitAreas, dragAreas, hideConfirmCancel, bracketOrderLabel, pnlText, buttonScale = 1) {
|
|
2740
|
+
function drawBracketLine(ctx, level, bracketType, bracketPrice, scale, priceRange, priceH, bracketLineX, chartW, theme, precision, draggingLabel, dragNewPrice, hitAreas, dragAreas, hideConfirmCancel, bracketOrderLabel, pnlText, buttonScale = 1, boxYOverride) {
|
|
2674
2741
|
const closeR = CLOSE_R * Math.min(Math.max(buttonScale, 1), 3);
|
|
2675
2742
|
const isDraggingThis = draggingLabel === `${level.label}:${bracketType}`;
|
|
2676
2743
|
const price = isDraggingThis && dragNewPrice !== null ? dragNewPrice : bracketPrice;
|
|
@@ -2695,7 +2762,13 @@ function drawBracketLine(ctx, level, bracketType, bracketPrice, scale, priceRang
|
|
|
2695
2762
|
const bW = BRACKET_BOX_PAD_X + textW + pnlW + BRACKET_BOX_PAD_X + closeSlot;
|
|
2696
2763
|
const bH = hideConfirmCancel ? BRACKET_BOX_H : Math.max(BRACKET_BOX_H, closeR * 2 + BRACKET_BOX_PAD_Y * 2);
|
|
2697
2764
|
const bX = bracketLineX - bW - 2;
|
|
2698
|
-
const
|
|
2765
|
+
const naturalBoxY = y - bH / 2;
|
|
2766
|
+
const bY = boxYOverride ?? naturalBoxY;
|
|
2767
|
+
const boxCenterY = bY + bH / 2;
|
|
2768
|
+
const displaced = Math.abs(bY - naturalBoxY) >= 2;
|
|
2769
|
+
if (displaced) {
|
|
2770
|
+
drawBracketLeader(ctx, bX + bW / 2, boxCenterY, y, theme);
|
|
2771
|
+
}
|
|
2699
2772
|
ctx.fillStyle = monoBoxBg(theme, false);
|
|
2700
2773
|
ctx.strokeStyle = monoColor(theme);
|
|
2701
2774
|
ctx.lineWidth = 0.8;
|
|
@@ -2705,15 +2778,15 @@ function drawBracketLine(ctx, level, bracketType, bracketPrice, scale, priceRang
|
|
|
2705
2778
|
ctx.fillStyle = monoColor(theme);
|
|
2706
2779
|
ctx.textAlign = "left";
|
|
2707
2780
|
ctx.textBaseline = "middle";
|
|
2708
|
-
ctx.fillText(boxLabel, bX + BRACKET_BOX_PAD_X,
|
|
2781
|
+
ctx.fillText(boxLabel, bX + BRACKET_BOX_PAD_X, boxCenterY);
|
|
2709
2782
|
if (pnlText) {
|
|
2710
2783
|
const isLoss = pnlText.trim().startsWith("-");
|
|
2711
2784
|
ctx.fillStyle = isLoss ? theme.candle.down : theme.candle.up;
|
|
2712
|
-
ctx.fillText(pnlText, bX + BRACKET_BOX_PAD_X + textW + BRACKET_BOX_PAD_X,
|
|
2785
|
+
ctx.fillText(pnlText, bX + BRACKET_BOX_PAD_X + textW + BRACKET_BOX_PAD_X, boxCenterY);
|
|
2713
2786
|
}
|
|
2714
2787
|
if (!hideConfirmCancel) {
|
|
2715
2788
|
const btnX = bX + bW - BRACKET_BOX_PAD_X / 2 - closeR;
|
|
2716
|
-
const btnY =
|
|
2789
|
+
const btnY = boxCenterY;
|
|
2717
2790
|
ctx.fillStyle = monoColor(theme);
|
|
2718
2791
|
ctx.beginPath();
|
|
2719
2792
|
ctx.arc(btnX, btnY, closeR, 0, Math.PI * 2);
|
|
@@ -2771,6 +2844,19 @@ function drawBracketLine(ctx, level, bracketType, bracketPrice, scale, priceRang
|
|
|
2771
2844
|
isBracket: true
|
|
2772
2845
|
});
|
|
2773
2846
|
}
|
|
2847
|
+
function drawBracketLeader(ctx, leaderX, pillCenterY, realY, theme) {
|
|
2848
|
+
if (Math.abs(pillCenterY - realY) < 2) return;
|
|
2849
|
+
ctx.save();
|
|
2850
|
+
ctx.strokeStyle = monoColor(theme);
|
|
2851
|
+
ctx.globalAlpha = 0.35;
|
|
2852
|
+
ctx.lineWidth = 1;
|
|
2853
|
+
ctx.setLineDash([2, 3]);
|
|
2854
|
+
ctx.beginPath();
|
|
2855
|
+
ctx.moveTo(leaderX, pillCenterY);
|
|
2856
|
+
ctx.lineTo(leaderX, realY);
|
|
2857
|
+
ctx.stroke();
|
|
2858
|
+
ctx.restore();
|
|
2859
|
+
}
|
|
2774
2860
|
function drawGripHandle(ctx, x, boxY, boxH, colors, hovered) {
|
|
2775
2861
|
const dotColor = hovered ? colors.dragHandleHover : colors.dragHandle;
|
|
2776
2862
|
const cols = 2;
|
|
@@ -17550,6 +17636,16 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17550
17636
|
this.expandedClusters = /* @__PURE__ */ new Set();
|
|
17551
17637
|
this.hoveredTradeLabel = null;
|
|
17552
17638
|
this.selectedTradeLabel = null;
|
|
17639
|
+
/**
|
|
17640
|
+
* Label of the level whose external edit form is currently open (i.e. the label for which
|
|
17641
|
+
* `tradeLevelEditOpen` was most recently emitted and no matching cancel/confirm has fired yet).
|
|
17642
|
+
* Distinct from `selectedTradeLabel`, which can also be set by a plain desktop hover-click
|
|
17643
|
+
* that just shows brackets without opening the form. Used so outside-click / cross-button
|
|
17644
|
+
* can issue a proper cancel (emit `tradeLevelEditCancelled`) even when no pending drag is
|
|
17645
|
+
* staged — e.g. the user opened the form on a position that has no SL/TP yet and clicks
|
|
17646
|
+
* elsewhere without adding anything.
|
|
17647
|
+
*/
|
|
17648
|
+
this._editOpenLabel = null;
|
|
17553
17649
|
/** Estimated PNL text for draft order SL/TP bracket lines, set by setDraftBracketPnl(). */
|
|
17554
17650
|
this.draftBracketPnl = {};
|
|
17555
17651
|
// TFC components
|
|
@@ -17984,6 +18080,30 @@ var _ChartEngine = class _ChartEngine {
|
|
|
17984
18080
|
this._handleTradeLevelClose(closeHit);
|
|
17985
18081
|
return;
|
|
17986
18082
|
}
|
|
18083
|
+
if (!this.drawingManager.getActiveTool()) {
|
|
18084
|
+
const editHit = hitTestTradeLevelEdit(x, y, this.tradeLevelEditAreas);
|
|
18085
|
+
if (editHit) {
|
|
18086
|
+
e.preventDefault();
|
|
18087
|
+
const level = this.levels.find((l) => l.label === editHit.label);
|
|
18088
|
+
if (level) {
|
|
18089
|
+
this.selectedTradeLabel = level.label;
|
|
18090
|
+
this.renderTradeLayer();
|
|
18091
|
+
const { slPrice: _slE, tpPrice: _tpE } = this._resolveBracketPrices(level);
|
|
18092
|
+
this._editOpenLabel = level.label;
|
|
18093
|
+
this.emitter.emit("tradeLevelEditOpen", {
|
|
18094
|
+
label: level.label,
|
|
18095
|
+
type: level.type,
|
|
18096
|
+
data: level.data,
|
|
18097
|
+
price: level.price,
|
|
18098
|
+
side: level.side,
|
|
18099
|
+
stopLossPrice: _slE,
|
|
18100
|
+
takeProfitPrice: _tpE,
|
|
18101
|
+
isFullscreen: this.isFullscreen
|
|
18102
|
+
});
|
|
18103
|
+
}
|
|
18104
|
+
return;
|
|
18105
|
+
}
|
|
18106
|
+
}
|
|
17987
18107
|
if (this.tradeDragHandler && !this.drawingManager.getActiveTool()) {
|
|
17988
18108
|
const dragHit = hitTestTradeLevelDrag(x, y, this.tradeLevelDragAreas);
|
|
17989
18109
|
if (dragHit) {
|
|
@@ -18001,6 +18121,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18001
18121
|
}
|
|
18002
18122
|
this.renderTradeLayer();
|
|
18003
18123
|
const { slPrice: _sl1, tpPrice: _tp1 } = this._resolveBracketPrices(level);
|
|
18124
|
+
this._editOpenLabel = level.label;
|
|
18004
18125
|
this.emitter.emit("tradeLevelEditOpen", {
|
|
18005
18126
|
label: level.label,
|
|
18006
18127
|
type: level.type,
|
|
@@ -18026,6 +18147,35 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18026
18147
|
return;
|
|
18027
18148
|
}
|
|
18028
18149
|
}
|
|
18150
|
+
if (this.tradeDragHandler && !this.drawingManager.getActiveTool()) {
|
|
18151
|
+
const hoverHit = hitTestTradeLevelHover(x, y, this.tradeHoverAreas);
|
|
18152
|
+
if (hoverHit) {
|
|
18153
|
+
e.preventDefault();
|
|
18154
|
+
const level = this.levels.find((l) => l.label === hoverHit.label);
|
|
18155
|
+
if (level) {
|
|
18156
|
+
const hasPending = this.pendingLevelDrags.has(hoverHit.label);
|
|
18157
|
+
if (this.selectedTradeLabel === hoverHit.label && !hasPending) {
|
|
18158
|
+
this.selectedTradeLabel = null;
|
|
18159
|
+
} else {
|
|
18160
|
+
this.selectedTradeLabel = hoverHit.label;
|
|
18161
|
+
}
|
|
18162
|
+
const { slPrice: _slH, tpPrice: _tpH } = this._resolveBracketPrices(level);
|
|
18163
|
+
this._editOpenLabel = level.label;
|
|
18164
|
+
this.renderTradeLayer();
|
|
18165
|
+
this.emitter.emit("tradeLevelEditOpen", {
|
|
18166
|
+
label: level.label,
|
|
18167
|
+
type: level.type,
|
|
18168
|
+
data: level.data,
|
|
18169
|
+
price: level.price,
|
|
18170
|
+
side: level.side,
|
|
18171
|
+
stopLossPrice: _slH,
|
|
18172
|
+
takeProfitPrice: _tpH,
|
|
18173
|
+
isFullscreen: this.isFullscreen
|
|
18174
|
+
});
|
|
18175
|
+
return;
|
|
18176
|
+
}
|
|
18177
|
+
}
|
|
18178
|
+
}
|
|
18029
18179
|
this.drawingManager.selectDrawing(null);
|
|
18030
18180
|
this.hideMobileDrawingFlyout();
|
|
18031
18181
|
if (this.expandedClusters.size > 0) {
|
|
@@ -18302,27 +18452,6 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18302
18452
|
this._handleTradeLevelClose(closedLevel);
|
|
18303
18453
|
return;
|
|
18304
18454
|
}
|
|
18305
|
-
if (this.tradeDragHandler && !this.drawingManager.getActiveTool()) {
|
|
18306
|
-
const dragHit = hitTestTradeLevelDrag(x, y, this.tradeLevelDragAreas);
|
|
18307
|
-
if (dragHit) {
|
|
18308
|
-
const level = this.levels.find((l) => l.label === dragHit.label);
|
|
18309
|
-
if (level) {
|
|
18310
|
-
if (!(this.hideLevelConfirmCancel && !dragHit.isBracket)) {
|
|
18311
|
-
this._startTradeDragFromHit(dragHit, level, y);
|
|
18312
|
-
this.drawCanvas.style.cursor = "ns-resize";
|
|
18313
|
-
return;
|
|
18314
|
-
}
|
|
18315
|
-
}
|
|
18316
|
-
}
|
|
18317
|
-
}
|
|
18318
|
-
if (this.tradeDragHandler && !this.drawingManager.getActiveTool()) {
|
|
18319
|
-
const addHit = hitTestTradeLevelAddBracket(x, y, this.tradeAddBracketAreas);
|
|
18320
|
-
if (addHit) {
|
|
18321
|
-
this._startAddBracketFromHit(addHit, y);
|
|
18322
|
-
this.drawCanvas.style.cursor = "ns-resize";
|
|
18323
|
-
return;
|
|
18324
|
-
}
|
|
18325
|
-
}
|
|
18326
18455
|
if (!this.drawingManager.getActiveTool()) {
|
|
18327
18456
|
const editHit = hitTestTradeLevelEdit(x, y, this.tradeLevelEditAreas);
|
|
18328
18457
|
if (editHit) {
|
|
@@ -18331,6 +18460,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18331
18460
|
this.selectedTradeLabel = level.label;
|
|
18332
18461
|
this.renderTradeLayer();
|
|
18333
18462
|
const { slPrice: _sl2, tpPrice: _tp2 } = this._resolveBracketPrices(level);
|
|
18463
|
+
this._editOpenLabel = level.label;
|
|
18334
18464
|
this.emitter.emit("tradeLevelEditOpen", {
|
|
18335
18465
|
label: level.label,
|
|
18336
18466
|
type: level.type,
|
|
@@ -18345,6 +18475,27 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18345
18475
|
return;
|
|
18346
18476
|
}
|
|
18347
18477
|
}
|
|
18478
|
+
if (this.tradeDragHandler && !this.drawingManager.getActiveTool()) {
|
|
18479
|
+
const addHit = hitTestTradeLevelAddBracket(x, y, this.tradeAddBracketAreas);
|
|
18480
|
+
if (addHit) {
|
|
18481
|
+
this._startAddBracketFromHit(addHit, y);
|
|
18482
|
+
this.drawCanvas.style.cursor = "ns-resize";
|
|
18483
|
+
return;
|
|
18484
|
+
}
|
|
18485
|
+
}
|
|
18486
|
+
if (this.tradeDragHandler && !this.drawingManager.getActiveTool()) {
|
|
18487
|
+
const dragHit = hitTestTradeLevelDrag(x, y, this.tradeLevelDragAreas);
|
|
18488
|
+
if (dragHit) {
|
|
18489
|
+
const level = this.levels.find((l) => l.label === dragHit.label);
|
|
18490
|
+
if (level) {
|
|
18491
|
+
if (!(this.hideLevelConfirmCancel && !dragHit.isBracket)) {
|
|
18492
|
+
this._startTradeDragFromHit(dragHit, level, y);
|
|
18493
|
+
this.drawCanvas.style.cursor = "ns-resize";
|
|
18494
|
+
return;
|
|
18495
|
+
}
|
|
18496
|
+
}
|
|
18497
|
+
}
|
|
18498
|
+
}
|
|
18348
18499
|
const hoverHit = hitTestTradeLevelHover(x, y, this.tradeHoverAreas);
|
|
18349
18500
|
if (hoverHit) {
|
|
18350
18501
|
const hasPending = this.pendingLevelDrags.has(hoverHit.label);
|
|
@@ -18358,6 +18509,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18358
18509
|
const level = this.levels.find((l) => l.label === hoverHit.label);
|
|
18359
18510
|
if (level) {
|
|
18360
18511
|
const { slPrice: _sl3, tpPrice: _tp3 } = this._resolveBracketPrices(level);
|
|
18512
|
+
this._editOpenLabel = level.label;
|
|
18361
18513
|
this.emitter.emit("tradeLevelEditOpen", {
|
|
18362
18514
|
label: level.label,
|
|
18363
18515
|
type: level.type,
|
|
@@ -18372,9 +18524,27 @@ var _ChartEngine = class _ChartEngine {
|
|
|
18372
18524
|
}
|
|
18373
18525
|
return;
|
|
18374
18526
|
} else if (this.selectedTradeLabel !== null) {
|
|
18375
|
-
if (!this.hideLevelConfirmCancel
|
|
18376
|
-
this.selectedTradeLabel
|
|
18377
|
-
this.
|
|
18527
|
+
if (!this.hideLevelConfirmCancel) {
|
|
18528
|
+
const lbl = this.selectedTradeLabel;
|
|
18529
|
+
const pending = this.pendingLevelDrags.get(lbl);
|
|
18530
|
+
const isEditFormOpen = this._editOpenLabel === lbl;
|
|
18531
|
+
if (!pending || pending.length === 0) {
|
|
18532
|
+
if (isEditFormOpen) {
|
|
18533
|
+
const lvl = this.levels.find((l) => l.label === lbl);
|
|
18534
|
+
this._editOpenLabel = null;
|
|
18535
|
+
if (lvl) {
|
|
18536
|
+
this.emitter.emit("tradeLevelEditCancelled", {
|
|
18537
|
+
label: lbl,
|
|
18538
|
+
type: lvl.type,
|
|
18539
|
+
isFullscreen: this.isFullscreen
|
|
18540
|
+
});
|
|
18541
|
+
}
|
|
18542
|
+
}
|
|
18543
|
+
this.selectedTradeLabel = null;
|
|
18544
|
+
this.renderTradeLayer();
|
|
18545
|
+
} else if (pending.every((c) => c.isNew)) {
|
|
18546
|
+
this.revertPendingChanges(lbl);
|
|
18547
|
+
}
|
|
18378
18548
|
}
|
|
18379
18549
|
}
|
|
18380
18550
|
if (this.expandedClusters.size > 0) {
|
|
@@ -20086,9 +20256,11 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20086
20256
|
const idx = this.levels.findIndex((l) => l.label === label);
|
|
20087
20257
|
if (idx === -1) return;
|
|
20088
20258
|
const lvl = this.levels[idx];
|
|
20259
|
+
if (lvl.lots === qty) return;
|
|
20089
20260
|
if (!this.originalQtyByLabel.has(label)) {
|
|
20090
20261
|
this.originalQtyByLabel.set(label, lvl.lots);
|
|
20091
20262
|
}
|
|
20263
|
+
const previousLots = this.originalQtyByLabel.get(label) ?? lvl.lots ?? qty;
|
|
20092
20264
|
this.levels = [
|
|
20093
20265
|
...this.levels.slice(0, idx),
|
|
20094
20266
|
{ ...lvl, lots: qty },
|
|
@@ -20097,6 +20269,13 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20097
20269
|
if (label !== this.draftOrderLabel) {
|
|
20098
20270
|
this.pendingQtyChanges.set(label, qty);
|
|
20099
20271
|
}
|
|
20272
|
+
this.emitter.emit("tradeLevelQtyChange", {
|
|
20273
|
+
label,
|
|
20274
|
+
type: label === this.draftOrderLabel ? "draft" : lvl.type,
|
|
20275
|
+
newLots: qty,
|
|
20276
|
+
previousLots,
|
|
20277
|
+
isFullscreen: this.isFullscreen
|
|
20278
|
+
});
|
|
20100
20279
|
this.renderTradeLayer();
|
|
20101
20280
|
}
|
|
20102
20281
|
openDraftQtyFlyout(pill) {
|
|
@@ -20305,7 +20484,17 @@ var _ChartEngine = class _ChartEngine {
|
|
|
20305
20484
|
newLevels[i] = { ...ml, lots: pendingQty };
|
|
20306
20485
|
}
|
|
20307
20486
|
}
|
|
20308
|
-
this.
|
|
20487
|
+
const preservedDraft = this.draftOrderLabel !== null ? this.levels.find(
|
|
20488
|
+
(l) => l.type === type && l.label === this.draftOrderLabel && !newLevels.some((n) => n.label === l.label)
|
|
20489
|
+
) : void 0;
|
|
20490
|
+
this.levels = [
|
|
20491
|
+
...this.levels.filter((l) => l.type !== type),
|
|
20492
|
+
...newLevels,
|
|
20493
|
+
...preservedDraft ? [preservedDraft] : []
|
|
20494
|
+
];
|
|
20495
|
+
if (this.selectedTradeLabel !== null && !this.levels.some((l) => l.label === this.selectedTradeLabel)) {
|
|
20496
|
+
this.selectedTradeLabel = null;
|
|
20497
|
+
}
|
|
20309
20498
|
this.recomputeRenderStyle();
|
|
20310
20499
|
this.renderTradeLayer();
|
|
20311
20500
|
return this;
|
|
@@ -21362,6 +21551,7 @@ var _ChartEngine = class _ChartEngine {
|
|
|
21362
21551
|
if (!changes) return;
|
|
21363
21552
|
const level = this.levels.find((l) => l.label === label);
|
|
21364
21553
|
if (!level) return;
|
|
21554
|
+
if (this._editOpenLabel === label) this._editOpenLabel = null;
|
|
21365
21555
|
if (label === this.draftOrderLabel) {
|
|
21366
21556
|
const mainEntry = changes.find((c) => c.field === "main");
|
|
21367
21557
|
const draft = level;
|
|
@@ -21455,6 +21645,15 @@ var _ChartEngine = class _ChartEngine {
|
|
|
21455
21645
|
}
|
|
21456
21646
|
this.originalQtyByLabel.delete(lbl);
|
|
21457
21647
|
}
|
|
21648
|
+
const revertedLvl = this.levels.find((l) => l.label === lbl);
|
|
21649
|
+
if (revertedLvl) {
|
|
21650
|
+
if (this._editOpenLabel === lbl) this._editOpenLabel = null;
|
|
21651
|
+
this.emitter.emit("tradeLevelEditCancelled", {
|
|
21652
|
+
label: lbl,
|
|
21653
|
+
type: revertedLvl.type,
|
|
21654
|
+
isFullscreen: this.isFullscreen
|
|
21655
|
+
});
|
|
21656
|
+
}
|
|
21458
21657
|
}
|
|
21459
21658
|
this.selectedTradeLabel = null;
|
|
21460
21659
|
this.draftBracketPnl = {};
|
|
@@ -22135,9 +22334,31 @@ var _ChartEngine = class _ChartEngine {
|
|
|
22135
22334
|
return;
|
|
22136
22335
|
}
|
|
22137
22336
|
const isDraftOrder = closedLevel.label === this.draftOrderLabel;
|
|
22337
|
+
const pendingBeforeRevert = this.pendingLevelDrags.get(closedLevel.label);
|
|
22338
|
+
const isEditFormOpen = this._editOpenLabel === closedLevel.label;
|
|
22339
|
+
const onlyIsNewPending = !!pendingBeforeRevert && pendingBeforeRevert.length > 0 && pendingBeforeRevert.every((c) => c.isNew);
|
|
22340
|
+
const noPending = !pendingBeforeRevert || pendingBeforeRevert.length === 0;
|
|
22341
|
+
const isCancelUnsavedEdit = !isDraftOrder && isEditFormOpen && (onlyIsNewPending || noPending);
|
|
22138
22342
|
this.revertPendingChanges(closedLevel.label);
|
|
22139
22343
|
this.pendingLevelDrags.delete(closedLevel.label);
|
|
22140
22344
|
if (isDraftOrder) return;
|
|
22345
|
+
if (isCancelUnsavedEdit) {
|
|
22346
|
+
if (noPending) {
|
|
22347
|
+
const lvl = this.levels.find((l) => l.label === closedLevel.label);
|
|
22348
|
+
this._editOpenLabel = null;
|
|
22349
|
+
this.selectedTradeLabel = null;
|
|
22350
|
+
this.renderTradeLayer();
|
|
22351
|
+
if (lvl) {
|
|
22352
|
+
this.emitter.emit("tradeLevelEditCancelled", {
|
|
22353
|
+
label: closedLevel.label,
|
|
22354
|
+
type: lvl.type,
|
|
22355
|
+
isFullscreen: this.isFullscreen
|
|
22356
|
+
});
|
|
22357
|
+
}
|
|
22358
|
+
}
|
|
22359
|
+
return;
|
|
22360
|
+
}
|
|
22361
|
+
if (this._editOpenLabel === closedLevel.label) this._editOpenLabel = null;
|
|
22141
22362
|
const originalLevel = this.levels.find((l) => l.label === closedLevel.label);
|
|
22142
22363
|
const isEntryOrder = originalLevel?.type === "position" && !!originalLevel.entryPriceEditable;
|
|
22143
22364
|
const closeAction = closedLevel.type === "position" && !isEntryOrder ? "CLOSED" : "CANCELLED";
|