Files
quantconnect--lean/Engine/DataFeeds/CurrencySubscriptionDataConfigManager.cs
T
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

164 lines
7.7 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 System.Collections.Generic;
using System.Linq;
using QuantConnect.Brokerages;
using QuantConnect.Data;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
namespace QuantConnect.Lean.Engine.DataFeeds
{
/// <summary>
/// Helper class to keep track of required internal currency <see cref="SubscriptionDataConfig"/>.
/// This class is used by the <see cref="UniverseSelection"/>
/// </summary>
public class CurrencySubscriptionDataConfigManager
{
private readonly HashSet<SubscriptionDataConfig> _toBeAddedCurrencySubscriptionDataConfigs;
private readonly HashSet<SubscriptionDataConfig> _addedCurrencySubscriptionDataConfigs;
private bool _ensureCurrencyDataFeeds;
private bool _pendingSubscriptionDataConfigs;
private readonly CashBook _cashBook;
private readonly Resolution _defaultResolution;
private readonly SecurityManager _securityManager;
private readonly SubscriptionManager _subscriptionManager;
private readonly ISecurityService _securityService;
/// <summary>
/// Creates a new instance
/// </summary>
/// <param name="cashBook">The cash book instance</param>
/// <param name="securityManager">The SecurityManager, required by the cash book for creating new securities</param>
/// <param name="subscriptionManager">The SubscriptionManager, required by the cash book for creating new subscription data configs</param>
/// <param name="securityService">The SecurityService, required by the cash book for creating new securities</param>
/// <param name="defaultResolution">The default resolution to use for the internal subscriptions</param>
public CurrencySubscriptionDataConfigManager(CashBook cashBook,
SecurityManager securityManager,
SubscriptionManager subscriptionManager,
ISecurityService securityService,
Resolution defaultResolution)
{
cashBook.Updated += (sender, args) =>
{
if (args.UpdateType == CashBookUpdateType.Added)
{
_ensureCurrencyDataFeeds = true;
}
};
_defaultResolution = defaultResolution;
_pendingSubscriptionDataConfigs = false;
_securityManager = securityManager;
_subscriptionManager = subscriptionManager;
_securityService = securityService;
_cashBook = cashBook;
_addedCurrencySubscriptionDataConfigs = new HashSet<SubscriptionDataConfig>();
_toBeAddedCurrencySubscriptionDataConfigs = new HashSet<SubscriptionDataConfig>();
}
/// <summary>
/// Will verify if there are any <see cref="SubscriptionDataConfig"/> to be removed
/// for a given added <see cref="Symbol"/>.
/// </summary>
/// <param name="addedSymbol">The symbol that was added to the data feed system</param>
/// <returns>The SubscriptionDataConfig to be removed, null if none</returns>
public SubscriptionDataConfig GetSubscriptionDataConfigToRemove(Symbol addedSymbol)
{
if (addedSymbol.SecurityType == SecurityType.Crypto
|| addedSymbol.SecurityType == SecurityType.CryptoFuture
|| addedSymbol.SecurityType == SecurityType.Forex
|| addedSymbol.SecurityType == SecurityType.Cfd)
{
var currencyDataFeed = _addedCurrencySubscriptionDataConfigs
.FirstOrDefault(x => x.Symbol == addedSymbol);
if (currencyDataFeed != null)
{
return currencyDataFeed;
}
}
return null;
}
/// <summary>
/// Will update pending currency <see cref="SubscriptionDataConfig"/>
/// </summary>
/// <returns>True when there are pending currency subscriptions <see cref="GetPendingSubscriptionDataConfigs"/></returns>
public bool UpdatePendingSubscriptionDataConfigs(IBrokerageModel brokerageModel)
{
if (_ensureCurrencyDataFeeds)
{
// this allows us to handle the case where SetCash is called when no security has been really added
EnsureCurrencySubscriptionDataConfigs(SecurityChanges.None, brokerageModel);
}
return _pendingSubscriptionDataConfigs;
}
/// <summary>
/// Will return any pending internal currency <see cref="SubscriptionDataConfig"/> and remove them as pending.
/// </summary>
/// <returns>Will return the <see cref="SubscriptionDataConfig"/> to be added</returns>
public IEnumerable<SubscriptionDataConfig> GetPendingSubscriptionDataConfigs()
{
var result = new List<SubscriptionDataConfig>();
if (_pendingSubscriptionDataConfigs)
{
foreach (var subscriptionDataConfig in _toBeAddedCurrencySubscriptionDataConfigs)
{
_addedCurrencySubscriptionDataConfigs.Add(subscriptionDataConfig);
result.Add(subscriptionDataConfig);
}
_toBeAddedCurrencySubscriptionDataConfigs.Clear();
_pendingSubscriptionDataConfigs = false;
}
return result;
}
/// <summary>
/// Checks the current <see cref="SubscriptionDataConfig"/> and adds new necessary currency pair feeds to provide real time conversion data
/// </summary>
/// <returns>True if a new currency was introduced, either as a new internal conversion feed or as a new cash
/// entry added to the cashbook. Lets callers skip follow up work like seeding the new conversion rates when
/// nothing was added</returns>
public bool EnsureCurrencySubscriptionDataConfigs(SecurityChanges securityChanges, IBrokerageModel brokerageModel)
{
// a new cash added to the cashbook also needs its conversion rate seeded, even when its conversion
// security is an already subscribed one and no new internal feed is introduced below
var newCashAdded = _ensureCurrencyDataFeeds;
_ensureCurrencyDataFeeds = false;
// remove any 'to be added' if the security has already been added
_toBeAddedCurrencySubscriptionDataConfigs.RemoveWhere(
config => securityChanges.AddedSecurities.Any(x => x.Symbol == config.Symbol));
var newConfigs = _cashBook.EnsureCurrencyDataFeeds(
_securityManager,
_subscriptionManager,
brokerageModel.DefaultMarkets,
securityChanges,
_securityService,
_defaultResolution);
foreach (var config in newConfigs)
{
_toBeAddedCurrencySubscriptionDataConfigs.Add(config);
}
_pendingSubscriptionDataConfigs = _toBeAddedCurrencySubscriptionDataConfigs.Any();
return newConfigs.Count > 0 || newCashAdded;
}
}
}