Skip to content

Commit

Permalink
optimize MaxOrder
Browse files Browse the repository at this point in the history
  • Loading branch information
buck54321 committed Jun 24, 2022
1 parent 7d30691 commit 2d1bfd7
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 60 deletions.
106 changes: 83 additions & 23 deletions client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1268,19 +1268,54 @@ func (btc *baseWallet) maxOrder(lotSize, feeSuggestion uint64, nfo *dex.Asset) (
}
// Start by attempting max lots with a basic fee.
basicFee := nfo.SwapSize * nfo.MaxFeeRate
lots := avail / (lotSize + basicFee)
for lots > 0 {
est, _, _, err := btc.estimateSwap(lots, lotSize, feeSuggestion, utxos, nfo, btc.useSplitTx, 1.0)
// The only failure mode of estimateSwap -> btc.fund is when there is
// not enough funds, so if an error is encountered, count down the lots
// and repeat until we have enough.
if err != nil {
lots--
continue
}
return utxos, est, nil
guess := avail / (lotSize + basicFee)

oneTooMany := dex.BinarySearch(&binaryLotSearcher{
low: 1,
high: guess * 10,
initialGuess: guess,
eval: func(lots uint64) bool {
if lots == 0 {
return false
}
_, _, _, err := btc.estimateSwap(lots, lotSize, feeSuggestion, utxos, nfo, btc.useSplitTx, 1.0)
// The only failure mode of estimateSwap -> btc.fund is when there is
// not enough funds, so if an error is encountered, we requested too many lots.
// To satisfy the binary searcher want to return true when lots is too high, and
// we'll get the first result for which the result is true.
return err != nil
},
})
if oneTooMany == dex.BinarySearchFailed {
return nil, nil, fmt.Errorf("our high limit was too low? high = %d, avail = %d, lotSize = %d, basicFee = %d",
guess*2, avail, lotSize, basicFee)
}
return utxos, &asset.SwapEstimate{}, nil

lots := uint64(oneTooMany) - 1
if lots == 0 {
return utxos, &asset.SwapEstimate{}, nil
}
// The binary search was successful, so there should be no way to error
// here, but we'll return the error anyway.
est, _, _, err = btc.estimateSwap(lots, lotSize, feeSuggestion, utxos, nfo, btc.useSplitTx, 1.0)
return utxos, est, err
}

// binaryLotSearcher implements dex.BinaryIndex to facilitate max order
// calculations.
type binaryLotSearcher struct {
low, high, initialGuess uint64
eval func(i uint64) bool
}

// Extents returns the search parameters.
func (s *binaryLotSearcher) Extents() (low, high, initialGuess int) {
return int(s.low), int(s.high), int(s.initialGuess)
}

// Eval runs the eval function for the specified index.
func (s *binaryLotSearcher) Eval(i int) bool {
return s.eval(uint64(i))
}

// sizeUnit returns the short form of the unit used to measure size, either
Expand Down Expand Up @@ -1755,18 +1790,24 @@ func fund(utxos []*compositeUTXO, enough func(uint64, uint64) bool) (
if len(okUTXOs) == 0 {
return false
}
// On each loop, find the smallest UTXO that is enough.
for _, txout := range okUTXOs {
if isEnoughWith(txout) {
addUTXO(txout)
return true
}

// Check if the largest output is too small.
lastUTXO := okUTXOs[len(okUTXOs)-1]
if !isEnoughWith(lastUTXO) {
addUTXO(lastUTXO)
okUTXOs = okUTXOs[0 : len(okUTXOs)-1]
continue
}
// No single UTXO was large enough. Add the largest (the last
// output) and continue.
addUTXO(okUTXOs[len(okUTXOs)-1])
// Pop the utxo.
okUTXOs = okUTXOs[:len(okUTXOs)-1]

// We only need one then. Find it.
idx := dex.BinarySearch(&binaryUTXOSearcher{
utxos: okUTXOs,
enough: isEnoughWith,
})
// No need to check idx == -1. We already verified that the last
// utxo passes above.
addUTXO(okUTXOs[idx])
return true
}
}

Expand All @@ -1781,6 +1822,25 @@ func fund(utxos []*compositeUTXO, enough func(uint64, uint64) bool) (
return
}

