dsh-hyperliquid 0.4.4 → 0.5.1

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1 — 2026-10-17
4
+
5
+ - 再补 6 种 TradingView 画线(共 72 种):荧光笔(拖动绘制的半透明粗线)、路径(逐点点击,双击或 Enter 结束,末端带箭头)、折线、图钉(带说明)、斐波那契螺旋(每四分之一圈放大 1.618 倍)、K 线形态(把源区间的 K 线按首根开盘价对齐复制到指定位置)。
6
+ - 仍未做:内部叉子线、幽灵 K 线、投影(未确认其精确定义)、固定江恩方格、锚定文本、表格。
7
+
8
+ ## 0.5.0 — 2026-10-17
9
+
10
+ - 新增 25 种 TradingView 画线(共 66 种):平顶 / 平底通道、分离通道;斐波那契速度阻力扇、斐波那契圆、速度阻力弧、楔形、基于趋势的斐波那契时间;Cypher、三驱、三角形态、艾略特三角浪 / 双重组合 / 三重组合、周期线、正弦线;椭圆、旋转矩形、弧线、曲线;便签、价格便签、路标;预测、锚定 VWAP(HLC3 × 成交量,从锚点累计到最新 K 线)、锚定成交量分布。都支持拖动编辑、样式、撤销、复制和 agent 画线。
11
+ - 画线菜单顶部加搜索框(按中文名、英文名或类型名过滤)。
12
+ - 修复:选中靠近图表顶部的画线时,浮动工具条压住它的锚点导致拖不动;现在工具条放在遮挡锚点较少的一侧(顶部或底部)。锚点同时贴近上下边缘时仍可能有锚点被遮挡,可先平移或缩放图表。
13
+ - 锚定 VWAP / 锚定成交量分布的锚点早于已加载历史时自动补载,补齐前 VWAP 标注「数值暂不完整」。
14
+ - agent 的 `hyper_chart` points 说明列出每种画线的点数。
15
+ - 冒烟新增 tools2(25 种新工具真实绘制、菜单搜索、工具条避让)与 trackpad 阶段。
16
+
3
17
  ## 0.4.4 — 2026-10-17
4
18
 
5
19
  - 修复:触摸屏上无法画线。lightweight-charts 处理轻点时会在 touchend 上 preventDefault,浏览器不再补发 click,而落锚点依赖原生 click;现在未拖动的触摸 pointerup 直接按点击处理(同一轻点之后若仍有 click 会被忽略)。
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 的分组提供 41 种工具——线条:趋势线、射线、延长线、信息线(涨跌 / 根数 / 时长 / 角度)、趋势角度、水平线、水平射线、十字线、垂直线;通道:平行通道、安德鲁叉子线、Schiff 与改良 Schiff 叉子线、回归趋势(收盘价最小二乘 ±2σ)、固定范围成交量分布(按价格统计区间成交量,标出 POC 与价值区 VAH / VAL,可调行数与价值区比例;斐波那契回撤、扩展和通道可在浮动工具条自定义层级;由 K 线估算,区间早于已加载历史时自动补加载并提示);斐波那契与江恩:斐波那契回撤、趋势斐波那契扩展、斐波那契通道、斐波那契时间周期、江恩箱、江恩方格、江恩扇形;形态:艾略特推动浪(0-5)与调整浪(A-B-C)、XABCD 谐波与 ABCD(自动标注斐波那契比值)、头肩形态(含颈线);形状:矩形、圆形、三角形、画笔;标注:文本、标注气泡、价格标签、箭头、图标;测量与头寸:价格区间、日期区间、日期与价格区间、多头 / 空头头寸(止盈默认 2R,显示 R:R)。点击过程中实时预览,Esc 取消,按住 Shift 把方向约束为水平或 45°;磁吸可选关 / 弱 / 强,吸附到 K 线开高低收。
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 线开高低收。
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` | 在会话图表上添加、修改(几何 / 颜色 / 线宽 / 线型 / 锁定 / 隐藏)或删除画线(41 种,含固定范围成交量分布、斐波那契、江恩、叉子线、回归、艾略特与谐波形态、标注与多空头寸;可设置同步到所有会话与价格提醒)和均线(SMA / EMA / SMMA / WMA,可选平滑);`action=view` 切换图表周期并显示 / 隐藏 RSI、MACD 等副图指标 |
112
+ | `hyper_chart` | 在会话图表上添加、修改(几何 / 颜色 / 线宽 / 线型 / 锁定 / 隐藏)或删除画线(72 种,含固定范围成交量分布、斐波那契、江恩、叉子线、回归、艾略特与谐波形态、标注与多空头寸;可设置同步到所有会话与价格提醒)和均线(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
@@ -952,6 +952,28 @@ body:has(.dhl-divider.dragging) * { cursor: col-resize !important; }
952
952
  /* Touch: the chart owns every gesture inside it (anchor and drawing drags, pans), as TradingView does;
953
953
  otherwise the browser claims a finger drag for scrolling and cancels the pointer mid-drag. */
954
954
  .dhl-chart, .dhl-chart-drawing-overlay, .dhl-chart-drawing-overlay * { touch-action: none; }
955
+
956
+ /* 0.5.0 tools: curves, ellipses, Fibonacci rings, notes and signposts. */
957
+ .dhl-drawing .dhl-curve, .dhl-drawing .dhl-fib-ring { fill: none; stroke: currentColor; stroke-width: var(--dhl-line-width, 1.6); stroke-dasharray: var(--dhl-dash, none); }
958
+ .dhl-drawing .dhl-fib-ring { stroke-width: 1.2; opacity: .85; }
959
+ .dhl-drawing ellipse.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); }
960
+ .dhl-drawing rect.dhl-drawing-mid { fill: none; stroke: currentColor; }
961
+ .dhl-drawing-hit .dhl-curve, .dhl-drawing-hit .dhl-fib-ring { pointer-events: stroke; }
962
+ .dhl-drawing .dhl-note-box { fill: color-mix(in srgb, #e2b93b 22%, var(--dsw-alias-bg-layer-1)); stroke: color-mix(in srgb, #e2b93b 70%, transparent); }
963
+ .dhl-drawing .dhl-signpost > line { stroke-width: 1.6; }
964
+
965
+ /* Moved below the drawing when the top position would cover its anchors (above the time axis). */
966
+ .dhl-draw-toolbar.at-bottom { top: auto; bottom: 40px; }
967
+
968
+ /* Search at the top of the drawing menu (stays visible while the list scrolls). */
969
+ .dhl-draw-popover .dhl-draw-search { position: sticky; top: -6px; z-index: 1; width: 100%; box-sizing: border-box; margin: -6px 0 4px; padding: 8px 10px; border: 0; border-bottom: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; outline: none; }
970
+ .dhl-draw-popover .dhl-draw-empty { margin: 10px 8px; color: var(--dsw-alias-label-secondary); font-size: 11.5px; }
971
+
972
+ /* 0.5.1: highlighter, pin, copied bars. */
973
+ .dhl-drawing .dhl-highlighter { stroke-width: calc(var(--dhl-line-width, 2) * 6); stroke-opacity: .32; }
974
+ .dhl-drawing .dhl-pin-glyph { fill: currentColor; fill-opacity: .25; stroke: currentColor; stroke-width: 2; }
975
+ .dhl-drawing .dhl-bars-copy { fill: none; stroke: currentColor; stroke-width: 1.3; opacity: .8; }
976
+ .dhl-drawing-hit .dhl-bars-copy, .dhl-drawing-hit .dhl-pin-glyph { pointer-events: stroke; }
955
977
  `;
956
978
 
957
979
  // src/client/locale.ts
@@ -17695,8 +17717,44 @@ var DRAWING_POINTS = {
17695
17717
  gannsquare: 2,
17696
17718
  schiff: 3,
17697
17719
  modschiff: 3,
17698
- icon: 1
17720
+ icon: 1,
17721
+ // TradingView's remaining common tools (0.5.0).
17722
+ flatchannel: 3,
17723
+ disjoint: 3,
17724
+ fibfan: 2,
17725
+ fibcircles: 2,
17726
+ fibarcs: 2,
17727
+ fibwedge: 3,
17728
+ fibtrendtime: 3,
17729
+ cypher: 5,
17730
+ threedrives: 6,
17731
+ tripattern: 4,
17732
+ elliotttri: 6,
17733
+ elliottwxy: 4,
17734
+ elliottwxyxz: 6,
17735
+ cycles: 2,
17736
+ sine: 2,
17737
+ ellipse: 2,
17738
+ rotrect: 3,
17739
+ arc: 3,
17740
+ curve: 3,
17741
+ note: 1,
17742
+ pricenote: 2,
17743
+ signpost: 1,
17744
+ avwap: 1,
17745
+ avprofile: 1,
17746
+ forecast: 2,
17747
+ // Freehand highlighter (0 = variable, like brush); path / polyline take clicks until a double-click or Enter.
17748
+ highlighter: 0,
17749
+ path: 0,
17750
+ polyline: 0,
17751
+ pin: 1,
17752
+ fibspiral: 2,
17753
+ barspattern: 3
17699
17754
  };
17755
+ var FREEHAND_KINDS = ["brush", "highlighter"];
17756
+ var MULTI_KINDS = ["path", "polyline"];
17757
+ var PATH_MAX = 50;
17700
17758
  var ICON_NAMES = ["flag", "star", "up", "down", "warning", "check", "cross", "fire", "target", "bell", "pin", "clock", "dollar", "shield", "bolt", "heart", "question", "info"];
