dsh-hyperliquid 0.5.1 → 0.5.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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.3 — 2026-10-17
4
+
5
+ - 新增锚定文本(按 TradingView 说明固定在屏幕位置,平移图表时不动,可拖动换位置)与表格(| 分隔单元格、; 或换行分隔行;工具条可固定到屏幕或取消固定,取消时贴回所在位置的时间与价格)。共 76 种。固定在屏幕上的画线不参与多选拖动;agent 用 `screenX/screenY` 固定、`unpin: true` 取消固定。
6
+ - 修复:指针停在画线上时,触控板滑动和滚轮缩放被画线的命中层截走,图表不响应;现在转发给下方的图表。
7
+ - 冒烟:锚定文本、表格固定 / 取消固定,每次平移都有「图表确实平移了」的对照检查;触控板阶段加「指针在画线上滑动仍平移」。
8
+
9
+ ## 0.5.2 — 2026-10-17
10
+
11
+ - 新增投影(TradingView 的扇形:起点、未来点、估计价格,标注涨跌幅)与幽灵 K 线(逐点点击设想路径,双击或 Enter 结束;从首点到末点每个 K 线周期一根,收盘跟随路径;每根高低差取其实体与「路径起点前 20 根真实 K 线平均高低差 × 0.6–1.4 的固定抖动」中的较大者,最多 2000 根。TradingView 未公开其生成算法,这是近似实现)。共 74 种。
12
+ - 查证 TradingView 帮助页后记录:内部叉子线的官方描述与改良 Schiff 相同、固定江恩方格没有说明文档,两者未做;锚定文本与表格仍未做。
13
+
3
14
  ## 0.5.1 — 2026-10-17
4
15
 
5
16
  - 再补 6 种 TradingView 画线(共 72 种):荧光笔(拖动绘制的半透明粗线)、路径(逐点点击,双击或 Enter 结束,末端带箭头)、折线、图钉(带说明)、斐波那契螺旋(每四分之一圈放大 1.618 倍)、K 线形态(把源区间的 K 线按首根开盘价对齐复制到指定位置)。
package/README.md CHANGED
@@ -58,7 +58,7 @@ dsh plugin --profile web add dsh-web3
58
58
  - **钱包与收益**:点击顶栏钱包查看地址、余额、权益和收益。实盘收益曲线来自 Hyperliquid 官方 portfolio 数据;模拟盘显示本地已实现与未实现收益。
59
59
  - **K 线**:支持交易所官方的 14 个周期,以及成交量、SMA20、MA60、EMA20/50、布林带、RSI、MACD、ATR14 与 UTC 日内 VWAP。图表类型可切换蜡烛、美国线、折线、面积和平均 K 线,并随会话保存;平均 K 线仅改变显示,点位引用仍使用原始 OHLC。向左拖动会自动加载更早的 K 线,最多可追溯到交易所提供的最近 5000 根。可以画水平线、趋势线,画线有实时预览和趋势线端点。手动画线与会话中 `hyper_chart` 添加的标注按会话和币种保存,在该币种的所有周期展示;agent 添加或删除后,打开的图表通常在下一次宿主事件轮询(约 1 秒)更新。明确点击 K 线、成交量柱或指标线,会在输入框里插入一个引用标签(如 `BTC 15m 10-15 13:45 · C 81397`);拖动图表不会引用。发送时标签展开为该点的完整 OHLC、量能与指标数据;点击标签可回到图上的那根 K 线。 「指标」菜单还可叠加最多 8 条可配置均线:SMA / EMA / SMMA(平滑均线,即 TradingView 的 RMA)/ WMA,周期 1–500,价格源可选 close / open / high / low / hl2 / hlc3 / ohlc4,并可再套一层平滑(虚线),算法与 TradingView Pine 一致;图例实时显示数值,agent 也能用 `hyper_chart` 把均线画到图上。
60
60
  - **导航与导出**:图表下方可查看最近 1/5/30 天、适应已加载全部数据、跳转到本地时间指定的日期,或回到最新行情;历史不足会自动补取,并在到达数据边界时提示。导出菜单可保存含画线和指标的 PNG,或导出已加载原始 OHLCV 的 CSV(UTC 时间)。指标图例与成交量跟随十字光标;VWAP 使用 UTC 每日重置的 HLC3 成交量加权价格,首日历史不全时不展示该日不完整结果。
61
- - **画线工具**:工具栏「画线」菜单按 TradingView 的分组提供 72 种工具(菜单顶部可搜索)——线条:趋势线、射线、延长线、信息线(涨跌 / 根数 / 时长 / 角度)、趋势角度、水平线、水平射线、十字线、垂直线;通道:平行通道、安德鲁叉子线、Schiff 与改良 Schiff 叉子线、回归趋势(收盘价最小二乘 ±2σ)、固定范围成交量分布(按价格统计区间成交量,标出 POC 与价值区 VAH / VAL,可调行数与价值区比例;斐波那契回撤、扩展和通道可在浮动工具条自定义层级;由 K 线估算,区间早于已加载历史时自动补加载并提示);斐波那契与江恩:斐波那契回撤、趋势斐波那契扩展、斐波那契通道、斐波那契时间周期、江恩箱、江恩方格、江恩扇形;形态:艾略特推动浪(0-5)与调整浪(A-B-C)、XABCD 谐波与 ABCD(自动标注斐波那契比值)、头肩形态(含颈线);形状:矩形、圆形、三角形、画笔;标注:文本、标注气泡、价格标签、箭头、图标;测量与头寸:价格区间、日期区间、日期与价格区间、多头 / 空头头寸(止盈默认 2R,显示 R:R);0.5.0 新增:平顶 / 平底通道、分离通道、斐波那契速度阻力扇 / 圆 / 弧 / 楔形 / 基于趋势的时间、Cypher、三驱、三角形态、艾略特三角浪与双重 / 三重组合、周期线、正弦线、椭圆、旋转矩形、弧线、曲线、便签、价格便签、路标、预测、锚定 VWAP、锚定成交量分布;0.5.1 新增:荧光笔、路径(逐点点击,双击或 Enter 结束)、折线、图钉、斐波那契螺旋、K 线形态(复制一段 K 线的形状到别处)。点击过程中实时预览,Esc 取消,按住 Shift 把方向约束为水平或 45°;磁吸可选关 / 弱 / 强,吸附到 K 线开高低收。
61
+ - **画线工具**:工具栏「画线」菜单按 TradingView 的分组提供 76 种工具(菜单顶部可搜索)——线条:趋势线、射线、延长线、信息线(涨跌 / 根数 / 时长 / 角度)、趋势角度、水平线、水平射线、十字线、垂直线;通道:平行通道、安德鲁叉子线、Schiff 与改良 Schiff 叉子线、回归趋势(收盘价最小二乘 ±2σ)、固定范围成交量分布(按价格统计区间成交量,标出 POC 与价值区 VAH / VAL,可调行数与价值区比例;斐波那契回撤、扩展和通道可在浮动工具条自定义层级;由 K 线估算,区间早于已加载历史时自动补加载并提示);斐波那契与江恩:斐波那契回撤、趋势斐波那契扩展、斐波那契通道、斐波那契时间周期、江恩箱、江恩方格、江恩扇形;形态:艾略特推动浪(0-5)与调整浪(A-B-C)、XABCD 谐波与 ABCD(自动标注斐波那契比值)、头肩形态(含颈线);形状:矩形、圆形、三角形、画笔;标注:文本、标注气泡、价格标签、箭头、图标;测量与头寸:价格区间、日期区间、日期与价格区间、多头 / 空头头寸(止盈默认 2R,显示 R:R);0.5.0 新增:平顶 / 平底通道、分离通道、斐波那契速度阻力扇 / 圆 / 弧 / 楔形 / 基于趋势的时间、Cypher、三驱、三角形态、艾略特三角浪与双重 / 三重组合、周期线、正弦线、椭圆、旋转矩形、弧线、曲线、便签、价格便签、路标、预测、锚定 VWAP、锚定成交量分布;0.5.1 新增:荧光笔、路径(逐点点击,双击或 Enter 结束)、折线、图钉、斐波那契螺旋、K 线形态(复制一段 K 线的形状到别处);0.5.2 新增:投影(扇形)、幽灵 K 线(沿设想路径生成假想 K 线);0.5.3 新增:锚定文本(固定在屏幕上)、表格(| 分隔单元格,; 分隔行,可固定到屏幕)。点击过程中实时预览,Esc 取消,按住 Shift 把方向约束为水平或 45°;磁吸可选关 / 弱 / 强,吸附到 K 线开高低收。
62
62
  - **编辑与管理画线**:点选后拖动锚点改形状、拖动线身整体移动;浮动工具条可通过齿轮精确编辑锚点时间、价格和名称,改颜色、线宽、线型,复制,设为该工具的默认样式(之后新画的同类线自动套用),锁定或删除。按住 Shift 或 Ctrl/Cmd 点击可多选,多选后整组拖动、改色、隐藏、锁定或删除(一步撤销)。快捷键:Alt+T 趋势线、Alt+H 水平线、Alt+J 水平射线、Alt+V 垂直线、Alt+C 十字线、Alt+F 斐波那契回撤、Alt+Shift+R 矩形;画线时按住 Ctrl/Cmd 临时反转磁吸,吸附点显示提示圈;Esc 逐层退出(取消当前拖动或工具 → 取消选择 → 退出放大图表)。Delete 删除,Ctrl/Cmd+C、V 复制粘贴,Ctrl/Cmd+Z 撤销、Shift+Ctrl/Cmd+Z 重做(新建、修改、删除和批量删除都可撤销)。图表下方的画线列表可逐条定位、显示 / 隐藏、锁定或删除;菜单里还有连续绘制、锁定所有画线和删除所有画线。点击菜单中的星标可收藏最多 12 个工具,图表上方显示常用画线快捷栏;收藏保存在当前浏览器。画线锚定在时间与价格上,平移、缩放、切换刻度和加载历史时始终贴合 K 线,也可延伸到最新 K 线右侧的空白区域;画线按会话和币种保存,切换会话随之切换,重启后保留,agent 用 `hyper_chart` 新增或修改的画线会实时出现在打开的图表上。
