Skip to content

Commit

Permalink
test fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanlott committed Nov 1, 2023
1 parent 3f03523 commit f82fb7c
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 59 deletions.
83 changes: 61 additions & 22 deletions pkg/orderbook/orderbook.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ package orderbook

import (
"context"
"log"
"sort"
"time"

"github.com/dylanlott/orderbook/pkg/accounts"
)

var delay time.Duration = time.Second * 1

// Order is a struct for representing a simple order in the books.
type Order struct {
ID string
Expand All @@ -34,6 +32,7 @@ type Match struct {
Price uint64 // at what price was each unit purchased by the buyer from the seller
Quantity uint64 // how many units were transferred from seller to buyer
Total uint64 // total = price * quantity
History []*Match
}

// Orderbook is the core interface of the library.
Expand All @@ -52,11 +51,12 @@ func Run(
accounts accounts.AccountManager,
in chan *Order,
out chan *Match,
fills chan []*Order,
status chan []*Order,
) {
// NB: buy and sell are not accessible anywhere but here for safety.
var buy, sell []*Order
handleMatches(ctx, accounts, buy, sell, in, out, status)
handleMatches(ctx, accounts, buy, sell, in, out, fills, status)
}

// handleMatches is a blocking function that handles the matches.
Expand All @@ -67,27 +67,29 @@ func handleMatches(
buy, sell []*Order,
in chan *Order,
out chan *Match,
fillsCh chan []*Order,
status chan []*Order,
) {
for {
// feed off the orders that accumulated since the last loop
for o := range in {
if o.Side == "buy" {
buy = append(buy, o)
} else {
sell = append(sell, o)
}
// feed off the orders that accumulated since the last loop
for o := range in {
if o.Side == "buy" {
buy = append(buy, o)
} else {
sell = append(sell, o)
}
// create the orderlist for state updates
orderlist := []*Order{}
orderlist = append(orderlist, buy...)
orderlist = append(orderlist, sell...)
status <- orderlist

// generate a list of matches and output them
matches := MatchOrders(accts, buy, sell)
matches, fills := MatchOrders(accts, buy, sell)
for _, match := range matches {
out <- &match
log.Printf("[MATCH DETECTED]: %+v", match)
out <- match
}
if len(fills) > 0 {
fillsCh <- fills
}
}
}
Expand All @@ -99,7 +101,7 @@ func handleMatches(
// matching sell options are exhausted,
// * When it exhausts all f it ratchets up the buy index again and finds all matching
// orders.
func MatchOrders(accts accounts.AccountManager, buyOrders []*Order, sellOrders []*Order) []Match {
func MatchOrders(accts accounts.AccountManager, buyOrders []*Order, sellOrders []*Order) ([]*Match, []*Order) {
sort.Slice(buyOrders, func(i, j int) bool {
return buyOrders[i].Price > buyOrders[j].Price
})
Expand All @@ -110,7 +112,8 @@ func MatchOrders(accts accounts.AccountManager, buyOrders []*Order, sellOrders [
// Initialize the index variables
buyIndex := 0
sellIndex := 0
var matches []Match
var matches []*Match
var fills []*Order

// Loop until there are no more Sell orders left
for sellIndex < len(sellOrders) {
Expand All @@ -119,12 +122,48 @@ func MatchOrders(accts accounts.AccountManager, buyOrders []*Order, sellOrders [
// Create a match and add it to the matches
sell := sellOrders[sellIndex]
buy := buyOrders[buyIndex]
m := Match{
Buy: buy,
Sell: sell,
Price: sell.Price,

available := sell.Open - sell.Filled
wanted := buy.Open - sell.Filled

var taken uint64 = 0

switch {
case available > wanted:
taken = wanted
sell.Filled += taken
buy.Filled += taken
case available < wanted:
taken = available
sell.Filled += taken
buy.Filled += taken
default: // availabel == wanted
taken = wanted
sell.Filled += taken
buy.Filled += taken
}

m := &Match{
Buy: buy,
Sell: sell,
Price: sell.Price,
Quantity: taken,
Total: taken * sell.Price,
}
matches = append(matches, m)

sell.History = append(sell.History, *m)
buy.History = append(buy.History, *m)

if sell.Filled == sell.Open {
sellOrders = append(sellOrders[:sellIndex], sellOrders[sellIndex+1:]...)
fills = append(fills, sell)
}
if buy.Filled == buy.Open {
buyOrders = append(buyOrders[:buyIndex], buyOrders[buyIndex+1:]...)
fills = append(fills, buy)
}

// Increment the Sell order index
sellIndex++
} else {
Expand All @@ -139,5 +178,5 @@ func MatchOrders(accts accounts.AccountManager, buyOrders []*Order, sellOrders [
}

// Return the list of filled orders
return matches
return matches, fills
}
92 changes: 55 additions & 37 deletions pkg/orderbook/orderbook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,70 @@ import (
"time"

"github.com/dylanlott/orderbook/pkg/accounts"
"github.com/stretchr/testify/require"

"github.com/brianvoe/gofakeit/v6"
)

var numTestOrders = 1000
var numTestAccounts = 10

func TestRunLoad(t *testing.T) {
in := make(chan *Order, 1)
out := make(chan *Match, 1)
status := make(chan []*Order, 1)
fills := make(chan []*Order)

// Generate default random accounts for testing
accts, ids := newTestAccountManager(t, numTestAccounts)

// Start the server
go Run(context.Background(), accts, in, out, fills, status)

// Consume the status updates
go func() {
for state := range status {
_ = state
}
}()

// Consume fills
go func() {
for fill := range fills {
t.Logf("[FILL]: %+v", fill)
}
}()

// Consume matches
go func() {
for match := range out {
var _ = match
}
}()

// Generate test orders
buy, sell := newTestOrders(numTestOrders)

for _, o := range buy {
o.AccountID = gofakeit.RandomString(ids)
in <- o
}
for _, o := range sell {
o.AccountID = gofakeit.RandomString(ids) // assign to a random account last of all
in <- o
}
}

func TestMatchOrders(t *testing.T) {
buy, sell := newTestOrders(1000)
_ = MatchOrders(&accounts.InMemoryManager{}, buy, sell)
matches, fills := MatchOrders(&accounts.InMemoryManager{}, buy, sell)
require.NotEmpty(t, matches)
require.NotEmpty(t, fills)
}

func BenchmarkMatchOrders(b *testing.B) {
buy, sell := newTestOrders(b.N)
_ = MatchOrders(&accounts.InMemoryManager{}, buy, sell)
_, _ = MatchOrders(&accounts.InMemoryManager{}, buy, sell)
}

func BenchmarkAttemptFill(b *testing.B) {
Expand Down Expand Up @@ -74,7 +126,7 @@ func newTestAccountManager(t *testing.T, num int) (accounts.AccountManager, []st
// and maxOpen, an equal chance to be owned by foo or bar,
// and with an even chance of being a buy or sell order.
func newTestOrders(count int) (buyOrders, sellOrders []*Order) {
log.Printf("count %d", count)
log.Printf("generating %d new test orders...", count)
rand.Seed(time.Now().UnixNano())

var minPrice, maxPrice = 100, 10_000
Expand Down Expand Up @@ -128,37 +180,3 @@ func newRandOrder(id, account string) Order {

return o
}

var numTestOrders = 10_000_000
var numTestAccounts = 1_000_000

func TestRunLoad(t *testing.T) {
in := make(chan *Order, 1)
out := make(chan *Match, 1)
status := make(chan []*Order, 1)

// Generate default random accounts for testing
accts, ids := newTestAccountManager(t, numTestAccounts)

// Start the server
go Run(context.Background(), accts, in, out, status)

// Consume the status updates
go func() {
for e := range status {
t.Logf("\ncurrent orders: %v\n", e)
}
}()

// Generate test orders
buy, sell := newTestOrders(numTestOrders)

for _, o := range buy {
o.AccountID = gofakeit.RandomString(ids)
in <- o
}
for _, o := range sell {
o.AccountID = gofakeit.RandomString(ids) // assign to a random account last of all
in <- o
}
}

0 comments on commit f82fb7c

Please sign in to comment.