Python量化投资:基于市场情绪指标的行业轮动策略构建与回测
2026/8/5 13:45:53 网站建设 项目流程

你有没有过这样的经历:看着A股四千多只股票、几十个行业板块,每天消息满天飞,却不知道资金到底在往哪里流?是追着新闻跑,还是跟着感觉走?或者,干脆把选择权交给那些听起来高大上却看不懂的“神秘指标”?

更让人头疼的是,很多策略听起来头头是道,但一说到具体怎么算、数据从哪来、代码怎么写,就变得语焉不详。结果就是,你兴冲冲地跟着一个“神奇公式”操作,最后发现要么数据对不上,要么根本跑不通,白白浪费了时间和精力。

今天要聊的,不是又一个“必胜秘籍”,而是一套完全透明、可复现的行业轮动观察框架。它源于一份公开的研究思路——用五个可量化的市场情绪指标,来刻画资金的“温度”和“流向”,试图找出每一阶段市场共识最强的行业。最关键的是,我们将把构建这五个指标、进行十年历史回测的全套Python代码和逻辑彻底公开。你不仅能看懂结论,更能亲手运行、验证、甚至改进它。

这套方法的价值不在于提供一个“圣杯”,而在于提供一个扎实的、工程化的分析起点。它把模糊的“市场情绪”变成了可计算的数字,把主观的“行业强弱”变成了可回溯的曲线。你会发现,投资的很多困惑,不是因为缺乏“内幕消息”,而是缺少一套把市场噪音转化为结构化信息的工具。

1. 为什么是“情绪指标”?超越基本面与技术面的第三维度

在讨论具体指标之前,我们必须先回答一个根本问题:为什么要在基本面分析(公司业绩、行业景气度)和技术分析(价格、成交量、形态)之外,额外关注“情绪指标”?

答案在于市场的短期驱动机制。基本面决定长期价值,但无法解释日度甚至周度的剧烈波动;技术面描述价格轨迹,但常常是情绪的“果”而非“因”。市场在多数时候,尤其是在趋势形成初期和末期,是由投资者的集体情绪——贪婪、恐惧、从众、过度乐观或悲观——所驱动的。这种情绪会直接反映在资金的行为上:是追涨还是杀跌?是集中涌入某个板块还是恐慌性撤离?

因此,情绪指标的本质,是试图量化资金行为的“强度”和“一致性”。它不预测公司的未来盈利,而是监测当下市场参与者的投票情况。当大量资金以强烈的共识涌入某个行业时,这个行业在短期内获得超额收益的概率就会显著增加。我们的目标,就是设计一套规则,去识别这种“共识强度”最高的时刻和方向。

基于这个逻辑,我们选取了五个维度来构建情绪指标体系。它们分别从价格强度、资金流向、波动特征和趋势动能等角度,捕捉市场的非理性冲动。

1.1 五个核心情绪指标的设计逻辑

这五个指标并非随意拼凑,而是各有侧重,共同编织出一张观察市场情绪的网。

  1. 收益率排名(Return Rank):这是最直接的情绪体现。我们不是简单看涨跌幅,而是计算一个行业指数相对于全市场所有行业指数的收益率百分位排名。比如,排名90%意味着它跑赢了90%的同行。高排名不仅代表上涨,更代表它在同期“比别的行业涨得更好”,这是一种相对强度的确认。

  2. 成交额占比变化(Turnover Change):钱在哪里,情绪就在哪里。我们计算一个行业成交额占全市场总成交额的比重,并观察这个比重的变化率。份额的快速提升,往往意味着资金正在加速涌入,市场关注度在急剧升温,这通常是行情启动或加速的重要信号。

  3. 振幅收窄(Volatility Contraction):这是一个反直觉但非常重要的指标。在趋势启动前,市场经常会出现一段时间的“沉寂”——价格波动幅度(日内高点与低点之差)持续收窄。这可以理解为多空力量在某个区间内达到短暂平衡,为后续的方向性突破积蓄能量。监测振幅从收窄到突然放大的过程,有助于捕捉突破临界点。

  4. 趋势强度(Trend Strength):我们使用经过平滑处理的动量指标(如一定周期内的收益率),来衡量趋势的稳健程度。强劲且稳定的正向动量,表明上涨并非一日游,而是得到了持续的买盘支撑,情绪正在向乐观一侧持续强化。

  5. 资金流向一致性(Flow Consistency):这个指标稍微复杂一些,它通过分析日内价格与成交量的关系(例如类似“资金流”的算法),判断推动价格上涨的成交量中,有多大比例是主动性买盘。高一致性意味着上涨是由真实的、持续的买入需求推动的,而非少数大单或尾盘偷袭所致,这代表了更扎实的情绪基础。

