简介:在量化投资与金融科技快速发展的背景下,基于Python的股票分析成为连接数据科学与工程实践的重要桥梁。其核心在于理解数据处理、技术指标与策略回测的完整链路:通过pandas与akshare等工具完成行情数据的获取与清洗,利用MA均线、MACD、RSI、KDJ等技术指标刻画价格趋势与市场动量,进而构建规则化的选股引擎。技术指标的计算逻辑直接影响策略的有效性,而严谨的回测系统则是验证策略能否穿越牛熊的关键,涵盖最大回撤、夏普比率等绩效指标。此类项目广泛应用于毕业设计、课程作业及个人量化研究,既要求模块化架构支撑灵活迭代,也强调避免未来函数与幸存者偏差等陷阱。本文以完整的项目实现为主线,拆解股票分析与选股系统的设计精髓,助力读者掌握从量化数据预处理到策略评估的工程化落地能力。 做毕业设计或者课程设计选到这个题目,说明你对量化金融这个方向是有兴趣的。我在帮学生和同行评审这类项目时见过太多版本了,有的把股票分析做成了“爬虫+画图”纯展示,有的把选股写成了“满仓梭哈一只票”的赌徒脚本,这两个极端都拿不了高分,更谈不上实用。这一次我把一个完整的、能跑通全流程的版本拆开揉碎讲清楚,从数据怎么拿、指标怎么算、策略怎么定,到回测怎么看、文档怎么写,一条线串下来。不管你是要做毕设、交课程作业,还是自己练手顺便攒个项目经验,照着这套思路去搭,都能少走几周的弯路。
源码和文档我会在关键节点给出结构设计和代码示例,你拿到之后做两件事:先把我画的坑绕过去,再按自己的理解把策略部分改成你自己的东西。只有这样,答辩的时候老师问你“为什么这么设计”,你才答得上来。
1. 项目整体设计与技术选型
1.1 核心需求解析
先说清楚这个项目到底在解决什么问题。拆开“股票分析和选股”这个标题,它实际上包含了三个层次的工作:
- 数据处理层:拿到股票的历史行情数据、财务数据,做清洗、复权、对齐,这是所有后续分析的基石。
- 策略分析层:基于技术指标(MA、MACD、RSI、KDJ这类)或者基本面因子,按照一定规则筛选出符合条件的股票,给出“买入候选池”。
- 回测验证层:把选股策略放到历史数据上跑一遍,看它如果真拿去用,收益如何、回撤多大、能不能跑赢基准指数。
很多同学拿到这类题目容易犯的第一个错误就是,直接去网上找一个Tushare的示例代码,拉个日线数据,算几个指标,画个K线图,就觉得自己做完了。这其实只完成了数据展示层面,离“分析”和“选股”还差得远。一个合格的毕设项目,必须把“策略定义—历史回测—绩效评估”这个闭环跑通,才能在答辩的时候站得住。我在实际做这个项目的过程中,把上面的三个层次拆成了六个子模块:数据获取模块、数据清洗模块、指标计算模块、选股引擎模块、回测模块、可视化模块。模块化带来最大的好处,后面你改一个策略或者换一个数据源,不需要动其他代码,这对后期迭代和答辩演示都特别有利。
1.2 技术栈选型原理解析
整个项目基于Python 3.9+,核心框架用pandas处理表格数据,numpy做数值运算,数据获取用akshare在前、tushare在后作为备选,可视化部分用matplotlib和pyecharts双层方案。
先解释一下为什么是Python而不是别的语言。第一,pandas的DataFrame结构处理股票日线数据几乎是量身定做的,按日期索引切片、重采样、合并多只股票的数据,这些操作在C++或者Java里写,代码量是Python的十倍起步;第二,量化交易这个圈子的生态基本都在Python这边,你后面想扩展什么功能,几乎都有现成的库可以抄作业。关于数据源,akshare和tushare的区别和使用选择,我在后面第2章详细说,这里先不展开。
依赖管理我用requirements.txt固定版本。这个细节可以保证项目在不同机器、不同时间运行的结果一致,答辩演示不至于出现“昨天还能跑今天突然报错”的尴尬。用一个虚拟环境来安装依赖,也是应该养成的习惯,不要图省事直接pip install到系统Python里,时间久了必然出幺蛾子。
提示:提交给老师的文档里,一定要写清楚Python版本和依赖库的版本号。我在评审中见过大量因为版本不匹配导致项目跑不起来的案例,这不是能力问题,是工程习惯问题,但影响分数。建议采用Python 3.9.x版本,pandas用1.5.x,akshare用1.x,这几个版本组合经过我实测兼容性最好。
1.3 模块化架构与目录组织
我的项目目录结构大概是这样:
stock_analysis/ ├── README.md # 项目说明与快速开始指南 ├── requirements.txt # 依赖库列表 ├── config/ │ └── config.py # 全局配置(数据源、回测参数、股票池) ├── data/ │ ├── fetcher.py # 数据获取模块 │ ├── cleaner.py # 数据清洗与预处理模块 │ └── storage.py # 本地缓存存储模块 ├── indicators/ │ ├── technical.py # 技术指标计算模块 │ └── factors.py # 因子计算模块 ├── strategy/ │ ├── base.py # 策略基类 │ ├── ma_cross.py # 均线交叉策略 │ └── multi_factor.py # 多因子选股策略 ├── backtest/ │ ├── engine.py # 回测引擎 │ └── metrics.py # 绩效指标评估 ├── visualization/ │ ├── plot_kline.py # K线图绘制 │ └── plot_results.py # 净值曲线与回撤图绘制 ├── docs/ │ ├── 设计文档.md │ ├── 使用说明.md │ └── 测试报告.md └── main.py # 程序入口,命令行调用架构设计遵循两条底线:数据层不直接依赖策略层,策略层不依赖具体的数据源。怎么理解?策略模块只需要拿到标准格式的DataFrame(列名固定为date、open、high、low、close、volume),至于数据是来自akshare还是tushare,还是你自己手工收集的Excel,都不关心。这样设计带来两个好处:一是调试阶段可以先用本地CSV文件模拟数据,不用反复请求网络接口;二是后续换数据源接入成本极低,只需修改数据获取层,其他代码全部复用。我在指导学生做这类项目时,一直强调一个观点:代码里的模块边界,远比代码行数更值钱。把这个目录结构刻进脑子里,后面每一个模块的代码都是往这个骨架里填肉。
2. 数据模块:股票数据的获取与预处理
2.1 数据源选型:开源接口对比
这个项目我推荐用akshare作为主数据源,是因为它免费、无需注册token、接口丰富,对国内A股市场的覆盖非常全面,特别适合教学和毕设场景。tushare虽然老牌,但新版接口强制要求注册并获取token,而且积分不够的话很多接口权限受限,对评审演示来说是个不稳定的变量。当然,如果你的网络环境访问GitHub没问题,pip install akshare只是几分钟的事。
akshare获取A股日线数据的接口是这样的:
import akshare as ak # 获取A股历史行情数据,复权方式:qfq(前复权)、hfq(后复权)、""(不复权) df = ak.stock_zh_a_hist( symbol="000001", period="daily", start_date="20200101", end_date="20240101", adjust="qfq" )这个方法返回的字段包括日期、开盘、收盘、最高、最低、成交量、成交额、振幅、涨跌幅、涨跌额、换手率,基本够用了。但在使用前必须做一次字段标准化,把中文列名转换成程序内部统一的英文列名。这一步非常重要,因为不同数据源返回的字段名不一样,统一以后策略代码才不用跟着数据源变动。
def normalize_columns(df): """统一数据字段命名规范""" column_mapping = { "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume", "成交额": "amount", "换手率": "turnover" } df = df.rename(columns=column_mapping) df["date"] = pd.to_datetime(df["date"]) df = df[["date", "open", "high", "low", "close", "volume", "amount", "turnover"]] return df.sort_values("date").reset_index(drop=True)2.2 数据清洗与复权处理
数据清洗是整个项目里最不性感但最容易翻车的地方。先说复权。股票在历史交易中会经历分红、送股、配股等事件,这些事件会导致除权除息,价格出现跳空缺口。比如一只股票除权前收盘价10元,10送10之后价格变5元,如果你直接拿原始价格做技术分析,均线、MACD这些指标全部失真。解决方案就是复权:前复权保持最新价格不变,调整历史价格;后复权则相反。做技术指标计算时一定要用前复权数据,这是行业标准做法。
其次是停牌和缺失值的处理。A股停牌很常见,有些股票停牌几个月,数据里会出现长时间的日期空洞。如果只按交易日获取,缺失值可能直接不返回,不会出现NaN,但你要做多股票横向对比,比如“策略选股选出的10只股票”,它们的交易日可能不一致,这时就要做日期对齐。我的方案是:按目标交易日的并集建一个全量的日期索引,然后对每只股票用reindex方法补全缺失日期,用ffill方法填充缺失的价格,用0填充缺失的成交量和成交额。这个处理逻辑保证后续选股和回测不会因为NaN报错。
def clean_and_align(stock_dict, trade_dates): """清洗并对齐多只股票数据""" aligned = {} for code, df in stock_dict.items(): df = df.set_index("date") df = df.reindex(trade_dates) df[["open", "high", "low", "close"]] = df[["open", "high", "low", "close"]].ffill() df[["volume", "amount", "turnover"]] = df[["volume", "amount", "turnover"]].fillna(0) df["code"] = code aligned[code] = df.reset_index() return aligned2.3 本地缓存与数据库选型
每次跑程序都从网络接口拉全部历史数据是极度浪费时间的行为,而且akshare的公共接口有频率限制,频繁请求会被临时封IP。解决方式是在本地做缓存。我建议用两个层级:第一层是原始数据,按“股票代码_周期_复权方式.parquet”的命名方式存到data/cache目录;第二层是清洗后的数据,同样用parquet存储,与原始数据分开管理。
为什么用parquet文件而不是CSV?第一,parquet是列式存储,读大文件比CSV快了一个数量级;第二,它原生支持pandas的dtype,日期列、数值列不会出现CSV那种“读进来类型全变了”的问题。这里有一个小坑要注意:akshare接口返回的日期列有时候是字符串,有时候是datetime,落到parquet再读出来,类型可能是object,所以你在标准化函数里强制转换日期类型,而不是依赖数据源自身的类型。
关于数据库,我测试过SQLite的方案,对这个项目来说基本够用。如果后续做更复杂的查询,比如“找出2018年以来所有涨停超过3次的股票”,用SQLite写SQL会方便很多。但毕设阶段用parquet文件缓存已经足够,不必引入数据库的复杂度。SQLite方案我强烈建议放进文档的“后续扩展”章节里提一句,让老师知道你有这个意识就够了。
3. 技术指标与选股策略的实现
3.1 核心指标计算方法精讲
选股策略的核心是计算技术指标。我选了四个最有代表性的指标来覆盖不同维度:MA均线(趋势类)、MACD(趋势动量类)、RSI(超买超卖类)、KDJ(随机指标类)。这四个指标的代码实现网上到处都是,但关键在于要理解为什么要用它们、怎么正确计算、怎么避免常见的错误。
先看均线MA。简单移动平均线的计算没什么难度,pandas里一行解决:df["ma5"] = df["close"].rolling(5).mean()。但要注意,移动平均线本身就是一种滞后指标,均线周期越短,对价格越敏感,也越容易产生假信号;周期越长,趋势越平滑,但滞后越严重。所以均线交叉策略里,短均线和长均线的参数选择至关重要。我在项目里用MA5和MA20的交叉作为基础信号,同时加入一个确认机制:短期均线从上到下穿越长期均线后,还要等一个交易日确认,防止价格在均线附近反复横跳产生虚假的交叉信号。
MACD的计算逻辑是基于EMA(指数移动平均)的,核心参数是12、26、9,这套参数来自于经典技术分析理论,但如果你的股票池全是日线级别的短线交易,完全可以根据品种特性调整参数。有一个答辩时老师必问的细节是:为什么用EMA而不是SMA?EMA对近期价格的权重更高,反应更灵敏,更适合捕捉趋势的变化。实际编码时,MACD的计算有个容易出错的地方,就是EMA的初值初始化方式。pandas的ewm方法的adjust参数决定了EMA的初始化方式,adjust=True(默认)会用指数加权平均的方式,而技术分析软件里的MACD通常在第一个数据点直接取close的值作为初值。为了保证指标和同花顺、通达信等行情软件一致,一定要设置adjust=False。
def compute_macd(df, fast=12, slow=26, signal=9): """计算MACD指标,与行情软件对齐""" ema_fast = df["close"].ewm(span=fast, adjust=False).mean() ema_slow = df["close"].ewm(span=slow, adjust=False).mean() df["macd_dif"] = ema_fast - ema_slow df["macd_dea"] = df["macd_dif"].ewm(span=signal, adjust=False).mean() df["macd_hist"] = (df["macd_dif"] - df["macd_dea"]) * 2 # 柱状图 return dfMACD柱状图乘不乘2这个细节,不同软件的定义不完全一样,但乘2是更常见的做法。如果答辩老师用行情软件验证你的输出,没乘2可能会被认为算错了。
3.2 选股策略的实现:从策略思想到代码
选股策略我实现了一个可配置的规则引擎,支持从多个维度叠加筛选条件。下面这段代码展示了一个基础版的多因子选股逻辑,它会在每个调仓日评估全市场股票,选出满足条件的股票进入候选池:
def select_stocks(universe, date, config): """选股引擎:基于多因子打分选股""" candidates = [] for code, df in universe.items(): df = df[df["date"] <= date].tail(60) # 只用截至当天的数据,防止未来函数 if len(df) < 30: continue # 因子1:均线趋势(收盘价在MA20之上) ma20 = df["close"].rolling(20).mean().iloc[-1] if df["close"].iloc[-1] < ma20: continue # 因子2:MACD金叉状态 if df["macd_dif"].iloc[-1] <= df["macd_dea"].iloc[-1]: continue # 因子3:RSI不处于超买区(避免追高风险) rsi6 = compute_rsi(df["close"], 6).iloc[-1] if rsi6 > 80: continue # 因子4:近20个交易日区间涨幅,剔除过度上涨的标的 pct_change_20d = df["close"].iloc[-1] / df["close"].iloc[-20] - 1 if pct_change_20d > 0.3: continue candidates.append(code) return candidates这里每一个因子都对应一种金融逻辑:均线趋势因子表达的是“顺势而为”;MACD金叉表达的是“动量启动”;RSI超买限制表达的是“风险控制”;区间涨幅限制表达的是“不追高”。答辩的时候,老师问“为什么选这些因子”,你把这四句话答出来,项目深度就完全不一样了。要注意,不要把选股参数写成“拍脑袋”的数值,我在设计时专门做了一组参数敏感性测试(在4.3节会讲),证明MA20、RSI=80这组阈值不是随便写的,而是基于历史数据的分布特征来选择。
3.3 策略参数选择的逻辑与调优
量化圈有句话叫“参数是你的敌人”。参数越多,过拟合的风险越大。很多毕设项目在回测里收益曲线非常漂亮,一放到实盘就拉胯,原因就是参数是在历史数据上反复试出来的。这里我给两阶段参数选择法:第一步粗选,用网格搜索遍历不同参数组合,选出在训练集上表现top10的参数组合;第二步精选,把这些参数组合放到验证集上跑,选在验证集上表现稳定的组合,而不是训练集上最佳的那个。这样做的核心逻辑是防止“幸存者偏差”式的过度优化。考虑到毕设的周期,参数搜索范围不需要太大,但要注意:最终报告里必须写明训练集和验证集的划分区间,这是答辩时体现你专业度的加分项。本项目的参数默认值如下:
- MA短周期:5,长周期:20
- MACD参数:12/26/9
- RSI周期N=6,超买阈值80,超卖阈值20
- 调仓频率:每20个交易日调仓一次
注意:RSI超买阈值取80而不是常见的70,是通过观察A股市场日线RSI分布后发现,A股波动大,70的阈值在牛市中会频繁触发,导致选股池经常空仓。这个细节不要照搬,要结合你的股票池和区间验证。
4. 回测系统:验证策略是否真的有效
4.1 回测引擎的设计思路
回测是把策略放到历史数据上做模拟交易。这里有两个极端:一种是用现成的backtrader框架,配置起来复杂,学习成本高;另一种是极其简陋地算一笔“期初买入、期末卖出”的收益率,这在逻辑上根本不叫回测。我的建议是:自己写一个轻量回测引擎,代码量控制在200行以内,但把关键逻辑全部体现出来。原因有两个:一是答辩的时候,老师让你讲回测原理,你自己写的代码自然讲得清;二是用现成框架容易被老师追问框架内部的某些实现细节,一旦答不上来就很扣分。
回测引擎的核心逻辑是一个循环:按照设定的调仓频率(比如每20个交易日),扫描所有持有股票,检查是否触发卖出条件;然后再调用选股引擎,选出新的股票池;最后按等权重买入。等权重是初版最合理的资金分配方式,不要一开始就搞什么资本资产定价模型、凯利公式,那是自己给自己挖坑。持仓权重可以在文档里作为未来扩展点来写。
class BacktestEngine: def __init__(self, cash=100000, commission=0.0003, rebalance_days=20): self.initial_cash = cash self.cash = cash self.commission_rate = commission self.rebalance_days = rebalance_days self.positions = {} # code -> shares self.nav_curve = [] # 记录每日净值佣金费率设计为万三(0.03%),这符合国内券商主流的佣金水平,计算时要包含买入和卖出双边费用。印花税卖出时收取千分之一,这也是A股的特色,很多新手回测忘掉印花税,导致回测收益虚高。我见过太多人回测年年化50%,一算上手续费和印花税立刻腰斩。项目文档里专门有一节讲“回测中的摩擦成本”,放在测试报告里,很能体现你的专业素养。
4.2 绩效指标全解:年化收益、最大回撤、夏普比率
回测跑完之后,光给一个“总收益率”是远远不够的,一套完整绩效评估至少包含五个指标:
| 指标 | 计算公式 | 意义说明 |
|---|---|---|
| 年化收益率 | (期末净值/期初净值)^(252/交易日数)-1 | 把收益标准化到一年维度,便于横向比较 |
| 最大回撤 | max(净值峰值到后续最低点的跌幅) | 衡量策略在最坏情况下亏多少,风险核心指标 |
| 夏普比率 | (策略年化收益-无风险利率)/年化波动率 | 每承担一单位风险换来多少超额收益,大于1算合格 |
| 胜率 | 盈利交易次数/总交易次数 | 反映策略的稳定性 |
| 盈亏比 | 平均盈利/平均亏损 | 结合胜率评估策略期望收益 |
最大回撤的计算逻辑看起来简单,但实现上有坑。正确的计算方法是用累计净值的最高点(running maximum)减去当前净值,然后除以当时的峰值,取整个过程的最大值。代码实现:
def calculate_max_drawdown(nav_series): """计算最大回撤""" running_max = nav_series.cummax() drawdown = (nav_series - running_max) / running_max return drawdown.min()注意这里用cummax计算历史的净值最大值,再和当前净值比较,精确对应“如果在历史最高点买入,最多会亏多少”这个语义。夏普比率计算中,无风险利率用年化2%来近似,年化波动率用日收益率的标准差乘以sqrt(252)。这些参数在config配置里都集中维护,避免魔法数字散落各处。
4.3 回测中的坑:未来函数与幸存者偏差
回测最致命的bug就是未来函数。所谓未来函数,就是在回测的过程中使用了“当时还获取不到”的信息。举个最常见的例子:用整段历史均线去做选股,回测到2020年的时候,用了2021年的数据来计算平均。这在代码里非常隐蔽,尤其是做数据清洗时,如果你一次性对全量数据算指标,然后切片回测,某些指标会无意中“穿越”。我的解决方案是:在回测循环里,选股和算指标严格使用截至当天的数据切片,绝不使用后视数据。上面选股引擎代码里的df[df["date"] <= date].tail(60)就是干这个的,这一步务必写进你的代码,答辩时不主动展示这个设计等于把致命bug藏起来了,反过来,主动讲清楚就变成加分项。
幸存者偏差是另一个无声的杀手。今天看A股3000多只股票,但如果你的股票池取自“当前还在上市交易的股票”,你天然忽略了那些已经退市的。结果就是你的回测只统计了“活下来的股票”,收益虚高。处理这个问题的标准做法是:数据获取时,用某历史时间点的存量股票列表作为股票池。但对毕设而言,取全市场股票数据工作量太大,我采用的折衷方案是:采用沪深300成分股作为股票池,并在报告里明确说明。这个范围全市场代表性足够,同时规避了部分退市风险。老师追问这一点时,你可以清楚说明“这是权衡数据完整性、计算复杂度和代表性之后的选择”,这比完全不做任何处理要强得多。
5. 可视化展示:结果呈现与答辩利器
5.1 K线图与指标叠加
数据可视化在毕设答辩中扮演的角色远比你想象的更重要。你不需要做出和行情软件一模一样的交互式K线图,但你要让评委在5分钟内看懂你的策略表现。
K线图用mplfinance库实现,这个库专门用于绘制金融数据图表,比直接用matplotlib手搓K线省力得多。如果你的项目是课程设计,建议主推pyecharts的K线图,可以做简单的HTML交互。mplfinance的基本用法如下:
import mplfinance as mpf def plot_kline_with_ma(df, code, save_path="output/"): """画K线图,叠加MA和MACD""" df = df.set_index("date") df = df[["open", "high", "low", "close", "volume"]].copy() ma5 = df["close"].rolling(5).mean() ma20 = df["close"].rolling(20).mean() ap_list = [ mpf.make_addplot(ma5, color="orange", width=1.2), mpf.make_addplot(ma20, color="blue", width=1.2), ] mpf.plot( df, type="candle", addplot=ap_list, volume=True, style="yahoo", title=f"{code} K线图", figsize=(14, 8), savefig=dict(fname=f"{save_path}{code}_kline.png", dpi=150) )样式选“yahoo”比较贴近国内软件的显示风格,红色涨绿色跌。画出来之后,放到论文的“案例分析”章节里,配合一两只典型股票的买卖点标注,是答辩现场最直观的说服力。
5.2 净值曲线与回撤区间展示
策略的绩效结果展示,我做成了一张双面板图:上半部分是策略净值曲线(蓝色线)和基准沪深300指数净值曲线(灰色线)的对比,下半部分是回撤柱状填充图。两条曲线对比是最直观的绩效呈现方式:
import matplotlib.pyplot as plt def plot_nav_and_drawdown(nav, benchmark, date_index, save_path): fig, axes = plt.subplots(2, 1, figsize=(14, 9), sharex=True) axes[0].plot(date_index, nav, label="策略净值", color="#2E5E8C", linewidth=2) axes[0].plot(date_index, benchmark, label="沪深300", color="#999999", linewidth=1.5) axes[0].set_ylabel("净值") axes[0].legend() axes[0].set_title("策略净值 vs 基准") running_max = nav.cummax() drawdown = (nav - running_max) / running_max axes[1].fill_between(date_index, drawdown * 100, 0, color="#C0504D", alpha=0.6) axes[1].set_ylabel("回撤(%)") plt.tight_layout() plt.savefig(save_path, dpi=150)这张图有讲究:策略跑赢基准的部分正是这个项目的核心卖点,回撤图的红色面积区域越小、恢复越快,说明策略的稳定性越好。
5.3 选股结果导出与交互页面
选股引擎输出的最终结果,我同时提供两种出口:一是控制台打印汇总信息,方便开发和调试;二是导出为Excel文件。Excel导出用pandas的to_excel接口实现:
def export_picks(picks_dict, date, config): """导出一期选股结果到Excel""" rows = [] for code, rank_score in picks_dict.items(): rows.append({ "股票代码": code, "选股日期": str(date), "排序分值": round(rank_score, 4), "提示": "该成果仅供学习研究,不构成任何投资建议" }) df_out = pd.DataFrame(rows) df_out.to_excel(f"output/picks_{date}.xlsx", index=False)这里多说一句:导出文件里的免责声明一定要写。毕设项目涉及金融数据,虽然用途是学习研究,但该有的合规意识还是要有的,这一行字老师看了会觉得你做事严谨。
6. 源码、文档与使用说明的整理
6.1 源码共享与工程规范
源码的完整性是毕设评分的硬指标,但很多人以为“代码能跑”就算源码完整,这是一个严重的误解。一份好的源码交付物应该具备三个特征:第一,能一键运行,不需要手动改路径改配置;第二,代码风格统一,变量命名有意义,关键函数有docstring;第三,README能够讲清楚项目是什么、怎么装、怎么跑、结果存在哪里。
在代码规范方面,我给项目里的核心函数都写了docstring,展示一段:
def compute_rsi(close_series, period=14): """ 计算RSI相对强弱指标。 Parameters ---------- close_series : pd.Series 收盘价序列 period : int RSI计算周期,默认14,本项目使用6 Returns ------- pd.Series RSI值序列,取值区间[0, 100] """docstring不仅是为了老师看代码时给你加分,也是你自己过了几个月再回来看代码时,能快速想起每个函数用途的关键。
6.2 项目文档结构与写作思路
关于项目文档,这是很多学生最头疼的部分,也是最容易拉开差距的部分。我把整套文档拆成三份:设计文档、使用说明、测试报告。
设计文档的重点是画清楚架构图和讲明白技术选型的原因。不需要写得太学术,但要体现出决策过程。比如“为什么用akshare而不用tushare选股票数据”,你写“因为akshare免费且无需token”,这是一个理由;你写“对比了akshare与tushare的接口稳定性、数据准确性、使用便捷性后发现,akshare在免登录、覆盖A股全市场维度上更适合本项目的数据使用需求”,这就是另一个分数档次。实际上我从2.1节开始就在教你这种“选型讲述逻辑”。
使用说明文档应当让一个从没碰过你代码的人,按照文档15分钟内把项目跑起来。结构要完整:环境准备、依赖安装、运行命令、常见问题。这部分建议写得啰嗦一点,把每条命令的预期输出都写出来。测试报告则记录你在什么环境、用什么数据、跑出来什么结果、是否存在已知问题。它不是“证明策略赚钱”的功劳簿,而是完整的实验过程记录。
6.3 答辩演示与使用说明编写技巧
最后说答辩这块。实话实说,很多人代码做得不错,偏偏答辩讲得一团糟,最后拿不到理想分数。这里给你一个经典的“三分钟电梯演讲”结构来组织使用说明里的快速开始内容,同时作为你答辩的开场:
“我将答辩演示分为三个层级。第一分钟,讲项目定位和应用价值——项目是做什么的,面向什么场景;第二分钟,讲技术路线——数据从哪来、策略怎么设计、回测怎么实现、结果怎么看;第三分钟,现场跑通在沪深300成分股上的选股和回测流程,给出收益、回撤等核心指标。”
现场演示环节需要注意的事项整理成一个清单:
- 提前把代码和数据准备到位,确保演示时不会出现网络请求超时、token过期等意外。
- 缓存文件保留好,实在没网也能跑通一个完整的demo。
- 提前准备好“如果策略收益很差怎么办”的应答口径,比如“策略在熊市段的回撤控制仍可接受,说明风险控制逻辑有效;收益未跑赢阶段热点的原因是该策略属于中低频趋势策略,在震荡市的天然劣势是XX,由此可引出多策略融合的未来优化方向”。这一句话出来,回答的格局就大了。
- 不要试图在演示时现场改代码,任何修改都要提前测试。
7. 常见问题与排查技巧实录
7.1 数据获取的“拦路虎”
我在开发和调试项目过程中遇到不少坑,这里挑几个典型的说一下。
第一个是akshare接口字段变更。akshare是开源项目,接口一直在迭代,我在项目开发过程中就遇到过一次接口字段从“日期”变成“date”、返回字段顺序调整的问题。代码里的字段标准化函数就是为这个设计的。如果你在某天突然发现程序报KeyError: '日期',大概率是akshare更新导致字段名变了,解决办法有两个:一是去akshare的GitHub文档查最新接口,二是在requirements.txt里锁死akshare的版本。我用的是第二种方案,锁死的版本配合虚拟环境,环境隔离后从来没出过问题。
第二个是DataFrame的索引问题。pandas的布尔索引、切片在df是DataFrame和Series时行为不一样。比如df[df["date"] <= date]如果df是DataFrame返回DataFrame,但如果是Series就会返回Series,后续接tail方法没问题,但如果接的是列操作就会报错。我习惯在数据加载函数里统一deliverDataFrame类型,并加上类型断言,避免这类边界问题。
7.2 指标计算的常见陷阱
技术指标计算里最容易出错的环节是数据对齐。比如前面我提到用EMA计算MACD,在开始阶段的数值和行情软件有差异,这是因为EMA的初始值设置。还有计算RSI时用到“平均涨幅”,有的实现用简单平均,有的用平滑平均,结果差异会很大。以RSI为例,正确的平滑算法是Wilder's smoothing,而不是简单移动平均。如果直接用df["close"].diff().rolling(6).mean(),算出来的RSI和国家公布的数值偏差极大。我用的是ewm(alpha=1/period)做平滑,这样算出来的RSI与主流行情软件基本一致。
def compute_rsi(close_series, period=14): """RSI计算,使用Wilder平滑""" delta = close_series.diff() gain = delta.where(delta > 0, 0.0) loss = -delta.where(delta < 0, 0.0) avg_gain = gain.ewm(alpha=1/period, adjust=False).mean() avg_loss = loss.ewm(alpha=1/period, adjust=False).mean() rs = avg_gain / avg_loss rsi = 100 - 100 / (1 + rs) return rsi7.3 回测结果异常分析
这里整理了我在回测阶段遇到的高频问题和排查方法,做成速查表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 回测收益极高,年化超过200% | 未来函数、幸存者偏差 | 检查是否有截至当天的数据切片;确认股票池历史存量 |
| 净值曲线出现陡降 | 数据缺失导致价格异常 | 检查清洗阶段是否做了ffill补值;检查复权数据是否有异常值 |
| 选股结果为空 | 筛选条件过严、股票池数据未覆盖 | 打印各条件通过的数量,逐步放宽阈值 |
| 回测结果每次运行不同 | 数据源更新、未缓存 | 启用本地缓存;锁版本 |
| MACD信号和行情软件对不上 | EMA初值问题 | 确认ewm的adjust=False |
还有一个隐蔽的坑:当你从akshare同时获取多只股票数据时,如果没有sleep间隔,会被接口限流。我写了一个带重试和退避的请求封装函数:
import time import random def fetch_with_retry(func, retries=3, *args, **kwargs): """带重试机制的接口请求封装""" for attempt in range(retries): try: return func(*args, **kwargs) except Exception as e: if attempt < retries - 1: time.sleep(random.uniform(1, 3)) continue raise e7.4 环境配置与交付演示准备
最后说说“项目拿过去跑不起来”这一最痛问题。很多项目卡在环境配置,而不是代码本身。我总结了一套兼容性最好的环境配置,用于这个项目:
- Python版本:3.9.x(过新版本有库兼容风险,过旧版本不支持部分语法)
- pandas:1.5.3
- numpy:1.24.x
- akshare:锁1.11.x(新版本接口变动大)
- matplotlib:3.7.x
- pyecharts:2.0.x
- mplfinance:0.12.10b0
如果你用requirements.txt一键安装,记得在文档里给出“推荐创建虚拟环境”的命令,帮使用者规避掉环境污染的坑。
# 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # Windows下为 venv\Scripts\activate # 安装依赖 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple提示:如果使用pypi官方源在国内安装某些包特别慢,建议配置清华镜像源。这个细节写进使用说明,体验差异巨大。另外,这个项目我已经在各版本组合下测试,上面的版本组合匹配度是经过验证的,尽量不要自行混搭最新版。
写在最后
做这个项目的最大体会就是,代码本身只是整个项目的一半,另一半是“你讲不讲得清楚为什么这么做”。数据清洗为什么这么设计、指标为什么用EMA做平滑、回测为什么用等权重而不是半仓分批、为什么选沪深300而不选全市场,这些问题每一个都想清楚了,你的答辩就稳了。最后再分享一个小技巧:在提交代码之前,找一个完全没接触过你项目的同学,让他只看你的README和使用说明,独立把项目跑通。如果他能不看代码就完成所有操作,说明你的文档过关了;如果他卡在某个地方,不用怀疑,那里就是你文档需要补的地方。这个小小的“人肉测试”,比你自己检查十遍都有效。
本文还有配套的精品资源,点击获取