Python股票系统源码详解:从环境搭建到量化回测实战

发布时间:2026/10/27 16:59:00
Python股票系统源码详解:从环境搭建到量化回测实战

简介:面向毕业设计、课程设计及个人实践场景的Python股票交易系统源码包,完整覆盖行情数据获取、清洗处理、策略回测、风险控制与可视化界面等模块。开发者可在Tkinter/PyQt图形界面基础上,实现移动平均线、RSI、布林带等经典交易策略,并通过supervisord、定时任务脚本完成自动化运行;前端页面与图表组件可直接复用,便于快速搭建可交互的交易监控面板。资源共143个文件,以29个Python脚本为核心,辅以JavaScript、HTML、CSS构成前端展示层,另有Shell脚本、Dockerfile与Nginx配置支持一键式部署,压缩包仅2.19MB,轻量易用。已有524人学习下载,适合具备一定Python基础、希望将编程与金融分析结合实践的学生及爱好者。包内目录结构清晰,可快速梳理从数据采集、策略执行到Web展示、运维部署的完整链路;源码整体可读性较好,可作为二次开发与功能扩展的起点。

1. 把这份 Python 股票系统源码下载下来之后,先别急着解压

拿到一个Python开发的股票系统源码.zip,绝大多数人的第一反应都是双击解压,然后双击main.py等着看曲线。我劝你先停一下,因为真这么干,大概率第一步就翻车——缺依赖、缺配置、Python 版本不对,三连击直接劝退。

这份源码本质上是一个「行情数据采集 → 技术指标计算 → 策略信号生成 → 回测验证 → 图表输出」的完整框架,面向的是想做量化投研、但不想从零搭轮子的从业者和学习者。它能帮你省掉搭骨架的时间,让你把精力放在策略本身。但它不是自动交易软件,不会给你连接券商实盘下单,更不是那种“输入代码就赚钱”的黑匣子。

适合谁?两类人:一是刚入 Python 量化、想看看真实项目怎么组织代码的初学者;二是已经写过零散脚本、想把「采集、计算、回测」串成一条流水线的进阶开发者。在动手之前,你只需要一点:明白这份源码的价值在框架,不在“预测涨跌”。下面我按拆项目的顺序,把这套代码从解压到跑通完整过一遍。

2. 为什么用 Python 写股票系统:技术栈选型与核心数据链路

2.1 选 Python 不是情怀:生态、库、迭代速度三个硬理由

你可能会问,股票系统用 Java、C++ 写也大有人在,为什么这份源码选了 Python?答案不复杂:日线级别、分钟级别的策略研究和回测,Python 的效率和开发速度是最划算的。

先说生态。pandas 的 DataFrame 对时间序列处理几乎是统治级的,rolling()、ewm()两行代码就能算均线和指数平滑,这在 C++ 里要自己写几十行。numpy 做向量化计算,回测时整个策略的信号生成可以用布尔数组一次性算完,不用写 for 循环。再加上 matplotlib 系的数据可视化,从数据到图表全程不换语言。

再说迭代速度。做策略的人一天要试十几个参数组合,Python 改完就能跑,跑完就能看结果。用编译型语言做这个事,编译等待时间会严重压缩你的试错空间。当然,如果是做毫秒级高频交易,Python 不适合——但那份源码的场景显然是日线或者分钟线级别的策略研究,这个定位下 Python 是最务实的选择。

常见的一个误区是拿 Python 股票系统和「源码建站」那一类 PHP 项目对比。网页源码是解压扔到服务器就能跑,Python 项目不是,它需要环境、依赖、配置三步全部就位。这不是源码有问题,是运行模型本身就不同。

2.2 数据链路全景:从行情源到策略引擎的六个环节

