ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python量化炒股:多角度分析股价与价值背离及行情预测

Python量化炒股:多角度分析股价与价值背离及行情预测 1. 项目背景与目标设定先说清楚这个项目到底在做什么。标题里写的是Python量化炒股分析多角度解析股价与价值背离及预测行情涨跌拆开来看核心就三件事第一用Python把股票数据拉下来第二从多个维度去判断股价和它的内在价值到底有没有偏离、偏离了多少第三基于这些偏离信号尝试对行情的涨跌方向做预测。听起来好像很玄乎其实底层逻辑并不复杂。做过几年交易或者看过一些基本面分析资料的人都知道股价短期是投票器长期是称重机。意思是短期内市场情绪、资金博弈、消息面会把价格推到离价值很远的地方但拉长时间看价格终究要向价值回归。这个项目做的就是用程序化的方式把这个偏离和回归的过程量化出来变成可回测、可复现、可执行的信号。适合谁来参考说实话门槛不算低但也不需要你是量化大神。我假设你已经会基本的Python语法知道pandas怎么操作DataFrame装过jupyter或者vscode跑过最简单的爬虫或者接口请求。如果你连这些还没搞定建议先去把Python基础过一遍再来不然看后面的代码会比较吃力。反过来如果你已经有一定交易经验但对怎么把想法变成代码还不太熟这篇文章也能给你一条比较完整的参考路径。这个项目的价值不在于给你一个稳赚不赔的策略——我可以直接说市面上不存在这种东西。它的价值在于给你一套完整的思路框架和可运行的代码骨架让你知道如何用数据去验证自己对市场的判断而不是凭感觉下单。2. 整体方案设计与数据获取2.1 技术方案选型为什么是Python做量化分析语言选择上无非就是Python、R、Matlab甚至现在也有人用JavaScript或者Go写交易系统。但我个人还是推荐Python原因很直接第一生态成熟。你想用的任何金融数据库、技术指标库、机器学习库Python基本都有现成的。akshare、tushare、baostock、yfinance这些数据源接口覆盖了A股、港股、美股、期货、加密货币等几乎所有主流市场不需要自己去写爬虫解析乱七八糟的HTML。pandas、numpy处理数据的能力更是没得说一行groupby就能完成Excel里折腾半天的聚合统计。第二策略验证效率高。从数据清洗到因子计算再到回测和可视化Python的整个工作流非常顺畅。我见过有人用Excel做回测数据一多就卡死更别提做参数敏感性分析了。Python在这方面的优势是碾压性的。第三社区资料丰富。遇到问题随便一搜都能找到解决方案尤其是量化这个细分领域GitHub上有大量开源项目可以参考。学习成本相对低。当然Python也有短板就是纯计算性能比C差很多。但对个人做研究级别的回测来说这个短板根本感知不到。真到了需要高频交易或者分钟级甚至tick级大规模回测的时候可以再用Cython、Numba优化或者直接用C重写核心逻辑这都不是问题。2.2 数据源对比与选型建议数据是整个项目的粮食数据质量直接决定分析结果靠不靠谱。我常用的是akshare和tushare这里做一个对比数据源免费额度数据范围稳定性适用场景akshare完全免费沪深京A股、港股、美股、期货、基金、宏观数据中等接口偶尔变动个人研究、教学演示tushare注册送积分积分越高权限越大A股、港股、美股、期货等较好接口稳定个人研究、小规模策略baostock完全免费A股日线/分钟线、指数、行业较好需要长时间历史数据回测yfinance完全免费美股、部分港股一般反爬较严美股研究我自己的偏好是研究A股用tushare的日线数据因为数据质量相对可靠复权因子处理得比较规范。如果只是快速验证想法akshare最方便不用注册账号就能用。注意akshare的接口偶尔会因为上游网站改版而出问题代码里最好加上异常重试机制否则跑数据跑到一半挂了会很崩溃。以tushare为例获取数据的基本流程是import tushare as ts import pandas as pd # 注册tushare账号后获取token ts.set_token(你的token) pro ts.pro_api() # 获取某只股票最近5年的日线数据 df pro.daily(ts_code600519.SH, start_date20200101, end_date20241231) # 获取基本面数据这里用市盈率、市净率等指标 # 先从股票列表里拿到基础信息 basic_info pro.stock_basic(ts_code600519.SH, fieldsts_code,name,industry,list_date) print(basic_info)日线数据拿到手之后先把日期转成datetime类型并设为索引按时间升序排列这一步看起来简单但特别重要很多新手拿到数据就开算结果因为顺序不对算出来的移动平均线全是错的。2.3 数据清洗与预处理规范原始数据不能直接用必须经过清洗。我一般按以下步骤处理去重检查是否有重复的日期记录尤其是从多个数据源拼接数据时容易出现。缺失值处理停牌日的数据往往是NaN需要forward fill或者直接删除。对于技术指标计算建议先fillna再计算否则ma5、ma10这种指标会断掉。复权处理做量化分析必须用前复权或者后复权数据否则遇到除权除息股价会产生虚假的跳水技术指标会完全失真。tushare里用pro.adj_factor()接口获取复权因子然后用收盘价乘以复权因子得到实际价格。异常值过滤某些极端行情下可能出现涨跌幅异常虽然A股有涨跌停限制但新股上市或者退市整理期会出现不正常的波动最好按涨跌幅阈值做过滤。清洗完之后把数据保存成本地csv或者parquet文件后续分析直接读文件不要每次跑都重新请求接口既慢又容易被限流。数据预处理这段我多说一句复权处理是新手最容易忽略但影响最大的一个环节。不夸张地说如果你拿不复权的数据去计算均线、MACD这类价格衍生指标遇到一次高送转整个信号体系就全乱了。项目里务必处理好。3. 股价与价值背离的多角度判断模型3.1 什么叫价值什么叫背离在动手写代码之前先想清楚一个核心问题用什么指标代表价值价值这个概念本身就有很多维度。从基本面角度可以用市盈率PE、市净率PB、股息率、盈利收益率E/P等从技术面角度可以用长期均线、布林带中轨等作为市场公认的合理估值中枢从量化角度还可以用行业平均估值、历史估值分位数等做相对比较。背离就是价格偏离这些价值参考线的程度。偏离得越远修复的动力越强——当然这只是理论上的说法实际情况里贵了还能更贵、便宜了还能更便宜的情况也大量存在所以背离信号只能作为参考不能作为无脑反向操作的依据。3.2 估值类背离指标PE/PB分位数模型最经典的估值背离指标是历史估值分位数。思路是把某只股票过去5年或者10年每天的PE、PB数据取出来算当前值处于历史区间的什么位置。如果当前PE处于历史的90%分位以上说明估值偏贵如果处于10%分位以下说明估值偏便宜。代码如下import tushare as ts import pandas as pd import numpy as np pro ts.pro_api(你的token) def get_valuation_basic(ts_code, start_date20190101, end_date20241231): 获取股票的历史每日估值指标 df pro.daily_basic( ts_codets_code, start_datestart_date, end_dateend_date, fieldsts_code,trade_date,close,pe_ttm,pb,ps_ttm,div_ratio,total_mv ) df[trade_date] pd.to_datetime(df[trade_date]) df df.sort_values(trade_date).reset_index(dropTrue) # 计算PE分位数剔除异常值PE为负数的亏损股需要单独处理 df[pe_ttm] df[pe_ttm].replace([np.inf, -np.inf], np.nan) valid_pe df.loc[df[pe_ttm] 0, pe_ttm].dropna() df[pe_percentile] df[pe_ttm].apply( lambda x: (valid_pe x).mean() if pd.notna(x) and x 0 else np.nan ) # 同理计算PB分位数 df[pb] df[pb].replace([np.inf, -np.inf], np.nan) valid_pb df.loc[df[pb] 0, pb].dropna() df[pb_percentile] df[pb].apply( lambda x: (valid_pb x).mean() if pd.notna(x) and x 0 else np.nan ) return df # 示例贵州茅台 valuation_df get_valuation_basic(600519.SH) print(valuation_df.tail())需要说明的是这里的分位数计算用的是过去全部有效观测值的累积分布不是滚动窗口。实际项目中建议用滚动5年窗口来计算分位数这样更符合当前估值和近5年比处于什么水平的语境避免因为公司基本面发生根本性变化比如从成长股变成价值股导致的历史区间失真。3.3 价格与技术面的背离信号均线偏离度和RSI极端值基本面估值是一个维度但A股市场很多时候炒的就是情绪和资金面基本面和价格可以长期背离。所以我们还需要从技术面找信号。我用得比较多的是均线偏离度。定义很简单偏离度 (当前价格 - 长期均线) / 长期均线 × 100%当偏离度超过正20%甚至30%时说明短期涨幅过大价格明显偏离内在中枢当偏离度低于负20%时说明超跌明显。代码如下# 读取清洗后的日线数据假设df包含close列 df[ma20] df[close].rolling(window20).mean() df[ma60] df[close].rolling(window60).mean() df[ma120] df[close].rolling(window120).mean() # 计算价格相对于120日均线的偏离度 df[deviation_ma120] (df[close] - df[ma120]) / df[ma120] * 100 # 标记极端偏离状态 df[overbought] df[deviation_ma120] 20 df[oversold] df[deviation_ma120] -20RSI相对强弱指标可以辅助判断超买超卖。RSI超过70通常认为是超买低于30是超卖。但A股市场经常出现RSI在70以上钝化的情况尤其是强势股可以连续几周RSI都挂在80以上不下来。所以RSI更适合做拐点确认而不是单一买卖依据。我的做法是把估值分位数、均线偏离度、RSI三个维度结合起来至少两个维度发出同向信号时才标记一次背离事件。单一信号经常是假信号多个维度共振时可信度明显提高。3.4 构建多维度背离评分体系与其分别看三个指标不如把它们整合成一个综合评分。这个评分就是多角度解析的落地点。具体思路PE分位数20%权重数值越高估值越贵偏离度越大。PB分位数20%权重同上。均线偏离度30%权重数值越极端短期情绪越亢奋或恐慌。RSI背离度30%权重RSI与价格走势是否形成顶背离或底背离。综合评分的计算公式可以做成def compute_composite_score(df): 计算综合背离评分 分数范围理论上 -100 到 100 正分代表价格高于内在价值偏贵负分代表价格低于内在价值偏便宜 # 对每个指标做归一化处理这里以PE分位数为例范围0~100 pe_score df[pe_percentile].fillna(50) * 100 # 均线偏离度映射到评分偏离越大分数越高 # 以±30%为极端阈值做线性映射 dev_score np.clip(df[deviation_ma120] / 30, -1, 1) * 100 # RSI映射50为中轴70以上偏多30以下偏空 rsi_score (df[rsi14] - 50) / 50 * 100 df[composite_score] ( pe_score * 0.2 df[pb_percentile].fillna(50) * 100 * 0.2 dev_score * 0.3 rsi_score * 0.3 ) return df这里的分值和权重都可以根据自己的经验调整不是标准答案。重要的是形成了多角度的统一度量框架后续做预测和回测都可以在这个score基础上进行。4. 构建行情涨跌预测模型4.1 预测的目标到底是什么先说清楚预测行情涨跌的定义。我们不是要预测明天涨还是跌这样过于短期的随机事件短线价格受太多不可控因素影响预测准确率很难稳定超过50%多少。更合理的做法是预测未来一段时间比如未来5天、10天、20天的收益率区间或者涨跌方向概率。所以我定义了两个预测目标方向预测未来N天收益率为正还是为负。幅度预测未来N天的累计收益率具体大概在什么范围。对于方向预测用分类模型对于幅度预测用回归模型。两者可以共用同一套特征。4.2 特征工程从数据中挖出有效因子特征工程决定了预测模型的上限。我整理了一下项目中用的特征分成四类价格动量类过去5天、10天、20天、60天的收益率短中长期动量。波动率类过去20天的年化波动率、ATR平均真实波幅。估值类上面计算的PE分位数、PB分位数、综合背离评分。成交类成交量变化率、换手率、量比。量价配合关系在A股尤其重要。一个比较实用的量价背离特征是价格上涨但成交量萎缩。这种量价背离往往说明上涨动力不足后续回调概率大。反过来价格下跌但成交量放大说明抛压沉重短期可能还有下跌惯性。特征代码示例# 假设df包含close, vol, amount等列 df[return_5] df[close].pct_change(5) df[return_10] df[close].pct_change(10) df[return_20] df[close].pct_change(20) df[volatility_20] df[close].pct_change().rolling(20).std() * np.sqrt(252) df[volume_change] df[vol].pct_change(5) df[turnover_rate] df[amount] / df[total_mv] * 100 # 量价背离信号价格上涨但成交量下降 df[price_up_volume_down] ((df[return_5] 0) (df[volume_change] 0)).astype(int) # 未来10天收益作为预测标签 df[future_return_10] df[close].shift(-10) / df[close] - 1 df[future_positive_10] (df[future_return_10] 0).astype(int)4.3 模型选择逻辑回归、随机森林还是XGBoost预测模型的选择上我试过不少最终保留了三个逻辑回归简单、可解释性强适合做baseline。系数可以直接看出来哪些特征对涨跌影响最大。随机森林非线性关系捕捉能力比逻辑回归强对异常值不敏感不需要大量特征工程。XGBoost / LightGBM实际表现最好对结构化数据几乎是无脑最优解但需要花时间调参且容易过拟合。我一般用逻辑回归做基线验证特征有效性如果逻辑回归都跑不出有效的信号测试集AUC低于0.52说明特征和数据质量有问题换更复杂的模型意义不大。先用简单模型验证思路再用复杂模型提精度这个顺序非常推荐。代码示例用随机森林from sklearn.model_selection import TimeSeriesSplit from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, roc_auc_score # 选择特征列和目标列 features [ return_5, return_10, return_20, volatility_20, volume_change, price_up_volume_down, pe_percentile, pb_percentile, composite_score, rsi14 ] X df[features].dropna() y df.loc[X.index, future_positive_10] # 时序交叉验证注意不要用普通K-Fold会数据泄露 tscv TimeSeriesSplit(n_splits5) auc_scores [] for train_idx, test_idx in tscv.split(X): X_train, X_test X.iloc[train_idx], X.iloc[test_idx] y_train, y_test y.iloc[train_idx], y.iloc[test_idx] model RandomForestClassifier(n_estimators200, max_depth6, random_state42) model.fit(X_train, y_train) y_pred_prob model.predict_proba(X_test)[:, 1] auc roc_auc_score(y_test, y_pred_prob) auc_scores.append(auc) print(fFold AUC: {auc:.4f}) print(fMean AUC: {np.mean(auc_scores):.4f})这里有一个非常关键的坑预测未来一定要用时间序列交叉验证不能用普通的K-Fold。普通K-Fold随机打乱样本后训练集里包含了未来数据模型等于偷看答案回测指标虚高实盘一用就现原形。TimeSeriesSplit保证训练集时间永远在测试集之前这才是接近真实场景的评估方式。4.4 预测结果的合理使用策略即使模型训练出来AUC到了0.58甚至0.6也绝对不意味着跟着模型买就能赚钱。这里面的差距主要在交易成本、滑点、市场冲击以及模型预测的是方向概率而不是确定性事实。我的实际用法是把模型的预测概率当成一个仓位调节器而不是简单的买卖信号。预测上涨概率超过0.55时可以适当加仓低于0.45时降低仓位或者空仓等待介于0.45到0.55之间属于信号模糊区不操作。这样避免了模型频繁发出信号导致交易成本吞噬收益的问题。5. 回测框架与效果评估5.1 从信号生成到收益曲线有了模型下一步就是回测。回测的框架不复杂核心就是正确地模拟交易逻辑满足买入条件时买入满足卖出条件时卖出记录每次交易的盈亏最后汇总成净值曲线。我自己比较习惯写简洁的回测代码不依赖现成的backtrader之类的框架因为策略逻辑一旦复杂起来调试自己的代码更直接。不过如果你想快速验证用backtrader、vectorbt、qlib都可以没必要重复造轮子。关键逻辑def backtest(df, buy_threshold0.55, sell_threshold0.45): 简单回测预测概率高于buy_threshold买入低于sell_threshold卖出 只做多头不做空 df df.copy() df[signal] 0 df.loc[df[pred_prob] buy_threshold, signal] 1 df.loc[df[pred_prob] sell_threshold, signal] -1 # 交易信号signal从0变成1代表买入从1变成0/-1代表卖出 # 这里用持币/持股两个状态简化处理 position 0 entry_price 0 trades [] for i in range(len(df)): if position 0 and df[signal].iloc[i] 1: # 买入 position 1 entry_price df[close].iloc[i] entry_date df[trade_date].iloc[i] elif position 1 and df[signal].iloc[i] ! 1: # 卖出 exit_price df[close].iloc[i] exit_date df[trade_date].iloc[i] return_pct (exit_price - entry_price) / entry_price trades.append({ entry_date: entry_date, exit_date: exit_date, entry_price: entry_price, exit_price: exit_price, return_pct: return_pct }) position 0 if position 1: # 回测结束时强制平仓 exit_price df[close].iloc[-1] return_pct (exit_price - entry_price) / entry_price trades.append({ entry_date: entry_date, exit_date: df[trade_date].iloc[-1], entry_price: entry_price, exit_price: exit_price, return_pct: return_pct }) trade_df pd.DataFrame(trades) # 计算累计收益 cumulative_return (trade_df[return_pct] 1).prod() - 1 # 计算胜率 win_rate (trade_df[return_pct] 0).mean() return trade_df, cumulative_return, win_rate这个回测逻辑做了极大简化没有考虑手续费和滑点所以出来的收益会比实际操作偏高。我在项目中会另外扣除双边千分之一点五左右的手续费和千分之二的滑点更贴近真实情况。5.2 回测指标怎么看不要只看收益率很多人做回测只盯着年化收益率这是大忌。一个年化50%的策略如果最大回撤60%普通人根本拿不住中途就割肉离场了。我比较看重的指标有这么几个指标含义我的经验参考值累计收益率整个回测周期的总收益越高越好但需结合回撤看年化收益率折算成年度的收益率比纯累计收益更可比最大回撤净值从高点到低点的最大跌幅控制在20%以内较舒服胜率盈利交易占比40%以上即可接受盈亏比平均盈利/平均亏损最好大于1.5夏普比率单位风险所获得的超额收益大于1算及格大于2比较优秀计算最大回撤的代码很简单# 假设equity_curve是每日净值序列 drawdown equity_curve / equity_curve.cummax() - 1 max_drawdown drawdown.min() print(f最大回撤: {max_drawdown:.2%})5.3 过拟合与参数稳健性检查量化交易最大的敌人是过拟合——模型在历史数据上表现完美一到实盘就亏钱。我在实际项目中遇到过太多次这种看起来很美的策略总结下来有几个防过拟合的手段参数不要过多逻辑回归、简单的阈值策略参数少相对不容易过拟合。XGBoost如果调参调得特别细很容易钻数据牛角尖。跨样本验证训练集用了2019-2022年的数据测试集就用2023-2024年的数据中间留出一段gap不要接触。如果跨样本表现明显变差说明策略不稳定。参数敏感度分析比如把买入阈值从0.55换成0.5、0.6观察收益变化是否剧烈。如果参数稍微一动结果就从盈利变成亏损这个策略基本不可用。换股票池测试在某一只股票上表现好的策略换到另外几只不同行业的股票上如果普遍有效说明策略逻辑有普适性如果只在特定股票上有效很可能就是过拟合了个股特性。我个人的要求是策略至少要在3只不同行业、不同市值风格的股票上都能跑出正收益且回撤可控才会考虑进一步投入精力做实盘验证。单只股票上的回测再漂亮我都持保留态度。6. 常见问题与项目经验总结6.1 数据获取失败与接口限流问题实际跑数据的时候最常见的问题就是接口请求报错。tushare有积分限制积分不够某些接口调用频率会被限制比如每分钟只能调用几次。我的处理方式是写一个简单的重试机制和本地缓存import time def fetch_with_retry(func, *args, max_retries3, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception as e: if attempt max_retries - 1: raise e time.sleep(2 ** attempt) return None另外本地缓存非常关键。第一次拉取数据后保存成parquet文件之后每次都先检查本地文件是否存在、数据是否覆盖到最新日期只增量拉取新的数据。这样既避免被限流又提升效率。6.2 预测模型AUC偏低是不是说明方法没用很多新手跑出AUC在0.52-0.56之间就觉得模型废了其实不是。金融预测本来信噪比就极低能稳定跑出0.55以上的AUC已经具备参考价值关键是看你怎么用这个信号。我见过一个实盘做得还不错的朋友他的模型AUC也就0.57左右但他把预测概率和仓位管理、止损规则结合起来把预期收益率不高但胜率相对高的交易挑出来做长期下来曲线很稳。模型不是用来预测每一次涨跌的是用来筛选你更有把握的时刻。相比之下那种声称自己模型AUC 0.8以上能稳定跑赢市场的人要么是用了未来数据要么是严重过拟合要么根本没有真实交易过。这个领域对神技保持警惕是必要的。6.3 实盘和回测的差距到底在哪里最后说一个老生常谈但依然很多人栽跟头的点回测和实盘的差距。我总结下来主要有三个来源一是交易成本。回测里如果不扣手续费和滑点实际收益会大打折扣。尤其做中短线频繁交易下光手续费就能吃掉大部分利润。二是信号延迟。回测中假设你能在信号出现的当天收盘价买入但实际操作中信号出现后你看到、确认、下单往往已经晚了半天甚至一天。尤其在开盘跳空的情况下买入价和信号价可以差很 多。三是心理因素。回测里最大回撤只是数字实盘中连续亏损十几笔大多数人都会开始怀疑模型、手动干预、甚至放弃策略。这一点很难用代码解决只能靠仓位管理控制回撤在心理承受范围内以及提前想好交易纪律。6.4 项目还可以怎么扩展这个项目做完之后我个人觉得有几个值得继续深入的方向引入行业对比把个股估值分位数变成个股vs行业的相对估值能过滤掉行业整体的系统性高估或低估。尝试多因子打分体系把估值、动量、波动率、资金流向等维度综合成选股因子从单票预测扩展到股票池排名筛选。做实盘模拟盘验证用模拟账户跑一个月记录实盘和回测的偏差再回头调整参数和模型。这一步是提升策略可信度的必经之路。根据我个人经验做完这个项目你对股价为什么会涨、为什么会跌的理解会比只看K线的人深入很多。量化的意义不在于找到圣杯而在于用数据把自己关于市场的模糊直觉变成一个可以检验和改进的系统。哪怕最终收益一般这个过程本身就已经值回票价了。
返回列表