第 6 章 · 03 UniverseSelection 动态选股 本节摘要:本节钻取 Lean 的动态选股层——UniverseSelection。它让算法不必一开始就把所有标的加进来,而是在运行中根据条件增删:比如每季度按 ROE 筛选基本面前 50 只股票。UniverseManager 是一个字典,管理所有 universe;Universe 抽象基类提供 钩子,子类如 CoarseFundamentalUniverse(用价格/成交量粗筛)和 FineFundamentalUniverse(用 PE/ROE 细筛)各自实现筛选逻辑。DataManager 在构造时挂上 事件——universe 一增删就同步建/拆订阅。
本节摘要:本节钻取 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.cs、SecurityChanges.cs、CoarseFundamental.cs、Engine/DataFeeds/DataManager.cs、Algorithm/QCAlgorithm.cs,精读并套用体系化模板。
⚠️ 注意:本节是"动态选股机制",不展开各 Universe 子类的具体筛选公式。重点是数据流:谁触发筛选 → 谁收到增删 → 算法如何响应。
阅读完本节,你应当能够:
传统回测是"固定股票池"——一开始 AddEquity("AAPL") 就一直盯着。但很多策略本质是轮动的:每季度选市值前 100、每月选动量前 20、每天按成交量筛活跃合约。如果一开始把全市场几千只都加进来,数据量爆炸;如果只加固定几只,又错过机会。
UniverseSelection 解决这个矛盾:算法声明一个筛选规则,引擎在运行中按规则增删订阅。被选中的标的自动建订阅、喂数据;被剔除的自动拆订阅(但若有持仓会保留到平仓)。
Common/Securities/UniverseManager.cs 是个字典,管理算法里所有 universe:
// 简化示意 public class UniverseManager : Dictionary<Symbol, Universe> { // 每加一个 universe,触发 CollectionChanged 事件 }
每次算法 AddUniverse(...) 或 UniverseSettings 配置新规则,都会向 UniverseManager 里加一条。一个算法可同时有多个 universe(如一个粗筛 + 一个细筛 + 一个期权链)。
💡 钻取要点:UniverseManager 实现了
ObservableDictionary,每次 Add/Remove 都触发CollectionChanged事件。下游的 DataManager 订阅这个事件,从而"universe 一变,数据流跟着变"。
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,避免无谓的订阅抖动。
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 数据,性能可控。这是工程上的"漏斗"思想。
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:
逻辑很清晰:
SubscriptionRequest(数据订阅请求)加进数据流。这个订阅会触发后续的选股动作。也就是说,UniverseManager 的变化通过事件机制自动传导到数据流——用户加一个 universe,引擎自动开始喂数据、跑筛选。
每次筛选完成后,引擎把"这一步新增/移除了哪些标的"打包成 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 是无变化时的单例,避免空对象分配。
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 区别于很多简单回测框架的关键能力。
SelectSymbols(utcTime, data) 钩子,可返回 Universe.Unchanged 跳过本次。UniverseManager.CollectionChanged,universe 变化自动建/拆订阅。第 6 章结束。你已经理解了 Lean 的数据格式与多资产抽象。下一章(第 7 章)钻取 160+ 技术指标体系——Indicators 项目与指标基类。