Files
MujicaUI/time_and_sales.go
T

138 lines
4.4 KiB
Go

package mujica
import (
"fmt"
"log/slog"
"time"
"github.com/egoist/mygo/ui"
"mujicaui/internal"
"mujicaui/theme"
)
// TradePrint is one executed trade on the tape.
type TradePrint struct {
// ID identifies the trade; required and unique.
ID string
Time time.Time
Price float64
Size float64
Side OrderSide
}
// TimeAndSalesOptions configure a TimeAndSales.
type TimeAndSalesOptions struct {
// Decimals of prices and sizes; zero means 2 and 0.
Decimals, SizeDecimals int
// LargeSize marks trades at or above it; zero marks none.
LargeSize float64
// Height of the list in DIPs; zero means 320.
Height float32
Status DataStatus
Error string
}
// TimeAndSalesState is the tape's follow state and list place.
type TimeAndSalesState struct {
// Paused stops new trades from showing until resumed.
Paused bool
frozen int
shown int
list ListState[string]
}
// TimeAndSales lists trades newest on top; Paused holds the view.
func TimeAndSales(c *ui.Context, s *TimeAndSalesState, trades []TradePrint, opts TimeAndSalesOptions) ChoiceResult {
dec := priceDecimals("TimeAndSales", opts.Decimals)
if opts.SizeDecimals < 0 || opts.LargeSize < 0 || opts.Height < 0 {
panic("mujica: TimeAndSales SizeDecimals, LargeSize and Height cannot be negative")
}
height := opts.Height
if height == 0 {
height = 320
}
if !s.Paused {
s.frozen = len(trades)
}
s.frozen = min(s.frozen, len(trades))
n := s.frozen
if !s.Paused && n != s.shown {
s.list.ScrollTo(0)
}
s.shown = n
k := Tokens(c)
d := Density(c)
var r ChoiceResult
r.Element = ui.Column(c).Gap(d.Unit() * 2).Height(height)
r.Element.Children(func() {
ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() {
ui.Text(c, msg(c, "time_and_sales.title")).Font(theme.SerifFont).FontWeight(600).Grow(1)
label := msg(c, "time_and_sales.pause")
if s.Paused {
label = msg(c, "time_and_sales.resume")
if waiting := len(trades) - n; waiting > 0 {
label = fmt.Sprintf(msg(c, "time_and_sales.resume_count"), waiting)
}
}
if Button(c, label, ButtonOptions{Variant: Secondary}).Clicked() {
s.Paused, r.changed = !s.Paused, true
slog.Debug("mujica: time_and_sales follow", "paused", s.Paused)
}
})
ui.Row(c).Padding(0, d.Unit()*4).Gap(d.Unit() * 2).Children(func() {
for i, key := range []string{"time_and_sales.time", "time_and_sales.price", "time_and_sales.size"} {
t := ui.Text(c, msg(c, key)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Grow(1).Basis(0)
if i > 0 {
t.TextAlign(ui.End)
}
}
ui.Box(c).Width(16)
})
at := func(i int) TradePrint { return trades[n-1-i] }
List(c, &s.list, n, ListOptions[string]{
Key: func(i int) string { return at(i).ID },
Label: func(i int) string {
t := at(i)
return timeAndSalesSpoken(c, t, dec, opts)
},
Status: opts.Status, Error: opts.Error, Empty: msg(c, "time_and_sales.empty"),
}, func(i int) { timeAndSalesRow(c, at(i), dec, opts) }).Element.Grow(1)
})
return r
}
func timeAndSalesLarge(t TradePrint, opts TimeAndSalesOptions) bool {
return opts.LargeSize > 0 && t.Size >= opts.LargeSize
}
// timeAndSalesSpoken reads a trade with its side and size.
func timeAndSalesSpoken(c *ui.Context, t TradePrint, dec int, opts TimeAndSalesOptions) string {
s := fmt.Sprintf("%s %s %s %s", t.Time.Format("15:04:05"), msg(c, fmt.Sprintf("order_book.side%d", t.Side)),
statisticFormat(t.Price, dec), statisticFormat(t.Size, opts.SizeDecimals))
if timeAndSalesLarge(t, opts) {
s += ", " + msg(c, "time_and_sales.large")
}
return s
}
// timeAndSalesRow is one trade with its side and large mark.
func timeAndSalesRow(c *ui.Context, t TradePrint, dec int, opts TimeAndSalesOptions) {
k := Tokens(c)
large := timeAndSalesLarge(t, opts)
sign := t.Side.sign()
ui.Text(c, t.Time.Format("15:04:05")).FontFeatures("tnum").TextColor(k.TextMuted).Grow(1).Basis(0).SingleLine()
ui.Row(c).Gap(2).Justify(ui.End).AlignItems(ui.Center).Grow(1).Basis(0).TextColor(trendColor(c, sign)).Children(func() {
ui.Icon(c, trendIcon(sign)).Size(12, 12)
ui.Text(c, statisticFormat(t.Price, dec)).FontFeatures("tnum").SingleLine()
})
size := ui.Text(c, statisticFormat(t.Size, opts.SizeDecimals)).FontFeatures("tnum").TextAlign(ui.End).Grow(1).Basis(0).SingleLine()
mark := ui.Box(c).Size(16, 16).Shrink(0)
if large {
size.FontWeight(700)
mark.Draw(func(p *ui.Painter, r ui.Rect) {
p.FillPath(internal.Diamond(r.X+r.W/2, r.Y+r.H/2, 10), k.AccentText)
})
}
}