Python爬虫和百度AI情感分析,如何让股吧评论变成股价信号

发布时间:2026/10/23 23:12:05
Python爬虫和百度AI情感分析,如何让股吧评论变成股价信号

简介:面向金融数据分析、Python爬虫与自然语言处理方向的读者,这套资源把股票评论采集、百度AI情感标注、MATLAB统计建模串联成一个可复现的研究流程。压缩包共10个文件,类型覆盖py爬虫与情感分析脚本、m格式的MATLAB处理程序、csv原始评论、xlsx情感与日期结果、png可视化图表,整体仅1.71MB,轻量易用。已有6461人学习。通过包内代码,可以学会用requests和BeautifulSoup抓取东方财富评论,调用百度AI接口为评论文本打上情感标签,再借助MATLAB清洗数据,统计评论情绪与股票涨跌之间的关联,并用Excel图表呈现时间走势与情感分布,形成从数据抓取、语义分析到统计展示的完整链路。对想快速搭建评论—股价分析原型、掌握跨工具协作的数据分析初学者和金融量化爱好者,是一份能少走弯路的完整实操参考。

1. 一个把「股吧评论」变成「涨跌信号」的三段式管线

如果你做过股票相关的数据分析,大概率会撞上同一个困惑:网上铺天盖地的评论到底对股价有没有影响?有人用词频统计,有人用情绪打分,结果往往是一团乱麻,看着像有规律,跑出来全是噪声。这个项目的切入点非常直接——用 Python 爬虫批量抓取股票评论,调用百度AI的语义分析接口给每一条评论打上情感标签,最后把清洗后的数据丢给 MATLAB 做股价关联性计算。整个流程跑完,你能拿到一个可复现的结论:评论情绪在什么时间窗口内与涨跌相关性最强,哪些股票的特征更明显。它适合谁?适合手里已有交易数据、想验证「舆情因子」是否有效的开发者,也适合拿它当模板,把评论分析能力迁移到其他场景的人。这条链路里每一步都是成熟组件,真正的功夫在怎么把这些组件拼起来,以及怎么处理拼装过程中的各种坑。

2. 数据采集层:用 Python 爬虫把评论从网页变成结构化数据

2.1 数据源选择与接口探查

做股票评论爬虫,第一个要决策的不是代码怎么写,而是数据从哪拿。常见的数据源有东方财富股吧、雪球、新浪财经个股评论页,这个项目选择了东方财富股吧的公开页面,原因是它的评论数据通过一个半公开的接口返回 JSON,不需要处理复杂的 HTML 解析,而且每条评论自带发布时间、用户名、评论内容、点赞数等字段,省去了自己写字段提取逻辑的工作。

接口的 URL 构造是这个样子的:

http://gbapi.eastmoney.com/comment/list?appid=appid01&fields1=code&fields2=title,content,publishtime,likecount&sort=1&pageindex=1&pagesize=50&code=sh600000

这里有几个关键参数需要说明:sort=1 表示按发布时间倒序,pageindex 是页码,pagesize 是每页条数,这个项目里设置为 50,code 是股票代码,注意必须带交易所前缀,sh 表示上交所,sz 表示深交所。这个接口不需要登录,请求头里带上一个正常的 User-Agent 就能直接返回 JSON 数据,极大降低了爬虫的复杂度。

不过要提醒一点,这个接口不是官方文档里公开的,属于前端页面内部调用的数据接口,因此字段名有变化的可能。我在跑这个项目时遇到过一次fields2里的参数名变更,导致解析出来的时间字段全是空值,最后是抓取页面源码对比字段才发现的。应对办法很简单:写代码前先打开浏览器开发者工具,切到 Network 面板,刷新评论页找到那个返回 JSON 的请求,看一遍响应结构,再动手写解析逻辑,这能省掉后面大量的调试时间。

2.2 爬虫主流程设计

