十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零搭建股票回测系统:backtrader多股回测与数据清洗实战

从零搭建股票回测系统:backtrader多股回测与数据清洗实战 1. 为什么我要自己搭一套回测系统最早动这个念头是因为我在实盘里连续踩了几次坑。同一个策略在某个平台上跑出来的年化收益漂亮得不像话换到另一个平台同样的参数结果直接腰斩。后来花了两周时间逐行对数据才发现问题出在复权处理上——一个用的是前复权一个用的是后复权分红送股那几天的价格序列完全对不上。从那以后我就下定决心核心的回测链路必须自己掌握数据怎么来的、信号怎么算的、成交怎么撮合的每一环都要看得见摸得着。股票策略回测系统说白了就是一台时光机。你把过去几年的行情数据喂进去把交易规则写清楚它帮你模拟如果当时这么操作现在会是什么结果。听起来简单但真正决定这套系统有没有参考价值的是三个东西数据质量、撮合逻辑、以及你对偏差的认知。市面上现成的回测平台不少但要么数据黑盒、要么手续费模型粗糙、要么不支持多标的组合稍微复杂一点的策略就跑不动了。这套系统适合谁如果你已经会用 Python 写点脚本懂基本的均线、动量概念想验证自己的选股或择时想法那自己搭一套是最划算的。不需要服务器集群一台普通笔记本配合 backtrader 这类成熟框架一个周末就能跑通从数据到结果的完整链路。下面我把自己这套系统的搭建过程完整拆一遍包括数据怎么存、框架怎么选、多股回测怎么处理、以及那些只有踩过才知道的坑。2. 整体架构设计与技术选型思路2.1 从数据到结果的四层结构一套能长期用的回测系统我把它拆成四层每层职责单一方便单独替换和调试。第一层是数据层负责原始行情的获取、清洗、复权、存储。第二层是策略层定义买卖信号的计算逻辑只关心什么时候买、什么时候卖不碰资金和订单。第三层是回测引擎层也就是 backtrader 这类框架负责撮合订单、计算持仓、扣手续费、记录净值曲线。第四层是分析层把回测输出的交易记录和净值序列做统计算夏普、最大回撤、胜率这些指标最后可视化。这么分层的好处是当结果不对劲时你能快速定位是哪一层出了问题。比如净值曲线异常平滑大概率是数据层用了未来函数比如成交价格总是当天最低点那是撮合逻辑写错了。2.2 为什么选 backtrader 而不是自己从零写我一开始也想过纯手写回测循环毕竟逻辑不复杂。但真写起来才发现光是处理订单类型市价、限价、止损、仓位管理、多标的资金分配这几块就够写上千行而且极容易出 bug。backtrader 的价值在于它把这些脏活累活都封装好了你只需要专注写策略逻辑。选它的几个实际理由一是事件驱动模型清晰每个 bar 到来时按顺序触发 next 方法符合真实交易的时序二是内置分析器丰富夏普、回撤、交易统计直接调用三是支持多数据源多股回测时不用自己写循环对齐时间轴四是社区资料多遇到问题基本能搜到答案。当然它也有缺点比如文档偏老、部分 API 设计反直觉但整体瑕不掩瑜。2.3 数据存储为什么我最终选了本地文件加数据库混合数据存哪儿这个问题我折腾过好几轮。最开始直接用 CSV简单是简单但股票数量一多每次回测都要重新读盘解析慢得让人抓狂。后来换成 SQLite查询快了但复权计算每次都要现算还是不够利索。最终的方案是混合存储原始未复权数据存 SQLite保证数据可追溯复权后的数据按标的存成独立的二进制文件parquet 格式回测时直接内存映射读取。这样既保留了原始数据的完整性又让回测读取速度提升了十几倍。parquet 的列式存储对回测特别友好因为回测通常只关心开高低收和成交量这几列不需要把整个表都读进来。提示复权数据一定要和原始数据分开存并且记录清楚用的是前复权还是后复权。我见过太多人因为复权方式混用导致回测结果完全失真。3. 数据获取与清洗的核心细节3.1 数据源的获取与字段规范数据是一切的根基。我用的数据源主要是公开的行情接口获取日线级别的开高低收、成交量、成交额以及复权因子。这里要特别注意字段的统一命名不同数据源对收盘价的叫法可能是 close、closing_price、CLOSE如果不做映射后面策略里引用字段就会出错。我习惯在数据层做一次标准化统一成 open、high、low、close、volume、amount 六个字段外加一个 trade_date 作为索引。复权因子单独存一列 adj_factor方便后续计算。数据获取这块建议写一个带重试机制的采集脚本因为网络请求偶尔会失败如果直接抛异常中断整个采集流程就得重来。import pandas as pd import time def fetch_daily(symbol, start, end, retry3): for i in range(retry): try: df your_data_api(symbol, start, end) df df.rename(columns{ Open: open, High: high, Low: low, Close: close, Volume: volume, Amount: amount }) df[trade_date] pd.to_datetime(df[trade_date]) return df.set_index(trade_date).sort_index() except Exception as e: if i retry - 1: raise time.sleep(2 ** i)3.2 复权处理前复权还是后复权这是回测里最容易出错的地方没有之一。前复权是以当前价格为基准往前调整历史价格会被改动适合看盘和展示后复权是以历史价格为基准往后调整当前价格会变高适合回测计算收益。为什么回测要用后复权因为回测是在时间轴上向前推进的你在某个历史时点买入当时的真实价格不应该被未来的分红送股影响。如果用前复权每次有新分红历史价格都会变回测结果就不稳定了。计算方式上后复权价格等于原始价格乘以复权因子。复权因子需要从数据源获取或者根据分红送股记录自己算。我建议直接用数据源提供的因子自己算容易漏掉配股、增发这些复杂情况。3.3 数据清洗的四个必做动作拿到原始数据后不能直接喂给回测引擎必须做清洗。我固定做四件事第一去重。同一交易日同一标的只保留一条记录重复的按最新覆盖。第二补缺。停牌日的数据是缺失的但回测引擎需要连续的时间轴我的做法是停牌日沿用前一日的收盘价成交量置零并在数据里标记 is_suspended 字段。第三异常值处理。涨跌幅超过合理范围的比如超过 20% 且非新股标记出来人工核查避免脏数据污染信号。第四时间对齐。多股回测时不同标的的交易日可能不完全一致需要统一到同一个交易日历上。注意停牌日的处理方式会显著影响回测结果。如果简单跳过停牌日会导致持仓时间被压缩如果沿用前收盘价则要确保策略不会在停牌日产生交易信号。3.4 数据质量的自检清单每次更新数据后我都会跑一遍自检脚本检查几个关键点数据的时间范围是否连续、每个标的的记录数是否合理、复权后的价格是否出现负值或极端跳变、成交量为零的天数占比是否异常。这套自检帮我提前发现过好几次数据源接口变更导致的字段错位问题。下面是我常用的自检项表格检查项判断标准异常处理时间连续性相邻交易日间隔不超过 10 个自然日检查是否漏采价格合理性复权后价格均为正核查复权因子成交量非停牌日成交量大于零标记可疑记录记录数与交易日历匹配补采缺失数据字段完整性无空值回填或剔除4. backtrader 策略编写与多股回测实操4.1 策略类的基本骨架backtrader 的策略写在一个继承自 bt.Strategy 的类里核心是init和 next 两个方法。init里声明指标next 里写交易逻辑。指标声明时backtrader 会自动处理数据对齐你不用担心索引问题。import backtrader as bt class DualMAStrategy(bt.Strategy): params ((fast, 5), (slow, 20),) def __init__(self): self.fast_ma bt.indicators.SMA( self.data.close, periodself.p.fast) self.slow_ma bt.indicators.SMA( self.data.close, periodself.p.slow) self.crossover bt.indicators.CrossOver( self.fast_ma, self.slow_ma) def next(self): if not self.position: if self.crossover 0: self.buy() elif self.crossover 0: self.close()这段代码就是最经典的双均线策略。快线上穿慢线买入下穿卖出。看着简单但里面有几个细节值得说params 用元组定义参数方便后续做参数优化CrossOver 指标返回 1 表示金叉、-1 表示死叉、0 表示无信号比手动比较大小更可靠。4.2 多股回测的数据结构处理多股回测是很多人卡住的地方。backtrader 支持传入多个数据源但策略里怎么区分不同标的、怎么分配资金需要自己设计。我的做法是给每个数据源起个名字在 next 里遍历所有数据对每个标的独立判断信号。class MultiStockStrategy(bt.Strategy): def __init__(self): self.indicators {} for d in self.datas: self.indicators[d._name] bt.indicators.SMA( d.close, period20) def next(self): for d in self.datas: ma self.indicators[d._name] if not self.getposition(d): if d.close[0] ma[0]: self.buy(datad, size100) else: if d.close[0] ma[0]: self.close(datad)这里的关键是 self.datas 是一个数据集合遍历它就能拿到每个标的。getposition(d) 查询该标的的持仓buy 和 close 都要指定 data 参数否则会操作错标的。资金分配上backtrader 默认按可用现金平均分配如果想自定义权重需要在 buy 时指定 size 或者用 sizer 控制。4.3 手续费与滑点的真实模拟回测结果和实盘的差距很大一部分来自手续费和滑点。backtrader 里通过 broker 设置cerebro.broker.setcommission(commission0.0003) cerebro.broker.set_slippage_perc(perc0.001)佣金我按万分之三设这是比较接近实际的水平。滑点设千分之一模拟买卖时的价格偏差。别小看这两个参数一个高频策略如果忽略它们回测收益可能虚高一大截。我做过对比同样的策略加上手续费和滑点后年化收益从 25% 掉到 18%最大回撤反而变大这才是更接近真实的数字。提示滑点设置要结合标的的流动性。大盘股滑点可以设小一点小盘股要设大一些否则回测会过于乐观。4.4 参数优化与过拟合防范backtrader 支持参数优化但我要泼一盆冷水优化出来的最优参数大概率是过拟合的。我早期特别喜欢跑参数网格找出夏普最高的那组结果实盘一用就亏。后来学乖了参数优化只用来观察策略的稳健性——如果一组参数在某个区间内表现都不错说明策略逻辑本身站得住如果只有孤零零一个点表现好那基本是拟合噪声。我的做法是把数据分成样本内和样本外样本内优化参数样本外验证。如果样本外表现和样本内差距超过 30%直接放弃这组参数。另外参数不要设太多两三个就够了参数越多过拟合风险越大。5. 回测结果分析与常见问题排查5.1 核心指标怎么看回测跑完backtrader 会输出一堆指标但真正需要重点看的是这几个年化收益率、最大回撤、夏普比率、胜率、盈亏比。年化收益高不代表策略好如果最大回撤也很大那可能是靠加杠杆堆出来的。夏普比率衡量的是单位风险的收益一般大于 1 才算及格。胜率和盈亏比要结合起来看胜率低但盈亏比高说明是截断亏损、让利润奔跑的类型也是可以接受的。指标含义参考标准年化收益率折算到每年的收益跑赢基准即可最大回撤从峰值到谷底的最大跌幅越小越好控制在 20% 内夏普比率单位风险的超额收益大于 1 及格大于 2 优秀胜率盈利交易占比结合盈亏比看盈亏比平均盈利除以平均亏损大于 1.5 较好5.2 常见问题速查表回测过程中遇到的问题我整理成了一张速查表基本都是踩过的坑问题现象可能原因排查方向净值曲线异常平滑使用了未来函数检查指标是否引用了未来数据成交价总是最优价撮合逻辑过于理想检查是否设置了滑点多股回测结果错乱数据时间轴未对齐统一交易日历收益虚高忽略手续费和滑点补上交易成本参数优化结果不稳定过拟合样本外验证停牌日产生交易停牌数据未标记增加停牌过滤5.3 未来函数最隐蔽的杀手未来函数是回测里最隐蔽也最致命的问题。什么叫未来函数就是你在计算今天的信号时用到了明天才知道的信息。比如用当天的收盘价决定当天开盘就买入这在现实中根本做不到。backtrader 的 next 方法是在 bar 收盘后触发的所以用 close[0] 做判断、下一根 bar 开盘成交是合理的。但如果你在指标里用了 shift(-1) 这类操作就引入了未来数据。排查未来函数的方法很简单把回测结果和实盘对比如果回测收益远高于实盘大概率有问题。另一个方法是逐行审查指标计算看有没有引用未来 bar 的数据。5.4 数据对齐的坑多股回测时不同标的的交易日历可能不一致。比如 A 股有停牌美股有假期如果直接拼接时间轴就乱了。我的处理方式是取所有标的交易日的并集缺失的用前值填充并标记数据来源。这样虽然会引入一些填充数据但保证了时间轴的一致性回测引擎不会因为索引错位而算错。注意填充数据只用于对齐时间轴不能用于产生交易信号。在策略里要判断当前 bar 是否为填充数据如果是跳过交易逻辑。6. 我踩过的坑和几条实操心得6.1 数据更新要自动化手动更新数据是灾难的开始。我一开始每周手动下载一次数据结果有次忘了回测用的还是上个月的数据白白浪费一天时间排查。后来写了个定时任务每天收盘后自动拉取当日数据、更新复权因子、重建 parquet 文件。自动化之后数据新鲜度有了保障回测结果也更可信。6.2 回测日志要详细backtrader 默认的日志很简略出问题时根本不够用。我在策略里加了详细的日志记录每笔交易的时间、价格、数量、触发信号都写进日志文件。这样当结果异常时可以回溯到具体是哪一笔交易出了问题。日志级别分 info 和 debug平时跑 info排查时开 debug。6.3 版本管理不能省策略代码、数据版本、回测配置这三样都要做版本管理。我用 git 管理策略代码每次回测记录 commit hash数据用日期戳标记版本回测配置存成 yaml 文件。这样任何时候都能复现某一次回测不会出现上次那个好结果怎么都跑不出来的情况。6.4 从小样本开始验证不要一上来就跑全市场几千只股票的回测先拿三五只标的跑通流程确认数据、策略、撮合都没问题再扩大范围。我见过有人直接跑全市场结果跑了三个小时发现数据有问题全部重来。小步快跑快速验证是搭系统的基本节奏。6.5 实盘前必须做模拟盘回测再好也只是历史。实盘前一定要跑一段时间的模拟盘用实时数据验证策略的执行逻辑。模拟盘能暴露很多回测发现不了的问题比如信号延迟、订单未成交、资金不足等。我一般会跑至少一个月的模拟盘确认策略在实时环境下的表现和回测接近才考虑小仓位实盘。这套系统搭下来前后花了大概两周的业余时间但后续省下的排查时间远超这个投入。核心链路掌握在自己手里每一个数字怎么来的都清清楚楚这种踏实感是现成平台给不了的。如果你也在纠结要不要自己搭我的建议是先跑通最小可用版本再逐步完善别想着一步到位。
返回列表