2

My goal is to accurantly add or remove 0.05% from values with 18 decimals in Python without converting them to floats. I made the two following solutions and they seem correct for me, but I am very unfamiliar with numbers in Python, therefore I would like to know if there's a better (in terms of accurancy) solution.

price_in_wei = 1000000000000000000 # = 1

# -0.05%
price_with_fee = (price_in_wei/1000)*995

# +0.05%
price_with_fee = (price_in_wei/1000)*1005

# -0.05%
price_with_fee = (price_in_wei*995)/1000

# +0.05%
price_with_fee = (price_in_wei*1005)/1000
rihekopo
  • 3,241
  • 4
  • 34
  • 63
  • 2
    Just curious what is your accuracy threshold? As precise as 18 decimal digits? –  Jun 07 '22 at 15:55
  • @KevinChoonLiangYew Based on the variable name choice (i.e. `price_in_wei`, `price_*`) it looks like a financial application. The answers to the question, ["Why not use Double or Float to represent currency?"](https://stackoverflow.com/questions/3730019/why-not-use-double-or-float-to-represent-currency) are pretty interesting but all basically boil down to, "small errors can become very expensive." FOREX markets, in particular, deal in [pips](https://www.investopedia.com/terms/p/pip.asp), which are much smaller units than, e.g., a USD cent. – webelo Jun 10 '22 at 20:54
  • Not an expert on this, but you could just right your own class of number that satisfies a certain accuracy threshold – Ryan Fu Jun 11 '22 at 01:45

4 Answers4

4

Let me suggest that you use decimal arithmetic using class decimal.Decimal. Even if you need the final result to be an integer value, using decimal arithmetic for intermediate calculations will provide greater accuracy. For the example you provided, what you are doing in the second set of calculations work well enough but only because of the specific values used. What if price_in_wei were instead 1000000000000000001? Your calculation would yield 9.95e+17 or, if converted to an int, 99500000000000000:

>>> price_in_wei
1000000000000000001
>>> price_with_fee = price_in_wei*995/1000
>>> price_with_fee
9.95e+17
>>> int(price_with_fee)
995000000000000000

But decimal arithmetic provides greater precision:

>>> from decimal import Decimal
>>> price_with_fee = Decimal(price_in_wei) * 995 / 1000
>>> price_with_fee
Decimal('995000000000000000.995')
>>> price_with_fee = int(price_with_fee.quantize(Decimal(1))) # round to an integer and convert to int
>>> price_with_fee
995000000000000001

But let's say your currency were US Dollars, which supports two places of precision following the decimal point (cents). If you want that precision, you should work exclusively with decimal arithmetic. For example:

>>> from decimal import Decimal, ROUND_HALF_UP
>>> price_in_wei = Decimal('1000000000000000003')
>>> price_with_fee = (price_in_wei * 995 / 1000).quantize(Decimal('1.00'), decimal.ROUND_HALF_UP) # round to two places
>>> price_with_fee
Decimal('995000000000000002.99')
Booboo
  • 38,656
  • 3
  • 37
  • 60
  • I think that this is the correct route. I think it is up to OP to describe/consider how they want to deal with results. For example, you can't pack `Decimal('995000000000000002.99')` into a JSON payload. So what what should the serialization strategy be? Return `995000000000000002.99` or - perhaps - some approximation that has to deal with/round the `.99` in some reasonable manner? – webelo Jun 10 '22 at 22:41
  • @webelo [This](https://ideone.com/iV6Pu6) is one way you would deal with JSON and Decimal, i.e. by using a custom encoding class and an *object_hook* with the standard decoder. Of course, if you are operating with other languages, then you should serialize a Decimal instance to just its string representation and let the other language deal with that. – Booboo Jun 11 '22 at 11:15
1

For accuracy, your second set of calculations will be more accurate. That is, you should multiply first, then divide. Python seems to accept very large integers. On my computer, I'm not sure if I can go above 1E308.

Also, as @RyanZhang has suggested you should use integer/floored division with the // operator. According to the docs, the result from the // operator is not guaranteed to be of type 'int', so you may need to always explicitly cast to int:

# -0.05%
price_with_fee = int((price_in_wei*995)//1000)

# +0.05%
price_with_fee = int((price_in_wei*1005)//1000)
bfris
  • 5,272
  • 1
  • 20
  • 37
1

Using decimal module would be a way to go because, as per the official documentation, "the decimal module has a user alterable precision (defaulting to 28 places) which can be as large as needed for a given problem."

I think, these two links will help:

https://docs.python.org/3/tutorial/floatingpoint.html

https://docs.python.org/3/library/decimal.html

BhusalC_Bipin
  • 801
  • 3
  • 12
0

If you want to avoid the conversion to floats, be sure to use integer division. This ensures the values stay as integers.

Logically, your code looks fine.

Ryan Zhang
  • 1,856
  • 9
  • 19
  • 2
    Logically the OP's code isn't really fine. If the OP follows your suggestion and simply replaces `/` with `//` then the OP is going to lose precision from the first two calculations. It should be pointed out that multiplication must be performed before integer division in order to preserve precision. e.g. `(price_in_wei*995)//1000` is the way to go, not `(price_in_wei//1000)*995`. – blhsing Jun 08 '22 at 07:14