bitshares / python-bitshares

Fully featured client-side library for the BitShares Blockchain - written entirely in python.

Home Page:http://docs.pybitshares.com/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Fixed-point math

bitphage opened this issue · comments

The library currently uses lots of floating-point math which leads to rounding errors, which is unacceptable for financial applications.

Here is some suggestions:

  • Amount class should present asset amount as Decimal
  • Price class should use Fraction for self["price"]
  • Internal conversions in Price class (.invert() should not use floating-point math
  • More general, all API objects exposed to library users should present prices as Fraction, and all amounts in Decimal
  • Market.buy() and Market.sell() methods should be refactored to use Decimal for amounts and Fraction for price
  • Add additional method to Market class to place an order with any amount of BASE and QUOTE, like place_raw_order(amount_to_sell: Amount, min_to_receive: Amount)

Actually, there are no rounding errors that could be prevented by using Decimal(). At least in a sense that Decimal would have the same rounding errors.

But I agree, that it would all look much better if it used Decimal.

Well, there was at least one error caused by FP math, which I tracked down and fixed: #171

Also here is the simple use-case which screws usage of FP math in calculation with amounts. Imagine we have an order and want to increment it's amount by 2% and compare to another order which already bigger by 2%:

from bitshares.amount import Amount

# calculate an order amount 2% bigger
amount = Amount('0.325 RUDEX.GOLOS')
m = 1.02
num1 = amount * m
print(num1)
# actually it's a float inside:
print(float(num1))

# simulate real order from blockchain which is 2% bigger
num2 = 0.325 * m
num2 = round(num2, 3)
print(num2)


# oops
print(num1 == num2)
0.332 RUDEX.GOLOS
0.3315
0.332
False

See? To get reliable results, a library user must round() all his calculations to make sure the results are matched with blockchain precision. Because of this, we're in dexbot doing various checks and hacks, and I'm not sure there are no errors (lol).

Oh well, that is a bug/feature.

The Amount class comes with its own way of dealing with multiplications.
Given that I (as developer) cannot know what the user wants to do with the Amount after multiplying a float with it, I decided to not strip it.
Thinking about it right now, it might have been a wrong decision that can be fixed easily.

The Fraction class for prices isn't optimal as it automatically converts (like from 5/10 to 1/2). If you think of the numerator and denominator as base amount and quote amount, it will mess things up. It would be simple enough to make a Fraction-class of our own for the purpose. It should use internal amounts (integers). I don't think you can properly assess a price without also taking the amounts into account, because ultimately the price is almost always tied to the rational number consisting of the amounts. If you want price * 1.08, then the next question is if you want the same amount of base, same amount of quote, or something else. The resulting price will differ a little depending on your answer to the second

In dexbot we're incrementing prices to place staggered orders, this is a test which shows actual order price and initially calculated price are different:

        closer_order = worker.place_closer_order(asset, order, place_order=True)
    
        # Test for correct price
>       assert closer_order['price'] == order['price'] * (1 + worker.increment)
E       assert 99.01980296969697 == 99.0099009894
E         -99.01980296969697
E         +99.0099009894

So, we should switch from incrementing prices to incrementing amount only (base or quote) to place staggered orders, and as a consequence we need a method to place orders specifying both base and quote amounts (see the first message)

Even better if we can do all calculations in internal amounts (integers), and place orders using them.

@bitfag I'm working on a method and a few helper methods to cater for our needs. I'll share them once I get them somewhat done. It is greatly expanded from this one