Merge pull request #59 from bitshares/bsip35
Add BSIP 35: Mitigate Rounding Issue On Order Matchingbsip53
commit
5bf3ddc5f6

@ 42,7 +42,6 @@ Number  Title 


[32](bsip0032.md)  Always Match Orders At Maker Price  Abit More  Protocol  Draft


[33](bsip0033.md)  Maker Orders With Better Prices Take Precedence  Abit More  Protocol  Draft


[34](bsip0034.md)  Always Trigger Margin Call When Call Price Above Or At Price Feed  Abit More  Protocol  Draft


[35](bsip0035.md)  Mitigate Rounding Issue On Order Matching  Abit More  Protocol  Draft


[36](bsip0036.md)  Remove expired price feeds on maintenance interval  oxarbitrage  Protocol  Draft


[37](bsip0037.md)  Allow new asset name to end with a number  oxarbitrage  Protocol  Draft









@ 0,0 +1,488 @@


BSIP: 0035


Title: Mitigate Rounding Issue On Order Matching


Author: Abit More <https://github.com/abitmore>


Status: Draft


Type: Protocol


Created: 20180219


Discussion: https://github.com/bitshares/bitsharescore/issues/132,


https://github.com/bitshares/bitsharescore/issues/184,


https://github.com/bitshares/bitsharescore/issues/342


Replaces: 


Worker: To be done




# Abstract




Under some circumstances, when two orders get matched, due to rounding,


one order may be paying more than enough, even paying something but receiving


nothing. This looks unfair.




This BSIP proposes an overall mechanism to mitigate rounding issue when matching


orders and avoid somethingfornothing issue completely.




This BSIP also sets two principles for order matching:


* never pay more than enough, and


* somethingfornothing shouldn't happen.




# Motivation




There are mechanisms in the system to try to avoid somethingfornothing issue,