// binaryUTXOSearcher searches a ascending sorted list of utxos for the smallest
// one that satisfies the enough condition.
// Satisfies dex.BinaryIndex.
type binaryUTXOSearcher struct {
utxos []*compositeUTXO
enough func(unspent *compositeUTXO) bool
}

// Extents returns the search parameters. For the binaryUTXOSearcher, the
// initial guess is the center of the slice.
func (s *binaryUTXOSearcher) Extents() (low, high, initial int) {
return 0, len(s.utxos) - 1, len(s.utxos) / 2
}

// Eval evaluates the utxo at index i against the enough filter.
func (s *binaryUTXOSearcher) Eval(i int) bool {
return s.enough(s.utxos[i])
}

// split will send a split transaction and return the sized output. If the
// split transaction is determined to be un-economical, it will not be sent,
// there is no error, and the input coins will be returned unmodified, but an
Expand Down
109 changes: 84 additions & 25 deletions client/asset/dcr/dcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -828,20 +828,51 @@ func (dcr *ExchangeWallet) maxOrder(lotSize, feeSuggestion uint64, nfo *dex.Asse

// Start by attempting max lots with a basic fee.
basicFee := nfo.SwapSize * nfo.MaxFeeRate
lots := avail / (lotSize + basicFee)
for lots > 0 {
est, _, _, err := dcr.estimateSwap(lots, lotSize, feeSuggestion, utxos, nfo, dcr.useSplitTx, 1.0)
// The only failure mode of estimateSwap -> dcr.fund is when there is
// not enough funds, so if an error is encountered, count down the lots
// and repeat until we have enough.
if err != nil {
lots--
continue
}
return utxos, est, nil
guess := avail / (lotSize + basicFee)

oneTooMany := dex.BinarySearch(&binaryLotSearcher{
low: 1,
high: guess * 10,
initialGuess: guess,
eval: func(lots uint64) bool {
_, _, _, err := dcr.estimateSwap(lots, lotSize, feeSuggestion, utxos, nfo, dcr.useSplitTx, 1.0)
// The only failure mode of estimateSwap -> btc.fund is when there is
// not enough funds, so if an error is encountered, we requested too many lots.
// To satisfy the binary searcher want to return true when lots is too high, and
// we'll get the first result for which the result is true.
return err != nil
},
})
if oneTooMany == dex.BinarySearchFailed {
return nil, nil, fmt.Errorf("our high limit was too low? high = %d, avail = %d, lotSize = %d, basicFee = %d",
guess*2, avail, lotSize, basicFee)
}

lots := uint64(oneTooMany) - 1
if lots == 0 {
return utxos, &asset.SwapEstimate{}, nil
}
// The binary search was successful, so there should be no way to error
// here, but we'll return the error anyway.
est, _, _, err = dcr.estimateSwap(lots, lotSize, feeSuggestion, utxos, nfo, dcr.useSplitTx, 1.0)
return utxos, est, err
}

// binaryLotSearcher implements dex.BinaryIndex to facilitate max order
// calculations.
type binaryLotSearcher struct {
low, high, initialGuess uint64
eval func(i uint64) bool
}

return nil, &asset.SwapEstimate{}, nil
// Extents returns the search parameters.
func (s *binaryLotSearcher) Extents() (low, high, initialGuess int) {
return int(s.low), int(s.high), int(s.initialGuess)
}

// Eval runs the eval function for the specified index.
func (s *binaryLotSearcher) Eval(i int) bool {
return s.eval(uint64(i))
}

// estimateSwap prepares an *asset.SwapEstimate.
Expand Down Expand Up @@ -1331,6 +1362,10 @@ func (dcr *ExchangeWallet) tryFund(utxos []*compositeUTXO, enough func(sum uint6
return nil
}

isEnoughWith := func(utxo *compositeUTXO) bool {
return enough(sum, size, utxo)
}

tryUTXOs := func(minconf int64) (ok bool, err error) {
sum, size = 0, 0
coins, spents, redeemScripts = nil, nil, nil
Expand All @@ -1347,22 +1382,26 @@ func (dcr *ExchangeWallet) tryFund(utxos []*compositeUTXO, enough func(sum uint6
if len(okUTXOs) == 0 {
return false, nil
}
// On each loop, find the smallest UTXO that is enough.
for _, txout := range okUTXOs {
if enough(sum, size, txout) {
if err = addUTXO(txout); err != nil {
return false, err
}
return true, nil
}

// Check if the largest output is too small.
lastUTXO := okUTXOs[len(okUTXOs)-1]
if !isEnoughWith(lastUTXO) {
addUTXO(lastUTXO)
okUTXOs = okUTXOs[0 : len(okUTXOs)-1]
continue
}
// No single UTXO was large enough. Add the largest (the last
// output) and continue.
if err = addUTXO(okUTXOs[len(okUTXOs)-1]); err != nil {

// We only need one then. Find it.
idx := dex.BinarySearch(&binaryUTXOSearcher{
utxos: okUTXOs,
enough: isEnoughWith,
})
// No need to check idx == -1. We already verified that the last
// utxo passes above.
if err = addUTXO(okUTXOs[idx]); err != nil {
return false, err
}
// Pop the utxo.
okUTXOs = okUTXOs[:len(okUTXOs)-1]
return true, nil
}
}

Expand All @@ -1371,6 +1410,7 @@ func (dcr *ExchangeWallet) tryFund(utxos []*compositeUTXO, enough func(sum uint6
if err != nil {
return 0, 0, nil, nil, nil, err
}

// Fallback to allowing 0-conf outputs.
if !ok {
ok, err = tryUTXOs(0)
Expand All @@ -1386,6 +1426,25 @@ func (dcr *ExchangeWallet) tryFund(utxos []*compositeUTXO, enough func(sum uint6
return
}

// binaryUTXOSearcher searches a ascending sorted list of utxos for the smallest
// one that satisfies the eval condition.
// Satisfies dex.BinaryIndex.
type binaryUTXOSearcher struct {
utxos []*compositeUTXO
enough func(*compositeUTXO) bool
}

// Extents returns the search parameters. For the binaryUTXOSearcher, the
// initial guess is the center of the slice.
func (s *binaryUTXOSearcher) Extents() (low, high, initial int) {
return 0, len(s.utxos) - 1, len(s.utxos) / 2
}

// Eval evaluates the utxo at index i against the enough filter.
func (s *binaryUTXOSearcher) Eval(i int) bool {
return s.enough(s.utxos[i])
}

// split will send a split transaction and return the sized output. If the
// split transaction is determined to be un-economical, it will not be sent,
// there is no error, and the input coins will be returned unmodified, but an
Expand Down
17 changes: 11 additions & 6 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -3744,8 +3744,10 @@ func (c *Core) MaxBuy(host string, base, quote uint32, rate uint64) (*MaxOrderEs
}

// MaxSell is the maximum-sized *OrderEstimate for a sell order on the specified
// market.
func (c *Core) MaxSell(host string, base, quote uint32) (*MaxOrderEstimate, error) {
// market. Redemption estimates are based on the provided rate. If a rate of
// zero is specified, the orderbook's mid-gap rate will be used for the
// estimate.
func (c *Core) MaxSell(host string, base, quote uint32, rate uint64) (*MaxOrderEstimate, error) {
baseAsset, quoteAsset, baseWallet, quoteWallet, err := c.marketWallets(host, base, quote)
if err != nil {
return nil, err
Expand Down Expand Up @@ -3791,11 +3793,14 @@ func (c *Core) MaxSell(host string, base, quote uint32) (*MaxOrderEstimate, erro
return nil, fmt.Errorf("%s wallet MaxOrder error: %v", unbip(base), err)
}

midGap, err := book.MidGap()
if err != nil {
return nil, fmt.Errorf("error calculating market rate for %s at %s: %v", mktID, host, err)
if rate == 0 {
rate, err = book.MidGap()
if err != nil {
return nil, fmt.Errorf("error calculating market rate for %s at %s: %v", mktID, host, err)
}
}
lotSize = calc.BaseToQuote(midGap, lotSize)

lotSize = calc.BaseToQuote(rate, lotSize)

preRedeem, err := quoteWallet.PreRedeem(&asset.PreRedeemForm{
LotSize: lotSize,
Expand Down
3 changes: 2 additions & 1 deletion client/webserver/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -922,11 +922,12 @@ func (s *WebServer) apiMaxSell(w http.ResponseWriter, r *http.Request) {
Host string `json:"host"`
Base uint32 `json:"base"`
Quote uint32 `json:"quote"`
Rate uint64 `json:"rate"`
}{}
if !readPost(w, r, form) {
return
}
maxSell, err := s.core.MaxSell(form.Host, form.Base, form.Quote)
maxSell, err := s.core.MaxSell(form.Host, form.Base, form.Quote, form.Rate)
if err != nil {
s.writeAPIError(w, fmt.Errorf("max order estimation error: %w", err))
return
Expand Down
2 changes: 1 addition & 1 deletion client/webserver/live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ func (c *TCore) MaxBuy(host string, base, quote uint32, rate uint64) (*core.MaxO
}, nil
}

func (c *TCore) MaxSell(host string, base, quote uint32) (*core.MaxOrderEstimate, error) {
func (c *TCore) MaxSell(host string, base, quote uint32, rate uint64) (*core.MaxOrderEstimate, error) {
mktID, _ := dex.MarketName(base, quote)
lotSize := tExchanges[host].Markets[mktID].LotSize
midGap, maxQty := getMarketStats(mktID)
Expand Down
5 changes: 3 additions & 2 deletions client/webserver/site/src/js/markets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,8 @@ export default class MarketsPage extends BasePage {
return
}
// We only fetch pre-sell once per balance update, so don't delay.
this.scheduleMaxEstimate('/api/maxsell', {}, 0, (res: MaxSell) => {
const rate = this.isLimit() ? this.adjustedRate() : 0
this.scheduleMaxEstimate('/api/maxsell', { rate }, 0, (res: MaxSell) => {
mkt.maxSell = res.maxSell
mkt.sellBalance = baseWallet.balance.available
this.setMaxOrder(res.maxSell.swap)
Expand All @@ -912,7 +913,7 @@ export default class MarketsPage extends BasePage {
// 0 delay for first fetch after balance update or market change, otherwise
// meter these at 1 / sec.
const delay = mkt.maxBuys ? 1000 : 0
this.scheduleMaxEstimate('/api/maxbuy', { rate: rate }, delay, (res: MaxBuy) => {
this.scheduleMaxEstimate('/api/maxbuy', { rate }, delay, (res: MaxBuy) => {
mkt.maxBuys[rate] = res.maxBuy
mkt.buyBalance = app().assets[mkt.quote.id].wallet.balance.available
this.setMaxOrder(res.maxBuy.swap)
Expand Down
2 changes: 1 addition & 1 deletion client/webserver/webserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ type clientCore interface {
Orders(*core.OrderFilter) ([]*core.Order, error)
Order(oid dex.Bytes) (*core.Order, error)
MaxBuy(host string, base, quote uint32, rate uint64) (*core.MaxOrderEstimate, error)
MaxSell(host string, base, quote uint32) (*core.MaxOrderEstimate, error)
MaxSell(host string, base, quote uint32, rate uint64) (*core.MaxOrderEstimate, error)
AccountExport(pw []byte, host string) (*core.Account, error)
AccountImport(pw []byte, account core.Account) error
AccountDisable(pw []byte, host string) error
Expand Down
2 changes: 1 addition & 1 deletion client/webserver/webserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func (c *TCore) Order(oid dex.Bytes) (*core.Order, error) { return nil, n
func (c *TCore) MaxBuy(host string, base, quote uint32, rate uint64) (*core.MaxOrderEstimate, error) {
return nil, nil
}
func (c *TCore) MaxSell(host string, base, quote uint32) (*core.MaxOrderEstimate, error) {
func (c *TCore) MaxSell(host string, base, quote uint32, rate uint64) (*core.MaxOrderEstimate, error) {
return nil, nil
}
func (c *TCore) PreOrder(*core.TradeForm) (*core.OrderEstimate, error) {
Expand Down
Loading

0 comments on commit 2d1bfd7

Please sign in to comment.