fix some of the match functions and add more tests

This commit is contained in:
Callum Waters
2022-10-17 16:06:30 +03:00
parent bbf1169aea
commit 920c5ad813
2 changed files with 154 additions and 32 deletions

View File

@@ -5,7 +5,7 @@ import (
)
type Market struct {
pair Pair // i.e. EUR/USD (a market is bidirectional)
pair Pair // i.e. EUR/USD (a market is bidirectional)
askOrders *AskOrders // i.e. buying EUR for USD
lowestAsk float64
bidOrders *BidOrders // i.e. selling EUR for USD or buying USD for EUR
@@ -13,27 +13,35 @@ type Market struct {
}
func NewMarket(p Pair) *Market {
return &Market{pair: p}
askOrders := make(AskOrders, 0)
bidOrders := make(BidOrders, 0)
return &Market{pair: p, askOrders: &askOrders, bidOrders: &bidOrders}
}
func (m *Market) AddBid(b OrderBid) {
func (m *Market) AddBid(b *OrderBid) {
heap.Push(m.bidOrders, b)
if b.MaxPrice > m.highestBid {
m.highestBid = b.MaxPrice
}
}
func (m *Market) AddAsk(a OrderAsk) {
func (m *Market) AddAsk(a *OrderAsk) {
heap.Push(m.askOrders, a)
if a.AskPrice < m.lowestAsk {
if a.AskPrice < m.lowestAsk || m.lowestAsk == 0 {
m.lowestAsk = a.AskPrice
}
}
// Match takes the set of bids and asks and matches them together.
// A bid matches an ask when the MaxPrice is greater than the AskPrice
// and the MaxQuantity is greater than the quantity.
// and the MaxQuantity is greater than the quantity.
func (m *Market) Match() *TradeSet {
// if one side doesn't have any orders than there is nothing to match
// and we return early
if m.askOrders.Len() == 0 || m.bidOrders.Len() == 0 {
return nil
}
if m.highestBid < m.lowestAsk {
// no orders match, we return early.
return nil
@@ -44,13 +52,27 @@ func (m *Market) Match() *TradeSet {
asks := make([]*OrderAsk, 0)
// get all the bids that are greater than the lowest ask. In order from heighest bid to lowest bid
for bid := heap.Pop(m.bidOrders).(*OrderBid); bid.MaxPrice >= m.lowestAsk; bid = heap.Pop(m.bidOrders).(*OrderBid) {
bids = append(bids, bid)
for m.bidOrders.Len() > 0 {
bid := heap.Pop(m.bidOrders).(*OrderBid)
if bid.MaxPrice < m.lowestAsk {
// we've reached the limit, push the bid back and break the loop
heap.Push(m.bidOrders, bid)
break
} else {
bids = append(bids, bid)
}
}
// get all the asks that are lower than the highest bid in the bids set. Ordered from lowest to highest ask
for ask := heap.Pop(m.askOrders).(*OrderAsk); ask.AskPrice <= bids[0].MaxPrice; ask = heap.Pop(m.askOrders).(*OrderAsk) {
asks = append(asks, ask)
for m.askOrders.Len() > 0 {
ask := heap.Pop(m.askOrders).(*OrderAsk)
if ask.AskPrice > bids[0].MaxPrice {
// the ask price is greater than the highest bid; push the ask back and break theh loop
heap.Push(m.askOrders, ask)
break
} else {
asks = append(asks, ask)
}
}
// this is to keep track of the index of the bids that have been matched
@@ -63,7 +85,12 @@ OUTER_LOOP:
ask := asks[i]
// start with the highest bid and increment down since we're more likely to find a match
for j := 0 ; j < len(bids); j++ {
for j := len(bids) - 1; j >= 0; j-- {
if _, ok := reserved[j]; ok {
// skip over the bids that have already been reserved
continue
}
bid := bids[j]
if bid.MaxPrice >= ask.AskPrice {
if bid.MaxQuantity >= ask.Quantity {
@@ -87,12 +114,19 @@ OUTER_LOOP:
heap.Push(m.askOrders, ask)
}
// if all available asks were matched then
// we never have the opportunity to update the lowest ask.
// Now we reset it to 0
if m.askOrders.Len() == 0 {
m.lowestAsk = 0
}
// add back the unmatched bids to the heap so they can be matched again in a later round.
// We also neeed to recalculate the new highest bid. First we tackle an edge case whereby all
// selected bids were matched. In this case we grab the next highest and set that as the new
// highest bid
m.highestBid = 0
if len(reserved) == len(bids) {
if len(reserved) == len(bids) && m.bidOrders.Len() > 0 {
newHighestBid := heap.Pop(m.bidOrders).(*OrderBid)
m.highestBid = newHighestBid.MaxPrice
heap.Push(m.bidOrders, newHighestBid)
@@ -106,6 +140,9 @@ OUTER_LOOP:
}
}
if len(t.MatchedOrders) == 0 {
return nil
}
return t
}

View File

@@ -9,41 +9,70 @@ import (
var testPair = orderbook.Pair{BuyersDenomination: "ATOM", SellersDenomination: "USD"}
func testBid(price, quantity float64) *orderbook.OrderBid {
return &orderbook.OrderBid{
MaxPrice: price,
MaxQuantity: quantity,
}
}
func testAsk(price, quantity float64) *orderbook.OrderAsk {
return &orderbook.OrderAsk{
AskPrice: price,
Quantity: quantity,
}
}
func TestTrackLowestAndHighestPrices(t *testing.T) {
market := orderbook.NewMarket(testPair)
require.Zero(t, market.LowestAsk())
require.Zero(t, market.HighestBid())
market.AddBid(orderbook.OrderBid{MaxPrice: 100})
require.Equal(t, 100, market.HighestBid())
market.AddBid(testBid(100, 10))
require.EqualValues(t, 100, market.HighestBid())
market.AddAsk(orderbook.OrderAsk{AskPrice: 50})
require.Equal(t, 50, market.LowestAsk())
market.AddAsk(testAsk(50, 10))
require.EqualValues(t, 50, market.LowestAsk())
market.AddAsk(orderbook.OrderAsk{AskPrice: 30})
require.Equal(t, 30, market.LowestAsk())
market.AddAsk(testAsk(30, 10))
require.EqualValues(t, 30, market.LowestAsk())
market.AddAsk(orderbook.OrderAsk{AskPrice: 40})
require.Equal(t, 30, market.LowestAsk())
market.AddAsk(testAsk(40, 10))
require.EqualValues(t, 30, market.LowestAsk())
}
func TestSimpleOrderMatching(t *testing.T) {
testcases := []struct {
bid orderbook.OrderBid
ask orderbook.OrderAsk
bid *orderbook.OrderBid
ask *orderbook.OrderAsk
match bool
}{
{
bid: orderbook.OrderBid{
MaxPrice: 50,
MaxQuantity: 10,
},
ask: orderbook.OrderAsk{
AskPrice: 50,
Quantity: 10,
},
bid: testBid(50, 10),
ask: testAsk(50, 10),
match: true,
},
{
bid: testBid(60, 10),
ask: testAsk(50, 10),
match: true,
},
{
bid: testBid(50, 10),
ask: testAsk(60, 10),
match: false,
},
{
bid: testBid(50, 5),
ask: testAsk(40, 10),
match: false,
},
{
bid: testBid(50, 15),
ask: testAsk(40, 10),
match: true,
},
}
for idx, tc := range testcases {
@@ -51,6 +80,62 @@ func TestSimpleOrderMatching(t *testing.T) {
market.AddAsk(tc.ask)
market.AddBid(tc.bid)
resp := market.Match()
require.Equal(t, tc.match, len(resp.MatchedOrders) == 1, idx)
if tc.match {
require.Len(t, resp.MatchedOrders, 1, idx)
} else {
require.Nil(t, resp)
}
}
}
}
func TestMultiOrderMatching(t *testing.T) {
testcases := []struct {
bids []*orderbook.OrderBid
asks []*orderbook.OrderAsk
expected []*orderbook.MatchedOrder
expectedLowestAsk float64
expectedHighestBid float64
}{
{
bids: []*orderbook.OrderBid{
testBid(50, 20),
testBid(40, 10),
testBid(30, 15),
},
asks: []*orderbook.OrderAsk{
testAsk(30, 15),
testAsk(30, 5),
},
expected: []*orderbook.MatchedOrder{
{
OrderAsk: testAsk(30, 5),
OrderBid: testBid(30, 15),
},
{
OrderAsk: testAsk(30, 15),
OrderBid: testBid(50, 20),
},
},
expectedLowestAsk: 0,
expectedHighestBid: 40,
},
}
for idx, tc := range testcases {
market := orderbook.NewMarket(testPair)
for _, ask := range tc.asks {
market.AddAsk(ask)
}
for _, bid := range tc.bids {
market.AddBid(bid)
}
resp := market.Match()
if len(tc.expected) == 0 {
require.Nil(t, resp, idx)
} else {
require.Equal(t, tc.expected, resp.MatchedOrders, idx)
}
require.EqualValues(t, tc.expectedLowestAsk, market.LowestAsk(), idx)
require.EqualValues(t, tc.expectedHighestBid, market.HighestBid(), idx)
}
}