199 lines
4.9 KiB
Go
199 lines
4.9 KiB
Go
package mujica
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
// Indicator functions are pure: same input, same output, NaN until enough data.
|
|
|
|
// indicatorCheck panics on a period below one.
|
|
func indicatorCheck(name string, n int) {
|
|
if n < 1 {
|
|
panic(fmt.Sprintf("mujica: %s period %d must be at least 1", name, n))
|
|
}
|
|
}
|
|
|
|
func indicatorNaN(n int) []float64 {
|
|
out := make([]float64, n)
|
|
for i := range out {
|
|
out[i] = math.NaN()
|
|
}
|
|
return out
|
|
}
|
|
|
|
// SMA is the simple moving average of the last n values.
|
|
func SMA(vals []float64, n int) []float64 {
|
|
indicatorCheck("SMA", n)
|
|
out := indicatorNaN(len(vals))
|
|
var sum float64
|
|
for i, v := range vals {
|
|
sum += v
|
|
if i >= n {
|
|
sum -= vals[i-n]
|
|
}
|
|
if i >= n-1 {
|
|
out[i] = sum / float64(n)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// EMA is the exponential moving average, k = 2/(n+1), seeded with the SMA of the first n values.
|
|
func EMA(vals []float64, n int) []float64 {
|
|
indicatorCheck("EMA", n)
|
|
out := indicatorNaN(len(vals))
|
|
k := 2 / float64(n+1)
|
|
var sum, prev float64
|
|
for i, v := range vals {
|
|
switch {
|
|
case i < n-1:
|
|
sum += v
|
|
case i == n-1:
|
|
prev = (sum + v) / float64(n)
|
|
out[i] = prev
|
|
default:
|
|
prev = v*k + prev*(1-k)
|
|
out[i] = prev
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Bollinger returns the n-period SMA and the bands width population deviations away.
|
|
func Bollinger(vals []float64, n int, width float64) (mid, upper, lower []float64) {
|
|
indicatorCheck("Bollinger", n)
|
|
if !(width > 0) {
|
|
panic("mujica: Bollinger width must be positive")
|
|
}
|
|
mid = SMA(vals, n)
|
|
upper, lower = indicatorNaN(len(vals)), indicatorNaN(len(vals))
|
|
for i := n - 1; i < len(vals); i++ {
|
|
var ss float64
|
|
for _, v := range vals[i-n+1 : i+1] {
|
|
ss += (v - mid[i]) * (v - mid[i])
|
|
}
|
|
d := width * math.Sqrt(ss/float64(n))
|
|
upper[i], lower[i] = mid[i]+d, mid[i]-d
|
|
}
|
|
return mid, upper, lower
|
|
}
|
|
|
|
// VWAP is the cumulative volume-weighted average of the typical price (H+L+C)/3.
|
|
func VWAP(candles []Candle) []float64 {
|
|
out := indicatorNaN(len(candles))
|
|
var pv, vol float64
|
|
for i, c := range candles {
|
|
pv += (c.High + c.Low + c.Close) / 3 * c.Volume
|
|
vol += c.Volume
|
|
if vol > 0 {
|
|
out[i] = pv / vol
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// MACD returns DIF (fast EMA - slow EMA), DEA (signal EMA of DIF) and the histogram DIF - DEA.
|
|
func MACD(vals []float64, fast, slow, signal int) (dif, dea, hist []float64) {
|
|
if fast >= slow {
|
|
panic("mujica: MACD fast period must be shorter than slow")
|
|
}
|
|
f, s := EMA(vals, fast), EMA(vals, slow)
|
|
dif = indicatorNaN(len(vals))
|
|
for i := range vals {
|
|
dif[i] = f[i] - s[i]
|
|
}
|
|
dea = indicatorNaN(len(vals))
|
|
if len(vals) >= slow {
|
|
tail := EMA(dif[slow-1:], signal)
|
|
copy(dea[slow-1:], tail)
|
|
}
|
|
hist = indicatorNaN(len(vals))
|
|
for i := range vals {
|
|
hist[i] = dif[i] - dea[i]
|
|
}
|
|
return dif, dea, hist
|
|
}
|
|
|
|
// RSI is Wilder's relative strength index over n periods, 0 to 100.
|
|
func RSI(vals []float64, n int) []float64 {
|
|
indicatorCheck("RSI", n)
|
|
out := indicatorNaN(len(vals))
|
|
var gain, loss float64
|
|
for i := 1; i < len(vals); i++ {
|
|
d := vals[i] - vals[i-1]
|
|
g, l := max(d, 0), max(-d, 0)
|
|
if i <= n {
|
|
gain += g
|
|
loss += l
|
|
if i < n {
|
|
continue
|
|
}
|
|
gain, loss = gain/float64(n), loss/float64(n)
|
|
} else {
|
|
gain = (gain*float64(n-1) + g) / float64(n)
|
|
loss = (loss*float64(n-1) + l) / float64(n)
|
|
}
|
|
if loss == 0 {
|
|
out[i] = 100
|
|
} else {
|
|
out[i] = 100 - 100/(1+gain/loss)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// KDJ is the stochastic oscillator: RSV over n, K and D smoothed by m1 and m2 from 50, J = 3K - 2D.
|
|
func KDJ(candles []Candle, n, m1, m2 int) (k, d, j []float64) {
|
|
indicatorCheck("KDJ", n)
|
|
indicatorCheck("KDJ m1", m1)
|
|
indicatorCheck("KDJ m2", m2)
|
|
k, d, j = indicatorNaN(len(candles)), indicatorNaN(len(candles)), indicatorNaN(len(candles))
|
|
pk, pd := 50.0, 50.0
|
|
for i := n - 1; i < len(candles); i++ {
|
|
lo, hi := math.Inf(1), math.Inf(-1)
|
|
for _, c := range candles[i-n+1 : i+1] {
|
|
lo, hi = min(lo, c.Low), max(hi, c.High)
|
|
}
|
|
rsv := 50.0
|
|
if hi > lo {
|
|
rsv = (candles[i].Close - lo) / (hi - lo) * 100
|
|
}
|
|
pk = (float64(m1-1)*pk + rsv) / float64(m1)
|
|
pd = (float64(m2-1)*pd + pk) / float64(m2)
|
|
k[i], d[i], j[i] = pk, pd, 3*pk-2*pd
|
|
}
|
|
return k, d, j
|
|
}
|
|
|
|
// OBV is on-balance volume: volume added on up closes, subtracted on down closes.
|
|
func OBV(candles []Candle) []float64 {
|
|
out := make([]float64, len(candles))
|
|
for i := 1; i < len(candles); i++ {
|
|
out[i] = out[i-1]
|
|
switch {
|
|
case candles[i].Close > candles[i-1].Close:
|
|
out[i] += candles[i].Volume
|
|
case candles[i].Close < candles[i-1].Close:
|
|
out[i] -= candles[i].Volume
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// HeikinAshi turns candles into Heikin-Ashi candles, keeping times and volumes.
|
|
func HeikinAshi(candles []Candle) []Candle {
|
|
out := make([]Candle, len(candles))
|
|
for i, c := range candles {
|
|
h := Candle{Time: c.Time, Volume: c.Volume, Close: (c.Open + c.High + c.Low + c.Close) / 4}
|
|
if i == 0 {
|
|
h.Open = (c.Open + c.Close) / 2
|
|
} else {
|
|
h.Open = (out[i-1].Open + out[i-1].Close) / 2
|
|
}
|
|
h.High, h.Low = max(c.High, h.Open, h.Close), min(c.Low, h.Open, h.Close)
|
|
out[i] = h
|
|
}
|
|
return out
|
|
}
|