任何股票系统的核心都是数据流。这份源码的功能能不能发挥,取决于你理不理解这条链路。我拆成六段来看:

  1. 数据源——免费公开行情接口或者第三方库(比如 akshare 这类聚合接口),负责把原始 K 线数据拉回来。
  2. 采集层——网络请求、限频控制、异常重试,把数据从接口安全地搬运到本地。
  3. 存储层——通常是 CSV 文件或者 SQLite,避免每次运行都重复请求接口。
  4. 指标计算层——基于原始 OHLCV 数据计算 MA、MACD、KDJ 等技术指标。
  5. 策略引擎——把指标转换成买卖信号,模拟持仓过程。
  6. 可视化层——把 K 线、信号、资金曲线画出来,供人判断。

给我的经验是,拿到源码第一件事不是看策略写了什么,而是先确认这条链路是否完整。链路断了,策略再好也跑不出结果。

2.3 源码目录结构解读:运行前先摸清每个文件的职责

这类源码解压后通常是一个标准的 Python 工程目录。我建议你先把目录树打印出来,对照着确认每个模块是不是都在。你用下面的命令看:

find . -maxdepth 2 -type f -name "*.py" | sort # 或者 Windows 下用 dir /s /b *.py

我按常见做法整理一个典型的目录结构,你手上的源码大概率跟这个神似:

stock_system/ ├── config.yaml # 全局配置:股票池、数据源、策略参数 ├── requirements.txt # 第三方依赖清单 ├── main.py # 程序入口 ├── collector/ │ ├── data_fetcher.py # 行情数据拉取(网络请求) │ └── stock_api.py # 数据源接口适配 ├── storage/ │ └── cache.py # 本地缓存读写(CSV/SQLite) ├── indicator/ │ ├── ma.py # 均线指标 │ ├── macd.py # MACD 指标 │ └── kdj.py # KDJ 指标 ├── strategy/ │ ├── base.py # 策略基类 │ └── ma_cross.py # 双均线策略示例 ├── backtest/ │ ├── engine.py # 回测引擎 │ └── metrics.py # 收益/回撤/夏普等指标 └── visualize/ └── chart.py # K 线图与买卖点绘制

看一下你解压出来的目录是不是覆盖了这些模块。如果缺了indicator/或者backtest/,那说明这份源码可能不是完整版,后续跑到对应环节会报ModuleNotFoundError。这个检查花不了一分钟,但能帮你避免后面白折腾。

另一点值得在运行前做的是确认 Python 版本要求。通常这类源码会在requirements.txt或者README里标注 Python 3.8 到 3.11。如果没标注,我一般会直接用 3.10 或者 3.11,这两个版本对 pandas 和 numpy 的兼容性最稳。

3. 把源码跑起来:环境准备、依赖安装与配置核对

3.1 Python 版本选择和虚拟环境隔离

先说结论:不要用系统全局的 Python 直接跑,隔离环境是必须的。原因很简单,你的机器上可能已经装过别的版本的 pandas、numpy,版本不对会让这份源码疯狂报错,而且报错信息往往跟真实问题隔着一层,非常难排查。

如果你还没装 Python,先去官网下安装包,装的时候记得勾选「Add Python to PATH」——这一步很多人漏了,导致后续在命令行里敲python提示找不到命令。装好之后,我一般会做三件事:确认版本、创建虚拟环境、激活虚拟环境。命令如下:

python --version # 确认输出是 Python 3.8 及以上,例如 Python 3.10.11 python -m venv venv # 在当前目录创建虚拟环境 venv,推荐放在源码根目录 # Windows 激活方式: venv\Scripts\activate # macOS / Linux 激活方式: source venv/bin/activate

创建虚拟环境的逻辑是给这个项目单独隔离出一套 Python 解释器和第三方库目录,不会污染全局环境,也不会被全局环境里已有的库干扰。激活后命令行前面会多出一个(venv)前缀,看到这个就说明你已经进入了虚拟环境。在这之后安装的所有库都只存在于venv目录内部。

提示:如果激活失败,Windows 用户检查一下是不是权限问题,用 PowerShell 管理员模式跑一次。macOS 用户如果用的是 zsh,确认路径没有拼错。

3.2 依赖清单逐个核对:pandas、numpy、requests、matplotlib

