From 2e2ca1cd1279fcac75212267f0c4db3114938938 Mon Sep 17 00:00:00 2001 From: Martin-Molinero Date: Thu, 25 Jun 2026 23:07:59 +0000 Subject: [PATCH 1/3] Seed runtime-added currency conversion rates immediately Fixes the spurious 'The conversion rate for is not available' runtime error caused by a two-path seeding asymmetry. The setup path (BaseSetupHandler.SetupCurrencyConversions) wires up a currency's conversion feed AND seeds its rate via history/last-known-price so the rate is non-zero right away. The runtime path (UniverseSelection.EnsureCurrencyDataFeeds, invoked during universe selection / SetCash mid-run) only created the conversion subscription and left the rate at 0 until the first bar of the pair arrived. Any conversion in that gap (classically a midnight scheduled SetHoldings firing before the day's first conversion-pair bar) threw. EnsureCurrencyDataFeeds now seeds newly introduced, still-zero-rate conversion securities and calls cash.Update(), mirroring the setup path. Seeding is gated behind a seedNewCurrencies flag (default true) so the setup caller, which performs its own optionally white-listed seeding, can opt out and not regress white-list semantics. SeedSecurities degrades gracefully when no history/data is available, leaving the rate at 0 as before, so live mode and no-history scenarios are safe. Adds a regression test exercising the runtime path. Co-Authored-By: Claude Opus 4.8 (1M context) --- Engine/DataFeeds/UniverseSelection.cs | 42 +++++++++++++++- Engine/Setup/BaseSetupHandler.cs | 6 ++- Tests/Engine/Setup/BaseSetupHandlerTests.cs | 55 +++++++++++++++++++++ 3 files changed, 100 insertions(+), 3 deletions(-) diff --git a/Engine/DataFeeds/UniverseSelection.cs b/Engine/DataFeeds/UniverseSelection.cs index 6bb15037b785..7fe83532c509 100644 --- a/Engine/DataFeeds/UniverseSelection.cs +++ b/Engine/DataFeeds/UniverseSelection.cs @@ -418,9 +418,49 @@ public bool AddPendingInternalDataFeeds(DateTime utcStart) /// /// Checks the current subscriptions and adds necessary currency pair feeds to provide real time conversion data /// - public void EnsureCurrencyDataFeeds(SecurityChanges securityChanges) + /// The security changes to consume + /// + /// When true (the default, used by the runtime universe selection path), any newly introduced currency + /// conversion securities are immediately seeded with their last known price so the conversion rate is + /// non-zero right away. passes false because it + /// performs its own (optionally white-listed) seeding right after calling this method during setup. + /// + public void EnsureCurrencyDataFeeds(SecurityChanges securityChanges, bool seedNewCurrencies = true) { _currencySubscriptionDataConfigManager.EnsureCurrencySubscriptionDataConfigs(securityChanges, _algorithm.BrokerageModel); + + if (!seedNewCurrencies) + { + return; + } + + // Seed any newly introduced currency conversion securities so they have a non-zero conversion rate + // immediately, instead of waiting for the first bar of the conversion pair to arrive. Otherwise any + // conversion requested in that gap (e.g. a scheduled order before the day's first conversion bar) would + // throw 'The conversion rate for is not available'. This mirrors what + // BaseSetupHandler.SetupCurrencyConversions does during setup, but for cashes added/required at runtime. + // We only target cashes that still have a zero conversion rate, so already seeded currencies aren't + // re-seeded. SeedSecurities degrades gracefully (no history/data leaves the rate at 0, same as today). + var cashToUpdate = _algorithm.Portfolio.CashBook.Values + .Where(cash => cash.CurrencyConversion != null && cash.ConversionRate == 0) + .ToList(); + + if (cashToUpdate.Count == 0) + { + return; + } + + var securitiesToUpdate = cashToUpdate + .SelectMany(cash => cash.CurrencyConversion.ConversionRateSecurities) + .Distinct() + .ToList(); + + AlgorithmUtils.SeedSecurities(securitiesToUpdate, _algorithm); + + foreach (var cash in cashToUpdate) + { + cash.Update(); + } } /// diff --git a/Engine/Setup/BaseSetupHandler.cs b/Engine/Setup/BaseSetupHandler.cs index 44998da47fc9..43929e9f3020 100644 --- a/Engine/Setup/BaseSetupHandler.cs +++ b/Engine/Setup/BaseSetupHandler.cs @@ -84,8 +84,10 @@ public static void SetupCurrencyConversions( IReadOnlyCollection currenciesToUpdateWhiteList = null) { // this is needed to have non-zero currency conversion rates during warmup - // will also set the Cash.ConversionRateSecurity - universeSelection.EnsureCurrencyDataFeeds(SecurityChanges.None); + // will also set the Cash.ConversionRateSecurity. + // We disable the runtime auto-seeding here because this method does its own (optionally white-listed) + // seeding right below; letting EnsureCurrencyDataFeeds seed as well would ignore the white list. + universeSelection.EnsureCurrencyDataFeeds(SecurityChanges.None, seedNewCurrencies: false); // now set conversion rates Func cashToUpdateFilter = currenciesToUpdateWhiteList == null diff --git a/Tests/Engine/Setup/BaseSetupHandlerTests.cs b/Tests/Engine/Setup/BaseSetupHandlerTests.cs index 01787c9c983e..3070f686ccf6 100644 --- a/Tests/Engine/Setup/BaseSetupHandlerTests.cs +++ b/Tests/Engine/Setup/BaseSetupHandlerTests.cs @@ -16,6 +16,7 @@ using NUnit.Framework; using QuantConnect.Data; +using QuantConnect.Data.UniverseSelection; using QuantConnect.Util; using QuantConnect.Data.Auxiliary; using QuantConnect.Lean.Engine.Setup; @@ -110,5 +111,59 @@ public void CurrencyConversionRateResolvedForWhiteListedCurrenciesOnly() Assert.AreEqual(0, algorithm.Portfolio.CashBook["EUR"].ConversionRate); Assert.AreEqual(0, algorithm.Portfolio.CashBook["USDT"].ConversionRate); } + + [Test] + public void RuntimeCurrencyConversionRateIsSeeded() + { + // Regression test for the runtime currency-conversion seeding gap: when a currency that requires a + // conversion feed is introduced after setup (e.g. a SetCash mid-algorithm, or a universe adding a + // security whose quote currency isn't yet in the cashbook), the runtime path + // (UniverseSelection.EnsureCurrencyDataFeeds) used to only create the conversion subscription without + // seeding its price. The rate therefore stayed 0 until the first bar of the pair arrived, and any + // conversion in that window threw 'The conversion rate for is not available'. + // After the fix, the runtime path seeds the new conversion security just like BaseSetupHandler does + // during setup, so the rate is non-zero immediately. + + var historyProvider = new SubscriptionDataReaderHistoryProvider(); + + var algorithm = new BrokerageSetupHandlerTests.TestAlgorithm { UniverseSettings = { Resolution = Resolution.Minute } }; + + historyProvider.Initialize(new HistoryProviderInitializeParameters( + null, + null, + TestGlobals.DataProvider, + TestGlobals.DataCacheProvider, + TestGlobals.MapFileProvider, + TestGlobals.FactorFileProvider, + null, + false, + new DataPermissionManager(), + algorithm.ObjectStore, + algorithm.Settings)); + algorithm.SetHistoryProvider(historyProvider); + + algorithm.SetStartDate(2015, 1, 24); + algorithm.SetCash("USD", 0); + + // Run setup so the engine is in the post-setup (runtime) state. + BaseSetupHandler.SetupCurrencyConversions(algorithm, algorithm.DataManager.UniverseSelection); + + // Now introduce a new currency at runtime, after setup has already run. This mirrors a SetCash mid-run + // or a universe adding a security with a new quote currency. Before the fix the conversion rate would + // remain 0 here until the first BTCUSD bar arrived. + algorithm.SetCash("BTC", 10); + + // The runtime path that wires up the conversion feed during universe selection. + algorithm.DataManager.UniverseSelection.EnsureCurrencyDataFeeds(SecurityChanges.None); + + // The new currency should have its conversion security and a non-zero rate already, without waiting for + // a live bar - so a conversion requested right now would no longer throw. + Assert.IsNotNull(algorithm.Portfolio.CashBook["BTC"].CurrencyConversion); + Assert.AreNotEqual(0, algorithm.Portfolio.CashBook["BTC"].ConversionRate); + Assert.IsTrue(algorithm.Portfolio.CashBook["BTC"].ValueInAccountCurrency > 0); + + // Sanity: converting the runtime currency does not throw. + Assert.DoesNotThrow(() => algorithm.Portfolio.CashBook.ConvertToAccountCurrency(10, "BTC")); + } } } From 0b4d3b2351816bbea4c523d8e08c976792b4e7db Mon Sep 17 00:00:00 2001 From: Martin-Molinero Date: Fri, 26 Jun 2026 11:25:31 +0000 Subject: [PATCH 2/3] Make runtime currency seeding robust and fix regression expectation CI failures from the runtime currency-conversion seeding change: 1. AlgorithmWarmupTests.WarmUpInternalSubscriptions threw ArgumentNullException because the new EnsureCurrencyDataFeeds seeding path ran GetLastKnownPrices in a stub where the conversion security lacked SymbolProperties. Pre-seeding is best-effort and must never break the algorithm, so wrap it in try/catch and degrade gracefully (leave the rate at 0, the pre-fix behavior) - matching the documented intent. The first conversion-pair bar still updates the rate. 2. ScheduledUniverseSelectionModelRegressionAlgorithm (C# + Python) asserted AlgorithmHistoryDataPoints == 0. The algorithm runtime-adds Forex pairs (EURGBP -> GBP cash) via scheduled universe selection; the fix now correctly seeds that runtime currency's conversion rate with a last-known-price history request (deterministically 50 points). The old 0 reflected the buggy unseeded behavior, so update the expectation to 50. No other statistics changed. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...UniverseSelectionModelRegressionAlgorithm.cs | 2 +- Engine/DataFeeds/UniverseSelection.cs | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Algorithm.CSharp/ScheduledUniverseSelectionModelRegressionAlgorithm.cs b/Algorithm.CSharp/ScheduledUniverseSelectionModelRegressionAlgorithm.cs index ff30b377cd86..4794355ea9d4 100644 --- a/Algorithm.CSharp/ScheduledUniverseSelectionModelRegressionAlgorithm.cs +++ b/Algorithm.CSharp/ScheduledUniverseSelectionModelRegressionAlgorithm.cs @@ -199,7 +199,7 @@ private void ExpectRemovals(SecurityChanges changes, params string[] tickers) /// /// Data Points count of the algorithm history /// - public int AlgorithmHistoryDataPoints => 0; + public int AlgorithmHistoryDataPoints => 50; /// /// Final status of the algorithm diff --git a/Engine/DataFeeds/UniverseSelection.cs b/Engine/DataFeeds/UniverseSelection.cs index 7fe83532c509..30e6c5a3bdec 100644 --- a/Engine/DataFeeds/UniverseSelection.cs +++ b/Engine/DataFeeds/UniverseSelection.cs @@ -455,11 +455,22 @@ public void EnsureCurrencyDataFeeds(SecurityChanges securityChanges, bool seedNe .Distinct() .ToList(); - AlgorithmUtils.SeedSecurities(securitiesToUpdate, _algorithm); + // Best-effort pre-seeding: it must never break the algorithm. If the last-known-price lookup can't run + // (e.g. no history provider, no data, or a conversion security missing properties), we degrade gracefully + // and leave the rate at 0 - exactly the pre-fix behavior - so the first conversion-pair bar still updates it. + try + { + AlgorithmUtils.SeedSecurities(securitiesToUpdate, _algorithm); - foreach (var cash in cashToUpdate) + foreach (var cash in cashToUpdate) + { + cash.Update(); + } + } + catch (Exception err) { - cash.Update(); + Log.Error($"UniverseSelection.EnsureCurrencyDataFeeds(): failed to seed runtime currency conversion rate(s), " + + $"they will be set once the first conversion-pair bar arrives. {err.Message}"); } } From ded006fee0b2cef1debaa1ea85e301eaed82ab59 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 26 Jun 2026 16:57:53 -0500 Subject: [PATCH 3/3] Seed runtime added currency conversion rates --- ...ncyConversionSeedingRegressionAlgorithm.cs | 174 ++++++++++++++++++ .../CurrencySubscriptionDataConfigManager.cs | 6 +- Engine/DataFeeds/UniverseSelection.cs | 37 ++-- Engine/Setup/BaseSetupHandler.cs | 4 +- Tests/Engine/Setup/BaseSetupHandlerTests.cs | 25 +-- 5 files changed, 202 insertions(+), 44 deletions(-) create mode 100644 Algorithm.CSharp/RuntimeCurrencyConversionSeedingRegressionAlgorithm.cs diff --git a/Algorithm.CSharp/RuntimeCurrencyConversionSeedingRegressionAlgorithm.cs b/Algorithm.CSharp/RuntimeCurrencyConversionSeedingRegressionAlgorithm.cs new file mode 100644 index 000000000000..60c17b097f2b --- /dev/null +++ b/Algorithm.CSharp/RuntimeCurrencyConversionSeedingRegressionAlgorithm.cs @@ -0,0 +1,174 @@ +/* + * 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.Collections.Generic; +using System.Linq; +using QuantConnect.Brokerages; +using QuantConnect.Data; +using QuantConnect.Data.UniverseSelection; +using QuantConnect.Interfaces; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm asserting that a currency added at runtime (here BTCEUR, from a scheduled event) has its + /// conversion rate seeded right away, so using it immediately no longer throws because the rate is still 0. + /// + public class RuntimeCurrencyConversionSeedingRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private Symbol _ltcusd; + private bool _addedAtRuntime; + private bool _assertedSeeded; + + /// + /// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized. + /// + public override void Initialize() + { + SetStartDate(2018, 4, 5); + SetEndDate(2018, 4, 5); + SetBrokerageModel(BrokerageName.GDAX, AccountType.Cash); + SetCash(100000); + + // Account currency asset that funds the loop + _ltcusd = AddCrypto("LTCUSD", Resolution.Minute).Symbol; + + // Add a non-account-currency asset at runtime, mirroring users that add assets from a scheduled event + Schedule.On(DateRules.EveryDay(), TimeRules.At(10, 0), () => + { + if (_addedAtRuntime) + { + return; + } + _addedAtRuntime = true; + AddCrypto("BTCEUR", Resolution.Minute); + }); + } + + /// + /// Runs right after the runtime-added security is wired up, the earliest point it can be used + /// + public override void OnSecuritiesChanged(SecurityChanges changes) + { + if (!changes.AddedSecurities.Any(security => security.Symbol.Value == "BTCEUR")) + { + return; + } + _assertedSeeded = true; + + // With the fix these are already seeded here. Without it they would still be 0 and the conversion below would throw. + var eur = Portfolio.CashBook["EUR"]; + var btc = Portfolio.CashBook["BTC"]; + if (eur.ConversionRate == 0 || btc.ConversionRate == 0) + { + throw new RegressionTestException( + $"Runtime-added currency conversion rates were not seeded (EUR={eur.ConversionRate}, BTC={btc.ConversionRate})"); + } + + if (Portfolio.CashBook.ConvertToAccountCurrency(100m, "EUR") <= 0) + { + throw new RegressionTestException("Expected a positive EUR -> account currency conversion"); + } + } + + /// + /// OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here. + /// + /// Slice object keyed by symbol containing the stock data + public override void OnData(Slice slice) + { + if (!_addedAtRuntime || Portfolio.Invested) + { + return; + } + + if (Securities[_ltcusd].Price != 0) + { + SetHoldings(_ltcusd, 0.5); + } + } + + /// + /// Makes sure the seeding path was actually exercised so the test can't silently pass + /// + public override void OnEndOfAlgorithm() + { + if (!_assertedSeeded) + { + throw new RegressionTestException("BTCEUR was never added at runtime, the seeding path was not exercised"); + } + } + + /// + /// 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 }; + + /// + /// Data Points count of all timeslices of algorithm + /// + public long DataPoints => 6005; + + /// + /// Data Points count of the algorithm history + /// + public int AlgorithmHistoryDataPoints => 591; + + /// + /// 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", "1"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0%"}, + {"Drawdown", "0%"}, + {"Expectancy", "0"}, + {"Start Equity", "100000.00"}, + {"End Equity", "99064.52"}, + {"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", "0"}, + {"Tracking Error", "0"}, + {"Treynor Ratio", "0"}, + {"Total Fees", "$149.18"}, + {"Estimated Strategy Capacity", "$160000.00"}, + {"Lowest Capacity Asset", "LTCUSD 2XR"}, + {"Portfolio Turnover", "50.20%"}, + {"Drawdown Recovery", "0"}, + {"OrderListHash", "69d27a394cffbd938ec23fbb451f37ae"} + }; + } +} diff --git a/Engine/DataFeeds/CurrencySubscriptionDataConfigManager.cs b/Engine/DataFeeds/CurrencySubscriptionDataConfigManager.cs index a67eae1bd00b..c72c12e7845d 100644 --- a/Engine/DataFeeds/CurrencySubscriptionDataConfigManager.cs +++ b/Engine/DataFeeds/CurrencySubscriptionDataConfigManager.cs @@ -131,7 +131,9 @@ public IEnumerable GetPendingSubscriptionDataConfigs() /// /// Checks the current and adds new necessary currency pair feeds to provide real time conversion data /// - public void EnsureCurrencySubscriptionDataConfigs(SecurityChanges securityChanges, IBrokerageModel brokerageModel) + /// True if new currency conversion feeds were introduced, false otherwise. Lets callers skip + /// follow up work like seeding the new conversion rates when nothing was added + public bool EnsureCurrencySubscriptionDataConfigs(SecurityChanges securityChanges, IBrokerageModel brokerageModel) { _ensureCurrencyDataFeeds = false; // remove any 'to be added' if the security has already been added @@ -150,6 +152,8 @@ public void EnsureCurrencySubscriptionDataConfigs(SecurityChanges securityChange _toBeAddedCurrencySubscriptionDataConfigs.Add(config); } _pendingSubscriptionDataConfigs = _toBeAddedCurrencySubscriptionDataConfigs.Any(); + + return newConfigs.Count > 0; } } } diff --git a/Engine/DataFeeds/UniverseSelection.cs b/Engine/DataFeeds/UniverseSelection.cs index 30e6c5a3bdec..6afee2ba7eb4 100644 --- a/Engine/DataFeeds/UniverseSelection.cs +++ b/Engine/DataFeeds/UniverseSelection.cs @@ -128,7 +128,7 @@ public SecurityChanges ApplyUniverseSelection(Universe universe, DateTime dateTi // if the input is already fundamental data we just need to filter it and pass it through var hasFundamentalData = universeData.Data.Count > 0 && universeData.Data[0] is Fundamental; - if(hasFundamentalData) + if (hasFundamentalData) { // Remove selected symbols that does not have fine fundamental data var anyDoesNotHaveFundamentalData = false; @@ -137,7 +137,8 @@ public SecurityChanges ApplyUniverseSelection(Universe universe, DateTime dateTi // which do not use coarse data as underlying, in which case it could happen that we try to load fine fundamental data that is missing, but no problem, // 'FineFundamentalSubscriptionEnumeratorFactory' won't emit it var set = selectSymbolsResult.ToHashSet(); - fineCollection.Data.AddRange(universeData.Data.OfType().Where(fundamental => { + fineCollection.Data.AddRange(universeData.Data.OfType().Where(fundamental => + { // we remove to we distict by symbol if (set.Remove(fundamental.Symbol)) { @@ -360,7 +361,7 @@ public bool AddPendingInternalDataFeeds(DateTime utcStart) resolution = supportedResolutions.OrderByDescending(x => x).First(); } - var subscriptionList = new List>() {subscriptionType}; + var subscriptionList = new List>() { subscriptionType }; var dataConfig = _algorithm.SubscriptionManager.SubscriptionDataConfigService.Add( securityBenchmark.Security.Symbol, resolution, @@ -419,28 +420,21 @@ public bool AddPendingInternalDataFeeds(DateTime utcStart) /// Checks the current subscriptions and adds necessary currency pair feeds to provide real time conversion data /// /// The security changes to consume - /// - /// When true (the default, used by the runtime universe selection path), any newly introduced currency - /// conversion securities are immediately seeded with their last known price so the conversion rate is - /// non-zero right away. passes false because it - /// performs its own (optionally white-listed) seeding right after calling this method during setup. - /// + /// Whether to seed the conversion rate of newly added currencies with their last + /// known price. The setup handler passes false because it performs its own (optionally white-listed) seeding public void EnsureCurrencyDataFeeds(SecurityChanges securityChanges, bool seedNewCurrencies = true) { - _currencySubscriptionDataConfigManager.EnsureCurrencySubscriptionDataConfigs(securityChanges, _algorithm.BrokerageModel); + var newCurrencyFeedsAdded = _currencySubscriptionDataConfigManager.EnsureCurrencySubscriptionDataConfigs(securityChanges, _algorithm.BrokerageModel); - if (!seedNewCurrencies) + // Only scan the cashbook and seed when a new conversion feed was actually introduced + if (!seedNewCurrencies || !newCurrencyFeedsAdded) { return; } - // Seed any newly introduced currency conversion securities so they have a non-zero conversion rate - // immediately, instead of waiting for the first bar of the conversion pair to arrive. Otherwise any - // conversion requested in that gap (e.g. a scheduled order before the day's first conversion bar) would - // throw 'The conversion rate for is not available'. This mirrors what - // BaseSetupHandler.SetupCurrencyConversions does during setup, but for cashes added/required at runtime. - // We only target cashes that still have a zero conversion rate, so already seeded currencies aren't - // re-seeded. SeedSecurities degrades gracefully (no history/data leaves the rate at 0, same as today). + // Seed the new conversion rates with their last known price so they are non-zero right away, instead of + // waiting for the first conversion pair bar to arrive. Otherwise a conversion needed in that gap would + // throw. This is the same thing BaseSetupHandler does during setup, but for cashes added at runtime. var cashToUpdate = _algorithm.Portfolio.CashBook.Values .Where(cash => cash.CurrencyConversion != null && cash.ConversionRate == 0) .ToList(); @@ -455,9 +449,6 @@ public void EnsureCurrencyDataFeeds(SecurityChanges securityChanges, bool seedNe .Distinct() .ToList(); - // Best-effort pre-seeding: it must never break the algorithm. If the last-known-price lookup can't run - // (e.g. no history provider, no data, or a conversion security missing properties), we degrade gracefully - // and leave the rate at 0 - exactly the pre-fix behavior - so the first conversion-pair bar still updates it. try { AlgorithmUtils.SeedSecurities(securitiesToUpdate, _algorithm); @@ -469,8 +460,8 @@ public void EnsureCurrencyDataFeeds(SecurityChanges securityChanges, bool seedNe } catch (Exception err) { - Log.Error($"UniverseSelection.EnsureCurrencyDataFeeds(): failed to seed runtime currency conversion rate(s), " + - $"they will be set once the first conversion-pair bar arrives. {err.Message}"); + // Seeding must never break the algorithm, the rate will be set on the first conversion pair bar + Log.Error($"UniverseSelection.EnsureCurrencyDataFeeds(): failed to seed runtime currency conversion rate(s): {err.Message}"); } } diff --git a/Engine/Setup/BaseSetupHandler.cs b/Engine/Setup/BaseSetupHandler.cs index 43929e9f3020..3d9c763a50f6 100644 --- a/Engine/Setup/BaseSetupHandler.cs +++ b/Engine/Setup/BaseSetupHandler.cs @@ -85,8 +85,8 @@ public static void SetupCurrencyConversions( { // this is needed to have non-zero currency conversion rates during warmup // will also set the Cash.ConversionRateSecurity. - // We disable the runtime auto-seeding here because this method does its own (optionally white-listed) - // seeding right below; letting EnsureCurrencyDataFeeds seed as well would ignore the white list. + // We don't let it seed the conversion rates here because we do that right below, + // where we can also limit the seeding to a specific white list of currencies universeSelection.EnsureCurrencyDataFeeds(SecurityChanges.None, seedNewCurrencies: false); // now set conversion rates diff --git a/Tests/Engine/Setup/BaseSetupHandlerTests.cs b/Tests/Engine/Setup/BaseSetupHandlerTests.cs index 3070f686ccf6..c2a032b6b293 100644 --- a/Tests/Engine/Setup/BaseSetupHandlerTests.cs +++ b/Tests/Engine/Setup/BaseSetupHandlerTests.cs @@ -115,14 +115,10 @@ public void CurrencyConversionRateResolvedForWhiteListedCurrenciesOnly() [Test] public void RuntimeCurrencyConversionRateIsSeeded() { - // Regression test for the runtime currency-conversion seeding gap: when a currency that requires a - // conversion feed is introduced after setup (e.g. a SetCash mid-algorithm, or a universe adding a - // security whose quote currency isn't yet in the cashbook), the runtime path - // (UniverseSelection.EnsureCurrencyDataFeeds) used to only create the conversion subscription without - // seeding its price. The rate therefore stayed 0 until the first bar of the pair arrived, and any - // conversion in that window threw 'The conversion rate for is not available'. - // After the fix, the runtime path seeds the new conversion security just like BaseSetupHandler does - // during setup, so the rate is non-zero immediately. + // When a currency requiring a conversion feed is introduced at runtime (a universe adding a security + // whose quote currency isn't in the cashbook yet), the runtime path used to wire up the conversion + // subscription without seeding its price, leaving the rate at 0 until the first pair bar. After the fix + // it seeds the new conversion security right away, just like BaseSetupHandler does during setup. var historyProvider = new SubscriptionDataReaderHistoryProvider(); @@ -145,24 +141,17 @@ public void RuntimeCurrencyConversionRateIsSeeded() algorithm.SetStartDate(2015, 1, 24); algorithm.SetCash("USD", 0); - // Run setup so the engine is in the post-setup (runtime) state. + // Run setup so the engine is in the post-setup (runtime) state BaseSetupHandler.SetupCurrencyConversions(algorithm, algorithm.DataManager.UniverseSelection); - // Now introduce a new currency at runtime, after setup has already run. This mirrors a SetCash mid-run - // or a universe adding a security with a new quote currency. Before the fix the conversion rate would - // remain 0 here until the first BTCUSD bar arrived. + // Introduce a new currency at runtime and drive the runtime path that wires up its conversion feed algorithm.SetCash("BTC", 10); - - // The runtime path that wires up the conversion feed during universe selection. algorithm.DataManager.UniverseSelection.EnsureCurrencyDataFeeds(SecurityChanges.None); - // The new currency should have its conversion security and a non-zero rate already, without waiting for - // a live bar - so a conversion requested right now would no longer throw. + // The new currency should already have a non-zero rate, without waiting for a live bar Assert.IsNotNull(algorithm.Portfolio.CashBook["BTC"].CurrencyConversion); Assert.AreNotEqual(0, algorithm.Portfolio.CashBook["BTC"].ConversionRate); Assert.IsTrue(algorithm.Portfolio.CashBook["BTC"].ValueInAccountCurrency > 0); - - // Sanity: converting the runtime currency does not throw. Assert.DoesNotThrow(() => algorithm.Portfolio.CashBook.ConvertToAccountCurrency(10, "BTC")); } }