第 8 章 · 03 Fees Slippage 与 TimeInForce 本节摘要:本节钻取订单成交的三大"修饰器"——手续费(Fees)、滑点(Slippage)、有效期(TimeInForce),以及把它们统一封装的 ,最后收束"成交对 Portfolio 的影响"与"回测模拟 vs 实盘真实"的差异总结。Fees 由 / 实现,按券商配置(美股 IB 每股 0.005 美元、最低 1 美元、最高交易额 0.5%;印度每股 0.
本节摘要:本节钻取订单成交的三大"修饰器"——手续费(Fees)、滑点(Slippage)、有效期(TimeInForce),以及把它们统一封装的
IBrokerageModel,最后收束"成交对 Portfolio 的影响"与"回测模拟 vs 实盘真实"的差异总结。Fees 由IFeeModel/FeeModel实现,按券商配置(美股 IB 每股 0.005 美元、最低 1 美元、最高交易额 0.5%;印度每股 0.01 卢比起),实盘成交时引擎按券商回报的OrderFee扣;Slippage 由ISlippageModel实现(默认NullSlippageModel不滑点,可选ConstantSlippageModel/VolumeShareSlippageModel),在 Fill 模型内部被调用叠加到成交价;TimeInForce 由TimeInForce抽象类与ITimeInForceHandler实现(GTC 一直有效/Day 当日有效/GTD 指定日期前有效),通过IsOrderExpired/IsFillValid两个钩子影响订单是否还活着。三者都挂在IBrokerageModel(券商模型)上,用户用SetBrokerageModel(BrokerageName.InteractiveBrokers)一行设置,引擎自动给每个 Security 配上对应的 Fill/Fee/Slippage/Settlement/BuyingPower 模型。期权行权(OptionExerciseOrder)走特殊的IOptionExerciseModel——行权产生标的买卖。读完本节,本章"订单→成交→入账"的完整闭环就拼齐了。
内容来源:原项目源码
Common/Orders/Fees/IFeeModel.cs、FeeModel.cs、InteractiveBrokersFeeModel.cs、Common/Orders/Slippage/ISlippageModel.cs、ConstantSlippageModel.cs、Common/Orders/TimeInForce.cs、TimeInForces/GoodTilDateTimeInForce.cs、Common/Interfaces/ITimeInForceHandler.cs、Common/Orders/OptionExercise/IOptionExerciseModel.cs、Common/Brokerages/IBrokerageModel.cs、DefaultBrokerageModel.cs、Algorithm/QCAlgorithm.cs,精读并套用体系化模板。
⚠️ 注意:本节是第 8 章的收束节。Fees/Slippage/TimeInForce 是"成交的修饰器"——它们不改变"是否成交",但改变"成交价多少、扣多少费、订单什么时候过期"。把它们和上两节(订单类型、Fill 模型)合起来,就是订单的完整生命周期。
阅读完本节,你应当能够:
IFeeModel/FeeModel 与 InteractiveBrokersFeeModel 的费率规则。ISlippageModel 在 Fill 模型里何时被调用、如何影响成交价。IBrokerageModel 统一封装的六大模型(Fill/Fee/Slippage/Settlement/BuyingPower/MarginInterestRate)。SetBrokerageModel 如何把券商模型下放到每个 Security。OptionExerciseOrder 走 IOptionExerciseModel 产生标的买卖。Common/Orders/Fees/IFeeModel.cs:25(接口,一个方法):
25 public interface IFeeModel 33 OrderFee GetOrderFee(OrderFeeParameters parameters); // 返回 OrderFee(金额+币种)
FeeModel.cs:25(基类,默认零手续费):
25 public class FeeModel : IFeeModel 33 public virtual OrderFee GetOrderFee(OrderFeeParameters parameters) 35 return new OrderFee(new CashAmount(0, "USD")); // 默认免费
Fees/ 下 30+ 个券商专属费率模型(InteractiveBrokersFeeModel/BinanceFeeModel/ZerodhaFeeModel/IndiaFeeModel/...),全部继承 FeeModel 重写 GetOrderFee。看 IB 美股(InteractiveBrokersFeeModel.cs:138-159):
138 case SecurityType.Equity: 140 switch (market) 142 case Market.USA: 143 equityFee = new EquityFee(Currencies.USD, feePerShare: 0.005m, minimumFee: 1, maximumFeeRate: 0.005m); 146 case Market.India: 147 equityFee = new EquityFee(Currencies.INR, feePerShare: 0.01m, minimumFee: 6, maximumFeeRate: 20); 151 var tradeValue = Math.Abs(order.GetValue(security)); 154 var tradeFee = equityFee.FeePerShare * quantity; // 每股费率 × 股数 158 var maximumPerOrder = equityFee.MaximumFeeRate * tradeValue; // 单笔上限 159 if (tradeFee < equityFee.MinimumFee) ... // 不低于最低费
IB 美股规则:每股 0.005 美元、最低 1 美元/笔、最高不超过交易额的 0.5%——典型的"按股数阶梯计费 + 兜底"。印度市场不同(每股 0.01 卢比、最低 6 卢比、最高 20 卢比封顶)。FX 走"按交易额百分比 + 最低单笔费"(L94-101),期权走"按合约数 × 函数"(L103-114),期货按合约 + 交易所费(L116-136)。每种资产类型一套规则,这正是为什么需要专门的 FeeModel 而不是一个常量。
💡 钻取要点:期权行权在 IB 是免费的——
InteractiveBrokersFeeModel.cs:76-86显式判断OrderType.OptionExercise且SecurityType.Option直接返回OrderFee.Zero(期货期权行权按期货费收)。这是实盘的真实规则在引擎里的精确复刻。
Common/Orders/Slippage/ISlippageModel.cs:23(接口):
23 public interface ISlippageModel 28 decimal GetSlippageApproximation(Security asset, Order order); // 返回绝对滑点金额
ConstantSlippageModel.cs:24(按固定百分比滑点):
24 public class ConstantSlippageModel : ISlippageModel 31 public ConstantSlippageModel(decimal slippagePercent) // 0~1 之间 39 public decimal GetSlippageApproximation(Security asset, Order order) 41 var lastData = asset.GetLastData(); 42 if (lastData == null) return 0; 44 return lastData.Value * _slippagePercent; // 当前价 × 滑点比例
Slippage/ 下五个实现:NullSlippageModel(默认,返回 0,不滑点)、ConstantSlippageModel(固定比例)、VolumeShareSlippageModel(按成交量份额,大单滑点多——回测机构策略常用)、MarketImpactSlippageModel(市场冲击模型)、AlphaStreamsSlippageModel。
关键在于滑点何时被调用——回顾上一节 FillModel.InternalMarketFill 的 L321:var slip = asset.SlippageModel.GetSlippageApproximation(asset, order),然后买单 FillPrice += slip、卖单 FillPrice -= slip。即滑点是在 Fill 模型算出"理论成交价"之后,再叠加一个不利方向的偏移。回测默认 NullSlippageModel(不滑点,偏乐观),实盘滑点真实存在(尤其大单和流动性差的标的)。
Common/Orders/TimeInForce.cs:28(抽象基类,同时实现 ITimeInForceHandler):
28 public abstract class TimeInForce : ITimeInForceHandler 33 public static readonly TimeInForce GoodTilCanceled = new GoodTilCanceledTimeInForce(); // GTC 默认 38 public static readonly TimeInForce Day = new DayTimeInForce(); // Day 当日 43 public static Func<DateTime, TimeInForce> GoodTilDate => // GTD 指定日期 (DateTime expiry) => new GoodTilDateTimeInForce(expiry); 51 public abstract bool IsOrderExpired(Security security, Order order); // 订单是否过期 60 public abstract bool IsFillValid(Security security, Order order, OrderEvent fill); // 成交是否有效
TimeInForces/ 下三个实现:GoodTilCanceledTimeInForce(永不过期,IsOrderExpired 恒 false)、DayTimeInForce(当日收盘过期)、GoodTilDateTimeInForce(指定日期的市场收盘过期)。看 GTD 的过期判定(GoodTilDateTimeInForce.cs:54):
54 public override bool IsOrderExpired(Security security, Order order) 61 switch (order.SecurityType) 63 case SecurityType.Forex: 64 case SecurityType.Cfd: 68 expired = time.ConvertToUtc(...) >= GetForexOrderExpiryDateTime(order); // FX:纽约 17 点 71 case SecurityType.Crypto: 74 expired = time.Date > Expiry.Date; // 币:过期日次日零点 77 default: // 股票/期权/期货 84 expired = time >= exchangeHours.GetLastDailyMarketClose(Expiry.Date, false); // 当日收盘
不同资产类型的"当日"定义不同:FX 用纽约时间 17 点(全球 FX 日切),币种用日历日零点,股票/期权/期货用市场收盘。IsOrderExpired 与 IsFillValid 由 TransactionHandler 在每次扫描订单时调用——过期订单会被撤销,无效成交会被拒绝。
💡 钻取要点:
OrderProperties.cs:42默认TimeInForce = GoodTilCanceled,即所有订单默认"一直挂着直到成交或撤销"。要改成 Day 或 GTD,需在DefaultOrderProperties或单笔订单的orderProperties参数里设置。注意QCAlgorithm.Trading.cs:1735的InvalidateGoodTilDateTimeInForce——MOO(开盘市价)和 MOC(收盘市价)不支持 GTD,会被强制重置为 GTC。
上述 Fee/Slippage/Fill/TimeInForce 都是"单个维度"的模型,IBrokerageModel(Common/Brokerages/IBrokerageModel.cs:34)把它们打包成"一家券商的完整画像":
34 public interface IBrokerageModel 39 AccountType AccountType { get; } // 现金/保证金账户 48 decimal RequiredFreeBuyingPowerPercent { get; } // 留存购买力比例 56 IReadOnlyDictionary<SecurityType, string> DefaultMarkets { get; } // 默认市场 69 bool CanSubmitOrder(Security security, Order order, out BrokerageMessageEvent message); // 能否下单 79 bool CanUpdateOrder(Security, Order, UpdateOrderRequest, out BrokerageMessageEvent); // 能否改单 91 bool CanExecuteOrder(Security security, Order order); // 能否执行(回测模拟用) 105 decimal GetLeverage(Security security); // 杠杆 119 IFillModel GetFillModel(Security security); // 成交模型 126 IFeeModel GetFeeModel(Security security); // 手续费模型 133 ISlippageModel GetSlippageModel(Security security); // 滑点模型 140 ISettlementModel GetSettlementModel(Security security); // 结算模型(T+N) 147 IMarginInterestRateModel GetMarginInterestRateModel(Security); // 保证金利率 163 IBuyingPowerModel GetBuyingPowerModel(Security security); // 购买力模型 178 IShortableProvider GetShortableProvider(Security security); // 做空提供商
一个接口管"这家券商怎么对待每种订单"。DefaultBrokerageModel.cs:222 是默认实现:
222 public virtual IFillModel GetFillModel(Security security) 224 switch (security.Type) 226 case SecurityType.Equity: return new EquityFillModel(); 228 case SecurityType.FutureOption: return new FutureOptionFillModel(); 230 case SecurityType.Future: return new FutureFillModel(); 234 default: return new ImmediateFillModel(); // 其他用立即成交 252 public virtual IFeeModel GetFeeModel(Security security) 258 case SecurityType.Forex: case SecurityType.Crypto: ... return new ConstantFeeModel(0m); 264 case SecurityType.Equity: case SecurityType.Option: ... return new InteractiveBrokersFeeModel(); 281 public virtual ISlippageModel GetSlippageModel(Security security) 283 return NullSlippageModel.Instance; // 默认不滑点
InteractiveBrokersBrokerageModel 等子类会重写 GetFeeModel 返回 InteractiveBrokersFeeModel、重写 GetFillModel 返回带 IB 特定规则(如涨跌停)的 Fill 模型。用户在算法里一行设置(QCAlgorithm.cs:1353):
1353 public void SetBrokerageModel(BrokerageName brokerage, AccountType accountType = AccountType.Margin) 1355 SetBrokerageModel(Brokerages.BrokerageModel.Create(Transactions, brokerage, accountType)); 1364 public void SetBrokerageModel(IBrokerageModel model) 1366 BrokerageModel = model; 1371 var brokerageSecurityInitializer = new BrokerageModelSecurityInitializer(model, SecuritySeeder.Null);
BrokerageModelSecurityInitializer 会在每个 Security 添加时,自动给它配 FillModel/FeeModel/SlippageModel/SettlementModel/BuyingPowerModel。这就是"一行 SetBrokerageModel,全套模型换好"的机制——回测想模拟 IB,就 SetBrokerageModel(BrokerageName.InteractiveBrokers);想模拟币安,就 SetBrokerageModel(BrokerageName.Binance)。
期权行权不是普通成交——它产生标的资产的买卖。Common/Orders/OptionExercise/IOptionExerciseModel.cs:25:
25 public interface IOptionExerciseModel 34 IEnumerable<OrderEvent> OptionExercise(Option option, OptionExerciseOrder order); // 返回多个事件
行权返回多个 OrderEvent(而不是一个):一个"期权合约归零"事件 + 一个"按行权价买卖标的"事件。看-call 行权:持有 1 张 IBM 看涨期权(行权价 150),行权后产生"买入 100 股 IBM @ 150"事件 + "期权合约数量归零"事件。DefaultExerciseModel.cs 是默认实现,会判断"实值则行权产生标的持仓、虚值则放弃"。注意 OptionExerciseOrder.GetValueImpl(上一节 Order.cs):行权价值按 option.GetExerciseQuantity(Quantity) * StrikePrice 算,不是按期权市场价。
每次成交(OrderEvent)回流后,引擎调 SecurityPortfolioManager.ProcessFill(orderEvent) 更新账本,影响三个维度:
Holdings.SetHoldings(avgPrice, newQuantity) 重算平均成本与持仓数量。买入加仓、卖出减仓、反向平仓再开仓。CashBook[quoteCurrency].AddAmount(-(fillPrice * fillQuantity + fee))——成交金额 + 手续费从对应币种现金扣除。卖出现金增加,买入现金减少。多币种时还涉及汇率换算。Math.Min(|fillQuantity|, |holdings|))产生已实现盈亏 (fillPrice - avgCost) * closedQuantity * sign,记入 Holdings.Profit;未平仓部分更新浮动盈亏 LastTradeProfit。TradeBuilder 同时记录这笔 Trade 的盈亏与最大回撤。这三步合起来就是"成交→入账"的闭环。回测里这套在 Portfolio.ProcessFill 同步完成;实盘里由券商回报触发 AccountChanged 事件,BrokerageTransactionHandler 同步刷新 Portfolio 持仓与现金(以券商实际余额为准)。
把本章三节合起来,回测与实盘的成交差异全貌:
| 维度 | 回测(BacktestingTransactionHandler) | 实盘(BrokerageTransactionHandler) |
|---|---|---|
| 成交价来源 | Fill 模型用历史 bar 算(理论价) | 交易所真实撮合(实际价) |
| 成交时机 | BacktestingBrokerage.Scan 同步扫描 |
券商异步推送 OrderEvent |
| 滑点 | 默认 NullSlippageModel(可配置) |
真实存在(无法关闭) |
| 手续费 | 按 IBrokerageModel.GetFeeModel 模拟 |
券商实际扣费 |
| 部分成交 | 默认全成交(可配 PartialFillModel) | 常见(流动性不足时) |
| 拒单 | 极少(除非 PreOrderChecks 不过) | 常见(资金不足/限价异常/券商规则) |
| 延迟 | 零(同步) | 有(网络 + 撮合) |
| 确定性 | 完全确定(相同数据相同结果) | 不确定(受市场环境影响) |
| 时钟驱动 | 数据时间(Synchronizer) | 墙钟(LiveSynchronizer + DateTime.UtcNow) |
这张表呼应第 3 章的"回测实盘统一"——引擎主体对两者零感知,差异完全收敛到 TransactionHandler 的两套实现 + config.json 的 environment 字段。回测的"乐观假设"(零滑点、全成交、零延迟)是回测收益通常高于实盘的主因,生产级回测建议配置真实的 Slippage/Fee 模型与 PartialFill 行为以贴近实盘。
IFeeModel.GetOrderFee 返回 OrderFee(金额+币种);IB 美股每股 0.005 美元/最低 1 美元/最高 0.5%;期权行权免费;30+ 券商专属费率模型。ISlippageModel.GetSlippageApproximation 返回绝对滑点金额,在 Fill 模型算出理论价后叠加(买单加、卖单减);默认 NullSlippageModel 不滑点,可选 Constant/VolumeShare/MarketImpact。SetBrokerageModel(BrokerageName.XXX) 一行设置,经 BrokerageModelSecurityInitializer 下放到每个 Security。IOptionExerciseModel 处理;实值行权产生标的持仓,虚值放弃。本章完。下一章(第 9 章)钻取 Algorithm Framework 五层——把本章的订单与上章的指标组织成可组装的策略框架(Alpha/Risk Management/Portfolio/Execution/Universe Selection)。