Files
MujicaUI/finance/markets_regression_test.go
T

224 lines
7.9 KiB
Go

package finance
import (
"fmt"
"math"
"strings"
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/chart"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/data"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/internal"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
)
func TestMarketsAncestorDisabled(t *testing.T) {
s := NewsFeedState{}
disabled := false
var last NewsFeedView
items := []NewsItem{{ID: "a", Title: "Alpha", Source: "Wire", Time: mktNow}, {ID: "b", Title: "Beta", Source: "Wire", Time: mktNow}}
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
ui.Column(c).Height(360).Disabled(disabled).Children(func() { last = NewsFeed(c, &s, items, NewsFeedOptions{Today: core.DateOf(mktNow)}) })
}, 640, 400)
tt.Frame()
_ = tt.Click("Alpha")
disabled = true
tt.Key(0, ui.KeyDown)
tt.Key(0, ui.KeyEnter)
if s.Read["b"] || last.Opened != "" {
t.Fatal("ancestor-disabled feed changed reads")
}
}
func TestCurrencyConverterSwapInvalidatesRate(t *testing.T) {
s := CurrencyConverterState{Amount: 100, From: "USD", To: "JPY"}
var last CurrencyConverterView
tt := chatTester(760, 300, func(c *ui.Context) {
last = CurrencyConverter(c, &s, []Currency{{"USD", "Dollar", 2}, {"JPY", "Yen", 0}}, CurrencyConverterOptions{Rate: 150})
})
_ = tt.Click("Swap currencies")
if s.From != "JPY" || s.To != "USD" || !math.IsNaN(last.Result) {
t.Fatalf("swapped pair %s/%s result %v", s.From, s.To, last.Result)
}
}
func TestGreeksTableLarge(t *testing.T) {
rows := make([]GreeksRow, 100000)
for i := range rows {
rows[i] = GreeksRow{Label: "Contract", Greeks: OptionGreeks{Delta: 0.5}}
}
tt := chatTester(640, 400, func(c *ui.Context) { GreeksTable(c, rows, GreeksTableOptions{}) })
if n := len(tt.Texts()); n > 200 {
t.Fatalf("built %d texts", n)
}
}
func TestSymbolSearchLarge(t *testing.T) {
rows := make([]MarketSymbol, 100000)
for i := range rows {
rows[i] = MarketSymbol{Code: data.StatisticFormat(float64(i), 0), Name: "Company"}
}
s := SymbolSearchState{Query: "company"}
tt := chatTester(640, 400, func(c *ui.Context) { SymbolSearch(c, &s, rows, SymbolSearchOptions{}) })
if n := len(tt.Texts()); n > 200 {
t.Fatalf("built %d texts", n)
}
}
func TestMarketsLargeViews(t *testing.T) {
const n = 100000
news := make([]NewsItem, n)
econ := make([]EconomicEvent, n)
earn := make([]EarningsEvent, n)
rows := make([]ScreenerRow, n)
strikes := make([]OptionStrike, n)
for i := range n {
id := fmt.Sprint(i)
news[i] = NewsItem{ID: id, Title: "Headline " + id, Time: mktNow}
econ[i] = EconomicEvent{ID: id, Country: "US", Title: "Event " + id, Importance: 3}
earn[i] = EarningsEvent{ID: id, Symbol: "S" + id}
rows[i] = ScreenerRow{Symbol: "S" + id, Values: map[string]float64{"pe": 10}}
strikes[i] = OptionStrike{Strike: float64(i)}
}
for _, tc := range []struct {
name string
view func(*ui.Context)
}{
{"news", func(c *ui.Context) {
s := ui.Local(c.Root(), "large.news", func() NewsFeedState { return NewsFeedState{} })
NewsFeed(c, s, news, NewsFeedOptions{}).Element.Height(350)
}},
{"economic", func(c *ui.Context) {
s := ui.Local(c.Root(), "large.econ", func() EconomicCalendarState { return EconomicCalendarState{} })
EconomicCalendar(c, s, econ, EconomicCalendarOptions{}).Element.Height(350)
}},
{"earnings", func(c *ui.Context) {
s := ui.Local(c.Root(), "large.earn", func() data.DataTableState[string] { return data.DataTableState[string]{} })
EarningsCalendar(c, s, earn, EarningsCalendarOptions{}).Element.Height(350)
}},
{"screener", func(c *ui.Context) {
s := ui.Local(c.Root(), "large.screen", func() ScreenerState { return ScreenerState{} })
Screener(c, s, []ScreenerField{{ID: "pe", Label: "P/E"}}, rows, ScreenerOptions{}).Element.Height(350)
}},
{"chain", func(c *ui.Context) {
s := ui.Local(c.Root(), "large.chain", func() OptionChainState { return OptionChainState{} })
OptionChain(c, s, []string{"Oct"}, strikes, OptionChainOptions{Spot: 50000}).Element.Height(350)
}},
} {
t.Run(tc.name, func(t *testing.T) {
tt := chatTester(960, 400, tc.view)
tt.Frame()
if count := len(tt.Texts()); count > 300 {
t.Fatalf("built %d texts", count)
}
})
}
}
func TestMarketHeatmapLargeGeometry(t *testing.T) {
m := chart.MarketHeatMemo{}
for i := range 100000 {
m.Tiles = append(m.Tiles, chart.MarketHeatTile{Name: fmt.Sprint(i), Cap: 1, Change: 1})
m.Caps = append(m.Caps, 1)
}
m.Layout(m.Caps, ui.Rect{W: 640, H: 240})
if count := len(m.Visible) + len(m.Small); count > 640*240/16+640/4+240/4 {
t.Fatalf("built %d marks", count)
}
if len(m.Rects) != 100000 {
t.Fatalf("keyboard points %d", len(m.Rects))
}
first := &m.Rects[0]
m.Layout(m.Caps, ui.Rect{W: 640, H: 240})
if first != &m.Rects[0] {
t.Fatal("idle layout recomputed")
}
}
func TestMarketsPaletteContrast(t *testing.T) {
for _, k := range []theme.Tokens{theme.Light(), theme.Dark()} {
for i, col := range internal.Palette(k) {
if ratio := theme.Contrast(col, k.Surface); ratio < 3 {
t.Fatalf("color %d ratio %v", i, ratio)
}
}
}
}
func TestMarketsChartStatesAndDisabled(t *testing.T) {
for _, name := range []string{"payoff", "heatmap", "allocation", "performance"} {
t.Run(name, func(t *testing.T) {
for _, status := range []data.DataStatus{data.DataLoading, data.DataFailed} {
frame := chart.ChartFrame{Label: "Chart", Height: 180, Status: status, Error: "Caller failed"}
retry := false
tt := chatTester(960, 420, func(c *ui.Context) {
var r chart.ChartResult
switch name {
case "payoff":
r = PayoffDiagram(c, bullSpread(), PayoffDiagramOptions{Frame: frame})
case "heatmap":
path := ui.Local(c.Root(), "path", func() []string { return nil })
r = chart.MarketHeatmap(c, path, mktHeat(), chart.MarketHeatmapOptions{Frame: frame})
case "allocation":
r = AssetAllocationChart(c, []AllocationItem{{"Stocks", 1, 100}}, AssetAllocationChartOptions{Frame: frame})
case "performance":
r = PerformanceChart(c, []float64{100, 110}, PerformanceChartOptions{Frame: frame, Times: mktTimes(2)})
}
retry = retry || r.Retried()
})
if status == data.DataFailed {
_ = tt.Click("Retry")
if !retry {
t.Fatal("retry missing")
}
} else if !strings.Contains(strings.Join(tt.Texts(), "|"), "Loading") {
t.Fatalf("loading %q", tt.Texts())
}
}
})
}
}
func TestMarketsLargeChartGeometry(t *testing.T) {
values := mktSeries(100000, 0)
times := make([]time.Time, 100000)
items := make([]AllocationItem, 100000)
for i := range times {
times[i] = mktNow.Add(time.Duration(i) * time.Minute)
items[i] = AllocationItem{Name: fmt.Sprint("Asset ", i), Value: 1}
}
for _, name := range []string{"payoff", "performance", "allocation"} {
t.Run(name, func(t *testing.T) {
var geometry *chart.ChartMemoEntry
tt := chatTester(960, 600, func(c *ui.Context) {
var r chart.ChartResult
frame := chart.ChartFrame{Label: "Large"}
switch name {
case "payoff":
r = PayoffDiagram(c, bullSpread(), PayoffDiagramOptions{Frame: frame, Points: 100000})
geometry = ui.Local(r.Element, "mujica.memo.payoff.geometry", func() chart.ChartMemoEntry { return chart.ChartMemoEntry{} })
case "performance":
r = PerformanceChart(c, values, PerformanceChartOptions{Frame: frame, Times: times})
geometry = ui.Local(r.Element, "mujica.memo.performance.geometry.Portfolio", func() chart.ChartMemoEntry { return chart.ChartMemoEntry{} })
case "allocation":
r = AssetAllocationChart(c, items, AssetAllocationChartOptions{Frame: frame})
}
})
tt.Frame()
if len(tt.Texts()) > 300 {
t.Fatalf("built %d texts", len(tt.Texts()))
}
if geometry != nil {
points := geometry.Val.([]int)
if len(points) > 4*960 {
t.Fatalf("built %d points", len(points))
}
first := &points[0]
tt.Frame()
if first != &geometry.Val.([]int)[0] {
t.Fatal("idle geometry recomputed")
}
}
})
}
}