diff --git a/Algorithm.CSharp/AddTimeValidationAndMarketHoursRegressionAlgorithm.cs b/Algorithm.CSharp/AddTimeValidationAndMarketHoursRegressionAlgorithm.cs
new file mode 100644
index 000000000000..858a073bf5da
--- /dev/null
+++ b/Algorithm.CSharp/AddTimeValidationAndMarketHoursRegressionAlgorithm.cs
@@ -0,0 +1,146 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using QuantConnect.Interfaces;
+
+namespace QuantConnect.Algorithm.CSharp
+{
+ ///
+ /// Regression algorithm asserting that adding a security with an unknown ticker/market combination fails fast
+ /// at add time naming the markets that do have the ticker, and that
+ /// works without a subscription
+ ///
+ public class AddTimeValidationAndMarketHoursRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
+ {
+ public override void Initialize()
+ {
+ SetStartDate(2013, 10, 07);
+ SetEndDate(2013, 10, 11);
+
+ AddEquity("SPY", Resolution.Minute);
+
+ // BNBUSD is not a coinbase pair: the add must fail naming the markets that do have it
+ AssertThrows(() => AddCrypto("BNBUSD", market: Market.Coinbase),
+ "Crypto 'BNBUSD' symbol could not be found in the database", "Markets with a 'BNBUSD' Crypto entry:", Market.Kraken);
+
+ // oanda has no crypto entries at all: the exchange hours failure must also name the valid markets
+ AssertThrows(() => AddCrypto("BTCUSD", market: Market.Oanda),
+ "Unable to locate exchange hours for Crypto-oanda-BTCUSD", "Markets with a 'BTCUSD' Crypto entry:", Market.Coinbase);
+
+ // exchange hours lookup must not require a subscription
+ var hours = MarketHours("IBM");
+ if (!TimeZones.NewYork.Equals(hours.TimeZone))
+ {
+ throw new RegressionTestException($"Unexpected time zone for IBM market hours: {hours.TimeZone}");
+ }
+
+ var cryptoHours = MarketHours(QuantConnect.Symbol.Create("BTCUSD", SecurityType.Crypto, Market.Coinbase));
+ if (!cryptoHours.IsMarketAlwaysOpen)
+ {
+ throw new RegressionTestException("Expected coinbase BTCUSD market to be always open");
+ }
+
+ // and the lookups must not have added any securities
+ if (Securities.Keys.Any(symbol => symbol.Value == "IBM" || symbol.Value == "BTCUSD" || symbol.Value == "BNBUSD"))
+ {
+ throw new RegressionTestException("No security should have been added by the market hours lookups or the failed adds");
+ }
+ }
+
+ private static void AssertThrows(Action addSecurity, params string[] expectedMessageParts)
+ {
+ try
+ {
+ addSecurity();
+ }
+ catch (ArgumentException exception)
+ {
+ foreach (var expectedMessagePart in expectedMessageParts)
+ {
+ if (!exception.Message.Contains(expectedMessagePart, StringComparison.InvariantCulture))
+ {
+ throw new RegressionTestException($"Expected message to contain '{expectedMessagePart}' but was: {exception.Message}");
+ }
+ }
+ return;
+ }
+
+ throw new RegressionTestException("Expected an ArgumentException to be thrown at add time");
+ }
+
+ ///
+ /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
+ ///
+ public bool CanRunLocally { get; } = true;
+
+ ///
+ /// This is used by the regression test system to indicate which languages this algorithm is written in.
+ ///
+ public List Languages { get; } = new() { Language.CSharp, Language.Python };
+
+ ///
+ /// Data Points count of all timeslices of algorithm
+ ///
+ public long DataPoints => 3943;
+
+ ///
+ /// Data Points count of the algorithm history
+ ///
+ public int AlgorithmHistoryDataPoints => 0;
+
+ ///
+ /// Final status of the algorithm
+ ///
+ public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
+
+ ///
+ /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
+ ///
+ public Dictionary ExpectedStatistics => new Dictionary
+ {
+ {"Total Orders", "0"},
+ {"Average Win", "0%"},
+ {"Average Loss", "0%"},
+ {"Compounding Annual Return", "0%"},
+ {"Drawdown", "0%"},
+ {"Expectancy", "0"},
+ {"Start Equity", "100000"},
+ {"End Equity", "100000"},
+ {"Net Profit", "0%"},
+ {"Sharpe Ratio", "0"},
+ {"Sortino Ratio", "0"},
+ {"Probabilistic Sharpe Ratio", "0%"},
+ {"Loss Rate", "0%"},
+ {"Win Rate", "0%"},
+ {"Profit-Loss Ratio", "0"},
+ {"Alpha", "0"},
+ {"Beta", "0"},
+ {"Annual Standard Deviation", "0"},
+ {"Annual Variance", "0"},
+ {"Information Ratio", "-8.91"},
+ {"Tracking Error", "0.223"},
+ {"Treynor Ratio", "0"},
+ {"Total Fees", "$0.00"},
+ {"Estimated Strategy Capacity", "$0"},
+ {"Lowest Capacity Asset", ""},
+ {"Portfolio Turnover", "0%"},
+ {"Drawdown Recovery", "0"},
+ {"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
+ };
+ }
+}
diff --git a/Algorithm.Python/AddTimeValidationAndMarketHoursRegressionAlgorithm.py b/Algorithm.Python/AddTimeValidationAndMarketHoursRegressionAlgorithm.py
new file mode 100644
index 000000000000..93f3a17aff18
--- /dev/null
+++ b/Algorithm.Python/AddTimeValidationAndMarketHoursRegressionAlgorithm.py
@@ -0,0 +1,58 @@
+# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from AlgorithmImports import *
+
+###
+### Regression algorithm asserting that adding a security with an unknown ticker/market combination fails fast
+### at add time naming the markets that do have the ticker, and that market_hours() works without a subscription
+###
+class AddTimeValidationAndMarketHoursRegressionAlgorithm(QCAlgorithm):
+ def initialize(self):
+ self.set_start_date(2013, 10, 7)
+ self.set_end_date(2013, 10, 11)
+
+ self.add_equity("SPY", Resolution.MINUTE)
+
+ # BNBUSD is not a coinbase pair: the add must fail naming the markets that do have it
+ self.assert_throws(lambda: self.add_crypto("BNBUSD", market=Market.COINBASE),
+ ["Crypto 'BNBUSD' symbol could not be found in the database", "Markets with a 'BNBUSD' Crypto entry:", Market.KRAKEN])
+
+ # oanda has no crypto entries at all: the exchange hours failure must also name the valid markets
+ self.assert_throws(lambda: self.add_crypto("BTCUSD", market=Market.OANDA),
+ ["Unable to locate exchange hours for Crypto-oanda-BTCUSD", "Markets with a 'BTCUSD' Crypto entry:", Market.COINBASE])
+
+ # exchange hours lookup must not require a subscription
+ hours = self.market_hours("IBM")
+ if str(hours.time_zone) != "America/New_York":
+ raise AssertionError(f"Unexpected time zone for IBM market hours: {hours.time_zone}")
+
+ crypto_hours = self.market_hours(Symbol.create("BTCUSD", SecurityType.CRYPTO, Market.COINBASE))
+ if not crypto_hours.is_market_always_open:
+ raise AssertionError("Expected coinbase BTCUSD market to be always open")
+
+ # and the lookups must not have added any securities
+ if any(symbol.value in ("IBM", "BTCUSD", "BNBUSD") for symbol in self.securities.keys()):
+ raise AssertionError("No security should have been added by the market hours lookups or the failed adds")
+
+ def assert_throws(self, add_security, expected_message_parts):
+ try:
+ add_security()
+ except Exception as exception:
+ message = str(exception)
+ for expected_message_part in expected_message_parts:
+ if expected_message_part not in message:
+ raise AssertionError(f"Expected message to contain '{expected_message_part}' but was: {message}")
+ return
+
+ raise AssertionError("Expected an exception to be thrown at add time")
diff --git a/Algorithm/QCAlgorithm.cs b/Algorithm/QCAlgorithm.cs
index daf195a7a1e1..cde137b064b6 100644
--- a/Algorithm/QCAlgorithm.cs
+++ b/Algorithm/QCAlgorithm.cs
@@ -3009,6 +3009,37 @@ public string Ticker(Symbol symbol)
return SecurityIdentifier.Ticker(symbol, Time);
}
+ ///
+ /// Gets the exchange hours of the market the given symbol trades in, from the market hours database.
+ /// The security is not required to have been added to the algorithm
+ ///
+ /// The symbol to get the exchange hours for
+ /// The exchange hours of the market the given symbol trades in
+ [DocumentationAttribute(SecuritiesAndPortfolio)]
+ [DocumentationAttribute(HandlingData)]
+ public SecurityExchangeHours MarketHours(Symbol symbol)
+ {
+ return MarketHoursDatabase.GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType);
+ }
+
+ ///
+ /// Gets the exchange hours of the market the given ticker trades in, from the market hours database.
+ /// The security is not required to have been added to the algorithm
+ ///
+ /// The ticker to get the exchange hours for. If it has not been added to the algorithm,
+ /// it is assumed to be an equity ticker in the default equity market
+ /// The exchange hours of the market the given ticker trades in
+ [DocumentationAttribute(SecuritiesAndPortfolio)]
+ [DocumentationAttribute(HandlingData)]
+ public SecurityExchangeHours MarketHours(string ticker)
+ {
+ if (!SymbolCache.TryGetSymbol(ticker, out var symbol))
+ {
+ symbol = QuantConnect.Symbol.Create(ticker, SecurityType.Equity, GetMarket(null, ticker, SecurityType.Equity));
+ }
+ return MarketHours(symbol);
+ }
+
///
/// Creates and adds a new to the algorithm
///
diff --git a/Common/Data/Auxiliary/LocalZipMapFileProvider.cs b/Common/Data/Auxiliary/LocalZipMapFileProvider.cs
index e90789432a32..98dad46bae28 100644
--- a/Common/Data/Auxiliary/LocalZipMapFileProvider.cs
+++ b/Common/Data/Auxiliary/LocalZipMapFileProvider.cs
@@ -142,7 +142,10 @@ private MapFileResolver GetMapFileResolver(AuxiliaryDataKey auxiliaryDataKey)
return result;
}
- throw new InvalidOperationException($"LocalZipMapFileProvider couldn't find any map files going all the way back to {endDate.ToShortDateString()} for {market}");
+ // surface the actual cause instead of provider internals: this means there is no mapping data for the market at all
+ throw new InvalidOperationException($"LocalZipMapFileProvider couldn't find any map files going all the way back to {endDate.ToShortDateString()} for {market}. " +
+ $"Map file zips are expected at '{MapFileZipHelper.GetMapFileZipFileName(market, yesterdayNewYork, auxiliaryDataKey.SecurityType)}'. " +
+ $"This usually means there is no {auxiliaryDataKey.SecurityType} data available for the '{market}' market in the data folder.");
}
}
}
diff --git a/Common/Messages/Messages.Brokerages.cs b/Common/Messages/Messages.Brokerages.cs
index fbdb8ee51da0..d23c688c9aba 100644
--- a/Common/Messages/Messages.Brokerages.cs
+++ b/Common/Messages/Messages.Brokerages.cs
@@ -245,7 +245,7 @@ public static class BinanceUSBrokerageModel
///
/// String message saying: The Binance.US brokerage does not currently support Margin trading
///
- public static string UnsupportedAccountType = "The Binance.US brokerage does not currently support Margin trading.";
+ public static string UnsupportedAccountType = "The Binance.US brokerage does not currently support Margin trading. Only AccountType.Cash is supported.";
}
///
@@ -433,7 +433,7 @@ public static class CoinbaseBrokerageModel
///
/// String message saying: The Coinbase brokerage does not currently support Margin trading
///
- public static string UnsupportedAccountType = "The Coinbase brokerage does not currently support Margin trading.";
+ public static string UnsupportedAccountType = "The Coinbase brokerage does not currently support Margin trading. Only AccountType.Cash is supported.";
///
/// Returns a string message saying the Stop Market orders are no longer supported since the given end date
diff --git a/Common/Messages/Messages.Securities.cs b/Common/Messages/Messages.Securities.cs
index 2e844357aad6..6718a66a982c 100644
--- a/Common/Messages/Messages.Securities.cs
+++ b/Common/Messages/Messages.Securities.cs
@@ -652,6 +652,24 @@ public static string SuggestedMarketBasedOnTicker(string market)
{
return $"Suggested market based on the provided ticker 'Market.{market.ToUpperInvariant()}'.";
}
+
+ ///
+ /// Returns a string message listing the markets that do have an entry for the given ticker and security type
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string MarketsWithTickerEntry(string ticker, SecurityType securityType, IEnumerable markets)
+ {
+ return $"Markets with a '{ticker}' {securityType} entry: {string.Join(", ", markets)}.";
+ }
+
+ ///
+ /// Returns a string message listing the markets that have entries for the given security type
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string MarketsWithSecurityTypeEntries(SecurityType securityType, IEnumerable markets)
+ {
+ return $"Markets with {securityType} entries: {string.Join(", ", markets)}.";
+ }
}
///
@@ -957,11 +975,17 @@ public static class SecurityService
{
///
/// Returns a string message saying the given Symbol could not be found in the Symbol Properties Database
+ /// for the requested market, naming the markets that do have an entry for it if any
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static string SymbolNotFoundInSymbolPropertiesDatabase(QuantConnect.Symbol symbol)
+ public static string SymbolNotFoundInSymbolPropertiesDatabase(QuantConnect.Symbol symbol, IReadOnlyCollection availableMarkets = null)
{
- return $"{symbol.SecurityType} '{symbol.Value}' symbol could not be found in the database for {symbol.ID.Market} market";
+ var message = $"{symbol.SecurityType} '{symbol.Value}' symbol could not be found in the database for {symbol.ID.Market} market.";
+ if (availableMarkets?.Count > 0)
+ {
+ message += $" {MarketHoursDatabase.MarketsWithTickerEntry(symbol.Value, symbol.SecurityType, availableMarkets)}";
+ }
+ return message;
}
}
diff --git a/Common/Securities/MarketHoursDatabase.cs b/Common/Securities/MarketHoursDatabase.cs
index e3f7c9e959c8..6e818119c19a 100644
--- a/Common/Securities/MarketHoursDatabase.cs
+++ b/Common/Securities/MarketHoursDatabase.cs
@@ -212,8 +212,34 @@ public virtual Entry GetEntry(string market, string symbol, SecurityType securit
throw new ArgumentException(exception);
}
- // there was nothing that really matched exactly
- throw new ArgumentException(Messages.MarketHoursDatabase.ExchangeHoursNotFound(key));
+
+ // There was nothing that really matched exactly: fail fast naming what was requested and what does exist,
+ // e.g. adding 'BTCUSD' crypto for the 'oanda' market will name the markets that do have a 'BTCUSD' crypto entry,
+ // instead of only surfacing the internal database key
+ var message = Messages.MarketHoursDatabase.ExchangeHoursNotFound(key);
+ var marketsWithTicker = !string.IsNullOrEmpty(symbol)
+ ? SymbolPropertiesDatabase.FromDataFolder().GetMarketsForSymbol(symbol, securityType)
+ : new List();
+ if (marketsWithTicker.Count > 0)
+ {
+ message += $" {Messages.MarketHoursDatabase.MarketsWithTickerEntry(symbol, securityType, marketsWithTicker)}";
+ }
+ else
+ {
+ // the ticker isn't in the symbol properties database for any market, so name the markets
+ // that have exchange hours entries for the requested security type instead
+ var marketsWithSecurityType = Entries.Keys
+ .Where(entryKey => entryKey.SecurityType == securityType && entryKey.Market != key.Market)
+ .Select(entryKey => entryKey.Market)
+ .Distinct()
+ .OrderBy(entryMarket => entryMarket)
+ .ToList();
+ if (marketsWithSecurityType.Count > 0)
+ {
+ message += $" {Messages.MarketHoursDatabase.MarketsWithSecurityTypeEntries(securityType, marketsWithSecurityType)}";
+ }
+ }
+ throw new ArgumentException(message);
}
return entry;
diff --git a/Common/Securities/SecurityService.cs b/Common/Securities/SecurityService.cs
index c6d820f895f5..b5896a85c51f 100644
--- a/Common/Securities/SecurityService.cs
+++ b/Common/Securities/SecurityService.cs
@@ -117,7 +117,10 @@ private Security CreateSecurity(Symbol symbol,
if (symbol.ID.SecurityType == SecurityType.Crypto && !_symbolPropertiesDatabase.ContainsKey(symbol.ID.Market, symbol, symbol.ID.SecurityType))
{
- throw new ArgumentException(Messages.SecurityService.SymbolNotFoundInSymbolPropertiesDatabase(symbol));
+ // fail fast at add time naming the requested ticker/market and the markets that do have the ticker,
+ // so an invalid combination like 'BNBUSD'/coinbase immediately points to the valid markets
+ throw new ArgumentException(Messages.SecurityService.SymbolNotFoundInSymbolPropertiesDatabase(symbol,
+ _symbolPropertiesDatabase.GetMarketsForSymbol(MarketHoursDatabase.GetDatabaseSymbolKey(symbol), symbol.ID.SecurityType)));
}
// For Futures Options that don't have a SPDB entry, the futures entry will be used instead.
@@ -155,7 +158,13 @@ private Security CreateSecurity(Symbol symbol,
}
else if (CurrencyPairUtil.IsValidSecurityType(symbol.SecurityType, false))
{
- throw new ArgumentException($"Failed to resolve base currency for '{symbol.ID.Symbol}', it might be missing from the Symbol database or market '{symbol.ID.Market}' could be wrong");
+ var message = $"Failed to resolve base currency for '{symbol.ID.Symbol}', it might be missing from the Symbol database or market '{symbol.ID.Market}' could be wrong.";
+ var availableMarkets = _symbolPropertiesDatabase.GetMarketsForSymbol(symbol.ID.Symbol, symbol.SecurityType);
+ if (availableMarkets.Count > 0)
+ {
+ message += $" {Messages.MarketHoursDatabase.MarketsWithTickerEntry(symbol.ID.Symbol, symbol.SecurityType, availableMarkets)}";
+ }
+ throw new ArgumentException(message);
}
}
diff --git a/Common/Securities/SymbolPropertiesDatabase.cs b/Common/Securities/SymbolPropertiesDatabase.cs
index 3af6c9dc2ab4..1aadf999bab3 100644
--- a/Common/Securities/SymbolPropertiesDatabase.cs
+++ b/Common/Securities/SymbolPropertiesDatabase.cs
@@ -14,6 +14,7 @@
*/
using QuantConnect.Util;
+using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
@@ -79,6 +80,23 @@ public bool TryGetMarket(string symbol, SecurityType securityType, out string ma
return false;
}
+ ///
+ /// Gets all markets that have an entry for the provided symbol/security type.
+ /// Useful to suggest valid markets when a lookup fails for a given market.
+ ///
+ /// The particular symbol being traded
+ /// The security type of the symbol
+ /// The markets that have an entry for the given symbol and security type, sorted alphabetically
+ public List GetMarketsForSymbol(string symbol, SecurityType securityType)
+ {
+ return Entries.Keys
+ .Where(key => key.SecurityType == securityType && string.Equals(key.Symbol, symbol, StringComparison.InvariantCultureIgnoreCase))
+ .Select(key => key.Market)
+ .Distinct()
+ .OrderBy(market => market)
+ .ToList();
+ }
+
///
/// Gets the symbol properties for the specified market/symbol/security-type
///
diff --git a/Tests/Algorithm/AlgorithmAddSecurityTests.cs b/Tests/Algorithm/AlgorithmAddSecurityTests.cs
index 28cedbefcfc8..8d5c7d5e35cb 100644
--- a/Tests/Algorithm/AlgorithmAddSecurityTests.cs
+++ b/Tests/Algorithm/AlgorithmAddSecurityTests.cs
@@ -258,6 +258,48 @@ public void DoesNotAddExtraIndexSubscriptionAfterAddingIndexOptionContract()
Assert.AreEqual(1, _algo.SubscriptionManager.Subscriptions.Count(x => x.Symbol == spx.Symbol));
}
+ [Test]
+ public void AddCryptoWithWrongMarketFailsFastNamingMarketsThatHaveTheTicker()
+ {
+ // BNBUSD is not a coinbase pair, but it does exist in other markets
+ var exception = Assert.Throws(() => _algo.AddCrypto("BNBUSD", market: Market.Coinbase));
+ StringAssert.Contains("Crypto 'BNBUSD' symbol could not be found in the database", exception.Message);
+ StringAssert.Contains($"Markets with a 'BNBUSD' {SecurityType.Crypto} entry:", exception.Message);
+ StringAssert.Contains(Market.Kraken, exception.Message);
+
+ // oanda has no crypto entries at all: the exchange hours failure names the markets that do have the ticker
+ exception = Assert.Throws(() => _algo.AddCrypto("BTCUSD", market: Market.Oanda));
+ StringAssert.Contains("Unable to locate exchange hours for Crypto-oanda-BTCUSD", exception.Message);
+ StringAssert.Contains($"Markets with a 'BTCUSD' {SecurityType.Crypto} entry:", exception.Message);
+ StringAssert.Contains(Market.Coinbase, exception.Message);
+ }
+
+ [Test]
+ public void MarketHoursDoesNotRequireASubscription()
+ {
+ // not added to the algorithm: a plain ticker is assumed to be an equity in the default market
+ var hours = _algo.MarketHours("IBM");
+ Assert.AreEqual(TimeZones.NewYork, hours.TimeZone);
+ Assert.IsFalse(_algo.Securities.Keys.Any(symbol => symbol.Value == "IBM"));
+
+ // symbol overload, not added either
+ var cryptoHours = _algo.MarketHours(Symbol.Create("BTCUSD", SecurityType.Crypto, Market.Coinbase));
+ Assert.IsTrue(cryptoHours.IsMarketAlwaysOpen);
+ Assert.IsFalse(_algo.Securities.Keys.Any(symbol => symbol.Value == "BTCUSD"));
+ }
+
+ [Test]
+ public void MarketHoursTickerOverloadResolvesThroughTheSymbolCache()
+ {
+ // once added, the ticker resolves to the added symbol instead of defaulting to equity
+ var crypto = _algo.AddCrypto("BTCUSD", market: Market.Coinbase);
+
+ var hours = _algo.MarketHours("BTCUSD");
+
+ Assert.AreEqual(crypto.Exchange.Hours.TimeZone, hours.TimeZone);
+ Assert.IsTrue(hours.IsMarketAlwaysOpen);
+ }
+
[TestCase("SPXW", "SPX")]
[TestCase("RUTW", "RUT")]
[TestCase("VIXW", "VIX")]
diff --git a/Tests/Common/Brokerages/CoinbaseBrokerageModelTests.cs b/Tests/Common/Brokerages/CoinbaseBrokerageModelTests.cs
index b3625809a9ac..a59836a49502 100644
--- a/Tests/Common/Brokerages/CoinbaseBrokerageModelTests.cs
+++ b/Tests/Common/Brokerages/CoinbaseBrokerageModelTests.cs
@@ -274,10 +274,14 @@ public void FeeModelReturnsCorrectOrderFeeForTakerLimitOrdersMinuteResolution()
[Test]
public void ThrowsWhenCalledWithMarginAccountType()
{
- Assert.Throws(() =>
+ var exception = Assert.Throws(() =>
{
new CoinbaseBrokerageModel(AccountType.Margin);
- }, "The Coinbase brokerage does not currently support Margin trading.");
+ });
+
+ // the error must name the supported configuration
+ StringAssert.Contains("The Coinbase brokerage does not currently support Margin trading.", exception.Message);
+ StringAssert.Contains($"Only AccountType.{AccountType.Cash} is supported.", exception.Message);
}
}
}
diff --git a/Tests/Common/Securities/MarketHoursDatabaseTests.cs b/Tests/Common/Securities/MarketHoursDatabaseTests.cs
index 74e94c204688..1c575c27ce6b 100644
--- a/Tests/Common/Securities/MarketHoursDatabaseTests.cs
+++ b/Tests/Common/Securities/MarketHoursDatabaseTests.cs
@@ -92,6 +92,34 @@ public void InitializesFromDataFolder()
Assert.AreNotEqual(0, provider.ExchangeHoursListing.Count);
}
+ [Test]
+ public void GetEntryFailureNamesMarketsWithAnEntryForTheTicker()
+ {
+ var dataBase = MarketHoursDatabase.FromDataFolder();
+
+ var exception = Assert.Throws(() => dataBase.GetEntry(Market.Oanda, "BTCUSD", SecurityType.Crypto));
+
+ StringAssert.Contains("Unable to locate exchange hours for Crypto-oanda-BTCUSD", exception.Message);
+ StringAssert.Contains($"Markets with a 'BTCUSD' {SecurityType.Crypto} entry:", exception.Message);
+ StringAssert.Contains(Market.Coinbase, exception.Message);
+ StringAssert.Contains(Market.Kraken, exception.Message);
+ }
+
+ [Test]
+ public void GetEntryFailureNamesMarketsWithEntriesForTheSecurityType()
+ {
+ var dataBase = MarketHoursDatabase.FromDataFolder();
+
+ // SPY is not in the symbol properties database for any market (equity entries are market-wide),
+ // so the message falls back to naming the markets that have equity exchange hours entries
+ var exception = Assert.Throws(() => dataBase.GetEntry("non-existing-market", "SPY", SecurityType.Equity));
+
+ StringAssert.Contains("Unable to locate exchange hours for Equity-non-existing-market-SPY", exception.Message);
+ StringAssert.Contains($"Markets with {SecurityType.Equity} entries:", exception.Message);
+ StringAssert.Contains(Market.USA, exception.Message);
+ StringAssert.Contains(Market.India, exception.Message);
+ }
+
[Test]
public void CorrectlyReadsUsEquityMarketHours()
{
diff --git a/Tests/Common/Securities/SecurityServiceTests.cs b/Tests/Common/Securities/SecurityServiceTests.cs
index 94625b7ac20b..94b45ffe814a 100644
--- a/Tests/Common/Securities/SecurityServiceTests.cs
+++ b/Tests/Common/Securities/SecurityServiceTests.cs
@@ -132,6 +132,20 @@ public void ThrowOnCreateCryptoNotDescribedInCSV()
}, "Symbol can't be found in the Symbol Properties Database");
}
+ [Test]
+ public void ThrowOnCreateCryptoNotDescribedInCsvNamingMarketsThatHaveTheTicker()
+ {
+ // BNBUSD is not a coinbase crypto pair, but other markets do have it
+ var symbol = Symbol.Create("BNBUSD", SecurityType.Crypto, Market.Coinbase);
+ var configs = _subscriptionManager.SubscriptionDataConfigService.Add(typeof(QuoteBar), symbol, Resolution.Minute, false, false, false);
+
+ var exception = Assert.Throws(() => _securityService.CreateSecurity(symbol, configs, 1.0m, false));
+
+ StringAssert.Contains($"Crypto 'BNBUSD' symbol could not be found in the database for {Market.Coinbase} market", exception.Message);
+ StringAssert.Contains($"Markets with a 'BNBUSD' {SecurityType.Crypto} entry:", exception.Message);
+ StringAssert.Contains(Market.Kraken, exception.Message);
+ }
+
[Test]
public void CanCreate_ConcreteOptions_WithCorrectSubscriptions()
{
diff --git a/Tests/Common/Securities/SymbolPropertiesDatabaseTests.cs b/Tests/Common/Securities/SymbolPropertiesDatabaseTests.cs
index 9efee18eb883..3024bff79ede 100644
--- a/Tests/Common/Securities/SymbolPropertiesDatabaseTests.cs
+++ b/Tests/Common/Securities/SymbolPropertiesDatabaseTests.cs
@@ -211,6 +211,28 @@ public void GetSymbolPropertiesListIsNotEmpty(string market, SecurityType securi
Assert.IsNotEmpty(spList);
}
+ [Test]
+ public void GetsMarketsForSymbol()
+ {
+ var db = SymbolPropertiesDatabase.FromDataFolder();
+
+ var markets = db.GetMarketsForSymbol("BTCUSD", SecurityType.Crypto);
+
+ CollectionAssert.Contains(markets, Market.Coinbase);
+ CollectionAssert.Contains(markets, Market.Bitfinex);
+ CollectionAssert.Contains(markets, Market.Kraken);
+ CollectionAssert.DoesNotContain(markets, Market.Oanda);
+ CollectionAssert.AreEqual(markets.OrderBy(market => market).ToList(), markets);
+
+ // markets are per security type
+ var cryptoFutureMarkets = db.GetMarketsForSymbol("BTCUSD", SecurityType.CryptoFuture);
+ CollectionAssert.Contains(cryptoFutureMarkets, Market.Binance);
+ CollectionAssert.DoesNotContain(cryptoFutureMarkets, Market.Coinbase);
+
+ // unknown ticker yields no markets
+ Assert.IsEmpty(db.GetMarketsForSymbol("NOTATICKER", SecurityType.Crypto));
+ }
+
[TestCase(Market.USA, SecurityType.Equity)]
[TestCase(Market.USA, SecurityType.Option)]
public void GetSymbolPropertiesListHasOneRow(string market, SecurityType securityType)