184 lines
6.2 KiB
Go
184 lines
6.2 KiB
Go
package mujica
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/internal"
|
|
"mujicaui/theme"
|
|
)
|
|
|
|
// DepthLevel is one price level of an order book.
|
|
type DepthLevel struct {
|
|
Price, Size float64
|
|
}
|
|
|
|
// DepthChartOptions configure a DepthChart.
|
|
type DepthChartOptions struct {
|
|
Frame ChartFrame
|
|
Decimals int
|
|
}
|
|
|
|
// depthData holds cumulative bids and asks around the midpoint.
|
|
type depthData struct {
|
|
price, total []float64
|
|
nBids int
|
|
mid, maxSize float64
|
|
}
|
|
|
|
// DepthChart paints cumulative depth and supports keyboard level readouts.
|
|
func DepthChart(c *ui.Context, bids, asks []DepthLevel, opts DepthChartOptions) ChartResult {
|
|
dec := priceDecimals("DepthChart", opts.Decimals)
|
|
ch := newChart(c, opts.Frame)
|
|
k := Tokens(c)
|
|
d := chartMemo(ch, "depth", [2]string{chartID(bids), chartID(asks)}, func() depthData { return depthBuild(bids, asks) })
|
|
bidCol, askCol := trendColor(c, 1), trendColor(c, -1)
|
|
keys := []chartKey{{Name: msg(c, "depth_chart.bids"), Color: bidCol, Shape: 1}, {Name: msg(c, "depth_chart.asks"), Color: askCol, Shape: 3}}
|
|
drawn := ui.Local(ch.res.Element, "mujica.depth.drawn", func() int { return 0 })
|
|
size := FontSize(c, theme.CaptionSize)
|
|
geom := func(r ui.Rect) (ui.Rect, internal.Linear, internal.Linear) {
|
|
plot := chartInset(r, 56, 26)
|
|
lo, hi := d.price[d.nBids-1], d.price[len(d.price)-1]
|
|
return plot, internal.Linear{D0: lo, D1: hi, R0: float64(plot.X), R1: float64(plot.X + plot.W)},
|
|
internal.Linear{D0: 0, D1: d.maxSize * 1.05, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)}
|
|
}
|
|
spec := chartSpec{n: len(d.price), keys: keys}
|
|
if len(bids) == 0 || len(asks) == 0 {
|
|
spec.n = 0
|
|
}
|
|
// Arrows move by ascending price.
|
|
order := func(i int) int {
|
|
if i < d.nBids {
|
|
return d.nBids - 1 - i
|
|
}
|
|
return i
|
|
}
|
|
spec.move = func(i int, key ui.Key) int {
|
|
pos := order(i)
|
|
if key == ui.KeyLeft || key == ui.KeyUp {
|
|
pos = max(pos-1, 0)
|
|
} else {
|
|
pos = min(pos+1, len(d.price)-1)
|
|
}
|
|
return order(pos)
|
|
}
|
|
spec.pick = func(r ui.Rect, x, y float32) int {
|
|
plot, xs, _ := geom(r)
|
|
if !plot.Contains(x, y) {
|
|
return -1
|
|
}
|
|
v := xs.Invert(x)
|
|
pos := sort.Search(len(d.price), func(i int) bool { return d.price[order(i)] >= v })
|
|
if pos == len(d.price) || pos > 0 && v-d.price[order(pos-1)] < d.price[order(pos)]-v {
|
|
pos--
|
|
}
|
|
return order(pos)
|
|
}
|
|
spec.readout = func(i int) string {
|
|
side := msg(c, "depth_chart.bid")
|
|
if i >= d.nBids {
|
|
side = msg(c, "depth_chart.ask")
|
|
}
|
|
return strings.Join([]string{side + " " + priceUngrouped(d.price[i], dec), fmt.Sprintf(msg(c, "depth_chart.total"), statisticFormat(d.total[i], 0)),
|
|
msg(c, "depth_chart.mid") + " " + priceUngrouped(d.mid, dec)}, " · ")
|
|
}
|
|
spec.draw = func(p *ui.Painter, r ui.Rect, active int) {
|
|
plot, xs, ys := geom(r)
|
|
_, _, ystep := internal.Nice(0, d.maxSize*1.05, 4)
|
|
internal.AxisY(p, plot, ys, internal.Ticks(0, d.maxSize*1.05, ystep), func(v float64) string { return volumeShort(v, ystep) }, size, k)
|
|
_, _, xstep := internal.Nice(xs.D0, xs.D1, max(2, int(plot.W/90)))
|
|
var tx []float32
|
|
var tl []string
|
|
for _, t := range internal.Ticks(xs.D0, xs.D1, xstep) {
|
|
tx, tl = append(tx, xs.Map(t)), append(tl, internal.FormatTick(t, xstep))
|
|
}
|
|
internal.AxisX(p, plot, tx, tl, size, k)
|
|
base := plot.Y + plot.H
|
|
*drawn = 0
|
|
for side, idx := range [][2]int{{0, d.nBids}, {d.nBids, len(d.price)}} {
|
|
col := []ui.Color{bidCol, askCol}[side]
|
|
var area, edge ui.Path
|
|
mx := internal.Snap(p, xs.Map(d.mid))
|
|
area.MoveTo(mx, base)
|
|
edge.MoveTo(mx, base)
|
|
prevY := base
|
|
indices := chartMemo(ch, fmt.Sprint("depth.indices.", side), [3]any{chartID(d.price), plot.X, plot.W}, func() []int {
|
|
return internal.Columns(idx[1]-idx[0], func(i int) float32 { return xs.Map(d.price[idx[0]+i]) }, func(i int) float64 { return d.total[idx[0]+i] })
|
|
})
|
|
*drawn += len(indices)
|
|
for _, at := range indices {
|
|
i := idx[0] + at
|
|
x, y := internal.Snap(p, xs.Map(d.price[i])), internal.Snap(p, ys.Map(d.total[i]))
|
|
area.LineTo(x, prevY).LineTo(x, y)
|
|
edge.LineTo(x, prevY).LineTo(x, y)
|
|
prevY = y
|
|
}
|
|
end := plot.X
|
|
if side == 1 {
|
|
end = plot.X + plot.W
|
|
}
|
|
area.LineTo(end, prevY).LineTo(end, base).Close()
|
|
edge.LineTo(end, prevY)
|
|
p.FillPath(&area, col.Alpha(0.2))
|
|
p.StrokePath(&edge, 2, col)
|
|
label := []string{msg(c, "depth_chart.bids"), msg(c, "depth_chart.asks")}[side]
|
|
chartText(p, label, []float32{plot.X + 6, plot.X + plot.W - 6}[side], plot.Y+4, size, col, []float32{0, 1}[side], 0)
|
|
}
|
|
mx := internal.Snap(p, xs.Map(d.mid))
|
|
for y := plot.Y; y < base; y += 6 {
|
|
p.Line(mx, y, mx, min(y+3, base), 1, k.TextMuted)
|
|
}
|
|
chartText(p, priceUngrouped(d.mid, dec), mx, plot.Y+4, size, k.Text, 0.5, 0)
|
|
if active >= 0 {
|
|
x, y := xs.Map(d.price[active]), ys.Map(d.total[active])
|
|
p.Line(internal.Snap(p, x), plot.Y, internal.Snap(p, x), base, 1, k.TextMuted)
|
|
shape := 1
|
|
if active >= d.nBids {
|
|
shape = 3
|
|
}
|
|
internal.Marker(p, shape, x, y, 10, keys[shape/3].Color)
|
|
}
|
|
}
|
|
res, _ := ch.build(spec)
|
|
return res
|
|
}
|
|
|
|
// depthBuild validates price order and computes cumulative volume.
|
|
func depthBuild(bids, asks []DepthLevel) depthData {
|
|
for i, l := range bids {
|
|
if math.IsNaN(l.Price) || math.IsInf(l.Price, 0) || math.IsInf(l.Size, 0) || !(l.Size > 0) || i > 0 && !(l.Price < bids[i-1].Price) {
|
|
panic(fmt.Sprintf("mujica: DepthChart bid %d needs a positive size and a price below the one before", i))
|
|
}
|
|
}
|
|
for i, l := range asks {
|
|
if math.IsNaN(l.Price) || math.IsInf(l.Price, 0) || math.IsInf(l.Size, 0) || !(l.Size > 0) || i > 0 && !(l.Price > asks[i-1].Price) {
|
|
panic(fmt.Sprintf("mujica: DepthChart ask %d needs a positive size and a price above the one before", i))
|
|
}
|
|
}
|
|
if len(bids) > 0 && len(asks) > 0 && !(bids[0].Price < asks[0].Price) {
|
|
panic("mujica: DepthChart best bid must be below best ask")
|
|
}
|
|
var d depthData
|
|
d.nBids = len(bids)
|
|
var sum float64
|
|
for _, l := range bids {
|
|
sum += l.Size
|
|
d.price, d.total = append(d.price, l.Price), append(d.total, sum)
|
|
}
|
|
d.maxSize = sum
|
|
sum = 0
|
|
for _, l := range asks {
|
|
sum += l.Size
|
|
d.price, d.total = append(d.price, l.Price), append(d.total, sum)
|
|
}
|
|
d.maxSize = max(d.maxSize, sum)
|
|
if len(bids) > 0 && len(asks) > 0 {
|
|
d.mid = (bids[0].Price + asks[0].Price) / 2
|
|
}
|
|
return d
|
|
}
|