63
63
  - **同步、提醒与贴图**:浮动工具条可把画线「同步到所有会话」(该币种的每个交易会话都显示同一条线,任一会话可修改,重启后保留),水平线、水平射线、趋势线、射线和延长线可开启「价格提醒」——宿主持续监控中价,穿越画线时交易台弹出提醒,提醒触发一次后自动关闭,可再次开启。图标工具附贴图库:18 个内置图标、交易 / 表情 / 符号三类约 60 个贴纸,也可输入或粘贴任意一个 emoji 作为贴纸。每张图(会话 + 币种,跨周期合计)最多 100 条画线。
64
64
  - **成交与决策原因**:每笔成交在图上的成交价位置打点(买绿卖红),有 AI 决策原因的点带圆环;点击查看方向、数量、价格、时间和当时的决策原因,「显示 → 成交原因」可把原因标签直接显示在图上。「回到决策会话分析」会切换到做出这笔决策的交易会话,并把成交与原因作为复盘引用放进输入框。
@@ -109,7 +109,7 @@ dsh plugin --profile web add dsh-web3
109
109
  |---|---|
110
110
  | `hyper_context` | 一次读取当前会话的币对、盘口、K 线、账户权益、仓位与风险限额 |
111
111
  | `hyper_market` | 行情:overview / book / candles / trades / catalog / indicators(SMA、EMA、RSI、MACD、布林带、ATR、Stochastic、ADX/DMI、OBV、支撑阻力,以及图上均线或指定均线的最新值) |
112
- | `hyper_chart` | 在会话图表上添加、修改(几何 / 颜色 / 线宽 / 线型 / 锁定 / 隐藏)或删除画线(72 种,含固定范围成交量分布、斐波那契、江恩、叉子线、回归、艾略特与谐波形态、标注与多空头寸;可设置同步到所有会话与价格提醒)和均线(SMA / EMA / SMMA / WMA,可选平滑);`action=view` 切换图表周期并显示 / 隐藏 RSI、MACD 等副图指标 |
112
+ | `hyper_chart` | 在会话图表上添加、修改(几何 / 颜色 / 线宽 / 线型 / 锁定 / 隐藏)或删除画线(76 种,含固定范围成交量分布、斐波那契、江恩、叉子线、回归、艾略特与谐波形态、标注与多空头寸;可设置同步到所有会话与价格提醒)和均线(SMA / EMA / SMMA / WMA,可选平滑);`action=view` 切换图表周期并显示 / 隐藏 RSI、MACD 等副图指标 |
113
113
  | `hyper_account` | 账户:state / orders / fills / orderStatus(支持 oid、cloid)/ review(窗口证据、成本及订单核对) |
114
114
  | `hyper_decide` | 实盘预检:提交交易意图与依据,返回 30 秒内有效的一次性 `decisionId` |
115
115
  | `hyper_order` | place / cancel / leverage;实盘下单必须带匹配的 `decisionId`,可附带止盈止损(目前仅测试网) |
package/client.js CHANGED
@@ -974,6 +974,17 @@ body:has(.dhl-divider.dragging) * { cursor: col-resize !important; }
974
974
  .dhl-drawing .dhl-pin-glyph { fill: currentColor; fill-opacity: .25; stroke: currentColor; stroke-width: 2; }
975
975
  .dhl-drawing .dhl-bars-copy { fill: none; stroke: currentColor; stroke-width: 1.3; opacity: .8; }
976
976
  .dhl-drawing-hit .dhl-bars-copy, .dhl-drawing-hit .dhl-pin-glyph { pointer-events: stroke; }
