bettermail/portal/app/models/exchange_account.rb
2022-11-12 02:27:46 +01:00

219 lines
6.8 KiB
Ruby

# == 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