mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-25 16:25:16 +00:00
Merge pull request #728 from zenixls2/feature/dmi
feature: add dmi indicator
This commit is contained in:
commit
7225a597f2
|
@ -25,7 +25,10 @@ func (inc *ATR) Update(high, low, cloze float64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if inc.RMA == nil {
|
if inc.RMA == nil {
|
||||||
inc.RMA = &RMA{IntervalWindow: types.IntervalWindow{Window: inc.Window}}
|
inc.RMA = &RMA{
|
||||||
|
IntervalWindow: types.IntervalWindow{Window: inc.Window},
|
||||||
|
Adjust: true,
|
||||||
|
}
|
||||||
inc.PreviousClose = cloze
|
inc.PreviousClose = cloze
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,25 @@ import (
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
python
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import pandas_ta as ta
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0],
|
||||||
|
"low": [39870.71, 39834.98, 39866.31, 40108.31, 40016.09, 40094.66, 40105.0, 40196.48, 40154.99, 39800.0, 39959.21, 39922.98, 39940.02, 39632.0, 39261.39, 39254.63, 39473.91, 39555.51, 39819.0, 40006.84],
|
||||||
|
"close": [40105.78, 39935.23, 40183.97, 40182.03, 40212.26, 40149.99, 40378.0, 40618.37, 40401.03, 39990.39, 40179.13, 40097.23, 40014.72, 39667.85, 39303.1, 39519.99,
|
||||||
|
39693.79, 39827.96, 40074.94, 40059.84]
|
||||||
|
}
|
||||||
|
|
||||||
|
high = pd.Series(data['high'])
|
||||||
|
low = pd.Series(data['low'])
|
||||||
|
close = pd.Series(data['close'])
|
||||||
|
result = ta.atr(high, low, close, length=14)
|
||||||
|
print(result)
|
||||||
|
*/
|
||||||
func Test_calculateATR(t *testing.T) {
|
func Test_calculateATR(t *testing.T) {
|
||||||
var bytes = []byte(`{
|
var bytes = []byte(`{
|
||||||
"high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0],
|
"high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0],
|
||||||
|
@ -35,7 +54,7 @@ func Test_calculateATR(t *testing.T) {
|
||||||
name: "test_binance_btcusdt_1h",
|
name: "test_binance_btcusdt_1h",
|
||||||
kLines: buildKLines(bytes),
|
kLines: buildKLines(bytes),
|
||||||
window: 14,
|
window: 14,
|
||||||
want: 364.048648,
|
want: 367.913903,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
113
pkg/indicator/dmi.go
Normal file
113
pkg/indicator/dmi.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package indicator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Refer: https://www.investopedia.com/terms/d/dmi.asp
|
||||||
|
// Refer: https://github.com/twopirllc/pandas-ta/blob/main/pandas_ta/trend/adx.py
|
||||||
|
//
|
||||||
|
// Directional Movement Index
|
||||||
|
// an indicator developed by J. Welles Wilder in 1978 that identifies in which
|
||||||
|
// direction the price of an asset is moving.
|
||||||
|
//go:generate callbackgen -type DMI
|
||||||
|
type DMI struct {
|
||||||
|
types.IntervalWindow
|
||||||
|
ADXSmoothing int
|
||||||
|
atr *ATR
|
||||||
|
DMP types.UpdatableSeries
|
||||||
|
DMN types.UpdatableSeries
|
||||||
|
DIPlus *types.Queue
|
||||||
|
DIMinus *types.Queue
|
||||||
|
ADX types.UpdatableSeries
|
||||||
|
PrevHigh, PrevLow float64
|
||||||
|
UpdateCallbacks []func(diplus, diminus, adx float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inc *DMI) Update(high, low, cloze float64) {
|
||||||
|
if inc.DMP == nil || inc.DMN == nil {
|
||||||
|
inc.DMP = &RMA{IntervalWindow: inc.IntervalWindow, Adjust: true}
|
||||||
|
inc.DMN = &RMA{IntervalWindow: inc.IntervalWindow, Adjust: true}
|
||||||
|
inc.ADX = &RMA{IntervalWindow: types.IntervalWindow{Window: inc.ADXSmoothing}, Adjust: true}
|
||||||
|
}
|
||||||
|
if inc.atr == nil {
|
||||||
|
inc.atr = &ATR{IntervalWindow: inc.IntervalWindow}
|
||||||
|
inc.atr.Update(high, low, cloze)
|
||||||
|
inc.PrevHigh = high
|
||||||
|
inc.PrevLow = low
|
||||||
|
inc.DIPlus = types.NewQueue(500)
|
||||||
|
inc.DIMinus = types.NewQueue(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inc.atr.Update(high, low, cloze)
|
||||||
|
up := high - inc.PrevHigh
|
||||||
|
dn := inc.PrevLow - low
|
||||||
|
inc.PrevHigh = high
|
||||||
|
inc.PrevLow = low
|
||||||
|
pos := 0.0
|
||||||
|
if up > dn && up > 0. {
|
||||||
|
pos = up
|
||||||
|
}
|
||||||
|
|
||||||
|
neg := 0.0
|
||||||
|
if dn > up && dn > 0. {
|
||||||
|
neg = dn
|
||||||
|
}
|
||||||
|
|
||||||
|
inc.DMP.Update(pos)
|
||||||
|
inc.DMN.Update(neg)
|
||||||
|
if inc.atr.Length() < inc.Window {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
k := 100. / inc.atr.Last()
|
||||||
|
dmp := inc.DMP.Last()
|
||||||
|
dmn := inc.DMN.Last()
|
||||||
|
inc.DIPlus.Update(k * dmp)
|
||||||
|
inc.DIMinus.Update(k * dmn)
|
||||||
|
dx := 100. * math.Abs(dmp-dmn) / (dmp + dmn)
|
||||||
|
inc.ADX.Update(dx)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inc *DMI) GetDIPlus() types.Series {
|
||||||
|
return inc.DIPlus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inc *DMI) GetDIMinus() types.Series {
|
||||||
|
return inc.DIMinus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inc *DMI) GetADX() types.Series {
|
||||||
|
return inc.ADX
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inc *DMI) Length() int {
|
||||||
|
return inc.ADX.Length()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inc *DMI) calculateAndUpdate(allKLines []types.KLine) {
|
||||||
|
if inc.ADX == nil {
|
||||||
|
for _, k := range allKLines {
|
||||||
|
inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64())
|
||||||
|
inc.EmitUpdate(inc.DIPlus.Last(), inc.DIMinus.Last(), inc.ADX.Last())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
k := allKLines[len(allKLines)-1]
|
||||||
|
inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64())
|
||||||
|
inc.EmitUpdate(inc.DIPlus.Last(), inc.DIMinus.Last(), inc.ADX.Last())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inc *DMI) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
|
||||||
|
if inc.Interval != interval {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inc.calculateAndUpdate(window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inc *DMI) Bind(updater KLineWindowUpdater) {
|
||||||
|
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
|
||||||
|
}
|
15
pkg/indicator/dmi_callbacks.go
Normal file
15
pkg/indicator/dmi_callbacks.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// Code generated by "callbackgen -type DMI"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package indicator
|
||||||
|
|
||||||
|
import ()
|
||||||
|
|
||||||
|
func (inc *DMI) OnUpdate(cb func(diplus float64, diminus float64, adx float64)) {
|
||||||
|
inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inc *DMI) EmitUpdate(diplus float64, diminus float64, adx float64) {
|
||||||
|
for _, cb := range inc.UpdateCallbacks {
|
||||||
|
cb(diplus, diminus, adx)
|
||||||
|
}
|
||||||
|
}
|
83
pkg/indicator/dmi_test.go
Normal file
83
pkg/indicator/dmi_test.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package indicator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
python:
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import pandas_ta as ta
|
||||||
|
|
||||||
|
data = pd.Series([0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9])
|
||||||
|
|
||||||
|
high = pd.Series([100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109])
|
||||||
|
|
||||||
|
low = pd.Series([80,81,82,83,84,85,86,87,88,89,80,81,82,83,84,85,86,87,88,89,80,81,82,83,84,85,86,87,88,89])
|
||||||
|
|
||||||
|
close = pd.Series([90,91,92,93,94,95,96,97,98,99,90,91,92,93,94,95,96,97,98,99,90,91,92,93,94,95,96,97,98,99])
|
||||||
|
|
||||||
|
result = ta.adx(high, low, close, 5, 14)
|
||||||
|
print(result['ADX_14'])
|
||||||
|
|
||||||
|
print(result['DMP_5'])
|
||||||
|
print(result['DMN_5'])
|
||||||
|
*/
|
||||||
|
func Test_DMI(t *testing.T) {
|
||||||
|
var Delta = 0.001
|
||||||
|
var highb = []byte(`[100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109]`)
|
||||||
|
var lowb = []byte(`[80,81,82,83,84,85,86,87,88,89,80,81,82,83,84,85,86,87,88,89,80,81,82,83,84,85,86,87,88,89]`)
|
||||||
|
var clozeb = []byte(`[90,91,92,93,94,95,96,97,98,99,90,91,92,93,94,95,96,97,98,99,90,91,92,93,94,95,96,97,98,99]`)
|
||||||
|
|
||||||
|
buildKLines := func(h, l, c []byte) (klines []types.KLine) {
|
||||||
|
var hv, cv, lv []fixedpoint.Value
|
||||||
|
_ = json.Unmarshal(h, &hv)
|
||||||
|
_ = json.Unmarshal(l, &lv)
|
||||||
|
_ = json.Unmarshal(c, &cv)
|
||||||
|
if len(hv) != len(lv) || len(lv) != len(cv) {
|
||||||
|
panic(fmt.Sprintf("length not equal %v %v %v", len(hv), len(lv), len(cv)))
|
||||||
|
}
|
||||||
|
for i, hh := range hv {
|
||||||
|
kline := types.KLine{High: hh, Low: lv[i], Close: cv[i]}
|
||||||
|
klines = append(klines, kline)
|
||||||
|
}
|
||||||
|
return klines
|
||||||
|
}
|
||||||
|
|
||||||
|
type output struct{dip float64; dim float64; adx float64}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
klines []types.KLine
|
||||||
|
want output
|
||||||
|
next output
|
||||||
|
total int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test_dmi",
|
||||||
|
klines: buildKLines(highb, lowb, clozeb),
|
||||||
|
want: output{dip: 4.85114, dim: 1.339736, adx: 37.857156},
|
||||||
|
next: output{dip: 4.813853, dim: 1.67532, adx: 36.111434},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
dmi := &DMI{
|
||||||
|
IntervalWindow: types.IntervalWindow{Window: 5},
|
||||||
|
ADXSmoothing: 14,
|
||||||
|
}
|
||||||
|
dmi.calculateAndUpdate(tt.klines)
|
||||||
|
assert.InDelta(t, dmi.GetDIPlus().Last(), tt.want.dip, Delta)
|
||||||
|
assert.InDelta(t, dmi.GetDIMinus().Last(), tt.want.dim, Delta)
|
||||||
|
assert.InDelta(t, dmi.GetADX().Last(), tt.want.adx, Delta)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -6,33 +6,42 @@ import (
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Refer: Running Moving Average
|
// Running Moving Average
|
||||||
|
// Refer: https://github.com/twopirllc/pandas-ta/blob/main/pandas_ta/overlap/rma.py#L5
|
||||||
|
// Refer: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ewm.html#pandas-dataframe-ewm
|
||||||
//go:generate callbackgen -type RMA
|
//go:generate callbackgen -type RMA
|
||||||
type RMA struct {
|
type RMA struct {
|
||||||
types.IntervalWindow
|
types.IntervalWindow
|
||||||
Values types.Float64Slice
|
Values types.Float64Slice
|
||||||
Sources types.Float64Slice
|
counter int
|
||||||
|
Adjust bool
|
||||||
|
tmp float64
|
||||||
|
sum float64
|
||||||
EndTime time.Time
|
EndTime time.Time
|
||||||
UpdateCallbacks []func(value float64)
|
UpdateCallbacks []func(value float64)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *RMA) Update(x float64) {
|
func (inc *RMA) Update(x float64) {
|
||||||
inc.Sources.Push(x)
|
lambda := 1 / float64(inc.Window)
|
||||||
|
if inc.counter == 0 {
|
||||||
|
inc.sum = 1
|
||||||
|
inc.tmp = x
|
||||||
|
} else {
|
||||||
|
if inc.Adjust {
|
||||||
|
inc.sum = inc.sum*(1-lambda) + 1
|
||||||
|
inc.tmp = inc.tmp + (x-inc.tmp)/inc.sum
|
||||||
|
} else {
|
||||||
|
inc.tmp = inc.tmp*(1-lambda) + x*lambda
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inc.counter++
|
||||||
|
|
||||||
if len(inc.Sources) < inc.Window {
|
if inc.counter < inc.Window {
|
||||||
inc.Values.Push(0)
|
inc.Values.Push(0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(inc.Sources) == inc.Window {
|
inc.Values.Push(inc.tmp)
|
||||||
inc.Values.Push(inc.Sources.Mean())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lambda := 1 / float64(inc.Window)
|
|
||||||
rma := (1-lambda)*inc.Values.Last() + lambda*x
|
|
||||||
inc.Values.Push(rma)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inc *RMA) Last() float64 {
|
func (inc *RMA) Last() float64 {
|
||||||
|
|
|
@ -63,6 +63,11 @@ type Series interface {
|
||||||
Length() int
|
Length() int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdatableSeries interface {
|
||||||
|
Series
|
||||||
|
Update(float64)
|
||||||
|
}
|
||||||
|
|
||||||
// The interface maps to pinescript basic type `series` for bool type
|
// The interface maps to pinescript basic type `series` for bool type
|
||||||
// Access the internal historical data from the latest to the oldest
|
// Access the internal historical data from the latest to the oldest
|
||||||
// Index(0) always maps to Last()
|
// Index(0) always maps to Last()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user