Files
2026-07-13 13:02:50 +08:00

470 lines
19 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;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
using QuantConnect.Util;
namespace QuantConnect.Data.UniverseSelection
{
/// <summary>
/// Provides a base class for all universes to derive from.
/// </summary>
public abstract class Universe : IDisposable
{
/// <summary>
/// Used to round the members time in universe <see cref="CanRemoveMember"/>, this is
/// done because we can not guarantee exact selection time in live mode, see GH issue 3287
/// </summary>
private TimeSpan? _minimumTimeInUniverseRoundingInterval;
/// <summary>
/// Gets a value indicating that no change to the universe should be made
/// </summary>
public static readonly UnchangedUniverse Unchanged = UnchangedUniverse.Instance;
private HashSet<Symbol> _previousSelections;
/// <summary>
/// Gets the internal security collection used to define membership in this universe
/// </summary>
public virtual ConcurrentDictionary<Symbol, Member> Securities
{
get;
private set;
}
/// <summary>
/// The currently selected symbol set
/// </summary>
/// <remarks>This set might be different than <see cref="Securities"/> which might hold members that are no longer selected
/// but have not been removed yet, this can be because they have some open position, orders, haven't completed the minimum time in universe</remarks>
public HashSet<Symbol> Selected
{
get; set;
} = [];
/// <summary>
/// True if this universe filter can run async in the data stack
/// </summary>
public virtual bool Asynchronous
{
get
{
if (UniverseSettings.Asynchronous.HasValue)
{
return UniverseSettings.Asynchronous.Value;
}
return false;
}
set
{
UniverseSettings.Asynchronous = value;
}
}
/// <summary>
/// Event fired when the universe selection has changed
/// </summary>
public event EventHandler SelectionChanged;
/// <summary>
/// Gets the security type of this universe
/// </summary>
public SecurityType SecurityType => Configuration.SecurityType;
/// <summary>
/// Gets the market of this universe
/// </summary>
public string Market => Configuration.Market;
/// <summary>
/// Gets the symbol of this universe
/// </summary>
public Symbol Symbol => Configuration.Symbol;
/// <summary>
/// Gets the data type of this universe
/// </summary>
public Type DataType => Configuration.Type;
/// <summary>
/// Flag indicating if disposal of this universe has been requested
/// </summary>
public virtual bool DisposeRequested
{
get;
protected set;
}
/// <summary>
/// Gets the settings used for subscriptions added for this universe
/// </summary>
public virtual UniverseSettings UniverseSettings
{
get; set;
}
/// <summary>
/// Gets the configuration used to get universe data
/// </summary>
public virtual SubscriptionDataConfig Configuration
{
get; private set;
}
/// <summary>
/// Gets the current listing of members in this universe. Modifications
/// to this dictionary do not change universe membership.
/// </summary>
public Dictionary<Symbol, Security> Members
{
get { return Securities.Select(x => x.Value.Security).ToDictionary(x => x.Symbol); }
}
/// <summary>
/// Initializes a new instance of the <see cref="Universe"/> class
/// </summary>
/// <param name="config">The configuration used to source data for this universe</param>
protected Universe(SubscriptionDataConfig config)
{
_previousSelections = new HashSet<Symbol>();
Securities = new ConcurrentDictionary<Symbol, Member>();
Configuration = config;
}
/// <summary>
/// Determines whether or not the specified security can be removed from
/// this universe. This is useful to prevent securities from being taken
/// out of a universe before the algorithm has had enough time to make
/// decisions on the security
/// </summary>
/// <param name="utcTime">The current utc time</param>
/// <param name="security">The security to check if its ok to remove</param>
/// <returns>True if we can remove the security, false otherwise</returns>
public virtual bool CanRemoveMember(DateTime utcTime, Security security)
{
// can always remove securities after dispose requested
if (DisposeRequested)
{
return true;
}
// can always remove delisted securities from the universe
if (security.IsDelisted)
{
return true;
}
Member member;
if (Securities.TryGetValue(security.Symbol, out member))
{
if (_minimumTimeInUniverseRoundingInterval == null)
{
// lets set _minimumTimeInUniverseRoundingInterval once
_minimumTimeInUniverseRoundingInterval = UniverseSettings.MinimumTimeInUniverse;
AdjustMinimumTimeInUniverseRoundingInterval();
}
var timeInUniverse = utcTime - member.Added;
if (timeInUniverse.Round(_minimumTimeInUniverseRoundingInterval.Value)
>= UniverseSettings.MinimumTimeInUniverse)
{
return true;
}
}
return false;
}
/// <summary>
/// Performs universe selection using the data specified
/// </summary>
/// <param name="utcTime">The current utc time</param>
/// <param name="data">The symbols to remain in the universe</param>
/// <returns>The data that passes the filter</returns>
public IEnumerable<Symbol> PerformSelection(DateTime utcTime, BaseDataCollection data)
{
// select empty set of symbols after dispose requested
if (DisposeRequested)
{
OnSelectionChanged();
return Enumerable.Empty<Symbol>();
}
var selections = data.FilteredContracts;
if (selections == null)
{
// only trigger selection if it hasn't already been run
var result = SelectSymbols(utcTime, data);
if (ReferenceEquals(result, Unchanged))
{
data.FilteredContracts = _previousSelections;
return Unchanged;
}
selections = result.ToHashSet();
data.FilteredContracts = selections;
}
var hasDiffs = _previousSelections.AreDifferent(selections);
_previousSelections = selections;
if (!hasDiffs)
{
return Unchanged;
}
OnSelectionChanged(selections);
return selections;
}
/// <summary>
/// Performs universe selection using the data specified
/// </summary>
/// <param name="utcTime">The current utc time</param>
/// <param name="data">The symbols to remain in the universe</param>
/// <returns>The data that passes the filter</returns>
public abstract IEnumerable<Symbol> SelectSymbols(DateTime utcTime, BaseDataCollection data);
/// <summary>
/// Creates and configures a security for the specified symbol
/// </summary>
/// <param name="symbol">The symbol of the security to be created</param>
/// <param name="algorithm">The algorithm instance</param>
/// <param name="marketHoursDatabase">The market hours database</param>
/// <param name="symbolPropertiesDatabase">The symbol properties database</param>
/// <returns>The newly initialized security object</returns>
/// <obsolete>The CreateSecurity won't be called</obsolete>
[Obsolete("CreateSecurity is obsolete and will not be called. The system will create the required Securities based on selected symbols")]
public virtual Security CreateSecurity(Symbol symbol, IAlgorithm algorithm, MarketHoursDatabase marketHoursDatabase, SymbolPropertiesDatabase symbolPropertiesDatabase)
{
throw new InvalidOperationException("CreateSecurity is obsolete and should not be called." +
"The system will create the required Securities based on selected symbols");
}
/// <summary>
/// Gets the subscription requests to be added for the specified security
/// </summary>
/// <param name="security">The security to get subscriptions for</param>
/// <param name="currentTimeUtc">The current time in utc. This is the frontier time of the algorithm</param>
/// <param name="maximumEndTimeUtc">The max end time</param>
/// <returns>All subscriptions required by this security</returns>
[Obsolete("This overload is obsolete and will not be called. It was not capable of creating new SubscriptionDataConfig due to lack of information")]
public virtual IEnumerable<SubscriptionRequest> GetSubscriptionRequests(Security security, DateTime currentTimeUtc, DateTime maximumEndTimeUtc)
{
throw new InvalidOperationException("This overload is obsolete and should not be called." +
"It was not capable of creating new SubscriptionDataConfig due to lack of information");
}
/// <summary>
/// Gets the subscription requests to be added for the specified security
/// </summary>
/// <param name="security">The security to get subscriptions for</param>
/// <param name="currentTimeUtc">The current time in utc. This is the frontier time of the algorithm</param>
/// <param name="maximumEndTimeUtc">The max end time</param>
/// <param name="subscriptionService">Instance which implements <see cref="ISubscriptionDataConfigService"/> interface</param>
/// <returns>All subscriptions required by this security</returns>
public virtual IEnumerable<SubscriptionRequest> GetSubscriptionRequests(Security security,
DateTime currentTimeUtc,
DateTime maximumEndTimeUtc,
ISubscriptionDataConfigService subscriptionService)
{
var result = subscriptionService.Add(security.Symbol,
UniverseSettings.Resolution,
UniverseSettings.FillForward,
UniverseSettings.ExtendedMarketHours,
dataNormalizationMode: UniverseSettings.DataNormalizationMode,
subscriptionDataTypes: UniverseSettings.SubscriptionDataTypes,
dataMappingMode: UniverseSettings.DataMappingMode,
contractDepthOffset: (uint)Math.Abs(UniverseSettings.ContractDepthOffset));
return result.Select(config => new SubscriptionRequest(isUniverseSubscription: false,
universe: this,
security: security,
configuration: config,
startTimeUtc: currentTimeUtc,
endTimeUtc: maximumEndTimeUtc));
}
/// <summary>
/// Determines whether or not the specified symbol is currently a member of this universe
/// </summary>
/// <param name="symbol">The symbol whose membership is to be checked</param>
/// <returns>True if the specified symbol is part of this universe, false otherwise</returns>
public bool ContainsMember(Symbol symbol)
{
return Securities.ContainsKey(symbol);
}
/// <summary>
/// Adds the specified security to this universe
/// </summary>
/// <param name="utcTime">The current utc date time</param>
/// <param name="security">The security to be added</param>
/// <param name="isInternal">True if internal member</param>
/// <returns>True if the security was successfully added,
/// false if the security was already in the universe</returns>
internal virtual bool AddMember(DateTime utcTime, Security security, bool isInternal)
{
// never add members to disposed universes
if (DisposeRequested)
{
return false;
}
if (security.IsDelisted)
{
return false;
}
return Securities.TryAdd(security.Symbol, new Member(utcTime, security, isInternal));
}
/// <summary>
/// Tries to remove the specified security from the universe. This
/// will first check to verify that we can remove the security by
/// calling the <see cref="CanRemoveMember"/> function.
/// </summary>
/// <param name="utcTime">The current utc time</param>
/// <param name="security">The security to be removed</param>
/// <returns>True if the security was successfully removed, false if
/// we're not allowed to remove or if the security didn't exist</returns>
internal virtual bool RemoveMember(DateTime utcTime, Security security)
{
if (CanRemoveMember(utcTime, security))
{
Member member;
return Securities.TryRemove(security.Symbol, out member);
}
return false;
}
/// <summary>
/// Marks this universe as disposed and ready to remove all child subscriptions
/// </summary>
public virtual void Dispose()
{
DisposeRequested = true;
}
/// <summary>
/// Event invocator for the <see cref="SelectionChanged"/> event
/// </summary>
/// <param name="selection">The current universe selection</param>
protected void OnSelectionChanged(HashSet<Symbol> selection = null)
{
SelectionChanged?.Invoke(this, new SelectionEventArgs(selection ?? new HashSet<Symbol>()));
}
/// <summary>
/// Provides a value to indicate that no changes should be made to the universe.
/// This value is intended to be returned by reference via <see cref="Universe.SelectSymbols"/>
/// </summary>
public sealed class UnchangedUniverse : IEnumerable<string>, IEnumerable<Symbol>
{
/// <summary>
/// Read-only instance of the <see cref="UnchangedUniverse"/> value
/// </summary>
public static readonly UnchangedUniverse Instance = new UnchangedUniverse();
private UnchangedUniverse() { }
IEnumerator<Symbol> IEnumerable<Symbol>.GetEnumerator() { yield break; }
IEnumerator<string> IEnumerable<string>.GetEnumerator() { yield break; }
IEnumerator IEnumerable.GetEnumerator() { yield break; }
}
/// <summary>
/// Will adjust the <see cref="_minimumTimeInUniverseRoundingInterval"/>
/// so rounding is performed as expected
/// </summary>
private void AdjustMinimumTimeInUniverseRoundingInterval()
{
if (_minimumTimeInUniverseRoundingInterval >= Time.OneDay)
{
_minimumTimeInUniverseRoundingInterval = Time.OneDay;
}
else if (_minimumTimeInUniverseRoundingInterval >= Time.OneHour)
{
_minimumTimeInUniverseRoundingInterval = Time.OneHour;
}
else if (_minimumTimeInUniverseRoundingInterval >= Time.OneMinute)
{
_minimumTimeInUniverseRoundingInterval = Time.OneMinute;
}
else if (_minimumTimeInUniverseRoundingInterval >= Time.OneSecond)
{
_minimumTimeInUniverseRoundingInterval = Time.OneSecond;
}
}
/// <summary>
/// Member of the Universe
/// </summary>
public sealed class Member
{
/// <summary>
/// DateTime when added
/// </summary>
public DateTime Added { get; init; }
/// <summary>
/// The security that was added
/// </summary>
public Security Security { get; init; }
/// <summary>
/// True if the security was added as internal by this universe
/// </summary>
public bool IsInternal { get; init; }
/// <summary>
/// Initialize a new member for the universe
/// </summary>
/// <param name="added">DateTime added</param>
/// <param name="security">Security to add</param>
/// <param name="isInternal">True if internal member</param>
public Member(DateTime added, Security security, bool isInternal)
{
Added = added;
Security = security;
IsInternal = isInternal;
}
}
/// <summary>
/// Event fired when the universe selection changes
/// </summary>
public class SelectionEventArgs : EventArgs
{
/// <summary>
/// The current universe selection
/// </summary>
public HashSet<Symbol> CurrentSelection { get; }
/// <summary>
/// Creates a new instance
/// </summary>
public SelectionEventArgs(HashSet<Symbol> currentSelection)
{
CurrentSelection = currentSelection;
}
}
}
}