/* * 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 Python.Runtime; using QuantConnect.Data; using QuantConnect.Data.Fundamental; using QuantConnect.Data.Market; using QuantConnect.Util; using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; using QuantConnect.Util; namespace QuantConnect.Python { /// /// Organizes a list of data to create pandas.DataFrames /// public class PandasData { private const string Open = "open"; private const string High = "high"; private const string Low = "low"; private const string Close = "close"; private const string Volume = "volume"; private const string AskOpen = "askopen"; private const string AskHigh = "askhigh"; private const string AskLow = "asklow"; private const string AskClose = "askclose"; private const string AskPrice = "askprice"; private const string AskSize = "asksize"; private const string BidOpen = "bidopen"; private const string BidHigh = "bidhigh"; private const string BidLow = "bidlow"; private const string BidClose = "bidclose"; private const string BidPrice = "bidprice"; private const string BidSize = "bidsize"; private const string LastPrice = "lastprice"; private const string Quantity = "quantity"; private const string Exchange = "exchange"; private const string Suspicious = "suspicious"; private const string OpenInterest = "openinterest"; // we keep these so we don't need to ask for them each time private static PyString _empty; private static PyObject _pandas; private static PyObject _seriesFactory; private static PyObject _dataFrameFactory; private static PyObject _multiIndexFactory; private static PyList _defaultNames; private static PyList _level2Names; private static PyList _level3Names; private readonly static HashSet _baseDataProperties = typeof(BaseData).GetProperties().ToHashSet(x => x.Name.ToLowerInvariant()); private readonly static ConcurrentDictionary> _membersByType = new (); private readonly static IReadOnlyList _standardColumns = new string [] { Open, High, Low, Close, LastPrice, Volume, AskOpen, AskHigh, AskLow, AskClose, AskPrice, AskSize, Quantity, Suspicious, BidOpen, BidHigh, BidLow, BidClose, BidPrice, BidSize, Exchange, OpenInterest }; private readonly Symbol _symbol; private readonly bool _isFundamentalType; private readonly Dictionary _series; private readonly IEnumerable _members = Enumerable.Empty(); /// /// Gets true if this is a custom data request, false for normal QC data /// public bool IsCustomData { get; } /// /// Implied levels of a multi index pandas.Series (depends on the security type) /// public int Levels { get; } = 2; /// /// Initializes an instance of /// public PandasData(object data) { if (_pandas == null) { using (Py.GIL()) { // Use our PandasMapper class that modifies pandas indexing to support tickers, symbols and SIDs _pandas = Py.Import("PandasMapper"); _seriesFactory = _pandas.GetAttr("Series"); _dataFrameFactory = _pandas.GetAttr("DataFrame"); using var multiIndex = _pandas.GetAttr("MultiIndex"); _multiIndexFactory = multiIndex.GetAttr("from_tuples"); _empty = new PyString(string.Empty); var time = new PyString("time"); var symbol = new PyString("symbol"); var expiry = new PyString("expiry"); _defaultNames = new PyList(new PyObject[] { expiry, new PyString("strike"), new PyString("type"), symbol, time }); _level2Names = new PyList(new PyObject[] { symbol, time }); _level3Names = new PyList(new PyObject[] { expiry, symbol, time }); } } // in the case we get a list/collection of data we take the first data point to determine the type // but it's also possible to get a data which supports enumerating we don't care about those cases if (data is not IBaseData && data is IEnumerable enumerable) { foreach (var item in enumerable) { data = item; break; } } var type = data.GetType(); _isFundamentalType = type == typeof(Fundamental); _symbol = ((IBaseData)data).Symbol; IsCustomData = Extensions.IsCustomDataType(_symbol, type); if (_symbol.SecurityType == SecurityType.Future) Levels = 3; if (_symbol.SecurityType.IsOption()) Levels = 5; IEnumerable columns = _standardColumns; if (IsCustomData || ((IBaseData)data).DataType == MarketDataType.Auxiliary) { var keys = (data as DynamicData)?.GetStorageDictionary() // if this is a PythonData instance we add in '__typename' which we don't want into the data frame .Where(x => !x.Key.StartsWith("__", StringComparison.InvariantCulture)).ToHashSet(x => x.Key); // C# types that are not DynamicData type if (keys == null) { if (_membersByType.TryGetValue(type, out _members)) { keys = _members.ToHashSet(x => x.Name.ToLowerInvariant()); } else { var members = type.GetMembers().Where(x => x.MemberType == MemberTypes.Field || x.MemberType == MemberTypes.Property).ToList(); var duplicateKeys = members.GroupBy(x => x.Name.ToLowerInvariant()).Where(x => x.Count() > 1).Select(x => x.Key); foreach (var duplicateKey in duplicateKeys) { throw new ArgumentException($"PandasData.ctor(): {Messages.PandasData.DuplicateKey(duplicateKey, type.FullName)}"); } // If the custom data derives from a Market Data (e.g. Tick, TradeBar, QuoteBar), exclude its keys keys = members.ToHashSet(x => x.Name.ToLowerInvariant()); keys.ExceptWith(_baseDataProperties); keys.ExceptWith(GetPropertiesNames(typeof(QuoteBar), type)); keys.ExceptWith(GetPropertiesNames(typeof(TradeBar), type)); keys.ExceptWith(GetPropertiesNames(typeof(Tick), type)); keys.Add("value"); _members = members.Where(x => keys.Contains(x.Name.ToLowerInvariant())).ToList(); _membersByType.TryAdd(type, _members); } } var customColumns = new HashSet(columns); customColumns.Add("value"); customColumns.UnionWith(keys); columns = customColumns; } _series = columns.ToDictionary(k => k, v => new Serie()); } /// /// Adds security data object to the end of the lists /// /// object that contains security data public void Add(object baseData) { var endTime = ((IBaseData)baseData).EndTime; foreach (var member in _members) { // TODO field/property.GetValue is expensive var key = member.Name.ToLowerInvariant(); var propertyMember = member as PropertyInfo; if (propertyMember != null) { var propertyValue = propertyMember.GetValue(baseData); if (_isFundamentalType && propertyMember.PropertyType.IsAssignableTo(typeof(FundamentalTimeDependentProperty))) { propertyValue = ((FundamentalTimeDependentProperty)propertyValue).Clone(new FixedTimeProvider(endTime)); } AddToSeries(key, endTime, propertyValue); continue; } else { var fieldMember = member as FieldInfo; if (fieldMember != null) { AddToSeries(key, endTime, fieldMember.GetValue(baseData)); } } } var storage = (baseData as DynamicData)?.GetStorageDictionary(); if (storage != null) { var value = ((IBaseData) baseData).Value; AddToSeries("value", endTime, value); foreach (var kvp in storage.Where(x => x.Key != "value" // if this is a PythonData instance we add in '__typename' which we don't want into the data frame && !x.Key.StartsWith("__", StringComparison.InvariantCulture))) { AddToSeries(kvp.Key, endTime, kvp.Value); } } else { var tick = baseData as Tick; if (tick != null) { AddTick(tick); } else { var tradeBar = baseData as TradeBar; var quoteBar = baseData as QuoteBar; Add(tradeBar, quoteBar); } } } /// /// Adds Lean data objects to the end of the lists /// /// object that contains trade bar information of the security /// object that contains quote bar information of the security public void Add(TradeBar tradeBar, QuoteBar quoteBar) { if (tradeBar != null) { var time = tradeBar.EndTime; GetSerie(Open).Add(time, tradeBar.Open); GetSerie(High).Add(time, tradeBar.High); GetSerie(Low).Add(time, tradeBar.Low); GetSerie(Close).Add(time, tradeBar.Close); GetSerie(Volume).Add(time, tradeBar.Volume); } if (quoteBar != null) { var time = quoteBar.EndTime; if (tradeBar == null) { GetSerie(Open).Add(time, quoteBar.Open); GetSerie(High).Add(time, quoteBar.High); GetSerie(Low).Add(time, quoteBar.Low); GetSerie(Close).Add(time, quoteBar.Close); } if (quoteBar.Ask != null) { GetSerie(AskOpen).Add(time, quoteBar.Ask.Open); GetSerie(AskHigh).Add(time, quoteBar.Ask.High); GetSerie(AskLow).Add(time, quoteBar.Ask.Low); GetSerie(AskClose).Add(time, quoteBar.Ask.Close); GetSerie(AskSize).Add(time, quoteBar.LastAskSize); } if (quoteBar.Bid != null) { GetSerie(BidOpen).Add(time, quoteBar.Bid.Open); GetSerie(BidHigh).Add(time, quoteBar.Bid.High); GetSerie(BidLow).Add(time, quoteBar.Bid.Low); GetSerie(BidClose).Add(time, quoteBar.Bid.Close); GetSerie(BidSize).Add(time, quoteBar.LastBidSize); } } } /// /// Adds a tick data point to this pandas collection /// /// object that contains tick information of the security public void AddTick(Tick tick) { var time = tick.EndTime; // We will fill some series with null for tick types that don't have a value for that series, so that we make sure // the indices are the same for every tick series. if (tick.TickType == TickType.Quote) { GetSerie(AskPrice).Add(time, tick.AskPrice); GetSerie(AskSize).Add(time, tick.AskSize); GetSerie(BidPrice).Add(time, tick.BidPrice); GetSerie(BidSize).Add(time, tick.BidSize); } else { // Trade and open interest ticks don't have these values, so we'll fill them with null. GetSerie(AskPrice).Add(time, null); GetSerie(AskSize).Add(time, null); GetSerie(BidPrice).Add(time, null); GetSerie(BidSize).Add(time, null); } GetSerie(Exchange).Add(time, tick.Exchange); GetSerie(Suspicious).Add(time, tick.Suspicious); GetSerie(Quantity).Add(time, tick.Quantity); if (tick.TickType == TickType.OpenInterest) { GetSerie(OpenInterest).Add(time, tick.Value); GetSerie(LastPrice).Add(time, null); } else { GetSerie(LastPrice).Add(time, tick.Value); GetSerie(OpenInterest).Add(time, null); } } /// /// Get the pandas.DataFrame of the current state /// /// Number of levels of the multi index /// pandas.DataFrame object public PyObject ToPandasDataFrame(int levels = 2) { List list; var symbol = _symbol.ID.ToString().ToPython(); // Create the index labels var names = _defaultNames; if (levels == 2) { // symbol, time names = _level2Names; list = new List { symbol, _empty }; } else if (levels == 3) { // expiry, symbol, time names = _level3Names; list = new List { _symbol.ID.Date.ToPython(), symbol, _empty }; } else { list = new List { _empty, _empty, _empty, symbol, _empty }; if (_symbol.SecurityType == SecurityType.Future) { list[0] = _symbol.ID.Date.ToPython(); } else if (_symbol.SecurityType.IsOption()) { list[0] = _symbol.ID.Date.ToPython(); list[1] = _symbol.ID.StrikePrice.ToPython(); list[2] = _symbol.ID.OptionRight.ToString().ToPython(); } } // creating the pandas MultiIndex is expensive so we keep a cash var indexCache = new Dictionary, PyObject>(new ListComparer()); // Returns a dictionary keyed by column name where values are pandas.Series objects using var pyDict = new PyDict(); foreach (var kvp in _series) { if (kvp.Value.ShouldFilter) continue; if (!indexCache.TryGetValue(kvp.Value.Times, out var index)) { using var tuples = kvp.Value.Times.Select(time => CreateTupleIndex(time, list)).ToPyListUnSafe(); using var namesDic = Py.kw("names", names); indexCache[kvp.Value.Times] = index = _multiIndexFactory.Invoke(new[] { tuples }, namesDic); foreach (var pyObject in tuples) { pyObject.Dispose(); } } // Adds pandas.Series value keyed by the column name using var pyvalues = new PyList(); for (var i = 0; i < kvp.Value.Values.Count; i++) { using var pyObject = kvp.Value.Values[i].ToPython(); pyvalues.Append(pyObject); } using var series = _seriesFactory.Invoke(pyvalues, index); pyDict.SetItem(kvp.Key, series); } _series.Clear(); foreach (var kvp in indexCache) { kvp.Value.Dispose(); } for (var i = 0; i < list.Count; i++) { DisposeIfNotEmpty(list[i]); } // Create the DataFrame var result = _dataFrameFactory.Invoke(pyDict); foreach (var item in pyDict) { item.Dispose(); } return result; } /// /// Only dipose of the PyObject if it was set to something different than empty /// private static void DisposeIfNotEmpty(PyObject pyObject) { if (!ReferenceEquals(pyObject, _empty)) { pyObject.Dispose(); } } /// /// Create a new tuple index /// private static PyTuple CreateTupleIndex(DateTime index, List list) { DisposeIfNotEmpty(list[list.Count - 1]); list[list.Count - 1] = index.ToPython(); return new PyTuple(list.ToArray()); } /// /// Adds data to dictionary /// /// The key of the value to get /// object to add to the value associated with the specific key /// to add to the value associated with the specific key. Can be null. private void AddToSeries(string key, DateTime time, object input) { var serie = GetSerie(key); serie.Add(time, input); } private Serie GetSerie(string key) { if (!_series.TryGetValue(key, out var value)) { throw new ArgumentException($"PandasData.GetSerie(): {Messages.PandasData.KeyNotFoundInSeries(key)}"); } return value; } /// /// Get the lower-invariant name of properties of the type that a another type is assignable from /// /// The type that is assignable from /// The type that is assignable by /// List of string. Empty list if not assignable from private static IEnumerable GetPropertiesNames(Type baseType, Type type) { return baseType.IsAssignableFrom(type) ? baseType.GetProperties().Select(x => x.Name.ToLowerInvariant()) : Enumerable.Empty(); } private class Serie { private static readonly IFormatProvider InvariantCulture = CultureInfo.InvariantCulture; public bool ShouldFilter { get; set; } = true; public List Times { get; set; } = new(); public List Values { get; set; } = new(); public void Add(DateTime time, object input) { var value = input is decimal ? Convert.ToDouble(input, InvariantCulture) : input; if (ShouldFilter) { // we need at least 1 valid entry for the series not to get filtered if (value is double) { if (!((double)value).IsNaNOrZero()) { ShouldFilter = false; } } else if (value is string) { if (!string.IsNullOrWhiteSpace((string)value)) { ShouldFilter = false; } } else if (value is bool) { if ((bool)value) { ShouldFilter = false; } } else if (value != null) { ShouldFilter = false; } } Values.Add(value); Times.Add(time); } public void Add(DateTime time, decimal input) { var value = Convert.ToDouble(input, InvariantCulture); if (ShouldFilter && !value.IsNaNOrZero()) { ShouldFilter = false; } Values.Add(value); Times.Add(time); } } private class FixedTimeProvider : ITimeProvider { private readonly DateTime _time; public DateTime GetUtcNow() => _time; public FixedTimeProvider(DateTime time) { _time = time; } } } }