decred.org/dcrdex@v1.0.5/client/core/helpers.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package core
     5  
     6  import (
     7  	"fmt"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"decred.org/dcrdex/dex"
    12  	"decred.org/dcrdex/dex/calc"
    13  	"decred.org/dcrdex/dex/order"
    14  )
    15  
    16  // OrderReader wraps a Order and provides methods for info display.
    17  // Whenever possible, add an OrderReader methods rather than a template func.
    18  type OrderReader struct {
    19  	*Order
    20  	BaseUnitInfo        dex.UnitInfo
    21  	BaseFeeUnitInfo     dex.UnitInfo
    22  	BaseFeeAssetSymbol  string
    23  	QuoteUnitInfo       dex.UnitInfo
    24  	QuoteFeeUnitInfo    dex.UnitInfo
    25  	QuoteFeeAssetSymbol string
    26  }
    27  
    28  // FromSymbol is the symbol of the asset which will be sent.
    29  func (ord *OrderReader) FromSymbol() string {
    30  	if ord.Sell {
    31  		return ord.BaseSymbol
    32  	}
    33  	return ord.QuoteSymbol
    34  }
    35  
    36  // FromSymbol is the symbol of the asset which will be received.
    37  func (ord *OrderReader) ToSymbol() string {
    38  	if ord.Sell {
    39  		return ord.QuoteSymbol
    40  	}
    41  	return ord.BaseSymbol
    42  }
    43  
    44  // FromTicker if the conventional unit for the from asset.
    45  func (ord *OrderReader) FromTicker() string {
    46  	if ord.Sell {
    47  		return ord.BaseUnitInfo.Conventional.Unit
    48  	}
    49  	return ord.QuoteUnitInfo.Conventional.Unit
    50  }
    51  
    52  // ToTicker if the conventional unit for the to asset.
    53  func (ord *OrderReader) ToTicker() string {
    54  	if ord.Sell {
    55  		return ord.QuoteUnitInfo.Conventional.Unit
    56  	}
    57  	return ord.BaseUnitInfo.Conventional.Unit
    58  }
    59  
    60  // FromFeeSymbol is the symbol of the asset used to pay swap fees.
    61  func (ord *OrderReader) FromFeeSymbol() string {
    62  	if ord.Sell {
    63  		return ord.BaseFeeAssetSymbol
    64  	}
    65  	return ord.QuoteFeeAssetSymbol
    66  }
    67  
    68  // ToFeeSymbol is the symbol of the asset used to pay redeem fees.
    69  func (ord *OrderReader) ToFeeSymbol() string {
    70  	if ord.Sell {
    71  		return ord.QuoteFeeAssetSymbol
    72  	}
    73  	return ord.BaseFeeAssetSymbol
    74  }
    75  
    76  // BaseFeeSymbol is the symbol of the asset used to pay the base asset's
    77  // network fees.
    78  func (ord *OrderReader) BaseFeeSymbol() string {
    79  	return ord.BaseFeeAssetSymbol
    80  }
    81  
    82  // QuoteFeeSymbol is the symbol of the asset used to pay the quote asset's
    83  // network fees.
    84  func (ord *OrderReader) QuoteFeeSymbol() string {
    85  	return ord.QuoteFeeAssetSymbol
    86  }
    87  
    88  // FromID is the asset ID of the asset which will be sent.
    89  func (ord *OrderReader) FromID() uint32 {
    90  	if ord.Sell {
    91  		return ord.BaseID
    92  	}
    93  	return ord.QuoteID
    94  }
    95  
    96  // FromID is the asset ID of the asset which will be received.
    97  func (ord *OrderReader) ToID() uint32 {
    98  	if ord.Sell {
    99  		return ord.QuoteID
   100  	}
   101  	return ord.BaseID
   102  }
   103  
   104  // Cancelable will be true for standing limit orders in status epoch or booked.
   105  func (ord *OrderReader) Cancelable() bool {
   106  	return ord.Type == order.LimitOrderType &&
   107  		ord.TimeInForce == order.StandingTiF &&
   108  		ord.Status <= order.OrderStatusBooked
   109  }
   110  
   111  // TypeString combines the order type and side into a single string.
   112  func (ord *OrderReader) TypeString() string {
   113  	s := "market"
   114  	if ord.Type == order.LimitOrderType {
   115  		s = "limit"
   116  		if ord.TimeInForce == order.ImmediateTiF {
   117  			s += " (i)"
   118  		}
   119  	}
   120  	if ord.Sell {
   121  		s += " sell"
   122  	} else {
   123  		s += " buy"
   124  	}
   125  	return s
   126  }
   127  
   128  // BaseQtyString formats the order quantity in units of the base asset.
   129  func (ord *OrderReader) BaseQtyString() string {
   130  	return formatQty(ord.Qty, ord.BaseUnitInfo)
   131  }
   132  
   133  // OfferString formats the order quantity in units of the outgoing asset,
   134  // performing a conversion if necessary.
   135  func (ord *OrderReader) OfferString() string {
   136  	if ord.Sell {
   137  		return formatQty(ord.Qty, ord.BaseUnitInfo)
   138  	} else if ord.Type == order.MarketOrderType {
   139  		return formatQty(ord.Qty, ord.QuoteUnitInfo)
   140  	}
   141  	return formatQty(calc.BaseToQuote(ord.Rate, ord.Qty), ord.QuoteUnitInfo)
   142  }
   143  
   144  // AskString will print the minimum received amount for the filled limit order.
   145  // The actual settled amount may be more.
   146  func (ord *OrderReader) AskString() string {
   147  	if ord.Type == order.MarketOrderType {
   148  		return "market rate"
   149  	}
   150  	if ord.Sell {
   151  		return formatQty(calc.BaseToQuote(ord.Rate, ord.Qty), ord.QuoteUnitInfo)
   152  	}
   153  	return formatQty(ord.Qty, ord.BaseUnitInfo)
   154  }
   155  
   156  // IsMarketBuy is true if this is a market buy order.
   157  func (ord *OrderReader) IsMarketBuy() bool {
   158  	return ord.Type == order.MarketOrderType && !ord.Sell
   159  }
   160  
   161  // IsMarketOrder is true if this is a market order.
   162  func (ord *OrderReader) IsMarketOrder() bool {
   163  	return ord.Type == order.MarketOrderType
   164  }
   165  
   166  // SettledFrom is the sum settled outgoing asset.
   167  func (ord *OrderReader) SettledFrom() string {
   168  	if ord.Sell {
   169  		return formatQty(ord.sumFrom(settledFilter), ord.BaseUnitInfo)
   170  	}
   171  	return formatQty(ord.sumFrom(settledFilter), ord.QuoteUnitInfo)
   172  }
   173  
   174  // SettledTo is the sum settled of incoming asset.
   175  func (ord *OrderReader) SettledTo() string {
   176  	if ord.Sell {
   177  		return formatQty(ord.sumTo(settledFilter), ord.QuoteUnitInfo)
   178  	}
   179  	return formatQty(ord.sumTo(settledFilter), ord.BaseUnitInfo)
   180  }
   181  
   182  // SettledPercent is the percent of the order which has completed settlement.
   183  func (ord *OrderReader) SettledPercent() string {
   184  	if ord.Type == order.CancelOrderType {
   185  		return ""
   186  	}
   187  	return ord.percent(settledFilter)
   188  }
   189  
   190  // FilledFrom is the sum filled in units of the outgoing asset. Excludes cancel
   191  // matches.
   192  func (ord *OrderReader) FilledFrom() string {
   193  	if ord.Sell {
   194  		return formatQty(ord.sumFrom(filledNonCancelFilter), ord.BaseUnitInfo)
   195  	}
   196  	return formatQty(ord.sumFrom(filledNonCancelFilter), ord.QuoteUnitInfo)
   197  }
   198  
   199  // FilledTo is the sum filled in units of the incoming asset. Excludes cancel
   200  // matches.
   201  func (ord *OrderReader) FilledTo() string {
   202  	if ord.Sell {
   203  		return formatQty(ord.sumTo(filledNonCancelFilter), ord.QuoteUnitInfo)
   204  	}
   205  	return formatQty(ord.sumTo(filledNonCancelFilter), ord.BaseUnitInfo)
   206  }
   207  
   208  // FilledPercent is the percent of the order that has filled, without percent
   209  // sign. Excludes cancel matches.
   210  func (ord *OrderReader) FilledPercent() string {
   211  	if ord.Type == order.CancelOrderType {
   212  		return ""
   213  	}
   214  	return ord.percent(filledNonCancelFilter)
   215  }
   216  
   217  // SideString is "sell" for sell orders, "buy" for buy orders, and "" for
   218  // cancels.
   219  func (ord *OrderReader) SideString() string {
   220  	if ord.Type == order.CancelOrderType {
   221  		return ""
   222  	}
   223  	if ord.Sell {
   224  		return "sell"
   225  	}
   226  	return "buy"
   227  }
   228  
   229  func (ord *OrderReader) percent(filter func(match *Match) bool) string {
   230  	var sum uint64
   231  	if ord.Sell || ord.IsMarketBuy() {
   232  		sum = ord.sumFrom(filter)
   233  	} else {
   234  		sum = ord.sumTo(filter)
   235  	}
   236  	return strconv.FormatFloat(float64(sum)/float64(ord.Qty)*100, 'f', 1, 64)
   237  }
   238  
   239  // sumFrom will sum the match quantities in units of the outgoing asset.
   240  func (ord *OrderReader) sumFrom(filter func(match *Match) bool) uint64 {
   241  	var v uint64
   242  	add := func(rate, qty uint64) {
   243  		if ord.Sell {
   244  			v += qty
   245  		} else {
   246  			v += calc.BaseToQuote(rate, qty)
   247  		}
   248  	}
   249  	for _, match := range ord.Matches {
   250  		if filter(match) {
   251  			add(match.Rate, match.Qty)
   252  		}
   253  	}
   254  	return v
   255  }
   256  
   257  // sumTo will sum the match quantities in units of the incoming asset.
   258  func (ord *OrderReader) sumTo(filter func(match *Match) bool) uint64 {
   259  	var v uint64
   260  	add := func(rate, qty uint64) {
   261  		if ord.Sell {
   262  			v += calc.BaseToQuote(rate, qty)
   263  		} else {
   264  			v += qty
   265  		}
   266  	}
   267  	for _, match := range ord.Matches {
   268  		if filter(match) {
   269  			add(match.Rate, match.Qty)
   270  		}
   271  	}
   272  	return v
   273  }
   274  
   275  // hasActiveMatches will be true if order has any matches that still require some
   276  // actions to complete settlement.
   277  func (ord *OrderReader) hasActiveMatches() bool {
   278  	for _, match := range ord.Matches {
   279  		if match.Active {
   280  			return true
   281  		}
   282  	}
   283  	return false
   284  }
   285  
   286  // StatusString is the order status.
   287  //
   288  // IMPORTANT: we have similar function in JS for UI, it must match this one exactly,
   289  // when updating make sure to update both!
   290  func (ord *OrderReader) StatusString() string {
   291  	isLive := ord.hasActiveMatches()
   292  	switch ord.Status {
   293  	case order.OrderStatusUnknown:
   294  		return "unknown"
   295  	case order.OrderStatusEpoch:
   296  		return "epoch"
   297  	case order.OrderStatusBooked:
   298  		if ord.Cancelling {
   299  			return "cancelling"
   300  		}
   301  		if isLive {
   302  			return "booked/settling"
   303  		}
   304  		return "booked"
   305  	case order.OrderStatusExecuted:
   306  		if isLive {
   307  			return "settling"
   308  		}
   309  		if ord.Filled == 0 && ord.Type != order.CancelOrderType {
   310  			return "no match"
   311  		}
   312  		return "executed"
   313  	case order.OrderStatusCanceled:
   314  		if isLive {
   315  			return "canceled/settling"
   316  		}
   317  		return "canceled"
   318  	case order.OrderStatusRevoked:
   319  		if isLive {
   320  			return "revoked/settling"
   321  		}
   322  		return "revoked"
   323  	}
   324  	return "unknown"
   325  }
   326  
   327  // SimpleRateString is the formatted match rate.
   328  func (ord *OrderReader) SimpleRateString() string {
   329  	if ord.Type == order.MarketOrderType {
   330  		return ord.AverageRateString()
   331  	}
   332  	return ord.formatRate(ord.Rate)
   333  }
   334  
   335  // RateString is a formatted rate with units.
   336  func (ord *OrderReader) RateString() string {
   337  	rateStr := ord.formatRate(ord.Rate)
   338  	if ord.Type == order.MarketOrderType {
   339  		nMatches := len(ord.Matches)
   340  		if nMatches == 0 {
   341  			return "market" // "market" is better than 0 BTC/ETH ?
   342  		}
   343  		rateStr = ord.AverageRateString()
   344  		if len(ord.Matches) > 1 {
   345  			rateStr = "~ " + rateStr // "~" only makes sense if the order has more than one match.
   346  		}
   347  	}
   348  	return fmt.Sprintf("%s %s/%s", rateStr, ord.QuoteUnitInfo.Conventional.Unit, ord.BaseUnitInfo.Conventional.Unit)
   349  }
   350  
   351  // AverageRateString returns a formatting string containing the average rate of
   352  // the matches that have been filled in an order.
   353  func (ord *OrderReader) AverageRateString() string {
   354  	if len(ord.Matches) == 0 {
   355  		return "0"
   356  	}
   357  	var baseQty, rateProduct uint64
   358  	for _, match := range ord.Matches {
   359  		baseQty += match.Qty
   360  		rateProduct += match.Rate * match.Qty // order ~ 1e16
   361  	}
   362  	return ord.formatRate(rateProduct / baseQty)
   363  }
   364  
   365  // SwapFeesString is a formatted string of the paid swap fees.
   366  func (ord *OrderReader) SwapFeesString() string {
   367  	if ord.Sell {
   368  		return formatQty(ord.FeesPaid.Swap, ord.BaseFeeUnitInfo)
   369  	}
   370  	return formatQty(ord.FeesPaid.Swap, ord.QuoteFeeUnitInfo)
   371  }
   372  
   373  // RedemptionFeesString is a formatted string of the paid redemption fees.
   374  func (ord *OrderReader) RedemptionFeesString() string {
   375  	if ord.Sell {
   376  		return formatQty(ord.FeesPaid.Redemption, ord.QuoteFeeUnitInfo)
   377  	}
   378  	return formatQty(ord.FeesPaid.Redemption, ord.BaseFeeUnitInfo)
   379  }
   380  
   381  // BaseAssetFees is a formatted string of the fees paid in the base asset.
   382  func (ord *OrderReader) BaseAssetFees() string {
   383  	if ord.Sell {
   384  		return ord.SwapFeesString()
   385  	}
   386  	return ord.RedemptionFeesString()
   387  }
   388  
   389  // QuoteAssetFees is a formatted string of the fees paid in the quote asset.
   390  func (ord *OrderReader) QuoteAssetFees() string {
   391  	if ord.Sell {
   392  		return ord.RedemptionFeesString()
   393  	}
   394  	return ord.SwapFeesString()
   395  }
   396  
   397  func (ord *OrderReader) FundingCoinIDs() []string {
   398  	ids := make([]string, 0, len(ord.FundingCoins))
   399  	for i := range ord.FundingCoins {
   400  		ids = append(ids, ord.FundingCoins[i].StringID)
   401  	}
   402  	return ids
   403  }
   404  
   405  // formatRate formats the specified rate as a conventional rate with trailing
   406  // zeros trimmed.
   407  func (ord *OrderReader) formatRate(msgRate uint64) string {
   408  	r := calc.ConventionalRate(msgRate, ord.BaseUnitInfo, ord.QuoteUnitInfo)
   409  	return trimTrailingZeros(strconv.FormatFloat(r, 'f', 8, 64))
   410  }
   411  
   412  // formatQty formats the quantity as a conventional string and trims trailing
   413  // zeros.
   414  func formatQty(qty uint64, unitInfo dex.UnitInfo) string {
   415  	return trimTrailingZeros(unitInfo.ConventionalString(qty))
   416  }
   417  
   418  // trimTrailingZeros trims trailing decimal zeros of a formatted float string.
   419  // The number is assumed to be formatted as a float with non-zero decimal
   420  // precision, i.e. there should be a decimal point in there.
   421  func trimTrailingZeros(s string) string {
   422  	return strings.TrimRight(strings.TrimRight(s, "0"), ".")
   423  }
   424  
   425  func settledFilter(match *Match) bool {
   426  	if match.IsCancel {
   427  		return false
   428  	}
   429  	return (match.Side == order.Taker && match.Status >= order.MatchComplete) ||
   430  		(match.Side == order.Maker && (match.Status >= order.MakerRedeemed))
   431  }
   432  
   433  func settlingFilter(match *Match) bool {
   434  	return (match.Side == order.Taker && match.Status < order.MatchComplete) ||
   435  		(match.Side == order.Maker && match.Status < order.MakerRedeemed)
   436  }
   437  
   438  func filledNonCancelFilter(match *Match) bool {
   439  	return !match.IsCancel
   440  }