Skip to content

Commit

Permalink
updates
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanlott committed May 4, 2023
1 parent 231ce3b commit b24e035
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 116 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# orderbook

> an experimental order matching exchange engine in Go.
## Development

`make test` runs the test suite.
`make test` runs the test suite.

Five iterations of this app are located in the `pkg/` directory, named `v1` through `v5` respectively.

The final implementation that applies the design points learned from the previous iterations lives in `pkg/orderbook`.

## Architecture
Expand All @@ -13,12 +16,12 @@ The two main packages are `accounts` and `orderbook`. Accounts holds an interfac

Orders are handled in the following process

1. OpWrites feed an order into the orderbook.
2. The book inserts it into the tree and calls attemptFill on it in a goroutine.
1. OpWrites feed an order into the orderbook.
2. The book inserts it into the tree and calls attemptFill on it in a goroutine.
3. It generates matches until it's filled. Matches are fed into the Match channel.
4. The match channel processes the payment (buy and sell side) and passes it on the fill channel.

The fills channel is the only way to receive an update on an order. The orderbook is intentionally abstracts away the actual books, both sell and buy side, such that nothing above it can access or change those values.
The fills channel is the only way to receive an update on an order. The orderbook is intentionally abstracts away the actual books, both sell and buy side, such that nothing above it can access or change those values.

### Persistence

Expand All @@ -27,4 +30,3 @@ Bolt or BadgerDB are being explored currently for storing orders in a simple on-
## Golem CLI

Golem is the CLI client written in Viper that starts the orderbook. Located in `cmd/golem` it currently has only the root command which starts the server. Viper handles the configuration of the application by loading in the `-config` file path as well as a `$HOME/.golem.yml` config file.

4 changes: 2 additions & 2 deletions cmd/golem/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ func main() {

func motd() {
fmt.Printf(`
$$\
$$ |
$$\
$$ |
$$$$$$\ $$$$$$\ $$ | $$$$$$\ $$$$$$\$$$$\
$$ __$$\ $$ __$$\ $$ |$$ __$$\ $$ _$$ _$$\
$$ / $$ |$$ / $$ |$$ |$$$$$$$$ |$$ / $$ / $$ |
Expand Down
6 changes: 6 additions & 0 deletions pkg/orderbook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ This package manages concurrent access to its buy and sell order lists, aka the
This package started as a direct copy of `pkg/v5` but this will be the continued implementation now, since our initial exploration is finished.

## Order fills

Order fills are carried out in the attemptFill function. The hard problem of order filling is proper price traversal. This package uses a binary tree to solve that problem.

`Node` is the main type used by binary tree. It populates a binary tree for easy traversal of prices in an up and down fashion. Each `Node` has a `Price` and a `[]*Order` list.

Matches are made by lining up the two buy and sell sides of the book and trying to fill as many items from the opposite side (the "book order") until the order being filled ("the fill order") is complete.
13 changes: 9 additions & 4 deletions pkg/orderbook/orderbook.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Package orderbook is an order-matching engine written in
// Go as part of an experiment of iteration on designs
// in a non-trivial domain.
package orderbook

import (
Expand Down Expand Up @@ -134,8 +137,6 @@ func attemptFill(
// match to sell
low := book.sell.FindMin()
if len(low.Orders) == 0 {
removed := book.buy.Remove(low.Price)
fmt.Printf("removed from the binary tree ### removed.Price: %v\n", removed.Price)
continue
}

Expand Down Expand Up @@ -246,8 +247,12 @@ func attemptFill(
}
}

// An alternative approach to order matching that relies on sorting
// two opposing slices of Orders.
// Matcher func allows a different matching algorithm to be swapped out
// in the orderbook.
type Matcher func(buy, sell []Order) []Order

// MatchOrders is an alternative approach to order matching that
// works by aligning two opposing sorted slices of Orders.
func MatchOrders(buyOrders []Order, sellOrders []Order) []Order {
// Sort the orders by price
sort.Slice(buyOrders, func(i, j int) bool {
Expand Down
57 changes: 17 additions & 40 deletions pkg/orderbook/tree.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
package orderbook

import "fmt"
import (
"fmt"
)

// Node is the atomic unit of the price index
// for the Orderbook. Multiple Nodes are bound
// together in a binary tree. Each Node has a
// left and right node. Nodes without any orders
// are removed from the index.
type Node struct {
Price uint64
Orders []*Order
Left *Node
Right *Node
}

// NewNode is a constructor function for returning
// a new default Node.
func NewNode(price uint64) *Node {
return &Node{
Price: price,
Orders: make([]*Order, 0),
}
}

// Insert adds an Order into the tree and returns
// the Node that it was inserted into.
func (n *Node) Insert(order *Order) *Node {
if n == nil {
n = NewNode(order.Price)
Expand All @@ -33,7 +44,7 @@ func (n *Node) Insert(order *Order) *Node {
return n
}

// Removes an Order from a Node's list of Orders.
// RemoveOrder removes an Order from a Node's list of Orders.
// * Must be called on the correct node.
func (n *Node) RemoveOrder(orderID string) bool {
for i, order := range n.Orders {
Expand All @@ -45,10 +56,6 @@ func (n *Node) RemoveOrder(orderID string) bool {
return false
}

func (n *Node) AddOrder(order *Order) {
n.Orders = append(n.Orders, order)
}

// Find returns the node for a given price.
func (n *Node) Find(price uint64) *Node {
if n == nil {
Expand All @@ -64,6 +71,7 @@ func (n *Node) Find(price uint64) *Node {
}
}

// List returns a list of all the Orders in the tree.
func (n *Node) List() []*Order {
if n == nil {
return nil
Expand All @@ -77,6 +85,7 @@ func (n *Node) List() []*Order {
return orders
}

// Print prints the contents of the tree to stdout
func (n *Node) Print() {
if n == nil {
return
Expand All @@ -86,40 +95,7 @@ func (n *Node) Print() {
n.Right.Print()
}

func (n *Node) Remove(value uint64) *Node {
if n == nil {
return nil
}
if value < n.Price {
n.Left = n.Left.Remove(value)
return n
} else if value > n.Price {
n.Right = n.Right.Remove(value)
return n
}
// If value == n.Value.Price, then we need to remove this node.
// There are three cases to consider:
// 1. The node has no children.
// 2. The node has one child.
// 3. The node has two children.
if n.Left == nil && n.Right == nil {
return nil
}
if n.Left == nil {
return n.Right
}
if n.Right == nil {
return n.Left
}
// If we get here, the node has two children.
// We can replace the node with its in-order successor
// (i.e., the smallest node in its right subtree).
successor := n.Right.FindMin()
n.Price = successor.Price
n.Right = n.Right.Remove(successor.Price)
return n
}

// FindMin returns the lowest price in the tree.
func (n *Node) FindMin() *Node {
if n == nil {
return nil
Expand All @@ -130,6 +106,7 @@ func (n *Node) FindMin() *Node {
return n.Left.FindMin()
}

// FindMax returns the highest price node in the tree
func (n *Node) FindMax() *Node {
if n == nil {
return nil
Expand Down
65 changes: 0 additions & 65 deletions pkg/orderbook/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ func TestList(t *testing.T) {
is := is.New(t)
root := NewNode(10)
seedRootTree(root)

orderlist := root.List()
is.Equal(len(orderlist), 6)
is.Equal(orderlist[0].Price, uint64(5))
Expand All @@ -60,67 +59,3 @@ func seedRootTree(root *Node) {
root.Insert(order5)
root.Insert(order6)
}

func TestNode_Remove(t *testing.T) {
var five = &Node{
Price: 5,
Right: nil,
Left: nil,
}
var one = &Node{
Price: 1,
Right: nil,
Left: nil,
}
type fields struct {
Price uint64
Orders []*Order
Left *Node
Right *Node
}
type args struct {
value uint64
}
tests := []struct {
name string
fields fields
args args
want *Node
}{
{
name: "should remove a childless node",
fields: fields{
Price: 2,
Left: one,
Right: five,
},
args: args{
value: 5,
},
want: &Node{
Price: 2,
Left: one,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
n := &Node{
Price: tt.fields.Price,
Orders: tt.fields.Orders,
Left: tt.fields.Left,
Right: tt.fields.Right,
}
got := n.Remove(tt.args.value)
if got.Price != tt.want.Price {
t.Fail()
}
if tt.want.Right != nil && got.Right.Price != tt.want.Right.Price {
t.Fail()
}
if tt.want.Left != nil && got.Left.Price != tt.want.Left.Price {
t.Fail()
}
})
}
}

0 comments on commit b24e035

Please sign in to comment.