第 3 章 · 02 Backtesting 与 LiveTrading 双实现对照 ★


文档摘要

第 3 章 · 02 Backtesting 与 LiveTrading 双实现对照 ★ 本节摘要:这是全书第一个高潮(★)。本节钻透 Lean 最优雅的设计——回测与实盘共用同一套引擎主体。上一节讲了七大 Handler 接口,本节揭示每个接口都有 与 两套实现。切换运行模式,引擎主体( / / )一行代码都不用改,只需改 里 字段,装配器就会把不同的 handler 类名字符串实例化成对应的实现。差异只收敛到三个分叉点:① 的 (时间推进方式)② 的 environment 选哪套 handler ③ 里几处 分支(consolidator 扫描/margin call/delisting)。用户写的 代码对运行模式完全无感。

第 3 章 · 02 Backtesting 与 LiveTrading 双实现对照 ★

本节摘要:这是全书第一个高潮(★)。本节钻透 Lean 最优雅的设计——回测与实盘共用同一套引擎主体。上一节讲了七大 Handler 接口,本节揭示每个接口都有 Backtesting*LiveTrading* 两套实现。切换运行模式,引擎主体(Program.cs/Engine.cs/AlgorithmManager.cs)一行代码都不用改,只需改 config.jsonenvironment 字段,装配器就会把不同的 handler 类名字符串实例化成对应的实现。差异只收敛到三个分叉点:① Engine.cs_liveMode ? LiveSynchronizer : Synchronizer(时间推进方式)② config.json 的 environment 选哪套 handler ③ AlgorithmManager 里几处 _liveMode 分支(consolidator 扫描/margin call/delisting)。用户写的 QCAlgorithm 代码对运行模式完全无感。读完本节,你会理解为什么 Lean 被称为"回测实盘无缝切换的典范"。

内容来源:原项目源码 Engine/Engine.csEngine/AlgorithmManager.csLauncher/config.jsonEngine/DataFeeds/{FileSystemDataFeed,LiveTradingDataFeed,Synchronizer,LiveSynchronizer}.cs、各 Handler 的双实现类,精读并套用体系化模板。

⚠️ 注意:本节是"高潮"节,篇幅充实。核心心法只有一句——引擎依赖接口,不依赖实现。所有模式差异都通过"换接口实现类"消化掉,用户代码与引擎主体都看不到 if(backtest)/if(live) 的 sprawl。请重点抓住"三个分叉点",它们是理解整个设计的钥匙。

学习目标

阅读完本节,你应当能够:

  1. 列出五大 Handler 的 Backtesting* 与 LiveTrading* 双实现类名。
  2. 说清 FileSystemDataFeed(读磁盘)与 LiveTradingDataFeed(实时行情)的本质差异。
  3. 复述三个分叉点的精确位置与作用。
  4. 解释为什么 live-paper 环境的 transaction-handler 仍是 BacktestingTransactionHandler
  5. 理解"实盘独有的 data-queue-handler"是什么。
  6. 说透"回测实盘统一"的本质——模式差异完全收敛到 config.json。

一、双实现对照总表

先给一张全景表,把七个接口的两套实现摆出来:

接口 回测实现 实盘实现
IDataFeed FileSystemDataFeed LiveTradingDataFeed
ISetupHandler BacktestingSetupHandler BrokerageSetupHandler
IResultHandler BacktestingResultHandler LiveTradingResultHandler
ITransactionHandler BacktestingTransactionHandler BrokerageTransactionHandler
IRealTimeHandler BacktestingRealTimeHandler LiveTradingRealTimeHandler
IBrokerage BacktestingBrokerage 真实券商(IB/FXCM/...)或 PaperBrokerage
(实盘独有) data-queue-handler(实时行情源)

注意命名规律:回测侧全用 Backtesting* 前缀(除 DataFeed 用 FileSystem、Brokerage 用 BacktestingBrokerage);实盘侧LiveTrading*Brokerage* 前缀。光看类名就能判断它服务哪种模式。

二、数据流:FileSystemDataFeed vs LiveTradingDataFeed

FileSystemDataFeed(回测,读磁盘)

Engine/DataFeeds/FileSystemDataFeed.cs:39-40:

39 /// <remarks>Filesystem datafeeds are incredibly fast</remarks> 40 public class FileSystemDataFeed : IDataFeed

源码注释直白——"文件系统数据源极快"。它的 CreateSubscription 直接从磁盘 Data/ 目录读取因子化、复权后的 CSV/zip 文件,没有网络开销,没有等待。回测能跑出"每秒百万数据点"的吞吐(L404 的 kps 统计),靠的就是纯本地 IO。

LiveTradingDataFeed(实盘,实时行情)

