144 lines
4.4 KiB
Go
144 lines
4.4 KiB
Go
package mujica
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"math"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/icons"
|
|
"mujicaui/internal"
|
|
)
|
|
|
|
// PriceAlertLine is a caller-held price alert drawn as a dashed line that the pointer drags.
|
|
type PriceAlertLine struct {
|
|
ID string
|
|
Price float64
|
|
// Label shows beside the price; empty shows the price only.
|
|
Label string
|
|
}
|
|
|
|
// CompareSymbol is another symbol's closes, aligned to the chart's candles, shown in percent.
|
|
type CompareSymbol struct {
|
|
Name string
|
|
Closes []float64
|
|
}
|
|
|
|
// compareLines adds the compare symbols' percent lines and sets percent mode.
|
|
func (m *mktCore) compareLines() {
|
|
if m.opts.Compare == nil || len(*m.opts.Compare) == 0 || m.to <= m.from {
|
|
return
|
|
}
|
|
m.base = m.candles[m.from].Close
|
|
pal := internal.Palette(m.k)
|
|
for i, s := range *m.opts.Compare {
|
|
if s.Name == "" || len(s.Closes) != len(m.candles) {
|
|
panic(fmt.Sprintf("mujica: CompareSymbol %q needs a Name and one close per candle", s.Name))
|
|
}
|
|
base := math.NaN()
|
|
for j := m.from; j < m.to && math.IsNaN(base); j++ {
|
|
base = s.Closes[j]
|
|
}
|
|
closes := s.Closes
|
|
vals := chartMemo(m.ch, "compare."+s.Name, [2]any{chartID(closes), base}, func() []float64 {
|
|
out := make([]float64, len(closes))
|
|
for j, v := range closes {
|
|
out[j] = (v/base - 1) * 100
|
|
}
|
|
return out
|
|
})
|
|
m.overlays = append(m.overlays, mktLine{name: s.Name, values: vals, color: pal[(i+1)%len(pal)], shape: len(m.overlays)})
|
|
}
|
|
}
|
|
|
|
// CompareLegend lists the compare symbols with their colors and a remove button each; Changed reports a removal.
|
|
func CompareLegend(c *ui.Context, symbols *[]CompareSymbol) ChoiceResult {
|
|
if symbols == nil {
|
|
panic("mujica: CompareLegend needs symbols")
|
|
}
|
|
k := Tokens(c)
|
|
pal := internal.Palette(k)
|
|
u := Density(c).Unit()
|
|
removed := -1
|
|
row := ui.Row(c).Wrap().Gap(u * 2).Role(ui.RoleList).Label(msg(c, "compare_symbol.label"))
|
|
row.Children(func() {
|
|
for i, s := range *symbols {
|
|
ui.Row(c).Key(s.Name).AlignItems(ui.Center).Gap(u).Padding(0, 0, 0, u*2).Radius(6).Border(1, k.Border).Role(ui.RoleListItem).Label(s.Name).Children(func() {
|
|
col := pal[(i+1)%len(pal)]
|
|
ui.Box(c).Size(10, 10).Draw(func(p *ui.Painter, r ui.Rect) { internal.Marker(p, i+1, r.X+5, r.Y+5, 9, col) })
|
|
ui.Text(c, s.Name).SingleLine()
|
|
if iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "compare_symbol.remove"), s.Name)).Clicked() && !row.IsDisabled() {
|
|
removed = i
|
|
}
|
|
})
|
|
}
|
|
})
|
|
if removed >= 0 {
|
|
slog.Debug("mujica: compare_symbol removed", "name", (*symbols)[removed].Name)
|
|
*symbols = append((*symbols)[:removed:removed], (*symbols)[removed+1:]...)
|
|
}
|
|
return ChoiceResult{row, removed >= 0}
|
|
}
|
|
|
|
// alertsCheck runs OnAlert for each alert the newest close crossed since the last frame.
|
|
func (m *mktCore) alertsCheck() {
|
|
if m.opts.Alerts == nil || len(m.candles) == 0 {
|
|
return
|
|
}
|
|
last := m.candles[len(m.candles)-1]
|
|
if m.st.alertSeen && last.Close != m.st.lastClose && m.opts.OnAlert != nil {
|
|
for _, a := range *m.opts.Alerts {
|
|
if (m.st.lastClose-a.Price)*(last.Close-a.Price) <= 0 && m.st.lastClose != a.Price {
|
|
slog.Debug("mujica: price_alert_line crossed", "id", a.ID)
|
|
m.opts.OnAlert(a, last)
|
|
}
|
|
}
|
|
}
|
|
m.st.lastClose, m.st.alertSeen = last.Close, true
|
|
}
|
|
|
|
// alertsInput drags an alert line that the press started on; it reports taking the drag.
|
|
func (m *mktCore) alertsInput(g mktGeom, x, y float32, held bool, dy float32) bool {
|
|
if m.opts.Alerts == nil || m.base != 0 {
|
|
return false
|
|
}
|
|
alerts := *m.opts.Alerts
|
|
if !held {
|
|
m.st.grab = -1
|
|
return false
|
|
}
|
|
if m.st.grab < 0 {
|
|
for i, a := range alerts {
|
|
if math.Abs(float64(g.ys.Map(a.Price)-(y-dy))) <= 5 {
|
|
m.st.grab = i
|
|
}
|
|
}
|
|
}
|
|
if m.st.grab < 0 || m.st.grab >= len(alerts) {
|
|
return false
|
|
}
|
|
p := math.Pow(10, float64(m.dec))
|
|
alerts[m.st.grab].Price = math.Round(g.ys.Invert(y)*p) / p
|
|
slog.Debug("mujica: price_alert_line moved", "id", alerts[m.st.grab].ID)
|
|
return true
|
|
}
|
|
|
|
// paintAlerts draws each alert as a dashed line with its price tag.
|
|
func (m *mktCore) paintAlerts(p *ui.Painter, g mktGeom, size float32) {
|
|
if m.opts.Alerts == nil || m.base != 0 {
|
|
return
|
|
}
|
|
for _, a := range *m.opts.Alerts {
|
|
y := internal.Snap(p, g.ys.Map(a.Price))
|
|
for x := g.plot.X; x < g.plot.X+g.plot.W; x += 8 {
|
|
p.Line(x, y, min(x+4, g.plot.X+g.plot.W), y, 1, m.k.Warning)
|
|
}
|
|
text := priceUngrouped(a.Price, m.dec)
|
|
if a.Label != "" {
|
|
text = a.Label + " " + text
|
|
}
|
|
mktTag(p, g.plot.X+g.plot.W, y, fmt.Sprintf(msg(m.c, "price_alert_line.tag"), text), size, m.k.Warning, m.k.Surface)
|
|
}
|
|
}
|