Files
MujicaUI/level2_quotes.go
T

124 lines
3.8 KiB
Go

package mujica
import (
"fmt"
"log/slog"
"time"
"github.com/egoist/mygo/ui"
"mujicaui/theme"
)
// L2Level is a level's price, size and order count.
type L2Level struct {
Price, Size float64
Orders int
}
// Level2Data holds the bid and ask levels, best first.
type Level2Data struct {
Bids, Asks []L2Level
}
// Level2QuotesOptions configure a Level2Quotes.
type Level2QuotesOptions struct {
// Decimals of prices and sizes; zero means 2 and 0.
Decimals, SizeDecimals int
// Levels shown each side; zero means 5.
Levels int
}
// level2Mark is a level's last values and size move.
type level2Mark struct {
price, size float64
dir int
at time.Time
seen bool
}
// Level2Quotes lists bid and ask levels; changes flash and mark.
func Level2Quotes(c *ui.Context, data Level2Data, opts Level2QuotesOptions) *ui.Element {
dec := priceDecimals("Level2Quotes", opts.Decimals)
if opts.SizeDecimals < 0 || opts.Levels < 0 {
panic("mujica: Level2Quotes SizeDecimals and Levels cannot be negative")
}
n := opts.Levels
if n == 0 {
n = 5
}
u := Density(c).Unit()
e := ui.Row(c).Gap(u * 3).AlignItems(ui.Start).Role(ui.RoleTable).Label(msg(c, "level2_quotes.label"))
marks := ui.Local(e, "marks", func() map[string]*level2Mark { return map[string]*level2Mark{} })
e.Children(func() {
for _, side := range []OrderSide{SideBuy, SideSell} {
levels := data.Bids
if side == SideSell {
levels = data.Asks
}
ui.Column(c).Grow(1).Basis(0).MinWidth(0).Children(func() {
for i := range n {
var l *L2Level
if i < len(levels) {
l = &levels[i]
}
level2Row(c, *marks, side, i, l, dec, opts.SizeDecimals)
}
})
}
})
return e
}
// level2Row is one level with its move mark.
func level2Row(c *ui.Context, marks map[string]*level2Mark, side OrderSide, i int, l *L2Level, dec, sizeDec int) {
k := Tokens(c)
d := Density(c)
name := fmt.Sprintf(msg(c, fmt.Sprintf("level2_quotes.side%d", side)), i+1)
key := fmt.Sprint(side, i)
m := marks[key]
if m == nil {
m = &level2Mark{}
marks[key] = m
}
row := ui.Row(c).Gap(d.Unit()*2).AlignItems(ui.Center).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).
Radius(theme.ControlRadius).BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border)
if l == nil {
row.Children(func() {
ui.Text(c, name).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).Width(28)
ui.Text(c, "—").TextColor(k.TextMuted)
})
return
}
if m.seen && (l.Price != m.price || l.Size != m.size) {
// A new price is a new level, not a move.
m.dir, m.at = 0, c.Now()
if l.Price == m.price {
m.dir = trendSign(l.Size - m.size)
}
slog.Debug("mujica: level2_quotes level changed", "level", name)
}
m.price, m.size, m.seen = l.Price, l.Size, true
if dur := Motion(c, priceTextFlash); dur > 0 && !m.at.IsZero() {
if t := c.Now().Sub(m.at); t < dur {
row.Background(k.Selection.Alpha(1 - float32(float64(t)/float64(dur))))
c.AnimationFrame()
}
}
col := trendColor(c, side.sign())
sizeText := statisticFormat(l.Size, sizeDec)
orders := fmt.Sprintf(msg(c, "level2_quotes.orders"), l.Orders)
row.Role(ui.RoleRow).Label(name + " " + statisticFormat(l.Price, dec) + " " + sizeText + " " + orders)
row.Children(func() {
ui.Text(c, name).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).Width(28)
ui.Text(c, statisticFormat(l.Price, dec)).TextColor(col).FontFeatures("tnum").FontWeight(600).Grow(1).Basis(0).SingleLine()
ui.Row(c).Gap(2).AlignItems(ui.Center).Justify(ui.End).Grow(1).Basis(0).Children(func() {
if m.dir != 0 {
ui.Icon(c, trendIcon(m.dir)).Size(12, 12).TextColor(trendColor(c, m.dir))
}
ui.Text(c, sizeText).FontFeatures("tnum").SingleLine()
})
ui.Text(c, fmt.Sprint(l.Orders)).TextColor(k.TextMuted).FontFeatures("tnum").TextAlign(ui.End).Width(36).SingleLine()
})
}