17701
17759
  var STICKERS = {
17702
17760
  trading: ["\u{1F4C8}", "\u{1F4C9}", "\u{1F4B0}", "\u{1F4B8}", "\u{1F680}", "\u{1F3AF}", "\u{1F525}", "\u{1F48E}", "\u{1F402}", "\u{1F43B}", "\u{1F40B}", "\u{1F6A8}", "\u{1F514}", "\u23F0", "\u{1F4CC}", "\u{1F9ED}", "\u{1F6D1}", "\u{1F3E6}", "\u2696\uFE0F", "\u{1FA99}"],
@@ -17708,8 +17766,8 @@ function isSingleEmoji(value) {
17708
17766
  if (typeof value !== "string" || !value || value.length > 32 || !/\p{Extended_Pictographic}|\p{Regional_Indicator}/u.test(value)) return false;
17709
17767
  return [...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(value)].length === 1;
17710
17768
  }
17711
- var FONT_KINDS = ["text", "callout"];
17712
- var FILL_KINDS = ["rect", "circle", "triangle", "channel", "fib", "regression", "gannbox", "pricerange", "daterange", "measure"];
17769
+ var FONT_KINDS = ["text", "callout", "note", "pricenote", "signpost"];
17770
+ var FILL_KINDS = ["rect", "circle", "triangle", "channel", "fib", "regression", "gannbox", "pricerange", "daterange", "measure", "flatchannel", "disjoint", "ellipse", "rotrect"];
17713
17771
  var FIB_LEVEL_KINDS = ["fib", "fibext", "fibchannel"];
17714
17772
  function volumeProfile(bars, { rows = 48, valueArea = 0.7 } = {}) {
17715
17773
  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);
@@ -17755,7 +17813,7 @@ var GANN_BOX_LEVELS = [0, 0.25, 0.382, 0.5, 0.618, 0.75, 1];
17755
17813
  var ALERT_KINDS = ["level", "hray", "trend", "ray", "extended"];
17756
17814
  var BRUSH_MIN = 2;
17757
17815
  var BRUSH_MAX = 500;
17758
- var TEXT_KINDS = ["text", "callout"];
17816
+ var TEXT_KINDS = ["text", "callout", "note", "pricenote", "signpost", "pin"];
17759
17817
  var DRAWING_CLICKS = { ...Object.fromEntries(Object.entries(DRAWING_POINTS)), long: 2, short: 2 };
17760
17818
  var DRAWING_KINDS = Object.keys(DRAWING_POINTS);
17761
17819
  var FIB_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
@@ -17785,11 +17843,15 @@ function drawingGeometry(kind, request, now = Date.now()) {
17785
17843
  }
17786
17844
  const need = DRAWING_POINTS[kind];
17787
17845
  const raw = request?.points;
17788
- if (kind === "brush") {
17789
- if (!Array.isArray(raw) || raw.length < BRUSH_MIN || raw.length > BRUSH_MAX) throw new Error(`brush \u9700\u8981 ${BRUSH_MIN}-${BRUSH_MAX} \u4E2A points`);
17846
+ if (FREEHAND_KINDS.includes(kind)) {
17847
+ if (!Array.isArray(raw) || raw.length < BRUSH_MIN || raw.length > BRUSH_MAX) throw new Error(`${kind} \u9700\u8981 ${BRUSH_MIN}-${BRUSH_MAX} \u4E2A points`);
17848
+ } else if (MULTI_KINDS.includes(kind)) {
17849
+ if (!Array.isArray(raw) || raw.length < 2 || raw.length > PATH_MAX) throw new Error(`${kind} \u9700\u8981 2-${PATH_MAX} \u4E2A points`);
17790
17850
  } else if (!Array.isArray(raw) || raw.length !== need) throw new Error(`${kind} \u9700\u8981 ${need} \u4E2A points\uFF08{ time, price }\uFF09`);
17791
17851
  const points2 = raw.map((item, index) => point(item?.time, item?.price, `\u7B2C ${index + 1} \u4E2A\u70B9`));
17792
- if (["trend", "ray", "extended", "channel", "fibchannel", "gannfan", "regression", "fibtime", "infoline", "trendangle"].includes(kind) && points2[0].time === points2[1].time) throw new Error("\u524D\u4E24\u4E2A\u70B9\u7684\u65F6\u95F4\u4E0D\u80FD\u76F8\u540C");
17852
+ 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
+ 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
+ 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");
17793
17855
  if (kind === "vprofile") {
17794
17856
  if (points2[0].time === points2[1].time) throw new Error("\u6210\u4EA4\u91CF\u5206\u5E03\u9700\u8981\u4E00\u4E2A\u65F6\u95F4\u533A\u95F4");
17795
17857
  if (points2[1].time < points2[0].time) points2.reverse();
@@ -17817,6 +17879,8 @@ var DRAWING_TOOLS = [
17817
17879
  { group: "lines", kind: "crossline", zh: "\u5341\u5B57\u7EBF", en: "Cross line", hintZh: "\u5355\u51FB\u653E\u7F6E\u6C34\u5E73\u4E0E\u5782\u76F4\u4EA4\u53C9\u7EBF\uFF08Alt+C\uFF09", hintEn: "Click to place a horizontal and vertical cross (Alt+C)", icon: "M12 3v18M3 12h18" },
17818
17880
  { group: "lines", kind: "vline", zh: "\u5782\u76F4\u7EBF", en: "Vertical line", hintZh: "\u5355\u51FB\u6807\u8BB0\u65F6\u95F4\uFF08Alt+V\uFF09", hintEn: "Click to mark a time (Alt+V)", icon: "M12 3v18" },
17819
17881
  { group: "channels", kind: "channel", zh: "\u5E73\u884C\u901A\u9053", en: "Parallel channel", hintZh: "\u70B9\u51FB\u4E24\u70B9\u753B\u57FA\u7EBF\uFF0C\u7B2C\u4E09\u70B9\u8BBE\u7F6E\u901A\u9053\u5BBD\u5EA6", hintEn: "Two points for the base, a third for width", icon: "M3 15 15 5M9 19 21 9" },
17882
+ { group: "channels", kind: "flatchannel", zh: "\u5E73\u9876/\u5E73\u5E95\u901A\u9053", en: "Flat top/bottom", hintZh: "\u4E24\u70B9\u753B\u8D8B\u52BF\u8FB9\uFF0C\u7B2C\u4E09\u70B9\u8BBE\u7F6E\u6C34\u5E73\u8FB9\u7684\u4EF7\u683C", hintEn: "Two points for the sloped side, a third for the flat side", icon: "M3 18 21 8M3 5h18" },
17883
+ { group: "channels", kind: "disjoint", zh: "\u5206\u79BB\u901A\u9053", en: "Disjoint channel", hintZh: "\u4E24\u70B9\u753B\u7B2C\u4E00\u6761\u8FB9\uFF0C\u7B2C\u4E09\u70B9\u653E\u7F6E\u659C\u7387\u76F8\u53CD\u7684\u7B2C\u4E8C\u6761\u8FB9", hintEn: "Two points for one side; a third places the mirrored side", icon: "M3 16 21 6M3 9l18 10" },
17820
17884
  { group: "channels", kind: "pitchfork", zh: "\u5B89\u5FB7\u9C81\u53C9\u5B50\u7EBF", en: "Pitchfork", hintZh: "\u70B9\u51FB\u67A2\u8F74\u70B9\uFF0C\u518D\u70B9\u51FB\u6CE2\u6BB5\u9AD8\u70B9\u4E0E\u4F4E\u70B9", hintEn: "Click the pivot, then the swing high and low", icon: "M4 20 12 4M12 4l8 8M12 4 4 12M8 16l12-4" },
17821
17885
  { group: "channels", kind: "schiff", zh: "Schiff \u53C9\u5B50\u7EBF", en: "Schiff pitchfork", hintZh: "\u540C\u53C9\u5B50\u7EBF\uFF0C\u8D77\u70B9\u79FB\u5230 A\u3001B \u4EF7\u683C\u4E2D\u70B9", hintEn: "Pitchfork with the handle at A\u2013B price midpoint", icon: "M4 14 12 4M12 4l8 8M12 4 4 12M8 16l12-4" },
17822
17886
  { group: "channels", kind: "modschiff", zh: "\u6539\u826F Schiff \u53C9\u5B50\u7EBF", en: "Modified Schiff pitchfork", hintZh: "\u540C\u53C9\u5B50\u7EBF\uFF0C\u8D77\u70B9\u79FB\u5230 A\u3001B \u65F6\u95F4\u4E0E\u4EF7\u683C\u4E2D\u70B9", hintEn: "Pitchfork with the handle at the A\u2013B midpoint", icon: "M6 12 12 4M12 4l8 8M12 4 4 12M8 16l12-4" },
@@ -17826,6 +17890,12 @@ var DRAWING_TOOLS = [
17826
17890
  { group: "fib", kind: "fibext", zh: "\u6590\u6CE2\u90A3\u5951\u6269\u5C55", en: "Trend-based fib extension", hintZh: "\u70B9\u51FB\u6CE2\u6BB5\u8D77\u70B9\u3001\u7EC8\u70B9\uFF0C\u518D\u70B9\u51FB\u56DE\u64A4\u70B9\uFF0C\u81EA\u56DE\u64A4\u70B9\u6295\u5C04", hintEn: "Swing start, swing end, then the retracement point", icon: "M3 20 9 6l6 9M15 4h7M15 8h7M15 12h7" },
17827
17891
  { group: "fib", kind: "fibchannel", zh: "\u6590\u6CE2\u90A3\u5951\u901A\u9053", en: "Fib channel", hintZh: "\u4E24\u70B9\u753B\u57FA\u7EBF\uFF0C\u7B2C\u4E09\u70B9\u8BBE\u7F6E\u901A\u9053\u5BBD\u5EA6", hintEn: "Two points for the base, a third for width", icon: "M3 19 21 9M3 15 21 5M3 11 21 1" },
17828
17892
  { group: "fib", kind: "fibtime", zh: "\u6590\u6CE2\u90A3\u5951\u65F6\u95F4\u5468\u671F", en: "Fib time zone", hintZh: "\u70B9\u51FB\u4E24\u70B9\u786E\u5B9A\u5355\u4F4D\u95F4\u9694", hintEn: "Click two points to set the unit spacing", icon: "M4 3v18M7 3v18M10 3v18M15 3v18M22 3v18" },
17893
+ { group: "fib", kind: "fibfan", zh: "\u6590\u6CE2\u90A3\u5951\u901F\u5EA6\u963B\u529B\u6247", en: "Fib speed resistance fan", hintZh: "\u70B9\u51FB\u539F\u70B9\uFF0C\u518D\u70B9\u51FB\u5BF9\u89D2\uFF0C\u6309\u4EF7\u683C\u4E0E\u65F6\u95F4\u6BD4\u4F8B\u753B\u6247\u7EBF", hintEn: "Click the origin, then the opposite corner", icon: "M3 21 21 3M3 21 21 9M3 21 21 15M3 21 9 3M3 21 15 3" },
17894
+ { group: "fib", kind: "fibcircles", zh: "\u6590\u6CE2\u90A3\u5951\u5706", en: "Fib circles", hintZh: "\u70B9\u51FB\u5706\u5FC3\uFF0C\u518D\u70B9\u51FB\u786E\u5B9A 1.0 \u534A\u5F84", hintEn: "Click the center, then the 1.0 radius", icon: "M12 7a5 5 0 1 0 0 10 5 5 0 0 0 0-10zM12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z" },
17895
+ { group: "fib", kind: "fibarcs", zh: "\u6590\u6CE2\u90A3\u5951\u5F27", en: "Fib speed resistance arcs", hintZh: "\u70B9\u51FB\u6CE2\u6BB5\u8D77\u70B9\uFF0C\u518D\u70B9\u51FB\u7EC8\u70B9\uFF0C\u4EE5\u7EC8\u70B9\u4E3A\u5706\u5FC3\u753B\u5F27", hintEn: "Click the swing start, then the end (arcs center on the end)", icon: "M4 20a8 8 0 0 1 16 0M8 20a4 4 0 0 1 8 0" },
17896
+ { group: "fib", kind: "fibwedge", zh: "\u6590\u6CE2\u90A3\u5951\u6954\u5F62", en: "Fib wedge", hintZh: "\u70B9\u51FB\u9876\u70B9\uFF0C\u518D\u70B9\u51FB\u4E24\u6761\u8FB9\u7684\u7AEF\u70B9", hintEn: "Click the apex, then the ends of the two sides", icon: "M3 12 21 4M3 12l18 8M15 6.7a7 7 0 0 1 0 10.6" },
17897
+ { group: "fib", kind: "fibtrendtime", zh: "\u57FA\u4E8E\u8D8B\u52BF\u7684\u6590\u6CE2\u90A3\u5951\u65F6\u95F4", en: "Trend-based fib time", hintZh: "\u4E24\u70B9\u786E\u5B9A\u8D8B\u52BF\u65F6\u957F\uFF0C\u7B2C\u4E09\u70B9\u8D77\u6309\u6BD4\u4F8B\u753B\u65F6\u95F4\u7EBF", hintEn: "Two points set the trend duration; lines start at the third", icon: "M3 18 9 6M12 3v18M16 3v18M21 3v18" },
17898
+ { group: "fib", kind: "fibspiral", zh: "\u6590\u6CE2\u90A3\u5951\u87BA\u65CB", en: "Fib spiral", hintZh: "\u70B9\u51FB\u4E2D\u5FC3\uFF0C\u518D\u70B9\u51FB\u786E\u5B9A\u8D77\u59CB\u534A\u5F84\u4E0E\u65B9\u5411\uFF08\u6BCF\u56DB\u5206\u4E4B\u4E00\u5708\u653E\u5927 1.618 \u500D\uFF09", hintEn: "Click the center, then the starting radius (grows 1.618\xD7 per quarter turn)", icon: "M12 12a2 2 0 1 1 2-2c0 3-3 5-6 4s-5-5-3-8 7-5 10-3 5 7 3 11" },
17829
17899
  { group: "fib", kind: "gannbox", zh: "\u6C5F\u6069\u7BB1", en: "Gann box", hintZh: "\u70B9\u51FB\u4E24\u4E2A\u5BF9\u89D2\uFF0C\u6309 0.25/0.382/0.5/0.618/0.75 \u5206\u5272", hintEn: "Click two corners; divided at Gann levels", icon: "M4 4h16v16H4zM4 12h16M12 4v16M4 4l16 16" },
17830
17900
  { group: "fib", kind: "gannsquare", zh: "\u6C5F\u6069\u65B9\u683C", en: "Gann square", hintZh: "\u70B9\u51FB\u4E24\u4E2A\u5BF9\u89D2\uFF0C\u753B\u56DB\u5206\u7F51\u683C\u3001\u5BF9\u89D2\u7EBF\u4E0E\u6247\u7EBF", hintEn: "Click two corners; quarters, diagonals and fans", icon: "M4 4h16v16H4zM4 4l16 16M20 4 4 20M4 12h16M12 4v16" },
17831
17901
  { group: "fib", kind: "gannfan", zh: "\u6C5F\u6069\u6247\u5F62", en: "Gann fan", hintZh: "\u70B9\u51FB\u539F\u70B9\uFF0C\u518D\u70B9\u51FB\u786E\u5B9A 1\xD71 \u7EBF", hintEn: "Click the origin, then a point on the 1\xD71 line", icon: "M3 21 21 3M3 21 21 12M3 21 12 3M3 21h18" },
@@ -17833,19 +17903,42 @@ var DRAWING_TOOLS = [
17833
17903
  { group: "patterns", kind: "elliottabc", zh: "\u827E\u7565\u7279\u8C03\u6574\u6D6A", en: "Elliott correction wave", hintZh: "\u4F9D\u6B21\u70B9\u51FB 0\u3001A\u3001B\u3001C \u56DB\u4E2A\u70B9", hintEn: "Click 0, A, B, C", icon: "M3 6l6 10 5-6 7 10" },
17834
17904
  { group: "patterns", kind: "xabcd", zh: "XABCD \u8C10\u6CE2", en: "XABCD pattern", hintZh: "\u4F9D\u6B21\u70B9\u51FB X\u3001A\u3001B\u3001C\u3001D\uFF0C\u663E\u793A\u6590\u6CE2\u90A3\u5951\u6BD4\u503C", hintEn: "Click X, A, B, C, D; shows Fibonacci ratios", icon: "M2 18 7 5l4 9 5-7 6 12M2 18l9-4M11 14l11 5" },
17835
17905
  { group: "patterns", kind: "abcd", zh: "ABCD \u5F62\u6001", en: "ABCD pattern", hintZh: "\u4F9D\u6B21\u70B9\u51FB A\u3001B\u3001C\u3001D", hintEn: "Click A, B, C, D", icon: "M3 19 9 6l5 8 7-10" },
17906
+ { group: "patterns", kind: "cypher", zh: "Cypher \u5F62\u6001", en: "Cypher pattern", hintZh: "\u4F9D\u6B21\u70B9\u51FB X\u3001A\u3001B\u3001C\u3001D\uFF0C\u663E\u793A B \u56DE\u64A4\u3001C \u6269\u5C55\u4E0E D \u5BF9 XC \u7684\u56DE\u64A4", hintEn: "Click X, A, B, C, D; shows B, C and XC ratios", icon: "M2 17 7 5l4 8 4-10 7 15M2 17l9-4" },
17907
+ { group: "patterns", kind: "threedrives", zh: "\u4E09\u9A71\u5F62\u6001", en: "Three drives pattern", hintZh: "\u4F9D\u6B21\u70B9\u51FB\u8D77\u70B9\u3001\u9A71\u52A8 1\u3001A\u3001\u9A71\u52A8 2\u3001B\u3001\u9A71\u52A8 3", hintEn: "Click start, drive 1, A, drive 2, B, drive 3", icon: "M2 20 6 13l3 3 4-8 3 3 5-8" },
17908
+ { group: "patterns", kind: "tripattern", zh: "\u4E09\u89D2\u5F62\u6001", en: "Triangle pattern", hintZh: "\u4F9D\u6B21\u70B9\u51FB A\u3001B\u3001C\u3001D\uFF0C\u8FDE\u7EBF\u5E76\u5EF6\u957F\u4E0A\u4E0B\u8FB9", hintEn: "Click A, B, C, D; the sides extend to the apex", icon: "M3 4 9 18l5-10 4 6M3 4l18 6M9 18l12-6" },
17909
+ { group: "patterns", kind: "elliotttri", zh: "\u827E\u7565\u7279\u4E09\u89D2\u6D6A", en: "Elliott triangle wave", hintZh: "\u4F9D\u6B21\u70B9\u51FB 0\u3001A\u3001B\u3001C\u3001D\u3001E", hintEn: "Click 0, A, B, C, D, E", icon: "M2 5l4 14 4-11 4 8 4-5 4 3" },
17910
+ { group: "patterns", kind: "elliottwxy", zh: "\u827E\u7565\u7279\u53CC\u91CD\u7EC4\u5408", en: "Elliott double combo", hintZh: "\u4F9D\u6B21\u70B9\u51FB 0\u3001W\u3001X\u3001Y", hintEn: "Click 0, W, X, Y", icon: "M3 6l6 10 5-6 7 10" },
17911
+ { group: "patterns", kind: "elliottwxyxz", zh: "\u827E\u7565\u7279\u4E09\u91CD\u7EC4\u5408", en: "Elliott triple combo", hintZh: "\u4F9D\u6B21\u70B9\u51FB 0\u3001W\u3001X\u3001Y\u3001X\u3001Z", hintEn: "Click 0, W, X, Y, X, Z", icon: "M2 6l4 8 3-4 4 7 3-4 6 9" },
17912
+ { group: "patterns", kind: "cycles", zh: "\u5468\u671F\u7EBF", en: "Cyclic lines", hintZh: "\u70B9\u51FB\u4E24\u70B9\u786E\u5B9A\u5468\u671F\uFF0C\u5411\u540E\u7B49\u8DDD\u753B\u5782\u76F4\u7EBF", hintEn: "Click two points to set the period", icon: "M5 3v18M10 3v18M15 3v18M20 3v18" },
17913
+ { group: "patterns", kind: "sine", zh: "\u6B63\u5F26\u7EBF", en: "Sine line", hintZh: "\u70B9\u51FB\u6CE2\u5CF0\uFF0C\u518D\u70B9\u51FB\u76F8\u90BB\u6CE2\u8C37", hintEn: "Click a peak, then the next trough", icon: "M2 12c2.5-8 5-8 7.5 0s5 8 7.5 0 3-5 5-4" },
17836
17914
  { group: "patterns", kind: "headshoulders", zh: "\u5934\u80A9\u5F62\u6001", en: "Head and shoulders", hintZh: "\u4F9D\u6B21\u70B9\u51FB\u5DE6\u5E95\u3001\u5DE6\u80A9\u3001\u9888\u7EBF\u70B9\u3001\u5934\u3001\u9888\u7EBF\u70B9\u3001\u53F3\u80A9\u3001\u53F3\u5E95", hintEn: "Click 7 points from left base to right base", icon: "M2 18l3-6 3 3 4-10 4 10 3-3 3 6M5 15h14" },
17837
17915
  { group: "shapes", kind: "rect", zh: "\u77E9\u5F62", en: "Rectangle", hintZh: "\u70B9\u51FB\u4E24\u4E2A\u5BF9\u89D2\uFF08Alt+Shift+R\uFF09", hintEn: "Click two opposite corners (Alt+Shift+R)", icon: "M4 6h16v12H4z" },
17838
17916
  { group: "shapes", kind: "circle", zh: "\u5706\u5F62", en: "Circle", hintZh: "\u70B9\u51FB\u5706\u5FC3\uFF0C\u518D\u70B9\u51FB\u8FB9\u7F18", hintEn: "Click the center, then the edge", icon: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z" },
17839
17917
  { group: "shapes", kind: "triangle", zh: "\u4E09\u89D2\u5F62", en: "Triangle", hintZh: "\u4F9D\u6B21\u70B9\u51FB\u4E09\u4E2A\u9876\u70B9", hintEn: "Click three corners", icon: "M12 4 21 20H3z" },
17918
+ { group: "shapes", kind: "ellipse", zh: "\u692D\u5706", en: "Ellipse", hintZh: "\u70B9\u51FB\u5916\u63A5\u77E9\u5F62\u7684\u4E24\u4E2A\u5BF9\u89D2", hintEn: "Click two corners of its bounding box", icon: "M12 6c5 0 9 2.7 9 6s-4 6-9 6-9-2.7-9-6 4-6 9-6z" },
17919
+ { group: "shapes", kind: "rotrect", zh: "\u65CB\u8F6C\u77E9\u5F62", en: "Rotated rectangle", hintZh: "\u4E24\u70B9\u753B\u4E00\u6761\u8FB9\uFF0C\u7B2C\u4E09\u70B9\u8BBE\u7F6E\u5BBD\u5EA6", hintEn: "Two points for one side, a third for the width", icon: "M4 14 14 4l6 6-10 10z" },
17920
+ { group: "shapes", kind: "arc", zh: "\u5F27\u7EBF", en: "Arc", hintZh: "\u70B9\u51FB\u8D77\u70B9\u3001\u7EC8\u70B9\uFF0C\u518D\u70B9\u51FB\u5F27\u7EBF\u7ECF\u8FC7\u7684\u70B9", hintEn: "Click start, end, then a point on the arc", icon: "M3 18c3-10 15-10 18 0" },
17921
+ { group: "shapes", kind: "curve", zh: "\u66F2\u7EBF", en: "Curve", hintZh: "\u70B9\u51FB\u8D77\u70B9\u3001\u7EC8\u70B9\uFF0C\u518D\u70B9\u51FB\u63A7\u5236\u70B9", hintEn: "Click start, end, then the control point", icon: "M3 18C8 2 14 22 21 6" },
17922
+ { group: "shapes", kind: "highlighter", zh: "\u8367\u5149\u7B14", en: "Highlighter", hintZh: "\u6309\u4F4F\u9F20\u6807\u62D6\u52A8\uFF0C\u753B\u534A\u900F\u660E\u7C97\u7EBF", hintEn: "Press and drag; a wide translucent stroke", icon: "M4 16c4-5 7 1 11-3s5-3 6-2M3 21h8" },
17923
+ { group: "shapes", kind: "path", zh: "\u8DEF\u5F84", en: "Path", hintZh: "\u9010\u70B9\u70B9\u51FB\uFF0C\u53CC\u51FB\u6216\u6309 Enter \u7ED3\u675F\uFF0C\u672B\u7AEF\u5E26\u7BAD\u5934", hintEn: "Click point by point; double-click or Enter to finish (arrow at the end)", icon: "M3 18 8 9l5 6 8-10M17 5h4v4" },
17924
+ { group: "shapes", kind: "polyline", zh: "\u6298\u7EBF", en: "Polyline", hintZh: "\u9010\u70B9\u70B9\u51FB\uFF0C\u53CC\u51FB\u6216\u6309 Enter \u7ED3\u675F", hintEn: "Click point by point; double-click or Enter to finish", icon: "M3 18 8 9l5 6 8-10" },
17840
17925
  { group: "shapes", kind: "brush", zh: "\u753B\u7B14", en: "Brush", hintZh: "\u6309\u4F4F\u9F20\u6807\u62D6\u52A8\u81EA\u7531\u7ED8\u5236", hintEn: "Press and drag to draw freehand", icon: "M3 17c3-6 5 2 8-3s4-6 7-4 3 4 3 4" },
17841
17926
  { group: "annotation", kind: "text", zh: "\u6587\u672C", en: "Text", hintZh: "\u5355\u51FB\u4F4D\u7F6E\u540E\u8F93\u5165\u6587\u5B57", hintEn: "Click a spot, then type", icon: "M5 6h14M12 6v13M9 19h6" },
17842
17927
  { group: "annotation", kind: "callout", zh: "\u6807\u6CE8\u6C14\u6CE1", en: "Callout", hintZh: "\u70B9\u51FB\u8981\u6807\u6CE8\u7684\u4F4D\u7F6E\uFF0C\u518D\u70B9\u51FB\u6587\u672C\u6846\u4F4D\u7F6E\uFF0C\u7136\u540E\u8F93\u5165\u6587\u5B57", hintEn: "Click the target, then the box, then type", icon: "M4 4h16v10H10l-4 5v-5H4z" },
17928
+ { 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
+ { 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
+ { 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" },
17931
+ { 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" },
17843
17932
  { 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" },
17844
17933
  { 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" },
17845
17934
  { group: "annotation", kind: "arrow", zh: "\u7BAD\u5934", en: "Arrow", hintZh: "\u70B9\u51FB\u8D77\u70B9\u548C\u7BAD\u5934\u6307\u5411\u7684\u4F4D\u7F6E", hintEn: "Click tail, then head", icon: "M4 20 20 4M12 4h8v8" },
17846
17935
  { group: "measure", kind: "pricerange", zh: "\u4EF7\u683C\u533A\u95F4", en: "Price range", hintZh: "\u70B9\u51FB\u4E24\u70B9\uFF0C\u6D4B\u91CF\u4EF7\u683C\u5DEE\u4E0E\u767E\u5206\u6BD4", hintEn: "Click two points to measure price change", icon: "M12 3v18M8 7l4-4 4 4M8 17l4 4 4-4M5 3h14M5 21h14" },
17847
17936
  { group: "measure", kind: "daterange", zh: "\u65E5\u671F\u533A\u95F4", en: "Date range", hintZh: "\u70B9\u51FB\u4E24\u70B9\uFF0C\u6D4B\u91CF K \u7EBF\u6839\u6570\u4E0E\u65F6\u957F", hintEn: "Click two points to measure bars and time", icon: "M3 12h18M7 8l-4 4 4 4M17 8l4 4-4 4M3 5v14M21 5v14" },
17848
17937
  { group: "measure", kind: "measure", zh: "\u65E5\u671F\u4E0E\u4EF7\u683C\u533A\u95F4", en: "Date & price range", hintZh: "\u70B9\u51FB\u8D77\u70B9\u548C\u7EC8\u70B9\uFF0C\u663E\u793A\u6DA8\u8DCC\u3001\u6839\u6570\u4E0E\u65F6\u957F", hintEn: "Click two points to measure change, bars and time", icon: "M12 3v18M8 7l4-4 4 4M8 17l4 4 4-4" },
17938
+ { 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
+ { 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
+ { 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" },
17941
+ { 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" },
17849
17942
  { 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" },
17850
17943
  { 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" }
17851
17944
  ];
@@ -17872,6 +17965,7 @@ var ICONS = {
17872
17965
  };
17873
17966
  var ICON_LIST = ICON_NAMES;
17874
17967
  var clicksNeeded = (kind) => DRAWING_CLICKS[kind];
17968
+ var isMultiKind = (kind) => MULTI_KINDS.includes(kind);
17875
17969
  function ToolIcon({ kind, size: size3 = 16 }) {
17876
17970
  return /* @__PURE__ */ React7.createElement("svg", { viewBox: "0 0 24 24", width: size3, height: size3, fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true" }, /* @__PURE__ */ React7.createElement("path", { d: toolOf(kind).icon }));
17877
17971
  }
@@ -18118,12 +18212,63 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
18118
18212
  body = p.length === 2 ? /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y }) : p.length >= 3 ? /* @__PURE__ */ React7.createElement("polygon", { className: "dhl-drawing-fill", points: p.slice(0, 3).map((point) => `${point.x},${point.y}`).join(" ") }) : null;
18119
18213
  break;
18120
18214
  }
18121
- case "brush": {
18215
+ case "brush":
18216
+ case "highlighter": {
18122
18217
  if (p.length < 2) break;
18123
- body = /* @__PURE__ */ React7.createElement("polyline", { className: "dhl-brush", points: p.map((point) => `${point.x},${point.y}`).join(" ") });
18218
+ body = /* @__PURE__ */ React7.createElement("polyline", { className: kind === "brush" ? "dhl-brush" : "dhl-brush dhl-highlighter", points: p.map((point) => `${point.x},${point.y}`).join(" ") });
18124
18219
  handles = [];
18125
18220
  break;
18126
18221
  }
18222
+ case "path":
18223
+ case "polyline": {
18224
+ if (p.length < 2) break;
18225
+ 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
+ break;
18227
+ }
18228
+ case "pin": {
18229
+ const value = textValue || label || tr("\u56FE\u9489", "Pin");
18230
+ const lines = wrapText(value, 220);
18231
+ const width = (16 + Math.max(...lines.map(textWidth))) * k, height = (10 + lines.length * 16) * k;
18232
+ const top = p[0].y - 30 - height;
18233
+ body = /* @__PURE__ */ React7.createElement("g", { className: "dhl-pin" }, /* @__PURE__ */ React7.createElement("path", { className: "dhl-pin-glyph", transform: `translate(${p[0].x - 9} ${p[0].y - 22}) scale(0.75)`, d: "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" }), /* @__PURE__ */ React7.createElement("rect", { className: "dhl-text-box dhl-note-box", x: p[0].x - width / 2, y: top, width, height, rx: "4" }), lines.map((line, index) => /* @__PURE__ */ React7.createElement(React7.Fragment, { key: index }, text(p[0].x - width / 2 + 8 * k, top + (17 + index * 16) * k - 2, line, "start", "dhl-drawing-text dhl-text-value"))));
18234
+ handles = [p[0]];
18235
+ break;
18236
+ }
18237
+ case "fibspiral": {
18238
+ if (!p[1]) break;
18239
+ const r0 = Math.hypot(p[1].x - p[0].x, p[1].y - p[0].y), a0 = Math.atan2(p[1].y - p[0].y, p[1].x - p[0].x);
18240
+ if (r0 < 1) break;
18241
+ const pts = [];
18242
+ for (let theta = -4 * Math.PI; theta <= 3 * Math.PI; theta += Math.PI / 32) {
18243
+ const r = r0 * Math.pow(1.618034, theta / (Math.PI / 2));
18244
+ if (r > 4 * Math.hypot(w, h)) break;
18245
+ pts.push(`${(p[0].x + r * Math.cos(a0 + theta)).toFixed(1)},${(p[0].y + r * Math.sin(a0 + theta)).toFixed(1)}`);
18246
+ }
18247
+ 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
+ break;
18249
+ }
18250
+ case "barspattern": {
18251
+ if (!p[1]) break;
18252
+ const [from, to] = points2[0].time <= points2[1].time ? [points2[0], points2[1]] : [points2[1], points2[0]];
18253
+ const source = view.candles?.(from.time, to.time) ?? [];
18254
+ if (!p[2]) {
18255
+ body = /* @__PURE__ */ React7.createElement("rect", { className: "dhl-drawing-mid", x: Math.min(p[0].x, p[1].x), y: 0, width: Math.abs(p[1].x - p[0].x), height: h, fill: "none" });
18256
+ break;
18257
+ }
18258
+ if (!source.length) {
18259
+ body = text(p[2].x, p[2].y, tr("\u6E90\u533A\u95F4\u6682\u65E0 K \u7EBF", "No bars in the source range"));
18260
+ break;
18261
+ }
18262
+ const dt = points2[2].time - source[0].time, dp = points2[2].price - source[0].open;
18263
+ let d = "";
18264
+ for (const bar of source) {
18265
+ const hi = view.project(bar.time + dt, bar.high + dp), lo = view.project(bar.time + dt, bar.low + dp), op = view.project(bar.time + dt, bar.open + dp), cl = view.project(bar.time + dt, bar.close + dp);
18266
+ if (!hi || !lo || !op || !cl) continue;
18267
+ d += `M${hi.x.toFixed(1)} ${hi.y.toFixed(1)}V${lo.y.toFixed(1)}M${(op.x - 3).toFixed(1)} ${op.y.toFixed(1)}H${op.x.toFixed(1)}M${cl.x.toFixed(1)} ${cl.y.toFixed(1)}H${(cl.x + 3).toFixed(1)}`;
18268
+ }
18269
+ body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("rect", { className: "dhl-drawing-mid", x: Math.min(p[0].x, p[1].x), y: 0, width: Math.abs(p[1].x - p[0].x), height: h, fill: "none" }), /* @__PURE__ */ React7.createElement("path", { className: "dhl-bars-copy", d, "data-bars": source.length }));
18270
+ break;
18271
+ }
18127
18272
  case "callout": {
18128
18273
  if (!p[1]) break;
18129
18274
  const value = textValue || label || tr("\u6807\u6CE8", "Note");
@@ -18255,16 +18400,24 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
18255
18400
  handles = [c0, c1];
18256
18401
  break;
18257
18402
  }
18403
+ case "avprofile":
18258
18404
  case "vprofile": {
18259
- if (!p[1]) break;
18260
- const [from, to] = points2[0].time <= points2[1].time ? points2 : [points2[1], points2[0]];
18261
- const x0 = Math.min(p[0].x, p[1].x), x1 = Math.max(p[0].x, p[1].x);
18405
+ const lastBar = kind === "avprofile" ? view.candles?.(points2[0].time, Number.MAX_SAFE_INTEGER).at(-1) : void 0;
18406
+ const endPoint = kind === "avprofile" ? lastBar ? view.project(lastBar.time, points2[0].price) : null : p[1];
18407
+ if (!endPoint) {
18408
+ if (kind === "avprofile") {
18409
+ body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, x2: p[0].x, y1: "0", y2: h }), text(p[0].x + 4, 16, tr("\u951A\u70B9\u4E4B\u540E\u6682\u65E0 K \u7EBF", "No bars after the anchor"), "start"));
18410
+ }
18411
+ break;
18412
+ }
18413
+ const [from, to] = kind === "avprofile" ? [points2[0], { time: lastBar.time, price: points2[0].price }] : points2[0].time <= points2[1].time ? points2 : [points2[1], points2[0]];
18414
+ const x0 = Math.min(p[0].x, endPoint.x), x1 = Math.max(p[0].x, endPoint.x);
18262
18415
  const settings = { rows: rows ?? 48, valueArea: valueArea ?? 0.7 };
18263
18416
  const profile = view.profile ? view.profile(from.time, to.time, settings) : volumeProfile(view.candles?.(from.time, to.time) ?? [], settings);
18264
18417
  const partial = view.firstTime != null && view.barMs != null && view.firstTime - from.time >= view.barMs;
18265
18418
  if (!profile) {
18266
18419
  body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("rect", { className: "dhl-drawing-fill", x: x0, y: 0, width: Math.max(1, x1 - x0), height: h }), text(x0 + 4, 16, tr("\u533A\u95F4\u5185\u6682\u65E0\u6210\u4EA4\u91CF\u6570\u636E", "No volume in this range"), "start"));
18267
- handles = [p[0], p[1]];
18420
+ handles = kind === "avprofile" ? [p[0]] : [p[0], p[1]];
18268
18421
  break;
18269
18422
  }
18270
18423
  const y = (price) => view.project(from.time, price)?.y;
@@ -18282,9 +18435,203 @@ var DrawingShape = React7.memo(function DrawingShape2({ kind, points: points2, l
18282
18435
  paths2[inValue ? "valueDown" : "outDown"] += rectPath(x0 + upWidth, top, downWidth, height);
18283
18436
  });
18284
18437
  body = /* @__PURE__ */ React7.createElement("g", { className: "dhl-vprofile", "data-rows": profile.rows.length }, yTop != null && yBottom != null ? /* @__PURE__ */ React7.createElement("rect", { className: "dhl-vprofile-range", x: x0, y: yTop, width: Math.max(1, x1 - x0), height: Math.max(1, yBottom - yTop) }) : null, /* @__PURE__ */ React7.createElement("g", { className: "dhl-vprofile-va" }, /* @__PURE__ */ React7.createElement("path", { className: "dhl-vprofile-up", d: paths2.valueUp }), /* @__PURE__ */ React7.createElement("path", { className: "dhl-vprofile-down", d: paths2.valueDown })), /* @__PURE__ */ React7.createElement("g", { className: "dhl-vprofile-out" }, /* @__PURE__ */ React7.createElement("path", { className: "dhl-vprofile-up", d: paths2.outUp }), /* @__PURE__ */ React7.createElement("path", { className: "dhl-vprofile-down", d: paths2.outDown })), yPoc != null ? /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("line", { className: "dhl-vprofile-poc", x1: x0, y1: yPoc, x2: x1, y2: yPoc }), text(x1 - 4, yPoc - 4, `POC ${fmtPrice(profile.poc)}`, "end")) : null, yVah != null ? /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("line", { className: "dhl-drawing-mid", x1: x0, y1: yVah, x2: x1, y2: yVah }), text(x1 - 4, yVah - 4, `VAH ${fmtPrice(profile.vah)}`, "end")) : null, yVal != null ? /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("line", { className: "dhl-drawing-mid", x1: x0, y1: yVal, x2: x1, y2: yVal }), text(x1 - 4, yVal + 12, `VAL ${fmtPrice(profile.val)}`, "end")) : null, yBottom != null ? text(x0 + 4, Math.min(h - 4, yBottom + 14), partial ? tr("K \u7EBF\u4F30\u7B97 \xB7 \u533A\u95F4\u524D\u6BB5\u5386\u53F2\u672A\u52A0\u8F7D", "Estimated from bars \xB7 early range not loaded") : tr("K \u7EBF\u4F30\u7B97", "Estimated from bars"), "start", partial ? "dhl-drawing-text dhl-vprofile-warn" : "dhl-drawing-text") : null);
18438
+ handles = kind === "avprofile" ? [p[0]] : [p[0], p[1]];
18439
+ break;
18440
+ }
18441
+ case "avwap": {
18442
+ const bars = view.candles?.(points2[0].time, Number.MAX_SAFE_INTEGER) ?? [];
18443
+ let pv = 0, vol = 0;
18444
+ const path = [];
18445
+ for (const bar of bars) {
18446
+ pv += (bar.high + bar.low + bar.close) / 3 * bar.volume;
18447
+ vol += bar.volume;
18448
+ const point = vol > 0 ? view.project(bar.time, pv / vol) : null;
18449
+ if (point) path.push(point);
18450
+ }
18451
+ if (path.length < 1 || vol <= 0) {
18452
+ body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, x2: p[0].x, y1: "0", y2: h }), text(p[0].x + 4, 16, tr("\u951A\u70B9\u4E4B\u540E\u6682\u65E0\u6210\u4EA4\u91CF", "No volume after the anchor"), "start"));
18453
+ break;
18454
+ }
18455
+ const last = path.at(-1);
18456
+ const partial = view.firstTime != null && view.barMs != null && view.firstTime - points2[0].time >= view.barMs;
18457
+ body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("polyline", { className: "dhl-pattern", points: path.map((point) => `${point.x},${point.y}`).join(" ") }), /* @__PURE__ */ React7.createElement("line", { className: "dhl-drawing-mid", x1: p[0].x, x2: p[0].x, y1: p[0].y - 14, y2: p[0].y + 14 }), text(Math.min(last.x + 4, w - 110), last.y - 5, `VWAP ${fmtPrice(pv / vol)}`), partial ? text(Math.min(last.x + 4, w - 160), last.y + 12, tr("\u951A\u70B9\u524D\u6BB5\u5386\u53F2\u672A\u52A0\u8F7D\uFF0C\u6570\u503C\u6682\u4E0D\u5B8C\u6574", "Early history not loaded; value incomplete"), "start", "dhl-drawing-text dhl-vprofile-warn") : null);
18458
+ handles = [p[0]];
18459
+ break;
18460
+ }
18461
+ case "forecast": {
18462
+ if (!p[1]) break;
18463
+ const [a, b] = points2;
18464
+ const change = b.price - a.price, pct = a.price > 0 ? change / a.price * 100 : 0;
18465
+ const up = change >= 0;
18466
+ const boxY = up ? p[1].y - 44 : p[1].y + 8;
18467
+ body = /* @__PURE__ */ React7.createElement("g", { className: `dhl-measure ${up ? "up" : "down"}` }, /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y }), /* @__PURE__ */ React7.createElement("polygon", { className: "dhl-arrow-head", points: arrowHead(p[0], p[1]) }), /* @__PURE__ */ React7.createElement("line", { className: "dhl-drawing-mid", x1: p[0].x, x2: p[1].x, y1: p[0].y, y2: p[0].y }), /* @__PURE__ */ React7.createElement("rect", { className: "dhl-measure-box", x: p[1].x - 78, y: boxY, width: "156", height: "36", rx: "4" }), text(p[1].x, boxY + 15, `${tr("\u76EE\u6807", "Target")} ${fmtPrice(b.price)} (${up ? "+" : ""}${pct.toFixed(2)}%)`, "middle"), text(p[1].x, boxY + 29, `${Math.abs(Math.round(view.bars(a.time, b.time)))} ${tr("\u6839", "bars")} \xB7 ${duration(b.time - a.time)}`, "middle"));
18468
+ break;
18469
+ }
18470
+ case "note":
18471
+ case "signpost": {
18472
+ const value = textValue || label || (kind === "note" ? tr("\u4FBF\u7B7E", "Note") : tr("\u8DEF\u6807", "Signpost"));
18473
+ const lines = wrapText(value, 220);
18474
+ const width = (16 + Math.max(...lines.map(textWidth))) * k, height = (10 + lines.length * 16) * k;
18475
+ const top = kind === "note" ? p[0].y - height / 2 : p[0].y - 46 - height;
18476
+ const left = kind === "note" ? p[0].x + 12 : p[0].x - 4;
18477
+ body = /* @__PURE__ */ React7.createElement("g", { className: kind === "note" ? "dhl-note" : "dhl-signpost" }, kind === "signpost" ? /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, x2: p[0].x, y1: p[0].y, y2: top + height }) : /* @__PURE__ */ React7.createElement("line", { className: "dhl-drawing-mid", x1: p[0].x, y1: p[0].y, x2: left, y2: p[0].y }), /* @__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 dhl-note-box", x: left, y: top, width, height, rx: "4" }), lines.map((line, index) => /* @__PURE__ */ React7.createElement(React7.Fragment, { key: index }, text(left + 8 * k, top + (17 + index * 16) * k - 2, line, "start", "dhl-drawing-text dhl-text-value"))));
18478
+ handles = [p[0]];
18479
+ break;
18480
+ }
18481
+ case "pricenote": {
18482
+ if (!p[1]) break;
18483
+ const value = `${fmtPrice(points2[0].price)} \xB7 ${textValue || label || tr("\u4EF7\u683C\u4FBF\u7B7E", "Price note")}`;
18484
+ const width = Math.min(300, 16 + textWidth(value)) * k;
18485
+ 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 dhl-note-box", x: p[1].x, y: p[1].y, width, height: 26 * k, rx: "4" }), text(p[1].x + 8 * k, p[1].y + 17 * k, value, "start", "dhl-drawing-text dhl-text-value"));
18486
+ break;
18487
+ }
18488
+ case "flatchannel":
18489
+ case "disjoint": {
18490
+ if (!p[1]) break;
18491
+ const base = /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y });
18492
+ if (!p[2]) {
18493
+ body = base;
18494
+ break;
18495
+ }
18496
+ const a2 = kind === "flatchannel" ? { x: p[0].x, y: p[2].y } : p[2];
18497
+ const b2 = kind === "flatchannel" ? { x: p[1].x, y: p[2].y } : { x: p[2].x + (p[1].x - p[0].x), y: p[2].y - (p[1].y - p[0].y) };
18498
+ body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("polygon", { className: "dhl-drawing-fill", points: `${p[0].x},${p[0].y} ${p[1].x},${p[1].y} ${b2.x},${b2.y} ${a2.x},${a2.y}` }), base, /* @__PURE__ */ React7.createElement("line", { x1: a2.x, y1: a2.y, x2: b2.x, y2: b2.y }));
18499
+ break;
18500
+ }
18501
+ case "fibfan": {
18502
+ if (!p[1]) break;
18503
+ const dx = p[1].x - p[0].x, dy = p[1].y - p[0].y;
18504
+ const ratios = [0.25, 0.382, 0.5, 0.618, 0.75];
18505
+ const ray = (end, key3, value) => {
18506
+ const seg = clipLine(p[0], end, w, h, 0);
18507
+ return seg ? /* @__PURE__ */ React7.createElement("g", { key: key3 }, /* @__PURE__ */ React7.createElement("line", { x1: seg[0].x, y1: seg[0].y, x2: seg[1].x, y2: seg[1].y }), text(end.x + 3, end.y - 3, value)) : null;
18508
+ };
18509
+ body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("rect", { className: "dhl-drawing-mid", x: Math.min(p[0].x, p[1].x), y: Math.min(p[0].y, p[1].y), width: Math.abs(dx), height: Math.abs(dy), fill: "none" }), ratios.map((r) => ray({ x: p[1].x, y: p[0].y + dy * r }, `p${r}`, String(r))), ratios.map((r) => ray({ x: p[0].x + dx * r, y: p[1].y }, `t${r}`, String(r))), ray(p[1], "one", "1"));
18510
+ break;
18511
+ }
18512
+ case "fibcircles":
18513
+ case "fibarcs": {
18514
+ if (!p[1]) break;
18515
+ const center = kind === "fibcircles" ? p[0] : p[1];
18516
+ const radius2 = Math.hypot(p[1].x - p[0].x, p[1].y - p[0].y);
18517
+ const levels2 = kind === "fibcircles" ? [0.382, 0.5, 0.618, 1, 1.618, 2.618] : [0.382, 0.5, 0.618, 1];
18518
+ const sweep = p[0].y > p[1].y ? 1 : 0;
18519
+ body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, levels2.map((level) => {
18520
+ const r = radius2 * level;
18521
+ return /* @__PURE__ */ React7.createElement("g", { key: level }, kind === "fibcircles" ? /* @__PURE__ */ React7.createElement("circle", { className: "dhl-fib-ring", cx: center.x, cy: center.y, r }) : /* @__PURE__ */ React7.createElement("path", { className: "dhl-fib-ring", d: `M${center.x - r} ${center.y}A${r} ${r} 0 0 ${sweep} ${center.x + r} ${center.y}` }), text(center.x + 3, center.y + (sweep ? -r : r) + (sweep ? 12 : -4), String(level)));
18522
+ }), /* @__PURE__ */ React7.createElement("line", { className: "dhl-drawing-mid", x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y }));
18523
+ break;
18524
+ }
18525
+ case "fibwedge": {
18526
+ if (!p[1]) break;
18527
+ if (!p[2]) {
18528
+ body = /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y });
18529
+ break;
18530
+ }
18531
+ const radius2 = Math.hypot(p[1].x - p[0].x, p[1].y - p[0].y);
18532
+ 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);
18533
+ const span = Math.atan2(Math.sin(a2 - a1), Math.cos(a2 - a1));
18534
+ body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[0].x + Math.cos(a1) * radius2, y2: p[0].y + Math.sin(a1) * radius2 }), /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[0].x + Math.cos(a2) * radius2, y2: p[0].y + Math.sin(a2) * radius2 }), [0.236, 0.382, 0.5, 0.618, 0.786, 1].map((level) => {
18535
+ const r = radius2 * level;
18536
+ const s = { x: p[0].x + Math.cos(a1) * r, y: p[0].y + Math.sin(a1) * r }, e = { x: p[0].x + Math.cos(a1 + span) * r, y: p[0].y + Math.sin(a1 + span) * r };
18537
+ return /* @__PURE__ */ React7.createElement("g", { key: level }, /* @__PURE__ */ React7.createElement("path", { className: "dhl-fib-ring", d: `M${s.x} ${s.y}A${r} ${r} 0 0 ${span > 0 ? 1 : 0} ${e.x} ${e.y}` }), text(s.x + 3, s.y - 3, String(level)));
18538
+ }));
18539
+ break;
18540
+ }
18541
+ case "fibtrendtime": {
18542
+ if (!p[1]) break;
18543
+ if (!p[2]) {
18544
+ body = /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y });
18545
+ break;
18546
+ }
18547
+ const unit = points2[1].time - points2[0].time;
18548
+ body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("line", { className: "dhl-drawing-mid", x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y }), [0, 0.382, 0.5, 0.618, 1, 1.382, 1.618, 2, 2.618].map((level) => {
18549
+ const x = view.project(points2[2].time + unit * level, points2[2].price)?.x;
18550
+ return x == null || x < -2 || x > w + 2 ? null : /* @__PURE__ */ React7.createElement("g", { key: level }, /* @__PURE__ */ React7.createElement("line", { className: level === 0 || level === 1 ? "dhl-fib-key" : void 0, x1: x, x2: x, y1: "0", y2: h }), text(x + 3, 12, String(level)));
18551
+ }));
18552
+ break;
18553
+ }
18554
+ case "cypher": {
18555
+ const [X, A, B, C, D] = points2;
18556
+ body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, p[2] ? /* @__PURE__ */ React7.createElement("polygon", { className: "dhl-drawing-fill", points: `${p[0].x},${p[0].y} ${p[1].x},${p[1].y} ${p[2].x},${p[2].y}` }) : null, p[4] ? /* @__PURE__ */ React7.createElement("polygon", { className: "dhl-drawing-fill", points: `${p[2].x},${p[2].y} ${p[3].x},${p[3].y} ${p[4].x},${p[4].y}` }) : null, wave(p, ["X", "A", "B", "C", "D"], text), p[2] ? guide(p[0], p[2], ratio(B.price - A.price, A.price - X.price), "xb", text) : null, p[3] ? guide(p[1], p[3], ratio(C.price - X.price, A.price - X.price), "xc", text) : null, p[4] ? guide(p[0], p[4], ratio(C.price - D.price, C.price - X.price), "xd", text) : null);
18557
+ break;
18558
+ }
18559
+ case "threedrives": {
18560
+ body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, wave(p, ["", "1", "A", "2", "B", "3"], text), p[2] ? guide(p[0], p[2], ratio(points2[2].price - points2[1].price, points2[1].price - points2[0].price), "a", text) : null, p[3] ? guide(p[1], p[3], ratio(points2[3].price - points2[2].price, points2[1].price - points2[2].price), "d2", text) : null, p[4] ? guide(p[2], p[4], ratio(points2[4].price - points2[3].price, points2[3].price - points2[2].price), "b", text) : null, p[5] ? guide(p[3], p[5], ratio(points2[5].price - points2[4].price, points2[3].price - points2[4].price), "d3", text) : null);
18561
+ break;
18562
+ }
18563
+ case "tripattern": {
18564
+ const upper = p[2] ? clipLine(p[0], p[2], w, h, 0) : null, lower = p[3] ? clipLine(p[1], p[3], w, h, 0) : null;
18565
+ body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, wave(p, ["A", "B", "C", "D"], text), upper ? /* @__PURE__ */ React7.createElement("line", { className: "dhl-neckline", x1: upper[0].x, y1: upper[0].y, x2: upper[1].x, y2: upper[1].y }) : null, lower ? /* @__PURE__ */ React7.createElement("line", { className: "dhl-neckline", x1: lower[0].x, y1: lower[0].y, x2: lower[1].x, y2: lower[1].y }) : null);
18566
+ break;
18567
+ }
18568
+ case "elliotttri":
18569
+ body = wave(p, ["0", "(A)", "(B)", "(C)", "(D)", "(E)"], text);
18570
+ break;
18571
+ case "elliottwxy":
18572
+ body = wave(p, ["0", "(W)", "(X)", "(Y)"], text);
18573
+ break;
18574
+ case "elliottwxyxz":
18575
+ body = wave(p, ["0", "(W)", "(X)", "(Y)", "(X)", "(Z)"], text);
18576
+ break;
18577
+ case "cycles": {
18578
+ if (!p[1]) break;
18579
+ const step = points2[1].time - points2[0].time;
18580
+ const xs = [];
18581
+ const spacing = (view.project(points2[0].time + step, points2[0].price)?.x ?? NaN) - p[0].x;
18582
+ const first = Number.isFinite(spacing) && spacing > 0 ? Math.max(0, Math.floor(-p[0].x / spacing) - 1) : 0;
18583
+ for (let n2 = first; n2 < first + 400; n2 += 1) {
18584
+ const x = view.project(points2[0].time + step * n2, points2[0].price)?.x;
18585
+ if (x == null || x > w + 2) break;
18586
+ if (x >= -2) xs.push(x);
18587
+ }
18588
+ body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, xs.map((x, index) => /* @__PURE__ */ React7.createElement("line", { key: index, className: index === 0 ? "dhl-fib-key" : void 0, x1: x, x2: x, y1: "0", y2: h })));
18285
18589
  handles = [p[0], p[1]];
