筹码分布逆向分析.md 9.8 KB

东方财富"筹码分布"逆向分析笔记

目标页面:https://quote.eastmoney.com/concept/sz300624.html?from=classic 抓取目标:页面右侧"筹码分布"图所表达的价格-持仓量数据 完成日期: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 容器命名

<div class="quotechart2022_c_cyq">
  <canvas width="270" height="577"></canvas>
</div>

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 接口参数

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 剥壳

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 档全在此区间)

差异在合理范围(差一日 + 盘中/收盘价差),算法移植正确