/*
* 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 Apache.Arrow;
using Apache.Arrow.Ipc;
using Apache.Arrow.Memory;
using Python.Runtime;
using QuantConnect.Data;
using QuantConnect.Indicators;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using QuantConnect.Data.Market;
using QuantConnect.Util;
namespace QuantConnect.Python
{
///
/// Converts data into a Pandas DataFrame
///
public class PandasConverter
{
private static dynamic _pandas;
private static dynamic _pa;
private static dynamic _np;
private static dynamic _filter;
private static dynamic _optionIndexes;
private static dynamic _optionFinalIndexes;
private static PyList _defaultIndexes;
private static HashSet _baseDataProperties = typeof(BaseData).GetProperties().ToHashSet(x => x.Name.ToLowerInvariant());
private MemoryAllocator _allocator = new PandasArrowMemoryAllocator();
// Re-use MemoryStream to avoid having to reallocate every time for every new DataFrame we create
private MemoryStream _ms = new MemoryStream();
private StringArray.Builder _tradeBarSymbols = new StringArray.Builder();
private TimestampArray.Builder _tradeBarTimes = new TimestampArray.Builder();
private TimestampArray.Builder _tradeBarExpiry = new TimestampArray.Builder();
private DoubleArray.Builder _tradeBarStrike = new DoubleArray.Builder();
private StringArray.Builder _tradeBarRight = new StringArray.Builder();
private DoubleArray.Builder _tradeBarOpen = new DoubleArray.Builder();
private DoubleArray.Builder _tradeBarHigh = new DoubleArray.Builder();
private DoubleArray.Builder _tradeBarLow = new DoubleArray.Builder();
private DoubleArray.Builder _tradeBarClose = new DoubleArray.Builder();
private DoubleArray.Builder _tradeBarVolume = new DoubleArray.Builder();
private StringArray.Builder _quoteBarSymbols = new StringArray.Builder();
private TimestampArray.Builder _quoteBarTimes = new TimestampArray.Builder();
private TimestampArray.Builder _quoteBarExpiry = new TimestampArray.Builder();
private DoubleArray.Builder _quoteBarStrike = new DoubleArray.Builder();
private StringArray.Builder _quoteBarRight = new StringArray.Builder();
private DoubleArray.Builder _quoteBarBidOpen = new DoubleArray.Builder();
private DoubleArray.Builder _quoteBarBidHigh = new DoubleArray.Builder();
private DoubleArray.Builder _quoteBarBidLow = new DoubleArray.Builder();
private DoubleArray.Builder _quoteBarBidClose = new DoubleArray.Builder();
private DoubleArray.Builder _quoteBarBidVolume = new DoubleArray.Builder();
private DoubleArray.Builder _quoteBarAskOpen = new DoubleArray.Builder();
private DoubleArray.Builder _quoteBarAskHigh = new DoubleArray.Builder();
private DoubleArray.Builder _quoteBarAskLow = new DoubleArray.Builder();
private DoubleArray.Builder _quoteBarAskClose = new DoubleArray.Builder();
private DoubleArray.Builder _quoteBarAskVolume = new DoubleArray.Builder();
private StringArray.Builder _tickSymbols = new StringArray.Builder();
private TimestampArray.Builder _tickTimes = new TimestampArray.Builder();
private TimestampArray.Builder _tickExpiry = new TimestampArray.Builder();
private DoubleArray.Builder _tickStrike = new DoubleArray.Builder();
private StringArray.Builder _tickRight = new StringArray.Builder();
private StringArray.Builder _tickExchange = new StringArray.Builder();
private BooleanArray.Builder _tickSuspicious = new BooleanArray.Builder();
private DoubleArray.Builder _tickValue = new DoubleArray.Builder();
private DoubleArray.Builder _tickQuantity = new DoubleArray.Builder();
private DoubleArray.Builder _tickBidPrice = new DoubleArray.Builder();
private DoubleArray.Builder _tickBidSize = new DoubleArray.Builder();
private DoubleArray.Builder _tickAskPrice = new DoubleArray.Builder();
private DoubleArray.Builder _tickAskSize = new DoubleArray.Builder();
private TimestampArray.Builder _openInterestTimes = new TimestampArray.Builder();
private StringArray.Builder _openInterestSymbols = new StringArray.Builder();
private TimestampArray.Builder _openInterestExpiry = new TimestampArray.Builder();
private DoubleArray.Builder _openInterestStrike = new DoubleArray.Builder();
private StringArray.Builder _openInterestRight = new StringArray.Builder();
private DoubleArray.Builder _openInterestValue = new DoubleArray.Builder();
private Dictionary> _customDataMembers = new Dictionary>();
private Dictionary> _customDataBuilders = new Dictionary>();
private List _customDataSymbols = new List();
private List _customDataTimes = new List();
private Dictionary> _customDataObjects = new Dictionary>();
///
/// Creates an instance of .
///
public PandasConverter()
{
if (_pandas == null)
{
using (Py.GIL())
{
// Cache the indexes to skip calling python as much as we can
_defaultIndexes = new PyList(new []{ new PyString("symbol"), new PyString("time") });
// pyarrow is used to create a DataFrame without having to serialize/deserialize/box multiple times.
// It also allows us to construct a DataFrame as a zero-copy operation. The DataFrame will be eventually
// copied to enable mutability of underlying data, but it will be done efficiently.
_pa = PythonEngine.ImportModule("pyarrow");
_np = PythonEngine.ImportModule("numpy");
_filter = new PyList(new[] { 0.ToPython(), string.Empty.ToPython(), false.ToPython() });
_optionIndexes = new PyList(new[] { new PyString("strike"), new PyString("type") });
_optionFinalIndexes = new PyList(new[]
{
new PyString("strike"),
new PyString("type"),
new PyString("symbol"),
new PyString("time")
});
// this python Remapper class will work as a proxy and adjust the
// input to its methods using the provided 'mapper' callable object
_pandas = PythonEngine.ModuleFromString("remapper",
@"import pandas as pd
from pandas.core.resample import Resampler, DatetimeIndexResampler, PeriodIndexResampler, TimedeltaIndexResampler
from pandas.core.groupby.generic import DataFrameGroupBy, SeriesGroupBy
from pandas.core.indexes.frozen import FrozenList as pdFrozenList
from pandas.core.window import Expanding, EWM, Rolling, Window
from pandas.core.computation.ops import UndefinedVariableError
from inspect import getmembers, isfunction, isgenerator
from functools import partial
from sys import modules
from clr import AddReference
AddReference(""QuantConnect.Common"")
from QuantConnect import *
def mapper(key):
'''Maps a Symbol object or a Symbol Ticker (string) to the string representation of
Symbol SecurityIdentifier. If cannot map, returns the object
'''
keyType = type(key)
if keyType is Symbol:
return str(key.ID)
if keyType is str:
kvp = SymbolCache.TryGetSymbol(key, None)
if kvp[0]:
return str(kvp[1].ID)
if keyType is list:
return [mapper(x) for x in key]
if keyType is tuple:
return tuple([mapper(x) for x in key])
if keyType is dict:
return {k:mapper(v) for k,v in key.items()}
return key
def try_wrap_as_index(obj):
'''Tries to wrap object if it is one of pandas' index objects.'''
objType = type(obj)
if objType is pd.Index:
return True, Index(obj)
if objType is pd.MultiIndex:
result = object.__new__(MultiIndex)
result._set_levels(obj.levels, copy=obj.copy, validate=False)
result._set_codes(obj.codes, copy=obj.copy, validate=False)
result._set_names(obj.names)
result.sortorder = obj.sortorder
return True, result
if objType is pdFrozenList:
return True, FrozenList(obj)
return False, obj
def try_wrap_as_pandas(obj):
'''Tries to wrap object if it is a pandas' object.'''
success, obj = try_wrap_as_index(obj)
if success:
return success, obj
objType = type(obj)
if objType is pd.DataFrame:
return True, DataFrame(data=obj)
if objType is pd.Series:
return True, Series(data=obj)
if objType is tuple:
anySuccess = False
results = list()
for item in obj:
success, result = try_wrap_as_pandas(item)
anySuccess |= success
results.append(result)
if anySuccess:
return True, tuple(results)
return False, obj
def try_wrap_resampler(obj, self):
'''Tries to wrap object if it is a pandas' Resampler object.'''
if not isinstance(obj, Resampler):
return False, obj
klass = CreateWrapperClass(type(obj))
return True, klass(self, groupby=obj.groupby, kind=obj.kind, axis=obj.axis)
def wrap_function(f):
'''Wraps function f with g.
Function g converts the args/kwargs to use alternative index keys
and the result of the f function call to the wrapper objects
'''
def g(*args, **kwargs):
if len(args) > 1:
args = mapper(args)
if len(kwargs) > 0:
kwargs = mapper(kwargs)
try:
result = f(*args, **kwargs)
except UndefinedVariableError as e:
# query/eval methods needs to look for a scope variable at a higher level
# since the wrapper classes are children of pandas classes
kwargs['level'] = kwargs.pop('level', 0) + 1
result = f(*args, **kwargs)
success, result = try_wrap_as_pandas(result)
if success:
return result
success, result = try_wrap_resampler(result, args[0])
if success:
return result
if isgenerator(result):
return ( (k, try_wrap_as_pandas(v)[1]) for k, v in result)
return result
g.__name__ = f.__name__
return g
def wrap_special_function(name, cls, fcls, gcls = None):
'''Replaces the special function of a given class by g that wraps fcls
This is how pandas implements them.
gcls represents an alternative for fcls
if the keyword argument has 'win_type' key for the Rolling/Window case
'''
fcls = CreateWrapperClass(fcls)
if gcls is not None:
gcls = CreateWrapperClass(fcls)
def g(*args, **kwargs):
if kwargs.get('win_type', None):
return gcls(*args, **kwargs)
return fcls(*args, **kwargs)
g.__name__ = name
setattr(cls, g.__name__, g)
def CreateWrapperClass(cls: type):
'''Creates wrapper classes.
Members of the original class are wrapped to allow alternative index look-up
'''
# Define a new class
klass = type(f'{cls.__name__}', (cls,) + cls.__bases__, dict(cls.__dict__))
def g(self, name):
'''Wrap '__getattribute__' to handle indices
Only need to wrap columns, index and levels attributes
'''
attr = object.__getattribute__(self, name)
if name in ['columns', 'index', 'levels']:
_, attr = try_wrap_as_index(attr)
return attr
g.__name__ = '__getattribute__'
g.__qualname__ = g.__name__
setattr(klass, g.__name__, g)
def wrap_union(f):
'''Wraps function f (union) with g.
Special case: The union method from index objects needs to
receive pandas' index objects to avoid infity recursion.
Function g converts the args/kwargs objects to one of pandas index objects
and the result of the f function call back to wrapper indexes objects
'''
def unwrap_index(obj):
'''Tries to unwrap object if it is one of this module wrapper's index objects.'''
objType = type(obj)
if objType is Index:
return pd.Index(obj)
if objType is MultiIndex:
result = object.__new__(pd.MultiIndex)
result._set_levels(obj.levels, copy=obj.copy, validate=False)
result._set_codes(obj.codes, copy=obj.copy, validate=False)
result._set_names(obj.names)
result.sortorder = obj.sortorder
return result
if objType is FrozenList:
return pdFrozenList(obj)
return obj
def g(*args, **kwargs):
args = tuple([unwrap_index(x) for x in args])
result = f(*args, **kwargs)
_, result = try_wrap_as_index(result)
return result
g.__name__ = f.__name__
return g
# We allow the wraopping of slot methods that are not inherited from object
# It will include operation methods like __add__ and __contains__
allow_list = set(x for x in dir(klass) if x.startswith('__')) - set(dir(object))
# Wrap class members of the newly created class
for name, member in getmembers(klass):
if name.startswith('_') and name not in allow_list:
continue
if isfunction(member):
if name == 'union':
member = wrap_union(member)
else:
member = wrap_function(member)
setattr(klass, name, member)
elif type(member) is property:
if type(member.fget) is partial:
func = CreateWrapperClass(member.fget.func)
fget = partial(func, name)
else:
fget = wrap_function(member.fget)
member = property(fget, member.fset, member.fdel, member.__doc__)
setattr(klass, name, member)
return klass
FrozenList = CreateWrapperClass(pdFrozenList)
Index = CreateWrapperClass(pd.Index)
MultiIndex = CreateWrapperClass(pd.MultiIndex)
Series = CreateWrapperClass(pd.Series)
DataFrame = CreateWrapperClass(pd.DataFrame)
wrap_special_function('groupby', Series, SeriesGroupBy)
wrap_special_function('groupby', DataFrame, DataFrameGroupBy)
wrap_special_function('ewm', Series, EWM)
wrap_special_function('ewm', DataFrame, EWM)
wrap_special_function('expanding', Series, Expanding)
wrap_special_function('expanding', DataFrame, Expanding)
wrap_special_function('rolling', Series, Rolling, Window)
wrap_special_function('rolling', DataFrame, Rolling, Window)
CreateSeries = pd.Series
setattr(modules[__name__], 'concat', wrap_function(pd.concat))");
}
}
}
///
/// Converts an enumerable of in a pandas.DataFrame
///
/// Enumerable of
/// containing a pandas.DataFrame
public PyObject GetDataFrame(IEnumerable data)
{
// Cleans up any resources we've used to allow for the next generation of DataFrames
// to be created with potentially zero allocations.
ClearBuilders();
var hasTrades = false;
var hasQuotes = false;
var hasSuspicious = false;
var hasExpiry = false;
var hasOption = false;
var tickHasTrades = false;
var tickHasQuotes = false;
var tickHasOpenInterest = false;
foreach (var slice in data)
{
// Add Quote/Tick symbols separately since they could potentially be dropped
// from the Slice.Keys call if only quotes were provided to the Slice.
// Related issues:
// https://github.com/QuantConnect/Lean/issues/4205
// https://github.com/QuantConnect/Lean/issues/4196
var symbols = slice.Keys.ToHashSet();
symbols.UnionWith(slice.QuoteBars.Keys);
symbols.UnionWith(slice.Ticks.Keys);
foreach (var symbol in symbols)
{
var tradeBar = slice.Bars.ContainsKey(symbol) && slice.Bars[symbol].GetType() == typeof(TradeBar) ? slice.Bars[symbol] : null;
var quoteBar = slice.QuoteBars.ContainsKey(symbol) && slice.QuoteBars[symbol].GetType() == typeof(QuoteBar) ? slice.QuoteBars[symbol] : null;
var ticks = slice.Ticks.ContainsKey(symbol) && slice.Ticks[symbol].GetType() == typeof(List) ? slice.Ticks[symbol] : null;
var sid = symbol.ID.ToString();
if (tradeBar != null)
{
hasTrades = true;
_tradeBarOpen.Append(ToDouble(tradeBar.Open));
_tradeBarHigh.Append(ToDouble(tradeBar.High));
_tradeBarLow.Append(ToDouble(tradeBar.Low));
_tradeBarClose.Append(ToDouble(tradeBar.Close));
_tradeBarVolume.Append(ToDouble(tradeBar.Volume));
_tradeBarSymbols.Append(sid);
_tradeBarTimes.Append(new DateTimeOffset(tradeBar.EndTime.Ticks, TimeSpan.Zero));
if (symbol.SecurityType == SecurityType.Future || symbol.SecurityType == SecurityType.Option)
{
hasExpiry = true;
_tradeBarExpiry.Append(new DateTimeOffset(symbol.ID.Date.Ticks, TimeSpan.Zero));
}
else
{
_tradeBarExpiry.AppendNull();
}
if (symbol.SecurityType == SecurityType.Option)
{
hasOption = true;
_tradeBarStrike.Append(ToDouble(symbol.ID.StrikePrice));
_tradeBarRight.Append(symbol.ID.OptionRight.ToString());
}
else
{
_tradeBarStrike.AppendNull();
_tradeBarRight.AppendNull();
}
}
if (quoteBar != null)
{
// To maintain old behavior and backwards compatibility, we will set the "OHLC" for TradeBars
// when no TradeBar exists in this timestep.
if (tradeBar == null)
{
_tradeBarOpen.Append(ToDouble(quoteBar.Open));
_tradeBarHigh.Append(ToDouble(quoteBar.High));
_tradeBarLow.Append(ToDouble(quoteBar.Low));
_tradeBarClose.Append(ToDouble(quoteBar.Close));
_tradeBarVolume.AppendNull();
_tradeBarSymbols.Append(sid);
_tradeBarTimes.Append(new DateTimeOffset(quoteBar.EndTime.Ticks, TimeSpan.Zero));
if (symbol.SecurityType == SecurityType.Future || symbol.SecurityType == SecurityType.Option)
{
hasExpiry = true;
_tradeBarExpiry.Append(new DateTimeOffset(symbol.ID.Date.Ticks, TimeSpan.Zero));
}
else
{
_tradeBarExpiry.AppendNull();
}
if (symbol.SecurityType == SecurityType.Option)
{
hasOption = true;
_tradeBarStrike.Append(ToDouble(symbol.ID.StrikePrice));
_tradeBarRight.Append(symbol.ID.OptionRight.ToString());
}
else
{
_tradeBarStrike.AppendNull();
_tradeBarRight.AppendNull();
}
}
if (quoteBar.Bid != null)
{
_quoteBarBidOpen.Append(ToDouble(quoteBar.Bid.Open));
_quoteBarBidHigh.Append(ToDouble(quoteBar.Bid.High));
_quoteBarBidLow.Append(ToDouble(quoteBar.Bid.Low));
_quoteBarBidClose.Append(ToDouble(quoteBar.Bid.Close));
_quoteBarBidVolume.Append(ToDouble(quoteBar.LastBidSize));
}
else
{
_quoteBarBidOpen.AppendNull();
_quoteBarBidHigh.AppendNull();
_quoteBarBidLow.AppendNull();
_quoteBarBidClose.AppendNull();
_quoteBarBidVolume.AppendNull();
}
if (quoteBar.Ask != null)
{
_quoteBarAskOpen.Append(ToDouble(quoteBar.Ask.Open));
_quoteBarAskHigh.Append(ToDouble(quoteBar.Ask.High));
_quoteBarAskLow.Append(ToDouble(quoteBar.Ask.Low));
_quoteBarAskClose.Append(ToDouble(quoteBar.Ask.Close));
_quoteBarAskVolume.Append(ToDouble(quoteBar.LastAskSize));
}
else
{
_quoteBarAskOpen.AppendNull();
_quoteBarAskHigh.AppendNull();
_quoteBarAskLow.AppendNull();
_quoteBarAskClose.AppendNull();
_quoteBarAskVolume.AppendNull();
}
hasQuotes = true;
_quoteBarSymbols.Append(sid);
_quoteBarTimes.Append(new DateTimeOffset(quoteBar.EndTime.Ticks, TimeSpan.Zero));
if (symbol.SecurityType == SecurityType.Future || symbol.SecurityType == SecurityType.Option)
{
hasExpiry = true;
_quoteBarExpiry.Append(new DateTimeOffset(symbol.ID.Date.Ticks, TimeSpan.Zero));
}
else
{
_quoteBarExpiry.AppendNull();
}
if (symbol.SecurityType == SecurityType.Option)
{
hasOption = true;
_quoteBarStrike.Append(ToDouble(symbol.ID.StrikePrice));
_quoteBarRight.Append(symbol.ID.OptionRight.ToString());
}
else
{
_quoteBarStrike.AppendNull();
_quoteBarRight.AppendNull();
}
}
if (ticks != null)
{
foreach (var tick in ticks)
{
if (tick.TickType == TickType.Trade || tick.TickType == TickType.Quote)
{
_tickSymbols.Append(sid);
_tickTimes.Append(new DateTimeOffset(tick.EndTime.Ticks, TimeSpan.Zero));
_tickValue.Append(ToDouble(tick.Value));
if (symbol.SecurityType == SecurityType.Future || symbol.SecurityType == SecurityType.Option)
{
hasExpiry = true;
_tickExpiry.Append(new DateTimeOffset(symbol.ID.Date.Ticks, TimeSpan.Zero));
}
else
{
_tickExpiry.AppendNull();
}
if (symbol.SecurityType == SecurityType.Option)
{
hasOption = true;
_tickStrike.Append(ToDouble(symbol.ID.StrikePrice));
_tickRight.Append(symbol.ID.OptionRight.ToString());
}
else
{
_tickStrike.AppendNull();
_tickRight.AppendNull();
}
}
if (tick.TickType == TickType.Trade)
{
tickHasTrades = true;
_tickQuantity.Append(ToDouble(tick.Quantity));
if (tick.Suspicious && !hasSuspicious)
{
hasSuspicious = true;
}
_tickSuspicious.Append(tick.Suspicious);
_tickExchange.Append(tick.Exchange);
_tickBidPrice.AppendNull();
_tickBidSize.AppendNull();
_tickAskPrice.AppendNull();
_tickAskSize.AppendNull();
}
else if (tick.TickType == TickType.Quote)
{
tickHasQuotes = true;
_tickQuantity.AppendNull();
_tickSuspicious.Append(tick.Suspicious);
_tickExchange.Append(tick.Exchange);
_tickBidPrice.Append(ToDouble(tick.BidPrice));
_tickBidSize.Append(ToDouble(tick.BidSize));
_tickAskPrice.Append(ToDouble(tick.AskPrice));
_tickAskSize.Append(ToDouble(tick.AskSize));
}
else
{
tickHasOpenInterest = true;
_openInterestTimes.Append(new DateTimeOffset(tick.EndTime.Ticks, TimeSpan.Zero));
_openInterestSymbols.Append(sid);
_openInterestValue.Append(ToDouble(tick.Value));
if (symbol.SecurityType == SecurityType.Future || symbol.SecurityType == SecurityType.Option)
{
hasExpiry = true;
_openInterestExpiry.Append(new DateTimeOffset(symbol.ID.Date.Ticks, TimeSpan.Zero));
}
else
{
_openInterestExpiry.AppendNull();
}
if (symbol.SecurityType == SecurityType.Option)
{
hasOption = true;
_openInterestStrike.Append(ToDouble(symbol.ID.StrikePrice));
_openInterestRight.Append(symbol.ID.OptionRight.ToString());
}
else
{
_openInterestStrike.AppendNull();
_openInterestRight.AppendNull();
}
}
}
}
if (tradeBar == null && quoteBar == null && ticks == null)
{
// If we've made it this far, we're dealing with an instance of custom data.
var baseData = (BaseData)slice[symbol];
var baseDataType = baseData.GetType();
var dynamicData = baseData as DynamicData;
var dynamicDataStorage = dynamicData?.GetStorageDictionary();
if (dynamicDataStorage != null)
{
dynamicDataStorage["value"] = dynamicData.Value;
}
var dynamicColumns = dynamicDataStorage?.Select(kvp => kvp.Key)
?.ToHashSet();
List customMembers;
if (dynamicColumns == null && !_customDataMembers.TryGetValue(baseDataType, out customMembers))
{
var members = baseDataType.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)
.ToList();
if (duplicateKeys.Count != 0)
{
throw new ArgumentException($"PandasConverter.GetDataFrame(): Duplicate keys \"{string.Join(", ", duplicateKeys)}\" were found in the class {baseDataType.FullName}");
}
// If the custom data derives from market data (i.e. Tick, TradeBar, QuoteBar), exclude its keys
var columns = members.Select(x => x.Name.ToLowerInvariant()).ToHashSet();
columns = columns.Except(_baseDataProperties)
.Except(GetPropertiesNames(typeof(QuoteBar), baseDataType))
.Except(GetPropertiesNames(typeof(TradeBar), baseDataType))
.Except(GetPropertiesNames(typeof(Tick), baseDataType))
.ToHashSet();
columns.Add("value");
_customDataMembers[baseDataType] = members.Where(x => columns.Contains(x.Name.ToLowerInvariant())).ToList();
}
if (dynamicColumns == null)
{
foreach (var member in _customDataMembers[baseDataType])
{
var columnName = member.Name.ToLowerInvariant();
var property = member as PropertyInfo;
var field = member as FieldInfo;
var memberType = property != null ? property.PropertyType : field.FieldType;
KeyValuePair builder;
if (!_customDataBuilders.TryGetValue(columnName, out builder))
{
builder = new KeyValuePair(memberType, CreateBuilder(memberType));
_customDataBuilders[columnName] = builder;
}
if (!AppendToBuilder(builder.Value, baseData, memberType, property, field))
{
List