整个采集模块的思路是:先通过一个股票代码列表批量获取每只股票的历史评论,再逐页翻取,最后统一落盘保存。代码结构分为三层:请求层、解析层、存储层。请求层负责构造 URL 和伪装请求头,解析层负责提取评论内容及元信息,存储层把数据写入 CSV 文件,方便后续 MATLAB 读取。

import requests import pandas as pd import time from datetime import datetime def fetch_comments(stock_code, page=1, page_size=50): url = "http://gbapi.eastmoney.com/comment/list" params = { "appid": "appid01", "fields1": "code", "fields2": "title,content,publishtime,likecount", "sort": "1", "pageindex": page, "pagesize": page_size, "code": stock_code } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://guba.eastmoney.com/" } resp = requests.get(url, params=params, headers=headers, timeout=10) resp.raise_for_status() data = resp.json() return data.get("result", {}).get("list", []) def crawl_stock(stock_code, max_pages=20): all_records = [] for page in range(1, max_pages + 1): try: items = fetch_comments(stock_code, page=page) if not items: break for item in items: all_records.append({ "stock_code": stock_code, "title": item.get("title", ""), "content": item.get("content", ""), "publish_time": datetime.fromtimestamp(item.get("publishtime", 0)), "like_count": item.get("likecount", 0) }) print(f"[INFO] {stock_code} 第{page}页,累计{len(all_records)}条") time.sleep(1.5) # 控制请求频率,避免被限流 except Exception as e: print(f"[ERROR] 第{page}页失败: {e}") continue return pd.DataFrame(all_records) if __name__ == "__main__": stock_list = ["sh600000", "sz000001", "sh601318"] df_all = pd.DataFrame() for code in stock_list: df = crawl_stock(code, max_pages=30) df_all = pd.concat([df_all, df], ignore_index=True) df_all.to_csv("stock_comments.csv", index=False, encoding="utf-8-sig") print(f"[DONE] 总共采集 {len(df_all)} 条评论")

这段代码里有一个值得注意的细节:crawl_stock内部使用了try...except包裹单页请求,失败时不会中断整只股票的采集流程,而是跳过当前页继续下一页。这个设计不是偷懒,而是爬虫场景下非常实用的容错策略——网络抖动、请求超时、接口偶发报错都是常态,如果一遇到异常就退出,整个采集任务就毁了。实际的抓取任务往往跑在夜间无人值守的情况下,每只股票 30 页、每页 50 条,单只股票就是 1500 条评论,多只股票累计上万条,中途挂掉重跑的成本非常高。

存储格式选 CSV 有两个原因:一是 MATLAB 读取 CSV 是一行命令的事,不需要额外安装数据库驱动;二是这个项目的数据量在几万条规模,远没到需要上数据库的程度,用 CSV 反而更直观,出了问题可以用 Excel 直接打开排查。注意编码用了utf-8-sig,这是为了 Excel 打开时不乱码,MATLAB 的readtable也能正确识别。

2.3 字段清洗与质量校验

评论数据拿到手并不意味着可以马上进入语义分析,这里有个经常被忽视的步骤:数据清洗。从股吧采集的评论噪音非常大,主要问题包括:广告内容、无意义的短文本(比如只发一个表情或一个标点)、重复评论、以及内容为空但点赞数为0的无效记录。

清洗逻辑的核心是一句话:长度太短的内容没有语义分析价值,直接丢弃。百度AI的情感分析接口对纯表情、纯标点这类内容返回的置信度极低,留着只会稀释有效信号。

def clean_comments(df): # 去除内容为空或纯标点的记录 df = df[df["content"].str.len() > 5] # 去除重复内容,保留第一条 df = df.drop_duplicates(subset=["content"]) # 去除广告特征明显的评论:含"加V" "微信" "q" 等关键词 ad_keywords = ["加微信", "代操盘", "涨停板", "内部消息", "牛股"] for kw in ad_keywords: df = df[~df["content"].str.contains(kw, na=False)] return df.reset_index(drop=True)

