持仓体检报告(A 股组合四维诊断 → 单页 HTML)
适用场景
用户给出 3–8 只 A 股(有时带仓位),要求:
- 逐只体检:估值贵不贵 / 基本面质量 / 主力资金动向 / 技术位置
- 给每只一个「保留 / 减仓 / 砍」的判断
- 组合视角:行业集中度、相关性、整体风险
- 给出加减仓优先级
- 输出单页 HTML,中文界面,涨红跌绿
交付物标准
单个 .html 文件,完全自包含(不引 CDN、不引 ECharts,图表手写 inline SVG),保证离线/嵌入预览可渲染。参考体量 60–120 KB。
必备版块:结论卡 → 组合分层 → 五只总览表 → 四维评分卡 → 估值分位图 → 成长×估值散点 → 主力资金 → 多周期涨跌 → 基本面质量 → 技术位置 → 逐只点评 → 组合视角(相关性热力图/风险贡献/降波情景/可交互仓位调整器)→ 加减仓优先级 → 前提追问 → 数据来源与免责。
执行流程
阶段 1|拉数据(并行)
先读 references/data-sources.md(含命令清单、字段口径、踩坑记录)。
要点:
1. westock-data:npx -y westock-data-clawhub@1.0.4
- kline <codes> --period day --limit 250 --fq qfq → 价格与波动
- technical <codes> --group all → 均线/MACD/KDJ/RSI/BOLL/SAR 一次取全
- asfund <codes> → 主力当日/5/10/20 日净流入、融资余额及环比、龙虎榜
- chip <codes> → 获利比例、平均成本、集中度
- finance <codes> --type lrb|zcfz|xjll --num 3~6、profile、dividend、shareholder
- ⚠️ 该 CLI 没有 quote 命令(渠道限制),现价取 technical 的 closePrice
2. neodata-financial-search:估值与最新财报的唯一权威口径
- python scripts/query.py --query "<股票> <代码> 最新滚动市盈率 市净率 估值历史分位" --data-type api
- --query "<股票> <代码> 2026年半年报 营业收入 归母净利润 扣非净利润 同比增长率 ROE" --data-type api
- --data-type doc 取机构研报、目标价、新闻(判断"为什么跌")
- 报 TOKEN_MISSING/TOKEN_EXPIRED 时才调 connect_cloud_service → --save-token
口径优先级(关键):westock 财务库期末可能落后一两个季度,neodata 往往已含最新中报。
→ 基本面判断以 neodata 为准;PE/PB 及其分位统一用 neodata,保证同一估值序列可比。 若两边不一致,在报告「数据来源与口径」里显式说明差异。
阶段 2|量化测算
跑 scripts/portfolio.py 的思路(可复制改写):
- 解析 kline markdown 表 → 近 120 日日收益率
- 样本协方差矩阵 → 组合年化波动(年化系数 √244)、个股年化波动
- 风险贡献 w_i·(Σw)_i / (w'Σw) → 找出「风险超配」的标的
- 两两相关系数矩阵 + 平均相关性
- 等权组合累计收益与最大回撤
- 降波情景:砍某只 / 换入低波资产(假设年化 18%、相关 0.25)→ 组合波动变化
务必算"风险贡献 vs 资金权重":这通常是报告里最有说服力的一张图(常见结论:某只高波动股占 20% 仓位却贡献 30%+ 风险)。
阶段 3|写作与判断框架
四维评分(0–10):估值(绝对水平+历史分位)、基本面(增长/盈利质量/财务结构)、资金(主力+杠杆资金)、技术(均线位置/超买超卖/趋势)。
判断要避免的三种错误:
1. 把"跌得多"当成"该砍"——必须区分"便宜"与"便宜得有原因"(业绩趋势转负 + 资金外流 + 减持信号 = 有原因)
2. 把"涨得多"当成"该留"——年内涨幅第一的票如果估值在 70% 分位、扣非利润近零、波动最大,它才是该砍的
3. 只看个股不看组合——底部区间的低相关标的可能是组合唯一的"分散器",砍掉它组合波动反而上升(用情景测算验证并写进报告)
必写的反直觉提醒:明确告诉用户这个结论与"卖赚钱的、留亏钱的"直觉相反,依据是估值分位/扣非质量/资金流向/波动贡献,不是浮盈浮亏。
仓位未知时的处理:显式声明"假设等权",并做一个交互式滑块面板(用内嵌协方差矩阵在 JS 里实时重算组合波动与风险贡献),让用户拖出自己的真实仓位看结论如何变化。
阶段 4|渲染与验证
- 图表全部手写 inline SVG,用 JS 函数生成后
innerHTML注入 viewBox="0 0 1040 …"+width="100%"自适应;配色用 CSS 变量同源(但 SVG 属性里不要写var(--x),直接写字面色值)- 验证(本机无 headless 浏览器时的低成本方案):
node --check查语法 + 自写 DOM 桩eval脚本,检查每个容器是否被写入、SVG 元素数量是否合理、innerHTML里有无NaN/undefined/Infinity
- 提取内联脚本做语法检查:python -c "import re;s=open('报告.html',encoding='utf-8').read();open('data/_chk.js','w',encoding='utf-8').write('\n'.join(re.findall(r'<script>(.*?)</script>',s,re.S)))"然后node --check data/_chk.js。(不要用 bash 进程替换<(...),Windows 下node --check <(...)会报Cannot find module 'C:\dev\fd\63') - 回验嵌入数值:用 Python 重新解析 HTML 里内嵌的
COV矩阵,独立复算一次等权波动率 / 风险贡献,确认与正文写的数字一致(防止改数据时漏改一处) - 可选视觉校验:本机有 Edge 时可用
msedge.exe --headless=new --disable-gpu --window-size=1280,12000 --screenshot=out.png <url>截图。注意file://路径含中文会失败(报 cmd.exe%VAR%语法错误)→ 先把 HTML 复制成 ASCII 路径(如data/preview.html)再截。再用 PIL 裁剪分段检查。 - 交付前用
present_files展示
阶段 5|标的被更正后如何最小改动重做
用户事后更正组合成员(「不是 A,是 B」)时,不要重跑整条链路:
- 先确认新代码(
search命令,或用行情接口探测;注意 A 股代码容易记错,如润贝航科是001316而非其他) - 只拉新标的的全套数据(kline/technical/asfund/chip/profile/finance/dividend/shareholder + neodata 估值/财报/研报),其余标的复用已有文件
- 复制
portfolio.py为portfolio2.py,改codes/names,重算协方差、相关性、风险贡献、情景——组合层面的每一个数字都会变 - 复制
scen.py为scen2.py重算降波情景(含低波资产假设) - 用
grep -c "<旧标的>"逐项清理正文残留(含 JS 数据对象、CORR/COV矩阵、order/keys数组、颜色映射、预设按钮、硬编码基准值如29.7) - 同步更新冒烟脚本
smoke.js里的 ranges 代码与 presets,再跑一次验证
大文件(80 KB+)批量改写的两个致命坑:
- 不要并发/连发多个
Edit:对同一个大 HTML 连续快速提交多个Edit,会出现部分改写被覆盖丢失(表现为验证时某些编号 MISS,且 grep 仍能搜到旧标的)。→ 逐个提交、每批改完立即grep校验;发现 MISS 就单独重跑那一条。稳妥做法是先用 Python 做一次全局str.replace处理高频替换(代码/名称/行业),再手工修剩余的数字与文案。 - JS 模板字符串里不要出现裸换行:形如
$("portNote").innerHTML = `${"...\n "}$("x")...`若反引号被换行截断,node --check会报Unexpected end of input。同理,模板字符串内不能出现中文全角引号 “ ”(会报Unexpected identifier)→ 中文引号改用「」。 - 每次改完必跑一遍
node --check+ 冒烟脚本,别攒到最后。
环境陷阱(Windows,必读)
- bash 工具 PATH 可能残缺(视会话/机器而定,有些会话里
ls/head/awk/cat正常可用,有些不全)。先试探一次:若常用命令报 not found,则文件与命令操作改走 PowerShell + Read 工具。 - PowerShell 工具吞 stdout:
Write-Output不显示 → 一律| Out-File -Encoding utf8落盘后用 Read 读 - 中文乱码:运行 CLI 前设
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8,否则输出被按 GBK 解码变成「鍛戒护」这类乱码 - 解析 CLI 输出的 markdown 表时按首个非空行找表头,别硬编码行号(不同命令前缀行数不同)
数据口径陷阱(容易静默算错)
- 250 日区间位置必须用收盘价(
close的 max/min),不要用high/low列。用 intraday high/low 会让区间变宽、pos250偏小,而且会让所有标的的数字一起变——一旦混用,报告里四个未改动标的的"距年线/区间"会悄悄改口径。 - 单标的 kline 与批量 kline 表头不同:批量(多代码逗号分隔)有
symbol列,单代码没有(date | open | last | high | low | volume | amount | exchange)。解析要分别处理。列索引:[0]=date [1]=open [2]=last(收盘) [3]=high [4]=low。 chip的chipConcentration90才是报告里用的"筹码集中度"(数值越小越集中);别误取70。- 非两融 / 小盘标的:
margintrade返回null、lhb/blocktrade返回「当日无龙虎榜/大宗交易数据」——属正常,正文写「—」或改述,不要当成抓取失败。 - wind 行业分类会误导:润贝航科主营航材分销+国产化自研,wind 因主营含航空油料/化学品把它归入"石油石化"。写行业集中度时要按实际业务描述并注明分类来源。
- 日均成交额差异巨大时,主力资金条形图会被最大流出项压扁(如 +1.13 亿 vs -59.7 亿),小节里要显式提示"图上几乎不可见但方向相反"。
dividend若做过送转:每股派现金额需按送转后的股本折算,否则股息率算错(例:10 派 4.36 元转 4 股,派息总额按转增前股本,折算到现股本约 0.31 元/股)。
免责声明(必须写入报告)
标明数据来源与截止日期、口径差异、评分卡为自建规则非机构评级,并明确"不构成投资建议"。