第 8 章 · 02 Fill 成交模型


文档摘要

第 8 章 · 02 Fill 成交模型 本节摘要:本节钻取 Lean 订单成交的核心—— 成交模型。订单提交之后"以什么价格、成交多少"完全由 Fill 模型决定,这是回测模拟撮合的灵魂。 接口只有一个方法 ,基类 用 switch 分发到 / / / / / / 等子方法; (回测默认)就是空的 子类——订单在下一个数据点立即全部成交。成交价计算有讲究:市价单取当前 bar 的成交价(用 优先取买卖盘报价),限价单用"限价或更优",止损单触发后按市价。每次成交产生一个 (携带 Status/FillPrice/FillQuantity/OrderFee), 把多次部分成交归并成一笔 并跟踪盈亏。回测成交由 调 → Fill 模型模拟;实盘成交由 接收真实券商推送的 OrderEvent。

第 8 章 · 02 Fill 成交模型

本节摘要:本节钻取 Lean 订单成交的核心——Common/Orders/Fills/ 成交模型。订单提交之后"以什么价格、成交多少"完全由 Fill 模型决定,这是回测模拟撮合的灵魂。IFillModel 接口只有一个方法 Fill(parameters),基类 FillModel 用 switch 分发到 MarketFill/LimitFill/StopMarketFill/StopLimitFill/MarketOnOpenFill/MarketOnCloseFill/ComboXxxFill 等子方法;ImmediateFillModel(回测默认)就是空的 FillModel 子类——订单在下一个数据点立即全部成交。成交价计算有讲究:市价单取当前 bar 的成交价(用 GetPrices 优先取买卖盘报价),限价单用"限价或更优",止损单触发后按市价。每次成交产生一个 OrderEvent(携带 Status/FillPrice/FillQuantity/OrderFee),TradeBuilder 把多次部分成交归并成一笔 Trade 并跟踪盈亏。回测成交由 BacktestingTransactionHandlerBacktestingBrokerage.Scan → Fill 模型模拟;实盘成交由 BrokerageTransactionHandler 接收真实券商推送的 OrderEvent。读完本节,你理解了"订单→成交"这一跳的完整机制。

内容来源:原项目源码 Common/Orders/Fills/IFillModel.csFillModel.csImmediateFillModel.csFill.csPrices.csCommon/Orders/OrderEvent.csCommon/Statistics/TradeBuilder.csEngine/TransactionHandlers/BacktestingTransactionHandler.cs,精读并套用体系化模板。

⚠️ 注意:本节聚焦"成交价怎么算 + 成交事件怎么发",订单类型字段(StopPrice/LimitPrice)在上一节,手续费/滑点/TimeInForce 在第 03 节。Fill 模型只决定"理论成交价",滑点是 Fill 模型内部调 ISlippageModel 加上去的(下节展开)。

学习目标

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

  1. 读懂 IFillModel 接口与 FillModel 基类的分发机制。
  2. 解释 ImmediateFillModel 为何只是空壳子类。
  3. 复述市价单/限价单/止损单的成交价计算规则。
  4. 理解 GetPrices 如何按订阅数据类型(Tick/QuoteBar/TradeBar)取价。
  5. 说出 OrderEvent 的关键字段与状态流转。
  6. 解释 TradeBuilder 如何把多次成交归并成一笔 Trade。
  7. 区分回测成交(BacktestingTransactionHandler + Fill 模型)与实盘成交(券商推送 OrderEvent)。

一、Fills 子目录:成交模型的家

Common/Orders/Fills/ 下 11 个文件:

文件 角色
IFillModel.cs 接口,只一个 Fill 方法
FillModel.cs 基类(约 1200 行),实现所有 XxxFill 子方法
ImmediateFillModel.cs 空壳子类(回测默认)
EquityFillModel.cs 股票专属(处理涨跌停/撮合时间)
FutureFillModel.cs 期货专属
FutureOptionFillModel.cs 期货期权专属
LatestPriceFillModel.cs 用最新价成交的变体
Fill.cs 返回值容器(装 OrderEvent 列表)
FillModelParameters.cs 入参容器(Security/Order/SecuritiesForOrders)
Prices.cs 价格快照(OHLC + current + endTime)

