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 asDecimal
Price
class should useFraction
forself["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 inDecimal
Market.buy()
andMarket.sell()
methods should be refactored to useDecimal
for amounts andFraction
for price- Add additional method to
Market
class to place an order with any amount of BASE and QUOTE, likeplace_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