
用 Python 揭秘均值回归策略:你的收益从何而来?
2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含 500 篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!
在交易台和研究台上,有一个被无数次重复、却几乎没有被系统化的动作:看到某只股票跌了 5%,然后切到新闻终端找原因,再打开情绪分析工具,最后在脑子里形成一个判断。这个流程的问题在于——它无法规模化、不可复现,事后也无法审计。
本文要拆解的,是一套用自然语言提问就能完成上述全流程的金融研究系统。你只要问一句「韩国市场为什么在抛售?」或「这个季度 NVDA 和 AMD 谁更强?」,系统就会自动汇集价格、新闻、情绪和宏观背景,跑一遍因果分析,最终给出一个可解释、每条结论都能追溯到具体新闻标题的结果。
对于正在学习 Python 的同学来说,这个项目是绝佳的工程范本:它几乎没有用什么花哨的算法,更多是用「确定性逻辑兜底 + LLM 增强」的思路,把一个看似很「AI」的需求拆成了几个清晰、可控、可降级的阶段。下面我们逐阶段来看。
系统把「一句话提问」拆成了四个环节:
核心设计哲学只有一句话:LLM 只负责「锦上添花」,而不是「雪中送炭」。即使大模型挂了,用户也一定能拿到一个有数据来源的确定性答案。
很多新手系统会直接把用户的提问映射到一只股票代码(ticker)。但这会在最有价值的问题上翻车——比如「为什么所有东西都在跌?」,因为这种问题根本没有单一的股票代码可对应。
所以第一步是一个「规划器(planner)」,它把意图分成四类:lookup(查询单只)、compare(对比)、diagnostic(诊断「为什么」)、market(大盘)。这里有一个很值得学习的工程取舍:优先用确定性的关键词匹配,只有匹配不到时才回退到 LLM。
def _classify(self, query, seed, region):
# 是否包含「下跌/急跌」这类描述价格变动的词
has_move = any(w in query for w in _MOVE_WORDS)
# 是否包含「原因/为什么/理由」这类追问原因的词
has_why = any(w in query for w in _WHY_WORDS)
# 是否涉及大盘,或地区不是全球(说明用户在问某个具体市场)
has_market = any(w in query for w in _MARKET_WORDS) or region != "GLOBAL"
# 既问「为什么/在跌」,又涉及市场或多个主题 → 走「诊断」路径(新闻优先)
if (has_why or has_move) and (has_market or len(themes) >= 2):
return "diagnostic", themes # news-first path(新闻优先)
# 有两个及以上候选标的 → 走「对比」路径
if len(seed) >= 2:
return "compare", themes
# 其余情况 → 普通「查询」
return "lookup", themes对于诊断类问题,系统采用「新闻优先(news-first)」策略:它不去猜某只股票,而是先在市场和行业的 ETF 代理上汇集新闻(例如:韩国 → EWY + SPY,半导体 → SOXX),让 LLM 读出新闻里的共同主线,然后才反推出真正在驱动行情的标的。
为什么把分类放在本地(on-device)做? 因为确定性的关键词匹配延迟可控,避免了把所有判断都丢给慢吞吞的模型调用,从而防止接口超时(504)。
延伸小知识:这种「快路径 + 慢路径」的设计在工程上非常常见。能用规则解决的就别调模型,既省钱(API 费用)又稳定(延迟可预测)。
any(...)配合一个关键词集合,是 Python 里做轻量级意图识别最朴素也最实用的写法。
这是整个系统在科学态度上最值得称道的地方。引擎会计算每日收益率,标记出超过阈值的异常波动,再在一个时间窗口内匹配相关新闻。但它从不声称因果关系,只产出带置信度的「候选解释」。
# 在指定日期附近的时间窗口内,找出匹配的新闻
matched = _news_within(news, r["date"], window_days)
# 用收益率符号判断当天涨跌方向:涨为 +1,跌为 -1
direction = 1 if r["ret_pct"] > 0 else -1
# 计算匹配新闻的平均情绪极性(取不到则回退到当日情绪)
avg_pol = mean([n["sentiment"]["polarity"] for n in matched]) or sentiment_on(...)
# 判断「情绪方向」是否与「价格方向」一致
aligned = None if avg_pol is None else (avg_pol > 0) == (direction > 0)
drivers.append({
"date": r["date"],
"return_pct": r["ret_pct"],
"impact": impact_score(r["ret_pct"]), # 影响力打分,范围 -5..+5
"confidence": _confidence(matched, aligned), # 新闻越多、情绪越一致 → 置信度越高
})置信度的设计逻辑很清晰:初始很低,每匹配到一条相关新闻就上升一点(但有上限),如果情绪方向和价格方向一致还会额外加分。最终结果按收益率的绝对值排序,让波动最大的行情先冒头。
易错点提醒:很多人会想当然地把「新闻和股价同时出现」当成「新闻导致了股价」。这就是经典的「相关不等于因果」陷阱。这套系统的做法很克制——它只说「这条新闻出现在这次波动附近,置信度多少」,把最终判断交还给人。这种诚实标注,恰恰是一个可被审计的系统应有的样子。
补充一点关于 mean([...]) or sentiment_on(...) 这个写法:当列表为空时 mean([]) 会触发异常或返回假值,利用 Python 的 or 短路特性回退到备用值,是一种简洁的兜底技巧——但要注意它在结果合法但为「假值」(如 0)时可能误触发,实际项目中需谨慎。
排名靠前的驱动因素、情绪趋势、宏观背景会一起交给 LLM,让它返回严格的 JSON:一段总结、3~5 条各自绑定到具体新闻标题的关键发现、一个情绪趋势标记,以及一句免责声明。
设计铁律是:LLM 只是在一个「已经存在的结果」上做增强。
# 先用启发式规则生成一个总是有效的基础结果(兜底)
base = _fallback(engine, ticker, series, lang)
# 如果没有配置 LLM,直接返回兜底结果
if not openrouter:
return base
try:
# 调用大模型,并强制要求返回 JSON 格式
parsed = json.loads(await openrouter.chat(chain, messages, response_format=JSON))
base["summary"] = parsed["summary"] # 用模型生成的总结覆盖
base["key_findings"] = clean(parsed["key_findings"]) # 清洗后覆盖关键发现
except Exception:
return base # 模型失败 → 直接返回兜底的启发式结果如果模型调用超时或返回了垃圾内容,用户依然能拿到一个有数据来源、确定性的答案。服务永远不会出现一片空白的尴尬场面。
工程思维:这就是所谓的「优雅降级(graceful degradation)」。注意这里用了
try/except把整个模型解析过程包起来,并且要求模型response_format=JSON——但即便如此也不能假设它一定返回合法 JSON,所以json.loads失败时立刻回退。这是接入任何外部不可靠服务时都该有的防御性编程习惯。
阶段二回答的是「这次波动附近有什么新闻?」,而事件研究回答的是一个更严格的问题:「在某条新闻发布之后,实际上发生了什么?」最妙的是,这一步完全用手头已有数据做纯计算,不需要额外调任何 API。
import bisect
# 找到新闻发布日当天或之后的第一个交易日的下标
i0 = bisect.bisect_left(dates, news_date)
base = closes[i0] # 以该交易日的收盘价为基准
# 分别计算 +1 / +3 / +5 个交易日后的收益率
for h in (1, 3, 5):
j = i0 + h
# 越界则置空,否则计算相对基准的涨跌百分比(保留两位小数)
rets[str(h)] = round((closes[j] - base) / base * 100, 2) if j < len(closes) else None它度量每个事件之后 +1/+3/+5 个交易日的「前向收益率」并取平均,给出一个完全不依赖 LLM 的理智检查(sanity check):如果那些被标为「正面」的新闻,平均之后却跟着负的前向收益,那么这个矛盾本身就是一个信号——而且它是用透明的方式算出来的。
bisect用法补充:bisect.bisect_left是 Python 标准库里在「有序列表」中做二分查找的利器。它返回的是「保持有序时应插入的位置」,用来快速定位「某日期之后的第一个交易日」非常合适,时间复杂度是 O(log n),比线性遍历高效得多。前提是dates必须是已排序的。
我们用「韩国市场为什么在抛售?」这个问题,把四个阶段串起来感受一下:
diagnostic,走新闻优先路径。整个过程的每一条结论,都能点回到一条具体的新闻标题。这就是「可解释」三个字的分量。
这套系统给学习 Python 的同学最大的启发,其实不在于它做了什么炫酷的 AI,而在于它的工程克制:
try/except 和 _fallback 保证服务在任何情况下都有结果可返回。bisect、mean、短路求值这些朴素工具,组合起来就能解决真实问题。把一个模糊的需求拆成「分类 → 分析 → 增强 → 验证」四个边界清晰、可单独测试、可单独降级的阶段——这种把复杂问题结构化的能力,比任何单一的库或算法都更值钱。建议刚入门的同学反复体会这套分层思路,它能用在你今后几乎所有的项目里。
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!
好文推荐
1. 用 Python 打造股票预测系统:Transformer 模型教程(一)
2. 用 Python 打造股票预测系统:Transformer 模型教程(二)
3. 用 Python 打造股票预测系统:Transformer 模型教程(三)
4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)
6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用
9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解
好书推荐