Skip to content

Commit

Permalink
Merge pull request lightninglabs#290 from halseth/matching-partial-mi…
Browse files Browse the repository at this point in the history
…n-units

matchmaker: remove orders below min units left from orderbook
  • Loading branch information
guggero authored Mar 12, 2021
2 parents 02cc8cc + aa1ade7 commit 78d8d36
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 27 deletions.
22 changes: 15 additions & 7 deletions venue/matching/matching_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
var emptyAcct [33]byte

type orderGenCfg struct {
numUnits orderT.SupplyUnit
numUnits []orderT.SupplyUnit
minUnitsMatch orderT.SupplyUnit
fixedRate uint32
duration uint32
Expand All @@ -30,6 +30,12 @@ type orderGenCfg struct {
type orderGenOption func(*orderGenCfg)

func staticUnitGen(numUnits orderT.SupplyUnit) orderGenOption {
return func(opt *orderGenCfg) {
opt.numUnits = []orderT.SupplyUnit{numUnits}
}
}

func oneOfUnitGen(numUnits ...orderT.SupplyUnit) orderGenOption {
return func(opt *orderGenCfg) {
opt.numUnits = numUnits
}
Expand Down Expand Up @@ -102,19 +108,21 @@ func staticAccountState(state account.State) orderGenOption {
}

func (o *orderGenCfg) supplyUnits(r *rand.Rand) orderT.SupplyUnit {
if o.numUnits != 0 {
return o.numUnits
if len(o.numUnits) > 0 {
return o.numUnits[r.Int()%len(o.numUnits)]
}

return orderT.SupplyUnit(r.Int31())
}

func (o *orderGenCfg) getMinUnitsMatch(r *rand.Rand) orderT.SupplyUnit { // nolint:unparam
func (o *orderGenCfg) getMinUnitsMatch(r *rand.Rand,
maxUnits orderT.SupplyUnit) orderT.SupplyUnit { // nolint:unparam

if o.minUnitsMatch != 0 {
return o.minUnitsMatch
}

return orderT.SupplyUnit(r.Int31())
return orderT.SupplyUnit(r.Int31())%maxUnits + 1
}

func (o *orderGenCfg) rate(r *rand.Rand) uint32 {
Expand Down Expand Up @@ -182,7 +190,7 @@ func genRandBid(r *rand.Rand, accts *acctFetcher, // nolint:dupl
acct := genRandAccount(r, genOptions...)

numUnits := genCfg.supplyUnits(r)
minUnitsMatch := genCfg.getMinUnitsMatch(r)
minUnitsMatch := genCfg.getMinUnitsMatch(r, numUnits)
b := &order.Bid{
Bid: orderT.Bid{
Kit: *orderT.NewKit(nonce),
Expand Down Expand Up @@ -224,7 +232,7 @@ func genRandAsk(r *rand.Rand, accts *acctFetcher, // nolint:dupl
acct := genRandAccount(r, genOptions...)

numUnits := genCfg.supplyUnits(r)
minUnitsMatch := genCfg.getMinUnitsMatch(r)
minUnitsMatch := genCfg.getMinUnitsMatch(r, numUnits)
a := &order.Ask{
Ask: orderT.Ask{
Kit: *orderT.NewKit(nonce),
Expand Down
23 changes: 19 additions & 4 deletions venue/matching/matchmaker.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,19 +301,31 @@ func filterAnomalies(matches []MatchedOrder,
for _, m := range matches {
ask := m.Details.Ask
bid := m.Details.Bid

skip := false
if ask.FixedRate > uint32(clearingPrice) {
log.Debugf("Filtered out ask %v (and matched bid %v) "+
"with fixed rate %v for clearing price %v",
ask.Nonce(), bid.Nonce(), ask.FixedRate,
clearingPrice)
continue
skip = true
}

if bid.FixedRate < uint32(clearingPrice) {
log.Debugf("Filtered out bid %v (and matched ask %v) "+
"with fixed rate %v for clearing price %v",
bid.Nonce(), ask.Nonce(), bid.FixedRate,
clearingPrice)
skip = true
}

// If we decided to filter out this match, add back the units
// to the ask and bid, as we use this for checking how much is
// lefter after this batch clear.
if skip {
filled := m.Details.Quote.UnitsMatched
ask.UnitsUnfulfilled += filled
bid.UnitsUnfulfilled += filled
continue
}

Expand Down Expand Up @@ -355,9 +367,12 @@ func (u *UniformPriceCallMarket) RemoveMatches(matches ...MatchedOrder) error {

o.UnitsUnfulfilled -= filled

// If no more units remain, it should not be added back
// to the order book.
if o.UnitsUnfulfilled == 0 {
// If no more units remain, or the remainder is less
// than the minimum match size, it should not be added
// back to the order book.
if o.UnitsUnfulfilled == 0 ||
o.UnitsUnfulfilled < o.MinUnitsMatch {

return false, nil
}
}
Expand Down
53 changes: 37 additions & 16 deletions venue/matching/matchmaker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ func TestMaybeClearClearingPriceConsistency(t *testing.T) { // nolint:gocyclo

acctDB, acctCacher, predicates := newAcctCacher()

n, y := 0, 0
y := 0
scenario := func(orders orderSet) bool {
// We'll start with making a new call market,
callMarket := NewUniformPriceCallMarket(
Expand Down Expand Up @@ -285,9 +285,8 @@ func TestMaybeClearClearingPriceConsistency(t *testing.T) { // nolint:gocyclo
acctCacher, dummyFilterChain, predicates,
)
if err != nil {
fmt.Println("clear error: ", err)
n++
return true
t.Logf("clear error: %v", err)
return false
}

// Indexes should not be mutated.
Expand Down Expand Up @@ -355,22 +354,35 @@ func TestMaybeClearClearingPriceConsistency(t *testing.T) { // nolint:gocyclo
return false
}

// If the order set was totally filled, then it
// shouldn't be found in current set of active orders.
if bid.UnitsUnfulfilled == 0 {
// If the order set was totally filled or the
// remaining units were less than its minimum match
// size, then it shouldn't be found in current set of
// active orders.
if bid.UnitsUnfulfilled == 0 ||
bid.UnitsUnfulfilled < bid.MinUnitsMatch {

if _, ok := callMarket.bidIndex[bid.Nonce()]; ok {
t.Logf("bid found in active orders " +
"after total fulfill")
t.Logf("bid %v found in active orders "+
"with unfulfilled units=%v "+
"and min units=%v", bid.Nonce(),
bid.UnitsUnfulfilled,
bid.MinUnitsMatch)
return false
}

bidNonce := bid.Nonce()
fullyConsumedOrders[bidNonce] = struct{}{}
}
if ask.UnitsUnfulfilled == 0 {
if ask.UnitsUnfulfilled == 0 ||
ask.UnitsUnfulfilled < ask.MinUnitsMatch {

if _, ok := callMarket.askIndex[ask.Nonce()]; ok {
t.Logf("ask found in active orders " +
"after total fulfill")
t.Logf("ask %v found in active orders "+
"with unfulfilled units=%v "+
"and min units=%v", ask.Nonce(),
ask.UnitsUnfulfilled,
ask.MinUnitsMatch)

return false
}

Expand Down Expand Up @@ -430,7 +442,12 @@ func TestMaybeClearClearingPriceConsistency(t *testing.T) { // nolint:gocyclo
// orders, as we'll get to test both the case where no
// orders match, or only a sub-set of the orders match
// and the intermediate state needs to be updated.
randOrderSet := genRandOrderSet(r, acctDB, 1000)
randOrderSet := genRandOrderSet(
r, acctDB,
1000,
staticDurationGen(testDuration),
oneOfUnitGen(50, 100, 200, 400, 777, 1000),
)

// We'll also supplements this set of orders with an
// pair of orders that we know will be totally filled.
Expand All @@ -439,7 +456,7 @@ func TestMaybeClearClearingPriceConsistency(t *testing.T) { // nolint:gocyclo
1,
staticRateGen(1000),
staticUnitGen(1000),
staticDurationGen(2),
staticDurationGen(testDuration),
staticMinUnitsMatchGen(1),
)

Expand All @@ -457,7 +474,7 @@ func TestMaybeClearClearingPriceConsistency(t *testing.T) { // nolint:gocyclo
t.Fatalf("clearing price consistency scenario failed: %v", err)
}

t.Logf("Total number of scenarios run: %v (%v positive, %v negative)", n+y, y, n)
t.Logf("Total number of scenarios run: %v (%v positive)", y, y)
}

// TestMaybeClearFilterFeeRates tests that orders with a max batch feerate
Expand Down Expand Up @@ -605,7 +622,11 @@ func TestMaybeClearClearingPriceInvariant(t *testing.T) {
// When generating the random set below, we'll cap the
// number of orders on both sides to ensure the test
// completes in a timely manner.
randOrderSet := genRandOrderSet(r, acctDB, 1000)
randOrderSet := genRandOrderSet(
r, acctDB,
1000,
staticDurationGen(testDuration),
)

v[0] = reflect.ValueOf(randOrderSet)
},
Expand Down

0 comments on commit 78d8d36

Please sign in to comment.