From 93088a204d660054ab326a943e4848be18d894bb Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Tue, 1 Aug 2017 15:40:49 -0400 Subject: [PATCH] Change the rounding used when matching bets to never round, bets are always matched at exactly the maker's odds. --- libraries/chain/betting_market_evaluator.cpp | 2 +- libraries/chain/betting_market_object.cpp | 51 +++++++++- libraries/chain/db_bet.cpp | 98 +++++++++++++------ .../graphene/chain/betting_market_object.hpp | 17 +++- libraries/plugins/bookie/bookie_api.cpp | 10 +- tests/betting/betting_tests.cpp | 90 +++++++++++++++++ 6 files changed, 229 insertions(+), 39 deletions(-) diff --git a/libraries/chain/betting_market_evaluator.cpp b/libraries/chain/betting_market_evaluator.cpp index eb1545a3..70be3317 100644 --- a/libraries/chain/betting_market_evaluator.cpp +++ b/libraries/chain/betting_market_evaluator.cpp @@ -274,7 +274,7 @@ void_result bet_place_evaluator::do_evaluate(const bet_place_operation& op) } // 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, + FC_ASSERT(bet_object::get_exact_matching_amount(op.amount_to_bet.amount, op.backer_multiplier, op.back_or_lay) != 0, "Bet cannot be matched"); #if 0 diff --git a/libraries/chain/betting_market_object.cpp b/libraries/chain/betting_market_object.cpp index 6cda06ee..fbcae738 100644 --- a/libraries/chain/betting_market_object.cpp +++ b/libraries/chain/betting_market_object.cpp @@ -1,29 +1,74 @@ #include +#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) +/* static */ share_type bet_object::get_approximate_matching_amount(share_type bet_amount, bet_multiplier_type backer_multiplier, bet_type back_or_lay, bool round_up /* = false */) { fc::uint128_t amount_to_match_128 = bet_amount.value; if (back_or_lay == bet_type::back) { amount_to_match_128 *= backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION; + if (round_up) + amount_to_match_128 += GRAPHENE_BETTING_ODDS_PRECISION - 1; amount_to_match_128 /= GRAPHENE_BETTING_ODDS_PRECISION; } else { amount_to_match_128 *= GRAPHENE_BETTING_ODDS_PRECISION; + if (round_up) + amount_to_match_128 += backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION - 1; amount_to_match_128 /= backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION; } return amount_to_match_128.to_uint64(); } -share_type bet_object::get_matching_amount() const +share_type bet_object::get_approximate_matching_amount(bool round_up /* = false */) const { - return get_matching_amount(amount_to_bet.amount, backer_multiplier, back_or_lay); + return get_approximate_matching_amount(amount_to_bet.amount, backer_multiplier, back_or_lay, round_up); } +/* static */ share_type bet_object::get_exact_matching_amount(share_type bet_amount, bet_multiplier_type backer_multiplier, bet_type back_or_lay) +{ + share_type back_ratio; + share_type lay_ratio; + std::tie(back_ratio, lay_ratio) = get_ratio(backer_multiplier); + if (back_or_lay == bet_type::back) + return bet_amount / back_ratio * lay_ratio; + else + return bet_amount / lay_ratio * back_ratio; +} + +share_type bet_object::get_exact_matching_amount() const +{ + return get_exact_matching_amount(amount_to_bet.amount, backer_multiplier, back_or_lay); +} + +/* static */ std::pair bet_object::get_ratio(bet_multiplier_type backer_multiplier) +{ + share_type gcd = boost::math::gcd(GRAPHENE_BETTING_ODDS_PRECISION, backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION); + return std::make_pair(GRAPHENE_BETTING_ODDS_PRECISION / gcd, (backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION) / gcd); +} + +std::pair bet_object::get_ratio() const +{ + return get_ratio(backer_multiplier); +} + +share_type bet_object::get_minimum_matchable_amount() const +{ + share_type gcd = boost::math::gcd(GRAPHENE_BETTING_ODDS_PRECISION, backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION); + return (back_or_lay == bet_type::back ? GRAPHENE_BETTING_ODDS_PRECISION : backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION) / gcd; +} + +share_type bet_object::get_minimum_matching_amount() const +{ + share_type gcd = boost::math::gcd(GRAPHENE_BETTING_ODDS_PRECISION, backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION); + return (back_or_lay == bet_type::lay ? GRAPHENE_BETTING_ODDS_PRECISION : backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION) / gcd; +} + + share_type betting_market_position_object::reduce() { share_type additional_not_cancel_balance = std::min(pay_if_payout_condition, pay_if_not_payout_condition); diff --git a/libraries/chain/db_bet.cpp b/libraries/chain/db_bet.cpp index a4eded19..e0918f5e 100644 --- a/libraries/chain/db_bet.cpp +++ b/libraries/chain/db_bet.cpp @@ -16,9 +16,13 @@ void database::cancel_bet( const bet_object& bet, bool create_virtual_op ) adjust_balance(bet.bettor_id, amount_to_refund); //return unmatched stake + fees //TODO: do special fee accounting as required if (create_virtual_op) - push_applied_operation(bet_canceled_operation(bet.bettor_id, bet.id, - bet.amount_to_bet, - bet.amount_reserved_for_fees)); + { + bet_canceled_operation bet_canceled_virtual_op(bet.bettor_id, bet.id, + bet.amount_to_bet, + bet.amount_reserved_for_fees); + //idump((bet_canceled_virtual_op)); + push_applied_operation(std::move(bet_canceled_virtual_op)); + } remove(bet); } @@ -55,7 +59,7 @@ void database::validate_betting_market_group_resolutions(const betting_market_gr { const betting_market_object& betting_market = *betting_market_itr; // every betting market in the group tied with resolution - idump((betting_market.id)(resolutions)); + //idump((betting_market.id)(resolutions)); assert(resolutions.count(betting_market.id)); ++betting_market_itr; } @@ -181,7 +185,7 @@ void database::resolve_betting_market_group(const betting_market_group_object& b // pay winning - rake adjust_balance(bettor_id, asset(payout_amounts - rake_amount, betting_market_group.asset_id)); // [ROL] - idump((payout_amounts)(net_profits.value)(rake_amount.value)); + //idump((payout_amounts)(net_profits.value)(rake_amount.value)); push_applied_operation(betting_market_group_resolved_operation(bettor_id, betting_market_group.id, @@ -212,14 +216,17 @@ void database::get_required_deposit_for_bet(const betting_market_object& betting bool maybe_cull_small_bet( database& db, const bet_object& bet_object_to_cull ) { /** - * There are times when this bet can't be matched (for example, it's now laying a 2:1 bet for - * 1 satoshi, so it could only be matched by half a satoshi). Remove these bets from - * the books. + * There are times when this bet can't be even partially matched at its stated odds + * For example, say it's a back bet for 20 satoshis at 1.92 odds (which comes out to 25:23 + * odds). It's not possible for it to match at exact odds since it's < 25 + * + * Remove these bets from the books to reduce clutter. */ - - if( bet_object_to_cull.get_matching_amount() == 0 ) + share_type minimum_matchable_amount = bet_object_to_cull.get_minimum_matchable_amount(); + if (bet_object_to_cull.amount_to_bet.amount < minimum_matchable_amount) { - ilog("applied epsilon logic"); + dlog("culling small bet of ${amount}, smaller than the minimum ${minimum_matchable_amount} at odds ${odds}", + ("amount", bet_object_to_cull.amount_to_bet.amount)("minimum_matchable_amount", minimum_matchable_amount)("odds", bet_object_to_cull.backer_multiplier)); db.cancel_bet(bet_object_to_cull); return true; } @@ -284,11 +291,14 @@ bool bet_was_matched(database& db, const bet_object& bet, // 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, bet.betting_market_id, - asset_amount_bet, - fee_paid, - actual_multiplier, - guaranteed_winnings_returned)); + + bet_matched_operation bet_matched_virtual_op(bet.bettor_id, bet.id, bet.betting_market_id, + asset_amount_bet, + fee_paid, + actual_multiplier, + guaranteed_winnings_returned); + //idump((bet_matched_virtual_op)); + db.push_applied_operation(std::move(bet_matched_virtual_op)); // update the bet on the books if (asset_amount_bet == bet.amount_to_bet) @@ -328,23 +338,51 @@ int match_bet(database& db, const bet_object& taker_bet, const bet_object& maker 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(); + //idump((taker_bet)(maker_bet)); - 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, maximum_amount_to_match, maker_bet.backer_multiplier, true); - result |= bet_was_matched(db, maker_bet, maximum_amount_to_match, taker_bet.amount_to_bet.amount, 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); + // using the maker's odds, figure out how much of the maker's bet we would match, rounding down + // go ahead and get look up the ratio for the bet (a bet with odds 1.92 will have a ratio 25:23) + share_type back_odds_ratio; + share_type lay_odds_ratio; + std::tie(back_odds_ratio, lay_odds_ratio) = maker_bet.get_ratio(); - result |= bet_was_matched(db, taker_bet, taker_amount, maker_amount, maker_bet.backer_multiplier, true); - result |= bet_was_matched(db, maker_bet, maker_amount, taker_amount, maker_bet.backer_multiplier, true) << 1; + // and make some shortcuts to get to the maker's and taker's side of the ratio + const share_type& maker_odds_ratio = maker_bet.back_or_lay == bet_type::back ? back_odds_ratio : lay_odds_ratio; + const share_type& taker_odds_ratio = maker_bet.back_or_lay == bet_type::back ? lay_odds_ratio : back_odds_ratio; + //idump((back_odds_ratio)(lay_odds_ratio)); + //idump((maker_odds_ratio)(taker_odds_ratio)); + + // now figure out how much of the maker bet we'll consume. We don't yet know whether the maker or taker + // will be the limiting factor. + share_type maximum_taker_factor = taker_bet.amount_to_bet.amount / taker_odds_ratio; + share_type maximum_maker_factor = maker_bet.amount_to_bet.amount / maker_odds_ratio; + share_type maximum_factor = std::min(maximum_taker_factor, maximum_maker_factor); + share_type maker_amount_to_match = maximum_factor * maker_odds_ratio; + share_type taker_amount_to_match = maximum_factor * taker_odds_ratio; + //idump((maker_amount_to_match)(taker_amount_to_match)); + + // TODO: analyze whether maximum_maker_amount_to_match can ever be zero here + assert(maker_amount_to_match != 0); + if (maker_amount_to_match == 0) + return 0; + +#ifndef NDEBUG + assert(taker_amount_to_match <= taker_bet.amount_to_bet.amount); + assert(taker_amount_to_match / taker_odds_ratio * taker_odds_ratio == taker_amount_to_match); + { + // verify we're getting the odds we expect + fc::uint128_t payout_128 = maker_amount_to_match.value; + payout_128 += taker_amount_to_match.value; + payout_128 *= GRAPHENE_BETTING_ODDS_PRECISION; + payout_128 /= maker_bet.back_or_lay == bet_type::back ? maker_amount_to_match.value : taker_amount_to_match.value; + assert(payout_128.to_uint64() == maker_bet.backer_multiplier); } +#endif + + //idump((taker_amount_to_match)(maker_amount_to_match)); + + result |= bet_was_matched(db, taker_bet, taker_amount_to_match, maker_amount_to_match, maker_bet.backer_multiplier, true); + result |= bet_was_matched(db, maker_bet, maker_amount_to_match, taker_amount_to_match, maker_bet.backer_multiplier, true) << 1; assert(result != 0); return result; diff --git a/libraries/chain/include/graphene/chain/betting_market_object.hpp b/libraries/chain/include/graphene/chain/betting_market_object.hpp index 715d997d..4e944871 100644 --- a/libraries/chain/include/graphene/chain/betting_market_object.hpp +++ b/libraries/chain/include/graphene/chain/betting_market_object.hpp @@ -98,8 +98,21 @@ class bet_object : public graphene::db::abstract_object< bet_object > 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; + static share_type get_approximate_matching_amount(share_type bet_amount, bet_multiplier_type backer_multiplier, bet_type back_or_lay, bool round_up = false); + + // returns the amount of a bet that completely matches this bet + share_type get_approximate_matching_amount(bool round_up = false) const; + + static share_type get_exact_matching_amount(share_type bet_amount, bet_multiplier_type backer_multiplier, bet_type back_or_lay); + share_type get_exact_matching_amount() const; + + static std::pair get_ratio(bet_multiplier_type backer_multiplier); + std::pair get_ratio() const; + + // returns the minimum amount this bet could have that could be matched at these odds + share_type get_minimum_matchable_amount() const; + // returns the minimum amount another user could bet to match this bet at these odds + share_type get_minimum_matching_amount() const; }; class betting_market_position_object : public graphene::db::abstract_object< betting_market_position_object > diff --git a/libraries/plugins/bookie/bookie_api.cpp b/libraries/plugins/bookie/bookie_api.cpp index e0fee55a..375cbbc5 100644 --- a/libraries/plugins/bookie/bookie_api.cpp +++ b/libraries/plugins/bookie/bookie_api.cpp @@ -41,6 +41,7 @@ binned_order_book bookie_api_impl::get_binned_order_book(graphene::chain::bettin { std::shared_ptr db = app.chain_database(); const auto& bet_odds_idx = db->get_index_type().indices().get(); + const chain_parameters& current_params = db->get_global_properties().parameters; graphene::chain::bet_multiplier_type bin_size = GRAPHENE_BETTING_ODDS_PRECISION; if (precision > 0) @@ -68,7 +69,8 @@ binned_order_book bookie_api_impl::get_binned_order_book(graphene::chain::bettin order_bin current_order_bin; current_order_bin.backer_multiplier = current_bin->backer_multiplier; - current_order_bin.amount_to_bet = current_bin->get_matching_amount();; + current_order_bin.amount_to_bet = current_bin->get_approximate_matching_amount(true /* round up */); + //idump((*current_bin)(current_order_bin)); if (current_bin->back_or_lay == bet_type::lay) result.aggregated_back_bets.emplace_back(std::move(current_order_bin)); else // current_bin is aggregating back positions @@ -91,12 +93,14 @@ binned_order_book bookie_api_impl::get_binned_order_book(graphene::chain::bettin { // if there is no current bin, create one appropriate for the bet we're processing current_bin = graphene::chain::bet_object(); + current_bin->backer_multiplier = (bet_odds_iter->backer_multiplier + bin_size - 1) / bin_size * bin_size; - current_bin->amount_to_bet.amount = 0; + current_bin->backer_multiplier = std::min(current_bin->backer_multiplier, current_params.max_bet_multiplier); current_bin->back_or_lay = bet_odds_iter->back_or_lay == bet_type::back ? bet_type::lay : bet_type::back; + current_bin->amount_to_bet.amount = 0; } - current_bin->amount_to_bet.amount += bet_odds_iter->get_matching_amount(); + current_bin->amount_to_bet.amount += bet_odds_iter->get_exact_matching_amount(); } if (current_bin) flush_current_bin(); diff --git a/tests/betting/betting_tests.cpp b/tests/betting/betting_tests.cpp index 0ec954da..f5b3a8e1 100644 --- a/tests/betting/betting_tests.cpp +++ b/tests/betting/betting_tests.cpp @@ -37,6 +37,7 @@ #include #include +#include //#include using namespace graphene::chain; @@ -144,6 +145,95 @@ BOOST_AUTO_TEST_CASE(simple_bet_win) } FC_LOG_AND_RETHROW() } +BOOST_AUTO_TEST_CASE(binned_order_books) +{ + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(); + + graphene::bookie::bookie_api bookie_api(app); + + // give alice and bob 10k each + transfer(account_id_type(), alice_id, asset(10000)); + transfer(account_id_type(), bob_id, asset(10000)); + + // place back bets at decimal odds of 1.55, 1.6, 1.65, 1.66, and 1.67 + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 155 * GRAPHENE_BETTING_ODDS_PRECISION / 100, 2); + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 16 * GRAPHENE_BETTING_ODDS_PRECISION / 10, 2); + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 165 * GRAPHENE_BETTING_ODDS_PRECISION / 100, 2); + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 166 * GRAPHENE_BETTING_ODDS_PRECISION / 100, 2); + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 167 * GRAPHENE_BETTING_ODDS_PRECISION / 100, 2); + + const auto& bet_odds_idx = db.get_index_type().indices().get(); + + auto bet_iter = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + while (bet_iter != bet_odds_idx.end() && + bet_iter->betting_market_id == capitals_win_market.id) + { + idump((*bet_iter)); + ++bet_iter; + } + + graphene::bookie::binned_order_book binned_orders_point_one = bookie_api.get_binned_order_book(capitals_win_market.id, 1); + idump((binned_orders_point_one)); + + // the binned orders returned should be chosen so that we if we assume those orders are real and we place + // matching lay orders, we will completely consume the underlying orders and leave no orders on the books + BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_back_bets.size(), 2); + BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_lay_bets.size(), 0); + for (const graphene::bookie::order_bin& binned_order : binned_orders_point_one.aggregated_back_bets) + { + // compute the matching lay order + share_type lay_amount = bet_object::get_approximate_matching_amount(binned_order.amount_to_bet, binned_order.backer_multiplier, bet_type::back, false /* round down */); + ilog("Alice is laying with ${lay_amount} at odds ${odds} to match the binned back amount ${back_amount}", ("lay_amount", lay_amount)("odds", binned_order.backer_multiplier)("back_amount", binned_order.amount_to_bet)); + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(lay_amount, asset_id_type()), binned_order.backer_multiplier, 2); + } + + bet_iter = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + while (bet_iter != bet_odds_idx.end() && + bet_iter->betting_market_id == capitals_win_market.id) + { + idump((*bet_iter)); + ++bet_iter; + } + + BOOST_CHECK(bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)) == bet_odds_idx.end()); + + // place lay bets at decimal odds of 1.55, 1.6, 1.65, 1.66, and 1.67 + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 155 * GRAPHENE_BETTING_ODDS_PRECISION / 100, 2); + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 16 * GRAPHENE_BETTING_ODDS_PRECISION / 10, 2); + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 165 * GRAPHENE_BETTING_ODDS_PRECISION / 100, 2); + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 166 * GRAPHENE_BETTING_ODDS_PRECISION / 100, 2); + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 167 * GRAPHENE_BETTING_ODDS_PRECISION / 100, 2); + + binned_orders_point_one = bookie_api.get_binned_order_book(capitals_win_market.id, 1); + idump((binned_orders_point_one)); + + // the binned orders returned should be chosen so that we if we assume those orders are real and we place + // matching lay orders, we will completely consume the underlying orders and leave no orders on the books + BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_back_bets.size(), 0); + BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_lay_bets.size(), 2); + for (const graphene::bookie::order_bin& binned_order : binned_orders_point_one.aggregated_lay_bets) + { + // compute the matching lay order + share_type back_amount = bet_object::get_approximate_matching_amount(binned_order.amount_to_bet, binned_order.backer_multiplier, bet_type::lay, false /* round down */); + ilog("Alice is backing with ${back_amount} at odds ${odds} to match the binned lay amount ${lay_amount}", ("back_amount", back_amount)("odds", binned_order.backer_multiplier)("lay_amount", binned_order.amount_to_bet)); + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(back_amount, asset_id_type()), binned_order.backer_multiplier, 2); + } + + bet_iter = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + while (bet_iter != bet_odds_idx.end() && + bet_iter->betting_market_id == capitals_win_market.id) + { + idump((*bet_iter)); + ++bet_iter; + } + + BOOST_CHECK(bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)) == bet_odds_idx.end()); + + } FC_LOG_AND_RETHROW() +} BOOST_AUTO_TEST_CASE( peerplays_sport_create_test ) {