☰
PanWatch marketdata 包拆解:一个 Symbol 值对象如何统一 A股、港股、美股代码
2026/10/1 3:36:47 网站建设 项目流程

PanWatch marketdata 包拆解:一个 Symbol 值对象如何统一 A股、港股、美股代码

【免费下载链接】PanWatchPanWatch — AI stock monitoring for A-shares, HK & US markets, powered by TradingAgents. Portfolio insights, real-time alerts & automated reports.|盯盘侠:覆盖 A股/港股/美股的 AI 盯盘、持仓分析、实时提醒与自动报告。项目地址: https://gitcode.com/GitHub_Trending/pa/PanWatch

PanWatch(盯盘侠)是一个覆盖 A股、港股、美股的 AI 盯盘项目,其行情数据层独立在 packages/marketdata 包中。今天拆一个容易被忽略、却决定"三市场统一"成败的小文件——Symbol值对象:它如何用一个不可变数据类,替掉散落在各数据源里的腾讯前缀、东财 secid、yfinance 后缀等转换逻辑。

为什么需要"归一化":同一只股票,三种写法

先看一个现实问题:茅台、腾讯、苹果这三只股票,在不同数据源里的"身份证"完全不同:

股票裸代码腾讯接口东财 secidyfinance
贵州茅台600519sh6005191.600519不支持
腾讯控股00700hk00700116.007000700.HK
苹果AAPLusAAPL105.AAPLAAPL

如果你同时接 3~4 家数据源做主备故障转移(PanWatch 的 Engine 就是这么做的),这些转换逻辑一旦散落在每个 vendor 里,就会变成一排排if market == "HK"的复制粘贴——改一处忘一处,就是线上 bug。

这正是Symbol要消灭的问题。模块开头的注释写得很直白:

跨市场股票代码值对象:一处归一化,替代散落各处的_to_market/前缀逻辑。

完整定义在 symbol.py。

Symbol 值对象:一个 frozen dataclass 的解剖

整个Symbol只有两个字段,靠frozen=True保证不可变——它是"值对象",不是"实体":

@dataclass(frozen=True) class Symbol: market: Market # Market.CN / Market.HK / Market.US code: str # 裸代码,如 "600519"

不可变带来两个好处:可以安全地做字典 key(比如按市场分组)、可以随手传参、不用担心被下游 vendor 悄悄改掉。

自动识别市场:三条正则 + 一个兜底

Symbol.parse("600519")不传市场也能猜对,靠的是三段规则(symbol.py):

_CN_RE = re.compile(r"^[036]\d{5}$") # 6 位,0/3/6 开头 _HK_RE = re.compile(r"^\d{5}$") # 5 位数字 _US_RE = re.compile(r"^[A-Z.]{1,6}$") # 1-6 位字母(含指数 .DJI)
  • 600519→ 6 位、6开头 →A股
  • 00700→ 5 位数字 →港股
  • AAPL→ 纯字母 →美股
  • 兜底:6 位数字当 A股,其余当美股

当然,猜测总有撞车的时候(比如某些美股代码恰好是 6 位数字)。所以parse支持显式传参:Symbol.parse("00700", "HK"),显式市场优先级永远高于猜测。

三个转换方法:把"方言"翻译给各家数据源

Symbol的三个转换方法,就是给三家数据源"说方言":

def to_tencent(self) -> str: if self.market == Market.HK: return f"hk{self.code}" if self.market == Market.US: return f"us{self.code}" return _cn_exchange(self.code) + self.code # sh/sz/bj + 代码
方法输入600519输入00700(HK)输入AAPL
to_tencent()sh600519hk00700usAAPL
to_eastmoney_secid()1.600519116.00700105.AAPL
to_yfinance()不支持(被拦截)0700.HKAAPL

注意港股在 yfinance 里的零填充细节:00700→0700.HK。这类"各家接口文档里不会主动提醒你的坑",收敛在一个文件里最容易维护。

A股的交易所前缀还有一套独立规则(symbol.py):920/83/87/88开头 → 北交所bj;5/6开头或900→ 上交所sh;其余 → 深交所sz。这套规则与宿主的 cn_symbol.py 保持一致,避免两处漂移。

谁在消费 Symbol:vendor 层的"单一入口"

Symbol不是摆设,它是所有 vendor 的入参类型。以腾讯行情源为例(tencent.py):

def fetch(self, symbols: list[Symbol], config: dict) -> list[Quote]: ... market = symbols[0].market.value codes = [s.to_tencent() for s in symbols]

vendor 拿到的已经是"解析好的值对象",它只负责:调to_tencent()翻译 → 发请求 → 解析成标准类型Quote。完全不需要关心代码长什么样。同样的模式贯穿各数据源:

  • 东财资金流:sym.to_eastmoney_secid()(capital_flow.py)
  • 东财报价/基本面:sym.to_eastmoney_secid()(eastmoney.py、fundamentals.py)
  • Yahoo K线:sym.to_yfinance()(kline.py)

跨市场自动分组:quotes 的"魔法"来自这里

marketdata 包最优雅的一个用法在 client.py 的quotes():

md.quotes(["600519", "00700", "AAPL"]) # 三个市场的代码混在一起传

内部先Symbol.parse逐个识别市场,再按市场分组,每组各走一次 Engine 主备链:

groups: dict[str, list[Symbol]] = {} for raw in symbols: sym = raw if isinstance(raw, Symbol) else Symbol.parse(raw, market) groups.setdefault(sym.market.value, []).append(sym)

调用方一行代码横跨三个市场,识别、分组、路由全部自动完成。这套行为的回归保障在 test_symbol.py 里,覆盖自动识别、显式市场、北交所前缀等场景:

assert Symbol.parse("00700").market == Market.HK assert Symbol.parse("920001").to_tencent() == "bj920001" assert Symbol.parse("00700", "HK").to_yfinance() == "0700.HK"

值对象设计模式:小文件里的大工程

回头看,Symbol不到 80 行代码,却体现了几个经典的值对象实践:

  • 单一事实来源:市场识别规则只写一遍,三个转换方法只写一遍,全项目不再有第二份if market == "HK";
  • 不可变 + 纯函数:frozen dataclass保证转换方法幂等、可安全共享;
  • 领域语言:Symbol.parse("600519").to_eastmoney_secid()读起来就是自然语言,新人五分钟能读懂整个文件;
  • 可测试性:不需要任何网络,纯字符串断言就能锁定所有边界情况。

小结

packages/marketdata 包整体是"可插拔数据源 + 主备故障转移"的架构,而Symbol是其中成本最低、收益最高的一块设计:用一个不可变值对象,把"同一只股票在三家接口里的三种写法"收敛到一处。如果你想给 PanWatch 加新数据源,建议第一步就是读 symbol.py——它 20 分钟就能读完,却能帮你避开所有市场前缀的坑。

【免费下载链接】PanWatchPanWatch — AI stock monitoring for A-shares, HK & US markets, powered by TradingAgents. Portfolio insights, real-time alerts & automated reports.|盯盘侠:覆盖 A股/港股/美股的 AI 盯盘、持仓分析、实时提醒与自动报告。项目地址: https://gitcode.com/GitHub_Trending/pa/PanWatch

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询