/* * 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.Market; using QuantConnect.Util; using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; namespace QuantConnect.Python { /// /// Organizes a list of data to create pandas.DataFrames /// public class PandasData { private static dynamic _pandas; private readonly static HashSet _baseDataProperties = typeof(BaseData).GetProperties().ToHashSet(x => x.Name.ToLowerInvariant()); private readonly static ConcurrentDictionary> _membersByType = new ConcurrentDictionary>(); private readonly Symbol _symbol; private readonly Dictionary, List>> _series; private readonly List _members; /// /// 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"); } } // 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(); IsCustomData = type.Namespace != typeof(Bar).Namespace; _members = new List(); _symbol = ((IBaseData)data).Symbol; if (_symbol.SecurityType == SecurityType.Future) Levels = 3; if (_symbol.SecurityType.IsOption()) Levels = 5; var columns = new HashSet { "open", "high", "low", "close", "lastprice", "volume", "askopen", "askhigh", "asklow", "askclose", "askprice", "asksize", "quantity", "suspicious", "bidopen", "bidhigh", "bidlow", "bidclose", "bidprice", "bidsize", "exchange", "openinterest" }; if (IsCustomData) { var keys = (data as DynamicData)?.GetStorageDictionary().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(): More than one \'{duplicateKey}\' member was found in \'{type.FullName}\' class."); } // 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); } } columns.Add("value"); columns.UnionWith(keys); } _series = columns.ToDictionary(k => k, v => Tuple.Create(new List(), new List())); } /// /// Adds security data object to the end of the lists /// /// object that contains security data public void Add(object baseData) { foreach (var member in _members) { var key = member.Name.ToLowerInvariant(); var endTime = ((IBaseData) baseData).EndTime; var propertyMember = member as PropertyInfo; if (propertyMember != null) { AddToSeries(key, endTime, propertyMember.GetValue(baseData)); continue; } var fieldMember = member as FieldInfo; if (fieldMember != null) { AddToSeries(key, endTime, fieldMember.GetValue(baseData)); } } var storage = (baseData as DynamicData)?.GetStorageDictionary(); if (storage != null) { var endTime = ((IBaseData) baseData).EndTime; var value = ((IBaseData) baseData).Value; AddToSeries("value", endTime, value); foreach (var kvp in storage.Where(x => x.Key != "value")) { AddToSeries(kvp.Key, endTime, kvp.Value); } } else { var ticks = new List { baseData as Tick }; var tradeBar = baseData as TradeBar; var quoteBar = baseData as QuoteBar; Add(ticks, tradeBar, quoteBar); } } /// /// Adds Lean data objects to the end of the lists /// /// List of object that contains tick information of the security /// object that contains trade bar information of the security /// object that contains quote bar information of the security public void Add(IEnumerable ticks, TradeBar tradeBar, QuoteBar quoteBar) { if (tradeBar != null) { var time = tradeBar.EndTime; AddToSeries("open", time, tradeBar.Open); AddToSeries("high", time, tradeBar.High); AddToSeries("low", time, tradeBar.Low); AddToSeries("close", time, tradeBar.Close); AddToSeries("volume", time, tradeBar.Volume); } if (quoteBar != null) { var time = quoteBar.EndTime; if (tradeBar == null) { AddToSeries("open", time, quoteBar.Open); AddToSeries("high", time, quoteBar.High); AddToSeries("low", time, quoteBar.Low); AddToSeries("close", time, quoteBar.Close); } if (quoteBar.Ask != null) { AddToSeries("askopen", time, quoteBar.Ask.Open); AddToSeries("askhigh", time, quoteBar.Ask.High); AddToSeries("asklow", time, quoteBar.Ask.Low); AddToSeries("askclose", time, quoteBar.Ask.Close); AddToSeries("asksize", time, quoteBar.LastAskSize); } if (quoteBar.Bid != null) { AddToSeries("bidopen", time, quoteBar.Bid.Open); AddToSeries("bidhigh", time, quoteBar.Bid.High); AddToSeries("bidlow", time, quoteBar.Bid.Low); AddToSeries("bidclose", time, quoteBar.Bid.Close); AddToSeries("bidsize", time, quoteBar.LastBidSize); } } if (ticks != null) { foreach (var tick in ticks) { if (tick == null) continue; var time = tick.EndTime; var column = tick.TickType == TickType.OpenInterest ? "openinterest" : "lastprice"; if (tick.TickType == TickType.Quote) { AddToSeries("askprice", time, tick.AskPrice); AddToSeries("asksize", time, tick.AskSize); AddToSeries("bidprice", time, tick.BidPrice); AddToSeries("bidsize", time, tick.BidSize); } AddToSeries("exchange", time, tick.Exchange); AddToSeries("suspicious", time, tick.Suspicious); AddToSeries("quantity", time, tick.Quantity); AddToSeries(column, time, tick.LastPrice); } } } /// /// Get the pandas.DataFrame of the current state /// /// Number of levels of the multi index /// pandas.DataFrame object public PyObject ToPandasDataFrame(int levels = 2) { var empty = new PyString(string.Empty); var list = Enumerable.Repeat(empty, 5).ToList(); list[3] = _symbol.ID.ToString().ToPython(); if (_symbol.SecurityType == SecurityType.Future) { list[0] = _symbol.ID.Date.ToPython(); list[3] = _symbol.ID.ToString().ToPython(); } if (_symbol.SecurityType.IsOption()) { list[0] = _symbol.ID.Date.ToPython(); list[1] = _symbol.ID.StrikePrice.ToPython(); list[2] = _symbol.ID.OptionRight.ToString().ToPython(); list[3] = _symbol.ID.ToString().ToPython(); } // Create the index labels var names = "expiry,strike,type,symbol,time"; if (levels == 2) { names = "symbol,time"; list.RemoveRange(0, 3); } if (levels == 3) { names = "expiry,symbol,time"; list.RemoveRange(1, 2); } Func filter = x => { var isNaNOrZero = x is double && ((double)x).IsNaNOrZero(); var isNullOrWhiteSpace = x is string && string.IsNullOrWhiteSpace((string)x); var isFalse = x is bool && !(bool)x; return x == null || isNaNOrZero || isNullOrWhiteSpace || isFalse; }; Func selector = x => { list[list.Count - 1] = x.ToPython(); return new PyTuple(list.ToArray()); }; // creating the pandas MultiIndex is expensive so we keep a cash var indexCache = new Dictionary, dynamic>(new ListComparer()); using (Py.GIL()) { // Returns a dictionary keyed by column name where values are pandas.Series objects var pyDict = new PyDict(); var splitNames = names.Split(','); foreach (var kvp in _series) { var values = kvp.Value.Item2; if (values.All(filter)) continue; dynamic index; if (!indexCache.TryGetValue(kvp.Value.Item1, out index)) { var tuples = kvp.Value.Item1.Select(selector).ToArray(); index = _pandas.MultiIndex.from_tuples(tuples, names: splitNames); indexCache[kvp.Value.Item1] = index; } // Adds pandas.Series value keyed by the column name pyDict.SetItem(kvp.Key, _pandas.Series(values, index)); } _series.Clear(); // Create the DataFrame return _pandas.DataFrame(pyDict); } } /// /// 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) { Tuple, List> value; if (_series.TryGetValue(key, out value)) { value.Item1.Add(time); value.Item2.Add(input is decimal ? input.ConvertInvariant() : input); } else { throw new ArgumentException($"PandasData.AddToSeries(): {key} key does not exist in series dictionary."); } } /// /// 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(); } } }