命名规律:接口 IFillModel → 基类 FillModel → 各资产类型子类。回测默认用 ImmediateFillModel(它继承 FillModel,啥也不重写)。

二、IFillModel 接口与 ImmediateFillModel

Fills/IFillModel.cs:23(接口极简,一个方法):

23 public interface IFillModel 30 Fill Fill(FillModelParameters parameters); // 返回 Fill(装 OrderEvent 列表)

入参 FillModelParameters 装 Security + Order(组合单还装 SecuritiesForOrders 多腿字典),返回 Fill(可枚举 OrderEvent,因为组合单一次返回多腿的成交)。注意 L21-22 的备注:"请用 FillModel 作为基类"——直接实现接口会被 Python 包装器漏掉。

Fills/ImmediateFillModel.cs:21(回测默认成交模型,空壳):

21 public class ImmediateFillModel : FillModel 22 { 23 }

是的,就这么多。"立即成交"的逻辑全在基类 FillModel 里,ImmediateFillModel 只是个命名占位——表示"我要默认行为"。用户要自定义成交(如部分成交),也是继承 FillModel 重写某个 XxxFill 方法,通过 security.FillModel = new MyFillModel() 替换。

三、FillModel.Fill:类型分发

FillModel.cs:59(总入口,按 OrderType 分发):

59 public virtual Fill Fill(FillModelParameters parameters) 63 Parameters = parameters; 65 var orderEvents = new List<OrderEvent>(1); 66 switch (parameters.Order.Type) 68 case OrderType.Market: orderEvents.Add(MarketFill(...)); break; 73 case OrderType.Limit: orderEvents.Add(LimitFill(...)); break; 78 case OrderType.LimitIfTouched: orderEvents.Add(LimitIfTouchedFill(...));break; 83 case OrderType.StopMarket:orderEvents.Add(StopMarketFill(...));break; 88 case OrderType.TrailingStop: orderEvents.Add(TrailingStopFill(...));break; 93 case OrderType.StopLimit: orderEvents.Add(StopLimitFill(...)); break; 98 case OrderType.MarketOnOpen: orderEvents.Add(MarketOnOpenFill(...)); break; 103 case OrderType.MarketOnClose: orderEvents.Add(MarketOnCloseFill(...));break; 108 case OrderType.ComboMarket: orderEvents = ComboMarketFill(...); break; 113 case OrderType.ComboLimit: orderEvents = ComboLimitFill(...); break; 118 case OrderType.ComboLegLimit: orderEvents = ComboLegLimitFill(...); break; 126 return new Fill(orderEvents);

注意 L69-71 的 PythonWrapper != null 三元——如果成交模型来自 Python,要走包装器调用 Python 重写的方法(PythonNet 的限制)。每种订单类型对应一个 XxxFill 虚方法,子类可按需重写。

四、MarketFill:市价单怎么成交

FillModel.cs:273(MarketFill 转发到 InternalMarketFill):

273 public virtual OrderEvent MarketFill(Security asset, MarketOrder order) 275 return InternalMarketFill(asset, order, order.Quantity); 282 private OrderEvent InternalMarketFill(Security asset, Order order, decimal quantity) 286 var fill = new OrderEvent(order, utcTime, OrderFee.Zero); 288 if (order.Status == OrderStatus.Canceled) return fill; // 已撤单不成交 291 if (!IsExchangeOpen(asset, false)) return fill; // 市场未开不成交 294 var prices = GetPricesCheckingPythonWrapper(asset, orderDirection); 304 if (pricesEndTimeUtc.Add(Parameters.StalePriceTimeSpan) < order.Time) 307 if (ShouldWaitForFreshDataOnStale(asset, pricesEndTimeUtc, order.Time, ...)) 309 return fill; // 数据陈旧等新数据 317 fill.FillPrice = GetMarketFillPrice(asset, order, prices, ...); // 取成交价 318 fill.Status = OrderStatus.Filled; // 立即全部成交 321 var slip = asset.SlippageModel.GetSlippageApproximation(asset, order); // 滑点 327 fill.FillPrice += slip; // 买单加滑点 330 fill.FillPrice -= slip; // 卖单减滑点 335 fill.FillQuantity = quantity; // 假设全部成交 338 return fill;

