Skip to content

Commit

Permalink
tatanka: Add orderbook map.
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeGruffins committed Feb 21, 2025
1 parent 6a2efe4 commit e39c239
Show file tree
Hide file tree
Showing 3 changed files with 397 additions and 8 deletions.
149 changes: 149 additions & 0 deletions tatanka/client/orderbook/orderbook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

package orderbook

import (
"sort"
"sync"

"decred.org/dcrdex/tatanka/tanka"
)

type OrderBooker interface {
OrderIDs() [][32]byte
Order(id [32]byte) *tanka.OrderUpdate
Orders(ids [][32]byte) []*tanka.OrderUpdate
FindOrders(filter *OrderFilter) []*tanka.OrderUpdate
AddUpdate(*tanka.OrderUpdate)
Delete(id [32]byte)
}

type OrderFilter struct {
IsSell *bool
Check func(*tanka.OrderUpdate) (ok, done bool)
}

var _ OrderBooker = (*OrderBook)(nil)

type OrderBook struct {
mtx sync.RWMutex
book map[[32]byte]*tanka.OrderUpdate
// buys and sells use the same pointers as the book and are sorted
// with the best trade first.
buys, sells []*tanka.OrderUpdate
}

func NewOrderBook() *OrderBook {
return &OrderBook{
book: make(map[[32]byte]*tanka.OrderUpdate),
}
}

func (ob *OrderBook) OrderIDs() [][32]byte {
ob.mtx.RLock()
defer ob.mtx.RUnlock()
ids := make([][32]byte, len(ob.book))
i := 0
for id := range ob.book {
ids[i] = id
i++
}
return ids
}

func (ob *OrderBook) Order(id [32]byte) *tanka.OrderUpdate {
ob.mtx.RLock()
defer ob.mtx.RUnlock()
return ob.book[id]
}

func (ob *OrderBook) Orders(ids [][32]byte) []*tanka.OrderUpdate {
ob.mtx.RLock()
defer ob.mtx.RUnlock()
ords := make([]*tanka.OrderUpdate, 0, len(ids))
for _, id := range ids {
if ou, has := ob.book[id]; has {
ords = append(ords, ou)
}
}
return ords
}

func (ob *OrderBook) FindOrders(filter *OrderFilter) (ous []*tanka.OrderUpdate) {
if filter == nil {
return nil
}
find := func(isSell bool) {
ords := ob.buys
if isSell {
ords = ob.sells
}
for _, ou := range ords {
if filter.Check != nil {
ok, done := filter.Check(ou)
if !ok {
continue
}
if done {
break
}
}
ous = append(ous, ou)
}
}
if filter.IsSell != nil {
find(*filter.IsSell)
} else {
find(false)
find(true)
}
return ous
}

func (ob *OrderBook) addOrderAndSort(ou *tanka.OrderUpdate) {
ords := &ob.buys
sortFn := func(i, j int) bool { return (*ords)[i].Rate > (*ords)[j].Rate }
if ou.Sell {
ords = &ob.sells
sortFn = func(i, j int) bool { return (*ords)[i].Rate < (*ords)[j].Rate }
}
*ords = append(*ords, ou)
sort.Slice(*ords, sortFn)
}

func (ob *OrderBook) deleteSortedOrder(ou *tanka.OrderUpdate) {
ords := &ob.buys
if ou.Sell {
ords = &ob.sells
}
for i, o := range *ords {
// Comparing pointers.
if o == ou {
*ords = append((*ords)[:i], (*ords)[i+1:]...)
break
}
}
}

func (ob *OrderBook) AddUpdate(ou *tanka.OrderUpdate) {
id := ou.ID()
ob.mtx.Lock()
defer ob.mtx.Unlock()
oldOU, has := ob.book[id]
ob.book[id] = ou
if has {
ob.deleteSortedOrder(oldOU)
}
ob.addOrderAndSort(ou)
}

func (ob *OrderBook) Delete(id [32]byte) {
ob.mtx.Lock()
defer ob.mtx.Unlock()
ou, has := ob.book[id]
if has {
delete(ob.book, id)
ob.deleteSortedOrder(ou)
}
}
196 changes: 196 additions & 0 deletions tatanka/client/orderbook/orderbook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package orderbook

import (
"testing"
"time"

"decred.org/dcrdex/dex/encode"
"decred.org/dcrdex/tatanka/tanka"
)