将这五个指标标准化后等权相加,就得到了一个行业的“综合情绪分数”。分数越高,代表该行业在多个维度上同时表现出强烈的乐观情绪和资金共识。

2. 从理论到数据:如何用Python构建可计算的指标

思路清晰了,下一步就是工程实现。空谈指标毫无意义,我们必须能计算出真实、连续的历史数据。这里会涉及数据获取、指标计算、回测框架三个核心部分。我们将使用AKShare这个强大的开源财经数据接口库来获取基础数据。

2.1 数据地基:获取可靠的行业指数数据

行业分析的基础是选择一套具有代表性、历史数据完整的行业指数。常见的分类有申万一级行业、中信一级行业等。这里以申万一级行业(2021版)为例,共31个行业。

首先,我们需要获取这些行业指数的日线行情数据,包括开盘价、收盘价、最高价、最低价、成交额。

import akshare as ak import pandas as pd import numpy as np from datetime import datetime, timedelta # 定义回测时间段 start_date = '2014-01-01' end_date = '2023-12-31' # 申万一级行业列表 (示例,实际名称需根据AKShare接口确认) # 注意:AKShare的行业指数接口可能随时间变化,以下为示例代码逻辑 industry_list = ['801010', '801020', '801030', ...] # 行业代码 industry_names = ['农林牧渔', '煤炭', '有色金属', ...] # 行业名称 all_industry_data = {} for code, name in zip(industry_list, industry_names): try: # 使用AKShare获取行业指数日线数据 df = ak.sw_index_daily(indicator=code, start_date=start_date, end_date=end_date) # 规范列名 df.rename(columns={'日期':'date', '开盘价':'open', '收盘价':'close', '最高价':'high', '最低价':'low', '成交额':'volume'}, inplace=True) df['date'] = pd.to_datetime(df['date']) df.set_index('date', inplace=True) df['industry_code'] = code df['industry_name'] = name all_industry_data[name] = df[['open', 'high', 'low', 'close', 'volume']] print(f"已获取 {name} 数据,长度:{len(df)}") except Exception as e: print(f"获取 {name}({code}) 数据失败: {e}") # 将所有行业数据按时间索引对齐,填充缺失值(前向填充) panel_data = pd.concat(all_industry_data, axis=1, join='outer') panel_data = panel_data.sort_index().ffill() # 按日期排序并前向填充

关键点:数据质量是回测的生命线。务必检查数据是否连续,有无异常值(如价格为零、成交额突变),并对停牌等导致的缺失值进行合理处理(如前向填充)。对齐所有行业的时间索引,是后续进行横截面比较的基础。

2.2 指标工程:将五个逻辑转化为Python代码

有了基础数据,我们就可以逐一实现那五个情绪指标。计算过程涉及大量的横截面(同一时间点不同行业间)比较和时间序列滚动计算。

