ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python股市情绪分析实战:从爬虫到情感指数与回测

Python股市情绪分析实战:从爬虫到情感指数与回测 简介面向股票市场研究爱好者与数据分析初学者的Python实战资源包聚焦利用互联网股评文本构建市场情绪指标并探索其与股市行情的关系。资源包含完整源码与配套数据覆盖数据读取、情感标注语料处理、模型训练、情绪指数计算及结果可视化等环节适合希望将自然语言处理用于量化投资分析的读者参考。压缩包共15个文件以Python脚本、CSV数据表、文本语料和结果图片为主其中4个py文件对应核心分析流程4个csv提供行情与模型得分数据整体49.31MB轻量易用。目前已有2000余人学习下载。通过源码与示例数据读者可复现“股评情感分析→构建指标→关联股市”的逻辑链理解非有效市场中投资者情绪的分析思路并在此基础上扩展自己的策略研究。1. 股市市场情绪分析为什么用Python能做出可落地的情绪指数盯盘盯久了你会发现很多股票的暴涨暴跌发生在新闻标题出现之前领先的是股吧、社交平台里的情绪变化。基于Python的股市市场情绪分析就是把散落的公开文本抓下来用NLP打成多空分数再聚合成一条随时间变化的市场情绪指数。它不是用来预测具体点位而是把盘面气氛这种黑匣子转成一条可回测、可观察的时间序列。这套方案适合三类人想给量化策略加另类数据的交易者想练python爬虫、python数据分析与可视化功底的开发者以及想验证消息面能不能影响收益的投资者。核心链路是采集、清洗、评分、聚合、回测。下文不空谈大模型直接用能跑的Python代码带你落地。2. 数据管道用Python爬虫把评论、行情和日历拉到一张表里做情绪分析首先要解决的不是模型而是数据源。如果把评论、价格和交易日历堆在三个地方后续每次分析都要手工对齐效率太低。我习惯先建一条数据管道让文本、行情和日历统一到一张按交易日组织的表里后面算情绪指数、算收益、回测都可以复用同一份数据。2.1 数据源怎么选文本源、行情源和日历源文本源我优先看股吧评论和财经新闻。股吧评论最贴近散户的真实情绪发帖时间字段比较规范缺点是噪音大包含广告、重复转发和无意义刷屏财经新闻表达相对克制情绪强度被磨平但胜在质量高、发布时间可信。行情源用公开的免费数据接口即可返回的日线数据里有date、open、close、high、low、volume足够算收益率。日历源不建议自己维护节假日表直接用行情数据里的date列生成交易日列表简单而且和回测区间完全一致。数据分类常见来源优点需要注意文本源股吧评论、财经新闻、雪球热帖直接反映多空情绪清洗难度大、发布时间需提取行情源免费数据接口或券商行情库提供价格和成交量注意限频、复权处理日历源交易所交易日历对齐时间序列避免节假日与停牌错位选择数据源有个铁律先问自己要研究多少只股票、每天需要多少样本。如果只做一两只票串行爬虫就够如果要做全市场就要考虑数据接口的限频和存储设计。我见过不少人一上来就想抓几千只股票的股吧评论结果爬虫被封、硬盘写满最后连一天干净数据都没有。第一步先把一只票的数据跑通再横向扩展。2.2 最小可用的评论抓取脚本从列表页到结构化DataFrame抓取方式不必一上来就用高并发框架requests加BeautifulSoup足够跑通全流程。下面这段代码抓一个讨论区列表页把标题、发布时间和链接提取出来。真实页面结构需要微调select里面的class名这很正常。import requests from bs4 import BeautifulSoup import time HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, } def fetch_page(url, paramsNone, retries3): for attempt in range(retries): try: resp requests.get(url, headersHEADERS, paramsparams, timeout10) if resp.status_code 200: resp.encoding utf-8 return resp.text except requests.RequestException as exc: print(f[{attempt 1}] 请求异常{exc}) time.sleep(2 ** attempt) return None def parse_thread(html): soup BeautifulSoup(html, html.parser) rows [] for item in soup.select(li.list-item): title item.select_one(a.title) pub_time item.select_one(span.time) if title and pub_time: rows.append({ ts: pub_time.get_text(stripTrue), title: title.get_text(stripTrue), href: title.get(href), }) return rows这段代码里的参数都值得说。timeout设成10秒避免某个链接卡住整个任务重试用指数退避第一次失败等1秒、第二次等2秒、第三次等4秒减少对目标站点的压力。User-Agent伪装成浏览器是为了避免最基础的请求拦截。parse_thread里的select的class必须按目标站点的实际HTML调整我第一次跑的时候就因为抄了一个过时的选择器解析出来全是空列表白白排查了半天。现在的讨论区页面很多已经改成接口返回JSON序列化后再渲染到页面上。遇到这种情况别硬啃HTML用浏览器开发者工具的网络面板去看真实请求地址然后用requests直接模拟解析JSON比解析HTML稳定得多。另外请求频率我一般控制在每秒1到2次加一个time.sleep(0.6)数据量小的研究任务完全够用。2.3 增量抓取与断点续爬用SQLite保存进度每天新增评论量不大但你必须每天定时补数据。用CSV文件做增量更新很痛苦我一般直接用SQLite它支持追加写入、去重查询、断点续爬而且不需要单独启动数据库服务。下面这段代码展示一条最简的落库逻辑import sqlite3 import pandas as pd import re DB_PATH sentiment.db def clean_text(raw: str) - str: text re.sub(r[^], , raw) text re.sub(rnbsp;, , text) text re.sub(r\s, , text) return text.strip() def save_comments(items): conn sqlite3.connect(DB_PATH) df pd.DataFrame(items) df[content] df[title].map(clean_text) df[created_at] pd.to_datetime(df[ts]) df.to_sql(comments, conn, if_existsappend, indexFalse) conn.commit() conn.close() def last_ts(): conn sqlite3.connect(DB_PATH) cur conn.execute(SELECT max(created_at) FROM comments) row cur.fetchone() conn.close() return row[0] if row and row[0] else 1970-01-01save_comments里的to_sql用的是append模式意味着重复执行会插入重复行。实际使用时我建议先执行一次去重把相同created_at和相同content的记录删掉。last_ts函数用于取上次抓到的最大时间抓取时只请求这个时间之后的数据这一步直接决定了断点续爬能不能工作。清洗这一步经常被低估。第一个坑是编码网页用gb2312而你用utf-8解析会看到一堆乱码第二个坑是Emoji表情Windows终端直接打印可能抛UnicodeEncodeError建议在清洗时统一替换成空字符串第三个坑是广告贴它们和股票情绪无关会稀释信号第四个坑是转发贴原始发布时间是转发时间而不是真正发帖时间字段里没有原文时间戳的话最好直接丢弃。把这些坑处理干净再进入情感打分后面的模型效果才会稳定。3. 从文本到分数情感模型选型与情绪指数合成数据落库后真正的重头戏是把每一条评论变成一个多空分数。这里的核心不是模型多复杂而是模型能不能理解中文金融文本的语境。因为是在A股场景里做情绪分析选型时要把通用NLP模型和金融领域特殊性放在一起考虑。3.1 为什么通用情感词典在股市里总是翻车第一版我做的是最常见的情感分析方案用SnowNLP给每条评论打一个0到1的分数再按天平均。回测时发现一个诡异现象指数大跌那天情绪分数反而很高。把样本打出来一看模型把“跌停”判成了积极情感。原因很简单这个通用模型训练语料以商品评论为主股票术语见得少遇到“跌停”这种没见过的词只能靠上下文瞎猜。后来我做了一组对比把通用情感模型、金融领域微调模型和“模型加词典规则”三种方案放在同一批股吧评论上跑结果差异非常明显。通用模型容易把“跌停”“爆雷”“商誉减值”打出正向高分金融领域微调模型准确率明显提升但遇到反讽和缩写还是会出错模型加词典规则最稳虽然看起来不酷但在实际交易信号里误判率最低。我把这个对比整理成一张选型表方案优点缺点适用阶段通用情感词典实现简单、零训练成本金融词误判高基线验证金融领域微调模型语义理解更准需要标注数据或下载模型正式研究模型加规则修正工程上最可控需要维护负面词表上线首选这里的结论是通用模型只能当基线不能直接信任。正式做情绪指数时至少要叠加一层领域规则做修正。3.2 用预训练中文情感模型给每条评论打分做中文情感分类最顺手的路径是transformers的pipeline接口它把分词、推理、结果解析全部封装好。推荐先把模型checkpoint下载到本地目录再加载本地路径避免每次运行都去拉权重。下面是一段可直接运行的打分函数from transformers import pipeline model_path ./fin_sentiment clf pipeline( sentiment-analysis, modelmodel_path, tokenizermodel_path, device-1, truncationTrue, max_length128 ) def polarity(text: str) - float: pred clf(text)[0] label pred[label].lower() if label in (positive, pos, 积极, 正面): return pred[score] return -pred[score]device参数很关键-1表示用CPU0表示用第一块GPU。几百条评论级别的研究任务用CPU完全够但如果做全市场监控建议用GPU并固定batch_size。max_length设成128既是控制推理速度也是避免过长的新闻通稿把模型输入撑爆。truncationTrue保证超长文本被阶段截断而不是报错。pipeline的label名称在不同模型里不一样可能是POSITIVE、LABEL_1或者“正面”所以代码里做了多关键词白名单匹配。polarity把模型输出的概率映射到[-1, 1]区间正向为正数负向为负数这样后面聚合的时候可以直接取平均。模型评分这一步没有太多玄学关键是记录好每条文本的分数和时间戳方便追溯。3.3 合成市场情绪指数按天聚合、滚动平滑和Z-Score单条评论的分数没有意义它必须在时间维度上聚合。最常见的聚合方式是按交易日取平均再计算短期平滑值和标准化值。这里的“短期”和“长期”窗口必须根据你的持仓周期来定不是越大越好。import pandas as pd def build_index(score_df: pd.DataFrame) - pd.DataFrame: score_df score_df.copy() score_df[trade_date] pd.to_datetime(score_df[trade_date]) daily score_df.groupby(trade_date).agg( scope(score, mean), volume(score, count) ).reset_index() daily[scope_sma5] daily[scope].rolling(5, min_periods3).mean() daily[scope_sma20] daily[scope].rolling(20, min_periods10).mean() daily[scope_z] ( (daily[scope] - daily[scope_sma20]) / daily[scope].rolling(20).std() ) return dailyscope是该交易日的平均情绪倾向volume是当天评论数可以当情绪权重看。scope_sma5用于捕捉短期情绪突变scope_sma20代表慢变量。scope_z是用20天均值和标准差做的标准化能识别偏离常态的极端情绪日这类日子往往是策略最关注的事件窗口。这里有个容易犯的错滚动平均千万不要用centerTrue居中对齐否则会用当天之后的数据去修正当天情绪造成未来函数。rolling窗口的min_periods参数控制样本不足时是否输出NaN比如刚上市的新股评论量少min_periods设成3能避免因为一两条评论就产生极端z值。如果你发现scope_z经常抖动不是因为方法错了而是评论样本太少这时候应该扩大股票池或者把窗口放宽。4. 从指数到策略相关性、反向信号与回测对齐情绪指数造出来后下一步是验证它到底有没有用。这一步很容易走偏很多人直接把情绪值画在K线图上肉眼觉得“底部吻合”就兴奋得不行。实际上肉眼看到的规律在样本外经常失效必须用统计方法先验证相关性再写一个没有未来函数的回测。4.1 先算IC和相关系数不要急着看收益判断一个因子有没有预测力最常用的指标是IC。所谓IC就是每个交易日的因子值与下一期收益的相关系数。IC的绝对值如果有0.03以上在A股已经算值得继续研究的弱有效因子。import pandas as pd def factor_ic(factor_df, price_df): data factor_df.merge(price_df, ontrade_date, howinner) data[ret_next] data[close].pct_change().shift(-1) valid data.dropna(subset[scope_z, ret_next]) ic valid[scope_z].corr(valid[ret_next]) rank_ic valid[scope_z].corr(valid[ret_next], methodspearman) print(fIC {ic:.4f}, RankIC {rank_ic:.4f}) return datapct_change得到当日收益率shift(-1)把它平移到前一个交易日这样今天的scope_z对应的就是明天的收益。merge用inner模式只保留两边都有的交易日避免节假日产生NaN。Pearson相关和Spearman相关我都会看因为金融数据里线性关系往往很弱单调关系反而更容易捕捉到。如果RankIC明显高于IC说明情绪因子和收益之间是非线性关系后面做信号时可以分位数切割而不是直接做线性回归。相关性低不代表因子没用它可能只在极端情绪日才生效。所以看到IC只有0.01时别急着删因子先做分层回测再说。4.2 情绪指数的反向择时极端恐慌是买入信号市场情绪在A股里经常是反向指标。散户在恐慌顶点容易交出筹码在情绪过热时又忍不住追高。因此这个zip方案里最常见的策略逻辑是情绪Z分数跌到极端低位时买入情绪回归到正常水平时离场。def generate_signals(daily: pd.DataFrame) - pd.DataFrame: df daily.copy() df[signal] 0 df.loc[df[scope_z] -1.5, signal] 1 df.loc[df[scope_z] 0.0, signal] 0 df[position] df[signal].replace(0, pd.NA).ffill().fillna(0) return dfsignal只在两个临界点变化scope_z低于-1.5时发出买入信号回到0以上时清仓。position通过ffill保存持仓状态也就是一旦买入中间不管情绪值怎么波动都继续持有直到回到0才离场。买入阈值定在-1.5离场定在0是为了捕捉恐慌情绪从极端恢复的完整阶段。这里有个设计取舍我没有加入“情绪过热做空”的信号。原因不是做空不赚钱而是很多交易者并不具备做空工具做空信号在连续逼空行情里也容易被反复打脸。如果你想加做空把止损条件设得比买入条件更严例如scope_z大于2.0才开空。阈值不能靠肉眼在样本上挑后续至少要做滚动窗口的参数检验否则很容易过拟合。4.3 回测代码的时序避坑信号生成与成交分离回测里最常见的翻车现场是信号和成交用同一个收盘价。情绪指数是收盘后综合当天评论算出来的再怎么快也要到第二天才能交易所以回测时必须让持仓信号滞后一天生效。def backtest_simple(daily, price_df, initial_cash1000000): merged daily[[trade_date, position]].merge( price_df, ontrade_date, howinner ) merged[ret] merged[close].pct_change() merged[strategy_ret] merged[position].shift(1) * merged[ret] merged[equity] initial_cash * (1 merged[strategy_ret]).cumprod() return mergedposition.shift(1)是这段代码里最重要的一个操作它保证今天收盘后的持仓状态只在明天的收益上生效。没有这个shift你的回测会在理论上拿到当天的完整涨幅实盘中根本做不到。另一个细节是佣金和印花税回测时每次交易至少要扣掉0.1%到0.2%的成本再加上滑点一个情绪因子的净收益可能从“看起来很香”变成“勉强打平”。我自己评估策略从来不看收益曲线的斜率而是看最大回撤和交易频率。情绪因子天然低频一年可能只有十几次交易样本少的时候夏普比率虚高看不出真实水平。所以下一步要做的不是调参数而是做显著性检验。5. 避坑数据采集、情感判断和回测中的五个血泪经验情绪分析这个方向看起来只有爬虫加NLP两件事但真正让它不靠谱的全是细节。我把这两年踩过的坑按“现象、原因、解决”写下来每一条都值得在代码里提前留个防护。5.1 评论数据断档导致指数断层先落库再考虑速度现象爬虫跑了半小时后突然拿不到新数据情绪指数在关键时间点出现空窗。原因单个IP频繁请求触发了站点风控后续请求全部被拒绝。解决把并发改成串行加随机延时抓到的每条评论先写SQLite不要攒在内存里等最后统一入库。如果数据量真的很大优先考虑目标站点是否提供官方API或数据合作渠道走正规通道比任何爬虫技巧都可靠。这个方向用来做研究的话每天抓几百条评论串行完全够用没必要冒风控风险。5.2 通用NLP模型把“跌停”判成积极现象模型在跌停日输出大量正向分数情绪指数和行情走势同涨同跌完全失去区分度。原因通用模型的训练语料以电商评论和社交文本为主没有学过“跌停、爆雷、立案调查”这类词的金融语境。解决在模型打分后加一层规则池。下面这段代码的思路是命中强负面词就直接翻转分数规则不需要很多几十个核心词就能覆盖大多数事故场景。NEG_WORDS [跌停, 爆雷, 立案调查, 退市, 商誉减值, 大幅减持] def rule_override(pred_score: float, text: str) - float: for word in NEG_WORDS: if word in text: return -abs(pred_score) return pred_score我把规则放在模型后面而不是前面因为模型对“业绩超预期”这类正向短语已经学得不错规则层只处理它最容易忽略的强负面词。abs(pred_score)保证结果一定是负值避免“跌停但概率很低”时模型分数本来就在0附近造成方向模糊。每一条负面词表都要有明确来源最好从历史事故公告里人工挑不要盲目堆词。5.3 用抓取时间代替发布时间造成未来函数现象回测收益曲线漂亮得吓人实盘却连续亏损。原因爬虫在下午抓到的评论被标记为当日信号但很多评论其实是昨天发帖后今天才被抓下来时间错位让策略偷看了未来。解决每条评论必须存储它页面原生的发布时间而不是抓取时间。做信号时统一取T-1日15点之后发布的评论把它们作为T日盘前的情绪。推理延迟也要考虑如果下午3点后才跑完全部评论最早只能等T1日再交易这个错位必须在回测代码里固定下来。5.4 情绪因子和收益相关性不显著先看IC值再谈优化现象辛辛苦苦合成了一整年的情绪指数Pearson相关只有0.012直接怀疑数据白抓了。原因单只股票每天的有效评论常常只有几十条噪声远大于信号情绪对收益的影响本身也不一定是线性的。解决把样本从一只票扩展到几十只再按情绪分数从低到高分成五组看最高组和最低组的年化收益差。如果分层单调性出不来说明这个数据源本身没有alpha此时换数据源比调参数重要不要硬拗模型。5.5 节假日和停牌导致时间序列错位先用交易日历对齐现象merge行情和情绪后出现大量NaN滚动均值在长假前产生缺口策略在长假空仓却显示盈利。原因A股交易日和自然日不同股票还会停牌直接用日期做merge会把非交易日也算进去。解决先由行情数据生成交易日序列再用reindex只保留交易日。停牌日的行情缺失不要用前值填充否则会生成不存在的涨跌幅。trade_dates price_df[trade_date].drop_duplicates().sort_values() daily daily.set_index(trade_date).reindex(trade_dates).reset_index()reindex之后缺失的情绪值我建议直接dropna而不是ffill。停牌期间的情绪本来就不该进入策略强行填充只会让回测看起来平滑实盘依然无法成交。6. 把情绪分数做成可上线的量化因子显著性、残差化和定时任务当情绪指数通过了初步IC验证下一步是把它当成标准的量化因子来打磨。这里有三件事我几乎每做一次都会遇到显著性检验、剔除市场贝塔、自动化定时更新。6.1 先过显著性检验IC值再高如果样本量太少也没用。我会把每天的IC算成一个序列再用t检验看它是否显著区别于0。from scipy import stats ic_series daily_ic[rank_ic].dropna() t_stat, p_value stats.ttest_1samp(ic_series, 0) print(ft{t_stat:.2f}, p{p_value:.4f})p值小于0.05绝对值t大于2才有资格进入下一步。否则不管曲线多好看样本外大概率打回原形。6.2 用残差剔除市场贝塔情绪因子很多时候只是大盘行情的影子真正独立于市场的情绪很少。我会把情绪值对大盘收益率做回归取残差作为新因子这样留下来的部分才更接近纯粹的情绪溢价。import statsmodels.api as sm X sm.add_constant(price_df[market_ret]) model sm.OLS(emotion_series, X).fit() emotion_resid model.resid残差化之后因子与市场整体走势的相关性会明显下降IC可能变弱但真实性更高。这一步在做多因子模型时尤其重要很多情绪因子单独看有效一放进组合就和市值因子高度重叠。6.3 生产化建议我现在的习惯是每天下午15点30分之后跑一次定时任务先把新增评论抓下来再跑情感打分更新情绪指数写回SQLite。如果连续三天抓取失败或情绪指数出现断档就发告警通知自己。阈值参数不固定每周滚动检查一次IC和分层收益如果不稳定就回退到保守参数。我做这个方向最大的教训是不该把情绪因子单独当救命稻草。它更像一个环境过滤器告诉你什么时候市场处于极度过热或恐慌。现在我依然会在恐慌信号出现时买一点但仓位永远不超过总资金的四分之一。这个方向值得做但请把预期调低它不会让你每天抓到涨停却能让你在恐慌的时候敢于出手。希望帮到你。本文还有配套的精品资源点击获取
返回列表