市价单的成交逻辑五步:① 已撤单直接返回空事件 ② 市场未开返回空事件(等下个 bar 再试)③ 取当前价 ④ 防陈旧数据(数据时间晚于订单时间才成交,否则等)⑤ 加滑点。关键在 318 行 Status = OrderStatus.Filled 与 335 行 FillQuantity = quantity——假设全部成交,这是 ImmediateFillModel 的"立即"含义。要做部分成交(PartialFill),重写此方法把 FillQuantity 设小一些、Status 设为 PartiallyFilled 即可(Tests/Engine/PartialFillModel.cs 是范例)。

成交价由 GetPrices 决定。FillModel.cs:1150:

1150 protected virtual Prices GetPrices(Security asset, OrderDirection direction) 1152 var low = asset.Low; var high = asset.High; 1154 var open = asset.Open; var close = asset.Close; var current = asset.Price; 1169 var tick = asset.Cache.GetData<Tick>(); 1170 if (tick != null && subscriptionTypes.Contains(typeof(Tick))) 1172 var price = direction == OrderDirection.Sell ? tick.BidPrice : tick.AskPrice; 1175 return new Prices(tick.Time, tick.EndTime, price, 0, 0, 0, 0); // 用买卖盘 1187 var quoteBar = asset.Cache.GetData<QuoteBar>(); 1190 var bar = direction == OrderDirection.Sell ? quoteBar.Bid : quoteBar.Ask; 1193 return new Prices(quoteBar.Time, quoteBar.EndTime, bar); // 用 QuoteBar 的 Bid/Ask

GetPrices 按"订阅了什么数据"取价:优先 Tick 的 Bid/Ask,其次 QuoteBar 的 Bid/Ask 边,最后才退到 TradeBar 的 OHLC。卖单用 Bid(买盘价)、买单用 Ask(卖盘价)——这就是点差(spread)的成本,市价单永远在不利一边成交。

💡 钻取要点:GetPrices 返回的 Prices 对象同时携带 endTime。后续每个 XxxFill 都会检查 pricesEndTime > order.Time——确保用的是订单提交之后的数据,而不是提交那一刻的快照。这是回测无前视(look-ahead)的关键防线:订单在 T 时刻提交,只能用 T 之后产生的 bar 成交,不能用 T 当时的 bar。

五、LimitFill:限价单怎么成交

FillModel.cs:670(限价单的核心规则——价格触及限价或更好时成交):

670 public virtual OrderEvent LimitFill(Security asset, LimitOrder order) 672 return InternalLimitFill(asset, order, order.LimitPrice, order.Quantity); 678 private OrderEvent InternalLimitFill(Security asset, Order order, decimal limitPrice, decimal quantity) 685 if (order.Status == OrderStatus.Canceled) return fill; 688 if (!IsExchangeOpen(asset)) return fill; 694 var prices = GetPricesCheckingPythonWrapper(asset, orderDirection); 698 if (pricesEndTime <= order.Time) return fill; // 防陈旧 701 switch (orderDirection) 703 case OrderDirection.Buy: 705 if (prices.Low < limitPrice) // 买单:最低价低于限价 708 fill.Status = OrderStatus.Filled; 711 fill.FillPrice = Math.Min(prices.High, limitPrice); // 取限价或更优 713 fill.FillQuantity = quantity; 716 case OrderDirection.Sell: 718 if (prices.High > limitPrice) // 卖单:最高价高于限价 721 fill.FillPrice = Math.Max(prices.Low, limitPrice); // 取限价或更优 725 fill.FillQuantity = quantity;

