Vibe-Trading screen_market 实战解析:从 clist 端点到排行榜的完整调用链路
【免费下载链接】Vibe-Trading"Vibe-Trading: Your Personal Trading Agent"项目地址: https://gitcode.com/GitHub_Trending/vi/Vibe-Trading
Vibe-Trading 里有一个只读工具screen_market:一次调用即可拿到 A 股、美股、港股全市场按涨跌幅、成交量、成交额、换手率排名的 Top N。读完本文你会掌握四件事:端点寻址与 fs 市场全集选择器、fid 排序字段映射、返回信封结构、按 IP 限流下的防护机制。
为什么不做逐 symbol 抓取
"今天谁涨得最猛、成交最活跃"是交易研究里最高频的问题。最直接的解法是遍历候选列表逐个拉行情——慢,而且每加一个 symbol 就多一次上游请求,很容易撞上免费数据源的限流墙。
screen_market换了个思路:直接问东方财富 push2 体系的榜单列表接口。给定一个市场全集选择器,服务端返回每只上市标的一行(最新价 + 常用排名指标),并且由服务端按指定字段预排序,客户端只取前 N 行。一个请求顶几百次逐标的抓取,响应体规模天然有界。
架构位置方面:
- 实现文件 agent/src/tools/market_screener_tool.py,类
MarketScreenerTool继承自 agent/src/agent/tools.py 中的BaseTool(L13-55); - 注册走 agent/src/tools/__init__.py 的自动发现:
_discover_subclasses(L34)用BaseTool.__subclasses__()(L64)递归收集所有子类,放进src/tools/就自动被 LLM 可用,无需手动登记; - 技能索引页 agent/src/skills/eastmoney/SKILL.md 的"选股检索"分类(L93-97)里登记了它的用途与适用市场。
Vibe-Trading screen_market 怎么取数:clist 端点与 fs 选择器
端点
整个工具只打一个 URL,模块级常量_CLIST_URL(market_screener_tool.py L30):
https://push2.eastmoney.com/api/qt/clist/getclist与项目里另一个东财端点分工明确:push2his 的 kline 接口(见 agent/backtest/loaders/eastmoney_client.py 的fetch_kline,L300)回答"某只标的的历史走势",clist回答"全市场谁排前面"。
市场全集选择器(fs)
三个市场的fs值原样保存在_MARKET_FS(L35-39):
| 源码键 | fs 实际取值 | 覆盖范围 |
|---|---|---|
a | m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048 | 深市主板、创业板、沪市主板、科创板、北交所 |
us | m:105,m:106,m:107 | NASDAQ、NYSE、AMEX |
hk | m:116,m:113,m:114,m:115,m:128 | 港股主板与创业板等 |
这些片段对应东财的 secid 寻址约定:沪市用市场号1、深市/北交所用0(与resolve_secid中 A 股1.<code>/0.<code>的写法一一对应);港股固定116前缀 + 5 位零填充代码;美股三所分别是105/106/107。
排序字段映射:sort_by → fid → 查询参数
fid 映射
_SORT_FID(L43-48)把对外的排序名翻译成东财字段号:
| sort_by | fid | 含义 |
|---|---|---|
change_pct(默认) | f3 | 涨跌幅 % |
volume | f5 | 成交量(手) |
amount | f6 | 成交额(货币单位) |
turnover | f8 | 换手率 % |
排序方向恒为降序——"榜单"语义就是取最大的前 N 名,所以方向参数写死不提供入口。
完整的查询参数拼装
_screen_market(L120-164)调用get_json时逐项拼装 params(L137-147):
| 参数 | 取值 | 说明 |
|---|---|---|
pn | 1 | 页码,固定第一页 |
pz | str(top_n) | 每页条数 |
po | 1 | 1 = 降序 |
fid | _SORT_FID[sort_by] | 服务端排序依据 |
fs | _MARKET_FS[market] | 市场全集 |
fields | f2,f3,f4,f5,f6,f8,f12,f14 | 请求返回的列(_FIELDS,L52) |
测试test_sort_by_maps_to_eastmoney_fid(agent/tests/test_market_screener_tool.py L80-90)断言了这条拼接链路:market="us", sort_by="amount"时请求参数中fid == "f6"、po == "1"、fs == "m:105,m:106,m:107"。
一行原始数据怎么变成规范记录
响应体里data.diff是行列表,每行是 field id 键控的 dict,_shape_row(L92-117)负责归一化:
| field id | 输出键 | 描述 | 注意事项 |
|---|---|---|---|
| f12 | code | 代码 | 缺失则整行丢弃 |
| f14 | name | 名称 | 缺省为空串 |
| f2 | price | 最新价 | 经_num转换 |
| f3 | change_pct | 涨跌幅 | 同上 |
| f4 | change | 涨跌额 | 同上 |
| f5 | volume | 成交量(手) | 同上 |
| f6 | amount | 成交额 | 同上 |
| f8 | turnover_rate | 换手率 % | 同上 |
两个细节值得展开:
- 哨兵值映射为 None。东财对无值单元格返回
"-",_num(L71-89)将其转成None而不是 0.0——停牌股的换手率是"没有",不是"零"。测试test_change_pct_screen_parses_rows(L46-78)里平安银行的f8就是"-",断言结果为turnover_rate is None。 - diff 结构双形态归一化。同一接口在不同主机上
data.diff可能是 list,也可能是按索引键控的 dict(键为"0"、"1"……)。_screen_market对 dict 形态执行list(diff.values())(L154-155),测试test_diff_as_dict_is_normalized(L92-101)专门覆盖了这条分支。
入参与校验:三个参数的合法边界
JSON Schema 定义在MarketScreenerTool.parameters(L179-210):
| 参数 | 类型 | 必选 | 合法取值 | 默认值 |
|---|---|---|---|---|
market | str | 是 | a/us/hk | 无 |
sort_by | str | 否 | change_pct/volume/amount/turnover | change_pct |
top_n | int | 否 | 1–100 的正整数 | 30(_DEFAULT_TOP_N,L56) |
execute(L213-256)的校验边界行为:
- market 白名单:非字符串或不在
_MARKET_FS键集中即返回"market must be one of ['a', 'us', 'hk']"; - sort_by 白名单:同上逻辑,报错信息列出四个合法键;
- top_n 的布尔陷阱:校验写的是
not isinstance(top_n, int) or isinstance(top_n, bool)(L236)——Python 里isinstance(True, int)为真,必须显式排除布尔,测试test_bool_top_n_rejected(L150-153)验证top_n=True会被拒; - 上限钳制:通过校验后
top_n = min(top_n, _MAX_TOP_N)(L238),_MAX_TOP_N = 100(L55),传 500 也只拿 100 行,绝不报错; - 兜底捕获:请求阶段的任何异常(含 429)在
execute里被捕获并转成错误信封(L240-244),测试test_http_failure_surfaces_as_error_envelope(L155-164)模拟RuntimeError("HTTP 429")验证异常不外抛。
返回信封长什么样
成功时返回 JSON 字符串(ensure_ascii=False,中文名不转义):
{ "ok": true, "market": "a", "source": "eastmoney", "data": { "market": "a", "sort_by": "change_pct", "rows": [ {"code": "600519", "name": "贵州茅台", "price": 1688.0, "change_pct": 9.98, "change": 153.0, "volume": 1234567.0, "amount": 2080000000.0, "turnover_rate": 1.23}, {"code": "000001", "name": "平安银行", "price": 11.5, "change_pct": 5.01, "change": 0.55, "volume": 9876543.0, "amount": 110000000.0, "turnover_rate": null} ] } }失败时(参数校验或请求异常):
{"ok": false, "error": "top_n must be a positive integer"}行列表嵌在data.rows下而非裸列表,源码 docstring 明确写了原因(L224-225):与项目内所有工具的data:{...}信封形状保持一致,下游解析代码可以统一处理。即便上游返回空数据,也是ok: true+ 空rows(测试test_rowless_payload_yields_empty_data,L111-122),不会退化成裸列表。
为什么不该裸调端点:共享限速层
东财按源 IP 限流,并对突发请求的客户端临时封禁。工具的每次请求都不自己发 HTTP,而是走 agent/backtest/loaders/eastmoney_client.py 的get_json(L112-133),后者固定以host_key="eastmoney"(_HOST_KEY,L38)进入 agent/backtest/loaders/_http.py 的节流体系:
- 进程级节流器:
HostThrottle(L46-106)按 host 桶记录上次发枪时刻,保证同桶相邻请求最小间隔;并发调用方链式排队,锁只持有记账期间,不同桶互不阻塞; - 随机抖动:间隔之上叠加最多 0.4 秒抖动(
_JITTER_MAX_S,L43),避免多个并发调用方同时到期齐射; - Session 复用:每进程每桶一个
requests.Session(L114-124),摊薄 TCP/TLS 握手; - 浏览器 UA:默认携带桌面 Chrome 的 User-Agent(L35-38),因为不少免费行情端点拒绝裸 requests 指纹;
- 最小间隔可调:默认 1.0 秒,环境变量可覆盖:
VIBE_TRADING_EASTMONEY_MIN_INTERVAL批量任务建议调大而不是调小。这意味着绕开工具直连clist端点的突发请求会失去节流保护,可能触发按 IP 的临时封禁——连累的是同 IP 下所有走共享节流层的调用方,包括 backtest 的东财 loader。
直接调用与 swarm 中的用法
以下示例在agent/目录下运行,导入根为agent/,无需 token:
# 场景 1:A 股今日涨幅榜前 20 from src.tools.market_screener_tool import MarketScreenerTool print(MarketScreenerTool().execute(market="a", sort_by="change_pct", top_n=20))# 场景 2:美股成交额榜前 10 print(MarketScreenerTool().execute(market="us", sort_by="amount", top_n=10))# 场景 3:港股换手率榜前 50 print(MarketScreenerTool().execute(market="hk", sort_by="turnover", top_n=50))repeatable = True(L211)允许同一轮对话里多次调用对比不同市场/排序。技能脚本 agent/src/skills/eastmoney/scripts/screen_search_example.py 演示了"先search_symbol解析标的、再screen_market看市场动向"的完整研究流程。
在 swarm 多 Agent 编排中,它是研究型 worker 的 universe 枚举工具:
- agent/src/swarm/presets/statistical_arbitrage_desk.yaml(L48):先
screen_market枚举当日热点,再get_market_data拉价格面板; - agent/src/swarm/presets/pairs_research_lab.yaml(L60):用
screen_market枚举{market}/{sector}候选池后再逐对扫描。
它不能做什么
- 数据范围:只有 A 股、美股、港股三大市场的行情列表,期货、期权、外汇不在覆盖内;
- 实时性:返回的是 push2 行情快照,适合当日排名类问题;历史榜单序列不在此工具职责内;
- 单位语义:
volume以"手"计,amount为货币单位成交额,turnover_rate是百分比;跨市场比较时单位口径不同(港股量纲与 A 股不同); - 空值语义:停牌等无值场景对应字段是
None而非 0,下游计算必须做空值防御; - 限流依赖:可靠性建立在共享节流层之上,节流是进程内 best-effort、不跨机器协调,高并发场景靠环境变量拉开间隔。
相关工具
- agent/src/tools/symbol_search_tool.py 的
search_symbol:把公司名/代码片段解析为候选 symbol,与screen_market组成"模糊查询 → 全市场扫描"的组合拳; - agent/backtest/loaders/eastmoney_client.py 的
fetch_kline:入选标的的历史 K 线拉取,榜单粗筛后的深度数据环节。
【免费下载链接】Vibe-Trading"Vibe-Trading: Your Personal Trading Agent"项目地址: https://gitcode.com/GitHub_Trending/vi/Vibe-Trading
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考