package mujica import ( "cmp" "fmt" "log/slog" "github.com/egoist/mygo/ui" "mujicaui/theme" ) // OrderStatus is where an order stands. type OrderStatus int const ( OrderOpen OrderStatus = iota OrderPartial OrderFilled OrderCanceled OrderRejected ) // working reports an order that can still fill. func (s OrderStatus) working() bool { return s == OrderOpen || s == OrderPartial } // OrderRow is one order; ID must be stable across updates. type OrderRow struct { ID, Symbol string Side OrderSide Type OrderType Price float64 Quantity float64 Filled float64 Status OrderStatus } // OrderFilter picks which orders an OrderTable shows. type OrderFilter int const ( OrdersAll OrderFilter = iota OrdersWorking OrdersFilled OrdersClosed ) func (f OrderFilter) keeps(s OrderStatus) bool { switch f { case OrdersWorking: return s.working() case OrdersFilled: return s == OrderFilled case OrdersClosed: return s == OrderCanceled || s == OrderRejected } return true } // OrderTableOptions configure an OrderTable. type OrderTableOptions struct { // Decimals of prices and quantities; zero means 2 and 0. Decimals, QuantityDecimals int // Version must change when rows change in place. Version int Status DataStatus Error string Disabled bool } // OrderTableState is the status filter and table state. type OrderTableState struct { Filter OrderFilter Table DataTableState[string] cache tradingRows[OrderRow] } // OrderTable lists orders with filter, progress and Cancel. func OrderTable(c *ui.Context, s *OrderTableState, rows []OrderRow, opts OrderTableOptions) TableActionResult { dec := priceDecimals("OrderTable", opts.Decimals) if opts.QuantityDecimals < 0 { panic("mujica: OrderTable with negative QuantityDecimals") } qd := opts.QuantityDecimals shown := s.cache.view(rows, opts.Version, s.Filter, func(o OrderRow) bool { if o.Quantity <= 0 || o.Filled < 0 || o.Filled > o.Quantity { panic(fmt.Sprintf("mujica: OrderTable order %s fills %v of %v", o.ID, o.Filled, o.Quantity)) } return s.Filter.keeps(o.Status) }) var r TableActionResult text := func(id, key string, t func(OrderRow) string) DataColumn[OrderRow] { return DataColumn[OrderRow]{ID: id, Title: msg(c, key), Sortable: true, Text: t, Compare: func(a, b OrderRow) int { return cmp.Compare(t(a), t(b)) }} } cols := []DataColumn[OrderRow]{ text("symbol", "position_table.symbol", func(o OrderRow) string { return o.Symbol }), {ID: "side", Title: msg(c, "order_table.side"), Text: func(o OrderRow) string { return msg(c, fmt.Sprintf("order_entry.side%d", o.Side)) }, Cell: func(o OrderRow) { tradingSide(c, o.Side) }}, text("type", "order_entry.type", func(o OrderRow) string { return msg(c, fmt.Sprintf("order_entry.type%d", o.Type)) }), {ID: "price", Title: msg(c, "order_book.price"), Align: ui.End, Sortable: true, Compare: func(a, b OrderRow) int { return cmp.Compare(a.Price, b.Price) }, Text: func(o OrderRow) string { if o.Type == OrderMarket { return msg(c, "order_table.market") } return statisticFormat(o.Price, dec) }}, {ID: "filled", Title: msg(c, "order_table.filled"), Width: 140, Text: func(o OrderRow) string { return statisticFormat(o.Filled, qd) + " / " + statisticFormat(o.Quantity, qd) }, Cell: func(o OrderRow) { orderTableProgress(c, o, qd) }}, text("status", "order_table.status", func(o OrderRow) string { return msg(c, fmt.Sprintf("order_table.status%d", o.Status)) }), {ID: "action", Title: msg(c, "position_table.action"), Width: 96, Text: func(OrderRow) string { return "" }, Cell: func(o OrderRow) { if o.Status.working() && Button(c, msg(c, "order_table.cancel"), ButtonOptions{Variant: Secondary}). Label(msg(c, "order_table.cancel")+" "+o.Symbol).Clicked() { r.id = o.ID } }}, } d := Density(c) r.Element = ui.Column(c).Gap(d.Unit() * 2).Disabled(opts.Disabled).Grow(1).FillWidth().MinHeight(0) r.Element.Children(func() { var segs []Segment for f := OrdersAll; f <= OrdersClosed; f++ { segs = append(segs, Segment{Label: msg(c, fmt.Sprintf("order_table.filter%d", f))}) } at := int(s.Filter) if SegmentedControl(c, &at, segs, SegmentedControlOptions{Label: msg(c, "order_table.filter"), Disabled: opts.Disabled}).Changed() { s.Filter = OrderFilter(at) slog.Debug("mujica: order_table filter", "filter", s.Filter) } r.Table = DataTable(c, &s.Table, shown, DataTableOptions[OrderRow, string]{Columns: cols, Key: func(o OrderRow) string { return o.ID }, Status: opts.Status, Error: opts.Error, Empty: msg(c, "order_table.empty")}) r.Table.Element.Grow(1) }) if r.Element.IsDisabled() { r.id = "" } if r.id != "" { slog.Debug("mujica: order_table cancel") } return r } // orderTableProgress shows the filled share as bar and count. func orderTableProgress(c *ui.Context, o OrderRow, qd int) { k := Tokens(c) share := float32(o.Filled / o.Quantity) ui.Row(c).Gap(Density(c).Unit() * 2).AlignItems(ui.Center).FillWidth().Children(func() { ui.Box(c).Size(48, 6).Shrink(0).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(r, k.SurfaceHover, theme.ControlRadius) p.Fill(ui.Rect{X: r.X, Y: r.Y, W: r.W * share, H: r.H}, k.AccentText, theme.ControlRadius) }) ui.Text(c, statisticFormat(o.Filled, qd)+" / "+statisticFormat(o.Quantity, qd)).FontFeatures("tnum").FontSize(FontSize(c, theme.CaptionSize)).SingleLine() }) } // tradingSide names a side with its arrow and trend color. func tradingSide(c *ui.Context, side OrderSide) { ui.Row(c).Gap(2).AlignItems(ui.Center).TextColor(trendColor(c, side.sign())).Children(func() { ui.Icon(c, trendIcon(side.sign())).Size(12, 12) ui.Text(c, msg(c, fmt.Sprintf("order_entry.side%d", side))).FontWeight(600).SingleLine() }) } // tradingRows is a filtered view, rebuilt only on change. type tradingRows[R any] struct { first *R n, ver int filter any valid bool visible []R } func (t *tradingRows[R]) view(rows []R, version int, filter any, keep func(R) bool) []R { var first *R if len(rows) > 0 { first = &rows[0] } if t.valid && t.first == first && t.n == len(rows) && t.ver == version && t.filter == filter { return t.visible } t.first, t.n, t.ver, t.filter, t.valid = first, len(rows), version, filter, true t.visible = make([]R, 0, len(rows)) for _, r := range rows { if keep(r) { t.visible = append(t.visible, r) } } return t.visible }