第 1 章 · 02 config.json 配置驱动与 Handler 装配


文档摘要

第 1 章 · 02 config.json 配置驱动与 Handler 装配 本节摘要:本节精读 ,这是理解 Lean "回测实盘统一"哲学的钥匙。config.json 采用两层结构:先加载所有顶层配置项,再把 字段指定的那个环境(如 )叠加到顶层——官方注释称之为 "layering affect"(分层叠加效应)。顶层定义全局参数与 10 个系统 handler 字符串;每个环境再定义 6-7 个算法 handler 字符串。切换回测与实盘的本质,就是换这 6-7 个字符串——用户算法代码(IAlgorithm)一行不用改。本节最后钻到装配机制: 反射 + MEF 自注册,把字符串变成实例。 内容来源:原项目源码 (39 KB)、 、 、 。 ⚠️ 注意:config.

第 1 章 · 02 config.json 配置驱动与 Handler 装配

本节摘要:本节精读 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.csCommon/Interfaces/IObjectStore.csCommon/Interfaces/IDataProvider.cs

⚠️ 注意:config.json 虽然扩展名是 .json,但支持 // 行注释(Lean 用自定义 JSON 解析器,允许注释)。所以你能在配置里看到大段中文/英文注释解释每项含义。直接用标准 System.Text.Json 严格模式解析会失败。C# 对照:Python 里这套配置对应 lean.json,字段名完全一致,只是用 json 模块读取——但要注意 Python 标准 json 不支持注释,需用 commentjson 或 Lean 自带解析器。

学习目标

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

  1. 说清 config.json 的两层结构(顶层全局 + environment 叠加)与 "layering affect" 的含义。
  2. 列举顶层 4 个全局参数(algorithm-type-name / algorithm-language / algorithm-location / data-folder)。
  3. 列举顶层 10 个系统 handler 字符串。
  4. 逐字段对比 backtesting / live-paper / live-interactive 三个环境的 handler 差异。
  5. 解释"为什么只换 6-7 个 handler 字符串,IAlgorithm 用户代码零改动"。
  6. 读懂 Composer.GetExportedValueByTypeName<T>(typeName) 的反射逻辑。
  7. 理解 MEF [InheritedExport(typeof(IXxx))] 自注册的作用。

一、两层结构与 layering affect

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"

执行顺序是两步:

  1. 加载顶层:把所有顶层的 key(如 algorithm-type-namelog-handlerdata-folder...)全部读进配置字典。
  2. 叠加 environment:取 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 CSharpPython 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 字符串

紧接着是 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,故放在顶层而非环境层。

四、环境特定 handler:核心差异点

config.json 的 environments 节点(config.json:364 起)定义了多个环境,每个环境覆盖/补充 6-7 个算法 handler。这是"回测实盘统一"的核心。

1. backtesting:全本地 6 件套(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 用的全局开关。

2. live-paper:换数据源 + 纸面券商(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——纸面券商的成交本质和回测一样,只是行情换成实时。

3. live-interactive:真实券商 7 件套(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-brokeragedata-queue-handler 都指向 InteractiveBrokersBrokerage——同一券商类同时充当交易通道和行情通道。这是真实券商的标准 7 件套(相对 paper 多了 brokerage 真委托 + data-queue 实时行情)。

4. 三环境对比表

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 分叉点会再次出现。

五、Composer.GetExportedValueByTypeName:字符串变实例

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 出来

逻辑分两段:

  1. 先在已注册的 parts 里找:用 MatchesTypeName(typeName) 做模糊匹配——typeName 可以是全限定名、全名、或只写类名(这就是 data-channel-provider: "DataChannelProvider" 能生效的原因)。
  2. 找不到就反射 new:扫描已加载程序集里所有能赋给 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 全走这条路。

六、MEF [InheritedExport]:接口级自注册

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 节)。

本节要点回顾

  1. 两层结构:config.json 先加载顶层,再把 environment 指定的环境叠加(layering affect),顶层是默认、环境是差异。
  2. 顶层 4 全局参数:algorithm-type-name / algorithm-language / algorithm-location / data-folder,决定算法身份和数据根目录。
  3. 顶层 10 系统 handler:log / messaging / job-queue / api / map-file / factor-file / data-provider / data-channel / object-store / data-aggregator,系统级与 run 模式无关。
  4. 三环境对比:backtesting 全本地 6 件套;live-paper 换 LiveTradingDataFeed + PaperBrokerage;live-interactive 换 BrokerageTransactionHandler + 真实券商 7 件套。
  5. 统一本质:只换 6-7 个 handler 字符串,IAlgorithm 用户代码零改动。
  6. Composer.GetExportedValueByTypeName:按字符串反射实例化,支持全限定名 / 全名 / 仅类名三种匹配。
  7. MEF [InheritedExport(typeof(IXxx))]:接口贴特性,实现类自动注册为零件,新写实现无需额外声明。

下一节,我们离开配置层,钻进 Launcher/Program.csMain 方法——看 Lean 启动时怎么读 config、装配两套 handler、new Engine、调 engine.Run。


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