ARTICLE DETAIL

资讯详情

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

Ptrade量化回测策略源码详解:从双均线信号到绩效评估

Ptrade量化回测策略源码详解:从双均线信号到绩效评估 做量化的人应该都听过Ptrade这个名字它是国内不少券商直接内置在客户端里的量化交易终端支持Python策略编写、历史回测、模拟交易和实盘交易。很多刚开始接触量化的朋友第一段代码就是在Ptrade的Notebook里跑通的。今天想把这个环境里的回测策略源码做一个比较完整的拆解讲清楚从策略思路到代码落地、再到回测结果分析的完整闭环。这篇内容会围绕一个基于双均线逻辑的示例策略展开解释每一段代码在真实回测环境里承担什么职责也会穿插我在实际调试过程中踩过的坑。适合刚学Python、想接触量化回测的初学者也适合已经在用Ptrade但希望把策略写得结构更清晰的交易爱好者。1. 内容整体设计与思路拆解1.1 为什么要选择Ptrade作为回测环境Ptrade在国内的普及度很高原因主要有几点。第一它直接集成在主流券商的交易客户端里这意味着你不需要额外搭建数据库、行情源和交易通道登录账户之后就能在同一个软件里完成从研究到回测再到模拟交易的完整链路。第二它内置了Python环境虽然这个环境相比本地的Anaconda要精简不少但已经涵盖了NumPy、Pandas这些量化分析必备的库。第三它的回测引擎底层是事件驱动架构能够比较真实地模拟订单撮合、涨跌停限制、手续费和滑点回测结论的参考价值要远高于那种简单的向量化回测。我个人更看重的是Ptrade的策略编写与调试体验。它把行情获取、下单、持仓查询封装成了非常简洁的API比如get_history、order_target_value这类函数几乎不需要你关心底层数据合约如何获取、券商接口如何对接只需要把精力集中在策略逻辑本身上。这对于初学者来说非常友好对于老手来说也能大幅缩短策略迭代周期。1.2 一个完整回测策略应该包含哪些模块很多初学者从网上找了一段源码复制进Ptrade里点了一下回测看到一条资金曲线就认为“策略已经写完了”。其实一个结构完整、便于调试和扩展的量化策略至少应该拆分成四个模块初始化模块负责设定策略名称、基准标的、初始资金、手续费率、滑点模型、股票池范围以及策略中需要预定义的全局变量。行情处理与信号生成模块这是策略的核心大脑负责从行情接口读取数据、计算技术指标、生成买入或卖出信号。交易执行模块负责把信号转化为实际订单记录交易价格、成交数量更新当前的持仓状态。绩效评估与日志输出模块负责输出每日账户净值、持仓明细、交易记录以及回测结束后的绩效指标汇总。需要说明的是Ptrade本身已经帮我们完成了模块四的大部分工作回测结束后它会自动呈现年化收益率、最大回撤、夏普比率、胜率等指标。因此我们在编写源码时重点精力应放在模块二和模块三。下面我就围绕这两个核心模块讲解一段可运行的双均线策略源码。2. 核心细节解析与实操要点2.1 双均线策略的信号生成逻辑双均线策略属于趋势跟踪策略的一种核心假设是当短期均线从下方向上穿越长期均线时市场动能由空转多产生买入信号反之当短期均线从上方向下穿越长期均线时市场动能转弱产生卖出信号。这个逻辑听起来很简单但真正落地到Ptrade的代码里有几个细节需要特别留意。在Ptrade中获取历史行情数据最常用的API是get_history。它支持多种参数组合比如get_history(10, 1d, close)表示获取最近10个交易日的日线收盘价。需要注意的是这里的返回结果通常是按时间倒序排列的最新一天的数据在最前面。如果你习惯使用Pandas的DataFrame需要先做一步倒序反转否则用rolling计算均线时得到的结果会完全是错的。均线的计算方式有两种。一种是用Pandas自带的rolling(window).mean()代码简洁另一种是用NumPy的卷积函数np.convolve计算效率更高。考虑到Ptrade回测引擎的性能瓶颈通常不在指标计算上我更推荐使用Pandas方式代码可读性更强方便后续把双均线策略扩展成三均线甚至多均线策略。def initialize(context): # 回测的基本设置 set_benchmark(000300.XSHG) set_option(use_real_price, True) set_order_cost(OrderCost(open_tax0.001, close_tax0.001, open_commission0.0003, close_commission0.0003, close_today_commission0, min_commission5), typestock) g.security 600000.XSHG g.short_window 5 g.long_window 20 g.stock_pool [600000.XSHG, 000001.XSHE, 600036.XSHG]上面这段初始化代码里包含了很多量化新手容易忽略的关键参数。set_benchmark决定绩效报告里默认对照的基准指数建议设定成沪深300或者中证500这样回测结束后可以直接看到策略相对于大盘的超额收益。set_option(use_real_price, True)表示使用真实价格成交如果这个参数设置错误回测结果会与实际交易产生明显偏差。set_order_cost里的手续费参数需要按照你所开户券商的实际费率填写印花税和佣金分开设置。2.2 订单执行与仓位管理细节信号生成只是第一步如何把信号转化成订单并且在回测中保持预期的仓位这需要精心设计。Ptrade里最常用的两个下单API是order_target_value和order。order_target_value(security, value)的含义是调整某只股票的持仓市值到指定数值。比如order_target_value(600000.XSHG, 100000)的含义是如果我当前持有这只股票市值5万元系统会自动买入约5万元如果当前持仓市值为12万元系统则会自动卖出约2万元。这种目标市值下单方式非常契合量化策略的调仓逻辑因为它不需要你在代码里反复计算“我需要买入多少股”只需要关心“我想让持仓变成什么状态”。相对地order函数用于一次性买入指定数量的股票适合那种按固定股数调仓的场景。我个人的习惯是在均线策略里优先使用order_target_value因为它天然实现了仓位归一化可以避免因为价格波动导致策略实际持仓比例偏离预期。def handle_data(context, data): if len(data) g.long_window: return for stock in g.stock_pool: price_data attribute_history(stock, g.long_window 1, 1d, [close], dfTrue) close_prices price_data[close] short_ma close_prices.rolling(windowg.short_window).mean() long_ma close_prices.rolling(windowg.long_window).mean() current_price context.portfolio.positions[stock].price if short_ma.iloc[-1] long_ma.iloc[-1] and short_ma.iloc[-2] long_ma.iloc[-2]: target_value context.portfolio.total_value / len(g.stock_pool) order_target_value(stock, target_value) elif short_ma.iloc[-1] long_ma.iloc[-1] and short_ma.iloc[-2] long_ma.iloc[-2]: order_target_value(stock, 0)这段handle_data函数是策略的主循环体。Ptrade的策略架构里handle_data默认在每分钟的bar数据到来时被调用一次。如果我只想进行日线级别的调仓需要在策略设置里把运行频率调整到按天运行或者在代码里加上时间过滤条件。上面代码中attribute_history取出了股票过去21个交易日的收盘价之所以取long_window 1而非long_window是为了在计算均线时保证short_ma.iloc[-2]这个“前一天的均线值”是有效的否则前一天的数据点会落入NaN区间。2.3 绩效评估维度与归因分析回测结束后Ptrade会生成详细的绩效报告。很多初学者只看年化收益率这一个数字这其实是远远不够的。我通常会用四个维度去审视一段回测源码的优劣收益维度总收益率、年化收益率、战胜基准的超额收益。但如果最大回撤过大再高的年化收益也没有实际意义。风险维度最大回撤、波动率、下行标准差。最大回撤决定了策略持仓过程中的心理承受能力也是后续做资金管理时最重要的参考指标。风险调整后收益夏普比率、卡玛比率、索提诺比率。夏普比率大于1说明策略的风险调整后收益表现不错大于2则属于非常优秀。交易质量维度胜率、盈亏比、平均持仓周期、换手率。一个胜率很高但盈亏比很低的策略长期运行下来的结果往往不如胜率适中但盈亏比极高的策略。这里有一个非常关键的概念需要特别指出夏普比率是基于日频收益计算出来的但它并不区分上行波动和下行波动。因此在实际使用中我会同时关注最大回撤和索提诺比率。索提诺比率只惩罚下行波动对上行波动不加惩罚更贴近真实交易者的风险感知。3. 实操过程与核心环节实现3.1 回测前需要确认的参数清单在把策略源码提交到回测引擎之前我建议你把以下参数逐个确认一遍把它们当成一项检查清单来用参数名称推荐初值说明回测区间2015.01.01 至 2023.12.31最好覆盖一轮完整的牛熊周期检验策略在极端行情下的表现初始资金1000000不宜太低否则资金分配和手续费对结果的影响会失真手续费率佣金万2.5~万3印花税千1按券商实际费率填写卖出时收取印花税滑点模型固定滑点 0.01元 或 比例滑点 0.1%回测默认滑点过小建议手动设置以贴近实盘股票池3~5只基本面差异较大的股票避免同质化标的导致策略结论失真调仓频率日线级别每日收盘判断均线策略不需要日内高频调仓日频足够3.2 使用滑点和手续费优化回测真实性很多人在Ptrade回测时完全不设置滑点只用默认价格成交这会让回测结果过于乐观。真实市场中你的大额买入订单会在买一价基础上推高价格大额卖出订单则会压低卖一价这就是滑点的来源。对于小资金回测来说滑点的影响或许可以忽略但如果你想验证一个策略未来是否有实盘潜力设置合理的滑点模型是必须的。在Ptrade中可以用set_slippage来设置滑点模型例如set_slippage(FixedSlippage(0.01))这一段代码的含义是每笔交易在成交价基础上多计入0.01元的滑点成本。对于单价10元左右的股票来说相当于每股多付出0.1%的交易摩擦成本这个数值比较贴近真实交易体验。如果你的策略交易频率很高建议同时把佣金费率调高一点因为高频策略对交易成本的敏感度是成倍放大的。3.3 完整回测源码的流程串联我整理了一份完整的可运行回测源码骨架把初始化、信号生成、交易执行、日志输出都串联在一起。这份源码可以直接复制到Ptrade的Notebook或者策略编辑器里运行但它只是一个教学性质的学习框架不建议直接用于实盘。# Ptrade量化回测策略——双均线示例仅供学习 def initialize(context): set_benchmark(000300.XSHG) set_option(use_real_price, True) set_order_cost(OrderCost(open_tax0, close_tax0.001, open_commission0.0003, close_commission0.0003, close_today_commission0, min_commission5), typestock) set_slippage(FixedSlippage(0.01)) g.security_pool [600000.XSHG, 000001.XSHE, 600036.XSHG] g.short_window 5 g.long_window 20 g.position_pct 0.9 def handle_data(context, data): # 每个交易日运行一次 for stock in g.security_pool: hist attribute_history(stock, g.long_window 1, 1d, [close], dfTrue) close hist[close] short_ma close.rolling(g.short_window).mean() long_ma close.rolling(g.long_window).mean() # 金叉买入 if short_ma.iloc[-1] long_ma.iloc[-1] and short_ma.iloc[-2] long_ma.iloc[-2]: target_value context.portfolio.total_value * g.position_pct / len(g.security_pool) log.info(金叉信号买入 %s目标市值 %.2f, stock, target_value) order_target_value(stock, target_value) # 死叉卖出 elif short_ma.iloc[-1] long_ma.iloc[-1] and short_ma.iloc[-2] long_ma.iloc[-2]: log.info(死叉信号清仓 %s, stock) order_target_value(stock, 0)这段代码的核心逻辑并不复杂但里面有几个容易出问题的细节值得单独说明。第一attribute_history函数返回的是DataFrame格式但如果取数频率设置为1d它默认返回的数据量是当前开盘前的历史数据不包含当天的实时bar。这意味着你用来计算均线的“当前值”实际上是昨收信号产生后订单会在当日开盘或者次日开盘执行存在一个天然的延迟。这个延迟在日线策略里是正常的也是回测贴近实盘交易所必须的不必刻意修正。第二金叉判断条件里用到了short_ma.iloc[-1]和short_ma.iloc[-2]两个值。前一个表示最新交易日的短期均线值后一个表示前一交易日的短期均线值。只有当“最新值大于长期均线、但前一值小于等于长期均线”时才认定为一个有效的金叉信号。如果你只判断short_ma.iloc[-1] long_ma.iloc[-1]那策略会将所有短期均线位于长期均线上方的交易日都视为买入日导致重复加仓。第三清仓时使用的是order_target_value(stock, 0)它会自动卖出当前持有的全部股票而不需要你来计算持仓数量。同时由于使用order_target_value而非order_percent即使某只股票在停牌期间无法交易也不会影响其他股票的调仓。4. 常见问题与排查技巧实录4.1 回测结果曲线消失或收益率为空这是我见过最多的问题。回测结束后收益曲线一片空白或者绩效报告显示收益率为0%但日志里又明明输出了下单记录。出现这种情况绝大多数原因是股票代码填写错误。Ptrade的股票代码格式是“证券代码 交易所后缀”上交所股票以.XSHG结尾深交所股票以.XSHE结尾。例如浦发银行写600000.XSHG平安银行写000001.XSHE。如果你只填了600000或者000001数据接口会返回空数据策略在handle_data第一步的attribute_history阶段就会得到空DataFrame整个策略静默失败。另一个常见原因是回测区间选择过短。如果你只回测最近10个交易日而长期均线窗口是20天那么在整个回测周期内策略都处于均线数据不足的状态无法产生任何交易信号收益曲线自然为空。建议回测区间至少覆盖半年以上并且比最长均线窗口多出至少两倍的时间长度。4.2 实盘模拟和回测结果差异明显有些人在Ptrade里做了回测觉得策略不错于是启用了模拟交易但发现模拟交易的结果与回测差距很大。这里我需要强调一个重要的观念回测结果和模拟盘结果永远不可能完全一致也不应该完全一致。原因在于回测引擎在处理信号时有一个无法避免的假设信号产生后按照某一时刻的价格成交。而模拟交易则是在信号产生后的下一个可交易时刻真实下单成交价格取决于当时的市场盘口深度。两者的差异主要体现在滑点、涨跌停限制和停牌处理上。我在实操中发现最典型的偏差来自涨跌停。回测中如果某只股票在信号产生当日涨停order_target_value依然会按涨停价尝试买入并假设成交。但模拟盘和实盘中涨停板上的买一单往往排队很长你的订单大概率无法排到。为了解决这一点我会在代码里加一个简单的过滤条件来判断当日是否涨停封板如果涨停则放弃买入而不是无脑下单。# 简易涨停过滤 current_data get_current_data() if current_data[stock].high_limit current_data[stock].last_price: log.info(股票 %s 涨停放弃买入, stock) continue4.3 均线策略参数过拟合的自查方法双均线策略虽然简单但同样面临参数过拟合的风险。很多人只是把short_window从3改成5、从5改成8然后把所有组合跑一遍选收益率最高的那个参数组合这种做法实际上是数据挖掘而不是策略开发。这样选出来的参数在未来行情中大概率会失效。我常用的自查方法是把样本区间分成两段前70%作为训练集后30%作为验证集。在训练集上做参数寻优然后把选出的最优参数放到验证集上跑一遍。只有在验证集上仍然表现稳定的参数才具备基本的实战价值。这个原则可以应用在几乎所有量化策略的开发流程中不仅限于均线策略。4.4 高频买入重复下单的问题双均线策略的雏形代码跑回测时经常出现超额买入的情况。比如某一次金叉信号出现后账户里买入了超出目标市值的股票。追溯原因往往是在handle_data的每次被调用时都去检查并执行买入逻辑而Ptrade在分钟级调用下一天内会触发多次handle_data。解决这个问题最简单的方法是把调仓判断限制在每天一次比如只在每天14:50之后执行调仓或者更简单地通过查看“今天是本周期内第几个交易日”来控制。更专业的做法是通过持仓状态的上下文判断比如加一个g.last_trade_date全局变量记录上一次调仓日期只在日期变化时才允许再次调仓。import datetime def handle_data(context, data): today context.blotter.current_dt.date() if hasattr(g, last_trade_date) and g.last_trade_date today: return # 执行调仓逻辑 g.last_trade_date today这样写的好处是即便handle_data一天内被调用多次策略也只会执行一次调仓判断既避免了重复下单也让回测结果的资金曲线更加平滑。4.5 日志与调试的实用技巧量化策略调试有一个很尴尬的地方你可能无法实时看到每一行代码的输出。Ptrade提供了log.info()函数来输出调试信息但很多新手不知道这些日志在哪里查看导致策略什么时候买入、什么时候卖出完全看不到出了问题也完全无法定位。我建议在策略的关键节点都加上日志输出比如信号产生、下单动作、持仓更新。更进一步的调试技巧是把每次调仓后的持仓市值和可用资金也打印出来这样回测结束后通过日志回放你可以清楚地看到整个交易过程的资金流向和仓位变化。log.info(当前持仓%s市值 %.2f可用资金 %.2f, context.portfolio.positions[stock].total_amount, context.portfolio.positions[stock].value, context.portfolio.available_cash)日志输出不只是为了调试更是后续策略归因分析的素材。一段真正的量化策略源码应该做到“每一个交易决策都有据可查”而不仅仅是回测报告上那一串冰冷的收益率数字。5. 关于策略源码的合规边界与学习路径建议5.1 为什么强调“仅供学习”标题里明确写了“仅供学习”这不是一句客套话。在真实的金融市场中任何量化策略的开发和实盘使用都需要考量合规边界。自己在Ptrade里编写策略、进行历史回测、参与模拟交易这属于个人学习和研究范畴。但如果你把策略策略源码打包成产品向不特定人群销售或者代客理财这就可能触碰监管红线。Ptrade这个平台本身是合规的量化交易终端但它服务于在不同券商开户的合法投资者。每个人都有权利学习量化交易知识、编写策略、优化模型但请一定记住回测赚钱的策略不代表实盘赚钱历史业绩不代表未来表现。所有策略在上线前都应该经过充分的风险评估而不是仅仅凭一段回测曲线做决策。5.2 从回测到认识的四个阶段量化的学习路径和其他技术领域类似通常经历以下几个阶段工具熟悉阶段学习Ptrade的IDE、API函数、回测设置知道从哪里获取数据、怎么下单。策略复现阶段把经典策略从论文或教材里翻译成代码跑通回测理解每一个参数的含义。独立开发阶段开始有自己的策略想法能够设计信号逻辑、资金管理、风险过滤形成完整的策略源码结构。摒弃单一策略阶段认识到没有永远有效的单一策略开始构建策略组合、动态调整参数、结合市场环境做切换。我见过很多人在第一阶段就停了下来原因不是学不会而是觉得回测报告里的那些公式太复杂产生了畏难情绪。实际上回测报告里的核心指标只需要理解五六个包括总收益率、最大回撤、夏普比率、胜率、盈亏比就足以完成80%的初级策略评估工作。剩下的那些高阶指标完全可以等策略模型逐步完善之后再深入学习。5.3 策略源码的工程化演进方向如果你想在这个方向上继续深入我建议不要止步于今天这段双均线示例。可以在代码基础上做几个方向的演进增加风险控制模块比如设置个股最大回撤止损、组合最大回撤预警、市场状态过滤器等。引入因子分析把单一的均线信号扩展为多因子打分模型比如动量因子、规模因子、价值因子综合选股。改进资金管理使用固定比例仓位还是波动率倒数仓位会对最终收益产生截然不同的影响。建立策略评价对比框架把多个策略放在同一回测区间、同一手续费参数下对比用客观指标而非主观偏好筛选策略。今天这段代码的价值不在于让读者机械地复制粘贴后跑出一个漂亮的回测数字而在于理解一个量化策略从想法到可执行代码之间的翻译过程。把交易逻辑正确地转换成代码逻辑才是量化源码最核心的能力。就我个人的实际体验来说刚开始学量化回测时最容易被“代码能不能跑通”困住反而忽略了“策略逻辑是否合理”。等写过几十个策略、反复调试过信号和仓位之后你会逐渐形成一种直觉拿到一段策略源码先看它的核心逻辑再看它的参数设置最后才看代码细节。这种从全局到局部的审视方式能帮你少走很多弯路。最后再分享一个实用的小技巧。在Ptrade里做回测时刚开始不要用真实的历史行情做全量回测可以先选最近一个月的时间窗口用较快的均线参数把策略跑通确认代码逻辑无误后再扩大到完整的时间区间。这样做的好处是即使出了BUG你也只需要看一个很短时间内的日志定位问题的速度会快很多。
返回列表