Skip to content

Commit

Permalink
client/core,app: Fix rate/volume decimal precision
Browse files Browse the repository at this point in the history
This adds fields to the core `Market` type about how many decimal
places are required to display the rate and volume for a market in
conventional units. These are used on the UI to display the rates
and volumes properly.
  • Loading branch information
martonp committed Mar 7, 2023
1 parent d8f3e90 commit 8117269
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 37 deletions.
57 changes: 46 additions & 11 deletions client/core/core.go
Expand Up @@ -299,6 +299,39 @@ func (dc *dexConnection) coreMarket(mktName string) *Market {
return mkt
}

// convRateNumDecimals returns the number of decimal places to use to display
// a market's volume in conventional units.
func convVolumeNumDecimals(lotSize uint64, baseUnitInfo *dex.UnitInfo) uint16 {
baseConversionFactor := float64(baseUnitInfo.Conventional.ConversionFactor)
conventionalLots := float64(lotSize) / baseConversionFactor

var d uint16
for ; d < 10; d++ {
if math.Floor(conventionalLots) == conventionalLots {
return d
}
conventionalLots *= 10
}

// no need to display more than 10 decimals
return d
}

// convRateNumDecimals returns the number of decimal places to use to display
// a market's rate in conventional units.
func convRateNumDecimals(rateStep uint64, baseUnitInfo, quoteUnitInfo *dex.UnitInfo) uint16 {
baseConversionFactor := float64(baseUnitInfo.Conventional.ConversionFactor)
quoteConversionFactor := float64(quoteUnitInfo.Conventional.ConversionFactor)
atomToConv := baseConversionFactor / quoteConversionFactor
minConventionalRate := float64(rateStep) * atomToConv
logMinRate := uint16(math.Log10(minConventionalRate))
// RateEncodingFactor is 1e8, so there will not be more than 8 decimals
if logMinRate >= 8 {
return 0
}
return 8 - logMinRate
}

func coreMarketFromMsgMarket(dc *dexConnection, msgMkt *msgjson.Market) *Market {
// The presence of the asset for every market was already verified when the
// dexConnection was created in connectDEX.
Expand All @@ -309,17 +342,19 @@ func coreMarketFromMsgMarket(dc *dexConnection, msgMkt *msgjson.Market) *Market
bconv, qconv := base.UnitInfo.Conventional.ConversionFactor, quote.UnitInfo.Conventional.ConversionFactor

mkt := &Market{
Name: msgMkt.Name,
BaseID: base.ID,
BaseSymbol: base.Symbol,
QuoteID: quote.ID,
QuoteSymbol: quote.Symbol,
LotSize: msgMkt.LotSize,
RateStep: msgMkt.RateStep,
EpochLen: msgMkt.EpochLen,
StartEpoch: msgMkt.StartEpoch,
MarketBuyBuffer: msgMkt.MarketBuyBuffer,
AtomToConv: float64(qconv) / float64(bconv),
Name: msgMkt.Name,
BaseID: base.ID,
BaseSymbol: base.Symbol,
QuoteID: quote.ID,
QuoteSymbol: quote.Symbol,
LotSize: msgMkt.LotSize,
RateStep: msgMkt.RateStep,
EpochLen: msgMkt.EpochLen,
StartEpoch: msgMkt.StartEpoch,
MarketBuyBuffer: msgMkt.MarketBuyBuffer,
AtomToConv: float64(bconv) / float64(qconv),
ConvRateNumDecimals: convRateNumDecimals(msgMkt.RateStep, &base.UnitInfo, &quote.UnitInfo),
ConvVolumeNumDecimals: convVolumeNumDecimals(msgMkt.LotSize, &base.UnitInfo),
}

trades, inFlight := dc.marketTrades(mkt.marketName())
Expand Down
79 changes: 79 additions & 0 deletions client/core/core_test.go
Expand Up @@ -10438,3 +10438,82 @@ func TestUpdateFeesPaid(t *testing.T) {
}
}
}

func TestConvVolumeNumDecimals(t *testing.T) {
tests := []struct {
lotSize uint64
unitInfo *dex.UnitInfo
want uint16
}{
{
lotSize: 2e6,
unitInfo: &dex.UnitInfo{
Conventional: dex.Denomination{
ConversionFactor: 1e8,
},
},
want: 2,
},
{
lotSize: 1e9,
unitInfo: &dex.UnitInfo{
Conventional: dex.Denomination{
ConversionFactor: 1e8,
},
},
want: 0,
},
}

for i, test := range tests {
got := convVolumeNumDecimals(test.lotSize, test.unitInfo)
if got != test.want {
t.Errorf("test %d: want %d, got %d", i, test.want, got)
}
}
}

