第 3 章 · 01 五大核心 Handler 接口 本节摘要:本节是全书第一个高潮(★)的开篇。Lean 把"引擎要做的所有事"切成五大核心 Handler 接口——IDataFeed(数据流)/ISetupHandler(装配)/IResultHandler(结果)/ITransactionHandler(交易)/IRealTimeHandler(定时),加上 IHistoryProvider(历史)与 IBrokerage(券商)。每个接口都标了 ,这是 MEF(Managed Extensibility Framework)的"自注册"标记,实现类只要写好就能被 Composer 扫描到,不用手动登记。
本节摘要:本节是全书第一个高潮(★)的开篇。Lean 把"引擎要做的所有事"切成五大核心 Handler 接口——IDataFeed(数据流)/ISetupHandler(装配)/IResultHandler(结果)/ITransactionHandler(交易)/IRealTimeHandler(定时),加上 IHistoryProvider(历史)与 IBrokerage(券商)。每个接口都标了
[InheritedExport(typeof(IXxx))],这是 MEF(Managed Extensibility Framework)的"自注册"标记,实现类只要写好就能被 Composer 扫描到,不用手动登记。这套接口设计是"回测实盘统一"的地基——下一节你会看到,每个接口都有 Backtesting* 与 LiveTrading* 两套实现,引擎主体对所有实现一视同仁。
内容来源:原项目源码
Engine/DataFeeds/IDataFeed.cs、Engine/Setup/ISetupHandler.cs、Engine/Results/IResultHandler.cs、Engine/TransactionHandlers/ITransactionHandler.cs、Engine/RealTime/IRealTimeHandler.cs、Common/Interfaces/IHistoryProvider.cs、Common/Interfaces/IBrokerage.cs,精读并套用体系化模板。
⚠️ 注意:本节只讲"接口契约",不讲具体实现。五大接口的实现在 Engine 子目录(
FileSystemDataFeed/BacktestingSetupHandler/...),IBrokerage/IHistoryProvider 的实现在 Brokerages 项目与 Engine/HistoricalData 子目录。双实现对照是第 02 节的主题。注意 IBrokerage 接口本身没有[InheritedExport],券商通过IBrokerageFactory(带[InheritedExport])装配——这是它与五大 Handler 的细微差别。
阅读完本节,你应当能够:
[InheritedExport(typeof(IXxx))] 的"接口级自注册"语义。IOrderProcessor/IOrderEventProvider 与 IRealTimeHandler 继承的 IEventSchedule。第 1-2 章我们看到 AlgorithmManager 是回测主循环,它每收到一个 TimeSlice 就调一次算法的 OnData。但 AlgorithmManager 自己不碰磁盘、不碰券商、不写日志——这些活全交给"Handler 插件层"。Lean 把引擎能力切成五块:
| 接口 | 文件 | 职责一句话 |
|---|---|---|
IDataFeed |
Engine/DataFeeds/IDataFeed.cs | 喂数据(行情/基本面)给算法 |
ISetupHandler |
Engine/Setup/ISetupHandler.cs | 装配算法+券商,执行 Initialize |
IResultHandler |
Engine/Results/IResultHandler.cs | 输出结果(图表/统计/日志/错误) |
ITransactionHandler |
Engine/TransactionHandlers/ITransactionHandler.cs | 处理订单与成交回填 |
IRealTimeHandler |
Engine/RealTime/IRealTimeHandler.cs | 定时事件(每天收盘、每月报告) |
外加两个 Common 层接口(不在 Engine 目录,但同样重要):
| 接口 | 文件 | 职责 |
|---|---|---|
IHistoryProvider |
Common/Interfaces/IHistoryProvider.cs | 提供 History() 请求的历史数据 |
IBrokerage |
Common/Interfaces/IBrokerage.cs | 券商抽象(下单/查仓/查现/连接) |
💡 钻取要点:这七个接口的奇妙之处——
Common/Interfaces/目录下共 46 个.cs文件(其中 43 个是真正的接口定义,另 3 个是*EventArgs/*Parameters辅助类)。它们是整个 Lean 项目的"契约中枢"。Engine 的五个 Handler 接口虽然定义在 Engine 目录,但它们的入参大量引用 Common/Interfaces 里的类型(如IAlgorithm/AlgorithmNodePacket/IBrokerage)。Common 层是"谁都依赖、谁都不依赖 Common"的最底层。
Engine/DataFeeds/IDataFeed.cs:28-69(精简):
28 [InheritedExport(typeof(IDataFeed))] 29 public interface IDataFeed 30 { 34 bool IsActive { get; } 35 42 void Initialize(IAlgorithm algorithm, 43 AlgorithmNodePacket job, 44 IResultHandler resultHandler, 45 IMapFileProvider mapFileProvider, 46 IFactorFileProvider factorFileProvider, 47 IDataProvider dataProvider, 48 IDataFeedSubscriptionManager subscriptionManager, 49 IDataFeedTimeProvider dataFeedTimeProvider, 50 IDataChannelProvider dataChannelProvider); 51 57 Subscription CreateSubscription(SubscriptionRequest request); 58 63 void RemoveSubscription(Subscription subscription); 64 68 void Exit(); 69 }
四个核心动作:
Initialize:注入算法、任务包、结果处理器、三个数据辅助 provider(地图文件/因子文件/数据通道)。这是所有 Handler 里入参最多的一个——因为数据流需要知道标的清单、复权方式、数据存放位置等。CreateSubscription:为某个 SubscriptionRequest(标的+分辨率+起止时间)创建一个 Subscription(订阅句柄),返回给 DataManager 管理。返回 null 表示创建失败。RemoveSubscription:移除订阅,回收资源。Exit:外部信号"该收尾了",关闭数据线程。💡 钻取要点:
[InheritedExport(typeof(IDataFeed))]写在接口上而非实现类上。这是 MEF 的"继承式导出"——任何实现IDataFeed的类(无论是FileSystemDataFeed还是LiveTradingDataFeed)都会被自动注册为这个契约的导出。这是 Lean 插件化的关键:新增一种数据源,只需public class XxxDataFeed : IDataFeed,编译进 DLL,Composer 启动时扫描程序集就自动发现它,不用改任何注册代码。
Engine/Setup/ISetupHandler.cs:29-104(精简):
29 [InheritedExport(typeof(ISetupHandler))] 30 public interface ISetupHandler : IDisposable 31 { 43 List<Exception> Errors { get; set; } 52 TimeSpan MaximumRuntime { get; } 60 decimal StartingPortfolioValue { get; } 68 DateTime StartingDate { get; } 76 int MaxOrders { get; } 87 IAlgorithm CreateAlgorithmInstance(AlgorithmNodePacket algorithmNodePacket, string assemblyPath); 96 IBrokerage CreateBrokerage(AlgorithmNodePacket algorithmNodePacket, IAlgorithm uninitializedAlgorithm, out IBrokerageFactory factory); 103 bool Setup(SetupHandlerParameters parameters); 104 }
三个关键方法构成了"装配三段式":
CreateAlgorithmInstance(job, assemblyPath):反射加载用户 DLL,实例化 IAlgorithm。assemblyPath 是 config.json 里 algorithm-location 指向的 DLL(如 QuantConnect.Algorithm.CSharp.dll)。回测与实盘都走这里,差异只在 DLL 里的用户代码。CreateBrokerage(job, algorithm, out factory):创建券商实例。回测返回 BacktestingBrokerage(模拟撮合),实盘返回真实券商(Interactive Brokers/盈透等)。out factory 顺带返回工厂,供后续创建消息处理器用。Setup(parameters):主入口。执行 algorithm.Initialize()(用户写的初始化代码),设置初始现金、起止日期、最大订单数等。返回 true/false 表示是否成功。💡 钻取要点:
ISetupHandler : IDisposable——五个核心接口里只有它继承IDisposable。因为 Setup 阶段可能持有非托管资源(如反射加载的程序集句柄)。Errors是List<Exception>,Setup 出错时不立即抛,而是累积到这里,由Engine.Run统一汇报(L271-293)。这是"宽容失败"模式——尽量收集所有错误一次性报给用户,而不是在第一个错误处崩溃。
Engine/Results/IResultHandler.cs:34-172(精简,挑关键方法):
34 [InheritedExport(typeof(IResultHandler))] 35 public interface IResultHandler : IStatisticsService 36 { 64 void Initialize(ResultHandlerInitializeParams parameters); 70 void DebugMessage(string message); 76 void SystemDebugMessage(string message); 88 void LogMessage(string message); 95 void ErrorMessage(string error, string stacktrace = ""); 102 void RuntimeError(string message, string stacktrace = ""); 115 void Sample(DateTime time); 122 void SetAlgorithm(IAlgorithm algorithm, decimal startingPortfolioValue); 142 void OrderEvent(OrderEvent newEvent); 152 void ProcessSynchronousEvents(bool forceProcess = false); 147 void Exit(); 159 void SaveResults(string name, Result result); 166 void AlgorithmNameUpdated(string name); 171 void AlgorithmTagsUpdated(HashSet<string> tags); 172 }
职责分三类:
DebugMessage/SystemDebugMessage/LogMessage/ErrorMessage/RuntimeError/BrokerageMessage。用户代码里 Debug("...")/Log("...") 最终都调到这里。回测实现写到磁盘 JSON,实盘实现推送到云端消息队列。Sample(time)——每天由 AlgorithmManager 调一次,采集组合净值/图表点,用于绘制权益曲线。SetAlgorithm 在装配完成后注入算法引用。Initialize/ProcessSynchronousEvents/Exit/SaveResults。ProcessSynchronousEvents 在算法主循环的同步点调用,处理累积的消息队列;SaveResults 在结束时把统计结果序列化到磁盘。💡 钻取要点:
IResultHandler : IStatisticsService——它还承担"统计服务"。回测时算夏普/最大回撤/胜率等指标都通过这条线。OrderEvent(newEvent)每次成交都回调,实盘实现用它实时推送订单状态给前端。这是结果处理器最频繁的调用入口。
Engine/TransactionHandlers/ITransactionHandler.cs:31-83(完整):
31 [InheritedExport(typeof(ITransactionHandler))] 32 public interface ITransactionHandler : IOrderProcessor, IOrderEventProvider 33 { 38 bool IsActive { get; } 46 ConcurrentDictionary<int, Order> Orders { get; } 54 IEnumerable<OrderEvent> OrderEvents { get; } 59 ConcurrentDictionary<int, OrderTicket> OrderTickets { get; } 67 void Initialize(IAlgorithm algorithm, IBrokerage brokerage, IResultHandler resultHandler); 72 void Exit(); 77 void ProcessSynchronousEvents(); 82 void AddOpenOrder(Order order, IAlgorithm algorithm); 83 }
注意它继承了两个接口:
IOrderProcessor:提供 Process(OrderRequest)——用户调 MarketOrder(...) 最终走这里。这是"下单入口"。IOrderEventProvider:提供 NewOrderEvent 事件——成交回填时触发,Portfolio 监听它更新持仓。两个字典是订单的全量存储:
| 字典 | key | value | 用途 |
|---|---|---|---|
Orders |
int(orderId) | Order | 所有订单的永久存储 |
OrderTickets |
int(orderId) | OrderTicket | 订单票据(用户可查/改/撤) |
💡 钻取要点:
Initialize(algorithm, brokerage, resultHandler)把券商注入进来。回测时brokerage是BacktestingBrokerage(模拟撮合),实盘时是真实券商。TransactionHandler 充当"用户下单 → 券商执行 → 成交回报"的中介。用户代码只看到MarketOrder(...)返回OrderTicket,完全不知道背后是模拟还是真实券商。这就是"回测实盘统一"在交易侧的体现。
Engine/RealTime/IRealTimeHandler.cs:30-67(完整):
30 [InheritedExport(typeof(IRealTimeHandler))] 31 public interface IRealTimeHandler : IEventSchedule 32 { 36 bool IsActive { get; } 43 void Setup(IAlgorithm algorithm, AlgorithmNodePacket job, IResultHandler resultHandler, IApi api, IIsolatorLimitResultProvider isolatorLimitProvider); 49 void SetTime(DateTime time); 53 void ScanPastEvents(DateTime time); 56 void Exit(); 61 void OnSecuritiesChanged(SecurityChanges changes); 67 }
注意它继承 IEventSchedule——提供 Add(ScheduledEvent)/Remove(ScheduledEvent),管理用户通过 Schedule.On(...) 注册的定时事件(如每天收盘前 10 分钟平仓)。
三个关键方法体现"时钟"语义:
Setup:装配阶段注入算法,把所有 ScheduledEvent 注册进来。SetTime(time):设置当前时间。回测实现把模拟时钟推进到数据时间;实盘实现用墙钟(wall clock)。这是回测与实盘在"时间维度"上的核心分叉(第 02 节详述)。ScanPastEvents(time):补漏——扫描那些因为某个时刻没有数据而"漏触发"的事件。比如用户设了"每天 09:30 触发",但某天 09:30 没有数据点(停牌),实盘靠墙钟能正常触发,回测靠这个方法补。💡 钻取要点:
OnSecuritiesChanged在动态调仓时被调用——当 universe 选股结果变化(新增/移除标的)时,RealTimeHandler 要把这些标的的定时事件也同步增删。这是五大 Handler 里唯一一个所有接口都实现的"联动回调"(IDataFeed/IResultHandler/IRealTimeHandler都有同名方法),体现了"标的集变化会波及数据订阅、结果统计、定时事件三处"的设计考量。
Common/Interfaces/IHistoryProvider.cs:27-48(精简):
27 [InheritedExport(typeof(IHistoryProvider))] 28 public interface IHistoryProvider : IDataProviderEvents 29 { 33 int DataPointCount { get; } 38 void Initialize(HistoryProviderInitializeParams parameters); 46 IEnumerable<Slice> GetHistory(IEnumerable<HistoryRequest> requests, DateTimeZone sliceTimeZone); 48 }
用户代码里 History<TradeBar>("SPY", 30, Resolution.Daily) 最终调 GetHistory,返回 IEnumerable<Slice>。DataPointCount 记录发射了多少数据点,用于回测结束时的吞吐量统计(Engine.Run L402 算"k data points per second")。回测用 SubscriptionDataReaderHistoryProvider 读磁盘,实盘用 BrokerageHistoryProvider 问券商要。
Common/Interfaces/IBrokerage.cs:29-160(精简挑核心):
29 public interface IBrokerage : IBrokerageCashSynchronizer, IDisposable 30 { 34 event EventHandler<BrokerageOrderIdChangedEvent> OrderIdChanged; 39 event EventHandler<List<OrderEvent>> OrdersStatusChanged; 47 event EventHandler<OrderUpdateEvent> OrderUpdated; 73 event EventHandler<AccountEvent> AccountChanged; 78 event EventHandler<BrokerageMessageEvent> Message; 82 string Name { get; } 88 bool IsConnected { get; } 94 List<Order> GetOpenOrders(); 99 List<Holding> GetAccountHoldings(); 106 List<CashAmount> GetCashBalance(); 113 bool PlaceOrder(Order order); 120 bool UpdateOrder(Order order); 127 bool CancelOrder(Order order); 132 void Connect(); 137 void Disconnect(); 154 IEnumerable<BaseData> GetHistory(HistoryRequest request); 160 }
IBrokerage 把券商能力抽象成"连接 + 查询 + 下单 + 事件回调"四类:
Connect/Disconnect/IsConnected。GetCashBalance(查现金)/GetAccountHoldings(查持仓)/GetOpenOrders(查未成交)。PlaceOrder/UpdateOrder/CancelOrder(下/改/撤),返回 bool 表示请求是否提交成功(不等于成交)。OrdersStatusChanged(订单状态变化)/OrderUpdated(订单字段更新,如止损价)/AccountChanged(账户变化)/Message(券商文本消息)。⚠️ 注意:IBrokerage 接口本身没有
[InheritedExport](看IBrokerage.cs:29那一行只有继承没有特性)。它的实现类通过IBrokerageFactory(带[InheritedExport(typeof(IBrokerageFactory))])装配——config.json 里live-mode-brokerage指定的是工厂类名(如"PaperBrokerage"),工厂再Create(brokerageName, algorithm, job)出券商实例。这是 IBrokerage 与五大 Handler 的装配路径差异:Handler 用Composer.Instance.GetExportedValueByTypeName<IResultHandler>(typeName)直接实例化,Brokerage 走工厂模式(因为券商创建需要算法引用、订单处理器注入等复杂参数,工厂能封装这些)。
通览七个接口,能提炼三条共性:
1. 都有 Initialize + Exit 生命周期。 装配阶段调 Initialize 注入依赖,收尾阶段调 Exit 关闭线程/连接。Engine.Run 的 try-finally 保证 Exit 一定被调用(L467-471)。
2. 都有 IsActive 标志。 Engine 收尾时(L429-433)轮询各 Handler 的 IsActive,等所有线程都退出才真正结束。这是优雅停机的协调点。
3. 入参都用 DTO 类封装。 如 SetupHandlerParameters/ResultHandlerInitializeParams/HistoryProviderInitializeParams。这是"参数对象"模式——避免方法签名随依赖增多而膨胀,新增依赖只改 DTO 类不改接口签名。
💡 钻取要点:这套接口设计是"开闭原则"的典范——对扩展开放(新增 Handler 实现只需继承接口),对修改关闭(Engine 主体代码不用改)。下一节你会看到,从回测切到实盘,Engine 主体一行代码都不用动,只换 config.json 里五个 handler 的类名字符串。这就是 Lean 引擎被誉为"C# 量化引擎典范"的核心原因。
[InheritedExport(typeof(IXxx))]。[InheritedExport] 写在接口上,实现类自动被 Composer 发现,不用手动登记。CreateAlgorithmInstance(反射加载 DLL)→ CreateBrokerage(创建券商)→ Setup(执行 Initialize)。IOrderProcessor(下单入口)+ IOrderEventProvider(成交事件),持 Orders/OrderTickets 两个字典。SetTime 推进模拟时钟(回测)/墙钟(实盘),ScanPastEvents 补漏触发。GetHistory 返回 IEnumerable<Slice>,带 DataPointCount 计数。[InheritedExport],走 IBrokerageFactory 工厂装配。下一节,我们看这七个接口的 Backtesting* 与 LiveTrading* 双实现如何对照——这是全书第一个高潮的核心:回测与实盘的统一,本质就是换 handler 实现类。