限价单的精髓在 705 与 711 行:买单当本 bar 的 Low 跌破限价时触发(说明价格曾到限价或更低),成交价取 Min(High, LimitPrice)——如果整根 bar 都在限价之下(High < LimitPrice),用 High(更贴近实际);否则用 LimitPrice(不会比限价差)。卖单对称。这个"取 Min/Max"是处理"限价单挂在远离盘口的位置"的细节——比如买单限价 100,但 bar 的 High 才 95,实际成交应该是 95 而不是 100(否则白赚 5 块)。

六、StopMarketFill:止损单怎么成交

止损单的逻辑分两阶段:先触发(价格穿越 StopPrice),触发后按市价成交FillModel.cs:347StopMarketFill(此处不贴全码)检查 bar 的 High/Low 是否穿越 StopPrice:买单止损(High ≥ StopPrice)、卖单止损(Low ≤ StopPrice)。穿越后状态置 Filled,成交价取 StopPrice(或市价)。StopLimitFill(FillModel.cs:486)更复杂——先看 StopPrice 触发(置 StopTriggered = true),触发后才转成限价单走 LimitFill 逻辑。MarketOnOpenFill/MarketOnCloseFill 则要求成交数据来自下一个开盘/收盘 bar。

七、OrderEvent:成交事件

每次 Fill 模型返回一个 OrderEvent(Common/Orders/OrderEvent.cs:30),它是订单状态变化的载体:

30 public class OrderEvent 45 public int OrderId { get; set; } // 关联订单ID 57 public Symbol Symbol { get; set; } 63 public DateTime UtcTime { get; set; } // 事件时间 69 public OrderStatus Status { get; set; } // Filled/PartiallyFilled/Cancelled/Invalid/New 75 public OrderFee OrderFee { get; set; } // 本笔手续费 81 public decimal FillPrice { get; set; } // 成交价 91 public string FillPriceCurrency { get; set; } // 成交价币种 97 public decimal FillQuantity { get; set; } // 成交数量(正买负卖) 107 public decimal AbsoluteFillQuantity => Math.Abs(FillQuantity); 113 public OrderDirection Direction { get; set; } // 方向 120 public string Message { get; set; } // 交易所消息 126 public bool IsAssignment { get; set; } // 是否期权被行权(空头被指定)

状态流转:New(提交)→ Submitted(已发券商)→ PartiallyFilled(部分成交,可多次)→ Filled(全部成交);任何时刻可转 Cancelled(撤销)或 Invalid(无效)。一个订单的完整生命周期是一串 OrderEvent。引擎拿到 OrderEvent 后调 Portfolio.ProcessFill(orderEvent) 更新持仓与现金(下节讲 Portfolio 影响)。

八、TradeBuilder:把多次成交归并成 Trade

一个订单可能分多次成交(部分成交或实盘券商分批回报)。Common/Statistics/TradeBuilder.cs:30 负责把同一标的的多笔成交按"开仓-平仓"配对,归并成一笔 Trade(有 EntryPrice/ExitPrice/Profit/Loss/MaxProfit/MaxDrawdown):

30 public class TradeBuilder : ITradeBuilder 63 private class Position 65 internal List<TradeState> PendingTrades { get; set; } // 待平仓的交易 66 internal List<OrderEvent> PendingFills { get; set; } // 待配对的成交 85 private readonly FillGroupingMethod _groupingMethod; // 配对方式 86 private readonly FillMatchingMethod _matchingMethod; // 匹配方式 82 private readonly List<Trade> _closedTrades = new List<Trade>(); 83 private readonly Dictionary<Symbol, Position> _positions = new Dictionary<Symbol, Position>();

FillGroupingMethod 决定"怎样算一笔交易":按交易标的(BySymbol)、按订单(ByOrder)、或按同一订单的所有成交(ByFill)。FillMatchingMethod 决定"开仓和平仓怎么配对":FIFO(先进先出)或 LIFO。TradeBuilder 还跟踪每笔 Trade 持续期间的 MaxProfitMaxDrawdown(L36-37),供统计指标(如 ProfitFactor、WinRate)使用。

