commit c23020eee5625bc85fed53e2d6a71c81de2828bd Author: lychiyu Date: Tue Jun 25 23:17:36 2024 +0800 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..4731499 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# indicator +The trade indicator. + +# Indicator Support +| indicator | support +|----------|------| +| EMA | Yes | +| SMA | Yes | +| SMMA | Yes | +| Stoch | No Test| +| StochRSI| Yes| + + +# Cheers to +Some indicator refer to [Gekko](https://github.com/thrasher-/gocryptotrader) + +Some indicator refer to tradingview wiki [StochRSI](https://www.tradingview.com/wiki/Stochastic_RSI_(STOCH_RSI)) diff --git a/boll.go b/boll.go new file mode 100644 index 0000000..47e5ba0 --- /dev/null +++ b/boll.go @@ -0,0 +1,52 @@ +package indicator + +import ( + "math" +) + +type Boll struct { + *SMA + k int + mid float64 + top float64 + bottom float64 +} + +func NewBoll(winLen, k int) *Boll { + b := new(Boll) + b.SMA = NewSMA(winLen) + b.k = k + return b +} + +func (b *Boll) Result() float64 { + return b.mid +} + +func (b *Boll) Update(price float64) { + b.SMA.Update(price) + b.Cal() +} + +func (b *Boll) Cal() { + b.mid = b.SMA.result + var sd float64 + for j := 0; j < b.winLen; j++ { + sd += math.Pow(b.prices[j]-b.mid, 2) + } + sd = math.Sqrt(sd/float64(b.winLen)) * float64(b.k) + b.top = b.mid + sd + b.bottom = b.mid - sd +} + +func (b *Boll) Top() float64 { + return b.top +} + +func (b *Boll) Bottom() float64 { + return b.bottom +} + +func (b *Boll) Indicator() map[string]float64 { + return map[string]float64{"result": b.Result(), "top": b.Top(), "bottom": b.Bottom()} +} diff --git a/common.go b/common.go new file mode 100644 index 0000000..c03184e --- /dev/null +++ b/common.go @@ -0,0 +1,17 @@ +package indicator + +type Updater interface { + Update(price float64) +} + +type Indicator interface { + Updater + Result() float64 +} + +type Crosser interface { + Updater + SlowResult() float64 + FastResult() float64 +} + diff --git a/cross_tool.go b/cross_tool.go new file mode 100644 index 0000000..44cfa24 --- /dev/null +++ b/cross_tool.go @@ -0,0 +1,60 @@ +package indicator + +type CrossTool struct { + crosser Crosser + fasts [3]float64 // prev fasts + slows [3]float64 // prev slows +} + +func NewCrossTool(crosser Crosser) *CrossTool { + ct := new(CrossTool) + ct.crosser = crosser + return ct +} + +func (ct *CrossTool) Update(price float64) { + ct.fasts[2] = ct.fasts[1] + ct.fasts[1] = ct.fasts[0] + ct.slows[2] = ct.slows[1] + ct.slows[1] = ct.slows[0] + + ct.crosser.Update(price) + + ct.fasts[0] = ct.crosser.FastResult() + ct.slows[0] = ct.crosser.SlowResult() +} + +func (ct *CrossTool) IsCrossUp() bool { + prevFast, prevSlow := ct.getPrev() + fast, slow := ct.getCurrent() + if prevFast < prevSlow && fast > slow { + // fmt.Println("up:", ct.fasts, ct.slows) + return true + } + return false +} + +func (ct *CrossTool) IsCrossDown() bool { + prevFast, prevSlow := ct.getPrev() + fast, slow := ct.getCurrent() + if prevFast > prevSlow && fast < slow { + // fmt.Println("down:", ct.fasts, ct.slows) + return true + } + return false +} + +func (ct *CrossTool) getCurrent() (fast, slow float64) { + fast, slow = ct.fasts[0], ct.slows[0] + return +} +func (ct *CrossTool) getPrev() (prevFast, prevSlow float64) { + prevFast = ct.fasts[1] + prevSlow = ct.slows[1] + // if prev fast is same of prev slow,see prev one + if prevFast == prevSlow { + prevFast = ct.fasts[2] + prevSlow = ct.slows[2] + } + return +} diff --git a/ema.go b/ema.go new file mode 100644 index 0000000..2abe723 --- /dev/null +++ b/ema.go @@ -0,0 +1,31 @@ +package indicator + +type EMA struct { + MABase + alpha float64 + bFirst bool +} + +func NewEMA(winLen int) *EMA { + e := new(EMA) + e.winLen = winLen + e.alpha = 2 / float64((e.winLen + 1)) + e.bFirst = true + return e +} + +func (e *EMA) Update(price float64) { + if e.bFirst { + e.result = price + e.bFirst = false + } + e.cal(price) +} + +// cal +// EMA = alpha * x + (1 - alpha) * EMA[1] +// alpha = 2 / (y + 1) +func (e *EMA) cal(price float64) { + oldResult := e.result + e.result = e.alpha*price + (1-e.alpha)*oldResult +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e794293 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.qtrade.icu/coin-quant/indicator + +go 1.22.0 diff --git a/ma_group.go b/ma_group.go new file mode 100644 index 0000000..2eee15a --- /dev/null +++ b/ma_group.go @@ -0,0 +1,26 @@ +package indicator + +type MAGroup struct { + fast Indicator + slow Indicator +} + +func NewMAGroup(fast, slow Indicator) *MAGroup { + mg := new(MAGroup) + mg.fast = fast + mg.slow = slow + return mg +} + +func (mg *MAGroup) Update(price float64) { + mg.fast.Update(price) + mg.slow.Update(price) +} + +func (mg *MAGroup) FastResult() float64 { + return mg.fast.Result() +} + +func (mg *MAGroup) SlowResult() float64 { + return mg.slow.Result() +} diff --git a/mabase.go b/mabase.go new file mode 100644 index 0000000..4646e32 --- /dev/null +++ b/mabase.go @@ -0,0 +1,10 @@ +package indicator + +type MABase struct { + winLen int //window length + result float64 +} + +func (m *MABase) Result() float64 { + return m.result +} diff --git a/macd.go b/macd.go new file mode 100644 index 0000000..e175498 --- /dev/null +++ b/macd.go @@ -0,0 +1,57 @@ +package indicator + +type MACD struct { + long Indicator + short Indicator + signal Indicator + dif float64 + dea float64 + result float64 +} + +// NewMACDWithSMA macd signal line with simple ma +func NewMACDWithSMA(short, long, signal int) *MACD { + ma := new(MACD) + ma.short = NewEMA(short) + ma.long = NewEMA(long) + ma.signal = NewSMA(signal) + return ma +} + +func NewMACD(short, long, signal int) *MACD { + ma := new(MACD) + ma.short = NewEMA(short) + ma.long = NewEMA(long) + ma.signal = NewEMA(signal) + return ma +} + +func (ma *MACD) Update(price float64) { + ma.long.Update(price) + ma.short.Update(price) + + ma.dif = ma.short.Result() - ma.long.Result() + ma.signal.Update(ma.dif) + ma.dea = ma.signal.Result() + ma.result = ma.dif - ma.dea +} + +func (ma *MACD) Result() float64 { + return ma.result +} + +func (ma *MACD) DIF() float64 { + return ma.dif +} + +func (ma *MACD) DEA() float64 { + return ma.dea +} + +func (ma *MACD) FastResult() float64 { + return ma.dif +} + +func (ma *MACD) SlowResult() float64 { + return ma.dea +} diff --git a/rsi.go b/rsi.go new file mode 100644 index 0000000..1bc9d9a --- /dev/null +++ b/rsi.go @@ -0,0 +1,52 @@ +package indicator + +type RSI struct { + winLen int + avgU *SMMA + avgD *SMMA + u float64 + d float64 + lastClose *float64 + rs float64 + result float64 +} + +func NewRSI(winLen int) *RSI { + r := new(RSI) + r.winLen = winLen + r.avgU = NewSMMA(r.winLen) + r.avgD = NewSMMA(r.winLen) + return r +} + +func (r *RSI) Update(price float64) { + if r.lastClose == nil { + r.lastClose = &price + return + } + if price > *r.lastClose { + r.u = price - *r.lastClose + r.d = 0 + } else { + r.u = 0 + r.d = *r.lastClose - price + } + r.avgU.Update(r.u) + r.avgD.Update(r.d) + uResult := r.avgU.Result() + dResult := r.avgD.Result() + r.rs = uResult / dResult + r.result = 100 - (100 / (1 + r.rs)) + if dResult == 0 { + if uResult != 0 { + r.result = 100 + } else { + r.result = 0 + } + } + r.lastClose = &price +} + +func (r *RSI) Result() float64 { + return r.result +} diff --git a/sma.go b/sma.go new file mode 100644 index 0000000..6ccbfa6 --- /dev/null +++ b/sma.go @@ -0,0 +1,25 @@ +package indicator + +type SMA struct { + MABase + prices []float64 + sum float64 + age int +} + +func NewSMA(winLen int) *SMA { + s := new(SMA) + s.winLen = winLen + s.prices = make([]float64, s.winLen) + s.age = 0 + return s +} + +func (s *SMA) Update(price float64) { + s.result = price + tail := s.prices[s.age] + s.prices[s.age] = price + s.sum += price - tail + s.result = s.sum / float64(s.winLen) + s.age = (s.age + 1) % s.winLen +} diff --git a/smma.go b/smma.go new file mode 100644 index 0000000..0d41064 --- /dev/null +++ b/smma.go @@ -0,0 +1,33 @@ +package indicator + +// SMMA Smoothed Moving Average (SMMA) +type SMMA struct { + MABase + sma *SMA + age int + bFirst bool +} + +func NewSMMA(winLen int) *SMMA { + sm := new(SMMA) + sm.winLen = winLen + sm.sma = NewSMA(sm.winLen) + sm.bFirst = true + return sm +} + +func (sm *SMMA) Update(price float64) { + if sm.bFirst { + nLen := sm.age + 1 + if nLen < sm.winLen { + sm.sma.Update(price) + } else if nLen == sm.winLen { + sm.sma.Update(price) + sm.result = sm.sma.Result() + sm.bFirst = false + } + sm.age++ + } else { + sm.result = (sm.result*float64(sm.winLen-1) + price) / float64(sm.winLen) + } +} diff --git a/stoch.go b/stoch.go new file mode 100644 index 0000000..eea480e --- /dev/null +++ b/stoch.go @@ -0,0 +1,74 @@ +package indicator + +// Stoch just test with StochRSI +type Stoch struct { + winLen int + prices []float64 + lowest float64 + highest float64 + age int + bFirst bool + result float64 + kSMA *SMA + dSMA *SMA +} + +func NewStoch(winLen, periodK, periodD int) *Stoch { + s := new(Stoch) + s.winLen = winLen + s.prices = make([]float64, s.winLen) + s.bFirst = true + s.kSMA = NewSMA(periodK) + s.dSMA = NewSMA(periodD) + return s +} + +func (s *Stoch) Update(price float64) { + if s.bFirst { + s.lowest = price + s.highest = price + s.bFirst = false + return + } + s.prices[s.age] = price + s.age = (s.age + 1) % s.winLen + s.highest = highest(s.prices) + s.lowest = lowest(s.prices) + if s.highest == s.lowest { + return + } + s.result = (100 * (price - s.lowest)) / (s.highest - s.lowest) + s.kSMA.Update(s.result) + s.dSMA.Update(s.kSMA.Result()) +} + +func (s *Stoch) Result() float64 { + return s.result +} + +func (s *Stoch) KResult() float64 { + return s.kSMA.Result() +} + +func (s *Stoch) DResult() float64 { + return s.dSMA.Result() +} +func lowest(prices []float64) (ret float64) { + ret = prices[0] + for _, v := range prices { + if v < ret { + ret = v + } + } + return +} + +func highest(prices []float64) (ret float64) { + ret = prices[0] + for _, v := range prices { + if v > ret { + ret = v + } + } + return +} diff --git a/stochrsi.go b/stochrsi.go new file mode 100644 index 0000000..dbadf08 --- /dev/null +++ b/stochrsi.go @@ -0,0 +1,43 @@ +package indicator + +// StochRSI +// result test with aicoin's bitmex data +// maybe it's difference with different website +type StochRSI struct { + winLen int + r *RSI + st *Stoch +} + +func NewStochRSI(winLen, rsiWinLen, k, d int) *StochRSI { + sr := new(StochRSI) + sr.winLen = winLen + sr.r = NewRSI(winLen) + sr.st = NewStoch(rsiWinLen, k, d) + return sr +} + +func (sr *StochRSI) Update(price float64) { + sr.r.Update(price) + sr.st.Update(sr.r.Result()) +} + +func (sr *StochRSI) KResult() float64 { + return sr.st.KResult() +} + +func (sr *StochRSI) DResult() float64 { + return sr.st.DResult() +} + +func (sr *StochRSI) Result() float64 { + return sr.st.Result() +} + +func (sr *StochRSI) FastResult() float64 { + return sr.st.KResult() +} + +func (sr *StochRSI) SlowResult() float64 { + return sr.st.DResult() +} diff --git a/tool.go b/tool.go new file mode 100644 index 0000000..c56047e --- /dev/null +++ b/tool.go @@ -0,0 +1,227 @@ +package indicator + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" +) + +type NewCommonIndicatorFunc func(params ...int) (CommonIndicator, error) + +var ( + ExtraIndicators = map[string]NewCommonIndicatorFunc{} +) + +// CommonIndicator +type CommonIndicator interface { + Indicator + Indicator() map[string]float64 +} + +func RegisterIndicator(name string, fn NewCommonIndicatorFunc) { + ExtraIndicators[name] = fn +} + +type JsonIndicator struct { + CommonIndicator +} + +func NewJsonIndicator(m CommonIndicator) *JsonIndicator { + return &JsonIndicator{CommonIndicator: m} +} + +func (j *JsonIndicator) MarshalJSON() (buf []byte, err error) { + ret := j.Indicator() + buf, err = json.Marshal(ret) + return +} + +func NewCommonIndicator(name string, params ...int) (ind CommonIndicator, err error) { + name = strings.ToUpper(name) + nLen := len(params) + if nLen == 0 { + err = fmt.Errorf("%s params can't be empty", name) + return + } + switch name { + case "EMA": + if nLen >= 2 { + maGroup := NewMAGroup(NewEMA(params[0]), NewEMA(params[1])) + ind = NewMixed(nil, maGroup) + } else { + ema := NewEMA(params[0]) + ind = NewMixed(ema, nil) + } + case "MACD": + if nLen < 3 { + err = fmt.Errorf("%s params not enough", name) + } else { + macd := NewMACD(params[0], params[1], params[2]) + ind = NewMixed(macd, macd) + } + case "SMAMACD": + if nLen < 3 { + err = fmt.Errorf("%s params not enough", name) + } else { + macd := NewMACDWithSMA(params[0], params[1], params[2]) + ind = NewMixed(macd, macd) + } + case "SMA": + if nLen >= 2 { + maGroup := NewMAGroup(NewSMA(params[0]), NewSMA(params[1])) + ind = NewMixed(nil, maGroup) + } else { + sma := NewSMA(params[0]) + ind = NewMixed(sma, nil) + } + case "SMMA": + if nLen >= 2 { + maGroup := NewMAGroup(NewSMMA(params[0]), NewSMMA(params[1])) + ind = NewMixed(nil, maGroup) + } else { + smma := NewSMMA(params[0]) + ind = NewMixed(smma, nil) + } + case "STOCHRSI": + if nLen < 4 { + err = fmt.Errorf("%s params not enough", name) + } else { + stochRSI := NewStochRSI(params[0], params[1], params[2], params[3]) + ind = NewMixed(stochRSI, stochRSI) + } + case "RSI": + if nLen >= 2 { + maGroup := NewMAGroup(NewRSI(params[0]), NewRSI(params[1])) + ind = NewMixed(nil, maGroup) + } else { + rsi := NewRSI(params[0]) + ind = NewMixed(rsi, nil) + } + case "BOLL": + if nLen >= 2 { + boll := NewBoll(params[0], params[1]) + ind = boll + } else { + err = fmt.Errorf("%s params not enough", name) + } + default: + fn, ok := ExtraIndicators[name] + if !ok { + err = fmt.Errorf("%s indicator not support", name) + } else { + ind, err = fn(params...) + } + } + if err == nil { + ind = NewJsonIndicator(ind) + } + return +} + +type Mixed struct { + indicator Indicator + crossIndicator Crosser + isSameOne bool + crossTool *CrossTool +} + +func NewMixed(indicator Indicator, crossIndicator Crosser) *Mixed { + m := new(Mixed) + m.indicator = indicator + m.crossIndicator = crossIndicator + if m.crossIndicator != nil { + m.crossTool = NewCrossTool(m.crossIndicator) + } + m.checkSameOne() + return m +} + +func (m *Mixed) checkSameOne() { + if reflect.ValueOf(m.indicator) == reflect.ValueOf(m.crossIndicator) { + m.isSameOne = true + } else { + m.isSameOne = false + } +} + +func (m *Mixed) Update(price float64) { + if m.crossTool != nil { + m.crossTool.Update(price) + } + if !m.isSameOne && m.indicator != nil { + m.indicator.Update(price) + } +} + +func (m *Mixed) FastResult() float64 { + if m.crossIndicator != nil { + return m.crossIndicator.FastResult() + } + return 0 +} + +func (m *Mixed) SlowResult() float64 { + if m.crossIndicator != nil { + return m.crossIndicator.SlowResult() + } + return 0 +} + +func (m *Mixed) IsCrossUp() bool { + if m.crossTool == nil { + fmt.Println("cross tool is nil") + return false + } + return m.crossTool.IsCrossUp() +} +func (m *Mixed) IsCrossDown() bool { + if m.crossTool == nil { + return false + } + return m.crossTool.IsCrossDown() +} + +func (m *Mixed) Result() float64 { + if m.indicator != nil { + return m.indicator.Result() + } + return 0 +} + +func (m *Mixed) SupportResult() bool { + if m.indicator != nil { + return true + } + return false +} + +func (m *Mixed) SupportSlowFast() bool { + if m.crossIndicator != nil { + return true + } + return false +} + +func (m *Mixed) Indicator() map[string]float64 { + ret := make(map[string]float64) + if m.SupportResult() { + ret["result"] = m.Result() + } + if m.SupportSlowFast() { + ret["fast"] = m.FastResult() + ret["slow"] = m.SlowResult() + if m.IsCrossDown() { + ret["crossDown"] = 1 + } else { + ret["crossDown"] = 0 + } + + if m.IsCrossUp() { + ret["crossUp"] = 1 + } else { + ret["crossUp"] = 0 + } + } + return ret +}