bsip35: overall solution for rounding issues
This commit is contained in:
parent
485df5cd4f
commit
d6c7fdc785
2 changed files with 190 additions and 69 deletions
|
@ -42,4 +42,4 @@ Number | Title |
|
||||||
[32](bsip-0032.md) | Always Match Orders At Maker Price | Abit More | Protocol | Draft
|
[32](bsip-0032.md) | Always Match Orders At Maker Price | Abit More | Protocol | Draft
|
||||||
[33](bsip-0033.md) | Maker Orders With Better Prices Take Precedence | Abit More | Protocol | Draft
|
[33](bsip-0033.md) | Maker Orders With Better Prices Take Precedence | Abit More | Protocol | Draft
|
||||||
[34](bsip-0034.md) | Always Trigger Margin Call When Call Price Above Or At Price Feed | Abit More | Protocol | Draft
|
[34](bsip-0034.md) | Always Trigger Margin Call When Call Price Above Or At Price Feed | Abit More | Protocol | Draft
|
||||||
[35](bsip-0035.md) | A Solution To Something-For-Nothing Issue | Abit More | Protocol | Draft
|
[35](bsip-0035.md) | Mitigate Rounding Issue On Order Matching | Abit More | Protocol | Draft
|
||||||
|
|
251
bsip-0035.md
251
bsip-0035.md
|
@ -1,5 +1,5 @@
|
||||||
BSIP: 0035
|
BSIP: 0035
|
||||||
Title: A Solution To Something-For-Nothing Issue
|
Title: Mitigate Rounding Issue On Order Matching
|
||||||
Author: Abit More <https://github.com/abitmore>
|
Author: Abit More <https://github.com/abitmore>
|
||||||
Status: Draft
|
Status: Draft
|
||||||
Type: Protocol
|
Type: Protocol
|
||||||
|
@ -12,17 +12,15 @@
|
||||||
# Abstract
|
# Abstract
|
||||||
|
|
||||||
Under some circumstances, when two orders get matched, due to rounding,
|
Under some circumstances, when two orders get matched, due to rounding,
|
||||||
one order may be paying something but receiving nothing,
|
one order may be paying more than enough, even paying something but receiving
|
||||||
the other order may be paying nothing but receiving something.
|
nothing. This looks unfair.
|
||||||
This is the so-called something-for-nothing issue.
|
|
||||||
|
|
||||||
This looks clearly unfair.
|
This BSIP proposes an overall mechanism to mitigate rounding issue when matching
|
||||||
|
orders and avoid something-for-nothing issue completely.
|
||||||
|
|
||||||
This BSIP proposes an overall mechanism to avoid something-for-nothing issue
|
This BSIP also sets two principles for order matching:
|
||||||
completely.
|
* never pay more than enough, and
|
||||||
|
* something-for-nothing shouldn't happen.
|
||||||
This BSIP also sets a principle: something-for-nothing shouldn't happen when
|
|
||||||
matching orders.
|
|
||||||
|
|
||||||
# Motivation
|
# Motivation
|
||||||
|
|
||||||
|
@ -30,6 +28,9 @@ There are mechanisms in the system to try to avoid something-for-nothing issue,
|
||||||
however, not all scenarios are well-handled, see [bitshares-core
|
however, not all scenarios are well-handled, see [bitshares-core
|
||||||
issue #184](https://github.com/bitshares/bitshares-core/issues/184) for example.
|
issue #184](https://github.com/bitshares/bitshares-core/issues/184) for example.
|
||||||
|
|
||||||
|
Other than that, rounding issue occurs frequently and has led to a lot of
|
||||||
|
confusion among market participants.
|
||||||
|
|
||||||
# Rationale
|
# Rationale
|
||||||
|
|
||||||
## Amounts, Prices and Rounding
|
## Amounts, Prices and Rounding
|
||||||
|
@ -188,6 +189,10 @@ compare `X' = X * maker_b_amount` with `Y' = Y * maker_a_amount`.
|
||||||
`Y / taker_price`, which means `Y / X"` may be higher than `taker_price`,
|
`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.
|
that said, the taker order may has been filled at a less favorable price.
|
||||||
|
|
||||||
|
## Issues With The Chosen Solution
|
||||||
|
|
||||||
|
### The Something-for-nothing Issue
|
||||||
|
|
||||||
When filling a small order at a less favorable price, the receiving
|
When filling a small order at a less favorable price, the receiving
|
||||||
amount is often rounded down to zero, thus causes the something-for-nothing
|
amount is often rounded down to zero, thus causes the something-for-nothing
|
||||||
issue. Current code tried to solve the issue by cancelling the smaller order
|
issue. Current code tried to solve the issue by cancelling the smaller order
|
||||||
|
@ -207,43 +212,101 @@ may be paying something for nothing in current system):
|
||||||
* when force settling after an asset has been globally settled, paying the force
|
* when force settling after an asset has been globally settled, paying the force
|
||||||
settle order from global settlement fund, process the settle order
|
settle order from global settlement fund, process the settle order
|
||||||
|
|
||||||
## The Improved Solution (This BSIP)
|
### The Broader Rounding Issue
|
||||||
|
|
||||||
The detailed rules proposes in this BSIP (new rules highlighted):
|
Something-for-nothing is only a subset of rounding issues, it's the most extreme
|
||||||
* match in favor of taker, or say, match at maker price
|
one. There are much more scenarios that one of the matched parties would be
|
||||||
* round down receiving amounts when possible
|
paying more than enough, although they're not paying something for nothing
|
||||||
* when matching two limit orders, round down the receiving amounts in favor
|
overall. Some scenarios are discussed in [bitshares-core
|
||||||
of bigger order, or say, try to fill the smaller order
|
issue #342](https://github.com/bitshares/bitshares-core/issues/342).
|
||||||
* **if the smaller order would get nothing after the round-down, cancel it**
|
|
||||||
* when matching a limit order with a call order, in favor of call order,
|
Take a scenario similar to the one described in the 4th comment of
|
||||||
round down receiving collateral amount
|
[bitshare-core
|
||||||
* **if the call order is receiving the whole debt amount (so the short
|
issue #132](https://github.com/bitshares/bitshares-core/issues/132) as an
|
||||||
position will be closed) but paying nothing, let it pay 1 Satoshi
|
example:
|
||||||
(round up);**
|
* Alice's order: Sell CORE at `$3 / 80 = $0.0375`, balance `50 CORE`
|
||||||
* **otherwise, if the limit order would get nothing after the round-down,
|
* Bob's order: Buy CORE at `$19 / 500 = $0.038`, balance `$100`
|
||||||
cancel it (it's smaller, so safe to cancel)**
|
|
||||||
* when matching a settle order with a call order, in favor of call order,
|
Current system would process them as follows:
|
||||||
round down receiving collateral amount
|
* If Alice's order is maker, use `$3 / 80` as match price; since Alice's order
|
||||||
* **if the call order is receiving the whole debt amount (so the short
|
is smaller, round in favor of Bob's order, so Alice will pay the whole `50
|
||||||
position will be closed) but paying nothing, let it pay 1 Satoshi
|
CORE` and get `round_down(50 CORE * $3 / 80 CORE) = round_down($1.6) = $1`,
|
||||||
(round up);**
|
the effective price would be `$1 / 50 = $0.02`;
|
||||||
* **otherwise, if the settle order would be completely filled but would
|
* If Bob's order is maker, use `$19 / 500` as match price; since Alice's order
|
||||||
receive nothing, cancel it;**
|
is smaller, round in favor of Bob's order, so Alice will pay the whole `50
|
||||||
* **otherwise, it means both orders won't be completely filled, which may
|
CORE` and get `round_down(50 CORE * $19 / 500 CORE = round_down($1.9) = $1`,
|
||||||
due to hitting `maximum_force_settlement_volume`, in this case, don't fill
|
the effective price would still be `$1 / 50 = $0.02`.
|
||||||
any one of the two orders, and stop matching for this asset at this block;**
|
|
||||||
* **that said, only round up when the call order is completely filled, so
|
Both results are far from Alice's desired price `$0.0375`. Actually, according
|
||||||
won't trigger a black swan event, nor need to check for it.**
|
to Bob's desired price, paying `round_up($1 * 500 CORE / $19) = 27 CORE` would
|
||||||
* when globally settling, in favor of call order, round down receiving
|
be enough, then the effective price would be `$1 / 27 = $0.037`, which is
|
||||||
collateral amount
|
still below Alice's desired price `$0.0375`, but much closer than `$0.02`.
|
||||||
* **when the asset is not a prediction market, if a call order would pay
|
|
||||||
nothing, let it pay 1 Satoshi (round up).**
|
## The Improved Solution Proposed By This BSIP
|
||||||
* when paying a settle order from global settlement fund, in favor of global
|
|
||||||
settlement fund, round down receiving collateral amount
|
The detailed rules proposed by this BSIP with new rules highlighted:
|
||||||
* If the settling amount is equal to total supply of that asset, pay the
|
|
||||||
|
* 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).**
|
||||||
|
|
||||||
|
* 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,
|
||||||
|
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
|
||||||
|
something-for-nothing 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;
|
whole remaining settlement fund to the settle order;
|
||||||
* **when the asset is not a prediction market, if the settle order would
|
* otherwise, in favor of global settlement fund since its volume is bigger,
|
||||||
receive nothing, raise an exception (aka let the operation fail).**
|
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 [bitshares-core
|
Take the example mentioned in the 4th comment of [bitshares-core
|
||||||
issue #132](https://github.com/bitshares/bitshares-core/issues/132):
|
issue #132](https://github.com/bitshares/bitshares-core/issues/132):
|
||||||
|
@ -256,55 +319,106 @@ Process:
|
||||||
since Bob's order is smaller, round in favor of Alice's order,
|
since Bob's order is smaller, round in favor of Alice's order,
|
||||||
so Bob will get
|
so Bob will get
|
||||||
`round_down($10 * 8 CORE / $3) = round_down(26.67 CORE) = 26 CORE`,
|
`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`.
|
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
|
* 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
|
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`,
|
`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`.
|
the effective price would still be `$10 / 26 CORE = $0.3846`.
|
||||||
* If Alice's order is a call order, always round in favor of it, we get
|
* If Alice's order is a call order, since it's bigger, round in favor of it,
|
||||||
same results.
|
we will get same results.
|
||||||
|
|
||||||
|
### Example 2
|
||||||
|
|
||||||
If we change the example to this:
|
If we change the example to this:
|
||||||
* Alice's order: Buy CORE at `3 CORE / $8 = 0.375`, balance `$1000000`
|
* 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`
|
* Bob's order: Sell CORE at `19 CORE / $50 = 0.38`, balance `10 CORE`
|
||||||
|
|
||||||
Process:
|
Process:
|
||||||
* If both orders are limit orders, we get same results as above
|
* If both orders are limit orders, we get similar results as above.
|
||||||
* If Bob's order is a call order, we should always round in favor of it,
|
* If Bob's order is a call order, it should have a debt amount which is an
|
||||||
however, it should have a debt amount which is an integer, for example
|
integer, for example `$26`, then
|
||||||
`$27`, then Alice would get
|
* Alice would get
|
||||||
* `round_down(27 * 3 / 8) = round_down(10.125) = 10 CORE` as a maker, or
|
* `round_up(26 * 3 / 8) = round_up(9.75) = 10 CORE` as a maker, or
|
||||||
* `round_down(27 * 19 / 50) = round_down(10.26) = 10 CORE` as a taker.
|
* `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
|
# Specifications
|
||||||
|
|
||||||
## When Matching Two Limit Orders
|
## When Matching Two Limit Orders
|
||||||
|
|
||||||
|
### Handling Something-For-Nothing Issue
|
||||||
|
|
||||||
In `match( const limit_order_object&, OrderType ... )` function of `database`
|
In `match( const limit_order_object&, OrderType ... )` function of `database`
|
||||||
class, after calculated `usd_receives` which is for the taker,
|
class, after calculated `usd_receives` which is for the taker,
|
||||||
check if it is zero.
|
check if it is zero.
|
||||||
If the answer is `true`, skip filling and see the order is filled, return `1`,
|
If the answer is `true`, skip filling and see the order is filled, return `1`,
|
||||||
so the order will be cancelled later.
|
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
|
## When Matching A Limit Order With A Call Order
|
||||||
|
|
||||||
In `check_call_orders(...)` function of `database` class,
|
In `check_call_orders(...)` function of `database` class,
|
||||||
after calculated `order_receives`, check if it is zero.
|
if the call order is smaller, round up `order_receives`,
|
||||||
If the answer is `true`,
|
otherwise round down `order_receives`.
|
||||||
* if `call_receives` is equal to `call_itr->debt`, set `order_receives` to `1`;
|
|
||||||
* otherwise, skip filling and cancel the limit order.
|
In the latter case, if `order_receives` is zero, skip filling and cancel the
|
||||||
|
limit order.
|
||||||
|
|
||||||
## When Matching A Settle Order With A Call Order
|
## When Matching A Settle Order With A Call Order
|
||||||
|
|
||||||
In `match( const call_order_object&, ... )` function of `database` class,
|
In `match( const call_order_object&, ... )` function of `database` class,
|
||||||
after calculated `call_pays`, check if it is zero.
|
if the call order is smaller, round up `call_pays`,
|
||||||
If the answer is `true`,
|
otherwise round down `call_pays`.
|
||||||
* if `call_receives` is equal to `call_debt`, set `call_pays` to `1`;
|
|
||||||
* otherwise, if `call_receives` is equal to `settle.balance`,
|
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`,
|
call `cancel_order(...)` with parameter set to `settle`,
|
||||||
then return a zero-amount collateral asset object;
|
then return a zero-amount collateral asset object;
|
||||||
* otherwise, return a zero-amount collateral asset object directly.
|
* otherwise, return a zero-amount 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
|
After returned, need to check the amount of returned asset at where calling the
|
||||||
`match(...)` function, specifically, `clear_expired_orders()` function of
|
`match(...)` function, specifically, `clear_expired_orders()` function of
|
||||||
|
@ -315,15 +429,22 @@ need to check the label, if found it's completed, process next asset.
|
||||||
|
|
||||||
## When Globally Settling
|
## When Globally Settling
|
||||||
|
|
||||||
In `global_settle_asset(...)` function of `database` class, check each `pays`,
|
In `global_settle_asset(...)` function of `database` class, round up `pays`.
|
||||||
once it's zero, and the asset is not a prediction market, let it be `1`.
|
|
||||||
|
|
||||||
## When Paying A Settle Order From Global Settlement Fund
|
## When Paying A Settle Order From Global Settlement Fund
|
||||||
|
|
||||||
In `do_apply(...)` function of `asset_settle_evaluator` class,
|
In `do_apply(...)` function of `asset_settle_evaluator` class,
|
||||||
after calculated `settled_amount` and adjusted it according to the "total
|
after calculated `settled_amount` and adjusted it according to the "total
|
||||||
supply" rule, check if it's zero. If the answer is `true`,
|
supply" rule, check if it's zero.
|
||||||
and the asset is not a prediction maket, throw a `fc::exception`.
|
|
||||||
|
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
|
# Discussion
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue