简介这是一份面向Python量化交易初学者的代码示例集合适合希望快速了解量化分析基本流程、因子计算与本地数据管理的读者。资源共253个文件压缩包仅252KB以132个txt文本、106个py脚本和8个csv数据文件为主另含json配置、sqlite数据库、markdown笔记及license文件整体结构清晰、便于按需检索。代码与数据覆盖股票、ETF、概念板块及因子表等多类样例配合Python演示从CSV读取行情、解析因子数据、调用sqlite存放结构化数据等常见操作txt文件多用于保存输出结果或补充说明py脚本则提供可修改的量化学习框架帮助初学者对照运行并逐步理解数据处理与简单建模链路适合配合教科书章节动手验证。多张csv表可作为手动练习素材db文件可用于体验数据库在量化数据管理中的作用json配置则方便调整参数。已有398人学习/下载适合入门阶段搭建自己的量化学习代码库并反复练习。1. 基于Python的量化交易学习代码示例先把回测跑通再谈策略量化交易学习代码示例最怕的不是策略不赚钱而是网上贴好的整段代码自己一跑就报错、一改就乱。Python 之所以成为多数人入门量化的首选语言不是因为执行速度快而是从行情清洗、信号生成到回测评估每个环节都能用几十行代码看到输出DataFrame 一打印哪里对哪里错立刻清楚。这篇文章按我平时帮人搭研究框架的顺序给你一套能直接本地跑起来的示例代码从目录结构写到逐 bar 回测再讲清楚五个最容易让结果作废的坑。适合两类人有一定 Python 基础、想系统入门量化的开发者以及被网上零散代码坑过、想建立自己回测框架的从业者。2. 搭好最小研究环境依赖选型、目录骨架与可复现配置环境这关卡住的人比想象中多。不是 Python 装不上而是库装了一堆、文件散在桌面数据、策略、回测混在一个目录里改一个参数动全身。我搭学习框架的习惯是先定目录、再做依赖选型、最后才是写策略。顺序反了后面每步都在补漏。2.1 依赖选型学习阶段只需要三个半库量化研究的核心库我一般只推荐 pandas、numpy、matplotlib 这三个再加 Python 标准库里的 pathlib 和 random简称三个半。pandas 负责行情表的读取、清洗和特征计算numpy 负责随机数、数学运算和数组操作matplotlib 负责把净值曲线和回撤画出来。第三方回测框架先不要装学习阶段的重点是理解回测内部的成交逻辑直接套框架会让整个过程变成黑匣子。mkdir quant_learning cd quant_learning python -m venv venv source venv/bin/activate # Windows 系统用 venv\Scripts\activate pip install pandas numpy matplotlib逻辑说明先建项目目录再创建虚拟环境最后安装三个核心库。虚拟环境的作用是把当前项目的依赖和系统 Python 隔离开避免不同项目之间出现版本冲突。你在真实项目里用 Python 做量化研究几乎都会先走这一步。参数说明venv 是 Python 自带的虚拟环境工具不需要额外安装source venv/bin/activate 只对 macOS 和 Linux 生效Windows 下改用 activate.bat。安装完成后可以用 pip list 检查版本pandas 和 numpy 装到当前稳定版本即可不需要锁定具体版本号。2.2 目录骨架数据、策略、回测各自独立代码示例如果只有一堆脚本学完第二天就不知道哪个文件是干嘛的。我一般把项目分成四个区域数据区、策略区、回测区和配置区。这样做的直接好处是换数据不用改策略改策略不用动回测。学习阶段养成分层的习惯后面接实盘或换数据源都会顺手很多。quant_learning/ ├── config.py ├── data/ │ ├── raw/ # 原始行情 CSV │ └── processed/ # 清洗后的数据 ├── strategies/ │ ├── __init__.py │ └── ma_cross.py # 双均线策略 ├── backtest/ │ ├── __init__.py │ ├── engine.py # 回测引擎 │ └── metrics.py # 绩效指标 ├── utils/ │ ├── __init__.py │ └── set_seed.py # 随机种子 └── main.py # 总入口目录说明strategies 放信号生成逻辑只负责输出目标仓位backtest 负责把仓位变成成交和净值曲线data 存放数据文件config.py 集中管理所有可调参数。这个结构的核心原则是数据和逻辑分离、策略和回测分离。你在自己的项目里可以直接复制这套骨架再按策略数量扩展子目录。config.py 是这套骨架里最值得认真写的部分。所有涉及金额、手续费、滑点、随机种子的参数我都集中放在这一个文件里而不是散落在各段代码中。这样回测完想改参数只动一处不会漏改。# config.py from pathlib import Path BASE_DIR Path(__file__).resolve().parent DATA_PATH { raw: BASE_DIR / data / raw, processed: BASE_DIR / data / processed, } RESULT_PATH BASE_DIR / results INITIAL_CASH 100_000.0 # 初始资金10 万元 COMMISSION_RATE 0.0003 # 双边手续费按万三估算 SLIPPAGE 0.001 # 滑点按千分之一估算 RANDOM_SEED 42 # 随机种子保证结果可复现逻辑说明BASE_DIR 用 pathlib 获取项目根目录之后所有路径都基于它拼接避免在 Windows 和 Linux 之间切换时路径出错。DATA_PATH 区分原始数据和清洗后数据processed 目录里存放的才是真正进入回测的数据文件。参数说明INITIAL_CASH 默认 10 万元你可以改成自己模拟的资金量。COMMISSION_RATE 和 SLIPPAGE 是回测结果是否可信的关键两个都设成 0 会让曲线好看但完全失真后面避坑章节会专门讲。2.3 让示例可复现随机种子不是玄学学习代码和实验代码最大的区别在于实验跑一次就行学习代码要反复改、反复跑。如果两次运行结果不一样你根本分不清是策略改动生效了还是随机性在捣乱。所以第一件事是固定随机种子。# utils/set_seed.py import os import random import numpy as np def set_seed(seed: int) - None: random.seed(seed) np.random.seed(seed) os.environ[PYTHONHASHSEED] str(seed)逻辑说明随机种子同时作用于 Python 内置 random、numpy 全局随机数。PYTHONHASHSEED 是环境变量用于固定字符串哈希的随机性在涉及集合或字典顺序时能进一步保证一致性。函数直接修改全局随机状态不需要返回值。参数说明seed 推荐设置成任意固定整数常见做法是 42 或 2024。注意固定种子只对用全局随机数生成的流程有效如果你用了 np.random.default_rng(seed) 这样的独立随机数生成器也需要在生成数据时显式传入同一个种子。3. 数据清洗是第一个分水岭行情文件从拿到手到能算信号所有策略都从行情数据开始。多数人拿到的行情文件不是不能读而是读进来之后没法直接用日期格式不一致、有重复行、停牌日成交量为 0、除权导致价格跳变。我在实际项目里见过的翻车现场一半以上发生在数据阶段而不是策略阶段。所以清洗这一步值得你用最笨的方法逐行检查一遍。3.1 行情 CSV 的标准形态先把列名对齐行情数据不管来自哪个渠道落地成 CSV 之后列名大同小异。学习示例里我会生成一份模拟行情数据日期的频率设为工作日价格用随机游走生成这样你不需要依赖任何外部接口就能跑通全流程。真实项目里替换成你的行情文件即可。# data/make_demo_data.py import numpy as np import pandas as pd rng np.random.default_rng(7) n 1000 returns rng.normal(0.0002, 0.02, n) price 100 * np.exp(np.cumsum(returns)) df pd.DataFrame({ date: pd.date_range(2021-01-04, periodsn, freqB), open: price, high: price * 1.01, low: price * 0.99, close: price, volume: rng.integers(100_000, 1_000_000, n), }) df.to_csv(data/raw/demo.csv, indexFalse)逻辑说明date_range 生成从 2021 年 1 月 4 日开始、共 1000 个工作日的时间序列freqB 表示工作日频率会自动跳过周末。价格序列用对数收益率的累积生成模拟现实中价格连续变化的形态。volume 用整数随机数模拟每日成交量。参数说明n 是数据长度1000 个交易日约等于 4 年足够让均线策略产生几十次交易信号。returns 的均值 0.0002 表示日收益的漂移项标准差 0.02 控制波动幅度。这两个参数直接决定生成价格曲线的走势特征你可以调大标准差让波动更剧烈。标准行情 CSV 建议统一成下面这张表的格式。字段含义注意事项date交易日期统一为 YYYY-MM-DD解析时用 parse_datesopen开盘价数值类型不能有 NaNhigh最高价应不小于 open 和 closelow最低价应不大于 open 和 closeclose收盘价策略信号大多基于收盘价计算volume成交量单位为股停牌日通常为 03.2 清洗三件事去重、对齐、处理缺失拿到 CSV 后不要急着算指标先做三步按日期排序并去重、把日期设为索引、处理缺失和异常值。这三步做完后面所有特征计算才有可靠基础。df pd.read_csv(data/raw/demo.csv, parse_dates[date]) df df.sort_values(date) df df.drop_duplicates(subsetdate, keeplast) df df.set_index(date).sort_index() df df[df[volume] 0] df[ret] df[close].pct_change() df df.replace([np.inf, -np.inf], np.nan).dropna()逻辑说明drop_duplicates 按日期去重keeplast 表示重复日期保留最后一行适合处理盘中多次快照的源数据。set_index 之后日期成为行索引后续算移动平均和回测循环都依赖这个索引。过滤 volume 为 0 的行是为了剔除停牌日这类日期价格往往没有真实成交。参数说明subsetdate 指定按 date 列去重如果你手里的数据还有 ticker 列需要同时传入 [date, ticker]。replace 处理 pct_change 产生的无穷值dropna 把第一行因为没有前日价格而缺失的 ret 去掉。对齐这一步最常见的是把两个不同来源的数据合并到同一时间轴上。比如行情主表里没有成交量需要从另一个文件补进来又或者你想把大盘指数和个股收盘价放在同一张表里计算相对强弱。# 另一份数据源按 date 索引的额外字段 extra_df pd.read_csv(data/raw/extra.csv, parse_dates[date], index_coldate) df df.join(extra_df, howleft) df df.sort_index().ffill().dropna()逻辑说明join 是 pandas 里按索引对齐的标准操作howleft 表示以主表日期为准extra_df 里没有的行保留为 NaN。ffill 向前填充把缺失价格填充为最近一个有效值常用于把低频数据对齐到日频。参数说明ffill 只适合填充本身变动不大的字段比如市值、评级这类慢变量价格和成交量不能盲目填充缺失就直接剔除或插值。dropna 此时会把没有任何额外数据的行删掉如果你希望保留主表全部行可以去掉这一步并单独处理缺失字段。3.3 特征构造从价格到信号的基础量数据干净之后下一步是把原始价格变成能触发信号的量。移动均线、收益率、波动率是三个最基础的构造块后面所有策略都在这三个量上做文章。构造特征时最需要注意的是窗口起点滚动窗口在序列头部会产出 NaN不能直接参与信号计算。df[ma_fast] df[close].rolling(5, min_periods5).mean() df[ma_slow] df[close].rolling(20, min_periods20).mean() df[volatility] df[ret].rolling(20).std() * np.sqrt(252) df[signal] 0 df.loc[df[ma_fast] df[ma_slow], signal] 1 df[position] df[signal].shift(1).fillna(0)逻辑说明ma_fast 是 5 日移动均线ma_slow 是 20 日移动均线二者关系是双均线策略的核心判断依据。volatility 用 20 日日收益标准差乘以根号 252得到一个年化波动率的估算值。signal 用布尔条件批量赋值当前快线上穿慢线时信号为 1。参数说明min_periods 控制滚动窗口至少需要多少个有效数据点5 日和 20 日分别要求至少有 5 根和 20 根 K 线否则输出 NaN。shift(1) 把信号整体后移一天这是回测里防止未来函数的关键一步后面第 4 章会详细展开。position 表示目标仓位1 代表满仓0 代表空仓。4. 写一个看得懂的逐bar回测双均线策略从信号到绩效学习阶段最适合拿双均线练手。逻辑足够简单又能覆盖信号生成、仓位管理、成交撮合、权益曲线这四个回测里必须想清楚的环节。等你把这条链路跑通再换成其他策略只是改信号生成那几行的事。4.1 信号与仓位为什么要用 shift(1)回测代码里最容易被忽略、也最容易埋雷的就是信号与成交的时序关系。你用 T 日收盘价算出金叉信号最早只能在 T1 日开盘甚至收盘才能成交。要是回测代码里用 T 日信号去成交 T 日价格等于提前知道了当天的结果这叫未来函数。df[signal] (df[ma_fast] df[ma_slow]).astype(int) df[position] df[signal].shift(1).fillna(0)逻辑说明signal 在 T 日收盘后才有确定值position 用 shift(1) 把它挪到 T1 日生效。fillna(0) 把序列最前面缺失的目标仓位补成 0也就是在没有信号前默认空仓。回测循环里读取的是 position 列而不是 signal 列这是防止未来函数最简单也最有效的做法。参数说明fillna(0) 只影响第一个交易日的仓位如果你希望第一天就持有可以把 0 改成 1但我不建议这样做。shift 的周期数在实际策略中可能不是 1比如你想用收盘信号第二天开盘成交shift(1) 正好如果想用 T 日信号 T2 日成交就改成 shift(2)。4.2 逐bar回测引擎最小的成交循环回测引擎不需要设计得多复杂学习阶段的代码示例核心就是一层 for 循环逐行读数据在每一根 K 线上判断是否调仓。我写回测引擎的习惯是持仓状态和现金分开记录每一根 K 线结束时计算一次总权益最后把权益序列汇总成净值曲线。# backtest/engine.py import pandas as pd def run_backtest(df, initial_cash100_000.0, commission0.0003, slippage0.001): cash initial_cash # 可用现金 position 0.0 # 当前持仓股数 records [] # 逐bar权益记录 for date, row in df.iterrows(): target row[position] # 目标仓位1 满仓0 空仓 if target 1 and position 0: buy_price row[close] * (1 slippage) volume cash / (buy_price * (1 commission)) cost volume * buy_price fee cost * commission cash cash - cost - fee position volume elif target 0 and position 0: sell_price row[close] * (1 - slippage) cash position * sell_price fee cash * commission cash cash - fee position 0.0 equity cash position * row[close] records.append({equity: equity, cash: cash, position: position}) cash_records pd.DataFrame(records) return cash_records逻辑说明代码从空仓开始target 等于 1 且当前没有持仓时买入买入价在收盘价基础上加上滑点手续费按成交金额的比例扣除。target 等于 0 且持有仓位时卖出卖出价在收盘价基础上减去滑点手续费从卖出现金里扣。每根 K 线结束时equity 等于现金加上当前持仓按收盘价计算的市值。参数说明initial_cash、commission、slippage 三个参数分别对应 config.py 里的初始资金、手续费率和滑点。volume 的计算里commission 是费率不是固定金额所以把它放到分母上相当于把手续费成本折进买入数量。这样处理能让现金永远不会为负实盘模拟也更贴近真实账户。4.3 绩效指标收益、回撤、夏普一个都不能少回测引擎跑出权益序列之后下一步是把权益序列换算成几个能对比的绩效数字。只看总收益率是最容易误判的因为一支策略可能在最后一天暴涨前面全是亏损。我一般会同时给出年化收益、最大回撤和夏普比率三个指标再配合净值曲线图一起看。# backtest/metrics.py import numpy as np def performance(equity_curve, periods_per_year252): equity equity_curve[equity] total_return equity.iloc[-1] / equity.iloc[0] - 1 years len(equity) / periods_per_year annual_return (1 total_return) ** (1 / years) - 1 daily_ret equity.pct_change().dropna() sharpe daily_ret.mean() / daily_ret.std() * np.sqrt(periods_per_year) drawdown equity / equity.cummax() - 1 max_drawdown drawdown.min() return { total_return: total_return, annual_return: annual_return, sharpe: sharpe, max_drawdown: max_drawdown, }逻辑说明总收益率用期末权益除以期初权益再减 1反映整个回测周期的收益。年化收益率用复利公式把总收益折算成按年计的水平这样不同时间长度的回测结果才能直接比较。夏普比率是单位波动能换来多少收益计算方式是日收益均值除以日收益标准差再乘以根号 252 做年化处理。参数说明periods_per_year 是每年的交易周期数股票日频数据用 252期货日频数据可以用 240 左右。drawdown 计算的是回撤比例equity.cummax() 是历史最高权益回撤值永远是负数或 0max_drawdown 取最小值就是最大回撤。夏普比率的无风险利率这里默认是 0实盘评估时应当减掉无风险利率。最后把整条链路用一个入口串联起来。main.py 负责按顺序完成数据读取、信号生成、回测运行和绩效输出让你每次改完策略只要执行一个文件就能看到全部结果。# main.py from data.loader import load_demo_data from strategies.ma_cross import generate_signals from backtest.engine import run_backtest from backtest.metrics import performance df load_demo_data() df generate_signals(df, fast5, slow20) curve run_backtest(df) print(performance(curve))逻辑说明load_demo_data 封装第 3 章的清洗流程返回已处理好的行情表generate_signals 接收行情表返回带 position 列的策略表run_backtest 消费 position 列输出权益序列performance 对权益序列做指标计算。四个函数各干一件事任何一个环节出问题都能单独调试。参数说明fast5 和 slow20 是双均线的窗口长度与生成信号时保持一致。日常迭代中你大概率会频繁修改这两个参数把它们做成函数入参比硬编码在策略函数内部要灵活得多。5. 回测代码避坑指南五个让结果变成废纸的常见错误回测这个环节新手最容易把结果做得很漂亮却不知道数据里埋着雷。下面五个坑是我在帮人看代码时遇到最多的每一条都有真实的翻车案例。前三条属于代码层面的逻辑错误后两条属于数据和方法层面的系统偏差遇到任何一个都足以让整份回测结果失去参考价值。5.1 未来函数回测曲线越漂亮越要警惕现象有位同学拿来的回测曲线净值几乎单边向上最大回撤不到 3%。我让他逐行打印行情和成交记录打印出来之后就发现了问题T 日收盘才能算出的均线信号在 T 日开盘价上就已经成交了相当于策略提前看到了当天的收盘结果。原因信号生成和成交价格取自同一根 K 线。均线是用收盘价算的收盘价在收盘后才确定所以任何基于收盘数据的信号最早也只能在第二天开盘成交。这类未来函数在多头排列、突破类策略里特别隐蔽因为肉眼很难从净值曲线上直接看出来。解决信号算完之后立即 shift(1)把 target 递延到下一根 K 线再参与成交循环。更严格的回测还会把成交价设成 T1 日的开盘价而不是 T 日的收盘价。代码里 position signal.shift(1) 就是干这件事的看到这句话基本可以判定代码作者没有忽略时序。5.2 幸存者偏差股票池本身在作弊现象另一次评估里策略回测只交易当前仍然上市的大市值股票10 年年化收益做出来 35%。但把退市股票加回去之后年化收益直接掉到 8%。因为那批被剔除的股票里有相当一部分在退市前经历了连续下跌。原因用今天的股票池去回测历史本身就是系统性作弊。十年前存在的股票很多已经退市或重组你在回测里看不到这些亏损账户里却要真实承受。选股策略、指数增强策略凡是涉及股票池的都逃不开这个偏差。解决回测时使用历史时点的成分股列表或者用包含退市数据在内的全量股票库。学习阶段可以先手动剔除 ST 股票和上市不足一年的次新股减少退市样本对结果的污染。5.3 手续费与滑点设为 0收益从哪来就从哪漏掉现象有个高频策略回测年化 200%实盘三个月亏掉一半本金。回看代码时发现 commission_rate 和 slippage 都是默认的 0每次买卖都不计成本等于策略在另一个没有摩擦的世界里赚钱。原因手续费和滑点在小周期策略里不是小数。商品期货日内交易每天几十次开平累积成本能吃掉大部分毛利。手续费率设成万三是保守估计流动性差的品种实际滑点远超千分之一。解决回测引擎的初始参数至少设置成双边手续费万三、滑点千分之一。如果你的策略平均持仓只有几天可以再把手续费调到万五、滑点调到千分之二观察结果变化看盈利还在不在。5.4 除权跳变K线图上一条假的大阴线现象某只股票在某天价格突然从 30 元跳到 15 元均线系统当天集体发出卖出信号回测在这个位置出现一次真实的亏损。但实际上那是除权日股票十送十价格减半但账户里的股数翻倍总资产并没有变化。原因行情数据没有做复权处理。原始价格在除权除息当天会出现断崖式跳变任何基于价格连续性的指标都会产生虚假信号均线、布林带、MACD 无一幸免。解决回测前统一使用复权价格。常见做法是选择前复权数据让历史价格按最新股本折算学习代码里没有除权事件但真实场景中一定要检查数据源是否已经做了复权字段没做就自己用复权因子处理。5.5 参数过拟合寻优结果在样本外的翻车现场现象把双均线的 5 和 20 两个参数放在网格里寻优找到一组样本内夏普特别高的组合。寻优完成后把同一组参数放到最近一年的数据上验证结果从正值变成负收益。原因参数越多、寻优范围越大越容易把噪声当成规律。回测数据本身就带有随机波动寻优过程会把那些恰好和历史波动吻合的参数组合挑出来这类组合对未来行情没有预测能力。解决先把数据按时间切成训练段和测试段训练段只用来找参数测试段只在参数确定后跑一次。在测试段成绩明显下滑的参数组合直接丢弃宁可接受平庸但稳健的结果。6. 别停在一次回测上网格寻优与样本外验证的做法一套回测代码跑出净值曲线只是起点。我写完学习示例都会补两件事参数寻优和样本外验证。双均线虽然只有 fast 和 slow 两个参数但 5 和 20 凭什么就是最优网格寻优能回答这个问题样本外验证能回答这个最优还能不能信。import itertools train df.iloc[:int(len(df) * 0.7)] test df.iloc[int(len(df) * 0.7):] results [] for fast, slow in itertools.product([5, 10, 20, 30], [30, 50, 80, 120]): if fast slow: continue train_df generate_signals(train.copy(), fastfast, slowslow) curve run_backtest(train_df) stats performance(curve) results.append((fast, slow, stats[sharpe], stats[max_drawdown])) results.sort(keylambda x: x[2], reverseTrue) print(results[:5])逻辑说明train 和 test 按时间顺序切分前 70% 的数据用于寻优后 30% 用于验证。itertools.product 生成两个参数列表的全部组合跳过 fast 大于等于 slow 的情况因为这种组合的金叉信号永远无法出现。每组参数都在训练段跑一次回测按夏普比率排序后打印前五名。参数说明fast 的范围是 5 到 30slow 的范围是 30 到 120fast 始终小于 slow。min_periods 在 generate_signals 内部会被设置成对应窗口长度这样慢线窗口为 120 时前 119 根 K 线不会产生信号属于正常的预热期。示例输出的最优参数组合只代表这组参数在训练段表现好。真正有效的验证方式是拿测试段数据用同样代码重新生成信号并回测观察夏普和回撤是否还在可接受范围内。有一次我给某策略做双均线寻优训练段夏普做到 3 以上换到测试段直接变成 0.5。从那以后我养成了一个习惯所有参数寻优都先切样本外再动手寻优结果只有在样本外跑出来的才算数。跑不出来的曲线一律当它不存在这比任何复杂优化技巧都重要。希望帮到你。本文还有配套的精品资源点击获取