-
Notifications
You must be signed in to change notification settings - Fork 105
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6a2efe4
commit e39c239
Showing
3 changed files
with
397 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.