This repository has been archived on 2022-11-30. You can view files and clone it, but cannot push or open issues or pull requests.
spectator/spectator.go

346 lines
5.7 KiB
Go

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 sizeLock sync.Mutex
var sizeChanged bool
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)
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 renderLoop(&scene, 100*time.Millisecond)
updateOrders("sell")
updateOrders("buy")
for msg := range ob.Msg {
updateOrders(msg.Side)
if msg.Type == "match" {
addTrade(msg)
}
}
}
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)
changed := time.NewTicker(2 * time.Second)
for {
select {
case <-changed.C:
if didsizeChanged() {
terminal.Clear()
setSizeChanged(false)
}
case <-timer.C:
scene.Render()
}
}
}
func recalcSizes(sz image.Point) {
numLock.Lock()
defer numLock.Unlock()
sizeLock.Lock()
defer sizeLock.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(t *exhibit.Terminal) {
go func() {
for s := range t.SizeChange {
recalcSizes(s)
setSizeChanged(true)
}
}()
}
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()
}
func addTrade(msg Message) {
if trades.Length() == 256 {
trades.Dequeue()
}
trades.Enqueue(msg)
max := history.Size().Y
length := trades.Length()
var num int
if length > max {
num = max
} else {
num = length
}
for i := 0; i < num; i++ {
var index int
adj := trades.Length() - i - 1
if adj < 0 {
break
} else {
index = adj
}
e := trades.Element(index)
if e != nil {
msg := e.(Message)
var attrs exhibit.Attributes
switch msg.Side {
case "buy":
attrs.ForegroundColor = exhibit.FGRed
case "sell":
attrs.ForegroundColor = exhibit.FGGreen
}
le := ListEntry{fmtHistoryEntry(msg), attrs}
history.AddEntry(le)
}
}
history.Commit()
}
func didsizeChanged() bool {
sizeLock.Lock()
defer sizeLock.Unlock()
return sizeChanged
}
func setSizeChanged(c bool) {
sizeLock.Lock()
defer sizeLock.Unlock()
sizeChanged = c
}