Files
MujicaUI/indicator_overlay.go
T

184 lines
5.6 KiB
Go

package mujica
import (
"fmt"
"log/slog"
"math"
"strconv"
"strings"
"github.com/egoist/mygo/ui"
"mujicaui/internal"
)
// IndicatorKind names a technical indicator.
type IndicatorKind int
const (
IndicatorMA IndicatorKind = iota
IndicatorEMA
IndicatorBOLL
IndicatorVWAP
IndicatorMACD
IndicatorRSI
IndicatorKDJ
IndicatorOBV
)
// indicatorInfo describes names, default parameters, and plotting locations.
var indicatorInfo = []struct {
name string
params []float64
overlay bool
}{
{"MA", []float64{20}, true}, {"EMA", []float64{20}, true}, {"BOLL", []float64{20, 2}, true}, {"VWAP", nil, true},
{"MACD", []float64{12, 26, 9}, false}, {"RSI", []float64{14}, false}, {"KDJ", []float64{9, 3, 3}, false}, {"OBV", nil, false},
}
func (k IndicatorKind) String() string {
if k < IndicatorMA || k > IndicatorOBV {
panic(fmt.Sprintf("mujica: unknown IndicatorKind %d", int(k)))
}
return indicatorInfo[k].name
}
// DefaultParams returns a fresh copy of the kind's usual parameters.
func (k IndicatorKind) DefaultParams() []float64 {
_ = k.String()
return append([]float64(nil), indicatorInfo[k].params...)
}
// IndicatorOverlay configures MA, EMA, BOLL, or VWAP.
type IndicatorOverlay struct {
Kind IndicatorKind
// Params nil uses the indicator's defaults.
Params []float64
// Values overrides calculation; supply one value per candle.
Values []float64
}
// indicatorParams resolves and checks params for kind.
func indicatorParams(k IndicatorKind, params []float64) []float64 {
_ = k.String()
want := indicatorInfo[k].params
if params == nil {
return want
}
if len(params) != len(want) {
panic(fmt.Sprintf("mujica: %v takes %d parameters, not %d", k, len(want), len(params)))
}
for i, p := range params {
if math.IsInf(p, 0) || !(p > 0) || (k != IndicatorBOLL || i == 0) && p != float64(int(p)) {
panic(fmt.Sprintf("mujica: %v parameter %v must be a positive whole number", k, p))
}
}
if k == IndicatorMACD && params[0] >= params[1] {
panic("mujica: MACD fast period must be below slow period")
}
return params
}
// indicatorName is the kind with its parameters, as "MACD(12,26,9)".
func indicatorName(k IndicatorKind, params []float64) string {
if len(params) == 0 {
return k.String()
}
parts := make([]string, len(params))
for i, p := range params {
parts[i] = strconv.FormatFloat(p, 'f', -1, 64)
}
return k.String() + "(" + strings.Join(parts, ",") + ")"
}
// indicatorSeries computes named indicator lines from candles.
func indicatorSeries(k IndicatorKind, params []float64, candles []Candle) []ChartSeries {
p := indicatorParams(k, params)
closes := make([]float64, len(candles))
for i, c := range candles {
closes[i] = c.Close
}
name := indicatorName(k, p)
switch k {
case IndicatorMA:
return []ChartSeries{{name, SMA(closes, int(p[0]))}}
case IndicatorEMA:
return []ChartSeries{{name, EMA(closes, int(p[0]))}}
case IndicatorBOLL:
mid, up, lo := Bollinger(closes, int(p[0]), p[1])
return []ChartSeries{{name, mid}, {name + " ↑", up}, {name + " ↓", lo}}
case IndicatorVWAP:
return []ChartSeries{{name, VWAP(candles)}}
case IndicatorMACD:
dif, dea, hist := MACD(closes, int(p[0]), int(p[1]), int(p[2]))
return []ChartSeries{{"DIF", dif}, {"DEA", dea}, {"MACD", hist}}
case IndicatorRSI:
return []ChartSeries{{name, RSI(closes, int(p[0]))}}
case IndicatorKDJ:
kk, d, j := KDJ(candles, int(p[0]), int(p[1]), int(p[2]))
return []ChartSeries{{"K", kk}, {"D", d}, {"J", j}}
}
return []ChartSeries{{name, OBV(candles)}}
}
// mktOverlayLines caches indicators until data or parameters change.
func mktOverlayLines(ch *chart, m *mktCore, candles []Candle) []mktLine {
pal := internal.Palette(m.k)
var out []mktLine
for i, o := range m.opts.Overlays {
if _ = o.Kind.String(); !indicatorInfo[o.Kind].overlay {
panic(fmt.Sprintf("mujica: %v is not an overlay indicator", o.Kind))
}
col := pal[[]int{3, 1, 5, 2, 4}[i%5]]
if o.Values != nil {
if len(o.Values) != len(candles) {
panic("mujica: IndicatorOverlay Values need one value per candle")
}
out = append(out, mktLine{name: indicatorName(o.Kind, indicatorParams(o.Kind, o.Params)), values: o.Values, color: col, shape: len(out)})
continue
}
key := fmt.Sprintf("%s|%d|%v", chartID(candles), o.Kind, o.Params)
series := chartMemo(ch, fmt.Sprint("overlay.", i), key, func() []ChartSeries { return indicatorSeries(o.Kind, o.Params, candles) })
for _, s := range series {
out = append(out, mktLine{name: s.Name, values: s.Values, color: col, shape: len(out)})
}
}
return out
}
// IndicatorParamsField edits validated indicator parameters in place.
func IndicatorParamsField(c *ui.Context, kind IndicatorKind, params *[]float64) ChoiceResult {
if params == nil {
panic("mujica: IndicatorParamsField needs params")
}
row := ui.Row(c).Gap(Density(c).Unit() * 2).AlignItems(ui.Center).Role(ui.RoleGroup).Label(kind.String())
if row.IsDisabled() {
copy := append([]float64(nil), (*params)...)
params = &copy
}
if *params == nil {
*params = kind.DefaultParams()
}
changed := false
indicatorParams(kind, *params)
row.Children(func() {
for i := range *params {
v := (*params)[i]
step, prec := 1.0, 0
if kind == IndicatorBOLL && i == 1 {
step, prec = 0.1, 1
}
if NumberInput(c, &v, NumberInputOptions{Min: step, Max: 500, Step: step, Precision: prec, Label: fmt.Sprintf("%v %d", kind, i+1)}).Element.Width(110).Key(i).Changed() || v != (*params)[i] {
if kind == IndicatorMACD && (i == 0 && v >= (*params)[1] || i == 1 && v <= (*params)[0]) {
continue
}
(*params)[i], changed = v, true
}
}
})
if changed {
slog.Debug("mujica: indicator_overlay params", "kind", kind)
}
return ChoiceResult{row, changed}
}