Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Algorithm.Python/PythonDictionaryFeatureRegressionAlgorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ def test_slice_dictionary(self):
if spy is None:
raise AssertionError('SPY is not in Slice')

if slice.contains_key(None):
raise AssertionError('Slice.contains_key(None) should return False instead of throwing')

if slice.get(None) is not None:
raise AssertionError('Slice.get(None) should return None instead of throwing')

if slice.bars.contains_key(None):
raise AssertionError('TradeBars.contains_key(None) should return False instead of throwing')

for symbol, bar in slice.bars.items():
self.plot(symbol, 'Price', bar.close)

Expand All @@ -74,6 +83,16 @@ def test_securities_dictionary(self):
if aapl is not None:
raise AssertionError('aapl is not None')

# A None key should behave like a missing key instead of throwing,
# e.g. when a symbol field is only assigned later in the algorithm
none_symbol = None
price = self.securities[none_symbol].price if self.securities.contains_key(none_symbol) else None
if price is not None:
raise AssertionError('Securities.contains_key(None) should return False instead of throwing')

if self.securities.get(none_symbol) is not None:
raise AssertionError('Securities.get(None) should return None instead of throwing')

for symbol, security in self.securities.items():
self.plot(symbol, 'Price', security.price)

Expand All @@ -95,6 +114,12 @@ def test_portfolio_dictionary(self):
if aapl is not None:
raise AssertionError('aapl is not None')

if self.portfolio.contains_key(None):
raise AssertionError('Portfolio.contains_key(None) should return False instead of throwing')

if self.portfolio.get(None) is not None:
raise AssertionError('Portfolio.get(None) should return None instead of throwing')

for symbol, holdings in self.portfolio.items():
msg = f'{symbol}: {holdings.leverage}'

Expand Down
5 changes: 5 additions & 0 deletions Common/Data/Market/OptionChains.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ public override bool Remove(KeyValuePair<Symbol, OptionChain> item)