虚拟环境激活后,下一步是安装依赖。这份源码的requirements.txt里通常会列出 pandas、numpy、requests、matplotlib、PyYAML 这几个基础库,可能还会有可选的 akshare。直接把整个清单装进去是最快的:

pip install -r requirements.txt

如果你装到一半报错,或者requirements.txt里是空的,那就手动逐个装。这里有一个很常见的问题——很多人卡在「python 安装 numpy 库的方法」这一步,具体表现是pip install numpy之后 import 还是报错。常见原因是 pip 源太慢或者超时,换国内镜像源可以解决:

pip install numpy pandas requests matplotlib PyYAML -i https://pypi.tuna.tsinghua.edu.cn/simple

装完之后验证一下,不要直接跑主程序,先确认每个库能不能正常导入。这一步花不了二十秒,但能在运行前过滤掉一半的翻车原因:

python -c "import pandas as pd; import numpy as np; import matplotlib; print('ok')"

如果输出了ok,说明核心依赖没问题。如果这里报错,优先检查是不是 pip 把包装到了别的 Python 环境里——用pip --version看它指向的路径是不是当前虚拟环境。

3.3 配置文件逐项拆解:股票池、时间区间、参数项

依赖装完之后,最容易被忽略但最值得认真看的就是配置文件。这类源码一般用 YAML 格式存配置,名字叫config.yaml。我在实战里见过太多人跳过配置直接运行,跑出来的结果跟自己预期完全对不上,最后发现是股票代码没改、时间区间不对。

一个典型的配置文件长这样:

# 股票池与数据区间 stock_pool: - "600519" # 贵州茅台 - "000001" # 平安银行 start_date: "2022-01-01" end_date: "2024-12-31" # 数据源配置 data_source: provider: "akshare" # 可选 akshare / sina / tencent retry_times: 3 # 失败重试次数 timeout: 10 # 单次请求超时(秒) # 技术指标参数 indicator: ma_short: 5 # 短均线周期 ma_long: 20 # 长均线周期 macd_fast: 12 macd_slow: 26 macd_signal: 9 # 回测参数 backtest: initial_capital: 100000 # 初始资金 10 万 commission: 0.0003 # 单边手续费 slippage: 0.001 # 滑点比例

每个参数都不是摆设。ma_short和ma_long直接决定双均线策略的买点卖点;initial_capital影响回测的仓位;commission和slippage如果不设置,回测结果会偏乐观,仓位大的时候误差很明显。

我的习惯是第一次运行只改股票池和日期区间,其他参数一律不动。等基线跑通了,再逐个调参,这样出了问题你知道是自己改了哪一项导致的,排查成本最低。

3.4 首次运行与日志验证:跑通基线才算数

配置改好之后,终于可以跑主程序了。在虚拟环境激活状态下执行:

python main.py

第一次运行建议盯着终端输出。正常情况下你会看到几类信息:数据采集的进度、指标计算的完成提示、回测的起止时间、最终的资金曲线和收益指标。如果只是闪一下就退出,那多半是程序里有异常没被捕获,你可以在命令后面加上-u强制不缓冲输出:

python -u main.py

跑完之后,检查一下项目目录里是不是生成了新的文件,比如data/目录下的 CSV 文件、results/目录下的图表。如果这些产物都出现了,说明这条链路已经通了。如果只跑了数据采集就停了,问题大概率卡在指标计算或者策略引擎,接下来要按模块单独调试。

注意:免费行情接口经常有限频限制,如果日志里出现请求频率过高或者HTTP 429,把data_source里的超时和重试参数调大,或者把采集频率降到每秒一次以下。

4. 核心模块实战:行情采集、指标计算、策略信号与可视化

4.1 行情数据采集:接口适配与本地缓存

行情采集是整条链路的第一个环节,也是最容易因为外部原因挂掉的地方。以常见的 akshare 第三方库为例,拉取日线数据的代码结构大致如下:

