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
```python 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')
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')
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.