Sort bets by odds for use in an order book
This commit is contained in:
parent
239d8ee885
commit
9f6edc649d
7 changed files with 343 additions and 11 deletions
|
|
@ -100,6 +100,7 @@ add_library( graphene_chain
|
|||
event_evaluator.cpp
|
||||
protocol/betting_market.cpp
|
||||
betting_market_evaluator.cpp
|
||||
betting_market_object.cpp
|
||||
db_bet.cpp
|
||||
|
||||
${HEADERS}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,10 @@ void_result bet_place_evaluator::do_evaluate(const bet_place_operation& op)
|
|||
FC_ASSERT(op.backer_multiplier % allowed_increment == 0, "Bet odds must be a multiple of ${allowed_increment}", ("allowed_increment", allowed_increment));
|
||||
}
|
||||
|
||||
// is it possible to match this bet
|
||||
FC_ASSERT(bet_object::get_matching_amount(op.amount_to_bet.amount, op.backer_multiplier, op.back_or_lay) != 0,
|
||||
"Bet cannot be matched");
|
||||
|
||||
// verify they reserved enough to cover the percentage fee
|
||||
uint16_t percentage_fee = current_params.current_fees->get<bet_place_operation>().percentage_fee;
|
||||
fc::uint128_t minimum_percentage_fee_calculation = op.amount_to_bet.amount.value;
|
||||
|
|
|
|||
45
libraries/chain/betting_market_object.cpp
Normal file
45
libraries/chain/betting_market_object.cpp
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#include <graphene/chain/betting_market_object.hpp>
|
||||
|
||||
namespace graphene { namespace chain {
|
||||
|
||||
/* static */ share_type bet_object::get_matching_amount(share_type bet_amount, bet_multiplier_type backer_multiplier, bet_type back_or_lay)
|
||||
{
|
||||
fc::uint128_t amount_to_match_128 = bet_amount.value;
|
||||
|
||||
if (back_or_lay == bet_type::back)
|
||||
{
|
||||
amount_to_match_128 *= backer_multiplier - GRAPHENE_100_PERCENT;
|
||||
amount_to_match_128 /= GRAPHENE_100_PERCENT;
|
||||
}
|
||||
else
|
||||
{
|
||||
amount_to_match_128 *= GRAPHENE_100_PERCENT;
|
||||
amount_to_match_128 /= backer_multiplier - GRAPHENE_100_PERCENT;
|
||||
}
|
||||
return amount_to_match_128.to_uint64();
|
||||
}
|
||||
|
||||
share_type bet_object::get_matching_amount() const
|
||||
{
|
||||
return get_matching_amount(amount_to_bet.amount, backer_multiplier, back_or_lay);
|
||||
}
|
||||
|
||||
share_type betting_market_position_object::reduce()
|
||||
{
|
||||
share_type additional_not_cancel_balance = std::min(pay_if_payout_condition, pay_if_not_payout_condition);
|
||||
if (additional_not_cancel_balance == 0)
|
||||
return 0;
|
||||
pay_if_payout_condition -= additional_not_cancel_balance;
|
||||
pay_if_not_payout_condition -= additional_not_cancel_balance;
|
||||
pay_if_not_canceled += additional_not_cancel_balance;
|
||||
|
||||
share_type immediate_winnings = std::min(pay_if_canceled, pay_if_not_canceled);
|
||||
if (immediate_winnings == 0)
|
||||
return 0;
|
||||
pay_if_canceled -= immediate_winnings;
|
||||
pay_if_not_canceled -= immediate_winnings;
|
||||
return immediate_winnings;
|
||||
}
|
||||
|
||||
} } // graphene::chain
|
||||
|
||||
|
|
@ -21,12 +21,174 @@ void database::cancel_bet( const bet_object& bet, bool create_virtual_op )
|
|||
|
||||
bool maybe_cull_small_bet( database& db, const bet_object& bet_object_to_cull )
|
||||
{
|
||||
/**
|
||||
* There are times when the AMOUNT_FOR_SALE * SALE_PRICE == 0 which means that we
|
||||
* have hit the limit where the seller is asking for nothing in return. When this
|
||||
* happens we must refund any balance back to the seller, it is too small to be
|
||||
* sold at the sale price.
|
||||
*
|
||||
* If the order is a taker order (as opposed to a maker order), so the price is
|
||||
* set by the counterparty, this check is deferred until the order becomes unmatched
|
||||
* (see #555) -- however, detecting this condition is the responsibility of the caller.
|
||||
*/
|
||||
|
||||
if( bet_object_to_cull.get_matching_amount() == 0 )
|
||||
{
|
||||
ilog("applied epsilon logic");
|
||||
db.cancel_bet(bet_object_to_cull);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
share_type adjust_betting_position(database& db, account_id_type bettor_id, betting_market_id_type betting_market_id, bet_type back_or_lay, share_type bet_amount, share_type fees_collected)
|
||||
{ try {
|
||||
assert(bet_amount >= 0);
|
||||
|
||||
share_type guaranteed_winnings_returned = 0;
|
||||
|
||||
if (bet_amount == 0)
|
||||
return guaranteed_winnings_returned;
|
||||
|
||||
auto& index = db.get_index_type<betting_market_position_index>().indices().get<by_bettor_betting_market>();
|
||||
auto itr = index.find(boost::make_tuple(bettor_id, betting_market_id));
|
||||
if (itr == index.end())
|
||||
{
|
||||
db.create<betting_market_position_object>([&](betting_market_position_object& position) {
|
||||
position.bettor_id = bettor_id;
|
||||
position.betting_market_id = betting_market_id;
|
||||
position.pay_if_payout_condition = back_or_lay == bet_type::back ? bet_amount : 0;
|
||||
position.pay_if_not_payout_condition = back_or_lay == bet_type::lay ? bet_amount : 0;
|
||||
position.pay_if_canceled = bet_amount;
|
||||
position.pay_if_not_canceled = 0;
|
||||
position.fees_collected = fees_collected;
|
||||
// this should not be reducible
|
||||
});
|
||||
} else {
|
||||
db.modify(*itr, [&](betting_market_position_object& position) {
|
||||
assert(position.bettor_id == bettor_id);
|
||||
assert(position.betting_market_id == betting_market_id);
|
||||
position.pay_if_payout_condition += back_or_lay == bet_type::back ? bet_amount : 0;
|
||||
position.pay_if_not_payout_condition += back_or_lay == bet_type::lay ? bet_amount : 0;
|
||||
position.pay_if_canceled += bet_amount;
|
||||
position.fees_collected += fees_collected;
|
||||
|
||||
guaranteed_winnings_returned = position.reduce();
|
||||
});
|
||||
}
|
||||
return guaranteed_winnings_returned;
|
||||
} FC_CAPTURE_AND_RETHROW((bettor_id)(betting_market_id)(bet_amount)) }
|
||||
|
||||
|
||||
bool bet_was_matched(database& db, const bet_object& bet, share_type amount_bet, bet_multiplier_type actual_multiplier, bool cull_if_small)
|
||||
{
|
||||
// calculate the percentage fee paid
|
||||
fc::uint128_t percentage_fee_128 = bet.amount_reserved_for_fees.value;
|
||||
percentage_fee_128 *= amount_bet.value;
|
||||
percentage_fee_128 += bet.amount_to_bet.amount.value - 1;
|
||||
percentage_fee_128 /= bet.amount_to_bet.amount.value;
|
||||
share_type fee_paid = percentage_fee_128.to_uint64();
|
||||
|
||||
// record their bet, modifying their position, and return any winnings
|
||||
share_type guaranteed_winnings_returned = adjust_betting_position(db, bet.bettor_id, bet.betting_market_id,
|
||||
bet.back_or_lay, amount_bet, fee_paid);
|
||||
db.adjust_balance(bet.bettor_id, asset(guaranteed_winnings_returned, bet.amount_to_bet.asset_id));
|
||||
|
||||
// generate a virtual "match" op
|
||||
asset asset_amount_bet(amount_bet, bet.amount_to_bet.asset_id);
|
||||
db.push_applied_operation(bet_matched_operation(bet.bettor_id, bet.id,
|
||||
asset_amount_bet,
|
||||
fee_paid,
|
||||
actual_multiplier,
|
||||
guaranteed_winnings_returned));
|
||||
|
||||
// update the bet on the books
|
||||
if (asset_amount_bet == bet.amount_to_bet)
|
||||
{
|
||||
db.remove(bet);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
db.modify(bet, [&](bet_object& bet_obj) {
|
||||
bet_obj.amount_to_bet -= asset_amount_bet;
|
||||
bet_obj.amount_reserved_for_fees -= fee_paid;
|
||||
});
|
||||
if (cull_if_small)
|
||||
return maybe_cull_small_bet(db, bet);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the two orders,
|
||||
*
|
||||
* @return a bit field indicating which orders were filled (and thus removed)
|
||||
*
|
||||
* 0 - no orders were matched
|
||||
* 1 - bid was filled
|
||||
* 2 - ask was filled
|
||||
* 3 - both were filled
|
||||
*/
|
||||
int match_bet(database& db, const bet_object& taker_bet, const bet_object& maker_bet )
|
||||
{
|
||||
assert(taker_bet.amount_to_bet.asset_id == maker_bet.amount_to_bet.asset_id);
|
||||
assert(taker_bet.amount_to_bet.amount > 0 && maker_bet.amount_to_bet.amount > 0);
|
||||
assert(taker_bet.backer_multiplier >= maker_bet.backer_multiplier);
|
||||
assert(taker_bet.back_or_lay != maker_bet.back_or_lay);
|
||||
|
||||
int result = 0;
|
||||
share_type maximum_amount_to_match = taker_bet.get_matching_amount();
|
||||
|
||||
if (maximum_amount_to_match <= maker_bet.amount_to_bet.amount)
|
||||
{
|
||||
// we will consume the entire taker bet
|
||||
result |= bet_was_matched(db, taker_bet, taker_bet.amount_to_bet.amount, maker_bet.backer_multiplier, true);
|
||||
result |= bet_was_matched(db, maker_bet, maximum_amount_to_match, maker_bet.backer_multiplier, true) << 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we will consume the entire maker bet. Figure out how much of the taker bet we can fill.
|
||||
share_type taker_amount = maker_bet.get_matching_amount();
|
||||
share_type maker_amount = bet_object::get_matching_amount(taker_amount, maker_bet.backer_multiplier, taker_bet.back_or_lay);
|
||||
|
||||
result |= bet_was_matched(db, taker_bet, taker_amount, maker_bet.backer_multiplier, true);
|
||||
result |= bet_was_matched(db, maker_bet, maker_amount, maker_bet.backer_multiplier, true) << 1;
|
||||
}
|
||||
|
||||
assert(result != 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool database::place_bet(const bet_object& new_bet_object)
|
||||
{
|
||||
return false;
|
||||
bet_id_type bet_id = new_bet_object.id;
|
||||
const asset_object& bet_asset = get(new_bet_object.amount_to_bet.asset_id);
|
||||
|
||||
const auto& bet_odds_idx = get_index_type<bet_object_index>().indices().get<by_odds>();
|
||||
|
||||
// TODO: it should be possible to simply check the NEXT/PREV iterator after new_order_object to
|
||||
// determine whether or not this order has "changed the book" in a way that requires us to
|
||||
// check orders. For now I just lookup the lower bound and check for equality... this is log(n) vs
|
||||
// constant time check. Potential optimization.
|
||||
|
||||
bet_type bet_type_to_match = new_bet_object.back_or_lay == bet_type::back ? bet_type::lay : bet_type::back;
|
||||
auto book_itr = bet_odds_idx.lower_bound(std::make_tuple(new_bet_object.betting_market_id, bet_type_to_match));
|
||||
auto book_end = bet_odds_idx.upper_bound(std::make_tuple(new_bet_object.betting_market_id, bet_type_to_match, new_bet_object.backer_multiplier));
|
||||
|
||||
int orders_matched_flags = 0;
|
||||
bool finished = false;
|
||||
while (!finished && book_itr != book_end)
|
||||
{
|
||||
auto old_book_itr = book_itr;
|
||||
++book_itr;
|
||||
// match returns 2 when only the old order was fully filled. In this case, we keep matching; otherwise, we stop.
|
||||
|
||||
orders_matched_flags = match_bet(*this, new_bet_object, *old_book_itr);
|
||||
finished = orders_matched_flags != 2;
|
||||
}
|
||||
|
||||
return (orders_matched_flags & 1) != 0;
|
||||
}
|
||||
|
||||
} }
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ void database::initialize_indexes()
|
|||
add_index< primary_index< buyback_index > >();
|
||||
|
||||
add_index< primary_index< simple_index< fba_accumulator_object > > >();
|
||||
add_index< primary_index< betting_market_position_multi_index > >();
|
||||
add_index< primary_index< betting_market_position_index > >();
|
||||
add_index< primary_index< global_betting_statistics_object_index > >();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@
|
|||
#include <graphene/chain/protocol/types.hpp>
|
||||
#include <graphene/db/object.hpp>
|
||||
#include <graphene/db/generic_index.hpp>
|
||||
#include <graphene/chain/protocol/betting_market.hpp>
|
||||
|
||||
#include <boost/multi_index/composite_key.hpp>
|
||||
|
||||
namespace graphene { namespace chain {
|
||||
|
||||
|
|
@ -75,6 +78,9 @@ class bet_object : public graphene::db::abstract_object< bet_object >
|
|||
share_type amount_reserved_for_fees; // same asset type as amount_to_bet
|
||||
|
||||
bet_type back_or_lay;
|
||||
|
||||
static share_type get_matching_amount(share_type bet_amount, bet_multiplier_type backer_multiplier, bet_type back_or_lay);
|
||||
share_type get_matching_amount() const;
|
||||
};
|
||||
|
||||
class betting_market_position_object : public graphene::db::abstract_object< betting_market_position_object >
|
||||
|
|
@ -91,6 +97,9 @@ class betting_market_position_object : public graphene::db::abstract_object< bet
|
|||
share_type pay_if_not_payout_condition;
|
||||
share_type pay_if_canceled;
|
||||
share_type pay_if_not_canceled;
|
||||
share_type fees_collected;
|
||||
|
||||
share_type reduce();
|
||||
};
|
||||
|
||||
typedef multi_index_container<
|
||||
|
|
@ -110,23 +119,138 @@ typedef multi_index_container<
|
|||
|
||||
typedef generic_index<betting_market_object, betting_market_object_multi_index_type> betting_market_object_index;
|
||||
|
||||
struct compare_bet_by_odds {
|
||||
bool operator()(const bet_object& lhs, const bet_object& rhs) const
|
||||
{
|
||||
return compare(lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier, lhs.id,
|
||||
rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier, rhs.id);
|
||||
}
|
||||
|
||||
template<typename T0>
|
||||
bool operator() (const std::tuple<T0>& lhs, const bet_object& rhs) const
|
||||
{
|
||||
return compare(std::get<0>(lhs), rhs.betting_market_id);
|
||||
}
|
||||
|
||||
template<typename T0>
|
||||
bool operator() (const bet_object& lhs, const std::tuple<T0>& rhs) const
|
||||
{
|
||||
return compare(lhs.betting_market_id, std::get<0>(rhs));
|
||||
}
|
||||
|
||||
template<typename T0, typename T1>
|
||||
bool operator() (const std::tuple<T0, T1>& lhs, const bet_object& rhs) const
|
||||
{
|
||||
return compare(std::get<0>(lhs), std::get<1>(lhs), rhs.betting_market_id, rhs.back_or_lay);
|
||||
}
|
||||
|
||||
template<typename T0, typename T1>
|
||||
bool operator() (const bet_object& lhs, const std::tuple<T0, T1>& rhs) const
|
||||
{
|
||||
return compare(lhs.betting_market_id, lhs.back_or_lay, std::get<0>(rhs), std::get<1>(rhs));
|
||||
}
|
||||
|
||||
template<typename T0, typename T1, typename T2>
|
||||
bool operator() (const std::tuple<T0, T1, T2>& lhs, const bet_object& rhs) const
|
||||
{
|
||||
return compare(std::get<0>(lhs), std::get<1>(lhs), std::get<2>(lhs),
|
||||
rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier);
|
||||
}
|
||||
|
||||
template<typename T0, typename T1, typename T2>
|
||||
bool operator() (const bet_object& lhs, const std::tuple<T0, T1, T2>& rhs) const
|
||||
{
|
||||
return compare(lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier,
|
||||
std::get<0>(rhs), std::get<1>(rhs), std::get<2>(rhs));
|
||||
}
|
||||
template<typename T0, typename T1, typename T2, typename T3>
|
||||
bool operator() (const std::tuple<T0, T1, T2, T3>& lhs, const bet_object& rhs) const
|
||||
{
|
||||
return compare(std::get<0>(lhs), std::get<1>(lhs), std::get<2>(lhs), std::get<3>(lhs),
|
||||
rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier, rhs.id);
|
||||
}
|
||||
|
||||
template<typename T0, typename T1, typename T2, typename T3>
|
||||
bool operator() (const bet_object& lhs, const std::tuple<T0, T1, T2, T3>& rhs) const
|
||||
{
|
||||
return compare(lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier, lhs.id,
|
||||
std::get<0>(rhs), std::get<1>(rhs), std::get<2>(rhs), std::get<3>(rhs));
|
||||
}
|
||||
bool compare(const betting_market_id_type& lhs_betting_market_id, const betting_market_id_type& rhs_betting_market_id)
|
||||
{
|
||||
return lhs_betting_market_id < rhs_betting_market_id;
|
||||
}
|
||||
bool compare(const betting_market_id_type& lhs_betting_market_id, bet_type lhs_bet_type,
|
||||
const betting_market_id_type& rhs_betting_market_id, bet_type rhs_bet_type) const
|
||||
{
|
||||
if (lhs_betting_market_id < rhs_betting_market_id)
|
||||
return true;
|
||||
if (lhs_betting_market_id > rhs_betting_market_id)
|
||||
return false;
|
||||
return lhs_bet_type < rhs_bet_type;
|
||||
}
|
||||
bool compare(const betting_market_id_type& lhs_betting_market_id, bet_type lhs_bet_type,
|
||||
bet_multiplier_type lhs_backer_multiplier,
|
||||
const betting_market_id_type& rhs_betting_market_id, bet_type rhs_bet_type,
|
||||
bet_multiplier_type rhs_backer_multiplier) const
|
||||
{
|
||||
if (lhs_betting_market_id < rhs_betting_market_id)
|
||||
return true;
|
||||
if (lhs_betting_market_id > rhs_betting_market_id)
|
||||
return false;
|
||||
if (lhs_bet_type < rhs_bet_type)
|
||||
return true;
|
||||
if (lhs_bet_type > rhs_bet_type)
|
||||
return false;
|
||||
return lhs_backer_multiplier < rhs_backer_multiplier;
|
||||
}
|
||||
bool compare(const betting_market_id_type& lhs_betting_market_id, bet_type lhs_bet_type,
|
||||
bet_multiplier_type lhs_backer_multiplier, const bet_id_type& lhs_bet_id,
|
||||
const betting_market_id_type& rhs_betting_market_id, bet_type rhs_bet_type,
|
||||
bet_multiplier_type rhs_backer_multiplier, const bet_id_type& rhs_bet_id) const
|
||||
{
|
||||
if (lhs_betting_market_id < rhs_betting_market_id)
|
||||
return true;
|
||||
if (lhs_betting_market_id > rhs_betting_market_id)
|
||||
return false;
|
||||
if (lhs_bet_type < rhs_bet_type)
|
||||
return true;
|
||||
if (lhs_bet_type > rhs_bet_type)
|
||||
return false;
|
||||
if (lhs_backer_multiplier < rhs_backer_multiplier)
|
||||
return true;
|
||||
if (lhs_backer_multiplier > rhs_backer_multiplier)
|
||||
return false;
|
||||
return lhs_bet_id < rhs_bet_id;
|
||||
}
|
||||
};
|
||||
|
||||
struct by_odds {};
|
||||
typedef multi_index_container<
|
||||
bet_object,
|
||||
indexed_by<
|
||||
ordered_unique< tag<by_id>, member< object, object_id_type, &object::id > > > > bet_object_multi_index_type;
|
||||
ordered_unique< tag<by_id>, member< object, object_id_type, &object::id > >,
|
||||
ordered_unique< tag<by_odds>, identity<bet_object>, compare_bet_by_odds > > > bet_object_multi_index_type;
|
||||
|
||||
typedef generic_index<bet_object, bet_object_multi_index_type> bet_object_index;
|
||||
|
||||
struct by_bettor_betting_market{};
|
||||
typedef multi_index_container<
|
||||
betting_market_position_object,
|
||||
indexed_by<
|
||||
ordered_unique< tag<by_id>, member< object, object_id_type, &object::id > > > > betting_market_position_multi_index_type;
|
||||
ordered_unique< tag<by_id>, member< object, object_id_type, &object::id > >,
|
||||
ordered_unique< tag<by_bettor_betting_market>,
|
||||
composite_key<
|
||||
betting_market_position_object,
|
||||
member<betting_market_position_object, account_id_type, &betting_market_position_object::bettor_id>,
|
||||
member<betting_market_position_object, betting_market_id_type, &betting_market_position_object::betting_market_id>
|
||||
> > > > betting_market_position_multi_index_type;
|
||||
|
||||
typedef generic_index<betting_market_position_object, betting_market_position_multi_index_type> betting_market_position_multi_index;
|
||||
typedef generic_index<betting_market_position_object, betting_market_position_multi_index_type> betting_market_position_index;
|
||||
} } // graphene::chain
|
||||
|
||||
FC_REFLECT_DERIVED( graphene::chain::betting_market_group_object, (graphene::db::object), (event_id)(options) )
|
||||
FC_REFLECT_DERIVED( graphene::chain::betting_market_object, (graphene::db::object), (group_id)(payout_condition)(asset_id) )
|
||||
FC_REFLECT_DERIVED( graphene::chain::bet_object, (graphene::db::object), (bettor_id)(betting_market_id)(amount_to_bet)(backer_multiplier)(amount_reserved_for_fees)(back_or_lay) )
|
||||
|
||||
FC_REFLECT_DERIVED( graphene::chain::betting_market_position_object, (graphene::db::object), (bettor_id)(betting_market_id)(pay_if_payout_condition)(pay_if_not_payout_condition)(pay_if_canceled)(pay_if_not_canceled) )
|
||||
FC_REFLECT_DERIVED( graphene::chain::betting_market_position_object, (graphene::db::object), (bettor_id)(betting_market_id)(pay_if_payout_condition)(pay_if_not_payout_condition)(pay_if_canceled)(pay_if_not_canceled)(fees_collected) )
|
||||
|
|
|
|||
|
|
@ -1649,9 +1649,7 @@ BOOST_AUTO_TEST_CASE( peerplays_sport_create_test )
|
|||
tx.operations.push_back(proposal_op);
|
||||
set_expiration(db, tx);
|
||||
sign(tx, init_account_priv_key);
|
||||
//sign( tx, philbin_private_key );
|
||||
|
||||
// Alice and Philbin signed, but asset issuer is invalid
|
||||
db.push_transaction(tx);
|
||||
}
|
||||
|
||||
|
|
@ -1767,9 +1765,7 @@ BOOST_AUTO_TEST_CASE( peerplays_sport_create_test )
|
|||
tx.operations.push_back(proposal_op);
|
||||
set_expiration(db, tx);
|
||||
sign(tx, init_account_priv_key);
|
||||
//sign( tx, philbin_private_key );
|
||||
|
||||
// Alice and Philbin signed, but asset issuer is invalid
|
||||
|
||||
db.push_transaction(tx);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue