New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ui: consistent rate formatting #2278
ui: consistent rate formatting #2278
Conversation
caae876
to
45fe1bb
Compare
client/webserver/site/src/js/doc.ts
Outdated
return threeSigFigs.format(v) | ||
} | ||
|
||
static formatFourSigFigs (v: number) { | ||
if (v >= 1000 || Math.round(v) === v) return oneFractionalDigit.format(v) | ||
return fourSigFigs.format(v) | ||
} | ||
|
||
static formatFiveSigFigs (v: number, prec?: number): string { | ||
if (v >= 10000) return intFormatter.format(Math.round(v)) | ||
else if (v < 1e5) return fullPrecisionFormatter(prec ?? 8 /* rate encoding factor */).format(v) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved fourSigFigs
func into doc.ts
(and renamed it to formatFourSigFigs
) so it is accessible both in markets.ts
and charts.ts
, but turns out there already are formatThreeSigFigs
and formatFiveSigFigs
that are pretty similar but use slightly different formatting rules ... which seems somewhat weird to me. Can't we use same formatting for all of these ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. It is weird. We really need consistent guidelines for display of numerical values. As I recently pointed out elsewhere, I typically draw a distinction between "display" values and "data" values. For "display" values, I think sig figs is appropriate, especially for numbers that have decimals (but for large numbers, I wouldn't, for instance, truncate 1,234,567 to 1,230,000). I personally feel like nobody ever needs more than 3 significant figures for display values, but I would entertain arguments for 4 or 5 sig figs too. For "data" values, for which it might be necessary to validate the number against e.g. an order, the full value should always be displayed, out to the number of decimals derived from UnitInfo
and/or RateStep
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One problem though is that Javascript seems to prioritize minimumSignificantDigits
over maximumFractionalDigits
. That's why we end up with values like 0.00000001000
for fourSigFigs.format(0.00000001)
. I guess a question I have is whether we can omit the minimumSignificantDigits
specifier in all cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, that's exactly why we unfortunately have to do this (v >= 1000)
else if (v < 1e5)
business. The ability to use the significant digits and fraction digits options together effectively is considered an experiential option. #2208 (comment)
Generally though I find 4 sig figs for smaller values to be the sweet spot, but as you said whole numbers precision should not be lost, only fractional values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with all of the above,
For "data" values the full value should always be displayed
we can't address this ^ in general by simply changing how we are using/combining different rounding modifiers (significant digits and fraction digits) because JS Number itself can't represent what is actually arbitrary precision Decimal.
Maybe in practice we don't need to (if those cases when we don't get precise representation for order rate/qty/... are very rare on DEX, and we can adjust those significant digits and fraction digits modifiers such that they fit our use-cases most closely. Otherwise we need to figure out how to handle Decimals precisely (use some 3rd party lib to handle it for us, for example).
Alternatively, we can probably implement our own AtomicDecimal
that will be represented as integer number of atoms (limited by Number.MAX_SAFE_INTEGER) convertable to/from string representation (as decimal) with specified precision (e.g. rate_step, perhaps trimming non-effectual 0s) also erroring when precise representation isn't possible. I think this is pretty much how dexc represents it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you point out, this is why we use integer representations of quantities and rates on the backend. These numbers find also their way to the frontend. Particularly with the rates, these whole numbers do not exceed 2^53 – 1 so there is no precision loss because the values fit into the IEEE 754 64-bit float's significand.
Only when we start doing math with these on the frontend can we run into trouble. When we divide the whole number "message rate" by the "rate encoding factor" (1e8), we get the decimal value. If the float string formatters are used with the appropriate "rate step" or even the full 8 fractional digits of precision, it is rounded so that there is no incorrect or misleading information presented (the order of precision loss is far smaller than the number of digits we retain and the formatters do not simply truncate). We only need to request the appropriate number of digits for a rate based on the market's rate step.
EDIT: but we do need to be smart about order of operations when doing mult/div with these floats. Keeping the magnitudes balanced is important.
Note, there is also a room to improve but that won't work while we are showing Market orders in the book with which is counterintuitive on its own and probably needs changing some time in future. EDIT: It will work actually while we trim with rounding via |
Also note, switching to using |
45fe1bb
to
1a17cc4
Compare
Reworked things in that last force-push, 1st commit applies these changes buck54321/dcrdex@2e0968c...buck54321:dcrdex:four-sig-figs 2nd commit:
in UI it looks like this now: |
client/webserver/site/src/js/doc.ts
Outdated
static decodeRateStep (rateStepEnc: number, baseUnitInfo: UnitInfo, quoteUnitInfo: UnitInfo) { | ||
const [qFactor, bFactor] = [quoteUnitInfo.conventional.conversionFactor, baseUnitInfo.conventional.conversionFactor] | ||
return rateStepEnc / (RateEncodingFactor * qFactor / bFactor) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could "decode" rate step in a simpler manner ? Just asking, in case I missed something.
Although on back-end ConventionalRateAlt
does it similarly too, so I guess not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wish this were easier. I'm constantly forgetting that all asset's conversion factors are not 1e8 anymore.
Although I really don't know if this is the best order of operations. Elsewhere we've found this was best: https://github.com/decred/dcrdex/pull/1733/files#diff-8466f141b8e33a7e492b165ae7b6509808343604c759f3a0cc36e06fc33f13efR892
#1733 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also adjust ConventionalRateAlt
then to do something like this ?
return float64(msgRate) * (float64(baseFactor) / float64(quoteFactor)) / RateEncodingFactor
It's doing this currently:
return float64(msgRate) / RateEncodingFactor * float64(baseFactor) / float64(quoteFactor)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure, but if we change the backend we should use big.Float for the math.
// which looks like a comma to me. ¯\_(ツ)_/¯ | ||
['ar-EG', '123.45678', undefined, '١٢٣٫٥'], | ||
['ar-EG', '1234', undefined, '١٬٢٣٤٫٠'], | ||
['ar-EG', '0.12345', 3, '٠٫١٢٣'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is failing because you're not passing the "preciseFormatter
" and fullPrecisionFormatter
uses navigator.languages
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So it is test-specific issue (and using navigator.languages
is what we want users to use in practice), right ? We can use something like this to accommodate this test (it passes now): 2577606
The reason I wanted to remove preciseFormatter
from there is because it kinda duplicates maxDecimals
param (according to my understanding) and it isn't clear what happens if preciseFormatter
has precision set to anything different from maxDecimals
value.
client/webserver/site/src/js/doc.ts
Outdated
@@ -291,6 +296,15 @@ export default class Doc { | |||
return fullPrecisionFormatter(prec).format(value) | |||
} | |||
|
|||
static decodeRateStep (rateStepEnc: number, baseUnitInfo: UnitInfo, quoteUnitInfo: UnitInfo) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a "decoded" rate step, which would be a ratio of atomic units. This is a rate step expressed in conventional units. So a name like conventionalRateStep
would be more fitting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got a bit confused by conventional
and decoded
, kinda assuming both to be same thing. Seems clear now, applied.
client/webserver/site/src/js/doc.ts
Outdated
@@ -291,6 +296,15 @@ export default class Doc { | |||
return fullPrecisionFormatter(prec).format(value) | |||
} | |||
|
|||
static decodeRateStep (rateStepEnc: number, baseUnitInfo: UnitInfo, quoteUnitInfo: UnitInfo) { | |||
const [qFactor, bFactor] = [quoteUnitInfo.conventional.conversionFactor, baseUnitInfo.conventional.conversionFactor] | |||
return rateStepEnc / (RateEncodingFactor * qFactor / bFactor) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty sure this is supposed to be bFactor / qFactor
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed as described here.
client/webserver/site/src/js/doc.ts
Outdated
const r = bui.conventional.conversionFactor / qui.conventional.conversionFactor | ||
const convRate = encRate * r / RateEncodingFactor | ||
const rateStepDigits = Doc.rateStepDigits(Doc.decodeRateStep(rateStepEnc, bui, qui)) | ||
return convRate.toFixed(rateStepDigits) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
toFixed
is not locale-aware. We can't use it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be fixed here, we might also want to add some locale-aware tests for formatRateFullPrecision
to convey the intent.
this.rateStep = rateStep / RateEncodingFactor * qFactor / bFactor | ||
this.rateStep = Doc.decodeRateStep(rateStepEnc, baseUnitInfo, quoteUnitInfo) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of things here. 1) it appears that this was wrong? See other comment on bFactor / qFactor
. 2) this.rateStep
should really be called this.conventionalRateStep
, but not really your problem here, so whatever.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- it appears that this was wrong? See other comment on bFactor / qFactor
Yep, it contradicts this:
func ConventionalRateAlt(msgRate uint64, baseFactor, quoteFactor uint64) float64 {
return float64(msgRate) / RateEncodingFactor * float64(baseFactor) / float64(quoteFactor)
}
note, in your diff provided below it still seems to be incorrect due to extra set of ()
used there, yet testFormatRateFullPrecision
tests pass; I've added some more unit-tests there and addressed it, and it seems to be working correctly now.
- this.rateStep should really be called this.conventionalRateStep
I would like to rename it, while we are at it, done here.
client/webserver/site/src/js/doc.ts
Outdated
* formatRate formats rate to represent it exactly at rate step precision, | ||
* trimming non-effectual zeros if there are any. | ||
*/ | ||
static formatRateToRateStep (encRate: number, bui: UnitInfo, qui: UnitInfo, rateStepEnc: number) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are formatting an encoded rate to a conventional rate. Can the method name reflect that? This is also full-precision, so maybe a name like formatRateFullPrecision
would be more apt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so maybe a name like formatRateFullPrecision would be more apt.
Yep, sounds good, full precision also implies that it is a conventional rate. Applied.
client/webserver/site/src/js/doc.ts
Outdated
static rateStepDigits (rateStepDec: number) { | ||
return Math.round(Math.log10(1 / rateStepDec)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might be onto something here using the conventional ratestep to determine the number of decimals to show. But, the argument is not just decoded (rateStepDec
), it's conventional, so the argument name should reflect that. Also, this is only used once. Do we really need a method for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But, the argument is not just decoded (rateStepDec), it's conventional, so the argument name should reflect that.
Renamed as TODO now.
Also, this is only used once. Do we really need a method for this?
Probably not, thought it might be re-used somewhere but seems unlikely, inlined.
client/webserver/site/src/js/doc.ts
Outdated
* formatRate formats rate to represent it exactly at rate step precision, | ||
* trimming non-effectual zeros if there are any. | ||
*/ | ||
static formatRateToRateStep (encRate: number, bui: UnitInfo, qui: UnitInfo, rateStepEnc: number) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've explored some tests to see if we're getting what we expect from this method.
buck54321/dcrdex@1a17cc4...buck54321:dcrdex:full-precision-rate-tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, I was lazy to figure out how to run those, now I did,
I'm not sure how to use npm run watch
properly (and it seems to be putting significant load on my laptop), so I added npm run build_dev
which is similar to npm run build
but allows for accessing those tests ... if you don't have better suggestions for me on this.
And applied changes too: 090f42a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how to use
npm run watch
properly
I think you have to use it with --no-embed-site
, but I'm not sure either tbh. It has been a while since I've done anything other than a full build.
] | ||
|
||
for (const [encRate, rateStep, qFactor, bFactor, expEncoding] of tests) { | ||
for (const k in fullPrecisionFormatters) delete fullPrecisionFormatters[k] // cleanup |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need to clean up fullPrecisionFormatters
because we might have wrong locales
there due to other tests running previously, which breaks tests depending on run order
client/webserver/site/src/js/doc.ts
Outdated
if (!locales) { | ||
locales = navigator.languages as string[] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fmt = new Intl.NumberFormat(locales || navigator.languages as string[], {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied.
client/webserver/site/src/js/doc.ts
Outdated
function formatSigFigsWithFormatters (sigFigs: number, intFormatter: Intl.NumberFormat, sigFigFormatter: Intl.NumberFormat, n: number, maxDecimals?: number, locales?: string | string[]): string { | ||
if (n >= Math.round(Math.pow(10, sigFigs - 1))) return intFormatter.format(n) | ||
const s = sigFigFormatter.format(n) | ||
if (typeof maxDecimals !== 'number') return s | ||
const fractional = sigFigFormatter.formatToParts(n).filter((part: Intl.NumberFormatPart) => part.type === 'fraction')[0].value | ||
if (fractional.length <= maxDecimals) return s | ||
return fullPrecisionFormatter(maxDecimals, locales).format(n) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose if we've adopted a four-only sig fig policy, we could simplify this a bit to get rid of the sigFigs
argument..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like that 0252336 ?
client/webserver/site/src/js/doc.ts
Outdated
const r = bui.conventional.conversionFactor / qui.conventional.conversionFactor | ||
const convRate = encRate * r / RateEncodingFactor | ||
const conventionalRateStep = Doc.conventionalRateStep(rateStepEnc, bui, qui) | ||
const rateStepDigits = Math.round(Math.log10(1 / conventionalRateStep)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this will work for rate steps of powers of 10. It doesn't seem to work for all rate steps though. For instance, paste this test into testFormatRateFullPrecision
.
// UTXO assets but with a rate step that's not a perfect power of 10.
// For a rate step of 500, a min rate would be e.g. rate step = 500.
// 5e2 / 1e8 = 5e-6 = 0.000005
[5e2, 500, 1e8, 1e8, '0.000005'],
which results in
TEST FAILED: f(500, 100000000, 100000000, 500) => 0.00001 != 0.000005
Maybe do this instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, applied.
Closing EOW. Please finalize. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whenever it's good with you, @buck54321.
Ideally a squash and rebase from @norwnd before merging, but I can handle that if need be.
0252336
to
2d0e493
Compare
Squashed & rebased. |
As a follow-up to #2174 we still have some rates in order book (& depth/candle charts) with trailing zeros:
in this PR I'm basically truncating those leading 0s, which works because those rates are already complying with their respective rate steps, so it looks more like this: