345 lines
5.7 KiB
Go
345 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"git.cotugno.family/kevin/spectator/exhibit"
|
|
"github.com/shopspring/decimal"
|
|
|
|
"image"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
"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
|
|
}
|