Using exec()
or eval()
is fine if you will tightly control the code that is used as an input to those functions, but it can lead to unsafe code and is a lazy coding practice when there are other safer ways to do what you want.
Something like this could work where you parse the input, and use predefined functions to evaluate them:
First, define the functions that you'll use, and add them to a dictionary:
def gt(row, key, value)
'''Returns True if row[key] > value'''
return row[key] > value
def lt(row, key, value)
'''Returns True if row[key] < value'''
return row[key] < value
def andf(val1, val2):
'''Returns val1 and val2'''
return val1 and val2
def orf(val1, val2):
'''Returns val1 or val2'''
return val1 or val2
operations = {">": gt, "<": lt, "and": andf, "or": orf}
Then, ask the user for their input, and parse and evaluate them. Functions are first-class citizens in python, which means you can treat them like any other object: in our case, we're going to store the function objects in a tuple that we create when we parse the user input. We will access these function objects through the tuple, and call them using the other elements of the tuple as arguments.
num_conditions = 3
conditions = []
bool_opers = []
for i in range(num_conditions):
c = input("Enter condition (e.g. RSI > 30): ").split(" ")
key = c[0]
operator = c[1]
value = float(c[2])
# Get the function object that corresponds to the input operator
op_func = operations[operator]
# Create a tuple containing the function object, key, and value and save it for later
conditions.append((op_func, key, value))
# Do the same thing for and/or
if i < (num_conditions - 1):
b_op = input("Enter and / or: ")
bool_opers.append(operations[b_op])
Now, suppose we want to apply the filter RSI > 30 and MACD > 0 and MA20 < 5
, we can run the code with the following inputs:
Enter condition (e.g. RSI > 30): RSI > 30
Enter and / or: and
Enter condition (e.g. RSI > 30): MACD > 0
Enter and / or: and
Enter condition (e.g. RSI > 30): MA20 < 5
conditions
is now
[(<function __main__.gt(row, key, value)>, 'RSI', 30.0),
(<function __main__.gt(row, key, value)>, 'MACD', 0.0),
(<function __main__.lt(row, key, value)>, 'MA20', 5.0)]
Each element of conditions
is a tuple, which contains the function object for comparison, the key in the row
, and the value for comparison.
and bool_opers
is:
[<function __main__.andf(val1, val2)>, <function __main__.andf(val1, val2)>]
Finally, we need to iterate over each row, and call the functions we've saved.
Let's say the data looks like so:
data = [{'RSI': 50, 'MACD': 10, 'MA20': 10},
{'RSI': 20, 'MACD': 10, 'MA20': 2},
{'RSI': 50, 'MACD': -10, 'MA20': 10},
{'RSI': 40, 'MACD': 10, 'MA20': 3},
{'RSI': 35, 'MACD': 20, 'MA20': 0}]
selected_rows = []
for row in data:
# Calculate all elements of `conditions`
conditions_resolved = [c[0](row, c[1], c[2]) for c in conditions]
# e.g the first element will be
# c[0]: the function object gt
# c[1]: The string "RSI"
# c[2]: The number 30.
# So this is equivalent to doing:
# gt(row, 'RSI', 30)
# Set the result as the value of the first condition
result = conditions_resolved[0]
# If you have 2 or more conditions
if num_conditions >= 2:
# Take subsequent bool_opers and conditions_resolved
# And successively apply the operation to the current result and the new condition
for operation, val2 in zip(bool_opers, conditions_resolved[1:]):
result = operation(result, val2)
# If final result is True, add row to selected_rows
if result:
selected_rows.append(row)
print(selected_rows)
Which selects the last two rows (the only ones that satisfy all our conditions)
[{'RSI': 40, 'MACD': 10, 'MA20': 3}, {'RSI': 35, 'MACD': 20, 'MA20': 0}]