private static Symbol GetCanonicalOptionSymbol(Symbol symbol)
{
if (ReferenceEquals(symbol, null))
{
return null;
}

if (symbol.SecurityType.HasOptions())
{
return Symbol.CreateCanonicalOption(symbol);
Expand Down
4 changes: 2 additions & 2 deletions Common/Data/Slice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ public T Get<T>(Symbol symbol)
/// <returns>True if this instance contains data for the symbol, false otherwise</returns>
public override bool ContainsKey(Symbol symbol)
{
return _data.Value.ContainsKey(symbol);
return !ReferenceEquals(symbol, null) && _data.Value.ContainsKey(symbol);
}

/// <summary>
Expand All @@ -540,7 +540,7 @@ public override bool TryGetValue(Symbol symbol, out dynamic data)
{
data = null;
SymbolData symbolData;
if (_data.Value.TryGetValue(symbol, out symbolData))
if (!ReferenceEquals(symbol, null) && _data.Value.TryGetValue(symbol, out symbolData))
{
data = symbolData.GetData();
return data != null;
Expand Down
7 changes: 6 additions & 1 deletion Common/Securities/CashBook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ public bool Remove(KeyValuePair<string, Cash> item)
/// <param name="symbol">Key.</param>
public override bool ContainsKey(string symbol)
{
return _currencies.ContainsKey(symbol);
return !ReferenceEquals(symbol, null) && _currencies.ContainsKey(symbol);
}

/// <summary>
Expand All @@ -291,6 +291,11 @@ public override bool ContainsKey(string symbol)
/// <param name="value">Value.</param>
public override bool TryGetValue(string symbol, out Cash value)
{
if (ReferenceEquals(symbol, null))
{
value = null;
return false;
}
return _currencies.TryGetValue(symbol, out value);
}

Expand Down
5 changes: 5 additions & 0 deletions Common/Securities/Positions/SecurityPositionGroupModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,11 @@ private void ResolvePositionGroups()
/// <returns>True if a group with the specified key was found, false otherwise</returns>
public override bool TryGetValue(PositionGroupKey key, out IPositionGroup value)
{
if (ReferenceEquals(key, null))
{
value = null;
return false;
}
return Groups.TryGetGroup(key, out value);
}
}
Expand Down
9 changes: 9 additions & 0 deletions Common/Securities/SecurityManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ public bool Contains(KeyValuePair<Symbol, Security> pair)
/// <returns>Bool true if contains this symbol pair</returns>
public override bool ContainsKey(Symbol symbol)
{
if (ReferenceEquals(symbol, null))
{
return false;
}
lock (_securityManager)
{
return _completeSecuritiesCollection.ContainsKey(symbol);
Expand Down Expand Up @@ -252,6 +256,11 @@ public ICollection<Symbol> Keys
/// <returns>True on successfully locating the security object</returns>
public override bool TryGetValue(Symbol symbol, out Security security)
{
if (ReferenceEquals(symbol, null))
{
security = null;
return false;
}
lock (_securityManager)
{
return _completeSecuritiesCollection.TryGetValue(symbol, out security);
Expand Down
7 changes: 6 additions & 1 deletion Common/Util/BaseExtendedDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ public BaseExtendedDictionary(IEnumerable<TValue> data, Func<TValue, TKey> keySe
/// <returns>true if the key was found; otherwise, false</returns>
public override bool TryGetValue(TKey key, out TValue value)
{
if (ReferenceEquals(key, null))
{
value = default;
return false;
}
return Dictionary.TryGetValue(key, out value);
}

Expand Down Expand Up @@ -170,7 +175,7 @@ public virtual void Add(KeyValuePair<TKey, TValue> item)
/// <returns>true if the dictionary contains an element with the specified key; otherwise, false</returns>
public override bool ContainsKey(TKey key)
{
return Dictionary.ContainsKey(key);
return !ReferenceEquals(key, null) && Dictionary.ContainsKey(key);
}

/// <summary>
Expand Down
37 changes: 37 additions & 0 deletions Tests/Common/ExtendedDictionaryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Statistics;
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Securities;
using QuantConnect.Securities.Positions;

namespace QuantConnect.Tests.Common
{
Expand Down Expand Up @@ -229,6 +233,39 @@ def set(dictionary, key, value):
Assert.IsInstanceOf<KeyNotFoundException>(exception.InnerException);
}

private static IEnumerable<TestCaseData> NullKeyTestCases()
{
var time = new DateTime(2025, 1, 1);
var securities = new SecurityManager(new TimeKeeper(time, TimeZones.NewYork));

yield return new TestCaseData(securities).SetArgDisplayNames(nameof(SecurityManager));
yield return new TestCaseData(new SecurityPortfolioManager(securities, new SecurityTransactionManager(null, securities), new AlgorithmSettings()))
.SetArgDisplayNames(nameof(SecurityPortfolioManager));
yield return new TestCaseData(new CashBook()).SetArgDisplayNames(nameof(CashBook));
yield return new TestCaseData(new Slice(time, new List<BaseData>(), time)).SetArgDisplayNames(nameof(Slice));
yield return new TestCaseData(new DataDictionary<TradeBar>()).SetArgDisplayNames("DataDictionary");
yield return new TestCaseData(new TradeBars()).SetArgDisplayNames(nameof(TradeBars));
yield return new TestCaseData(new OptionChains()).SetArgDisplayNames(nameof(OptionChains));
yield return new TestCaseData(new FuturesChains()).SetArgDisplayNames(nameof(FuturesChains));
yield return new TestCaseData(new UniverseManager()).SetArgDisplayNames(nameof(UniverseManager));
yield return new TestCaseData(new SecurityPositionGroupModel()).SetArgDisplayNames(nameof(SecurityPositionGroupModel));
}

[TestCaseSource(nameof(NullKeyTestCases))]
public void DictionariesHandleNullKeysGracefully(object dictionary)
{
AssertNullKeyIsHandledGracefully((dynamic)dictionary);
}

private static void AssertNullKeyIsHandledGracefully<TKey, TValue>(ExtendedDictionary<TKey, TValue> dictionary)
where TKey : class
where TValue : class
{
Assert.IsFalse(dictionary.ContainsKey(null));
Assert.IsFalse(dictionary.TryGetValue(null, out _));
Assert.IsNull(dictionary.get(null));
}

private class TestDictionary<TKey, TValue> : ExtendedDictionary<TKey, TValue>
{
private readonly Dictionary<TKey, TValue> _data = new();
Expand Down
Loading