backtrader2 / backtrader

Python Backtesting library for trading strategies
https://www.backtrader.com
GNU General Public License v3.0
238 stars 54 forks source link

Math Functions #35

Closed neilsmurphy closed 4 years ago

neilsmurphy commented 4 years ago

This issue seeks to add new scalar math functionality such as math.log to the available functions in Backtrader.

The functionality of And/Max/Min is located in functions.py.

class Max(MultiLogic):
    flogic = max

They use the math library in conjunction with a class MultiLogic. This is designed to work with two or more lines.

class MultiLogic(Logic):
    def next(self):
        self[0] = self.flogic([arg[0] for arg in self.args])

    def once(self, start, end):
        # cache python dictionary lookups
        dst = self.array
        arrays = [arg.array for arg in self.args]
        flogic = self.flogic

        for i in range(start, end):
            dst[i] = flogic([arr[i] for arr in arrays])

Essentially what the MultiLogic does is iterate through the lines at a particular bar, create a list, and then apply the math function to the list. Of course, Max and And and the like are looking for an iterable.

When we try to do this with a single line like math.log, then it breaks because math.log is looking for a scalar but is getting a list.

dst[i] = flogic([arr[i] for arr in arrays])

We could adjust multilogic to find out if the list is only length of one, and then select the item [0] to make it a scalar. This does work but I don't like it because we are playing with a built in class and I would prefer to leave it alone.

So instead I created a new class called SingleLogic.

class SingleLogic(Logic):
    def next(self):
        self[0] = self.flogic(self.args[0])

    def once(self, start, end):
        # cache python dictionary lookups
        dst = self.array
        flogic = self.flogic

        for i in range(start, end):
            dst[i] = flogic(self.args[0].array[i])

With this single logic class we can now use all of the math library functions that require a scalar. Here are some examples:


class Log(SingleLogic):
    flogic = math.log10

class Ceiling(SingleLogic):
    flogic = math.ceil

class Floor(SingleLogic):
    flogic = math.floor

class Abs(SingleLogic):
    flogic = math.fabs

Then I used them in a basic strategy like this:

   def __init__(self):

        self.ma = bt.ind.EMA(period=10)
        self.cross = bt.ind.CrossOver(self.datas[0].close, self.ma)
        # Single logic
        self.lg = bt.Log(self.datas[0].close)
        self.cl = bt.Ceiling(self.datas[0].close)
        self.fl = bt.Floor(self.datas[0].close)
        self.cross_abs = bt.Abs(self.cross)

        # Check Multi still works
        self.mx = bt.Max(self.datas[0].close, self.datas[0].open)

    def next(self):

        self.log(f" open {self.datas[0].open[0]:.2f} close {self.datas[0].close[0]:.2f}, max {self.mx[0]:.2f}, "
                 f"log {self.lg[0]:5.3f}, ceiling {self.cl[0]:5.3f}, floor {self.fl[0]:5.3f} "
                 f"cross {self.cross[0]:2.0f} abs cross {self.cross_abs[0]:2.0f}")

And get output of this:

/home/runout/projects/scratch/venv/bin/python "/home/runout/projects/scratch/20200726 math.py"
2020-01-16,  open 222.57 close 221.77, max 222.57, log 2.346, ceiling 222.000, floor 221.000 cross  0 abs cross  0
2020-01-17,  open 222.03 close 222.14, max 222.14, log 2.347, ceiling 223.000, floor 222.000 cross  0 abs cross  0
2020-01-21,  open 222.16 close 221.44, max 222.16, log 2.345, ceiling 222.000, floor 221.000 cross  0 abs cross  0
2020-01-22,  open 222.31 close 221.32, max 222.31, log 2.345, ceiling 222.000, floor 221.000 cross  0 abs cross  0
2020-01-23,  open 220.75 close 219.76, max 220.75, log 2.342, ceiling 220.000, floor 219.000 cross  0 abs cross  0
2020-01-24,  open 220.80 close 217.94, max 220.80, log 2.338, ceiling 218.000, floor 217.000 cross -1 abs cross  1
2020-01-27,  open 213.10 close 214.87, max 214.87, log 2.332, ceiling 215.000, floor 214.000 cross  0 abs cross  0
2020-01-28,  open 216.14 close 217.79, max 217.79, log 2.338, ceiling 218.000, floor 217.000 cross  0 abs cross  0
2020-01-29,  open 221.44 close 223.23, max 223.23, log 2.349, ceiling 224.000, floor 223.000 cross  1 abs cross  1
2020-01-30,  open 206.53 close 209.53, max 209.53, log 2.321, ceiling 210.000, floor 209.000 cross -1 abs cross  1
2020-01-31,  open 208.43 close 201.91, max 208.43, log 2.305, ceiling 202.000, floor 201.000 cross  0 abs cross  0
2020-02-03,  open 203.44 close 204.19, max 204.19, log 2.310, ceiling 205.000, floor 204.000 cross  0 abs cross  0
2020-02-04,  open 206.62 close 209.83, max 209.83, log 2.322, ceiling 210.000, floor 209.000 cross  0 abs cross  0
2020-02-05,  open 212.51 close 210.11, max 212.51, log 2.322, ceiling 211.000, floor 210.000 cross  0 abs cross  0
2020-02-06,  open 210.47 close 210.85, max 210.85, log 2.324, ceiling 211.000, floor 210.000 cross  0 abs cross  0
2020-02-07,  open 210.30 close 212.33, max 212.33, log 2.327, ceiling 213.000, floor 212.000 cross  1 abs cross  1
2020-02-10,  open 211.52 close 213.06, max 213.06, log 2.329, ceiling 214.000, floor 213.000 cross  0 abs cross  0

It is very easy now to create new functions from the math library. We just have to pick the ones we want to implement. And of course we'll need some testing and documentation.

neilsmurphy commented 4 years ago

Is everyone ok with updating functions.py with this? Or should we seek to make an indicator? @backtrader2/admins

bigdavediode commented 4 years ago

Functions.py makes more sense if it's just a simple operator

On Tuesday, July 28, 2020, 03:48:43 AM PDT, Neil Murphy <notifications@github.com> wrote:  

Is everyone ok with updating functions.py with this? Or should we seek to make an indicator? @backtrader2/admins

— You are receiving this because you are on a team that was mentioned. Reply to this email directly, view it on GitHub, or unsubscribe.

vladisld commented 4 years ago

Sounds good. Adding a function instead of indicator may potentially enable real vectorizing in the future (as the logic could be recursively mapped to numpy operators).

ab-trader commented 4 years ago

Agree, functions.py is better place.

neilsmurphy commented 4 years ago

This is ready to go for quite some time, but I'm shy to mess up the main backtrader repository using cherry pick, since I don't know how to do it.