package main import ( "git.kevincotugno.com/kcotugno/spectator/exhibit" "github.com/shopspring/decimal" "image" "log" "time" "sync" "unicode/utf8" ) const ( coin = "ETH-USD" timeFormat = "15:04:05" ) var trades = NewQueue() var terminal *exhibit.Terminal var ob *OrderBook var window *exhibit.WindowWidget var topAsks *exhibit.ListWidget var topBids *exhibit.ListWidget var midPrice *exhibit.ListWidget var history *exhibit.ListWidget var numLock sync.Mutex var num int var low, high decimal.Decimal func main() { var err error terminal = exhibit.Init() defer terminal.Shutdown() terminal.HideCursor() window = &exhibit.WindowWidget{} window.SetBorder(exhibit.Border{Visible: true, Attributes: exhibit.Attributes{ForegroundColor: exhibit.FGYellow}}) topAsks = &exhibit.ListWidget{} topBids = &exhibit.ListWidget{} midPrice = &exhibit.ListWidget{} midPrice.SetSize(image.Pt(24, 1)) history = &exhibit.ListWidget{} window.AddWidget(topAsks) window.AddWidget(midPrice) window.AddWidget(topBids) window.AddWidget(history) scene := exhibit.Scene{terminal, window} watchSize(terminal.SizeChange) ob, err = NewOrderBook(coin) if err != nil { log.Fatal(err) } go func() { Loop: for e := range terminal.Event { switch e { case exhibit.Eventq: fallthrough case exhibit.EventCtrC: ob.Shutdown() break Loop } } }() go func() { <-ob.Err }() go renderLoop(&scene, 100*time.Millisecond) updateOrders("sell") updateOrders("buy") for msg := range ob.Msg { updateOrders(msg.Side) } } func numPerSide() int { numLock.Lock() defer numLock.Unlock() return num } func numOfOrderPerSide(y int) int { total := y - 3 - 2 return (total / 2) } func flatten(entries Entries) (decimal.Decimal, decimal.Decimal) { var price, size decimal.Decimal for _, v := range entries { price = v.Price size = size.Add(v.Size) } return price, size } func renderLoop(scene *exhibit.Scene, interval time.Duration) { timer := time.NewTicker(interval) for { select { case <-timer.C: scene.Render() } } } func recalcSizes(sz image.Point) { numLock.Lock() defer numLock.Unlock() num = numOfOrderPerSide(sz.Y) if history.Size() != image.Pt(33, sz.Y) { history.SetSize(image.Pt(33, sz.Y)) } hOr := image.Pt(sz.X-35, 0) if history.Origin() != hOr { history.SetOrigin(hOr) } num := numOfOrderPerSide(sz.Y) size := image.Point{23, num} if topAsks.Size() != size { topAsks.SetSize(size) } bOrigin := image.Pt(0, size.Y+3) if topBids.Origin() != bOrigin { topBids.SetOrigin(bOrigin) } if topBids.Size() != size { topBids.SetSize(size) } mOr := image.Pt(0, num+1) if midPrice.Origin() != mOr { midPrice.SetOrigin(mOr) } } func padString(value string, length int) string { c := utf8.RuneCountInString(value) if c >= length { return value } pad := length - c var s string for i := 0; i < pad; i++ { s = s + " " } return s + value } func fmtObEntry(price, size decimal.Decimal) string { s := padString(price.StringFixed(2), 8) s = s + " " s = s + padString(size.StringFixed(8), 14) return s } func fmtHistoryEntry(msg Message) string { var arrow string switch msg.Side { case "buy": arrow = "↓" case "sell": arrow = "↑" } s := padString(msg.Size.StringFixed(8), 14) s = s + " " s = s + padString(msg.Price.StringFixed(2), 8) s = s + arrow s = s + " " s = s + msg.Time.Local().Format(timeFormat) return s } func fmtMid(high, low decimal.Decimal) string { diff := low.Sub(high) mid := high.Add(diff.Div(decimal.New(2, 0))).StringFixed(3) return padString(mid, 9) + padString(diff.StringFixed(2), 14) } func watchSize(c <-chan image.Point) { go func() { for s := range c { recalcSizes(s) } }() } func updateOrders(side string) { n := numPerSide() entries := ob.Entries(side, n) switch side { case "sell": updateAsks(entries) case "buy": updateBids(entries) } midPrice.AddEntry(ListEntry{Value: fmtMid(high, low)}) midPrice.Commit() } func updateAsks(entries []Entries) { for i := len(entries) - 1; i >= 0; i-- { entry := entries[i] price, size := flatten(entry) topAsks.AddEntry(ListEntry{Value: fmtObEntry(price, size), Attrs: exhibit.Attributes{ForegroundColor: exhibit.FGRed}}) if i == 0 { low = price } } topAsks.Commit() } func updateBids(entries []Entries) { for i := 0; i < len(entries); i++ { entry := entries[i] price, size := flatten(entry) topBids.AddEntry(ListEntry{Value: fmtObEntry(price, size), Attrs: exhibit.Attributes{ForegroundColor: exhibit.FGGreen}}) if i == 0 { high = price } } topBids.Commit() }