做量化炒股这几年,我最大的感受是:散户最容易犯的错,不是买错股票,而是看不出股价已经在悄悄偏离它本该在的位置。所谓"价值背离",通俗点讲,就是股价短期被情绪推着跑,跑得太快或太慢,最终总要回到合理区间。而Python量化炒股分析,本质上就是把这套"找偏离、等回归"的逻辑,用代码变成一套可以反复执行的规则。这篇内容我围绕股价与价值背离的多角度识别、行情涨跌预测的建模过程,把从数据准备、指标计算到模型训练回测的完整链路写清楚,适合那些已经掌握了Python基础、想往量化方向再迈一步的读者参考,也适合刚接触量化但不知道从哪下手的同学当个路线图。
1. 量化分析的整体设计思路
1.1 为什么我用Python做量化而不是别的工具
很多人一上来就纠结选平台、选软件,其实工具不重要,重要的是你能不能把你的想法用可验证的方式表达出来。Python在这件事上有天然优势:行情数据获取有现成的库,指标计算有pandas和numpy,画图有matplotlib,建模型有scikit-learn和xgboost,回测自己写几十行代码就能搞定。一套流程全在一个生态里走完,不需要来回切换工具,光这一点就省掉大量时间。
另外,Python处理时间序列数据非常顺手。你会发现,做量化的大部分工作其实是在折腾数据——对齐日期、处理停牌、算收益率、切训练集和测试集。pandas的DataFrame和resample这些功能,写起来就像在操作一张Excel表,但速度比手工快几个数量级。我做第一版策略的时候,从拉数据到出回测结果,整个流程不到一百行代码就跑通了,这要是用传统软件,光是手工整理K线就要浪费半天。
更重要的是,Python能让你把"拍脑袋"变成"可重复实验"。同一个策略,参数改一下、数据区间改一下,结果可能天差地别。用手工盯盘判断,你永远不会知道到底是策略本身有效还是运气好。用Python,你可以把历史数据切成好几段反复测试,逻辑上是否靠谱一目了然。这才是量化的本质:不是预测未来,而是验证你在过去总结的规律还能不能继续用。
1.2 价值背离到底在说什么
我做个类比你就明白了。股价好比一个人跑马拉松时的瞬时速度,价值是他的平均配速。短期内他可能冲得很快,也可能因为累了突然慢下来,但偏离太远之后,身体会自动把他拉回平均配速附近。股票市场也一样,价格短期被资金、情绪、消息推动,可能远远脱离它的内在价值或技术中枢,但市场的力量最终会让它回归。
但"价值"这个词在量化里很难直接定义,所以实战中我们用各种代理指标。比如均线代表一段时间内的平均持仓成本,这就是一个技术层面的"价值中枢";MACD的DIF线代表短期动量相对于长期动量的强弱,这反映的是资金推动的持续性;RSI代表一段时间内涨跌力量的对比,这体现的是供需的失衡程度。当价格创新高但某个指标没有同步创新高,或者价格下跌但成交量却萎缩,这些错位都叫背离。
背离的多角度分析,本质上就是在找不同维度之间的"不协调"。单看价格看不出问题,单看成交量也看不出问题,但把它们放在一起对照,信号就出来了。这也是我在策略里坚持同时监控好几个维度的原因——单一指标骗人,多个指标一起背离,可信度会高很多。
1.3 从"识别背离"到"预测涨跌"的闭环
背离识别只是第一步,它给你的是一个候选股票池和方向判断,但真正要把这个信号变成可执行的交易决策,还需要两步:把信号转成特征,再用模型判断这个信号在当前市场环境下值不值得信。我自己的框架是三层结构:数据层负责拿干净的数据;信号层负责计算各种背离指标;决策层负责把信号喂给模型,输出涨跌概率。
三层分开写有个好处,你可以单独替换任何一层而不影响其他层。比如你觉得某个指标不好用了,只需要在信号层改一行代码,模型层不用动。如果你想换模型,也只需要改决策层。这样整个体系维护起来很轻松,而且每层都可以单独测试,出了问题能快速定位。
2. 数据准备与分析环境搭建
2.1 行情数据源怎么选
做量化最怕的不是策略不对,而是数据脏。我最早用网络爬虫自己抓数据,维护成本太高,后来干脆用现成的数据源,省心不少。国内常见的免费数据源有akshare、tushare、baostock,各有侧重,我简单列个对比。
| 数据源 | 覆盖范围 | 优缺点 | 获取方式 |
|---|---|---|---|
| akshare | A股、港股、美股、期货、基金 | 接口丰富,更新快,但部分接口不稳定 | 直接pip install,文档齐全 |
| tushare | A股为主 | 数据质量高,积分制,高级接口需要积分 | 注册后使用token调用 |
| baostock | A股 | 免费且稳定,历史数据全,接口规范 | 直接pip install,无需注册 |
我个人习惯用akshare拉日线数据,因为它覆盖的品种多,而且不用注册就能用。但如果你要做分钟级别的策略,建议用收费的数据源,因为免费接口的分钟线经常有缺失和延迟。我刚开始做量化时就吃过亏,用的是免费分钟线数据,回测时看着没问题,一到实盘就发现某些时段的数据明显偏少,导致策略信号根本没触发。
不管用哪个数据源,拿到数据后第一件事就是检查三个东西:日期是否连续、是否有重复行、是否有极端异常值。我一般会用一行代码检查重复:
# 检查并去除重复的日期索引 df = df[~df.index.duplicated(keep='last')] # 按日期排序 df = df.sort_index()2.2 前复权、后复权到底用哪个
这是新手最容易忽略的细节。股票会分红、送股、拆股,价格会突然跳空。如果不复权,技术指标的连续性会被破坏,背离信号会出现大量假信号。复权有两种:前复权,以当前价格为基准调整历史价格;后复权,以上市首日价格为基准调整后续价格。
我个人的建议是:做历史回测用后复权,做实时信号用前复权。原因很简单,后复权的价格序列是真实的财富变化曲线,计算收益率最准确;前复权保证了最新价格和实盘一致,方便你观察当前股价位置和指标值。但注意,前复权有个坑——它随着每次新分红会重新调整全部历史数据,所以如果你隔一段时间重新拉数据,历史指标可能会变化,回测结果会和之前对不上。
# 一个简单的前复权处理示例 # factor为复权因子,通常数据源会直接提供 factor = df['factor'].shift(1) # 或直接用后复权价/最新复权价计算 df['adj_close'] = df['close'] * df['factor'] / df['factor'].iloc[-1]2.3 技术指标计算的核心代码骨架
技术指标的库有很多,比如TA-Lib,但说实话,在得到核心指标之前,我更推荐自己用pandas算一遍。原因不是为了造轮子,而是让你真正搞明白指标是怎么算出来的,后面调参、做变体才有底。比如MACD,如果不知道它是EMA12和EMA26的差,你就不知道为什么在震荡市它会频繁金叉死叉。
我自己常用的几个指标计算方式如下:
import pandas as pd import numpy as np # MACD指标 def calc_macd(close, fast=12, slow=26, signal=9): ema_fast = close.ewm(span=fast, adjust=False).mean() ema_slow = close.ewm(span=slow, adjust=False).mean() dif = ema_fast - ema_slow dea = dif.ewm(span=signal, adjust=False).mean() macd_hist = (dif - dea) * 2 # 有些软件用2倍放大 return dif, dea, macd_hist # RSI指标 def calc_rsi(close, period=14): diff = close.diff() gain = diff.clip(lower=0).rolling(window=period).mean() loss = (-diff.clip(upper=0)).rolling(window=period).mean() rs = gain / loss rsi = 100 - (100 / (1 + rs)) return rsi # 均线 def calc_ma(close, periods=[5, 10, 20, 60]): return pd.DataFrame({'MA' + str(p): close.rolling(p).mean() for p in periods})注意EWM的adjust=False参数很重要。如果不加这个参数,pandas默认使用调整指数加权,算出来的EMA和股票软件里的不完全一致,可能导致后续背离信号和你在行情软件上看到的对不上。
3. 多角度背离识别的实战拆解
3.1 价格与均线的背离:趋势中的刹车信号
均线是散户最熟悉的指标,但它不只是用来画支撑阻力的。价格与均线的背离,在我看来是趋势中最有价值的刹车信号。最典型的一种情况是:股价还在创出新高,但MA20或MA60已经走平甚至掉头向下,这说明短期资金推不动长期趋势,涨势快到头了。
识别逻辑其实不复杂。我们盯的是价格创新高时,均线是否同步创新高。这里有个容易出错的地方,就是"创新高"的时间窗口怎么界定。我通常用过去20个交易日作为比较窗口,如果当前收盘价创了20日新高,但MA20没有创20日新高,就标记为一个弱背离信号。
def detect_ma_divergence(df, price_col='close', ma_col='ma20', lookback=20): price_high = df[price_col].rolling(lookback).max() ma_high = df[ma_col].rolling(lookback).max() # 价格创新高但均线未创新高 condition = (df[price_col] >= price_high.shift(1)) & (df[ma_col] < ma_high.shift(1)) return condition这里用shift(1)是为了防止用当天的数据比较当天,产生未来视角。这种细节就是量化和手工看盘最大的不同,统计上的严谨性直接决定信号质量。
3.2 价格与MACD的顶背离和底背离
MACD背离是我策略里权重最高的信号,因为它本质上反映的是动能的衰竭。顶背离的形态是:股价创新高,但MACD的DIF高点却比前一次高点低,说明推动价格上涨的动能正在减弱,随时可能掉头。底背离则反过来,股价创新低,但DIF低点比上一次低点高,说明下跌动能衰竭,反弹可能就在眼前。
实战中识别MACD背离,最麻烦的是怎么定义"创新高"和"前一次高点"。用肉眼很容易,但写成代码就会遇到各种奇葩情况。我常用的方法是:先用scipy的argrelextrema找到DIF的局部极值点,再去匹配对应的价格极值点,比较两段区间。
from scipy.signal import argrelextrema import numpy as np def find_local_extrema(series, order=5): # 找到局部极大值和极小值 max_idx = argrelextrema(series.values, np.greater_equal, order=order)[0] min_idx = argrelextrema(series.values, np.less_equal, order=order)[0] return max_idx, min_idxorder参数决定找的是多少个交易日级别的极值,这个值很敏感。我回测过不同周期,5用在日线级别比较合适,太小的order会找到一堆噪音极值,太大的order又会漏掉关键转折点。另一个细节是,趋势行情中MACD会长时间钝化,背离信号频繁出现但每次调整幅度都很小,这时候我的处理方式是叠加一个趋势过滤器,比如DIF本身还在零轴上方时才考虑顶背离做空或减仓。
3.3 量价背离:没人接盘的上涨走不远
成交量是市场的投票器,量价背离是我最看重的辅助验证。典型的顶背离是:股价创新高,但成交量明显小于前一波高点的量,说明这一波上涨没有新增资金进场,完全是存量资金在拉升,属于典型的"无量上涨",后续大概率回踩。
量价背离的判断难点在于,成交量的比较需要考虑当时的市场环境。牛市里成交量普遍放大,熊市里普遍萎缩,直接比较绝对值没有意义。我的做法是把成交量做标准化,用成交量除以过去20日的平均量,得到量比,再看量比的变化。
def calc_volume_ratio(df, window=20): df['vol_ma'] = df['volume'].rolling(window).mean() df['vol_ratio'] = df['volume'] / df['vol_ma'] return df如果股价创新高时,vol_ratio小于1,说明当日成交量甚至低于近期平均水平,这个顶背离的可靠性就很高。我自己实测下来,量价配合的底背离,后续上涨的概率明显高于单纯看价格和指标的背离。但也要注意,有些主力会通过对倒放量制造假象,所以量价背离更适合作为过滤器,而不是唯一入场依据。
3.4 RSI背离:超买超卖区域的极端信号
RSI背离和MACD背离有点像,但又有区别。MACD看的是趋势动能的中期变化,RSI看的是短期超买超卖的修复。我一般会在RSI进入超买区(70以上)或超卖区(30以下)之后,再寻找背离信号,这样能过滤掉大部分无效的波动。
举个例子,股价在下跌过程中出现一个低点A,RSI是25;随后反弹几天又跌出新低点B,股价确实比A低,但RSI是35,没有再创新低,这就是底背离,说明卖压已经衰竭。这种信号在趋势末尾特别有效,但问题是一旦出现,往往意味着行情已经走了一段,入场时机要配合其他信号一起确认。
我用的RSI背离代码并不复杂,关键是在极值点附近做匹配。这里提醒一下,RSI默认周期是14,但在中国市场我测试过9和21,各有特点。短周期RSI反应快假信号多,长周期RSI反应慢但信号稳定。我自己在日线上用14,在周线上用21,分层判断。
4. 用机器学习预测行情涨跌
4.1 标签定义:预测的目标到底是什么
做预测模型第一步不是选算法,而是定义标签。到底预测什么?是预测明天涨跌,还是未来五天涨跌,还是预测未来二十天的收益率超过某个阈值?标签定义不同,模型学习的规律完全不同,难度也不在一个量级。
我的经验是:短期预测(1-3天)噪音太大,模型很容易过拟合。中期预测(5-10天)更适合捕捉背离后的回归行情。我用的是未来5个交易日的收益率作为标签,然后通过一个阈值把它二值化。比如未来5日涨幅超过3%标记为1,跌幅超过3%标记为0,中间的情况丢弃,因为那些模糊样本模型很难学出稳定规律。
def make_labels(df, horizon=5, threshold=0.03): future_return = df['close'].shift(-horizon) / df['close'] - 1 df['label'] = 0 df.loc[future_return > threshold, 'label'] = 1 df.loc[future_return < -threshold, 'label'] = -1 # 丢弃模糊样本 df = df[df['label'] != 0].copy() return df注意这里用shift(-horizon)实际上是用了未来数据来构造标签,这是允许的,因为标签本来就是放在训练阶段用。但一定要确保训练时不会把标签当作特征使用,否则就是未来函数泄露。
4.2 特征工程:把背离信号变成模型语言
机器学习模型本身不理解"顶背离"这种概念,它只认识数字。所以要把我们前面识别到的背离信号转成数值特征。我常用的做法有两类。一类是硬编码信号:有背离标记为1,没有为0,顶背离和底背离分开。另一类是连续型特征:直接用DIF和股价的偏离度、成交量比、RSI值这些连续变量,让模型自己去学习阈值。
features = pd.DataFrame({ 'macd_dif': dif, 'macd_dea': dea, 'macd_hist': hist, 'rsi': rsi, 'vol_ratio': vol_ratio, 'price_ma20_ratio': df['close'] / ma20 - 1, 'price_ma60_ratio': df['close'] / ma60 - 1, 'ma5_ma20_ratio': ma5 / ma20 - 1, })我的经验是,连续型特征通常比硬编码信号更能提升模型效果,因为硬编码信号丢失了太多信息。比如同样是顶背离,DIF偏离程度的轻重缓急不同,后续下跌的幅度也会不同。把price_ma20_ratio这种偏离比例交给模型,它能学到"偏离超过某个程度就该回归"的规律。
4.3 模型选型对比
我用过逻辑回归、随机森林、XGBoost和LSTM,简单说下各自的表现。逻辑回归最稳,虽然预测精度一般,但不会乱来,而且你能看每个特征的系数,解释性强。随机森林在特征不多的时候表现很好,不容易过拟合。XGBoost精度通常最高,但对参数敏感,调参不当容易过拟合。LSTM我在日线上试过,效果没有想象中好,因为日线数据长度有限,而且股市噪音太多,序列模型的优势发挥不出来。
| 模型 | 预测精度 | 训练速度 | 可解释性 | 我的评价 |
|---|---|---|---|---|
| 逻辑回归 | 中等 | 快 | 高 | 适合做基准线 |
| 随机森林 | 较好 | 中等 | 中等 | 默认选择,稳 |
| XGBoost | 好 | 中等 | 低 | 精度高但要防过拟合 |
| LSTM | 不稳定 | 慢 | 低 | 数据量不够时慎用 |
最终我用的方案是XGBoost做主力,逻辑回归做对照。每次迭代新特征,我都会先跑逻辑回归看特征系数方向是否合理。如果逻辑回归的系数方向和市场常识矛盾,那多半是特征构造出了bug,模型精度再高也不可信。这个检查习惯帮我避免了好几次低级错误。
4.4 回测与风控:别高兴太早
模型训练完之后,回测是最后一道关卡。我的回测框架逻辑并不复杂:在每一天收盘后,用当天之前的数据训练模型,然后对下一天的信号做预测,再根据预测结果决定持仓。这里最关键的细节是训练集和测试集必须按时间切分,绝对不能随机打乱,否则就是典型的未来函数泄露。
回测时要考虑几个现实因素。第一是手续费和滑点,A股佣金按万几算,印花税卖出时收千分之0.5,看起来不多,但高频交易累积下来非常可观。我一般按双边万2.5佣金加滑点0.1%来算,宁可高估成本,也不要不切实际地低估。第二是涨跌停影响,涨停时买不进、跌停时卖不出,如果回测里假设都能成交,实盘结果会大打折扣。
# 简化版回测循环 def backtest(df, signals, fee_rate=0.00025, slippage=0.001): position = 0 equity = 1.0 for i in range(len(df)): if signals.iloc[i] > 0.5 and position == 0: position = 1 equity *= (1 - fee_rate - slippage) elif signals.iloc[i] < 0.5 and position == 1: position = 0 equity *= (1 - fee_rate - slippage) if position == 1: equity *= (1 + df['return'].iloc[i]) return equity这套回测我只用来做相对比较,而不是追求绝对精确。它够用就好,真正上实盘之前还是要用支持更多细节的回测库,比如backtrader或自研事件驱动回测。
5. 实战中踩过的坑与排查技巧
5.1 未来函数泄露:回测结果骗人的头号原因
这个话题我在好几个技术群里都强调过,仍然看到不少人犯。未来函数泄露就是你在回测的某个时间点上,用了那个时间点还拿不到的数据。最常见的几个位置:一是用了shift(-n)构造标签后,没有对齐好索引,导致某些特征里混进了未来信息;二是用了整个数据集做归一化,均值、标准差包含了未来数据;三是用全历史数据训练模型,然后在历史中间某一天做预测。
排查方法很简单,每次回测之前先做一个"随机标签"测试。把标签随机打乱,然后跑一遍完整的训练和回测。如果随机标签也能获得正收益,那你的流程里一定有泄露。这是我每次迭代策略必做的体检项目,成本很低,却能拦住绝大多数隐藏bug。
5.2 过拟合:策略参数越来越复杂,收益却越来越差
刚开始做量化的时候,我特别喜欢给策略加参数,什么均线周期、背离窗口、出场阈值,全部调一遍,然后就发现训练集收益漂亮得吓人,一换数据区间就崩盘。后来我总结了一条铁律:参数超过三个,就要开始怀疑自己是不是在拟合历史。
应对方法有两个。一是做滚动测试,把历史数据按年份切块,逐年看策略表现。如果某几年的收益依赖某几个特定参数,说明策略不稳。二是减少参数的自由度,比如背离窗口固定用20,不再去优化它,除非有明显的逻辑支撑。在模型层面,我会用交叉验证配合早停,XGBoost的max_depth控制在3-5之间,特征数量控制在10个以内,宁可欠拟合也不要过拟合。
5.3 数据对齐的隐性陷阱:停牌股和交易日不一致
A股很常见的问题就是停牌。一只股票停牌一个月,复牌后会连续补涨或补跌。如果你没有处理停牌期间的数据,直方图上的收益率会出现断层。我的处理方式是,在计算指标时跳过停牌日,而不是把停牌当作跌幅0%来处理。否则连续几天价格为0收益率,会严重扭曲均线和RSI的值。
还有交易日不一致的问题,比如不同股票因为上市时间不同,历史长度不一样。在做横截面比较时,一定要统一时间轴。我用pandas的reindex把日期对齐到相同的交易日历,缺失值按前向填充处理,但前向填充只适用于价格,不适用于成交量,宁可填0也不能用前面的量冒充。
5.4 模型在市场风格切换时会失效
这是实盘中最无奈的事情。我的策略在某段时间表现很好,但市场一转型就连续回撤。比如价值风格占优时,价背离类策略很好用;主题炒作盛行时,动量类策略胜率更高;极端单边下跌时,所有策略都失效,只有空仓是对的。
针对这个,我的解决方案是给策略叠加一个市场状态过滤器。最简单的做法是看指数长期均线,比如过去60日上证收盘价高于MA60就只做多,低于MA60就减少仓位甚至空仓。我承认这个办法很土,但实测下来能过滤掉很多系统性风险。在模型层面,我会定期用最近一年的数据重新训练模型,让模型自己适应新的市场特征,而不是一套模型跑到底。
6. 一点个人体会
回头看我做量化这几年的过程,最大的收获不是某套策略赚了多少钱,而是建立了一套"怀疑一切"的工作习惯。每次回测结果很漂亮,我都会先问一句:是不是哪里泄露了?每次模型预测很准,我都会问一句:是不是过拟合了?每次实盘连续亏钱,我都会问一句:是不是市场环境变了,策略逻辑已经不成立了?
如果你现在刚开始接触Python量化炒股分析,我不建议一上来就搞复杂的深度学习模型。先把数据处理好,把MACD背离、量价背离这些基础信号用代码表达清楚,能稳定识别之后,再往模型预测的方向走。保持简单的逻辑,保留足够的可解释性,这套东西才能在你的交易体系里长期活下去。