dsh-hyperliquid 0.4.1 → 0.4.3
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/CHANGELOG.md +12 -0
- package/client.js +90 -29
- package/lib/chart-drawings.d.ts +4 -1
- package/lib/chart-drawings.js +38 -1
- package/lib/remote.js +1 -1
- package/lib/tools.js +6 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.3 — 2026-10-17
|
|
4
|
+
|
|
5
|
+
- 趋势线可分别向左、向右延长(浮动工具条两个开关,agent 工具 `extendLeft` / `extendRight`);价格提醒按延长后的线判断,延长的线不参与屏幕外剔除。
|
|
6
|
+
- 「点击没反应」根因补充验证:0.1.0 两次点击间隔 300ms、450ms 同样漏画 20/20,550ms 0/20,分界与图表库 500ms 双击窗口一致;当前版本同样间隔 0 漏画。
|
|
7
|
+
|
|
8
|
+
## 0.4.2 — 2026-10-17
|
|
9
|
+
|
|
10
|
+
- 画线对象属性对齐 TradingView:按周期显示(仅本周期,水平线的轴价格标签同样隐藏)、文本与标注气泡字号(10–32px)与粗体 / 斜体、矩形 / 圆 / 三角 / 通道 / 斐波那契 / 区间等的背景填充浓淡;浮动工具条、agent 工具参数、撤销与复制都支持。限定周期的画线在其他周期不绘制但保留在画线列表。
|
|
11
|
+
- 浮动工具条加入「隐藏」,并固定为一行(窄图表上原先会折成三行、挡住图上方的画线)。
|
|
12
|
+
- 新增 `docs/tradingview-drawing-checklist.md` 逐项对照(通用交互、对象属性、工具分组)。
|
|
13
|
+
- 修正「点击没反应」的根因说明:lightweight-charts 在 500ms 双击窗口内不为第二次点击触发单击回调;原生 click 路径已修复(0.1.0 对照:间隔 100ms 漏画 20/20、700ms 0/20)。
|
|
14
|
+
|
|
3
15
|
## 0.4.1 — 2026-10-17
|
|
4
16
|
|
|
5
17
|
- 屏幕外剔除:完全位于可视区之外的有限范围画线不再渲染(会延伸到边缘、超出锚点,或带标签 / 提醒 / 同步徽章的画线不剔除;选中的画线不剔除)。
|
package/client.js
CHANGED
|
@@ -691,10 +691,10 @@ body:has(.dhl-divider.dragging) * { cursor: col-resize !important; }
|
|
|
691
691
|
/* Drawings: projected SVG over the price pane. */
|
|
692
692
|
.dhl-drawing line, .dhl-drawing polygon { stroke: currentColor; stroke-width: var(--dhl-line-width, 1.8); stroke-dasharray: var(--dhl-dash, none); fill: none; vector-effect: non-scaling-stroke; }
|
|
693
693
|
.dhl-drawing.role-support line, .dhl-drawing.role-resistance line, .dhl-drawing.role-neutral line { stroke-dasharray: var(--dhl-dash, 6 4); }
|
|
694
|
-
.dhl-drawing .dhl-drawing-fill { fill: currentColor; fill-opacity: .08; stroke: currentColor; stroke-width: 1.2; }
|
|
694
|
+
.dhl-drawing .dhl-drawing-fill { fill: currentColor; fill-opacity: var(--dhl-fill-opacity, .08); stroke: currentColor; stroke-width: 1.2; }
|
|
695
695
|
.dhl-drawing .dhl-drawing-mid { stroke-dasharray: 4 4; stroke-width: 1; opacity: .7; }
|
|
696
696
|
.dhl-drawing-text { fill: currentColor; stroke: none; font-size: 10.5px; font-variant-numeric: tabular-nums; paint-order: stroke; }
|
|
697
|
-
.dhl-drawing .dhl-fib-band { stroke: none; fill-opacity: .07; }
|
|
697
|
+
.dhl-drawing .dhl-fib-band { stroke: none; fill-opacity: var(--dhl-fill-opacity, .07); }
|
|
698
698
|
.dhl-drawing .dhl-fib-band.band-0 { fill: #ef6f6c; } .dhl-drawing .dhl-fib-band.band-1 { fill: #e2a943; } .dhl-drawing .dhl-fib-band.band-2 { fill: #3b9c6d; }
|
|
699
699
|
.dhl-drawing .dhl-fib-key { stroke-width: 2.2; }
|
|
700
700
|
.dhl-measure.up { color: var(--dsw-alias-state-success-primary); } .dhl-measure.down { color: var(--dsw-alias-state-error-primary); }
|
|
@@ -716,14 +716,14 @@ body:has(.dhl-divider.dragging) * { cursor: col-resize !important; }
|
|
|
716
716
|
.dhl-drawing.locked .dhl-chart-anchor { cursor: default; }
|
|
717
717
|
.dhl-chart-anchor:hover { fill: currentColor; }
|
|
718
718
|
.dhl-text-box { fill: var(--dsw-alias-bg-layer-1); fill-opacity: .92; stroke: currentColor; stroke-width: 1; }
|
|
719
|
-
.dhl-drawing .dhl-text-value { fill: var(--dsw-alias-label-primary); font-size: 12px; }
|
|
719
|
+
.dhl-drawing .dhl-text-value { fill: var(--dsw-alias-label-primary); font-size: var(--dhl-font-size, 12px); font-weight: var(--dhl-font-weight, 400); font-style: var(--dhl-font-style, normal); }
|
|
720
720
|
.dhl-position .dhl-position-stop { fill: var(--dsw-alias-state-error-primary); fill-opacity: .16; stroke: none; }
|
|
721
721
|
.dhl-position .dhl-position-target { fill: var(--dsw-alias-state-success-primary); fill-opacity: .16; stroke: none; }
|
|
722
722
|
.dhl-drawing .dhl-position .dhl-position-entry { stroke: var(--dsw-alias-label-secondary); stroke-width: 1.5; stroke-dasharray: none; }
|
|
723
723
|
.dhl-drawing .dhl-position-text { fill: var(--dsw-alias-label-primary); font-size: 11px; font-weight: 600; }
|
|
724
724
|
|
|
725
725
|
/* Floating style toolbar for the selected drawing. */
|
|
726
|
-
.dhl-draw-toolbar { position: absolute; z-index: 6; top: 30px; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 6px; max-width: calc(100% - 80px); flex-wrap:
|
|
726
|
+
.dhl-draw-toolbar { position: absolute; z-index: 6; top: 30px; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 6px; max-width: calc(100% - 80px); flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; justify-content: flex-start; padding: 5px 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-1); box-shadow: 0 8px 24px color-mix(in srgb, var(--dsw-alias-bg-base) 45%, transparent); font-size: 12px; }
|
|
727
727
|
.dhl-draw-toolbar-name { color: var(--dsw-alias-label-secondary); padding: 0 4px; white-space: nowrap; }
|
|
728
728
|
.dhl-draw-toolbar-group { display: inline-flex; align-items: center; gap: 2px; padding-left: 6px; border-left: 1px solid var(--dsw-alias-border-l1); }
|
|
729
729
|
.dhl-draw-toolbar button { display: inline-grid; place-items: center; min-width: 26px; height: 26px; padding: 0 4px; border: 0; border-radius: 5px; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; font: inherit; }
|
|
@@ -770,7 +770,7 @@ body:has(.dhl-divider.dragging) * { cursor: col-resize !important; }
|
|
|
770
770
|
.dhl-drawing .dhl-brush { fill: none; stroke: currentColor; stroke-width: var(--dhl-line-width, 2); stroke-dasharray: var(--dhl-dash, none); stroke-linecap: round; stroke-linejoin: round; }
|
|
771
771
|
.dhl-drawing-hit .dhl-brush { pointer-events: stroke; }
|
|
772
772
|
.dhl-drawing .dhl-arrow-head, .dhl-drawing .dhl-callout-dot { fill: currentColor; stroke: none; }
|
|
773
|
-
.dhl-drawing circle.dhl-drawing-fill { fill: currentColor; fill-opacity: .08; stroke: currentColor; stroke-width: var(--dhl-line-width, 1.5); stroke-dasharray: var(--dhl-dash, none); }
|
|
773
|
+
.dhl-drawing circle.dhl-drawing-fill { fill: currentColor; fill-opacity: var(--dhl-fill-opacity, .08); stroke: currentColor; stroke-width: var(--dhl-line-width, 1.5); stroke-dasharray: var(--dhl-dash, none); }
|
|
774
774
|
.dhl-drawing polyline.dhl-drawing-mid { fill: none; stroke: currentColor; }
|
|
775
775
|
|
|
776
776
|
/* Drawings object list (under the chart). */
|
|
@@ -946,6 +946,9 @@ body:has(.dhl-divider.dragging) * { cursor: col-resize !important; }
|
|
|
946
946
|
.dhl-limits-hint { display: block; margin-top: 2px; color: var(--dsw-alias-label-secondary); font-size: 12px; }
|
|
947
947
|
.dhl-limits-scroll .dhl-risk-error { margin: 10px 0 0; }
|
|
948
948
|
.dhl-wallet-form ~ .dhl-limits-facts { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); }
|
|
949
|
+
|
|
950
|
+
/* One row like TradingView's floating toolbar: a wrapped toolbar covered drawings near the top of a narrow chart. */
|
|
951
|
+
.dhl-draw-toolbar > * { flex: none; }
|
|
949
952
|
`;
|
|
950
953
|
|
|
951
954
|
// src/client/locale.ts
|
|
@@ -17702,6 +17705,8 @@ function isSingleEmoji(value) {
|
|
|
17702
17705
|
if (typeof value !== "string" || !value || value.length > 32 || !/\p{Extended_Pictographic}|\p{Regional_Indicator}/u.test(value)) return false;
|
|
17703
17706
|
return [...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(value)].length === 1;
|
|
17704
17707
|
}
|
|
17708
|
+
var FONT_KINDS = ["text", "callout"];
|
|
17709
|
+
var FILL_KINDS = ["rect", "circle", "triangle", "channel", "fib", "regression", "gannbox", "pricerange", "daterange", "measure"];
|
|
17705
17710
|
var FIB_LEVEL_KINDS = ["fib", "fibext", "fibchannel"];
|
|
17706
17711
|
function volumeProfile(bars, { rows = 48, valueArea = 0.7 } = {}) {
|
|
17707
17712
|
const valid = bars.filter((bar) => [bar.open, bar.high, bar.low, bar.close, bar.volume].every(Number.isFinite) && bar.high >= bar.low && bar.volume >= 0);
|
|
@@ -17903,9 +17908,9 @@ function drawingLabel(kind, points2, price) {
|
|
|
17903
17908
|
const when = new Date(first.time).toLocaleString(tr("zh-CN", "en-US"), { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false });
|
|
17904
17909
|
return `${name} ${when}`;
|
|
17905
17910
|
}
|
|
17906
|
-
function clipLine(a, b, w, h, from) {
|
|
17911
|
+
function clipLine(a, b, w, h, from, to = Infinity) {
|
|
17907
17912
|
const dx = b.x - a.x, dy = b.y - a.y;
|
|
17908
|
-
let t0 = from, t1 =
|
|
17913
|
+
let t0 = from, t1 = to;
|
|
17909
17914
|
for (const [p, q] of [[-dx, a.x], [dx, w - a.x], [-dy, a.y], [dy, h - a.y]]) {
|
|
17910
17915
|
if (p === 0) {
|
|
17911
17916
|
if (q < 0) return null;
|
|
@@ -17987,7 +17992,8 @@ function guide(a, b, value, key3, text) {
|
|
|
17987
17992
|
}
|
|
17988
17993
|
var BADGE_PATHS = { alert: "M6 16V11a6 6 0 0 1 12 0v5l2 2H4zM10 20a2 2 0 0 0 4 0", shared: "M4 9h13l-3-3M20 15H7l3 3" };
|
|
17989
17994
|
var DASH = { solid: void 0, dashed: "7 5", dotted: "1.5 4" };
|
|
17990
|
-
var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, label, text: textValue, className, view, anchors = false, interactive = false, color, lineWidth, lineStyle, id, alert, shared, rows, valueArea, levels }) {
|
|
17995
|
+
var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, label, text: textValue, className, view, anchors = false, interactive = false, color, lineWidth, lineStyle, id, alert, shared, rows, valueArea, levels, fontSize, bold, italic, fillOpacity, extendLeft, extendRight }) {
|
|
17996
|
+
const k = (fontSize ?? 12) / 12;
|
|
17991
17997
|
const badges = [...alert ? ["alert"] : [], ...shared ? ["shared"] : []];
|
|
17992
17998
|
const { width: w, height: h } = view;
|
|
17993
17999
|
const px = points2.map((point) => view.project(point.time, point.price));
|
|
@@ -18008,9 +18014,17 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
|
|
|
18008
18014
|
body = /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, x2: p[0].x, y1: "0", y2: h });
|
|
18009
18015
|
handles = [{ x: p[0].x, y: Math.min(h - 12, Math.max(12, p[0].y)) }];
|
|
18010
18016
|
break;
|
|
18011
|
-
case "trend":
|
|
18012
|
-
|
|
18017
|
+
case "trend": {
|
|
18018
|
+
if (!p[1]) break;
|
|
18019
|
+
if (!extendLeft && !extendRight) {
|
|
18020
|
+
body = /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y });
|
|
18021
|
+
break;
|
|
18022
|
+
}
|
|
18023
|
+
const [l, r] = p[0].x <= p[1].x ? [p[0], p[1]] : [p[1], p[0]];
|
|
18024
|
+
const seg = clipLine(l, r, w, h, extendLeft ? -Infinity : 0, extendRight ? Infinity : 1);
|
|
18025
|
+
body = seg ? /* @__PURE__ */ React7.createElement("line", { x1: seg[0].x, y1: seg[0].y, x2: seg[1].x, y2: seg[1].y }) : null;
|
|
18013
18026
|
break;
|
|
18027
|
+
}
|
|
18014
18028
|
case "ray":
|
|
18015
18029
|
case "extended": {
|
|
18016
18030
|
if (!p[1]) break;
|
|
@@ -18025,8 +18039,8 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
|
|
|
18025
18039
|
body = base;
|
|
18026
18040
|
break;
|
|
18027
18041
|
}
|
|
18028
|
-
const
|
|
18029
|
-
const offset = points2[2].price - (points2[0].price + (points2[1].price - points2[0].price) *
|
|
18042
|
+
const k2 = points2[1].time === points2[0].time ? 0 : (points2[2].time - points2[0].time) / (points2[1].time - points2[0].time);
|
|
18043
|
+
const offset = points2[2].price - (points2[0].price + (points2[1].price - points2[0].price) * k2);
|
|
18030
18044
|
const a2 = view.project(points2[0].time, points2[0].price + offset), b2 = view.project(points2[1].time, points2[1].price + offset);
|
|
18031
18045
|
const am = view.project(points2[0].time, points2[0].price + offset / 2), bm = view.project(points2[1].time, points2[1].price + offset / 2);
|
|
18032
18046
|
if (!a2 || !b2 || !am || !bm) {
|
|
@@ -18110,8 +18124,8 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
|
|
|
18110
18124
|
case "callout": {
|
|
18111
18125
|
if (!p[1]) break;
|
|
18112
18126
|
const value = textValue || label || tr("\u6807\u6CE8", "Note");
|
|
18113
|
-
const width = Math.min(280, 16 + [...value].reduce((sum, ch) => sum + (ch.charCodeAt(0) > 255 ? 12 : 6.6), 0));
|
|
18114
|
-
body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y +
|
|
18127
|
+
const width = Math.min(280, 16 + [...value].reduce((sum, ch) => sum + (ch.charCodeAt(0) > 255 ? 12 : 6.6), 0)) * k;
|
|
18128
|
+
body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y + 13 * k }), /* @__PURE__ */ React7.createElement("circle", { className: "dhl-callout-dot", cx: p[0].x, cy: p[0].y, r: "3" }), /* @__PURE__ */ React7.createElement("rect", { className: "dhl-text-box", x: p[1].x, y: p[1].y, width, height: 26 * k, rx: "6" }), text(p[1].x + 8 * k, p[1].y + 17 * k, value, "start", "dhl-drawing-text dhl-text-value"));
|
|
18115
18129
|
break;
|
|
18116
18130
|
}
|
|
18117
18131
|
case "fibext": {
|
|
@@ -18132,8 +18146,8 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
|
|
|
18132
18146
|
body = /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y });
|
|
18133
18147
|
break;
|
|
18134
18148
|
}
|
|
18135
|
-
const
|
|
18136
|
-
const offset = points2[2].price - (points2[0].price + (points2[1].price - points2[0].price) *
|
|
18149
|
+
const k2 = (points2[2].time - points2[0].time) / (points2[1].time - points2[0].time || 1);
|
|
18150
|
+
const offset = points2[2].price - (points2[0].price + (points2[1].price - points2[0].price) * k2);
|
|
18137
18151
|
body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, (levels ?? FIB_LEVELS).map((level) => {
|
|
18138
18152
|
const a2 = view.project(points2[0].time, points2[0].price + offset * level), b2 = view.project(points2[1].time, points2[1].price + offset * level);
|
|
18139
18153
|
const seg = a2 && b2 ? clipLine(a2, b2, w, h, 0) : null;
|
|
@@ -18271,8 +18285,8 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
|
|
|
18271
18285
|
case "text": {
|
|
18272
18286
|
const value = textValue || label || tr("\u6587\u672C", "Text");
|
|
18273
18287
|
const lines = wrapText(value, 300);
|
|
18274
|
-
const width = 14 + Math.max(...lines.map(textWidth));
|
|
18275
|
-
body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("rect", { className: "dhl-text-box", x: p[0].x, y: p[0].y - 20, width, height: 8 + lines.length * 16, rx: "4" }), lines.map((line, index) => /* @__PURE__ */ React7.createElement(React7.Fragment, { key: index }, text(p[0].x + 7, p[0].y -
|
|
18288
|
+
const width = (14 + Math.max(...lines.map(textWidth))) * k;
|
|
18289
|
+
body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("rect", { className: "dhl-text-box", x: p[0].x, y: p[0].y - 20 * k, width, height: (8 + lines.length * 16) * k, rx: "4" }), lines.map((line, index) => /* @__PURE__ */ React7.createElement(React7.Fragment, { key: index }, text(p[0].x + 7 * k, p[0].y + (-4 + index * 16) * k, line, "start", "dhl-drawing-text dhl-text-value"))));
|
|
18276
18290
|
break;
|
|
18277
18291
|
}
|
|
18278
18292
|
case "measure": {
|
|
@@ -18302,7 +18316,15 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
|
|
|
18302
18316
|
}
|
|
18303
18317
|
}
|
|
18304
18318
|
if (body == null) return null;
|
|
18305
|
-
const style = {
|
|
18319
|
+
const style = {
|
|
18320
|
+
color,
|
|
18321
|
+
"--dhl-line-width": lineWidth ? String(lineWidth) : void 0,
|
|
18322
|
+
"--dhl-dash": lineStyle ? DASH[lineStyle] ?? "none" : void 0,
|
|
18323
|
+
"--dhl-fill-opacity": fillOpacity != null ? String(fillOpacity) : void 0,
|
|
18324
|
+
"--dhl-font-size": fontSize ? `${fontSize}px` : void 0,
|
|
18325
|
+
"--dhl-font-weight": bold ? "700" : void 0,
|
|
18326
|
+
"--dhl-font-style": italic ? "italic" : void 0
|
|
18327
|
+
};
|
|
18306
18328
|
return /* @__PURE__ */ React7.createElement("g", { className, style, "data-mark-id": id }, body, label && !isTextKind(kind) && kind !== "icon" && p[0] ? text(Math.min(Math.max(p[0].x + 6, 4), w - 120), p[0].y - 6, label) : null, badges?.length && p[0] ? badges.map((badge, index) => /* @__PURE__ */ React7.createElement("svg", { key: badge, className: "dhl-drawing-badge", x: Math.min(Math.max(p[0].x + 6, 4), w - 40) + index * 16, y: p[0].y + 5, width: "13", height: "13", viewBox: "0 0 24 24" }, /* @__PURE__ */ React7.createElement("path", { d: BADGE_PATHS[badge], fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round" }))) : null, interactive ? /* @__PURE__ */ React7.createElement("g", { className: "dhl-drawing-hit", "data-mark-id": id, "data-hit": "body" }, body) : null, anchors ? handles.map((point, index) => /* @__PURE__ */ React7.createElement("circle", { key: index, className: "dhl-chart-anchor", "data-mark-id": id, "data-anchor": index, cx: point.x, cy: point.y, r: "5" })) : null);
|
|
18307
18329
|
});
|
|
18308
18330
|
|
|
@@ -18345,6 +18367,8 @@ function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone,
|
|
|
18345
18367
|
setText(mark.label);
|
|
18346
18368
|
}, [mark.id, mark.label]);
|
|
18347
18369
|
const locked = Boolean(mark.locked) || globallyLocked;
|
|
18370
|
+
const interval = useStore().interval;
|
|
18371
|
+
const onlyHere = Boolean(mark.visibleIntervals?.length);
|
|
18348
18372
|
const defaultLevels = (mark.levels ?? (mark.kind === "fibext" ? FIB_EXT_LEVELS : FIB_LEVELS)).join(", ");
|
|
18349
18373
|
const [levelsText, setLevelsText] = React8.useState(defaultLevels);
|
|
18350
18374
|
React8.useEffect(() => {
|
|
@@ -18419,7 +18443,7 @@ function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone,
|
|
|
18419
18443
|
onClick: () => onEdit({ lineStyle: value })
|
|
18420
18444
|
},
|
|
18421
18445
|
/* @__PURE__ */ React8.createElement("svg", { viewBox: "0 0 18 18", width: "16", height: "16", "aria-hidden": "true" }, /* @__PURE__ */ React8.createElement("line", { x1: "2", x2: "16", y1: "9", y2: "9", stroke: "currentColor", strokeWidth: "2", strokeDasharray: value === "dashed" ? "4 3" : value === "dotted" ? "1 3" : void 0, strokeLinecap: "round" }))
|
|
18422
|
-
))) : null, FIB_LEVEL_KINDS.includes(mark.kind) ? /* @__PURE__ */ React8.createElement("span", { className: "dhl-draw-toolbar-group", role: "group", "aria-label": tr("\u6590\u6CE2\u90A3\u5951\u5C42\u7EA7", "Fibonacci levels") }, /* @__PURE__ */ React8.createElement(
|
|
18446
|
+
))) : null, mark.kind === "trend" ? /* @__PURE__ */ React8.createElement("span", { className: "dhl-draw-toolbar-group", role: "group", "aria-label": tr("\u5EF6\u957F", "Extend") }, /* @__PURE__ */ React8.createElement("button", { type: "button", className: mark.extendLeft ? "on" : "", "aria-pressed": Boolean(mark.extendLeft), disabled: locked, "aria-label": tr("\u5411\u5DE6\u5EF6\u957F", "Extend left"), title: tr("\u5411\u5DE6\u5EF6\u957F", "Extend left"), onClick: () => onEdit({ extendLeft: !mark.extendLeft }) }, /* @__PURE__ */ React8.createElement("svg", { viewBox: "0 0 24 24", width: "15", height: "15", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true" }, /* @__PURE__ */ React8.createElement("path", { d: "M3 12h13", strokeDasharray: "2 2.5" }), /* @__PURE__ */ React8.createElement("path", { d: "M14 12h7" }), /* @__PURE__ */ React8.createElement("circle", { cx: "14", cy: "12", r: "1.8" }), /* @__PURE__ */ React8.createElement("path", { d: "M6 9l-3 3 3 3" }))), /* @__PURE__ */ React8.createElement("button", { type: "button", className: mark.extendRight ? "on" : "", "aria-pressed": Boolean(mark.extendRight), disabled: locked, "aria-label": tr("\u5411\u53F3\u5EF6\u957F", "Extend right"), title: tr("\u5411\u53F3\u5EF6\u957F", "Extend right"), onClick: () => onEdit({ extendRight: !mark.extendRight }) }, /* @__PURE__ */ React8.createElement("svg", { viewBox: "0 0 24 24", width: "15", height: "15", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true" }, /* @__PURE__ */ React8.createElement("path", { d: "M3 12h7" }), /* @__PURE__ */ React8.createElement("path", { d: "M10 12h11", strokeDasharray: "2 2.5" }), /* @__PURE__ */ React8.createElement("circle", { cx: "10", cy: "12", r: "1.8" }), /* @__PURE__ */ React8.createElement("path", { d: "M18 9l3 3-3 3" })))) : null, FONT_KINDS.includes(mark.kind) || FILL_KINDS.includes(mark.kind) ? /* @__PURE__ */ React8.createElement("span", { className: "dhl-draw-toolbar-group", role: "group", "aria-label": tr("\u6587\u5B57\u4E0E\u586B\u5145", "Text and fill") }, FONT_KINDS.includes(mark.kind) ? /* @__PURE__ */ React8.createElement(Select, { "aria-label": tr("\u5B57\u53F7", "Font size"), title: tr("\u5B57\u53F7", "Font size"), value: String(mark.fontSize ?? 12), disabled: locked, onChange: (event) => onEdit({ fontSize: Number(event.target.value) }) }, [10, 12, 14, 18, 24, 32].map((size3) => /* @__PURE__ */ React8.createElement("option", { key: size3, value: size3 }, `${size3}px`))) : null, FONT_KINDS.includes(mark.kind) ? /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement("button", { type: "button", className: mark.bold ? "on" : "", "aria-pressed": Boolean(mark.bold), disabled: locked, "aria-label": tr("\u7C97\u4F53", "Bold"), title: tr("\u7C97\u4F53", "Bold"), onClick: () => onEdit({ bold: !mark.bold }) }, /* @__PURE__ */ React8.createElement("b", null, "B")), /* @__PURE__ */ React8.createElement("button", { type: "button", className: mark.italic ? "on" : "", "aria-pressed": Boolean(mark.italic), disabled: locked, "aria-label": tr("\u659C\u4F53", "Italic"), title: tr("\u659C\u4F53", "Italic"), onClick: () => onEdit({ italic: !mark.italic }) }, /* @__PURE__ */ React8.createElement("i", null, "I"))) : null, FILL_KINDS.includes(mark.kind) ? /* @__PURE__ */ React8.createElement(Select, { "aria-label": tr("\u80CC\u666F\u586B\u5145", "Background fill"), title: tr("\u80CC\u666F\u586B\u5145", "Background fill"), value: String(mark.fillOpacity ?? (mark.kind === "fib" ? 0.07 : 0.08)), disabled: locked, onChange: (event) => onEdit({ fillOpacity: Number(event.target.value) }) }, [[0, "\u65E0\u586B\u5145", "No fill"], [mark.kind === "fib" ? 0.07 : 0.08, "\u6DE1", "Light"], [0.18, "\u4E2D", "Medium"], [0.32, "\u6D53", "Strong"]].map(([value, zh, en]) => /* @__PURE__ */ React8.createElement("option", { key: String(value), value: String(value) }, tr(String(zh), String(en))))) : null) : null, FIB_LEVEL_KINDS.includes(mark.kind) ? /* @__PURE__ */ React8.createElement("span", { className: "dhl-draw-toolbar-group", role: "group", "aria-label": tr("\u6590\u6CE2\u90A3\u5951\u5C42\u7EA7", "Fibonacci levels") }, /* @__PURE__ */ React8.createElement(
|
|
18423
18447
|
"input",
|
|
18424
18448
|
{
|
|
18425
18449
|
className: "dhl-draw-toolbar-text dhl-fib-levels",
|
|
@@ -18434,7 +18458,19 @@ function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone,
|
|
|
18434
18458
|
},
|
|
18435
18459
|
onBlur: applyLevels
|
|
18436
18460
|
}
|
|
18437
|
-
)) : null, mark.kind === "vprofile" ? /* @__PURE__ */ React8.createElement("span", { className: "dhl-draw-toolbar-group", role: "group", "aria-label": tr("\u6210\u4EA4\u91CF\u5206\u5E03\u8BBE\u7F6E", "Volume profile settings") }, /* @__PURE__ */ React8.createElement(Select, { "aria-label": tr("\u4EF7\u683C\u884C\u6570", "Rows"), title: tr("\u4EF7\u683C\u884C\u6570", "Rows"), value: String(mark.rows ?? 48), disabled: locked, onChange: (event) => onEdit({ rows: Number(event.target.value) }) }, [24, 48, 96, 150].map((value) => /* @__PURE__ */ React8.createElement("option", { key: value, value }, tr(`${value} \u884C`, `${value} rows`)))), /* @__PURE__ */ React8.createElement(Select, { "aria-label": tr("\u4EF7\u503C\u533A\u6BD4\u4F8B", "Value area"), title: tr("\u4EF7\u503C\u533A\u6210\u4EA4\u91CF\u5360\u6BD4", "Value area volume"), value: String(mark.valueArea ?? 0.7), disabled: locked, onChange: (event) => onEdit({ valueArea: Number(event.target.value) }) }, [0.68, 0.7, 0.8].map((value) => /* @__PURE__ */ React8.createElement("option", { key: value, value }, `VA ${Math.round(value * 100)}%`)))) : null, /* @__PURE__ */ React8.createElement("span", { className: "dhl-draw-toolbar-group" }, /* @__PURE__ */ React8.createElement(
|
|
18461
|
+
)) : null, mark.kind === "vprofile" ? /* @__PURE__ */ React8.createElement("span", { className: "dhl-draw-toolbar-group", role: "group", "aria-label": tr("\u6210\u4EA4\u91CF\u5206\u5E03\u8BBE\u7F6E", "Volume profile settings") }, /* @__PURE__ */ React8.createElement(Select, { "aria-label": tr("\u4EF7\u683C\u884C\u6570", "Rows"), title: tr("\u4EF7\u683C\u884C\u6570", "Rows"), value: String(mark.rows ?? 48), disabled: locked, onChange: (event) => onEdit({ rows: Number(event.target.value) }) }, [24, 48, 96, 150].map((value) => /* @__PURE__ */ React8.createElement("option", { key: value, value }, tr(`${value} \u884C`, `${value} rows`)))), /* @__PURE__ */ React8.createElement(Select, { "aria-label": tr("\u4EF7\u503C\u533A\u6BD4\u4F8B", "Value area"), title: tr("\u4EF7\u503C\u533A\u6210\u4EA4\u91CF\u5360\u6BD4", "Value area volume"), value: String(mark.valueArea ?? 0.7), disabled: locked, onChange: (event) => onEdit({ valueArea: Number(event.target.value) }) }, [0.68, 0.7, 0.8].map((value) => /* @__PURE__ */ React8.createElement("option", { key: value, value }, `VA ${Math.round(value * 100)}%`)))) : null, /* @__PURE__ */ React8.createElement("span", { className: "dhl-draw-toolbar-group" }, /* @__PURE__ */ React8.createElement(
|
|
18462
|
+
"button",
|
|
18463
|
+
{
|
|
18464
|
+
type: "button",
|
|
18465
|
+
className: onlyHere ? "on" : "",
|
|
18466
|
+
"aria-pressed": onlyHere,
|
|
18467
|
+
disabled: locked,
|
|
18468
|
+
title: onlyHere ? tr(`\u4EC5\u5728 ${mark.visibleIntervals.join("\u3001")} \u663E\u793A\uFF1B\u70B9\u51FB\u6539\u4E3A\u6240\u6709\u5468\u671F`, `Shown on ${mark.visibleIntervals.join(", ")} only; click to show on all`) : tr(`\u53EA\u5728\u5F53\u524D\u5468\u671F\uFF08${interval}\uFF09\u663E\u793A`, `Show on the current interval (${interval}) only`),
|
|
18469
|
+
"aria-label": onlyHere ? tr("\u6539\u4E3A\u6240\u6709\u5468\u671F\u663E\u793A", "Show on all intervals") : tr("\u4EC5\u672C\u5468\u671F\u663E\u793A", "Show on this interval only"),
|
|
18470
|
+
onClick: () => onEdit({ visibleIntervals: onlyHere ? null : [interval] })
|
|
18471
|
+
},
|
|
18472
|
+
/* @__PURE__ */ React8.createElement("svg", { viewBox: "0 0 24 24", width: "15", height: "15", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true" }, /* @__PURE__ */ React8.createElement("rect", { x: "4", y: "5", width: "16", height: "15", rx: "2" }), /* @__PURE__ */ React8.createElement("path", { d: "M4 10h16M9 3v4M15 3v4" }))
|
|
18473
|
+
), /* @__PURE__ */ React8.createElement("button", { type: "button", disabled: locked, title: tr("\u7CBE\u786E\u7F16\u8F91\u65F6\u95F4\u3001\u4EF7\u683C\u4E0E\u540D\u79F0", "Edit exact time, price and name"), "aria-label": tr("\u7F16\u8F91\u753B\u7EBF\u5750\u6807", "Edit drawing coordinates"), onClick: onCoordinates }, /* @__PURE__ */ React8.createElement(UiIcon, { name: "settings", size: 15 })), /* @__PURE__ */ React8.createElement(
|
|
18438
18474
|
"button",
|
|
18439
18475
|
{
|
|
18440
18476
|
type: "button",
|
|
@@ -18483,7 +18519,10 @@ function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone,
|
|
|
18483
18519
|
onClick: () => onEdit({ locked: !mark.locked })
|
|
18484
18520
|
},
|
|
18485
18521
|
/* @__PURE__ */ React8.createElement("svg", { viewBox: "0 0 24 24", width: "15", height: "15", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true" }, /* @__PURE__ */ React8.createElement("rect", { x: "5", y: "11", width: "14", height: "9", rx: "2" }), /* @__PURE__ */ React8.createElement("path", { d: mark.locked ? "M8 11V8a4 4 0 0 1 8 0v3" : "M8 11V8a4 4 0 0 1 7.5-2" }))
|
|
18486
|
-
), /* @__PURE__ */ React8.createElement("button", { type: "button",
|
|
18522
|
+
), /* @__PURE__ */ React8.createElement("button", { type: "button", title: tr("\u9690\u85CF\uFF08\u5728\u753B\u7EBF\u5217\u8868\u91CC\u53EF\u91CD\u65B0\u663E\u793A\uFF09", "Hide (show it again from the drawing list)"), "aria-label": tr("\u9690\u85CF\u753B\u7EBF", "Hide drawing"), onClick: () => {
|
|
18523
|
+
onEdit({ hidden: true });
|
|
18524
|
+
onClose();
|
|
18525
|
+
} }, /* @__PURE__ */ React8.createElement("svg", { viewBox: "0 0 24 24", width: "15", height: "15", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true" }, /* @__PURE__ */ React8.createElement("path", { d: "M3 12s3.5-6 9-6 9 6 9 6-3.5 6-9 6-9-6-9-6zM4 20 20 4" }))), /* @__PURE__ */ React8.createElement("button", { type: "button", disabled: locked, title: tr("\u5220\u9664\uFF08Delete\uFF09", "Delete (Del)"), "aria-label": tr("\u5220\u9664\u753B\u7EBF", "Delete drawing"), onClick: onRemove }, /* @__PURE__ */ React8.createElement("svg", { viewBox: "0 0 24 24", width: "15", height: "15", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true" }, /* @__PURE__ */ React8.createElement("path", { d: "M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13" }))), /* @__PURE__ */ React8.createElement("button", { type: "button", title: tr("\u53D6\u6D88\u9009\u62E9\uFF08Esc\uFF09", "Deselect (Esc)"), "aria-label": tr("\u53D6\u6D88\u9009\u62E9", "Deselect"), onClick: onClose }, /* @__PURE__ */ React8.createElement(UiIcon, { name: "close", size: 14 }))));
|
|
18487
18526
|
}
|
|
18488
18527
|
|
|
18489
18528
|
// src/client/components/drawing-editor.tsx
|
|
@@ -18728,7 +18767,7 @@ function cachedProfile(candles, from, to, options = {}) {
|
|
|
18728
18767
|
if (!byRange.has(key3)) byRange.set(key3, volumeProfile(candles.filter((c) => c.t >= from && c.t <= to).map((c) => ({ open: c.o, high: c.h, low: c.l, close: c.c, volume: c.v })), options));
|
|
18729
18768
|
return byRange.get(key3);
|
|
18730
18769
|
}
|
|
18731
|
-
function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMode, onDraw, onSelect, priceScale, visibleRange, onVisibleRange, tradeEvents, onNeedOlder, olderState, focus, maLines, magnet, drawingsLocked, onEditMark, onRemoveMark, onRemoveMarks, onEditMarks, onEditEach, showReasons, onAnalyzeFill, onCloneMark, selectRequest, chartType = "candles", command = null, caption = "", indicatorSettings, showDataWindow = false, link, compact = false, readOnly = false }) {
|
|
18770
|
+
function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMode, onDraw, onSelect, priceScale, visibleRange, onVisibleRange, tradeEvents, onNeedOlder, olderState, focus, maLines, magnet, drawingsLocked, onEditMark, onRemoveMark, interval, onRemoveMarks, onEditMarks, onEditEach, showReasons, onAnalyzeFill, onCloneMark, selectRequest, chartType = "candles", command = null, caption = "", indicatorSettings, showDataWindow = false, link, compact = false, readOnly = false }) {
|
|
18732
18771
|
const boxRef = React10.useRef(null);
|
|
18733
18772
|
const clipId = React10.useRef("dhl-plot-" + Math.random().toString(36).slice(2));
|
|
18734
18773
|
const linkRef = React10.useRef(link);
|
|
@@ -19456,6 +19495,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
19456
19495
|
const styleOf = (mark) => mark.lineStyle === "dotted" ? LineStyle.Dotted : mark.lineStyle === "dashed" || !mark.lineStyle && mark.source !== "user" ? LineStyle.Dashed : LineStyle.Solid;
|
|
19457
19496
|
for (const mark of marks) {
|
|
19458
19497
|
if (mark.kind !== "level" || mark.hidden || !Number.isFinite(mark.price) || Number(mark.price) <= 0) continue;
|
|
19498
|
+
if (interval && mark.visibleIntervals?.length && !mark.visibleIntervals.includes(interval)) continue;
|
|
19459
19499
|
priceLines.current.set(mark.id, r.candles.createPriceLine({
|
|
19460
19500
|
price: Number(mark.price),
|
|
19461
19501
|
color: colorOf(mark),
|
|
@@ -19465,7 +19505,9 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
19465
19505
|
title: mark.label
|
|
19466
19506
|
}));
|
|
19467
19507
|
}
|
|
19468
|
-
|
|
19508
|
+
;
|
|
19509
|
+
window.__dhlPriceLines = () => priceLines.current.size;
|
|
19510
|
+
}, [marks, themeVersion, chartType, interval]);
|
|
19469
19511
|
const warming = candles.length < 2;
|
|
19470
19512
|
const markById = React10.useMemo(() => new Map(marks.map((mark) => [mark.id, mark])), [marks]);
|
|
19471
19513
|
const markByIdRef = React10.useRef(markById);
|
|
@@ -19510,6 +19552,17 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
19510
19552
|
React10.useEffect(() => {
|
|
19511
19553
|
if (selectedId && !markById.has(selectedId)) setSelectedId(null);
|
|
19512
19554
|
}, [markById, selectedId]);
|
|
19555
|
+
React10.useEffect(() => {
|
|
19556
|
+
const hiddenHere = (id) => {
|
|
19557
|
+
const limit = markById.get(id)?.visibleIntervals;
|
|
19558
|
+
return Boolean(interval && limit?.length && !limit.includes(interval));
|
|
19559
|
+
};
|
|
19560
|
+
if (selectedId && hiddenHere(selectedId)) setSelectedId(null);
|
|
19561
|
+
setGroup((current2) => {
|
|
19562
|
+
const next = current2.filter((id) => !hiddenHere(id));
|
|
19563
|
+
return next.length === current2.length ? current2 : next;
|
|
19564
|
+
});
|
|
19565
|
+
}, [interval, markById, selectedId]);
|
|
19513
19566
|
React10.useEffect(() => {
|
|
19514
19567
|
if (drawMode) setSelectedId(null);
|
|
19515
19568
|
}, [drawMode]);
|
|
@@ -19621,7 +19674,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
19621
19674
|
const cloneOf = React10.useCallback((mark) => {
|
|
19622
19675
|
const line = timelineRef.current;
|
|
19623
19676
|
const shifted = markPoints(mark).map((point) => ({ time: line.toTime(line.toLogical(point.time) + 5), price: point.price }));
|
|
19624
|
-
const style = Object.fromEntries(Object.entries({ color: mark.color, lineWidth: mark.lineWidth, lineStyle: mark.lineStyle, levels: mark.levels, rows: mark.rows, valueArea: mark.valueArea }).filter(([, value]) => value != null));
|
|
19677
|
+
const style = Object.fromEntries(Object.entries({ color: mark.color, lineWidth: mark.lineWidth, lineStyle: mark.lineStyle, levels: mark.levels, rows: mark.rows, valueArea: mark.valueArea, fontSize: mark.fontSize, bold: mark.bold, italic: mark.italic, extendLeft: mark.extendLeft, extendRight: mark.extendRight, fillOpacity: mark.fillOpacity, visibleIntervals: mark.visibleIntervals }).filter(([, value]) => value != null));
|
|
19625
19678
|
return { kind: mark.kind, geometry: geometryPatch(mark.kind, shifted), style, label: mark.label };
|
|
19626
19679
|
}, []);
|
|
19627
19680
|
const clipboard = React10.useRef(null);
|
|
@@ -19868,8 +19921,9 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
19868
19921
|
const live = draft?.id === mark.id ? draft.points : groupDraft?.get(mark.id) ?? points2;
|
|
19869
19922
|
const shown = mark.kind === "level" ? [{ time: lastTime, price: live[0].price }] : live;
|
|
19870
19923
|
const selected = selectedId === mark.id || group.includes(mark.id);
|
|
19924
|
+
if (interval && mark.visibleIntervals?.length && !mark.visibleIntervals.includes(interval)) return null;
|
|
19871
19925
|
const clampedDecoration = !(mark.source === "user" || mark.kind === "level") || Boolean(mark.alert) || Boolean(mark.shared);
|
|
19872
|
-
if (!selected && !clampedDecoration && FINITE_KINDS.has(mark.kind) && offscreen(shown, view)) return null;
|
|
19926
|
+
if (!selected && !clampedDecoration && !mark.extendLeft && !mark.extendRight && FINITE_KINDS.has(mark.kind) && offscreen(shown, view)) return null;
|
|
19873
19927
|
return /* @__PURE__ */ React10.createElement(
|
|
19874
19928
|
DrawingShape,
|
|
19875
19929
|
{
|
|
@@ -19891,7 +19945,13 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
19891
19945
|
shared: mark.shared,
|
|
19892
19946
|
rows: mark.rows,
|
|
19893
19947
|
valueArea: mark.valueArea,
|
|
19894
|
-
levels: mark.levels
|
|
19948
|
+
levels: mark.levels,
|
|
19949
|
+
fontSize: mark.fontSize,
|
|
19950
|
+
bold: mark.bold,
|
|
19951
|
+
italic: mark.italic,
|
|
19952
|
+
fillOpacity: mark.fillOpacity,
|
|
19953
|
+
extendLeft: mark.extendLeft,
|
|
19954
|
+
extendRight: mark.extendRight
|
|
19895
19955
|
}
|
|
19896
19956
|
);
|
|
19897
19957
|
}), /* @__PURE__ */ React10.createElement("g", { className: "dhl-fills" }, fills.map((event) => {
|
|
@@ -19967,7 +20027,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
|
|
|
19967
20027
|
var React11 = __toESM(require("react"), 1);
|
|
19968
20028
|
var HISTORY_LIMIT = 100;
|
|
19969
20029
|
var GEOMETRY = ["price", "fromTime", "fromPrice", "toTime", "toPrice", "points"];
|
|
19970
|
-
var STYLE = ["color", "lineWidth", "lineStyle", "locked", "hidden", "shared", "alert", "rows", "valueArea", "levels"];
|
|
20030
|
+
var STYLE = ["color", "lineWidth", "lineStyle", "locked", "hidden", "shared", "alert", "rows", "valueArea", "levels", "visibleIntervals", "fontSize", "fillOpacity", "bold", "italic", "extendLeft", "extendRight"];
|
|
19971
20031
|
var FLAGS = /* @__PURE__ */ new Set(["locked", "hidden", "shared", "alert"]);
|
|
19972
20032
|
function recreateRequest(mark) {
|
|
19973
20033
|
const out = { kind: mark.kind, label: mark.label, role: mark.role, coin: mark.coin, interval: mark.interval };
|
|
@@ -20170,7 +20230,7 @@ var DrawingList = React12.memo(function DrawingList2({ marks, locked, onSelect,
|
|
|
20170
20230
|
const hidden = marks.filter((mark) => mark.hidden).length;
|
|
20171
20231
|
return /* @__PURE__ */ React12.createElement("section", { className: "dhl-drawing-list", lang: language, "aria-label": tr("\u753B\u7EBF\u5217\u8868", "Drawings") }, /* @__PURE__ */ React12.createElement("button", { type: "button", className: "dhl-drawing-list-head", "aria-expanded": open, onClick: toggle }, /* @__PURE__ */ React12.createElement("span", null, tr("\u753B\u7EBF", "Drawings"), " ", marks.length, hidden ? tr(` \xB7 \u9690\u85CF ${hidden}`, ` \xB7 ${hidden} hidden`) : ""), /* @__PURE__ */ React12.createElement(UiIcon, { name: open ? "chevronDown" : "chevronRight", size: 14 })), open ? /* @__PURE__ */ React12.createElement("ul", null, marks.map((mark) => {
|
|
20172
20232
|
const tool = toolOf(mark.kind);
|
|
20173
|
-
return /* @__PURE__ */ React12.createElement("li", { key: mark.id, className: mark.hidden ? "is-hidden" : "" }, /* @__PURE__ */ React12.createElement("button", { type: "button", className: "dhl-drawing-list-name", title: tr("\u5728\u56FE\u4E0A\u5B9A\u4F4D\u5E76\u9009\u4E2D", "Find and select on the chart"), onClick: () => onSelect(mark.id), disabled: mark.hidden }, /* @__PURE__ */ React12.createElement("span", { style: { color: mark.color } }, /* @__PURE__ */ React12.createElement(ToolIcon, { kind: mark.kind, size: 14 })), /* @__PURE__ */ React12.createElement("b", null, mark.label), /* @__PURE__ */ React12.createElement("small", null, mark.alert ? /* @__PURE__ */ React12.createElement("span", { className: "dhl-list-flag", title: tr("\u4EF7\u683C\u63D0\u9192\u5DF2\u5F00\u542F", "Price alert armed") }, /* @__PURE__ */ React12.createElement(UiIcon, { name: "bell", size: 12 })) : null, mark.shared ? /* @__PURE__ */ React12.createElement("span", { className: "dhl-list-flag", title: tr("\u5DF2\u540C\u6B65\u5230\u6240\u6709\u4F1A\u8BDD", "Synced to all sessions") }, /* @__PURE__ */ React12.createElement(UiIcon, { name: "sync", size: 12 })) : null, tr(tool.zh, tool.en), " \xB7 ", mark.interval, mark.source === "agent" ? " \xB7 agent" : "")), /* @__PURE__ */ React12.createElement("button", { type: "button", "aria-pressed": !mark.hidden, title: mark.hidden ? tr("\u663E\u793A", "Show") : tr("\u9690\u85CF", "Hide"), "aria-label": `${mark.hidden ? tr("\u663E\u793A", "Show") : tr("\u9690\u85CF", "Hide")} ${mark.label}`, onClick: () => onEdit(mark.id, { hidden: !mark.hidden }) }, /* @__PURE__ */ React12.createElement(UiIcon, { name: mark.hidden ? "eyeOff" : "eye", size: 14 })), /* @__PURE__ */ React12.createElement("button", { type: "button", className: mark.locked ? "on" : "", "aria-pressed": Boolean(mark.locked), disabled: locked, title: mark.locked ? tr("\u89E3\u9501", "Unlock") : tr("\u9501\u5B9A", "Lock"), "aria-label": `${mark.locked ? tr("\u89E3\u9501", "Unlock") : tr("\u9501\u5B9A", "Lock")} ${mark.label}`, onClick: () => onEdit(mark.id, { locked: !mark.locked }) }, /* @__PURE__ */ React12.createElement(UiIcon, { name: mark.locked ? "lock" : "unlock", size: 14 })), /* @__PURE__ */ React12.createElement("button", { type: "button", disabled: locked || mark.locked, title: tr("\u5220\u9664", "Delete"), "aria-label": `${tr("\u5220\u9664\u753B\u7EBF", "Remove line")} ${mark.label}`, onClick: () => onRemove(mark.id) }, /* @__PURE__ */ React12.createElement(UiIcon, { name: "trash", size: 14 })));
|
|
20233
|
+
return /* @__PURE__ */ React12.createElement("li", { key: mark.id, "data-mark-id": mark.id, className: mark.hidden ? "is-hidden" : "" }, /* @__PURE__ */ React12.createElement("button", { type: "button", className: "dhl-drawing-list-name", title: tr("\u5728\u56FE\u4E0A\u5B9A\u4F4D\u5E76\u9009\u4E2D", "Find and select on the chart"), onClick: () => onSelect(mark.id), disabled: mark.hidden }, /* @__PURE__ */ React12.createElement("span", { style: { color: mark.color } }, /* @__PURE__ */ React12.createElement(ToolIcon, { kind: mark.kind, size: 14 })), /* @__PURE__ */ React12.createElement("b", null, mark.label), /* @__PURE__ */ React12.createElement("small", null, mark.alert ? /* @__PURE__ */ React12.createElement("span", { className: "dhl-list-flag", title: tr("\u4EF7\u683C\u63D0\u9192\u5DF2\u5F00\u542F", "Price alert armed") }, /* @__PURE__ */ React12.createElement(UiIcon, { name: "bell", size: 12 })) : null, mark.shared ? /* @__PURE__ */ React12.createElement("span", { className: "dhl-list-flag", title: tr("\u5DF2\u540C\u6B65\u5230\u6240\u6709\u4F1A\u8BDD", "Synced to all sessions") }, /* @__PURE__ */ React12.createElement(UiIcon, { name: "sync", size: 12 })) : null, tr(tool.zh, tool.en), " \xB7 ", mark.interval, mark.visibleIntervals?.length ? tr(` \xB7 \u4EC5 ${mark.visibleIntervals.join("/")} \u663E\u793A`, ` \xB7 ${mark.visibleIntervals.join("/")} only`) : "", mark.source === "agent" ? " \xB7 agent" : "")), /* @__PURE__ */ React12.createElement("button", { type: "button", "aria-pressed": !mark.hidden, title: mark.hidden ? tr("\u663E\u793A", "Show") : tr("\u9690\u85CF", "Hide"), "aria-label": `${mark.hidden ? tr("\u663E\u793A", "Show") : tr("\u9690\u85CF", "Hide")} ${mark.label}`, onClick: () => onEdit(mark.id, { hidden: !mark.hidden }) }, /* @__PURE__ */ React12.createElement(UiIcon, { name: mark.hidden ? "eyeOff" : "eye", size: 14 })), /* @__PURE__ */ React12.createElement("button", { type: "button", className: mark.locked ? "on" : "", "aria-pressed": Boolean(mark.locked), disabled: locked, title: mark.locked ? tr("\u89E3\u9501", "Unlock") : tr("\u9501\u5B9A", "Lock"), "aria-label": `${mark.locked ? tr("\u89E3\u9501", "Unlock") : tr("\u9501\u5B9A", "Lock")} ${mark.label}`, onClick: () => onEdit(mark.id, { locked: !mark.locked }) }, /* @__PURE__ */ React12.createElement(UiIcon, { name: mark.locked ? "lock" : "unlock", size: 14 })), /* @__PURE__ */ React12.createElement("button", { type: "button", disabled: locked || mark.locked, title: tr("\u5220\u9664", "Delete"), "aria-label": `${tr("\u5220\u9664\u753B\u7EBF", "Remove line")} ${mark.label}`, onClick: () => onRemove(mark.id) }, /* @__PURE__ */ React12.createElement(UiIcon, { name: "trash", size: 14 })));
|
|
20174
20234
|
})) : null);
|
|
20175
20235
|
});
|
|
20176
20236
|
|
|
@@ -21592,6 +21652,7 @@ function DeskColumn({ onCollapse }) {
|
|
|
21592
21652
|
indicators,
|
|
21593
21653
|
expanded: chartExpanded,
|
|
21594
21654
|
resetToken: chartResetToken,
|
|
21655
|
+
interval: s.interval,
|
|
21595
21656
|
drawMode,
|
|
21596
21657
|
onDraw: draw,
|
|
21597
21658
|
onSelect: (selection) => insertChartReference(selection, coin, s.interval),
|
package/lib/chart-drawings.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type DrawingKind = 'level' | 'hray' | 'vline' | 'text' | 'trend' | 'ray' | 'extended' | 'fib' | 'rect' | 'measure' | 'channel' | 'long' | 'short' | 'fibext' | 'fibchannel' | 'fibtime' | 'gannfan' | 'pitchfork' | 'regression' | 'vprofile' | 'arrow' | 'callout' | 'circle' | 'triangle' | 'brush' | 'crossline' | 'pricelabel' | 'infoline' | 'trendangle' | 'pricerange' | 'daterange' | 'elliott' | 'elliottabc' | 'xabcd' | 'abcd' | 'headshoulders' | 'gannbox' | 'gannsquare' | 'schiff' | 'modschiff' | 'icon'
|
|
2
2
|
export type LineStyleName = 'solid' | 'dashed' | 'dotted'
|
|
3
|
-
export interface DrawingStyle { levels?: number[]; rows?: number; valueArea?: number; color?: string; lineWidth?: number; lineStyle?: LineStyleName; locked?: boolean; hidden?: boolean; shared?: boolean; alert?: boolean }
|
|
3
|
+
export interface DrawingStyle { visibleIntervals?: string[]; fontSize?: number; bold?: boolean; italic?: boolean; extendLeft?: boolean; extendRight?: boolean; fillOpacity?: number; levels?: number[]; rows?: number; valueArea?: number; color?: string; lineWidth?: number; lineStyle?: LineStyleName; locked?: boolean; hidden?: boolean; shared?: boolean; alert?: boolean }
|
|
4
4
|
export declare const DRAWING_POINTS: Record<DrawingKind, number>
|
|
5
5
|
export declare const DRAWING_KINDS: DrawingKind[]
|
|
6
6
|
export declare const DRAWING_CLICKS: Record<DrawingKind, number>
|
|
@@ -26,3 +26,6 @@ export declare function volumeProfile(bars: Array<{ open: number; high: number;
|
|
|
26
26
|
export declare const PROFILE_ROWS_MIN: number
|
|
27
27
|
export declare const PROFILE_ROWS_MAX: number
|
|
28
28
|
export declare const FIB_LEVEL_KINDS: DrawingKind[]
|
|
29
|
+
export declare const INTERVALS: string[]
|
|
30
|
+
export declare const FONT_KINDS: DrawingKind[]
|
|
31
|
+
export declare const FILL_KINDS: DrawingKind[]
|
package/lib/chart-drawings.js
CHANGED
|
@@ -52,6 +52,12 @@ export function isSingleEmoji(value) {
|
|
|
52
52
|
* from the POC toward the heavier neighbour until it holds `valueArea` of the volume.
|
|
53
53
|
* @param {Array<{ open: number, high: number, low: number, close: number, volume: number }>} bars
|
|
54
54
|
*/
|
|
55
|
+
/** Chart intervals a drawing can be limited to (same set as the desk). */
|
|
56
|
+
export const INTERVALS = ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '8h', '12h', '1d', '3d', '1w', '1M']
|
|
57
|
+
/** Text drawings whose font size can be set. */
|
|
58
|
+
export const FONT_KINDS = ['text', 'callout']
|
|
59
|
+
/** Drawings with a background fill whose opacity can be set. */
|
|
60
|
+
export const FILL_KINDS = ['rect', 'circle', 'triangle', 'channel', 'fib', 'regression', 'gannbox', 'pricerange', 'daterange', 'measure']
|
|
55
61
|
/** Fibonacci drawings whose levels can be customised. */
|
|
56
62
|
export const FIB_LEVEL_KINDS = ['fib', 'fibext', 'fibchannel']
|
|
57
63
|
export const PROFILE_ROWS_MIN = 6
|
|
@@ -100,7 +106,8 @@ export function linePriceAt(mark, time) {
|
|
|
100
106
|
if (!a) return null
|
|
101
107
|
if (mark.kind === 'hray') return time >= a.time ? a.price : null
|
|
102
108
|
if (!b || b.time === a.time) return null
|
|
103
|
-
|
|
109
|
+
// A trend line can be extended left and/or right (TradingView's per-line extend option).
|
|
110
|
+
if (mark.kind === 'trend' && ((!mark.extendLeft && time < Math.min(a.time, b.time)) || (!mark.extendRight && time > Math.max(a.time, b.time)))) return null
|
|
104
111
|
if (mark.kind === 'ray' && (time - a.time) * (b.time - a.time) < 0) return null
|
|
105
112
|
return a.price + (b.price - a.price) * (time - a.time) / (b.time - a.time)
|
|
106
113
|
}
|
|
@@ -188,6 +195,36 @@ export function drawingStyle(request) {
|
|
|
188
195
|
if (!LINE_STYLES.includes(request.lineStyle)) throw new Error(`线型必须为 ${LINE_STYLES.join('/')}`)
|
|
189
196
|
style.lineStyle = request.lineStyle
|
|
190
197
|
}
|
|
198
|
+
// TradingView object properties: visibility per interval, text size, background fill.
|
|
199
|
+
if (request?.visibleIntervals != null) {
|
|
200
|
+
const list = Array.isArray(request.visibleIntervals) ? [...new Set(request.visibleIntervals.map(String))] : []
|
|
201
|
+
if (!list.length || list.some((iv) => !INTERVALS.includes(iv))) throw new Error(`visibleIntervals 需要 1 个以上周期:${INTERVALS.join('/')}`)
|
|
202
|
+
style.visibleIntervals = list
|
|
203
|
+
}
|
|
204
|
+
if (request?.fontSize != null) {
|
|
205
|
+
if (request?.kind != null && !FONT_KINDS.includes(request.kind)) throw new Error(`fontSize 只适用于 ${FONT_KINDS.join('/')}`)
|
|
206
|
+
const size = Number(request.fontSize)
|
|
207
|
+
if (!Number.isInteger(size) || size < 10 || size > 32) throw new Error('字号必须为 10-32 的整数')
|
|
208
|
+
style.fontSize = size
|
|
209
|
+
}
|
|
210
|
+
for (const flag of ['extendLeft', 'extendRight']) {
|
|
211
|
+
if (request?.[flag] == null) continue
|
|
212
|
+
if (request?.kind != null && request.kind !== 'trend') throw new Error(`${flag} 只适用于 trend`)
|
|
213
|
+
if (typeof request[flag] !== 'boolean') throw new Error(`${flag} 必须为布尔值`)
|
|
214
|
+
style[flag] = request[flag]
|
|
215
|
+
}
|
|
216
|
+
for (const flag of ['bold', 'italic']) {
|
|
217
|
+
if (request?.[flag] == null) continue
|
|
218
|
+
if (request?.kind != null && !FONT_KINDS.includes(request.kind)) throw new Error(`${flag} 只适用于 ${FONT_KINDS.join('/')}`)
|
|
219
|
+
if (typeof request[flag] !== 'boolean') throw new Error(`${flag} 必须为布尔值`)
|
|
220
|
+
style[flag] = request[flag]
|
|
221
|
+
}
|
|
222
|
+
if (request?.fillOpacity != null) {
|
|
223
|
+
if (request?.kind != null && !FILL_KINDS.includes(request.kind)) throw new Error(`fillOpacity 只适用于 ${FILL_KINDS.join('/')}`)
|
|
224
|
+
const opacity = Number(request.fillOpacity)
|
|
225
|
+
if (!Number.isFinite(opacity) || opacity < 0 || opacity > 0.6) throw new Error('填充透明度必须在 0-0.6 之间')
|
|
226
|
+
style.fillOpacity = opacity
|
|
227
|
+
}
|
|
191
228
|
// Custom Fibonacci levels (TradingView lets each fib drawing choose its own).
|
|
192
229
|
if (request?.levels != null) {
|
|
193
230
|
if (request?.kind != null && !FIB_LEVEL_KINDS.includes(request.kind)) throw new Error(`levels 只适用于 ${FIB_LEVEL_KINDS.join('/')}`)
|
package/lib/remote.js
CHANGED
|
@@ -575,7 +575,7 @@ export class HyperRemote extends TypertRemoteService {
|
|
|
575
575
|
const fields = { ...drawingStyle({ ...patch, kind: current.kind }) }
|
|
576
576
|
if (patch?.alert === true) fields.alertTriggeredAt = undefined
|
|
577
577
|
// null clears a style back to its default (undo of a first restyle relies on this).
|
|
578
|
-
for (const key of ['color', 'lineWidth', 'lineStyle', 'rows', 'valueArea', 'levels']) if (key in patch && patch[key] === null) fields[key] = undefined
|
|
578
|
+
for (const key of ['color', 'lineWidth', 'lineStyle', 'rows', 'valueArea', 'levels', 'visibleIntervals', 'fontSize', 'fillOpacity', 'bold', 'italic', 'extendLeft', 'extendRight']) if (key in patch && patch[key] === null) fields[key] = undefined
|
|
579
579
|
if (geometryKeys.some((key) => key in patch)) Object.assign(fields, drawingGeometry(current.kind, merged))
|
|
580
580
|
if ('label' in patch) {
|
|
581
581
|
const label = String(patch.label ?? '').trim()
|
package/lib/tools.js
CHANGED
|
@@ -252,6 +252,11 @@ function buildTools(remote) {
|
|
|
252
252
|
smoothingPeriod: { type: 'number', description: 'kind=ma 可选:平滑周期' },
|
|
253
253
|
color: { type: 'string', description: '可选 #RRGGBB 颜色(画线或均线)' },
|
|
254
254
|
lineWidth: { type: 'number', description: '画线线宽 1-4' },
|
|
255
|
+
visibleIntervals: { type: 'array', items: { type: 'string' }, description: '只在这些周期显示(如 ["4h","1d"]);省略=所有周期' },
|
|
256
|
+
fontSize: { type: 'number', description: 'kind=text/callout:字号 10-32' },
|
|
257
|
+
extendLeft: { type: 'boolean', description: 'kind=trend:向左延长' }, extendRight: { type: 'boolean', description: 'kind=trend:向右延长' },
|
|
258
|
+
bold: { type: 'boolean', description: 'kind=text/callout:粗体' }, italic: { type: 'boolean', description: 'kind=text/callout:斜体' },
|
|
259
|
+
fillOpacity: { type: 'number', description: '矩形/圆/三角形/通道/斐波那契/区间等的背景填充透明度 0-0.6(0=无填充)' },
|
|
255
260
|
levels: { type: 'array', items: { type: 'number' }, description: 'kind=fib/fibext/fibchannel:自定义斐波那契层级,如 [0,0.382,0.5,0.618,1,1.618]' },
|
|
256
261
|
rows: { type: 'number', description: 'kind=vprofile:价格行数 6-200,默认 48' },
|
|
257
262
|
valueArea: { type: 'number', description: 'kind=vprofile:价值区成交量占比 0.5-0.95,默认 0.7' },
|
|
@@ -306,7 +311,7 @@ function buildTools(remote) {
|
|
|
306
311
|
return { text: `已移除均线:${maLabel(removed)}(${removed.id})`, line: removed }
|
|
307
312
|
}
|
|
308
313
|
if (args?.action === 'update') {
|
|
309
|
-
const patch = Object.fromEntries(['points', 'price', 'fromTime', 'fromPrice', 'toTime', 'toPrice', 'label', 'color', 'lineWidth', 'lineStyle', 'locked', 'hidden', 'shared', 'alert', 'rows', 'valueArea', 'levels'].filter((key) => args?.[key] != null).map((key) => [key, args[key]]))
|
|
314
|
+
const patch = Object.fromEntries(['points', 'price', 'fromTime', 'fromPrice', 'toTime', 'toPrice', 'label', 'color', 'lineWidth', 'lineStyle', 'locked', 'hidden', 'shared', 'alert', 'rows', 'valueArea', 'levels', 'visibleIntervals', 'fontSize', 'fillOpacity', 'bold', 'italic', 'extendLeft', 'extendRight'].filter((key) => args?.[key] != null).map((key) => [key, args[key]]))
|
|
310
315
|
const mark = remote.editChartMark(args?.id, patch, sessionId)
|
|
311
316
|
return { text: `已更新${DRAWING_NAMES[mark.kind] ?? mark.kind}:${mark.label}(${mark.id})`, mark }
|
|
312
317
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-hyperliquid",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Hyperliquid trading desk for the DeepSeek Harness (dsh) - market workspace, native agent conversation and guarded live / paper trading over the Hyperliquid API.",
|
|
5
5
|
"icon": "./icon.svg",
|
|
6
6
|
"type": "module",
|