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 }