637 lines
23 KiB
C#
637 lines
23 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.Linq;
|
|
using QuantConnect.Data;
|
|
using QuantConnect.Data.Market;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using QuantConnect.Data.Fundamental;
|
|
using QuantConnect.Data.UniverseSelection;
|
|
using QuantConnect.Util;
|
|
using Python.Runtime;
|
|
using QuantConnect.Python;
|
|
|
|
namespace QuantConnect.Securities
|
|
{
|
|
/// <summary>
|
|
/// Base class caching spot for security data and any other temporary properties.
|
|
/// </summary>
|
|
public class SecurityCache
|
|
{
|
|
// let's share the empty readonly version, so we don't need null checks
|
|
private static readonly IReadOnlyList<BaseData> _empty = new List<BaseData>();
|
|
|
|
// this is used to prefer quote bar data over the tradebar data
|
|
private DateTime _lastQuoteBarUpdate;
|
|
private DateTime _lastOHLCUpdate;
|
|
private BaseData _lastData;
|
|
|
|
private readonly object _locker = new();
|
|
private IReadOnlyList<BaseData> _lastTickQuotes = _empty;
|
|
private IReadOnlyList<BaseData> _lastTickTrades = _empty;
|
|
private Dictionary<Type, IReadOnlyList<BaseData>> _dataByType;
|
|
|
|
private Dictionary<string, object> _properties;
|
|
private LocalTimeKeeper _localTimeKeeper;
|
|
private bool _subscribeToDateChangedEvent;
|
|
|
|
/// <summary>
|
|
/// Gets the trading session information
|
|
/// </summary>
|
|
public Session Session { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets the most recent price submitted to this cache
|
|
/// </summary>
|
|
public decimal Price { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the most recent open submitted to this cache
|
|
/// </summary>
|
|
public decimal Open { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the most recent high submitted to this cache
|
|
/// </summary>
|
|
public decimal High { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the most recent low submitted to this cache
|
|
/// </summary>
|
|
public decimal Low { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the most recent close submitted to this cache
|
|
/// </summary>
|
|
public decimal Close { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the most recent bid submitted to this cache
|
|
/// </summary>
|
|
public decimal BidPrice { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the most recent ask submitted to this cache
|
|
/// </summary>
|
|
public decimal AskPrice { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the most recent bid size submitted to this cache
|
|
/// </summary>
|
|
public decimal BidSize { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the most recent ask size submitted to this cache
|
|
/// </summary>
|
|
public decimal AskSize { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the most recent volume submitted to this cache
|
|
/// </summary>
|
|
public decimal Volume { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the most recent open interest submitted to this cache
|
|
/// </summary>
|
|
public long OpenInterest { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Collection of keyed custom properties
|
|
/// </summary>
|
|
public Dictionary<string, object> Properties
|
|
{
|
|
get
|
|
{
|
|
if (_properties == null)
|
|
{
|
|
_properties = new Dictionary<string, object>();
|
|
}
|
|
return _properties;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a list of market data points to the local security cache for the current market price.
|
|
/// </summary>
|
|
/// <remarks>Internally uses <see cref="AddData"/> using the last data point of the provided list
|
|
/// and it stores by type the non fill forward points using <see cref="StoreData"/></remarks>
|
|
public void AddDataList(IReadOnlyList<BaseData> data, Type dataType, bool? containsFillForwardData = null, bool isInternalConfig = false)
|
|
{
|
|
SubscribeToTimeUpdatedEvent();
|
|
var nonFillForwardData = data;
|
|
// maintaining regression requires us to NOT cache FF data
|
|
if (containsFillForwardData != false)
|
|
{
|
|
var dataFiltered = new List<BaseData>(data.Count);
|
|
for (var i = 0; i < data.Count; i++)
|
|
{
|
|
var dataPoint = data[i];
|
|
if (!dataPoint.IsFillForward)
|
|
{
|
|
dataFiltered.Add(dataPoint);
|
|
}
|
|
}
|
|
nonFillForwardData = dataFiltered;
|
|
}
|
|
if (nonFillForwardData.Count != 0)
|
|
{
|
|
StoreData(nonFillForwardData, dataType);
|
|
}
|
|
else if (dataType == typeof(OpenInterest))
|
|
{
|
|
StoreData(data, typeof(OpenInterest));
|
|
}
|
|
|
|
// Session -> Current OHLCV of the day
|
|
if (Session != null && !isInternalConfig && LeanData.IsCommonLeanDataType(dataType))
|
|
{
|
|
for (int i = 0; i < data.Count; i++)
|
|
{
|
|
Session.Update(data[i]);
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < data.Count; i++)
|
|
{
|
|
ProcessDataPoint(data[i], cacheByType: false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a new market data point to the local security cache for the current market price.
|
|
/// Rules:
|
|
/// Don't cache fill forward data.
|
|
/// Always return the last observation.
|
|
/// If two consecutive data has the same time stamp and one is Quotebars and the other Tradebar, prioritize the Quotebar.
|
|
/// </summary>
|
|
public void AddData(BaseData data)
|
|
{
|
|
ProcessDataPoint(data, cacheByType: true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Will consume the given data point updating the cache state and it's properties
|
|
/// </summary>
|
|
/// <param name="data">The data point to process</param>
|
|
/// <param name="cacheByType">True if this data point should be cached by type</param>
|
|
protected virtual void ProcessDataPoint(BaseData data, bool cacheByType)
|
|
{
|
|
var tick = data as Tick;
|
|
if (tick?.TickType == TickType.OpenInterest)
|
|
{
|
|
if (cacheByType)
|
|
{
|
|
StoreDataPoint(data);
|
|
}
|
|
OpenInterest = (long)tick.Value;
|
|
|
|
// Update the session with the latest open interest
|
|
Session?.Update(data);
|
|
return;
|
|
}
|
|
|
|
// Only cache non fill-forward data and non auxiliary
|
|
if (data.IsFillForward) return;
|
|
|
|
if (cacheByType)
|
|
{
|
|
StoreDataPoint(data);
|
|
}
|
|
|
|
// we store auxiliary data by type but we don't use it to set 'lastData' nor price information
|
|
if (data.DataType == MarketDataType.Auxiliary) return;
|
|
|
|
var isDefaultDataType = SubscriptionManager.IsDefaultDataType(data);
|
|
|
|
// don't set _lastData if receive quotebar then tradebar w/ same end time. this
|
|
// was implemented to grant preference towards using quote data in the fill
|
|
// models and provide a level of determinism on the values exposed via the cache.
|
|
if ((_lastData == null
|
|
|| _lastQuoteBarUpdate != data.EndTime
|
|
|| data.DataType != MarketDataType.TradeBar)
|
|
// we will only set the default data type to preserve determinism and backwards compatibility
|
|
&& isDefaultDataType)
|
|
{
|
|
_lastData = data;
|
|
}
|
|
|
|
if (tick != null)
|
|
{
|
|
if (tick.Value != 0) Price = tick.Value;
|
|
|
|
switch (tick.TickType)
|
|
{
|
|
case TickType.Trade:
|
|
if (tick.Quantity != 0) Volume = tick.Quantity;
|
|
break;
|
|
|
|
case TickType.Quote:
|
|
if (tick.BidPrice != 0) BidPrice = tick.BidPrice;
|
|
if (tick.BidSize != 0) BidSize = tick.BidSize;
|
|
|
|
if (tick.AskPrice != 0) AskPrice = tick.AskPrice;
|
|
if (tick.AskSize != 0) AskSize = tick.AskSize;
|
|
break;
|
|
}
|
|
return;
|
|
}
|
|
|
|
var bar = data as IBar;
|
|
if (bar != null)
|
|
{
|
|
// we will only set OHLC values using the default data type to preserve determinism and backwards compatibility.
|
|
// Gives priority to QuoteBar over TradeBar, to be removed when default data type completely addressed GH issue 4196
|
|
if ((_lastQuoteBarUpdate != data.EndTime || _lastOHLCUpdate != data.EndTime) && isDefaultDataType)
|
|
{
|
|
_lastOHLCUpdate = data.EndTime;
|
|
if (bar.Open != 0) Open = bar.Open;
|
|
if (bar.High != 0) High = bar.High;
|
|
if (bar.Low != 0) Low = bar.Low;
|
|
if (bar.Close != 0)
|
|
{
|
|
Price = bar.Close;
|
|
Close = bar.Close;
|
|
}
|
|
}
|
|
|
|
var tradeBar = bar as TradeBar;
|
|
if (tradeBar != null)
|
|
{
|
|
if (tradeBar.Volume != 0) Volume = tradeBar.Volume;
|
|
}
|
|
|
|
var quoteBar = bar as QuoteBar;
|
|
if (quoteBar != null)
|
|
{
|
|
_lastQuoteBarUpdate = quoteBar.EndTime;
|
|
if (quoteBar.Ask != null && quoteBar.Ask.Close != 0) AskPrice = quoteBar.Ask.Close;
|
|
if (quoteBar.Bid != null && quoteBar.Bid.Close != 0) BidPrice = quoteBar.Bid.Close;
|
|
if (quoteBar.LastBidSize != 0) BidSize = quoteBar.LastBidSize;
|
|
if (quoteBar.LastAskSize != 0) AskSize = quoteBar.LastAskSize;
|
|
}
|
|
}
|
|
else if (data.DataType != MarketDataType.Auxiliary)
|
|
{
|
|
if (data.DataType != MarketDataType.Base || data.Price != 0)
|
|
{
|
|
Price = data.Price;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores the specified data list in the cache WITHOUT updating any of the cache properties, such as Price
|
|
/// </summary>
|
|
/// <param name="data">The collection of data to store in this cache</param>
|
|
/// <param name="dataType">The data type</param>
|
|
public virtual void StoreData(IReadOnlyList<BaseData> data, Type dataType)
|
|
{
|
|
if (dataType == typeof(Tick))
|
|
{
|
|
var tick = data[data.Count - 1] as Tick;
|
|
switch (tick?.TickType)
|
|
{
|
|
case TickType.Trade:
|
|
_lastTickTrades = data;
|
|
return;
|
|
case TickType.Quote:
|
|
_lastTickQuotes = data;
|
|
return;
|
|
}
|
|
}
|
|
|
|
lock (_locker)
|
|
{
|
|
_dataByType ??= new();
|
|
_dataByType[dataType] = data;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method to update the open interest cache property from a chain universe data point,
|
|
/// which carries the contracts daily open interest
|
|
/// </summary>
|
|
/// <param name="data">The data point being stored</param>
|
|
protected void UpdateOpenInterest(BaseData data)
|
|
{
|
|
if (data is BaseChainUniverseData chainUniverseData)
|
|
{
|
|
OpenInterest = (long)chainUniverseData.OpenInterest;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method to update the open interest cache property from the last data point of a stored data list
|
|
/// </summary>
|
|
/// <param name="data">The data list being stored</param>
|
|
protected void UpdateOpenInterest(IReadOnlyList<BaseData> data)
|
|
{
|
|
if (data.Count != 0)
|
|
{
|
|
UpdateOpenInterest(data[data.Count - 1]);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get last data packet received for this security if any else null
|
|
/// </summary>
|
|
/// <returns>BaseData type of the security</returns>
|
|
public BaseData GetData()
|
|
{
|
|
return _lastData;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get last data packet received for this security of the specified type
|
|
/// </summary>
|
|
/// <typeparam name="T">The data type</typeparam>
|
|
/// <returns>The last data packet, null if none received of type</returns>
|
|
public T GetData<T>()
|
|
where T : BaseData
|
|
{
|
|
return GetData(typeof(T)) as T;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the last data packet of the specified Python type.
|
|
/// </summary>
|
|
/// <param name="pyType">The Python type to convert and match</param>
|
|
/// <returns>The last data packet as a PyObject, or null if not found</returns>
|
|
public PyObject GetData(PyObject pyType)
|
|
{
|
|
using var _ = Py.GIL();
|
|
if (!pyType.TryCreateType(out var type))
|
|
{
|
|
return null;
|
|
}
|
|
// Try to retrieve data using the exact type
|
|
var data = GetData(type);
|
|
|
|
// If no data is found and the type is or derives from PythonData,
|
|
// fallback to retrieving data for the base PythonData type
|
|
if (data == null && typeof(PythonData).IsAssignableFrom(type))
|
|
{
|
|
// This can happen when the user manually adds data from Python using AddData()
|
|
data = GetData<PythonData>();
|
|
}
|
|
return data.ToPython();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the last data packet of the specified type
|
|
/// </summary>
|
|
/// <param name="type">The type of data to retrieve</param>
|
|
/// <returns>The last data packet of the specified type, or null if none found</returns>
|
|
private BaseData GetData(Type type)
|
|
{
|
|
IReadOnlyList<BaseData> list;
|
|
if (!TryGetValue(type, out list) || list.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
return list[list.Count - 1];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all data points of the specified type from the most recent time step
|
|
/// that produced data for that type
|
|
/// </summary>
|
|
public IEnumerable<T> GetAll<T>()
|
|
{
|
|
if (typeof(T) == typeof(Tick))
|
|
{
|
|
return _lastTickTrades.Concat(_lastTickQuotes).Cast<T>();
|
|
}
|
|
|
|
lock (_locker)
|
|
{
|
|
if (_dataByType == null || !_dataByType.TryGetValue(typeof(T), out var list))
|
|
{
|
|
return new List<T>();
|
|
}
|
|
|
|
return list.Cast<T>();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset cache storage and free memory
|
|
/// </summary>
|
|
public void Reset()
|
|
{
|
|
Price = 0;
|
|
|
|
Open = 0;
|
|
High = 0;
|
|
Low = 0;
|
|
Close = 0;
|
|
|
|
BidPrice = 0;
|
|
BidSize = 0;
|
|
AskPrice = 0;
|
|
AskSize = 0;
|
|
|
|
Volume = 0;
|
|
OpenInterest = 0;
|
|
|
|
_lastData = null;
|
|
_dataByType = null;
|
|
_lastTickQuotes = _empty;
|
|
_lastTickTrades = _empty;
|
|
|
|
_lastOHLCUpdate = default;
|
|
_lastQuoteBarUpdate = default;
|
|
Session?.Reset();
|
|
UnsubscribeToTimeUpdatedEvent();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets whether or not this dynamic data instance has data stored for the specified type
|
|
/// </summary>
|
|
public bool HasData(Type type)
|
|
{
|
|
return TryGetValue(type, out _);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets whether or not this dynamic data instance has data stored for the specified type
|
|
/// </summary>
|
|
public bool TryGetValue(Type type, out IReadOnlyList<BaseData> data)
|
|
{
|
|
if (type == typeof(Fundamentals))
|
|
{
|
|
// for backwards compatibility
|
|
type = typeof(FundamentalUniverse);
|
|
}
|
|
else if (type == typeof(ETFConstituentData))
|
|
{
|
|
// for backwards compatibility
|
|
type = typeof(ETFConstituentUniverse);
|
|
}
|
|
else if (type == typeof(Tick))
|
|
{
|
|
var quote = _lastTickQuotes.LastOrDefault();
|
|
var trade = _lastTickTrades.LastOrDefault();
|
|
var isQuoteDefaultDataType = quote != null && SubscriptionManager.IsDefaultDataType(quote);
|
|
var isTradeDefaultDataType = trade != null && SubscriptionManager.IsDefaultDataType(trade);
|
|
|
|
// Currently, IsDefaultDataType returns true for both cases,
|
|
// So we will return the list with the tick with the most recent timestamp
|
|
if (isQuoteDefaultDataType && isTradeDefaultDataType)
|
|
{
|
|
data = quote.EndTime > trade.EndTime ? _lastTickQuotes : _lastTickTrades;
|
|
return true;
|
|
}
|
|
|
|
data = isQuoteDefaultDataType ? _lastTickQuotes : _lastTickTrades;
|
|
return data?.Count > 0;
|
|
}
|
|
|
|
data = default;
|
|
return _dataByType != null && _dataByType.TryGetValue(type, out data);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the <see cref="LocalTimeKeeper"/> to be used for this <see cref="SecurityCache"/>.
|
|
/// This is the source of this instance's time.
|
|
/// </summary>
|
|
/// <param name="localTimeKeeper">The source of this <see cref="Security"/>'s time.</param>
|
|
public virtual void SetLocalTimeKeeper(LocalTimeKeeper localTimeKeeper)
|
|
{
|
|
UnsubscribeToTimeUpdatedEvent();
|
|
// Assign the new LocalTimeKeeper
|
|
_localTimeKeeper = localTimeKeeper;
|
|
SubscribeToTimeUpdatedEvent();
|
|
}
|
|
|
|
private void SubscribeToTimeUpdatedEvent()
|
|
{
|
|
if (!_subscribeToDateChangedEvent && _localTimeKeeper != null)
|
|
{
|
|
_subscribeToDateChangedEvent = true;
|
|
_localTimeKeeper.TimeUpdated += OnTimeUpdated;
|
|
}
|
|
}
|
|
|
|
private void UnsubscribeToTimeUpdatedEvent()
|
|
{
|
|
if (_localTimeKeeper != null && _subscribeToDateChangedEvent)
|
|
{
|
|
_subscribeToDateChangedEvent = false;
|
|
_localTimeKeeper.TimeUpdated -= OnTimeUpdated;
|
|
}
|
|
}
|
|
|
|
private void OnTimeUpdated(object sender, TimeUpdatedEventArgs e)
|
|
{
|
|
// Triggered when the algorithm sets a new local time from timeSlice.Time
|
|
Session?.Scan(e.Time);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void StoreDataPoint(BaseData data)
|
|
{
|
|
if (data.GetType() == typeof(Tick))
|
|
{
|
|
var tick = data as Tick;
|
|
switch (tick?.TickType)
|
|
{
|
|
case TickType.Trade:
|
|
_lastTickTrades = new List<BaseData> { tick };
|
|
break;
|
|
case TickType.Quote:
|
|
_lastTickQuotes = new List<BaseData> { tick };
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
lock (_locker)
|
|
{
|
|
_dataByType ??= new();
|
|
// Always keep track of the last observation
|
|
IReadOnlyList<BaseData> list;
|
|
if (!_dataByType.TryGetValue(data.GetType(), out list))
|
|
{
|
|
list = new List<BaseData> { data };
|
|
_dataByType[data.GetType()] = list;
|
|
}
|
|
else
|
|
{
|
|
// we KNOW this one is actually a list, so this is safe
|
|
// we overwrite the zero entry so we're not constantly newing up lists
|
|
((List<BaseData>)list)[0] = data;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method that modifies the target security cache instance to use the
|
|
/// type cache of the source
|
|
/// </summary>
|
|
/// <remarks>Will set in the source cache any data already present in the target cache</remarks>
|
|
/// <remarks>This is useful for custom data securities which also have an underlying security,
|
|
/// will allow both securities to access the same data by type</remarks>
|
|
/// <param name="sourceToShare">The source cache to use</param>
|
|
/// <param name="targetToModify">The target security cache that will be modified</param>
|
|
public static void ShareTypeCacheInstance(SecurityCache sourceToShare, SecurityCache targetToModify)
|
|
{
|
|
sourceToShare._dataByType ??= new();
|
|
if (targetToModify._dataByType != null)
|
|
{
|
|
lock (targetToModify._locker)
|
|
{
|
|
lock (sourceToShare._locker)
|
|
{
|
|
foreach (var kvp in targetToModify._dataByType)
|
|
{
|
|
sourceToShare._dataByType.TryAdd(kvp.Key, kvp.Value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
targetToModify._dataByType = sourceToShare._dataByType;
|
|
targetToModify._lastTickTrades = sourceToShare._lastTickTrades;
|
|
targetToModify._lastTickQuotes = sourceToShare._lastTickQuotes;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies the split to the security cache values
|
|
/// </summary>
|
|
internal void ApplySplit(Split split)
|
|
{
|
|
Price *= split.SplitFactor;
|
|
Open *= split.SplitFactor;
|
|
High *= split.SplitFactor;
|
|
Low *= split.SplitFactor;
|
|
Close *= split.SplitFactor;
|
|
Volume /= split.SplitFactor;
|
|
BidPrice *= split.SplitFactor;
|
|
AskPrice *= split.SplitFactor;
|
|
AskSize /= split.SplitFactor;
|
|
BidSize /= split.SplitFactor;
|
|
|
|
// Adjust values for the last data we have cached
|
|
Action<BaseData> scale = data => data.Scale((target, factor, _) => target * factor, 1 / split.SplitFactor, split.SplitFactor, decimal.Zero);
|
|
_dataByType?.Values.DoForEach(x => x.DoForEach(scale));
|
|
_lastTickQuotes.DoForEach(scale);
|
|
_lastTickTrades.DoForEach(scale);
|
|
}
|
|
}
|
|
}
|