func TestConvRateNumDecimals(t *testing.T) {
tests := []struct {
rateStep uint64
baseUnitInfo *dex.UnitInfo
quoteUnitInfo *dex.UnitInfo
want uint16
}{
{
rateStep: 1e6,
baseUnitInfo: &dex.UnitInfo{
Conventional: dex.Denomination{
ConversionFactor: 1e6,
},
},
quoteUnitInfo: &dex.UnitInfo{
Conventional: dex.Denomination{
ConversionFactor: 1e6,
},
},
want: 2,
},
{
rateStep: 1e6,
baseUnitInfo: &dex.UnitInfo{
Conventional: dex.Denomination{
ConversionFactor: 1e9,
},
},
quoteUnitInfo: &dex.UnitInfo{
Conventional: dex.Denomination{
ConversionFactor: 1e6,
},
},
want: 0,
},
}

for i, test := range tests {
got := convRateNumDecimals(test.rateStep, test.baseUnitInfo, test.quoteUnitInfo)
if got != test.want {
t.Errorf("test %d: want %d, got %d", i, test.want, got)
}
}
}
8 changes: 8 additions & 0 deletions client/core/types.go
Expand Up @@ -507,6 +507,14 @@ type Market struct {
// with a TemporaryID to match with a notification once asynchronous order
// submission is complete.
InFlightOrders []*InFlightOrder `json:"inflight"`
// ConvRateNumDecimals is the maximum amount of decimal places possible
// to display the rate in a conventional units. This is calculated here
// to avoid javascript's floating point precision issues.
ConvRateNumDecimals uint16 `json:"convRateNumDecimals"`
// ConvVolumeNumDecimals is the maximum amount of decimal places possible
// to display the volume in a conventional units. This is calculated here
// to avoid javascript's floating point precision issues.
ConvVolumeNumDecimals uint16 `json:"convVolumeNumDecimals"`
}

// BaseContractLocked is the amount of base asset locked in un-redeemed
Expand Down
30 changes: 16 additions & 14 deletions client/webserver/site/src/js/markets.ts
Expand Up @@ -572,7 +572,7 @@ export default class MarketsPage extends BasePage {
setMarketDetails(s.tmpl, mkt)
if (mkt.spot) {
const bconv = xc.assets[mkt.baseid].unitInfo.conventional.conversionFactor
s.tmpl.volume.textContent = fourSigFigs(mkt.spot.vol24 / bconv)
s.tmpl.volume.textContent = withNumDecimals(mkt.spot.vol24 / bconv, mkt.convVolumeNumDecimals)
setPriceAndChange(s.tmpl, xc, mkt)
}
}
Expand Down Expand Up @@ -600,8 +600,8 @@ export default class MarketsPage extends BasePage {

const qconv = app().unitInfo(this.market.cfg.quoteid, this.market.dex).conventional.conversionFactor
for (const s of this.stats) {
s.tmpl.high.textContent = high > 0 ? fourSigFigs(high / qconv) : '-'
s.tmpl.low.textContent = low > 0 ? fourSigFigs(low / qconv) : '-'
s.tmpl.high.textContent = high > 0 ? withNumDecimals(high / qconv, this.market.cfg.convRateNumDecimals) : '-'
s.tmpl.low.textContent = low > 0 ? withNumDecimals(low / qconv, this.market.cfg.convRateNumDecimals) : '-'
}
}

