From 9f6edc649d138a5109954a6a31c70969f423a550 Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Thu, 23 Mar 2017 16:22:25 -0400 Subject: [PATCH] Sort bets by odds for use in an order book --- libraries/chain/CMakeLists.txt | 1 + libraries/chain/betting_market_evaluator.cpp | 4 + libraries/chain/betting_market_object.cpp | 45 +++++ libraries/chain/db_bet.cpp | 164 +++++++++++++++++- libraries/chain/db_init.cpp | 2 +- .../graphene/chain/betting_market_object.hpp | 132 +++++++++++++- tests/tests/operation_tests2.cpp | 6 +- 7 files changed, 343 insertions(+), 11 deletions(-) create mode 100644 libraries/chain/betting_market_object.cpp diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index d91209ce..78d785c3 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -100,6 +100,7 @@ add_library( graphene_chain event_evaluator.cpp protocol/betting_market.cpp betting_market_evaluator.cpp + betting_market_object.cpp db_bet.cpp ${HEADERS} diff --git a/libraries/chain/betting_market_evaluator.cpp b/libraries/chain/betting_market_evaluator.cpp index 1cae7947..503a6618 100644 --- a/libraries/chain/betting_market_evaluator.cpp +++ b/libraries/chain/betting_market_evaluator.cpp @@ -122,6 +122,10 @@ void_result bet_place_evaluator::do_evaluate(const bet_place_operation& op) FC_ASSERT(op.backer_multiplier % allowed_increment == 0, "Bet odds must be a multiple of ${allowed_increment}", ("allowed_increment", allowed_increment)); } + // is it possible to match this bet + FC_ASSERT(bet_object::get_matching_amount(op.amount_to_bet.amount, op.backer_multiplier, op.back_or_lay) != 0, + "Bet cannot be matched"); + // verify they reserved enough to cover the percentage fee uint16_t percentage_fee = current_params.current_fees->get().percentage_fee; fc::uint128_t minimum_percentage_fee_calculation = op.amount_to_bet.amount.value; diff --git a/libraries/chain/betting_market_object.cpp b/libraries/chain/betting_market_object.cpp new file mode 100644 index 00000000..ca008915 --- /dev/null +++ b/libraries/chain/betting_market_object.cpp @@ -0,0 +1,45 @@ +#include + +namespace graphene { namespace chain { + +/* static */ share_type bet_object::get_matching_amount(share_type bet_amount, bet_multiplier_type backer_multiplier, bet_type back_or_lay) +{ + fc::uint128_t amount_to_match_128 = bet_amount.value; + + if (back_or_lay == bet_type::back) + { + amount_to_match_128 *= backer_multiplier - GRAPHENE_100_PERCENT; + amount_to_match_128 /= GRAPHENE_100_PERCENT; + } + else + { + amount_to_match_128 *= GRAPHENE_100_PERCENT; + amount_to_match_128 /= backer_multiplier - GRAPHENE_100_PERCENT; + } + return amount_to_match_128.to_uint64(); +} + +share_type bet_object::get_matching_amount() const +{ + return get_matching_amount(amount_to_bet.amount, backer_multiplier, back_or_lay); +} + +share_type betting_market_position_object::reduce() +{ + share_type additional_not_cancel_balance = std::min(pay_if_payout_condition, pay_if_not_payout_condition); + if (additional_not_cancel_balance == 0) + return 0; + pay_if_payout_condition -= additional_not_cancel_balance; + pay_if_not_payout_condition -= additional_not_cancel_balance; + pay_if_not_canceled += additional_not_cancel_balance; + + share_type immediate_winnings = std::min(pay_if_canceled, pay_if_not_canceled); + if (immediate_winnings == 0) + return 0; + pay_if_canceled -= immediate_winnings; + pay_if_not_canceled -= immediate_winnings; + return immediate_winnings; +} + +} } // graphene::chain + diff --git a/libraries/chain/db_bet.cpp b/libraries/chain/db_bet.cpp index 99e58f56..0c5c4d3d 100644 --- a/libraries/chain/db_bet.cpp +++ b/libraries/chain/db_bet.cpp @@ -21,12 +21,174 @@ void database::cancel_bet( const bet_object& bet, bool create_virtual_op ) bool maybe_cull_small_bet( database& db, const bet_object& bet_object_to_cull ) { + /** + * There are times when the AMOUNT_FOR_SALE * SALE_PRICE == 0 which means that we + * have hit the limit where the seller is asking for nothing in return. When this + * happens we must refund any balance back to the seller, it is too small to be + * sold at the sale price. + * + * If the order is a taker order (as opposed to a maker order), so the price is + * set by the counterparty, this check is deferred until the order becomes unmatched + * (see #555) -- however, detecting this condition is the responsibility of the caller. + */ + + if( bet_object_to_cull.get_matching_amount() == 0 ) + { + ilog("applied epsilon logic"); + db.cancel_bet(bet_object_to_cull); + return true; + } return false; } +share_type adjust_betting_position(database& db, account_id_type bettor_id, betting_market_id_type betting_market_id, bet_type back_or_lay, share_type bet_amount, share_type fees_collected) +{ try { + assert(bet_amount >= 0); + + share_type guaranteed_winnings_returned = 0; + + if (bet_amount == 0) + return guaranteed_winnings_returned; + + auto& index = db.get_index_type().indices().get(); + auto itr = index.find(boost::make_tuple(bettor_id, betting_market_id)); + if (itr == index.end()) + { + db.create([&](betting_market_position_object& position) { + position.bettor_id = bettor_id; + position.betting_market_id = betting_market_id; + position.pay_if_payout_condition = back_or_lay == bet_type::back ? bet_amount : 0; + position.pay_if_not_payout_condition = back_or_lay == bet_type::lay ? bet_amount : 0; + position.pay_if_canceled = bet_amount; + position.pay_if_not_canceled = 0; + position.fees_collected = fees_collected; + // this should not be reducible + }); + } else { + db.modify(*itr, [&](betting_market_position_object& position) { + assert(position.bettor_id == bettor_id); + assert(position.betting_market_id == betting_market_id); + position.pay_if_payout_condition += back_or_lay == bet_type::back ? bet_amount : 0; + position.pay_if_not_payout_condition += back_or_lay == bet_type::lay ? bet_amount : 0; + position.pay_if_canceled += bet_amount; + position.fees_collected += fees_collected; + + guaranteed_winnings_returned = position.reduce(); + }); + } + return guaranteed_winnings_returned; +} FC_CAPTURE_AND_RETHROW((bettor_id)(betting_market_id)(bet_amount)) } + + +bool bet_was_matched(database& db, const bet_object& bet, share_type amount_bet, bet_multiplier_type actual_multiplier, bool cull_if_small) +{ + // calculate the percentage fee paid + fc::uint128_t percentage_fee_128 = bet.amount_reserved_for_fees.value; + percentage_fee_128 *= amount_bet.value; + percentage_fee_128 += bet.amount_to_bet.amount.value - 1; + percentage_fee_128 /= bet.amount_to_bet.amount.value; + share_type fee_paid = percentage_fee_128.to_uint64(); + + // record their bet, modifying their position, and return any winnings + share_type guaranteed_winnings_returned = adjust_betting_position(db, bet.bettor_id, bet.betting_market_id, + bet.back_or_lay, amount_bet, fee_paid); + db.adjust_balance(bet.bettor_id, asset(guaranteed_winnings_returned, bet.amount_to_bet.asset_id)); + + // generate a virtual "match" op + asset asset_amount_bet(amount_bet, bet.amount_to_bet.asset_id); + db.push_applied_operation(bet_matched_operation(bet.bettor_id, bet.id, + asset_amount_bet, + fee_paid, + actual_multiplier, + guaranteed_winnings_returned)); + + // update the bet on the books + if (asset_amount_bet == bet.amount_to_bet) + { + db.remove(bet); + return true; + } + else + { + db.modify(bet, [&](bet_object& bet_obj) { + bet_obj.amount_to_bet -= asset_amount_bet; + bet_obj.amount_reserved_for_fees -= fee_paid; + }); + if (cull_if_small) + return maybe_cull_small_bet(db, bet); + return false; + } +} + +/** + * Matches the two orders, + * + * @return a bit field indicating which orders were filled (and thus removed) + * + * 0 - no orders were matched + * 1 - bid was filled + * 2 - ask was filled + * 3 - both were filled + */ +int match_bet(database& db, const bet_object& taker_bet, const bet_object& maker_bet ) +{ + assert(taker_bet.amount_to_bet.asset_id == maker_bet.amount_to_bet.asset_id); + assert(taker_bet.amount_to_bet.amount > 0 && maker_bet.amount_to_bet.amount > 0); + assert(taker_bet.backer_multiplier >= maker_bet.backer_multiplier); + assert(taker_bet.back_or_lay != maker_bet.back_or_lay); + + int result = 0; + share_type maximum_amount_to_match = taker_bet.get_matching_amount(); + + if (maximum_amount_to_match <= maker_bet.amount_to_bet.amount) + { + // we will consume the entire taker bet + result |= bet_was_matched(db, taker_bet, taker_bet.amount_to_bet.amount, maker_bet.backer_multiplier, true); + result |= bet_was_matched(db, maker_bet, maximum_amount_to_match, maker_bet.backer_multiplier, true) << 1; + } + else + { + // we will consume the entire maker bet. Figure out how much of the taker bet we can fill. + share_type taker_amount = maker_bet.get_matching_amount(); + share_type maker_amount = bet_object::get_matching_amount(taker_amount, maker_bet.backer_multiplier, taker_bet.back_or_lay); + + result |= bet_was_matched(db, taker_bet, taker_amount, maker_bet.backer_multiplier, true); + result |= bet_was_matched(db, maker_bet, maker_amount, maker_bet.backer_multiplier, true) << 1; + } + + assert(result != 0); + return result; +} + bool database::place_bet(const bet_object& new_bet_object) { - return false; + bet_id_type bet_id = new_bet_object.id; + const asset_object& bet_asset = get(new_bet_object.amount_to_bet.asset_id); + + const auto& bet_odds_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. + + bet_type bet_type_to_match = new_bet_object.back_or_lay == bet_type::back ? bet_type::lay : bet_type::back; + auto book_itr = bet_odds_idx.lower_bound(std::make_tuple(new_bet_object.betting_market_id, bet_type_to_match)); + auto book_end = bet_odds_idx.upper_bound(std::make_tuple(new_bet_object.betting_market_id, bet_type_to_match, new_bet_object.backer_multiplier)); + + int orders_matched_flags = 0; + bool finished = false; + while (!finished && book_itr != book_end) + { + auto old_book_itr = book_itr; + ++book_itr; + // match returns 2 when only the old order was fully filled. In this case, we keep matching; otherwise, we stop. + + orders_matched_flags = match_bet(*this, new_bet_object, *old_book_itr); + finished = orders_matched_flags != 2; + } + + return (orders_matched_flags & 1) != 0; } } } diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index b85dcc07..b908adfb 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -270,7 +270,7 @@ void database::initialize_indexes() add_index< primary_index< buyback_index > >(); add_index< primary_index< simple_index< fba_accumulator_object > > >(); - add_index< primary_index< betting_market_position_multi_index > >(); + add_index< primary_index< betting_market_position_index > >(); add_index< primary_index< global_betting_statistics_object_index > >(); } diff --git a/libraries/chain/include/graphene/chain/betting_market_object.hpp b/libraries/chain/include/graphene/chain/betting_market_object.hpp index 7fad68df..92159c04 100644 --- a/libraries/chain/include/graphene/chain/betting_market_object.hpp +++ b/libraries/chain/include/graphene/chain/betting_market_object.hpp @@ -26,6 +26,9 @@ #include #include #include +#include + +#include namespace graphene { namespace chain { @@ -75,6 +78,9 @@ class bet_object : public graphene::db::abstract_object< bet_object > share_type amount_reserved_for_fees; // same asset type as amount_to_bet bet_type back_or_lay; + + static share_type get_matching_amount(share_type bet_amount, bet_multiplier_type backer_multiplier, bet_type back_or_lay); + share_type get_matching_amount() const; }; class betting_market_position_object : public graphene::db::abstract_object< betting_market_position_object > @@ -91,6 +97,9 @@ class betting_market_position_object : public graphene::db::abstract_object< bet share_type pay_if_not_payout_condition; share_type pay_if_canceled; share_type pay_if_not_canceled; + share_type fees_collected; + + share_type reduce(); }; typedef multi_index_container< @@ -110,23 +119,138 @@ typedef multi_index_container< typedef generic_index betting_market_object_index; +struct compare_bet_by_odds { + bool operator()(const bet_object& lhs, const bet_object& rhs) const + { + return compare(lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier, lhs.id, + rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier, rhs.id); + } + + template + bool operator() (const std::tuple& lhs, const bet_object& rhs) const + { + return compare(std::get<0>(lhs), rhs.betting_market_id); + } + + template + bool operator() (const bet_object& lhs, const std::tuple& rhs) const + { + return compare(lhs.betting_market_id, std::get<0>(rhs)); + } + + template + bool operator() (const std::tuple& lhs, const bet_object& rhs) const + { + return compare(std::get<0>(lhs), std::get<1>(lhs), rhs.betting_market_id, rhs.back_or_lay); + } + + template + bool operator() (const bet_object& lhs, const std::tuple& rhs) const + { + return compare(lhs.betting_market_id, lhs.back_or_lay, std::get<0>(rhs), std::get<1>(rhs)); + } + + template + bool operator() (const std::tuple& lhs, const bet_object& rhs) const + { + return compare(std::get<0>(lhs), std::get<1>(lhs), std::get<2>(lhs), + rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier); + } + + template + bool operator() (const bet_object& lhs, const std::tuple& rhs) const + { + return compare(lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier, + std::get<0>(rhs), std::get<1>(rhs), std::get<2>(rhs)); + } + template + bool operator() (const std::tuple& lhs, const bet_object& rhs) const + { + return compare(std::get<0>(lhs), std::get<1>(lhs), std::get<2>(lhs), std::get<3>(lhs), + rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier, rhs.id); + } + + template + bool operator() (const bet_object& lhs, const std::tuple& rhs) const + { + return compare(lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier, lhs.id, + std::get<0>(rhs), std::get<1>(rhs), std::get<2>(rhs), std::get<3>(rhs)); + } + bool compare(const betting_market_id_type& lhs_betting_market_id, const betting_market_id_type& rhs_betting_market_id) + { + return lhs_betting_market_id < rhs_betting_market_id; + } + bool compare(const betting_market_id_type& lhs_betting_market_id, bet_type lhs_bet_type, + const betting_market_id_type& rhs_betting_market_id, bet_type rhs_bet_type) const + { + if (lhs_betting_market_id < rhs_betting_market_id) + return true; + if (lhs_betting_market_id > rhs_betting_market_id) + return false; + return lhs_bet_type < rhs_bet_type; + } + bool compare(const betting_market_id_type& lhs_betting_market_id, bet_type lhs_bet_type, + bet_multiplier_type lhs_backer_multiplier, + const betting_market_id_type& rhs_betting_market_id, bet_type rhs_bet_type, + bet_multiplier_type rhs_backer_multiplier) const + { + if (lhs_betting_market_id < rhs_betting_market_id) + return true; + if (lhs_betting_market_id > rhs_betting_market_id) + return false; + if (lhs_bet_type < rhs_bet_type) + return true; + if (lhs_bet_type > rhs_bet_type) + return false; + return lhs_backer_multiplier < rhs_backer_multiplier; + } + bool compare(const betting_market_id_type& lhs_betting_market_id, bet_type lhs_bet_type, + bet_multiplier_type lhs_backer_multiplier, const bet_id_type& lhs_bet_id, + const betting_market_id_type& rhs_betting_market_id, bet_type rhs_bet_type, + bet_multiplier_type rhs_backer_multiplier, const bet_id_type& rhs_bet_id) const + { + if (lhs_betting_market_id < rhs_betting_market_id) + return true; + if (lhs_betting_market_id > rhs_betting_market_id) + return false; + if (lhs_bet_type < rhs_bet_type) + return true; + if (lhs_bet_type > rhs_bet_type) + return false; + if (lhs_backer_multiplier < rhs_backer_multiplier) + return true; + if (lhs_backer_multiplier > rhs_backer_multiplier) + return false; + return lhs_bet_id < rhs_bet_id; + } +}; + +struct by_odds {}; typedef multi_index_container< bet_object, indexed_by< - ordered_unique< tag, member< object, object_id_type, &object::id > > > > bet_object_multi_index_type; + ordered_unique< tag, member< object, object_id_type, &object::id > >, + ordered_unique< tag, identity, compare_bet_by_odds > > > bet_object_multi_index_type; typedef generic_index bet_object_index; +struct by_bettor_betting_market{}; typedef multi_index_container< betting_market_position_object, indexed_by< - ordered_unique< tag, member< object, object_id_type, &object::id > > > > betting_market_position_multi_index_type; + ordered_unique< tag, member< object, object_id_type, &object::id > >, + ordered_unique< tag, + composite_key< + betting_market_position_object, + member, + member + > > > > betting_market_position_multi_index_type; -typedef generic_index betting_market_position_multi_index; +typedef generic_index betting_market_position_index; } } // graphene::chain FC_REFLECT_DERIVED( graphene::chain::betting_market_group_object, (graphene::db::object), (event_id)(options) ) FC_REFLECT_DERIVED( graphene::chain::betting_market_object, (graphene::db::object), (group_id)(payout_condition)(asset_id) ) FC_REFLECT_DERIVED( graphene::chain::bet_object, (graphene::db::object), (bettor_id)(betting_market_id)(amount_to_bet)(backer_multiplier)(amount_reserved_for_fees)(back_or_lay) ) -FC_REFLECT_DERIVED( graphene::chain::betting_market_position_object, (graphene::db::object), (bettor_id)(betting_market_id)(pay_if_payout_condition)(pay_if_not_payout_condition)(pay_if_canceled)(pay_if_not_canceled) ) +FC_REFLECT_DERIVED( graphene::chain::betting_market_position_object, (graphene::db::object), (bettor_id)(betting_market_id)(pay_if_payout_condition)(pay_if_not_payout_condition)(pay_if_canceled)(pay_if_not_canceled)(fees_collected) ) diff --git a/tests/tests/operation_tests2.cpp b/tests/tests/operation_tests2.cpp index d9d24356..6fb1cf71 100644 --- a/tests/tests/operation_tests2.cpp +++ b/tests/tests/operation_tests2.cpp @@ -1649,9 +1649,7 @@ BOOST_AUTO_TEST_CASE( peerplays_sport_create_test ) tx.operations.push_back(proposal_op); set_expiration(db, tx); sign(tx, init_account_priv_key); - //sign( tx, philbin_private_key ); - // Alice and Philbin signed, but asset issuer is invalid db.push_transaction(tx); } @@ -1767,9 +1765,7 @@ BOOST_AUTO_TEST_CASE( peerplays_sport_create_test ) tx.operations.push_back(proposal_op); set_expiration(db, tx); sign(tx, init_account_priv_key); - //sign( tx, philbin_private_key ); - - // Alice and Philbin signed, but asset issuer is invalid + db.push_transaction(tx); }