Files
JosueNina eb12c8fa65 Seed runtime-added currency conversion rates immediately (#9568)
* Seed runtime-added currency conversion rates immediately

Fixes the spurious 'The conversion rate for <currency> 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* Seed runtime added currency conversion rates

* Seed currencies with no new conversion feed and dedup the seeding helper

---------

Co-authored-by: Martin-Molinero <Martin-Molinero@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:40:54 -03:00

86 lines
3.4 KiB
C#

/*
* 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 QuantConnect.Interfaces;
using QuantConnect.Securities;
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect
{
/// <summary>
/// Provides utility methods for or related to algorithms
/// </summary>
public static class AlgorithmUtils
{
/// <summary>
/// Seeds the provided securities with their last known prices from the algorithm
/// </summary>
/// <param name="securities">The securities to seed</param>
/// <param name="algorithm">The algorithm instance</param>
public static void SeedSecurities(IReadOnlyCollection<Security> securities, IAlgorithm algorithm)
{
var securitiesToSeed = securities.Where(x => x.Price == 0);
var data = algorithm.GetLastKnownPrices(securitiesToSeed.Select(x => x.Symbol));
foreach (var security in securitiesToSeed)
{
if (data.TryGetValue(security.Symbol, out var seedData))
{
foreach (var datum in seedData)
{
security.SetMarketPrice(datum);
}
}
}
}
/// <summary>
/// Seeds an initial conversion rate for the cashbook currencies that don't have one yet, so they are
/// non-zero right away instead of waiting for the first conversion pair bar to arrive
/// </summary>
/// <param name="algorithm">The algorithm instance</param>
/// <param name="currenciesToUpdateWhiteList">
/// If passed, only the currencies in the CashBook contained in this list will be updated.
/// By default, if not passed (null), all currencies in the cashbook without a properly set up currency conversion will be updated.
/// </param>
public static void SeedCurrencyConversionRates(IAlgorithm algorithm, IReadOnlyCollection<string> currenciesToUpdateWhiteList = null)
{
Func<Cash, bool> cashToUpdateFilter = currenciesToUpdateWhiteList == null
? (x) => x.CurrencyConversion != null && x.ConversionRate == 0
: (x) => currenciesToUpdateWhiteList.Contains(x.Symbol);
var cashToUpdate = algorithm.Portfolio.CashBook.Values.Where(cashToUpdateFilter).ToList();
if (cashToUpdate.Count == 0)
{
return;
}
var securitiesToUpdate = cashToUpdate
.SelectMany(x => x.CurrencyConversion.ConversionRateSecurities)
.Distinct()
.ToList();
SeedSecurities(securitiesToUpdate, algorithm);
foreach (var cash in cashToUpdate)
{
cash.Update();
}
}
}
}