18286
18590
  break;
18287
18591
  }
18592
+ case "sine": {
18593
+ if (!p[1]) break;
18594
+ const half = p[1].x - p[0].x;
18595
+ if (Math.abs(half) < 2) {
18596
+ body = /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y });
18597
+ break;
18598
+ }
18599
+ const mid = (p[0].y + p[1].y) / 2, amp = (p[0].y - p[1].y) / 2;
18600
+ const step = Math.max(2, Math.abs(half) / 16);
18601
+ const pts = [];
18602
+ for (let x = 0; x <= w; x += step) pts.push(`${x.toFixed(1)},${(mid + amp * Math.cos(Math.PI * (x - p[0].x) / half)).toFixed(1)}`);
18603
+ body = /* @__PURE__ */ React7.createElement("polyline", { className: "dhl-pattern", points: pts.join(" ") });
18604
+ break;
18605
+ }
18606
+ case "ellipse": {
18607
+ if (!p[1]) break;
18608
+ body = /* @__PURE__ */ React7.createElement("ellipse", { className: "dhl-drawing-fill", cx: (p[0].x + p[1].x) / 2, cy: (p[0].y + p[1].y) / 2, rx: Math.abs(p[1].x - p[0].x) / 2, ry: Math.abs(p[1].y - p[0].y) / 2 });
18609
+ break;
18610
+ }
18611
+ case "rotrect": {
18612
+ if (!p[1]) break;
18613
+ if (!p[2]) {
18614
+ body = /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y });
18615
+ break;
18616
+ }
18617
+ const ux = p[1].x - p[0].x, uy = p[1].y - p[0].y, len = Math.hypot(ux, uy) || 1;
18618
+ const nx = -uy / len, ny = ux / len;
18619
+ const d = (p[2].x - p[0].x) * nx + (p[2].y - p[0].y) * ny;
18620
+ const c = { x: p[1].x + nx * d, y: p[1].y + ny * d }, e = { x: p[0].x + nx * d, y: p[0].y + ny * d };
18621
+ body = /* @__PURE__ */ React7.createElement("polygon", { className: "dhl-drawing-fill", points: `${p[0].x},${p[0].y} ${p[1].x},${p[1].y} ${c.x},${c.y} ${e.x},${e.y}` });
18622
+ break;
18623
+ }
18624
+ case "arc":
18625
+ case "curve": {
18626
+ if (!p[1]) break;
18627
+ if (!p[2]) {
18628
+ body = /* @__PURE__ */ React7.createElement("line", { x1: p[0].x, y1: p[0].y, x2: p[1].x, y2: p[1].y });
18629
+ break;
18630
+ }
18631
+ const control = kind === "arc" ? { x: 2 * p[2].x - (p[0].x + p[1].x) / 2, y: 2 * p[2].y - (p[0].y + p[1].y) / 2 } : p[2];
18632
+ body = /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("path", { className: "dhl-curve", d: `M${p[0].x} ${p[0].y}Q${control.x} ${control.y} ${p[1].x} ${p[1].y}` }), kind === "curve" && anchors ? /* @__PURE__ */ React7.createElement("polyline", { className: "dhl-drawing-mid", points: `${p[0].x},${p[0].y} ${p[2].x},${p[2].y} ${p[1].x},${p[1].y}` }) : null);
18633
+ break;
18634
+ }
18288
18635
  case "text": {
18289
18636
  const value = textValue || label || tr("\u6587\u672C", "Text");
18290
18637
  const lines = wrapText(value, 300);
@@ -18360,7 +18707,7 @@ function saveStyleTemplate(kind, style) {
18360
18707
  // src/client/components/draw-toolbar.tsx
18361
18708
  var DRAW_COLORS = ["#4c82d8", "#2fd3a0", "#ef5f6c", "#e2a943", "#9b86db", "#26b5c8", "#8a8f98"];
18362
18709
  var STYLES = [["solid", "\u5B9E\u7EBF", "Solid"], ["dashed", "\u865A\u7EBF", "Dashed"], ["dotted", "\u70B9\u7EBF", "Dotted"]];
18363
- function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone, onCoordinates }) {
18710
+ function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone, onCoordinates, placement = "top" }) {
18364
18711
  const [savedDefault, setSavedDefault] = React8.useState(false);
18365
18712
  React8.useEffect(() => {
18366
18713
  setSavedDefault(false);
@@ -18391,7 +18738,7 @@ function DrawToolbar({ mark, globallyLocked, onEdit, onRemove, onClose, onClone,
18391
18738
  const style = mark.lineStyle ?? "solid";
18392
18739
  const tool = toolOf(mark.kind);
18393
18740
  const stop = (event) => event.stopPropagation();
18394
- return /* @__PURE__ */ React8.createElement("div", { className: "dhl-draw-toolbar", role: "toolbar", "aria-label": tr("\u753B\u7EBF\u6837\u5F0F", "Drawing style"), onPointerDown: stop, onClick: stop }, /* @__PURE__ */ React8.createElement("span", { className: "dhl-draw-toolbar-name" }, tr(tool.zh, tool.en)), isTextKind(mark.kind) ? /* @__PURE__ */ React8.createElement(
18741
+ return /* @__PURE__ */ React8.createElement("div", { className: "dhl-draw-toolbar" + (placement === "bottom" ? " at-bottom" : ""), role: "toolbar", "aria-label": tr("\u753B\u7EBF\u6837\u5F0F", "Drawing style"), onPointerDown: stop, onClick: stop }, /* @__PURE__ */ React8.createElement("span", { className: "dhl-draw-toolbar-name" }, tr(tool.zh, tool.en)), isTextKind(mark.kind) ? /* @__PURE__ */ React8.createElement(
18395
18742
  "input",
18396
18743
  {
18397
18744
  className: "dhl-draw-toolbar-text",
@@ -18728,7 +19075,23 @@ function cssVar(el, name, fallback) {
18728
19075
  function chartData(points2) {
18729
19076
  return points2.map((point) => ({ time: point.time, value: point.value }));
18730
19077
  }
19078
+ var TOOLBAR_CLEARANCE = 84;
18731
19079
  var FINITE_KINDS = /* @__PURE__ */ new Set([
19080
+ "highlighter",
19081
+ "path",
19082
+ "polyline",
19083
+ "pin",
19084
+ "ellipse",
19085
+ "curve",
19086
+ "note",
19087
+ "pricenote",
19088
+ "signpost",
19089
+ "forecast",
19090
+ "cypher",
19091
+ "threedrives",
19092
+ "elliotttri",
19093
+ "elliottwxy",
19094
+ "elliottwxyxz",
18732
19095
  "trend",
18733
19096
  "rect",
18734
19097
  "measure",
@@ -19022,6 +19385,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19022
19385
  box.addEventListener("pointerup", move, true);
19023
19386
  box.addEventListener("pointercancel", cancel, true);
19024
19387
  let touchTapAt = -Infinity;
19388
+ let lastMultiClick = null, multiFinishedAt = null;
19025
19389
  const fromTouch = (event) => event.pointerType === "touch" || Boolean(event.sourceCapabilities?.firesTouchEvents);
19026
19390
  const chartClick = (event) => {
19027
19391
  if (event.type === "click" && fromTouch(event) && performance.now() - touchTapAt < 800) return false;
@@ -19044,6 +19408,25 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19044
19408
  onDrawRef.current({ kind: "level", price: point.price });
19045
19409
  return;
19046
19410
  }
19411
+ if (isMultiKind(mode)) {
19412
+ const now = performance.now(), here = { x: event.clientX, y: event.clientY };
19413
+ const near = (other, within = Infinity) => Boolean(other && now - other.at < within && Math.hypot(here.x - other.x, here.y - other.y) <= 6);
19414
+ if (near(multiFinishedAt, 500)) {
19415
+ multiFinishedAt = null;
19416
+ return;
19417
+ }
19418
+ if (pending.current.length && near(lastMultiClick)) {
19419
+ multiFinishedAt = { ...here, at: now };
19420
+ lastMultiClick = null;
19421
+ finishMultiRef.current();
19422
+ return;
19423
+ }
19424
+ lastMultiClick = { ...here, at: now };
19425
+ pending.current = [...pending.current, point];
19426
+ setPendingPoints(pending.current);
19427
+ if (pending.current.length >= PATH_MAX) finishMultiRef.current();
19428
+ return;
19429
+ }
19047
19430
  const clicks = [...pending.current, point];
19048
19431
  if (clicks.length < clicksNeeded(mode)) {
19049
19432
  pending.current = clicks;
@@ -19494,6 +19877,26 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19494
19877
  setPendingPoints([]);
19495
19878
  setDrawingHover(null);
19496
19879
  }, [drawMode]);
19880
+ const finishMulti = () => {
19881
+ const mode = drawModeRef.current;
19882
+ const points2 = pending.current;
19883
+ pending.current = [];
19884
+ setPendingPoints([]);
19885
+ if (mode && isMultiKind(mode) && points2.length >= 2) onDrawRef.current({ kind: mode, points: points2 });
19886
+ };
19887
+ const finishMultiRef = React10.useRef(finishMulti);
19888
+ finishMultiRef.current = finishMulti;
19889
+ React10.useEffect(() => {
19890
+ if (!drawMode || !isMultiKind(drawMode)) return void 0;
19891
+ const onKey = (event) => {
19892
+ const element = event.target;
19893
+ if (event.key !== "Enter" || element?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(element?.tagName ?? "")) return;
19894
+ event.preventDefault();
19895
+ finishMultiRef.current();
19896
+ };
19897
+ window.addEventListener("keydown", onKey, true);
19898
+ return () => window.removeEventListener("keydown", onKey, true);
19899
+ }, [drawMode]);
19497
19900
  React10.useEffect(() => {
19498
19901
  if (resetToken > 0) refs.current?.chart.timeScale().fitContent();
19499
19902
  }, [resetToken]);
@@ -19521,6 +19924,12 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19521
19924
  window.__dhlPriceLines = () => priceLines.current.size;
19522
19925
  }, [marks, themeVersion, chartType, interval]);
19523
19926
  const warming = candles.length < 2;
19927
+ const toolbarPlacement = (mark) => {
19928
+ if (!view) return "top";
19929
+ const ys = markPoints(mark).map((point) => view.project(point.time, point.price)?.y).filter((y) => y != null);
19930
+ const top = ys.filter((y) => y < TOOLBAR_CLEARANCE).length, bottom = ys.filter((y) => y > view.height - TOOLBAR_CLEARANCE).length;
19931
+ return bottom < top ? "bottom" : "top";
19932
+ };
19524
19933
  const markById = React10.useMemo(() => new Map(marks.map((mark) => [mark.id, mark])), [marks]);
19525
19934
  const markByIdRef = React10.useRef(markById);
19526
19935
  markByIdRef.current = markById;
@@ -19635,7 +20044,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19635
20044
  const abortGesture = React10.useRef(null);
19636
20045
  React10.useEffect(() => () => abortGesture.current?.(), []);
19637
20046
  const [brushPoints, setBrushPoints] = React10.useState([]);
19638
- const beginBrush = (event) => {
20047
+ const beginBrush = (event, freehandKind) => {
19639
20048
  const snap = snapRef.current;
19640
20049
  const first = snap?.(event.nativeEvent, void 0, false);
19641
20050
  if (!snap || !first) return;
@@ -19674,7 +20083,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19674
20083
  if (collected.length < 2) return;
19675
20084
  const stride = Math.ceil(collected.length / BRUSH_MAX);
19676
20085
  const points2 = collected.filter((_, index) => index % stride === 0 || index === collected.length - 1).slice(-BRUSH_MAX);
19677
- onDrawRef.current({ kind: "brush", points: points2 });
20086
+ onDrawRef.current({ kind: freehandKind, points: points2 });
19678
20087
  };
19679
20088
  abortGesture.current?.();
19680
20089
  abortGesture.current = cancel;
@@ -19733,8 +20142,8 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19733
20142
  }
19734
20143
  }, [selectRequest, markById, timeline]);
19735
20144
  const beginDrag = React10.useCallback((event) => {
19736
- if (drawModeRef.current === "brush" && event.button === 0) {
19737
- beginBrush(event);
20145
+ if (drawModeRef.current && FREEHAND_KINDS.includes(drawModeRef.current) && event.button === 0) {
20146
+ beginBrush(event, drawModeRef.current);
19738
20147
  return;
19739
20148
  }
19740
20149
  if (drawModeRef.current || event.button !== 0) return;
@@ -19929,7 +20338,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19929
20338
  const studyAt = (key3) => studies[key3].find((point) => point.time === inspectedTime)?.value;
19930
20339
  const inspectedStudies = studyDefinitions(settings).filter(([id]) => indicators.has(id)).map(([, key3, label]) => [label, studyAt(key3)]);
19931
20340
  const paneCount = ["rsi14", "macd", "atr14", "stochastic", "adx", "obv"].filter((id) => indicators.has(id)).length;
19932
- 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 === "brush" ? " 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 }) => {
20341
+ 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 }) => {
19933
20342
  const live = draft?.id === mark.id ? draft.points : groupDraft?.get(mark.id) ?? points2;
19934
20343
  const shown = mark.kind === "level" ? [{ time: lastTime, price: live[0].price }] : live;
19935
20344
  const selected = selectedId === mark.id || group.includes(mark.id);
@@ -19945,7 +20354,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19945
20354
  points: shown,
19946
20355
  view,
19947
20356
  text: mark.label,
19948
- dataVersion: mark.kind === "vprofile" || mark.kind === "regression" ? candles : void 0,
20357
+ dataVersion: mark.kind === "vprofile" || mark.kind === "regression" || mark.kind === "avwap" || mark.kind === "avprofile" || mark.kind === "barspattern" ? candles : void 0,
19949
20358
  className: `dhl-drawing ${roleClass(mark)}${mark.kind === "level" ? " is-level" : ""}${selected ? " selected" : ""}${mark.locked || drawingsLocked ? " locked" : ""}`,
19950
20359
  label: mark.source === "user" || mark.kind === "level" ? void 0 : mark.label,
19951
20360
  anchors: selected,
@@ -19971,10 +20380,10 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
19971
20380
  if (!point) return null;
19972
20381
  const short = event.reason.length > 18 ? event.reason.slice(0, 17) + "\u2026" : event.reason;
19973
20382
  return /* @__PURE__ */ React10.createElement("g", { key: event.id, className: `dhl-fill ${event.isBuy ? "buy" : "sell"}${event.reason ? " has-reason" : ""}${selectedFill === event.id ? " selected" : ""}`, "data-fill-id": event.id }, event.reason ? /* @__PURE__ */ React10.createElement("circle", { className: "dhl-fill-ring", cx: point.x, cy: point.y, r: "9" }) : null, /* @__PURE__ */ React10.createElement("circle", { className: "dhl-fill-dot", cx: point.x, cy: point.y, r: "5" }), showReasons && short ? /* @__PURE__ */ React10.createElement("text", { className: "dhl-fill-reason", x: point.x + 12, y: point.y + 4 }, short) : null);
19974
- })), brushPoints.length > 1 ? /* @__PURE__ */ React10.createElement(DrawingShape, { kind: "brush", points: brushPoints, view, className: "dhl-drawing dhl-drawing-preview role-user" }) : null, drawMode && drawingHover && hoverSnapped ? (() => {
20383
+ })), brushPoints.length > 1 ? /* @__PURE__ */ React10.createElement(DrawingShape, { kind: drawMode === "highlighter" ? "highlighter" : "brush", points: brushPoints, view, className: "dhl-drawing dhl-drawing-preview role-user" }) : null, drawMode && drawingHover && hoverSnapped ? (() => {
19975
20384
  const at = view.project(drawingHover.time, drawingHover.price);
19976
20385
  return at ? /* @__PURE__ */ React10.createElement("circle", { className: "dhl-snap-ring", cx: at.x, cy: at.y, r: "6" }) : null;
19977
- })() : null, drawMode && drawMode !== "brush" && previewPoints.length ? /* @__PURE__ */ React10.createElement(DrawingShape, { kind: drawMode, points: completePreview(drawMode, previewPoints), view, className: "dhl-drawing dhl-drawing-preview role-user", anchors: true }) : null)) : null, fillCard && fillAt && view ? /* @__PURE__ */ React10.createElement(
20386
+ })() : null, drawMode && !FREEHAND_KINDS.includes(drawMode) && previewPoints.length ? /* @__PURE__ */ React10.createElement(DrawingShape, { kind: drawMode, points: completePreview(drawMode, previewPoints), view, className: "dhl-drawing dhl-drawing-preview role-user", anchors: true }) : null)) : null, fillCard && fillAt && view ? /* @__PURE__ */ React10.createElement(
19978
20387
  "div",
19979
20388
  {
19980
20389
  className: "dhl-fill-card",
@@ -20006,6 +20415,7 @@ function CandleChart({ candles, marks, indicators, expanded, resetToken, drawMod
20006
20415
  {
20007
20416
  mark: selectedMark,
20008
20417
  globallyLocked: drawingsLocked,
20418
+ placement: toolbarPlacement(selectedMark),
20009
20419
  onCoordinates: () => setEditingId(selectedMark.id),
20010
20420
  onEdit: (patch) => onEditMark(selectedMark.id, patch),
20011
20421
  onClone: () => onCloneMark(cloneOf(selectedMark)),
@@ -21379,6 +21789,7 @@ function DeskColumn({ onCollapse }) {
21379
21789
  const [textAt, setTextAt] = React26.useState(null);
21380
21790
  const [selectRequest, setSelectRequest] = React26.useState(null);
21381
21791
  const [textValue, setTextValue] = React26.useState("");
21792
+ const [toolQuery, setToolQuery] = React26.useState("");
21382
21793
  const [confirmClear, setConfirmClear] = React26.useState(false);
21383
21794
  const [pickerOpen, setPickerOpen] = React26.useState(false);
21384
21795
  const [chartExpanded, setChartExpanded] = React26.useState(false);
@@ -21542,7 +21953,7 @@ function DeskColumn({ onCollapse }) {
21542
21953
  }, [indicators, s.candles, s.candlesOlder]);
21543
21954
  React26.useEffect(() => {
21544
21955
  if (s.candlesOlder !== "idle" || s.candles.length < 2) return;
21545
- const earliest = Math.min(...s.chartMarks.filter((mark2) => !mark2.hidden && (mark2.kind === "vprofile" || mark2.kind === "regression")).map((mark2) => Math.min(...(mark2.points ?? []).map((point) => point.time))));
21956
+ const earliest = Math.min(...s.chartMarks.filter((mark2) => !mark2.hidden && ["vprofile", "regression", "avwap", "avprofile"].includes(mark2.kind)).map((mark2) => Math.min(...(mark2.points ?? []).map((point) => point.time))));
21546
21957
  if (Number.isFinite(earliest) && s.candles[0].t > earliest) void loadOlderCandles();
21547
21958
  }, [s.chartMarks, s.candles, s.candlesOlder]);
21548
21959
  React26.useEffect(() => {
@@ -21599,7 +22010,30 @@ function DeskColumn({ onCollapse }) {
21599
22010
  }
21600
22011
  }
21601
22012
  )
21602
- ), pickerOpen ? /* @__PURE__ */ React26.createElement(MarketPicker, { activeCoin: coin, onClose: () => setPickerOpen(false) }) : null, /* @__PURE__ */ React26.createElement("div", { className: "dhl-market-workspace" + (s.tapePlacement === "below" ? " dhl-tape-below" : "") }, /* @__PURE__ */ React26.createElement("section", { className: "dhl-chart-panel" + (chartExpanded ? " dhl-chart-panel-expanded" : ""), "aria-label": tr("\u884C\u60C5\u56FE\u8868", "Market chart") }, /* @__PURE__ */ React26.createElement("div", { className: "dhl-chart-tabs", role: "tablist", "aria-label": tr("\u56FE\u8868\u5185\u5BB9", "Chart content") }, /* @__PURE__ */ React26.createElement("button", { type: "button", role: "tab", "aria-selected": chartTab === "chart", className: chartTab === "chart" ? "on" : "", onClick: () => saveSessionWorkspace({ chartTab: "chart" }) }, tr("K \u7EBF", "Chart")), row?.kind !== "spot" && row?.kind !== "outcome" ? /* @__PURE__ */ React26.createElement("button", { type: "button", role: "tab", "aria-selected": chartTab === "funding", className: chartTab === "funding" ? "on" : "", onClick: () => saveSessionWorkspace({ chartTab: "funding" }) }, tr("\u8D44\u91D1\u8D39\u5386\u53F2", "Funding history")) : null), chartTab === "chart" ? /* @__PURE__ */ React26.createElement("div", { role: "tabpanel" }, /* @__PURE__ */ React26.createElement("div", { className: "dhl-chart-tools" }, /* @__PURE__ */ React26.createElement("div", { className: "dhl-intervals", "aria-label": tr("K \u7EBF\u5468\u671F", "Chart interval") }, INTERVALS.map((iv) => /* @__PURE__ */ React26.createElement("button", { key: iv, type: "button", className: s.interval === iv ? "on" : "", onClick: () => changeInterval(iv) }, iv)), /* @__PURE__ */ React26.createElement(Select, { "aria-label": tr("\u66F4\u591A K \u7EBF\u5468\u671F", "More chart intervals"), value: MORE_INTERVALS.includes(s.interval) ? s.interval : "", onChange: (event) => changeInterval(event.target.value) }, /* @__PURE__ */ React26.createElement("option", { value: "", disabled: true }, tr("\u66F4\u591A", "More")), MORE_INTERVALS.map((iv) => /* @__PURE__ */ React26.createElement("option", { key: iv, value: iv }, iv)))), /* @__PURE__ */ React26.createElement("div", { className: "dhl-chart-actions" }, /* @__PURE__ */ React26.createElement(Select, { "aria-label": tr("\u56FE\u8868\u7C7B\u578B", "Chart type"), value: chartType, onChange: (event) => saveSessionWorkspace({ chartType: event.target.value }) }, [["candles", "\u8721\u70DB\u56FE", "Candles"], ["bars", "\u7F8E\u56FD\u7EBF", "OHLC bars"], ["line", "\u6298\u7EBF\u56FE", "Line"], ["area", "\u9762\u79EF\u56FE", "Area"], ["heikinAshi", "\u5E73\u5747K\u7EBF", "Heikin Ashi"]].map(([value, zh, en]) => /* @__PURE__ */ React26.createElement("option", { key: value, value }, tr(zh, en)))), /* @__PURE__ */ React26.createElement("details", { className: "dhl-layer-menu" }, /* @__PURE__ */ React26.createElement("summary", null, tr("\u663E\u793A", "Show"), /* @__PURE__ */ React26.createElement(UiIcon, { name: "chevronDown", size: 12 })), /* @__PURE__ */ React26.createElement("div", { className: "dhl-layer-popover" }, [["orders", tr("\u59D4\u6258", "Orders")], ["fills", tr("\u6210\u4EA4", "Fills")], ["reasons", tr("\u6210\u4EA4\u539F\u56E0", "Fill reasons")], ["drawings", tr("\u753B\u7EBF", "Drawings")]].map(([id, label]) => /* @__PURE__ */ React26.createElement("label", { key: id }, /* @__PURE__ */ React26.createElement("input", { type: "checkbox", checked: layers[id], onChange: (event) => saveSessionWorkspace({ chartLayers: { ...layers, [id]: event.target.checked } }) }), label)))), /* @__PURE__ */ React26.createElement("details", { className: "dhl-indicator-menu" }, /* @__PURE__ */ React26.createElement("summary", { "aria-label": tr("\u9009\u62E9\u56FE\u8868\u6307\u6807", "Choose chart indicators") }, tr("\u6307\u6807", "Indicators"), " ", /* @__PURE__ */ React26.createElement("span", { className: "dhl-indicator-count" }, indicators.size + maLines.length)), /* @__PURE__ */ React26.createElement("div", { className: "dhl-indicator-popover", role: "group", "aria-label": tr("\u56FE\u8868\u6307\u6807", "Chart indicators") }, /* @__PURE__ */ React26.createElement("strong", null, tr("\u56FE\u8868\u6307\u6807", "Chart indicators")), INDICATOR_CHOICES.map(([id, label, description]) => /* @__PURE__ */ React26.createElement("button", { type: "button", key: id, className: indicators.has(id) ? "on" : "", "aria-pressed": indicators.has(id), onClick: () => toggleIndicator(id) }, /* @__PURE__ */ React26.createElement("span", { className: "dhl-indicator-check", "aria-hidden": "true" }, indicators.has(id) ? /* @__PURE__ */ React26.createElement(UiIcon, { name: "check", size: 13 }) : null), /* @__PURE__ */ React26.createElement("span", null, /* @__PURE__ */ React26.createElement("b", null, id === "rsi14" ? `RSI ${studySettings.rsi.period}` : id === "bollinger20" ? `BOLL ${studySettings.bollinger.period} \xD7 ${studySettings.bollinger.multiplier}` : tr(label, INDICATOR_EN[id]?.[0] ?? label)), /* @__PURE__ */ React26.createElement("small", null, id === "macd" ? `${studySettings.macd.fast} / ${studySettings.macd.slow} / ${studySettings.macd.signal}` : id === "bollinger20" ? tr("\u5747\u7EBF\u4E0E\u6807\u51C6\u5DEE\u901A\u9053", "Mean and standard deviation bands") : tr(description, INDICATOR_EN[id]?.[1] ?? description))))), /* @__PURE__ */ React26.createElement(IndicatorEditor, { key: s.activeSessionId, settings: s.workspace?.indicatorSettings, indicators: [...indicators], maLines, onApply: saveSessionWorkspace }), /* @__PURE__ */ React26.createElement(MaEditor, { lines: maLines, onChange: (next) => saveSessionWorkspace({ maLines: next }) }))), /* @__PURE__ */ React26.createElement("button", { type: "button", className: s.workspace?.showDataWindow ? "on" : "", "aria-pressed": s.workspace?.showDataWindow ?? false, onClick: () => saveSessionWorkspace({ showDataWindow: !s.workspace?.showDataWindow }) }, tr("\u6570\u636E\u7A97\u53E3", "Data window")), /* @__PURE__ */ React26.createElement(StrategyMenu, { candles: s.candles, coin, interval: s.interval }), /* @__PURE__ */ React26.createElement("details", { className: "dhl-draw-menu" + (drawMode ? " active" : ""), ref: drawMenu }, /* @__PURE__ */ React26.createElement("summary", { "aria-label": tr("\u753B\u7EBF\u5DE5\u5177", "Drawing tools") }, drawMode ? /* @__PURE__ */ React26.createElement(React26.Fragment, null, /* @__PURE__ */ React26.createElement(ToolIcon, { kind: drawMode, size: 14 }), tr(toolOf(drawMode).zh, toolOf(drawMode).en)) : tr("\u753B\u7EBF", "Draw"), /* @__PURE__ */ React26.createElement(UiIcon, { name: "chevronDown", size: 12 })), /* @__PURE__ */ React26.createElement("div", { className: "dhl-draw-popover", role: "menu", "aria-label": tr("\u753B\u7EBF\u5DE5\u5177", "Drawing tools") }, DRAWING_TOOLS.map((tool, index) => /* @__PURE__ */ React26.createElement(React26.Fragment, { key: tool.kind }, index > 0 && DRAWING_TOOLS[index - 1].group !== tool.group ? /* @__PURE__ */ React26.createElement("hr", null) : null, /* @__PURE__ */ React26.createElement("div", { className: "dhl-draw-tool-row" }, /* @__PURE__ */ React26.createElement(
22013
+ ), pickerOpen ? /* @__PURE__ */ React26.createElement(MarketPicker, { activeCoin: coin, onClose: () => setPickerOpen(false) }) : null, /* @__PURE__ */ React26.createElement("div", { className: "dhl-market-workspace" + (s.tapePlacement === "below" ? " dhl-tape-below" : "") }, /* @__PURE__ */ React26.createElement("section", { className: "dhl-chart-panel" + (chartExpanded ? " dhl-chart-panel-expanded" : ""), "aria-label": tr("\u884C\u60C5\u56FE\u8868", "Market chart") }, /* @__PURE__ */ React26.createElement("div", { className: "dhl-chart-tabs", role: "tablist", "aria-label": tr("\u56FE\u8868\u5185\u5BB9", "Chart content") }, /* @__PURE__ */ React26.createElement("button", { type: "button", role: "tab", "aria-selected": chartTab === "chart", className: chartTab === "chart" ? "on" : "", onClick: () => saveSessionWorkspace({ chartTab: "chart" }) }, tr("K \u7EBF", "Chart")), row?.kind !== "spot" && row?.kind !== "outcome" ? /* @__PURE__ */ React26.createElement("button", { type: "button", role: "tab", "aria-selected": chartTab === "funding", className: chartTab === "funding" ? "on" : "", onClick: () => saveSessionWorkspace({ chartTab: "funding" }) }, tr("\u8D44\u91D1\u8D39\u5386\u53F2", "Funding history")) : null), chartTab === "chart" ? /* @__PURE__ */ React26.createElement("div", { role: "tabpanel" }, /* @__PURE__ */ React26.createElement("div", { className: "dhl-chart-tools" }, /* @__PURE__ */ React26.createElement("div", { className: "dhl-intervals", "aria-label": tr("K \u7EBF\u5468\u671F", "Chart interval") }, INTERVALS.map((iv) => /* @__PURE__ */ React26.createElement("button", { key: iv, type: "button", className: s.interval === iv ? "on" : "", onClick: () => changeInterval(iv) }, iv)), /* @__PURE__ */ React26.createElement(Select, { "aria-label": tr("\u66F4\u591A K \u7EBF\u5468\u671F", "More chart intervals"), value: MORE_INTERVALS.includes(s.interval) ? s.interval : "", onChange: (event) => changeInterval(event.target.value) }, /* @__PURE__ */ React26.createElement("option", { value: "", disabled: true }, tr("\u66F4\u591A", "More")), MORE_INTERVALS.map((iv) => /* @__PURE__ */ React26.createElement("option", { key: iv, value: iv }, iv)))), /* @__PURE__ */ React26.createElement("div", { className: "dhl-chart-actions" }, /* @__PURE__ */ React26.createElement(Select, { "aria-label": tr("\u56FE\u8868\u7C7B\u578B", "Chart type"), value: chartType, onChange: (event) => saveSessionWorkspace({ chartType: event.target.value }) }, [["candles", "\u8721\u70DB\u56FE", "Candles"], ["bars", "\u7F8E\u56FD\u7EBF", "OHLC bars"], ["line", "\u6298\u7EBF\u56FE", "Line"], ["area", "\u9762\u79EF\u56FE", "Area"], ["heikinAshi", "\u5E73\u5747K\u7EBF", "Heikin Ashi"]].map(([value, zh, en]) => /* @__PURE__ */ React26.createElement("option", { key: value, value }, tr(zh, en)))), /* @__PURE__ */ React26.createElement("details", { className: "dhl-layer-menu" }, /* @__PURE__ */ React26.createElement("summary", null, tr("\u663E\u793A", "Show"), /* @__PURE__ */ React26.createElement(UiIcon, { name: "chevronDown", size: 12 })), /* @__PURE__ */ React26.createElement("div", { className: "dhl-layer-popover" }, [["orders", tr("\u59D4\u6258", "Orders")], ["fills", tr("\u6210\u4EA4", "Fills")], ["reasons", tr("\u6210\u4EA4\u539F\u56E0", "Fill reasons")], ["drawings", tr("\u753B\u7EBF", "Drawings")]].map(([id, label]) => /* @__PURE__ */ React26.createElement("label", { key: id }, /* @__PURE__ */ React26.createElement("input", { type: "checkbox", checked: layers[id], onChange: (event) => saveSessionWorkspace({ chartLayers: { ...layers, [id]: event.target.checked } }) }), label)))), /* @__PURE__ */ React26.createElement("details", { className: "dhl-indicator-menu" }, /* @__PURE__ */ React26.createElement("summary", { "aria-label": tr("\u9009\u62E9\u56FE\u8868\u6307\u6807", "Choose chart indicators") }, tr("\u6307\u6807", "Indicators"), " ", /* @__PURE__ */ React26.createElement("span", { className: "dhl-indicator-count" }, indicators.size + maLines.length)), /* @__PURE__ */ React26.createElement("div", { className: "dhl-indicator-popover", role: "group", "aria-label": tr("\u56FE\u8868\u6307\u6807", "Chart indicators") }, /* @__PURE__ */ React26.createElement("strong", null, tr("\u56FE\u8868\u6307\u6807", "Chart indicators")), INDICATOR_CHOICES.map(([id, label, description]) => /* @__PURE__ */ React26.createElement("button", { type: "button", key: id, className: indicators.has(id) ? "on" : "", "aria-pressed": indicators.has(id), onClick: () => toggleIndicator(id) }, /* @__PURE__ */ React26.createElement("span", { className: "dhl-indicator-check", "aria-hidden": "true" }, indicators.has(id) ? /* @__PURE__ */ React26.createElement(UiIcon, { name: "check", size: 13 }) : null), /* @__PURE__ */ React26.createElement("span", null, /* @__PURE__ */ React26.createElement("b", null, id === "rsi14" ? `RSI ${studySettings.rsi.period}` : id === "bollinger20" ? `BOLL ${studySettings.bollinger.period} \xD7 ${studySettings.bollinger.multiplier}` : tr(label, INDICATOR_EN[id]?.[0] ?? label)), /* @__PURE__ */ React26.createElement("small", null, id === "macd" ? `${studySettings.macd.fast} / ${studySettings.macd.slow} / ${studySettings.macd.signal}` : id === "bollinger20" ? tr("\u5747\u7EBF\u4E0E\u6807\u51C6\u5DEE\u901A\u9053", "Mean and standard deviation bands") : tr(description, INDICATOR_EN[id]?.[1] ?? description))))), /* @__PURE__ */ React26.createElement(IndicatorEditor, { key: s.activeSessionId, settings: s.workspace?.indicatorSettings, indicators: [...indicators], maLines, onApply: saveSessionWorkspace }), /* @__PURE__ */ React26.createElement(MaEditor, { lines: maLines, onChange: (next) => saveSessionWorkspace({ maLines: next }) }))), /* @__PURE__ */ React26.createElement("button", { type: "button", className: s.workspace?.showDataWindow ? "on" : "", "aria-pressed": s.workspace?.showDataWindow ?? false, onClick: () => saveSessionWorkspace({ showDataWindow: !s.workspace?.showDataWindow }) }, tr("\u6570\u636E\u7A97\u53E3", "Data window")), /* @__PURE__ */ React26.createElement(StrategyMenu, { candles: s.candles, coin, interval: s.interval }), /* @__PURE__ */ React26.createElement("details", { className: "dhl-draw-menu" + (drawMode ? " active" : ""), ref: drawMenu }, /* @__PURE__ */ React26.createElement("summary", { "aria-label": tr("\u753B\u7EBF\u5DE5\u5177", "Drawing tools") }, drawMode ? /* @__PURE__ */ React26.createElement(React26.Fragment, null, /* @__PURE__ */ React26.createElement(ToolIcon, { kind: drawMode, size: 14 }), tr(toolOf(drawMode).zh, toolOf(drawMode).en)) : tr("\u753B\u7EBF", "Draw"), /* @__PURE__ */ React26.createElement(UiIcon, { name: "chevronDown", size: 12 })), /* @__PURE__ */ React26.createElement("div", { className: "dhl-draw-popover", role: "menu", "aria-label": tr("\u753B\u7EBF\u5DE5\u5177", "Drawing tools") }, /* @__PURE__ */ React26.createElement(
22014
+ "input",
22015
+ {
22016
+ className: "dhl-draw-search",
22017
+ type: "search",
22018
+ value: toolQuery,
22019
+ placeholder: tr(`\u641C\u7D22 ${DRAWING_TOOLS.length} \u79CD\u753B\u7EBF\u5DE5\u5177`, `Search ${DRAWING_TOOLS.length} drawing tools`),
22020
+ "aria-label": tr("\u641C\u7D22\u753B\u7EBF\u5DE5\u5177", "Search drawing tools"),
22021
+ onChange: (event) => setToolQuery(event.target.value),
22022
+ onKeyDown: (event) => {
22023
+ if (event.key === "Escape" && toolQuery) {
22024
+ event.stopPropagation();
22025
+ setToolQuery("");
22026
+ }
22027
+ }
22028
+ }
22029
+ ), (() => {
22030
+ const query = toolQuery.trim().toLowerCase();
22031
+ const shown = query ? DRAWING_TOOLS.filter((tool) => `${tool.zh} ${tool.en} ${tool.kind}`.toLowerCase().includes(query)) : DRAWING_TOOLS;
22032
+ return shown.length ? null : /* @__PURE__ */ React26.createElement("p", { className: "dhl-draw-empty" }, tr("\u6CA1\u6709\u5339\u914D\u7684\u753B\u7EBF\u5DE5\u5177", "No matching tools"));
22033
+ })(), DRAWING_TOOLS.filter((tool) => {
22034
+ const query = toolQuery.trim().toLowerCase();
22035
+ return !query || `${tool.zh} ${tool.en} ${tool.kind}`.toLowerCase().includes(query);
22036
+ }).map((tool, index, shown) => /* @__PURE__ */ React26.createElement(React26.Fragment, { key: tool.kind }, index > 0 && shown[index - 1].group !== tool.group ? /* @__PURE__ */ React26.createElement("hr", null) : null, /* @__PURE__ */ React26.createElement("div", { className: "dhl-draw-tool-row" }, /* @__PURE__ */ React26.createElement(
21603
22037
  "button",
21604
22038
  {
21605
22039
  type: "button",
@@ -1,4 +1,4 @@
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'
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'
2
2
  export type LineStyleName = 'solid' | 'dashed' | 'dotted'
3
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>
@@ -29,3 +29,6 @@ export declare const FIB_LEVEL_KINDS: DrawingKind[]
29
29
  export declare const INTERVALS: string[]
30
30
  export declare const FONT_KINDS: DrawingKind[]
31
31
  export declare const FILL_KINDS: DrawingKind[]
32
+ export declare const FREEHAND_KINDS: DrawingKind[]
33
+ export declare const MULTI_KINDS: DrawingKind[]
34
+ export declare const PATH_MAX: number
@@ -23,7 +23,21 @@ export const DRAWING_POINTS = {
23
23
  // Advanced: Elliott waves, harmonic / chart patterns, Gann box & square, pitchfork variants, icons.
24
24
  elliott: 6, elliottabc: 4, xabcd: 5, abcd: 4, headshoulders: 7,
25
25
  gannbox: 2, gannsquare: 2, schiff: 3, modschiff: 3, icon: 1,
26
+ // TradingView's remaining common tools (0.5.0).
27
+ flatchannel: 3, disjoint: 3,
28
+ fibfan: 2, fibcircles: 2, fibarcs: 2, fibwedge: 3, fibtrendtime: 3,
29
+ cypher: 5, threedrives: 6, tripattern: 4, elliotttri: 6, elliottwxy: 4, elliottwxyxz: 6, cycles: 2, sine: 2,
30
+ ellipse: 2, rotrect: 3, arc: 3, curve: 3,
31
+ note: 1, pricenote: 2, signpost: 1,
32
+ avwap: 1, avprofile: 1, forecast: 2,
33
+ // Freehand highlighter (0 = variable, like brush); path / polyline take clicks until a double-click or Enter.
34
+ highlighter: 0, path: 0, polyline: 0, pin: 1, fibspiral: 2, barspattern: 3,
26
35
  }
36
+ /** Freehand kinds drawn by dragging. */
37
+ export const FREEHAND_KINDS = ['brush', 'highlighter']
38
+ /** Kinds drawn click by click until a double-click or Enter. */
39
+ export const MULTI_KINDS = ['path', 'polyline']
40
+ export const PATH_MAX = 50
27
41
  /** Icon stickers (the drawing's label holds the icon name). */
28
42
  export const ICON_NAMES = ['flag', 'star', 'up', 'down', 'warning', 'check', 'cross', 'fire', 'target', 'bell', 'pin', 'clock', 'dollar', 'shield', 'bolt', 'heart', 'question', 'info']
29
43
  /** Sticker library (TradingView's emoji stickers): category → emoji. An icon label is a name or `emoji:<char>`. */
@@ -55,9 +69,9 @@ export function isSingleEmoji(value) {
55
69
  /** Chart intervals a drawing can be limited to (same set as the desk). */
56
70
  export const INTERVALS = ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '8h', '12h', '1d', '3d', '1w', '1M']
57
71
  /** Text drawings whose font size can be set. */
58
- export const FONT_KINDS = ['text', 'callout']
72
+ export const FONT_KINDS = ['text', 'callout', 'note', 'pricenote', 'signpost']
59
73
  /** 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']
74
+ export const FILL_KINDS = ['rect', 'circle', 'triangle', 'channel', 'fib', 'regression', 'gannbox', 'pricerange', 'daterange', 'measure', 'flatchannel', 'disjoint', 'ellipse', 'rotrect']
61
75
  /** Fibonacci drawings whose levels can be customised. */
62
76
  export const FIB_LEVEL_KINDS = ['fib', 'fibext', 'fibchannel']
63
77
  export const PROFILE_ROWS_MIN = 6
@@ -115,7 +129,7 @@ export function linePriceAt(mark, time) {
115
129
  export const BRUSH_MIN = 2
116
130
  export const BRUSH_MAX = 500
117
131
  /** Kinds whose label is free text shown on the chart (up to 200 characters). */
118
- export const TEXT_KINDS = ['text', 'callout']
132
+ export const TEXT_KINDS = ['text', 'callout', 'note', 'pricenote', 'signpost', 'pin']
119
133
  /** Clicks that create a drawing when they differ from its stored anchor count. */
120
134
  export const DRAWING_CLICKS = { ...Object.fromEntries(Object.entries(DRAWING_POINTS)), long: 2, short: 2 }
121
135
  export const DRAWING_KINDS = Object.keys(DRAWING_POINTS)
@@ -156,11 +170,15 @@ export function drawingGeometry(kind, request, now = Date.now()) {
156
170
  }
157
171
  const need = DRAWING_POINTS[kind]
158
172
  const raw = request?.points
159
- if (kind === 'brush') {
160
- if (!Array.isArray(raw) || raw.length < BRUSH_MIN || raw.length > BRUSH_MAX) throw new Error(`brush 需要 ${BRUSH_MIN}-${BRUSH_MAX} 个 points`)
173
+ if (FREEHAND_KINDS.includes(kind)) {
174
+ if (!Array.isArray(raw) || raw.length < BRUSH_MIN || raw.length > BRUSH_MAX) throw new Error(`${kind} 需要 ${BRUSH_MIN}-${BRUSH_MAX} 个 points`)
175
+ } else if (MULTI_KINDS.includes(kind)) {
176
+ if (!Array.isArray(raw) || raw.length < 2 || raw.length > PATH_MAX) throw new Error(`${kind} 需要 2-${PATH_MAX} 个 points`)
161
177
  } else if (!Array.isArray(raw) || raw.length !== need) throw new Error(`${kind} 需要 ${need} 个 points({ time, price })`)
162
178
  const points = raw.map((item, index) => point(item?.time, item?.price, `第 ${index + 1} 个点`))
163
- if (['trend', 'ray', 'extended', 'channel', 'fibchannel', 'gannfan', 'regression', 'fibtime', 'infoline', 'trendangle'].includes(kind) && points[0].time === points[1].time) throw new Error('前两个点的时间不能相同')
179
+ 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
+ if ((kind === 'cycles' || kind === 'sine' || kind === 'fibtrendtime') && points[1].time < points[0].time) throw new Error('第二个点必须晚于第一个点')
181
+ if (kind === 'barspattern' && points[1].time <= points[0].time) throw new Error('K 线形态的源区间终点必须晚于起点')
164
182
  if (kind === 'vprofile') {
165
183
  if (points[0].time === points[1].time) throw new Error('成交量分布需要一个时间区间')
166
184
  // Only the time range matters, so either drawing direction is fine.
package/lib/tools.js CHANGED
@@ -10,9 +10,11 @@ import { closedCandles, computeIndicators } from './indicators.js'
10
10
  import { MA_KINDS, MA_SOURCES, maLabel, maLatest, normalizeMaLine, normalizeMaLines } from './moving-average.js'
11
11
  import { INDICATOR_IDS, computeStudies, normalizeIndicatorSettings, studyDefinitions } from './studies.js'
12
12
  import { reviewLedger } from './review.js'
13
- import { DRAWING_KINDS, drawingStyle, volumeProfile } from './chart-drawings.js'
13
+ import { DRAWING_KINDS, DRAWING_POINTS, TEXT_KINDS, drawingStyle, volumeProfile } from './chart-drawings.js'
14
14
 
15
- const DRAWING_NAMES = { 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: '图标' }
15
+ const DRAWING_NAMES = { highlighter: '荧光笔', path: '路径', polyline: '折线', pin: '图钉', fibspiral: '斐波那契螺旋', barspattern: 'K 线形态', flatchannel: '平顶/平底通道', disjoint: '分离通道', fibfan: '斐波那契速度阻力扇', fibcircles: '斐波那契圆', fibarcs: '斐波那契弧', fibwedge: '斐波那契楔形', fibtrendtime: '基于趋势的斐波那契时间',
16
+ cypher: 'Cypher 形态', threedrives: '三驱形态', tripattern: '三角形态', elliotttri: '艾略特三角浪', elliottwxy: '艾略特双重组合', elliottwxyxz: '艾略特三重组合', cycles: '周期线', sine: '正弦线',
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: '图标' }
16
18
  import { canonicalMarketCoin } from './market-symbol.js'
17
19
  import { addEntry, journalDigest, removeStrategy, updateProfile, upsertStrategy } from './journal.js'
18
20
  import { analyzeTrading, checkupText } from './analysis.js'
@@ -244,7 +246,7 @@ function buildTools(remote) {
244
246
  coin: { type: 'string', description: '默认会话绑定币对' },
245
247
  interval: { type: 'string', description: '默认当前交易台周期' },
246
248
  kind: { type: 'string', enum: [...DRAWING_KINDS, 'ma'], description: 'add 必填:画线类型或 ma 均线' },
247
- points: { type: 'array', description: 'hray/vline/ray/extended/channel/fib/rect/measure 的锚点,按点击顺序', 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' ? '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'] } },
248
250
  maType: { type: 'string', enum: MA_KINDS, description: 'kind=ma:均线类型;smma 即平滑均线(RMA)' },
249
251
  period: { type: 'number', description: 'kind=ma:周期,1-500 的整数' },
250
252
  source: { type: 'string', enum: MA_SOURCES, description: 'kind=ma:价格源,默认 close' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-hyperliquid",
3
- "version": "0.4.4",
3
+ "version": "0.5.1",
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
- - 41 种画线(趋势线、水平线/射线、通道、斐波那契回撤/扩展/通道(可自定义层级 `levels`)、江恩、叉子线、回归趋势、**固定范围成交量分布 vprofile**(返回 POC / VAH / VAL)、艾略特浪与谐波形态、多空头寸、文字和图标等),用 `hyper_chart`。已有画线用 `update` 修改,不必删了重画。
61
+ - 72 种画线(趋势线、水平线/射线、通道、斐波那契回撤/扩展/通道(可自定义层级 `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` 让画线同步到该币种的所有会话。