← Back to home@tianyagk

dsh-tradewatcher

DeepSeek Harness (DSH) web plugin: 盯盘 market-dashboard sidebar tab — three quote strips with hover intraday charts, watchlist with industry alpha, ledger-driven portfolio P&L, A-share boards & detail drawer, host-side Eastmoney relay + read-only agent tools.

Stars
4
Language
TypeScript
Created
Sep 10, 2026
Updated
Oct 21, 2026
GitHub repo

Introduction

dsh-tradewatcher(盯盘)

一个 DeepSeek Harness Web 插件:通过宿主客户端 Slots 服务注册一个全局「盯盘」面板(左侧栏图标 sidebar.panellist + 主面板 main,两者 id/key 同为 tradewatcher),提供 A股/国际/商品行情条 + 悬浮分时图、行业热力图、分组自选、流水驱动的分组持仓账本(市值/浮动盈亏/当日盈亏/已实现),并把持仓记录以只读模型工具 + 明文 JSON 双通道开放给 dsh 会话分析。

界面与数据组织参考浏览器插件「爱盯盘」(52etf.site);行情为免费公开接口的延迟行情(非 L2),仅作盯盘参考。

  • 安装与使用 → 本文档
  • 历次变更、口径修订与实测依据 → CHANGELOG.md
  • 待开发清单(含「动手前必须探活的源」与「明确不做」) → docs/ROADMAP.md

功能

面板由七个页签组成:自选 / 持仓 / 大盘(含护盘信号与底部位置分析)/ 云图 / 日历 / 工具箱 / 复盘。下面按模块说明。

行情条与详情图

  • 顶部行情条(四行,默认每 10s 自动刷新 + 手动刷新,页签隐藏时自动暂停)
    1. A股指数:上证指数、深证成指、创业板指、科创50、沪深300、中证500
    2. 国际市场:韩国KOSPI200、法国CAC40、英国富时100、欧洲斯托克50、标普500、纳斯达克、德国DAX30、日经225、恒生指数
    3. 国债收益率:美债10Y、日债10Y、德债10Y、英债10Y
    4. 大宗商品:生猪主连、焦煤主连、豆粕主连、螺纹钢主连、布伦特原油当月、COMEX铜、COMEX白银、伦敦金现
    • 每卡片:名称 / 现价 / 当日涨跌值 / 涨跌幅(红涨绿跌;浅色/深色/跟随系统主题可切);右上角五态色点(实时/延迟/定稿/未取到/无行情源,见「数据出处与状态」)
    • hover 悬浮卡:当天分时图(源缺失自动回退近 5 日收盘走势),附昨收基准虚线、最后行情时间与成交额
    • 点击卡片打开右侧详情抽屉(键盘 Enter/Space 亦可)
  • 自选/持仓每行带分时缩略线,hover 叠加均价线与分时首笔 / 分时高 / 分时低 / 分时振幅(口径:取自分时抽样点,不是交易所口径的今开/最高/最低 —— 官方开高低在详情抽屉;图上不加常驻文字:密集布局里加字就是噪音);均价线用与价格同一套 y 映射,缺口不连线
  • 详情抽屉:固定信息头(名称/代码/现价/涨跌幅/所属行业板块及板块涨跌幅/今开/昨收/换手率/最高/最低/成交量/成交额/市盈率/总市值)+ 三窗格专业图:主图 + 成交量(红绿柱、最大量标注)+ MACD(HIST 柱 + DIF/DEA 线,12/26/9,HIST=(DIF−DEA)×2),主图与子图均带纵轴刻度线(1/2/5×10ⁿ 步长);K 线叠加 MA5/10/30/60(橙/蓝/紫/灰,附当前值图例,均线在全量序列上计算后再截取可视区间)
  • 周期:分时 / 五日 / 日K / 周K / 月K / 年K(记住上次选中)
    • 分时主图叠加均价线;五日图为 5 个交易日连续拼接(午休/隔夜已折叠,交易日之间画分隔线)
    • 东财 trends2 的 ndays 参数会被上游忽略(只回当日),因此五日视图改用 5 分钟 K 线拼接:新浪为主源(A股/ETF/指数,一次可取 6–8 个交易日)、腾讯 5 分钟 K 线兜底
    • 本地按日归档 + 拼接(v0.31.0 起):每次成功取到某一日的分时(任何来源,含 LKG 兜底命中那一次)都会按 secid + 交易日 归档;港/美股、国际指数、外盘商品、期货这些市场,五日档改用本地归档拼接——覆盖多少天、缺哪天,写在口径条上(我们的两条多日渠道(新浪 5 分钟线、腾讯 5 分钟线)只覆盖沪/深,其余市场靠本地归档逐日累积;这是「我们没覆盖」,不等于「上游没有」。2026-10-11 实测:东财 trends2 忽略 ndays(ndays=1 与 5 回包逐点相同),腾讯 5 分钟线对港股未产出 ≥2 个交易日)(本地拼接 3/5 天 · 缺 2026-10-07),缺的日子只列不补
    • ⚠ 不得期待历史归档:归档从本版起累积,装好当天最多只有 1 天(界面上会写「仅当日,从本版起累积」),之后每过一天多一天,最多拼到 5 天。过去的日子没有归档过,追不回来——这不是 bug,也不需要"补数"
  • 缩放:日期宽度双滑块 + 近1月/3月/6月/1年/全部 快捷键,图表内滚轮以光标为中心缩放(最少 8 根);键盘:←/→ 平移(Shift 加速 10 根)、+/- 缩放、Home/End 到两端,滑块手柄是标准 slider 语义
  • 买卖点:图上按持仓流水标注 B/S(悬停显示「数量 @ 价格 · 分组/持仓」);周/月/年 K 把同周期多笔归并为 B2/S3;同一根 bar 的多笔合并为一个标记,悬停给当日小结(笔数 · 合计股数 · 均价);图下有区间图例(区间内 B/S 笔数、净买卖股数、买卖均价),缩放把交易移出可视范围时补一句 (区间外另有 N 笔)
  • 读屏摘要:.tw-sr 文本与图同源同一次计算(区间、首末收盘、区间涨跌幅、最高最低、买卖点笔数与净买卖),不会出现"摘要说涨、图上在跌"
  • 顶部口径条:日K · klt 101 · 前复权 · 截至 10-09 14:31(当根未收盘,MA/MACD 已含未收盘当根) —— 复权用宿主回包的实际生效口径;「截至」用本地最近一次成功取数时刻,不拿"现在"顶替

自选

  • 分组的新建/改名/备注/删除(删除=归档,可恢复)+ 组内添加(名称/代码搜索补全)/移除/移动分组;搜索池覆盖 A股/港股/美股/ETF/基金/指数/期货主连(如 688825、02155)
  • A股行展示所属行业板块名、板块当日涨幅、个股相对板块 alpha(个股涨跌幅 − 板块涨跌幅)(行业归属 10 分钟缓存,板块涨幅随行情轮询同步刷新);板块涨跌缺失时显示 — 并给出原因,禁用 0 代替(0 会被读成"没涨没跌",那是错的信息)
  • 排序:默认顺序 / 当日涨跌 / 总市值(组内生效,记忆在 prefs.json,同键再点切换升降序);排序沿用面板头部的段控(v0.36.0 起不再有列头排序 —— 列表上方那排列头与段控功能重复,已按用户要求删除),任何宽度下都只有这一个入口
  • 年初至今(YTD)列:口径见「口径规范 → 年初至今」;显示 — 时可在 tooltip 看到具体原因
  • 异动提示:放量/价格异动/量价共振的行带左侧竖条 + 缓慢呼吸背景(不是整行变红——整行红会与"跌了"的语义冲突;prefers-reduced-motion 下退化为静态底色),面板头部「异动 N」徽标可展开队列;同一条目 30 分钟静默(记在 localStorage)

持仓

  • 分组 CRUD(同上归档语义)+ 组内证券的「买入/卖出/调整/编辑/移动/明细/移除」
  • 每分组与总览显示总市值、浮动盈亏、当日盈亏、累计已实现;每行含浮动盈亏率与当日盈亏率、可用(可卖)数量、累计费用(含占累计成交额百分比)、标的年初至今(YTD)
  • 排序:默认顺序 / 市值 / 持仓盈亏(随所选成本口径)/ 当日盈亏 / 仓位占比
  • 总额口径:默认只含 A股,港/美股逐项列在「不含 N 项 ▼」里并可下钻看原因;也可切到固定汇率折算(见「折算口径」)
  • 除权提示:复用已同步的日历事件,顶部列出涉及持仓的公司行为与日期,持仓卡片标「N 天后除权」;不自动改账(真实的送转/派息以券商为准,正确动作是在「调整」里按实际到账录入)
  • 成本口径可切换(右上角分段开关,偏好存 prefs.json),切档弹 toast 报差值而不是只改数字

