219 lines
6.8 KiB
Ruby
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
|