From dad1ca3beeed6186585ab509d15c86cd630157ec Mon Sep 17 00:00:00 2001 From: Nathan Hourt Date: Fri, 26 Jun 2015 15:11:41 -0400 Subject: [PATCH] Refactor: Move limit order execution to database This logic was previously located in limit_order_create_evaluator, but other code may need it in the future, so it should be made available at the database level. --- libraries/chain/asset_evaluator.cpp | 38 ++++++- libraries/chain/asset_object.cpp | 2 + libraries/chain/db_market.cpp | 107 +++++++++--------- .../include/graphene/chain/asset_object.hpp | 1 + .../chain/include/graphene/chain/config.hpp | 5 +- .../chain/include/graphene/chain/database.hpp | 11 +- .../chain/include/graphene/chain/types.hpp | 4 +- libraries/chain/limit_order_evaluator.cpp | 75 +++--------- 8 files changed, 118 insertions(+), 125 deletions(-) diff --git a/libraries/chain/asset_evaluator.cpp b/libraries/chain/asset_evaluator.cpp index 536450cd..4e78b808 100644 --- a/libraries/chain/asset_evaluator.cpp +++ b/libraries/chain/asset_evaluator.cpp @@ -54,7 +54,11 @@ void_result asset_create_evaluator::do_evaluate( const asset_create_operation& o const asset_object& backing_backing = backing_bitasset_data.options.short_backing_asset(d); FC_ASSERT( !backing_backing.is_market_issued(), "May not create a bitasset backed by a bitasset backed by a bitasset." ); - } + FC_ASSERT( op.issuer != GRAPHENE_COMMITTEE_ACCOUNT || backing_backing.get_id() == asset_id_type(), + "May not create a blockchain-controlled market asset which is not backed by CORE."); + } else + FC_ASSERT( op.issuer != GRAPHENE_COMMITTEE_ACCOUNT || backing.get_id() == asset_id_type(), + "May not create a blockchain-controlled market asset which is not backed by CORE."); FC_ASSERT( op.bitasset_options->feed_lifetime_sec > chain_parameters.block_interval && op.bitasset_options->force_settlement_delay_sec > chain_parameters.block_interval ); } @@ -192,13 +196,28 @@ void_result asset_update_evaluator::do_evaluate(const asset_update_operation& o) { try { database& d = db(); - if( o.new_issuer ) FC_ASSERT(d.find_object(*o.new_issuer)); - const asset_object& a = o.asset_to_update(d); auto a_copy = a; a_copy.options = o.new_options; a_copy.validate(); + if( o.new_issuer ) + { + FC_ASSERT(d.find_object(*o.new_issuer)); + if( a.is_market_issued() && *o.new_issuer == GRAPHENE_COMMITTEE_ACCOUNT ) + { + const asset_object& backing = a.bitasset_data(d).options.short_backing_asset(d); + if( backing.is_market_issued() ) + { + const asset_object& backing_backing = backing.bitasset_data(d).options.short_backing_asset(d); + FC_ASSERT( backing_backing.get_id() == asset_id_type(), + "May not create a blockchain-controlled market asset which is not backed by CORE."); + } else + FC_ASSERT( backing.get_id() == asset_id_type(), + "May not create a blockchain-controlled market asset which is not backed by CORE."); + } + } + //There must be no bits set in o.permissions which are unset in a.issuer_permissions. FC_ASSERT(!(o.new_options.issuer_permissions & ~a.options.issuer_permissions), "Cannot reinstate previously revoked issuer permissions on an asset."); @@ -257,6 +276,19 @@ void_result asset_update_bitasset_evaluator::do_evaluate(const asset_update_bita { FC_ASSERT(a.dynamic_asset_data_id(d).current_supply == 0); FC_ASSERT(d.find_object(o.new_options.short_backing_asset)); + + if( a.issuer == GRAPHENE_COMMITTEE_ACCOUNT ) + { + const asset_object& backing = a.bitasset_data(d).options.short_backing_asset(d); + if( backing.is_market_issued() ) + { + const asset_object& backing_backing = backing.bitasset_data(d).options.short_backing_asset(d); + FC_ASSERT( backing_backing.get_id() == asset_id_type(), + "May not create a blockchain-controlled market asset which is not backed by CORE."); + } else + FC_ASSERT( backing.get_id() == asset_id_type(), + "May not create a blockchain-controlled market asset which is not backed by CORE."); + } } bitasset_to_update = &b; diff --git a/libraries/chain/asset_object.cpp b/libraries/chain/asset_object.cpp index f5bab206..a1694a68 100644 --- a/libraries/chain/asset_object.cpp +++ b/libraries/chain/asset_object.cpp @@ -16,6 +16,8 @@ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include +#include +#include #include diff --git a/libraries/chain/db_market.cpp b/libraries/chain/db_market.cpp index 3822566f..28e7fa1c 100644 --- a/libraries/chain/db_market.cpp +++ b/libraries/chain/db_market.cpp @@ -109,7 +109,49 @@ void database::cancel_order( const limit_order_object& order, bool create_virtua // TODO: create a virtual cancel operation } - remove( order ); + remove(order); +} + +bool database::apply_order(const limit_order_object& new_order_object, bool allow_black_swan) +{ + auto order_id = new_order_object.id; + const asset_object& sell_asset = get(new_order_object.amount_for_sale().asset_id); + const asset_object& receive_asset = get(new_order_object.amount_to_receive().asset_id); + + // Possible optimization: We only need to check calls if both are true: + // - The new order is at the front of the book + // - The new order is below the call limit price + bool called_some = check_call_orders(sell_asset, allow_black_swan); + called_some |= check_call_orders(receive_asset, allow_black_swan); + if( called_some && !find_object(order_id) ) // then we were filled by call order + return true; + + const auto& limit_price_idx = get_index_type().indices().get(); + + // TODO: it should be possible to simply check the NEXT/PREV iterator after new_order_object to + // determine whether or not this order has "changed the book" in a way that requires us to + // check orders. For now I just lookup the lower bound and check for equality... this is log(n) vs + // constant time check. Potential optimization. + + auto max_price = ~new_order_object.sell_price; + auto limit_itr = limit_price_idx.lower_bound(max_price.max()); + auto limit_end = limit_price_idx.upper_bound(max_price); + + bool finished = false; + while( !finished && limit_itr != limit_end ) + { + auto old_limit_itr = limit_itr; + ++limit_itr; + // match returns 2 when only the old order was fully filled. In this case, we keep matching; otherwise, we stop. + finished = (match(new_order_object, *old_limit_itr, old_limit_itr->sell_price) != 2); + } + + //Possible optimization: only check calls if the new order completely filled some old order + //Do I need to check both assets? + check_call_orders(sell_asset, allow_black_swan); + check_call_orders(receive_asset, allow_black_swan); + + return find_object(order_id) == nullptr; } /** @@ -306,7 +348,7 @@ bool database::fill_order(const force_settlement_object& settle, const asset& pa /** * Starting with the least collateralized orders, fill them if their - * call price is above the max(lowest bid,call_limit). + * call price is above the max(lowest bid,call_limit). * * This method will return true if it filled a short or limit * @@ -316,7 +358,7 @@ bool database::fill_order(const force_settlement_object& settle, const asset& pa * * @return true if a margin call was executed. */ -bool database::check_call_orders( const asset_object& mia, bool enable_black_swan ) +bool database::check_call_orders(const asset_object& mia, bool enable_black_swan) { try { if( !mia.is_market_issued() ) return false; const asset_bitasset_data_object& bitasset = mia.bitasset_data(*this); @@ -333,42 +375,13 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa auto max_price = price::max( mia.id, bitasset.options.short_backing_asset ); // stop when limit orders are selling too little USD for too much CORE auto min_price = bitasset.current_feed.max_short_squeeze_price(); - /* - // edump((bitasset.current_feed)); - edump((min_price.to_real())(min_price)); - edump((max_price.to_real())(max_price)); - //auto min_price = price::min( mia.id, bitasset.options.short_backing_asset ); - idump((bitasset.current_feed.settlement_price)(bitasset.current_feed.settlement_price.to_real())); - { - for( const auto& order : limit_price_index ) - wdump((order)(order.sell_price.to_real())); - - for( const auto& call : call_price_index ) - idump((call)(call.call_price.to_real())); - - // limit pirce index is sorted from highest price to lowest price. - //auto limit_itr = limit_price_index.lower_bound( price::max( mia.id, bitasset.options.short_backing_asset ) ); - wdump((max_price)(max_price.to_real())); - wdump((min_price)(min_price.to_real())); - } - */ assert( max_price.base.asset_id == min_price.base.asset_id ); - // wlog( "from ${a} Debt/Col to ${b} Debt/Col ", ("a", max_price.to_real())("b",min_price.to_real()) ); // NOTE limit_price_index is sorted from greatest to least auto limit_itr = limit_price_index.lower_bound( max_price ); - auto limit_end = limit_price_index.upper_bound( min_price ); + auto limit_end = limit_price_index.upper_bound( min_price ); - /* - if( limit_itr != limit_price_index.end() ) - wdump((*limit_itr)(limit_itr->sell_price.to_real())); - if( limit_end != limit_price_index.end() ) - wdump((*limit_end)(limit_end->sell_price.to_real())); -*/ - - if( limit_itr == limit_end ) - { -// wlog( "no orders available to fill margin calls" ); + if( limit_itr == limit_end ) { return false; } @@ -389,32 +402,26 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa usd_for_sale = limit_itr->amount_for_sale(); } else return filled_limit; -// wdump((match_price)); -// edump((usd_for_sale)); match_price.validate(); -// wdump((match_price)(~call_itr->call_price) ); if( match_price > ~call_itr->call_price ) { return filled_limit; } auto usd_to_buy = call_itr->get_debt(); -// edump((usd_to_buy)); if( usd_to_buy * match_price > call_itr->get_collateral() ) { FC_ASSERT( enable_black_swan ); - //elog( "black swan, we do not have enough collateral to cover at this price" ); - globally_settle_asset( mia, call_itr->get_debt() / call_itr->get_collateral() ); + globally_settle_asset(mia, call_itr->get_debt() / call_itr->get_collateral()); return true; } asset call_pays, call_receives, order_pays, order_receives; if( usd_to_buy >= usd_for_sale ) { // fill order - //ilog( "filling all of limit order" ); call_receives = usd_for_sale; order_receives = usd_for_sale * match_price; call_pays = order_receives; @@ -422,9 +429,7 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa filled_limit = true; filled_call = (usd_to_buy == usd_for_sale); - } - else // fill call - { + } else { // fill call call_receives = usd_to_buy; order_receives = usd_to_buy * match_price; call_pays = order_receives; @@ -435,10 +440,10 @@ bool database::check_call_orders( const asset_object& mia, bool enable_black_swa auto old_call_itr = call_itr; if( filled_call ) ++call_itr; - fill_order( *old_call_itr, call_pays, call_receives ); + fill_order(*old_call_itr, call_pays, call_receives); auto old_limit_itr = filled_limit ? limit_itr++ : limit_itr; - fill_order( *old_limit_itr, order_pays, order_receives ); + fill_order(*old_limit_itr, order_pays, order_receives); } // whlie call_itr != call_end return filled_limit; @@ -454,16 +459,6 @@ void database::pay_order( const account_object& receiver, const asset& receives, adjust_balance(receiver.get_id(), receives); } -/** - * For Market Issued assets Managed by Delegates, any fees collected in the MIA need - * to be sold and converted into CORE by accepting the best offer on the table. - */ -bool database::convert_fees( const asset_object& mia ) -{ - if( mia.issuer != account_id_type() ) return false; - return false; -} - asset database::calculate_market_fee( const asset_object& trade_asset, const asset& trade_amount ) { assert( trade_asset.id == trade_amount.asset_id ); diff --git a/libraries/chain/include/graphene/chain/asset_object.hpp b/libraries/chain/include/graphene/chain/asset_object.hpp index 7f81e274..ce15b0ba 100644 --- a/libraries/chain/include/graphene/chain/asset_object.hpp +++ b/libraries/chain/include/graphene/chain/asset_object.hpp @@ -33,6 +33,7 @@ namespace graphene { namespace chain { class account_object; + class database; using namespace graphene::db; /** diff --git a/libraries/chain/include/graphene/chain/config.hpp b/libraries/chain/include/graphene/chain/config.hpp index 75da2610..dc875d66 100644 --- a/libraries/chain/include/graphene/chain/config.hpp +++ b/libraries/chain/include/graphene/chain/config.hpp @@ -64,8 +64,8 @@ /** * These ratios are fixed point numbers with a denominator of GRAPHENE_COLLATERAL_RATIO_DENOM, the - * minimum maitenance collateral is therefore 1.001x and the default - * maintenance ratio is 1.75x + * minimum maitenance collateral is therefore 1.001x and the default + * maintenance ratio is 1.75x */ ///@{ #define GRAPHENE_COLLATERAL_RATIO_DENOM 1000 @@ -92,6 +92,7 @@ #define GRAPHENE_DEFAULT_BURN_PERCENT_OF_FEE (20*GRAPHENE_1_PERCENT) #define GRAPHENE_WITNESS_PAY_PERCENT_PRECISION (1000000000) #define GRAPHENE_DEFAULT_MAX_ASSERT_OPCODE 1 +#define GRAPHENE_DEFAULT_FEE_LIQUIDATION_THRESHOLD GRAPHENE_BLOCKCHAIN_PRECISION * 100; #define GRAPHENE_GENESIS_TIMESTAMP (1431700000) /// Should be divisible by GRAPHENE_DEFAULT_BLOCK_INTERVAL #define GRAPHENE_MAX_WORKER_NAME_LENGTH 63 diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index 2d340c00..9e003fe6 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -348,6 +348,16 @@ namespace graphene { namespace chain { void cancel_order(const force_settlement_object& order, bool create_virtual_op = true); void cancel_order(const limit_order_object& order, bool create_virtual_op = true); + /** + * @brief Process a new limit order through the markets + * @param order The new order to process + * @return true if order was completely filled; false otherwise + * + * This function takes a new limit order, and runs the markets attempting to match it with existing orders + * already on the books. + */ + bool apply_order(const limit_order_object& new_order_object, bool allow_black_swan = true); + /** * Matches the two orders, * @@ -381,7 +391,6 @@ namespace graphene { namespace chain { // helpers to fill_order void pay_order( const account_object& receiver, const asset& receives, const asset& pays ); - bool convert_fees( const asset_object& mia ); asset calculate_market_fee(const asset_object& recv_asset, const asset& trade_amount); asset pay_market_fees( const asset_object& recv_asset, const asset& receives ); diff --git a/libraries/chain/include/graphene/chain/types.hpp b/libraries/chain/include/graphene/chain/types.hpp index e537a9ba..13669f77 100644 --- a/libraries/chain/include/graphene/chain/types.hpp +++ b/libraries/chain/include/graphene/chain/types.hpp @@ -355,7 +355,7 @@ namespace graphene { namespace chain { uint64_t account_len5_fee = 5*UINT64_C(1000000000); ///< about $100 uint64_t account_len4_fee = 5*UINT64_C(2000000000); ///< about $200 uint64_t account_len3_fee = 5*3000000000; ///< about $300 - uint64_t account_len2_fee = 5*4000000000; ///< about $400 + uint64_t account_len2_fee = 5*4000000000; ///< about $400 uint32_t asset_create_fee = 5ll*500000000; ///< about $35 for LTM, the cost to register the cheapest asset uint32_t asset_update_fee = 150000; ///< the cost to modify a registered asset uint32_t asset_issue_fee = 700000; ///< the cost to print a UIA and send it to an account @@ -456,6 +456,7 @@ namespace graphene { namespace chain { share_type witness_pay_per_block = GRAPHENE_DEFAULT_WITNESS_PAY_PER_BLOCK; ///< CORE to be allocated to witnesses (per block) share_type worker_budget_per_day = GRAPHENE_DEFAULT_WORKER_BUDGET_PER_DAY; ///< CORE to be allocated to workers (per day) uint16_t max_predicate_opcode = GRAPHENE_DEFAULT_MAX_ASSERT_OPCODE; ///< predicate_opcode must be less than this number + share_type fee_liquidation_threshold = GRAPHENE_DEFAULT_FEE_LIQUIDATION_THRESHOLD; ///< value in CORE at which accumulated fees in blockchain-issued market assets should be liquidated void validate()const { @@ -614,6 +615,7 @@ FC_REFLECT( graphene::chain::chain_parameters, (witness_pay_per_block) (worker_budget_per_day) (max_predicate_opcode) + (fee_liquidation_threshold) ) FC_REFLECT_TYPENAME( graphene::chain::share_type ) diff --git a/libraries/chain/limit_order_evaluator.cpp b/libraries/chain/limit_order_evaluator.cpp index 7560357c..49f7713e 100644 --- a/libraries/chain/limit_order_evaluator.cpp +++ b/libraries/chain/limit_order_evaluator.cpp @@ -21,7 +21,7 @@ #include namespace graphene { namespace chain { -void_result limit_order_create_evaluator::do_evaluate( const limit_order_create_operation& op ) +void_result limit_order_create_evaluator::do_evaluate(const limit_order_create_operation& op) { try { database& d = db(); @@ -32,9 +32,9 @@ void_result limit_order_create_evaluator::do_evaluate( const limit_order_create_ _receive_asset = &op.min_to_receive.asset_id(d); if( _sell_asset->options.whitelist_markets.size() ) - FC_ASSERT( _sell_asset->options.whitelist_markets.find( _receive_asset->id ) != _sell_asset->options.whitelist_markets.end() ); + FC_ASSERT( _sell_asset->options.whitelist_markets.find(_receive_asset->id) != _sell_asset->options.whitelist_markets.end() ); if( _sell_asset->options.blacklist_markets.size() ) - FC_ASSERT( _sell_asset->options.blacklist_markets.find( _receive_asset->id ) == _sell_asset->options.blacklist_markets.end() ); + FC_ASSERT( _sell_asset->options.blacklist_markets.find(_receive_asset->id) == _sell_asset->options.blacklist_markets.end() ); if( _sell_asset->enforce_white_list() ) FC_ASSERT( _seller->is_authorized_asset( *_sell_asset ) ); if( _receive_asset->enforce_white_list() ) FC_ASSERT( _seller->is_authorized_asset( *_receive_asset ) ); @@ -45,10 +45,10 @@ void_result limit_order_create_evaluator::do_evaluate( const limit_order_create_ return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } -object_id_type limit_order_create_evaluator::do_apply( const limit_order_create_operation& op ) +object_id_type limit_order_create_evaluator::do_apply(const limit_order_create_operation& op) { try { const auto& seller_stats = _seller->statistics(db()); - db().modify( seller_stats, [&]( account_statistics_object& bal ){ + db().modify(seller_stats, [&](account_statistics_object& bal) { if( op.amount_to_sell.asset_id == asset_id_type() ) { bal.total_core_in_orders += op.amount_to_sell.amount; @@ -57,70 +57,21 @@ object_id_type limit_order_create_evaluator::do_apply( const limit_order_create_ db().adjust_balance(op.seller, -op.amount_to_sell); - const auto& new_order_object = db().create( [&]( limit_order_object& obj ){ + const auto& new_order_object = db().create([&](limit_order_object& obj){ obj.seller = _seller->id; obj.for_sale = op.amount_to_sell.amount; obj.sell_price = op.get_price(); obj.expiration = op.expiration; }); - limit_order_id_type result = new_order_object.id; // save this because we may remove the object by filling it + limit_order_id_type order_id = new_order_object.id; // save this because we may remove the object by filling it + bool filled = db().apply_order(new_order_object); - // Possible optimization: We only need to check calls if both are true: - // - The new order is at the front of the book - // - The new order is below the call limit price - bool called_some = db().check_call_orders(*_sell_asset); - called_some |= db().check_call_orders(*_receive_asset); - if( called_some && !db().find(result) ) // then we were filled by call order - return result; + FC_ASSERT( !op.fill_or_kill || filled ); - const auto& limit_order_idx = db().get_index_type(); - const auto& limit_price_idx = limit_order_idx.indices().get(); - - // TODO: it should be possible to simply check the NEXT/PREV iterator after new_order_object to - // determine whether or not this order has "changed the book" in a way that requires us to - // check orders. For now I just lookup the lower bound and check for equality... this is log(n) vs - // constant time check. Potential optimization. - - auto max_price = ~op.get_price(); //op.min_to_receive / op.amount_to_sell; - auto limit_itr = limit_price_idx.lower_bound( max_price.max() ); - auto limit_end = limit_price_idx.upper_bound( max_price ); - - for( auto tmp = limit_itr; tmp != limit_end; ++tmp ) - { - assert( tmp != limit_price_idx.end() ); - } - - bool filled = false; - //if( new_order_object.amount_to_receive().asset_id(db()).is_market_issued() ) - if( _receive_asset->is_market_issued() ) - { // then we may also match against shorts - if( _receive_asset->bitasset_data(db()).options.short_backing_asset == asset_id_type() ) - { - bool converted_some = db().convert_fees( *_receive_asset ); - // just incase the new order was completely filled from fees - if( converted_some && !db().find(result) ) // then we were filled by call order - return result; - } - } - - while( !filled && limit_itr != limit_end ) - { - auto old_limit_itr = limit_itr; - ++limit_itr; - filled = (db().match( new_order_object, *old_limit_itr, old_limit_itr->sell_price ) != 2 ); - } - - //Possible optimization: only check calls if the new order completely filled some old order - //Do I need to check both assets? - db().check_call_orders(*_sell_asset); - db().check_call_orders(*_receive_asset); - - FC_ASSERT( !op.fill_or_kill || db().find_object(result) == nullptr ); - - return result; + return order_id; } FC_CAPTURE_AND_RETHROW( (op) ) } -void_result limit_order_cancel_evaluator::do_evaluate( const limit_order_cancel_operation& o ) +void_result limit_order_cancel_evaluator::do_evaluate(const limit_order_cancel_operation& o) { try { database& d = db(); @@ -130,7 +81,7 @@ void_result limit_order_cancel_evaluator::do_evaluate( const limit_order_cancel_ return void_result(); } FC_CAPTURE_AND_RETHROW( (o) ) } -asset limit_order_cancel_evaluator::do_apply( const limit_order_cancel_operation& o ) +asset limit_order_cancel_evaluator::do_apply(const limit_order_cancel_operation& o) { try { database& d = db(); @@ -138,7 +89,7 @@ asset limit_order_cancel_evaluator::do_apply( const limit_order_cancel_operation auto quote_asset = _order->sell_price.quote.asset_id; auto refunded = _order->amount_for_sale(); - db().cancel_order( *_order, false /* don't create a virtual op*/ ); + db().cancel_order(*_order, false /* don't create a virtual op*/); // Possible optimization: order can be called by canceling a limit order iff the canceled order was at the top of the book. // Do I need to check calls in both assets?