A股大盘

  • 12 栏网格的紧凑布局(v0.33.0 重排:单行 25px 指标条 + 45px 指数卡矩阵,取代原先每个数值一张两行卡):B1 市场宽度(家数/两市成交额/上涨占比 + 历史分位分布条)+ B2 六大指数卡矩阵 + B3 护盘信号(收起态,无数据时塌成一行)+ B4 板块/资金/ETF + B5 搜索与详情
  • 涨跌家数:优先用上游直接给的字段;拿不到时由本插件自行按沪深A股全量统计(口径见「口径规范 → 涨跌家数」)。取不到就显示 — 并把每个尝试过的源与各自的原因写在 tooltip 里,不用 0 顶替
  • 涨跌家数历史分位(样本 ≥5 个交易日后显示;样本不足给原因不给 0)
  • 行业/概念板块涨跌排行与主力资金排行(口径行常显:数据时刻 / 来源;15:05 后标「定稿」;金额与占比两种排序可切,占比只在本页 40 条内重排并写明)
  • ETF 排行;个股/基金搜索 + 详情卡(分时大图、换手/量比/市盈率/市值)
  • 缺失时收缩而不是留空:家数缺失时三格合并成一格且原因只出现一次;其余块缺失只影响一格/一行(块高变化 ≤±8px)

状态可见性(侧栏徽标与降级态)

  • 侧栏徽标:左侧栏「盯盘」图标上带一个小徽标,不打开面板也知道发生了什么 —— 内容只有持仓当日盈亏率(带符号短标记,如 -2.3%;0.0% 是真的零;算不出时徽标为空、悬停里给原因,不用 0 冒充);颜色跟随 prefs.redUp(红涨绿跌 ↔ 绿涨红跌);隐身视图下只显示沪深300 点位,不出现任何金额。侧栏标签恒为「盯盘」(不带 · 数据 HH:mm:ss、也不带 · 已收盘 —— 用户明确不要后缀);数据时刻与「已收盘」状态在侧栏条目的悬停 title(当日盈亏 -1.6% · 数据 15:05:56 · 已收盘)与面板内,信息没有丢。图标只有 18px、字形最多 4 个字符。徽标数据来自 GET /tradewatcher/badge,只读同一份 rescue 快照与持仓汇总,不触发新的采样 —— 徽标是常驻的,若它自己打一遍上游,等于把"看一眼侧栏"变成取数压力。 ⚠ 护盘等级不进这个徽标:v0.25.0 曾有一条"护盘 ≥「疑似护盘」就显示「护」"的优先分支,它顶掉当日涨跌幅;2026-10-16 按用户要求删除(同一个 18px 的位置放两种语义,用户既分不清这个字是"我的"还是"市场的",也分不清它为什么变)。护盘等级有自己的位置(护盘面板 + tradewatcher_rescue 工具);要放回侧栏必须另立入口。回归防线在 src/client/badgeView.test.ts(徽标改成「护」或任何非涨跌幅文本 ⇒ 断言必败)
  • 卡片五态色点与列表头汇总:见「数据出处与状态」
  • 降级态一律显式:护盘暂停时分数位显示 — 而非上次的分数;数据来自备用源/上次成功结果时行上标 source 与「上次数据」;上游熔断时状态条简述 + tooltip 明细(逐主机剩余时间、最后错误),并说明哪种因子"等几分钟能补齐"、哪种"该源根本没有,不是等的问题"
  • 本会话未采样 ≠ 采样缺口:收盘后重启用持久化的上次快照并红标「上次数据」,gap 只反映当日真实失败

备份与迁移

持仓页「备份」按钮:导出把 watch/positions/ledger/prefs 打包成一份带 schemaVersion 与校验和的 JSON;导入是两段式 —— 选文件 → 预览(校验 + 覆盖前后差异)→ 确认覆盖。覆盖前宿主把原文件写成 .bak,导入后立即重读内存(不需要重启)。

校验不通过一律拒绝并逐条给原因:非对象、缺 schemaVersion、版本高于插件支持、缺文件、流水条目无法解析、引用不存在的持仓/分组 id、重放出现负持仓、校验和不匹配。最后一条是硬约束:宁可不导入,也不导入一份算不平的账 —— 一旦写进去,之后所有成本、盈亏、已实现都建立在一个错的账本上,而用户没有任何手段看出"从哪一天开始不对"。

大盘云图

只保留第三方站点(自绘云图已于 v0.62.0 删除,见 CHANGELOG.md 的对应条目):

  • 第三方站点:内嵌 iframe 到第三方热力图站点(地址见 src/client/CloudMap.tsx 的 CLOUD_MAP_URL;可重新加载/新窗口打开)。信息量更全(钻取、K 线、复盘),但面积与颜色的口径由站点决定,本插件无法保证

本插件不抓取该站内容,iframe 只做展示与降级说明。

iframe 有三态降级:加载中 / 已加载 / 超时(8 秒)。超时显示占位(域名 + 最后成功加载时间 + 一句"本插件无法区分是本机网络还是该站点的问题")并给一次「重试」(重建 iframe 并重新计时);最后成功时间记在 localStorage,跨会话可见;状态徽标常驻显示当前处于哪一态。

护盘信号(A股大盘页内)

识别「符合国家队历史行为模式的宽基 ETF 护盘行为」。常驻状态条(等级/评分/归因)+ 六通道标的卡 + 因子明细表(含贡献度列)+ 分时量能图 + 今日信号时间线与近 60 日历史回看。信号达「疑似护盘」时面板自动展开。

  • 监测通道:默认核心 6 只宽基(沪深300ETF华泰柏瑞 510300 / 上证50ETF华夏 510050 / 中证500ETF南方 510500 / 中证1000ETF华夏 512100 / 科创50ETF华夏 588000 / 创业板ETF易方达 159915),可在设置里追加 510310 / 510330 / 159919 / 588080;共振按指数去重
  • 六因子加权:量能 0.22 / 超大单 0.26 / 脉冲 0.18 / 持续性 0.12 / 量价背离 0.14 / 共振 0.08,乘时点系数(早盘 0.4 → 尾盘 1.1)得 0–100 评分;等级 ≥75 强护盘信号 / 55–74 疑似护盘 / 35–54 资金异动 / <35 平静
  • 因子语义:F2 以核心通道(沪深300/上证50)为主,仅外围通道净流入时打 0.7 折,核心通道超大单净流出达成交额 −15% 时封顶为「资金异动」;F3 区分盘中脉冲(14:30 前,打 0.6 折)与尾盘突袭(满分权重),命中线为该时段 P90;F4 用「时间 + 幅度」(最近 5 分钟超大单净增 ÷ 该窗口成交额,锚点 5%/12%/25%);F6 共振列出标的名并分层,核心 0 只时不得给「强护盘信号」
  • 阈值来源全部标注:量能倍数用历史分位数标定(腾讯日K 500 根 × 6 只 = 2886 样本,P75/P90/P95 = 1.19/1.57/1.92x,对应 40/70/100 分);日内进度曲线用新浪 5 分钟线 126 个交易日标定(10:00 已占全天 29.6%、11:30 60.9%、14:30 86.0%),因此「同时点量能倍数」天然按时间进度归一;超大单强度因免费源已无日频资金流历史暂用经验锚点,host 采样器自建样本满 20 个交易日后自动改用自建分位数(面板显示当前来源与样本天数)
  • 防误报约束(每条都写进归因,不做静默降级):指数明显上涨时封顶(+1.0% 以上最多「资金异动」;+0.3% 以上不得为「强护盘」,因为放量上涨更可能是追涨);「强护盘」要求超大单强度达高门槛;无有效超大单且无脉冲时最多「资金异动」
  • 采样:host 侧常驻采样器,仅工作日 09:25–11:35 / 12:55–15:05 活跃,常态 30s、尾盘(默认 14:30 起)自动切 15s(可在面板设置里改),每次采样 1 个批量请求(约 2 请求/分钟),页面关闭也持续记录
  • 非活跃时段:分数位显示 —(不把上次的分数留在那里假装是当前分),状态行 采样暂停 · 下次 10-09 09:25(已收盘)(跨周末指向下周一);暂停时段一律不出现「采样缺口」字样——那时的缺口不是正在发生的事
  • 数据:rescue-log.json(60 天滚动)保存每日信号事件、5 分钟抽样、20 日均额基准、自建样本与出分日志(各因子原始值 + 当次生效阈值,粒度 5 分钟刻度 + 等级变化);⚠ 免费源无 ETF 资金流历史,历史回看只能从本模块上线日起算

