diff --git a/libraries/app/api.cpp b/libraries/app/api.cpp index d46eab07b..b59bc93bc 100644 --- a/libraries/app/api.cpp +++ b/libraries/app/api.cpp @@ -510,7 +510,7 @@ namespace graphene { namespace app { break; case impl_fba_accumulator_object_type: break; - case impl_betting_market_position_object_type: + case impl_betting_market_group_position_object_type: break; case impl_global_betting_statistics_object_type: break; diff --git a/libraries/chain/betting_market_evaluator.cpp b/libraries/chain/betting_market_evaluator.cpp index e1d64e3c6..65e36557d 100644 --- a/libraries/chain/betting_market_evaluator.cpp +++ b/libraries/chain/betting_market_evaluator.cpp @@ -77,6 +77,11 @@ void_result betting_market_group_create_evaluator::do_evaluate(const betting_mar FC_ASSERT(d.head_block_time() >= HARDFORK_1000_TIME); FC_ASSERT(trx_state->_is_proposed_trx); + FC_ASSERT(op.resolution_constraint == betting_market_resolution_constraint::exactly_one_winner || + op.resolution_constraint == betting_market_resolution_constraint::at_most_one_winner, + "invalid value for resolution_constraint"); + + // the event_id in the operation can be a relative id. If it is, // resolve it and verify that it is truly an event object_id_type resolved_event_id = op.event_id; @@ -115,7 +120,11 @@ object_id_type betting_market_group_create_evaluator::do_apply(const betting_mar betting_market_group_obj.asset_id = op.asset_id; betting_market_group_obj.never_in_play = op.never_in_play; betting_market_group_obj.delay_before_settling = op.delay_before_settling; + betting_market_group_obj.resolution_constraint = op.resolution_constraint; }); + + fc_ddump(fc::logger::get("betting"), (op)(new_betting_market_group)); + return new_betting_market_group.id; } FC_CAPTURE_AND_RETHROW( (op) ) } @@ -186,7 +195,7 @@ void_result betting_market_group_update_evaluator::do_apply(const betting_market bet_obj.end_of_delay.reset(); }); - d.place_bet(delayed_bet); + d.try_to_match_bet(delayed_bet); } } } @@ -196,7 +205,8 @@ void_result betting_market_group_update_evaluator::do_apply(const betting_market void_result betting_market_create_evaluator::do_evaluate(const betting_market_create_operation& op) { try { - FC_ASSERT(db().head_block_time() >= HARDFORK_1000_TIME); + database& d = db(); + FC_ASSERT(d.head_block_time() >= HARDFORK_1000_TIME); FC_ASSERT(trx_state->_is_proposed_trx); // the betting_market_group_id in the operation can be a relative id. If it is, @@ -209,7 +219,13 @@ void_result betting_market_create_evaluator::do_evaluate(const betting_market_cr resolved_betting_market_group_id.type() == betting_market_group_id_type::type_id, "betting_market_group_id must refer to a betting_market_group_id_type"); _group_id = resolved_betting_market_group_id; - FC_ASSERT(db().find_object(_group_id), "Invalid betting_market_group specified"); + FC_ASSERT(d.find_object(_group_id), "Invalid betting_market_group specified"); + + // you can't add a betting market to a group that already has active bets in it + auto& position_index = d.get_index_type().indices().get(); + auto position_iter = position_index.lower_bound(_group_id); + FC_ASSERT(position_iter == position_index.end(), + "You cannot add a betting market to a group which already has bets placed in it"); return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } @@ -231,9 +247,9 @@ void_result betting_market_update_evaluator::do_evaluate(const betting_market_up FC_ASSERT(d.head_block_time() >= HARDFORK_1000_TIME); FC_ASSERT(trx_state->_is_proposed_trx); _betting_market = &op.betting_market_id(d); - FC_ASSERT(op.new_group_id.valid() || op.new_description.valid() || op.new_payout_condition.valid(), "nothing to change"); + FC_ASSERT(op.new_group_id || op.new_description || op.new_payout_condition, "nothing to change"); - if (op.new_group_id.valid()) + if (op.new_group_id) { // the betting_market_group_id in the operation can be a relative id. If it is, // resolve it and verify that it is truly an betting_market_group @@ -246,6 +262,15 @@ void_result betting_market_update_evaluator::do_evaluate(const betting_market_up "betting_market_group_id must refer to a betting_market_group_id_type"); _group_id = resolved_betting_market_group_id; FC_ASSERT(d.find_object(_group_id), "invalid betting_market_group specified"); + + // you can't move a betting market to or from a group that already has active bets in it + auto& position_index = d.get_index_type().indices().get(); + auto position_iter = position_index.lower_bound(_betting_market->group_id); + FC_ASSERT(position_iter == position_index.end(), + "You cannot move a betting market out of a group which already has bets placed in it"); + position_iter = position_index.lower_bound(_group_id); + FC_ASSERT(position_iter == position_index.end(), + "You cannot move a betting market into a group which already has bets placed in it"); } return void_result(); @@ -274,7 +299,7 @@ void_result bet_place_evaluator::do_evaluate(const bet_place_operation& op) FC_ASSERT( op.amount_to_bet.asset_id == _betting_market_group->asset_id, "Asset type bet does not match the market's asset type" ); - ddump((_betting_market_group->get_status())); + //ddump((_betting_market_group->get_status())); FC_ASSERT( _betting_market_group->get_status() != betting_market_group_status::frozen, "Unable to place bets while the market is frozen" ); FC_ASSERT( _betting_market_group->get_status() != betting_market_group_status::closed, @@ -308,6 +333,39 @@ void_result bet_place_evaluator::do_evaluate(const bet_place_operation& op) FC_ASSERT(op.amount_to_bet.amount > share_type(), "Cannot place a bet with zero amount"); +#ifdef ENABLE_SIMPLE_CROSS_MARKET_MATCHING + // As a special case, when we have a betting market group with two markets that has exactly + // one winner, we treat it as one market, with backs in the second market going on the order books + // as lays in the first, and lays in the second market becoming backs on the first. + auto& betting_market_index = d.get_index_type().indices().get(); + auto betting_markets_in_group = boost::make_iterator_range(betting_market_index.equal_range(_betting_market_group->id)); + if (boost::size(betting_markets_in_group) == 2 && + _betting_market->id == betting_markets_in_group.back().id && + _betting_market_group->resolution_constraint == betting_market_resolution_constraint::exactly_one_winner) + { + _betting_market = &betting_markets_in_group.front(); + _effective_bet_type = op.back_or_lay == bet_type::back ? bet_type::lay : bet_type::back; + _new_odds = GRAPHENE_BETTING_ODDS_PRECISION + GRAPHENE_BETTING_ODDS_PRECISION * GRAPHENE_BETTING_ODDS_PRECISION / (op.backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION); + } + else + _effective_bet_type = op.back_or_lay; +#endif + + // if this is the first bet in the betting market group, do sanity checks + // if there are already other bets, then we don't need to do them because we know + // that we did the check on the first bet and we prevented anything important from + // changing while those bets existed. + auto& position_index = d.get_index_type().indices().get(); + auto position_iter = position_index.lower_bound(_betting_market_group->id); + if (position_iter == position_index.end()) + { + auto& betting_market_index = d.get_index_type().indices().get(); + auto betting_markets_in_group = boost::make_iterator_range(betting_market_index.equal_range(_betting_market_group->id)); + FC_ASSERT(boost::size(betting_markets_in_group) > 1 || + _betting_market_group->resolution_constraint == betting_market_resolution_constraint::at_most_one_winner, + "A betting market group with only one market must be set to at_most_one_winner"); + } + return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } @@ -317,10 +375,16 @@ object_id_type bet_place_evaluator::do_apply(const bet_place_operation& op) const bet_object& new_bet = d.create([&](bet_object& bet_obj) { bet_obj.bettor_id = op.bettor_id; - bet_obj.betting_market_id = op.betting_market_id; + bet_obj.betting_market_id = _betting_market->id; bet_obj.amount_to_bet = op.amount_to_bet; bet_obj.backer_multiplier = op.backer_multiplier; +#ifdef ENABLE_SIMPLE_CROSS_MARKET_MATCHING + bet_obj.back_or_lay = _effective_bet_type; + bet_obj.placed_as_back_or_lay = op.back_or_lay; +#else bet_obj.back_or_lay = op.back_or_lay; +#endif + if (_betting_market_group->bets_are_delayed()) { // the bet will be included in the block at time `head_block_time() + block_interval`, so make the delay relative // to the time it's included in a block @@ -329,18 +393,16 @@ object_id_type bet_place_evaluator::do_apply(const bet_place_operation& op) }); bet_id_type new_bet_id = new_bet.id; // save the bet id here, new_bet may be deleted during place_bet() + + // call place_bet() which pays for the bet and adjusts the position, but leaves the bet unmatched. + // the bet could be canceled here if it was too small to ever match + bool bet_was_removed = d.place_bet(new_bet); - // place the bet, this may return guaranteed winnings + // this may return guaranteed winnings ddump((_betting_market_group->bets_are_delayed())(_current_params->live_betting_delay_time())); - if (!_betting_market_group->bets_are_delayed() || _current_params->live_betting_delay_time() <= 0) - d.place_bet(new_bet); - - // now that their guaranteed winnings have been returned, check whether they have enough in their account to place the bet - FC_ASSERT( d.get_balance( *fee_paying_account, *_asset ).amount >= op.amount_to_bet.amount, "insufficient balance", - ("balance", d.get_balance(*fee_paying_account, *_asset))("amount_to_bet", op.amount_to_bet.amount) ); - - // pay for it - d.adjust_balance(fee_paying_account->id, -op.amount_to_bet); + if (!bet_was_removed && + (!_betting_market_group->bets_are_delayed() || _current_params->live_betting_delay_time() <= 0)) + d.try_to_match_bet(new_bet); return new_bet_id; } FC_CAPTURE_AND_RETHROW( (op) ) } diff --git a/libraries/chain/betting_market_group_object.cpp b/libraries/chain/betting_market_group_object.cpp index 584039c9c..26983a2b2 100644 --- a/libraries/chain/betting_market_group_object.cpp +++ b/libraries/chain/betting_market_group_object.cpp @@ -358,6 +358,7 @@ betting_market_group_object::betting_market_group_object(const betting_market_gr total_matched_bets_amount(rhs.total_matched_bets_amount), never_in_play(rhs.never_in_play), delay_before_settling(rhs.delay_before_settling), + resolution_constraint(rhs.resolution_constraint), settling_time(rhs.settling_time), my(new impl(this)) { @@ -376,6 +377,7 @@ betting_market_group_object& betting_market_group_object::operator=(const bettin total_matched_bets_amount = rhs.total_matched_bets_amount; never_in_play = rhs.never_in_play; delay_before_settling = rhs.delay_before_settling; + resolution_constraint = rhs.resolution_constraint; settling_time = rhs.settling_time; my->state_machine = rhs.my->state_machine; @@ -435,7 +437,7 @@ betting_market_group_status betting_market_group_object::get_status() const (void)&state_constants_are_correct; betting_market_group_state state = (betting_market_group_state)my->state_machine.current_state()[0]; - ddump((state)); + // ddump((state)); switch (state) { @@ -539,6 +541,16 @@ void betting_market_group_object::dispatch_new_status(database& db, betting_mark } +bool betting_market_group_position_object::is_empty() const +{ + for (const betting_market_position& market_position : market_positions) + if (market_position.pay_if_payout_condition != 0 || + market_position.pay_required_by_unmatched_bets != 0) + return false; + return pay_if_canceled == 0; +} + + } } // graphene::chain namespace fc { @@ -554,6 +566,7 @@ namespace fc { ("total_matched_bets_amount", betting_market_group_obj.total_matched_bets_amount) ("never_in_play", betting_market_group_obj.never_in_play) ("delay_before_settling", betting_market_group_obj.delay_before_settling) + ("resolution_constraint", betting_market_group_obj.resolution_constraint) ("settling_time", betting_market_group_obj.settling_time) ("status", betting_market_group_obj.get_status()); @@ -570,6 +583,7 @@ namespace fc { betting_market_group_obj.total_matched_bets_amount = v["total_matched_bets_amount"].as(); betting_market_group_obj.never_in_play = v["never_in_play"].as(); betting_market_group_obj.delay_before_settling = v["delay_before_settling"].as(); + betting_market_group_obj.resolution_constraint = v["resolution_constraint"].as(); betting_market_group_obj.settling_time = v["settling_time"].as>(); graphene::chain::betting_market_group_status status = v["status"].as(); const_cast(betting_market_group_obj.my->state_machine.current_state())[0] = (int)status; diff --git a/libraries/chain/betting_market_object.cpp b/libraries/chain/betting_market_object.cpp index a0beeb7d6..079d840ea 100644 --- a/libraries/chain/betting_market_object.cpp +++ b/libraries/chain/betting_market_object.cpp @@ -122,24 +122,6 @@ share_type bet_object::get_minimum_matching_amount() const 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); - 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; -} - // betting market object implementation namespace { diff --git a/libraries/chain/db_bet.cpp b/libraries/chain/db_bet.cpp index 8c3e13575..23987ee7f 100644 --- a/libraries/chain/db_bet.cpp +++ b/libraries/chain/db_bet.cpp @@ -36,11 +36,111 @@ namespace graphene { namespace chain { +// helper functions to return the amount of exposure provided or required for a given bet +// if invert is false, returns the wins provided by the bet when it matches, +// if invert is true, returns the wins required for the bet when it is unmatched +std::vector get_unmatched_bet_position_for_bet(database& db, + const bet_object& bet, + share_type amount_bet, bool invert) +{ + const betting_market_object& betting_market = bet.betting_market_id(db); + const betting_market_group_object& betting_market_group = betting_market.group_id(db); + const auto& betting_market_index = db.get_index_type().indices().get(); + auto betting_markets_in_group = boost::make_iterator_range(betting_market_index.equal_range(betting_market_group.id)); + uint32_t number_of_betting_markets = boost::size(betting_markets_in_group); + + bet_type effective_bet_type; + if (invert) + effective_bet_type = bet.back_or_lay == bet_type::back ? bet_type::lay : bet_type::back; + else + effective_bet_type = bet.back_or_lay; + + std::vector wins_required; + if (number_of_betting_markets == 0) + return wins_required; +#ifdef ENABLE_SIMPLE_CROSS_MARKET_MATCHING + else if (number_of_betting_markets == 1 || + (number_of_betting_markets == 2 && + betting_market_group.resolution_constraint == betting_market_resolution_constraint::exactly_one_winner)) + { + // in both cases, we have exactly one real betting market with two possible outcome + wins_required.resize(2); + wins_required[effective_bet_type == bet_type::back ? 0 : 1] = amount_bet; + return wins_required; + } +#endif + else + { + uint32_t number_of_market_positions = number_of_betting_markets; + if (betting_market_group.resolution_constraint != betting_market_resolution_constraint::exactly_one_winner) + ++number_of_market_positions; + wins_required.reserve(number_of_market_positions); + for (const betting_market_object& betting_market_in_group : betting_markets_in_group) + wins_required.push_back((effective_bet_type == bet_type::back) == (betting_market_in_group.id == bet.betting_market_id) ? + amount_bet : 0); + if (betting_market_group.resolution_constraint != betting_market_resolution_constraint::exactly_one_winner) + wins_required.push_back(effective_bet_type == bet_type::back ? 0 : amount_bet); + return wins_required; + } +} + +std::vector get_wins_required_for_unmatched_bet(database& db, const bet_object& bet, share_type amount_bet) +{ + return get_unmatched_bet_position_for_bet(db, bet, amount_bet, true); +} + +std::vector get_wins_provided_by_matched_bet(database& db, const bet_object& bet, share_type amount_matched) +{ + return get_unmatched_bet_position_for_bet(db, bet, amount_matched, false); +} + + void database::cancel_bet( const bet_object& bet, bool create_virtual_op ) { - asset amount_to_refund = bet.amount_to_bet; + // Find out how much of the bettor's position this bet leans on, so we know how much to refund + std::vector wins_required_for_canceled_bet = get_wins_required_for_unmatched_bet(*this, bet, bet.amount_to_bet.amount); + + const betting_market_object& betting_market = bet.betting_market_id(*this); + auto& position_index = get_index_type().indices().get(); + auto position_itr = position_index.find(boost::make_tuple(bet.bettor_id, betting_market.group_id)); + assert(position_itr != position_index.end()); // they must have had a position to account for this bet + const betting_market_group_position_object& position = *position_itr; + assert(position.market_positions.size() == wins_required_for_canceled_bet.size()); + assert(!position.market_positions.empty()); + + asset amount_to_refund; + modify(position, [&](betting_market_group_position_object& market_group_position) { + // to compute the amount we're able to refund, we + // * subtract out the pay_required_by_unmatched_bets caused by this bet + // * find the maximum amount of unused win position, which is the amount the + // bettor is guaranteed to get back if any of the markets in this group wins + // * limit this amount by their cancel position + for (unsigned i = 0; i < market_group_position.market_positions.size(); ++i) + market_group_position.market_positions[i].pay_required_by_unmatched_bets -= wins_required_for_canceled_bet[i]; + share_type possible_refund = market_group_position.market_positions[0].pay_if_payout_condition - + market_group_position.market_positions[0].pay_required_by_unmatched_bets; + for (unsigned i = 1; i < market_group_position.market_positions.size(); ++i) + possible_refund = std::min(possible_refund, market_group_position.market_positions[i].pay_if_payout_condition - + market_group_position.market_positions[i].pay_required_by_unmatched_bets); + amount_to_refund = asset(std::min(possible_refund, market_group_position.pay_if_canceled), + bet.amount_to_bet.asset_id); + + // record that we refunded the bettor in their position + market_group_position.pay_if_canceled -= amount_to_refund.amount; + for (betting_market_position& market_position : market_group_position.market_positions) + market_position.pay_if_payout_condition -= amount_to_refund.amount; + }); + + // we could rewrite this to only modify if not removed + if (position.is_empty()) + remove(position); + //TODO: update global statistics - adjust_balance(bet.bettor_id, amount_to_refund); + // refund to their balance + if (amount_to_refund.amount > 0) + adjust_balance(bet.bettor_id, amount_to_refund); + + // and create a virtual op recording that this happened if (create_virtual_op) { bet_canceled_operation bet_canceled_virtual_op(bet.bettor_id, bet.id, @@ -48,12 +148,15 @@ void database::cancel_bet( const bet_object& bet, bool create_virtual_op ) //fc_idump(fc::logger::get("betting"), (bet_canceled_virtual_op)); push_applied_operation(std::move(bet_canceled_virtual_op)); } + + // finally, drop the bet from the order books remove(bet); } -void database::cancel_all_unmatched_bets_on_betting_market(const betting_market_object& betting_market) +// Cancel all bets (for all bettors) on the given betting market +void cancel_all_unmatched_bets_on_betting_market(database& db, const betting_market_object& betting_market) { - const auto& bet_odds_idx = get_index_type().indices().get(); + const auto& bet_odds_idx = db.get_index_type().indices().get(); // first, cancel all bets on the active books auto book_itr = bet_odds_idx.lower_bound(std::make_tuple(betting_market.id)); @@ -62,7 +165,7 @@ void database::cancel_all_unmatched_bets_on_betting_market(const betting_market_ { auto old_book_itr = book_itr; ++book_itr; - cancel_bet(*old_book_itr, true); + db.cancel_bet(*old_book_itr, true); } // then, cancel any delayed bets on that market. We don't have an index for @@ -74,7 +177,26 @@ void database::cancel_all_unmatched_bets_on_betting_market(const betting_market_ auto old_book_itr = book_itr; ++book_itr; if (old_book_itr->betting_market_id == betting_market.id) - cancel_bet(*old_book_itr, true); + db.cancel_bet(*old_book_itr, true); + } +} + +// Cancel all bets (for all bettors) on the given betting market. This loops through all betting markets in the group +// and cancels each bet in each market separately. We do it this way because it allows us to generate virtual operations +// that inform the betters that their bets were canceled. +// If this wasn't desired, we could do this much more efficiently: +// - walk through all market positions for this group, and zero out the pay_required_for_unmatched_bets +// - if possible, refund any funds that were backing those bets +// - remove the bets from the order books +void database::cancel_all_unmatched_bets_on_betting_market_group(const betting_market_group_object& betting_market_group) +{ + auto& betting_market_index = get_index_type().indices().get(); + auto betting_market_itr = betting_market_index.lower_bound(betting_market_group.id); + while (betting_market_itr != betting_market_index.end() && betting_market_itr->group_id == betting_market_group.id) + { + const betting_market_object& betting_market = *betting_market_itr; + ++betting_market_itr; + cancel_all_unmatched_bets_on_betting_market(*this, betting_market); } } @@ -82,11 +204,13 @@ void database::validate_betting_market_group_resolutions(const betting_market_gr const std::map& resolutions) { auto& betting_market_index = get_index_type().indices().get(); - auto betting_markets_in_group = boost::make_iterator_range(betting_market_index.equal_range(betting_market_group.id)); + auto betting_markets_in_group = boost::make_iterator_range(betting_market_index.equal_range(boost::make_tuple(betting_market_group.id))); // we must have one resolution for each betting market FC_ASSERT(resolutions.size() == boost::size(betting_markets_in_group), - "You must publish resolutions for all ${size} markets in the group, you published ${published}", ("size", boost::size(betting_markets_in_group))("published", resolutions.size())); + "You must publish resolutions for all ${size} markets in the group, you published ${published}", + ("size", boost::size(betting_markets_in_group)) + ("published", resolutions.size())); // both are sorted by id, we can walk through both and verify that they match unsigned number_of_wins = 0; @@ -106,21 +230,10 @@ void database::validate_betting_market_group_resolutions(const betting_market_gr if (number_of_cancels != 0) FC_ASSERT(number_of_cancels == resolutions.size(), "You must cancel all betting markets or none of the betting markets in the group"); - else + else if (betting_market_group.resolution_constraint == betting_market_resolution_constraint::exactly_one_winner) FC_ASSERT(number_of_wins == 1, "There must be exactly one winning market"); -} - -void database::cancel_all_unmatched_bets_on_betting_market_group(const betting_market_group_object& betting_market_group) -{ - auto& betting_market_index = get_index_type().indices().get(); - auto betting_market_itr = betting_market_index.lower_bound(betting_market_group.id); - while (betting_market_itr != betting_market_index.end() && betting_market_itr->group_id == betting_market_group.id) - { - const betting_market_object& betting_market = *betting_market_itr; - ++betting_market_itr; - cancel_all_unmatched_bets_on_betting_market(betting_market); - } - + else + FC_ASSERT(number_of_wins == 0 || number_of_wins == 1, "There must be exactly zero or one winning market"); } void database::resolve_betting_market_group(const betting_market_group_object& betting_market_group, @@ -167,126 +280,120 @@ void database::settle_betting_market_group(const betting_market_group_object& be const asset_dividend_data_object& core_asset_dividend_data_obj = (*core_asset_obj.dividend_data_id)(*this); rake_account_id = core_asset_dividend_data_obj.dividend_distribution_account; } + uint16_t rake_fee_percentage = get_global_properties().parameters.betting_rake_fee_percentage(); - affiliate_payout_helper payout_helper( *this, betting_market_group ); + affiliate_payout_helper payout_helper(*this, betting_market_group); - // collect the resolutions of all markets in the BMG: they were previously published and - // stored in the individual betting markets - std::map resolutions_by_market_id; - - // collecting bettors and their positions - std::map > bettor_positions_map; + // figure out which position we're paying out -- it will either be the cancel position, or the + // pay_if_payout_condition of one of the market positions. The BMG position has an array of market + // positions, store the index of the winning position here: (or leave invalid for cancel) + fc::optional winning_position_index; auto& betting_market_index = get_index_type().indices().get(); - // [ROL] it seems to be my mistake - wrong index used - //auto& position_index = get_index_type().indices().get(); - auto& position_index = get_index_type().indices().get(); auto betting_market_itr = betting_market_index.lower_bound(betting_market_group.id); - while (betting_market_itr != betting_market_index.end() && betting_market_itr->group_id == betting_market_group.id) - { - const betting_market_object& betting_market = *betting_market_itr; - FC_ASSERT(betting_market_itr->resolution, "Unexpected error settling betting market ${market_id}: no published resolution", - ("market_id", betting_market_itr->id)); - resolutions_by_market_id.emplace(betting_market.id, *betting_market_itr->resolution); - - ++betting_market_itr; - cancel_all_unmatched_bets_on_betting_market(betting_market); - - auto position_itr = position_index.lower_bound(betting_market.id); - - while (position_itr != position_index.end() && position_itr->betting_market_id == betting_market.id) - { - const betting_market_position_object& position = *position_itr; - ++position_itr; - - bettor_positions_map[position.bettor_id].push_back(&position); - } - } - // walking through bettors' positions and collecting winings and fees respecting asset_id - for (const auto& bettor_positions_pair: bettor_positions_map) + // in the unlikely case that this betting market group has no betting markets, we can skip all of the + // payout code + if (betting_market_itr != betting_market_index.end()) { - uint16_t rake_fee_percentage = get_global_properties().parameters.betting_rake_fee_percentage(); - share_type net_profits; - share_type payout_amounts; - account_id_type bettor_id = bettor_positions_pair.first; - const std::vector& bettor_positions = bettor_positions_pair.second; - - for (const betting_market_position_object* position : bettor_positions) + uint32_t current_betting_market_index = 0; + uint32_t cancel_count = 0; + while (betting_market_itr != betting_market_index.end() && betting_market_itr->group_id == betting_market_group.id) { - betting_market_resolution_type resolution; - try + assert(betting_market_itr->resolution); // the blockchain should not allow you to publish an invalid resolution + fc_ilog(fc::logger::get("betting"), "Market: ${market_id}: ${resolution}", ("market_id", betting_market_itr->id)("resolution", betting_market_itr->resolution)); + FC_ASSERT(betting_market_itr->resolution, "Unexpected error settling betting market ${market_id}: no published resolution", + ("market_id", betting_market_itr->id)); + if (*betting_market_itr->resolution == betting_market_resolution_type::cancel) { - resolution = resolutions_by_market_id.at(position->betting_market_id); + // sanity check -- if one market is canceled, they all must be: + assert(cancel_count == current_betting_market_index); + FC_ASSERT(cancel_count == current_betting_market_index, + "Unexpected error settling betting market group ${market_group_id}: all markets in a group must be canceled together", + ("market_group_id", betting_market_group.id)); + ++cancel_count; } - catch (std::out_of_range&) + else if (*betting_market_itr->resolution == betting_market_resolution_type::win) { - FC_THROW_EXCEPTION(fc::key_not_found_exception, "Unexpected betting market ID, shouldn't happen"); + // sanity check -- we can never have two markets win + assert(!winning_position_index); + FC_ASSERT(!winning_position_index, + "Unexpected error settling betting market group ${market_group_id}: there can only be one winner", + ("market_group_id", betting_market_group.id)); + winning_position_index = current_betting_market_index; } + ++betting_market_itr; + ++current_betting_market_index; + } + // now that we've walked over all markets, we should know that: + // - all markets were canceled + // - one market was the winner + // - no markets won + // in the last case, we will have one more market position than we have markets, which functions + // as a "none of the above", so we'll make that the winner. + if (!cancel_count) + { + if (betting_market_group.resolution_constraint == betting_market_resolution_constraint::exactly_one_winner) + { + assert(winning_position_index); + FC_ASSERT(winning_position_index, + "Unexpected error settling betting market group ${market_group_id}: there must be one winner", + ("market_group_id", betting_market_group.id)); + } + else if (!winning_position_index) + winning_position_index = current_betting_market_index; + } - ///if (cancel) - /// resolution = betting_market_resolution_type::cancel; - ///else - ///{ - /// // checked in evaluator, should never happen, see above - /// assert(resolutions.count(position->betting_market_id)); - /// resolution = resolutions.at(position->betting_market_id); - ///} + auto& position_index = get_index_type().indices().get(); + auto position_iter = position_index.lower_bound(betting_market_group.id); + while (position_iter != position_index.end() && + position_iter->betting_market_group_id == betting_market_group.id) + { + const betting_market_group_position_object& position = *position_iter; + ++position_iter; + share_type net_profits; + share_type total_payout; + if (winning_position_index) + { + assert(winning_position_index < position.market_positions.size()); + FC_ASSERT(winning_position_index < position.market_positions.size(), + "Unexpected error paying bettor ${bettor_id} for market group ${market_group_id}: market positions array is the wrong size", + ("bettor_id", position.bettor_id)("market_group_id", betting_market_group.id)); + total_payout = position.market_positions[*winning_position_index].pay_if_payout_condition; + net_profits = std::max(total_payout - position.pay_if_canceled, 0); + } + else // canceled + total_payout = position.pay_if_canceled; - switch (resolution) + // pay the fees to the dividend-distribution account if net profit + share_type rake_amount; + if (net_profits.value > 0 && rake_account_id) { - case betting_market_resolution_type::win: - { - share_type total_payout = position->pay_if_payout_condition + position->pay_if_not_canceled; - payout_amounts += total_payout; - net_profits += total_payout - position->pay_if_canceled; - break; - } - case betting_market_resolution_type::not_win: - { - share_type total_payout = position->pay_if_not_payout_condition + position->pay_if_not_canceled; - payout_amounts += total_payout; - net_profits += total_payout - position->pay_if_canceled; - break; - } - case betting_market_resolution_type::cancel: - payout_amounts += position->pay_if_canceled; - break; - default: - continue; + rake_amount = ((fc::uint128_t(net_profits.value) * rake_fee_percentage + GRAPHENE_100_PERCENT - 1) / GRAPHENE_100_PERCENT).to_uint64(); + share_type affiliates_share; + if (rake_amount.value) + affiliates_share = payout_helper.payout(position.bettor_id, rake_amount); + assert(rake_amount.value >= affiliates_share.value); + FC_ASSERT(rake_amount.value >= affiliates_share.value); + if (rake_amount.value > affiliates_share.value) + adjust_balance(*rake_account_id, asset(rake_amount - affiliates_share, betting_market_group.asset_id)); } - remove(*position); - } + + // pay winning - rake + adjust_balance(position.bettor_id, asset(total_payout - rake_amount, betting_market_group.asset_id)); - // pay the fees to the dividend-distribution account if net profit - share_type rake_amount; - if (net_profits.value > 0 && rake_account_id) - { - rake_amount = ((fc::uint128_t(net_profits.value) * rake_fee_percentage + GRAPHENE_100_PERCENT - 1) / GRAPHENE_100_PERCENT).to_uint64(); - share_type affiliates_share; - if (rake_amount.value) - affiliates_share = payout_helper.payout( bettor_id, rake_amount ); - FC_ASSERT( rake_amount.value >= affiliates_share.value ); - if (rake_amount.value > affiliates_share.value) - adjust_balance(*rake_account_id, asset(rake_amount - affiliates_share, betting_market_group.asset_id)); + push_applied_operation(betting_market_group_resolved_operation(position.bettor_id, + betting_market_group.id, + total_payout - rake_amount, + rake_amount)); + + remove(position); } - - // pay winning - rake - adjust_balance(bettor_id, asset(payout_amounts - rake_amount, betting_market_group.asset_id)); - // [ROL] - //fc_idump(fc::logger::get("betting"), (payout_amounts)(net_profits.value)(rake_amount.value)); - - push_applied_operation(betting_market_group_resolved_operation(bettor_id, - betting_market_group.id, - resolutions_by_market_id, - payout_amounts, - rake_amount)); - } + } // end if the group had at least one market // At this point, the betting market group will either be in the "graded" or "canceled" state, // if it was graded, mark it as settled. if it's canceled, let it remain canceled. - bool was_canceled = betting_market_group.get_status() == betting_market_group_status::canceled; if (!was_canceled) @@ -303,8 +410,6 @@ void database::settle_betting_market_group(const betting_market_group_object& be remove(betting_market); } - const event_object& event = betting_market_group.event_id(*this); - fc_dlog(fc::logger::get("betting"), "removing betting market group ${id}", ("id", betting_market_group.id)); remove(betting_market_group); @@ -334,89 +439,111 @@ void database::remove_completed_events() } } -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 matched_amount) -{ 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 + matched_amount : 0; - position.pay_if_not_payout_condition = back_or_lay == bet_type::lay ? bet_amount + matched_amount : 0; - position.pay_if_canceled = bet_amount; - position.pay_if_not_canceled = 0; - // 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 + matched_amount : 0; - position.pay_if_not_payout_condition += back_or_lay == bet_type::lay ? bet_amount + matched_amount : 0; - position.pay_if_canceled += bet_amount; - - guaranteed_winnings_returned = position.reduce(); - }); - } - return guaranteed_winnings_returned; -} FC_CAPTURE_AND_RETHROW((bettor_id)(betting_market_id)(bet_amount)) } - - // called twice when a bet is matched, once for the taker, once for the maker -bool bet_was_matched(database& db, const bet_object& bet, +bool bet_was_matched(database& db, const bet_object& bet, + const betting_market_object& betting_market, share_type amount_bet, share_type amount_matched, bet_multiplier_type actual_multiplier, bool refund_unmatched_portion) { - // 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, amount_matched); - db.adjust_balance(bet.bettor_id, asset(guaranteed_winnings_returned, bet.amount_to_bet.asset_id)); + // get the wins provided by this match + std::vector additional_wins = get_wins_provided_by_matched_bet(db, bet, amount_bet + amount_matched); + + // We are removing all or part of this bet from the unmatched order book so we will need to adjust + // pay_required_for_unmatched_bets accordingly + share_type amount_removed_from_unmatched_bets = refund_unmatched_portion ? bet.amount_to_bet.amount : amount_bet; + std::vector wins_required_for_unmatched_bet = get_wins_required_for_unmatched_bet(db, bet, amount_removed_from_unmatched_bets); + assert(additional_wins.size() == wins_required_for_unmatched_bet.size()); + + fc_dlog(fc::logger::get("betting"), "The newly-matched bets provides wins of ${wins_required_for_unmatched_bet}", (wins_required_for_unmatched_bet)); + + auto& position_index = db.get_index_type().indices().get(); + auto position_itr = position_index.find(boost::make_tuple(bet.bettor_id, betting_market.group_id)); + assert(position_itr != position_index.end()); // they must have had a position to account for this bet + const betting_market_group_position_object& position = *position_itr; + assert(position.market_positions.size() == additional_wins.size()); + share_type not_cancel; + db.modify(position, [&](betting_market_group_position_object& market_group_position) { + for (unsigned i = 0; i < additional_wins.size(); ++i) + { + market_group_position.market_positions[i].pay_if_payout_condition += additional_wins[i]; + if (i == 0) + not_cancel = market_group_position.market_positions[i].pay_if_payout_condition; + else + not_cancel = std::min(not_cancel, market_group_position.market_positions[i].pay_if_payout_condition); + } + for (unsigned i = 0; i < additional_wins.size(); ++i) + { + market_group_position.market_positions[i].pay_if_payout_condition -= not_cancel; + market_group_position.market_positions[i].pay_required_by_unmatched_bets -= wins_required_for_unmatched_bet[i]; + } + }); - // generate a virtual "match" op - asset asset_amount_bet(amount_bet, bet.amount_to_bet.asset_id); + // pay for the bet out of the not cancel + assert(not_cancel >= amount_bet); + not_cancel -= amount_bet; + + // save this, we'll need it later if we remove the bet + bet_id_type bet_id = bet.id; + account_id_type bettor_id = bet.bettor_id; + asset_id_type bet_asset_id = bet.amount_to_bet.asset_id; - bet_matched_operation bet_matched_virtual_op(bet.bettor_id, bet.id, - asset_amount_bet, - actual_multiplier, - guaranteed_winnings_returned); - //fc_edump(fc::logger::get("betting"), (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) + // adjust the bet, removing it if it completely matched + bool bet_was_removed = false; + if (amount_bet == bet.amount_to_bet.amount) { db.remove(bet); - return true; + bet_was_removed = true; } else { db.modify(bet, [&](bet_object& bet_obj) { - bet_obj.amount_to_bet -= asset_amount_bet; + bet_obj.amount_to_bet.amount -= amount_bet; }); if (refund_unmatched_portion) { - db.cancel_bet(bet); - return true; + db.remove(bet); + bet_was_removed = true; } else - return false; + bet_was_removed = false; } + + // now, figure out how much of the remaining unmatched bets aren't covered by the position + share_type max_shortfall; + for (const betting_market_position& market_position : position.market_positions) + max_shortfall = std::max(max_shortfall, market_position.pay_required_by_unmatched_bets - market_position.pay_if_payout_condition); + assert(not_cancel >= max_shortfall); // else there was a bet placed that shouldn't have been allowed + share_type possible_refund = not_cancel - max_shortfall; + share_type actual_refund = std::min(possible_refund, position.pay_if_canceled); + + // do the refund + not_cancel -= actual_refund; + if (not_cancel > 0 || actual_refund > 0) + db.modify(position, [&](betting_market_group_position_object& market_group_position) { + for (betting_market_position& market_position : market_group_position.market_positions) + market_position.pay_if_payout_condition += not_cancel; + market_group_position.pay_if_canceled -= actual_refund; + }); + + db.adjust_balance(bettor_id, asset(actual_refund, bet_asset_id)); + + // generate a virtual "match" op + asset asset_amount_bet(amount_bet, bet_asset_id); + bet_matched_operation bet_matched_virtual_op(bettor_id, bet_id, + asset_amount_bet, + actual_multiplier, + actual_refund); + + db.push_applied_operation(std::move(bet_matched_virtual_op)); + + fc_dlog(fc::logger::get("betting"), "After matching bet, new market position is: ${position}", (position)); + if (position.is_empty()) + db.remove(position); + + return bet_was_removed; } /** @@ -429,9 +556,9 @@ bool bet_was_matched(database& db, const bet_object& bet, * 2 - maker_bet was filled and removed from the books * 3 - both were filled and removed from the books */ -int match_bet(database& db, const bet_object& taker_bet, const bet_object& maker_bet ) +int match_bet(database& db, const bet_object& taker_bet, const bet_object& maker_bet, const betting_market_object& betting_market) { - //fc_idump(fc::logger::get("betting"), (taker_bet)(maker_bet)); + //fc_ddump(fc::logger::get("betting"), (taker_bet)(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.back_or_lay == bet_type::back ? taker_bet.backer_multiplier <= maker_bet.backer_multiplier : @@ -463,14 +590,18 @@ int match_bet(database& db, const bet_object& taker_bet, const bet_object& maker share_type maximum_factor_taker_is_willing_to_pay = taker_bet.amount_to_bet.amount / taker_odds_ratio; share_type maximum_taker_factor = maximum_factor_taker_is_willing_to_pay; +#ifdef ENABLE_SIMPLE_CROSS_MARKET_MATCHING + if (taker_bet.placed_as_back_or_lay == bet_type::lay) { +#else if (taker_bet.back_or_lay == bet_type::lay) { +#endif share_type maximum_factor_taker_is_willing_to_receive = taker_bet.get_exact_matching_amount() / maker_odds_ratio; - //fc_idump(fc::logger::get("betting"), (maximum_factor_taker_is_willing_to_pay)); + fc_idump(fc::logger::get("betting"), (maximum_factor_taker_is_willing_to_pay)); bool taker_was_limited_by_matching_amount = maximum_factor_taker_is_willing_to_receive < maximum_factor_taker_is_willing_to_pay; if (taker_was_limited_by_matching_amount) maximum_taker_factor = maximum_factor_taker_is_willing_to_receive; } - //fc_idump(fc::logger::get("betting"), (maximum_factor_taker_is_willing_to_pay)(maximum_taker_factor)); + fc_idump(fc::logger::get("betting"), (maximum_factor_taker_is_willing_to_pay)(maximum_taker_factor)); 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); @@ -556,7 +687,28 @@ int match_bet(database& db, const bet_object& taker_bet, const bet_object& maker ("taker_odds", taker_bet.backer_multiplier)); fc_ddump(fc::logger::get("betting"), (taker_bet)); - db.adjust_balance(taker_bet.bettor_id, asset(taker_refund_amount, taker_bet.amount_to_bet.asset_id)); + // we don't really refund it here, we just adjust their position and this "refunded" amount will simply be added + // to the amount returned to their balance by bet_was_matched + auto& position_index = db.get_index_type().indices().get(); + auto position_itr = position_index.find(boost::make_tuple(taker_bet.bettor_id, betting_market.group_id)); + assert(position_itr != position_index.end()); // they must have had a position to account for this bet + FC_ASSERT(position_itr != position_index.end(), "Unexpected error, bettor ${bettor_id} had a bet but no position", ("bettor_id", taker_bet.bettor_id)); + std::vector wins_required_for_refunded_bet = get_wins_required_for_unmatched_bet(db, taker_bet, taker_refund_amount); + assert(wins_required_for_refunded_bet.size() == position_itr->market_positions.size()); + FC_ASSERT(wins_required_for_refunded_bet.size() == position_itr->market_positions.size(), + "Unexpected error, bettor's position didn't have the right amount of values"); + db.modify(*position_itr, [&](betting_market_group_position_object& market_group_position) { + for (uint32_t i = 0; i < market_group_position.market_positions.size(); ++i) + { + betting_market_position& market_position = market_group_position.market_positions[i]; + assert(market_position.pay_required_by_unmatched_bets >= wins_required_for_refunded_bet[i]); + FC_ASSERT(market_position.pay_required_by_unmatched_bets >= wins_required_for_refunded_bet[i], + "Unexpected error, bettor had an unmatched bet which wasn't accounted for in their exposure (expected: >= ${expected}, actual: ${actual})", + ("expected", wins_required_for_refunded_bet[i])("actual", market_position.pay_required_by_unmatched_bets)); + market_position.pay_required_by_unmatched_bets -= wins_required_for_refunded_bet[i]; + } + }); + // TODO: update global statistics bet_adjusted_operation bet_adjusted_op(taker_bet.bettor_id, taker_bet.id, asset(taker_refund_amount, taker_bet.amount_to_bet.asset_id)); @@ -567,8 +719,8 @@ int match_bet(database& db, const bet_object& taker_bet, const bet_object& maker // if the maker bet stays on the books, we need to make sure the taker bet is removed from the books (either it fills completely, // or any un-filled amount is canceled) - result |= bet_was_matched(db, taker_bet, taker_amount_to_match, maker_amount_to_match, maker_bet.backer_multiplier, !maker_bet_will_completely_match); - result |= bet_was_matched(db, maker_bet, maker_amount_to_match, taker_amount_to_match, maker_bet.backer_multiplier, false) << 1; + result |= bet_was_matched(db, taker_bet, betting_market, taker_amount_to_match, maker_amount_to_match, maker_bet.backer_multiplier, !maker_bet_will_completely_match); + result |= bet_was_matched(db, maker_bet, betting_market, maker_amount_to_match, taker_amount_to_match, maker_bet.backer_multiplier, false) << 1; assert(result != 0); return result; @@ -579,7 +731,8 @@ int match_bet(database& db, const bet_object& taker_bet, const bet_object& maker bool database::place_bet(const bet_object& new_bet_object) { // We allow users to place bets for any amount, but only amounts that are exact multiples of the odds - // ratio can be matched. Immediately return any unmatchable amount in this bet. + // ratio can be matched. + // We'll want to refund any unmatchable amount if we determine that they can place the bet. share_type minimum_matchable_amount = new_bet_object.get_minimum_matchable_amount(); share_type scale_factor = new_bet_object.amount_to_bet.amount / minimum_matchable_amount; share_type rounded_bet_amount = scale_factor * minimum_matchable_amount; @@ -587,19 +740,57 @@ bool database::place_bet(const bet_object& new_bet_object) if (rounded_bet_amount == share_type()) { // the bet was too small to match at all, cancel the bet - cancel_bet(new_bet_object, true); + // don't go through the normal cancel() code, we've never modified the position + // to account for it, so we don't need any of the calculations in cancel() + push_applied_operation(bet_canceled_operation(new_bet_object.bettor_id, new_bet_object.id, + new_bet_object.amount_to_bet)); + remove(new_bet_object); return true; } - else if (rounded_bet_amount != new_bet_object.amount_to_bet.amount) + + // we might be able to place the bet, we need to see if they have enough balance or exposure + // to cover the bet. + const betting_market_object& betting_market = new_bet_object.betting_market_id(*this); + const betting_market_group_object& betting_market_group = betting_market.group_id(*this); + + const auto& betting_market_index = get_index_type().indices().get(); + auto betting_markets_in_group = boost::make_iterator_range(betting_market_index.equal_range(betting_market_group.id)); + uint32_t number_of_betting_markets = boost::size(betting_markets_in_group); + + + // the remaining amount is placed as a bet, so account for it as if we paid for it + std::vector position_required = get_wins_required_for_unmatched_bet(*this, new_bet_object, rounded_bet_amount); + fc_ilog(fc::logger::get("betting"), "Wins required for unmatched bet: ${position_required}", (position_required)); + + auto& position_index = get_index_type().indices().get(); + auto position_itr = position_index.find(boost::make_tuple(new_bet_object.bettor_id, betting_market.group_id)); + share_type amount_to_pay_from_balance; + if (position_itr == position_index.end()) // if they have no position, they must pay the worst case + for (const share_type& required : position_required) + amount_to_pay_from_balance = std::max(amount_to_pay_from_balance, required); + else // we might be able to lean on their current position + for (unsigned i = 0; i < position_required.size(); ++i) + amount_to_pay_from_balance = std::max(amount_to_pay_from_balance, + position_required[i] + position_itr->market_positions[i].pay_required_by_unmatched_bets - + position_itr->market_positions[i].pay_if_payout_condition); + + fc_idump(fc::logger::get("betting"), (amount_to_pay_from_balance)); + if (amount_to_pay_from_balance > 0) + adjust_balance(new_bet_object.bettor_id, asset(-amount_to_pay_from_balance, betting_market_group.asset_id)); + + // still here? the bettor had enough funds to place the bet + + // if we had to round the bet amount, we should now generate a virtual op informing + // the bettor that we rounded their bet down. + if (rounded_bet_amount != new_bet_object.amount_to_bet.amount) { asset stake_returned = new_bet_object.amount_to_bet; stake_returned.amount -= rounded_bet_amount; modify(new_bet_object, [&rounded_bet_amount](bet_object& modified_bet_object) { - modified_bet_object.amount_to_bet.amount = rounded_bet_amount; + modified_bet_object.amount_to_bet.amount = rounded_bet_amount; }); - adjust_balance(new_bet_object.bettor_id, stake_returned); // TODO: update global statistics bet_adjusted_operation bet_adjusted_op(new_bet_object.bettor_id, new_bet_object.id, stake_returned); @@ -607,22 +798,60 @@ bool database::place_bet(const bet_object& new_bet_object) push_applied_operation(std::move(bet_adjusted_op)); fc_dlog(fc::logger::get("betting"), "Refunded ${refund_amount} to round the bet down to something that can match exactly, new bet: ${new_bet}", - ("refund_amount", stake_returned.amount) - ("new_bet", new_bet_object)); + ("refund_amount", stake_returned.amount) + ("new_bet", new_bet_object)); + } + + // Now, adjust their position. We need to add the position_required for this unmatched bet, and if they had to + // pay from their balance, also adjust their current pay & cancel positions + const betting_market_group_position_object* position; + if (position_itr == position_index.end()) + { + position = &create([&](betting_market_group_position_object& market_group_position) { + market_group_position.bettor_id = new_bet_object.bettor_id; + market_group_position.betting_market_group_id = betting_market.group_id; + market_group_position.market_positions.resize(position_required.size()); + for (unsigned i = 0; i < position_required.size(); ++i) + { + market_group_position.market_positions[i].pay_required_by_unmatched_bets = position_required[i]; + market_group_position.market_positions[i].pay_if_payout_condition = amount_to_pay_from_balance; + } + market_group_position.pay_if_canceled = amount_to_pay_from_balance; + }); + } else { + position = &*position_itr; + modify(*position, [&](betting_market_group_position_object& market_group_position) { + assert(market_group_position.bettor_id == new_bet_object.bettor_id); + assert(market_group_position.betting_market_group_id == betting_market.group_id); + assert(market_group_position.market_positions.size() == position_required.size()); + + for (unsigned i = 0; i < position_required.size(); ++i) + { + market_group_position.market_positions[i].pay_required_by_unmatched_bets += position_required[i]; + market_group_position.market_positions[i].pay_if_payout_condition += amount_to_pay_from_balance; + } + market_group_position.pay_if_canceled += amount_to_pay_from_balance; + }); } + return false; +} + +bool database::try_to_match_bet(const bet_object& bet_object) +{ + fc_dlog(fc::logger::get("betting"), "Attempting to match bet ${bet_object}", (bet_object)); + const betting_market_object& betting_market = bet_object.betting_market_id(*this); const auto& bet_odds_idx = get_index_type().indices().get(); - 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)); + bet_type bet_type_to_match = 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(bet_object.betting_market_id, bet_type_to_match)); + auto book_end = bet_odds_idx.upper_bound(std::make_tuple(bet_object.betting_market_id, bet_type_to_match, bet_object.backer_multiplier)); // fc_ilog(fc::logger::get("betting"), ""); // fc_ilog(fc::logger::get("betting"), "------------ order book ------------------"); // for (auto itr = book_itr; itr != book_end; ++itr) // fc_idump(fc::logger::get("betting"), (*itr)); // fc_ilog(fc::logger::get("betting"), "------------ order book ------------------"); - int orders_matched_flags = 0; bool finished = false; while (!finished && book_itr != book_end) @@ -630,13 +859,13 @@ bool database::place_bet(const bet_object& new_bet_object) auto old_book_itr = book_itr; ++book_itr; - orders_matched_flags = match_bet(*this, new_bet_object, *old_book_itr); + orders_matched_flags = match_bet(*this, bet_object, *old_book_itr, betting_market); // we continue if the maker bet was completely consumed AND the taker bet was not finished = orders_matched_flags != 2; } if (!(orders_matched_flags & 1)) - fc_ddump(fc::logger::get("betting"), (new_bet_object)); + fc_ddump(fc::logger::get("betting"), (bet_object)); // return true if the taker bet was completely consumed diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index d58c68f75..480a87016 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -162,8 +162,8 @@ const uint8_t betting_market_object::type_id; const uint8_t bet_object::space_id; const uint8_t bet_object::type_id; -const uint8_t betting_market_position_object::space_id; -const uint8_t betting_market_position_object::type_id; +const uint8_t betting_market_group_position_object::space_id; +const uint8_t betting_market_group_position_object::type_id; const uint8_t global_betting_statistics_object::space_id; const uint8_t global_betting_statistics_object::type_id; @@ -295,7 +295,7 @@ void database::initialize_indexes() add_index< primary_index< special_authority_index > >(); add_index< primary_index< buyback_index > >(); add_index< primary_index< simple_index< fba_accumulator_object > > >(); - add_index< primary_index< betting_market_position_index > >(); + add_index< primary_index< betting_market_group_position_index > >(); add_index< primary_index< global_betting_statistics_object_index > >(); //add_index< primary_index >(); //add_index< primary_index >(); diff --git a/libraries/chain/db_update.cpp b/libraries/chain/db_update.cpp index ad98837ed..f36dc5120 100644 --- a/libraries/chain/db_update.cpp +++ b/libraries/chain/db_update.cpp @@ -201,8 +201,8 @@ void database::place_delayed_bets() // we use an awkward looping mechanism here because there's a case where we are processing the // last delayed bet before the "real" order book starts and `iter` was pointing at the first - // real order. The place_bet() call can cause the that real order to be deleted, so we need - // to decide whether this is the last delayed bet before `place_bet` is called. + // real order. The try_to_match_bet() call can cause the that real order to be deleted, so we need + // to decide whether this is the last delayed bet before `try_to_match_bet` is called. bool last = iter == bet_odds_idx.end() || !iter->end_of_delay || *iter->end_of_delay > head_block_time(); @@ -230,7 +230,7 @@ void database::place_delayed_bets() bet_obj.end_of_delay.reset(); }); - place_bet(bet_to_place); + try_to_match_bet(bet_to_place); } } } FC_CAPTURE_AND_RETHROW() } diff --git a/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp b/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp index eb245c4a0..4d51b2b0e 100644 --- a/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp +++ b/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp @@ -109,6 +109,7 @@ namespace graphene { namespace chain { const chain_parameters* _current_params; const asset_object* _asset; share_type _stake_plus_fees; + bet_type _effective_bet_type; }; class bet_cancel_evaluator : public evaluator diff --git a/libraries/chain/include/graphene/chain/betting_market_object.hpp b/libraries/chain/include/graphene/chain/betting_market_object.hpp index f69b03f33..895b850d8 100644 --- a/libraries/chain/include/graphene/chain/betting_market_object.hpp +++ b/libraries/chain/include/graphene/chain/betting_market_object.hpp @@ -51,7 +51,7 @@ struct by_event_id; struct by_settling_time; struct by_betting_market_group_id; -class betting_market_rules_object : public graphene::db::abstract_object< betting_market_rules_object > +class betting_market_rules_object : public graphene::db::abstract_object { public: static const uint8_t space_id = protocol_ids; @@ -62,7 +62,7 @@ class betting_market_rules_object : public graphene::db::abstract_object< bettin internationalized_string_type description; }; -class betting_market_group_object : public graphene::db::abstract_object< betting_market_group_object > +class betting_market_group_object : public graphene::db::abstract_object { public: static const uint8_t space_id = protocol_ids; @@ -87,6 +87,8 @@ class betting_market_group_object : public graphene::db::abstract_object< bettin uint32_t delay_before_settling; + betting_market_resolution_constraint resolution_constraint; + fc::optional settling_time; // the time the payout will occur (set after grading) bool bets_are_allowed() const { @@ -99,6 +101,8 @@ class betting_market_group_object : public graphene::db::abstract_object< bettin } betting_market_group_status get_status() const; + unsigned get_number_of_betting_markets() const; + unsigned get_number_of_market_positions() const; // serialization functions: // for serializing to raw, go through a temporary sstream object to avoid @@ -130,7 +134,7 @@ class betting_market_group_object : public graphene::db::abstract_object< bettin std::unique_ptr my; }; -class betting_market_object : public graphene::db::abstract_object< betting_market_object > +class betting_market_object : public graphene::db::abstract_object { public: static const uint8_t space_id = protocol_ids; @@ -182,7 +186,21 @@ class betting_market_object : public graphene::db::abstract_object< betting_mark std::unique_ptr my; }; -class bet_object : public graphene::db::abstract_object< bet_object > + +// Uncomment to enable the incomplete support for simple cross-market matching. +// When enabled on a market group with two markets, we collapse the markets into +// one order book (backing in one market is really betting on the same outcome as +// laying in the other market). We will convert any bets in the second market into +// the corresponding bets in the first, thus a back in the first market could be +// matched against a back in the second market. +// There are some problems to work out though. Ones that come to mind are: +// - we don't permit the inverse odds for every odds you can bet. +// (we allow odds of 5.5 (2:9), but we don't allow the inverse 9:2, which would be 1.222222) +// - we would need to fake up an order book for the second market either in the bookie +// plugin or the UI +// #define ENABLE_SIMPLE_CROSS_MARKET_MATCHING + +class bet_object : public graphene::db::abstract_object { public: static const uint8_t space_id = protocol_ids; @@ -197,6 +215,10 @@ class bet_object : public graphene::db::abstract_object< bet_object > bet_multiplier_type backer_multiplier; bet_type back_or_lay; + +#ifdef ENABLE_SIMPLE_CROSS_MARKET_MATCHING + bet_type placed_as_back_or_lay; +#endif fc::optional end_of_delay; @@ -217,47 +239,53 @@ class bet_object : public graphene::db::abstract_object< bet_object > share_type get_minimum_matching_amount() const; }; -class betting_market_position_object : public graphene::db::abstract_object< betting_market_position_object > +class betting_market_position { + public: + share_type pay_if_payout_condition; + share_type pay_required_by_unmatched_bets; +}; + +class betting_market_group_position_object : public graphene::db::abstract_object { public: static const uint8_t space_id = implementation_ids; - static const uint8_t type_id = impl_betting_market_position_object_type; + static const uint8_t type_id = impl_betting_market_group_position_object_type; account_id_type bettor_id; - betting_market_id_type betting_market_id; + betting_market_group_id_type betting_market_group_id; - share_type pay_if_payout_condition; - share_type pay_if_not_payout_condition; share_type pay_if_canceled; - share_type pay_if_not_canceled; - share_type fees_collected; + std::vector market_positions; - share_type reduce(); + bool is_empty() const; }; typedef multi_index_container< betting_market_rules_object, indexed_by< - ordered_unique< tag, member< object, object_id_type, &object::id > > - > > betting_market_rules_object_multi_index_type; + ordered_unique, member> + >> betting_market_rules_object_multi_index_type; typedef generic_index betting_market_rules_object_index; typedef multi_index_container< betting_market_group_object, indexed_by< - ordered_unique< tag, member< object, object_id_type, &object::id > >, - ordered_non_unique< tag, member >, - ordered_non_unique< tag, member, &betting_market_group_object::settling_time> > - > > betting_market_group_object_multi_index_type; + ordered_unique, member>, + ordered_non_unique, member>, + ordered_non_unique, member, &betting_market_group_object::settling_time>> + >> betting_market_group_object_multi_index_type; typedef generic_index betting_market_group_object_index; typedef multi_index_container< betting_market_object, indexed_by< - ordered_unique< tag, member< object, object_id_type, &object::id > >, - ordered_non_unique< tag, member > - > > betting_market_object_multi_index_type; + ordered_unique, member>, + ordered_unique, + composite_key, + member>> + >> betting_market_object_multi_index_type; typedef generic_index betting_market_object_index; @@ -591,31 +619,31 @@ struct by_bettor_and_odds {}; typedef multi_index_container< bet_object, indexed_by< - ordered_unique< tag, member< object, object_id_type, &object::id > >, - ordered_unique< tag, identity, compare_bet_by_odds >, - ordered_non_unique< tag, member >, - ordered_unique< tag, identity, compare_bet_by_bettor_then_odds > > > bet_object_multi_index_type; + ordered_unique, member>, + ordered_unique, identity, compare_bet_by_odds>, + ordered_non_unique, member>, + ordered_unique, identity, compare_bet_by_bettor_then_odds>>> bet_object_multi_index_type; typedef generic_index bet_object_index; -struct by_bettor_betting_market{}; -struct by_betting_market_bettor{}; +struct by_bettor_betting_market_group{}; +struct by_betting_market_group_bettor{}; typedef multi_index_container< - betting_market_position_object, + betting_market_group_position_object, indexed_by< - ordered_unique< tag, member< object, object_id_type, &object::id > >, - ordered_unique< tag, + ordered_unique, member>, + ordered_unique, composite_key< - betting_market_position_object, - member, - member > >, - ordered_unique< tag, + betting_market_group_position_object, + member, + member>>, + ordered_unique, composite_key< - betting_market_position_object, - member, - member > > - > > betting_market_position_multi_index_type; + betting_market_group_position_object, + member, + member>> + >> betting_market_group_position_multi_index_type; -typedef generic_index betting_market_position_index; +typedef generic_index betting_market_group_position_index; template @@ -623,7 +651,7 @@ inline Stream& operator<<( Stream& s, const betting_market_object& betting_marke { // pack all fields exposed in the header in the usual way // instead of calling the derived pack, just serialize the one field in the base class - // fc::raw::pack >(s, betting_market_obj); + // fc::raw::pack>(s, betting_market_obj); fc::raw::pack(s, betting_market_obj.id); fc::raw::pack(s, betting_market_obj.group_id); fc::raw::pack(s, betting_market_obj.description); @@ -642,7 +670,7 @@ template inline Stream& operator>>( Stream& s, betting_market_object& betting_market_obj ) { // unpack all fields exposed in the header in the usual way - //fc::raw::unpack >(s, betting_market_obj); + //fc::raw::unpack>(s, betting_market_obj); fc::raw::unpack(s, betting_market_obj.id); fc::raw::unpack(s, betting_market_obj.group_id); fc::raw::unpack(s, betting_market_obj.description); @@ -664,7 +692,7 @@ inline Stream& operator<<( Stream& s, const betting_market_group_object& betting { // pack all fields exposed in the header in the usual way // instead of calling the derived pack, just serialize the one field in the base class - // fc::raw::pack >(s, betting_market_group_obj); + // fc::raw::pack>(s, betting_market_group_obj); fc::raw::pack(s, betting_market_group_obj.id); fc::raw::pack(s, betting_market_group_obj.description); fc::raw::pack(s, betting_market_group_obj.event_id); @@ -673,6 +701,7 @@ inline Stream& operator<<( Stream& s, const betting_market_group_object& betting fc::raw::pack(s, betting_market_group_obj.total_matched_bets_amount); fc::raw::pack(s, betting_market_group_obj.never_in_play); fc::raw::pack(s, betting_market_group_obj.delay_before_settling); + fc::raw::pack(s, betting_market_group_obj.resolution_constraint); fc::raw::pack(s, betting_market_group_obj.settling_time); // fc::raw::pack the contents hidden in the impl class std::ostringstream stream; @@ -686,7 +715,7 @@ template inline Stream& operator>>( Stream& s, betting_market_group_object& betting_market_group_obj ) { // unpack all fields exposed in the header in the usual way - //fc::raw::unpack >(s, betting_market_group_obj); + //fc::raw::unpack>(s, betting_market_group_obj); fc::raw::unpack(s, betting_market_group_obj.id); fc::raw::unpack(s, betting_market_group_obj.description); fc::raw::unpack(s, betting_market_group_obj.event_id); @@ -695,6 +724,7 @@ inline Stream& operator>>( Stream& s, betting_market_group_object& betting_marke fc::raw::unpack(s, betting_market_group_obj.total_matched_bets_amount); fc::raw::unpack(s, betting_market_group_obj.never_in_play); fc::raw::unpack(s, betting_market_group_obj.delay_before_settling); + fc::raw::unpack(s, betting_market_group_obj.resolution_constraint); fc::raw::unpack(s, betting_market_group_obj.settling_time); // fc::raw::unpack the contents hidden in the impl class @@ -711,6 +741,11 @@ inline Stream& operator>>( Stream& s, betting_market_group_object& betting_marke FC_REFLECT_DERIVED( graphene::chain::betting_market_rules_object, (graphene::db::object), (name)(description) ) FC_REFLECT_DERIVED( graphene::chain::betting_market_group_object, (graphene::db::object), (description) ) FC_REFLECT_DERIVED( graphene::chain::betting_market_object, (graphene::db::object), (group_id) ) -FC_REFLECT_DERIVED( graphene::chain::bet_object, (graphene::db::object), (bettor_id)(betting_market_id)(amount_to_bet)(backer_multiplier)(back_or_lay)(end_of_delay) ) - -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) ) +FC_REFLECT_DERIVED( graphene::chain::bet_object, (graphene::db::object), (bettor_id)(betting_market_id)(amount_to_bet)(backer_multiplier)(back_or_lay) +#ifdef ENABLE_SIMPLE_CROSS_MARKET_MATCHING + (placed_as_back_or_lay) +#endif + (end_of_delay) ) + +FC_REFLECT(graphene::chain::betting_market_position, (pay_if_payout_condition)(pay_required_by_unmatched_bets)) +FC_REFLECT_DERIVED( graphene::chain::betting_market_group_position_object, (graphene::db::object), (bettor_id)(betting_market_group_id)(pay_if_canceled)(market_positions) ) diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index af50a94b0..4744eefd9 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -391,7 +391,6 @@ namespace graphene { namespace chain { /// @{ @group Betting Market Helpers void cancel_bet(const bet_object& bet, bool create_virtual_op = true); - void cancel_all_unmatched_bets_on_betting_market(const betting_market_object& betting_market); void cancel_all_unmatched_bets_on_betting_market_group(const betting_market_group_object& betting_market_group); void validate_betting_market_group_resolutions(const betting_market_group_object& betting_market_group, const std::map& resolutions); @@ -402,12 +401,24 @@ namespace graphene { namespace chain { /** * @brief Process a new bet * @param new_bet_object The new bet to process - * @return true if order was completely filled; false otherwise * - * This function takes a new bet and attempts to match it with existing - * bets already on the books. + * This function takes a new bet and adjusts the bettor's market positions, + * and pays for the bet out of the bettor's balance if needed. + * + * @return true if the bet was removed from the order books */ bool place_bet(const bet_object& new_bet_object); + /** + * @brief Matches a bet, if possible + * @param bet_object the bet to attempt to match + * + * This attempts to match it with existing bets already on the books. It is called + * immediately after the bet is placed, or if live betting is on, immediately after + * the delay expires and the bet becomes matchable. + * + * @return true if the bet was fully matched and removed from the order books + */ + bool try_to_match_bet(const bet_object& bet_object); ///@} /** diff --git a/libraries/chain/include/graphene/chain/protocol/betting_market.hpp b/libraries/chain/include/graphene/chain/protocol/betting_market.hpp index 26b6f2637..e216af61d 100644 --- a/libraries/chain/include/graphene/chain/protocol/betting_market.hpp +++ b/libraries/chain/include/graphene/chain/protocol/betting_market.hpp @@ -71,12 +71,12 @@ struct betting_market_rules_update_operation : public base_operation enum class betting_market_status { - unresolved, /// no grading has been published for this betting market - frozen, /// bets are suspended, no bets allowed - graded, /// grading of win or not_win has been published - canceled, /// the betting market is canceled, no further bets are allowed - settled, /// the betting market has been paid out - BETTING_MARKET_STATUS_COUNT + unresolved, /// no grading has been published for this betting market + frozen, /// bets are suspended, no bets allowed + graded, /// grading of win or not_win has been published + canceled, /// the betting market is canceled, no further bets are allowed + settled, /// the betting market has been paid out + BETTING_MARKET_STATUS_COUNT }; @@ -97,6 +97,18 @@ enum class betting_market_group_status BETTING_MARKET_GROUP_STATUS_COUNT }; +/** + * The constraint on they resolutions of the betting markets in a group. + * The blockchain will have different rules for calculating how much + * guaranteed profits bettors are allowed to use when placing additional + * bets on markets in this group. + */ +enum class betting_market_resolution_constraint +{ + exactly_one_winner, /// one market will win, or the market group will be canceled + at_most_one_winner, /// no markets will win, one market will win, or the market group will be canceled + BETTING_MARKET_RESOLUTION_CONSTRAINT_COUNT +}; struct betting_market_group_create_operation : public base_operation { @@ -140,6 +152,8 @@ struct betting_market_group_create_operation : public base_operation */ uint32_t delay_before_settling; + betting_market_resolution_constraint resolution_constraint; + extensions_type extensions; account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } @@ -233,7 +247,6 @@ struct betting_market_group_resolved_operation : public base_operation account_id_type bettor_id; betting_market_group_id_type betting_market_group_id; - std::map resolutions; share_type winnings; // always the asset type of the betting market group share_type fees_paid; // always the asset type of the betting market group @@ -243,12 +256,10 @@ struct betting_market_group_resolved_operation : public base_operation betting_market_group_resolved_operation() {} betting_market_group_resolved_operation(account_id_type bettor_id, betting_market_group_id_type betting_market_group_id, - const std::map& resolutions, share_type winnings, share_type fees_paid) : bettor_id(bettor_id), betting_market_group_id(betting_market_group_id), - resolutions(resolutions), winnings(winnings), fees_paid(fees_paid) { @@ -442,11 +453,15 @@ FC_REFLECT_ENUM( graphene::chain::betting_market_group_status, (frozen) (canceled) (BETTING_MARKET_GROUP_STATUS_COUNT) ) +FC_REFLECT_ENUM( graphene::chain::betting_market_resolution_constraint, + (exactly_one_winner) + (at_most_one_winner) + (BETTING_MARKET_RESOLUTION_CONSTRAINT_COUNT)) FC_REFLECT( graphene::chain::betting_market_group_create_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::betting_market_group_create_operation, (fee)(description)(event_id)(rules_id)(asset_id) - (never_in_play)(delay_before_settling) + (never_in_play)(delay_before_settling)(resolution_constraint) (extensions) ) FC_REFLECT( graphene::chain::betting_market_group_update_operation::fee_parameters_type, (fee) ) @@ -469,7 +484,7 @@ FC_REFLECT( graphene::chain::betting_market_group_resolve_operation, FC_REFLECT( graphene::chain::betting_market_group_resolved_operation::fee_parameters_type, ) FC_REFLECT( graphene::chain::betting_market_group_resolved_operation, - (bettor_id)(betting_market_group_id)(resolutions)(winnings)(fees_paid)(fee) ) + (bettor_id)(betting_market_group_id)(winnings)(fees_paid)(fee) ) FC_REFLECT( graphene::chain::betting_market_group_cancel_unmatched_bets_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::betting_market_group_cancel_unmatched_bets_operation, diff --git a/libraries/chain/include/graphene/chain/protocol/types.hpp b/libraries/chain/include/graphene/chain/protocol/types.hpp index 4b6e1589e..9e1fc486a 100644 --- a/libraries/chain/include/graphene/chain/protocol/types.hpp +++ b/libraries/chain/include/graphene/chain/protocol/types.hpp @@ -170,7 +170,7 @@ namespace graphene { namespace chain { impl_asset_dividend_data_type, impl_pending_dividend_payout_balance_for_holder_object_type, impl_distributed_dividend_balance_data_type, - impl_betting_market_position_object_type, + impl_betting_market_group_position_object_type, impl_global_betting_statistics_object_type }; @@ -247,7 +247,7 @@ namespace graphene { namespace chain { class fba_accumulator_object; class asset_dividend_data_object; class pending_dividend_payout_balance_for_holder_object; - class betting_market_position_object; + class betting_market_group_position_object; class global_betting_statistics_object; typedef object_id< implementation_ids, impl_global_property_object_type, global_property_object> global_property_id_type; @@ -271,7 +271,7 @@ namespace graphene { namespace chain { typedef object_id< implementation_ids, impl_special_authority_object_type, special_authority_object > special_authority_id_type; typedef object_id< implementation_ids, impl_buyback_object_type, buyback_object > buyback_id_type; typedef object_id< implementation_ids, impl_fba_accumulator_object_type, fba_accumulator_object > fba_accumulator_id_type; - typedef object_id< implementation_ids, impl_betting_market_position_object_type, betting_market_position_object > betting_market_position_id_type; + typedef object_id< implementation_ids, impl_betting_market_group_position_object_type, betting_market_group_position_object > betting_market_position_id_type; typedef object_id< implementation_ids, impl_global_betting_statistics_object_type, global_betting_statistics_object > global_betting_statistics_id_type; typedef fc::array symbol_type; @@ -425,7 +425,7 @@ FC_REFLECT_ENUM( graphene::chain::impl_object_type, (impl_asset_dividend_data_type) (impl_pending_dividend_payout_balance_for_holder_object_type) (impl_distributed_dividend_balance_data_type) - (impl_betting_market_position_object_type) + (impl_betting_market_group_position_object_type) (impl_global_betting_statistics_object_type) ) diff --git a/libraries/plugins/bookie/bookie_api.cpp b/libraries/plugins/bookie/bookie_api.cpp index 838c038c8..7936b31f3 100644 --- a/libraries/plugins/bookie/bookie_api.cpp +++ b/libraries/plugins/bookie/bookie_api.cpp @@ -40,6 +40,11 @@ #include #include +#ifdef DEFAULT_LOGGER +# undef DEFAULT_LOGGER +#endif +#define DEFAULT_LOGGER "bookie_plugin" + namespace graphene { namespace bookie { namespace detail { diff --git a/libraries/plugins/bookie/bookie_plugin.cpp b/libraries/plugins/bookie/bookie_plugin.cpp index f15ac2f7c..ba9a907c5 100644 --- a/libraries/plugins/bookie/bookie_plugin.cpp +++ b/libraries/plugins/bookie/bookie_plugin.cpp @@ -41,12 +41,10 @@ #include -#if 0 -# ifdef DEFAULT_LOGGER -# undef DEFAULT_LOGGER -# endif -# define DEFAULT_LOGGER "bookie_plugin" +#ifdef DEFAULT_LOGGER +# undef DEFAULT_LOGGER #endif +#define DEFAULT_LOGGER "bookie_plugin" namespace graphene { namespace bookie { @@ -296,7 +294,7 @@ void bookie_plugin_impl::on_block_applied( const signed_block& ) if( op.op.which() == operation::tag::value ) { const bet_matched_operation& bet_matched_op = op.op.get(); - //idump((bet_matched_op)); + idump((bet_matched_op)); const asset& amount_bet = bet_matched_op.amount_bet; // object may no longer exist //const bet_object& bet = bet_matched_op.bet_id(db); diff --git a/libraries/plugins/witness/witness.cpp b/libraries/plugins/witness/witness.cpp index dce1234a7..401a27bd1 100644 --- a/libraries/plugins/witness/witness.cpp +++ b/libraries/plugins/witness/witness.cpp @@ -204,7 +204,7 @@ block_production_condition::block_production_condition_enum witness_plugin::bloc break; case block_production_condition::no_private_key: ilog("Not producing block because I don't have the private key for ${scheduled_key}", - ("n", capture["n"])("t", capture["t"])("c", capture["c"])); + ("scheduled_key", capture["scheduled_key"])); break; case block_production_condition::low_participation: elog("Not producing block because node appears to be on a minority fork with only ${pct}% witness participation", diff --git a/libraries/wallet/include/graphene/wallet/wallet.hpp b/libraries/wallet/include/graphene/wallet/wallet.hpp index 0ba0782b5..18a023a1b 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -1676,6 +1676,7 @@ class wallet_api event_id_type event_id, betting_market_rules_id_type rules_id, asset_id_type asset_id, + betting_market_resolution_constraint constraint, bool broadcast = false); signed_transaction propose_update_betting_market_group( @@ -1722,8 +1723,8 @@ class wallet_api bool broadcast = false); signed_transaction cancel_bet(string betting_account, - bet_id_type bet_id, - bool broadcast = false); + bet_id_type bet_id, + bool broadcast = false); signed_transaction propose_resolve_betting_market_group( const string& proposing_account, diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index 74c285bfa..acd5958ba 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -5389,6 +5389,7 @@ signed_transaction wallet_api::propose_create_betting_market_group( event_id_type event_id, betting_market_rules_id_type rules_id, asset_id_type asset_id, + betting_market_resolution_constraint constraint, bool broadcast /*= false*/) { FC_ASSERT( !is_locked() ); @@ -5399,6 +5400,7 @@ signed_transaction wallet_api::propose_create_betting_market_group( betting_market_group_create_op.event_id = event_id; betting_market_group_create_op.rules_id = rules_id; betting_market_group_create_op.asset_id = asset_id; + betting_market_group_create_op.resolution_constraint = constraint; proposal_create_operation prop_op; prop_op.expiration_time = expiration_time; diff --git a/multimarket_betting_simulator.html b/multimarket_betting_simulator.html new file mode 100644 index 000000000..b0aa4cb16 --- /dev/null +++ b/multimarket_betting_simulator.html @@ -0,0 +1,1226 @@ + + + + Peerplays Multi-Market Bookie Sandbox + + + + + + + + + + + + + + + + + + + + + + +
+
+ + diff --git a/tests/betting/betting_tests.cpp b/tests/betting/betting_tests.cpp index eef57bac1..77d2a095a 100644 --- a/tests/betting/betting_tests.cpp +++ b/tests/betting/betting_tests.cpp @@ -50,9 +50,13 @@ struct enable_betting_logging_config { { fc::logger::get("betting").add_appender(fc::appender::get("stdout")); fc::logger::get("betting").set_log_level(fc::log_level::debug); + + fc::logger::get("bookie_plugin").add_appender(fc::appender::get("stdout")); + fc::logger::get("bookie_plugin").set_log_level(fc::log_level::debug); } ~enable_betting_logging_config() { fc::logger::get("betting").remove_appender(fc::appender::get("stdout")); + fc::logger::get("bookie_plugin").remove_appender(fc::appender::get("stdout")); } }; BOOST_GLOBAL_FIXTURE( enable_betting_logging_config ); @@ -130,113 +134,41 @@ using namespace graphene::chain::keywords; // 1.58 50:29 | 2.34 50:67 | 4.6 5:18 | 25 1:24 | 430 1:429 | // 1.59 100:59 | 2.36 25:34 | 4.7 10:37 -#define CREATE_ICE_HOCKEY_BETTING_MARKET(never_in_play, delay_before_settling) \ - create_sport({{"en", "Ice Hockey"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}); \ - generate_blocks(1); \ - const sport_object& ice_hockey = *db.get_index_type().indices().get().rbegin(); \ - create_event_group({{"en", "NHL"}, {"zh_Hans", "國家冰球聯盟"}, {"ja", "ナショナルホッケーリーグ"}}, ice_hockey.id); \ - generate_blocks(1); \ - const event_group_object& nhl = *db.get_index_type().indices().get().rbegin(); \ - create_event({{"en", "Washington Capitals/Chicago Blackhawks"}, {"zh_Hans", "華盛頓首都隊/芝加哥黑鷹"}, {"ja", "ワシントン・キャピタルズ/シカゴ・ブラックホークス"}}, {{"en", "2016-17"}}, nhl.id); \ - generate_blocks(1); \ - const event_object& capitals_vs_blackhawks = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market_rules({{"en", "NHL Rules v1.0"}}, {{"en", "The winner will be the team with the most points at the end of the game. The team with fewer points will not be the winner."}}); \ - generate_blocks(1); \ - const betting_market_rules_object& betting_market_rules = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market_group({{"en", "Moneyline"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ - generate_blocks(1); \ - const betting_market_group_object& moneyline_betting_markets = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market(moneyline_betting_markets.id, {{"en", "Washington Capitals win"}}); \ - generate_blocks(1); \ - const betting_market_object& capitals_win_market = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market(moneyline_betting_markets.id, {{"en", "Chicago Blackhawks win"}}); \ - generate_blocks(1); \ - const betting_market_object& blackhawks_win_market = *db.get_index_type().indices().get().rbegin(); \ - (void)capitals_win_market; (void)blackhawks_win_market; - -// create the basic betting market, plus groups for the first, second, and third period results -#define CREATE_EXTENDED_ICE_HOCKEY_BETTING_MARKET(never_in_play, delay_before_settling) \ - CREATE_ICE_HOCKEY_BETTING_MARKET(never_in_play, delay_before_settling) \ - create_betting_market_group({{"en", "First Period Result"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ - generate_blocks(1); \ - const betting_market_group_object& first_period_result_betting_markets = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market(first_period_result_betting_markets.id, {{"en", "Washington Capitals win"}}); \ - generate_blocks(1); \ - const betting_market_object& first_period_capitals_win_market = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market(first_period_result_betting_markets.id, {{"en", "Chicago Blackhawks win"}}); \ - generate_blocks(1); \ - const betting_market_object& first_period_blackhawks_win_market = *db.get_index_type().indices().get().rbegin(); \ - (void)first_period_capitals_win_market; (void)first_period_blackhawks_win_market; \ - \ - create_betting_market_group({{"en", "Second Period Result"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ - generate_blocks(1); \ - const betting_market_group_object& second_period_result_betting_markets = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market(second_period_result_betting_markets.id, {{"en", "Washington Capitals win"}}); \ - generate_blocks(1); \ - const betting_market_object& second_period_capitals_win_market = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market(second_period_result_betting_markets.id, {{"en", "Chicago Blackhawks win"}}); \ - generate_blocks(1); \ - const betting_market_object& second_period_blackhawks_win_market = *db.get_index_type().indices().get().rbegin(); \ - (void)second_period_capitals_win_market; (void)second_period_blackhawks_win_market; \ - \ - create_betting_market_group({{"en", "Third Period Result"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ - generate_blocks(1); \ - const betting_market_group_object& third_period_result_betting_markets = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market(third_period_result_betting_markets.id, {{"en", "Washington Capitals win"}}); \ - generate_blocks(1); \ - const betting_market_object& third_period_capitals_win_market = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market(third_period_result_betting_markets.id, {{"en", "Chicago Blackhawks win"}}); \ - generate_blocks(1); \ - const betting_market_object& third_period_blackhawks_win_market = *db.get_index_type().indices().get().rbegin(); \ - (void)third_period_capitals_win_market; (void)third_period_blackhawks_win_market; - -#define CREATE_TENNIS_BETTING_MARKET() \ - create_betting_market_rules({{"en", "Tennis Rules v1.0"}}, {{"en", "The winner is the player who wins the last ball in the match."}}); \ - generate_blocks(1); \ - const betting_market_rules_object& tennis_rules = *db.get_index_type().indices().get().rbegin(); \ - create_sport({{"en", "Tennis"}}); \ - generate_blocks(1); \ - const sport_object& tennis = *db.get_index_type().indices().get().rbegin(); \ - create_event_group({{"en", "Wimbledon"}}, tennis.id); \ - generate_blocks(1); \ - const event_group_object& wimbledon = *db.get_index_type().indices().get().rbegin(); \ - create_event({{"en", "R. Federer/T. Berdych"}}, {{"en", "2017"}}, wimbledon.id); \ - generate_blocks(1); \ - const event_object& berdych_vs_federer = *db.get_index_type().indices().get().rbegin(); \ - create_event({{"en", "M. Cilic/S. Querrye"}}, {{"en", "2017"}}, wimbledon.id); \ - generate_blocks(1); \ - const event_object& cilic_vs_querrey = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market_group({{"en", "Moneyline 1st sf"}}, berdych_vs_federer.id, tennis_rules.id, asset_id_type(), false, 0); \ - generate_blocks(1); \ - const betting_market_group_object& moneyline_berdych_vs_federer = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market_group({{"en", "Moneyline 2nd sf"}}, cilic_vs_querrey.id, tennis_rules.id, asset_id_type(), false, 0); \ - generate_blocks(1); \ - const betting_market_group_object& moneyline_cilic_vs_querrey = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market(moneyline_berdych_vs_federer.id, {{"en", "T. Berdych defeats R. Federer"}}); \ - generate_blocks(1); \ - const betting_market_object& berdych_wins_market = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market(moneyline_berdych_vs_federer.id, {{"en", "R. Federer defeats T. Berdych"}}); \ - generate_blocks(1); \ - const betting_market_object& federer_wins_market = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market(moneyline_cilic_vs_querrey.id, {{"en", "M. Cilic defeats S. Querrey"}}); \ - generate_blocks(1); \ - const betting_market_object& cilic_wins_market = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market(moneyline_cilic_vs_querrey.id, {{"en", "S. Querrey defeats M. Cilic"}});\ - generate_blocks(1); \ - const betting_market_object& querrey_wins_market = *db.get_index_type().indices().get().rbegin(); \ - create_event({{"en", "R. Federer/M. Cilic"}}, {{"en", "2017"}}, wimbledon.id); \ - generate_blocks(1); \ - const event_object& cilic_vs_federer = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market_group({{"en", "Moneyline final"}}, cilic_vs_federer.id, tennis_rules.id, asset_id_type(), false, 0); \ - generate_blocks(1); \ - const betting_market_group_object& moneyline_cilic_vs_federer = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market(moneyline_cilic_vs_federer.id, {{"en", "R. Federer defeats M. Cilic"}}); \ - generate_blocks(1); \ - const betting_market_object& federer_wins_final_market = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market(moneyline_cilic_vs_federer.id, {{"en", "M. Cilic defeats R. Federer"}}); \ - generate_blocks(1); \ - const betting_market_object& cilic_wins_final_market = *db.get_index_type().indices().get().rbegin(); \ - (void)federer_wins_market;(void)cilic_wins_market;(void)federer_wins_final_market; (void)cilic_wins_final_market; (void)berdych_wins_market; (void)querrey_wins_market; +std::string format_internationalized_string(const internationalized_string_type& internationalized) +{ + if (internationalized.empty()) + return "empty"; + try + { + return internationalized.at("en"); + } + catch (std::out_of_range&) + { + return internationalized.begin()->second; + } +} + +void dump_market_position(const database& db, const betting_market_group_position_object& position) +{ + const account_object& bettor = position.bettor_id(db); + const betting_market_group_object& betting_market_group = position.betting_market_group_id(db); + idump((betting_market_group)); + BOOST_TEST_MESSAGE("Position for " << bettor.name << " in market group " << format_internationalized_string(betting_market_group.description)); + BOOST_TEST_MESSAGE("\tcancel: " << position.pay_if_canceled.value); + + + auto& betting_market_index = db.get_index_type().indices().get(); + auto betting_markets_in_group = boost::make_iterator_range(betting_market_index.equal_range(betting_market_group.id)); + uint32_t i = 0; + for (const betting_market_object& betting_market : betting_markets_in_group) + { + BOOST_TEST_MESSAGE("\t" << format_internationalized_string(betting_market.payout_condition) << ":\twin: " << position.market_positions[i].pay_if_payout_condition.value << "\trequired for unmatched: " << position.market_positions[i].pay_required_by_unmatched_bets.value); + ++i; + } + if (i == 1 || betting_market_group.resolution_constraint == betting_market_resolution_constraint::at_most_one_winner) + BOOST_TEST_MESSAGE("\tnone of the above:\twin: " << position.market_positions[i].pay_if_payout_condition.value << "\trequired for unmatched: " << position.market_positions[i].pay_required_by_unmatched_bets.value); +} + BOOST_FIXTURE_TEST_SUITE( betting_tests, database_fixture ) @@ -300,7 +232,7 @@ BOOST_AUTO_TEST_CASE(simple_bet_win) try { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); // give alice and bob 10k each transfer(account_id_type(), alice_id, asset(10000)); @@ -310,9 +242,21 @@ BOOST_AUTO_TEST_CASE(simple_bet_win) place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 11 * GRAPHENE_BETTING_ODDS_PRECISION); place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000, asset_id_type()), 11 * GRAPHENE_BETTING_ODDS_PRECISION); + for (const betting_market_group_position_object& o : db.get_index_type().indices()) + { + dump_market_position(db, o); + BOOST_TEST_MESSAGE("\tbalance: " << get_balance(o.bettor_id, asset_id_type())); + } + // reverse positions at 1:1 place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(1100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(1100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + for (const betting_market_group_position_object& o : db.get_index_type().indices()) + { + dump_market_position(db, o); + BOOST_TEST_MESSAGE("\tbalance: " << get_balance(o.bettor_id, asset_id_type())); + } } FC_LOG_AND_RETHROW() } @@ -321,7 +265,7 @@ BOOST_AUTO_TEST_CASE(binned_order_books) try { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, true); graphene::bookie::bookie_api bookie_api(app); @@ -336,14 +280,20 @@ BOOST_AUTO_TEST_CASE(binned_order_books) place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 166 * GRAPHENE_BETTING_ODDS_PRECISION / 100); place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 167 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + for (const betting_market_group_position_object& o : db.get_index_type().indices()) + { + dump_market_position(db, o); + BOOST_TEST_MESSAGE("\tbalance: " << get_balance(o.bettor_id, asset_id_type())); + } + 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; + 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); @@ -357,18 +307,20 @@ BOOST_AUTO_TEST_CASE(binned_order_books) BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_lay_bets.size(), 0u); 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, true /* round up */); - 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); + // 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, true /* round up */); + 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); } 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; + idump((*bet_iter)); + ++bet_iter; } BOOST_CHECK(bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)) == bet_odds_idx.end()); @@ -387,37 +339,38 @@ BOOST_AUTO_TEST_CASE(binned_order_books) // 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 // - // for the bets bob placed above, we shoudl get 356 @ 1.6, 99 @ 1.5 + // for the bets bob placed above, we should get 356 @ 1.6, 99 @ 1.5 BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_back_bets.size(), 0u); BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_lay_bets.size(), 2u); 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, true /* round up */); - 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); - - ilog("After alice's bet, order book is:"); - 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; - } + // 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, true /* round up */); + 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); + + ilog("After alice's bet, order book is:"); + 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; + } } - - 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) + for (const betting_market_group_position_object& o : db.get_index_type().indices()) { - idump((*bet_iter)); - ++bet_iter; + dump_market_position(db, o); + BOOST_TEST_MESSAGE("\tbalance: " << get_balance(o.bettor_id, asset_id_type())); } + // EMF: note: this check currently fails, but given the same series of bets actually placed above, + // the simulator generates the same result (small amounts remianing on the order books). This + // suggests the algorithm in the bookie plugin isn't generating the right fake order books here. BOOST_CHECK(bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)) == bet_odds_idx.end()); - } FC_LOG_AND_RETHROW() } @@ -426,7 +379,7 @@ BOOST_AUTO_TEST_CASE( peerplays_sport_create_test ) try { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); // give alice and bob 10M each transfer(account_id_type(), alice_id, asset(10000000)); @@ -463,7 +416,7 @@ BOOST_AUTO_TEST_CASE( cancel_unmatched_in_betting_group_test ) try { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, true); // give alice and bob 10M each transfer(account_id_type(), alice_id, asset(10000000)); @@ -475,7 +428,7 @@ BOOST_AUTO_TEST_CASE( cancel_unmatched_in_betting_group_test ) place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); // place unmatched place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(500, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - place_bet(bob_id, blackhawks_win_market.id, bet_type::lay, asset(600, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(600, asset_id_type()), 1.1 * GRAPHENE_BETTING_ODDS_PRECISION); BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 - 500); BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000 - 600); @@ -495,7 +448,7 @@ BOOST_AUTO_TEST_CASE(match_using_takers_expected_amounts) { generate_blocks(1); ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); transfer(account_id_type(), alice_id, asset(10000000)); share_type alice_expected_balance = 10000000; @@ -542,13 +495,112 @@ ilog("message"); FC_LOG_AND_RETHROW() } + +#ifdef ENABLE_SIMPLE_CROSS_MARKET_MATCHING +// same as the above test, but place the bets on the other "virtual" market +BOOST_AUTO_TEST_CASE(match_using_takers_expected_amounts_inverted) +{ + try + { + generate_blocks(1); + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); + + transfer(account_id_type(), alice_id, asset(10000000)); + share_type alice_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + // lay 46 at 1.94 odds (50:47) -- this is too small to be placed on the books and there's + // nothing for it to match, so it should be canceled + BOOST_TEST_MESSAGE("lay 46 at 1.94 odds (50:47) -- this is too small to be placed on the books and there's nothing for it to match, so it should be canceled"); + place_bet(alice_id, blackhawks_win_market.id, bet_type::lay, asset(46, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + BOOST_TEST_MESSAGE("alice's balance should be " << alice_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); +ilog("message"); + // lay 50 at 1.94 odds (50:47) -- this becomes a back bet at an exact amount, nothing surprising should happen here + BOOST_TEST_MESSAGE("lay 50 at 1.94 odds (50:47) -- this becomes a back bet at an exact amount, nothing surprising should happen here"); + place_bet(alice_id, blackhawks_win_market.id, bet_type::lay, asset(50, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + alice_expected_balance -= 50; + BOOST_TEST_MESSAGE("alice's balance should be " << alice_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + // lay 182 at 1.91 odds (100:91) -- this becomes a back bet as an inexact match, we should get refunded 82 and leave a bet for 100 on the books + place_bet(alice_id, blackhawks_win_market.id, bet_type::lay, asset(182, asset_id_type()), 191 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + alice_expected_balance -= 100; + BOOST_TEST_MESSAGE("alice's balance should be " << alice_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + const auto& bet_odds_idx = db.get_index_type().indices().get(); + ilog("Order books after alice's bets, capitals_win_market:"); + 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; + } + + ilog("Order books after alice's bets, blackhawks_win_market:"); + bet_iter = bet_odds_idx.lower_bound(std::make_tuple(blackhawks_win_market.id)); + while (bet_iter != bet_odds_idx.end() && + bet_iter->betting_market_id == blackhawks_win_market.id) + { + idump((*bet_iter)); + ++bet_iter; + } + + transfer(account_id_type(), bob_id, asset(10000000)); + share_type bob_expected_balance = 10000000; + BOOST_TEST_MESSAGE("bob's balance should be " << bob_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + + // now have bob match it with a back of 300 at 1.5 (2:1) + // This should: + // match the full 50 @ 1.94 with 47 + // match the full 100 @ 1.91 with 91 + // bob's balance goes down by 300 (138 is matched, 162 is still on the books) + // leaves a back bet of 150 @ 1.5 on the books + BOOST_TEST_MESSAGE("now have bob match it with a back of 300 at 1.5"); + place_bet(bob_id, blackhawks_win_market.id, bet_type::back, asset(300, asset_id_type()), 15 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + bob_expected_balance -= 300; + BOOST_TEST_MESSAGE("bob's balance should be " << bob_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + + for (const betting_market_group_position_object& o : db.get_index_type().indices()) + { + dump_market_position(db, o); + BOOST_TEST_MESSAGE("\tbalance: " << get_balance(o.bettor_id, asset_id_type())); + } + ilog("Order books after bob's bets, capitals_win_market:"); + 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; + } + + ilog("Order books after bob's bets, blackhawks_win_market:"); + bet_iter = bet_odds_idx.lower_bound(std::make_tuple(blackhawks_win_market.id)); + while (bet_iter != bet_odds_idx.end() && + bet_iter->betting_market_id == blackhawks_win_market.id) + { + idump((*bet_iter)); + ++bet_iter; + } + + } + FC_LOG_AND_RETHROW() +} +#endif // defined(ENABLE_SIMPLE_CROSS_MARKET_MATCHING) + BOOST_AUTO_TEST_CASE(match_using_takers_expected_amounts2) { try { generate_blocks(1); ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); transfer(account_id_type(), alice_id, asset(10000000)); share_type alice_expected_balance = 10000000; @@ -586,7 +638,7 @@ BOOST_AUTO_TEST_CASE(match_using_takers_expected_amounts3) { generate_blocks(1); ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); transfer(account_id_type(), alice_id, asset(10000000)); share_type alice_expected_balance = 10000000; @@ -619,7 +671,7 @@ BOOST_AUTO_TEST_CASE(match_using_takers_expected_amounts4) { generate_blocks(1); ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); transfer(account_id_type(), alice_id, asset(10000000)); share_type alice_expected_balance = 10000000; @@ -674,7 +726,7 @@ BOOST_AUTO_TEST_CASE(match_using_takers_expected_amounts5) { generate_blocks(1); ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); transfer(account_id_type(), alice_id, asset(10000000)); share_type alice_expected_balance = 10000000; @@ -718,7 +770,7 @@ BOOST_AUTO_TEST_CASE(match_using_takers_expected_amounts6) { generate_blocks(1); ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); graphene::bookie::bookie_api bookie_api(app); @@ -836,7 +888,7 @@ BOOST_AUTO_TEST_CASE(inexact_odds) { generate_blocks(1); ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); transfer(account_id_type(), alice_id, asset(10000000)); share_type alice_expected_balance = 10000000; @@ -877,41 +929,13 @@ BOOST_AUTO_TEST_CASE(inexact_odds) } BOOST_AUTO_TEST_CASE(bet_reversal_test) -{ - // test whether we can bet our entire balance in one direction, then reverse our bet (while having zero balance) - try - { - generate_blocks(1); - ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); - - transfer(account_id_type(), alice_id, asset(10000000)); - share_type alice_expected_balance = 10000000; - BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); - - // back with our entire balance - place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(10000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), 0); - - // reverse the bet - place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(20000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), 0); - - // try to re-reverse it, but go too far - BOOST_CHECK_THROW( place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(30000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION), fc::exception); - BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), 0); - } - FC_LOG_AND_RETHROW() -} - -BOOST_AUTO_TEST_CASE(bet_against_exposure_test) { // test whether we can bet our entire balance in one direction, have it match, then reverse our bet (while having zero balance) try { generate_blocks(1); ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); transfer(account_id_type(), alice_id, asset(10000000)); transfer(account_id_type(), bob_id, asset(10000000)); @@ -925,15 +949,18 @@ BOOST_AUTO_TEST_CASE(bet_against_exposure_test) alice_expected_balance -= 10000000; BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance); - // lay with bob's entire balance, which fully matches bob's bet + // lay with bob's entire balance, which fully matches alice's bet place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(10000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); bob_expected_balance -= 10000000; BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance); // reverse the bet - place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(20000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(20000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance); + // cancel the bet we just placed + cancel_unmatched_bets(moneyline_betting_markets.id); + // try to re-reverse it, but go too far BOOST_CHECK_THROW( place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(30000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION), fc::exception); BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance); @@ -946,7 +973,7 @@ BOOST_AUTO_TEST_CASE(persistent_objects_test) try { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); graphene::bookie::bookie_api bookie_api(app); @@ -1043,7 +1070,7 @@ BOOST_AUTO_TEST_CASE(test_settled_market_states) try { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); graphene::bookie::bookie_api bookie_api(app); @@ -1099,7 +1126,7 @@ BOOST_AUTO_TEST_CASE(delayed_bets_test) // test live betting ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); generate_blocks(1); @@ -1287,8 +1314,8 @@ BOOST_AUTO_TEST_CASE( testnet_witness_block_production_error ) { try { - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); - create_betting_market_group({{"en", "Unused"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); + create_betting_market_group({{"en", "Unused"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), false, 0, true); generate_blocks(1); const betting_market_group_object& unused_betting_markets = *db.get_index_type().indices().get().rbegin(); @@ -1320,13 +1347,13 @@ BOOST_AUTO_TEST_CASE( cancel_one_event_in_group ) // usual sequence and cancel it, verify that it doesn't alter the other event in the group try { - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); // create a second event in the same betting market group create_event({{"en", "Boston Bruins/Pittsburgh Penguins"}}, {{"en", "2016-17"}}, nhl.id); generate_blocks(1); const event_object& bruins_vs_penguins = *db.get_index_type().indices().get().rbegin(); - create_betting_market_group({{"en", "Moneyline"}}, bruins_vs_penguins.id, betting_market_rules.id, asset_id_type(), false, 0); + create_betting_market_group({{"en", "Moneyline"}}, bruins_vs_penguins.id, betting_market_rules.id, asset_id_type(), false, 0, true); generate_blocks(1); const betting_market_group_object& bruins_penguins_moneyline_betting_markets = *db.get_index_type().indices().get().rbegin(); create_betting_market(bruins_penguins_moneyline_betting_markets.id, {{"en", "Boston Bruins win"}}); @@ -1454,7 +1481,7 @@ struct simple_bet_test_fixture_2 : database_fixture { simple_bet_test_fixture_2() { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); // give alice and bob 10k each transfer(account_id_type(), alice_id, asset(10000)); @@ -1488,7 +1515,7 @@ BOOST_AUTO_TEST_CASE(sport_update_test) try { ACTORS( (alice) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); update_sport(ice_hockey.id, {{"en", "Hockey on Ice"}, {"zh_Hans", "冰"}, {"ja", "アイスホッケ"}}); transfer(account_id_type(), alice_id, asset(10000000)); @@ -1503,7 +1530,7 @@ BOOST_AUTO_TEST_CASE(sport_delete_test) { try { - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); const auto& event_group_1 = create_event_group({{"en", "group1"}}, ice_hockey.id); const auto& event_group_2 = create_event_group({{"en", "group2"}}, ice_hockey.id); @@ -1521,27 +1548,28 @@ BOOST_AUTO_TEST_CASE(sport_delete_test) BOOST_AUTO_TEST_CASE(sport_delete_test_not_proposal) { - try - { - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); - - sport_delete_operation sport_delete_op; - sport_delete_op.sport_id = ice_hockey.id; - - BOOST_CHECK_THROW(force_operation_by_witnesses(sport_delete_op), fc::exception); - } FC_LOG_AND_RETHROW() + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); + + sport_delete_operation sport_delete_op; + sport_delete_op.sport_id = ice_hockey.id; + + BOOST_CHECK_THROW(force_operation_by_witnesses(sport_delete_op), fc::exception); + } FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_CASE(sport_delete_test_not_existed_sport) { - try - { - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); - - delete_sport(ice_hockey.id); - - BOOST_CHECK_THROW(delete_sport(ice_hockey.id), fc::exception); - } FC_LOG_AND_RETHROW() + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); + + sport_id_type ice_hockey_id = ice_hockey.id; + delete_sport(ice_hockey_id); + + BOOST_CHECK_THROW(delete_sport(ice_hockey_id), fc::exception); + } FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_CASE(event_group_update_test) @@ -1549,7 +1577,7 @@ BOOST_AUTO_TEST_CASE(event_group_update_test) try { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); transfer(account_id_type(), alice_id, asset(10000000)); transfer(account_id_type(), bob_id, asset(10000000)); @@ -1655,46 +1683,46 @@ struct test_markets_groups test_markets_groups(database_fixture& db_fixture, event_id_type event_id, betting_market_rules_id_type betting_market_rules_id) { - market_group_upcoming = &db_fixture.create_betting_market_group({{"en", "market group upcoming"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0); - market_group_frozen_upcoming = &db_fixture.create_betting_market_group({{"en", "market group frozen_upcoming"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0); + market_group_upcoming = &db_fixture.create_betting_market_group({{"en", "market group upcoming"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0, true); + market_group_frozen_upcoming = &db_fixture.create_betting_market_group({{"en", "market group frozen_upcoming"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0, true); db_fixture.db.modify(*market_group_frozen_upcoming, [&](betting_market_group_object& market_group) { market_group.on_frozen_event(db_fixture.db); }); - market_group_in_play = &db_fixture.create_betting_market_group({{"en", "market group in_play"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0); + market_group_in_play = &db_fixture.create_betting_market_group({{"en", "market group in_play"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0, true); db_fixture.db.modify(*market_group_in_play, [&](betting_market_group_object& market_group) { market_group.on_in_play_event(db_fixture.db); }); - market_group_frozen_in_play = &db_fixture.create_betting_market_group({{"en", "market group frozen_in_play"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0); + market_group_frozen_in_play = &db_fixture.create_betting_market_group({{"en", "market group frozen_in_play"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0, true); db_fixture.db.modify(*market_group_frozen_in_play, [&](betting_market_group_object& market_group) { market_group.on_in_play_event(db_fixture.db); market_group.on_frozen_event(db_fixture.db); }); - market_group_closed = &db_fixture.create_betting_market_group({{"en", "market group closed"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0); + market_group_closed = &db_fixture.create_betting_market_group({{"en", "market group closed"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0, true); db_fixture.db.modify(*market_group_closed, [&](betting_market_group_object& market_group) { market_group.on_closed_event(db_fixture.db, true); }); - market_group_graded = &db_fixture.create_betting_market_group({{"en", "market group graded"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0); + market_group_graded = &db_fixture.create_betting_market_group({{"en", "market group graded"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0, true); db_fixture.db.modify(*market_group_graded, [&](betting_market_group_object& market_group) { market_group.on_closed_event(db_fixture.db, true); market_group.on_graded_event(db_fixture.db); }); - market_group_canceled = &db_fixture.create_betting_market_group({{"en", "market group canceled"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0); + market_group_canceled = &db_fixture.create_betting_market_group({{"en", "market group canceled"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0, true); db_fixture.db.modify(*market_group_canceled, [&](betting_market_group_object& market_group) { market_group.on_canceled_event(db_fixture.db, true); }); - market_group_settled = &db_fixture.create_betting_market_group({{"en", "market group settled"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0); + market_group_settled = &db_fixture.create_betting_market_group({{"en", "market group settled"}}, event_id, betting_market_rules_id, asset_id_type(), false, 0, true); db_fixture.db.modify(*market_group_settled, [&](betting_market_group_object& market_group) { market_group.on_closed_event(db_fixture.db, true); @@ -1756,7 +1784,7 @@ BOOST_AUTO_TEST_CASE(event_group_delete_test) try { ACTORS( (alice)(bob) ) - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); const int initialAccountAsset = 10000000; const int betAsset = 1000000; @@ -1766,7 +1794,7 @@ BOOST_AUTO_TEST_CASE(event_group_delete_test) const auto& event = create_event({{"en", "event"}}, {{"en", "2016-17"}}, nhl.id); - const auto& market_group = create_betting_market_group({{"en", "market group"}}, event.id, betting_market_rules.id, asset_id_type(), false, 0); + const auto& market_group = create_betting_market_group({{"en", "market group"}}, event.id, betting_market_rules.id, asset_id_type(), false, 0, true); //to make bets be not removed immediately update_betting_market_group_impl(market_group.id, fc::optional(), @@ -1830,44 +1858,44 @@ BOOST_AUTO_TEST_CASE(event_group_delete_test) BOOST_AUTO_TEST_CASE(event_group_delete_test_with_matched_bets) { - try - { - ACTORS( (alice)(bob) ) - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); - - const int initialAccountAsset = 10000000; - const int betAsset = 100000; - - transfer(account_id_type(), alice_id, asset(initialAccountAsset)); - transfer(account_id_type(), bob_id, asset(initialAccountAsset)); - generate_blocks(1); - - const auto& event = create_event({{"en", "event"}}, {{"en", "2016-17"}}, nhl.id); - generate_blocks(1); - - const auto& market_group = create_betting_market_group({{"en", "market group"}}, event.id, betting_market_rules.id, asset_id_type(), false, 0); - generate_blocks(1); - - const auto& market = create_betting_market(market_group.id, {{"en", "market"}}); - generate_blocks(1); - - place_bet(alice_id, market.id, bet_type::back, asset(betAsset, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - place_bet(bob_id, market.id, bet_type::lay, asset(betAsset, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - generate_blocks(1); - - delete_event_group(nhl.id); - generate_blocks(1); - - BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), initialAccountAsset); - BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), initialAccountAsset); - } FC_LOG_AND_RETHROW() + try + { + ACTORS( (alice)(bob) ) + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); + + const int initialAccountAsset = 10000000; + const int betAsset = 100000; + + transfer(account_id_type(), alice_id, asset(initialAccountAsset)); + transfer(account_id_type(), bob_id, asset(initialAccountAsset)); + generate_blocks(1); + + const auto& event = create_event({{"en", "event"}}, {{"en", "2016-17"}}, nhl.id); + generate_blocks(1); + + const auto& market_group = create_betting_market_group({{"en", "market group"}}, event.id, betting_market_rules.id, asset_id_type(), false, 0, false); + generate_blocks(1); + + const auto& market = create_betting_market(market_group.id, {{"en", "market"}}); + generate_blocks(1); + + place_bet(alice_id, market.id, bet_type::back, asset(betAsset, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(bob_id, market.id, bet_type::lay, asset(betAsset, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + generate_blocks(1); + + delete_event_group(nhl.id); + generate_blocks(1); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), initialAccountAsset); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), initialAccountAsset); + } FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_CASE(event_group_delete_test_not_proposal) { try { - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); event_group_delete_operation event_group_delete_op; event_group_delete_op.event_group_id = nhl.id; @@ -1880,7 +1908,7 @@ BOOST_AUTO_TEST_CASE(event_group_delete_test_not_existed_event_group) { try { - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); delete_event_group(nhl.id); @@ -1893,7 +1921,7 @@ BOOST_AUTO_TEST_CASE(event_update_test) try { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); transfer(account_id_type(), alice_id, asset(10000000)); transfer(account_id_type(), bob_id, asset(10000000)); @@ -1941,7 +1969,7 @@ BOOST_AUTO_TEST_CASE(betting_market_rules_update_test) try { ACTORS( (alice) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); fc::optional empty; fc::optional name = internationalized_string_type({{"en", "NHL Rules v1.1"}}); @@ -1964,7 +1992,7 @@ BOOST_AUTO_TEST_CASE(betting_market_group_update_test) try { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); transfer(account_id_type(), alice_id, asset(10000000)); place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); @@ -2006,7 +2034,7 @@ BOOST_AUTO_TEST_CASE(betting_market_update_test) try { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); transfer(account_id_type(), alice_id, asset(10000000)); place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); @@ -2051,7 +2079,7 @@ BOOST_AUTO_TEST_CASE(event_driven_standard_progression_1) { try { - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); graphene::bookie::bookie_api bookie_api(app); // save the event id for checking after it is deleted event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; @@ -2094,7 +2122,7 @@ BOOST_AUTO_TEST_CASE(event_driven_standard_progression_1_with_delay) { try { - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 60 /* seconds */); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 60 /* seconds */, false); graphene::bookie::bookie_api bookie_api(app); // save the ids for checking after it is deleted @@ -2165,7 +2193,7 @@ BOOST_AUTO_TEST_CASE(event_driven_standard_progression_2) { try { - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); graphene::bookie::bookie_api bookie_api(app); // save the event id for checking after it is deleted @@ -2253,7 +2281,7 @@ BOOST_AUTO_TEST_CASE(event_driven_standard_progression_2_never_in_play) { try { - CREATE_ICE_HOCKEY_BETTING_MARKET(true, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(true, 0, false); graphene::bookie::bookie_api bookie_api(app); // save the event id for checking after it is deleted @@ -2339,7 +2367,7 @@ BOOST_AUTO_TEST_CASE(event_driven_standard_progression_3) { try { - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); graphene::bookie::bookie_api bookie_api(app); // save the event id for checking after it is deleted @@ -2412,7 +2440,7 @@ BOOST_AUTO_TEST_CASE(event_driven_progression_errors_1) { try { - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); graphene::bookie::bookie_api bookie_api(app); // save the event id for checking after it is deleted @@ -2515,7 +2543,7 @@ BOOST_AUTO_TEST_CASE(event_driven_progression_errors_2) { try { - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); graphene::bookie::bookie_api bookie_api(app); // save the event id for checking after it is deleted @@ -2567,7 +2595,7 @@ BOOST_AUTO_TEST_CASE(betting_market_group_driven_standard_progression) { try { - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); graphene::bookie::bookie_api bookie_api(app); // save the event id for checking after it is deleted @@ -2765,7 +2793,7 @@ BOOST_FIXTURE_TEST_CASE( another_event_group_update_test, database_fixture) try { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); transfer(account_id_type(), alice_id, asset(10000000)); transfer(account_id_type(), bob_id, asset(10000000)); @@ -2922,6 +2950,7 @@ BOOST_AUTO_TEST_CASE( wimbledon_2017_gentelmen_singles_final_test ) BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); update_betting_market_group(moneyline_cilic_vs_federer_id, _status = betting_market_group_status::closed); + // federer wins resolve_betting_market_group(moneyline_cilic_vs_federer_id, {{cilic_wins_final_market_id, betting_market_resolution_type::/*don't use cancel - there are bets for berdych*/not_win}, @@ -2939,6 +2968,96 @@ BOOST_AUTO_TEST_CASE( wimbledon_2017_gentelmen_singles_final_test ) BOOST_AUTO_TEST_SUITE_END() +// + +BOOST_FIXTURE_TEST_SUITE( multimarket_tests, database_fixture ) + +BOOST_AUTO_TEST_CASE(create_invalid_market_group) +{ + try + { + ACTORS( (alice)(bob) ) + + const int initialAccountAsset = 10000000; + + transfer(account_id_type(), alice_id, asset(initialAccountAsset)); + transfer(account_id_type(), bob_id, asset(initialAccountAsset)); + generate_blocks(1); + + create_sport({{"en", "Ice Hockey"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}); + generate_blocks(1); + const sport_object& ice_hockey = *db.get_index_type().indices().get().rbegin(); + + create_event_group({{"en", "NHL"}, {"zh_Hans", "國家冰球聯盟"}, {"ja", "ナショナルホッケーリーグ"}}, ice_hockey.id); + generate_blocks(1); + const event_group_object& nhl = *db.get_index_type().indices().get().rbegin(); + + create_event({{"en", "Washington Capitals/Chicago Blackhawks"}, {"zh_Hans", "華盛頓首都隊/芝加哥黑鷹"}, {"ja", "ワシントン・キャピタルズ/シカゴ・ブラックホークス"}}, {{"en", "2016-17"}}, nhl.id); + generate_blocks(1); + const event_object& capitals_vs_blackhawks = *db.get_index_type().indices().get().rbegin(); + + create_betting_market_rules({{"en", "NHL Rules v1.0"}}, {{"en", "The winner will be the team with the most points at the end of the game. The team with fewer points will not be the winner."}}); + generate_blocks(1); + const betting_market_rules_object& betting_market_rules = *db.get_index_type().indices().get().rbegin(); + + BOOST_TEST_MESSAGE("create a betting market group with exactly one winner"); + create_betting_market_group({{"en", "Moneyline"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), false, 0, true); + generate_blocks(1); + const betting_market_group_object& moneyline_betting_markets = *db.get_index_type().indices().get().rbegin(); + + BOOST_TEST_MESSAGE("create one betting market in it"); + create_betting_market(moneyline_betting_markets.id, {{"en", "Washington Capitals win"}}); + generate_blocks(1); + const betting_market_object& capitals_win_market = *db.get_index_type().indices().get().rbegin(); + + BOOST_TEST_MESSAGE("When alice places the first bet, we should reject it because the market is invalid " + "(a market with one market and one guaranteed winner doesn't make sense)"); + BOOST_CHECK_THROW( place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION), fc::exception); + + BOOST_TEST_MESSAGE("We can fix it by adding a second market"); + create_betting_market(moneyline_betting_markets.id, {{"en", "Chicago Blackhawks win"}}); + generate_blocks(1); + const betting_market_object& blackhawks_win_market = *db.get_index_type().indices().get().rbegin(); + + BOOST_TEST_MESSAGE("This bet should now succeed"); + BOOST_CHECK_NO_THROW(place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION)); + + BOOST_TEST_MESSAGE("create a different betting market group. This one will not have the exactly_one_winner restriction."); + create_betting_market_group({{"en", "Unused"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), false, 0, false); + generate_blocks(1); + const betting_market_group_object& unused_betting_markets = *db.get_index_type().indices().get().rbegin(); + + BOOST_TEST_MESSAGE("Try to move one of the betting markets into the new group, this should fail"); + // This will fail, but since it's processed as a proposal, the blockchain will keep retrying until it + // expires. So instead, check whether it worked. + update_betting_market(blackhawks_win_market.id, unused_betting_markets.id, fc::optional()); + generate_blocks(1); + BOOST_CHECK(blackhawks_win_market.group_id == moneyline_betting_markets.id); + + BOOST_TEST_MESSAGE("Canceling bets so we can do the move"); + cancel_unmatched_bets(moneyline_betting_markets.id); + generate_blocks(1); + + BOOST_TEST_MESSAGE("Try to move one of the betting markets into the new group, this should succeed"); + update_betting_market(blackhawks_win_market.id, unused_betting_markets.id, fc::optional()); + generate_blocks(1); + BOOST_CHECK(blackhawks_win_market.group_id == unused_betting_markets.id); + + BOOST_TEST_MESSAGE("Try again to place a bet in the first market. It should fail because there is only one market in the group."); + BOOST_CHECK_THROW( place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION), fc::exception); + + BOOST_TEST_MESSAGE("But we should be able to place it in the second market."); + place_bet(alice_id, blackhawks_win_market.id, bet_type::back, asset(1000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + BOOST_CHECK_NO_THROW( place_bet(alice_id, blackhawks_win_market.id, bet_type::back, asset(1000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION)); + + BOOST_TEST_MESSAGE("Try to move the capitals win market into the new group, this should fail"); + update_betting_market(capitals_win_market.id, unused_betting_markets.id, fc::optional()); + generate_blocks(1); + BOOST_CHECK(capitals_win_market.group_id == moneyline_betting_markets.id); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_SUITE_END() //#define BOOST_TEST_MODULE "C++ Unit Tests for Graphene Blockchain Database" diff --git a/tests/common/betting_test_markets.hpp b/tests/common/betting_test_markets.hpp index f67dc0677..11c576ccb 100644 --- a/tests/common/betting_test_markets.hpp +++ b/tests/common/betting_test_markets.hpp @@ -30,7 +30,7 @@ using namespace graphene::chain; -#define CREATE_ICE_HOCKEY_BETTING_MARKET(never_in_play, delay_before_settling) \ +#define CREATE_ICE_HOCKEY_BETTING_MARKET(never_in_play, delay_before_settling, allow_a_draw) \ create_sport({{"en", "Ice Hockey"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}); \ generate_blocks(1); \ const sport_object& ice_hockey = *db.get_index_type().indices().get().rbegin(); \ @@ -43,7 +43,7 @@ using namespace graphene::chain; create_betting_market_rules({{"en", "NHL Rules v1.0"}}, {{"en", "The winner will be the team with the most points at the end of the game. The team with fewer points will not be the winner."}}); \ generate_blocks(1); \ const betting_market_rules_object& betting_market_rules = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market_group({{"en", "Moneyline"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ + create_betting_market_group({{"en", "Moneyline"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling, !allow_a_draw); \ generate_blocks(1); \ const betting_market_group_object& moneyline_betting_markets = *db.get_index_type().indices().get().rbegin(); \ create_betting_market(moneyline_betting_markets.id, {{"en", "Washington Capitals win"}}); \ @@ -56,8 +56,8 @@ using namespace graphene::chain; // create the basic betting market, plus groups for the first, second, and third period results #define CREATE_EXTENDED_ICE_HOCKEY_BETTING_MARKET(never_in_play, delay_before_settling) \ - CREATE_ICE_HOCKEY_BETTING_MARKET(never_in_play, delay_before_settling) \ - create_betting_market_group({{"en", "First Period Result"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ + CREATE_ICE_HOCKEY_BETTING_MARKET(never_in_play, delay_before_settling, false) \ + create_betting_market_group({{"en", "First Period Result"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling, true); \ generate_blocks(1); \ const betting_market_group_object& first_period_result_betting_markets = *db.get_index_type().indices().get().rbegin(); \ create_betting_market(first_period_result_betting_markets.id, {{"en", "Washington Capitals win"}}); \ @@ -68,7 +68,7 @@ using namespace graphene::chain; const betting_market_object& first_period_blackhawks_win_market = *db.get_index_type().indices().get().rbegin(); \ (void)first_period_capitals_win_market; (void)first_period_blackhawks_win_market; \ \ - create_betting_market_group({{"en", "Second Period Result"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ + create_betting_market_group({{"en", "Second Period Result"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling, true); \ generate_blocks(1); \ const betting_market_group_object& second_period_result_betting_markets = *db.get_index_type().indices().get().rbegin(); \ create_betting_market(second_period_result_betting_markets.id, {{"en", "Washington Capitals win"}}); \ @@ -79,7 +79,7 @@ using namespace graphene::chain; const betting_market_object& second_period_blackhawks_win_market = *db.get_index_type().indices().get().rbegin(); \ (void)second_period_capitals_win_market; (void)second_period_blackhawks_win_market; \ \ - create_betting_market_group({{"en", "Third Period Result"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ + create_betting_market_group({{"en", "Third Period Result"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling, true); \ generate_blocks(1); \ const betting_market_group_object& third_period_result_betting_markets = *db.get_index_type().indices().get().rbegin(); \ create_betting_market(third_period_result_betting_markets.id, {{"en", "Washington Capitals win"}}); \ @@ -106,10 +106,10 @@ using namespace graphene::chain; create_event({{"en", "M. Cilic/S. Querrye"}}, {{"en", "2017"}}, wimbledon.id); \ generate_blocks(1); \ const event_object& cilic_vs_querrey = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market_group({{"en", "Moneyline 1st sf"}}, berdych_vs_federer.id, tennis_rules.id, asset_id_type(), false, 0); \ + create_betting_market_group({{"en", "Moneyline 1st sf"}}, berdych_vs_federer.id, tennis_rules.id, asset_id_type(), false, 0, true); \ generate_blocks(1); \ const betting_market_group_object& moneyline_berdych_vs_federer = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market_group({{"en", "Moneyline 2nd sf"}}, cilic_vs_querrey.id, tennis_rules.id, asset_id_type(), false, 0); \ + create_betting_market_group({{"en", "Moneyline 2nd sf"}}, cilic_vs_querrey.id, tennis_rules.id, asset_id_type(), false, 0, true); \ generate_blocks(1); \ const betting_market_group_object& moneyline_cilic_vs_querrey = *db.get_index_type().indices().get().rbegin(); \ create_betting_market(moneyline_berdych_vs_federer.id, {{"en", "T. Berdych defeats R. Federer"}}); \ @@ -127,7 +127,7 @@ using namespace graphene::chain; create_event({{"en", "R. Federer/M. Cilic"}}, {{"en", "2017"}}, wimbledon.id); \ generate_blocks(1); \ const event_object& cilic_vs_federer = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market_group({{"en", "Moneyline final"}}, cilic_vs_federer.id, tennis_rules.id, asset_id_type(), false, 0); \ + create_betting_market_group({{"en", "Moneyline final"}}, cilic_vs_federer.id, tennis_rules.id, asset_id_type(), false, 0, true); \ generate_blocks(1); \ const betting_market_group_object& moneyline_cilic_vs_federer = *db.get_index_type().indices().get().rbegin(); \ create_betting_market(moneyline_cilic_vs_federer.id, {{"en", "R. Federer defeats M. Cilic"}}); \ @@ -148,7 +148,7 @@ struct simple_bet_test_fixture : database_fixture { simple_bet_test_fixture() { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); // give alice and bob 10k each transfer(account_id_type(), alice_id, asset(10000)); diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index c6a216406..efc09ff0b 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -227,16 +227,10 @@ void database_fixture::verify_asset_supplies( const database& db ) for( const fba_accumulator_object& fba : db.get_index_type< simple_index< fba_accumulator_object > >() ) total_balances[ asset_id_type() ] += fba.accumulated_fba_fees; - for (const bet_object& o : db.get_index_type().indices()) + for (const betting_market_group_position_object& o : db.get_index_type().indices()) { - total_balances[o.amount_to_bet.asset_id] += o.amount_to_bet.amount; - } - for (const betting_market_position_object& o : db.get_index_type().indices()) - { - const betting_market_object& betting_market = o.betting_market_id(db); - const betting_market_group_object& betting_market_group = betting_market.group_id(db); + const betting_market_group_object& betting_market_group = o.betting_market_group_id(db); total_balances[betting_market_group.asset_id] += o.pay_if_canceled; - total_balances[betting_market_group.asset_id] += o.fees_collected; } total_balances[asset_id_type()] += db.get_dynamic_global_properties().witness_budget; @@ -1423,7 +1417,8 @@ const betting_market_group_object& database_fixture::create_betting_market_group betting_market_rules_id_type rules_id, asset_id_type asset_id, bool never_in_play, - uint32_t delay_before_settling) + uint32_t delay_before_settling, + bool exactly_one_winner) { try { betting_market_group_create_operation betting_market_group_create_op; betting_market_group_create_op.description = description; @@ -1432,6 +1427,7 @@ const betting_market_group_object& database_fixture::create_betting_market_group betting_market_group_create_op.asset_id = asset_id; betting_market_group_create_op.never_in_play = never_in_play; betting_market_group_create_op.delay_before_settling = delay_before_settling; + betting_market_group_create_op.resolution_constraint = exactly_one_winner ? betting_market_resolution_constraint::exactly_one_winner : betting_market_resolution_constraint::at_most_one_winner; process_operation_by_witnesses(betting_market_group_create_op); const auto& betting_market_group_index = db.get_index_type().indices().get(); @@ -1491,12 +1487,23 @@ void database_fixture::update_betting_market(betting_market_id_type betting_mark bet_place_op.backer_multiplier = backer_multiplier; bet_place_op.back_or_lay = back_or_lay; + set_expiration( db, trx ); trx.operations.push_back(bet_place_op); trx.validate(); - processed_transaction ptx = db.push_transaction(trx, ~0); - trx.operations.clear(); - BOOST_CHECK_MESSAGE(ptx.operation_results.size() == 1, "Place Bet Transaction should have had exactly one operation result"); - return ptx.operation_results.front().get().as(); + try + { + processed_transaction ptx = db.push_transaction(trx, ~0); + trx.operations.clear(); + BOOST_CHECK_EQUAL(ptx.operation_results.size(), 1); + if (ptx.operation_results.size() != 1) + wlog("Actual operations generated by place_bet: ${ops}", ("ops", ptx.operation_results)); + return ptx.operation_results.front().get().as(); + } + catch (...) + { + trx.operations.clear(); + throw; + } } FC_CAPTURE_AND_RETHROW( (bettor_id)(back_or_lay)(amount_to_bet) ) } diff --git a/tests/common/database_fixture.hpp b/tests/common/database_fixture.hpp index 200d1897a..1fd2f5b06 100644 --- a/tests/common/database_fixture.hpp +++ b/tests/common/database_fixture.hpp @@ -341,7 +341,8 @@ struct database_fixture { betting_market_rules_id_type rules_id, asset_id_type asset_id, bool never_in_play, - uint32_t delay_before_settling); + uint32_t delay_before_settling, + bool exactly_one_winner); void update_betting_market_group_impl(betting_market_group_id_type betting_market_group_id, fc::optional description, fc::optional rules_id, diff --git a/tests/tests/affiliate_tests.cpp b/tests/tests/affiliate_tests.cpp index ab109ad3f..05b7dfcb5 100644 --- a/tests/tests/affiliate_tests.cpp +++ b/tests/tests/affiliate_tests.cpp @@ -521,7 +521,7 @@ BOOST_AUTO_TEST_CASE( bookie_payout_test ) affiliate_test_helper ath( *this ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0, false); // place bets at 10:1 place_bet(ath.paula_id, capitals_win_market.id, bet_type::back, asset(10000, asset_id_type()), 11 * GRAPHENE_BETTING_ODDS_PRECISION); @@ -562,7 +562,7 @@ BOOST_AUTO_TEST_CASE( bookie_payout_test ) create_event({{"en", "Washington Capitals/Chicago Blackhawks"}, {"zh_Hans", "華盛頓首都隊/芝加哥黑鷹"}, {"ja", "ワシントン・キャピタルズ/シカゴ・ブラックホークス"}}, {{"en", "2016-17"}}, nhl.id); \ generate_blocks(1); \ const event_object& capitals_vs_blackhawks2 = *db.get_index_type().indices().get().rbegin(); \ - create_betting_market_group({{"en", "Moneyline"}}, capitals_vs_blackhawks2.id, betting_market_rules.id, btc_id, false, 0); + create_betting_market_group({{"en", "Moneyline"}}, capitals_vs_blackhawks2.id, betting_market_rules.id, btc_id, false, 0, true); generate_blocks(1); const betting_market_group_object& moneyline_betting_markets2 = *db.get_index_type().indices().get().rbegin(); create_betting_market(moneyline_betting_markets2.id, {{"en", "Washington Capitals win"}});