def calculate_emotion_indicators(panel_data, lookback_period=20): """ 计算五个情绪指标 panel_data: MultiIndex DataFrame, 第一层是行业名,第二层是OHLCV等字段 lookback_period: 部分指标(如趋势强度)的计算周期 """ results = {} # 准备一个空的DataFrame来存储每个行业的每日指标 emotion_df = pd.DataFrame(index=panel_data.index.levels[0]) # 索引为日期 for industry in panel_data.columns.levels[0]: df = panel_data[industry] # 1. 收益率排名 (使用过去5日收益率) df['return_5d'] = df['close'].pct_change(5) # 每日横截面排名:计算每个交易日,该行业收益率在所有行业中的百分位 # 这里需要在循环外进行横截面计算,稍后统一处理 # 2. 成交额占比变化 # 先计算全市场每日总成交额 # total_volume = panel_data.xs('volume', axis=1, level=1).sum(axis=1) # 该行业成交额占比 # df['volume_share'] = df['volume'] / total_volume # 占比的5日变化率 # df['volume_share_chg'] = df['volume_share'].pct_change(5) # 3. 振幅收窄 (过去10日振幅的标准差 / 均值,比值下降表示收窄) df['daily_range'] = (df['high'] - df['low']) / df['close'].shift(1) # 日内振幅率 df['range_ma'] = df['daily_range'].rolling(window=10).mean() df['range_std'] = df['daily_range'].rolling(window=10).std() df['volatility_ratio'] = df['range_std'] / df['range_ma'] # 比值小则波动收敛 # 4. 趋势强度 (使用价格在20日均线之上的幅度与稳定性) df['ma20'] = df['close'].rolling(window=20).mean() df['trend_strength'] = (df['close'] - df['ma20']) / df['ma20'] # 价格偏离均线的百分比 # 可以进一步用过去5日trend_strength的均值或斜率来平滑 # 5. 资金流向一致性 (简易版:价涨量增为正,价跌量增为负,累计) df['price_chg'] = df['close'].pct_change() df['volume_chg'] = df['volume'].pct_change() # 定义一个简单的流向:价格变化与成交量变化符号一致时,强度大 df['flow_raw'] = df['price_chg'] * df['volume_chg'] df['flow_consistency'] = df['flow_raw'].rolling(window=5).mean() # 5日平滑 results[industry] = df[['return_5d', 'volatility_ratio', 'trend_strength', 'flow_consistency']] # 横截面排名计算 (以收益率排名为例) # 将所有行业的‘return_5d’提取出来,形成一个日期*行业的DataFrame return_cross = pd.DataFrame({ind: results[ind]['return_5d'] for ind in results}) # 计算每日的百分位排名 return_rank = return_cross.rank(axis=1, pct=True) # 将排名等信息合并回每个行业的数据框 for industry in results: results[industry]['return_rank'] = return_rank[industry] # 同理,可以计算其他需要横截面比较的指标... # 计算综合情绪分数 (假设五个指标已准备好,这里仅为示例) # 先对每个指标进行归一化(z-score或分位数归一化) # emotion_score = (return_rank + volume_share_chg_norm + ... ) / 5 # results[industry]['emotion_score'] = emotion_score return results

关键点

  • 横截面计算:像收益率排名这类指标,必须在每个交易日对所有行业进行计算,才能体现“相对强弱”。
  • 周期选择:计算指标所用的时间窗口(如5日、10日、20日)需要根据市场节奏调整。周期太短噪音大,太长则滞后严重。
  • 归一化:不同指标的量纲和分布不同(如百分比变化和比值),在合成综合分数前必须进行标准化处理(如转换为每日的百分位排名或Z-Score),否则量级大的指标会主导结果。

2.3 回测引擎:验证策略的历史表现

计算出每日的情绪分数后,我们就可以构建策略了。一个简单的策略是:每周(或每日)收盘后,计算所有行业的综合情绪分数,买入排名前N(如前三)的行业,持有固定周期后调仓。

def backtest_emotion_strategy(emotion_data_dict, top_n=3, hold_period=5, initial_capital=1000000): """ 情绪策略回测 emotion_data_dict: 字典,key为行业名,value为包含‘emotion_score’等指标的DataFrame top_n: 每期买入排名前几的行业 hold_period: 持有期(交易日) """ all_dates = sorted(list(emotion_data_dict.values())[0].index) # 获取所有交易日 rebalance_dates = all_dates[::hold_period] # 按持有期调仓 portfolio = {} # 记录当前持仓 {行业: 买入日期} capital = initial_capital equity_curve = [] # 资金曲线 trade_log = [] # 交易记录 for i, current_date in enumerate(all_dates): # 调仓日逻辑 if current_date in rebalance_dates: # 清空现有持仓(假设调仓日全部卖出) if portfolio: for industry in list(portfolio.keys()): # 计算卖出收益(使用当日收盘价) # ... 简化处理,这里需要价格数据 trade_log.append({'date':current_date, 'action':'sell', 'industry':industry}) portfolio.clear() # 选择新标的:获取当前日期所有行业的情绪分数 scores_today = {} for industry, df in emotion_data_dict.items(): if current_date in df.index: scores_today[industry] = df.loc[current_date, 'emotion_score'] # 选出分数最高的top_n个行业 selected = sorted(scores_today.items(), key=lambda x: x[1], reverse=True)[:top_n] # 等权重买入 capital_per_industry = capital / len(selected) for industry, score in selected: portfolio[industry] = {'buy_date': current_date, 'buy_capital': capital_per_industry} trade_log.append({'date':current_date, 'action':'buy', 'industry':industry, 'score':score}) # 每日更新组合市值(简化:假设价格使用收盘价,需从原始数据获取) # total_value = capital + sum(持仓市值) # equity_curve.append((current_date, total_value)) # 计算回测指标:年化收益率、夏普比率、最大回撤等 # equity_series = pd.Series([v for d, v in equity_curve], index=[d for d, v in equity_curve]) # ... 计算指标代码 ... return { 'equity_curve': equity_curve, 'trade_log': trade_log, # 'performance_metrics': metrics }

