# == Schema Information # # Table name: exchange_accounts # # id :string not null, primary key # exchange_id :string not null # exchange_name :string not null # login_uid :string not null # login_pass :string not null # apikey :string not null # contact_name :string not null # contact_email :string not null # account_balance :float default(10000.0) # account_balance_last_checked :datetime # ssl_pem :text # allow_multiple_bets_per_event :boolean default(FALSE) # created_at :datetime not null # updated_at :datetime not null # log :jsonb # betting_enabled :boolean default(FALSE) # last_session_token :string # last_session_token_saved_at :datetime # status :string default("inactive"), not null # stake_strategy :jsonb # last_log_time :datetime # class ExchangeAccount < ApplicationRecord has_many :bets has_many :source_subscriptions, dependent: :delete_all has_many :tip_sources, through: :source_subscriptions has_many :subscription_runs, through: :source_subscriptions enum status: { active: 'active', inactive: 'inactive'} DEFAULT_STRATEGY = { max_bankroll_per_bet: 0.2, stake_sizing: 'MINIMUM', #[MINIMUM,FIXED, HYBRID, PROPORTIONAL] min_odds_to_bet: 1.1, max_odds_to_bet: 3.0, min_ev: 2, max_ev: 7, fixed_stake_size: 3.0, odds_margin: 0.1, kelly_multiplier: 0.3, }.freeze DEFAULT_HYBRID_STRATEGY = { max_bets: 10, action_on_max_losses: 'REBASE', #[REBASE] pause_in_minutes: 0, rebase_status: :complete, current_stake: 0.0, last_account_balance: nil, account_balance_checked_at: nil, max_losses: 0.1 }.freeze def json_payload { id: id, exchange_name: exchange_name, exchange_id: exchange_id, contact_name: contact_name, contact_email: contact_email, account_balance: account_balance, is_active: active?, can_bet: can_bet?, stake_strategy_name: stake_strategy_description, log: subscription_runs.empty? ? nil : last_run.log, last_log_time: subscription_runs.empty? ? updated_at : last_run.created_at, stake_strategy_config: current_stake_strategy, source_types: tip_sources.empty? ? 'None' : tip_sources.pluck(:source_type).join(' , ') } end def last_run subscription_runs&.latest end def self.mounted_account find_by_id(ENV['EXCHANGE_ACCOUNT']) end def self.mounted_account_payload return {} unless mounted_account mounted_account.json_payload end def optimal_stake(bet, exchange_minimum, stake_available) return exchange_minimum if minimum_stake? x = [stake_by_strategy(bet).round(1), stake_available].min [x, exchange_minimum].max end # # account_balance will ultimately come from accounts model def actual_account_balance return account_balance if betting_enabled simulated_account_balance end def simulated_account_balance account_balance + total_won - (total_lost + total_open) end def total_won(subset = my_bets) subset.won.sum(:expected_value).round(1) end def total_lost(subset = my_bets) subset.lost.sum(:stake).round(1) end def total_open(subset = my_bets) subset.open.sum(:stake).round(1) end def can_bet? return false unless betting_enabled actual_account_balance > total_open end def my_bets Bet.unscoped.where(exchange_account_id: id) end def current_stake_strategy if stake_strategy.blank? update(stake_strategy: DEFAULT_STRATEGY) end @current_stake_strategy ||= stake_strategy.with_indifferent_access end def minimum_stake? stake_strategy_description == 'MINIMUM' end def stake_strategy_description return current_stake_strategy[:stake_sizing] unless current_stake_strategy[:stake_sizing].blank? 'MINIMUM' end def stake_by_strategy(bet) return classic_kelly_stake_sizing(bet) if current_stake_strategy[:stake_sizing] == 'KELLY' return proportional_stake if current_stake_strategy[:stake_sizing] == 'PROPORTIONAL' return current_stake_strategy[:fixed_stake_size] if current_stake_strategy[:stake_sizing] == 'FIXED' return hybrid_stake_sizing if current_stake_strategy[:stake_sizing] == 'HYBRID' 0 end # def kelly_stake_sizing(bet) # proportional = proportional_stake # multiplier = current_stake_strategy[:kelly_multiplier] || 0.3 # kelly_factor = bet.tip_provider_percent * multiplier # stake = kelly_factor * proportional # # return 0 if stake.negative? # # [stake, proportional].min # end def classic_kelly_stake_sizing(bet) multiplier = current_stake_strategy[:kelly_multiplier] || 0.3 max_per_bet = proportional_stake kelly(odds: bet.executed_odds.to_f, probs: bet.tip_provider_percent, balance: actual_account_balance, multiplier: multiplier, per_bet_max: max_per_bet) end def kelly(odds:, probs:, balance: , multiplier:, per_bet_max:) kelly_stake_percent = (((odds * probs) -1) / (odds - 1 ) * multiplier)/100 stake = kelly_stake_percent * balance return 0 if stake.negative? [per_bet_max, stake].min end private def hybrid_stake_sizing hybrid_strategy = current_stake_strategy[:hybrid] || DEFAULT_HYBRID_STRATEGY.clone last_based_account_balance = hybrid_strategy[:last_account_balance] last_based_account_balance ||= rebase_hybrid_strategy max_losses = hybrid_strategy[:max_losses] max_allowable_losses = last_based_account_balance * max_losses difference = actual_account_balance - last_based_account_balance if difference.abs >= max_allowable_losses rebase_hybrid_strategy end current_stake_strategy[:hybrid][:current_stake] # raise "[Bet sizing] Betting on '#{id}' paused due to stake management rules" end def rebase_hybrid_strategy puts 'Rebasing hybrid stake because account balances have gained/loss at least the max' hybrid = current_stake_strategy[:hybrid] || DEFAULT_HYBRID_STRATEGY.clone(freeze: false) hybrid[:last_account_balance] = actual_account_balance hybrid[:current_stake] = ((actual_account_balance * hybrid[:max_losses]) / hybrid[:max_bets]).round(2) hybrid[:account_balance_checked_at] = Time.now current_stake_strategy[:hybrid] = hybrid update(stake_strategy: current_stake_strategy) hybrid[:current_stake] end def proportional_stake (current_stake_strategy[:max_bankroll_per_bet] * actual_account_balance) end end