第 1 章 · 02 config.json 配置驱动与 Handler 装配 本节摘要:本节精读 ,这是理解 Lean "回测实盘统一"哲学的钥匙。config.json 采用两层结构:先加载所有顶层配置项,再把 字段指定的那个环境(如 )叠加到顶层——官方注释称之为 "layering affect"(分层叠加效应)。顶层定义全局参数与 10 个系统 handler 字符串;每个环境再定义 6-7 个算法 handler 字符串。切换回测与实盘的本质,就是换这 6-7 个字符串——用户算法代码(IAlgorithm)一行不用改。本节最后钻到装配机制: 反射 + MEF 自注册,把字符串变成实例。 内容来源:原项目源码 (39 KB)、 、 、 。 ⚠️ 注意:config.
本节摘要:本节精读
Launcher/config.json,这是理解 Lean "回测实盘统一"哲学的钥匙。config.json 采用两层结构:先加载所有顶层配置项,再把environment字段指定的那个环境(如backtesting)叠加到顶层——官方注释称之为 "layering affect"(分层叠加效应)。顶层定义全局参数与 10 个系统 handler 字符串;每个环境再定义 6-7 个算法 handler 字符串。切换回测与实盘的本质,就是换这 6-7 个字符串——用户算法代码(IAlgorithm)一行不用改。本节最后钻到装配机制:Composer.GetExportedValueByTypeName<T>(typeName)反射 + MEF[InheritedExport]自注册,把字符串变成实例。
内容来源:原项目源码
Launcher/config.json(39 KB)、Common/Util/Composer.cs、Common/Interfaces/IObjectStore.cs、Common/Interfaces/IDataProvider.cs。
⚠️ 注意:config.json 虽然扩展名是
.json,但支持//行注释(Lean 用自定义 JSON 解析器,允许注释)。所以你能在配置里看到大段中文/英文注释解释每项含义。直接用标准System.Text.Json严格模式解析会失败。C# 对照:Python 里这套配置对应lean.json,字段名完全一致,只是用json模块读取——但要注意 Python 标准json不支持注释,需用commentjson或 Lean 自带解析器。
阅读完本节,你应当能够:
Composer.GetExportedValueByTypeName<T>(typeName) 的反射逻辑。[InheritedExport(typeof(IXxx))] 自注册的作用。config.json 开头(Launcher/config.json:1-9)就明确说了它的工作方式:
1 { 2 // this configuration file works by first loading all top-level 3 // configuration items and then will load the specified environment 4 // on top, this provides a layering affect. environment names can be 5 // anything, and just require definition in this file. There's 6 // two predefined environments, 'backtesting' and 'live', feel free 7 // to add more! 8 9 "environment": "backtesting", // "live-paper", "backtesting", "live-interactive", "live-interactive-iqfeed"
执行顺序是两步:
algorithm-type-name、log-handler、data-folder...)全部读进配置字典。environment 字段的值(这里是 "backtesting"),去 environments 节点里找到同名子对象,把它里面的 key 逐个覆盖/补充到配置字典里。这个"覆盖"就是注释里说的 layering affect(分层叠加效应)——同一 key,环境层会覆盖顶层;不同 key,环境层会补充进来。所以顶层写的是"所有环境共享的默认值",环境层写的是"本环境特有的差异"。
💡 钻取要点:这种"顶层默认 + 环境覆盖"的设计,本质是把配置的不变部分(全局参数、系统 handler)和可变部分(每个 run 模式的算法 handler)分离。切换模式时只改一行
"environment": "live-interactive",其他都不动。这与 vnpy 的vt_setting.json(单层、无 environment 概念)和 Docker Compose 的override文件是同一类思路。
顶层有 4 个最关键的非 handler 全局参数(config.json:11-25):
11 // algorithm class selector 12 "algorithm-type-name": "BasicTemplateFrameworkAlgorithm", 14 // Algorithm language selector - options CSharp, Python 15 "algorithm-language": "CSharp", 17 //Physical DLL location 18 "algorithm-location": "QuantConnect.Algorithm.CSharp.dll", 19 //"algorithm-location": "../../../Algorithm.Python/BasicTemplateFrameworkAlgorithm.py", ... 24 // engine 25 "data-folder": "../../../Data/",
四个参数决定了"跑什么算法 + 数据在哪":
| 参数 | 含义 | 示例 |
|---|---|---|
algorithm-type-name |
算法的类名(不含命名空间也行) | BasicTemplateFrameworkAlgorithm |
algorithm-language |
CSharp 或 Python |
CSharp |
algorithm-location |
算法 DLL 或 .py 文件路径 | QuantConnect.Algorithm.CSharp.dll |
data-folder |
历史数据根目录 | ../../../Data/ |
注意 algorithm-location 的两种形态:写 C# 时指向编译产物 .dll,写 Python 时指向源码 .py(注释行 config.json:19 给了 Python 示例)。Engine 在 Setup.CreateAlgorithmInstance 时按语言走两条不同的加载路径——C# 用 Assembly.LoadFrom(dll) 反射,Python 用 AlgorithmPythonWrapper 包装。
⚠️ 注意:切换 Python 算法时,三个字段都要一起改:
algorithm-type-name改成 Python 类名、algorithm-language改成"Python"、algorithm-location改成.py路径。漏改任意一个,Engine 找不到类就会报Unable to find type。
紧接着是 10 个系统 handler(config.json:34-44),它们管的是"引擎自己怎么和外界沟通",与具体算法无关:
34 // handlers 35 "log-handler": "QuantConnect.Logging.CompositeLogHandler", 36 "messaging-handler": "QuantConnect.Messaging.Messaging", 37 "job-queue-handler": "QuantConnect.Queues.JobQueue", 38 "api-handler": "QuantConnect.Api.Api", 39 "map-file-provider": "QuantConnect.Data.Auxiliary.LocalDiskMapFileProvider", 40 "factor-file-provider": "QuantConnect.Data.Auxiliary.LocalDiskFactorFileProvider", 41 "data-provider": "QuantConnect.Lean.Engine.DataFeeds.DefaultDataProvider", 42 "data-channel-provider": "DataChannelProvider", 43 "object-store": "QuantConnect.Lean.Engine.Storage.LocalObjectStore", 44 "data-aggregator": "QuantConnect.Lean.Engine.DataFeeds.AggregationManager",
10 个 handler 的分工:
| handler 字符串 | 接口 | 默认实现做什么 |
|---|---|---|
log-handler |
ILogHandler |
日志落盘/上送(CompositeLogHandler 聚合多通道) |
messaging-handler |
IMessagingHandler |
给云端/IDE 发实时消息 |
job-queue-handler |
IJobQueueHandler |
拉取算法 job(本地默认 JobQueue 假队列) |
api-handler |
IApi |
调 QuantConnect 平台 REST API |
map-file-provider |
IMapFileProvider |
提供 symbol 映射(改名/退市),本地从磁盘读 |
factor-file-provider |
IFactorFileProvider |
提供复权因子,本地从磁盘读 |
data-provider |
IDataProvider |
文件级数据读取,默认本地磁盘流 |
data-channel-provider |
IDataChannelProvider |
数据下载通道(本地/云端) |
object-store |
IObjectStore |
算法持久化存储(KV),默认本地文件 |
data-aggregator |
IDataAggregator |
数据聚合器,默认 AggregationManager |
这 10 个 handler 是"系统级"的——本地跑和云端跑都用同一套默认值。注意绝大多数字符串都是全限定类名(命名空间.类名),唯独 data-channel-provider 的值 "DataChannelProvider" 只写类名——因为它是 Lean 内部的简化别名,Composer 会做模糊匹配(见第六节的 MatchesTypeName)。
💡 钻取要点:这些 handler 在
Initializer.GetSystemHandlers()里统一装配(Engine/Initializer.cs:63-71),封装成LeanEngineSystemHandlers容器。它们与"算法 run 模式"无关——回测和实盘都用同一套日志/API/数据 provider,故放在顶层而非环境层。
config.json 的 environments 节点(config.json:364 起)定义了多个环境,每个环境覆盖/补充 6-7 个算法 handler。这是"回测实盘统一"的核心。
config.json:367-376)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": [ "QuantConnect.Lean.Engine.HistoricalData.SubscriptionDataReaderHistoryProvider" ], 375 "transaction-handler": "QuantConnect.Lean.Engine.TransactionHandlers.BacktestingTransactionHandler" 376 },
6 个 handler 全是本地实现:数据从文件系统读(FileSystemDataFeed),成交本地模拟(BacktestingTransactionHandler),定时事件本地触发(BacktestingRealTimeHandler),结果本地写盘(BacktestingResultHandler)。live-mode: false 是给 Globals.LiveMode 用的全局开关。
config.json:379-391)379 "live-paper": { 380 "live-mode": true, 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 },
与 backtesting 的差异:
data-feed-handler 换成 LiveTradingDataFeed(实时行情而非文件)。live-mode-brokerage: "PaperBrokerage"(纸面券商,成交还是本地模拟)。data-queue-handler(实时数据队列)。setup-handler / result-handler / real-time-handler 都换 live 版本。transaction-handler 仍是 BacktestingTransactionHandler——纸面券商的成交本质和回测一样,只是行情换成实时。config.json:446-457)446 "live-interactive": { 450 "live-mode-brokerage": "InteractiveBrokersBrokerage", 451 "setup-handler": "QuantConnect.Lean.Engine.Setup.BrokerageSetupHandler", 452 "result-handler": "QuantConnect.Lean.Engine.Results.LiveTradingResultHandler", 453 "data-feed-handler": "QuantConnect.Lean.Engine.DataFeeds.LiveTradingDataFeed", 454 "data-queue-handler": [ "InteractiveBrokersBrokerage" ], 455 "real-time-handler": "QuantConnect.Lean.Engine.RealTime.LiveTradingRealTimeHandler", 456 "transaction-handler": "QuantConnect.Lean.Engine.TransactionHandlers.BrokerageTransactionHandler", ... 457 },
注意此时 transaction-handler 终于换成 BrokerageTransactionHandler(真实委托走券商网关),live-mode-brokerage 与 data-queue-handler 都指向 InteractiveBrokersBrokerage——同一券商类同时充当交易通道和行情通道。这是真实券商的标准 7 件套(相对 paper 多了 brokerage 真委托 + data-queue 实时行情)。
| handler 角色 | backtesting | live-paper | live-interactive |
|---|---|---|---|
| setup | BacktestingSetupHandler | BrokerageSetupHandler | BrokerageSetupHandler |
| result | BacktestingResultHandler | LiveTradingResultHandler | LiveTradingResultHandler |
| data-feed | FileSystemDataFeed | LiveTradingDataFeed | LiveTradingDataFeed |
| real-time | BacktestingRealTimeHandler | LiveTradingRealTimeHandler | LiveTradingRealTimeHandler |
| transaction | BacktestingTransactionHandler | BacktestingTransactionHandler | BrokerageTransactionHandler |
| live-mode-brokerage | (无) | PaperBrokerage | InteractiveBrokersBrokerage |
| data-queue-handler | (无) | LiveDataQueue | InteractiveBrokersBrokerage |
| live-mode | false | true | true |
只换 6-7 个 handler 字符串,IAlgorithm 用户代码(OnData / Initialize / SetHoldings)零改动——这就是 Lean "回测实盘统一"的本质。对比 vnpy 要为实盘单独写 gateway 适配,vnpy 的策略代码也要适配 gateway 差异;Lean 把适配层全压到 handler,用户代码纯逻辑。
💡 钻取要点:为什么 backtesting 没有
live-mode-brokerage?因为回测的"成交"是BacktestingTransactionHandler+ 内置BacktestingBrokerage(Engine 在Setup.CreateBrokerage时给回测兜底创建)。而 live 模式必须有live-mode-brokerage字段——BrokerageSetupHandler读它来实例化真实券商。这套差异在第 2 章第 02 节 Engine.Run 的 L113 分叉点会再次出现。
config.json 里的 handler 都是字符串,Engine 怎么把它们变成对象?核心是 Composer.GetExportedValueByTypeName<T>(typeName)(Common/Util/Composer.cs:207):
203 /// <param name="typeName">The name of the type to find. This can be an assembly 204 /// qualified name, a full name, or just the type's name</param> 207 public T GetExportedValueByTypeName<T>(string typeName, bool forceTypeNameOnExisting = true) 208 { 209 ... 213 var instance = GetParts<T>().FirstOrDefault(x => !forceTypeNameOnExisting || x.GetType().MatchesTypeName(typeName)); 214 if (instance != null) 215 { 216 return instance; 217 } 218 // 否则遍历所有已加载类型,找匹配的并 new 出来
逻辑分两段:
MatchesTypeName(typeName) 做模糊匹配——typeName 可以是全限定名、全名、或只写类名(这就是 data-channel-provider: "DataChannelProvider" 能生效的原因)。T 的类型,匹配到就 Activator.CreateInstance 实例化。Initializer.Start() 里加载 log-handler 就是典型用法(Engine/Initializer.cs:48):
48 Log.LogHandler = Composer.Instance.GetExportedValueByTypeName<ILogHandler>(Config.Get("log-handler", "CompositeLogHandler"));
读字符串 → 反射实例化 → 注入。10 个系统 handler 和 6-7 个算法 handler 全走这条路。
GetParts<T>() 怎么知道有哪些实现类可用?靠 .NET 的 MEF(System.ComponentModel.Composition)。Lean 在每个 handler 接口上贴 [InheritedExport],例如(Common/Interfaces/IObjectStore.cs:26-27):
26 [InheritedExport(typeof(IObjectStore))] 27 public interface IObjectStore : IDisposable, IEnumerable<KeyValuePair<string, byte[]>>
IDataProvider 也一样(Common/Interfaces/IDataProvider.cs:26-27):
26 [InheritedExport(typeof(IDataProvider))] 27 public interface IDataProvider
[InheritedExport(typeof(IXxx))] 的语义是:任何实现这个接口的类,都会被 MEF 自动当作该接口的导出件注册,不需要在实现类上再贴 [Export]。所以写一个新券商,只要 public class XxxBrokerage : IBrokerage,它就自动进了 Composer 的零件库,config.json 里写类名即可引用。
Composer 构造时会扫所有已加载程序集,把带 [InheritedExport] 接口的实现收集起来(Composer.cs:270):
270 .Where(interfaceType => interfaceType.GetCustomAttribute<InheritedExportAttribute>() != null);
💡 钻取要点:这套"接口贴
[InheritedExport]+ Composer 按名反射"的设计,等价于 vnpy 里importlib扫描插件目录 + 字典登记的思路,但更"自动化"——C# 的 MEF 在编译期就把元数据嵌进 DLL,运行时反射即可,不需要约定目录结构。代价是首次扫描所有程序集较慢(Engine 构造时用Task.Run异步预热,见第 2 章第 01 节)。
environment 指定的环境叠加(layering affect),顶层是默认、环境是差异。下一节,我们离开配置层,钻进
Launcher/Program.cs的Main方法——看 Lean 启动时怎么读 config、装配两套 handler、new Engine、调 engine.Run。