-
Notifications
You must be signed in to change notification settings - Fork 649
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
Rounding issue when matching orders #342
Comments
Actually there is a deep discussion in #132 which IMHO worth reading. |
@abitmore, as I posted on bitsharestalk last night...
Part of the update on my post:
For reference, here's the link for original post with the full update: http://bit.ly/2tKiDfb |
@alexpmorris Thanks for the contribution. We all aware of the issue, the challenge is we need a solution or maybe find a better trade-off. If you've read through the comments in issue #132 (which I guess you haven't), perhaps you'll come with more ideas. |
The picture attached above explains exactly why we described it as a rounding issue: the taker has never overpaid more than 1 satoshi of WHALESHARE every take. |
There's got to be a better way to handle this @abitmore, and I'd be happy to work with you to figure it out. Now that I know exactly what's going on, I'm pretty sure I know what needs to be solved. I'll try to come up with an ideal solution, and defer to you on if and how it can be safely implemented with minimal likelihood of "unintended consequences". |
@abitmore Here's a potential fix for this problem. As I described in the pull request:
Here is what the test case is expecting to receive: Since I couldn't get through all the tests, and I've already spent quite a bit of time on this, I at least wanted to share what I came up with so far, and give the community the opportunity to "rip it" apart a bit to see what else I may have missed so far. Here were the results of my updated code, as shown in openledger when linked to my local testnet (though manually entered via |
|
Was about to suggest the same. |
I used to_real() to extract the rounding error. It also seemed to work very well to identify a buy order from a sell order of the non-core asset, because all buy orders entered resulted in (real_order_price > 1), and all the sell orders resulted in (real_order_price < 1). This makes perfect sense to me, because how else would one determine the side of the book to place an order (for both the UI and for order matching), which by extension, also enabled me to extrapolate "amount_to_sell" versus "amount_to_buy". And while this also seemed consistent throughout my testing, are you saying this may not always be the case? Also, if there's a better way to extract both of these values using the template primitives versus using to_real() as you described, that should be a relatively minor adjustment to make. |
Trying to distinguish a buy from a sell by looking at the price doesn't make sense at all. What happens to your logic if BTS goes above 1 USD, for example? |
That was my first thought as well, except that internally within each order object, base and core are maintained as total quantities to exchange versus each other, along with which side is "for_sale", which implicitly should contain all the information we need. And of course, there must still exist a consistent way that the front-end and the matching engine always knows which side to target (bids and offers). Furthermore, every test case I've tried seems to verify this. I also gave examples where BTS was trading below 1, above 1, and where the matching even took place through 1. Here are all the test cases, along with a few screenshots as well. Again, all these test cases behaved exactly as I would have expected: sell_asset nathan 250 BTS 1000 TEST 100000 false true <-- BUY 1000 TEST @ 0.25 sell_asset nathan 1000 BTS 250 TEST 100000 false true <-- BUY 250 TEST @ 4 TAKER SELLS (below 1 / above 1) sell_asset nathan 25 BTS 100 TEST 100000 false true <-- BUY 100 TEST @ 0.25 (bts) sell_asset nathan 400 BTS 100 TEST 100000 false true <-- BUY 100 TEST @ 4 (bts) TAKER BUYS (below 1 / above 1) sell_asset nathan 100 TEST 25 BTS 100000 false true <-- SELL 100 TEST @ 0.25 (bts) sell_asset nathan 100 TEST 400 BTS 100000 false true <-- SELL 100 TEST @ 4 (bts) TAKER SELLS (above 1 through below 1) sell_asset nathan 125 BTS 100 TEST 100000 false true <-- BUY 100 TEST @ 1.25 (bts) TAKER BUYS (below 1 through above 1) sell_asset nathan 100 TEST 80 BTS 100000 false true <-- SELL 100 TEST @ 0.80 (bts) And finally... TAKER SELLS small lot too low TAKER BUYS small lot too high Note, if you would like to try reproducing these results, every example I gave should work directly in the cli_wallet (just leave off the <-- comment). I also used:
between test sessions to retrieve order ids and cancel orders for the next test. |
What you didn't test is the case that the book contains more at a better price than the taker offers. Like this (didn't test myself, I hope you get the idea): sell_asset nathan 400 TEST 100 BTS 100000 false true <-- SELL 400 TEST @ 0.25 (bts) sell_asset nathan 400 TEST 1000 BTS 100000 false true <-- SELL 400 TEST @ 2.5 (bts) In one of these cases the taker should buy 400 with your modifications, in the other case 100. |
sell_asset nathan 400 TEST 100 BTS 100000 false true <-- SELL 400 TEST @ 0.25 (bts) sell_asset nathan 400 TEST 1000 BTS 100000 false true <-- SELL 400 TEST @ 2.5 (bts) Again, isn't this exactly what should be expected? |
Yup, that's what I meant. Thanks for noticing. My first example was bad, because the taker price is exactly 1. What about sell_asset nathan 400 TEST 100 BTS 100000 false true <-- SELL 400 TEST @ 0.25 (bts) I'm still struggling with the logic you have implemented there. Can you explain in simple terms the reasoning behind it? For example, most of the time core_min_counter_size should be equal to core_for_sale.amount, but I suspect due to your use of doubles and rounding it may not be. At the same time, usd_max_counter_size will always be <= usd_for_sale.amount, because otherwise the two orders would not be matched. |
sell_asset nathan 400 TEST 100 BTS 100000 false true <-- SELL 400 TEST @ 0.25 (bts) in
Essentially, the logic assures that you are both receiving and paying at an exact satoshi increment (as @abitmore had described), and not above or below on each side of the matched order. in
|
Thanks, but I still don't get it. How/why does the real_order_price play a role there? Are you sure the orders arrive on the blockchain exactly as you enter them in the GUI?
Yes, that's the current logic. |
Sorry for the delay in responding, but it turns out you both were right about the inversion issue. I'm actually a bit confused myself, given how they all seemed to show up correctly as you saw in the screenshots. @pmconrad, you asked if I was entering them into the GUI, which may somehow still have been compensating somehow to obfuscate the problem. However, all orders were entered directly into the However, internally, upon further review, the issue you brought up was certainly there. I might have tried using openledger to enter orders as well, but I would have had to change the genesis chain_id to my private testnet, which seemed non-trivial at the time. As such, I spent some time rethinking how this could possibly be implemented by:
What I came up with is a bit of a "hack", but so far it does seem to work. However, it would still require a mod to the UIs to pass in
The information is added into each order via the less often used Further analysis would also be required to determine if this can become a target vector for manipulation, by either reversing the flag or leaving it out (again, partly because as described above, if this "flag" is not set, match() will revert to its default pattern of behavior). I also created a whole set of test cases based on the examples we discussed in this thread, and so far (assuming I got all the test cases correct) the solution is passing them all. More test cases will likely be needed to really run this idea through the grinder. The main changes are in an updated match() function, and a new fill_order_rounded() which adds a parameter Link to code updates: https://github.com/alexpmorris/bitshares-core/commits/master |
Actually the A "normal" fix would be: add a new optional Avoid using |
That's great to hear @abitmore, as I agree the other options would be much better if it could easily be implemented without causing issues elsewhere. My idea of using |
TBH the code is far from usable. I'm not sure if it compiles. For example, Thanks for your efforts anyway. If you want your code to be merged, or be used by others to work upon, please at least follow the existing coding style/format/practice, avoid duplicate code blocks and etc. Otherwise it would be more efficient for others to redo the job from scratch. I have a rough image of solution of this issue in my head, but don't have much time to code it right now. I'll work on it later when got time and if it's still not fixed. |
Once we have "buys", we can add more options/flags to UIA's which can be decided by the issuers, for example, define a market pair is "un-flip-able", so only accept "sells" from on side and "buys" from the other side, and we can add restrictions to the price struct in that pair, for example enforce |
https://github.com/alexpmorris/bitshares-core/tree/order-rounding-fix With this latest revision, I believe the order matching problem is solved. I also combined almost all the code into the rounded_match() function, called from database::match() in db_market.cpp - this should enable the code to be accessed by any template parameter that also calls match(). I also created a barrage of test cases, all of which pass (assuming I got all the necessary test cases correct, of course). In addition, for full functionality all a limit order needs is the assignment of one additional optional flag, isCoreBuySell. It turns out the problem wasn't differentiating a buy from a sell (or above/below 1), it was differentiating an inverted buy/sell (ie. isCoreBuySell versus isTestBuySell). If you think of openledger, we always know when an order is a buy or a sell. What we don't know is which LEG they're trying to buy and sell. Once the target leg is identified (explicitly or by default), any remainder that doesn't evenly match is simply discarded and refunded. I tried to use references that make it easier to follow what's going on from a practical standpoint, hence the use of terms such as "isCoreBuySell" and "isTestBuySell", since that's really what we need to know: is the order BUYING TEST / SELLING TEST, or BUYING CORE / SELLING CORE. By default, it is currently set to BUY TEST / SELL TEST (ie. buy DOW with USD / sell DOW with USD). For most cases, I believe it is much less often the other way around. Another interesting thing that I discovered is that while STEEMIT did clean up the code quite a bit, this problem affects ALL chains based on graphene 2.0 (bitshares, steem, golos, peerplays, even eos)! That includes the STEEMIT internal market as well. So, even better, if this fix solves the problem for one, it should solve the issue for all the other chains as well. Finally, if you would like to see my test runs, or the version I used for debugging (including all the idump(()) calls), you can find that code here: https://github.com/alexpmorris/crypto-playpen/tree/master/bitshares-core |
I'm sorry, but I still don't see how comparing order_price to book_price can yield useful information. This only makes sense if you assume a certain market order, which is entirely the wrong approach. |
The order on the book might have a same flag set as well ( sell or buy, or say, want to get more or get a refund ), and should be checked as well. Besides that, call orders and settle orders can not be cancelled/refunded, so their behavior are simpler. |
https://github.com/alexpmorris/bitshares-core/tree/order-rounding-fix You're right @pmconrad, that variable was still mislabeled. We already know it's a match ( as we're already in match() ). The template ( greater_than / less_than ) is comparing the size of the incoming order to the size of the sitting order, and based on that, the code goes on to adjust for the target leg. I've changed the variable name to reflect that. As a trader, if I place a limit order, I don't want to receive a fill above (or below, for a sell) the price of a standing limit order, and comparing my order size ratio versus the book's order size ratio assures that I will evenly match that order before moving on to the next order on the book. This solution appears to accomplish that goal. Keep in mind that all orders already on the book are properly rounded versus their available size, so all that's really necessary is to match the size of the incoming order to size that rounds evenly with the order that is already available. And again, any remainder that cannot be properly matched in the face of a standing order is simply discarded and refunded. As @abitmore said, call and settle orders are "simpler", in that they can probably just be handled in the current fashion, since there is an incentive to "penalize" those orders anyway. As such, the default mode of operation is now set to "no action" (ie. I also made the other adjustments you both suggested, and added the
|
Done with #830. Closing. |
@alexpmorris if you have time, can you help verify it's fixed in testnet (https://testnet.bitshares.eu/)? |
@abitmore I tried matching a bunch of orders with extreme price and size combinations on the testnet, and while the size could still be greater than expected (as expected, since that was the premise of my proposed "isCoreBuySell" extension), all the match prices appear to be right in line with what I'd expect. As such, definitely a step in the right direction. 👍 |
@alexpmorris thank you! |
As @alexpmorris reported in this steemit post, there is an rounding issue when matching orders. For example:
The last matched order was selling 0.185 BTS, but apparently it's not enough to get 21 satoshi of HERO, so finally it got 20. However, to get 20 satoshi of HERO, only need to pay 0.1838 BTS.
Current implementation is in favor of the maker (it gets all the amount that below a satoshi of the other token). I propose to change it to in favor of the taker, or anything that is fairer. For example, if the rest is less than a satoshi, cancel the order and return the rest to the taker. I guess it's hard to implement due to the design of price structure. Another side effect is that people may be unable to sell all balance of one token.
Earlier discussion in #338. Probably related: #132, #184.
The text was updated successfully, but these errors were encountered: