Skip to content

Commit f910480

Browse files
authored
Fix runtime error when an option universe is removed and re-added in the same time step (#9626)
* Fix universe subscription exception when option universe is removed and re-added in the same time step * Remove incorrect issue reference from comments * Use RegressionTestException in regression algorithm * Clean up stale universe members when universe is removed and re-added in the same time step * Convert regression algorithms to C# and centralize universe selection data handling * Simplify fix by parking colliding universe subscription requests and re-issuing them on removal * Restore universe subscription request invariant exception * Report parked universe subscription requests as successfully added
1 parent f34dbc9 commit f910480

4 files changed

Lines changed: 467 additions & 0 deletions

File tree

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System.Collections.Generic;
17+
using System.Linq;
18+
using QuantConnect.Data;
19+
using QuantConnect.Data.UniverseSelection;
20+
using QuantConnect.Interfaces;
21+
22+
namespace QuantConnect.Algorithm.CSharp
23+
{
24+
/// <summary>
25+
/// Regression algorithm asserting that when an option universe is removed and one for the same
26+
/// underlying is re-added in the same time step, the previously selected contracts that are not
27+
/// re-selected by the new universe are properly cleaned up: their subscriptions are removed and
28+
/// removed security changes are emitted for them.
29+
/// </summary>
30+
public class OptionUniverseRemovedAndReAddedMemberCleanupRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
31+
{
32+
private Symbol _canonical;
33+
private bool _readded;
34+
private bool _checked;
35+
private List<Symbol> _oldMembers;
36+
private HashSet<Symbol> _removedSymbols;
37+
38+
public override void Initialize()
39+
{
40+
SetStartDate(2014, 6, 6);
41+
SetEndDate(2014, 6, 9);
42+
SetCash(100000);
43+
44+
_removedSymbols = new HashSet<Symbol>();
45+
46+
// Wide filter: several strikes and expirations will be selected
47+
var option = AddOption("AAPL", Resolution.Minute);
48+
option.SetFilter(-2, 2, 0, 180);
49+
_canonical = option.Symbol;
50+
}
51+
52+
public override void OnSecuritiesChanged(SecurityChanges changes)
53+
{
54+
foreach (var security in changes.RemovedSecurities)
55+
{
56+
_removedSymbols.Add(security.Symbol);
57+
}
58+
}
59+
60+
public override void OnData(Slice slice)
61+
{
62+
if (!_readded)
63+
{
64+
if (Time.Hour < 10)
65+
{
66+
return;
67+
}
68+
69+
var universe = UniverseManager[_canonical];
70+
if (universe.Members.Count == 0)
71+
{
72+
return;
73+
}
74+
75+
_oldMembers = universe.Members.Keys.Where(symbol => !symbol.IsCanonical()).ToList();
76+
77+
// Remove the universe and re-add it with a narrower filter in the same time step:
78+
// most of the previously selected contracts will not be re-selected by the new universe
79+
RemoveSecurity(_canonical);
80+
var option = AddOption("AAPL", Resolution.Minute);
81+
option.SetFilter(universeFilter => universeFilter.Strikes(0, 0).Expiration(0, 30));
82+
_canonical = option.Symbol;
83+
_readded = true;
84+
}
85+
else if (!_checked && (Time.Hour > 10 || Time.Minute >= 30))
86+
{
87+
_checked = true;
88+
AssertOldMembersCleanedUp();
89+
}
90+
}
91+
92+
private void AssertOldMembersCleanedUp()
93+
{
94+
var currentMembers = UniverseManager[_canonical].Members.Keys.ToHashSet();
95+
var subscribed = SubscriptionManager.Subscriptions.Select(config => config.Symbol).ToHashSet();
96+
97+
var notReselected = _oldMembers.Where(symbol => !currentMembers.Contains(symbol)).ToList();
98+
if (notReselected.Count == 0)
99+
{
100+
throw new RegressionTestException("Expected some previously selected contracts to not be re-selected");
101+
}
102+
103+
var stillSubscribed = notReselected.Where(subscribed.Contains).ToList();
104+
if (stillSubscribed.Count > 0)
105+
{
106+
throw new RegressionTestException(
107+
$"Expected the subscriptions of the {notReselected.Count} deselected contracts to be removed, " +
108+
$"but {stillSubscribed.Count} are still subscribed, e.g. {string.Join(", ", stillSubscribed.Take(3))}");
109+
}
110+
111+
var missingRemovedEvents = notReselected.Where(symbol => !_removedSymbols.Contains(symbol)).ToList();
112+
if (missingRemovedEvents.Count > 0)
113+
{
114+
throw new RegressionTestException(
115+
$"Expected removed security changes for the {notReselected.Count} deselected contracts, " +
116+
$"but {missingRemovedEvents.Count} were not notified, e.g. {string.Join(", ", missingRemovedEvents.Take(3))}");
117+
}
118+
}
119+
120+
public override void OnEndOfAlgorithm()
121+
{
122+
if (!_readded)
123+
{
124+
throw new RegressionTestException("The option universe was never removed and re-added");
125+
}
126+
if (!_checked)
127+
{
128+
throw new RegressionTestException("The clean up assertions were never performed");
129+
}
130+
}
131+
132+
/// <summary>
133+
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
134+
/// </summary>
135+
public bool CanRunLocally { get; } = true;
136+
137+
/// <summary>
138+
/// This is used by the regression test system to indicate which languages this algorithm is written in.
139+
/// </summary>
140+
public List<Language> Languages { get; } = new() { Language.CSharp };
141+
142+
/// <summary>
143+
/// Data Points count of all timeslices of algorithm
144+
/// </summary>
145+
public long DataPoints => 34380;
146+
147+
/// <summary>
148+
/// Data Points count of the algorithm history
149+
/// </summary>
150+
public int AlgorithmHistoryDataPoints => 0;
151+
152+
/// <summary>
153+
/// Final status of the algorithm
154+
/// </summary>
155+
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
156+
157+
/// <summary>
158+
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
159+
/// </summary>
160+
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
161+
{
162+
{"Total Orders", "0"},
163+
{"Average Win", "0%"},
164+
{"Average Loss", "0%"},
165+
{"Compounding Annual Return", "0%"},
166+
{"Drawdown", "0%"},
167+
{"Expectancy", "0"},
168+
{"Start Equity", "100000"},
169+
{"End Equity", "100000"},
170+
{"Net Profit", "0%"},
171+
{"Sharpe Ratio", "0"},
172+
{"Sortino Ratio", "0"},
173+
{"Probabilistic Sharpe Ratio", "0%"},
174+
{"Loss Rate", "0%"},
175+
{"Win Rate", "0%"},
176+
{"Profit-Loss Ratio", "0"},
177+
{"Alpha", "0"},
178+
{"Beta", "0"},
179+
{"Annual Standard Deviation", "0"},
180+
{"Annual Variance", "0"},
181+
{"Information Ratio", "-9.486"},
182+
{"Tracking Error", "0.008"},
183+
{"Treynor Ratio", "0"},
184+
{"Total Fees", "$0.00"},
185+
{"Estimated Strategy Capacity", "$0"},
186+
{"Lowest Capacity Asset", ""},
187+
{"Portfolio Turnover", "0%"},
188+
{"Drawdown Recovery", "0"},
189+
{"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
190+
};
191+
}
192+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System.Collections.Generic;
17+
using System.Linq;
18+
using QuantConnect.Data;
19+
using QuantConnect.Interfaces;
20+
21+
namespace QuantConnect.Algorithm.CSharp
22+
{
23+
/// <summary>
24+
/// Regression algorithm asserting that removing an option universe and re-adding one for the
25+
/// same underlying within the same time step does not throw, and that the re-added universe
26+
/// keeps providing option chain data.
27+
/// </summary>
28+
public class OptionUniverseRemovedAndReAddedRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
29+
{
30+
private Symbol _canonical;
31+
private bool _readded;
32+
private int _chainsBefore;
33+
private int _chainsAfter;
34+
35+
public override void Initialize()
36+
{
37+
SetStartDate(2014, 6, 6);
38+
SetEndDate(2014, 6, 9);
39+
SetCash(100000);
40+
41+
_canonical = AddAaplOption();
42+
}
43+
44+
private Symbol AddAaplOption()
45+
{
46+
var option = AddOption("AAPL", Resolution.Minute);
47+
option.SetFilter(-2, 2, 0, 180);
48+
return option.Symbol;
49+
}
50+
51+
public override void OnData(Slice slice)
52+
{
53+
var hasChain = slice.OptionChains.TryGetValue(_canonical, out var chain) && chain.Any();
54+
55+
if (!_readded)
56+
{
57+
if (hasChain)
58+
{
59+
_chainsBefore++;
60+
}
61+
62+
if (Time.Hour >= 10)
63+
{
64+
// Remove the option universe and re-add one for the same underlying in the same time step
65+
RemoveSecurity(_canonical);
66+
_canonical = AddAaplOption();
67+
_readded = true;
68+
}
69+
}
70+
else if (hasChain)
71+
{
72+
_chainsAfter++;
73+
}
74+
}
75+
76+
public override void OnEndOfAlgorithm()
77+
{
78+
if (!_readded)
79+
{
80+
throw new RegressionTestException("The option universe was never removed and re-added");
81+
}
82+
if (_chainsBefore == 0)
83+
{
84+
throw new RegressionTestException("Expected option chain data before the universe was removed");
85+
}
86+
if (_chainsAfter == 0)
87+
{
88+
throw new RegressionTestException("Expected option chain data after the universe was re-added");
89+
}
90+
}
91+
92+
/// <summary>
93+
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
94+
/// </summary>
95+
public bool CanRunLocally { get; } = true;
96+
97+
/// <summary>
98+
/// This is used by the regression test system to indicate which languages this algorithm is written in.
99+
/// </summary>
100+
public List<Language> Languages { get; } = new() { Language.CSharp };
101+
102+
/// <summary>
103+
/// Data Points count of all timeslices of algorithm
104+
/// </summary>
105+
public long DataPoints => 108983;
106+
107+
/// <summary>
108+
/// Data Points count of the algorithm history
109+
/// </summary>
110+
public int AlgorithmHistoryDataPoints => 0;
111+
112+
/// <summary>
113+
/// Final status of the algorithm
114+
/// </summary>
115+
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
116+
117+
/// <summary>
118+
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
119+
/// </summary>
120+
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
121+
{
122+
{"Total Orders", "0"},
123+
{"Average Win", "0%"},
124+
{"Average Loss", "0%"},
125+
{"Compounding Annual Return", "0%"},
126+
{"Drawdown", "0%"},
127+
{"Expectancy", "0"},
128+
{"Start Equity", "100000"},
129+
{"End Equity", "100000"},
130+
{"Net Profit", "0%"},
131+
{"Sharpe Ratio", "0"},
132+
{"Sortino Ratio", "0"},
133+
{"Probabilistic Sharpe Ratio", "0%"},
134+
{"Loss Rate", "0%"},
135+
{"Win Rate", "0%"},
136+
{"Profit-Loss Ratio", "0"},
137+
{"Alpha", "0"},
138+
{"Beta", "0"},
139+
{"Annual Standard Deviation", "0"},
140+
{"Annual Variance", "0"},
141+
{"Information Ratio", "-9.486"},
142+
{"Tracking Error", "0.008"},
143+
{"Treynor Ratio", "0"},
144+
{"Total Fees", "$0.00"},
145+
{"Estimated Strategy Capacity", "$0"},
146+
{"Lowest Capacity Asset", ""},
147+
{"Portfolio Turnover", "0%"},
148+
{"Drawdown Recovery", "0"},
149+
{"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
150+
};
151+
}
152+
}

0 commit comments

Comments
 (0)