dsh-hyperliquid 0.4.2 → 0.4.4

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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.4 — 2026-10-17
4
+
5
+ - 修复:触摸屏上无法画线。lightweight-charts 处理轻点时会在 touchend 上 preventDefault,浏览器不再补发 click,而落锚点依赖原生 click;现在未拖动的触摸 pointerup 直接按点击处理(同一轻点之后若仍有 click 会被忽略)。
6
+ - 修复:手指拖动锚点或画线时被浏览器当作页面滚动而中途取消(pointercancel);图表区域设为 `touch-action: none`,与 TradingView 一致由图表处理全部手势。
7
+ - 冒烟新增 touch 阶段(点画、选中、拖锚点、单指平移)。
8
+
9
+ ## 0.4.3 — 2026-10-17
10
+
11
+ - 趋势线可分别向左、向右延长(浮动工具条两个开关,agent 工具 `extendLeft` / `extendRight`);价格提醒按延长后的线判断,延长的线不参与屏幕外剔除。
12
+ - 「点击没反应」根因补充验证:0.1.0 两次点击间隔 300ms、450ms 同样漏画 20/20,550ms 0/20,分界与图表库 500ms 双击窗口一致;当前版本同样间隔 0 漏画。
13
+
3
14
  ## 0.4.2 — 2026-10-17
4
15
 
5
16
  - 画线对象属性对齐 TradingView:按周期显示(仅本周期,水平线的轴价格标签同样隐藏)、文本与标注气泡字号(10–32px)与粗体 / 斜体、矩形 / 圆 / 三角 / 通道 / 斐波那契 / 区间等的背景填充浓淡;浮动工具条、agent 工具参数、撤销与复制都支持。限定周期的画线在其他周期不绘制但保留在画线列表。
package/client.js CHANGED
@@ -949,6 +949,9 @@ body:has(.dhl-divider.dragging) * { cursor: col-resize !important; }
949
949
 
950
950
  /* One row like TradingView's floating toolbar: a wrapped toolbar covered drawings near the top of a narrow chart. */
951
951
  .dhl-draw-toolbar > * { flex: none; }
