简介一份以SVM模型为核心的量化交易策略完整源码面向希望在实战中接触数据采集、特征工程、模型训练与组合管理的开发者也适合金融工程方向的学生对照学习解决从行情获取到仓位建议的整套流程问题。压缩包内共18个文件包含10个Python脚本、7个SQL表结构文件与1个Markdown说明文档包体仅56KBpy文件对应全流程各环节sql文件便于初始化数据库表md文件提供工程结构阅读指引。Python脚本按序完成利用tushare接口采集日线行情并入库、整合本地行情构建训练集、对个股进行SVM建模与预测、通过回测和推进式建模计算Precision/Recall/F1指标最后基于马科维茨理论进行投资组合仓位配置SQL脚本则定义股票池、资金、评估结果等表结构。读者可以获得一套可运行的量化策略工程骨架了解从原始行情到最终仓位建议的完整数据流也可以替换数据源或模型算法进行二次改造。目前已有3566人学习下载适合初学者快速建立量化研究的基本流程认知亦可供有经验者参考工程组织方式。1. 量化策略源码它到底是什么值不值得亲自写你翻出一份量化策略源码装上依赖、改个参数回测净值曲线漂亮得不像话可一到模拟盘就开始连续亏钱。这种事在量化圈太常见了源码本身没有错错在它背后那套假设没人去验证。很多人以为拿到源码就等于拿到印钞机但真正的问题是——你连它为什么赚钱、在什么条件下失效都说不清。这篇笔记只讲一件事把一份“能跑”的量化策略源码改造成一份“能信、能改、能上线验证”的量化策略源码。适合那些手里有代码但不知道怎么验证的人也适合想从零写出第一份可回测策略的开发者。我会把信号生成、回测引擎、参数敏感性和常见翻车点一条条拆开讲最后给你一个让策略结果可复现的收尾技巧。2. 量化策略源码的骨架把交易思路翻译成能跑的代码2.1 从思路到信号的三个必经步骤拿到一份量化策略源码先不要急着读代码问自己一个问题它到底在赚什么钱常见策略无非两类选股型和择时型。选股型靠的是横截面上选出一篮子相对强的标的赚的是相对收益择时型靠的是对单标的的多空判断赚的是绝对收益。无论哪类源码里都跑不掉三个步骤信号生成、仓位确定、风控约束。信号生成负责回答“什么时候买、什么时候卖”常见实现是因子阈值或者多条均线交叉仓位确定回答“每一次买多少”常见实现是固定手数、固定比例或者凯利公式变体风控约束回答“错了怎么办”常见实现是止损线、最大回撤熔断和单笔风险预算。很多源码把这三层搅在一起写一个函数里又算指标又下单又调仓这种代码跑起来没问题但你想单独验证其中某一层时就会非常痛苦。所以拿到源码第一步不是改参数而是把它按这三层拆开。拆完之后你会发现所谓量化策略源码本质上就是一个“数据进、信号出、仓位管、风控拦”的管道。任何一个环节出问题最终净值曲线都会说谎。下面我用一个最小可回测的骨架把这条管道串起来这也是我自己每次验证新策略时的起点。2.2 最小可回测的 Python 策略骨架先给你一份可以直接跑通的双均线策略骨架。它能完成从数据加载到净值计算的全流程也是我改任何策略之前先打底的模板import pandas as pd import numpy as np # 加载日线数据列名统一为 datetime/open/high/low/close/volume df pd.read_csv(daily_klines.csv, parse_dates[datetime]) df df.set_index(datetime).sort_index() fast 10 slow 30 # 计算双均线 df[ma_fast] df[close].rolling(fast).mean() df[ma_slow] df[close].rolling(slow).mean() # 生成目标持仓1 表示持有0 表示空仓 df[signal] np.where(df[ma_fast] df[ma_slow], 1, 0) # 关键用 shift(1) 避免用到当日收盘信息去交易当日 df[position] df[signal].shift(1).fillna(0) # 收益计算持有期间拿当日收益 df[ret] df[close].pct_change().fillna(0) df[strategy_ret] df[position] * df[ret] # 净值曲线 df[equity] (1 df[strategy_ret]).cumprod() print(df[[close, ma_fast, ma_slow, signal, position, strategy_ret]].head(40))这段代码的逻辑是这样的rolling(fast).mean()和rolling(slow).mean()算出两条均线np.where在快线高于慢线时给出目标持仓 1否则为 0。注意我用shift(1)把 signal 向后挪了一根 K 线原因很简单当根 K 线的收盘价是用来计算均线的而收盘价只有在收盘后才知道所以真实交易只能在下一根 K 线执行。这一步不做你的回测结果就会被“未来函数”污染曲线好看但实盘做不到。参数说明fast10和slow30是均线窗口窗口越短信号越灵敏震荡行情里反复开平仓的次数也会变多。position是 0 或 1意味着要么全仓、要么空仓这是最简单也最容易高估收益的仓位方式。真实交易里你不会满仓进出但作为策略骨架先验证信号本身有没有 alpha比一上来就接复杂仓位模型更靠谱。2.3 数据预处理让源码不乱跑的底线策略源码的输入端最容易埋雷。我收到过不少“跑不出结果”的代码最后查下来都是数据没洗干净时间戳重复、缺失值没人管、除权缺口让均线直接拉出假突破。所以我一般在读策略逻辑之前会先把 K 线数据统一过一遍清洗函数def clean_klines(df): # 只保留需要的列并去重 df df[[datetime, open, high, low, close, volume]] df df.drop_duplicates(subset[datetime]).sort_values(datetime) # 缺失值前向填充保证连续交易 df df.ffill() # 过滤成交量或价格为 0 的异常行 df df[(df[close] 0) (df[volume] 0)] # 统一 datetime 索引 df df.set_index(datetime) return df这段清洗函数做了四件事去重、排序、前向填充、过滤异常价格。ffill()能把停牌日或缺失的行情用上一交易日数据补齐但要注意如果某只股票停牌时间太长前向填充会让价格一直横着技术指标却仍在变化这会在策略里产生大量“假信号”。我一般还会再加一个条件连续填充超过 5 个交易日的数据直接置空宁可少一个信号也不让假信号混进来。跟数据预处理强相关的是复权问题。如果原始数据是后复权或未复权碰上除权除息价格会出现瞬间跳空均线和动量因子都会把这个缺口当成真实波动从而触发一堆不该出现的买卖点。常见做法是统一转成前复权价格后再计算指标或者把复权因子单独存一列在计算因子时乘上去。这一层处理不好后面所有策略源码都是白搭。3. 选因子还是选信号策略源码的核心模块怎么落地3.1 因子计算的向量化写法与常见坑策略源码里最常看到的一段代码是因子计算。很多新手习惯用 for 循环一条条算指标数据量一大就跑得极慢。pandas 的向量化运算几乎能覆盖 90% 的因子计算需求。以 RSI 为例这是动量类策略里常用的指标def calc_rsi(close, window14): delta close.diff() gain delta.clip(lower0) loss -delta.clip(upper0) avg_gain gain.rolling(window).mean() avg_loss loss.rolling(window).mean() rs avg_gain / avg_loss rsi 100 - (100 / (1 rs)) # avg_loss 为 0 时 rsi 取 100避免除零 rsi rsi.replace([np.inf, -np.inf], 100).fillna(50) return rsi这个函数用rolling(window).mean()做了 N 日平均涨跌幅完全不用 for 循环。clip(lower0)是把负收益截断成 0这样 gain 里只保留上涨部分loss 则取负收益的绝对值。rs avg_gain / avg_loss是涨跌幅比值再换算成 0 到 100 的 RSI 值。replace([np.inf, -np.inf], 100)处理的是 avg_loss 等于 0 的极端行情这种时候 RSI 定义为 100 是行业惯例。参数 window 是回看周期14 是经典值但用在 5 分钟 K 线上和用在日线上效果天差地别。做短线、做高频建议把窗口压到 6 到 10做波段14 到 20 更稳。我这里用的是简单移动平均计算 avg_gain如果你想更平滑可以改成ewm(alpha1/window).mean()这种指数加权方式对近期行情更敏感但也会放大噪声。选哪种没有绝对标准关键是同一个窗口在你的整个回测期里保持固定否则结果不可比。3.2 入场和出场规则的两种表达阈值与状态机因子算完之后下一步就是把因子值翻译成交易动作。最直接的做法是阈值判断比如 RSI 小于 30 买入、大于 70 卖出。但直接对连续数值做阈值判断会出现一个典型问题RSI 在 30 附近上下反复穿越策略就会在同一天开仓平仓好几次手续费都不够亏的。这时候你需要的是状态机写法# 记录目标持仓1 代表持仓0 代表空仓 df[target_pos] np.where(df[rsi] 30, 1, np.where(df[rsi] 70, 0, np.nan)) # 前向填充没有新信号时保持上一状态 df[target_pos] df[target_pos].ffill().fillna(0) # 用 shift 避免未来函数 df[position] df[target_pos].shift(1).fillna(0) # 记录状态变化点用于查看实际交易发生的位置 df[trade_signal] df[position].diff().fillna(0)这段逻辑的关键在np.nan和ffill()。当 RSI 处于 30 到 70 之间时target_pos 被设为 NaNffill()会把它补成最近一次非空的值。换句话说只有 RSI 跌破 30 才开仓只有升破 70 才平仓中间无论怎么震荡都不会产生新信号。trade_signal是 position 的差分1 表示刚进场-1 表示刚离场你可以用它在回测里统计交易次数。对比纯阈值判断状态机写法付出的代价是信号会滞后RSI 跌破 20 也不会触发开仓必须等它回到 30 以下。但换来的好处是交易次数大幅减少源码在实盘里的可执行性也更高。实盘中你要么在信号出现时手动下单要么用程序化交易接口频繁抖动会让你疲于奔命还会让手续费吃掉大部分利润。3.3 参数暴露表哪些参数必须暴露哪些写死也无妨拿到策略源码后最先做的事应该是看它暴露了哪些参数。很多源码把参数写成函数里的魔法数字改一次要和代码搏斗一次。我习惯把所有可调参数集中到一张配置表里既方便网格搜索也方便记录每次实验的版本。下面是我做过的一个双均线 RSI 过滤策略的参数表参数名含义经验范围建议fast_window快均线周期5 ~ 20必须暴露slow_window慢均线周期20 ~ 60必须暴露rsi_windowRSI 回看周期6 ~ 20必须暴露rsi_buy开仓阈值20 ~ 40建议暴露rsi_sell平仓阈值60 ~ 80建议暴露commission单边手续费率0.0001 ~ 0.001必须暴露slippage滑点比例0 ~ 0.002必须暴露我的原则是凡是影响买卖点判断的参数都要暴露出来做敏感性测试凡是只影响计算精度的参数比如数据读取批次大小写死就行。参数暴露得太多网格搜索的组合数会爆炸容易过拟合暴露得太少你又没法判断策略是不是只在某一组参数下才有效。至于 commission 和 slippage它们不属于策略逻辑但直接决定回测可不可信必须放在最显眼的位置让所有人看到。4. 回测与过拟合量化策略源码的照妖镜4.1 回测框架的选型从零写还是用现成市面上有不少现成的回测框架功能齐全集成了撮合、绩效分析和可视化。但对一份策略源码来说我更推荐你先写一个几十行的极简回测循环跑通逻辑后再决定要不要换框架。原因很实在现成框架封装层次高出了问题你很难分清是策略的问题还是框架的假设问题。从零写一遍你对这套源码的每一步逻辑都会更有数。下面是一个带手续费和简单资金约束的循环撮合骨架initial_capital 100_000 cash initial_capital position 0 commission 0.0003 # 单边万三 equity_curve [] for bar in range(1, len(df)): # df[position] 是已经 shift 过的目标持仓 target int(df[position].iloc[bar]) price df[open].iloc[bar] # 用开盘价成交 if target 1 and position 0: # 全仓买入扣手续费 shares int(cash / (price * (1 commission))) cash - shares * price * (1 commission) position shares elif target 0 and position 0: # 清仓卖出扣手续费 cash position * price * (1 - commission) position 0 # 每日盯市现金 持仓市值 equity_curve.append(cash position * df[close].iloc[bar]) df[equity] pd.Series(equity_curve, indexdf.index[1:])这个循环做的事很简单目标持仓和当前持仓不一致时先买入或卖出再用每天的收盘价计算账户总资产。shares int(cash / (price * (1 commission)))里取整是模拟 A 股按手买入的限制你如果做其他市场可以把整数限制改成按股或按币种的最小单位。用开盘价成交是假设信号在前一根 K 线收盘后产生第二天开盘执行。对比现成框架这个手写版本没考虑涨跌停、停牌、分红等规则但它有个明显好处你能看到每一笔交易发生在哪一天、价格怎么取的、资金怎么变的。如果策略在某个区间净值异常跳变你可以直接从 equity_curve 对应区间查根因。先跑通手写版再去套现成框架是我验证陌生源码时的标准路径。4.2 手续费、滑点和涨跌停回测里最容易骗自己的三处回测结果难看你至少知道策略不行回测结果太好看你反而要警惕。我见过太多源码倒在了手续费和滑点两座大山上。先看一组简单假设双边万三手续费加上单笔 0.1% 滑点一个每年换手 50 次的策略光这两项成本就是 50 × (0.0003 × 2 0.001) 0.08也就是 8%。如果你的策略年化只有 10%扣完成本只剩 2%。在代码里加上滑点只需一行改动buy_price price * (1 slippage commission) sell_price price * (1 - slippage - commission)slippage建议至少设成 0.001 到 0.002尤其是小市值标的或者流动性差的品种。真实交易里还有冲击成本你下的单子越大成交价越不利。回测里如果每次都是满仓进出滑点按固定比例算都低估了冲击成本。涨跌停是另一个高频高估来源。涨停板时你想买买不进跌停板时你想卖卖不掉。很多源码的过滤方式是买入信号出现在涨停板上时仍以涨停价成交这显然是错的。简单做法是如果某根 K 线最高价等于涨停价且当前是买入信号跳过这根 K 线不成交如果最低价等于跌停价且当前是卖出信号同样跳过。宁可不交易也不能让回测做出实盘做不出的单子。4.3 参数敏感性与样本外验证源码能用的关键一步策略源码到底能不能用不看它在历史数据上赚多少看它换一组参数、换一段行情之后还赚不赚钱。我一般会在回测之后立刻做一个参数网格扫描grid_results {} for fast in range(5, 30, 5): for slow in range(30, 80, 10): if fast slow: continue metrics run_backtest(fast, slow, commission0.0003) # run_backtest 返回年化收益、最大回撤、交易次数等指标 grid_results[(fast, slow)] metrics # 输出表现最好和最差的参数组合对比差距 sorted_results sorted(grid_results.items(), keylambda x: x[1][annual_return], reverseTrue) print(Top 5 参数组合, sorted_results[:5]) print(Bottom 5 参数组合, sorted_results[-5:])这里run_backtest是封装好的回测函数参数 fast 和 slow 作为输入。判断参数稳不稳定的方法很简单看 Top 组合和 Bottom 组合的绩效差距。如果优参数年化 40%但相邻参数年化只有 5%说明策略对参数极其敏感大概率是在某段历史行情里拟合出来的如果一大片参数区间都有类似的年化和回撤说明策略背后有相对稳定的统计规律。样本外验证是参数敏感性测试的下一步。把数据按时间切成三段前 60% 做参数优化中 20% 做验证最后 20% 做最终测试。只在第一段里选参数第二段确认参数不过拟合第三段模拟“未来”。任何源码如果没做这个切分你看到的净值曲线都只是在自说自话。我习惯把这三段的结果都打印出来对比年化、回撤和交易次数如果训练段漂亮但样本外明显走平直接弃用比纠结调参要节省时间得多。5. 避坑与排查量化策略源码里最常见的 5 个翻车现场5.1 未来函数回测好看实盘哑火的头号原因现象回测净值曲线几乎不回撤交易胜率高达 80%但同一份源码在模拟盘里连续亏损。原因代码里用了未来数据。最常见的是用当根 K 线收盘价计算信号同时又在当根 K 线的收盘价附近成交等于盘中就提前知道了收盘结果。我排查过一份源码它的买入条件写成了df[close] df[ma20]成交价却用的是df[close]看起来只是少了一个 shift实际上已经把未来信息偷进了回测。解决所有涉及收盘价计算的信号必须先shift(1)再判断成交价用下一根 K 线的开盘价。排查时可以写一个断言如果position不为 0 的 K 线里有任何一根的signal是用当前 K 线数据算出来的直接报错。我的习惯是策略代码里永远不出现“同一根 K 线上既算信号又成交”的写法。5.2 收益率算错复利、基准和年化的三道关现象回测报告显示年化收益 50%但你用期末净值自己一算只有 25% 左右。原因年化计算方式错了。常见误用是把每日收益率的简单平均值乘以 252这在日收益波动大时会把年化严重拉高。正确做法是用复利计算期末净值再用净值反推年化。total_return equity.iloc[-1] / equity.iloc[0] - 1 trading_days len(equity) annual_return (1 total_return) ** (252 / trading_days) - 1这段代码先算整个回测期的总收益率再按 252 个交易日折算成年化。如果你的策略持仓周期长比如平均持有 10 天以上用 252 这个固定数偏差不会太大如果是日内策略应该用分钟级交易时段数来折算否则年化会严重失真。另外回测报告里一定要同时给出基准收益比如沪深 300 指数同期的涨跌幅。如果你的策略年化 15%、回撤 8%但同期基准年化也有 20%那这份源码的价值就大打折扣。5.3 数据对齐错误时间戳与复权因子现象回测中某只标的在除权日前后频繁出现买入信号而且这些交易大多盈利。原因原始数据没有复权或复权因子使用不当。除权当天价格会断崖式下跌均线和动量因子都会把它识别成“超跌”于是策略不断买入随后价格填权上涨回测里就变成了一堆捡漏式盈利。但实盘里除权缺口不会带来真实利润这些信号完全是数据假象。解决数据预处理阶段统一转成前复权价格。前复权会把历史价格按除权因子调低保证价格连续指标计算才可信。如果是同时跑多只标的的多标的策略还要检查时间戳对齐不同标的数据有时因为停牌日期索引不一致直接merge会把后面的数据错位映射到错误日期上。建议先用reindex对齐到统一交易日历再前向填充缺失值。5.4 资金管理被忽略胜率很高却亏钱现象策略源码里有完整的买卖信号统计下来胜率 65%但账户净值持续缓慢下跌。原因仓位没有跟风险挂钩。常见写法是每次买入固定金额结果盈利时仓位小、亏损时仓位大胜率再高也扛不住不对称的盈亏分布。另一类问题是亏损后加倍仓位补仓连续两次亏损就能把账户打穿。解决给策略加上简单的风险预算。我常用的做法是单笔风险不超过总资金的 1%按下述公式计算买入数量先根据最近 N 根 K 线的波动幅度估算止损距离再用仓位不是按下单金额算而是按“止损价与入场价的距离”反推股数。比如总资金 10 万、单笔风险 1% 也就是 1000 元止损距离是 0.5 元那买入数量就是 2000 股。这样做的好处是不管标的波动多大单笔亏损都在可控范围内。5.5 源码可读性差自己改不动、跑不重现象拿到源码想换个周期从日线改成 30 分钟 K 线结果改了三个文件还没跑通。原因源码里没有参数集中管理常量散落在各个函数里K 线周期、手续费、滑点、初始资金全是魔法数字。更麻烦的是回测结果没有记录当时的参数版本你想复现某一张净值图发现根本不知道用的哪组参数。解决把源码按“数据预处理、因子计算、信号生成、回测撮合、绩效统计”五个模块拆开每个模块对应一个函数主入口只调函数不写逻辑。参数使用配置字典管理回测结束时把配置字典和结果指标一起序列化为 JSON 存档。这样下次跑出新的结果你可以查到这个结果来自哪组参数、哪份数据才能对比迭代。代码不怕简单怕的是改不动。6. 让源码经得起检验的收尾技巧给每次回测加上数字签名最后分享一个我踩过坑之后养成的习惯每次回测结束时不仅保存净值曲线还把当次运行的完整参数和运行环境一起打包成一个 JSON 文件。听起来简单但能救命的。有段时间我改了一个因子里的 RSI 窗口没记录过了两天回头看回测数据发现净值曲线和之前不一样怎么都想不起改了什么最后只能把代码 git 回退一个个 commit 对比浪费了整整一个下午。从那次之后我的所有策略代码都在回测函数末尾加上这段存档逻辑import json import time def save_backtest_record(config, metrics, equity_curve): record { timestamp: time.strftime(%Y%m%d_%H%M%S), config: config, # 包含所有参数、数据文件路径、手续费滑点假设 metrics: metrics, # 年化、回撤、夏普、交易次数 equity_curve_last_10: equity_curve.tail(10).tolist(), random_seed: config.get(random_seed, 0) } with open(fbacktest_record_{record[timestamp]}.json, w, encodingutf-8) as f: json.dump(record, f, ensure_asciiFalse, indent2)这段代码把配置、绩效指标、净值最后 10 个值和随机种子一起写入文件。config 里必须记录数据文件名、复权方式、手续费率、滑点率、所有策略参数和生成时间。有了这个 JSON你可以在两周后准确回答三个问题这个结果是用哪份数据跑的用了哪些参数当时的假设是什么我还习惯在 JSON 之外生成一份同样的 markdown 报告里面包含参数表、样本内外绩效对比和净值图路径。别人拿到你的源码和报告能照着跑出完全一致的结果这才算一份合格的量化策略源码。多年下来我发现能持续产生收益的策略都不是靠某个神秘因子而是靠一套让自己不会骗自己的流程。希望这个存档习惯对你也有用。本文还有配套的精品资源点击获取