diff --git a/pkg/indicator/dmi.go b/pkg/indicator/dmi.go new file mode 100644 index 000000000..580fe3339 --- /dev/null +++ b/pkg/indicator/dmi.go @@ -0,0 +1,109 @@ +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} + inc.DMN = &RMA{IntervalWindow: inc.IntervalWindow} + inc.ADX = &RMA{IntervalWindow: types.IntervalWindow{Window: inc.ADXSmoothing}} + } + 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 + } + + k := 100. / inc.atr.Last() + inc.DMP.Update(pos) + inc.DMN.Update(neg) + dmp := inc.DMP.Last() + dmn := inc.DMN.Last() + inc.DIPlus.Update(k * dmp) + inc.DIMinus.Update(k * dmn) + dx := 100. * k * 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) +} diff --git a/pkg/indicator/dmi_callbacks.go b/pkg/indicator/dmi_callbacks.go new file mode 100644 index 000000000..93e8dd14d --- /dev/null +++ b/pkg/indicator/dmi_callbacks.go @@ -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) + } +} diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index d987a46f0..23e4a942c 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -63,6 +63,11 @@ type Series interface { Length() int } +type UpdatableSeries interface { + Series + Update(float64) +} + // The interface maps to pinescript basic type `series` for bool type // Access the internal historical data from the latest to the oldest // Index(0) always maps to Last()