wip markets

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 14:54:26 -04:00
1 parent fbd584b6aa
commit b2fafd99ce
21 files changed
+2836

No files matched your search

+130
View File
@@ -0,0 +1,130 @@
package mujica
import (
"cmp"
"fmt"
"log/slog"
"math"
"github.com/egoist/mygo/ui"
"mujicaui/internal"
"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 is what went in; it turns TotalReturn into a percent.
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.
type PortfolioSummaryView struct {
Element *ui.Element
Changed bool
}
// PortfolioSummary shows total assets, today's profit and total return, for all accounts or one.
func PortfolioSummary(c *ui.Context, account *string, accounts []PortfolioAccount, opts PortfolioSummaryOptions) PortfolioSummaryView {
if account == nil {
panic("mujica: PortfolioSummary needs an account")
}
dec := priceDecimals("PortfolioSummary", opts.Decimals)
cur := cmp.Or(opts.Currency, "$")
k := Tokens(c)
u := 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 := PortfolioSummaryView{}
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 := []RadioItem[string]{{Value: "", Label: msg(c, "portfolio_summary.all")}}
for _, a := range accounts {
items = append(items, RadioItem[string]{Value: a.ID, Label: a.Name})
}
RadioGroup(c, account, items, RadioGroupOptions{Horizontal: true, Label: 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, msg(c, "portfolio_summary.total"), func() {
ui.Text(c, cur+statisticFormat(sum.Total, dec)).Font(theme.SerifFont).FontSize(FontSize(c, theme.H1Size)).FontFeatures("tnum")
})
prev := sum.Total - sum.DayPnL
portfolioFigure(c, msg(c, "portfolio_summary.day"), func() {
portfolioSigned(c, sum.DayPnL, cur, dec, portfolioPercent(sum.DayPnL, prev))
})
portfolioFigure(c, msg(c, "portfolio_summary.return"), func() {
portfolioSigned(c, sum.TotalReturn, cur, dec, portfolioPercent(sum.TotalReturn, sum.Cost))
})
})
})
v.Changed = *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(FontSize(c, theme.CaptionSize)).TextColor(Tokens(c).TextMuted)
value()
})
}
// portfolioSigned shows a signed amount and percent with the trend arrow.
func portfolioSigned(c *ui.Context, v float64, cur string, dec int, pct float64) {
sign := trendSign(v)
text := cur + 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(trendColor(c, sign)).Label(trendWord(c, sign) + " " + text).Children(func() {
ui.Icon(c, trendIcon(sign)).Size(16, 16)
ui.Text(c, text).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).FontFeatures("tnum")
if !math.IsNaN(pct) {
ui.Text(c, fmt.Sprintf("(%s%%)", statisticSigned(pct, 2))).FontFeatures("tnum")
}
})
}
// portfolioPercent is part of base in percent, NaN when base is zero.
func portfolioPercent(part, base float64) float64 {
if base == 0 {
return math.NaN()
}
return part / base * 100
}