113 lines
3.9 KiB
Go
113 lines
3.9 KiB
Go
package mujica
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/internal"
|
|
"mujicaui/theme"
|
|
)
|
|
|
|
// TradingSession is a phase from Start to End, in minutes after the market's local midnight.
|
|
type TradingSession struct {
|
|
Phase MarketPhase
|
|
Start, End int
|
|
}
|
|
|
|
// SessionMarket is a market, its time zone from the caller, and its daily sessions.
|
|
type SessionMarket struct {
|
|
Name string
|
|
Location *time.Location
|
|
Sessions []TradingSession
|
|
// Holiday closes the market all day.
|
|
Holiday bool
|
|
}
|
|
|
|
// TradingSessionClockOptions configure a TradingSessionClock.
|
|
type TradingSessionClockOptions struct {
|
|
// Now is the instant shown; zero uses the window clock.
|
|
Now time.Time
|
|
}
|
|
|
|
// TradingSessionClock shows each market's local time, phase, next change and a day bar of its sessions.
|
|
func TradingSessionClock(c *ui.Context, markets []SessionMarket, opts TradingSessionClockOptions) *ui.Element {
|
|
now := opts.Now
|
|
if now.IsZero() {
|
|
now = c.Now()
|
|
}
|
|
k := Tokens(c)
|
|
u := Density(c).Unit()
|
|
size := FontSize(c, theme.CaptionSize)
|
|
g := ui.Grid(c).ColumnTracks(ui.FitContent(), ui.FitContent(), ui.Fr(1)).GapX(u * 4).GapY(u * 3).AlignItems(ui.Center)
|
|
g.Children(func() {
|
|
for _, m := range markets {
|
|
tradingSessionCheck(m)
|
|
local := now.In(m.Location)
|
|
phase, next, nextPhase := tradingSessionPhase(m, local)
|
|
ui.Column(c).Key(m.Name).Gap(2).Children(func() {
|
|
ui.Text(c, m.Name).Font(theme.SerifFont).FontWeight(600)
|
|
ui.Text(c, local.Format("15:04 MST")).FontSize(size).TextColor(k.TextMuted).FontFeatures("tnum")
|
|
})
|
|
MarketStatus(c, phase, MarketStatusOptions{Next: next, NextPhase: nextPhase, Now: now})
|
|
minute := float32(local.Hour()*60 + local.Minute())
|
|
ui.Box(c).MinWidth(160).Height(22).Draw(func(p *ui.Painter, r ui.Rect) {
|
|
bar := ui.Rect{X: r.X, Y: r.Y + 4, W: r.W, H: 10}
|
|
p.Fill(bar, k.Background, 2)
|
|
p.Stroke(bar, k.Border, 2, 1)
|
|
for _, s := range m.Sessions {
|
|
col := k.Info.Alpha(0.45)
|
|
if s.Phase == MarketOpen {
|
|
col = k.Success
|
|
}
|
|
x0, x1 := bar.X+bar.W*float32(s.Start)/1440, bar.X+bar.W*float32(s.End)/1440
|
|
p.Fill(ui.Rect{X: x0, Y: bar.Y + 1, W: x1 - x0, H: bar.H - 2}, col, 1)
|
|
}
|
|
x := internal.Snap(p, bar.X+bar.W*minute/1440)
|
|
p.FillPath(internal.Diamond(x, bar.Y+bar.H+3, 8), k.Text)
|
|
p.Line(x, bar.Y-2, x, bar.Y+bar.H, 1.5, k.Text)
|
|
}).Role(ui.RoleImage).Label(m.Name + " " + msg(c, marketPhases[phase].key))
|
|
}
|
|
})
|
|
// Times and phases move on the minute.
|
|
c.After(time.Minute - time.Duration(now.Second())*time.Second)
|
|
return g
|
|
}
|
|
|
|
func tradingSessionCheck(m SessionMarket) {
|
|
if m.Name == "" || m.Location == nil {
|
|
panic("mujica: SessionMarket needs a Name and a Location")
|
|
}
|
|
for i, s := range m.Sessions {
|
|
if s.Start < 0 || s.End > 1440 || s.Start >= s.End || i > 0 && s.Start < m.Sessions[i-1].End {
|
|
panic("mujica: TradingSession spans must be ordered within 0..1440 minutes")
|
|
}
|
|
if s.Phase != MarketPreMarket && s.Phase != MarketOpen && s.Phase != MarketAfterHours {
|
|
panic("mujica: TradingSession Phase must be pre-market, open or after-hours")
|
|
}
|
|
}
|
|
}
|
|
|
|
// tradingSessionPhase is the phase at local, the next change, and the phase it brings.
|
|
func tradingSessionPhase(m SessionMarket, local time.Time) (MarketPhase, time.Time, MarketPhase) {
|
|
if m.Holiday || len(m.Sessions) == 0 {
|
|
return MarketHoliday, time.Time{}, MarketClosed
|
|
}
|
|
mid := time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, m.Location)
|
|
at := func(min int) time.Time { return mid.Add(time.Duration(min) * time.Minute) }
|
|
cur := local.Hour()*60 + local.Minute()
|
|
for i, s := range m.Sessions {
|
|
switch {
|
|
case cur < s.Start:
|
|
return MarketClosed, at(s.Start), s.Phase
|
|
case cur < s.End:
|
|
next := MarketClosed
|
|
if i+1 < len(m.Sessions) && m.Sessions[i+1].Start == s.End {
|
|
next = m.Sessions[i+1].Phase
|
|
}
|
|
return s.Phase, at(s.End), next
|
|
}
|
|
}
|
|
return MarketClosed, at(1440 + m.Sessions[0].Start), m.Sessions[0].Phase
|
|
}
|