MFI Generic

In the recent Canonical vs Non-Canonical post, the MFI (aka MoneyFlowIndicator) was developed.

Although it is developed in the canonical way, it does still offer some room for improvement and becoming generic.

Let's focus of the 1st lines of the implementation, the ones which create the typical price

class MFI_Canonical(bt.Indicator):
    lines = ('mfi',)
    params = dict(period=14)

    def __init__(self):
        tprice = (self.data.close + self.data.low + self.data.high) / 3.0
        mfraw = tprice * self.data.volume
        ...

A typical instantiation would look like this

class MyMFIStrategy(bt.Strategy):

    def __init__(self):
        mfi = bt.MFI_Canonical(self.data)

The problem here should be obvious: "One needs an input for the indicator which features close, low, high and volume components (aka *lines in the backtrader ecosystem)"*

It may, of course, be the case that one wishes to create a MoneyFlowIndicator using components from different data sources (lines from data feeds or lines from other indicators) As simple as wanting to give the close a lot more weight, without having to develop a specific indicator. Considering the industry-standard OHLCV field ordering, a multiple inputs, extra weight for close, instantiation could look like this

class MyMFIStrategy2(bt.Strategy):

    def __init__(self):
        wclose = self.data.close * 5.0
        mfi = bt.MFI_Canonical(self.data.high, self.data.low,
                               wclose, self.data.volume)

Or because the user previously worked with ta-lib and fancies the multiple inputs style.

Supporting multiple inputs

backtrader tries to be as pythonic as possible and the self.datas array containing the list of data feeds in the system (and which is auto-magically provided to your strategy) can be queried for its length. Let's use this to discriminate what the caller wants and properly calculate tprice and mfraw

class MFI_MultipleInputs(bt.Indicator):
    lines = ('mfi',)
    params = dict(period=14)

    def __init__(self):
        if len(self.datas) == 1:
            # 1 data feed passed, must have components
            tprice = (self.data.close + self.data.low + self.data.high) / 3.0
            mfraw = tprice * self.data.volume
        else:
            # if more than 1 data feed, individual components in OHLCV order
            tprice = (self.data0 + self.data1 + self.data2) / 3.0
            mfraw = tprice * self.data3

        # No changes with regards to previous implementation
        flowpos = bt.ind.SumN(mfraw * (tprice > tprice(-1)), period=self.p.period)
        flowneg = bt.ind.SumN(mfraw * (tprice < tprice(-1)), period=self.p.period)

        mfiratio = bt.ind.DivByZero(flowpos, flowneg, zero=100.0)
        self.l.mfi = 100.0 - 100.0 / (1.0 + mfiratio)

NOTE

Notice how the individual components are referenced as self.dataX (such as self.data0, self.data1)

This is the same as using self.datas[x], as in self.datas[0] ...


Let's see graphically that this indicator produces the same results as the canonical one, and the same results when the multiple inputs correspond to the original components of the data feed. To do so, it will be run in a strategy as this

class MyMFIStrategy2(bt.Strategy):

    def __init__(self):
        MFI_Canonical(self.data)
        MFI_MultipleInputs(self.data, plotname='MFI Single Input')
        MFI_MultipleInputs(self.data.high,
                           self.data.low,
                           self.data.close,
                           self.data.volume,
                           plotname='MFI Multiple Inputs')

!MFI Results Check

Without having to resort to check each value, it should be obvious from the picture that the results are the same for the three.

Let's finally see what happens if put a lot more weight on to the close. Let's run like this.

class MyMFIStrategy2(bt.Strategy):
    def __init__(self):

        MFI_MultipleInputs(self.data)
        MFI_MultipleInputs(self.data.high,
                           self.data.low,
                           self.data.close * 5.0,
                           self.data.volume,
                           plotname='MFI Close * 5.0')

!MFI Close * 5.0

Whether this makes sense or not is left to the reader, but one can clearly see that adding weight to the close has altered the pattern.

Conclusion

By simple using the pythonic len, one can transform an indicator which uses a data feed with multiple components (and fixed names) into an indicator which accepts multiple generic inputs.