简介:面向量化交易初学者的Python学习代码包,围绕行情数据获取、因子计算、策略回测与可视化展示等典型环节进行组织,适合希望用编程落地交易思路、但缺少完整工程参照的开发者学习使用。压缩包共253个文件,大小约252KB,其中主体为106个Python脚本和132个txt说明文档,另含少量CSV数据表、JSON配置、Markdown笔记与数据库文件,便于对照代码梳理行情数据到交易信号的完整逻辑。资源内置股票、ETF、概念板块等样例数据,可配合脚本直接运行,帮助理解从原始行情到策略信号的处理流程;同时通过txt说明与py脚本的对应关系,能快速掌握数据清洗、指标计算、回测评估的完整路径。已有398人学习下载,资源轻量易上手,对正在搭建个人量化研究框架、希望快速验证策略想法的读者尤为实用。
1. 基于Python的量化交易学习代码示例:先看清回测曲线是怎么骗人的
我见过太多这个开头的故事:某开发者从论坛复制来一段双均线回测代码,屏幕上是一条两年 180% 的漂亮曲线,于是他开好账户、按同样参数下单,结果两个月把回测里的“安全垫”亏掉大半。问题不在双均线,而在那段代码示例没告诉他三件事:信号到底哪根 K 线生效、手续费是否被写成了零、数据里有没有偷偷用了未来信息。这篇笔记要交付的,是一套基于 Python 的量化交易学习代码示例——从行情数据清洗、信号生成、最小回测到样本外验证,每一段都能复现,每一行都讲得清为什么。适合会基础 Python、真想理解量化交易而不是只想抄代码的从业者。
2. 先搭最小闭环:数据清洗、收益率与第一个双均线信号
2.1 为什么第一站是趋势跟踪而不是机器学习
很多人的学习路径一开始就错了:上来就学机器学习预测涨跌,数据还没洗干净就开始训练模型,最后得到一个在历史上“预测精准”、在实盘里完全失效的黑匣子。量化交易的主流策略按交易逻辑可以分成三类:趋势跟踪、均值回归、统计套利。趋势跟踪的逻辑最朴素——涨了就跟,跌了就走;双均线是它最经典的代码示例。
均值回归(布林带、RSI 超买超卖)适合震荡市,但“当前到底是震荡还是趋势”这个问题本身就需要先判断,一旦判断错,均值回归会在单边行情里反复止损。统计套利则要做配对关系、协整检验,对新手来说概念负担太重。所以学习代码示例的第一站我强烈建议选双均线:参数少、可视化直观、就算策略不赚钱,你的交易逻辑也能被代码清清楚楚表达出来。
数据频率同样要选对。日线优先,理由有三个:数据量小、噪音低、回测速度快;分钟级数据留给后面研究执行成本时再说。高频策略对行情延迟、手续费极其敏感,学习阶段容易被微观噪音带偏,连信号本身是否有效都判断不出来。先在一个干净、低频、慢速的环境里把闭环跑通,再谈加速。
2.2 用 pandas 清洗日线行情:数据脏是回测翻车的第一源头
行情数据只要来自真实数据源,就一定会出现重复行、缺失列、时间乱序这类问题。直接把 CSV 读进来就跑策略,等于让后面的所有结论都建立在脏数据上。下面这段是我处理单标的日线行情的最小代码:
import pandas as pd import numpy as np df = pd.read_csv("daily_ohlcv.csv", parse_dates=["date"]) df = df.sort_values("date").drop_duplicates(subset=["date"]) df.set_index("date", inplace=True) df = df[["open", "high", "low", "close", "volume"]].astype(float) df = df.dropna() print(df.head()) print("索引是否按时间升序:", df.index.is_monotonic_increasing)第一行用parse_dates把日期列从字符串转成时间类型,这是后续时间对齐的前提。drop_duplicates(subset=["date"])处理的是同一根 K 线被数据源重复返回的情况;如果重复行不去掉,后面rolling和pct_change会在同一天上重复计算,收益曲线出现肉眼看不见的偏移。set_index之后,rolling窗口才会按时间顺序对齐。最后把价格列统一转成float,避免某些数据源把价格读成字符串导致运算直接报错。
注意:清洗要分情况,并不是无脑
dropna()。如果只是 high/low 个别缺失而 close 完整,可以先ffill()一次再丢弃剩余缺失;一旦 close 缺失,那就必须丢弃该行或整段剔除,因为收益计算依赖 close,缺失会连带污染后面所有指标。
复权问题也要在清洗阶段就解决。学习阶段如果拿到的是不复权价格,分红除权会造成价格跳变,双均线会在除权日前后产生假信号。建议直接使用前复权或后复权数据,并在整个学习周期里固定一种口径,不要混用。
2.3 收益率两种算法:算术收益与对数收益
把收盘价序列变成收益率序列,是量化交易代码示例里最基础却最容易被忽略的一步。收益率有两种常见算法,它们的适用场景完全不同:
df["ret"] = df["close"].pct_change() # 算术收益 df["log_ret"] = np.log(df["close"] / df["close"].shift(1)) # 对数收益pct_change()算的是算术收益,即今天的价格相对昨天涨了百分之几;回测净值的累乘(1 + ret).cumprod()用的就是它,直观且符合资金曲线的真实增长方式。对数收益则用log(今天 / 昨天),它的好处是可以跨时间直接相加——多日累计对数收益等于每日对数收益之和,这在统计建模和机器学习特征工程里非常方便。
在日线级别上,两者数值非常接近;如果你发现两者差距很大,说明行情出现了剧烈跳变或数据存在异常,这时候要回头看清洗步骤,而不是继续往下走。两个序列的第一行都是 NaN,这是没有前一天数据导致的,回测循环里要提前fillna(0)或dropna(),别带着 NaN 去算cumprod(),否则整条资金曲线都会变成 NaN。
2.4 双均线信号的第一版代码:先理解信号再谈收益
双均线策略的核心只有一句话:快线上穿慢线做多,快线下穿慢线离场。用前两节的数据,信号生成可以写成这样:
fast, slow = 10, 60 df["ma_fast"] = df["close"].rolling(fast).mean() df["ma_slow"] = df["close"].rolling(slow).mean() df["signal"] = (df["ma_fast"] > df["ma_slow"]).astype(int) print(df[["close", "ma_fast", "ma_slow", "signal"]].tail())rolling(fast).mean()计算最近 fast 根 K 线的收盘价均值,rolling(slow).mean()同理;两者比较的结果转成int,得到 1 或 0 的信号序列——1 表示快线在上、多头状态,0 表示空仓。输出里可以看到,前slow - 1行的均线是 NaN,对应位置信号也是 NaN 或按比较结果出现异常,这是正常现象,是窗口尚未填满导致的。
如果你希望更早产生信号,可以给rolling加min_periods参数,例如rolling(fast).mean(min_periods=5),意思是窗口样本不足时也用已有的 5 根 K 线计算。但在学习阶段,我建议保持默认,因为早期均线样本不足,数值抖动剧烈,容易被当成交易信号。信号列现在只是“今天收盘时确认的市场状态”,它还不能直接参与收益计算——能不能按这个信号成交,什么时候成交,是下一章的核心问题。
3. 把信号变成可回测的收益:shift、成本与三个必调参数
3.1 信号生成两种写法:循环好读,向量化快
把上一章的signal变成真正的持仓,很多初学者第一反应是写循环:遍历每一天,看今天信号和昨天持仓是否一致,不一致就换仓。这种写法最接近实盘直觉,值得每个人都写一遍:
signal_arr = df["signal"].to_numpy() pos_arr = np.zeros(len(signal_arr)) pos = 0 for i in range(len(signal_arr)): if i > 0 and signal_arr[i - 1] != pos: pos = signal_arr[i - 1] pos_arr[i] = pos df["position_loop"] = pos_arr这段循环的逻辑是:持仓状态只在前一天信号发生变化时更新。你今天下的单,只能依据昨天收盘后的信号,这符合“收盘看到信号、次日再行动”的实盘节奏。循环写法的问题也很明显:pandas 行级循环慢,几千行日线数据无所谓,几百万行分钟数据就会卡到无法接受。
所以生产代码里更常用向量化写法,一行替代整个循环:
df["position"] = df["signal"].shift(1).fillna(0).astype(int)shift(1)把前一天的信号搬到今天,等价于循环里“前一天信号决定今天持仓”的逻辑;fillna(0)处理序列开头没有前一日信号的空档。学习阶段我建议两种写法各自跑一遍,然后比对position_loop和position是否完全一致——这是极好的自查练习,既能验证你对shift的理解,也能帮你熟悉 pandas 的调试方式。
3.2 shift(1) 是灵魂:为什么信号必须滞后一根 K 线
这里值得单独用一节讲清楚。如果当天收盘时算信号、又按当天收盘价成交,等于你在收盘那一刻同时完成了“看到结果”和“下订单”两个动作——实盘里这不可能做到,这就是最常见的未来函数。你能拿到的最早成交价是次日开盘,所以最保守、也最通用的做法,就是让持仓严格滞后信号一根 K 线。
有了正确的持仓序列,策略收益的计算就顺理成章了:
df["ret"] = df["close"].pct_change().fillna(0) df["strategy_ret"] = df["position"] * df["ret"] turnover = df["position"].diff().abs().fillna(df["position"]) cost = 0.0005 df["strategy_ret"] = df["strategy_ret"] - turnover * cost df["net_value"] = (1 + df["strategy_ret"]).cumprod()逐行解释:position * ret表示当前持仓吃到的当日涨跌,空仓时 position 为 0,当日策略收益就是 0。position.diff().abs()计算相邻两天持仓的变化量:0→1 或 1→0 都是 1,表示发生了一次换仓;第一行没有前值,fillna(df["position"])让第一天如果直接持仓也计入一次建仓成本。每换仓一次就扣掉一个cost,这是单边成本的简化建模。最后cumprod()把每日收益累乘成净值曲线。
注意:这里的成本模型是简化版——只在换仓时扣一次单边费用。真实交易里买卖各收一次,部分市场卖出方向还有额外税费,这些细节等你能跑通最小闭环之后再加进去。
3.3 三个必调参数:快慢窗口、单边成本、滑点
双均线代码示例里真正影响结果的参数就三个:快线窗口、慢线窗口、交易成本。下面是学习阶段常用的取值范围和我的默认建议:
| 参数 | 常见范围 | 对结果的影响 | 学习阶段建议 |
|---|---|---|---|
| 快线窗口 fast | 5~20 | 越短越敏感,信号越多,换手越高 | 从 10 开始 |
| 慢线窗口 slow | 40~120 | 越慢越迟钝,信号越少,单笔持仓越久 | 从 60 开始 |
| 单边交易成本 | 0.0003~0.001 | 直接削减换仓收益,对高换手策略最致命 | 先设 0.0005 |
| 滑点 | 0.0001~0.0005 | 影响大单和快速下跌时的实际成交 | 学习阶段并入成本 |
有了上面的向量化代码,参数网格搜索写起来非常短,用itertools.product就能把快慢窗口组合全部跑一遍:
import itertools for f, s in itertools.product([5, 10, 20], [40, 60, 120]): tmp = df.copy() tmp["ma_fast"] = tmp["close"].rolling(f).mean() tmp["ma_slow"] = tmp["close"].rolling(s).mean() tmp["signal"] = (tmp["ma_fast"] > tmp["ma_slow"]).astype(int) tmp["position"] = tmp["signal"].shift(1).fillna(0) tmp["ret"] = tmp["close"].pct_change().fillna(0) tmp["strategy_ret"] = (tmp["position"] * tmp["ret"] - tmp["position"].diff().abs().fillna(tmp["position"]) * cost) net = (1 + tmp["strategy_ret"]).cumprod().iloc[-1] print(f"fast={f}, slow={s}, 末期净值={net:.4f}")每组参数跑完打出一个末期净值,9 组结果一对比,你就能直观看到参数对收益的影响方向。但我要提前泼一盆冷水:在同一段数据上来回调参、只挑最好的一组,这是过拟合的开端,具体坑在第 5 章细讲。现在先把“能跑通”变成“能看懂”,再到“能判断好坏”。
4. 自己写一个最小回测框架:百行代码看清资金曲线
4.1 学习阶段不要一上来就套现成回测框架
市面上有很多成熟的回测框架,功能强大,但封装也太深。信号怎么撮合、成本怎么扣、滑点怎么加,全部是黑匣子。新手拿着框架跑出一条漂亮曲线,根本说不清收益到底来自策略逻辑,还是来自框架里某个默认假设——这种说不清,实盘里迟早要还回去。
我一般的做法是:学习阶段坚持自己写回测循环。自己写不等于造轮子,而是把每个假设摊开在眼前——信号何时生效、换仓何时扣费、当日收益怎么乘,每一行都是自己敲的,出了问题你知道去查哪里。等用自己框架把策略逻辑验证透了,再迁移到成熟框架做扩展、做多标的,性价比才最高。
4.2 逐 K 线回测主循环:换仓才扣成本,持仓吃涨跌
最小回测框架的核心就是一个逐 K 线循环。它做的事可以概括为两句话:仓位变化时扣一次成本,其余时间让持仓去吃当日涨跌。下面是完整实现:
def run_backtest(df, fast=10, slow=60, cost=0.0005): df = df.copy() df["ret"] = df["close"].pct_change().fillna(0) df["ma_fast"] = df["close"].rolling(fast).mean() df["ma_slow"] = df["close"].rolling(slow).mean() df["signal"] = (df["ma_fast"] > df["ma_slow"]).astype(int) df["position"] = df["signal"].shift(1).fillna(0).astype(int) equity = 1.0 eq_list = [] for i in range(len(df)): pos_today = df["position"].iloc[i] pos_prev = df["position"].iloc[i - 1] if i > 0 else 0 if pos_today != pos_prev: equity *= (1 - cost) equity *= (1 + pos_today * df["ret"].iloc[i]) eq_list.append(equity) df["equity"] = eq_list return df result = run_backtest(df, fast=10, slow=60, cost=0.0005) result["equity"].plot(title="Dual MA Equity Curve")函数入口是原始行情和三个参数,返回带equity列的 DataFrame。循环里先判断今天持仓与昨天是否不同,不同说明发生了一次换仓,净值先乘(1 - cost);然后无论是否换仓,都要让当前持仓吃今天的涨跌,即再乘(1 + pos_today * ret)。空仓时pos_today为 0,这一步乘 1,净值不变。画曲线前记得import matplotlib.pyplot as plt,如果不想看图,直接打印result["equity"].tail()也能核对数值。
注意:这个框架是单标的、满仓或空仓模型,没有仓位管理,也没有组合层。它只回答一个问题:这个信号按日线收盘价在历史上能吃到多少收益。仓位管理、多标分配是后面单独要学的模块,别在这个最小框架里硬塞。
4.3 三个关键指标:年化收益、最大回撤、夏普比率
光有资金曲线还不够,曲线是用眼睛看的,指标才是用来对比的。学习阶段我建议先掌握三个指标,它们的计算代码可以封装成函数:
def report_metrics(net_value, periods_per_year=252, risk_free=0.02): rets = net_value.pct_change().dropna() total_return = net_value.iloc[-1] / net_value.iloc[0] - 1 years = len(net_value) / periods_per_year ann_return = (1 + total_return) ** (1 / years) - 1 ann_vol = rets.std() * np.sqrt(periods_per_year) sharpe = (ann_return - risk_free) / ann_vol drawdown = net_value / net_value.cummax() - 1 max_drawdown = drawdown.min() return { "总收益": round(total_return, 4), "年化收益": round(ann_return, 4), "年化波动": round(ann_vol, 4), "夏普比率": round(sharpe, 2), "最大回撤": round(max_drawdown, 4), } metrics = report_metrics(result["equity"]) print(metrics)年化收益把不同长度的回测折算到同一时间尺度,方便策略间对比;最大回撤记录净值从最高点跌到最低点的幅度,这是你能不能拿住策略的关键指标——回撤 50% 的策略,即使年化 30%,实盘里大多数人也拿不住。夏普比率衡量每承担一单位波动换回多少超额收益,大于 1 算合格;趋势策略常年落在 0.5 到 1.5 之间都是正常的,别被网上一堆“年化 200%、夏普 3”的曲线绑架判断力。这三个指标足够支撑学习阶段的策略体检。
5. 量化交易学习避坑:四条让回测曲线集体翻车的错误
回测翻车不是运气问题,是假设问题。下面四条是我和身边人踩得最多、也最容易让新手把“幻觉”当成“能力”的错误,每条按“现象→原因→解决”展开,下次看到相似曲线时先对号入座。
5.1 未来函数:信号当天生效,曲线漂亮得不像话
现象:回测里年化收益惊人、最大回撤只有 3%、几乎天天赚钱,曲线平滑得像教科书。原因:当天收盘价同时用于计算信号和成交,走完这一步你等于提前知道了收盘结果再去下单,这就是未来函数在作弊。解决:无条件遵守position = signal.shift(1),更保守的做法是用次日开盘价作为成交价,把开盘跳空的风险也交给市场。自查技巧很实用:把signal换成随机生成的 0/1 序列跑一遍回测,如果随机信号都能赚钱,说明回测框架本身就在作弊,和策略无关。
5.2 幸存者偏差:你的股票池今天才存在
现象:全市场选股的十年回测收益很惊人,点开买入记录却发现名单里有大量已经退市的股票。原因:大多数免费行情数据源默认只返回当前还在上市的股票,历史回测时,那些已经退市的“烂股票”根本不在池子里,回测曲线自然自动剔除了最差的一批标的。解决:学习阶段尽量固定为单标的回测,或者使用某个指数成分股的历史快照做样本池,不要轻易相信“全市场扫描”式回测结论。复权价格确定一种口径并保持一致,前复权和后复权对历史收益的计算结果有明显差异,混用会让结论不可比。
5.3 过拟合:参数微调就崩,说明你只是在背题
现象:(10, 60) 这组参数年化 40%,改成 (9, 58) 年化直接归零;参数网格里只有一个孤零零的峰值。原因:你在同一段数据上反复挑最好参数,本质是给历史数据背答案,每一次“微调后变好”都在加深对噪音的记忆。解决:把数据切成样本内和样本外两段,样本内允许调参,样本外只允许跑一次;调参完成后,把胜出参数周围的邻域跑一遍,好的策略参数带应该是一块平坦的高原,而不是一根孤立的尖峰。参数带越平坦,实盘里才越敢用。
5.4 成本与成交假设过乐观
现象:策略换手率极高但回测仍然赚钱,实盘却连手续费都覆盖不了。原因:cost被设成了 0,而且假设每天都能按收盘价无滑点成交。解决:单边成本从 0.0005 起步,保守可以到 0.001;对涨跌停这类无法成交的场景要加过滤,比如当日价格触及涨停且当前空仓时,禁止买入并把信号顺延到下一根 K 线。记住一条规律:回测里最便宜的成交价,实盘里往往就是最难成交的价位,成本假设宁可悲观不要乐观。
6. 最后一道门槛:样本外验证与纸上交易
6.1 样本外只跑一次,参数永不回头
参数定下来之后,最后一关是用样本外数据验证。做法是把完整行情按时间切成两段,前 70% 用来调参,后 30% 只在最终验证时跑一次:
split = int(len(df) * 0.7) train_df = df.iloc[:split].copy() test_df = df.iloc[split:].copy() # 1. 只在 train_df 上做参数网格搜索,选定一组参数 # 2. 用同一组参数在 test_df 上调用 run_backtest # 3. 记录 test_df 的指标,此后不再回头改任何参数很多人样本外一亏,立刻回去调参,这等于把样本外也变成了样本内,验证就失去了意义。我第一次把慢线从 60 改成 120,用同一段数据来回调,调出一条漂亮曲线,样本外直接腰斩。那之后我给自己定了一条规矩:测试段上绝对不二次修改参数。这条规矩救了我很多次。
6.2 纸上交易跑一个月,验证你的假设
代码验证之外,纸上交易是最后一道体检。做法很简单:每天收盘后按最新信号得到目标仓位,记录当天信号、次日实际可成交价、回测假设成交价三者之间的差异。跑满一个月后统计两件事:实际成交误差是否落在你设的滑点假设之内;扣掉真实手续费后,账户净值曲线是否还和回测同方向。如果误差可控、方向一致,再考虑投入真实资金;如果误差远大于假设,回测参数再漂亮也要先调假设。
代码示例能交付的是一条从数据到信号的完整练习路径,但真正让策略成立的,是你在背后做过的每一次假设验证。别让回测曲线替你做决定,让样本外和纸上交易替你背书。希望帮到你。
本文还有配套的精品资源,点击获取