179 lines
5.7 KiB
Go
179 lines
5.7 KiB
Go
package mujica
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"math"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/icons"
|
|
"mujicaui/internal"
|
|
"mujicaui/theme"
|
|
)
|
|
|
|
// DepthLadderOptions configure a DepthLadder. TickSize is required.
|
|
type DepthLadderOptions struct {
|
|
TickSize float64
|
|
// Decimals of prices and sizes; zero means 2 and 0.
|
|
Decimals, SizeDecimals int
|
|
// Rows shown; zero means 15.
|
|
Rows int
|
|
Disabled bool
|
|
}
|
|
|
|
// DepthLadderState is the ladder's scroll from last price, in ticks.
|
|
type DepthLadderState struct {
|
|
Offset int
|
|
cache depthLadderCache
|
|
}
|
|
|
|
type depthLadderCache struct {
|
|
key bookKey
|
|
bids, asks map[int64]float64
|
|
}
|
|
|
|
// DepthLadderResult is a built DepthLadder and its clicked cell.
|
|
type DepthLadderResult struct {
|
|
Element *ui.Element
|
|
price float64
|
|
side OrderSide
|
|
}
|
|
|
|
// Clicked returns the clicked cell's price and side.
|
|
func (r DepthLadderResult) Clicked() (price float64, side OrderSide, ok bool) {
|
|
return r.price, r.side, r.side != 0
|
|
}
|
|
|
|
// DepthLadder is a price ladder around last; cell clicks order.
|
|
func DepthLadder(c *ui.Context, s *DepthLadderState, last float64, book OrderBookData, opts DepthLadderOptions) DepthLadderResult {
|
|
dec := priceDecimals("DepthLadder", opts.Decimals)
|
|
if opts.SizeDecimals < 0 || opts.Rows < 0 {
|
|
panic("mujica: DepthLadder SizeDecimals and Rows cannot be negative")
|
|
}
|
|
scale := math.Pow(10, float64(dec))
|
|
tick := int64(math.Round(opts.TickSize * scale))
|
|
if tick < 1 || math.Abs(opts.TickSize*scale-float64(tick)) > 1e-6 {
|
|
panic(fmt.Sprintf("mujica: DepthLadder TickSize %v is not a whole number of %d-decimal units", opts.TickSize, dec))
|
|
}
|
|
rows := opts.Rows
|
|
if rows == 0 {
|
|
rows = 15
|
|
}
|
|
depthLadderRefresh(s, book, scale, tick)
|
|
center := int64(math.Round(last*scale/float64(tick))) * tick
|
|
k := Tokens(c)
|
|
d := Density(c)
|
|
var r DepthLadderResult
|
|
r.Element = ui.Column(c).Gap(d.Unit()).Disabled(opts.Disabled).Role(ui.RoleTable).Label(msg(c, "depth_ladder.label"))
|
|
disabled := r.Element.IsDisabled()
|
|
move := func(by int) {
|
|
if !disabled && by != 0 {
|
|
s.Offset += by
|
|
slog.Debug("mujica: depth_ladder scrolled", "offset", s.Offset)
|
|
}
|
|
}
|
|
if r.Element.Shortcut(0, ui.KeyPageUp) {
|
|
move(rows / 2)
|
|
}
|
|
if r.Element.Shortcut(0, ui.KeyPageDown) {
|
|
move(-rows / 2)
|
|
}
|
|
r.Element.Children(func() {
|
|
ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() {
|
|
ui.Text(c, msg(c, "depth_ladder.bid")).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).Grow(1).Basis(0).TextAlign(ui.End)
|
|
ui.Text(c, msg(c, "depth_ladder.price")).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).Grow(1).Basis(0).TextAlign(ui.Center)
|
|
ui.Text(c, msg(c, "depth_ladder.ask")).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).Grow(1).Basis(0)
|
|
})
|
|
grid := ui.Column(c).FocusGroup(ui.Vertical | ui.Horizontal)
|
|
grid.HandleInput(func(ev ui.InputEvent) bool {
|
|
if ev.Kind != ui.InputScroll || disabled || ev.DY == 0 {
|
|
return false
|
|
}
|
|
move(int(math.Copysign(1, float64(-ev.DY))))
|
|
return true
|
|
})
|
|
grid.Children(func() {
|
|
top := center + int64(s.Offset+rows/2)*tick
|
|
for i := range rows {
|
|
t := top - int64(i)*tick
|
|
depthLadderRow(c, &r, s, t, t == center, scale, dec, opts.SizeDecimals)
|
|
}
|
|
})
|
|
if s.Offset != 0 {
|
|
if Button(c, msg(c, "depth_ladder.recenter"), ButtonOptions{Variant: Ghost, Icon: icons.Must("refresh-cw"), Disabled: disabled}).Clicked() {
|
|
s.Offset = 0
|
|
}
|
|
}
|
|
})
|
|
return r
|
|
}
|
|
|
|
// depthLadderRow is a buy cell, the price, a sell cell.
|
|
func depthLadderRow(c *ui.Context, r *DepthLadderResult, s *DepthLadderState, t int64, current bool, scale float64, dec, sizeDec int) {
|
|
k := Tokens(c)
|
|
d := Density(c)
|
|
price := float64(t) / scale
|
|
text := statisticFormat(price, dec)
|
|
ui.Row(c).Key(t).MinHeight(d.ControlHeight()).AlignItems(ui.Stretch).BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border).Children(func() {
|
|
cell := func(side OrderSide, size float64) {
|
|
label := fmt.Sprintf(msg(c, fmt.Sprintf("depth_ladder.order%d", side)), text)
|
|
b := ui.ButtonBase(c).Grow(1).Basis(0).Padding(0, d.Unit()*2).Label(label)
|
|
if side == SideBuy {
|
|
b.Justify(ui.End)
|
|
} else {
|
|
b.Justify(ui.Start)
|
|
}
|
|
internal.Ring(b, k.Focus, 0)
|
|
if b.Hovered() {
|
|
b.Background(trendColor(c, side.sign()).Alpha(0.14))
|
|
}
|
|
b.Children(func() {
|
|
if size > 0 {
|
|
ui.Text(c, statisticFormat(size, sizeDec)).TextColor(trendColor(c, side.sign())).FontFeatures("tnum").FontWeight(600).SingleLine()
|
|
}
|
|
})
|
|
if b.Clicked() && !b.IsDisabled() {
|
|
r.price, r.side = price, side
|
|
slog.Debug("mujica: depth_ladder order", "side", side)
|
|
}
|
|
}
|
|
cell(SideBuy, s.cache.bids[t])
|
|
mid := ui.Row(c).Grow(1).Basis(0).Justify(ui.Center).AlignItems(ui.Center).Gap(d.Unit()).Background(k.Background)
|
|
if current {
|
|
mid.Background(k.Selection).Label(msg(c, "depth_ladder.last") + " " + text)
|
|
mid.DrawOver(func(p *ui.Painter, rect ui.Rect) {
|
|
internal.Frame(p, rect, 2, 4, k.AccentText)
|
|
})
|
|
}
|
|
mid.Children(func() {
|
|
if current {
|
|
ui.Box(c).Size(8, 8).Draw(func(p *ui.Painter, rect ui.Rect) {
|
|
p.FillPath(internal.Diamond(rect.X+4, rect.Y+4, 8), k.AccentText)
|
|
})
|
|
}
|
|
t := ui.Text(c, text).FontFeatures("tnum").SingleLine()
|
|
if current {
|
|
t.FontWeight(700)
|
|
}
|
|
})
|
|
cell(SideSell, s.cache.asks[t])
|
|
})
|
|
}
|
|
|
|
// depthLadderRefresh indexes sizes by tick when the book changed.
|
|
func depthLadderRefresh(s *DepthLadderState, book OrderBookData, scale float64, tick int64) {
|
|
key := bookKeyOf(book, float64(tick)/scale)
|
|
if s.cache.bids != nil && s.cache.key == key {
|
|
return
|
|
}
|
|
index := func(ls []BookLevel) map[int64]float64 {
|
|
m := make(map[int64]float64, len(ls))
|
|
for _, l := range ls {
|
|
m[int64(math.Round(l.Price*scale/float64(tick)))*tick] += l.Size
|
|
}
|
|
return m
|
|
}
|
|
s.cache = depthLadderCache{key: key, bids: index(book.Bids), asks: index(book.Asks)}
|
|
}
|