FillModel.LimitIfTouchedFill checks the wrong side of the quote (sell vs ask, buy vs bid), so LIT orders fill across the spread
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 初心者へのやさしさ
- 55/100
調査の方向性
Start with Common/Orders/Fills/FillModel.cs and compare LimitIfTouchedFill with the equity model behavior described in the issue; the intended rule needs confirmation because the linked documentation disagrees. Read Tests/Common/Orders/Fills/LimitIfTouchedSpreadTests.cs and the existing ImmediateFillModelTests.PerformsLimitIfTouchedFillBuy/Sell cases. Done means the intended quote side is established and tests cover a spread where bid and ask differ.
索引モデルが issue の本文から書いたものです。
説明
Expected Behavior
After the trigger is touched, a LIT order should behave like a limit order: a buy fills only when the ask is <= limit, and a sell fills only when the bid is >= limit. That is what EquityFillModel.LimitIfTouchedFill does (ask for buys, bid for sells). The base FillModel already prices this way in LimitFill, StopLimitFill and GetPrices.
Actual Behavior
Common/Orders/Fills/FillModel.cs, LimitIfTouchedFill uses the opposite side of the quote:
// line 659, Sell
if (GetAskPrice(asset, out pricesEndTime) >= order.LimitPrice)
// line 675, Buy
if (GetBidPrice(asset, out pricesEndTime) <= order.LimitPrice)
The order then fills at order.LimitPrice. A buy LIT fills as soon as the bid reaches the limit, even when no seller is offering at that price. A sell LIT fills as soon as the ask reaches the limit. Either way the order fills at its limit price on the far side of the spread.
Example: an option quotes bid 1.00 / ask 1.20. A buy LIT with limit 1.05 fills at 1.05 once triggered, but nobody is offering at 1.05.
This affects every security that uses the base implementation: ImmediateFillModel, FutureFillModel and FutureOptionFillModel, which covers options, futures, future options, forex, crypto, CFDs and indices. Equities are not affected because EquityFillModel overrides the method.
The code has been this way since LIT orders were added in 643e8754 (#5164). The existing tests ImmediateFillModelTests.PerformsLimitIfTouchedFillBuy/Sell use quote bars where bid == ask (101/101/100.5/101 and 100/100/99/99), so they cannot catch which side is used.
Docs: which behaviour is intended?
The documentation does not agree with itself, so I'd like to confirm the intended rule before opening a PR:
- Immediate model page (https://www.quantconnect.com/docs/v2/writing-algorithms/reality-modeling/trade-fills/supported-models/immediate-model) describes the current code: "Buy orders fill when the bid price <= limit price and sell orders fill when the ask price >= limit price." and "The order fills at the limit price."
- Equity model page (https://www.quantconnect.com/docs/v2/writing-algorithms/reality-modeling/trade-fills/supported-models/equity-model), LIT fill table: Buy "Best effort ask price <= limit price", fill price "min(best effort ask price, limit price)"; Sell "Best effort bid price >= limit price", fill price "max(best effort bid price, limit price)".
- LIT order type page (https://www.quantconnect.com/docs/v2/writing-algorithms/trading-and-orders/order-types/limit-if-touched-orders) uses both: "fills the order when the bid close price is less than or equal to the limit price, and sets the fill price to the minimum of the ask price and the limit price" (buy), and "fills the order when the ask close price is greater than or equal to the limit price, and sets the fill price to the maximum of the bid price and the limit price" (sell). The base model always fills at the limit price, so this sentence does not match any current implementation.
If the Immediate model page describes the intended behaviour, this is a docs and consistency question rather than a bug. If the Equity model rule is the intended one, I'd propose the fix below.
Potential Solution
Swap the two price getters in FillModel.LimitIfTouchedFill:
- if (GetAskPrice(asset, out pricesEndTime) >= order.LimitPrice)
+ if (GetBidPrice(asset, out pricesEndTime) >= order.LimitPrice)
...
- if (GetBidPrice(asset, out pricesEndTime) <= order.LimitPrice)
+ if (GetAskPrice(asset, out pricesEndTime) <= order.LimitPrice)
I tested this locally. The two new tests below pass, and all 35 LIT-related unit tests (including those two) pass. This issue does not cover whether the fill price should also follow the Equity model (min(ask, limit) / max(bid, limit)) rather than always using the limit price. I can include that change if you want it.
Regression algorithms that may need their statistics updated:
ContinuousFutureLimitIfTouchedOrderRegressionAlgorithm(C#), which usesFutureFillModel. This is the only LIT regression algorithm I found on a non-equity security.LimitIfTouchedRegressionAlgorithm/LimitIfTouchedAsyncRegressionAlgorithm(C# and Python) andSplitEquityRegressionAlgorithmtrade equities throughEquityFillModel. They should not change, but I would re-run them to check.
If you confirm the intended behaviour, I can open a PR with the code change, the tests and any regression algorithm updates. I can also send a matching fix to QuantConnect/Documentation for whichever pages are wrong.
Reproducing the Problem
Unit test Tests/Common/Orders/Fills/LimitIfTouchedSpreadTests.cs uses ImmediateFillModel with SPY minute TradeBar and QuoteBar subscriptions. The spread is set wider than the limit, so neither order should fill:
- Buy LIT, quantity 100, trigger 101.5, limit 100. TradeBar low 99.8 touches the trigger. Quote: bid 99.90, ask 100.30. The ask is above the limit, so the order should not fill. Current code compares the bid (99.90 <= 100) and fills 100 @ 100.
- Sell LIT, quantity -100, trigger 101.5, limit 103. TradeBar high 103.1 touches the trigger. Quote: bid 102.70, ask 103.10. The bid is below the limit, so the order should not fill. Current code compares the ask (103.10 >= 103) and fills -100 @ 103.
Both cases assert OrderStatus.None and fail on master. Both pass with the swap above.
System Information
Linux (kernel 7.0), .NET SDK 10.0.401, Lean master at 9ecfed9.
Checklist
- I have completely filled out this template
- I have confirmed that this issue exists on the current
masterbranch - I have confirmed that this is not a duplicate issue by searching issues
- I have provided detailed steps to reproduce the issue
- 主要言語
- C#
- スター
- 22.5k
- フォーク
- 5.4k
- 平均マージ
- 22時間 6分
- マージ済み PR(30日)
- 81
環境構築
このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。
- Dockerfile または Docker Compose ファイルあり
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
QuantConnect/Lean のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
QuantConnect/Lean#10082 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
QuantConnect/Lean#10059 ·
メンテナーはふだん 1 日以内に返信
-
library-request
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
QuantConnect/Lean#10034 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
QuantConnect/Lean#9965 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
QuantConnect/Lean#9955 ·
メンテナーはふだん 1 日以内に返信
QuantConnect/Lean の issue をすべて見る
似ている issue
-
bug emitter:client:csharp
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
microsoft/typespec#12572 · コメント 1 件 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
needs-area-label
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 2 日以内に返信
-
AWS Lambda: KeyNotFoundException drops HTTP-triggered transactions when allowAllHeaders is disabledオープンbug
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
newrelic/newrelic-dotnet-agent#3949 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
area-networking
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
dotnet/aspnetcore#70545 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 87/100
メンテナーはふだん 1 日以内に返信