138 lines
4.4 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|