Files
2026-07-13 13:02:50 +08:00

148 lines
5.9 KiB
Python

# 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.
'''
Lean Pandas Remapper
Wraps key indexing functions of Pandas to remap keys to SIDs when accessing dataframes.
Allowing support for indexing of Lean created Indexes with tickers like "SPY", Symbol objs, and SIDs
'''
import pandas as pd
from pandas.core.indexes.frozen import FrozenList as pdFrozenList
from clr import AddReference
AddReference("QuantConnect.Common")
from QuantConnect import *
class PandasColumn(str):
'''
PandasColumn is a wrapper class for a pandas column that allows for the column to be used as a key
and properly compared to strings, regardless of whether it's a C# or Python string
(since the hash of a C# string and the same Python string are different).
'''
def __new__(cls, key):
return super().__new__(cls, key)
def __eq__(self, other):
# We need this since Lean created data frames might contain Symbol objects in the indexes
if type(other) is Symbol:
return False
# For non-strings str.__eq__ returns NotImplemented, which Python resolves to False
return super().__eq__(other)
def __hash__(self):
return super().__hash__()
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 tuple:
return tuple(mapper(x) for x in key)
if keyType is str:
kvp = SymbolCache.try_get_symbol(key, None)
if kvp[0]:
return kvp[1]
return key
if keyType is list:
return [mapper(x) for x in key]
if keyType is dict:
return {k: mapper(v) for k, v in key.items()}
return key
def wrap_keyerror_function(f):
'''Wraps function f with wrapped_function, used for functions that throw KeyError when not found.
wrapped_function converts the args / kwargs to use alternative index keys and then calls the function.
If this fails we fall back to the original key and try it as well, if they both fail we throw our error.
'''
def wrapped_function(*args, **kwargs):
# Map args & kwargs and execute function
try:
newargs = args
newkwargs = kwargs
if len(args) > 1:
newargs = mapper(args)
if len(kwargs) > 0:
newkwargs = mapper(kwargs)
return f(*newargs, **newkwargs)
except KeyError as e:
pass
# Execute original
# Allows for df, Series, etc indexing for keys like 'SPY' if they exist
try:
return f(*args, **kwargs)
except KeyError as e:
mKey = [str(arg) for arg in newargs if isinstance(arg, str) or isinstance(arg, Symbol)]
oKey = [str(arg) for arg in args if isinstance(arg, str) or isinstance(arg, Symbol)]
raise KeyError(f"No key found for either mapped or original key. Mapped Key: {mKey}; Original Key: {oKey}")
wrapped_function.__name__ = f.__name__
return wrapped_function
def wrap_bool_function(f):
'''Wraps function f with wrapped_function, used for functions that reply true/false if key is found.
wrapped_function attempts with the original args, if its false, it converts the args / kwargs to use
alternative index keys and then attempts with the mapped args.
'''
def wrapped_function(*args, **kwargs):
# Try the original args; if true just return true
originalResult = f(*args, **kwargs)
if originalResult:
return originalResult
# Try our mapped args; return this result regardless
newargs = args
newkwargs = kwargs
if len(args) > 1:
newargs = mapper(args)
if len(kwargs) > 0:
newkwargs = mapper(kwargs)
return f(*newargs, **newkwargs)
wrapped_function.__name__ = f.__name__
return wrapped_function
# Wrap all core indexing functions that are shared, yet still throw key errors if index not found
pd.core.indexing._LocationIndexer.__getitem__ = wrap_keyerror_function(pd.core.indexing._LocationIndexer.__getitem__)
pd.core.indexing._ScalarAccessIndexer.__getitem__ = wrap_keyerror_function(pd.core.indexing._ScalarAccessIndexer.__getitem__)
pd.core.indexes.base.Index.get_loc = wrap_keyerror_function(pd.core.indexes.base.Index.get_loc)
# Wrap our DF _getitem__ as well, even though most pathways go through the above functions
# There are cases like indexing with an array that need to be mapped earlier to stop KeyError from arising
pd.core.frame.DataFrame.__getitem__ = wrap_keyerror_function(pd.core.frame.DataFrame.__getitem__)
# For older version of pandas we may need to wrap extra functions
if (int(pd.__version__.split('.')[0]) < 1):
pd.core.indexes.base.Index.get_value = wrap_keyerror_function(pd.core.indexes.base.Index.get_value)
# Special cases where we need to wrap a function that won't throw a keyerror when not found but instead returns true or false
# Wrap __contains__ to support Python syntax like 'SPY' in DataFrame
pd.core.indexes.base.Index.__contains__ = wrap_bool_function(pd.core.indexes.base.Index.__contains__)
# For compatibility with PandasData.cs usage of this module (Previously wrapped classes)
FrozenList = pdFrozenList
Index = pd.Index
MultiIndex = pd.MultiIndex
Series = pd.Series
DataFrame = pd.DataFrame