init push - laying out the project

This commit is contained in:
Mike Sutton
2022-11-12 02:27:46 +01:00
commit 14e163a1a5
183 changed files with 20069 additions and 0 deletions

View File

@ -0,0 +1,15 @@
# == Schema Information
#
# Table name: accounts
#
# id :uuid not null, primary key
# name :string
# contact_id :uuid not null
# subdomain :string not null
# style :text
# size :bigint default(10000)
# created_at :datetime not null
# updated_at :datetime not null
#
class Account < ApplicationRecord
end

View File

@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end

82
portal/app/models/bet.rb Normal file
View File

@ -0,0 +1,82 @@
# == Schema Information
#
# Table name: bets
#
# id :uuid not null, primary key
# tip_provider_id :string not null
# tip_provider_bet_id :string not null
# tip_provider_percent :float not null
# exchange_id :string not null
# exchange_event_name :string not null
# exchange_event_id :string
# tip_provider_odds :string
# expected_value :float
# exchange_bet_id :string
# stake :float default(0.0), not null
# outcome :string default("processing"), not null
# created_at :datetime not null
# updated_at :datetime not null
# exchange_account_id :string
# tip_provider_event_id :string
# exchange_odds :string
# team1 :string not null
# team2 :string
# tip_provider_market_details :jsonb
# exchange_market_details :jsonb
# log :jsonb
# original_json :jsonb
# executed_odds :string
# placement_attempts :integer default(0)
# period :string
#
class Bet < ApplicationRecord
enum outcome: { processing: 'processing', won: 'won', lost: 'lost', open: 'open', expired: 'expired', skipped: 'skipped', ignored: 'ignored', errored: 'errored', voided: 'voided', cancelled: 'cancelled'}
scope :placed_bets, -> { where('outcome in (?)', %w[won lost open voided cancelled]) }
scope :retryable_bets, -> { where("outcome in ('expired', 'skipped')") }
default_scope { where(exchange_account_id: ENV['EXCHANGE_ACCOUNT']) }
belongs_to :exchange_account
include Loggable
def json_payload
{
id: id,
exchange: exchange_id,
exchange_event_name: exchange_event_name,
created_at: updated_at,
exchange_odds: exchange_odds,
tip_provider_odds: tip_provider_odds,
expected_value: expected_value&.round(1),
stake: stake,
outcome: outcome,
market_identifier: tip_provider_market_details.to_s,
exchange_market_details: exchange_market_details.to_s,
bet_placement_type: bet_placement_type,
executed_odds: executed_odds,
ev_percent: tip_provider_percent.round(2),
event_scheduled_time: original_json['started_at'] ? Time.at(original_json['started_at']) : "",
log: show_log
}
end
def self.calculate_expected_value(stake, odds)
(stake * odds) - stake
end
private
def placed_bet?
open? || lost? || won? || voided?
end
def bet_placement_type
if placed_bet?
return 'Simulated' unless exchange_bet_id
return "Placed [#{exchange_bet_id}]"
end
''
end
end

View File

@ -0,0 +1,5 @@
class BetfairEvent < ApplicationRecord
has_many :betfair_event_runners, dependent: :delete_all, primary_key: :event_id
enum status: { open: 'open', closed: 'closed' }
end

View File

@ -0,0 +1,6 @@
class BetfairEventRunner < ApplicationRecord
belongs_to :betfair_event, foreign_key: :event_id
has_many :betfair_runner_odds, dependent: :delete_all
scope :runners_for_open_events, -> { joins("INNER JOIN betfair_events ON betfair_event_runners.event_id = betfair_events.event_id").where(betfair_events: { status: 'open' }) }
end

View File

@ -0,0 +1,4 @@
class BetfairRunnerOdd < ApplicationRecord
belongs_to :betfair_event_runner
enum bet_type: {back: 'back', lay: 'lay'}
end

View File

@ -0,0 +1,6 @@
module Latest
extend ActiveSupport::Concern
included do
scope :latest, -> { order(created_at: :desc).first }
end
end

View File

@ -0,0 +1,218 @@
# == 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

View File