977
+
978
+ /* 0.5.2: projection sector, ghost feed candles. */
979
+ .dhl-drawing path.dhl-sector { fill: currentColor; fill-opacity: var(--dhl-fill-opacity, .1); stroke: currentColor; stroke-width: var(--dhl-line-width, 1.4); }
980
+ .dhl-drawing .dhl-ghost-up { fill: color-mix(in srgb, var(--dsw-alias-state-success-primary) 45%, transparent); stroke: var(--dsw-alias-state-success-primary); stroke-width: 1; opacity: .7; }
981
+ .dhl-drawing .dhl-ghost-down { fill: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent); stroke: var(--dsw-alias-state-error-primary); stroke-width: 1; opacity: .7; }
982
+ .dhl-drawing-hit .dhl-ghost path, .dhl-drawing-hit .dhl-sector { pointer-events: all; }
983
+
984
+ /* 0.5.3: tables and screen-pinned annotations. */
985
+ .dhl-drawing .dhl-table-rule { stroke: currentColor; stroke-width: 1; opacity: .35; }
986
+ .dhl-drawing .dhl-table-head { font-weight: 650; }
987
+ .dhl-drawing-hit .dhl-table-box { pointer-events: all; }
977
988
  `;
978
989
 
979
990
  // src/client/locale.ts
@@ -17750,10 +17761,17 @@ var DRAWING_POINTS = {
17750
17761
  polyline: 0,
17751
17762
  pin: 1,
17752
17763
  fibspiral: 2,
17753
- barspattern: 3
17764
+ barspattern: 3,
17765
+ // Projection (TradingView's sector: origin, a future point, an estimated price); ghost feed is drawn like a path.
17766
+ projection: 3,
17767
+ ghostfeed: 0,
17768
+ // Screen-anchored text (TradingView's anchored text) and a table (cells split by |, rows by ; or a new line).
17769
+ anchoredtext: 1,
17770
+ table: 1
17754
17771
  };
17772
+ var SCREEN_KINDS = ["anchoredtext", "table"];
17755
17773
  var FREEHAND_KINDS = ["brush", "highlighter"];
17756
- var MULTI_KINDS = ["path", "polyline"];
17774
+ var MULTI_KINDS = ["path", "polyline", "ghostfeed"];
17757
17775
  var PATH_MAX = 50;
17758
17776
  var ICON_NAMES = ["flag", "star", "up", "down", "warning", "check", "cross", "fire", "target", "bell", "pin", "clock", "dollar", "shield", "bolt", "heart", "question", "info"];
17759
17777
  var STICKERS = {
@@ -17766,7 +17784,7 @@ function isSingleEmoji(value) {
17766
17784
  if (typeof value !== "string" || !value || value.length > 32 || !/\p{Extended_Pictographic}|\p{Regional_Indicator}/u.test(value)) return false;
17767
17785
  return [...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(value)].length === 1;
17768
17786
  }
17769
- var FONT_KINDS = ["text", "callout", "note", "pricenote", "signpost"];
17787
+ var FONT_KINDS = ["text", "callout", "note", "pricenote", "signpost", "anchoredtext", "table"];
17770
17788
  var FILL_KINDS = ["rect", "circle", "triangle", "channel", "fib", "regression", "gannbox", "pricerange", "daterange", "measure", "flatchannel", "disjoint", "ellipse", "rotrect"];
17771
17789
  var FIB_LEVEL_KINDS = ["fib", "fibext", "fibchannel"];
17772
17790
  function volumeProfile(bars, { rows = 48, valueArea = 0.7 } = {}) {
@@ -17813,7 +17831,7 @@ var GANN_BOX_LEVELS = [0, 0.25, 0.382, 0.5, 0.618, 0.75, 1];
17813
17831
  var ALERT_KINDS = ["level", "hray", "trend", "ray", "extended"];
17814
17832
  var BRUSH_MIN = 2;
17815
17833
  var BRUSH_MAX = 500;
17816
- var TEXT_KINDS = ["text", "callout", "note", "pricenote", "signpost", "pin"];
17834
+ var TEXT_KINDS = ["text", "callout", "note", "pricenote", "signpost", "pin", "anchoredtext", "table"];
17817
17835
  var DRAWING_CLICKS = { ...Object.fromEntries(Object.entries(DRAWING_POINTS)), long: 2, short: 2 };
17818
17836
  var DRAWING_KINDS = Object.keys(DRAWING_POINTS);
17819
17837
  var FIB_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
@@ -17852,6 +17870,8 @@ function drawingGeometry(kind, request, now = Date.now()) {
17852
17870
  if (["trend", "ray", "extended", "channel", "fibchannel", "gannfan", "regression", "fibtime", "infoline", "trendangle", "flatchannel", "disjoint", "fibfan", "fibtrendtime", "cycles", "sine", "forecast"].includes(kind) && points2[0].time === points2[1].time) throw new Error("\u524D\u4E24\u4E2A\u70B9\u7684\u65F6\u95F4\u4E0D\u80FD\u76F8\u540C");
17853
17871
  if ((kind === "cycles" || kind === "sine" || kind === "fibtrendtime") && points2[1].time < points2[0].time) throw new Error("\u7B2C\u4E8C\u4E2A\u70B9\u5FC5\u987B\u665A\u4E8E\u7B2C\u4E00\u4E2A\u70B9");
17854
17872
  if (kind === "barspattern" && points2[1].time <= points2[0].time) throw new Error("K \u7EBF\u5F62\u6001\u7684\u6E90\u533A\u95F4\u7EC8\u70B9\u5FC5\u987B\u665A\u4E8E\u8D77\u70B9");
17873
+ if (kind === "projection" && points2[1].time <= points2[0].time) throw new Error("\u6295\u5F71\u7684\u7B2C\u4E8C\u4E2A\u70B9\u5FC5\u987B\u5728\u8D77\u70B9\u4E4B\u540E\uFF08\u672A\u6765\u65B9\u5411\uFF09");
17874
+ if (kind === "ghostfeed" && points2.some((point2, index) => index > 0 && point2.time <= points2[index - 1].time)) throw new Error("\u5E7D\u7075 K \u7EBF\u7684\u70B9\u5FC5\u987B\u6309\u65F6\u95F4\u4ECE\u5DE6\u5230\u53F3");
17855
17875
  if (kind === "vprofile") {
17856
17876
  if (points2[0].time === points2[1].time) throw new Error("\u6210\u4EA4\u91CF\u5206\u5E03\u9700\u8981\u4E00\u4E2A\u65F6\u95F4\u533A\u95F4");
17857
17877
  if (points2[1].time < points2[0].time) points2.reverse();
@@ -17928,6 +17948,8 @@ var DRAWING_TOOLS = [
17928
17948
  { group: "annotation", kind: "note", zh: "\u4FBF\u7B7E", en: "Note", hintZh: "\u5355\u51FB\u4F4D\u7F6E\u540E\u8F93\u5165\u4FBF\u7B7E\u5185\u5BB9", hintEn: "Click a spot, then type the note", icon: "M5 4h14v11l-5 5H5zM14 20v-5h5" },
17929
17949
  { group: "annotation", kind: "pricenote", zh: "\u4EF7\u683C\u4FBF\u7B7E", en: "Price note", hintZh: "\u70B9\u51FB\u8981\u6807\u6CE8\u7684\u4EF7\u683C\uFF0C\u518D\u70B9\u51FB\u4FBF\u7B7E\u4F4D\u7F6E\uFF0C\u7136\u540E\u8F93\u5165\u6587\u5B57", hintEn: "Click the price, then the note position, then type", icon: "M3 16h6M9 16l4-6h8v8h-8z" },
17930
17950
  { group: "annotation", kind: "signpost", zh: "\u8DEF\u6807", en: "Signpost", hintZh: "\u5355\u51FB K \u7EBF\u4F4D\u7F6E\u540E\u8F93\u5165\u8DEF\u6807\u6587\u5B57", hintEn: "Click a bar, then type the signpost text", icon: "M8 21V4M8 4h11l-2 3 2 3H8" },
17951
+ { group: "annotation", kind: "anchoredtext", zh: "\u951A\u5B9A\u6587\u672C", en: "Anchored text", hintZh: "\u5355\u51FB\u4F4D\u7F6E\u540E\u8F93\u5165\u6587\u5B57\uFF1B\u56FA\u5B9A\u5728\u5C4F\u5E55\u4E0A\uFF0C\u5E73\u79FB\u56FE\u8868\u65F6\u4E0D\u52A8", hintEn: "Click a spot, then type; pinned to the screen while the chart scrolls", icon: "M5 6h14M12 6v13M9 19h6M18 15l3 3-3 3" },
17952
+ { group: "annotation", kind: "table", zh: "\u8868\u683C", en: "Table", hintZh: "\u5355\u51FB\u4F4D\u7F6E\u540E\u8F93\u5165\u5185\u5BB9\uFF1A| \u5206\u9694\u5355\u5143\u683C\uFF0C; \u5206\u9694\u884C\uFF1B\u53EF\u5728\u5DE5\u5177\u6761\u56FA\u5B9A\u5230\u5C4F\u5E55", hintEn: "Click, then type: | between cells, ; between rows; pin it from the toolbar", icon: "M4 5h16v14H4zM4 10h16M4 15h16M10 5v14" },
17931
17953
  { group: "annotation", kind: "pin", zh: "\u56FE\u9489", en: "Pin", hintZh: "\u5355\u51FB\u4F4D\u7F6E\u540E\u8F93\u5165\u8BF4\u660E\uFF0C\u56FE\u4E0A\u663E\u793A\u56FE\u9489\u548C\u8BF4\u660E", hintEn: "Click a spot, then type; shows a pin with its note", icon: "M12 21s-6-5.6-6-10a6 6 0 0 1 12 0c0 4.4-6 10-6 10zM12 8.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z" },
17932
17954
  { group: "annotation", kind: "pricelabel", zh: "\u4EF7\u683C\u6807\u7B7E", en: "Price label", hintZh: "\u5355\u51FB\u5728\u8BE5\u4EF7\u683C\u5904\u653E\u7F6E\u6807\u7B7E", hintEn: "Click to tag a price", icon: "M3 12l5-6h13v12H8z" },
17933
17955
  { group: "annotation", kind: "icon", zh: "\u56FE\u6807", en: "Icon", hintZh: "\u5355\u51FB\u4F4D\u7F6E\u540E\u9009\u62E9\u56FE\u6807", hintEn: "Click a spot, then pick an icon", icon: "M12 3l2.6 5.6 6 .7-4.5 4.1 1.2 6L12 16.6 6.7 19.4l1.2-6L3.4 9.3l6-.7z" },
@@ -17938,6 +17960,8 @@ var DRAWING_TOOLS = [
17938
17960
  { group: "measure", kind: "forecast", zh: "\u9884\u6D4B", en: "Forecast", hintZh: "\u70B9\u51FB\u8D77\u70B9\uFF0C\u518D\u70B9\u51FB\u9884\u6D4B\u76EE\u6807\uFF0C\u663E\u793A\u6DA8\u8DCC\u4E0E\u65F6\u957F", hintEn: "Click the source, then the target; shows change and time", icon: "M3 17 10 12M10 12l11-7M17 5h4v4M10 12v.01" },
17939
17961
  { group: "measure", kind: "avwap", zh: "\u951A\u5B9A VWAP", en: "Anchored VWAP", hintZh: "\u5355\u51FB\u951A\u5B9A\u7684 K \u7EBF\uFF0C\u4ECE\u8BE5\u5904\u7D2F\u8BA1\u5230\u6700\u65B0 K \u7EBF", hintEn: "Click the anchor bar; accumulates to the latest bar", icon: "M4 4v16M4 16c4-1 6-6 10-7s5 2 6 0" },
17940
17962
  { group: "measure", kind: "avprofile", zh: "\u951A\u5B9A\u6210\u4EA4\u91CF\u5206\u5E03", en: "Anchored volume profile", hintZh: "\u5355\u51FB\u951A\u5B9A\u7684 K \u7EBF\uFF0C\u7EDF\u8BA1\u4ECE\u8BE5\u5904\u5230\u6700\u65B0\u7684\u6210\u4EA4\u91CF\u5206\u5E03", hintEn: "Click the anchor bar; volume by price up to the latest bar", icon: "M4 3v18M4 6h7M4 10h12M4 14h9M4 18h5" },
17963
+ { group: "measure", kind: "projection", zh: "\u6295\u5F71", en: "Projection", hintZh: "\u70B9\u51FB\u8D77\u70B9\u3001\u672A\u6765\u65B9\u5411\u70B9\uFF0C\u518D\u70B9\u51FB\u4F30\u8BA1\u4EF7\u683C\uFF0C\u753B\u51FA\u6247\u5F62\u5E76\u6807\u6CE8\u6DA8\u8DCC\u5E45", hintEn: "Click the origin, a future point, then the estimated price; draws a sector with the change", icon: "M4 18 20 10M4 18l14-12M4 18M16 6a10 10 0 0 1 4 4" },
17964
+ { group: "measure", kind: "ghostfeed", zh: "\u5E7D\u7075 K \u7EBF", en: "Ghost feed", hintZh: "\u9010\u70B9\u70B9\u51FB\u8BBE\u60F3\u7684\u4EF7\u683C\u8DEF\u5F84\uFF0C\u53CC\u51FB\u6216\u6309 Enter \u7ED3\u675F\uFF0C\u6CBF\u8DEF\u5F84\u751F\u6210\u5047\u60F3 K \u7EBF", hintEn: "Click the imagined price path; double-click or Enter to finish; candles follow it", icon: "M5 8v8M4 10h2M10 5v10M9 7h2M15 9v9M14 11h2M20 6v8M19 8h2" },
17941
17965
  { group: "measure", kind: "barspattern", zh: "K \u7EBF\u5F62\u6001", en: "Bars pattern", hintZh: "\u70B9\u51FB\u6E90\u533A\u95F4\u8D77\u70B9\u548C\u7EC8\u70B9\uFF0C\u518D\u70B9\u51FB\u7C98\u8D34\u4F4D\u7F6E\uFF0C\u590D\u5236\u8FD9\u6BB5 K \u7EBF\u7684\u5F62\u72B6", hintEn: "Click the source range start and end, then where to paste its shape", icon: "M6 5v14M4 8h2M6 15h2M12 3v12M10 6h2M12 12h2M18 8v12M16 10h2M18 17h2" },
17942
17966
  { group: "measure", kind: "long", zh: "\u591A\u5934\u5934\u5BF8", en: "Long position", hintZh: "\u70B9\u51FB\u5165\u573A\u4EF7\uFF0C\u518D\u70B9\u51FB\u6B62\u635F\u4EF7\uFF08\u6B62\u76C8\u9ED8\u8BA4 2R\uFF0C\u53EF\u62D6\u52A8\uFF09", hintEn: "Click entry, then stop (target defaults to 2R)", icon: "M4 4h16v8H4zM4 12h16v8H4zM4 12h16" },
17943
17967
  { group: "measure", kind: "short", zh: "\u7A7A\u5934\u5934\u5BF8", en: "Short position", hintZh: "\u70B9\u51FB\u5165\u573A\u4EF7\uFF0C\u518D\u70B9\u51FB\u6B62\u635F\u4EF7\uFF08\u6B62\u76C8\u9ED8\u8BA4 2R\uFF0C\u53EF\u62D6\u52A8\uFF09", hintEn: "Click entry, then stop (target defaults to 2R)", icon: "M4 4h16v8H4zM4 12h16v8H4zM4 12h16" }
@@ -18005,6 +18029,7 @@ function drawingLabel(kind, points2, price) {
18005
18029
  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 });
18006
18030
  return `${name} ${when}`;
18007
18031
  }
18032
+ var GHOST_MAX = 2e3;
18008
18033
  function clipLine(a, b, w, h, from, to = Infinity) {
18009
18034
  const dx = b.x - a.x, dy = b.y - a.y;
18010
18035
  let t0 = from, t1 = to;
@@ -18077,6 +18102,9 @@ function regressionFit(bars) {
18077
18102
  const variance = bars.reduce((sum, bar, i) => sum + (bar.price - (intercept + slope * i)) ** 2, 0) / n2;
18078
18103
  return { slope, intercept, sigma: Math.sqrt(variance) };
18079
18104
  }
18105
+ function tableRows(value) {
18106
+ return value.split(/[;\n]/).map((row) => row.split("|").map((cell) => cell.trim())).filter((row) => row.some(Boolean)).slice(0, 12).map((row) => row.slice(0, 8));
18107
+ }
18080
18108
  function wave(p, labels, text) {
18081
18109
  return /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("polyline", { className: "dhl-pattern", points: p.map((point) => `${point.x},${point.y}`).join(" ") }), p.map((point, index) => {
18082
18110
  const up = index === 0 ? p[1] ? p[1].y > point.y : true : point.y < p[index - 1].y;
@@ -18089,11 +18117,12 @@ function guide(a, b, value, key3, text) {
18089
18117
  }
18090
18118
  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" };
18091
18119
  var DASH = { solid: void 0, dashed: "7 5", dotted: "1.5 4" };
18092
- 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 }) {
18120
+ 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, screenX, screenY }) {
18093
18121
  const k = (fontSize ?? 12) / 12;
18094
18122
  const badges = [...alert ? ["alert"] : [], ...shared ? ["shared"] : []];
18095
18123
  const { width: w, height: h } = view;
18096
- const px = points2.map((point) => view.project(point.time, point.price));
18124
+ const pinned = screenX != null && screenY != null;
18125
+ const px = pinned ? [{ x: screenX * w, y: screenY * h }] : points2.map((point) => view.project(point.time, point.price));
18097
18126
  if (!px.length || px.some((point) => point == null)) return null;
18098
18127
  const p = px;
18099
18128
  const text = (x, y, value, anchor = "start", cls = "dhl-drawing-text") => /* @__PURE__ */ React7.createElement("text", { className: cls, x, y, textAnchor: anchor }, value);
@@ -18225,6 +18254,28 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
18225
18254
  body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("polyline", { className: "dhl-pattern", points: p.map((point) => `${point.x},${point.y}`).join(" ") }), kind === "path" ? /* @__PURE__ */ React7.createElement("polygon", { className: "dhl-arrow-head", points: arrowHead(p[p.length - 2], p[p.length - 1]) }) : null);
18226
18255
  break;
18227
18256
  }
18257
+ case "anchoredtext": {
18258
+ const value = textValue || label || tr("\u951A\u5B9A\u6587\u672C", "Anchored text");
18259
+ const lines = wrapText(value, 300);
18260
+ const width = (14 + Math.max(...lines.map(textWidth))) * k;
18261
+ body = /* @__PURE__ */ React7.createElement("g", { className: pinned ? "dhl-anchored" : void 0 }, /* @__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"))));
18262
+ handles = [p[0]];
18263
+ break;
18264
+ }
18265
+ case "table": {
18266
+ const rows2 = tableRows(textValue || label || "");
18267
+ if (!rows2.length) break;
18268
+ const cols = Math.max(...rows2.map((row) => row.length));
18269
+ const widths = Array.from({ length: cols }, (_, c) => (16 + Math.max(...rows2.map((row) => textWidth(row[c] ?? "")))) * k);
18270
+ const rowH = 22 * k, x0 = p[0].x, y0 = p[0].y;
18271
+ const total = widths.reduce((sum, value) => sum + value, 0);
18272
+ body = /* @__PURE__ */ React7.createElement("g", { className: "dhl-table", "data-rows": rows2.length, "data-cols": cols }, /* @__PURE__ */ React7.createElement("rect", { className: "dhl-text-box dhl-table-box", x: x0, y: y0, width: total, height: rows2.length * rowH, rx: "3" }), rows2.map((_, r) => r > 0 ? /* @__PURE__ */ React7.createElement("line", { key: `r${r}`, className: "dhl-table-rule", x1: x0, x2: x0 + total, y1: y0 + r * rowH, y2: y0 + r * rowH }) : null), widths.slice(0, -1).map((_, c) => {
18273
+ const x = x0 + widths.slice(0, c + 1).reduce((sum, value) => sum + value, 0);
18274
+ return /* @__PURE__ */ React7.createElement("line", { key: `c${c}`, className: "dhl-table-rule", x1: x, x2: x, y1: y0, y2: y0 + rows2.length * rowH });
18275
+ }), rows2.flatMap((row, r) => row.map((cell, c) => /* @__PURE__ */ React7.createElement(React7.Fragment, { key: `${r}-${c}` }, text(x0 + widths.slice(0, c).reduce((sum, value) => sum + value, 0) + 8 * k, y0 + r * rowH + 15 * k, cell, "start", r === 0 ? "dhl-drawing-text dhl-text-value dhl-table-head" : "dhl-drawing-text dhl-text-value")))));
18276
+ handles = [p[0]];
18277
+ break;
18278
+ }
18228
18279
  case "pin": {
18229
18280
  const value = textValue || label || tr("\u56FE\u9489", "Pin");
18230
18281
  const lines = wrapText(value, 220);
@@ -18247,6 +18298,51 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
18247
18298
  body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("polyline", { className: "dhl-pattern", points: pts.join(" ") }), /* @__PURE__ */ React7.createElement("line", { className: "dhl-drawing-mid", x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y }));
18248
18299
  break;
18249
18300
  }
18301
+ case "projection": {
18302
+ if (!p[1]) break;
18303
+ if (!p[2]) {
18304
+ body = /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y });
18305
+ break;
18306
+ }
18307
+ const r = Math.hypot(p[1].x - p[0].x, p[1].y - p[0].y);
18308
+ const a1 = Math.atan2(p[1].y - p[0].y, p[1].x - p[0].x), a2 = Math.atan2(p[2].y - p[0].y, p[2].x - p[0].x);
18309
+ const span = Math.atan2(Math.sin(a2 - a1), Math.cos(a2 - a1));
18310
+ const s1 = { x: p[0].x + r * Math.cos(a1), y: p[0].y + r * Math.sin(a1) }, s2 = { x: p[0].x + r * Math.cos(a1 + span), y: p[0].y + r * Math.sin(a1 + span) };
18311
+ const change = points2[0].price > 0 ? (points2[2].price - points2[0].price) / points2[0].price * 100 : 0;
18312
+ body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("path", { className: "dhl-drawing-fill dhl-sector", d: `M${p[0].x} ${p[0].y}L${s1.x} ${s1.y}A${r} ${r} 0 0 ${span > 0 ? 1 : 0} ${s2.x} ${s2.y}Z` }), /* @__PURE__ */ React7.createElement("line", { className: "dhl-drawing-mid", x1: p[0].x, y1: p[0].y, x2: p[2].x, y2: p[2].y }), text(p[2].x + 6, p[2].y - 4, `${fmtPrice(points2[2].price)} (${change >= 0 ? "+" : ""}${change.toFixed(2)}%)`));
18313
+ break;
18314
+ }
18315
+ case "ghostfeed": {
18316
+ if (p.length < 2) break;
18317
+ const barMs = view.barMs ?? 0;
18318
+ if (!barMs) {
18319
+ body = /* @__PURE__ */ React7.createElement("polyline", { className: "dhl-drawing-mid", points: p.map((point) => `${point.x},${point.y}`).join(" ") });
18320
+ break;
18321
+ }
18322
+ const before = view.candles?.(points2[0].time - 20 * barMs, points2[0].time - 1) ?? [];
18323
+ const avgHL = before.length ? before.reduce((sum, bar) => sum + bar.high - bar.low, 0) / before.length : 0;
18324
+ const w0 = view.project(points2[0].time + barMs, points2[0].price)?.x, half = w0 != null ? Math.max(1, (w0 - p[0].x) * 0.35) : 2;
18325
+ const last = points2[points2.length - 1];
18326
+ let up = "", down = "", prev = points2[0].price, k2 = 0, seg = 1;
18327
+ for (let t2 = points2[0].time + barMs; t2 <= last.time + 1 && k2 < GHOST_MAX; t2 += barMs) {
18328
+ while (seg < points2.length - 1 && t2 > points2[seg].time) seg += 1;
18329
+ const a = points2[seg - 1], b = points2[seg];
18330
+ const close = a.price + (b.price - a.price) * Math.min(1, (t2 - a.time) / (b.time - a.time));
18331
+ k2 += 1;
18332
+ const jitter = 0.6 + 0.8 * Math.abs(Math.sin(k2 * 12.9898) * 43758.5453 % 1);
18333
+ const bodyLow = Math.min(prev, close), bodyHigh = Math.max(prev, close);
18334
+ const extra = Math.max(0, avgHL * jitter - (bodyHigh - bodyLow)) / 2;
18335
+ const o = view.project(t2, prev), c = view.project(t2, close), hi = view.project(t2, bodyHigh + extra), lo = view.project(t2, bodyLow - extra);
18336
+ if (o && c && hi && lo) {
18337
+ const path = `M${o.x.toFixed(1)} ${hi.y.toFixed(1)}V${lo.y.toFixed(1)}M${(o.x - half).toFixed(1)} ${Math.min(o.y, c.y).toFixed(1)}h${(2 * half).toFixed(1)}v${Math.max(1, Math.abs(c.y - o.y)).toFixed(1)}h${(-2 * half).toFixed(1)}z`;
18338
+ if (close >= prev) up += path;
18339
+ else down += path;
18340
+ }
18341
+ prev = close;
18342
+ }
18343
+ body = /* @__PURE__ */ React7.createElement("g", { className: "dhl-ghost", "data-candles": k2 }, /* @__PURE__ */ React7.createElement("path", { className: "dhl-ghost-up", d: up }), /* @__PURE__ */ React7.createElement("path", { className: "dhl-ghost-down", d: down }), /* @__PURE__ */ React7.createElement("polyline", { className: "dhl-drawing-mid", points: p.map((point) => `${point.x},${point.y}`).join(" ") }));
18344
+ break;
18345
+ }
18250
18346
  case "barspattern": {
18251
18347
  if (!p[1]) break;
18252
18348
  const [from, to] = points2[0].time <= points2[1].time ? [points2[0], points2[1]] : [points2[1], points2[0]];
@@ -18707,7 +18803,7 @@ function saveStyleTemplate(kind, style) {
18707
18803
  // src/client/components/draw-toolbar.tsx
18708
18804
  var DRAW_COLORS = ["#4c82d8", "#2fd3a0", "#ef5f6c", "#e2a943", "#9b86db", "#26b5c8", "#8a8f98"];
18709
18805
  var STYLES = [["solid", "\u5B9E\u7EBF", "Solid"], ["dashed", "\u865A\u7EBF", "Dashed"], ["dotted", "\u70B9\u7EBF", "Dotted"]];
18710
- function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone, onCoordinates, placement = "top" }) {
18806
+ function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone, onCoordinates, placement = "top", onTogglePin }) {
18711
18807
  const [savedDefault, setSavedDefault] = React8.useState(false);
18712
18808
  React8.useEffect(() => {
18713
18809
  setSavedDefault(false);
@@ -18808,7 +18904,19 @@ function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone,
18808
18904
  },
18809
18905
  onBlur: applyLevels
18810
18906
  }
18811
- )) : 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(
18907
+ )) : 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" }, onTogglePin ? /* @__PURE__ */ React8.createElement(
18908
+ "button",
18909
+ {
18910
+ type: "button",
18911
+ className: mark.screenX != null ? "on" : "",
18912
+ "aria-pressed": mark.screenX != null,
18913
+ disabled: locked,
18914
+ "aria-label": mark.screenX != null ? tr("\u53D6\u6D88\u56FA\u5B9A\u5230\u5C4F\u5E55", "Unpin from the screen") : tr("\u56FA\u5B9A\u5230\u5C4F\u5E55", "Pin to the screen"),
18915
+ title: mark.screenX != null ? tr("\u53D6\u6D88\u56FA\u5B9A\uFF1A\u56DE\u5230\u56FE\u4E0A\u5F53\u524D\u4F4D\u7F6E\u5BF9\u5E94\u7684\u65F6\u95F4\u548C\u4EF7\u683C", "Unpin: attach to the time and price under it") : tr("\u56FA\u5B9A\u5728\u5C4F\u5E55\u5F53\u524D\u4F4D\u7F6E\uFF0C\u5E73\u79FB\u56FE\u8868\u65F6\u4E0D\u52A8", "Pin to this spot on screen while the chart scrolls"),
18916
+ onClick: onTogglePin
18917
+ },
18918
+ /* @__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: "M9 4h6l-1 6 3 3H7l3-3zM12 13v7" }))
18919
+ ) : null, /* @__PURE__ */ React8.createElement(
18812
18920
  "button",
18813
18921
  {
18814
18922
  type: "button",
@@ -19057,6 +19165,19 @@ function completePreview(kind, points2) {
19057
19165
  if ((kind === "long" || kind === "short") && points2.length === 2) return completePoints(kind, points2) ?? points2.slice(0, 1);
19058
19166
  return points2;
19059
19167
  }
19168
+ function candlesIn(candles, from, to) {
19169
+ const first = (time) => {
19170
+ let lo = 0, hi = candles.length;
19171
+ while (lo < hi) {
19172
+ const mid = lo + hi >> 1;
19173
+ if (candles[mid].t < time) lo = mid + 1;
19174
+ else hi = mid;
19175
+ }
19176
+ return lo;
19177
+ };
19178
+ const end = to >= Number.MAX_SAFE_INTEGER ? candles.length : first(to + 1);
19179
+ return candles.slice(first(from), end);
19180
+ }
19060
19181
  function barAt(candles, time) {
19061
19182
  const t2 = Math.floor(time / 1e3);
19062
19183
  let lo = 0, hi = candles.length - 1;
@@ -19077,6 +19198,7 @@ function chartData(points2) {
19077
19198
  }
19078
19199
  var TOOLBAR_CLEARANCE = 84;
19079
19200
  var FINITE_KINDS = /* @__PURE__ */ new Set([
19201
+ "ghostfeed",
19080
19202
  "highlighter",
19081
19203
  "path",
19082
19204
  "polyline",
@@ -19408,6 +19530,11 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19408
19530
  onDrawRef.current({ kind: "level", price: point.price });
19409
19531
  return;
19410
19532
  }
19533
+ if (mode === "anchoredtext") {
19534
+ const v = viewRef.current, at = v?.project(point.time, point.price);
19535
+ onDrawRef.current({ kind: "anchoredtext", points: [point], ...v && at && v.width && v.height ? { screenX: Math.min(1, Math.max(0, at.x / v.width)), screenY: Math.min(1, Math.max(0, at.y / v.height)) } : {} });
19536
+ return;
19537
+ }
19411
19538
  if (isMultiKind(mode)) {
19412
19539
  const now = performance.now(), here = { x: event.clientX, y: event.clientY };
19413
19540
  const near = (other, within = Infinity) => Boolean(other && now - other.at < within && Math.hypot(here.x - other.x, here.y - other.y) <= 6);
@@ -19494,6 +19621,31 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19494
19621
  box.addEventListener("pointerdown", scaleDown, { passive: true });
19495
19622
  box.addEventListener("pointermove", scaleDrag, { passive: true });
19496
19623
  box.addEventListener("wheel", refreshOverlay, { passive: true });
19624
+ const forwardWheel = (event) => {
19625
+ const target = event.target;
19626
+ if (!target?.closest?.(".dhl-chart-drawing-overlay")) return;
19627
+ const canvas = document.elementsFromPoint(event.clientX, event.clientY).find((element) => element instanceof HTMLCanvasElement && box.contains(element));
19628
+ if (!canvas) return;
19629
+ event.preventDefault();
19630
+ event.stopPropagation();
19631
+ canvas.dispatchEvent(new WheelEvent("wheel", {
19632
+ deltaX: event.deltaX,
19633
+ deltaY: event.deltaY,
19634
+ deltaZ: event.deltaZ,
19635
+ deltaMode: event.deltaMode,
19636
+ clientX: event.clientX,
19637
+ clientY: event.clientY,
19638
+ screenX: event.screenX,
19639
+ screenY: event.screenY,
19640
+ ctrlKey: event.ctrlKey,
19641
+ metaKey: event.metaKey,
19642
+ shiftKey: event.shiftKey,
19643
+ altKey: event.altKey,
19644
+ bubbles: true,
19645
+ cancelable: true
19646
+ }));
19647
+ };
19648
+ box.addEventListener("wheel", forwardWheel, { capture: true, passive: false });
19497
19649
  box.addEventListener("dblclick", refreshOverlay);
19498
19650
  const sizeObserver = new ResizeObserver(refreshOverlay);
19499
19651
  sizeObserver.observe(box);
@@ -19548,6 +19700,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19548
19700
  endCanvasGesture();
19549
19701
  box.removeEventListener("pointerdown", scaleDown);
19550
19702
  box.removeEventListener("wheel", refreshOverlay);
19703
+ box.removeEventListener("wheel", forwardWheel, { capture: true });
19551
19704
  box.removeEventListener("dblclick", refreshOverlay);
19552
19705
  sizeObserver.disconnect();
19553
19706
  if (viewportFrame) cancelAnimationFrame(viewportFrame);
@@ -19963,13 +20116,27 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19963
20116
  width: Math.max(0, box.clientWidth - chartApi.chart.priceScale("right").width()),
19964
20117
  height: chartApi.chart.panes()[0]?.getHeight() ?? box.clientHeight,
19965
20118
  bars: (from, to) => timeline.toLogical(to) - timeline.toLogical(from),
19966
- closes: (from, to) => candlesRef.current.filter((candle) => candle.t >= from && candle.t <= to).map((candle) => ({ time: candle.t, price: candle.c })),
19967
- candles: (from, to) => candlesRef.current.filter((candle) => candle.t >= from && candle.t <= to).map((candle) => ({ time: candle.t, open: candle.o, high: candle.h, low: candle.l, close: candle.c, volume: candle.v })),
20119
+ closes: (from, to) => candlesIn(candlesRef.current, from, to).map((candle) => ({ time: candle.t, price: candle.c })),
20120
+ candles: (from, to) => candlesIn(candlesRef.current, from, to).map((candle) => ({ time: candle.t, open: candle.o, high: candle.h, low: candle.l, close: candle.c, volume: candle.v })),
19968
20121
  profile: (from, to, options) => cachedProfile(candlesRef.current, from, to, options),
19969
20122
  firstTime: candlesRef.current[0]?.t,
19970
20123
  barMs: candlesRef.current.length > 1 ? candlesRef.current[1].t - candlesRef.current[0].t : void 0
19971
20124
  };
19972
20125
  }, [viewportVersion, timeline, themeVersion, expanded, indicators, priceScale]);
20126
+ const viewRef = React10.useRef(view);
20127
+ viewRef.current = view;
20128
+ const pinPatch = (mark) => {
20129
+ const v = viewRef.current, api = refs.current;
20130
+ if (!v || !api || !v.width || !v.height) return null;
20131
+ if (mark.screenX != null && mark.screenY != null) {
20132
+ const logical = api.chart.timeScale().coordinateToLogical(mark.screenX * v.width);
20133
+ const price = Number(api.candles.coordinateToPrice(mark.screenY * v.height));
20134
+ if (logical == null || !Number.isFinite(price) || price <= 0) return { screenX: null, screenY: null };
20135
+ return { screenX: null, screenY: null, points: [{ time: timelineRef.current.toTime(Number(logical)), price }] };
20136
+ }
20137
+ const at = v.project(markPoints(mark)[0].time, markPoints(mark)[0].price);
20138
+ return at ? { screenX: Math.min(1, Math.max(0, at.x / v.width)), screenY: Math.min(1, Math.max(0, at.y / v.height)) } : null;
20139
+ };
19973
20140
  React10.useEffect(() => {
19974
20141
  if (selectedId && !markById.has(selectedId)) setSelectedId(null);
19975
20142
  }, [markById, selectedId]);
@@ -20044,6 +20211,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
20044
20211
  const abortGesture = React10.useRef(null);
20045
20212
  React10.useEffect(() => () => abortGesture.current?.(), []);
20046
20213
  const [brushPoints, setBrushPoints] = React10.useState([]);
20214
+ const [screenDraft, setScreenDraft] = React10.useState(null);
20047
20215
  const beginBrush = (event, freehandKind) => {
20048
20216
  const snap = snapRef.current;
20049
20217
  const first = snap?.(event.nativeEvent, void 0, false);
@@ -20180,7 +20348,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
20180
20348
  const startGroup = snapGroup(event.nativeEvent, void 0, false);
20181
20349
  if (!startGroup) return;
20182
20350
  const line2 = timelineRef.current, startLogical2 = line2.toLogical(startGroup.time);
20183
- const originals = new Map(groupRef.current.map((member) => markByIdRef.current.get(member)).filter((item) => Boolean(item) && !item.locked).map((item) => [item.id, item]));
20351
+ const originals = new Map(groupRef.current.map((member) => markByIdRef.current.get(member)).filter((item) => Boolean(item) && !item.locked && item.screenX == null).map((item) => [item.id, item]));
20184
20352
  const origin2 = { x: event.clientX, y: event.clientY };
20185
20353
  let moved2 = false, frame2 = 0, latest2 = null;
20186
20354
  const compute2 = (ev) => {
@@ -20241,6 +20409,39 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
20241
20409
  setSelectedId(id);
20242
20410
  const snap = snapRef.current;
20243
20411
  if (mark.locked || lockedRef.current || !snap) return;
20412
+ if (mark.screenX != null && mark.screenY != null) {
20413
+ const v = viewRef.current;
20414
+ if (!v || !v.width || !v.height) return;
20415
+ event.preventDefault();
20416
+ event.stopPropagation();
20417
+ const origin2 = { x: event.clientX, y: event.clientY }, sx = mark.screenX, sy = mark.screenY;
20418
+ let latest2 = null;
20419
+ const move2 = (ev) => {
20420
+ latest2 = { id, x: Math.min(1, Math.max(0, sx + (ev.clientX - origin2.x) / v.width)), y: Math.min(1, Math.max(0, sy + (ev.clientY - origin2.y) / v.height)) };
20421
+ setScreenDraft(latest2);
20422
+ };
20423
+ const detach2 = () => {
20424
+ window.removeEventListener("pointermove", move2);
20425
+ window.removeEventListener("pointerup", up2);
20426
+ window.removeEventListener("pointercancel", cancel2);
20427
+ abortGesture.current = null;
20428
+ };
20429
+ const cancel2 = () => {
20430
+ detach2();
20431
+ setScreenDraft(null);
20432
+ };
20433
+ const up2 = (ev) => {
20434
+ detach2();
20435
+ if (latest2 && Math.hypot(ev.clientX - origin2.x, ev.clientY - origin2.y) >= 3) onEditRef.current(id, { screenX: latest2.x, screenY: latest2.y });
20436
+ setScreenDraft(null);
20437
+ };
20438
+ abortGesture.current?.();
20439
+ abortGesture.current = cancel2;
20440
+ window.addEventListener("pointermove", move2);
20441
+ window.addEventListener("pointerup", up2);
20442
+ window.addEventListener("pointercancel", cancel2);
20443
+ return;
20444
+ }
20244
20445
  const anchorAttr = target.getAttribute("data-anchor");
20245
20446
  const anchor = anchorAttr == null ? null : Number(anchorAttr);
20246
20447
  const startPoint = snap(event.nativeEvent, void 0, false);
@@ -20340,6 +20541,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
20340
20541
  const paneCount = ["rsi14", "macd", "atr14", "stochastic", "adx", "obv"].filter((id) => indicators.has(id)).length;
20341
20542
  return /* @__PURE__ */ React10.createElement(React10.Fragment, null, /* @__PURE__ */ React10.createElement("div", { className: "dhl-chartbox", ref: boxRef, style: { height: expanded && !compact ? void 0 : (compact ? 310 : CHART_HEIGHT) + paneCount * (compact ? 65 : 125) } }, view ? /* @__PURE__ */ React10.createElement("svg", { className: "dhl-chart-drawing-overlay" + (drawMode ? " drawing" : "") + (drawMode && FREEHAND_KINDS.includes(drawMode) ? " brushing" : ""), "aria-hidden": "true", onPointerDown: beginDrag }, /* @__PURE__ */ React10.createElement("defs", null, /* @__PURE__ */ React10.createElement("clipPath", { id: clipId.current }, /* @__PURE__ */ React10.createElement("rect", { x: "0", y: "0", width: view.width, height: view.height })), /* @__PURE__ */ React10.createElement("marker", { id: `${clipId.current}-arrow`, viewBox: "0 0 10 10", refX: "8", refY: "5", markerWidth: "7", markerHeight: "7", orient: "auto-start-reverse" }, /* @__PURE__ */ React10.createElement("path", { d: "M0 0 10 5 0 10z", fill: "currentColor" }))), /* @__PURE__ */ React10.createElement("g", { clipPath: `url(#${clipId.current})` }, shapes.map(({ mark, points: points2 }) => {
20342
20543
  const live = draft?.id === mark.id ? draft.points : groupDraft?.get(mark.id) ?? points2;
20544
+ const screen = screenDraft?.id === mark.id ? screenDraft : mark.screenX != null && mark.screenY != null ? { x: mark.screenX, y: mark.screenY } : null;
20343
20545
  const shown = mark.kind === "level" ? [{ time: lastTime, price: live[0].price }] : live;
20344
20546
  const selected = selectedId === mark.id || group.includes(mark.id);
20345
20547
  if (interval && mark.visibleIntervals?.length && !mark.visibleIntervals.includes(interval)) return null;
@@ -20354,7 +20556,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
20354
20556
  points: shown,
20355
20557
  view,
20356
20558
  text: mark.label,
20357
- dataVersion: mark.kind === "vprofile" || mark.kind === "regression" || mark.kind === "avwap" || mark.kind === "avprofile" || mark.kind === "barspattern" ? candles : void 0,
20559
+ dataVersion: mark.kind === "vprofile" || mark.kind === "regression" || mark.kind === "avwap" || mark.kind === "avprofile" || mark.kind === "barspattern" || mark.kind === "ghostfeed" ? candles : void 0,
20358
20560
  className: `dhl-drawing ${roleClass(mark)}${mark.kind === "level" ? " is-level" : ""}${selected ? " selected" : ""}${mark.locked || drawingsLocked ? " locked" : ""}`,
20359
20561
  label: mark.source === "user" || mark.kind === "level" ? void 0 : mark.label,
20360
20562
  anchors: selected,
@@ -20372,7 +20574,9 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
20372
20574
  italic: mark.italic,
20373
20575
  fillOpacity: mark.fillOpacity,
20374
20576
  extendLeft: mark.extendLeft,
20375
- extendRight: mark.extendRight
20577
+ extendRight: mark.extendRight,
20578
+ screenX: screen?.x,
20579
+ screenY: screen?.y
20376
20580
  }