Engine/DataFeeds/LiveTradingDataFeed.cs(类声明注释):

/// <summary> /// Provides an implementation of <see cref="IDataFeed"/> that is designed to deal with /// live, remote data sources /// </summary> public class LiveTradingDataFeed : IDataFeed

实盘数据源面向"远程实时数据"。它的数据不来自磁盘,而来自 data-queue-handler(实盘独有组件)。config.json 里每个实盘环境都带这个字段:

// Launcher/config.json:388(live-paper 环境) "data-queue-handler": [ "QuantConnect.Lean.Engine.DataFeeds.Queues.LiveDataQueue" ],
// Launcher/config.json:399(live-zerodha 环境,真实券商) "data-queue-handler": [ "ZerodhaBrokerage" ],

💡 钻取要点(高潮心法):注意 live-zerodhadata-queue-handler 直接是券商类名 ZerodhaBrokerage——一个类同时实现 IBrokerage(下单)和 IDataQueueHandler(行情)。这是实盘的典型设计:券商既报行情又接订单,一个 WebSocket 通道复用。回测没有这个字段,因为数据全在磁盘,不需要"队列订阅"。这就是"实盘独有组件"的来历——回测的 FileSystemDataFeed 直接读文件,实盘的 LiveTradingDataFeed 要先通过 data-queue-handler 订阅实时流,再喂给算法。

三、交易:BacktestingTransactionHandler vs BrokerageTransactionHandler

BacktestingTransactionHandler(模拟撮合)

用户下单 → 它直接调 BacktestingBrokerage.PlaceOrderBacktestingBrokerage 内部用 IFillModel(如 ImmediateFillModel)算出成交价 → 触发 OrdersStatusChanged 事件 → TransactionHandler 把成交回填给 Portfolio。整个过程在内存里瞬间完成,没有滑点(除非用户配置了 ISlippageModel)。

BrokerageTransactionHandler(真实成交)

用户下单 → 它把订单转发给真实券商(IBrokerage.PlaceOrder)→ 券商通过 API 提交到交易所 → 交易所部分成交/全部成交 → 券商回调 OrdersStatusChanged → TransactionHandler 回填。整个过程异步、有延迟、可能部分成交、可能被拒单。

特殊环境:live-paper

config.json 的 live-paper 环境(Launcher/config.json:378-391)有个反直觉的配置:

378 // defines the 'live-paper' environment 379 "live-paper": { 380 "live-mode": true, 382 // the paper brokerage requires the BacktestingTransactionHandler 383 "live-mode-brokerage": "PaperBrokerage", 385 "setup-handler": "QuantConnect.Lean.Engine.Setup.BrokerageSetupHandler", 386 "result-handler": "QuantConnect.Lean.Engine.Results.LiveTradingResultHandler", 387 "data-feed-handler": "QuantConnect.Lean.Engine.DataFeeds.LiveTradingDataFeed", 388 "data-queue-handler": [ "QuantConnect.Lean.Engine.DataFeeds.Queues.LiveDataQueue" ], 389 "real-time-handler": "QuantConnect.Lean.Engine.RealTime.LiveTradingRealTimeHandler", 390 "transaction-handler": "QuantConnect.Lean.Engine.TransactionHandlers.BacktestingTransactionHandler" 391 },

L382 的注释 // the paper brokerage requires the BacktestingTransactionHandler 点破了——live-mode: true(实盘时钟),数据是实时的(LiveTradingDataFeed),但交易仍走模拟撮合(BacktestingTransactionHandler)。因为 PaperBrokerage 继承自 BacktestingBrokerage(Brokerages/Paper/PaperBrokerage.cs:29public class PaperBrokerage : BacktestingBrokerage),它的撮合逻辑就是回测那套。对比 live-zerodha 的注释(L397)// real brokerage implementations require the BrokerageTransactionHandler——真实券商才需要 BrokerageTransactionHandler

💡 钻取要点(高潮心法):live-paper 是个"混合模式"——用实盘的时钟和数据,用回测的撮合。这恰恰证明了 Lean 的解耦深度:时钟、数据、撮合三者各自独立可换。你能组合出"实盘数据 + 模拟撮合"这种纸面交易模式,靠的就是每个维度都有独立接口。这就是"统一"的真正含义——不是"只有回测和实盘两种模式",而是"任意组合 handler 的多维开关"。

四、Setup 与 RealTime:BrokerageSetupHandler vs BacktestingSetupHandler

BacktestingSetupHandler:从 config.json 读 starting-cash/start-date/end-date 等数值,设置到 algorithm,然后调 Initialize()。初始现金是用户在 config 里写的数字。