Expand Down Expand Up @@ -1264,8 +1264,8 @@ export default class MarketsPage extends BasePage {
details.side.textContent = header.side.textContent = OrderUtil.sellString(ord)
details.side.classList.add(ord.sell ? 'sellcolor' : 'buycolor')
header.side.classList.add(ord.sell ? 'sellcolor' : 'buycolor')
details.qty.textContent = header.qty.textContent = fourSigFigs(ord.qty / this.market.baseUnitInfo.conventional.conversionFactor)
details.rate.textContent = fourSigFigs(ord.rate / this.market.rateConversionFactor)
details.qty.textContent = header.qty.textContent = withNumDecimals(ord.qty / this.market.baseUnitInfo.conventional.conversionFactor, this.market.cfg.convVolumeNumDecimals)
details.rate.textContent = withNumDecimals(ord.rate / this.market.rateConversionFactor, this.market.cfg.convRateNumDecimals)
header.baseSymbol.textContent = ord.baseSymbol.toUpperCase()
details.type.textContent = OrderUtil.typeString(ord)
this.updateMetaOrder(mord)
Expand Down Expand Up @@ -3065,16 +3065,18 @@ function sortedMarkets (): ExchangeMarket[] {
return mkts
}

const FourSigFigs = new Intl.NumberFormat((navigator.languages as string[]), {
minimumSignificantDigits: 4,
maximumSignificantDigits: 4
})

const intFormatter = new Intl.NumberFormat((navigator.languages as string[]))

function fourSigFigs (v: number): string {
if (v < 100) return FourSigFigs.format(v)
return intFormatter.format(Math.round(v))
function withNumDecimals (v: number, digits: number) : string {
if (v > 100) {
return intFormatter.format(Math.round(v))
}

const formatter = new Intl.NumberFormat((navigator.languages as string[]), {
maximumFractionDigits: digits,
minimumFractionDigits: digits
})
return formatter.format(v)
}

function setMarketDetails (tmpl: Record<string, PageElement>, mkt: Market) {
Expand All @@ -3087,7 +3089,7 @@ function setMarketDetails (tmpl: Record<string, PageElement>, mkt: Market) {

function setPriceAndChange (tmpl: Record<string, PageElement>, xc: Exchange, mkt: Market) {
if (!mkt.spot) return
tmpl.price.textContent = fourSigFigs(app().conventionalRate(mkt.baseid, mkt.quoteid, mkt.spot.rate, xc))
tmpl.price.textContent = withNumDecimals(app().conventionalRate(mkt.baseid, mkt.quoteid, mkt.spot.rate, xc), mkt.convRateNumDecimals)
const sign = mkt.spot.change24 > 0 ? '+' : ''
tmpl.change.classList.add(mkt.spot.change24 >= 0 ? 'buycolor' : 'sellcolor')
tmpl.change.textContent = `${sign}${(mkt.spot.change24 * 100).toFixed(1)}%`
Expand Down
2 changes: 2 additions & 0 deletions client/webserver/site/src/js/registry.ts
Expand Up @@ -66,6 +66,8 @@ export interface Market {
spot: Spot | undefined
atomToConv: number
inflight: InFlightOrder[]
convRateNumDecimals: number
convVolumeNumDecimals: number
}

export interface InFlightOrder extends Order {
Expand Down
30 changes: 18 additions & 12 deletions client/webserver/site/src/js/wallets.ts
Expand Up @@ -712,11 +712,10 @@ export default class WalletsPage extends BasePage {
if (mkt.baseid === assetID || mkt.quoteid === assetID) markets.push([xc.host, mkt])
}
}

const conversionFactor = app().unitInfo(assetID).conventional.conversionFactor
const spotVolume = (assetID: number, mkt: Market): number => {
const spot = mkt.spot
if (!spot) return 0
const conversionFactor = app().unitInfo(assetID).conventional.conversionFactor
const volume = assetID === mkt.baseid ? spot.vol24 : spot.vol24 * spot.rate / OrderUtil.RateEncodingFactor
return volume / conversionFactor
}
Expand All @@ -743,10 +742,15 @@ export default class WalletsPage extends BasePage {

if (spot) {
const convRate = app().conventionalRate(baseid, quoteid, spot.rate, exchanges[host])
tmpl.price.textContent = fourSigFigs(convRate)
tmpl.price.textContent = withNumDecimals(convRate, mkt.convRateNumDecimals)
tmpl.priceQuoteUnit.textContent = quotesymbol.toUpperCase()
tmpl.priceBaseUnit.textContent = basesymbol.toUpperCase()
tmpl.volume.textContent = fourSigFigs(spotVolume(assetID, mkt))
let volumeNumDecimals = mkt.convVolumeNumDecimals
if (assetID !== baseid) {
const maxNumDecimals = Math.round(Math.log10(conversionFactor))
volumeNumDecimals = Math.min(maxNumDecimals, 4)

Check failure on line 751 in client/webserver/site/src/js/wallets.ts

View workflow job for this annotation

GitHub Actions / Build JS (16.x)

Multiple spaces found before 'Math'

Check failure on line 751 in client/webserver/site/src/js/wallets.ts

View workflow job for this annotation

GitHub Actions / Build JS (18.x)

Multiple spaces found before 'Math'
}
tmpl.volume.textContent = withNumDecimals(spotVolume(assetID, mkt), volumeNumDecimals)
tmpl.volumeUnit.textContent = assetID === baseid ? basesymbol.toUpperCase() : quotesymbol.toUpperCase()
} else Doc.hide(tmpl.priceBox, tmpl.volumeBox)
Doc.bind(row, 'click', () => app().loadPage('markets', { host, base: baseid, quote: quoteid }))
Expand Down Expand Up @@ -1298,14 +1302,16 @@ function assetIsConfigurable (assetID: number) {
return defs.length > 1 || (zerothOpts && zerothOpts.length > 0)
}

const FourSigFigs = new Intl.NumberFormat((navigator.languages as string[]), {
minimumSignificantDigits: 4,
maximumSignificantDigits: 4
})

const intFormatter = new Intl.NumberFormat((navigator.languages as string[]))
function withNumDecimals (v: number, digits: number) : string {
if (v > 100) {
return intFormatter.format(Math.round(v))
}

const formatter = new Intl.NumberFormat((navigator.languages as string[]), {
maximumFractionDigits: digits,
minimumFractionDigits: digits
})

function fourSigFigs (v: number): string {
if (v < 100) return FourSigFigs.format(v)
return intFormatter.format(Math.round(v))
return formatter.format(v)
}

0 comments on commit 8117269

Please sign in to comment.