however, not all scenarios are wellhandled, see [bitsharescore


issue #184](https://github.com/bitshares/bitsharescore/issues/184) for example.




Other than that, rounding issue occurs frequently and has led to a lot of


confusion among market participants, see [bitsharescore


issue #342](https://github.com/bitshares/bitsharescore/issues/342) for example.




# Rationale




## Amounts, Prices and Rounding




Amounts in the system are integers with perasset fixed precisions.


The minimum positive amount of an asset is called one Satoshi.




Prices in the system are rational numbers, which are expressed as


`base_amount / quote_amount` (precisions are omitted here).




To calculate how much amount of asset B is equivalent to some amount of


asset A, need to calculate `amount_of_a * a_to_b_price` which is


`amount_of_a * b_amount_in_price / a_amount_in_price`. The accurate result


of this formula is a rational number. To convert it to the final result which


is an amount, which is an integer, may need to round.




## Order Matching




An order means someone is willing to give out some amount of asset X expecting


to get some amount of asset Y. The ratio between the two assets is the price of


the order. The price can be expressed as either `x_amount / y_amount` or


`y_amount / x_amount`, when we know which amount in the price is of which asset,


the two expressions are equivalent. The amount of asset X is known and fixed.




In a market, E.G. the X:Y market, some people are selling X for Y, some people


are selling Y for X (or say buying X with Y). Orders are classified by type


(buy or sell), then ordered by price. For each type, the order offering the


best price is on the top. So, in every market there may be a top buy order and


a top sell order, name them highest bid and lowest ask, so there is


a highest bid price (in terms of `asset X amount / asset Y amount`),


and a lowest ask price (in terms of `asset X amount / asset Y amount` as well).




When the highest bid price is higher or equal to the lowest ask price, the two


top orders can be matched with each other.




## The Match Price




In a continuous trading market, orders are placed one by one, when comparing


every two orders, it's deterministic that one order is placed earlier than the


other.




In BitShares, it doesn't mean that the transaction that contains the first


order is signed before the transaction contains the second, but means that


the first order is processed earlier than the second in the witness node that


produced the block that contains the second order.




When two orders get matched, the one placed earlier is maker, the other one


is taker. Say, the maker provides an offer, the taker accept the offer.


So, when calculating who will get how much, we use the maker order's price,


aka maker price, as the match price.




## The Need for Compromise




When matching two orders, due to rounding, usually we're unable to completely


satisfy both parties.




Here is an example mentioned in the 4th comment of [bitsharescore


issue #132](https://github.com/bitshares/bitsharescore/issues/132):




Alice's order: Sell CORE at $3 / 8, balance 1000000 CORE


Bob's order: Buy CORE at $19 / 50, balance $10




Both assets have precision of 1, i.e. the order balances are


1000000 COREsatoshis and 10 USDsatoshis repectively.




Alice is selling at $3/8 CORE = $0.375 / CORE and Bob is buying at


$19 / 50 CORE = $0.38, so based on the price, Alice and Bob should match.




Bob's $10 / $0.38 ~ 26.3. So 26.3 is the fewest CORE he is willing to accept


(assuming that the meaning of "price" is "the least favorable exchange rate


a party is willing to accept in trade"). Combined with the design restriction


that satoshis are indivisible, in practice this means Bob will only accept 27


or more CORE for his $10.




But $10 / 27 gives a price smaller than $0.370 and $0.371, which is smaller


than Alice's sale price of $0.375. So neither party can fill this offer.




We need to come to a compromise.




## The Possible Solutions




There are some possible solutions listed in the 5th comment of [bitsharescore


issue #132](https://github.com/bitshares/bitsharescore/issues/132):




 (a) Fill someone at a less favorable exchange rate than the price they


specified in their order. Downside: This violates the above definition of


price; i.e. if a user enters a price intending the system to never sell below


that price in any circumstance, the system will not always behave in a way


which fulfills that user intent.




 (b) Keep both orders on the books. Downside: This complicates the matching


algorithm, as now Alice might be able to match an order behind Bob's order.


Naive implementation would have potentially unbounded matching complexity;


a more clever implementation might be possible but would require substantial


design and testing effort.




 (c) Cancel an order. This is complicated by the fact that an order such as


a margin call cannot be cancelled. Downside: When there are margin calls


happening, it seems perverse to delete a large order that's willing to fill


them just because the lead margin call happens to fall in a narrow window


which causes a rounding issue. Also, orders cancelled by this mechanism


cannot be refunded. Otherwise an attacker who wants to consume


a lot of memory on all nodes could create a large number of orders, then


trigger this case to cancel them all, getting their investment in deferred


cancellation fees back without paying the cancel op's perorder fee as


intended.




 (d) Require all orders to use the same denominator. Altcoin exchanges and


many realworld markets like the stock market solve this problem by specifying


one asset as the denominator asset, specifying a "tick" which is the smallest


unit of price precision, and requiring all prices to conform.


Downside: Complicates the implementation of flipped market UI, may require


reworking part of market GUI, reduces user flexibility, new asset fields


required to specify precision, if `n` assets exist then `O(n^2)` markets


could exist and we need to figure out how to determine the precision


requirement for all of them.




## The Chosen Solution




Current code actually implemented (a) in the first place: when matching two


orders, if there is a rounding issue, the order with smaller volume will be


filled at a less favorable price. It's the least bad compromise since it has


the most efficiency (highest traded volume while not hard to implement) among


the solutions.




The algorithm can be described as follows (sample code is


[here](https://github.com/bitshares/bitsharescore/blob/2.0.171105a/libraries/chain/db_market.cpp#L311L324)):




Assuming the maker order is selling amount `X` of asset A, with price


`maker_price = maker_b_amount / maker_a_amount`; assuming the taker is buying


asset A with amount `Y` of asset B, with price


`taker_price = taker_b_amount / taker_a_amount`. Anyway, since the two orders


will be matched at maker price, the taker price doesn't matter here as long


as it's higher than or equal to maker price. Note: currently all limit orders


are implemented as sell limit orders, so in the example, the taker order can


only specify amount of asset B but not amount of asset A.




Now compare `X * maker_price` with `Y`. To be accurate (avoid rounding),


compare `X' = X * maker_b_amount` with `Y' = Y * maker_a_amount`.


* The best scenario is when `X' == Y'`, which means both orders can be


completely filled at `maker_price`.


* If `X' < Y'`, it means the maker order can be completely filled but the


taker order can't, aka the maker order is smaller.


In this case, maker pay amount `X` of asset A to taker, taker pay amount


`Y" = round_down( X' / maker_a_amount )` of asset B to maker.


Note: due to rounded down, it's possible that `Y"` is smaller than the


rational number


`X * maker_price`, which means `Y" / X` may be lower than `maker_price`,


that said, the maker order may has been filled at a less favorable price.


* If `X' > Y'`, it means the taker order can be completely filled but the


maker order can't, aka the taker order is smaller.


In this case, taker pay amount `Y` of asset B to maker, maker pay amount


`X" = round_down( Y' / maker_b_amount )` of asset A to taker.


Note: due to rounded down, it's possible that `X"` is smaller than the


rational number


`Y / taker_price`, which means `Y / X"` may be higher than `taker_price`,


that said, the taker order may has been filled at a less favorable price.




## Issues With The Chosen Solution




### The Somethingfornothing Issue




When filling a small order at a less favorable price, the receiving


amount is often rounded down to zero, thus causes the somethingfornothing


issue. Current code tried to solve the issue by cancelling the smaller order


when it would receive nothing, but only applied this rule in a few senarios


(the processed parties won't be paying something for nothing):


* when matching two limit orders, processed the maker


* when matching a limit order with a call order, processed the call order


* when matching a settle order with a call order, processed the call order


* when globally settling, processed the call order




Other senarios that need to be processed as well (these tobeprocessed parties


may be paying something for nothing in current system):


* when matching two limit orders, process the taker


* when matching a limit order with a call order, process the limit order


* when matching a force settle order with a call order, process the settle order


* when globally settling, process the settlement fund


* when force settling after an asset has been globally settled, paying the force


settle order from global settlement fund, process the settle order




### The Broader Rounding Issue




Somethingfornothing is only a subset of rounding issues, it's the most extreme


one. There are much more scenarios that one of the matched parties would be


paying more than enough, although they're not paying something for nothing


overall. Some scenarios are discussed in [bitsharescore


issue #342](https://github.com/bitshares/bitsharescore/issues/342).




Take a scenario similar to the one described in the 4th comment of


[bitsharecore


issue #132](https://github.com/bitshares/bitsharescore/issues/132) as an


example:


* Alice's order: Sell CORE at `$3 / 80 = $0.0375`, balance `50 CORE`


* Bob's order: Buy CORE at `$19 / 500 = $0.038`, balance `$100`




Current system would process them as follows:


* If Alice's order is maker, use `$3 / 80` as match price; since Alice's order


is smaller, round in favor of Bob's order, so Alice will pay the whole `50


CORE` and get `round_down(50 CORE * $3 / 80 CORE) = round_down($1.6) = $1`,


the effective price would be `$1 / 50 = $0.02`;


* If Bob's order is maker, use `$19 / 500` as match price; since Alice's order


is smaller, round in favor of Bob's order, so Alice will pay the whole `50


CORE` and get `round_down(50 CORE * $19 / 500 CORE = round_down($1.9) = $1`,


the effective price would still be `$1 / 50 = $0.02`.




Both results are far from Alice's desired price `$0.0375`. Actually, according


to Bob's desired price, paying `round_up($1 * 500 CORE / $19) = 27 CORE` would


be enough, then the effective price would be `$1 / 27 = $0.037`, which is


still below Alice's desired price `$0.0375`, but much closer than `$0.02`.




## The Improved Solution Proposed By This BSIP




The detailed rules proposed by this BSIP with new rules highlighted:




* match in favor of taker, or say, match at maker price;




* round the receiving amounts according to rules below.




* When matching two limit orders, round down the receiving amount of the


smaller order,


* **if the smaller order would get nothing, cancel it;**


* **otherwise, calculate the amount that the smaller order would pay as


`round_up(receiving_amount * match_price)`.**


* **After filled both orders, for each remaining order (with a positive


amount remaining), check the remaining amount, if the amount is too small


so the order would receive nothing on next match, cancel the order.**




* When matching a limit order with a call order,


* **if the call order is receiving the whole debt amount, which means it's


smaller and the short position will be closed after the match, round up its


paying amount; otherwise,** round down its paying amount.


* **In the latter case,**


* **if the limit order would receive nothing, cancel it (it's smaller,


so safe to cancel);**


* **otherwise, calculate the amount that the limit order would pay as


`round_up(receiving_amount * match_price)`. After filled both orders,


if the limit order still exists, the remaining amount might be too small,


so cancel it.**




* When matching a settle order with a call order,


* **if the call order is receiving the whole debt amount, which means it's


smaller and the short position will be closed after the match, round up its


paying amount; otherwise,** round down its paying amount.


* **In the latter case,**


* **if the settle order would receive nothing,**


* **if the settle order would be completely filled, cancel it;**


* **otherwise, it means both orders won't be completely filled, which


may due to hitting `maximum_force_settlement_volume`, in this case,


don't fill any of the two orders, and stop matching for this asset at


this block;**


* **otherwise (if the settle order would not receive nothing), calculate


the amount that the settle order would pay as


`round_up(receiving_amount * match_price)`. After filled both orders,


if the settle order still exists,


match the settle order with the call order again. In the new match, either


the settle order will be cancelled due to too small, or we will stop


matching due to hitting `maximum_force_settlement_volume`.**


* **That said, only round up the collateral amount paid by the call order


when it is completely filled, so if the call order still exist after the


match, its collateral ratio won't be lower than before, which means we won't


trigger a black swan event, nor need to check whether a black swan event


would be triggered.**




* When globally settling, **in favor of global settlement fund, round up


collateral amount.**




* When paying a settle order from global settlement fund, for predition


markets, there would be no rounding issue, also no need to deal with


somethingfornothing issue; for other assets, apply rules below:


* if the settling amount is equal to total supply of that asset, pay the


whole remaining settlement fund to the settle order;


* otherwise, in favor of global settlement fund since its volume is bigger,


round down collateral amount. **If the settle order would receive nothing,


raise an exception (aka let the operation fail). Otherwise, calculate the


amount that the settle order would pay as


`round_up(receiving_amount * match_price)`; after filled the order, if there


is still some amount remaining in the order, return it to the owner.**




## Examples Of The Improved Solution




### Example 1




Take the example mentioned in the 4th comment of [bitsharescore


issue #132](https://github.com/bitshares/bitsharescore/issues/132):


* Alice's order: Sell CORE at `$3 / 8 = $0.375`, balance `1000000 CORE`


* Bob's order: Buy CORE at `$19 / 50 = $0.38`, balance `$10`




Process:


* If both orders are limit orders


* If Alice's order is maker, use `$3 / 8` as match price;


since Bob's order is smaller, round in favor of Alice's order,


so Bob will get


`round_down($10 * 8 CORE / $3) = round_down(26.67 CORE) = 26 CORE`,


and Alice will get


`round_up(26 CORE * $3 / 8 CORE) = round_up($9.75) = $10`,


the effective price would be `$10 / 26 CORE = $0.3846`.


* If Bob's order is maker, use `$19 / 50` as match price; since Bob's


order is smaller, round in favor of Alice's order, so Bob will get


`round_down($10 * 50 CORE / $19 = round_down(26.32 CORE) = 26 CORE`,


and Alice will get


`round_up(26 CORE * $19 / 50 CORE) = round_up($9.88) = $10`,


the effective price would still be `$10 / 26 CORE = $0.3846`.


* If Alice's order is a call order, since it's bigger, round in favor of it,


we will get same results.




### Example 2




If we change the example to this:


* Alice's order: Buy CORE at `3 CORE / $8 = 0.375`, balance `$1000000`


* Bob's order: Sell CORE at `19 CORE / $50 = 0.38`, balance `10 CORE`




Process:


* If both orders are limit orders, we get similar results as above.


* If Bob's order is a call order, it should have a debt amount which is an


integer, for example `$26`, then


* Alice would get


* `round_up(26 * 3 / 8) = round_up(9.75) = 10 CORE` as a maker, or


* `round_up(26 * 19 / 50) = round_up(9.88) = 10 CORE` as a taker.


* Bob would get the full debt amount which is `$26`.


* If Bob's order is a call order, but the debt amount is a bit high,


for example `$27`, then Alice would get


* `round_up(27 * 3 / 8) = round_up(10.125) = 11 CORE` as a maker, or


* `round_up(27 * 19 / 50) = round_up(10.26) = 11 CORE` as a taker.




However, since the collateral is only `10 CORE`, this match will fail and


trigger a black swan event.




### Example 3




If we change the example to that one used above:


* Alice's order: Sell CORE at `$3 / 80 = $0.0375`, balance `50 CORE`


* Bob's order: Buy CORE at `$19 / 500 = $0.038`, balance `$100`




Assuming both orders are limit orders, they'll be processed as follows:


* If Alice's order is maker, use `$3 / 80` as match price; since Alice's order


is smaller, round in favor of Bob's order, so Alice will get


`round_down(50 CORE * $3 / 80 CORE) = round_down($1.6) = $1`,


and Bob will get `round_up($1 * 80 CORE / $3) = round_up($26.67) = $27`,


the effective price would be `$1 / 27 = $0.037`;


* If Bob's order is maker, use `$19 / 500` as match price; since Alice's order


is smaller, round in favor of Bob's order, so Alice will get


`round_down(50 CORE * $19 / 500 CORE = round_down($1.9) = $1`,


and Bob will get `round_up($1 * 500 CORE / $19) = round_up($26.3) = $27`,


the effective price would also be `$1 / 27 = $0.037`.




# Specifications




## When Matching Two Limit Orders




### Handling SomethingForNothing Issue




In `match( const limit_order_object&, OrderType ... )` function of `database`


class, after calculated `usd_receives` which is for the taker,


check if it is zero.


If the answer is `true`, skip filling and see the order is filled, return `1`,


so the order will be cancelled later.




### Handling Rounding Issue




In `match( const limit_order_object&, OrderType ... )` function of `database`


class, after calculated `receives` for the smaller order, if it isn't zero,


calculate `pays` for it as `round_up(receives * match_price)`.




If the smaller order is taker, after filled, even if there is still some amount


remaining in the order, see it as completely filled and set the lowest bit of


return value to `1`.




If the smaller order is maker, since it will be culled when filling,


no need to change the logic.




## When Matching A Limit Order With A Call Order




In `check_call_orders(...)` function of `database` class,


if the call order is smaller, round up `order_receives`,


otherwise round down `order_receives`.




In the latter case,


* if `order_receives` is zero, skip filling and cancel the limit order.


* otherwise, calculate `order_pays` as


`round_up(order_receives * match_price)`, then the limit order will be


either completely filled, or culled due to too small after partially filled.




## When Matching A Settle Order With A Call Order




In `match( const call_order_object&, ... )` function of `database` class,


if the call order is smaller, round up `call_pays`,


otherwise round down `call_pays`.




In the latter case, check if `call_pays` is zero.


* If the answer is `true`,


* if `call_receives` is equal to `settle.balance`,


call `cancel_order(...)` with parameter set to `settle`,


then return a zeroamount collateral asset object;


* otherwise, return a zeroamount collateral asset object directly.


* Otherwise, calculate `call_receives` as `round_up(call_pays * match_price)`,


then fill both orders normally. If the settle order still exists after the


match, it will be processed again later but with different condition.




After returned, need to check the amount of returned asset at where calling the


`match(...)` function, specifically, `clear_expired_orders()` function of


`database` class. If the returned amount is `0`, break out of the `while` loop.


If the settle order is still there and the returned amount is `0`,


label that processing of this asset has completed. Also, in the outer loop,


need to check the label, if found it's completed, process next asset.




## When Globally Settling




In `global_settle_asset(...)` function of `database` class, round up `pays`.




## When Paying A Settle Order From Global Settlement Fund




In `do_apply(...)` function of `asset_settle_evaluator` class,


after calculated `settled_amount` and adjusted it according to the "total


supply" rule, check if it's zero.




If the answer is `true`, and the asset is not a prediction market,


throw a `fc::exception`.




If the answer is `false`, and the asset is not a prediction market,


and `op.amount.amount` is not equal to `mia_dyn.current_supply`,


calculate `pays` as `round_up(settled_amount * bitasset.settlement_price)`,


then, only deduct `pays` from total supply, and refund


`op.amount.amount  pays` to the user.




# Discussion




There is an argument suggests when matching call orders, we should always


round in favour of the call. If a settlement receives 0 collateral as a result,


that's acceptable, because the settlement price is unknown at the time when


settlement is requested, so no guarantee is violated (within the range of


rounding errors). This should keep the collateral > 0 as long as there is


outstanding debt. A counterargument supports rounding up to 1 Satoshi since


rounding down to zero may break the promise of "every smart coin is backed


by something".




There is an argument says breaking the `min_to_receive` limit is a nogo,


because that's why it's called a "limit order". A counterargument says


slightly breaking the limit is the least bad compromise.




# Summary for Shareholders




[to be added if any]




# Copyright




This document is placed in the public domain.




# See Also




* https://github.com/bitshares/bitsharescore/issues/132


* https://github.com/bitshares/bitsharescore/issues/184


* https://github.com/bitshares/bitsharescore/issues/342

Loading…
Reference in New Issue