# collector/data_fetcher.py import akshare as ak import pandas as pd def fetch_daily_data(symbol: str, start_date: str, end_date: str) -> pd.DataFrame: """ 拉取个股日线数据 :param symbol: 股票代码,如 '600519' :param start_date: 起始日期 'YYYYMMDD' :param end_date: 结束日期 'YYYYMMDD' :return: 包含 OHLCV 的 DataFrame """ df = ak.stock_zh_a_hist( symbol=symbol, period="daily", start_date=start_date, end_date=end_date, adjust="qfq" # 前复权,避免除权跳空 ) # 重命名列为统一格式,方便后续指标计算 df.rename( columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume", }, inplace=True, ) return df[["date", "open", "high", "low", "close", "volume"]] if __name__ == "__main__": data = fetch_daily_data("600519", "20220101", "20241231") print(data.head())

这段代码的逻辑是:调 akshare 的接口拉取指定股票在区间内的日 K,然后做列名归一化。adjust="qfq"这个参数很重要——如果股票在区间内发生过分红送股,不复权数据会出现价格跳空,算出来的技术指标和回测结果全失真;前复权则以最新价格为基准倒推历史价格,保证连续性。

我一般会在采集之后立刻做一步本地缓存,把数据存成 CSV,后续调试策略时不需要反复请求接口,省时间也避免触发限频:

# 接上代码,在 main 中调用时: data.to_csv("data/600519_daily.csv", index=False, encoding="utf-8-sig")

4.2 技术指标计算:pandas 实现 MA 与 MACD

指标计算层是策略的信号来源。双均线策略只需要两条均线,逻辑简单但对初学者很友好。MA 的计算在 pandas 里是一行代码的事:

# indicator/ma.py import pandas as pd def add_ma(df: pd.DataFrame, short: int = 5, long: int = 20) -> pd.DataFrame: """ 给 DataFrame 添加短周期和长周期均线 """ df = df.copy() df["ma_short"] = df["close"].rolling(window=short).mean() df["ma_long"] = df["close"].rolling(window=long).mean() return df

rolling(window=short).mean()表示对 close 列取长度为short的滑动窗口平均值。窗口越短,均线越贴近价格本身,信号越多但噪声也大;窗口越长,均线越平滑,信号越少但更稳。5 和 20 是双均线策略里最常见的组合之一,属于短期交易常用的参数。

MACD 的计算稍微复杂一点,核心是 EMA(指数移动平均)的差离值。pandas 里有现成的ewm方法,不需要手动算权重:

# indicator/macd.py def add_macd(df: pd.DataFrame, fast: int = 12, slow: int = 26, signal: int = 9) -> pd.DataFrame: df = df.copy() # 计算快线和慢线的 EMA,span 是平滑窗口大小 ema_fast = df["close"].ewm(span=fast, adjust=False).mean() ema_slow = df["close"].ewm(span=slow, adjust=False).mean() # DIF = 快线 EMA - 慢线 EMA df["dif"] = ema_fast - ema_slow # DEA = DIF 的 signal 周期 EMA,即信号线 df["dea"] = df["dif"].ewm(span=signal, adjust=False).mean() # MACD 柱状图 = 2 * (DIF - DEA),国内行情软件通用算法 df["macd"] = (df["dif"] - df["dea"]) * 2 return df

ewm(span=..., adjust=False)是标准 EMA 算法,adjust=False表示直接按指数加权递归计算,不需要校正。DIF 和 DEA 的金叉死叉对应策略的买卖信号,看盘软件里通常把 MACD 画成红绿柱子,就是这个代码的最终产物。

4.3 策略引擎与回测骨架:向量化信号生成

双均线策略的买卖规则很简单:短均线上穿长均线(金叉)买入,短均线下穿长均线(死叉)卖出。用向量化方式生成信号,代码比循环清晰得多:

# strategy/ma_cross.py def generate_signals(df: pd.DataFrame) -> pd.DataFrame: """ 基于双均线生成交易信号 signal 取值:1 表示买入(持有),0 表示卖出(空仓) """ df = df.copy() # 前一行短均线 <= 前一行长均线,当前行短均线 > 当前行长均线 → 金叉 df["signal"] = ( (df["ma_short"] > df["ma_long"]) & (df["ma_short"].shift(1) <= df["ma_long"].shift(1)) ).astype(int) # 死叉时信号为 0 df["signal"] = df["signal"].where( ~((df["ma_short"] < df["ma_long"]) & (df["ma_short"].shift(1) >= df["ma_long"].shift(1))), 0 ) return df

