fix preorder, postorder and inorder

This commit is contained in:
c9s 2021-05-22 11:36:58 +08:00
parent 09d68057c5
commit 56b2c8845b
14 changed files with 377 additions and 268 deletions

View File

@ -48,11 +48,11 @@ var rootCmd = &cobra.Command{
stream.SetPublicOnly() stream.SetPublicOnly()
stream.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{}) stream.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{})
stream.OnBookSnapshot(func(book types.OrderBook) { stream.OnBookSnapshot(func(book types.SliceOrderBook) {
// log.Infof("book snapshot: %+v", book) // log.Infof("book snapshot: %+v", book)
}) })
stream.OnBookUpdate(func(book types.OrderBook) { stream.OnBookUpdate(func(book types.SliceOrderBook) {
// log.Infof("book update: %+v", book) // log.Infof("book update: %+v", book)
}) })

View File

@ -32,7 +32,7 @@ func (store *MarketDataStore) SetKLineWindows(windows map[types.Interval]types.K
store.KLineWindows = windows store.KLineWindows = windows
} }
func (store *MarketDataStore) OrderBook() types.OrderBook { func (store *MarketDataStore) OrderBook() types.SliceOrderBook {
return store.orderBook.Copy() return store.orderBook.Copy()
} }
@ -42,7 +42,7 @@ func (store *MarketDataStore) KLinesOfInterval(interval types.Interval) (kLines
return kLines, ok return kLines, ok
} }
func (store *MarketDataStore) handleOrderBookUpdate(book types.OrderBook) { func (store *MarketDataStore) handleOrderBookUpdate(book types.SliceOrderBook) {
if book.Symbol != store.Symbol { if book.Symbol != store.Symbol {
return return
} }
@ -52,7 +52,7 @@ func (store *MarketDataStore) handleOrderBookUpdate(book types.OrderBook) {
store.EmitOrderBookUpdate(store.orderBook) store.EmitOrderBookUpdate(store.orderBook)
} }
func (store *MarketDataStore) handleOrderBookSnapshot(book types.OrderBook) { func (store *MarketDataStore) handleOrderBookSnapshot(book types.SliceOrderBook) {
if book.Symbol != store.Symbol { if book.Symbol != store.Symbol {
return return
} }

View File

@ -49,10 +49,10 @@ var orderbookCmd = &cobra.Command{
s := ex.NewStream() s := ex.NewStream()
s.SetPublicOnly() s.SetPublicOnly()
s.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{}) s.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{})
s.OnBookSnapshot(func(book types.OrderBook) { s.OnBookSnapshot(func(book types.SliceOrderBook) {
log.Infof("orderbook snapshot: %s", book.String()) log.Infof("orderbook snapshot: %s", book.String())
}) })
s.OnBookUpdate(func(book types.OrderBook) { s.OnBookUpdate(func(book types.SliceOrderBook) {
log.Infof("orderbook update: %s", book.String()) log.Infof("orderbook update: %s", book.String())
}) })

View File

@ -319,7 +319,7 @@ type DepthEvent struct {
Asks []DepthEntry Asks []DepthEntry
} }
func (e *DepthEvent) OrderBook() (book types.OrderBook, err error) { func (e *DepthEvent) OrderBook() (book types.SliceOrderBook, err error) {
book.Symbol = e.Symbol book.Symbol = e.Symbol
for _, entry := range e.Bids { for _, entry := range e.Bids {

View File

@ -351,16 +351,16 @@ func checksumString(bids, asks [][]json.Number) string {
var errUnmatchedChecksum = fmt.Errorf("unmatched checksum") var errUnmatchedChecksum = fmt.Errorf("unmatched checksum")
func toGlobalOrderBook(r orderBookResponse) (types.OrderBook, error) { func toGlobalOrderBook(r orderBookResponse) (types.SliceOrderBook, error) {
bids, err := toPriceVolumeSlice(r.Bids) bids, err := toPriceVolumeSlice(r.Bids)
if err != nil { if err != nil {
return types.OrderBook{}, fmt.Errorf("can't convert bids to priceVolumeSlice: %w", err) return types.SliceOrderBook{}, fmt.Errorf("can't convert bids to priceVolumeSlice: %w", err)
} }
asks, err := toPriceVolumeSlice(r.Asks) asks, err := toPriceVolumeSlice(r.Asks)
if err != nil { if err != nil {
return types.OrderBook{}, fmt.Errorf("can't convert asks to priceVolumeSlice: %w", err) return types.SliceOrderBook{}, fmt.Errorf("can't convert asks to priceVolumeSlice: %w", err)
} }
return types.OrderBook{ return types.SliceOrderBook{
// ex. BTC/USDT // ex. BTC/USDT
Symbol: toGlobalSymbol(strings.ToUpper(r.Market)), Symbol: toGlobalSymbol(strings.ToUpper(r.Market)),
Bids: bids, Bids: bids,

View File

@ -174,7 +174,7 @@ func (e *BookEvent) Time() time.Time {
return time.Unix(0, e.Timestamp*int64(time.Millisecond)) return time.Unix(0, e.Timestamp*int64(time.Millisecond))
} }
func (e *BookEvent) OrderBook() (snapshot types.OrderBook, err error) { func (e *BookEvent) OrderBook() (snapshot types.SliceOrderBook, err error) {
snapshot.Symbol = strings.ToUpper(e.Market) snapshot.Symbol = strings.ToUpper(e.Market)
for _, bid := range e.Bids { for _, bid := range e.Bids {

View File

@ -2,11 +2,8 @@ package types
import ( import (
"fmt" "fmt"
"strings"
"sync" "sync"
"github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/sigchan" "github.com/c9s/bbgo/pkg/sigchan"
) )
@ -20,236 +17,55 @@ func (p PriceVolume) String() string {
return fmt.Sprintf("PriceVolume{ price: %f, volume: %f }", p.Price.Float64(), p.Volume.Float64()) return fmt.Sprintf("PriceVolume{ price: %f, volume: %f }", p.Price.Float64(), p.Volume.Float64())
} }
//go:generate callbackgen -type RBOrderBook type OrderBook interface {
type RBOrderBook struct { Spread() (fixedpoint.Value, bool)
Symbol string BestAsk() (PriceVolume, bool)
Bids *RBTree BestBid() (PriceVolume, bool)
Asks *RBTree Reset()
Load(book SliceOrderBook)
loadCallbacks []func(book *RBOrderBook) Update(book SliceOrderBook)
updateCallbacks []func(book *RBOrderBook)
}
func NewRBOrderBook(symbol string) *RBOrderBook {
return &RBOrderBook{
Symbol: symbol,
Bids: NewRBTree(),
Asks: NewRBTree(),
}
}
func (b *RBOrderBook) BestBid() (PriceVolume, bool) {
return PriceVolume{}, true
}
func (b *RBOrderBook) BestAsk() (PriceVolume, bool) {
return PriceVolume{}, true
}
//go:generate callbackgen -type OrderBook
type OrderBook struct {
Symbol string
Bids PriceVolumeSlice
Asks PriceVolumeSlice
loadCallbacks []func(book *OrderBook)
updateCallbacks []func(book *OrderBook)
}
func (b *OrderBook) Spread() (fixedpoint.Value, bool) {
bestBid, ok := b.BestBid()
if !ok {
return 0, false
}
bestAsk, ok := b.BestAsk()
if !ok {
return 0, false
}
return bestAsk.Price - bestBid.Price, true
}
func (b *OrderBook) BestBid() (PriceVolume, bool) {
if len(b.Bids) == 0 {
return PriceVolume{}, false
}
return b.Bids[0], true
}
func (b *OrderBook) BestAsk() (PriceVolume, bool) {
if len(b.Asks) == 0 {
return PriceVolume{}, false
}
return b.Asks[0], true
}
func (b *OrderBook) IsValid() (bool, error) {
bid, hasBid := b.BestBid()
ask, hasAsk := b.BestAsk()
if !hasBid {
return false, errors.New("empty bids")
}
if !hasAsk {
return false, errors.New("empty asks")
}
if bid.Price > ask.Price {
return false, fmt.Errorf("bid price %f > ask price %f", bid.Price.Float64(), ask.Price.Float64())
}
return true, nil
}
func (b *OrderBook) PriceVolumesBySide(side SideType) PriceVolumeSlice {
switch side {
case SideTypeBuy:
return b.Bids
case SideTypeSell:
return b.Asks
}
return nil
}
func (b *OrderBook) CopyDepth(depth int) (book OrderBook) {
book = *b
book.Bids = book.Bids.CopyDepth(depth)
book.Asks = book.Asks.CopyDepth(depth)
return book
}
func (b *OrderBook) Copy() (book OrderBook) {
book = *b
book.Bids = book.Bids.Copy()
book.Asks = book.Asks.Copy()
return book
}
func (b *OrderBook) updateAsks(pvs PriceVolumeSlice) {
for _, pv := range pvs {
if pv.Volume == 0 {
b.Asks = b.Asks.Remove(pv.Price, false)
} else {
b.Asks = b.Asks.Upsert(pv, false)
}
}
}
func (b *OrderBook) updateBids(pvs PriceVolumeSlice) {
for _, pv := range pvs {
if pv.Volume == 0 {
b.Bids = b.Bids.Remove(pv.Price, true)
} else {
b.Bids = b.Bids.Upsert(pv, true)
}
}
}
func (b *OrderBook) update(book OrderBook) {
b.updateBids(book.Bids)
b.updateAsks(book.Asks)
}
func (b *OrderBook) Reset() {
b.Bids = nil
b.Asks = nil
}
func (b *OrderBook) Load(book OrderBook) {
b.Reset()
b.update(book)
b.EmitLoad(b)
}
func (b *OrderBook) Update(book OrderBook) {
b.update(book)
b.EmitUpdate(b)
}
func (b *OrderBook) Print() {
fmt.Printf(b.String())
}
func (b *OrderBook) String() string {
sb := strings.Builder{}
sb.WriteString("BOOK ")
sb.WriteString(b.Symbol)
sb.WriteString("\n")
if len(b.Asks) > 0 {
sb.WriteString("ASKS:\n")
for i := len(b.Asks) - 1; i >= 0; i-- {
sb.WriteString("- ASK: ")
sb.WriteString(b.Asks[i].String())
sb.WriteString("\n")
}
}
if len(b.Bids) > 0 {
sb.WriteString("BIDS:\n")
for _, bid := range b.Bids {
sb.WriteString("- BID: ")
sb.WriteString(bid.String())
sb.WriteString("\n")
}
}
return sb.String()
} }
type MutexOrderBook struct { type MutexOrderBook struct {
sync.Mutex sync.Mutex
*OrderBook *SliceOrderBook
} }
func NewMutexOrderBook(symbol string) *MutexOrderBook { func NewMutexOrderBook(symbol string) *MutexOrderBook {
return &MutexOrderBook{ return &MutexOrderBook{
OrderBook: &OrderBook{Symbol: symbol}, SliceOrderBook: &SliceOrderBook{Symbol: symbol},
} }
} }
func (b *MutexOrderBook) Load(book OrderBook) { func (b *MutexOrderBook) Load(book SliceOrderBook) {
b.Lock() b.Lock()
defer b.Unlock() b.SliceOrderBook.Load(book)
b.Unlock()
b.OrderBook.Reset()
b.OrderBook.update(book)
b.EmitLoad(b.OrderBook)
} }
func (b *MutexOrderBook) Reset() { func (b *MutexOrderBook) Reset() {
b.Lock() b.Lock()
b.OrderBook.Reset() b.SliceOrderBook.Reset()
b.Unlock() b.Unlock()
} }
func (b *MutexOrderBook) CopyDepth(depth int) OrderBook { func (b *MutexOrderBook) CopyDepth(depth int) SliceOrderBook {
b.Lock() b.Lock()
defer b.Unlock() defer b.Unlock()
return b.OrderBook.CopyDepth(depth) return b.SliceOrderBook.CopyDepth(depth)
} }
func (b *MutexOrderBook) Get() OrderBook { func (b *MutexOrderBook) Get() SliceOrderBook {
b.Lock() b.Lock()
defer b.Unlock() defer b.Unlock()
return b.OrderBook.Copy() return b.SliceOrderBook.Copy()
} }
func (b *MutexOrderBook) Update(update OrderBook) { func (b *MutexOrderBook) Update(update SliceOrderBook) {
b.Lock() b.Lock()
defer b.Unlock() b.SliceOrderBook.Update(update)
b.Unlock()
b.OrderBook.update(update)
b.EmitUpdate(b.OrderBook)
} }
// StreamOrderBook receives streaming data from websocket connection and // StreamOrderBook receives streaming data from websocket connection and
@ -268,7 +84,7 @@ func NewStreamBook(symbol string) *StreamOrderBook {
} }
func (sb *StreamOrderBook) BindStream(stream Stream) { func (sb *StreamOrderBook) BindStream(stream Stream) {
stream.OnBookSnapshot(func(book OrderBook) { stream.OnBookSnapshot(func(book SliceOrderBook) {
if sb.Symbol != book.Symbol { if sb.Symbol != book.Symbol {
return return
} }
@ -277,7 +93,7 @@ func (sb *StreamOrderBook) BindStream(stream Stream) {
sb.C.Emit() sb.C.Emit()
}) })
stream.OnBookUpdate(func(book OrderBook) { stream.OnBookUpdate(func(book SliceOrderBook) {
if sb.Symbol != book.Symbol { if sb.Symbol != book.Symbol {
return return
} }

View File

@ -1,25 +0,0 @@
// Code generated by "callbackgen -type OrderBook"; DO NOT EDIT.
package types
import ()
func (b *OrderBook) OnLoad(cb func(book *OrderBook)) {
b.loadCallbacks = append(b.loadCallbacks, cb)
}
func (b *OrderBook) EmitLoad(book *OrderBook) {
for _, cb := range b.loadCallbacks {
cb(book)
}
}
func (b *OrderBook) OnUpdate(cb func(book *OrderBook)) {
b.updateCallbacks = append(b.updateCallbacks, cb)
}
func (b *OrderBook) EmitUpdate(book *OrderBook) {
for _, cb := range b.updateCallbacks {
cb(book)
}
}

View File

@ -24,7 +24,7 @@ func BenchmarkOrderBook_Load(b *testing.B) {
bids = append(bids, PriceVolume{fixedpoint.NewFromFloat(1000 - 0.1 - p), fixedpoint.NewFromFloat(1)}) bids = append(bids, PriceVolume{fixedpoint.NewFromFloat(1000 - 0.1 - p), fixedpoint.NewFromFloat(1)})
} }
b.Run("RBOrderBook", func(b *testing.B) { b.Run("RBTOrderBook", func(b *testing.B) {
book := NewRBOrderBook("ETHUSDT") book := NewRBOrderBook("ETHUSDT")
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
for _, ask := range asks { for _, ask := range asks {
@ -36,8 +36,8 @@ func BenchmarkOrderBook_Load(b *testing.B) {
} }
}) })
b.Run("OrderBook", func(b *testing.B) { b.Run("SliceOrderBook", func(b *testing.B) {
book := &OrderBook{} book := &SliceOrderBook{}
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
for _, ask := range asks { for _, ask := range asks {
book.Asks = book.Asks.Upsert(ask, false) book.Asks = book.Asks.Upsert(ask, false)
@ -64,7 +64,7 @@ func BenchmarkOrderBook_UpdateAndInsert(b *testing.B) {
rbBook.Bids.Upsert(bid.Price, bid.Volume) rbBook.Bids.Upsert(bid.Price, bid.Volume)
} }
b.Run("RBOrderBook", func(b *testing.B) { b.Run("RBTOrderBook", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
var price = fixedpoint.NewFromFloat(rand.Float64() * 2000.0) var price = fixedpoint.NewFromFloat(rand.Float64() * 2000.0)
if price >= fixedpoint.NewFromFloat(1000) { if price >= fixedpoint.NewFromFloat(1000) {
@ -75,7 +75,7 @@ func BenchmarkOrderBook_UpdateAndInsert(b *testing.B) {
} }
}) })
sliceBook := &OrderBook{} sliceBook := &SliceOrderBook{}
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
for _, ask := range asks { for _, ask := range asks {
sliceBook.Asks = sliceBook.Asks.Upsert(ask, false) sliceBook.Asks = sliceBook.Asks.Upsert(ask, false)
@ -84,7 +84,7 @@ func BenchmarkOrderBook_UpdateAndInsert(b *testing.B) {
sliceBook.Bids = sliceBook.Bids.Upsert(bid, true) sliceBook.Bids = sliceBook.Bids.Upsert(bid, true)
} }
} }
b.Run("OrderBook", func(b *testing.B) { b.Run("SliceOrderBook", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
var price = fixedpoint.NewFromFloat(rand.Float64() * 2000.0) var price = fixedpoint.NewFromFloat(rand.Float64() * 2000.0)
if price >= fixedpoint.NewFromFloat(1000) { if price >= fixedpoint.NewFromFloat(1000) {
@ -97,7 +97,7 @@ func BenchmarkOrderBook_UpdateAndInsert(b *testing.B) {
} }
func TestOrderBook_IsValid(t *testing.T) { func TestOrderBook_IsValid(t *testing.T) {
ob := OrderBook{ ob := SliceOrderBook{
Bids: PriceVolumeSlice{ Bids: PriceVolumeSlice{
{fixedpoint.NewFromFloat(100.0), fixedpoint.NewFromFloat(1.5)}, {fixedpoint.NewFromFloat(100.0), fixedpoint.NewFromFloat(1.5)},
{fixedpoint.NewFromFloat(90.0), fixedpoint.NewFromFloat(2.5)}, {fixedpoint.NewFromFloat(90.0), fixedpoint.NewFromFloat(2.5)},

132
pkg/types/rbtorderbook.go Normal file
View File

@ -0,0 +1,132 @@
package types
import (
"fmt"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/pkg/errors"
)
//go:generate callbackgen -type RBTOrderBook
type RBTOrderBook struct {
Symbol string
Bids *RBTree
Asks *RBTree
loadCallbacks []func(book *RBTOrderBook)
updateCallbacks []func(book *RBTOrderBook)
}
func NewRBOrderBook(symbol string) *RBTOrderBook {
return &RBTOrderBook{
Symbol: symbol,
Bids: NewRBTree(),
Asks: NewRBTree(),
}
}
func (b *RBTOrderBook) BestBid() (PriceVolume, bool) {
right := b.Bids.Rightmost(b.Bids.Root)
if right != nil {
return PriceVolume{Price: right.Key, Volume: right.Value}, true
}
return PriceVolume{}, false
}
func (b *RBTOrderBook) BestAsk() (PriceVolume, bool) {
left := b.Asks.Leftmost(b.Bids.Root)
if left != nil {
return PriceVolume{Price: left.Key, Volume: left.Value}, true
}
return PriceVolume{}, false
}
func (b *RBTOrderBook) Spread() (fixedpoint.Value, bool) {
bestBid, ok := b.BestBid()
if !ok {
return 0, false
}
bestAsk, ok := b.BestAsk()
if !ok {
return 0, false
}
return bestAsk.Price - bestBid.Price, true
}
func (b *RBTOrderBook) IsValid() (bool, error) {
bid, hasBid := b.BestBid()
ask, hasAsk := b.BestAsk()
if !hasBid {
return false, errors.New("empty bids")
}
if !hasAsk {
return false, errors.New("empty asks")
}
if bid.Price > ask.Price {
return false, fmt.Errorf("bid price %f > ask price %f", bid.Price.Float64(), ask.Price.Float64())
}
return true, nil
}
func (b *RBTOrderBook) Load(book SliceOrderBook) {
b.Reset()
b.update(book)
b.EmitLoad(b)
}
func (b *RBTOrderBook) Update(book SliceOrderBook) {
b.update(book)
b.EmitUpdate(b)
}
func (b *RBTOrderBook) Reset() {
b.Bids = NewRBTree()
b.Asks = NewRBTree()
}
func (b *RBTOrderBook) updateAsks(pvs PriceVolumeSlice) {
for _, pv := range pvs {
if pv.Volume == 0 {
b.Asks.Delete(pv.Price)
} else {
b.Asks.Upsert(pv.Price, pv.Volume)
}
}
}
func (b *RBTOrderBook) updateBids(pvs PriceVolumeSlice) {
for _, pv := range pvs {
if pv.Volume == 0 {
b.Bids.Delete(pv.Price)
} else {
b.Bids.Upsert(pv.Price, pv.Volume)
}
}
}
func (b *RBTOrderBook) update(book SliceOrderBook) {
b.updateBids(book.Bids)
b.updateAsks(book.Asks)
}
func (b *RBTOrderBook) load(book SliceOrderBook) {
b.Reset()
b.updateBids(book.Bids)
b.updateAsks(book.Asks)
}
func (b *RBTOrderBook) Print() {
b.Bids.PostorderOf(b.Bids.Root, func(n *RBNode) {
fmt.Printf("bid: %f x %f", n.Key.Float64(), n.Value.Float64())
})
b.Asks.PostorderOf(b.Asks.Root, func(n *RBNode) {
fmt.Printf("ask: %f x %f", n.Key.Float64(), n.Value.Float64())
})
}

View File

@ -341,26 +341,38 @@ func (tree *RBTree) Successor(current *RBNode) *RBNode {
return newNode return newNode
} }
func (tree *RBTree) Preorder(current *RBNode, cb func(n *RBNode)) { func (tree *RBTree) Preorder(cb func(n *RBNode)) {
tree.PreorderOf(tree.Root, cb)
}
func (tree *RBTree) PreorderOf(current *RBNode, cb func(n *RBNode)) {
if current != nil { if current != nil {
cb(current) cb(current)
tree.Preorder(current.Left, cb) tree.PreorderOf(current.Left, cb)
tree.Preorder(current.Right, cb) tree.PreorderOf(current.Right, cb)
} }
} }
func (tree *RBTree) Inorder(current *RBNode, cb func(n *RBNode)) { func (tree *RBTree) Inorder(cb func(n *RBNode)) {
tree.InorderOf(tree.Root, cb)
}
func (tree *RBTree) InorderOf(current *RBNode, cb func(n *RBNode)) {
if current != nil { if current != nil {
tree.Preorder(current.Left, cb) tree.InorderOf(current.Left, cb)
cb(current) cb(current)
tree.Preorder(current.Right, cb) tree.InorderOf(current.Right, cb)
} }
} }
func (tree *RBTree) Postorder(current *RBNode, cb func(n *RBNode)) { func (tree *RBTree) Postorder(cb func(n *RBNode)) {
tree.PostorderOf(tree.Root, cb)
}
func (tree *RBTree) PostorderOf(current *RBNode, cb func(n *RBNode)) {
if current != nil { if current != nil {
tree.Preorder(current.Left, cb) tree.PostorderOf(current.Left, cb)
tree.Preorder(current.Right, cb) tree.PostorderOf(current.Right, cb)
cb(current) cb(current)
} }
} }

174
pkg/types/sliceorderbook.go Normal file
View File

@ -0,0 +1,174 @@
package types
import (
"fmt"
"strings"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/pkg/errors"
)
// SliceOrderBook is a general order book structure which could be used
// for RESTful responses and websocket stream parsing
//go:generate callbackgen -type SliceOrderBook
type SliceOrderBook struct {
Symbol string
Bids PriceVolumeSlice
Asks PriceVolumeSlice
loadCallbacks []func(book *SliceOrderBook)
updateCallbacks []func(book *SliceOrderBook)
}
func (b *SliceOrderBook) Spread() (fixedpoint.Value, bool) {
bestBid, ok := b.BestBid()
if !ok {
return 0, false
}
bestAsk, ok := b.BestAsk()
if !ok {
return 0, false
}
return bestAsk.Price - bestBid.Price, true
}
func (b *SliceOrderBook) BestBid() (PriceVolume, bool) {
if len(b.Bids) == 0 {
return PriceVolume{}, false
}
return b.Bids[0], true
}
func (b *SliceOrderBook) BestAsk() (PriceVolume, bool) {
if len(b.Asks) == 0 {
return PriceVolume{}, false
}
return b.Asks[0], true
}
func (b *SliceOrderBook) IsValid() (bool, error) {
bid, hasBid := b.BestBid()
ask, hasAsk := b.BestAsk()
if !hasBid {
return false, errors.New("empty bids")
}
if !hasAsk {
return false, errors.New("empty asks")
}
if bid.Price > ask.Price {
return false, fmt.Errorf("bid price %f > ask price %f", bid.Price.Float64(), ask.Price.Float64())
}
return true, nil
}
func (b *SliceOrderBook) PriceVolumesBySide(side SideType) PriceVolumeSlice {
switch side {
case SideTypeBuy:
return b.Bids
case SideTypeSell:
return b.Asks
}
return nil
}
func (b *SliceOrderBook) CopyDepth(depth int) (book SliceOrderBook) {
book = *b
book.Bids = book.Bids.CopyDepth(depth)
book.Asks = book.Asks.CopyDepth(depth)
return book
}
func (b *SliceOrderBook) Copy() (book SliceOrderBook) {
book = *b
book.Bids = book.Bids.Copy()
book.Asks = book.Asks.Copy()
return book
}
func (b *SliceOrderBook) updateAsks(pvs PriceVolumeSlice) {
for _, pv := range pvs {
if pv.Volume == 0 {
b.Asks = b.Asks.Remove(pv.Price, false)
} else {
b.Asks = b.Asks.Upsert(pv, false)
}
}
}
func (b *SliceOrderBook) updateBids(pvs PriceVolumeSlice) {
for _, pv := range pvs {
if pv.Volume == 0 {
b.Bids = b.Bids.Remove(pv.Price, true)
} else {
b.Bids = b.Bids.Upsert(pv, true)
}
}
}
func (b *SliceOrderBook) load(book SliceOrderBook) {
b.Reset()
b.update(book)
}
func (b *SliceOrderBook) update(book SliceOrderBook) {
b.updateBids(book.Bids)
b.updateAsks(book.Asks)
}
func (b *SliceOrderBook) Reset() {
b.Bids = nil
b.Asks = nil
}
func (b *SliceOrderBook) Load(book SliceOrderBook) {
b.load(book)
b.EmitLoad(b)
}
func (b *SliceOrderBook) Update(book SliceOrderBook) {
b.update(book)
b.EmitUpdate(b)
}
func (b *SliceOrderBook) Print() {
fmt.Printf(b.String())
}
func (b *SliceOrderBook) String() string {
sb := strings.Builder{}
sb.WriteString("BOOK ")
sb.WriteString(b.Symbol)
sb.WriteString("\n")
if len(b.Asks) > 0 {
sb.WriteString("ASKS:\n")
for i := len(b.Asks) - 1; i >= 0; i-- {
sb.WriteString("- ASK: ")
sb.WriteString(b.Asks[i].String())
sb.WriteString("\n")
}
}
if len(b.Bids) > 0 {
sb.WriteString("BIDS:\n")
for _, bid := range b.Bids {
sb.WriteString("- BID: ")
sb.WriteString(bid.String())
sb.WriteString("\n")
}
}
return sb.String()
}

View File

@ -94,21 +94,21 @@ func (stream *StandardStream) EmitKLine(kline KLine) {
} }
} }
func (stream *StandardStream) OnBookUpdate(cb func(book OrderBook)) { func (stream *StandardStream) OnBookUpdate(cb func(book SliceOrderBook)) {
stream.bookUpdateCallbacks = append(stream.bookUpdateCallbacks, cb) stream.bookUpdateCallbacks = append(stream.bookUpdateCallbacks, cb)
} }
func (stream *StandardStream) EmitBookUpdate(book OrderBook) { func (stream *StandardStream) EmitBookUpdate(book SliceOrderBook) {
for _, cb := range stream.bookUpdateCallbacks { for _, cb := range stream.bookUpdateCallbacks {
cb(book) cb(book)
} }
} }
func (stream *StandardStream) OnBookSnapshot(cb func(book OrderBook)) { func (stream *StandardStream) OnBookSnapshot(cb func(book SliceOrderBook)) {
stream.bookSnapshotCallbacks = append(stream.bookSnapshotCallbacks, cb) stream.bookSnapshotCallbacks = append(stream.bookSnapshotCallbacks, cb)
} }
func (stream *StandardStream) EmitBookSnapshot(book OrderBook) { func (stream *StandardStream) EmitBookSnapshot(book SliceOrderBook) {
for _, cb := range stream.bookSnapshotCallbacks { for _, cb := range stream.bookSnapshotCallbacks {
cb(book) cb(book)
} }
@ -133,7 +133,7 @@ type StandardStreamEventHub interface {
OnKLine(cb func(kline KLine)) OnKLine(cb func(kline KLine))
OnBookUpdate(cb func(book OrderBook)) OnBookUpdate(cb func(book SliceOrderBook))
OnBookSnapshot(cb func(book OrderBook)) OnBookSnapshot(cb func(book SliceOrderBook))
} }

View File

@ -44,9 +44,9 @@ type StandardStream struct {
kLineCallbacks []func(kline KLine) kLineCallbacks []func(kline KLine)
bookUpdateCallbacks []func(book OrderBook) bookUpdateCallbacks []func(book SliceOrderBook)
bookSnapshotCallbacks []func(book OrderBook) bookSnapshotCallbacks []func(book SliceOrderBook)
} }
func (stream *StandardStream) Subscribe(channel Channel, symbol string, options SubscribeOptions) { func (stream *StandardStream) Subscribe(channel Channel, symbol string, options SubscribeOptions) {