I'm writing a script for stock / crpyto trading. A very crude strategy is to buy when the RSI (a technical indicator in stock) reaches 50 and sell when the RSI falls below 50 for 3 consecutive days or below 40 in a single day. My origin script is like this:
def simpleRSIstrategy(indicator, threshold1 = 50, threshold2 = 40, days = 3):
buySellTable = dict()
# simple RSI strategy
hold = False
count = 0
for i in range(len(indicator)):
# buying strategy
if indicator['RSI7'][i] > threshold1 and not hold:
date = indicator.index[i]
buySellTable[date] = 'Buy'
hold = True
# selling strategy
if indicator['RSI7'][i] < threshold1 and hold:
count += 1
if count == days or indicator['RSI7'][i] < threshold2:
date = indicator.index[i]
buySellTable[date] = 'Sell'
hold = False
count = 0
if indicator['RSI7'][i] > threshold1 and hold:
count = 0
return buySellTable
What this script is supposed to do is applying the aforementioned simple RSI strategy and return the 'buySellTable', which is a dictionary with the dates as keys and 'Buy' and 'Sell' as items. There is nothing wrong with this script. Since it's a crude strategy, it's necessary to get optimized. I want to split it into two parts - buying strategy and selling strategy so that I can do the optimization respectively. Then I rewrote it into the following scripts:
def simpleRSIstrategy_split(indicator, threshold1 = 50, threshold2 = 40, days = 3):
buySellTable = dict()
hold = False
count = 0
startIdx = 0
while True:
simpleBuyStrategy_split(indicator, threshold1)
simpleSellStrategy_split(indicator, threshold1, threshold2, days)
if startIdx == len(indicator)-1:
break
return buySellTable
def simpleBuyStrategy_split(indicator, threshold1):
global startIdx, hold, buySellTable
for i in range(startIdx, len(indicator)):
if indicator['RSI7'][i] > threshold1 and not hold:
date = indicator.index[i]
buySellTable[date] = 'Buy'
hold = True
startIdx = i+1
break
def simpleSellStrategy_split(indicator, threshold1, threshold2, days):
global startIdx, count, hold, buySellTable
for i in range(startIdx, len(indicator)):
if indicator['RSI7'][i] < threshold1 and hold:
count += 1
if count == days or indicator['RSI7'][i] < threshold2:
date = indicator.index[i]
buySellTable[date] = 'Sell'
hold = False
count = 0
startIdx = i+1
break
if indicator['RSI7'][i] > threshold1 and hold:
count = 0
In the 'simpleRSIstrategy_split' script, I want to treat buySellTable, hold, count and startIdx as global variables that can be used and edited by the scripts 'simpleBuyStrategy_split' and 'simpleSellStrategy_split'. But I got the error message when I run the script: name 'startIdx' is not defined. I check the output from the console and found that when it's excuting the line
for i in range(startIdx, len(indicator)):
in the script 'simpleBuyStrategy_split', it complains that startIdx is not defined. But I already defined it as a global variable. I don't know why this happens and how can I solve it?
You got the scoping wrong. Python is a lexically scoped language.
Without knowing, you are writing closures. Meaning you use variables in simpleBuyStrategy_split
and simpleSellStrategy_split
which are not in the function argument list - but outside the function definition.
Basically
buySellTable = dict()
hold = False
count = 0
startIdx = 0
You treat them as if they would have dynamic scoping. (that they would change dependent where these two functions are called).
Since python arguments are mostly call-by-reference (due to performance reasons) - so they work like C pointers, the arguments themselves are changed if they are changed in the function body - except when they are deep-copied explicitely in the function body.
Therefore you could use them as if they are dynamically scoped, if you just pass them as function arguments.
Like this:
# first define them globally
buySellTable = dict()
hold = False
count = 0
startIdx = 0
# then in each function definition, pass them as function arguments
def simpleRSIstrategy_split(indicator, threshold1 = 50, threshold2 = 40, days = 3, buySellTable=buySellTable, hold=hold, count=count, startIdx=startIdx):
while True:
simpleBuyStrategy_split(indicator, threshold1, buySellTable=buySellTable, hold=hold, count=count, startIdx=startIdx)
simpleSellStrategy_split(indicator, threshold1, threshold2, days, buySellTable=buySellTable, hold=hold, count=count, startIdx=startIdx)
if startIdx == len(indicator)-1:
break
return buySellTable
def simpleBuyStrategy_split(indicator, threshold1, buySellTable=buySellTable, hold=hold, count=count, startIdx=startIdx):
for i in range(startIdx, len(indicator)):
if indicator['RSI7'][i] > threshold1 and not hold:
date = indicator.index[i]
buySellTable[date] = 'Buy'
hold = True
startIdx = i+1
break
def simpleSellStrategy_split(indicator, threshold1, threshold2, days, buySellTable=buySellTable, hold=hold, count=count, startIdx=startIdx):
for i in range(startIdx, len(indicator)):
if indicator['RSI7'][i] < threshold1 and hold:
count += 1
if count == days or indicator['RSI7'][i] < threshold2:
date = indicator.index[i]
buySellTable[date] = 'Sell'
hold = False
count = 0
startIdx = i+1
break
if indicator['RSI7'][i] > threshold1 and hold:
count = 0
I see, somehow you realized that they have globally to refer, but then, the simpleRSI function split must also do it:
buySellTable = dict()
hold = False
count = 0
startIdx = 0
def simpleRSIstrategy_split(indicator, threshold1 = 50, threshold2 = 40, days = 3):
global buySellTable, hold, count, startIdx
while True:
simpleBuyStrategy_split(indicator, threshold1)
simpleSellStrategy_split(indicator, threshold1, threshold2, days)
if startIdx == len(indicator)-1:
break
return buySellTable
And the other two functions then as you had them. This might also work.
But this is not so nice. Because one should avoid global variables.
In Python, you would avoid global variables by using classes.
You could create a class where the global variables are class Attributes. (prepended by: self.
And these three functions are methods of the class.
Since they have then to refer to these four variables using self.
you don't need the 'global' delcarations in them.
And the class itself is encapsulated.
Like this:
class SimpleRSITrader:
def __init__(self, indicator, threshold1 = 50, threshold2 = 40, days = 3, rsi='RSI7'):
self.buy_sell_table = {}
self.hold = False
self.count = 0
self.start_idx = 0
self.indicator = indicator
self.thresh1 = threshold1
self.thrseh2 = threshold2
self.days = days
self.rsi = rsi
def run(self):
while True:
self.buy()
self.sell()
if self.startIdx == len(self.indicator)-1:
break
return self.buy_sell_table
def buy(self):
for i in range(self.start_idx, len(self.indicator)):
if self.indicator[self.rsi][i] > self.thresh1 and not self.hold:
date = self.indicator.index[i]
self.buy_sell_table[date] = 'Buy'
self.hold = True
self.start_idx = i + 1
break
def sell(self):
for i in range(self.start_idx, len(self.indicator)):
if self.hold:
if self.indictaor[self.rsi] < self.thresh1:
self.count += 1
if count == self.days or self.indictoar[self.rsi][i] < self.thresh2:
date = self.indicator.index[i]
self.buy_sell_table[date] = 'Sell'
self.hold = False
self.count = 0
self.start_idx = i + 1
break
if self.indicator[self.rsi][i] > self.thresh1:
self.count = 0
The good thing of this is, you can anytime from outside check the status of the attributes. And thus also communicate with other parts of the program - while you don't have global variables - because the previously global variables are now encapsulated in the class - therefore packed into objects.
So the idea is that for each indicator with new threshold settings and day settings you create a new instance for the simple RSI strategy.
This class, you can reuse even for different RSI's (other than 'RSI7').
Through encapsulation, the context for your buy and sell becomes clear - thus the method names or attribute names become simpler too - therefore everything more easily readable and your code is structured - every former global variables and functions are now encapsulated into the class - thus the code is clearly structured and the context of each of them clear - therefore you can be shorter with your names.