shift(1)的作用是取前一行的数据,这样我们才能判断“从上一个状态变成当前状态”的穿越动作。.where(条件, 0)表示不满足条件的位置替换成 0。注意这里生成的 signal 是事件信号,不是持仓状态,回测引擎需要把它转成持仓:

# backtest/engine.py def run_backtest(df: pd.DataFrame, initial_capital: float = 100000.0, commission: float = 0.0003): df = df.copy() # 用 forward fill 把事件信号转成持仓状态:买入后一直持有,直到死叉 df["position"] = df["signal"].replace(0, pd.NA).ffill().fillna(0) # 计算每日收益率(简单收益率) df["daily_return"] = df["close"].pct_change().fillna(0) # 扣除手续费和滑点,这里简化为每笔交易固定成本比例 df["strategy_return"] = df["position"].shift(1) * df["daily_return"] df["strategy_return"] = df["strategy_return"] - \ (df["signal"].diff().abs() * (commission + 0.001)).fillna(0) df["equity"] = initial_capital * (1 + df["strategy_return"]).cumprod() return df

position.shift(1)是为了避免未来函数——今天的持仓决策只能基于昨天的信号,今天的收盘价不能用来决定今天的仓位,这一行是回测正确与否的分水岭。signal.diff().abs()计算出买卖动作发生的位置,每发生一次就扣一次手续费和滑点,这样回测结果才接近真实成本。

4.4 数据可视化:K 线图与买卖点叠加

最后是可视化。用 matplotlib 手动画 K 线需要自己画矩形和影线,代码稍长但灵活度高;如果你装了 mplfinance 可以直接用。手动方式的核心逻辑是用matplotlib.patches.Rectangle画实体,用vlines画影线:

# visualize/chart.py import matplotlib.pyplot as plt import matplotlib.patches as patches def plot_kline_with_signals(df: pd.DataFrame, symbol: str): fig, ax = plt.subplots(figsize=(14, 7)) for i, row in df.iterrows(): color = "#e60000" if row["close"] >= row["open"] else "#009900" # 画 K 线实体:矩形,x 是索引位置,y 是开盘价,高度是实体长度 rect = patches.Rectangle( (i - 0.3, min(row["open"], row["close"])), 0.6, abs(row["close"] - row["open"]), facecolor=color, edgecolor=color, ) ax.add_patch(rect) # 画上下影线 ax.vlines(i, row["low"], row["high"], color=color, linewidth=0.8) # 叠加两条均线和买卖点 df["ma_short"].plot(ax=ax, color="#f39c12", label="MA5") df["ma_long"].plot(ax=ax, color="#2980b9", label="MA20") buy_points = df.index[df["signal"] == 1] sell_points = df.index[df["signal"].diff().fillna(0) == -1] # 死叉事件 ax.scatter(buy_points, df.loc[buy_points, "close"], marker="^", color="red", s=100, label="BUY") ax.scatter(sell_points, df.loc[sell_points, "close"], marker="v", color="green", s=100, label="SELL") plt.title(f"{symbol} K-Line with Signal") plt.legend() plt.show()

这段代码把 K 线实体、影线、均线和买卖点画在同一张图上,视觉上直接验证策略的逻辑是否正确。如果你发现买卖点的位置和市场公认的高点低点偏差很大,优先检查信号生成逻辑,而不是怀疑绘图代码。

5. 避坑记录:依赖、编码、时区、数据源和日期轴的五个常见坑

5.1 依赖版本冲突:pandas 2.x 与 numpy 1.x 不兼容

现象:运行main.py时,终端报ValueError: numpy.dtype size changed, may indicate binary incompatibility,或者直接崩溃退出,没有任何业务日志。