func testOrders() []*tanka.OrderUpdate {
mustParse := func(s string) time.Time {
t, err := time.Parse(time.RFC1123, s)
if err != nil {
panic(err)
}
return t
}

var peer tanka.PeerID
copy(peer[:], encode.RandomBytes(32))
baseID, quoteID := uint32(0), uint32(42)
lowbuy := &tanka.OrderUpdate{
Sig: encode.RandomBytes(32),
Order: &tanka.Order{
From: peer,
BaseID: baseID,
QuoteID: quoteID,
Sell: false,
Qty: 1000,
Rate: 123,
LotSize: 3,
Nonce: 0,
Stamp: mustParse("Sun, 12 Dec 2024 12:23:00 UTC"),
},
Expiration: mustParse("Sun, 12 Jan 2025 12:23:00 UTC"),
Settled: 500,
}
highbuy := &tanka.OrderUpdate{
Sig: encode.RandomBytes(32),
Order: &tanka.Order{
From: peer,
BaseID: baseID,
QuoteID: quoteID,
Sell: false,
Qty: 1000,
Rate: 1234,
LotSize: 2,
Nonce: 1,
Stamp: mustParse("Sun, 12 Dec 2024 12:23:00 UTC"),
},
Expiration: mustParse("Sun, 12 Jan 2025 12:23:00 UTC"),
Settled: 500,
}
lowsell := &tanka.OrderUpdate{
Sig: encode.RandomBytes(32),
Order: &tanka.Order{
From: peer,
BaseID: baseID,
QuoteID: quoteID,
Sell: true,
Qty: 1000,
Rate: 12345,
LotSize: 2,
Nonce: 2,
Stamp: mustParse("Sun, 12 Dec 2024 12:23:00 UTC"),
},
Expiration: mustParse("Sun, 12 Jan 2025 12:23:00 UTC"),
Settled: 500,
}
highsell := &tanka.OrderUpdate{
Sig: encode.RandomBytes(32),
Order: &tanka.Order{
From: peer,
BaseID: baseID,
QuoteID: quoteID,
Sell: true,
Qty: 1000,
Rate: 123456,
LotSize: 4,
Nonce: 3,
Stamp: mustParse("Sun, 12 Dec 2024 12:23:00 UTC"),
},
Expiration: mustParse("Sun, 12 Jan 2025 12:23:00 UTC"),
Settled: 500,
}
return []*tanka.OrderUpdate{lowbuy, highbuy, lowsell, highsell}
}

func TestOrderIDs(t *testing.T) {
ob := NewOrderBook()
for _, o := range testOrders() {
ob.AddUpdate(o)
}

ous := ob.OrderIDs()
if len(ous) != 4 {
t.Fatalf("wanted 4 but got %d orders", len(ous))
}
}

func TestOrders(t *testing.T) {
ob := NewOrderBook()
var oids [][32]byte
for _, o := range testOrders() {
ob.AddUpdate(o)
oids = append(oids, o.ID())
}
ous := ob.Orders(oids)
if len(ous) != 4 {
t.Fatalf("wanted 4 but got %d orders", len(oids))
}
}

func TestFindOrders(t *testing.T) {
ob := NewOrderBook()
tOrds := testOrders()
for _, o := range tOrds {
ob.AddUpdate(o)
}
yes, no := true, false

tests := []struct {
name string
filter *OrderFilter
wantOrderLen int
wantOrderIdx []int
}{{
name: "all orders",
filter: new(OrderFilter),
wantOrderLen: 4,
wantOrderIdx: []int{1, 0, 2, 3},
}, {
name: "sells",
filter: &OrderFilter{
IsSell: &yes,
},
wantOrderLen: 2,
wantOrderIdx: []int{2, 3},
}, {
name: "buys",
filter: &OrderFilter{
IsSell: &no,
},
wantOrderLen: 2,
wantOrderIdx: []int{1, 0},
}, {
name: "lot size over 2",
filter: &OrderFilter{
Check: func(ou *tanka.OrderUpdate) (ok, done bool) {
return ou.LotSize > 2, false
},
},
wantOrderLen: 2,
wantOrderIdx: []int{0, 3},
}, {
name: "lot size over 2 and sell",
filter: &OrderFilter{
IsSell: &yes,
Check: func(ou *tanka.OrderUpdate) (ok, done bool) {
return ou.LotSize > 2, false
},
},
wantOrderLen: 1,
wantOrderIdx: []int{3},
}, {
name: "buy done after one",
filter: &OrderFilter{
IsSell: &no,
Check: func() func(*tanka.OrderUpdate) (ok, done bool) {
var i int
return func(ou *tanka.OrderUpdate) (ok, done bool) {
defer func() { i++ }()
return true, i == 1
}
}(),
},
wantOrderLen: 1,
wantOrderIdx: []int{1},
}, {
name: "nil filter",
}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ous := ob.FindOrders(test.filter)
if len(ous) != test.wantOrderLen {
t.Fatalf("wanted %d but got %d orders", test.wantOrderLen, len(ous))
}
for i, idx := range test.wantOrderIdx {
if tOrds[idx].ID() != ous[i].ID() {
t.Fatalf("returned order at idx %d not equal to test order at %d", idx, i)
}
}
})
}
}
Loading

0 comments on commit e39c239

Please sign in to comment.