diff --git a/go.sum b/go.sum index b27272a..65e99d9 100644 --- a/go.sum +++ b/go.sum @@ -16,4 +16,4 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file diff --git a/indicator_money_flow.go b/indicator_money_flow.go new file mode 100644 index 0000000..f8522c5 --- /dev/null +++ b/indicator_money_flow.go @@ -0,0 +1,74 @@ +package techan + +import ( + "math" + + "github.com/sdcoffey/big" +) + +type moneyFlowIndexIndicator struct { + mfIndicator Indicator + oneHundred big.Decimal +} + +// NewMoneyFlowIndexIndicator returns a derivative Indicator which returns the money flow index of the base indicator +// in a given time frame. A more in-depth explanation of money flow index can be found here: +// https://www.investopedia.com/terms/m/mfi.asp +func NewMoneyFlowIndexIndicator(series *TimeSeries, timeframe int) Indicator { + return moneyFlowIndexIndicator{ + mfIndicator: NewMoneyFlowRatioIndicator(series, timeframe), + oneHundred: big.NewFromString("100"), + } +} + +func (mfi moneyFlowIndexIndicator) Calculate(index int) big.Decimal { + moneyFlowRatio := mfi.mfIndicator.Calculate(index) + + return mfi.oneHundred.Sub(mfi.oneHundred.Div(big.ONE.Add(moneyFlowRatio))) +} + +type moneyFlowRatioIndicator struct { + typicalPrice Indicator + volume Indicator + window int +} + +// NewMoneyFlowRatioIndicator returns a derivative Indicator which returns the money flow ratio of the base indicator +// in a given time frame. Money flow ratio is the positive money flow divided by the negative money flow during the +// same time frame +func NewMoneyFlowRatioIndicator(series *TimeSeries, timeframe int) Indicator { + return moneyFlowRatioIndicator{ + typicalPrice: NewTypicalPriceIndicator(series), + volume: NewVolumeIndicator(series), + window: timeframe, + } +} + +func (mfr moneyFlowRatioIndicator) Calculate(index int) big.Decimal { + if index < mfr.window-1 { + return big.ZERO + } + + positiveMoneyFlow := big.ZERO + negativeMoneyFlow := big.ZERO + + typicalPrice := mfr.typicalPrice.Calculate(index) + + for i := index; i > index-mfr.window && i > 0; i-- { + prevTypicalPrice := mfr.typicalPrice.Calculate(i - 1) + + if typicalPrice.GT(prevTypicalPrice) { + positiveMoneyFlow = positiveMoneyFlow.Add(typicalPrice.Mul(mfr.volume.Calculate(i))) + } else if typicalPrice.LT(prevTypicalPrice) { + negativeMoneyFlow = negativeMoneyFlow.Add(typicalPrice.Mul(mfr.volume.Calculate(i))) + } + + typicalPrice = prevTypicalPrice + } + + if negativeMoneyFlow.EQ(big.ZERO) { + return big.NewDecimal(math.Inf(1)) + } + + return positiveMoneyFlow.Div(negativeMoneyFlow) +} diff --git a/indicator_money_flow_test.go b/indicator_money_flow_test.go new file mode 100644 index 0000000..9d64289 --- /dev/null +++ b/indicator_money_flow_test.go @@ -0,0 +1,45 @@ +package techan + +import ( + "math" + "testing" +) + +var series = mockTimeSeriesOCHLV( + []float64{10, 12, 12, 8, 1000}, + []float64{11, 14, 14, 9, 1500}, + []float64{10, 20, 24, 10, 1200}, + []float64{9, 10, 11, 9, 1800}, + []float64{11, 14, 14, 9, 2000}, + []float64{9, 10, 11, 9, 1300}, +) + +func TestMoneyFlowIndexIndicator(t *testing.T) { + indicator := NewMoneyFlowIndexIndicator(series, 3) + + expectedValues := []float64{ + 0, + 0, + 100, + 69.0189, + 71.9917, + 44.3114, + } + + indicatorEquals(t, expectedValues, indicator) +} + +func TestMoneyFlowRatioIndicator(t *testing.T) { + indicator := NewMoneyFlowRatioIndicator(series, 3) + + expectedValues := []float64{ + 0, + 0, + math.Inf(1), + 2.2278, + 2.5704, + 0.7957, + } + + indicatorEquals(t, expectedValues, indicator) +} diff --git a/testutils.go b/testutils.go index dd0303c..c9e8dad 100644 --- a/testutils.go +++ b/testutils.go @@ -4,21 +4,22 @@ import ( "fmt" "math" "math/rand" + "strconv" "testing" "time" - "strconv" - "github.com/sdcoffey/big" "github.com/stretchr/testify/assert" ) -var candleIndex int -var mockedTimeSeries = mockTimeSeriesFl( - 64.75, 63.79, 63.73, - 63.73, 63.55, 63.19, - 63.91, 63.85, 62.95, - 63.37, 61.33, 61.51) +var ( + candleIndex int + mockedTimeSeries = mockTimeSeriesFl( + 64.75, 63.79, 63.73, + 63.73, 63.55, 63.19, + 63.91, 63.85, 62.95, + 63.37, 61.33, 61.51) +) func randomTimeSeries(size int) *TimeSeries { vals := make([]string, size) @@ -56,6 +57,22 @@ func mockTimeSeriesOCHL(values ...[]float64) *TimeSeries { return ts } +func mockTimeSeriesOCHLV(values ...[]float64) *TimeSeries { + ts := NewTimeSeries() + for i, ochlv := range values { + candle := NewCandle(NewTimePeriod(time.Unix(int64(i), 0), time.Second)) + candle.OpenPrice = big.NewDecimal(ochlv[0]) + candle.ClosePrice = big.NewDecimal(ochlv[1]) + candle.MaxPrice = big.NewDecimal(ochlv[2]) + candle.MinPrice = big.NewDecimal(ochlv[3]) + candle.Volume = big.NewDecimal(ochlv[4]) + + ts.AddCandle(candle) + } + + return ts +} + func mockTimeSeries(values ...string) *TimeSeries { ts := NewTimeSeries() for _, val := range values {