Skip to content
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

BSIP38 add target_cr option to call order #838

Merged
merged 26 commits into from
May 2, 2018
Merged

Conversation

abitmore
Copy link
Member

@abitmore abitmore commented Apr 10, 2018

This is PR for #834, aka BSIP38: Add target collateral ratio option to short positions.

The code is ready for review, but still need (lots of) test cases.

  • wrapping a call_order_update_operation with extension in a proposal is not allowed before hard fork
  • difficult edge cases e.g. mssr=1001 and mcr=tcr=1002 or something
  • matching then generate block

@abitmore abitmore added this to the 201805 - Consensus Changing Release milestone Apr 10, 2018
/** this is slightly more expensive than limit orders, this pricing impacts prediction markets */
struct fee_parameters_type { uint64_t fee = 20 * GRAPHENE_BLOCKCHAIN_PRECISION; };

asset fee;
account_id_type funding_account; ///< pays fee, collateral, and cover
asset delta_collateral; ///< the amount of collateral to add to the margin position
asset delta_debt; ///< the amount of the debt to be paid off, may be negative to issue new debt
extensions_type extensions;

typedef vector<extension<options_type>> extension_type; // use a vector here to be compatible with old JSON
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapping the extension in a 1-element-vector is ugly. Is it really worth it?
I suppose that everyone is ignoring the extensions at this time, so I'd tend to sacrifice JSON compatibility for future readability.

Copy link
Member Author

@abitmore abitmore Apr 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sacrificing JSON compatibility means all UI / bot / client software need to update before any API node is updated (before and after hard fork), and need to use different JSON format for different version of nodes (before hardfork). It affects not only reading from the blockchain, but also broadcasting new transactions. Giving that we have many different clients now, I don't think it's a good idea to break the compatibility. (I personally is using a 2016 build of bitshares-ui and don't want to upgrade :P)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually for clients that submit extensions, an empty array is treated like an empty object: https://github.com/bitshares/bitshares-core/blob/master/libraries/chain/include/graphene/chain/protocol/ext.hpp#L169

So we're breaking compatibility only when reading from the chain, and even there it won't matter in client languages that treat objects and arrays mostly the same (like JavaScript).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So old clients can still broad transactions with new nodes even without the vector. That makes sense. I'll change this as suggested.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed vector.

call.call_price = price::call_price(call.get_debt(), call.get_collateral(),
_bitasset_data->current_feed.maintenance_collateral_ratio);
}
call.target_collateral_ratio = new_target_cr;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usual semantics of *_update operations is "if absent leave untouched", while here it is "if absent delete". This is confusing.
Perhaps make the extension field an optional<optional<uint16_t>>, or use a special value like 0 for deletion.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think optional<optional<uint16_t>> can be jsonified correctly.

Since type of that field is optional already, it would be always confusing about how to delete it, IMHO it's easier to treat it as an always-required field, aka always update.

Another approach is to use 2 fields, one for existence and the other for the real value. But I think it's too complicated since the optional type is just for doing this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd still prefer to keep the "if absent leave untouched" semantic. I suppose the option will not be available in the UI (all UI's) very soon, so someone who is used to trade via UI might want to set the tcr once via cli_wallet and continue normal operation using the UI.

Since tcr < 1000 doesn't make sense anyway, you could treat and invalid value (i. e. < 1000) as a deliberate delete request.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think it's clearer to just update as is (which is written in BSIP).

Compare the two approaches:

  1. update as is
  • found null in op -> set null in obj
  • found x in op -> set x in obj
  1. update by checking if it's a special number
  • found null in op -> don't change obj (no matter what it was)
  • found 0 in op -> set null in obj
  • found x(>0) in op -> set x(>0) in obj

IMHO the 2nd one will lead to more confusion.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thing related is that call_order_update_operation is not only for updating a call order, but also for creating and removing, although removing is irrelevant here.

If the option is in "if absent leave untouched" semantic, the user may expect that the system will "remember" the option when a position is closed (either intendedly or unintendedly), also sometimes may forget to set when recreating a new position, both may lead to unintended behavior.

Compare the two approaches (I think the 1st one is better):

  1. create new position as is
  • found null in op -> set null in obj
  • found x in op -> set x in obj
  1. create new position checking if it's a special number
  • found null in op -> set null in obj?
  • found 0 in op -> set null in obj
  • found x(>0) in op -> set x(>0) in obj

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, seems we have to agree to disagree (again ;-) ).

BSIP is authoritative though, so I'm OK with leaving it as it is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps propose a BSIP change? E.G. drop optional from object, use value 0 as "not set" instead.


