diff --git a/betting_simulator.html b/betting_simulator.html index ed336685..014422e7 100644 --- a/betting_simulator.html +++ b/betting_simulator.html @@ -33,6 +33,24 @@ let percentage_fee = new Big(0);//new Big('0.02'); let bet_id_sequence = 0; + + let stop_on_error = true; + let error_count = 0; + let stop_on_warning = false; + let warning_count = 0; + + let log_warning = (message) => { + ++warning_count; + Notification.error({message: message, delay: 10000}); + //console.log("Warning: " + message); + }; + let log_error = (message) => { + ++error_count; + Notification.error({message: message, delay: 10000}); + //console.log("Error: " + message); + }; + + class LogEntry { constructor(description) { this.description = description; @@ -84,6 +102,12 @@ differencePosition.unused_locked_in_profit = differencePosition.unused_locked_in_profit.minus(otherPosition.unused_locked_in_profit); return differencePosition; } + has_nonzero_component() { + return ! (this.balance.eq(0) && this.balance_at_last_payout.eq(0) && this.win.eq(0) && this.not_win.eq(0) && + this.cancel.eq(0) && this.not_cancel.eq(0) && this.fees_paid.eq(0) && this.refundable_unmatched_bets.eq(0) && + this.unmatched_back_bets.eq(0) && this.unmatched_lay_bets.eq(0) && this.unused_lay_exposure.eq(0) && + this.unused_back_exposure.eq(0) && this.unused_locked_in_profit.eq(0)); + } reduce(parent_event_log_entry) { let additional_not_cancel_balance = big_min(this.win, this.not_win); this.win = this.win.minus(additional_not_cancel_balance); @@ -166,28 +190,38 @@ $scope.invariant_is_violated = (system, account_name) => { let balance_record = $scope.account_balances[system][account_name]; - if (!balance_record.balance.plus(balance_record.cancel).plus(balance_record.refundable_unmatched_bets).plus(balance_record.fees_paid).eq(balance_record.balance_at_last_payout)) + if (!balance_record.balance.plus(balance_record.cancel).plus(balance_record.refundable_unmatched_bets).plus(balance_record.fees_paid).eq(balance_record.balance_at_last_payout)) { + log_error(`Invariant violated for ${balance_record.account_name}, cancel + refundable + fees != balance_at_last_payout`); return true; + } - if (!balance_record.win.eq(0) && !balance_record.not_win.eq(0)) + if (!balance_record.win.eq(0) && !balance_record.not_win.eq(0)) { + log_error(`Invariant violated for ${balance_record.account_name}, either win or not_win must be 0`); return true; + } - if (!balance_record.cancel.eq(0) && !balance_record.not_cancel.eq(0)) + if (!balance_record.cancel.eq(0) && !balance_record.not_cancel.eq(0)) { + log_error(`Invariant violated for ${balance_record.account_name}, either cancel or not_cancel must be 0`); return true; + } if (system == 'simple') { if (balance_record.not_win.lt(balance_record.unused_lay_exposure) || balance_record.win.lt(balance_record.unused_back_exposure) || - balance_record.not_cancel.lt(balance_record.unused_locked_in_profit)) + balance_record.not_cancel.lt(balance_record.unused_locked_in_profit)) { + log_error(`Invariant violated for ${balance_record.account_name}, not_win < unused_lay_exposure or win < unused_back_exposure or not_cancel < unused_locked_in_profit`); return true; + } if (!balance_record.unmatched_lay_bets.plus(balance_record.unmatched_back_bets).eq( balance_record.refundable_unmatched_bets.plus( balance_record.win).minus(balance_record.unused_back_exposure).plus( balance_record.not_win).minus(balance_record.unused_lay_exposure).plus( - balance_record.not_cancel).minus(balance_record.unused_locked_in_profit))) + balance_record.not_cancel).minus(balance_record.unused_locked_in_profit))) { + log_error(`Invariant violated for ${balance_record.account_name}, unmatched_lay_bets + unmatched_back_bets != refundable_unmatched_bets + (win - unused_back_exposure) + (not_win - unused_lay_exposure) + (not_cancel - unused_locked_in_profit)`); return true; + } } return false; @@ -265,7 +299,8 @@ amount_bet).minus( delta_refundable_unmatched_bets) parent_event_log_entry.add_log_message(`return_amount: ${return_amount.toFixed()}`); - console.assert(return_amount.gte(0), 'Error, negative return amount'); + if (return_amount.lt(0)) + log_error(`Return amount of ${return_amount.toFixed()} for account ${bettor} is negative`); bettor_balances_simple_system.balance = bettor_balances_simple_system.balance.plus(return_amount); }; @@ -493,12 +528,10 @@ if (new_bet.back_or_lay == 'back') { exposure = bettor_balances_simple_system.unused_lay_exposure; - bettor_balances_simple_system.unmatched_back_bets = bettor_balances_simple_system.unmatched_back_bets.plus(new_bet.amount_to_bet); } else { exposure = bettor_balances_simple_system.unused_back_exposure; - bettor_balances_simple_system.unmatched_lay_bets = bettor_balances_simple_system.unmatched_lay_bets.plus(new_bet.amount_to_bet); } let amount_of_exposure_leaned_on = big_min(new_bet.amount_to_bet, exposure); @@ -510,9 +543,15 @@ if (required_funds.lte(bettor_balances_simple_system.balance)) { if (new_bet.back_or_lay == 'back') + { bettor_balances_simple_system.unused_lay_exposure = bettor_balances_simple_system.unused_lay_exposure.minus(amount_of_exposure_leaned_on); + bettor_balances_simple_system.unmatched_back_bets = bettor_balances_simple_system.unmatched_back_bets.plus(new_bet.amount_to_bet); + } else + { bettor_balances_simple_system.unused_back_exposure = bettor_balances_simple_system.unused_back_exposure.minus(amount_of_exposure_leaned_on); + bettor_balances_simple_system.unmatched_lay_bets = bettor_balances_simple_system.unmatched_lay_bets.plus(new_bet.amount_to_bet); + } bettor_balances_simple_system.unused_locked_in_profit = bettor_balances_simple_system.unused_locked_in_profit.minus(amount_of_locked_in_profit_leaned_on); if (required_funds.gt(0)) @@ -584,7 +623,7 @@ let bet_is_allowed_simple_system = register_bet_simple_system(new_bet, event_log_entry); // if we were allowed to palce the bet, add it to the order books and then see if we can match it now - if (bet_is_allowed_complex_system || bet_is_allowed_simple_system) { + if (bet_is_allowed_complex_system && bet_is_allowed_simple_system) { let order_book = new_bet.back_or_lay == 'back' ? $scope.order_book.backs : $scope.order_book.lays; let order_book_to_match_against = new_bet.back_or_lay == 'back' ? $scope.order_book.lays : $scope.order_book.backs; @@ -594,6 +633,11 @@ let accounts_affected = try_to_match_bet(new_bet, new_bet.back_or_lay, order_book_to_match_against, event_log_entry); event_log_entry.add_log_message(`After matching bet, accounts_affected is ${Array.from(accounts_affected).join(',')}`); update_unmatched_bets_complex_system(accounts_affected, event_log_entry); + return true; + } + else + { + return false; } }; @@ -694,21 +738,68 @@ return Math.floor(Math.random() * (max - min)) + min; }; + let valid_bet_odds_list = []; + let make_valid_bet_odds_list = () => { + let valid_bet_odds_table = [ [ 2, new Big( '.0100')], /* <= 2: 0.01 */ + [ 3, new Big( '.0200')], /* <= 3: 0.02 */ + [ 4, new Big( '.0500')], /* <= 4: 0.05 */ + [ 6, new Big( '.1000')], /* <= 6: 0.10 */ + [ 10, new Big( '.2000')], /* <= 10: 0.20 */ + [ 20, new Big( '.5000')], /* <= 20: 0.50 */ + [ 30, new Big( '1.0000')], /* <= 30: 1.00 */ + [ 50, new Big( '2.0000')], /* <= 50: 2.00 */ + [ 100, new Big( '5.0000')], /* <= 100: 5.00 */ + [ 1000, new Big('10.0000')] ]; /* <= 1000: 10.00 */ + + let current_odds = new Big('1'); + for (let i = 0; i < valid_bet_odds_table.length; ++i) { + while (current_odds < valid_bet_odds_table[i][0]) { + current_odds = current_odds.plus(valid_bet_odds_table[i][1]); + valid_bet_odds_list.push(current_odds); + } + } + }; + make_valid_bet_odds_list(); + //valid_bet_odds_list.forEach((odds) => { console.log(`odds: ${odds.toFixed()}`); }); + let random_bet_timer = null; + let number_of_random_bets_placed = 0; + let number_of_random_bets_to_place = 1000; + let max_difference = new Big(0); + let place_random_bet = () => { let back_or_lay = get_random_int(0, 2) == 0 ? 'back' : 'lay'; - let bettor = all_account_names[get_random_int(0, 2 /* all_account_names.length*/ )]; + let bettor = all_account_names[get_random_int(0, 3/*all_account_names.length*/)]; let amount_to_bet = get_random_int(1, 11) * 10; - let odds = get_random_int(2,11); + let odds = valid_bet_odds_list[get_random_int(1, valid_bet_odds_list.length)]; - $scope.place_bet(new Bet(bettor, back_or_lay, amount_to_bet, odds, 0)); + //let original_balance_simple = $scope.account_balances.simple[bettor].clone(); + //let original_balance_complex = $scope.account_balances.complex[bettor].clone(); + if ($scope.place_bet(new Bet(bettor, back_or_lay, amount_to_bet, odds, 0))) { + if (number_of_random_bets_placed % 100 == 0) + console.log(`placed bet #${number_of_random_bets_placed}`); + } else { + // unable to place bet due to lack of funds, randomly cancel a bet instead + if (number_of_random_bets_placed % 100 == 0) + console.log(`bet rejected because of insufficient funds, canceling a random bet instead #${number_of_random_bets_placed}`); + let bettor_bets = $scope.order_book['backs'].filter( (x) => x.bettor == bettor ).concat($scope.order_book['lays'].filter( (x) => x.bettor == bettor )); + let bet_to_cancel = bettor_bets[get_random_int(0, bettor_bets.length)]; + $scope.cancel_bet_interactive(bet_to_cancel); + } + //let final_balance_simple = $scope.account_balances.simple[bettor].clone(); + //let final_balance_complex = $scope.account_balances.complex[bettor].clone(); + //let delta_balance_simple = final_balance_simple.minus(original_balance_simple); + //let delta_balance_complex = final_balance_complex.minus(original_balance_complex); + //if (delta_balance_simple.has_nonzero_component() || delta_balance_complex.has_nonzero_component()) + // console.log(`${bettor} delta balances simple: ${JSON.stringify(delta_balance_simple)}, complex: ${JSON.stringify(delta_balance_complex)}`); // check global invariants for (var balance_system_name in $scope.account_balances) if ($scope.account_balances.hasOwnProperty(balance_system_name)) if ($scope.global_invariant_is_violated(balance_system_name)) { - Notification.error({message: `Random bet violated global invariant for ${balance_system_name}`, delay: 10000}); - $interval.cancel(random_bet_timer); + log_error(`Random bet violated global invariant for ${balance_system_name}`); + if (stop_on_error) + $interval.cancel(random_bet_timer); return; } @@ -719,8 +810,9 @@ for (var account_name in balance_system) if (balance_system.hasOwnProperty(account_name)) if ($scope.invariant_is_violated(balance_system_name, account_name)) { - Notification.error({message: `Random bet violated account invariant for ${account_name} in ${balance_system_name}`, delay: 10000}); - $interval.cancel(random_bet_timer); + log_error(`Random bet violated account invariant for ${account_name} in ${balance_system_name}`); + if (stop_on_error) + $interval.cancel(random_bet_timer); return; } } @@ -736,13 +828,30 @@ $scope.position_element_is_inconsistent(account_name, 'not_cancel') || $scope.position_element_is_inconsistent(account_name, 'refundable_unmatched_bets') || $scope.position_element_is_inconsistent(account_name, 'fees_paid')) { - Notification.error({message: `Random bet from ${bettor} exposed difference between simple and complex accounting system`, delay: 10000}); - $interval.cancel(random_bet_timer); + max_difference = big_max(max_difference, $scope.account_balances.complex[bettor].balance.minus($scope.account_balances.simple[bettor].balance)); + log_warning(`Random bet from ${bettor} exposed difference between simple and complex accounting system, max difference is ${max_difference.toFixed()}`); + if (stop_on_warning) + $interval.cancel(random_bet_timer); return; } + ++number_of_random_bets_placed; + if (number_of_random_bets_placed >= number_of_random_bets_to_place) + $interval.cancel(random_bet_timer); + $scope.clear_event_log(); }; - //random_bet_timer = $interval(place_random_bet, 150); + + // Enable this block to place bets without any display, stopping when there is an error + // for (let i = 0; i < 10000; ++i) { + // place_random_bet(); + // if (stop_on_error && error_count) + // break; + // if (stop_on_warning && warning_count) + // break; + // } + + // enable this to place random bets at a pace you can watch + // random_bet_timer = $interval(place_random_bet, 150); //$scope.place_bet(new Bet("alice", "back", 10, 10, 0)); diff --git a/libraries/app/database_api.cpp b/libraries/app/database_api.cpp index b5b9b989..56a60e36 100644 --- a/libraries/app/database_api.cpp +++ b/libraries/app/database_api.cpp @@ -424,6 +424,11 @@ dynamic_global_property_object database_api_impl::get_dynamic_global_properties( return _db.get(dynamic_global_property_id_type()); } +global_betting_statistics_object database_api::get_global_betting_statistics() const +{ + return my->get_global_betting_statistics(); +} + global_betting_statistics_object database_api_impl::get_global_betting_statistics() const { return _db.get(global_betting_statistics_id_type()); @@ -904,18 +909,33 @@ vector database_api_impl::list_sports() const return boost::copy_range >(sport_object_idx); } +vector database_api::list_event_groups(sport_id_type sport_id) const +{ + return my->list_event_groups(sport_id); +} + vector database_api_impl::list_event_groups(sport_id_type sport_id) const { const auto& event_group_idx = _db.get_index_type().indices().get(); return boost::copy_range >(event_group_idx.equal_range(sport_id)); } +vector database_api::list_betting_market_groups(event_id_type event_id) const +{ + return my->list_betting_market_groups(event_id); +} + vector database_api_impl::list_betting_market_groups(event_id_type event_id) const { const auto& betting_market_group_idx = _db.get_index_type().indices().get(); return boost::copy_range >(betting_market_group_idx.equal_range(event_id)); } +vector database_api::list_betting_markets(betting_market_group_id_type betting_market_group_id) const +{ +return my->list_betting_markets(betting_market_group_id); +} + vector database_api_impl::list_betting_markets(betting_market_group_id_type betting_market_group_id) const { const auto& betting_market_idx = _db.get_index_type().indices().get(); diff --git a/libraries/app/include/graphene/app/database_api.hpp b/libraries/app/include/graphene/app/database_api.hpp index a720cff7..5ac04c7c 100644 --- a/libraries/app/include/graphene/app/database_api.hpp +++ b/libraries/app/include/graphene/app/database_api.hpp @@ -639,6 +639,13 @@ FC_API(graphene::app::database_api, (list_assets) (lookup_asset_symbols) + // Peerplays + (get_global_betting_statistics) + (list_sports) + (list_event_groups) + (list_betting_market_groups) + (list_betting_markets) + // Markets / feeds (get_order_book) (get_limit_orders)