💡 钻取要点:TradeBuilder 的存在是为了回答"我这笔交易赚了多少"——而不是"我这次成交赚了多少"。比如分 3 次买入 100 股、1 次卖出 100 股平仓,TradeBuilder 把这 4 次成交配对成 1 笔 Trade,算出这笔 Trade 的盈亏、最大浮盈、最大回撤。回测结果里的 "Total Trades"、"Average Win"、"Average Loss" 都基于 _closedTrades 列表统计。

九、回测成交 vs 实盘成交

成交的"驱动源"在回测与实盘截然不同,但都通过 OrderEvent 这同一格式回流到引擎:

回测成交(BacktestingTransactionHandler + BacktestingBrokerage):

Engine/TransactionHandlers/BacktestingTransactionHandler.cs:30(继承自 BrokerageTransactionHandler):

30 public class BacktestingTransactionHandler : BrokerageTransactionHandler 65 protected override bool SynchronousProcessing => !(ConcurrencyEnabled && _algorithm.LiveMode); // 回测同步处理 70 public override void ProcessSynchronousEvents() 75 ProcessPendingRequests(); // 处理待提交订单 78 base.ProcessSynchronousEvents(); 80 _brokerage.Scan(); // 扫描撮合(关键!) 86 _brokerage.ProcessDelistings(...);

回测的关键在 L80 的 _brokerage.Scan()——BacktestingBrokerage.Scan 在每个时间循环里被调用,它遍历所有未成交订单,对每个订单调 IFillModel.Fill(即 ImmediateFillModel)用当前 bar 算成交,生成 OrderEvent。整个过程同步、确定性、零延迟——给定相同的历史数据,两次回测的成交完全一致。

实盘成交(BrokerageTransactionHandler + 真实券商):

实盘不调 Scan,而是订阅券商的 OrdersStatusChangedAccountChanged 事件——券商通过 WebSocket/API 推送真实的部分成交、全部成交、撤单确认。BrokerageTransactionHandler 收到推送后包装成 OrderEvent 回流。整个过程异步、有延迟、可能部分成交、可能拒单——成交价是交易所真实撮合的结果,不是模型算的。

本节要点回顾

  1. IFillModel 接口极简(一个 Fill 方法),FillModel 基类实现所有 XxxFill 子方法,ImmediateFillModel(回测默认)是空壳子类。
  2. FillModel.Fill 按 OrderType switch 分发到 MarketFill/LimitFill/StopMarketFill/StopLimitFill/MarketOnOpenFill/MarketOnCloseFill/ComboXxxFill
  3. 市价单取当前价(优先 Tick 的 Bid/Ask,卖单用 Bid、买单用 Ask),假设全部成交,加滑点。
  4. 限价单当 bar 的 Low/High 穿越限价时成交,成交价取"限价或更优"(Min(High, LimitPrice) / Max(Low, LimitPrice))。
  5. 止损单两阶段:StopPrice 触发后按市价;StopLimit 触发后转限价单。
  6. GetPrices 按订阅数据类型取价:Tick > QuoteBar > TradeBar;返回的 Prices 带 endTime 用于防陈旧(防前视)。
  7. OrderEvent 携带 Status/FillPrice/FillQuantity/OrderFee,状态流转 New→Submitted→PartiallyFilled→Filled(或 Cancelled/Invalid)。
  8. TradeBuilderFillGroupingMethod(BySymbol/ByOrder/ByFill)和 FillMatchingMethod(FIFO/LIFO)把多次成交归并成 Trade,跟踪 MaxProfit/MaxDrawdown。
  9. 回测成交:BacktestingTransactionHandler.ProcessSynchronousEvents_brokerage.Scan() → Fill 模型算价;实盘成交:BrokerageTransactionHandler 接券商推送 OrderEvent。

下一节,我们钻进 Fees(手续费)、Slippage(滑点)、TimeInForce(GTC/Day/GTD)与 IBrokerageModel(统一封装),并总结成交对 Portfolio 的影响与回测实盘差异。


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