第 6 章 · 03 UniverseSelection 动态选股


文档摘要

第 6 章 · 03 UniverseSelection 动态选股 本节摘要:本节钻取 Lean 的动态选股层——UniverseSelection。它让算法不必一开始就把所有标的加进来,而是在运行中根据条件增删:比如每季度按 ROE 筛选基本面前 50 只股票。UniverseManager 是一个字典,管理所有 universe;Universe 抽象基类提供 钩子,子类如 CoarseFundamentalUniverse(用价格/成交量粗筛)和 FineFundamentalUniverse(用 PE/ROE 细筛)各自实现筛选逻辑。DataManager 在构造时挂上 事件——universe 一增删就同步建/拆订阅。

第 6 章 · 03 UniverseSelection 动态选股

本节摘要:本节钻取 Lean 的动态选股层——UniverseSelection。它让算法不必一开始就把所有标的加进来,而是在运行中根据条件增删:比如每季度按 ROE 筛选基本面前 50 只股票。UniverseManager 是一个字典,管理所有 universe;Universe 抽象基类提供 SelectSymbols(utcTime, data) 钩子,子类如 CoarseFundamentalUniverse(用价格/成交量粗筛)和 FineFundamentalUniverse(用 PE/ROE 细筛)各自实现筛选逻辑。DataManager 在构造时挂上 UniverseManager.CollectionChanged 事件——universe 一增删就同步建/拆订阅。每个时间步的增删结果通过 SecurityChanges 装进 TimeSlice 传给算法,用户在 OnSecuritiesChanged(changes) 回调里处理新加入/移除的标的。动态选股是组合策略(Alpha/Portfolio/Risk 框架)的 Selection 层基础。

内容来源:原项目源码 Common/Data/UniverseSelection/Universe.csSecurityChanges.csCoarseFundamental.csEngine/DataFeeds/DataManager.csAlgorithm/QCAlgorithm.cs,精读并套用体系化模板。

⚠️ 注意:本节是"动态选股机制",不展开各 Universe 子类的具体筛选公式。重点是数据流:谁触发筛选 → 谁收到增删 → 算法如何响应。

学习目标

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

  1. 解释 UniverseSelection 解决什么问题(运行中增删标的)。
  2. 说清 UniverseManager 的角色(管理多个 universe 的字典)。
  3. 区分 CoarseFundamental(粗筛)与 FineFundamental(细筛)的用途。
  4. 读懂 Universe.SelectSymbols 抽象钩子。
  5. 解释 DataManager 如何通过 CollectionChanged 监听增删。
  6. 说清 SecurityChanges 如何在 TimeSlice 里传递增删、用户如何在 OnSecuritiesChanged 响应。

一、为什么需要动态选股

传统回测是"固定股票池"——一开始 AddEquity("AAPL") 就一直盯着。但很多策略本质是轮动的:每季度选市值前 100、每月选动量前 20、每天按成交量筛活跃合约。如果一开始把全市场几千只都加进来,数据量爆炸;如果只加固定几只,又错过机会。

UniverseSelection 解决这个矛盾:算法声明一个筛选规则,引擎在运行中按规则增删订阅。被选中的标的自动建订阅、喂数据;被剔除的自动拆订阅(但若有持仓会保留到平仓)。

二、UniverseManager:管理多个 universe

Common/Securities/UniverseManager.cs 是个字典,管理算法里所有 universe:

// 简化示意 public class UniverseManager : Dictionary<Symbol, Universe> { // 每加一个 universe,触发 CollectionChanged 事件 }

每次算法 AddUniverse(...)UniverseSettings 配置新规则,都会向 UniverseManager 里加一条。一个算法可同时有多个 universe(如一个粗筛 + 一个细筛 + 一个期权链)。

💡 钻取要点:UniverseManager 实现了 ObservableDictionary,每次 Add/Remove 都触发 CollectionChanged 事件。下游的 DataManager 订阅这个事件,从而"universe 一变,数据流跟着变"。

三、Universe 抽象基类与 SelectSymbols

Common/Data/UniverseSelection/Universe.cs:30:

30 public abstract class Universe : IDisposable 48 public virtual ConcurrentDictionary<Symbol, Member> Securities { get; } // 当前成员 59 public HashSet<Symbol> Selected { get; set; } // 当前选中的标的集合 86 public event EventHandler SelectionChanged; // 选股变化事件 203 public IEnumerable<Symbol> PerformSelection(DateTime utcTime, BaseDataCollection data) 216 var result = SelectSymbols(utcTime, data); // 调子类实现的筛选 244 public abstract IEnumerable<Symbol> SelectSymbols(DateTime utcTime, BaseDataCollection data);

要点:

  • Securities:这个 universe 当前"成员名单"(ConcurrentDictionary,线程安全)。
  • Selected:最近一次筛选选中的标的(可能 ≠ Securities,因为持仓中的标的不立即移除)。
  • PerformSelection:引擎在筛选时刻调用,内部转调 SelectSymbols
  • SelectSymbols:抽象钩子,子类实现筛选逻辑,返回"这次应该选中的 Symbol 列表"。

Universe.cs:41 还有一个哨兵:

41 public static readonly UnchangedUniverse Unchanged = UnchangedUniverse.Instance;

SelectSymbols 可以返回 Universe.Unchanged——表示"这次不调整,保持原样"。常用于"只在月初筛选"的规则:非筛选日直接返回 Unchanged,避免无谓的订阅抖动。

四、CoarseFundamental 与 FineFundamental

Lean 为股票基本面选股预置了两级 universe:

Universe 数据 典型筛选 量级
CoarseFundamental 每日价格 + 成交量 + DollarVolume 选 DollarVolume 前 1000 全市场约 8000 只 → 粗筛到 1000
FineFundamental 完整基本面(PE/ROE/市值/财报...) 选 ROE 前 50 1000 → 细筛到 50

Common/Data/UniverseSelection/CoarseFundamental.cs:25:

25 public class CoarseFundamental : BaseData 26 { 30 public string Market => Symbol.ID.Market; 35 public virtual double DollarVolume { get; } // 当日成交额(粗筛核心) 40 public virtual long Volume { get; } // 当日成交量 45 public virtual bool HasFundamentalData { get; } // 是否有基本面 50 public virtual decimal PriceFactor { get; } // 价格因子(分红调整) 55 public virtual decimal SplitFactor { get; } // 拆股因子 60 public decimal PriceScaleFactor => PriceFactor * SplitFactor; 65 public decimal AdjustedPrice => Price * PriceScaleFactor;

Coarse 每只股票一行,关键字段是 DollarVolume(成交额)——用它排序选流动性高的票。HasFundamentalData 让粗筛顺便过滤掉没有基本面的标的,直接接到 FineFundamental。

两级级联的典型用法:Coarse 选出 DollarVolume 前 1000 → Fine 在这 1000 里按 ROE 选前 50 → 这 50 只才是真正交易的标的。

💡 钻取要点:为什么分两级?因为 Fine 数据量巨大(每只股票几十个财务字段),全市场都加载太慢。先用 Coarse(只有价格+量)筛到 1000 只,再加载这 1000 只的 Fine 数据,性能可控。这是工程上的"漏斗"思想。

五、DataManager:挂 CollectionChanged 监听

Engine/DataFeeds/DataManager.cs:90 在构造函数里挂上监听:

90 algorithm.UniverseManager.CollectionChanged += (sender, args) => 91 { 92 var universe = args.Value; 93 switch (args.Action) 95 case NotifyCollectionChangedAction.Replace: 96 case NotifyCollectionChangedAction.Add: 97 var config = universe.Configuration; 98 var start = algorithm.UtcTime; ... 175 AddSubscription( 176 new SubscriptionRequest(true, universe, security, config, start, end)); 182 break; 184 case NotifyCollectionChangedAction.Remove:

逻辑很清晰:

  • Add:universe 新增时,DataManager 立刻建一个 SubscriptionRequest(数据订阅请求)加进数据流。这个订阅会触发后续的选股动作。
  • Remove:universe 移除时,拆订阅、清理。

也就是说,UniverseManager 的变化通过事件机制自动传导到数据流——用户加一个 universe,引擎自动开始喂数据、跑筛选。

六、SecurityChanges:在 TimeSlice 里传递增删

每次筛选完成后,引擎把"这一步新增/移除了哪些标的"打包成 SecurityChanges,放进 TimeSlice 传给算法。Common/Data/UniverseSelection/SecurityChanges.cs:26:

26 public class SecurityChanges 34 private readonly IReadOnlySet<Security> _addedSecurities; 37 private readonly IReadOnlySet<Security> _removedSecurities; 42 public int Count => _addedSecurities.Count + _removedSecurities.Count + ...; 68 public IReadOnlyList<Security> AddedSecurities => GetFilteredList(_addedSecurities, ...); 80 public IReadOnlyList<Security> RemovedSecurities => GetFilteredList(_removedSecurities, ...); 31 public static readonly SecurityChanges None = new (...); // 无变化时的单例

SecurityChanges 不可变(IReadOnlySet),还区分了 internal add/remove(引擎内部用的,如期货连续合约移仓)与用户的 add/remove。SecurityChanges.None 是无变化时的单例,避免空对象分配。

七、OnSecuritiesChanged:用户回调

Algorithm/QCAlgorithm.cs:1152:

1146 /// <summary> 1147 /// Event fired each time the we add/remove securities from the data feed 1148 /// </summary> 1149 /// <param name="changes">Security additions/removals for this time step</param> 1152 public virtual void OnSecuritiesChanged(SecurityChanges changes) 1153 { 1154 }

这是用户可重写的虚方法。每步收到 SecurityChanges,典型用法:

public override void OnSecuritiesChanged(SecurityChanges changes) { foreach (var added in changes.AddedSecurities) { // 新标的加入:给它建指标、设初始仓位 added.SetFeeModel(new MyFeeModel()); } foreach (var removed in changes.RemovedSecurities) { // 标的被移除:平仓清理 Liquidate(removed.Symbol); } }

注意:被 RemoveSecurity 移除的标的若仍有持仓或挂单,引擎会保留订阅直到平仓——这通过 RemovedSecurities 的备注说明("may still be receiving data due to existing holdings or open orders")。

八、动态选股在框架策略里的地位

Lean 的 Algorithm.Framework 把策略拆成五层:Universe Selection(选股)→ Alpha(信号)→ Portfolio(仓位)→ Execution(执行)→ Risk(风控)。UniverseSelection 是最上游——它决定"看哪些标的",Alpha 只能在选中的标的上产生信号。

Algorithm/Selection/UniverseSelectionModel.cs 是这层的抽象基类,各子类(ManualUniverseSelectionModel / OptionChainedUniverseSelectionModel / FundamentalUniverseSelectionModel 等)实现各自的选股规则,通过 QCAlgorithm.SetUniverseSelection(model) 接入。框架模式下,AlphaModel 自己也有 OnSecuritiesChanged(Algorithm/Alphas/AlphaModel.cs:59),universe 变化时同步通知 Alpha 调整状态。

💡 钻取要点:动态选股 + Alpha 模型的 OnSecuritiesChanged 联动,让"轮动策略"在 Lean 里写起来非常自然:声明规则 → 引擎自动筛选 → 自动通知各层。这是 Lean 区别于很多简单回测框架的关键能力。

本节要点回顾

  1. UniverseSelection 解决运行中按规则增删标的的问题,避免一开始加载全市场。
  2. UniverseManager 是字典,管理所有 universe,Add/Remove 触发 CollectionChanged。
  3. Universe 抽象基类 提供 SelectSymbols(utcTime, data) 钩子,可返回 Universe.Unchanged 跳过本次。
  4. CoarseFundamental(价格+DollarVolume 粗筛)→ FineFundamental(PE/ROE 细筛)两级漏斗。
  5. DataManager 在构造时挂 UniverseManager.CollectionChanged,universe 变化自动建/拆订阅。
  6. SecurityChanges 不可变,装进 TimeSlice 传给算法,区分 AddedSecurities/RemovedSecurities。
  7. OnSecuritiesChanged 是用户重写的回调,被移除标的若有持仓则保留订阅直到平仓。
  8. 框架五层里 Universe Selection 是最上游,Alpha 层通过 OnSecuritiesChanged 联动。

第 6 章结束。你已经理解了 Lean 的数据格式与多资产抽象。下一章(第 7 章)钻取 160+ 技术指标体系——Indicators 项目与指标基类。


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