这段清洗函数里用了三个过滤条件,背后的逻辑分别对应三类问题:短文本无分析价值、重复评论会拉偏情绪分布、广告会制造虚假的情绪信号。实际使用中你会发现,清洗比例通常在 15% 到 30% 之间,也就是说一万条原始评论里,最后能用的大概七千到八千条。这不是坏现象,反而说明数据质量是可控的。

清洗完成后,建议输出一份统计报告,包括每只股票的评论总数、时间跨度、平均评论长度,这份报告会在后续与股价数据关联时用来判断数据量是否足以支撑分析结论。如果一只股票只有两百条评论,那它计算出来的情绪指标几乎没有任何统计意义。

3. 语义分析层:百度AI接口的接入与批量处理策略

3.1 情感倾向分析接口的调用方式

评论数据准备好了,接下来是语义分析。这个项目选用的百度AI情感倾向分析接口,属于自然语言处理中的文本分类能力,每条文本会返回一个情感极性标签(0 表示消极、1 表示中性、2 表示积极)以及对应的置信度。接口本身不复杂,有效的用法才是关键。

调用前你需要先注册应用,在控制台创建应用后获取 API Key 和 Secret Key,然后换取访问令牌。这一步是固定的流程,之后每次调用接口都要带上这个令牌,令牌有效期大约一个月,过期后需要重新获取。