交易日历维护(每月一次,5 分钟)

护盘模块按交易日历判断"今天是否交易":休市日不采样、不出等级、不写当日分(否则假日的"量能平稳"会被 算成一份平静的当日分 —— 那就是假信号)。判定优先级:外部日历文件 > 内置假日表 > 周末规则。

  • 文件位置(用户自己的状态目录,升级不会覆盖):~/.dsh/dsh-tradewatcher/trading-calendar.json
  • 格式:按月列出休市日(只列公告里的法定休市,周末不用写 —— 周末由星期判定)。 没有法定休市的月份写 "2026-11": [] 即可 —— 这条空数组同时表示"本月已覆盖"(缺键 = 未覆盖 ⇒ 会告警并退回内置表)。 为什么列休市日而不是列交易日:一个月里法定休市只有 0–5 天,交易日有 18–23 天,前者写得更少、更不容易抄错。
  • 样例(2026 年 10 月,来源:上交所《关于上海证券交易所2026年部分节假日休市安排的通知》上证公告〔2025〕45号):
{
  "v": 1,
  "source": "上交所 2026 年部分节假日休市安排",
  "updatedAt": "2026-10-08",
  "months": {
    "2026-10": ["2026-10-01", "2026-10-02", "2026-10-05", "2026-10-06", "2026-10-07"],
    "2026-11": []
  }
}
  • 数据从哪来:交易所每年 12 月发布次年安排(上交所「休市安排」页 https://www.sse.com.cn/disclosure/dealinstruc/closed/), 每月初扫一眼该页有没有新的节假日公告即可(年中通常只有"中秋/国庆"这类补充公告)。
  • 改完自查:npm run calendar:check(只读)—— 打印文件路径、是否覆盖当月、本月休市日、被忽略的周末条目、 损坏文件的隔离去向,以及未来 10 天的交易日判定。也可在护盘面板上看:未覆盖当月时面板与 agent 工具输出 都会显示 ⚠ 交易日历未更新到本月(YYYY-MM)…。
  • 文件写坏了怎么办:schema 不合法(版本不对、日期格式错、日期不在所属月份、重复)或 JSON 解析失败时, 文件会被改名隔离成 trading-calendar.json.invalid-<时间戳>(旁边留一份 .reason.txt 说明原因), 并退回内置假日表;面板与工具输出都会显示这次隔离。不会静默按空表跑。
  • 失败方向:日历过期的后果只是多采一天(把假期当交易日),不会把真交易日误判成休市 —— 后者会漏掉 真实信号,是更糟的失败。所以判定顺序把"确定性的日历"放在"上游行情推断"之前。

底部位置 / 形态 / 概率(与护盘并列,不进入护盘评分)

回答"当前位置是否具备底部特征",避免把两件事混成一个数:

  • 位置:距 60/250 日最高回撤、近 60 日收盘分位、距 60 日最低、连续下跌天数、是否创 60 日新低 → 位置分(0–100,低位高分)
  • 日内形态:现价相对当日最低的回升幅度、下影线占振幅比例、当日最低点出现的时刻、是否"跌破前 20 日低后收回"、是否"创 60 日新低后收回" → 形态分
  • 底部概率(历史频率):把当前状态与历史同类日类比(近 60 日收盘分位 ≤ 25% 且 当日量能 ≥ 前 20 日均量 × 1),统计其后 5 日内最高价达到当日收盘 ×(1+目标) 的频率,同时给出无条件基线与样本量,并附 Wilson 95% 区间;样本 N < 30 标「样本少」
  • 诚实边界:① 概率是历史同类情形的频率,不是预测,必须与 N 和基线一起读;② 日内形态不进入概率(没有可回算的历史分钟数据,混进去就是伪精确),它只作实时修正参考;③ 位置低 ≠ 见底,护盘也可能发生在下跌半程

日历

财经日历模块。月历网格按重要性三档语义色标记(高 / 中 / 低):颜色是独立语义色阶(--tw-imp-1/2/3),不复用涨跌色、不随红涨绿跌翻转,隐身视图下统一灰化。格子里只显示事件名,点日期弹出当天事件详情卡(公布时刻、类别、重要度、来源、关联标的、备注),支持手动事件的增/删/改与自动事件的隐藏。

  • 自动同步(东财数据中心,6 小时缓存):新股申购/上市日历(全市场);自选 + 持仓标的的财报预约披露日期、分红除权除息/股权登记日;国际/国内宏观(东财 H5 财经日历,覆盖窗口过去 7 天 ~ 未来 92 天,分页 50 条/页、4 并发,约 1–2 秒),含美联储议息会议、联邦基金利率目标、各国央行利率决议、非农、失业率、PCE、PPI、GDP、ISM、零售、初请失业金,以及中国 CPI/PPI/社融/进出口等;每条带北京时间公布时刻(00:00 视为未定时刻不显示)
  • 归类与重要度由名称启发式推导(数据源本身不含重要度字段):国家前缀 → 中国/中国香港/中国台湾/中国澳门 归国内宏观,其余核心经济体归国际宏观,无前缀的会议/行业事件归其他;非核心经济体(泰国、土耳其)不入库。重要度:议息/利率决议/联邦基金利率/LPR/非农/CPI/PCE/GDP/两会/关税 → 高;PPI/PMI/零售/社融/失业金/库存等 → 中;其余 → 低(港口/保税区高频库存分项降为低)
  • 改期留痕与勾稽:上游会悄悄改期,因此按 autoKey 对比上一条,日期/结束日/标题变化即记 from → to(最多 5 条),事件卡标 可能变更 ×N,悬停给出历次变更的时刻与前后值(手动事件不受影响);格点与事件卡上 实心 ●=有持仓 / 空心 ○=仅自选 / 无标记=无关(用字形而非颜色——颜色已被重要度占用),当天事件列表默认按勾稽排序(持仓 → 自选 → 其他,组内按重要度与时刻)
  • 手动维护:源里没有的事件(美国 CPI(BLS)不在东财免费源内)、未上市公司 IPO 与上市日期、自定义提醒——可在页面新增,也可以直接让会话里的 agent 用 tradewatcher_calendar_add 写入

视图与隐私

  • 三档视图(Alt+M 轮换,持久化):完整(默认)/ 紧凑(隐藏说明文字与脚注)/ 隐身(金额模糊为 ¥••••、盈亏保留正负号、涨跌色转灰阶)
  • 金额遮罩做在格式化出口内部,因此"新增一处显示金额忘了判断"不会漏出真实数字;只影响显示,取数、告警、agent 工具返回完全不受影响。价格与点位不属于"金额",照常显示
  • 动效纪律(v0.87.0,参考竞品做法后自定):常驻/循环动画只动 transform/opacity;关闭态 display:none(不留透明层吃点击);面板不可见或页面切后台 ⇒ 根节点 data-idle=true 暂停全部动画;尊重 prefers-reduced-motion。细则与阈值见 docs/DESIGN-UIUX-ADOPT.md
  • 截图 / 录屏保护(行情条右侧「▣」):面板不透明度 35%–100%(只作用于本插件面板);数字模糊(把价格/涨跌/盈亏/成交额糊住,鼠标移到某一行或某张卡上时该行临时显形)。模糊目标是"所有使用 --tw-mono 的数字面"清单,界面上写明边界:只覆盖本插件渲染的数字,整屏不可读请配合隐身视图

宽屏布局

面板在宽屏下用宽度装更多内容而不是把内容拉长:行情条是自动填充网格(宽屏多列、窄屏换行,没有内容被藏进横向滚动);自选与持仓列表在 ≥1080px 时铺成多列(按列竖向填充,名次自上而下读是 1/2/3,同高度约两倍条目;窗口变窄自动退回单列);提示文案限宽 96ch。

安装

前置:插件按 dsh 0.2.x 的客户端 Slots 契约注册面板(ctx.slots.inject('sidebar.panellist' | 'main', …)),不依赖任何第三方侧边栏插件;宿主侧只需 webServer + webRuntime 两个服务(由 dsh web 提供)。旧版 ctx.betterSidebar 已在 0.2 移除。

# 1) 构建(lib/index.js + lib/client.js)
cd dsh-tradewatcher && npm install && npm run build   # 或 npm run check(含类型检查/测试/自检)

# 2) 注册进 web profile(默认 ~/.dsh/profiles/web,可用 DSH_PROFILE_DIR 覆盖)
bash scripts/install.sh

# 3) 重启 dsh web 进程(必须:宿主半区改动只有重启后才生效)
#    重启后点左侧栏「盯盘」图标(走势线)打开主面板

卸载:bash scripts/uninstall.sh,然后同样重启 web。

插件为「双面」结构:主机半区(quote 中继、文件存储、/tradewatcher/* 路由、agent 工具)+ 浏览器半区(Tab UI),通过 dsh.bundle.patch(cordis.patch.yml 的 insert 行)随 bundle 自动挂载,无需手改 profile 的 cordis 配置。

快速上手

  1. 左侧栏点「盯盘」图标打开主面板;顶部三行行情条是 A股指数 / 国际市场 / 大宗商品,点任意卡片打开右侧详情抽屉(分时/五日/日K/周K/月K/年K)。
  2. 「自选」里新建分组 → 添加证券(可搜代码或名称)→ 行上可移除/移动分组;排序开关在面板右上。
  3. 「持仓」里新建分组 → +持仓 添加标的 → 行上「买/卖」录入流水(数量/价格/费用)→ 分组与总览自动核算。数量与成本不落盘,全部由流水推导,所以请始终用买/卖/调整录入,而不是直接改数字。
  4. 想离线复盘或让会话分析:数据都在 ~/.dsh/dsh-tradewatcher/*.json(明文),也可以直接用 tradewatcher_portfolio 等模型工具问。

数据与接口

数据文件(DSH_HOME 默认 ~/.dsh)

文件内容
~/.dsh/dsh-tradewatcher/watch.json自选:{ groups: [{id,name,order,archived?,note?}], items: [{id,groupId,secid,name,note?,createdAt}] }
~/.dsh/dsh-tradewatcher/positions.json持仓:{ groups:[…], items:[{id,groupId,secid,name,note?,createdAt}] } —— 数量/成本不落盘,全部由流水推导
~/.dsh/dsh-tradewatcher/ledger.json逐笔流水/审计(append-only):{v, entries:[{id,ts,actor,verb,groupId?,posId?,secid?,qty?,price?,fee?,note?,meta?}]}
~/.dsh/dsh-tradewatcher/prefs.json主题/刷新间隔/配色/排序/视图/折算口径/截图保护等偏好
~/.dsh/dsh-tradewatcher/quotes-lkg.json行情 last-known-good 快照(每只证券最近一次有效读数,进程重启后仍可兜底,非账本数据)
~/.dsh/dsh-tradewatcher/trends-lkg.json分时序列 last-known-good(上游失败时返回上一份好图并标 staleAt)
~/.dsh/dsh-tradewatcher/trends/<secid>/<YYYY-MM-DD>.json按日归档的分时(本地拼接五日的原料)。保留最近 12 个交易日;单日 >512KB 或单标的总量 >8MB 时不写并在界面提示「本次未归档」;清理只删本目录里的日期文件。可在设置里用 trendArchive 关闭
~/.dsh/dsh-tradewatcher/board-lkg.json板块/资金/ETF 排行的 last-known-good(全部上游不可用时显示上次成功结果并标注时间)
~/.dsh/dsh-tradewatcher/klines/<secid>_<周期>_<口径>.jsonK 线本地缓存:首次查看一次性拉全量(日 800 / 周 400 / 月 240 根),之后每次只增量拉最新几根并合并;上游不可用时直接回退缓存。复权口径进文件名(_0/_1/_2),不同口径互不顶替
~/.dsh/dsh-tradewatcher/breadth.json涨跌家数每日快照:收盘后(15:05 起,15:30 后定稿)记录当日涨/跌/平家数与两市成交额,保留 60 日窗口 + 30 天余量
~/.dsh/dsh-tradewatcher/calendar.json财经日历事件库(自动同步 + 手动维护,含改期留痕)
~/.dsh/dsh-tradewatcher/trading-calendar.json交易日历缓存(npm run calendar:check 与跨市场休市判断用;按年缓存,缺失时按规则推算并标注)
~/.dsh/dsh-tradewatcher/fupan.json复盘笔记(第 8 行 fupan):{ v:1, notes:[{id,date,text,pinnedTo?,source,at}] },at = 最后一次写入时刻。有界:单条正文 ≤4000 字、≤2000 条、序列化后 ≤512 KB;条数/体积超限时按「最久未更新」丢弃并在回包 dropped 段逐条留痕(置顶的最后才丢),单条超长直接 400 不截断。读不到(权限/磁盘)≠ 没有笔记 ⇒ 本次启动拒绝写回并如实披露;坏 JSON 先隔离保留原字节再从容开始
~/.dsh/dsh-tradewatcher/global-flow.json工具箱「全球资金流蛋糕模型」的每日份额快照:一天一条、保留 90 天(一天 ≈ 0.9KB);成分一变,旧快照整份作废并留痕,不与新成分混用
~/.dsh/dsh-tradewatcher/macro-log.json工具箱「全球资金流蛋糕模型」的宏观两档自采样:每个交易日一条(广义美元指数 / 美国 10Y 收益率)+ 每源诊断留痕(diag:本次各源是否可用、值、原因 —— 真机排障的现场证据),保留 120 天。用途:两个源都没有可无歧义解读的"昨收"字段 ⇒ 「较上日」与「5 日变化」只由本插件自采样算,绝不用来源不明的字段充当变化量;不足 ⇒ 界面显示「未取到」+ 原因,不写 0
~/.dsh/dsh-tradewatcher/market-extras.json大宗商品/债券等扩展行情缓存(last-known-good;上游失败时回退上次成功结果并标注时间)
~/.dsh/dsh-tradewatcher/leverage.json工具箱「杠杆温度计」的两融历史:{ v:1, lastFetchDay, lastAttemptAt, cross, rows:[{d,rzye,rqye,ltsz,rzyezb,rzjme}] }(单位:元 / %)。每天最多取一次上游(两融 T+1,当天数据不会中途变);首次 6 页回填 4018 个交易日(2010-03-31 起,≈610 KB),之后每天追加一行;上限 4500 行 / 1 MB(丢最旧的并留痕)。读不到 ≠ 没有(坏 JSON 先隔离保留原字节,不覆写);cross 只内部记录深交所单市与合计的量级校验,不进回包
~/.dsh/dsh-tradewatcher/rescue-log.json护盘信号:每日信号事件、5 分钟抽样、20 日均额基准、自建样本、出分日志(60 天滚动)
~/.dsh/dsh-tradewatcher/macro-archive.json宏观后台归档(npm run macro:archive):BIS 等官方长期序列(单次约 25 秒 ⇒ 只允许后台跑,不进交互路径),带抓取时间与来源;同样走 robots.txt 门禁与「每主机 ≤5 次/小时」预算
~/.dsh/dsh-tradewatcher/write-log.json写操作日志(目前只覆盖插件代写的日历事件新增:tradewatcher_calendar_add 这类;持仓/流水的写入由侧边栏自己维护、不经这里):每条记 by(会话 id)/反向操作/是否已撤销。撤销不删历史(撤销本身也留痕),保留最近 500 条。真机上若从未用工具写过日历,这个文件不会存在(不是漏了)
~/.dsh/dsh-tradewatcher/*.json.bak导入备份前自动留下的原文件副本,用于人工回滚
  • verb:buy买入 / sell卖出 / adjust调整 / add新建持仓 / remove移除持仓 / gcreate新建分组 / grename改名 / gdelete归档 / grestore还原 / gmove移动或编辑 / pnote备注
  • secid 为东财代码,格式 市场.代码:1.600519(沪A)、1.688825(科创板)、0.300750(深A)、1.510300(沪ETF)、116.02155/116.00700(港股)、100.KOSPI200(全球指数)、114.lhm(生猪主连)等

会话内分析(双通道)

  1. 模型工具(只读,重启后对所有会话可见)
    • tradewatcher_portfolio — 分组汇总 + 每只持仓(数量/成本/现价/市值/浮盈/当日/已实现/可用数量/费用)
    • tradewatcher_ledger — 逐笔流水,可按 posId/groupId 过滤(追溯完整交易过程)
    • tradewatcher_watchlist / tradewatcher_quotes / tradewatcher_search — 自选、实时行情、证券搜索(cn/intl/commodity/bond/all 预设组)
    • tradewatcher_calendar / tradewatcher_calendar_add — 财经日历读取与手动事件写入(唯一的写工具,带节流与反向操作登记)
    • tradewatcher_undo — 撤销本插件的写入(按 id 或 last=N;listOnly=true 只列出可撤销项)
    • tradewatcher_rescue — 护盘信号(六因子 + 贡献度 + 出分日志 + 采样窗口)
    • tradewatcher_ytd — 年初至今(前复权口径,含基准说明与缺失明细)
    • 每个读数都带数据出处:asOf / stale / source / missing[](见「数据出处与状态」)
    • 系统提示词已注册引导段落(插件在会话工具列表即显形,如出现工具名即说明挂载成功)
  2. 明文 JSON 文件:上述数据文件路径固定、格式如上,任何会话可用文件工具直接读取做离线分析/回溯校验

HTTP 路由(同源,浏览器信任围栏保护,POST 校验 Origin)

GET /tradewatcher/health|badge|quotes?ids=|trend?secid=|kline?secid=|detail?secid=|suggest?q=|board?scope=&sort=&pn=&pz=|anomaly?ids=|ytd?ids=|breadth、GET|POST /tradewatcher/watch|portfolio|prefs|calendar、GET /tradewatcher/ledger、GET /tradewatcher/trades?secid=、GET /tradewatcher/industry|industries、GET|POST /tradewatcher/backup(导出/导入备份;POST body {mode:'preview'|'apply', bundle})

  • /tradewatcher/kline 的 fqt:0=不复权 1=前复权(默认)2=后复权;回包 kline.fqt 是实际生效口径(指数/期货恒为 0 并置 fqSupported:false),kline.asOf/barOpen 供口径条显示"数据截至几点、当根是否已收盘"
  • /tradewatcher/badge 只读同一份 rescue 快照与持仓汇总,不触发新采样
  • /tradewatcher/anomaly 用 K 线本地缓存 + 行情涨跌幅判定自选异动,不额外打上游

口径规范

以下为当前生效的规则。每条改动的来龙去脉与实测依据见 CHANGELOG.md。

时间口径

全插件的时间标签只有一套口径:Asia/Shanghai(src/host/time.ts)。hhmmOf / dayOf / weekdayOf / calToday / shanghaiDayStart 全部来自同一实现,并在 4 个时区(Asia/Shanghai、UTC、America/New_York、Australia/Sydney)下断言一致;npm test 会把这套断言跑两遍(宿主 TZ 与 TZ=UTC),因为只有显式切 TZ 才锁得住时区回归。

注意 inTradingWindow 里的星期也必须按北京时间——北京时间周一 10:00 在 TZ=America/New_York 下是周日 22:00,用宿主星期会把真实交易日判成休市。收盘后是否"定稿"同样按北京时间 15:05 判定(不用浏览器本地时区,否则容器里的 UTC 浏览器会把收盘后判成盘中)。

排序

选择存进 prefs.json,同键再点一次切换升降序。两条硬约定(src/client/sort.ts,8 组断言锁死):

  1. 无效值恒沉底:无行情、无市值、无盈亏、NaN/Infinity 一律排在最后 —— 升序也不许跑到最前(null 参与数值比较会退化成 0,正是这类功能最常见的错误)。
  2. 稳定且可预期:同值条目保持原顺序,排序不会在每次行情轮询后重新洗牌。

持仓的盈亏键跟随当前成本口径(摊薄口径排「持仓盈亏」,均价口径排「浮动盈亏」)——排序用的数与表格里显示的那一列必须是同一个。

市值来源:东财 f20(全部标的)→ 腾讯 f45(沪/深/港股,原始单位亿元,已 ×1e8 转元);新浪备用源不提供市值(int_/nf_/hf_ 行情里没有该字段),因此国际指数、商品、期货经新浪兜底时没有市值 → 排序时沉底,界面上也不显示这一栏(不编造)。

watchSort / portSort 走主机侧白名单校验(非法键、非布尔方向抛错,界面能提示出来);明文文件被手改成非法值时装载即回退默认(启动不因一个坏偏好失败)。

secid 与大小写

规则:secid 在存储、路由、工具入参、上游请求里一律保留原样;只有"判断是否同一标的"时才做大小写无关比较(secidKey())。

为什么必须如此:113.rbm / 114.lhm 这类商品/期货后缀区分大小写,而三家的映射关系完全不同 —— 东财用 f13.f12 回包(商品是小写)、腾讯映射沪/深/港(数字代码)、新浪 nf_*/hf_* 以小写 secid 为键。任何一处把 secid 改写成大写,都会让"上游请求"与"备用源映射"双双落空。上游映射与回包均大小写不敏感,因此历史写入的大写数据自动可用,不需要迁移脚本。

配套的诚实性:/quotes 返回 missing[](请求了但没有任何源给出价格的标的,界面显示「暂无可用行情源」,与「暂无行情」区分)与 requested/truncated(超过 160 项上限时如实回报,不静默截断);搜索结果标注 hasFallback(仅东财源的标的必然更易空行,提前说清而不是让用户以为是 bug)。

行情取数、缓存与新鲜度

  • 逐标的缓存槽:/quotes(行情条 + 自选 + 持仓)与 /portfolio(仅持仓)标的互相包含,逐标的槽让持仓那几只不再重复请求(实测一拍 12 次上游:东财 8 + 腾讯 1 + 新浪 3;东财熔断后稳态 0 次,持仓页子集请求 0 次)
  • 新鲜度窗口 = refreshSec(夹在 5–60 秒),改动设置立即生效。代价如实说:东财健康时上游请求约 6 次/分(每次一个批量请求覆盖全部标的);想更省就把刷新间隔调大,界面文案与数据新鲜度会一致
  • last-known-good:接口瞬时缺价/漏返时按字段级回填,快照持久化到 quotes-lkg.json 跨重启生效;单批缺码先换备机补拉;客户端保留已拉取旧值、空样本不回写——价格不会在"数字 / —"间闪烁;停牌等真实无价标的仍显示 —
  • 休市定稿零回源(isSettledOffline):休市且本地缓存更新时间晚于最近一个工作日 15:05 时,K 线与分时直接复用本地并标 cached: true(定稿是完整数据,只是不必再问上游;与 stale 语义区分)。此时客户端 TTL 也放长(分时 5min / 五日 10min / K 线 1h),收盘后反复开关面板一个请求都不发。盘中永不成立
  • 客户端缓存(chartCache.ts):同标的同周期在 TTL 内零 HTTP,同键并发请求合并为一个(悬浮卡与抽屉同时要同一份数据时只发一次),失败保留上次成功值并如实标注。TTL:分时 30s / 五日 120s / K 线 600s

数据出处与状态

  • 读工具统一契约:每个读数返回 asOf(数据时刻)/ stale(降级复用)/ source(em·tencent·sina·lkg·local·mixed·none)/ missing[](缺失项 + 原因)。asOf 是数据被观测到的时刻,不是响应生成时刻

  • 缺失分两类:no-source(上游根本没有这份数据,重试无用)与 transient(本次失败,稍后重试可能恢复)—— 两者对应的下一步动作完全不同,因此不许含糊

  • 卡片五态(6px 色点 + tooltip 给出源与观测时刻):

    色点状态判据
    绿实时有价、非兜底、观测时刻在刷新间隔内
    黄延迟兜底值(source='lkg')或观测时刻超过刷新间隔 3 倍
    灰定稿复用整批 cached(非交易时段 + 无兜底行)
    灰未取到本轮没拿到这一行(该标的本次没请求,或首拍还没回来)——等下一拍,不是"三源都没有"
    红无行情源三源都没有可用价格(服务端给出的结论)

    列表头汇总 12/23 实时 · 9 延迟 · 2 未取到,五个数字与逐卡判定共用同一个函数,因此必然加得上;分母是屏幕上的卡片数(用户隐藏的卡不请求、也不计入,隐藏张数写在徽标悬停里)。未开盘(定稿)时不显示绿色;灰色与黄色语义不同:定稿是"数据已确定、不需要回源",延迟是"上游这次没给,退回旧值"

  • 错误码分级:400 请求本身有问题 · 413 请求体过大 · 415 非 application/json · 503 上游不可用(熔断中/连接被关闭/超时/上游 4xx-5xx/非 JSON,附 retry-after)· 500 未归类(本插件 bug)。不把"上游被限流"报成"你的请求写错了"

  • 熔断按主机隔离:行情主机被限流不影响搜索/日历(不同主机)。/rescue 与 /health 返回 breakerSummary(逐主机明细 + minutesLeft 最早可重试 + 最近失败原因);展示的剩余时间取最早恢复的那台(收盘后改为"下次开盘自动重试")

  • 缺失不许用 0 代替:板块涨幅、费用占比、涨跌幅等任何"取不到"的值显示 — 并给出原因,不显示 0 —— 0 会被读成"没涨没跌"/"没有手续费",那是错的信息

  • 降级不许假装实时:降级复用的数据标 stale;"上次成功结果"与"本次计算"分开标注;定稿缓存标明"未回源";失败时刻与数据时刻分列(失败不改写 ts)

成本与盈亏口径

  • 成本口径:买入按移动加权(费用计入摊薄成本);卖出确认已实现盈亏(费用于卖出时扣除);当日盈亏 = 隔夜 (现价−昨收)×数量 + 当日每笔买卖差额 − 当日费用;昨收取行情接口 昨收(期货主连按接口口径的昨结基准,见「已知边界」)
  • 成本口径可切换(偏好存 prefs.json):
    • 摊薄口径(默认,与多数券商 App 一致):摊薄成本 = (累计买入含费 − 累计卖出净额) ÷ 剩余数量;持仓盈亏 = (现价 − 摊薄成本) × 数量,已实现盈亏已折进剩余持仓成本;卖出亏损会抬高摊薄成本,卖出盈利会压低(可能为负)
    • 均价口径:买入移动加权均价(卖出不改成本);浮动盈亏单独显示,已实现盈亏另列
    • 两者恒等关系:摊薄持仓盈亏 = 均价浮动盈亏 + 累计已实现(selftest 断言"逐项差值之和 == 总额差值")
  • 盈亏率:浮动盈亏率 = 浮动盈亏 ÷(摊薄成本×数量);当日盈亏率 = 当日盈亏 ÷(期初数量×昨收 + 当日买入金额);无法估值(无昨收)时显示 —
  • 可用(可卖)数量:A股 T+1(今日买入部分当日不可卖),ETF/LOF、港股、美股 T+0;判定集中在 isT0Secid() 一处(沪深场内基金代码段 5xxxxx / 15–19xxxx),拿不准的一律按 T+1 —— 保守方向是"绝不说今天买的能卖"
  • 累计费用给绝对金额同时给占累计成交额的百分比(没有参照物的手续费数看不出贵不贵);成交额为 0 时占比是 — 而不是 0
  • 价格与成本保留 4 位小数(ETF/基金等低价标的 0.948 不会被四舍五入成 0.95)

折算口径(跨市场)

港/美股以原币种计价,两种档位:

  • 不折算(默认):总额只含 A股,其余逐项列在 unpriced[](why:no-fx 口径问题 / no-quote 行情问题),界面给 不含 N 项 ▼ 可下钻看原因
  • 固定汇率:填 1 港元 = ? 人民币 / 1 美元 = ? 人民币(填了哪个币种就折哪个),折算后计入总额,fxRates 回读实际生效汇率并显示在总额旁 —— "这个人民币数字按什么汇率算的"必须答得上来

两档之外没有第三种:按 1:1 悄悄加进去会让总额看起来完整、其实错了(错的口径比缺的口径更危险 —— 用户不会去质疑一个看起来正常的数)。汇率只接受 0.01–100,越界一律拒绝(把 7.15 填成 715 会得到差 100 倍的人民币市值)。

实时汇率暂不可用:汇率源尚未验证连通性与字段口径(中间价还是即期、符号怎么写都未确认)。宿主对 fxMode='live' 显式报错说明原因,而不是接受一个什么都不做的档位。

涨跌家数

优先用上游直接给的(指数的 f104/f105/f106),只有在它拿不到时才由本插件自行统计 —— 回包带 source 区分两者(em-index = 上游给的;em-clist = 本插件按沪深A股全量自行统计)。

自行统计的口径(回包 caliber 里写明,界面上也能读到):

  • 范围:沪深A股全量,含科创板/创业板,不含北交所(fs = m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23);
  • 判据:涨跌幅 > 0 记上涨、= 0 记平盘、< 0 记下跌;
  • 数字时刻是"统计完成时刻",与上方行情条的数据时刻不是同一个时刻 —— 两处各自标注,不混用;
  • 合理性护栏:三类之和必须等于总数、扫过的行数不得少于总数、三者全 0 而总数 > 0 一律按失败处理(宁可不给,也不给一个看起来正常但错的数);
  • 不做涨停/跌停家数:它需要每个标的的涨跌停价(主板 ±10%、创业板/科创板 ±20%、ST ±5%、新股首日不设限……)并处理停牌与无昨收,按涨跌幅近似会在 ST/新股/科创上稳定地算错。
  • 第三条链路(按涨跌幅降序的分页边界搜索)已实现但未启用:需要"按涨跌幅降序返回分页"的取数实现(deps.sortedPctPage 已留注入位)。

每天的快照(分位样本)只在家数与成交额都真实时记一条;缺的那天就缺,不补。

年初至今(YTD)

YTD =(现价 − 本年内第一个交易日收盘价)÷ 该收盘价 × 100%,序列用前复权(不复权序列在除权除息日会跳空下跌,那不是真实收益)。口径常量是单一来源(shared/model.ts 的 YTD_CALIBER),界面 tooltip 与 agent 工具引用同一句。

三处必须随结果说明的边界:

  • 指数 / 期货 / 板块(fqSupported=false):按原始价格(点位、合约价)计算,tooltip 写明"该标的不适用复权"。
  • 本年内上市:序列里没有更早的交易日 → 基准是上市首日,标注为「上市首日至今,不是年初至今」(按年初读会高估)。
  • 年内第一个交易日就是今天:口径要求的是收盘价,此时不给数(missing[].why='no-source',今天重试无用),不拿未收盘价或上一年收盘顶替。

取数:宿主按日 memo 复用 em.fetchKline(磁盘缓存 + 增量 + 单飞),并发 ≤4、失败有冷却、不重试轰炸;单次上限 60 个标的,超出如实回报 truncated;缺失一律 — 并给原因,不用 0 顶替。

K 线与复权

  • 默认前复权:前复权序列连续、最适合判断当前位置与相对成本(不复权会把除权跳空伪装成暴跌,所以不能当默认)。段控 前复权 / 后复权 / 不复权,口径常显在脚注
  • 显示的是实际生效口径(用宿主回包的 fqt,不是用户请求的那个值)—— 错误的口径标注比没有标注更糟
  • 按标的记忆:全局默认 + 每只标的一份选择,切周期不重置
  • 适用性由宿主判定(fqSupported):股票 / ETF / 基金 / 港美股可复权;指数(沪市 1.000*、深市 0.399*、100.* 国际指数)、期货(113/114/115.*、101/112.* 外盘商品)、板块(90.*)一律回落不复权,段控禁用并标「不适用」,tooltip 说明"按点位/合约价,没有除权除息"——禁用必须给理由,不能只灰掉
  • 口径进缓存键:klines/<secid>_<klt>_<fqt>.json,三个口径各自一份、各自增量。请求口径无缓存时宁可回源失败也不拿另一口径顶替 —— 否则图上会出现两套价格拼出来的假连续序列,而 stale 标注救不了它
  • 指标与图同源:MA5/10/30/60、MACD 与 K 线共用同一份序列(不存在"画线复权、指标不复权");B/S 买卖点按当前口径对齐坐标系,tooltip 里仍是真实成交价
  • 详情头不被改写:今开/昨收/最高/最低/成交量始终是真实成交价,脚注写明「(详情头为真实成交价)」,避免拿复权后的历史价去核对今日昨收

涨跌家数分位

大盘页在涨跌家数旁给出 上涨占比 62.1% · 近 60 日分位 78%。三条口径写死并随结果返回:

  • 指标 = 上涨家数占比(up ÷ up+down+even),不是涨跌比、也不是家数之差
  • 基准 = 之前 N 个交易日(不含今天)—— 把今天算进基准是自我参照
  • N < 5 不给分位(返回 null,不是 0):5 天的"分位"是噪音

快照只在收盘后(15:05 起;15:30 后标记定稿、不再被后续读数的抖动覆盖)写入 breadth.json。盘中家数一直在变,记进去等于把某一时刻的横截面混进"每日定稿",几天后分位就不可解释了。数据源与大盘页同一处(上证/深证行情里的 f104/f105/f106),不另开链路。

自选异动

  • 判据:量能倍数(同时点口径:今日量 ÷ (20 日均量 × 日内进度),阈值 2.5x,复用护盘标定的进度曲线)、价格异动(|涨跌幅| ≥ 5%)、量价共振(|涨跌幅| ≥ 3% 且量能 ≥ 2x —— 单条都不越线、组合越线)
  • 为什么用同时点:早盘 10:00 的进度才 33%,拿全天均量直接比会得出"每天都缩量",反过来用全天口径比又会把早盘的正常量报成放量
  • 样本 < 5 个交易日不判定,"没判定"(样本不足/非交易时段/日线里还没有今天这一根)与"判定过且无异常"严格分开显示 —— 前者不是结论,说成"正常"是错的信息
  • 提醒策略:同一条目 30 分钟静默(记在 localStorage,刷新页面后不会把刚看过的再提醒一遍)。静默只压制提醒,不抹掉"它正在异动"这个事实 —— 队列里仍以"静默中"列出,行高亮也不消失。否则会出现"明明在放量、徽标却是 0",然后没人再信这个徽标

统计护栏

概率类结论(底部概率)一律给 Wilson 95% 区间:20 个样本的 80% 与 500 个样本的 80% 完全不是一回事,只给点估计会被读成"准确率"。样本 N < 30 加「样本少」标记。用 Wilson 而不是朴素正态近似——后者在 p̂ 接近 0/1 或 n 小时会给出越界区间(概率 100%、n=5 时上界 >100%)。注意 Wilson 是 score 区间,围绕调整后的估计对称,因此极端比例下 p̂ 可以落在区间外。

护盘信号的边界

识别的只是「符合国家队护盘行为模式的放量 + 超大单净流入」,不能证明买入方是汇金/国新/诚通(它们不披露日内成交),回答与展示都按概率性信号处理。超大单为东财按单笔金额的分类口径(非席位数据);ETF 天量含做市双边报价与套利盘,故始终用超大单净额与量价背离交叉验证。免费源已无 ETF 资金流历史(东财 fflow 仅当日 1 行、push2his 常限流),故 F2 初期为经验锚点、历史回看自上线日起。

子组件与开关

本插件在一个 bundle 内拆成八行(core / calendar / cloudmap / market / watchlist / portfolio / tools / fupan),各自可独立开关: 停用某一行 = 该行的路由 404 + 工具从列表消失 + 侧栏 tab 消失(不灰显、不留空壳)。 行清单的唯一权威处是本包的 cordis.patch.yml(该文件的 - insert: 块,共八行:tradewatcher-core/-calendar/-watchlist/-portfolio/-market/-cloudmap/-tools(v0.71.0)/-fupan(v0.111.0),全部启用、每行 config: { row: … }); 行归属与设计见 docs/DESIGN-SUBPLUGINS.md,落地与验证记录见 docs/AUDIT-IMPL35.md。

本节的行数与页签数、cordis.patch.yml 的行、src/index.ts 的 ROW_ROUTES、src/client/index.tsx 的 tabs 五处由 src/readmeCounts.test.ts 锁死(改一处不改另一处 ⇒ 测试红)。

关闭某一行:不改本包,在 profile 层(~/.dsh/profiles/<profile>/cordis.patch.yml)按 id 覆盖即可 —— 只关日历就写:

- id: tradewatcher-calendar
  disabled: true

改完 cordis.patch.yml(或 profile 层行开关)必须重启 dsh web:宿主行只在启动时装载,刷新页面不够。 core 行不要关 —— 行情条/详情图//tradewatcher/rows 等共享路由都挂在它上面。

agent 工具索引

本表由 src/host/tools.ts 的 defs 决定;src/host/toolIndex.test.ts 会锁住名字与数量(加了工具不改表就红)。 计数:12 个工具。

分类名称一句话用途计数
行情quotes批量实时行情(预设 cn/intl/commodity/bond/all 或任意 secid)1
行情search在证券池里搜代码/名称2
分析ytd年初至今涨跌幅(前复权序列算,带口径与样本)3
分析position52 周区间位置% / 距高低点%(前复权,窗口随结果返回)4
分析group_rank自选分组一屏排序 + 统计药丸 + 分档汇总 + 文本分位5
分析rescue护盘信号(六因子)+信号的前瞻表现(forward,带基准与持有期)6
分析calendar财经日历(宏观/IPO/财报/分红)7
分析calendar_add往日历写一条手动事件8
分析undo撤销本插件做过的写入9
持仓portfolio持仓总览与盈亏(含组合构成、做T 配对、每股费用)10
持仓ledger逐笔流水(默认 500,超限如实报 truncated)11
持仓watchlist自选列表(分组结构)12

开发

逐轮审计与验收报告(含历史版本的过程记录)见 docs/archive/(AUDIT-*、COMPETITOR-NOTES.md、 各批实现自述 AUDIT-IMPL*.md),口径以本 README 的「口径规范」为准; 未落地的设计稿(如 DESIGN-DASHBOARD.md)顶部会写明"未落地",不要照它改代码。

开发原则:不冗余解释、不啰嗦(可执行版本)

写界面文案与提示时按这七条做,评审与验收也按这七条查:

  1. 常驻可见的说明性文本只留一行,且只写"影响什么 + 原因";长解释、复权口径、边界条件一律进 title 与 aria-label(读屏要读得到 —— 只挂在 title 上等于键盘/触屏拿不到)。
  2. 同一事实全局只留一处,优先级固定:行级 tooltip > 面板级 hint > 正文。同一个缺口不要在正文、面板、tooltip 各写一遍。
  3. 恒真 / 恒假句不渲染;空值不占位:缺失一律 — + 原因,不用 0(0 会被读成真实值)。
  4. 状态词、时间格式、单位用词表统一:未取到(本次失败)/ 上游无此数据(结构性)/ 数据时刻(不用"观测时刻/统计于/判定于")/ HH:mm:ss(页面级)/ MM-DD HH:mm(跨日历史)。
  • 不可算:输入不足、算不出来(例如成本未录入、样本不足、账不平)。它与 未取到(取数失败)是两种状态,不得合并。

  • 加载中:界面瞬时态(正在加载的转圈提示),不属于 R19 的那 10 个状态词;不要改成「进行中」。

    例外:窄容器(侧栏徽标的悬停 title,宽度只够一行)允许简写成 数据 HH:mm:ss —— 那里放不下「数据时刻」四个字,规则与实现必须一致,故在此写明。(侧栏标签恒为「盯盘」,不带任何时刻后缀,见上文「状态可见性」。)

  1. 结构性限制不进红色告警 —— 红色只留给故障("该市场只有当日分时"是限制,不是故障)。
  2. 口径标签只写通用词(如 涨跌幅),具体口径进 title/aria-label(如"相对当日开盘价 / 相对序列首点");可见文本里不堆括号解释。
  3. 新数字面必须登记进 styles.ts 的两条 data-blur=1 清单(隐身档要糊上、悬停显形);涨跌着色一律走 toneClass(tone, redUp) / dirClass(...),不许写死红绿。

来由:docs/archive/AUDIT-COPY2.md 的用词表 + v0.34–v0.37 的执行经验(同一缺口三处重复、口径写进正文导致标签变长、 结构性限制染红)。完整决策记录见 docs/DECISIONS.md。

src/
  index.ts            # 主机半区入口(路由 + 工具注册)
  host/em.ts          # 东财行情中继(push2delay/push2/push2his 多主机回退 + TTL 缓存)
  host/store.ts       # 文件存储 + 校验 + append-only 流水(纯函数可测)
  host/portfolio.ts   # 流水重放 → 持仓/分组/总览核算(纯函数)
  host/anomaly.ts     # 自选异动判定(纯函数 judgeAnomaly + 批量 detectAnomalies)
  host/breadth.ts     # 涨跌家数快照与历史分位(纯函数 percentileOf)
  host/backup.ts      # 备份导出/校验(纯函数 makeBundle/verifyBundle/describeConflicts)
  host/writeLog.ts    # 写操作日志与撤销登记
  host/rescue.ts      # 护盘采样与评分(六因子 + 时点系数 + 出分日志)
  host/routes.ts      # /tradewatcher/* HTTP
  host/tools.ts       # tradewatcher_* agent 工具 + 提示词段落
  host/selftest.ts    # npm run selftest(核算断言 + 实时接口冒烟,网络项 soft)
  client/index.tsx    # 浏览器半区:ctx.slots 注册(sidebar.panellist 图标 + main 面板)+ 装配
  client/*.tsx        # TopBar/自选/持仓/大盘/云图 + SVG 分时图 + 悬浮卡
  host/ytd.ts         # 年初至今(前复权基准 + 按日 memo + 并发有界的批量计算)
  client/ytdView.ts   # YTD 展示纯函数(文案/口径 tooltip/缺失摘要)
  shared/model.ts     # 两端共享类型 + 固定代码
  shared/stats.ts     # Wilson 区间(纯函数)
  • 构建:npm run build(esbuild → lib/index.js ESM、lib/client.js CJS + __ModuleLoader__ 工厂)
  • 门禁:npm run check = typecheck → test → selftest → build
    • typecheck:tsc --noEmit;build.mjs 构建前会先跑它,失败即中断(可用 TW_SKIP_TYPECHECK=1 临时跳过);类型错误退出码 1、tsc 未能启动(依赖没装)退出码 2、被信号杀死退出码 3 —— 不把"没装 typescript"报成"类型检查不通过"
    • test:node --test --experimental-strip-types,覆盖客户端纯函数(指标/格式化/排序/状态判定/时区/区间估计/布局)与路由层(src/host/routes.test.ts:请求项全覆盖断言、大小写回归、400/413/415/503 语义、信任围栏、截断回报;该文件把 DSH_HOME 指向临时目录,不读不写用户真实缓存)
    • selftest:host 侧集成自测(纯函数断言为硬失败,真实网络探针为 soft);同样把 DSH_HOME 指向临时目录
    • build:产物校验 —— 客户端必须包含一组必须存在的 UI 文案片段(含 esbuild 的 \xHH/\uXXXX 两种转义形式,避免假阴性),数量与清单在 build.mjs 的 REQUIRED_CLIENT_SNIPPETS
  • 版本号单源:package.json.version 经 esbuild define 注入客户端(__TW_VERSION__),构建期校验产物确实含该版本号
  • 客户端在浏览器中的 react/react-dom/cordis 由 web shell 模块表提供(externals);其余全部内联
  • 纯逻辑与 React 分离:可断言的逻辑(状态判定、排序、会计、布局、区间估计)放在不 import react 的模块里,因此 node --test 可以直接跑;hook 单独成文件
  • 新增数字展示面时,需同时把选择器加进 styles.ts 里数字模糊的两条规则(否则截图保护会漏数)

明确不做的项(附理由,避免反复评估)

外部代码评审(v0.15.1 基线)还提出若干结构性优化,经实测后明确不做或推迟,理由如下:

  • 账务单遍分组(O(P×N) → O(N)):实测本机账本为 103 条流水 × 10 只持仓 = 1030 次遍历,/portfolio 响应 0.28s 且主要耗时在上游行情等待。此规模下优化遍历无收益。
  • 流水改 JSONL 追加写、模块级缓存加 LRU、上游加令牌桶限流:单用户单进程、标的不满百;上游全挂的主要风险已由按主机熔断覆盖。
  • 拆分 em.ts / rescue.ts / tools.ts 为多文件、把模块级单例改工厂注入:功能仍在演进期,拆分与并行开发的冲突成本大于收益;新增模块按 bottom.ts 的方式办(纯函数、单主题、有断言)。
  • 引入 vitest / husky / CI 供应商:改用零依赖的 node --test;本机单机项目不引入 CI 服务。
  • CSRF token 等完整加固:实测服务只监听 127.0.0.1,局域网不可达,故不引入 token 体系;但请求 Content-Type 断言已实装(readBody 要求 application/json,否则 415)—— 浏览器对 <form> 只能发 urlencoded/text-plain/multipart,因此这一条把跨站表单整类挡在路由之外,与 Origin / sec-fetch-site 围栏互补。

已知边界

  • K 线数据源:腾讯历史 K 线为主(A股/港股/美股,稳定且快),东财 push2his 兜底(指数/期货等腾讯不覆盖的标的),两源统一不复权口径避免混源污染缓存;东财 klt=104 实际是季K,本插件的「年K」由月K本地重采样;历史接口的「200 + 空数组」按失败处理并切换数据源
  • 行情来自东方财富免费公开接口(push2delay 主、push2/push2his 备;历史接口偶发限流会自动重试/降级);延迟数据,盘中偶有缺口;国际指数与商品按源时区展示
  • 待复核:100.KOSPI200 的数值水平。本机 quotes-lkg.json 里东财返回的 100.KOSPI200(名为「韩国KOSPI200」)为 1088.15,而同一批其它国际指数的水平与公开行情一致(CAC40 8078 / FTSE 9284 / 恒生 24523 / 日经 44946 / 标普 6643)。KOSPI200 长期在数百点区间,1088 与之不符 —— 怀疑东财该 secid 指向的不是 KOSPI200 指数本身(或其口径不同)。新浪无 int_kospi200、腾讯无对应映射,因此两个备用源都无法交叉验证;东财行情主机对本机不可达时也无法复核。在验证清楚之前,不要把该数值当作决策依据;东财可达时可用 tradewatcher_quotes 100.KOSPI200 与其它公开源对照
  • 国际指数/商品的备用源缺口:东财不可达时的兜底覆盖为 20/23(腾讯 6 个 A股指数 + 新浪 14 个:7 国际指数、4 国内期货、3 外盘商品)。缺 韩国KOSPI200 / 法国CAC40 / 欧洲斯托克50 —— 三家免费源(hq.sinajs.cn、qt.gtimg.cn、smartbox 搜索)都没有对应符号,只能等东财恢复;此时它们会带 source: lkg 与滞后标记,而不是显示假数据
  • 待复核:国内期货主连的昨收字段。本机缓存里 4 个国内期货主连(生猪/焦煤/豆粕/螺纹钢)来自新浪源时出现"昨收与最新价完全相同 → 涨跌幅 0.00%",而同一条记录的当日区间明显有波动(如生猪 open 11200 / high 11230 / low 10970 / 最新 11105)。怀疑新浪 nf_* 布局对该类合约的昨收字段取错;在拿到真实返回原文之前不改解析器(按错的下标改只会把"错得很明显"换成"错得看不出来")。看到 0.00% 时请以行情软件为准
  • 上游连接抖动:本机到东财主机的连接会随机被立刻关闭(实测瞬时失败率 20–75%,与响应大小、IPv4/IPv6 选择无关,表现为 UND_ERR_SOCKET,几十毫秒即返回)。因此上游请求统一走 fetchAny:「2 轮 × 每主机 2 次」(最多 8 次尝试)+ 抖动退避 + 9 秒总截止;实测分时接口冷启动成功率从 3/12 提升到 12/12、板块接口从 1/3 提升到 6/6,且正常时首次尝试即成功(240–500ms)。行情主路径已对齐同一骨架并补上失败日志与熔断
  • 分时序列兜底:trends-lkg.json 保存上一份好图并在 staleAt 标记快照时间,命中后写 20s 短缓存(显式 grace: 0,不被 peek 的 45s 宽限吞掉)避免断网期间按行重试形成风暴;客户端同样不因一次失败抹掉缩略图(失败只短缓存 15s)
  • 分时图对没有当日分时的标的回退近 5 日日线;push2his 限流时悬停图可能短暂显示占位
  • 期货主连「昨收/昨结」等口径字段随接口语义展示,若做精确期货结算对账请以结算单为准
  • 跨市场标的盈亏按接口币种金额展示;未开折算时不计入总额(见「折算口径」)
  • 删除分组 = 归档(保留持仓与全部流水);「移除持仓」要求数量为 0(先卖后移),保证流水完整可追溯
  • 财经日历:宏观自动源为东财 H5 财经日历(免费公开接口),它不含美国 CPI/BLS 数据(该源有 PCE、PPI、非农、失业率,但没有 CPI),也不含未上市公司 IPO 传闻;这类事件请用页面 + 事件 或 tradewatcher_calendar_add 手动补录。宏观重要度为名称启发式推导,非官方评级;公布值/预期值该源未提供

变更记录

历次变更、口径修订与实测依据见 CHANGELOG.md。

License

MIT