第 5 章 · 03 Slice 用户数据包与 Fill-Forward


文档摘要

第 5 章 · 03 Slice 用户数据包与 Fill-Forward 本节摘要:本节钻取用户在 里真正接触到的对象—— 。它是 TimeSlice 的"用户可见子集",继承 ,既是字典(可 或 索引),又按数据类型分桶(Bars/QuoteBars/Ticks/OptionChains/FuturesChains + 辅助数据 Splits/Dividends/Delistings/SymbolChangedEvents/MarginInterestRates)。本节还会讲清 Fill-Forward——缺失数据时用上一根填充,保证时间序列连续,这是回测可重现的关键。

第 5 章 · 03 Slice 用户数据包与 Fill-Forward

本节摘要:本节钻取用户在 OnData 里真正接触到的对象——Slice。它是 TimeSlice 的"用户可见子集",继承 ExtendedDictionary<Symbol, dynamic>,既是字典(可 slice["SPY"]slice.Bars["SPY"] 索引),又按数据类型分桶(Bars/QuoteBars/Ticks/OptionChains/FuturesChains + 辅助数据 Splits/Dividends/Delistings/SymbolChangedEvents/MarginInterestRates)。本节还会讲清 Fill-Forward——缺失数据时用上一根填充,保证时间序列连续,这是回测可重现的关键。最后扫一遍 Engine/DataFeeds 的子目录(Enumerators/Queues/Transport/WorkScheduling),把数据投喂子系统的完整链路连起来。

内容来源:原项目源码 Common/Data/Slice.cs(共 700+ 行)、Engine/DataFeeds/Enumerators/FillForwardEnumerator.csEngine/DataFeeds/Enumerators 目录。

⚠️ 注意:本节是用户写策略时打交道最多的数据结构。Slice 的索引语义、Fill-Forward 的填充规则、各类枚举器的分工,都是日常策略开发会反复踩的细节。

学习目标

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

  1. 列出 Slice 的九个分桶字段(Bars/QuoteBars/Ticks/OptionChains/FuturesChains + Splits/Dividends/Delistings/SymbolChangedEvents/MarginInterestRates)。
  2. slice["SPY"]slice.Bars["SPY"] 索引数据,理解两者差异。
  3. 说清 Fill-Forward 的语义——为什么订阅时默认 fillForward=true
  4. 认识 Enumerators 子目录里的几种核心枚举器(FillForward/Refresh/Frontfill/Source)。
  5. 描述 Queues/Transport/WorkScheduling 三个子目录的职责。

一、Slice 类头与字段

Common/Data/Slice.cs:33-46:

33 public class Slice : ExtendedDictionary<Symbol, dynamic>, IEnumerable<KeyValuePair<Symbol, BaseData>> 34 { 35 private Ticks _ticks; 36 private TradeBars _bars; 37 private QuoteBars _quoteBars; 38 private OptionChains _optionChains; 39 private FuturesChains _futuresChains; 41 // aux data 42 private Splits _splits; 43 private Dividends _dividends; 44 private Delistings _delistings; 45 private SymbolChangedEvents _symbolChangedEvents; 46 private MarginInterestRates _marginInterestRates;

Slice 继承 ExtendedDictionary<Symbol, dynamic>——既是字典(支持 slice[symbol] 索引),又按数据类型分九个桶(各有属性访问器 L86-120):

属性 类型 含义
Bars TradeBars OHLCV 成交 K 线(主数据)
QuoteBars QuoteBars bid/ask 报价 K 线(外汇/CFD 常用)
Ticks Ticks 逐笔成交
OptionChains OptionChains 期权链
FuturesChains FuturesChains 期货链
Splits Splits 拆股事件
Dividends Dividends 分红事件
Delistings Delistings 退市事件
SymbolChangedEvents SymbolChangedEvents 代码变更(如 AOL→TWX)
MarginInterestRates MarginInterestRates 保证金利率

每个桶都是 DataDictionary<T>(按 Symbol 索引)。Time/UtcTime/HasData/AllData 是顶层属性。

💡 钻取要点:为什么分桶而不是一个大字典?因为不同数据类型语义不同——Bars[symbol] 返回 TradeBar(有 OHLCV),Splits[symbol] 返回 Split(只有 SplitFactor)。分桶让用户用类型安全的属性访问,而不是从一个 dynamic 字典里强转。同时 slice[symbol] 兜底返回 dynamic,任何类型都能拿到。

二、Slice 索引器

Common/Data/Slice.cs:333-351:

333 public override dynamic this[Symbol symbol] 334 { 335 get 336 { 337 SymbolData value; 338 if (_data.Value.TryGetValue(symbol, out value)) 339 { 340 return value.GetData(); 341 } 342 CheckForImplicitlyCreatedSymbol(symbol); 343 throw new KeyNotFoundException($"'{symbol}' wasn't found in the Slice object, " + $"likely because there was no-data at this moment in time and it wasn't possible " + $"to fillforward historical data. Please check the data exists before accessing " + $"it with data.ContainsKey(\"{symbol}\")"); 344 } 345 set 346 { 347 // 只读,不允许 set 348 throw new NotSupportedException("The Slice object is read-only."); 349 } 350 }

slice["SPY"] 走这个索引器,从内部 _data(Lazy<DataDictionary<SymbolData>>)取出 SymbolData,返回 dynamic(可能是 TradeBar/QuoteBar/Tick 等,取决于订阅类型)。关键:如果这个时刻没数据(且没 fill-forward),抛 KeyNotFoundException——异常消息直接提示用户"用 ContainsKey 先检查"。这是新手最常踩的坑:

// 错误:某根 bar 没数据会抛异常 var bar = slice["SPY"]; // 正确:先检查 if (slice.ContainsKey(_spy)) { var bar = slice[_spy]; // 或 slice.Bars[_spy] }

slice.Bars["SPY"] 是按类型访问,语义更明确(我知道我要 TradeBar),也抛同样的异常但带类型上下文。

三、Fill-Forward 前向填充

Fill-Forward 是回测可重现的关键机制。考虑这个场景:你订阅了 SPY 和 AAPL 分钟数据,但某分钟 SPY 没成交(流动性低/盘整),那这一分钟的 Slice 里 SPY 该是什么?

没有 Fill-Forward:Slice 里没 SPY,slice.Bars.ContainsKey(spy) == false,策略得自己处理缺失。
有 Fill-Forward:AddEquity("SPY", Resolution.Minute, fillForward: true)(默认 true),引擎用上一根 SPY bar 的 close 填充这一分钟(开盘=最高=最低=收盘=上一根收盘,Volume=0),Slice 里始终有 SPY。

实现是 FillForwardEnumerator(Engine/DataFeeds/Enumerators/FillForwardEnumerator.cs:36):

32 /// <summary> 33 /// The FillForwardEnumerator wraps an existing base data enumerator and inserts extra 'base data' instances 34 /// on a specified fill forward resolution 35 /// </summary> 36 public class FillForwardEnumerator : IEnumerator<BaseData> 37 { 38 private DateTime? _delistedTime; 39 private BaseData _previous; // 上一条真实数据 40 private bool _ended; 41 private bool _isFillingForward; // 当前是否在填充

它包装一个底层枚举器,每次 MoveNext 时检查"按 fill-forward resolution,是否应该有数据但实际没"——如果是,就用 _previous 复制一份,改时间戳和 Volume,_isFillingForward = true 标记这是填充出来的。

⚠️ 重要说明:Fill-Forward 不是"伪造数据",而是"用最后已知价占位"。填充的 bar Volume=0(没成交),OHLC 都等于上一根 close。策略可以靠 bar.Volume == 0 判断这是填充数据。这个机制保证了:slice.Bars 在盘中每分钟都有 SPY,策略逻辑(比如"每分钟检查均线")不会因为偶尔缺数据而中断。

实盘用 LiveFillForwardEnumerator(同目录),逻辑类似但要处理实时乱序到达的问题。

四、Enumerators 子目录

Engine/DataFeeds/Enumerators/ 有 30+ 个枚举器,组合成"枚举器链"。每个枚举器实现 IEnumerator<BaseData>,包装底层枚举器加一层处理。核心几类:

枚举器 职责
FillForwardEnumerator Fill-Forward 前向填充(本节)
LiveFillForwardEnumerator 实盘 Fill-Forward
RefreshEnumerator 刷新(更新最新数据)
FrontfillEnumerator(FrontierAware 系) 前向裁剪,处理订阅起始时间之前的数据
FastForwardEnumerator 快进(跳过过期数据)
FilterEnumerator 过滤(按条件丢弃)
EnqueueableEnumerator 队列式,跨线程生产消费
ConcatEnumerator 拼接多个枚举器
BaseDataCollectionAggregatorEnumerator 聚合 BaseDataCollection
AuxiliaryDataEnumerator 辅助数据(拆股/分红)注入
DelistingEventProvider/DividendEventProvider 事件数据提供者

Factories 子目录是"枚举器工厂"——SubscriptionDataReaderSubscriptionEnumeratorFactory 把上面的枚举器按订阅配置组装成链:

Source(SubscriptionDataReader 读磁盘) → Frontfill(裁剪起始) → FillForward(前向填充) → Filter(过滤) → AuxiliaryData(注入拆股/分红)

链的顺序很重要,比如 FillForward 必须在 Source 之后(得先有原始数据才能填充),在 AuxiliaryData 之前(填充完再叠事件)。第 7 章会详讲这条链的组装。

💡 钻取要点:这是"装饰器模式"(GoF)的标准应用。每个枚举器只管一件事,组合起来就成了完整的数据处理流水线。Lean 用 IEnumerator 链而非函数管道,是因为 IEnumerator 是 C# 流式处理的天然抽象,且支持跨线程(EnqueueableEnumerator)。

五、Queues / Transport / WorkScheduling 子目录

Engine/DataFeeds/ 还有三个子目录,服务于实盘数据接入:

Queues(Engine/DataFeeds/Queues/):实时数据队列。IDataQueue 是实盘券商推送的适配器接口,各券商实现自己的(如 IEXDataQueue/BinanceDataQueue/InteractiveBrokersDataQueue)。LiveDataQueue 是通用包装。实盘的 LiveTradingDataFeed 内部就持一个 IDataQueue,从它读实时 tick。

Transport(Engine/DataFeeds/Transport/):订阅源传输层。ISubscriptionDataSourceReader 定义"如何从一个数据源(URL/文件/API)读出 BaseData 流",支持 HTTP/FTP/本地文件。主要用于自定义数据和基本面数据的下载。

WorkScheduling(Engine/DataFeeds/WorkScheduling/):工作调度。实盘要做"每 N 秒拉一次"或"定时刷新"这类调度,这里提供 WorkItem/WorkScheduler 抽象。AsyncWorker 单线程串行执行调度任务,避免并发问题。

三者合力:Queues 接券商实时数据 → Transport 拉远程数据 → WorkScheduling 调度定时任务。回测几乎不用这三个(数据都在磁盘,FileSystemDataFeed 直接读),它们是实盘/自定义数据的专用基础设施。

本节要点回顾

  1. Slice 九分桶:Bars(TradeBars OHLCV)/QuoteBars(bid ask)/Ticks/OptionChains/FuturesChains + Splits/Dividends/Delistings/SymbolChangedEvents/MarginInterestRates,每桶按 Symbol 索引。
  2. 两种索引:slice["SPY"] 走 dynamic 索引器,slice.Bars["SPY"] 按类型访问;缺数据抛 KeyNotFoundException,需用 ContainsKey 预检。
  3. Fill-Forward:订阅默认 fillForward=true,缺失数据用上一根 close 填充(Volume=0),由 FillForwardEnumerator 包装底层枚举器实现。实盘用 LiveFillForwardEnumerator
  4. Enumerators 链:30+ 枚举器各管一事(Source/Frontfill/FillForward/Filter/AuxiliaryData),用装饰器模式组合成流水线,顺序敏感。
  5. Queues/Transport/WorkScheduling:实盘专用——Queues 接券商推送,Transport 拉远程数据,WorkScheduling 调度定时任务。回测几乎不用。

至此,第 4-5 章钻取了用户 API 层(QCAlgorithm)与数据流层(DataFeed/Synchronizer/Slice)。下一章,我们钻取数据本身——从磁盘读取的格式(LEAN zip+CSV)、多资产类型支持、SubscriptionDataReader 的解析细节。


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