关键点

  • 回测假设:这是一个高度简化的回测,忽略了交易成本(佣金、印花税)、冲击成本、以及买入卖出是否能在收盘价成交等问题。严谨的回测必须考虑这些因素。
  • 幸存者偏差:我们使用的是当前存在的行业指数回溯历史,这忽略了历史上已消失或重组的行业,可能高估历史表现。
  • 过拟合风险:五个指标、它们的权重、计算周期、调仓周期、Top N数量等都是可以优化的参数。但过度优化(在历史数据上寻找最佳参数组合)会导致策略在未来失效。必须使用样本外数据测试或采用稳健的参数。

3. 十年回测结果揭示了什么?数据背后的洞察与陷阱

运行上述回测框架(经过完整实现和数据清洗后),我们可能会得到一条从2014年到2023年的净值曲线。这里我们讨论一般性的发现,而非具体的收益数字(因为数据源、参数和处理细节的差异会导致结果不同)。

3.1 可能观察到的模式

  1. 阶段性有效:情绪策略在趋势明朗、主线清晰的牛市或结构性行情中(如2014-2015上半年,2019-2020的消费科技行情,2021年的新能源行情),往往能快速捕捉到领涨行业,表现突出。这是因为强烈的赚钱效应会吸引资金持续涌入,形成正反馈,情绪指标能很好地度量这种“热度”。

  2. 震荡市钝化:在窄幅震荡、轮动快速的“电风扇”行情中,策略可能频繁换仓,导致交易成本侵蚀利润,甚至左右挨打。因为情绪指标捕捉的是“共识”,而震荡市中缺乏持续共识。

  3. 极端行情下的风险:在市场恐慌性下跌的初期(如2015年下半年,2018年),情绪指标最高的行业可能并非避风港,反而是之前涨幅过大、情绪最亢奋的板块,补跌风险巨大。这说明情绪指标是动量指标,而非反转指标,它不预测趋势的终结。

3.2 必须警惕的回测陷阱

看到一条漂亮的净值曲线时,务必保持冷静,检查以下陷阱:

  • 前视偏差(Look-ahead Bias):这是最致命的错误。例如,使用了当日收盘价计算指标,却在当日开盘就交易。在实际中,你只能在收盘后获得全部数据。我们的代码必须严格使用shift(1)等方式,确保交易决策基于历史信息。
  • 数据窥探(Data Snooping):如果根据十年全样本数据反复调整参数直到曲线完美,那么这个策略几乎肯定在未来失效。正确的做法是将数据分为训练集(如2014-2019)和测试集(2020-2023),在训练集上确定逻辑和大致参数范围,在测试集上验证。
  • 交易成本:忽略成本的回测是“纸上富贵”。A股买卖双边佣金加印花税,频繁调仓(如每周)的成本不容小觑。一个年化收益20%的策略,如果周度调仓,交易成本可能轻易吃掉5%以上。
  • 流动性问题:策略若应用于行业指数ETF,需检查历史成交量是否支持即时买卖。若应用于一篮子个股,冲击成本更大。

注意:公开回测结果的目的绝不是为了展示一个“高收益策略”,而是为了完整演示从数据获取、指标构建到回测验证的全过程方法论。任何未经严格样本外检验和实盘模拟的策略,都不应直接用于真实投资。

4. 从研究到实践:如何将情绪指标融入你的决策系统

一套完整的分析框架,其价值远不止于生成一个买卖信号。更重要的是,它为你提供了一个结构化的市场观察工具。以下是如何将其工程化、实用化的建议。

4.1 定位:辅助工具,而非圣杯