原因:pandas 2.x 在编译时依赖特定版本的 numpy,而你虚拟环境里装的 numpy 版本过低或者过高,二进制接口对不上。这类问题在源码解压到新机器上时极其常见,因为每个人的全局环境不一样。

解决:先卸载重装,让 pip 自动解析依赖关系。我一般这样处理:

pip uninstall pandas numpy -y pip install numpy pandas --upgrade

如果还不行,就指定版本安装:pandas 1.5.3 搭配 numpy 1.23.5 是经过大量项目验证的稳定组合。版本锁定比盲目装最新版可靠得多。

5.2 中文乱码:CSV 编码问题导致数据读入异常

现象:数据采集完成后,本地 CSV 文件用记事本打开是正常的中文,但pd.read_csv()读进来之后,列名变成一堆乱码,或者直接报UnicodeDecodeError。

原因:Windows 环境默认编码是 GBK,而 Python 的open()和 pandas 默认用 UTF-8 解码。数据文件是 UTF-8 写入的,但 Windows 的某些中间环节把它转成了 GBK 或者混合编码。

解决:读取时显式指定编码,不再依赖默认值:

df = pd.read_csv("data/600519_daily.csv", encoding="utf-8-sig")

写入时也用utf-8-sig,这个编码会带 BOM 头,Excel 打开不会乱码,pandas 再读回来也不会出问题。从那以后我写 CSV 一律utf-8-sig,没有再踩过这个坑。

5.3 时区错位:行情数据日期偏移一天,回测结果失真

现象:拉下来的数据里最后一根 K 线日期是昨天,或者某一天的行情被归到了第二天;更隐蔽的是回测里信号的买卖日期和实际 K 线日期对不上,看起来像是信号滞后了一天。

原因:行情接口返回的时间戳是 UTC 或者交易所时区,而你本地datetime.now()取的是系统时区。如果在日期格式化时没统一时区,00:00附近的 K 线会被归到错误的自然日。日线还好,分钟线级别的错位会直接毁掉策略评估。

解决:所有时间处理统一用交易日历,而不是自然时区。我在代码里要求所有日期字段都是纯字符串YYYY-MM-DD,禁止混用 datetime 对象;如果要转换,显式指定tz="Asia/Shanghai"。数据落地之前对date列做一次去重和排序,确认没有重复值和日期倒退。

5.4 免费数据源接口失效:URL 变更导致采集失败

现象:昨天跑得好好的,今天再运行,日志里出现HTTP Error 403或者超时;有时候返回的是空 DataFrame,程序不报错但后续指标计算全部净失败。

原因:免费行情接口的 URL、请求头校验或者限频策略经常调整。数据源把爬虫识别出来之后直接拒绝服务,或者接口地址已经更新,你的代码还指向旧地址。

解决:不把鸡蛋放在一个篮子里。我给这份源码做了一层数据源适配,核心思路是:配置文件里指定主备两个数据源,主数据源失败自动切换备用。另外,所有请求都加上常见浏览器请求头,降低被识别为脚本的概率:

headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "application/json", }

并且把采集结果立刻落盘,即使接口挂了你还有本地缓存可以用,不至于整个流程断掉。

5.5 matplotlib 日期轴标签挤成一团:画图横坐标太密集

现象:把 K 线图画出来之后,X 轴上的日期标签重重叠叠,完全看不清具体日期,整个横轴看起来像一团黑块。很多人卡在这一步,以为是自己的显示器分辨率不够。

原因:matplotlib 默认对时间序列的刻度定位策略是「密集覆盖」,一年两百多个交易日它恨不得全标出来。而 K 线图的 X 轴实际是整数索引,如果你用日期字符串当 x 轴,matplotlib 会为每一天生成一个刻度。

解决:手动控制刻度数量和格式。最简洁的方式是指定每隔一段时间标一个日期,并且把标签旋转 45 度防止重叠:

import matplotlib.dates as mdates ax.xaxis.set_major_locator(mdates.MonthLocator(interval=2)) # 每隔 2 个月标一个刻度 ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m")) plt.xticks(rotation=45)

