1818using System . Collections . Generic ;
1919using System . Linq ;
2020using Python . Runtime ;
21+ using QuantConnect . Configuration ;
2122using QuantConnect . Data ;
2223using QuantConnect . Data . UniverseSelection ;
2324using QuantConnect . Interfaces ;
25+ using QuantConnect . Logging ;
2426using QuantConnect . Util ;
2527
2628namespace QuantConnect . Lean . Engine . DataFeeds . Enumerators . Factories
@@ -30,10 +32,15 @@ namespace QuantConnect.Lean.Engine.DataFeeds.Enumerators.Factories
3032 /// </summary>
3133 public class LiveCustomDataSubscriptionEnumeratorFactory : ISubscriptionEnumeratorFactory
3234 {
35+ // when the expected universe file is not available yet, we fall back to the backup universe file ("*.backup"),
36+ // if any, as a last resort, when the market is open or within this time span before the next market open
37+ private static readonly TimeSpan UniverseFileBackupFallbackWindow =
38+ TimeSpan . FromMinutes ( Config . GetInt ( "universe-file-backup-fallback-minutes" , 30 ) ) ;
39+
3340 private readonly TimeSpan _minimumIntervalCheck ;
3441 private readonly ITimeProvider _timeProvider ;
3542 private readonly Func < DateTime , DateTime > _dateAdjustment ;
36- private readonly Func < SubscriptionDataSource , DateTime , SubscriptionDataSource > _sourceAdjustment ;
43+ private readonly bool _fallBackToBackupUniverseFiles ;
3744 private readonly IObjectStore _objectStore ;
3845
3946 /// <summary>
@@ -43,17 +50,17 @@ public class LiveCustomDataSubscriptionEnumeratorFactory : ISubscriptionEnumerat
4350 /// <param name="objectStore">The object store to use</param>
4451 /// <param name="dateAdjustment">Func that allows adjusting the datetime to use</param>
4552 /// <param name="minimumIntervalCheck">Allows specifying the minimum interval between each enumerator refresh and data check, default is 30 minutes</param>
46- /// <param name="sourceAdjustment">Optional func that allows adjusting the data source to read from, given the source and the current utc time,
47- /// e.g. to fall back to an alternative source when the expected one is not available .
48- /// It is evaluated at the same cadence as the enumerator refreshes </param>
53+ /// <param name="fallBackToBackupUniverseFiles">Whether to fall back to the backup universe file ("*.backup"), if any, as a last resort
54+ /// when the expected universe file is not available and the market is open or close to opening .
55+ /// Only meaningful for universe subscriptions backed by local files </param>
4956 public LiveCustomDataSubscriptionEnumeratorFactory ( ITimeProvider timeProvider , IObjectStore objectStore ,
5057 Func < DateTime , DateTime > dateAdjustment = null , TimeSpan ? minimumIntervalCheck = null ,
51- Func < SubscriptionDataSource , DateTime , SubscriptionDataSource > sourceAdjustment = null )
58+ bool fallBackToBackupUniverseFiles = false )
5259 {
5360 _timeProvider = timeProvider ;
5461 _dateAdjustment = dateAdjustment ;
5562 _minimumIntervalCheck = minimumIntervalCheck ?? TimeSpan . FromMinutes ( 30 ) ;
56- _sourceAdjustment = sourceAdjustment ;
63+ _fallBackToBackupUniverseFiles = fallBackToBackupUniverseFiles ;
5764 _objectStore = objectStore ;
5865 }
5966
@@ -72,6 +79,7 @@ public IEnumerator<BaseData> CreateEnumerator(SubscriptionRequest request, IData
7279 var frontier = Ref . Create ( _dateAdjustment ? . Invoke ( request . StartTimeLocal ) ?? request . StartTimeLocal ) ;
7380 var lastSourceRefreshTime = DateTime . MinValue ;
7481 var sourceFactory = config . GetBaseDataInstance ( ) ;
82+ var sourceAdjustment = _fallBackToBackupUniverseFiles ? GetUniverseFileBackupSourceAdjustment ( request , dataProvider ) : null ;
7583
7684 // this is refreshing the enumerator stack for each new source
7785 var refresher = new RefreshEnumerator < BaseData > ( ( ) =>
@@ -87,9 +95,9 @@ public IEnumerator<BaseData> CreateEnumerator(SubscriptionRequest request, IData
8795 lastSourceRefreshTime = utcNow ;
8896 var localDate = _dateAdjustment ? . Invoke ( utcNow . ConvertFromUtc ( config . ExchangeTimeZone ) . Date ) ?? utcNow . ConvertFromUtc ( config . ExchangeTimeZone ) . Date ;
8997 var source = sourceFactory . GetSource ( config , localDate , true ) ;
90- if ( _sourceAdjustment != null )
98+ if ( sourceAdjustment != null )
9199 {
92- source = _sourceAdjustment ( source , utcNow ) ;
100+ source = sourceAdjustment ( source , utcNow ) ;
93101 }
94102
95103 // fetch the new source and enumerate the data source reader
@@ -207,6 +215,55 @@ IDataProvider dataProvider
207215 return SubscriptionDataSourceReader . ForSource ( source , dataCacheProvider , config , date , true , baseDataInstance , dataProvider , _objectStore ) ;
208216 }
209217
218+ /// <summary>
219+ /// Gets a source adjustment for universe files as a safety net for when the expected universe file
220+ /// is not available yet: when the market is open or close to opening (within <see cref="UniverseFileBackupFallbackWindow"/>
221+ /// of the next market open), it falls back to the backup universe file ("*.backup") if present, as a last resort.
222+ /// It is evaluated at the same cadence as the enumerator refreshes
223+ /// </summary>
224+ private static Func < SubscriptionDataSource , DateTime , SubscriptionDataSource > GetUniverseFileBackupSourceAdjustment (
225+ SubscriptionRequest request , IDataProvider dataProvider )
226+ {
227+ var exchangeHours = request . Security . Exchange . Hours ;
228+ return ( source , utcNow ) =>
229+ {
230+ if ( source . TransportMedium != SubscriptionTransportMedium . LocalFile )
231+ {
232+ return source ;
233+ }
234+
235+ var localTime = utcNow . ConvertFromUtc ( exchangeHours . TimeZone ) ;
236+ // only fall back when the market is open or close to opening, when the expected universe file should already be available
237+ if ( ! exchangeHours . IsOpen ( localTime , extendedMarketHours : false )
238+ // if the market is closed, GetNextMarketOpen returns the next day open
239+ && exchangeHours . GetNextMarketOpen ( localTime , extendedMarketHours : false ) - localTime > UniverseFileBackupFallbackWindow )
240+ {
241+ return source ;
242+ }
243+
244+ if ( CanFetchDataSource ( dataProvider , source ) )
245+ {
246+ return source ;
247+ }
248+
249+ var backupSource = new SubscriptionDataSource ( source . Source + ".backup" , source . TransportMedium , source . Format ) ;
250+ if ( CanFetchDataSource ( dataProvider , backupSource ) )
251+ {
252+ Log . Trace ( $ "LiveCustomDataSubscriptionEnumeratorFactory.GetUniverseFileBackupSourceAdjustment(): universe file '{ source . Source } ' is not available, " +
253+ $ "falling back to backup universe file '{ backupSource . Source } '") ;
254+ return backupSource ;
255+ }
256+
257+ return source ;
258+ } ;
259+ }
260+
261+ private static bool CanFetchDataSource ( IDataProvider dataProvider , SubscriptionDataSource source )
262+ {
263+ using var stream = dataProvider . Fetch ( source . Source ) ;
264+ return stream != null ;
265+ }
266+
210267 private bool SourceRequiresFastForward ( SubscriptionDataSource source )
211268 {
212269 return source . TransportMedium == SubscriptionTransportMedium . LocalFile
0 commit comments