wip markets
This commit is contained in:
21 files changed
+2836
No files matched your search
@@ -0,0 +1,177 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/internal"
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// PerformanceChartOptions configure a PerformanceChart.
|
||||
type PerformanceChartOptions struct {
|
||||
Frame ChartFrame
|
||||
// Times dates each value; required, ascending.
|
||||
Times []time.Time
|
||||
// Benchmark is compared against the portfolio; nil shows none.
|
||||
Benchmark []float64
|
||||
Name, BenchmarkName string
|
||||
}
|
||||
|
||||
// Drawdown is the deepest fall from a peak: indices and the fall in percent.
|
||||
type Drawdown struct {
|
||||
Peak, Trough int
|
||||
Percent float64
|
||||
}
|
||||
|
||||
// MaxDrawdown finds the largest peak-to-trough fall of positive values; Percent ≤ 0.
|
||||
func MaxDrawdown(values []float64) Drawdown {
|
||||
var dd Drawdown
|
||||
peak := 0
|
||||
for i, v := range values {
|
||||
if v > values[peak] {
|
||||
peak = i
|
||||
}
|
||||
if f := (v/values[peak] - 1) * 100; f < dd.Percent {
|
||||
dd = Drawdown{Peak: peak, Trough: i, Percent: f}
|
||||
}
|
||||
}
|
||||
return dd
|
||||
}
|
||||
|
||||
// PerformanceChart plots cumulative return against a benchmark and shades the maximum drawdown.
|
||||
func PerformanceChart(c *ui.Context, values []float64, opts PerformanceChartOptions) ChartResult {
|
||||
for _, vs := range [][]float64{values, opts.Benchmark} {
|
||||
for _, v := range vs {
|
||||
if !(v > 0) || math.IsInf(v, 0) {
|
||||
panic("mujica: PerformanceChart values must be positive")
|
||||
}
|
||||
}
|
||||
}
|
||||
if opts.Benchmark != nil && len(opts.Benchmark) != len(values) {
|
||||
panic("mujica: PerformanceChart Benchmark length differs")
|
||||
}
|
||||
ch := newChart(c, opts.Frame)
|
||||
k := Tokens(c)
|
||||
pal := internal.Palette(k)
|
||||
size := FontSize(c, theme.CaptionSize)
|
||||
name := opts.Name
|
||||
if name == "" {
|
||||
name = msg(c, "performance_chart.portfolio")
|
||||
}
|
||||
bname := opts.BenchmarkName
|
||||
if bname == "" {
|
||||
bname = msg(c, "performance_chart.benchmark")
|
||||
}
|
||||
series := chartMemo(ch, "returns", [4]string{chartID(values), chartID(opts.Benchmark), name, bname}, func() []ChartSeries {
|
||||
out := []ChartSeries{{Name: name, Values: performanceReturns(values)}}
|
||||
if opts.Benchmark != nil {
|
||||
out = append(out, ChartSeries{Name: bname, Values: performanceReturns(opts.Benchmark)})
|
||||
}
|
||||
return out
|
||||
})
|
||||
n := len(values)
|
||||
ax := chartMemo(ch, "x", chartID(opts.Times), func() chartX { return newChartX(n, nil, opts.Times) })
|
||||
dd := chartMemo(ch, "dd", chartID(values), func() Drawdown { return MaxDrawdown(values) })
|
||||
lo, hi, ok := chartYRange(ch, series, true, nil)
|
||||
if !ok {
|
||||
n = 0
|
||||
}
|
||||
keys := make([]chartKey, len(series))
|
||||
for i, s := range series {
|
||||
keys[i] = chartKey{Name: s.Name, Color: pal[i], Shape: i}
|
||||
}
|
||||
var ticks []float64
|
||||
var step float64
|
||||
if n > 0 {
|
||||
lo, hi, step = internal.Nice(lo, hi, 4)
|
||||
ticks = internal.Ticks(lo, hi, step)
|
||||
}
|
||||
pct := func(v float64) string { return statisticSigned(v, 2) + "%" }
|
||||
geom := func(r ui.Rect) (ui.Rect, internal.Linear, internal.Linear) {
|
||||
plot := chartInset(r, 56, 26)
|
||||
return plot, ax.scale(plot), internal.Linear{D0: lo, D1: hi, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)}
|
||||
}
|
||||
spec := chartSpec{n: n, keys: keys, toggle: true}
|
||||
spec.pick = func(r ui.Rect, x, y float32) int {
|
||||
plot, xs, _ := geom(r)
|
||||
if !plot.Contains(x, y) {
|
||||
return -1
|
||||
}
|
||||
return ax.nearest(xs.Invert(x))
|
||||
}
|
||||
spec.readout = func(i int) string {
|
||||
parts := []string{opts.Times[i].Format("2006-01-02")}
|
||||
for _, s := range series {
|
||||
if !ch.hidden(s.Name) {
|
||||
parts = append(parts, s.Name+" "+pct(s.Values[i]))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, " · ")
|
||||
}
|
||||
spec.draw = func(p *ui.Painter, r ui.Rect, active int) {
|
||||
plot, xs, ys := geom(r)
|
||||
x := func(i int) float32 { return internal.Snap(p, xs.Map(ax.vals[i])) }
|
||||
if dd.Percent < 0 && !ch.hidden(name) {
|
||||
band := ui.Rect{X: x(dd.Peak), Y: plot.Y, W: x(dd.Trough) - x(dd.Peak), H: plot.H}
|
||||
p.Fill(band, trendColor(c, -1).Alpha(0.1), 0)
|
||||
p.Line(band.X, plot.Y, band.X, plot.Y+plot.H, 1, trendColor(c, -1).Alpha(0.5))
|
||||
p.Line(band.X+band.W, plot.Y, band.X+band.W, plot.Y+plot.H, 1, trendColor(c, -1).Alpha(0.5))
|
||||
chartText(p, fmt.Sprintf(msg(c, "performance_chart.drawdown"), pct(dd.Percent)), band.X+4, plot.Y+2, size, chartInk(k, k.Surface), 0, 0)
|
||||
}
|
||||
internal.AxisY(p, plot, ys, ticks, func(v float64) string { return internal.FormatTick(v, step) + "%" }, size, k)
|
||||
tx, tl := ax.ticks(plot)
|
||||
internal.AxisX(p, plot, tx, tl, size, k)
|
||||
zero := internal.Snap(p, ys.Map(0))
|
||||
p.Line(plot.X, zero, plot.X+plot.W, zero, 1, k.ControlBorder)
|
||||
for si, s := range series {
|
||||
if ch.hidden(s.Name) {
|
||||
continue
|
||||
}
|
||||
var line ui.Path
|
||||
idx := internal.Columns(n, func(i int) float32 { return xs.Map(ax.vals[i]) }, func(i int) float64 { return s.Values[i] })
|
||||
for j, i := range idx {
|
||||
if j == 0 {
|
||||
line.MoveTo(x(i), internal.Snap(p, ys.Map(s.Values[i])))
|
||||
} else {
|
||||
line.LineTo(x(i), internal.Snap(p, ys.Map(s.Values[i])))
|
||||
}
|
||||
}
|
||||
p.StrokePath(&line, 2, keys[si].Color)
|
||||
every := max(1, int(float32(n)/max(plot.W/48, 1)))
|
||||
for i := 0; i < n; i += every {
|
||||
internal.Marker(p, si, x(i), ys.Map(s.Values[i]), 7, keys[si].Color)
|
||||
}
|
||||
}
|
||||
if active >= 0 {
|
||||
p.Line(x(active), plot.Y, x(active), plot.Y+plot.H, 1, k.TextMuted)
|
||||
for si, s := range series {
|
||||
if !ch.hidden(s.Name) {
|
||||
internal.Marker(p, si, x(active), ys.Map(s.Values[active]), 13, k.Surface)
|
||||
internal.Marker(p, si, x(active), ys.Map(s.Values[active]), 9, keys[si].Color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res, _ := ch.build(spec)
|
||||
if dd.Percent < 0 && n > 0 {
|
||||
res.Element.Children(func() {
|
||||
ui.Text(c, fmt.Sprintf(msg(c, "performance_chart.drawdown_range"), pct(dd.Percent),
|
||||
opts.Times[dd.Peak].Format("2006-01-02"), opts.Times[dd.Trough].Format("2006-01-02"))).FontSize(size).TextColor(k.TextMuted)
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// performanceReturns turns values into cumulative percent return from the first.
|
||||
func performanceReturns(values []float64) []float64 {
|
||||
out := make([]float64, len(values))
|
||||
for i, v := range values {
|
||||
out[i] = (v/values[0] - 1) * 100
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user