BrokerageSetupHandler(Engine/Setup/BrokerageSetupHandler.cs:282-289):

282 if (liveJob.Brokerage != "PaperBrokerage") 283 { 284 //Zero the CashBook - we'll populate directly from brokerage 285 foreach (var kvp in algorithm.Portfolio.CashBook) 286 { 287 kvp.Value.SetAmount(0); 288 } 289 }

真实券商环境会清零 CashBook,然后从券商 GetCashBalance() 拉真实余额填充。PaperBrokerage 不走这步(用用户配置的初始现金)。这是实盘装配的关键安全设计——绝不信任用户写的初始金额,以券商实际余额为准,否则会爆仓。

RealTime 双实现:BacktestingRealTimeHandlerSetTime(time) 把时钟设到数据时间(算法时间旅行);LiveTradingRealTimeHandlerDateTime.UtcNow 墙钟。两者的 ScanPastEvents 都补漏,但触发源不同。

五、三个分叉点(本节核心)

引擎主体对回测/实盘并非"零感知"——有三个精确定义的分支点。其余全部统一。请记住这三个位置。

分叉点①:Engine.cs L113——Synchronizer 选择(时间推进)

Engine/Engine.cs:113:

113 var synchronizer = _liveMode ? new LiveSynchronizer() : new Synchronizer();

Synchronizer(Engine/DataFeeds/Synchronizer.cs:30)回测按数据时间推进——数据流里下一个 TimeSlice 的时间戳就是当前时间,算法在历史数据里"时间旅行"。

LiveSynchronizer : Synchronizer(Engine/DataFeeds/LiveSynchronizer.cs:31-100)实盘按墙钟推进:

31 public class LiveSynchronizer : Synchronizer 39 { 40 private LiveTimeProvider _frontierTimeProvider; 60 _frontierTimeProvider = new LiveTimeProvider(realTime: TimeProvider); 100 var now = DateTime.UtcNow; // 墙钟驱动

L100 的 DateTime.UtcNow 是实盘的"时间源"。LiveSynchronizer 在墙钟到了下一个数据点的时刻才释放 TimeSlice,没数据就等。

💡 钻取要点:这是唯一一处引擎主体直接根据 _liveMode 选择具体类的地方。但它选的不是 Handler(Handler 由 config 装配),而是 Synchronizer——时间推进器。Synchronizer 不在五大 Handler 之列(它是 Engine 内部的数据同步器),所以这个分叉点更像是"引擎内部的时间策略选择",而非"模式差异"。用户的 QCAlgorithm 完全看不到这个分支。

分叉点②:config.json environment——Handler 套件选择(数据+成交)

这是最核心的分叉点。回测环境(Launcher/config.json:366-376):

366 // defines the 'backtesting' environment 367 "backtesting": { 368 "live-mode": false, 370 "setup-handler": "QuantConnect.Lean.Engine.Setup.BacktestingSetupHandler", 371 "result-handler": "QuantConnect.Lean.Engine.Results.BacktestingResultHandler", 372 "data-feed-handler": "QuantConnect.Lean.Engine.DataFeeds.FileSystemDataFeed", 373 "real-time-handler": "QuantConnect.Lean.Engine.RealTime.BacktestingRealTimeHandler", 374 "history-provider": [ "SubscriptionDataReaderHistoryProvider" ], 375 "transaction-handler": "QuantConnect.Lean.Engine.TransactionHandlers.BacktestingTransactionHandler" 376 },

实盘环境(以 live-zerodha 为例,L394-407):

394 "live-zerodha": { 395 "live-mode": true, 398 "live-mode-brokerage": "ZerodhaBrokerage", 399 "data-queue-handler": [ "ZerodhaBrokerage" ], 401 "setup-handler": "QuantConnect.Lean.Engine.Setup.BrokerageSetupHandler", 402 "result-handler": "QuantConnect.Lean.Engine.Results.LiveTradingResultHandler", 403 "data-feed-handler": "QuantConnect.Lean.Engine.DataFeeds.LiveTradingDataFeed", 404 "real-time-handler": "QuantConnect.Lean.Engine.RealTime.LiveTradingRealTimeHandler", 405 "transaction-handler": "QuantConnect.Lean.Engine.TransactionHandlers.BrokerageTransactionHandler", 406 "history-provider": [ "BrokerageHistoryProvider", "SubscriptionDataReaderHistoryProvider" ] 407 },

对比两段,差异全在 data-feed-handlertransaction-handler——这是"数据源 + 成交"两个维度的切换。装配器(下一节讲的 Composer)读这些字符串,反射实例化成接口对象。引擎主体拿到的只是 IDataFeed/ITransactionHandler 接口引用,不知道具体类型。

分叉点③:AlgorithmManager 内部——几处 _liveMode 分支

AlgorithmManager.cs 里有几处根据 _liveMode 走不同逻辑,它们处理"回测与实盘在算法主循环里的细微差异":

L217(consolidator 扫描精度):

211 // will scan registered consolidators for which we've past the expected scan call. 212 // In live mode we want to round down to the second, so we don't scan too far into the future: 217 var pastConsolidatorsScanTime = _liveMode ? time.RoundDown(Time.OneSecond) : time;

实盘把扫描时间向下取整到秒,避免因为墙钟的毫秒抖动把"未来几百毫秒"的数据错误地塞进当前 K 线。回测按精确时间走。

L345(margin call 触发时机):

344 // perform margin calls, in live mode we can also use realtime to emit these 345 if (time >= nextMarginCallTime || (_liveMode && nextMarginCallTime > DateTime.UtcNow))

回测只在数据时间到了才检查强平;实盘额外允许"即使数据时间没到,只要墙钟已经过了预定时间也检查"——因为实盘可能短暂没数据,但不能因此漏掉强平。

L526(delisting 跟踪):

525 // Only track pending delistings in non-live mode. 526 if (!algorithm.LiveMode) 527 { 528 // Keep this up to date even though we don't process delistings here anymore

退市跟踪只在回测做——实盘退市由券商事件驱动,不在这里处理。

💡 钻取要点(高潮心法):这三处分支处理的都是"回测数据精确、实盘数据有抖动/缺失"带来的工程细节,跟用户的策略逻辑无关。用户写 OnData 时完全不用关心这些——QCAlgorithm 代码对运行模式零感知。这就是 Lean 设计的胜利:把所有模式差异压缩到引擎内部的几个工程点上,把它们留给引擎作者处理,用户的策略代码保持纯净。

六、回测实盘统一的本质

把三个分叉点合起来看,统一性的全貌就清晰了:

统一性体现在三处代码对所有模式通用:

  1. Program.cs:入口逻辑完全一样——读 config、调 Initializer.GetSystemHandlers/GetAlgorithmHandlers、new Engine、engine.Run。不区分模式。
  2. Engine.cs:除了 L113 选 Synchronizer,其余逻辑(装配算法、初始化各 Handler、跑 AlgorithmManager、收尾)对所有模式完全一样。
  3. AlgorithmManager.cs:除了 L217/345/526 三处工程细节,主循环(while 里收 TimeSlice → 调 OnData → 处理订单)对所有模式完全一样。

模式差异完全收敛到 config.json 的 environment 字段。改一个字符串("backtesting""live-interactive"),就从一个模式切到另一个。这是 Lean 最引以为傲的特性——量化研究者写的策略,今天回测、明天上实盘,代码一行不改。

💡 钻取要点(高潮心法收束):"回测实盘统一"不是营销口号,而是依赖反转原则的工程落地。引擎主体(Program/Engine/AlgorithmManager)依赖接口(IDataFeed/ITransactionHandler/...),不依赖实现(FileSystemDataFeed/BrokerageTransactionHandler/...)。回测与实盘的差异被封装在各自的实现类里,对引擎透明。下一节讲 Composer 怎么把 config 里的字符串变成接口实例——这是这套机制的最后一环。

本节要点回顾

  1. 双实现总表:五大 Handler 都有 Backtesting*LiveTrading* 两套实现,命名规律清晰。
  2. 数据流差异:FileSystemDataFeed(读磁盘,incredibly fast)vs LiveTradingDataFeed(实时,依赖 data-queue-handler)。
  3. data-queue-handler 实盘独有:回测无此字段;实盘券商常同时实现 IBrokerage+IDataQueueHandler(如 ZerodhaBrokerage)。
  4. 三个分叉点:① Engine.cs L113 Synchronizer 选择(数据时间 vs 墙钟)② config.json environment 选 Handler 套件(数据+成交)③ AlgorithmManager L217/345/526 工程细节(consolidator/margin call/delisting)。
  5. live-paper 混合模式:live-mode: true + LiveTradingDataFeed + BacktestingTransactionHandler(因 PaperBrokerage 继承 BacktestingBrokerage),注释明确"paper brokerage requires the BacktestingTransactionHandler"。
  6. 统一本质:用户代码与引擎主体(Program/Engine/AlgorithmManager)对所有模式通用,差异完全收敛到 config.json 的 environment 字段。
  7. 设计哲学:依赖反转——引擎依赖接口不依赖实现,这是"一行 config 切换模式"的工程根基。

下一节,我们钻进 Composer——它用 MEF + 反射把 config.json 里的类名字符串变成接口实例,完成"回测实盘统一"的最后一环。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U