/*
* 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
{
///
/// Base class caching spot for security data and any other temporary properties.
///
public class SecurityCache
{
// let's share the empty readonly version, so we don't need null checks
private static readonly IReadOnlyList _empty = new List();
// 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 _lastTickQuotes = _empty;
private IReadOnlyList _lastTickTrades = _empty;
private Dictionary> _dataByType;
private Dictionary _properties;
///
/// Gets the most recent price submitted to this cache
///
public decimal Price { get; private set; }
///
/// Gets the most recent open submitted to this cache
///
public decimal Open { get; private set; }
///
/// Gets the most recent high submitted to this cache
///
public decimal High { get; private set; }
///
/// Gets the most recent low submitted to this cache
///
public decimal Low { get; private set; }
///
/// Gets the most recent close submitted to this cache
///
public decimal Close { get; private set; }
///
/// Gets the most recent bid submitted to this cache
///
public decimal BidPrice { get; private set; }
///
/// Gets the most recent ask submitted to this cache
///
public decimal AskPrice { get; private set; }
///
/// Gets the most recent bid size submitted to this cache
///
public decimal BidSize { get; private set; }
///
/// Gets the most recent ask size submitted to this cache
///
public decimal AskSize { get; private set; }
///
/// Gets the most recent volume submitted to this cache
///
public decimal Volume { get; private set; }
///
/// Gets the most recent open interest submitted to this cache
///
public long OpenInterest { get; private set; }
///
/// Collection of keyed custom properties
///
public Dictionary Properties
{
get
{
if (_properties == null)
{
_properties = new Dictionary();
}
return _properties;
}
}
///
/// Add a list of market data points to the local security cache for the current market price.
///
/// Internally uses using the last data point of the provided list
/// and it stores by type the non fill forward points using
public void AddDataList(IReadOnlyList data, Type dataType, bool? containsFillForwardData = null)
{
var nonFillForwardData = data;
// maintaining regression requires us to NOT cache FF data
if (containsFillForwardData != false)
{
var dataFiltered = new List(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));
}
var last = data[data.Count - 1];
ProcessDataPoint(last, cacheByType: false);
}
///
/// 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.
///
public void AddData(BaseData data)
{
ProcessDataPoint(data, cacheByType: true);
}
///
/// Will consume the given data point updating the cache state and it's properties
///
/// The data point to process
/// True if this data point should be cached by type
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;
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;
}
}
}
///
/// Stores the specified data list in the cache WITHOUT updating any of the cache properties, such as Price
///
/// The collection of data to store in this cache
/// The data type
public void StoreData(IReadOnlyList 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;
}
}
///
/// Get last data packet received for this security if any else null
///
/// BaseData type of the security
public BaseData GetData()
{
return _lastData;
}
///
/// Get last data packet received for this security of the specified type
///
/// The data type
/// The last data packet, null if none received of type
public T GetData()
where T : BaseData
{
return GetData(typeof(T)) as T;
}
///
/// Retrieves the last data packet of the specified Python type.
///
/// The Python type to convert and match
/// The last data packet as a PyObject, or null if not found
public PyObject GetData(PyObject pyType)
{
using var _ = Py.GIL();
if (!pyType.TryCreateType(out var type))
{
return null;
}
type = typeof(PythonData).IsAssignableFrom(type) ? typeof(PythonData) : type;
return GetData(type).ToPython();
}
///
/// Get the last data packet of the specified type
///
/// The type of data to retrieve
/// The last data packet of the specified type, or null if none found
private BaseData GetData(Type type)
{
IReadOnlyList list;
if (!TryGetValue(type, out list) || list.Count == 0)
{
return null;
}
return list[list.Count - 1];
}
///
/// Gets all data points of the specified type from the most recent time step
/// that produced data for that type
///
public IEnumerable GetAll()
{
if (typeof(T) == typeof(Tick))
{
return _lastTickTrades.Concat(_lastTickQuotes).Cast();
}
lock (_locker)
{
if (_dataByType == null || !_dataByType.TryGetValue(typeof(T), out var list))
{
return new List();
}
return list.Cast();
}
}
///
/// Reset cache storage and free memory
///
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;
}
///
/// Gets whether or not this dynamic data instance has data stored for the specified type
///
public bool HasData(Type type)
{
return TryGetValue(type, out _);
}
///
/// Gets whether or not this dynamic data instance has data stored for the specified type
///
public bool TryGetValue(Type type, out IReadOnlyList 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);
}
[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 { tick };
break;
case TickType.Quote:
_lastTickQuotes = new List { tick };
break;
}
}
else
{
lock (_locker)
{
_dataByType ??= new();
// Always keep track of the last observation
IReadOnlyList list;
if (!_dataByType.TryGetValue(data.GetType(), out list))
{
list = new List { 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)list)[0] = data;
}
}
}
}
///
/// Helper method that modifies the target security cache instance to use the
/// type cache of the source
///
/// Will set in the source cache any data already present in the target cache
/// This is useful for custom data securities which also have an underlying security,
/// will allow both securities to access the same data by type
/// The source cache to use
/// The target security cache that will be modified
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;
}
///
/// Applies the split to the security cache values
///
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 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);
}
}
}