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

资讯详情

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

NautilusTrader量化交易终极指南-第4章第1节-量化基础-从向量化到事件驱动

NautilusTrader量化交易终极指南-第4章第1节-量化基础-从向量化到事件驱动 NautilusTrader量化交易终极指南-第4章第1节-从向量化到事件驱动一句话导读同样的策略用 pandas 整列一次性算出回测收益和用事件驱动逐条消息处理看起来只是写法差异实际上决定了你能不能把回测结果当真。本文导航为什么先讲这个pandas 向量化回测长什么样NautilusTrader 事件驱动怎么写两种范式到底差在哪可信度与实盘一致性的本质小结下节预告为什么先讲这个我在研发生成式 AI 应用的日志系统时养成的习惯是先搞清楚数据从哪来到哪去做量化也一样。你写回测本质上是在模拟一套系统在真实行情流里如何做决策。这个如何是逐个 tick 地响应还是拿着整张 K 线表算一遍结果根本是两码事。NautilusTrader 敢拿 Rust 写核心、用纳秒时间戳、把事件驱动一路贯彻到底出发点就是要让回测和实盘跑同一套代码。这一节我从最经典的 pandas 向量化回测讲起再带你切到 NautilusTrader 的事件驱动写法把为什么回测结果不可信这件事讲透。pandas 向量化回测长什么样先看最典型的向量化做法。拿日线 K 线算个均线金叉策略用 pandas 直接把整列的价格一口气算完。importpandasaspd# 假设 df 是 [open,high,low,close,volume] 的日线df[ma_fast]df[close].rolling(10).mean()df[ma_slow]df[close].rolling(30).mean()# 逐行其实还是整列向量化生成持仓信号df[signal](df[ma_fast]df[ma_slow]).astype(int)df[position]df[signal].diff().fillna(0)# 1 开多-1 平多# 用次日收盘价成交df[returns]df[close].pct_change()df[strategy](df[position].shift(1)*df[returns]).fillna(0)df[equity](1df[strategy]).cumprod()这段代码在学术界和很多入门教程里是标准写法跑起来也快——pandas 底层是 numpy整列加减乘除全是 C 速度。但它有几个问题我要拎出来说信号在收盘后才知道却用当天收盘价成交。现实中你看到收盘价的那一刻这个 bar 已经结束最快也只能用下一根的开盘或收盘成交。虽然上面代码用了shift(1)假装下一天但很多人这里根本不 shift直接当天成交等于你能看到未来。没有滑点、没有手续费、没有最小交易单位。pct_change()假设你随时可以按收盘价买卖任意数量账户里钱够不够从不管。持仓变化是事后累加的。整张表先算完信号再往回套收益等于所有决策点在计算开始前就全看过了这不是一个在时间轴上推进的过程。向量化的本质是离线批量计算——它假设整段历史都躺在你面前你随时可以回看。这在数据处理里是好事在回测里恰恰是灾难因为它让偷看未来变得几乎无成本。NautilusTrader 事件驱动怎么写NautilusTrader 不这么干。它把回测看成一场重放行情数据作为事件流一条条喂给策略策略只能基于当前及之前已经看到的消息做决定看不到后面任何一根 K 线。写法示意如下为突出重点我简化了合约和参数配置fromnautilus_trader.modelimportBar,BarTypefromnautilus_trader.model.enumsimportOrderSide,TimeInForcefromnautilus_trader.trading.strategyimportStrategyclassMaCross(Strategy):def__init__(self,bar_type,fast10,slow30):super().__init__()self.bar_typebar_type self.fastfast self.slowslow self.fast_ma[]self.slow_ma[]defon_start(self):# 订阅 1 分钟的 bar 行情self.subscribe_bars(self.bar_type)defon_bar(self,bar:Bar):# 事件驱动每来一根新 bar 触发一次self.fast_ma.append(bar.close.as_double())self.slow_ma.append(bar.close.as_double())self.fast_maself.fast_ma[-self.fast:]self.slow_maself.slow_ma[-self.slow:]iflen(self.slow_ma)self.slow:return# 数据不够不交易fast_valsum(self.fast_ma)/len(self.fast_ma)slow_valsum(self.slow_ma)/len(self.slow_ma)iffast_valslow_valandnotself.portfolio.is_flat(self.symbol):# 金叉开多self.submit_order(self.order_factory.market(instrument_idself.symbol,order_sideOrderSide.BUY,quantityself.qty,time_in_forceTimeInForce.GTC,))eliffast_valslow_valandnotself.portfolio.is_flat(self.symbol):# 死叉平仓走人self.close_all_positions(self.symbol)关键不在算法本身在于触发机制on_bar是被消息总线驱动的回调。内核在某个时间点上收到一根 bar就把这条消息交给策略策略判断、下单订单再作为事件回到执行引擎确认成交。整条链路是时间上一个点一个点推进的策略拿到的永远是此刻的世界视图永远不可能用到未来的数据。这就是事件驱动和向量化的分水岭。两种范式到底差在哪我用一张图把两种范式的时间处理方式对比一下事件驱动向量化非顺序可任意回看只能看当前及之前的事件只用未来可知,结果偏乐观整张K线表载入内存信号整列一次算出收益向量逐元素相乘一次性累积成权益曲线行情逐条流入 MessageBus内核按时间戳排序分发策略 on_bar 回调处理当前bar订单事件进入执行引擎撮合/成交回调回策略左边是把整个时间轴拍平成一列,右边是顺着时间轴一格一格走。本质区别是向量化假设全部信息同时可得事件驱动强行建立信息的时间有序性。再看几个具体层面维度pandas 向量化事件驱动计算方式整列 numpy 一次性算逐事件回调顺序执行时间模型编排行号可随机访问单调递增时间戳只前不退未来信息容易不小心用到结构上杜绝摩擦成本难加入需要后处理引擎层面原生处理滑点手续费换到实盘无法复用重写同一套代码直接跑可信度与实盘一致性的本质这件事我想了很久最后归结为一句话向量化回测测的不是你实盘会跑的策略而是已知全部历史的最优事后策略。实盘里信号来了你要当场决定买不买没有整张表可以回头看。而向量化回测偷偷把未来递给了策略结果就是回测收益系统性偏高——我见过不少因子回测年化 30%上实盘直接变负的案例十有八九是信号和成交时间错位或者用了未来数据。NautilusTrader 之所以坚持事件驱动是为了达成一个工程目标train once, run everywhere。回测、模拟、实盘跑的是同一套策略代码、同一套内核逻辑只是行情来源和执行通道不同。回测里on_bar收到的 bar 是重放的缓存实盘里收到的是交易所推送策略不需要知道区别。这带来的直接好处是两个回测可信。因为执行路径和实盘一致滑点、手续费、部分成交、订单拒绝这些真实摩擦力都在回测里被建模了收益曲线不再是一片虚高。上线零迁移成本。策略写一次回测赚了切 Live 直接把运行环境从BacktestEngine换成LiveEngine冒的风险只是适配器的网络延迟而不是逻辑重写。这也是我强烈建议你从第一天就用事件驱动框架而不是用写论文的向量化工具做可实盘策略的原因。你可以在 pandas 里做因子探索和快速验证真到要上策略必须迁到事件驱动。小结pandas 向量化回测是离线批量计算结构上允许偷看未来回测收益可信度低。事件驱动让策略只能基于当前和过去的事件做决策从架构上杜绝未来函数。NautilusTrader 用同一套代码跑回测、模拟、实盘保证回测与实盘一致。因子探索可以用 pandas可交付策略必须走事件驱动。下节预告既然事件驱动强调确定性地推进时间那时间的精度和价格的精度就极其重要。下一节我讲 NautilusTrader 为确定性而生的领域模型——UnixNanos 纳秒时间戳、Price/Quantity 定点数精度以及为什么用浮点做结算价格会翻车。如果这篇对你有帮助点赞收藏关注三连量化路上不迷路。
返回列表