/// Calculate maximum quantity of collateral to sell and debt to cover to meet @ref target_collateral_ratio.
/// @return a pair of assets, the first item is collateral to sell, the second is debt to cover
pair<asset, asset> get_max_sell_receive_pair( price match_price,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why return both? You only ever use the second member of the return value.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, in the beginning, I only returned the first member; while coding I noticed that the second is needed, so changed it to return a pair; at last I found the first is not being used in main code, but is heavily used in test cases, in addition I'm not sure whether it will be useful for potential new APIs, so decided to leave it there.

Do you think it's necessary to change it to only return the second?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not strictly necessary, but a significant simplification IMO.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed pair.


max_amount_to_sell = ( (debt + x) * target_CR - collateral * feed_price )
/ (target_CR * match_price - feed_price)
= ( (debt + x) * tCR / DENOM - collateral * fp_debt_amt / fp_coll_amt + target_CR * x )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last product in this line (target_CR*x) is too much.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Updated.

const uint16_t maintenance_collateral_ratio )const
{ try {
// make sure feed_price is in collateral / debt format
if( feed_price.base.asset_id != call_price.base.asset_id )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check should always be true (feed price is defined as debt/collateral, see asset.hpp).
(That's just a remark. Probably a good idea to be defensive here.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the caller may pass in a reversed price pair.

if( feed_price.base.asset_id != call_price.base.asset_id )
feed_price = ~feed_price;

FC_ASSERT( feed_price.base.asset_id == call_price.base.asset_id
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should always be true, by definition.
(That's just a remark. Probably a good idea to be defensive here.)

GRAPHENE_REQUIRE_THROW( borrow( alice, bitusd.amount(100000), core.amount(400000), 1750 ), fc::exception );
GRAPHENE_REQUIRE_THROW( borrow( alice, bitusd.amount(100000), core.amount(400000), 1751 ), fc::exception );
GRAPHENE_REQUIRE_THROW( borrow( alice, bitusd.amount(100000), core.amount(400000), 65535 ), fc::exception );

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test that wraps a call_order_update with extension in a proposal.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's planned but not yet done. Will do definitely.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added proposal tests.

" sell full (perfect), sell full (<0.01%), sell full (<0.1%),sell full (<1%), sell full (other), ...,"
" sell some (perfect), sell some (<0.01%), sell some (<0.1%),sell some (<1%), sell some (other), ... ]" );
idump( (total)(count) );

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add some tests for the difficult edge cases. I suppose mssr=1001 and mcr=tcr=1002 or something will be problematic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added 1001/1002 to random tests. The result shows that ~99.6% will cover full debt.

if( extensions.size() > 0 )
{
const auto& options = extensions.front().value;
size_t count = 0; // use a count variable for future expansions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't make assumptions about the logic of future extensions. Just check that tcr is valid right now, instead of counting stuff. (See account extensions for example.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO nonempty is basic requirement for extensions field. Additional validation may or may not apply when adding new fields (no more validation this time since TCR is optional). Btw there are more fields to be added as discussed in bitshares/bsips#51, rather than counting after added them, I decided to count from now on.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In regards to account extensions, since it's not using a container which can jsonify to an array, but replaced flat_set<static_variant<X>> with extension<Y> directly (thus have broken JSON compatibility), it should be allowed if the extensions field is empty. So there are individual validations about the fields inside.

I used a vector here, so doesn't make sense to allow an empty object inside. The validation code here is for different things.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even when more fields are added, the actual count is irrelevant, as long as it is > 0.
If we remove the vector wrapper, count==0 will have to be accepted, which makes the whole check here useless.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed vector, so removed validation.

@@ -97,15 +97,19 @@ namespace graphene { namespace chain {
const asset& a = *this;
if( a.asset_id == b.base.asset_id )
{
// TODO replace exception type with price_multiplication_undefined?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove these unrelated comments. If you think this might be a good idea, open a github issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realized that the definitions of those exceptions are commented out. Will remove the unrelated comments.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

@abitmore abitmore changed the base branch from bsip35-order-rounding to hardfork April 26, 2018 19:38
@abitmore
Copy link
Member Author

More test cases added. Ready for final review.

@abitmore abitmore requested a review from pmconrad May 1, 2018 15:00
tx.operations.push_back( prop );
db.current_fee_schedule().set_fee( tx.operations.back() );
set_expiration( db, tx );
db.push_transaction(trx, ~0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're pushing the wrong transaction here... :-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch 💯

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kind of bug is hard to detect, especially when the code is in a lambda, changing [&] to [this] here doesn't help. :-(

What's best practice?

@abitmore abitmore merged commit 14c9786 into hardfork May 2, 2018
@abitmore abitmore deleted the bsip38-call-target-cr branch May 13, 2018 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants