pythonglobal-variables

How to solve the problem that the global variable is not defined in python?


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?


Solution

  • 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.

    Solution: Encapsulate Using Classes

    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.