import requests import json import pandas as pd API_KEY = "你的API_Key" SECRET_KEY = "你的Secret_Key" def get_access_token(): url = "https://aip.baidubce.com/oauth/2.0/token" params = { "grant_type": "client_credentials", "client_id": API_KEY, "client_secret": SECRET_KEY } resp = requests.post(url, params=params) return resp.json().get("access_token") def sentiment_analysis(text, access_token): url = "https://aip.baidubce.com/rpc/2.0/nlp/v1/sentiment_classify" headers = {"Content-Type": "application/json"} payload = { "text": text[:200] # 接口限制单条文本长度,超过时截断 } params = {"access_token": access_token} resp = requests.post(url, headers=headers, params=params, data=json.dumps(payload)) result = resp.json() if "items" in result and result["items"]: return result["items"][0] return {"sentiment": 1, "confidence": 0.0, "positive_prob": 0.33, "negative_prob": 0.33} def batch_analyze(df, access_token): sentiments = [] for idx, row in df.iterrows(): result = sentiment_analysis(row["content"], access_token) sentiments.append({ "sentiment": result["sentiment"], "confidence": result["confidence"], "positive_prob": result["positive_prob"], "negative_prob": result["negative_prob"] }) if (idx + 1) % 100 == 0: print(f"[INFO] 已分析 {idx + 1} 条") df_result = df.copy() df_result["sentiment"] = [s["sentiment"] for s in sentiments] df_result["confidence"] = [s["confidence"] for s in sentiments] df_result["positive_prob"] = [s["positive_prob"] for s in sentiments] df_result["negative_prob"] = [s["negative_prob"] for s in sentiments] return df_result if __name__ == "__main__": token = get_access_token() df = pd.read_csv("stock_comments.csv", encoding="utf-8-sig") df = df.dropna(subset=["content"]) df_result = batch_analyze(df, token) df_result.to_csv("stock_comments_sentiment.csv", index=False, encoding="utf-8-sig")

这段代码有几个值得拆解的细节。首先是text[:200]这个截断逻辑,百度AI的情感分析接口对单条文本有长度上限,超出的部分会被截断或者直接报错,但评论内容通常不会那么长,截断只是防御性写法。其次是return result["items"][0]这行,接口返回的是一个列表,虽然目前只返回一个元素,但按列表结构解析是最稳妥的。最后是sentiment_analysis函数内部对异常返回的兜底:如果接口返回结构不符合预期,返回一个中性情感的默认值,避免整个批量任务中断。

3.2 批量调用的性能瓶颈与优化

如果把上面的代码直接跑一万条评论,你会遇到一个非常现实的问题:太慢了。每条评论都要发起一次 HTTP 请求,平均耗时在 0.5 到 1 秒之间,一万条就是一两个小时,而且这还是理想状态。实际跑起来会出现两类问题:一是请求频率过高触发接口限流,返回错误码;二是网络超时导致部分评论没有分析结果。

针对限流问题,百度AI的接口有 QPS 限制,默认每秒只能处理几个请求。常见的做法是在请求之间加入延时控制,下面的代码用了time.sleep(0.1)把请求频率控制在每 100 毫秒一个,这样一万条评论大约需要 15 到 20 分钟,虽然还是慢,但至少稳定。

# 批量分析时的限速控制 import time def batch_analyze_rate_limited(df, access_token): results = [] for idx, row in df.iterrows(): retry = 0 while retry < 3: # 最多重试3次 try: result = sentiment_analysis(row["content"], access_token) results.append(result) break except Exception as e: retry += 1 if retry >= 3: print(f"[ERROR] 第{idx}条失败: {e}") results.append({"sentiment": 1, "confidence": 0.0}) time.sleep(2) # 失败后等待更长时间再重试 time.sleep(0.1) # 控制请求频率 if (idx + 1) % 500 == 0: print(f"[INFO] 进度:{idx + 1}/{len(df)}") return pd.DataFrame(results)

这段代码把原来的循环重构成了带重试机制的版本,核心变化是内层while retry < 3的循环。它的作用是:当单条请求失败时不会直接跳过,而是重试最多三次,每次失败后等待两秒再重试,给了网络恢复的时间。这个机制让我在跑大批量数据时免去了反复人工重启的痛苦。

实际项目里我还建议把分析结果每 1000 条增量保存一次到磁盘,这样即使中途程序崩了,已经分析完的结果不会丢,重启后把已分析的评论过滤掉继续跑就行。这个习惯在长时间运行的脚本里价值极大。

3.3 情绪聚合与时间窗口对应

语义分析的结果还是逐条的,要把它变成能与股价走势对齐的信号,需要做聚合处理。聚合的核心逻辑是把分散在时间轴上的评论情绪按一定粒度聚合成情绪指标,这个粒度就是后续分析的时间窗口。

最常用的聚合方式是按天聚合。把某只股票一天内的所有评论情绪值加权平均,权重的选择有两种流派:一种是等权平均,简单直接;另一种是按点赞数加权,认为高赞评论更有影响力。这个项目提供参数让你切换,默认使用按点赞数加权的方式,因为真实场景中一条获得大量点赞的评论对市场情绪的影响远超普通评论。

def aggregate_daily(df): df["date"] = pd.to_datetime(df["publish_time"]).dt.date grouped = df.groupby(["stock_code", "date"]).apply( lambda x: pd.Series({ "sentiment_mean": (x["sentiment"].astype(float) * (x["like_count"] + 1)).sum() / (x["like_count"] + 1).sum(), "positive_ratio": (x["sentiment"] == 2).mean(), "negative_ratio": (x["sentiment"] == 0).mean(), "comment_count": len(x) }) ).reset_index() return grouped

聚合结果是一个包含日期、个股代码、情感均值、积极比例、消极比例、评论数量的数据表。其中sentiment_mean的范围在 0 到 2 之间,1 表示中性,大于 1 偏积极,小于 1 偏消极。这个指标就是后续 MATLAB 分析时与涨跌幅做相关的核心变量。

4. MATLAB 数据处理层:从 CSV 到相关性结论

4.1 数据导入与时间对齐

到了 MATLAB 这一步,数据源就变成了两个文件:stock_comments_sentiment.csv(评论情绪聚合结果)和股价历史数据(用 MATLAB 的readtable从 CSV 或者直接从 MATLAB 内置的财经工具箱接口拉取)。这里有一个关键问题:评论数据的时间粒度是天,股价数据也是天,但两者可能存在交易日和非交易日的差异。

评论在周六周日是有人发的,但股市不开盘,所以这些评论按天聚出来后没有对应的股价数据。处理方式是直接过滤掉非交易日的评论数据,或者把周五和周末的评论合并到周一。这个项目采用的策略是简单过滤,因为周末评论量本身就小,直接剔除对结果影响不大。

% 导入评论情绪数据 sentiment_data = readtable('stock_comments_sentiment.csv'); % 导入股价数据,假设csv中包含日期和收盘价字段 price_data = readtable('stock_prices.csv'); % 转换日期格式 sentiment_data.date = datetime(sentiment_data.date, 'InputFormat', 'yyyy-MM-dd'); price_data.date = datetime(price_data.date, 'InputFormat', 'yyyy-MM-dd'); % 按日期内连接,对齐到交易日 merged_data = innerjoin(sentiment_data, price_data, 'Keys', 'date'); % 过滤没有评论的交易日 merged_data = merged_data(~isnan(merged_data.sentiment_mean), :);

innerjoin在这里非常关键,它的功能是取两个数据表的交集,只保留两边同时存在的日期。这能避免一种尴尬情况:你拿着某一天的评论情绪去对比当天的股价数据,结果那天是周末,股价是上一个交易日的,对不上。

4.2 相关性与领先滞后分析

核心分析目标很明确:验证评论情绪和涨跌之间到底有没有关系,并且找出这种关系在时间上的方向性——评论是领先于股价变动,还是同步,还是滞后。领先意味着评论能作为预测因子,同步意味着只能做解释,滞后则说明评论只是股价的影子和回声。

% 计算情绪均值与涨跌幅的皮尔逊相关系数 returns = [0; diff(log(price_data.close))]; % 计算对数收益率 sentiment_mean = merged_data.sentiment_mean; % 对齐长度 min_len = min(length(returns), length(sentiment_mean)); returns = returns(1:min_len); sentiment_mean = sentiment_mean(1:min_len); % 同步相关性 [R_sync, P_sync] = corrcoef(sentiment_mean, returns); % 计算领先相关:用前一天的评论情绪对应当天的涨跌幅 sentiment_lag1 = sentiment_mean(1:end-1); returns_lag1 = returns(2:end); [R_lag1, P_lag1] = corrcoef(sentiment_lag1, returns_lag1); % 输出结果 fprintf('同步相关系数:%.4f, p值:%.4f\n', R_sync(1,2), P_sync(1,2)); fprintf('领先1天相关系数:%.4f, p值:%.4f\n', R_lag1(1,2), P_lag1(1,2));

这段代码的核心思想是滑动时间窗对齐。同步相关就是把同一日的情绪和同一日的涨跌幅对应起来;领先1天相关则是把前一天的评论情绪和当天的涨跌幅对齐。如果领先1天的相关系数显著大于同步相关系数,说明评论情绪确实具有一定的先行指标特征。

注意,这里有个常见的统计陷阱:股价数据不是独立样本,它带有自相关性,所以对照组必须做随机化检验。怎么检验呢?把评论情绪的时间序列随机打乱 1000 次,再重新与涨跌幅计算相关,得到相关性的随机分布。如果原始相关性落在随机分布的 95% 置信区间之外,那才说明结果不是碰巧出来的。

% 随机置换检验 num_perm = 1000; perm_corr = zeros(num_perm, 1); for i = 1:num_perm perm_order = randperm(length(sentiment_lag1)); perm_corr(i) = corr(sentiment_lag1(perm_order)', returns_lag1'); end p_value = mean(abs(perm_corr) >= abs(R_lag1(1,2))); fprintf('置换检验p值:%.4f\n', p_value);

置换检验的写法很简单,但它的意义重大。你算出来的相关系数可能只有 0.1 左右,看着不显著,但如果置换检验显示这样的相关系数在随机情况下出现的概率低于 5%,那它依然是有效的信号。反之,如果你算出来一个 0.5 的相关系数,但置换检验说不显著,那大概率是数据量太少或者样本中有几个异常点拉高了相关系数,要回到数据检查。

4.3 结果的可视化输出

分析结果光有数值还不够,可视化能让人一眼看出情绪与涨跌是否存在肉眼可见的联动。MATLAB 在可视化方面几乎是零成本就能出图。

% 绘制情绪均值与收盘价的双轴图 figure; yyaxis left plot(merged_data.date, merged_data.sentiment_mean, 'b-', 'LineWidth', 1.5); ylabel('情感均值'); yyaxis right plot(merged_data.date, merged_data.close, 'r-', 'LineWidth', 1.5); ylabel('收盘价'); xlabel('日期'); legend({'情感均值', '收盘价'}); saveas(gcf, 'sentiment_vs_price.png');

这张图能直观地展示一个现象:情感均值的波动是否与股价走势存在对应关系。值得注意的是,看了图之后不要急着下结论,视觉上的「看起来有关系」往往经不住统计检验,这也是我在这个项目里学到的血泪经验——有一组数据画出来曲线几乎同步,相关系数只有 0.03,完全经不起检验。视觉判断是真的会骗人的。

5. 避坑指南:跑通这条链路必须知道的 5 个问题

这个项目涉及的环节多,每个环节都有典型的坑。我把实际运行中撞过的几个问题列出来,虽然看起来零碎,但每一条都让人浪费过至少半天时间。

问题1:爬虫跑着跑着突然所有请求都返回 403

原因:请求频率过高,目标服务器的反爬机制把 IP 临时封禁了。现象不是立刻发生,而是累计请求到一定数量后突然失效。

解决:请求之间加入固定延时随机化,不要用固定值。把time.sleep(1.5)改成time.sleep(random.uniform(1.0, 2.0))。另外,设置请求失败后自动暂停 60 秒再继续,给封禁时间窗口一个自然降温的机会。

问题2:百度AI接口返回error_code=18,提示 QPS 超限

原因:并发请求数超过接口上限,免费配额通常只有每秒 2 个请求。如果你在代码里用了多线程加速,撞限流的概率会急剧放大。

解决:放弃多线程,回到单线程加固定 sleep。如果实在要加速,去控制台申请提高 QPS 配额。对于这个项目的数据量来说,单线程完全能接受,没必要冒险。

问题3:MATLAB 读 CSV 后,日期列变成一串数字而不是日期格式

原因:MATLAB 的readtable默认把日期列按文本读取,而 CSV 里如果是2024-01-15这样的格式,它会被识别成字符串,转换成datetime时如果不指定InputFormat,解析就会出问题。

解决:readtable读取时使用setvaropts指定日期列的格式,或者在读取后用datetime(string_column, 'InputFormat', 'yyyy-MM-dd')显式转换。这里的教训是:CSV 导出时日期格式一定要统一,最好用 ISO 格式%Y-%m-%d,避免出现 1 月 2 日被解析成 1 月 2 日还是 2 月 1 日的混乱。

问题4:情绪均值几乎恒等于 1,看不出任何波动

原因:数据中包含大量中性评论,也就是情感分析返回 sentiment=1 的文本充塞了样本。股吧里大量评论是「打卡」「路过」「学习一下」这类无实质内容的短文本,它们清洗时因为长度超过阈值而存活下来,拉平了整体情绪值。

解决:在清洗阶段增加一个过滤条件,剔除含「打卡」「路过」「签到」等典型中性词的评论,或者更简单粗暴的将清洗长度阈值从 5 提到 10。实话说,纯中性评论挤占样本是常见问题,过滤后的情绪分布会更有区分度。

问题5:领先相关性分析结果不稳定,换一只股票结果就大变

原因:单只股票的评论数量太少,计算出的情绪均值标准差大,导致相关系数置信区间过宽。这是小样本的天然问题,不是代码写错了。

解决:设定最低评论量阈值。在聚合阶段,过滤掉当日评论数少于 5 条的数据点,这类数据点不可靠。同时,分析结果按板块或行业合并计算,行业层面的情绪信号通常比单只股票稳定得多。

6. 二次开发:把相关性变成可用的回调信号

如果你不满足于「算出一个相关系数」,那这套管线最自然的升级方向,是把这个相关性结果转化成一个可回测的交易信号。

具体来说,需要做三件事:构建一个情绪打分指标、定义信号生成规则、做历史回测统计胜率和盈亏比。

先看情绪打分指标怎么升级。前面聚合得到的是一个 0 到 2 之间的均值,这个数值不能直接用,因为不同股票的情绪分布尺度不一样。有的股票评论区一直偏乐观,均值长期在 1.4 附近;有的股票评论区冷清,情绪均值在 0.9 到 1.1 之间波动。直接拿原始均值做横向比较没有意义,需要做标准化。标准化方式就看给定股票近 N 日情绪均值的 z-score:

# 将情绪均值转为z-score信号 import numpy as np import pandas as pd def add_zscore_signal(df, window=20): df = df.sort_values(["stock_code", "date"]).reset_index(drop=True) # 按股票分组计算滚动均值与标准差 grouped = df.groupby("stock_code")["sentiment_mean"] df["ma_sentiment"] = grouped.transform(lambda x: x.rolling(window, min_periods=5).mean()) df["std_sentiment"] = grouped.transform(lambda x: x.rolling(window, min_periods=5).std()) df["zscore"] = (df["sentiment_mean"] - df["ma_sentiment"]) / df["std_sentiment"].replace(0, np.nan) return df.dropna(subset=["zscore"])

z-score 大于 1.5 表示情绪显著偏高,小于 -1.5 表示情绪显著偏低。信号生成规则可以是:Z 值从低于 -1 回升穿过 -0.5 时发出买入信号,Z 值从高于 1 回落穿过 0.5 时发出卖出信号。这本质上是均值回归的思路,它的隐含假设是情绪极端化后会向均值回归,而回归的方向会被股价走势提前兑现。

回测时,把信号与股价数据对齐,计算每次信号发出后未来 1 天、3 天、5 天的累计收益。回测代码的骨架是这样的:

def backtest_signal(df, price_data, hold_days=3): results = [] for idx, row in df.iterrows(): if row["signal"] == 0: continue # 找到对应股票的后续股价 future_prices = price_data[ (price_data["stock_code"] == row["stock_code"]) & (price_data["date"] > row["date"]) ].sort_values("date") future_prices = future_prices.iloc[:hold_days] if len(future_prices) < hold_days: continue entry_price = future_prices.iloc[0]["close"] exit_price = future_prices.iloc[-1]["close"] ret = (exit_price - entry_price) / entry_price results.append({ "signal_date": row["date"], "stock_code": row["stock_code"], "signal": row["signal"], "return": ret }) result_df = pd.DataFrame(results) return result_df

回测结果输出后,看一眼两个核心指标:胜率(收益大于 0 的信号占比)和平均收益(所有信号收益的均值)。如果胜率在 60% 以上、平均收益为正值且经过了足够多的信号样本量验证,那这套舆情因子就有实际参考价值了。

回过头来复盘整个项目时,我认为它最有价值的不是代码本身,而是那条完整的数据处理链路:从公开网页到爬虫,从爬虫到结构化数据,从结构化数据到 AI 语义分析,从语义分析到统计验证,每一步的决策都决定了最终结论的可信度。从那以后我每做一个类似的数据分析项目,都会强制走一遍「数据质量检查 → 指标构建 → 随机性检验 → 可视化确认」这套流程,不跳过任何一环,因为跳过的环节会在最后用最难看的方式报复你。希望帮到你。

本文还有配套的精品资源,点击获取