# 东方财富"筹码分布"逆向分析笔记 > 目标页面: > 抓取目标:页面右侧"筹码分布"图所表达的价格-持仓量数据 > 完成日期:2026-05-22 --- ## 一、问题与第一直觉的误区 页面右侧的"筹码分布"是一个 **canvas** 渲染的柱状图。看到 canvas,最容易掉进两个坑: 1. **误区 A**:去解析 canvas 像素,做图像识别 → 又慢又脆弱,是最坏的方案。 2. **误区 B**:直接断言"它一定有一个 chips 接口",去 Network 面板搜 `chip` / `cyq` 关键字 → 抓不到任何东西就以为是接口被加密了。 **正确认知**:canvas 只是渲染层。**绝大多数 canvas 图表的数据来自后端 JSON**,但也存在一种情况:**前端用更基础的数据(如 K 线)在本地算出图表所需的衍生数据**。这次的筹码分布就属于后者。 --- ## 二、判定数据来源的通用三步法 遇到任何不知来源的页面数据,按这个顺序定位: ``` 1. 监听完整请求生命周期 → 是 XHR / Fetch / Script(JSONP) / WebSocket / EventSource? 2. 找到触发点 → 哪个用户操作引起哪条请求? 3. 都找不到匹配请求?→ 数据多半是前端本地算的,去 JS 源码里找算法 ``` 本次实战路径: | 阶段 | 操作 | 结论 | |---|---|---| | ① 用户初判 | 在 Network 面板按"筹码分布"按钮后搜 `chip` | 搜不到 → 误以为是普通 K 线接口 `kline/get` | | ② 工具复核 | 用 MCP `js-reverse` 打开页面,程序化点击"日K" + "筹码分布"按钮,对比点击前后的所有请求 | 发现点击筹码分布后**唯一新增的 script 请求仍是同一个 K 线接口** | | ③ 推论 | 既然没有专门接口,必是前端算的 | 去前端代码里找算法 | | ④ 定位算法 | 在 `quotechart2022.js` 里搜 `cyq`("筹码"拼音首字母)| 找到 `./src/modules/tools/indicator/cyq.ts` 里的 `CYQCalculator` 类 | > 关键命名线索:东方财富前端**用拼音首字母给模块命名**——筹码分布 = ChouMaYun → `cyq`、除权除息 = ChuQuanChuXi → `cqcx`。遇到中文产品名的接口/类,先试拼音首字母。 --- ## 三、定位过程的关键证据 ### 证据 1:点击前后请求对比 点击"筹码分布"按钮前后,所有 XHR/Fetch/Script 请求里**没有任何新的数据接口**,只新增了: - 两条埋点 `Web_Event.gif`(用户行为统计) - 一条与 K 线主图共用的 `push2his.eastmoney.com/api/qt/stock/kline/get`(点击触发了图表重绘) → 没有专属接口 = 数据是前端算的。 ### 证据 2:DOM 容器命名 ```html
``` `cyq` 容器的存在直接坐实了模块归属。 ### 证据 3:源码模块路径 `quotechart2022.js` 包含两个 cyq 相关 webpack 模块: - `./src/modules/kline/cyq.ts`(drawCYQ —— 画图) - `./src/modules/tools/indicator/cyq.ts`(CYQCalculator —— **算法**) 源码大小适中(约 2KB),适合人肉逆向。 --- ## 四、CYQCalculator 算法精解 ### 4.1 输入输出 ``` 输入: klinedata : [{open, close, high, low, hsl}, ...] K 线 + 换手率 fator : 150 价格档位数 (默认) range : None | int 滑窗根数 (None=全部) e : int 要算到第几根 K 线 (索引) 输出: x : [w0, w1, ..., w149] 各价位的筹码权重 y : [p0, p1, ..., p149] 对应的价格档 benefitPart : float 获利比例 avgCost : "xx.xx" 平均成本 (50% 分位价) percentChips : { 90: {...}, 70: {...} } 成本集中区间 ``` ### 4.2 算法本质:三角分布 + 指数衰减 筹码分布建模思路: 1. **价格离散化**:把 [min(low), max(high)] 等分成 150 档。 2. **每根 K 线视为一笔"新筹码"**:成交量按"三角分布"分摊到当日的价格区间——平均价处最密,向 high/low 两端线性递减。 3. **换手率作为衰减因子**:当日换手率 `A = hsl/100`,意味着旧筹码被替换 A 比例。所以处理每根 K 线时: - 旧筹码先衰减:`chips[*] *= (1 - A)` - 新筹码再叠加:用三角形 PDF 把 `A` 单位的筹码摊进 [low, high] 区间。 这个模型的合理性:**老股东持仓被换手稀释**,**新成交价格在均价附近最密集**——符合直觉。 ### 4.3 三角分布的 PDF 公式 对当日 K 线(low=L, high=H, mid=M=(O+C+H+L)/4),在价格 x 处的权重: ``` 峰值 y₀ = 2 / (H - L) (面积归一化为 1) 左半区: x ∈ [L, M] w(x) = (x - L) / (M - L) · y₀ 右半区: x ∈ (M, H] w(x) = (H - x) / (H - M) · y₀ 其他: w(x) = 0 ``` 最终再乘以本日换手比例 `A`,叠加到对应价格档位。 ### 4.4 衍生指标 - **获利比例** `benefitPart`:当前 close 之下的累计筹码占比(即"在当前价位之下买入的人都赚了")。 - **平均成本** `avgCost`:累计筹码到 50% 时对应的价格(中位数成本)。 - **N% 成本区间** `percentChips[N]`:累计筹码从 `(1-N)/2` 到 `(1+N)/2` 区间对应的价格段——例如 90% 意味着丢掉最高 5% 和最低 5% 的极端档位。 - **集中度** `concentration = (high - low) / (high + low)`:区间相对宽度,**越小代表筹码越集中**。 --- ## 五、Python 复刻关键细节 ### 5.1 接口参数 ```python url = "https://push2his.eastmoney.com/api/qt/stock/kline/get" params = dict( secid = "0.300624", # 0.=深市, 1.=沪市 ut = "fa5fd1943c7b386f172d6893dbfba10b", # 通用 token,无需动态获取 fields1 = "f1,f2,f3,f4,f5,f6", fields2 = "f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61", klt = 101, # 101=日K, 102=周K, 103=月K, 5/15/30/60=分钟K fqt = 1, # 0=不复权, 1=前复权, 2=后复权 (网页默认前复权) end = "20990101", lmt = 210, # 网页用 210 根 cb = "cb", # JSONP 回调名占位 ) ``` ### 5.2 K 线返回字段映射 (fields2) | 字段 | 含义 | 备注 | |---|---|---| | f51 | 日期 | `YYYY-MM-DD` 字符串 | | f52 | 开盘 | | | f53 | 收盘 | | | f54 | 最高 | | | f55 | 最低 | | | f56 | 成交量 | 单位:手 | | f57 | 成交额 | | | f58 | 振幅 | % | | f59 | 涨跌幅 | % | | f60 | 涨跌额 | | | f61 | **换手率** | **筹码算法必需** % | 数据格式:`data.klines` 是字符串数组,每行用 `,` 拼接,按 `fields2` 顺序对应。 ### 5.3 JSONP 剥壳 ```python import re, json, requests r = requests.get(url, params=params, timeout=10) data = json.loads(re.search(r"\((.*)\);?$", r.text.strip()).group(1))["data"] ``` ### 5.4 算法移植注意点 1. **`a/b||0` 这种 JS 习惯写法** —— Python 翻译时要加空值兜底:`(d["hsl"] or 0) / 100`。 2. **`toPrecision(12)/1`** —— JS 用来截断浮点误差,Python 用普通浮点累加误差也小到可忽略,无需特意处理。 3. **`.toFixed(2)`** 返回字符串 —— Python 用 `f"{x:.2f}"` 对齐,**别用 `round()`**(类型不一致会害下游对比)。 4. **`Math.floor / Math.ceil`** —— 直接 `math.floor / math.ceil`。 5. **边界处理** —— `if abs(m - low) < 1e-8` 这种是避免除零,必须保留。 --- ## 六、用 MCP js-reverse 做浏览器逆向的实战姿势 这次实战用到的关键能力: | 工具 | 用途 | |---|---| | `new_page` / `select_page` | 控浏览器开页面 | | `evaluate_script` | 程序化点击、读 DOM、查 window 全局变量 (注意 `mainWorld:true` 才能拿到页面真实 JS 上下文) | | `list_network_requests` | 关键。可按 `resourceTypes` 过滤、按 `urlFilter` 子串过滤 | | `search_in_sources` | 全量 JS 源码搜关键字。**逆向利器** —— 用拼音首字母搜中文功能模块 | | `get_script_source` | 取指定行/字节范围的源码(minified 文件用 `offset/length`) | ### 标准逆向流程模板 ``` 1. new_page → 打开目标 2. evaluate_script (DOM/click) → 触发目标功能 3. list_network_requests → 找触发后新增的请求 ├─ 找到了 → 复刻接口参数即可 └─ 找不到 → 进入第 4 步 4. search_in_sources → 用功能关键字 (拼音首字母 / 英文 / 业务名) 搜源码 5. get_script_source → 读出算法 6. 把算法移植到 Python / 其他语言 ``` --- ## 七、本案例可复用的通用经验 1. **canvas ≠ 数据被加密**。先排查请求层,再排查 JS 计算层,最后才考虑像素解析。 2. **"点击触发"功能**:一定要程序化对比点击前后的请求差,肉眼看 Network 面板容易漏。 3. **中文产品名的逆向**:试拼音首字母(cyq / cqcx / hsl / jzlx ...)。 4. **JSONP 接口**:是 `script` 类型而不是 `xhr`,按 resourceType 过滤时别漏。 5. **东方财富的字段编号体系**(f1~f9xx)有公开整理资料,遇到陌生字段先查表再猜。 6. **`ut` 参数** `fa5fd1943c7b386f172d6893dbfba10b` 是东方财富一个长期通用的访问 token,可以直接硬编码。 --- ## 八、文件清单 - `eastmoney_spider.py` —— 抓取脚本(含完整算法移植) - `筹码分布逆向分析.md` —— 本文档 ## 九、验证结果 | 指标 | 截图(2026-05-21 盘中) | 本脚本(2026-05-22 收盘后) | |---|---|---| | 平均成本 | 69.05 | 68.64 | | 获利比例 | 0.18% | 0.73% | | 价格区间 | 61.16 ~ 125.17 | 与之极接近 | | 主峰位置 | 70 元附近 | 67~68 元(权重最高 15 档全在此区间) | 差异在合理范围(差一日 + 盘中/收盘价差),**算法移植正确**。