MonthLocator(interval=2)表示每两个月放置一个主刻度,DateFormatter("%Y-%m")把日期格式化成「年-月」。这一招对任何时间序列图都适用,不只是股票 K 线。

6. 把回测结果做成汇报级报告:收益曲线、最大回撤、夏普比率一次算清

跑完回测拿到一张 K 线买卖点图还不够——你得把策略的好坏变成数字,才能在多个参数组合之间做对比,也才能让自己信服这套策略是否值得实盘验证。这一章的做法是把回测里输出的资产曲线,加工成一份标准的绩效报告。

核心绩效指标有三个:年化收益率、最大回撤、夏普比率。年化收益率告诉你赚钱快不快;最大回撤告诉你最惨的时候亏多少;夏普比率告诉你每承担一份风险换来多少超额收益,通常大于 1 才算合格。

# backtest/metrics.py import numpy as np import pandas as pd def compute_metrics(df: pd.DataFrame, initial_capital: float = 100000.0, risk_free_rate: float = 0.02, periods_per_year: int = 252): """ 计算回测绩效指标 :param df: 包含 equity 列的 DataFrame,daily_return 为每日策略收益率 :param risk_free_rate: 无风险利率,默认 2% :param periods_per_year: 每年交易日数,日线数据用 252 :return: 指标字典 """ # 资产曲线的每日变化率 daily_returns = df["equity"].pct_change().dropna() # 累计收益 total_days = len(df) total_return = df["equity"].iloc[-1] / initial_capital - 1 # 年化收益率:复利换算 annual_return = (1 + total_return) ** (periods_per_year / total_days) - 1 # 最大回撤:资产从峰值跌到谷底的最大幅度 rolling_max = df["equity"].cummax() drawdown = df["equity"] / rolling_max - 1 max_drawdown = drawdown.min() # 夏普比率:超额收益均值 / 标准差,再年化 excess_returns = daily_returns - risk_free_rate / periods_per_year sharpe_ratio = np.sqrt(periods_per_year) * excess_returns.mean() / excess_returns.std() return { "total_return": total_return, "annual_return": annual_return, "max_drawdown": max_drawdown, "sharpe_ratio": sharpe_ratio, }

这段代码的核心是rolling_max和drawdown。资产峰值用cummax()从始至终滚动追踪,每一步的当前资产与峰值相比跌了多少就是回撤,取最小值就是最大回撤。很多初写回测的人会把回撤算错,常见错误是直接用资产最大值减最小值,忽略了「回撤是相对前高,不是相对初始资金」这个定义。

拿到指标之后,我习惯再画一张图,把资产曲线和回撤曲线上下排列,这会暴露很多数字掩盖的问题。用 matplotlib 的subplots两行代码搞定:

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 8), sharex=True) ax1.plot(df["equity"], color="#2980b9", label="Equity Curve") ax1.set_ylabel("Capital") ax2.fill_between(df.index, drawdown * 100, 0, color="#e60000", alpha=0.3) ax2.set_ylabel("Drawdown (%)") plt.show()

下半张图如果出现一条长时间贴着 -20% 以下的深红色区域,说明这个策略有相当长的一段时期处在深度亏损状态,即使整体收益为正,实盘心态也很难扛住。

最后说一个我自己的习惯。早先我拿到类似的股票源码,第一反应都是解压、跑通、看曲线,觉得能出图就算成功了。后来被数据源失效、时区错位、回测未来函数这些问题连续坑过几次,才意识到:跑通只是开始,做空数据比对、锁版本、写自动缓存,这些看起来枯燥的步骤才是真正让一套源码能在你手里稳定复现的关键。从那以后,我每次拿到新源码都强制走一遍固定流程:隔离环境 → 锁定依赖版本 → 跑通数据采集 → 核对信号 → 再优化参数。这套流程让「源码我收到了但跑不出结果」这句话彻底消失在了我的工作习惯里。希望帮到你。

本文还有配套的精品资源,点击获取