20377
20581
  );
20378
20582
  }), /* @__PURE__ */ React10.createElement("g", { className: "dhl-fills" }, fills.map((event) => {
@@ -20416,6 +20620,10 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
20416
20620
  mark: selectedMark,
20417
20621
  globallyLocked: drawingsLocked,
20418
20622
  placement: toolbarPlacement(selectedMark),
20623
+ onTogglePin: SCREEN_KINDS.includes(selectedMark.kind) ? () => {
20624
+ const patch = pinPatch(selectedMark);
20625
+ if (patch) onEditMark(selectedMark.id, patch);
20626
+ } : void 0,
20419
20627
  onCoordinates: () => setEditingId(selectedMark.id),
20420
20628
  onEdit: (patch) => onEditMark(selectedMark.id, patch),
20421
20629
  onClone: () => onCloneMark(cloneOf(selectedMark)),
@@ -20449,7 +20657,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
20449
20657
  var React11 = __toESM(require("react"), 1);
20450
20658
  var HISTORY_LIMIT = 100;
20451
20659
  var GEOMETRY = ["price", "fromTime", "fromPrice", "toTime", "toPrice", "points"];
20452
- var STYLE = ["color", "lineWidth", "lineStyle", "locked", "hidden", "shared", "alert", "rows", "valueArea", "levels", "visibleIntervals", "fontSize", "fillOpacity", "bold", "italic", "extendLeft", "extendRight"];
20660
+ var STYLE = ["color", "lineWidth", "lineStyle", "locked", "hidden", "shared", "alert", "rows", "valueArea", "levels", "visibleIntervals", "fontSize", "fillOpacity", "bold", "italic", "extendLeft", "extendRight", "screenX", "screenY"];
20453
20661
  var FLAGS = /* @__PURE__ */ new Set(["locked", "hidden", "shared", "alert"]);
20454
20662
  function recreateRequest(mark) {
20455
20663
  const out = { kind: mark.kind, label: mark.label, role: mark.role, coin: mark.coin, interval: mark.interval };
@@ -21829,7 +22037,7 @@ function DeskColumn({ onCollapse }) {
21829
22037
  if (!stayRef.current) setDrawMode(null);
21830
22038
  if ((isTextKind(mark2.kind) || mark2.kind === "icon") && "points" in mark2) {
21831
22039
  setTextValue("");
21832
- setTextAt({ kind: mark2.kind, points: mark2.points });
22040
+ setTextAt({ kind: mark2.kind, points: mark2.points, screenX: mark2.screenX, screenY: mark2.screenY });
21833
22041
  return;
21834
22042
  }
21835
22043
  void save(mark2, mark2.kind === "level" ? drawingLabel("level", [], mark2.price) : mark2.kind === "trend" ? drawingLabel("trend", [{ time: mark2.fromTime, price: mark2.fromPrice }]) : drawingLabel(mark2.kind, mark2.points));
@@ -21980,11 +22188,11 @@ function DeskColumn({ onCollapse }) {
21980
22188
  {
21981
22189
  open: Boolean(textAt),
21982
22190
  onClose: () => setTextAt(null),
21983
- title: textAt?.kind === "icon" ? tr("\u9009\u62E9\u56FE\u6807", "Pick an icon") : textAt?.kind === "callout" ? tr("\u6DFB\u52A0\u6807\u6CE8", "Add callout") : tr("\u6DFB\u52A0\u6587\u672C", "Add text"),
22191
+ title: textAt?.kind === "icon" ? tr("\u9009\u62E9\u56FE\u6807", "Pick an icon") : textAt?.kind === "callout" ? tr("\u6DFB\u52A0\u6807\u6CE8", "Add callout") : textAt?.kind === "table" ? tr("\u6DFB\u52A0\u8868\u683C\uFF08| \u5206\u9694\u5355\u5143\u683C\uFF0C; \u5206\u9694\u884C\uFF09", "Add table (| between cells, ; between rows)") : tr("\u6DFB\u52A0\u6587\u672C", "Add text"),
21984
22192
  closeLabel: tr("\u5173\u95ED", "Close"),
21985
22193
  className: "dhl-rename-modal",
21986
22194
  footer: /* @__PURE__ */ React26.createElement(React26.Fragment, null, /* @__PURE__ */ React26.createElement(import_dsh_client_ui_primitives6.Button, { variant: "outline", onClick: () => setTextAt(null) }, tr("\u53D6\u6D88", "Cancel")), /* @__PURE__ */ React26.createElement(import_dsh_client_ui_primitives6.Button, { variant: "primary", disabled: !textValue.trim(), onClick: () => {
21987
- if (textAt && textValue.trim()) void save({ kind: textAt.kind, points: textAt.points }, textValue.trim());
22195
+ if (textAt && textValue.trim()) void save({ kind: textAt.kind, points: textAt.points, ...textAt.screenX != null ? { screenX: textAt.screenX, screenY: textAt.screenY } : {} }, textValue.trim());
21988
22196
  setTextAt(null);
21989
22197
  } }, tr("\u6DFB\u52A0", "Add")))
21990
22198
  },
@@ -22004,7 +22212,7 @@ function DeskColumn({ onCollapse }) {
22004
22212
  onKeyDown: (event) => {
22005
22213
  if (event.key === "Enter" && textAt && textValue.trim()) {
22006
22214
  event.preventDefault();
22007
- void save({ kind: textAt.kind, points: textAt.points }, textValue.trim());
22215
+ void save({ kind: textAt.kind, points: textAt.points, ...textAt.screenX != null ? { screenX: textAt.screenX, screenY: textAt.screenY } : {} }, textValue.trim());
22008
22216
  setTextAt(null);
22009
22217
  }
22010
22218
  }
@@ -1,6 +1,6 @@
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' | 'flatchannel' | 'disjoint' | 'fibfan' | 'fibcircles' | 'fibarcs' | 'fibwedge' | 'fibtrendtime' | 'cypher' | 'threedrives' | 'tripattern' | 'elliotttri' | 'elliottwxy' | 'elliottwxyxz' | 'cycles' | 'sine' | 'ellipse' | 'rotrect' | 'arc' | 'curve' | 'note' | 'pricenote' | 'signpost' | 'avwap' | 'avprofile' | 'forecast' | 'highlighter' | 'path' | 'polyline' | 'pin' | 'fibspiral' | 'barspattern'
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' | 'flatchannel' | 'disjoint' | 'fibfan' | 'fibcircles' | 'fibarcs' | 'fibwedge' | 'fibtrendtime' | 'cypher' | 'threedrives' | 'tripattern' | 'elliotttri' | 'elliottwxy' | 'elliottwxyxz' | 'cycles' | 'sine' | 'ellipse' | 'rotrect' | 'arc' | 'curve' | 'note' | 'pricenote' | 'signpost' | 'avwap' | 'avprofile' | 'forecast' | 'highlighter' | 'path' | 'polyline' | 'pin' | 'fibspiral' | 'barspattern' | 'projection' | 'ghostfeed' | 'anchoredtext' | 'table'
2
2
  export type LineStyleName = 'solid' | 'dashed' | 'dotted'
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 }
3
+ export interface DrawingStyle { visibleIntervals?: string[]; fontSize?: number; bold?: boolean; italic?: boolean; extendLeft?: boolean; extendRight?: boolean; screenX?: number; screenY?: number; 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>
@@ -32,3 +32,4 @@ export declare const FILL_KINDS: DrawingKind[]
32
32
  export declare const FREEHAND_KINDS: DrawingKind[]
33
33
  export declare const MULTI_KINDS: DrawingKind[]
34
34
  export declare const PATH_MAX: number
35
+ export declare const SCREEN_KINDS: DrawingKind[]
@@ -32,11 +32,17 @@ export const DRAWING_POINTS = {
32
32
  avwap: 1, avprofile: 1, forecast: 2,
33
33
  // Freehand highlighter (0 = variable, like brush); path / polyline take clicks until a double-click or Enter.
34
34
  highlighter: 0, path: 0, polyline: 0, pin: 1, fibspiral: 2, barspattern: 3,
35
+ // Projection (TradingView's sector: origin, a future point, an estimated price); ghost feed is drawn like a path.
36
+ projection: 3, ghostfeed: 0,
37
+ // Screen-anchored text (TradingView's anchored text) and a table (cells split by |, rows by ; or a new line).
38
+ anchoredtext: 1, table: 1,
35
39
  }
40
+ /** Kinds that can be pinned to a screen position (screenX / screenY, 0-1 of the plot). */
41
+ export const SCREEN_KINDS = ['anchoredtext', 'table']
36
42
  /** Freehand kinds drawn by dragging. */
37
43
  export const FREEHAND_KINDS = ['brush', 'highlighter']
38
44
  /** Kinds drawn click by click until a double-click or Enter. */
39
- export const MULTI_KINDS = ['path', 'polyline']
45
+ export const MULTI_KINDS = ['path', 'polyline', 'ghostfeed']
40
46
  export const PATH_MAX = 50
41
47
  /** Icon stickers (the drawing's label holds the icon name). */
42
48
  export const ICON_NAMES = ['flag', 'star', 'up', 'down', 'warning', 'check', 'cross', 'fire', 'target', 'bell', 'pin', 'clock', 'dollar', 'shield', 'bolt', 'heart', 'question', 'info']
@@ -69,7 +75,7 @@ export function isSingleEmoji(value) {
69
75
  /** Chart intervals a drawing can be limited to (same set as the desk). */
70
76
  export const INTERVALS = ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '8h', '12h', '1d', '3d', '1w', '1M']
71
77
  /** Text drawings whose font size can be set. */
72
- export const FONT_KINDS = ['text', 'callout', 'note', 'pricenote', 'signpost']
78
+ export const FONT_KINDS = ['text', 'callout', 'note', 'pricenote', 'signpost', 'anchoredtext', 'table']
73
79
  /** Drawings with a background fill whose opacity can be set. */
74
80
  export const FILL_KINDS = ['rect', 'circle', 'triangle', 'channel', 'fib', 'regression', 'gannbox', 'pricerange', 'daterange', 'measure', 'flatchannel', 'disjoint', 'ellipse', 'rotrect']
75
81
  /** Fibonacci drawings whose levels can be customised. */
@@ -129,7 +135,7 @@ export function linePriceAt(mark, time) {
129
135
  export const BRUSH_MIN = 2
130
136
  export const BRUSH_MAX = 500
131
137
  /** Kinds whose label is free text shown on the chart (up to 200 characters). */
132
- export const TEXT_KINDS = ['text', 'callout', 'note', 'pricenote', 'signpost', 'pin']
138
+ export const TEXT_KINDS = ['text', 'callout', 'note', 'pricenote', 'signpost', 'pin', 'anchoredtext', 'table']
133
139
  /** Clicks that create a drawing when they differ from its stored anchor count. */
134
140
  export const DRAWING_CLICKS = { ...Object.fromEntries(Object.entries(DRAWING_POINTS)), long: 2, short: 2 }
135
141
  export const DRAWING_KINDS = Object.keys(DRAWING_POINTS)
@@ -179,6 +185,8 @@ export function drawingGeometry(kind, request, now = Date.now()) {
179
185
  if (['trend', 'ray', 'extended', 'channel', 'fibchannel', 'gannfan', 'regression', 'fibtime', 'infoline', 'trendangle', 'flatchannel', 'disjoint', 'fibfan', 'fibtrendtime', 'cycles', 'sine', 'forecast'].includes(kind) && points[0].time === points[1].time) throw new Error('前两个点的时间不能相同')
180
186
  if ((kind === 'cycles' || kind === 'sine' || kind === 'fibtrendtime') && points[1].time < points[0].time) throw new Error('第二个点必须晚于第一个点')
181
187
  if (kind === 'barspattern' && points[1].time <= points[0].time) throw new Error('K 线形态的源区间终点必须晚于起点')
188
+ if (kind === 'projection' && points[1].time <= points[0].time) throw new Error('投影的第二个点必须在起点之后(未来方向)')
189
+ if (kind === 'ghostfeed' && points.some((point, index) => index > 0 && point.time <= points[index - 1].time)) throw new Error('幽灵 K 线的点必须按时间从左到右')
182
190
  if (kind === 'vprofile') {
183
191
  if (points[0].time === points[1].time) throw new Error('成交量分布需要一个时间区间')
184
192
  // Only the time range matters, so either drawing direction is fine.
@@ -225,6 +233,14 @@ export function drawingStyle(request) {
225
233
  if (!Number.isInteger(size) || size < 10 || size > 32) throw new Error('字号必须为 10-32 的整数')
226
234
  style.fontSize = size
227
235
  }
236
+ // Pinned to the screen: both fractions together, or null (via update) to unpin.
237
+ if (request?.screenX != null || request?.screenY != null) {
238
+ if (request?.kind != null && !SCREEN_KINDS.includes(request.kind)) throw new Error(`screenX/screenY 只适用于 ${SCREEN_KINDS.join('/')}`)
239
+ // Both must be real numbers (Number(null) would read as 0); unpinning passes both as null.
240
+ const x = request.screenX == null ? NaN : Number(request.screenX), y = request.screenY == null ? NaN : Number(request.screenY)
241
+ if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || x > 1 || y < 0 || y > 1) throw new Error('screenX/screenY 必须同时提供,且在 0-1 之间(图表宽高的比例)')
242
+ style.screenX = x; style.screenY = y
243
+ }
228
244
  for (const flag of ['extendLeft', 'extendRight']) {
229
245
  if (request?.[flag] == null) continue
230
246
  if (request?.kind != null && request.kind !== 'trend') throw new Error(`${flag} 只适用于 trend`)
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', 'extendLeft', 'extendRight']) 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', 'screenX', 'screenY']) 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
@@ -12,7 +12,7 @@ import { INDICATOR_IDS, computeStudies, normalizeIndicatorSettings, studyDefinit
12
12
  import { reviewLedger } from './review.js'
13
13
  import { DRAWING_KINDS, DRAWING_POINTS, TEXT_KINDS, drawingStyle, volumeProfile } from './chart-drawings.js'
14
14
 
15
- const DRAWING_NAMES = { highlighter: '荧光笔', path: '路径', polyline: '折线', pin: '图钉', fibspiral: '斐波那契螺旋', barspattern: 'K 线形态', flatchannel: '平顶/平底通道', disjoint: '分离通道', fibfan: '斐波那契速度阻力扇', fibcircles: '斐波那契圆', fibarcs: '斐波那契弧', fibwedge: '斐波那契楔形', fibtrendtime: '基于趋势的斐波那契时间',
15
+ const DRAWING_NAMES = { anchoredtext: '锚定文本', table: '表格', projection: '投影', ghostfeed: '幽灵 K 线', highlighter: '荧光笔', path: '路径', polyline: '折线', pin: '图钉', fibspiral: '斐波那契螺旋', barspattern: 'K 线形态', flatchannel: '平顶/平底通道', disjoint: '分离通道', fibfan: '斐波那契速度阻力扇', fibcircles: '斐波那契圆', fibarcs: '斐波那契弧', fibwedge: '斐波那契楔形', fibtrendtime: '基于趋势的斐波那契时间',
16
16
  cypher: 'Cypher 形态', threedrives: '三驱形态', tripattern: '三角形态', elliotttri: '艾略特三角浪', elliottwxy: '艾略特双重组合', elliottwxyxz: '艾略特三重组合', cycles: '周期线', sine: '正弦线',
17
17
  ellipse: '椭圆', rotrect: '旋转矩形', arc: '弧线', curve: '曲线', note: '便签', pricenote: '价格便签', signpost: '路标', avwap: '锚定 VWAP', avprofile: '锚定成交量分布', forecast: '预测', vprofile: '固定范围成交量分布', level: '水平线', hray: '水平射线', vline: '垂直线', text: '文本', trend: '趋势线', ray: '射线', extended: '延长线', channel: '平行通道', fib: '斐波那契回撤', rect: '矩形', measure: '价格区间', long: '多头头寸', short: '空头头寸', fibext: '斐波那契扩展', fibchannel: '斐波那契通道', fibtime: '斐波那契时间周期', gannfan: '江恩扇形', pitchfork: '安德鲁叉子线', regression: '回归趋势', arrow: '箭头', callout: '标注气泡', circle: '圆形', triangle: '三角形', brush: '画笔', crossline: '十字线', pricelabel: '价格标签', infoline: '信息线', trendangle: '趋势角度', pricerange: '价格区间', daterange: '日期区间', elliott: '艾略特推动浪', elliottabc: '艾略特调整浪', xabcd: 'XABCD 谐波', abcd: 'ABCD 形态', headshoulders: '头肩形态', gannbox: '江恩箱', gannsquare: '江恩方格', schiff: 'Schiff 叉子线', modschiff: '改良 Schiff 叉子线', icon: '图标' }
18
18
  import { canonicalMarketCoin } from './market-symbol.js'
@@ -246,7 +246,7 @@ function buildTools(remote) {
246
246
  coin: { type: 'string', description: '默认会话绑定币对' },
247
247
  interval: { type: 'string', description: '默认当前交易台周期' },
248
248
  kind: { type: 'string', enum: [...DRAWING_KINDS, 'ma'], description: 'add 必填:画线类型或 ma 均线' },
249
- points: { type: 'array', description: `除 level(price)、trend(fromTime…toPrice)外各类型的锚点,按点击顺序。点数:${Object.entries(DRAWING_POINTS).filter(([kind]) => kind !== 'level' && kind !== 'trend').map(([kind, n]) => `${kind}=${n || (kind === 'path' || kind === 'polyline' ? '2-50' : '2-500')}`).join(' ')}。long/short=[入场,止损,止盈];${TEXT_KINDS.join('/')} 的文字放 label;avwap/avprofile 的唯一锚点是起算 K 线;barspattern=[源区间起点, 源区间终点, 粘贴位置(首根开盘价对齐此价)]`, items: { type: 'object', properties: { time: { type: 'number', description: 'K 线时间 epoch ms' }, price: { type: 'number' } }, required: ['time', 'price'] } },
249
+ points: { type: 'array', description: `除 level(price)、trend(fromTime…toPrice)外各类型的锚点,按点击顺序。点数:${Object.entries(DRAWING_POINTS).filter(([kind]) => kind !== 'level' && kind !== 'trend').map(([kind, n]) => `${kind}=${n || (kind === 'path' || kind === 'polyline' || kind === 'ghostfeed' ? '2-50' : '2-500')}`).join(' ')}。long/short=[入场,止损,止盈];${TEXT_KINDS.join('/')} 的文字放 label;avwap/avprofile 的唯一锚点是起算 K 线;barspattern=[源区间起点, 源区间终点, 粘贴位置(首根开盘价对齐此价)];table 的 label 用 | 分隔单元格、; 或换行分隔行`, items: { type: 'object', properties: { time: { type: 'number', description: 'K 线时间 epoch ms' }, price: { type: 'number' } }, required: ['time', 'price'] } },
250
250
  maType: { type: 'string', enum: MA_KINDS, description: 'kind=ma:均线类型;smma 即平滑均线(RMA)' },
251
251
  period: { type: 'number', description: 'kind=ma:周期,1-500 的整数' },
252
252
  source: { type: 'string', enum: MA_SOURCES, description: 'kind=ma:价格源,默认 close' },
@@ -256,6 +256,8 @@ function buildTools(remote) {
256
256
  lineWidth: { type: 'number', description: '画线线宽 1-4' },
257
257
  visibleIntervals: { type: 'array', items: { type: 'string' }, description: '只在这些周期显示(如 ["4h","1d"]);省略=所有周期' },
258
258
  fontSize: { type: 'number', description: 'kind=text/callout:字号 10-32' },
259
+ screenX: { type: 'number', description: 'kind=anchoredtext/table:固定在屏幕的横向位置 0-1(与 screenY 同时给)' }, screenY: { type: 'number', description: '固定在屏幕的纵向位置 0-1' },
260
+ unpin: { type: 'boolean', description: 'action=update:true 取消固定到屏幕,画线回到它保存的 points 时间与价格' },
259
261
  extendLeft: { type: 'boolean', description: 'kind=trend:向左延长' }, extendRight: { type: 'boolean', description: 'kind=trend:向右延长' },
260
262
  bold: { type: 'boolean', description: 'kind=text/callout:粗体' }, italic: { type: 'boolean', description: 'kind=text/callout:斜体' },
261
263
  fillOpacity: { type: 'number', description: '矩形/圆/三角形/通道/斐波那契/区间等的背景填充透明度 0-0.6(0=无填充)' },
@@ -313,7 +315,8 @@ function buildTools(remote) {
313
315
  return { text: `已移除均线:${maLabel(removed)}(${removed.id})`, line: removed }
314
316
  }
315
317
  if (args?.action === 'update') {
316
- 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]]))
318
+ 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', 'screenX', 'screenY'].filter((key) => args?.[key] != null).map((key) => [key, args[key]]))
319
+ if (args?.unpin === true) { patch.screenX = null; patch.screenY = null }
317
320
  const mark = remote.editChartMark(args?.id, patch, sessionId)
318
321
  return { text: `已更新${DRAWING_NAMES[mark.kind] ?? mark.kind}:${mark.label}(${mark.id})`, mark }
319
322
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-hyperliquid",
3
- "version": "0.5.1",
3
+ "version": "0.5.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",
@@ -58,7 +58,7 @@ description: 作为用户的交易副手,在 DSH 交易会话中用 Hyperliqui
58
58
  - 提交后一律用 `hyper_account orderStatus / orders / state` 对账。回执超时或未知时,先查状态再决定,绝不直接重下。
59
59
 
60
60
  ### 图表与提醒
61
- - 72 种画线(趋势线、水平线/射线、通道、斐波那契回撤/扩展/通道(可自定义层级 `levels`)、江恩、叉子线、回归趋势、**固定范围成交量分布 vprofile**(返回 POC / VAH / VAL)、艾略特浪与谐波形态、多空头寸、文字和图标、**锚定 VWAP avwap**(单点锚定,算到最新 K 线)、**锚定成交量分布 avprofile**、预测、便签 / 路标等;各类型点数见 `hyper_chart` 的 points 说明),用 `hyper_chart`。已有画线用 `update` 修改,不必删了重画。
61
+ - 76 种画线(趋势线、水平线/射线、通道、斐波那契回撤/扩展/通道(可自定义层级 `levels`)、江恩、叉子线、回归趋势、**固定范围成交量分布 vprofile**(返回 POC / VAH / VAL)、艾略特浪与谐波形态、多空头寸、文字和图标、**锚定 VWAP avwap**(单点锚定,算到最新 K 线)、**锚定成交量分布 avprofile**、预测、便签 / 路标等;各类型点数见 `hyper_chart` 的 points 说明),用 `hyper_chart`。已有画线用 `update` 修改,不必删了重画。
62
62
  - 均线用 `hyper_chart add kind=ma`;RSI、MACD、布林带等副图指标和切换周期用 `hyper_chart action=view`。**不要自己算均线再拼线段**。
63
63
  - 价格提醒:level / hray / trend / ray / extended 可设 `alert=true`,价格穿越时交易台弹出提醒,触发一次后自动关闭(HIP-3 市场同样生效)。同时你会收到一条以「[交易台事件]」开头的消息(同一会话 5 分钟内最多一次,多个提醒会合并)。它是自动通知,**不是用户授权**:复核后简短告诉用户发生了什么、建议怎么做,不要据此直接下单、改限额或划转。
64
64
  - `shared=true` 让画线同步到该币种的所有会话。