Merge Market Charts; port to public ChartSpec
This commit is contained in:
117 files changed
+7249
-2
No files matched your search
@@ -1,3 +1,28 @@
|
||||
package main
|
||||
|
||||
var mktchartPages = []page{}
|
||||
import "github.com/egoist/mygo/ui"
|
||||
|
||||
var mktchartPages = []page{
|
||||
{ID: "141", Name: "CandlestickChart", ZhName: "K 线", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "141") }, Code: mktCode141, Keys: mktKeys},
|
||||
{ID: "142", Name: "OHLCChart", ZhName: "美国线", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "142") }, Code: mktCode142, Keys: mktKeys},
|
||||
{ID: "143", Name: "HeikinAshiChart", ZhName: "平均 K 线", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "143") }, Code: mktCode143, Keys: mktKeys},
|
||||
{ID: "144", Name: "LineQuoteChart", ZhName: "分时图", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "144") }, Code: mktCode144, Keys: mktKeys},
|
||||
{ID: "145", Name: "VolumeChart", ZhName: "成交量", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "145") }, Code: mktCode145, Keys: mktKeys},
|
||||
{ID: "146", Name: "DepthChart", ZhName: "深度图", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "146") }, Code: mktCode146, Keys: mktKeys},
|
||||
{ID: "147", Name: "VolumeProfile", ZhName: "成交量分布", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "147") }, Code: mktCode147, Keys: mktKeys},
|
||||
{ID: "148", Name: "MultiChartLayout", ZhName: "多图布局", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "148") }, Code: mktCode148, Keys: mktKeys},
|
||||
{ID: "149", Name: "ChartSync", ZhName: "图表同步", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "149") }, Code: mktCode149, Keys: mktKeys},
|
||||
{ID: "150", Name: "TimeRangeSelector", ZhName: "时间范围", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "150") }, Code: mktCode150, Keys: mktKeys},
|
||||
{ID: "151", Name: "IntervalSelector", ZhName: "周期选择", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "151") }, Code: mktCode151, Keys: mktKeys},
|
||||
{ID: "152", Name: "ChartTypeSwitcher", ZhName: "图表类型", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "152") }, Code: mktCode152, Keys: mktKeys},
|
||||
{ID: "153", Name: "IndicatorOverlay", ZhName: "主图指标", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "153") }, Code: mktCode153, Keys: mktKeys},
|
||||
{ID: "154", Name: "IndicatorPane", ZhName: "指标副图", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "154") }, Code: mktCode154, Keys: mktKeys},
|
||||
{ID: "155", Name: "IndicatorSelector", ZhName: "指标选择", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "155") }, Code: mktCode155, Keys: mktKeys},
|
||||
{ID: "156", Name: "DrawingTools", ZhName: "画线", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "156") }, Code: mktCode156, Keys: mktKeys},
|
||||
{ID: "157", Name: "DrawingToolbar", ZhName: "画线工具栏", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "157") }, Code: mktCode157, Keys: mktKeys},
|
||||
{ID: "158", Name: "CompareSymbol", ZhName: "品种对比", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "158") }, Code: mktCode158, Keys: mktKeys},
|
||||
{ID: "159", Name: "PriceAlertLine", ZhName: "价格预警", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "159") }, Code: mktCode159, Keys: mktKeys},
|
||||
{ID: "160", Name: "CandleCountdown", ZhName: "K 线倒计时", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "160") }, Code: mktCode160, Keys: mktKeys},
|
||||
}
|
||||
|
||||
const mktKeys = "Tab focuses controls and charts; arrows move focus or chart readouts; Space selects; Home/End jump; +/- zoom; Shift+arrows pan; Alt+1–6 pick drawing tools; Delete removes; Ctrl/Command+Z undoes; Escape closes panels."
|
||||
@@ -0,0 +1,219 @@
|
||||
package main
|
||||
|
||||
const mktCode141 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
|
||||
candles := []mujica.Candle{
|
||||
{Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200},
|
||||
{Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600},
|
||||
{Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900},
|
||||
}
|
||||
var window mujica.MarketView
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode142 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
|
||||
candles := []mujica.Candle{
|
||||
{Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200},
|
||||
{Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600},
|
||||
{Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900},
|
||||
}
|
||||
var window mujica.MarketView
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.OHLCChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode143 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
|
||||
candles := []mujica.Candle{
|
||||
{Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200},
|
||||
{Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600},
|
||||
{Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900},
|
||||
}
|
||||
var window mujica.MarketView
|
||||
raw := false
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.HeikinAshiChart(c, candles, &window, &raw, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode144 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
|
||||
points := []mujica.QuotePoint{{Time: at, Price: 101, Volume: 100}, {Time: at.Add(time.Hour), Price: 103, Volume: 200}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.LineQuoteChart(c, points, mujica.LineQuoteChartOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, PrevClose: 100, RegularOpen: at.Add(30 * time.Minute), RegularClose: at.Add(45 * time.Minute)})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode145 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
|
||||
candles := []mujica.Candle{
|
||||
{Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200},
|
||||
{Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600},
|
||||
{Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900},
|
||||
}
|
||||
var window mujica.MarketView
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.VolumeChart(c, candles, &window, mujica.VolumeChartOptions{Frame: mujica.ChartFrame{Label: "Volume"}})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode146 = ` bids := []mujica.DepthLevel{{Price: 99, Size: 10}, {Price: 98, Size: 20}}
|
||||
asks := []mujica.DepthLevel{{Price: 101, Size: 15}, {Price: 102, Size: 25}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.DepthChart(c, bids, asks, mujica.DepthChartOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode147 = ` bins := []mujica.ProfileBin{{Low: 99, High: 100, Volume: 30}, {Low: 100, High: 101, Volume: 80}, {Low: 101, High: 102, Volume: 40}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.VolumeProfile(c, bins, mujica.VolumeProfileOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode148 = ` state := mujica.MultiChartState{Count: 2}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.MultiChartLayout(c, &state, mujica.MultiChartLayoutOptions{Height: 200}, func(i int, cell *mujica.ChartCell) {
|
||||
mujica.TextInput(c, &cell.Symbol, mujica.TextInputOptions{Label: "Symbol"})
|
||||
})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode149 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
|
||||
candles := []mujica.Candle{
|
||||
{Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200},
|
||||
{Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600},
|
||||
{Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900},
|
||||
}
|
||||
var window mujica.MarketView
|
||||
var sync mujica.ChartSync
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
sync.Link(c, "price")
|
||||
mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Sync: &sync, SyncID: "price"})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode150 = ` value := mujica.MarketRange{Preset: mujica.Range1M}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.TimeRangeSelector(c, &value, mujica.TimeRangeSelectorOptions{})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode151 = ` value := mujica.Interval("1h")
|
||||
favorites := []mujica.Interval{"1m", "1h", "1D"}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.IntervalSelector(c, &value, &favorites, mujica.IntervalSelectorOptions{})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode152 = ` value := mujica.ChartCandles
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.ChartTypeSwitcher(c, &value, mujica.ChartTypeSwitcherOptions{})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode153 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
|
||||
candles := []mujica.Candle{
|
||||
{Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200},
|
||||
{Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600},
|
||||
{Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900},
|
||||
}
|
||||
var window mujica.MarketView
|
||||
overlays := []mujica.IndicatorOverlay{{Kind: mujica.IndicatorMA, Params: []float64{2}}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Overlays: overlays})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode154 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
|
||||
candles := []mujica.Candle{
|
||||
{Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200},
|
||||
{Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600},
|
||||
{Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900},
|
||||
}
|
||||
var window mujica.MarketView
|
||||
panes := mujica.IndicatorPanes{Panes: []mujica.IndicatorPaneSpec{{Kind: mujica.IndicatorRSI, Params: []float64{2}}}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.IndicatorPane(c, candles, &window, &panes, mujica.IndicatorPaneOptions{Height: 200, Label: "BTC"})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode155 = ` var overlays []mujica.IndicatorOverlay
|
||||
var panes mujica.IndicatorPanes
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.IndicatorSelector(c, &overlays, &panes, mujica.IndicatorSelectorOptions{})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode156 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
|
||||
candles := []mujica.Candle{
|
||||
{Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200},
|
||||
{Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600},
|
||||
{Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900},
|
||||
}
|
||||
var window mujica.MarketView
|
||||
drawings := mujica.DrawingTools{Tool: mujica.ToolTrendLine}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Drawings: &drawings})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode157 = ` var drawings mujica.DrawingTools
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.DrawingToolbar(c, &drawings, mujica.DrawingToolbarOptions{Horizontal: true})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode158 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
|
||||
candles := []mujica.Candle{
|
||||
{Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200},
|
||||
{Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600},
|
||||
{Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900},
|
||||
}
|
||||
var window mujica.MarketView
|
||||
symbols := []mujica.CompareSymbol{{Name: "ETH", Closes: []float64{20, 21, 23}}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.CompareLegend(c, &symbols)
|
||||
mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Compare: &symbols})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode159 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
|
||||
candles := []mujica.Candle{
|
||||
{Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200},
|
||||
{Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600},
|
||||
{Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900},
|
||||
}
|
||||
var window mujica.MarketView
|
||||
alerts := []mujica.PriceAlertLine{{ID: "take-profit", Price: 105}}
|
||||
triggered := ""
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Alerts: &alerts, OnAlert: func(alert mujica.PriceAlertLine, last mujica.Candle) {
|
||||
triggered = alert.ID
|
||||
}})
|
||||
ui.Text(c, triggered)
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
|
||||
const mktCode160 = ` view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.CandleCountdown(c, "1h", mujica.CandleCountdownOptions{Location: time.UTC})
|
||||
}
|
||||
ui.Render(view, 640, 400, 1)`
|
||||
@@ -0,0 +1,214 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
mujica "mujicaui"
|
||||
)
|
||||
|
||||
func mktDemo(c *ui.Context, log func(string), id string) {
|
||||
s := ui.Local(c.Root(), id, mktInitial)
|
||||
ui.Column(c).Gap(12).FillWidth().Children(func() {
|
||||
frame := mktControls(c, s, log)
|
||||
candles, points, bids, asks, bins := s.candles, s.points, s.bids, s.asks, s.bins
|
||||
if s.status == "Empty" {
|
||||
candles = nil
|
||||
points = nil
|
||||
bids = nil
|
||||
asks = nil
|
||||
bins = nil
|
||||
}
|
||||
ui.Column(c).Disabled(s.disabled).Gap(12).FillWidth().Children(func() {
|
||||
opts := mujica.CandlestickOptions{Frame: frame, Type: s.chartType}
|
||||
chart := func() { mktLog(log, id, mujica.CandlestickChart(c, candles, &s.window, opts)) }
|
||||
switch id {
|
||||
case "141":
|
||||
if mujica.ChartTypeSwitcher(c, &s.chartType, mujica.ChartTypeSwitcherOptions{}).Changed() {
|
||||
log(fmt.Sprintf("Chart type: %d", s.chartType))
|
||||
}
|
||||
opts.Type = s.chartType
|
||||
chart()
|
||||
case "142":
|
||||
mktLog(log, id, mujica.OHLCChart(c, candles, &s.window, opts))
|
||||
case "143":
|
||||
before := s.raw
|
||||
mktLog(log, id, mujica.HeikinAshiChart(c, candles, &s.window, &s.raw, opts))
|
||||
if before != s.raw {
|
||||
log(fmt.Sprintf("Raw: %v", s.raw))
|
||||
}
|
||||
case "144":
|
||||
if mujica.NumberInput(c, &s.previous, mujica.NumberInputOptions{Min: 80, Max: 120, Step: 0.5, Label: "Previous close"}).Changed() {
|
||||
log(fmt.Sprintf("Previous close: %.2f", s.previous))
|
||||
}
|
||||
if mujica.Checkbox(c, &s.extended, "Extended sessions", mujica.CheckboxOptions{}).Changed() {
|
||||
log(fmt.Sprintf("Extended sessions: %v", s.extended))
|
||||
}
|
||||
o := mujica.LineQuoteChartOptions{Frame: frame, PrevClose: s.previous}
|
||||
if s.extended {
|
||||
o.RegularOpen = s.points[0].Time.Add(90 * time.Minute)
|
||||
o.RegularClose = s.points[0].Time.Add(8 * time.Hour)
|
||||
}
|
||||
mktLog(log, id, mujica.LineQuoteChart(c, points, o))
|
||||
case "145":
|
||||
opts.Frame.Height = 150
|
||||
chart()
|
||||
frame.Label = "Volume"
|
||||
frame.Height = 100
|
||||
mktLog(log, id, mujica.VolumeChart(c, candles, &s.window, mujica.VolumeChartOptions{Frame: frame}))
|
||||
case "146":
|
||||
mktLog(log, id, mujica.DepthChart(c, bids, asks, mujica.DepthChartOptions{Frame: frame}))
|
||||
case "147":
|
||||
if mujica.NumberInput(c, &s.fraction, mujica.NumberInputOptions{Min: 0.1, Max: 1, Step: 0.1, Label: "Value area fraction"}).Changed() {
|
||||
log(fmt.Sprintf("Value area: %.1f", s.fraction))
|
||||
}
|
||||
mktLog(log, id, mujica.VolumeProfile(c, bins, mujica.VolumeProfileOptions{Frame: frame, ValueArea: s.fraction}))
|
||||
case "148":
|
||||
if mujica.MultiChartLayout(c, &s.layout, mujica.MultiChartLayoutOptions{Height: 420}, func(i int, cell *mujica.ChartCell) {
|
||||
if mujica.TextInput(c, &cell.Symbol, mujica.TextInputOptions{Label: fmt.Sprintf("Symbol %d", i+1)}).Changed() {
|
||||
log(fmt.Sprintf("Cell %d: %s", i+1, cell.Symbol))
|
||||
}
|
||||
items := make([]mujica.SelectOption[mujica.Interval], len(mujica.Intervals))
|
||||
for j, iv := range mujica.Intervals {
|
||||
items[j] = mujica.SelectOption[mujica.Interval]{Value: iv, Label: string(iv)}
|
||||
}
|
||||
if mujica.Select(c, &cell.Interval, items, mujica.SelectOptions{Label: fmt.Sprintf("Interval %d", i+1)}).Changed() {
|
||||
log(fmt.Sprintf("Cell %d: %s", i+1, cell.Interval))
|
||||
}
|
||||
f := frame
|
||||
f.Label = cell.Symbol
|
||||
f.Height = 70
|
||||
mktLog(log, fmt.Sprint(i), mujica.CandlestickChart(c, candles, &cell.View, mujica.CandlestickOptions{Frame: f}))
|
||||
}).Changed() {
|
||||
log(fmt.Sprintf("Layout: %d, active %d", s.layout.Count, s.layout.Active))
|
||||
}
|
||||
case "149":
|
||||
for _, chartID := range []string{"price", "other"} {
|
||||
ui.Column(c).Key(chartID).Children(func() {
|
||||
if s.sync.Link(c, chartID).Changed() {
|
||||
log("Sync: " + chartID)
|
||||
}
|
||||
v := &s.window
|
||||
if chartID == "other" {
|
||||
v = &s.second
|
||||
}
|
||||
o := opts
|
||||
o.Frame.Height = 100
|
||||
o.Frame.Label = chartID
|
||||
o.Sync = &s.sync
|
||||
o.SyncID = chartID
|
||||
mktLog(log, chartID, mujica.CandlestickChart(c, candles, v, o))
|
||||
})
|
||||
}
|
||||
case "150":
|
||||
if mujica.TimeRangeSelector(c, &s.rangeValue, mujica.TimeRangeSelectorOptions{}).Changed() {
|
||||
log(fmt.Sprintf("Range: %d", s.rangeValue.Preset))
|
||||
}
|
||||
from, to := s.rangeValue.Bounds(c.Now(), time.UTC)
|
||||
ui.Text(c, from.Format("2006-01-02")+" — "+to.Format("2006-01-02"))
|
||||
case "151":
|
||||
before := fmt.Sprint(s.favorites)
|
||||
if mujica.IntervalSelector(c, &s.interval, &s.favorites, mujica.IntervalSelectorOptions{}).Changed() {
|
||||
log("Interval: " + string(s.interval))
|
||||
}
|
||||
if before != fmt.Sprint(s.favorites) {
|
||||
log("Favorites: " + fmt.Sprint(s.favorites))
|
||||
}
|
||||
mujica.CandleCountdown(c, s.interval, mujica.CandleCountdownOptions{})
|
||||
case "152":
|
||||
if mujica.ChartTypeSwitcher(c, &s.chartType, mujica.ChartTypeSwitcherOptions{}).Changed() {
|
||||
log(fmt.Sprintf("Chart type: %d", s.chartType))
|
||||
}
|
||||
opts.Type = s.chartType
|
||||
chart()
|
||||
case "153":
|
||||
if mujica.IndicatorSelector(c, &s.overlays, &s.panes, mujica.IndicatorSelectorOptions{}).Changed() {
|
||||
log("Overlays changed")
|
||||
}
|
||||
opts.Overlays = s.overlays
|
||||
chart()
|
||||
case "154":
|
||||
if mujica.IndicatorSelector(c, &s.overlays, &s.panes, mujica.IndicatorSelectorOptions{}).Changed() {
|
||||
log("Indicators changed")
|
||||
}
|
||||
r := mujica.IndicatorPane(c, candles, &s.window, &s.panes, mujica.IndicatorPaneOptions{Label: "BTC", Height: 320, Status: frame.Status, Error: frame.Error})
|
||||
if r.PointChanged {
|
||||
log(fmt.Sprintf("Pane %d: point %d", r.Pane, r.Point))
|
||||
}
|
||||
if r.Removed >= 0 {
|
||||
log(fmt.Sprintf("Removed pane %d", r.Removed))
|
||||
}
|
||||
if r.Resized {
|
||||
log("Panes resized")
|
||||
}
|
||||
case "155":
|
||||
if mujica.IndicatorSelector(c, &s.overlays, &s.panes, mujica.IndicatorSelectorOptions{}).Changed() {
|
||||
log("Indicators changed")
|
||||
}
|
||||
opts.Overlays = s.overlays
|
||||
chart()
|
||||
case "156", "157":
|
||||
if mujica.TextInput(c, &s.drawings.Text, mujica.TextInputOptions{Label: "Annotation text"}).Changed() {
|
||||
log("Annotation text changed")
|
||||
}
|
||||
if mujica.DrawingToolbar(c, &s.drawings, mujica.DrawingToolbarOptions{Horizontal: true}).Changed() {
|
||||
log(fmt.Sprintf("Tool: %d, locked: %v", s.drawings.Tool, s.drawings.Locked))
|
||||
}
|
||||
before := fmt.Sprint(s.drawings.Items)
|
||||
opts.Drawings = &s.drawings
|
||||
chart()
|
||||
if before != fmt.Sprint(s.drawings.Items) {
|
||||
log(fmt.Sprintf("Drawings: %d", len(s.drawings.Items)))
|
||||
}
|
||||
case "158":
|
||||
if mujica.CompareLegend(c, &s.compare).Changed() {
|
||||
log("Compared symbol removed")
|
||||
}
|
||||
if mujica.Button(c, "Restore ETH", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
|
||||
s.compare = mktInitial().compare
|
||||
log("Restored ETH")
|
||||
}
|
||||
if len(candles) > 0 {
|
||||
opts.Compare = &s.compare
|
||||
}
|
||||
chart()
|
||||
case "159":
|
||||
before := s.alerts[0].Price
|
||||
opts.Alerts = &s.alerts
|
||||
opts.OnAlert = func(a mujica.PriceAlertLine, last mujica.Candle) {
|
||||
log(fmt.Sprintf("Alert %s crossed %.2f", a.ID, last.Close))
|
||||
}
|
||||
if mujica.Button(c, "Append crossing candle", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
|
||||
last := s.candles[len(s.candles)-1]
|
||||
next := last
|
||||
next.Time = last.Time.Add(time.Hour)
|
||||
next.Open = last.Close
|
||||
next.Close = s.alerts[0].Price + 2
|
||||
if last.Close > s.alerts[0].Price {
|
||||
next.Close = s.alerts[0].Price - 2
|
||||
}
|
||||
next.High = max(next.Open, next.Close) + 1
|
||||
next.Low = min(next.Open, next.Close) - 1
|
||||
s.candles = append(s.candles[:len(s.candles):len(s.candles)], next)
|
||||
candles = s.candles
|
||||
log("Appended candle")
|
||||
}
|
||||
chart()
|
||||
if before != s.alerts[0].Price {
|
||||
log(fmt.Sprintf("Alert price: %.2f", s.alerts[0].Price))
|
||||
}
|
||||
case "160":
|
||||
if mujica.IntervalSelector(c, &s.interval, &s.favorites, mujica.IntervalSelectorOptions{}).Changed() {
|
||||
log("Interval: " + string(s.interval))
|
||||
}
|
||||
if mujica.Checkbox(c, &s.hidden, "Hide countdown", mujica.CheckboxOptions{}).Changed() {
|
||||
log(fmt.Sprintf("Hidden: %v", s.hidden))
|
||||
}
|
||||
if !s.hidden {
|
||||
mujica.CandleCountdown(c, s.interval, mujica.CandleCountdownOptions{})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
mujica "mujicaui"
|
||||
)
|
||||
|
||||
type mktDemoState struct {
|
||||
candles []mujica.Candle
|
||||
points []mujica.QuotePoint
|
||||
bids, asks []mujica.DepthLevel
|
||||
bins []mujica.ProfileBin
|
||||
window mujica.MarketView
|
||||
second mujica.MarketView
|
||||
sync mujica.ChartSync
|
||||
layout mujica.MultiChartState
|
||||
interval mujica.Interval
|
||||
favorites []mujica.Interval
|
||||
rangeValue mujica.MarketRange
|
||||
chartType mujica.ChartType
|
||||
overlays []mujica.IndicatorOverlay
|
||||
panes mujica.IndicatorPanes
|
||||
drawings mujica.DrawingTools
|
||||
compare []mujica.CompareSymbol
|
||||
alerts []mujica.PriceAlertLine
|
||||
status string
|
||||
disabled, raw, redUp, hidden, extended bool
|
||||
previous, fraction float64
|
||||
}
|
||||
|
||||
func mktInitial() mktDemoState {
|
||||
s := mktDemoState{status: "Ready", interval: "1h", favorites: []mujica.Interval{"1m", "1h", "1D"}, previous: 100, fraction: 0.7, extended: true}
|
||||
start := time.Date(2026, 10, 5, 8, 0, 0, 0, time.UTC)
|
||||
price := 100.0
|
||||
compare := make([]float64, 180)
|
||||
for i := range 180 {
|
||||
closeAt := price + math.Sin(float64(i)*0.7)*1.5 + math.Cos(float64(i)*0.13)*0.8
|
||||
s.candles = append(s.candles, mujica.Candle{Time: start.Add(time.Duration(i) * time.Hour), Open: price, High: max(price, closeAt) + 0.8, Low: min(price, closeAt) - 0.7, Close: closeAt, Volume: 1000 + float64(i%17)*130})
|
||||
s.points = append(s.points, mujica.QuotePoint{Time: start.Add(time.Duration(i) * 5 * time.Minute), Price: 100 + 2*math.Sin(float64(i)/10), Volume: 100 + float64(i%7)*20})
|
||||
compare[i] = 20 + float64(i)*0.08
|
||||
price = closeAt
|
||||
}
|
||||
for i := range 40 {
|
||||
s.bids = append(s.bids, mujica.DepthLevel{Price: 99.9 - float64(i)*0.1, Size: float64(10 + i*3)})
|
||||
s.asks = append(s.asks, mujica.DepthLevel{Price: 100.1 + float64(i)*0.1, Size: float64(8 + i*4)})
|
||||
}
|
||||
s.bins = mujica.VolumeProfileOf(s.candles, 24)
|
||||
s.compare = []mujica.CompareSymbol{{Name: "ETH", Closes: compare}}
|
||||
s.alerts = []mujica.PriceAlertLine{{ID: "target", Price: 104, Label: "Target"}}
|
||||
s.overlays = []mujica.IndicatorOverlay{{Kind: mujica.IndicatorMA, Params: []float64{10}}}
|
||||
s.panes = mujica.IndicatorPanes{Panes: []mujica.IndicatorPaneSpec{{Kind: mujica.IndicatorMACD}, {Kind: mujica.IndicatorRSI}}}
|
||||
s.layout.Count = 2
|
||||
for i := range s.layout.Cells {
|
||||
s.layout.Cells[i] = mujica.ChartCell{Symbol: fmt.Sprintf("Symbol %d", i+1), Interval: "1h"}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func mktControls(c *ui.Context, s *mktDemoState, log func(string)) mujica.ChartFrame {
|
||||
ui.Row(c).Gap(12).Wrap().Children(func() {
|
||||
if mujica.Select(c, &s.status, []mujica.SelectOption[string]{{Value: "Ready", Label: "Ready"}, {Value: "Loading", Label: "Loading"}, {Value: "Failed", Label: "Failed"}, {Value: "Empty", Label: "Empty"}}, mujica.SelectOptions{Label: "Data state"}).Changed() {
|
||||
log("Data state: " + s.status)
|
||||
}
|
||||
if mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}).Changed() {
|
||||
log(fmt.Sprintf("Disabled: %v", s.disabled))
|
||||
}
|
||||
if mujica.Checkbox(c, &s.redUp, "Red up", mujica.CheckboxOptions{}).Changed() {
|
||||
log(fmt.Sprintf("Red up: %v", s.redUp))
|
||||
}
|
||||
})
|
||||
trend := mujica.GreenUp
|
||||
if s.redUp {
|
||||
trend = mujica.RedUp
|
||||
}
|
||||
mujica.UseTrend(c, trend)
|
||||
f := mujica.ChartFrame{Label: "BTC", Height: 220}
|
||||
switch s.status {
|
||||
case "Loading":
|
||||
f.Status = mujica.DataLoading
|
||||
case "Failed":
|
||||
f.Status = mujica.DataFailed
|
||||
f.Error = "Archive unavailable"
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func mktLog(log func(string), name string, r mujica.ChartResult) {
|
||||
if r.Changed() {
|
||||
log(fmt.Sprintf("%s: point %d", name, r.Point))
|
||||
}
|
||||
if r.Retried() {
|
||||
log(name + ": retry")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
mujica "mujicaui"
|
||||
)
|
||||
|
||||
func mktDemoTester(t *testing.T, id string) (*ui.Tester, *[]string) {
|
||||
t.Helper()
|
||||
events := &[]string{}
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
ui.Scroll(c).Fill().Padding(12).Children(func() { mktDemo(c, func(s string) { *events = append(*events, s) }, id) })
|
||||
}, 900, 800)
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||||
return tt, events
|
||||
}
|
||||
|
||||
func TestMarketChartPagesHaveInteractiveControls(t *testing.T) {
|
||||
if len(mktchartPages) != 20 {
|
||||
t.Fatalf("pages %d", len(mktchartPages))
|
||||
}
|
||||
for _, p := range mktchartPages {
|
||||
t.Run(p.ID, func(t *testing.T) {
|
||||
if p.Category != "Market Charts" || p.Code == "" || p.Keys == "" {
|
||||
t.Fatal("incomplete page")
|
||||
}
|
||||
tt, events := mktDemoTester(t, p.ID)
|
||||
if err := tt.Click("Red up"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(*events) == 0 || (*events)[len(*events)-1] != "Red up: true" {
|
||||
t.Fatalf("control events %q", *events)
|
||||
}
|
||||
if err := tt.Click("Disabled"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if (*events)[len(*events)-1] != "Disabled: true" {
|
||||
t.Fatalf("disabled event %q", *events)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarketChartDemosLogKeyboardAndAlerts(t *testing.T) {
|
||||
for _, id := range []string{"141", "142", "143", "144", "145", "146", "147", "153", "155", "156", "157", "158", "159"} {
|
||||
t.Run(id, func(t *testing.T) {
|
||||
tt, events := mktDemoTester(t, id)
|
||||
tabUntil(t, tt, "BTC")
|
||||
tt.Key(0, ui.KeyRight)
|
||||
want := id + ": point 0"
|
||||
found := false
|
||||
for _, e := range *events {
|
||||
found = found || e == want
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("keyboard event %q, want %s", *events, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
tt, events := mktDemoTester(t, "159")
|
||||
if err := tt.Click("Append crossing candle"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := false
|
||||
for _, e := range *events {
|
||||
found = found || strings.HasPrefix(e, "Alert target crossed ")
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("alert events %q", *events)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user