首先必须明确,情绪指标系统应该作为你决策系统的辅助模块,而不是唯一依据。它的核心作用是:

  • 扫描与预警:每日/每周帮你快速扫描全行业,找出资金情绪最亢奋或最低迷的板块,节省手动翻阅的时间。
  • 确认与过滤:当你基于基本面或技术面看好某个行业时,可以用情绪指标来观察市场资金是否也形成了共识。如果看好一个行业但情绪分数持续低迷,可能需要反思逻辑是否被市场认可。
  • 感知市场水温:综合情绪分数的市场平均值或中位数,可以作为一个整体市场风险偏好的观测指标。分数普遍极高时,提示市场可能过热;普遍极低时,可能孕育反弹。

4.2 工程化部署:打造你的自动化看板

对于有兴趣的开发者,可以将这套系统工程化:

  1. 自动化数据流水线:使用定时任务(如Cron, Airflow, Celery),每日收盘后自动从AKShare等数据源拉取数据,计算指标,更新数据库。
  2. 结果可视化:用Plotly、Dash或Streamlit构建一个交互式看板。看板可以包括:
    • 行业情绪分数热力图(按时间、行业两个维度)。
    • 排名前列行业的净值走势与大盘对比。
    • 各细分情绪指标的雷达图,分析领涨行业的情绪驱动来源(是量价齐升?还是波动收敛突破?)。
  3. 预警通知:当某个行业的情绪分数突破历史阈值,或排名发生剧烈变动时,通过邮件、钉钉、Telegram Bot发送通知。
# 示例:使用Plotly绘制行业情绪热力图(简化版) import plotly.express as px # 假设 emotion_score_matrix 是一个 DataFrame,索引为日期,列为行业,值为情绪分数 # emotion_score_matrix = pd.DataFrame({ind: data['emotion_score'] for ind, data in emotion_data_dict.items()}) fig = px.imshow(emotion_score_matrix.T, # 转置,让行业在Y轴 labels=dict(x="Date", y="Industry", color="Emotion Score"), x=emotion_score_matrix.index.strftime('%Y-%m'), y=emotion_score_matrix.columns, color_continuous_scale="RdBu_r", # 红蓝渐变色,红色代表高情绪 aspect="auto") fig.update_layout(title='行业情绪分数热力图 (2014-2023)') fig.show()

4.3 策略迭代与优化方向

基础框架搭建完成后,可以考虑以下优化方向,但这每一步都要警惕过拟合:

  • 指标加权:五个等权指标是否最优?或许可以通过历史数据或机器学习方法(如IC值分析)动态调整权重。但需极度谨慎。
  • 多周期融合:同时计算短期(5日)、中期(20日)、长期(60日)的情绪分数,观察其共振效应。多周期同时看多,信号更强。
  • 结合宏观与事件:在情绪分数的基础上,加入宏观因子(利率、流动性)或事件因子(政策发布、财报季)进行过滤。例如,在货币紧缩期,降低高情绪行业的配置权重。
  • 风险控制模块:引入最大回撤止损、波动率仓位控制等。当组合回撤超过一定幅度,或市场整体波动率急剧放大时,强制降低仓位。

4.4 最重要的提醒:理解边界,管理预期

最后,也是最重要的一点,是理解任何量化模型的边界:

  • 它解释过去,但不必然预测未来:市场参与者的行为模式会变化,过去的规律可能失效。
  • 它是概率工具,不是水晶球:高情绪分数只代表上涨的概率可能提升,不代表必然上涨。需要与赔率(风险收益比)结合考虑。
  • 极端行情是模型的“盲区”:金融危机、政策巨变等系统性风险下,所有历史关系都可能被打破。
  • 保持透明,持续维护:这就是我们开源全部代码的原因。只有你完全理解每一行代码在算什么,你才能知道策略何时可能失效,并做出调整。

投资没有一劳永逸的圣杯。这套情绪指标系统,更像是一副为你量身定制的“市场体温计”和“资金流向探测仪”。它不能代替你思考,但能帮你更清晰、更及时地看到市场正在发生什么。剩下的,依然是对商业本质的理解、对风险的控制,以及对自己情绪的管理。从这个意义上说,最好的策略,永远是那个你能完全理解、深度信任,并能坚持执行下去的策略。

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

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

立即咨询