@ -0,0 +1,20 @@
module Loggable
extend ActiveSupport::Concern
def set_log(s)
log_this(s, false)
end
def log_this(s, append = true)
x = append ? log || [] : []
x << "[#{Time.now}]: #{s}"
update(log: x)
end
def show_log
x = log || []
return 'Nothing logged' if x.length.zero?
x.join(',')
end
end

View File

@ -0,0 +1,17 @@
# == Schema Information
#
# Table name: source_subscriptions
#
# id :uuid not null, primary key
# exchange_account_id :string
# tip_source_id :uuid
# created_at :datetime not null
# updated_at :datetime not null
#
class SourceSubscription < ApplicationRecord
belongs_to :tip_source, dependent: :delete
belongs_to :exchange_account
has_many :subscription_runs
has_many :tip_source_data, through: :tip_source
end

View File

@ -0,0 +1,17 @@
# == Schema Information
#
# Table name: subscription_runs
#
# id :uuid not null, primary key
# source_subscription_id :uuid
# tip_source_data_id :uuid
# log :jsonb
# created_at :datetime not null
# updated_at :datetime not null
#
class SubscriptionRun < ApplicationRecord
include Loggable
include Latest
belongs_to :tip_source_data
belongs_to :source_subscription
end

View File

@ -0,0 +1,22 @@
# == Schema Information
#
# Table name: tip_sources
#
# id :uuid not null, primary key
# tipster_account_id :string
# description :string
# source_type :string
# active :boolean default(FALSE)
# filters :jsonb
# created_at :datetime not null
# updated_at :datetime not null
#
class TipSource < ApplicationRecord
belongs_to :tipster_account, dependent: :delete
has_many :source_subscriptions, dependent: :delete_all
has_many :tip_source_data, class_name: 'TipSourceData'
has_many :subscription_runs, through: :source_subscriptions
scope :active_sources, -> { where(active: true)}
end

View File

@ -0,0 +1,15 @@
# == Schema Information
#
# Table name: tip_source_data
#
# id :uuid not null, primary key
# tip_source_id :uuid
# data :jsonb
# created_at :datetime not null
# updated_at :datetime not null
#
class TipSourceData < ApplicationRecord
include Latest
belongs_to :tip_source, dependent: :delete
has_many :source_subscriptions
end

View File

@ -0,0 +1,18 @@
# == Schema Information
#
# Table name: tipster_accounts
#
# id :string not null, primary key
# tipster_name :string not null
# apikey :string not null
# filter_ids :string not null
# contact_name :string not null
# contact_email :string not null
# created_at :datetime not null
# updated_at :datetime not null
#
class TipsterAccount < ApplicationRecord
has_many :tip_sources
has_many :tip_source_data, through: :tip_sources, class_name: 'TipSourceData'
has_many :tip_source_data_processing_runs, through: :tip_source_data
end

82
portal/app/models/user.rb Normal file
View File

@ -0,0 +1,82 @@
# == Schema Information
#
# Table name: users
#
# id :uuid not null, primary key
# first_name :string
# last_name :string
# email :string default(""), not null
# encrypted_password :string default(""), not null
# reset_password_token :string
# reset_password_sent_at :datetime
# remember_created_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# uid :string
# provider :string
#
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and
devise :database_authenticatable, :registerable, :omniauthable
def name
"#{first_name} #{last_name}"
end
def self.find_or_create_from_auth_hash(auth)
user = where(email: auth.info.email).first
unless user.present?
user = where(provider: auth.provider, uid: auth.uid).first_or_initialize
user.email = auth.info.email
user.password = Devise.friendly_token.first(8)
end
unless user.provider.present?
user.provider = auth.provider
user.uid = auth.uid
end
user.first_name = auth.info.first_name
user.last_name = auth.info.last_name
user.save!
user
end
def fullname
"#{first_name} #{last_name}"
end
def confirmation_token
if read_attribute(:confirmation_token).nil?
self.confirmation_token = generate_confirmation_token
save!
end
read_attribute(:confirmation_token)
end
def json_payload
{
id: id,
email: email,
first_name: first_name,
last_name: last_name,
}
end
def admin?
%w[mike@wizewerx.com].include?(email)
end
private
def generate_confirmation_token
token = Devise.friendly_token(10)
token = Devise.friendly_token(10) while User.where(confirmation_token: token).count.positive?
self.confirmation_token = token
end
end