Files
MujicaUI/indicators.go
T

199 lines
4.7 KiB
Go

package mujica
import (
"fmt"
"math"
)
// Indicator helpers are pure; NaN marks insufficient 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 computes the n-period simple moving average.
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 uses 2/(n+1), seeded by the first n-period SMA.
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 SMA and population-deviation bands.
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 averages typical prices weighted by cumulative volume.
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, signal EMA, and their difference.
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 computes Wilder's relative strength index.
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 computes stochastic K, D, and J from RSV.
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 accumulates signed volume according to close direction.
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
}