952
+ /* Touch: the chart owns every gesture inside it (anchor and drawing drags, pans), as TradingView does;
953
+ otherwise the browser claims a finger drag for scrolling and cancels the pointer mid-drag. */
954
+ .dhl-chart, .dhl-chart-drawing-overlay, .dhl-chart-drawing-overlay * { touch-action: none; }
952
955
  `;
953
956
 
954
957
  // src/client/locale.ts
@@ -17908,9 +17911,9 @@ function drawingLabel(kind, points2, price) {
17908
17911
  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 });
17909
17912
  return `${name} ${when}`;
17910
17913
  }
17911
- function clipLine(a, b, w, h, from) {
17914
+ function clipLine(a, b, w, h, from, to = Infinity) {
17912
17915
  const dx = b.x - a.x, dy = b.y - a.y;
17913
- let t0 = from, t1 = Infinity;
17916
+ let t0 = from, t1 = to;
17914
17917
  for (const [p, q] of [[-dx, a.x], [dx, w - a.x], [-dy, a.y], [dy, h - a.y]]) {
17915
17918
  if (p === 0) {
17916
17919
  if (q < 0) return null;
@@ -17992,7 +17995,7 @@ function guide(a, b, value, key3, text) {
17992
17995
  }
17993
17996
  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" };
17994
17997
  var DASH = { solid: void 0, dashed: "7 5", dotted: "1.5 4" };
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 }) {
17998
+ 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
17999
  const k = (fontSize ?? 12) / 12;
17997
18000
  const badges = [...alert ? ["alert"] : [], ...shared ? ["shared"] : []];
17998
18001
  const { width: w, height: h } = view;
@@ -18014,9 +18017,17 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
18014
18017
  body = /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, x2: p[0].x, y1: "0", y2: h });
18015
18018
  handles = [{ x: p[0].x, y: Math.min(h - 12, Math.max(12, p[0].y)) }];
18016
18019
  break;
18017
- case "trend":
18018
- body = p[1] ? /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y }) : null;
18020
+ case "trend": {
18021
+ if (!p[1]) break;
18022
+ if (!extendLeft && !extendRight) {
18023
+ body = /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y });
18024
+ break;
18025
+ }
18026
+ const [l, r] = p[0].x <= p[1].x ? [p[0], p[1]] : [p[1], p[0]];
18027
+ const seg = clipLine(l, r, w, h, extendLeft ? -Infinity : 0, extendRight ? Infinity : 1);
18028
+ body = seg ? /* @__PURE__ */ React7.createElement("line", { x1: seg[0].x, y1: seg[0].y, x2: seg[1].x, y2: seg[1].y }) : null;
18019
18029
  break;
18030
+ }
18020
18031
  case "ray":
18021
18032
  case "extended": {
18022
18033
  if (!p[1]) break;
@@ -18435,7 +18446,7 @@ function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone,
18435
18446
  onClick: () => onEdit({ lineStyle: value })
18436
18447
  },
18437
18448
  /* @__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" }))
18438
- ))) : 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(
18449
+ ))) : 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(
18439
18450
  "input",
18440
18451
  {
18441
18452
  className: "dhl-draw-toolbar-text dhl-fib-levels",
@@ -19010,13 +19021,16 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19010
19021
  box.addEventListener("pointermove", move, true);
19011
19022
  box.addEventListener("pointerup", move, true);
19012
19023
  box.addEventListener("pointercancel", cancel, true);
19024
+ let touchTapAt = -Infinity;
19025
+ const fromTouch = (event) => event.pointerType === "touch" || Boolean(event.sourceCapabilities?.firesTouchEvents);
19013
19026
  const chartClick = (event) => {
19027
+ if (event.type === "click" && fromTouch(event) && performance.now() - touchTapAt < 800) return false;
19014
19028
  const visible = chart.timeScale().getVisibleLogicalRange();
19015
19029
  const panned = !pressStillAClick(press?.range ?? null, visible ? { from: visible.from, to: visible.to } : null);
19016
19030
  const clicked = Boolean(press && !press.dragged && !panned && event.button === 0);
19017
19031
  press = null;
19018
- if (!clicked) return;
19019
- if (!(event.target instanceof HTMLCanvasElement)) return;
19032
+ if (!clicked) return false;
19033
+ if (!(event.target instanceof HTMLCanvasElement)) return false;
19020
19034
  const rect = box.getBoundingClientRect();
19021
19035
  const x = event.clientX - rect.left;
19022
19036
  const y = event.clientY - rect.top;
@@ -19067,6 +19081,11 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19067
19081
  }
19068
19082
  };
19069
19083
  box.addEventListener("click", chartClick);
19084
+ const touchTap = (event) => {
19085
+ if (event.pointerType !== "touch" || press?.id !== event.pointerId) return;
19086
+ if (chartClick(event) !== false) touchTapAt = performance.now();
19087
+ };
19088
+ box.addEventListener("pointerup", touchTap);
19070
19089
  let rangeTimer = null;
19071
19090
  let viewportFrame = 0;
19072
19091
  const refreshOverlay = () => {
@@ -19136,6 +19155,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19136
19155
  box.removeEventListener("pointerup", move, true);
19137
19156
  box.removeEventListener("pointercancel", cancel, true);
19138
19157
  box.removeEventListener("click", chartClick);
19158
+ box.removeEventListener("pointerup", touchTap);
19139
19159
  chart.timeScale().unsubscribeVisibleLogicalRangeChange(refreshOverlay);
19140
19160
  box.removeEventListener("pointermove", scaleDrag);
19141
19161
  box.removeEventListener("pointerdown", beginCanvasGesture);
@@ -19666,7 +19686,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19666
19686
  const cloneOf = React10.useCallback((mark) => {
19667
19687
  const line = timelineRef.current;
19668
19688
  const shifted = markPoints(mark).map((point) => ({ time: line.toTime(line.toLogical(point.time) + 5), price: point.price }));
19669
- 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, fillOpacity: mark.fillOpacity, visibleIntervals: mark.visibleIntervals }).filter(([, value]) => value != null));
19689
+ 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));
19670
19690
  return { kind: mark.kind, geometry: geometryPatch(mark.kind, shifted), style, label: mark.label };
19671
19691
  }, []);
19672
19692
  const clipboard = React10.useRef(null);
@@ -19915,7 +19935,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19915
19935
  const selected = selectedId === mark.id || group.includes(mark.id);
19916
19936
  if (interval && mark.visibleIntervals?.length && !mark.visibleIntervals.includes(interval)) return null;
19917
19937
  const clampedDecoration = !(mark.source === "user" || mark.kind === "level") || Boolean(mark.alert) || Boolean(mark.shared);
19918
- if (!selected && !clampedDecoration && FINITE_KINDS.has(mark.kind) && offscreen(shown, view)) return null;
19938
+ if (!selected && !clampedDecoration && !mark.extendLeft && !mark.extendRight && FINITE_KINDS.has(mark.kind) && offscreen(shown, view)) return null;
19919
19939
  return /* @__PURE__ */ React10.createElement(
19920
19940
  DrawingShape,
19921
19941
  {
@@ -19941,7 +19961,9 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19941
19961
  fontSize: mark.fontSize,
19942
19962
  bold: mark.bold,
19943
19963
  italic: mark.italic,
19944
- fillOpacity: mark.fillOpacity
19964
+ fillOpacity: mark.fillOpacity,
19965
+ extendLeft: mark.extendLeft,
19966
+ extendRight: mark.extendRight
19945
19967
  }
19946
19968
  );
19947
19969
  }), /* @__PURE__ */ React10.createElement("g", { className: "dhl-fills" }, fills.map((event) => {
@@ -20017,7 +20039,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
20017
20039
  var React11 = __toESM(require("react"), 1);
20018
20040
  var HISTORY_LIMIT = 100;
20019
20041
  var GEOMETRY = ["price", "fromTime", "fromPrice", "toTime", "toPrice", "points"];
20020
- var STYLE = ["color", "lineWidth", "lineStyle", "locked", "hidden", "shared", "alert", "rows", "valueArea", "levels", "visibleIntervals", "fontSize", "fillOpacity", "bold", "italic"];
20042
+ var STYLE = ["color", "lineWidth", "lineStyle", "locked", "hidden", "shared", "alert", "rows", "valueArea", "levels", "visibleIntervals", "fontSize", "fillOpacity", "bold", "italic", "extendLeft", "extendRight"];
20021
20043
  var FLAGS = /* @__PURE__ */ new Set(["locked", "hidden", "shared", "alert"]);
20022
20044
  function recreateRequest(mark) {
20023
20045
  const out = { kind: mark.kind, label: mark.label, role: mark.role, coin: mark.coin, interval: mark.interval };
@@ -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 { visibleIntervals?: string[]; fontSize?: number; bold?: boolean; italic?: boolean; fillOpacity?: number; 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>
@@ -106,7 +106,8 @@ export function linePriceAt(mark, time) {
106
106
  if (!a) return null
107
107
  if (mark.kind === 'hray') return time >= a.time ? a.price : null
108
108
  if (!b || b.time === a.time) return null
109
- if (mark.kind === 'trend' && (time < Math.min(a.time, b.time) || time > Math.max(a.time, b.time))) return null
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
110
111
  if (mark.kind === 'ray' && (time - a.time) * (b.time - a.time) < 0) return null
111
112
  return a.price + (b.price - a.price) * (time - a.time) / (b.time - a.time)
112
113
  }
@@ -206,6 +207,12 @@ export function drawingStyle(request) {
206
207
  if (!Number.isInteger(size) || size < 10 || size > 32) throw new Error('字号必须为 10-32 的整数')
207
208
  style.fontSize = size
208
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
+ }
209
216
  for (const flag of ['bold', 'italic']) {
210
217
  if (request?.[flag] == null) continue
211
218
  if (request?.kind != null && !FONT_KINDS.includes(request.kind)) throw new Error(`${flag} 只适用于 ${FONT_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', 'visibleIntervals', 'fontSize', 'fillOpacity', 'bold', 'italic']) 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
@@ -254,6 +254,7 @@ function buildTools(remote) {
254
254
  lineWidth: { type: 'number', description: '画线线宽 1-4' },
255
255
  visibleIntervals: { type: 'array', items: { type: 'string' }, description: '只在这些周期显示(如 ["4h","1d"]);省略=所有周期' },
256
256
  fontSize: { type: 'number', description: 'kind=text/callout:字号 10-32' },
257
+ extendLeft: { type: 'boolean', description: 'kind=trend:向左延长' }, extendRight: { type: 'boolean', description: 'kind=trend:向右延长' },
257
258
  bold: { type: 'boolean', description: 'kind=text/callout:粗体' }, italic: { type: 'boolean', description: 'kind=text/callout:斜体' },
258
259
  fillOpacity: { type: 'number', description: '矩形/圆/三角形/通道/斐波那契/区间等的背景填充透明度 0-0.6(0=无填充)' },
259
260
  levels: { type: 'array', items: { type: 'number' }, description: 'kind=fib/fibext/fibchannel:自定义斐波那契层级,如 [0,0.382,0.5,0.618,1,1.618]' },
@@ -310,7 +311,7 @@ function buildTools(remote) {
310
311
  return { text: `已移除均线:${maLabel(removed)}(${removed.id})`, line: removed }
311
312
  }
312
313
  if (args?.action === 'update') {
313
- 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'].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]]))
314
315
  const mark = remote.editChartMark(args?.id, patch, sessionId)
315
316
  return { text: `已更新${DRAWING_NAMES[mark.kind] ?? mark.kind}:${mark.label}(${mark.id})`, mark }
316
317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-hyperliquid",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
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",