Files
MujicaUI/finance/portfolio_summary.go

126 lines
4.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package finance
import (
"cmp"
"fmt"
"log/slog"
"math"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/data"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/input"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/internal"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
)
// PortfolioAccount is one account's value and profit.
type PortfolioAccount struct {
ID, Name string
Total float64
// DayPnL is today's profit; TotalReturn the profit since inception.
DayPnL, TotalReturn float64
Cost float64
}
// PortfolioSummaryOptions configure a PortfolioSummary.
type PortfolioSummaryOptions struct {
// Currency prefixes amounts; empty is "$".
Currency string
// Decimals of amounts; zero means 2.
Decimals int
Disabled bool
}
// PortfolioSummaryView is a built summary; Changed reports another account chosen.
func PortfolioSummary(c *ui.Context, account *string, accounts []PortfolioAccount, opts PortfolioSummaryOptions) core.ChoiceResult {
if account == nil {
panic("mujica: PortfolioSummary needs an account")
}
dec := core.PriceDecimals("PortfolioSummary", opts.Decimals)
cur := cmp.Or(opts.Currency, "$")
k := core.Tokens(c)
u := core.Density(c).Unit()
var sum PortfolioAccount
found := *account == ""
for _, a := range accounts {
if a.ID == "" {
panic("mujica: PortfolioAccount needs an ID")
}
if *account == "" || a.ID == *account {
found = found || a.ID == *account
sum.Total += a.Total
sum.DayPnL += a.DayPnL
sum.TotalReturn += a.TotalReturn
sum.Cost += a.Cost
}
}
if !found {
panic("mujica: PortfolioSummary account " + *account + " is not listed")
}
before := *account
v := core.ChoiceResult{}
v.Element = ui.Column(c).Gap(u*3).Padding(u*4).Background(k.Surface).Border(theme.BorderWidth, k.Border).
Radius(theme.CardRadius).Disabled(opts.Disabled)
v.Element.Children(func() {
if len(accounts) > 1 {
items := []input.RadioItem[string]{{Value: "", Label: core.Msg(c, "portfolio_summary.all")}}
for _, a := range accounts {
items = append(items, input.RadioItem[string]{Value: a.ID, Label: a.Name})
}
input.RadioGroup(c, account, items, input.RadioGroupOptions{Horizontal: true, Label: core.Msg(c, "portfolio_summary.accounts"), Disabled: opts.Disabled})
}
ui.Box(c).FillWidth().Height(9).Draw(func(p *ui.Painter, r ui.Rect) { internal.RuleDiamond(p, r, 6, 5, k.Ornament) })
ui.Row(c).Wrap().Gap(u * 8).Children(func() {
portfolioFigure(c, core.Msg(c, "portfolio_summary.total"), func() {
ui.Text(c, cur+data.StatisticFormat(sum.Total, dec)).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H1Size)).FontFeatures("tnum")
})
prev := sum.Total - sum.DayPnL
portfolioFigure(c, core.Msg(c, "portfolio_summary.day"), func() {
portfolioSigned(c, sum.DayPnL, cur, dec, portfolioPercent(sum.DayPnL, prev))
})
portfolioFigure(c, core.Msg(c, "portfolio_summary.return"), func() {
portfolioSigned(c, sum.TotalReturn, cur, dec, portfolioPercent(sum.TotalReturn, sum.Cost))
})
})
})
v = core.NewChoiceResult(v.Element, *account != before && !v.Element.IsDisabled())
if v.Changed() {
slog.Debug("mujica: portfolio_summary account", "id", *account)
}
return v
}
func portfolioFigure(c *ui.Context, title string, value func()) {
ui.Column(c).Gap(2).Role(ui.RoleGroup).Label(title).Children(func() {
ui.Text(c, title).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(core.Tokens(c).TextMuted)
value()
})
}
func portfolioSigned(c *ui.Context, v float64, cur string, dec int, pct float64) {
sign := core.TrendSign(v)
text := cur + data.StatisticFormat(math.Abs(v), dec)
if sign > 0 {
text = "+" + text
} else if sign < 0 {
text = "−" + text
}
ui.Row(c).Gap(4).AlignItems(ui.Center).TextColor(core.TrendColor(c, sign)).Label(core.TrendWord(c, sign) + " " + text).Children(func() {
ui.Icon(c, core.TrendIcon(sign)).Size(16, 16)
ui.Text(c, text).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600).FontFeatures("tnum")
if !math.IsNaN(pct) {
ui.Text(c, fmt.Sprintf("(%s%%)", data.StatisticSigned(pct, 2))).FontFeatures("tnum")
}
})
}
func portfolioPercent(part, base float64) float64 {
if base == 0 {
return math.NaN()
}
return part / base * 100
}