init push - laying out the project
This commit is contained in:
2
portal/app/assets/config/manifest.js
Normal file
2
portal/app/assets/config/manifest.js
Normal file
@ -0,0 +1,2 @@
|
||||
//= link_tree ../images
|
||||
//= link_directory ../stylesheets .css
|
0
portal/app/assets/images/.keep
Normal file
0
portal/app/assets/images/.keep
Normal file
BIN
portal/app/assets/images/favicon.png
Normal file
BIN
portal/app/assets/images/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
BIN
portal/app/assets/images/logo-white.png
Normal file
BIN
portal/app/assets/images/logo-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
portal/app/assets/images/logo.png
Normal file
BIN
portal/app/assets/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
15
portal/app/assets/stylesheets/application.css
Normal file
15
portal/app/assets/stylesheets/application.css
Normal file
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
||||
* listed below.
|
||||
*
|
||||
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
|
||||
* vendor/assets/stylesheets directory can be referenced here using a relative path.
|
||||
*
|
||||
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
||||
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
||||
* files in this directory. Styles in this file should be added after the last require_* statement.
|
||||
* It is generally better to create a new file per style scope.
|
||||
*
|
||||
*= require_tree .
|
||||
*= require_self
|
||||
*/
|
4
portal/app/channels/application_cable/channel.rb
Normal file
4
portal/app/channels/application_cable/channel.rb
Normal file
@ -0,0 +1,4 @@
|
||||
module ApplicationCable
|
||||
class Channel < ActionCable::Channel::Base
|
||||
end
|
||||
end
|
4
portal/app/channels/application_cable/connection.rb
Normal file
4
portal/app/channels/application_cable/connection.rb
Normal file
@ -0,0 +1,4 @@
|
||||
module ApplicationCable
|
||||
class Connection < ActionCable::Connection::Base
|
||||
end
|
||||
end
|
9
portal/app/channels/exchange_account_channel.rb
Normal file
9
portal/app/channels/exchange_account_channel.rb
Normal file
@ -0,0 +1,9 @@
|
||||
class ExchangeAccountChannel < ApplicationCable::Channel
|
||||
def subscribed
|
||||
stream_from "exchange_account:#{params[:id]}"
|
||||
end
|
||||
|
||||
def unsubscribed
|
||||
# Any cleanup needed when channel is unsubscribed
|
||||
end
|
||||
end
|
@ -0,0 +1,3 @@
|
||||
class Api::AuthenticatedApiController < ActionController::Base
|
||||
before_action :authenticate_user!
|
||||
end
|
11
portal/app/controllers/api/v1/account_controller.rb
Normal file
11
portal/app/controllers/api/v1/account_controller.rb
Normal file
@ -0,0 +1,11 @@
|
||||
class Api::V1::AccountController < Api::AuthenticatedApiController
|
||||
def create
|
||||
render json: { success: true} and return
|
||||
end
|
||||
|
||||
# def is_domain_valid
|
||||
# valid = !Helpbuild::Ecosystem.reserved_account_domains.include?(params[:domain])
|
||||
# valid &= /[A-Za-z0-9-]{1,63}/.match(params[:domain]).present?
|
||||
# render json: { success: valid} and return
|
||||
# end
|
||||
end
|
7
portal/app/controllers/api/v1/app_controller.rb
Normal file
7
portal/app/controllers/api/v1/app_controller.rb
Normal file
@ -0,0 +1,7 @@
|
||||
class Api::V1::AppController < Api::AuthenticatedApiController
|
||||
skip_before_action :authenticate_user!, only: [:configuration]
|
||||
def configuration
|
||||
rtn = {exchange_accounts: ExchangeAccount.pluck(:id).sort}
|
||||
render json: { success: true, config: rtn} and return
|
||||
end
|
||||
end
|
104
portal/app/controllers/api/v1/bets_controller.rb
Normal file
104
portal/app/controllers/api/v1/bets_controller.rb
Normal file
@ -0,0 +1,104 @@
|
||||
class Api::V1::BetsController < Api::AuthenticatedApiController
|
||||
include ApplicationHelper
|
||||
include Pagy::Backend
|
||||
skip_before_action :authenticate_user!, only: %i[add_placed_bet tips]
|
||||
skip_before_action :verify_authenticity_token, only: %i[add_placed_bet tips]
|
||||
|
||||
def index
|
||||
exchange_account = params[:exchange_account_id].blank? ? ExchangeAccount.mounted_account : ExchangeAccount.find(params[:exchange_account_id])
|
||||
|
||||
bets = filter_by_date_range(exchange_account.my_bets, 'created_at')
|
||||
|
||||
bets = bets.where(outcome: params[:outcome]) unless params[:outcome].blank?
|
||||
bets = bets.where("exchange_event_name ILIKE '%%#{params[:event_name]}%%'") unless params[:event_name].blank?
|
||||
|
||||
ev_range = params[:expected_value]
|
||||
if ev_range.present?
|
||||
v_symbolize = JSON.parse(ev_range).symbolize_keys
|
||||
bets = bets.where("expected_value >= #{v_symbolize[:min]} AND expected_value <= #{v_symbolize[:max]}")
|
||||
end
|
||||
# add additional filters later
|
||||
#
|
||||
summary = summary_dashboard(exchange_account, bets)
|
||||
|
||||
pagy, bets = pagy(bets)
|
||||
|
||||
pagy_json = pagy_metadata(pagy)
|
||||
|
||||
|
||||
render json: { summary: summary, pagy: pagy_json, bets: bets.order(created_at: :desc).map(&:json_payload) }
|
||||
end
|
||||
|
||||
def add_placed_bet
|
||||
# push a placed bet from an authenticated client
|
||||
wpb = 'WEB_PLACED_BETS'
|
||||
|
||||
ex = ExchangeAccount.find_by(id: wpb)
|
||||
stake = (params['stake'] || 0).to_f
|
||||
executed_odds = (params['odds_accepted'] || 0).to_f
|
||||
record = {
|
||||
tip_provider_id: 'betburger',
|
||||
tip_provider_bet_id: params['betburger_bet_id'],
|
||||
tip_provider_percent: (params['value_bet_ev_percent'] || 0).to_f,
|
||||
tip_provider_odds: (params['original_odds'] || 0).to_f,
|
||||
team1: "Not Set",
|
||||
team2: "Not Set",
|
||||
exchange_id: params['bookmaker'],
|
||||
exchange_event_name: params['match'],
|
||||
exchange_market_details: {market: params['market'], selection: params['selection']},
|
||||
stake: stake,
|
||||
executed_odds: executed_odds,
|
||||
expected_value: Bet.calculate_expected_value(stake, executed_odds),
|
||||
original_json: params.to_json,
|
||||
outcome: {'Bet placed': 'open', 'Odds changed': 'expired'}[params['placer_result']] || 'skipped',
|
||||
log: [params['placer_result']],
|
||||
}
|
||||
ex.my_bets.create(record)
|
||||
render json: { success: true }
|
||||
end
|
||||
|
||||
def tips
|
||||
id = params[:id]
|
||||
resp = { success: false }
|
||||
ts = id.nil? ? nil : TipSource.find_by(id: id)
|
||||
if ts
|
||||
resp[:success] = true
|
||||
latest_source_data = ts.tip_source_data.latest
|
||||
resp[:data] = latest_source_data.blank? ? {} : latest_source_data.data
|
||||
else
|
||||
resp[:message] = 'Unknown tip source requested'
|
||||
end
|
||||
render json: resp
|
||||
end
|
||||
|
||||
def summary_dashboard(exchange_account, bets)
|
||||
{
|
||||
exchange_account: exchange_account.json_payload,
|
||||
total_win_amount: exchange_account.total_won(bets),
|
||||
total_lost_amount: exchange_account.total_lost(bets),
|
||||
total_risked_amount: exchange_account.total_open(bets),
|
||||
total_tips: bets.count,
|
||||
total_tips_skipped: bets.skipped.count,
|
||||
total_tips_processing: bets.processing.count,
|
||||
total_tips_expired: bets.expired.count,
|
||||
total_tips_ignored: bets.ignored.count,
|
||||
total_tips_voided: bets.voided.count,
|
||||
total_placed_bets: bets.placed_bets.count,
|
||||
total_placed_bets_won: bets.won.count,
|
||||
total_placed_bets_lost: bets.lost.count,
|
||||
total_placed_bets_open: bets.open.count,
|
||||
average_odds_won: odd_averages(bets.won),
|
||||
average_odds_lost: odd_averages(bets.lost),
|
||||
average_odds: odd_averages(bets),
|
||||
running_since: exchange_account.my_bets.minimum(:created_at) || exchange_account.created_at,
|
||||
}
|
||||
end
|
||||
|
||||
def odd_averages(bets)
|
||||
return 0 if bets.count.zero?
|
||||
|
||||
total_odds = 0
|
||||
bets.map { |b| total_odds += b.tip_provider_odds.to_f }
|
||||
(total_odds / bets.count).round(2)
|
||||
end
|
||||
end
|
@ -0,0 +1,14 @@
|
||||
class Api::V1::ExchangeAccountsController < Api::AuthenticatedApiController
|
||||
def update
|
||||
exchange_account = ExchangeAccount.find(params[:exchange_account_id])
|
||||
render json: { success: false} and return unless exchange_account
|
||||
|
||||
payload = {}
|
||||
payload[:betting_enabled] = params['canBet'] unless params['canBet'].nil?
|
||||
payload[:status] = params['isActive'] == true ? 'active' : 'inactive' unless params['isActive'].nil?
|
||||
|
||||
exchange_account.update(payload) unless payload.empty?
|
||||
render json: { success: true} and return
|
||||
end
|
||||
|
||||
end
|
13
portal/app/controllers/api/v1/users_controller.rb
Normal file
13
portal/app/controllers/api/v1/users_controller.rb
Normal file
@ -0,0 +1,13 @@
|
||||
class Api::V1::UsersController < Api::AuthenticatedApiController
|
||||
skip_before_action :authenticate_user!, only: [ :auth]
|
||||
|
||||
def auth
|
||||
unless user_signed_in?
|
||||
render json: { success: false }
|
||||
return
|
||||
end
|
||||
|
||||
render json: { success: true, user: current_user.json_payload}
|
||||
end
|
||||
|
||||
end
|
2
portal/app/controllers/application_controller.rb
Normal file
2
portal/app/controllers/application_controller.rb
Normal file
@ -0,0 +1,2 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
end
|
4
portal/app/controllers/pages_controller.rb
Normal file
4
portal/app/controllers/pages_controller.rb
Normal file
@ -0,0 +1,4 @@
|
||||
class PagesController < ApplicationController
|
||||
def index
|
||||
end
|
||||
end
|
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Users
|
||||
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||
def keycloakopenid
|
||||
Rails.logger.debug(request.env['omniauth.auth'])
|
||||
@user = User.find_or_create_from_auth_hash(request.env['omniauth.auth'])
|
||||
sign_in_and_redirect @user, event: :authentication
|
||||
end
|
||||
|
||||
def failure
|
||||
redirect_to root_path
|
||||
end
|
||||
end
|
||||
end
|
25
portal/app/helpers/application_helper.rb
Normal file
25
portal/app/helpers/application_helper.rb
Normal file
@ -0,0 +1,25 @@
|
||||
module ApplicationHelper
|
||||
def render_activities(_activities)
|
||||
'Activities'
|
||||
end
|
||||
|
||||
def filter_by_date_range(records, field_string)
|
||||
return records if field_string.blank?
|
||||
|
||||
field = params[field_string.to_sym]
|
||||
|
||||
return records unless field
|
||||
|
||||
ea_symbolize = JSON.parse(field).symbolize_keys
|
||||
|
||||
parsed_date_from = nil
|
||||
parsed_date_to = nil
|
||||
|
||||
parsed_date_from = DateTime.parse(ea_symbolize[:from]) unless ea_symbolize[:from].nil?
|
||||
parsed_date_to = DateTime.parse(ea_symbolize[:to]) unless ea_symbolize[:to].nil?
|
||||
|
||||
records = records.where("#{field_string} >= ? AND #{field_string} <= ?", parsed_date_from, parsed_date_to + 1.day) unless parsed_date_from.nil?
|
||||
|
||||
records
|
||||
end
|
||||
end
|
11
portal/app/helpers/mailer_style_helper.rb
Normal file
11
portal/app/helpers/mailer_style_helper.rb
Normal file
@ -0,0 +1,11 @@
|
||||
module MailerStyleHelper
|
||||
def mail_logo_style
|
||||
"background: #222C36;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 10px 10px 0 0"
|
||||
end
|
||||
end
|
||||
# reload!; ProjectMailer.notify_helper_for_join_request(ProjectHelper.first).deliver_now
|
14
portal/app/jobs/account_sync_and_reconciliation_job.rb
Normal file
14
portal/app/jobs/account_sync_and_reconciliation_job.rb
Normal file
@ -0,0 +1,14 @@
|
||||
class AccountSyncAndReconciliationJob < ApplicationJob
|
||||
queue_as :high
|
||||
|
||||
def perform(args = {})
|
||||
ExchangeAccount.active.each do | ea|
|
||||
am = Integrations::Betfair::AccountManager.new(ea)
|
||||
bm = Integrations::Betfair::BetManager.new(ea)
|
||||
puts "Refreshing account balance on '#{ea.id}'"
|
||||
am.refresh_account_balance
|
||||
puts "Reconcile open bets on '#{ea.id}'"
|
||||
bm.check_qualified_bet_outcome(ea.my_bets.open)
|
||||
end
|
||||
end
|
||||
end
|
8
portal/app/jobs/application_job.rb
Normal file
8
portal/app/jobs/application_job.rb
Normal file
@ -0,0 +1,8 @@
|
||||
class ApplicationJob < ActiveJob::Base
|
||||
sidekiq_options retry: false
|
||||
# Automatically retry jobs that encountered a deadlock
|
||||
# retry_on ActiveRecord::Deadlocked
|
||||
|
||||
# Most jobs are safe to ignore if the underlying records are no longer available
|
||||
# discard_on ActiveJob::DeserializationError
|
||||
end
|
63
portal/app/jobs/bet_placement_service.rb
Normal file
63
portal/app/jobs/bet_placement_service.rb
Normal file
@ -0,0 +1,63 @@
|
||||
class BetPlacementService < ApplicationJob
|
||||
queue_as :high
|
||||
|
||||
def perform(args = {})
|
||||
# do not proceed if we have already procssed a bet for this tip
|
||||
bet = args[:bet]
|
||||
# get a valid betfair instance for the account we are using, halt if not valid.
|
||||
exchange = Integrations::Betfair::BetManager.new(bet.exchange_account)
|
||||
return unless exchange
|
||||
|
||||
last_step = 'Checking event id'
|
||||
begin
|
||||
# check this bet with the exchange and populate the event id if it is missing
|
||||
|
||||
unless bet.exchange_event_id
|
||||
bet.exchange_event_id = exchange.bet_event(bet)
|
||||
end
|
||||
bet.placement_attempts += 1
|
||||
|
||||
# check the odds are still good at the exchange for that event
|
||||
last_step = 'Checking odds'
|
||||
prices_and_stakes = exchange.bet_odds(bet)
|
||||
prices = prices_and_stakes[:prices]
|
||||
|
||||
bet.exchange_odds = prices_and_stakes
|
||||
bet.outcome = 'expired'
|
||||
tip_odds = bet.tip_provider_odds.to_f
|
||||
|
||||
# use tip odds or get closest match to tip odds offered by the exchange
|
||||
last_step = 'Determining if odds optimal'
|
||||
margin = tip_odds * bet.exchange_account.current_stake_strategy[:odds_margin]
|
||||
max_tip_odds = tip_odds + margin
|
||||
odds_to_execute_at = prices.min_by { |num| (max_tip_odds - num).abs }
|
||||
raise 'Cannot determine optimal odds' unless odds_to_execute_at
|
||||
if odds_to_execute_at.between?(tip_odds, tip_odds + margin)
|
||||
last_step = 'Determining stake and placing bet'
|
||||
odds_stake = prices_and_stakes[:stakes][odds_to_execute_at.to_s]
|
||||
|
||||
bet.executed_odds = odds_to_execute_at.to_s
|
||||
# stake will always fill the max available or the limits of our stake management - whichever is the lower.
|
||||
stake = bet.exchange_account.optimal_stake(bet, exchange.minimum_stake, odds_stake)
|
||||
|
||||
raise 'Optimal stake is zero. Not taking bet' if stake.zero?
|
||||
|
||||
bet.outcome = 'open'
|
||||
bet.stake = stake
|
||||
bet.expected_value = Bet.calculate_expected_value(stake, odds_to_execute_at)
|
||||
if bet.exchange_account.can_bet?
|
||||
bet.exchange_bet_id = exchange.place_bet(bet, stake)
|
||||
end
|
||||
else
|
||||
bet.log << '[Bet placement] Tip odds not found in latest prices, nudge too far out'
|
||||
end
|
||||
rescue Exception => e
|
||||
x = "Placement #{bet.placement_attempts}--->Last step: #{last_step}. Error: #{e.message}. Skipping"
|
||||
bet.log ||= []
|
||||
bet.log << x
|
||||
bet.outcome = e.message.include?('PERMISSION_DENIED') ? 'errored' : 'skipped'
|
||||
ensure
|
||||
bet.save!
|
||||
end
|
||||
end
|
||||
end
|
8
portal/app/jobs/clear_old_pulls_job.rb
Normal file
8
portal/app/jobs/clear_old_pulls_job.rb
Normal file
@ -0,0 +1,8 @@
|
||||
class ClearOldPullsJob < ApplicationJob
|
||||
queue_as :high
|
||||
|
||||
def perform(args = {})
|
||||
max_hours_to_keep = ENV['MAX_HISTORY_HOURS']&.to_i || 2
|
||||
TipSourceData.where("created_at < ?", max_hours_to_keep.hours.ago).delete_all
|
||||
end
|
||||
end
|
10
portal/app/jobs/process_subscription_job.rb
Normal file
10
portal/app/jobs/process_subscription_job.rb
Normal file
@ -0,0 +1,10 @@
|
||||
class ProcessSubscriptionJob < ApplicationJob
|
||||
queue_as :high
|
||||
|
||||
def perform(args = {})
|
||||
subscription = args[:subscription]
|
||||
tsd = args[:tsd]
|
||||
bb = Integrations::Betburger.new()
|
||||
bb.process_subscription_tips(subscription: subscription, data: tsd)
|
||||
end
|
||||
end
|
11
portal/app/jobs/pull_event_markets_job.rb
Normal file
11
portal/app/jobs/pull_event_markets_job.rb
Normal file
@ -0,0 +1,11 @@
|
||||
class PullEventMarketsJob < ApplicationJob
|
||||
queue_as :medium
|
||||
|
||||
def perform(args = {})
|
||||
ea = ExchangeAccount.find_by(id: ENV['BETFAIR_HUNTER_ACCOUNT'])
|
||||
raise 'No Betfair hunter account' unless ea
|
||||
|
||||
hunter = Integrations::Betfair::OpportunityHunter.new(ea)
|
||||
hunter.event_markets_and_selections #refresh the markets to include
|
||||
end
|
||||
end
|
13
portal/app/jobs/pull_latest_odds_prices_job.rb
Normal file
13
portal/app/jobs/pull_latest_odds_prices_job.rb
Normal file
@ -0,0 +1,13 @@
|
||||
class PullLatestOddsPricesJob < ApplicationJob
|
||||
queue_as :medium
|
||||
|
||||
def perform(args = {})
|
||||
ea = ExchangeAccount.find_by(id: ENV['BETFAIR_HUNTER_ACCOUNT'])
|
||||
raise 'No Betfair hunter account' unless ea
|
||||
|
||||
hunter = Integrations::Betfair::OpportunityHunter.new(ea)
|
||||
BetfairEventRunner.runners_for_open_events.each do | runner |
|
||||
PullRunnerOddsJob.perform_later(hunter: hunter, runner: runner)
|
||||
end
|
||||
end
|
||||
end
|
9
portal/app/jobs/pull_runner_odds_job.rb
Normal file
9
portal/app/jobs/pull_runner_odds_job.rb
Normal file
@ -0,0 +1,9 @@
|
||||
class PullRunnerOddsJob < ApplicationJob
|
||||
queue_as :medium
|
||||
|
||||
def perform(args = {})
|
||||
hunter = params[:oh]
|
||||
runner = params[:runner]
|
||||
hunter.runner_odds(runner)
|
||||
end
|
||||
end
|
16
portal/app/jobs/pull_tips_job.rb
Normal file
16
portal/app/jobs/pull_tips_job.rb
Normal file
@ -0,0 +1,16 @@
|
||||
class PullTipsJob < ApplicationJob
|
||||
queue_as :high
|
||||
|
||||
def perform(args = {})
|
||||
ta = TipsterAccount.find_by(id: ENV['TIPSTER_ACCOUNT'])
|
||||
bb = Integrations::Betburger.new(ta)
|
||||
ta.tip_sources.active_sources.each do |ts|
|
||||
tsd = bb.pull_and_save_tips(ts)
|
||||
ts.source_subscriptions.each do |ss|
|
||||
next unless ss.exchange_account.active?
|
||||
|
||||
ProcessSubscriptionJob.perform_later(subscription: ss, tsd: tsd)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
13
portal/app/jobs/pull_upcoming_events_job.rb
Normal file
13
portal/app/jobs/pull_upcoming_events_job.rb
Normal file
@ -0,0 +1,13 @@
|
||||
class PullUpcomingEventsJob < ApplicationJob
|
||||
queue_as :medium
|
||||
|
||||
def perform(args = {})
|
||||
ea = ExchangeAccount.find_by(id: ENV['BETFAIR_HUNTER_ACCOUNT'])
|
||||
raise 'No Betfair hunter account' unless ea
|
||||
|
||||
hunter = Integrations::Betfair::OpportunityHunter.new(ea)
|
||||
top_of_hour = Time.now.beginning_of_hour
|
||||
hunter.events_in_timeframe(from: top_of_hour, to: top_of_hour + 1.hour) #pull events
|
||||
hunter.event_markets_and_selections #refresh the markets to include
|
||||
end
|
||||
end
|
6
portal/app/lib/general_helper.rb
Normal file
6
portal/app/lib/general_helper.rb
Normal file
@ -0,0 +1,6 @@
|
||||
class GeneralHelper
|
||||
|
||||
def self.broadcast_to_account_channel(account_id, data)
|
||||
ExchangeAccountChannel.broadcast_to(account_id, data)
|
||||
end
|
||||
end
|
940
portal/app/lib/integrations/betburger.rb
Normal file
940
portal/app/lib/integrations/betburger.rb
Normal file
@ -0,0 +1,940 @@
|
||||
module Integrations
|
||||
class Betburger
|
||||
include HTTParty
|
||||
attr_reader :access_token
|
||||
|
||||
URIS = {
|
||||
live: 'https://rest-api-lv.betburger.com/api/v1/valuebets/bot_pro_search',
|
||||
prematch: 'https://rest-api-pr.betburger.com/api/v1/valuebets/bot_pro_search',
|
||||
}.freeze.with_indifferent_access
|
||||
|
||||
def initialize(tipster_account=nil)
|
||||
super()
|
||||
tipster_account ||= TipsterAccount.find_by(id: ENV['TIPSTER_ACCOUNT'])
|
||||
@access_token = tipster_account.apikey
|
||||
end
|
||||
|
||||
def pull_and_save_tips(source)
|
||||
url = URIS[source.source_type]
|
||||
return unless url
|
||||
|
||||
response = self.class.post(url, { body: { per_page: 32, search_filter: source.filters, access_token: @access_token } })
|
||||
source.tip_source_data.create(data: response)
|
||||
end
|
||||
|
||||
def process_subscription_tips(subscription:, data:,place_bets: true)
|
||||
|
||||
return unless subscription
|
||||
|
||||
tsd = data || subscription.tip_source_data.last
|
||||
|
||||
# tips already processed
|
||||
return if subscription.subscription_runs.where(tip_source_data_id: tsd.id).exists?
|
||||
|
||||
json = tsd.data
|
||||
return unless json['total_by_filter'].to_i.positive?
|
||||
|
||||
percentages = {}
|
||||
vb_refs = json['source']['value_bets']
|
||||
vb_refs.each do |vb_ref|
|
||||
percentages[vb_ref['bet_id']] = { percent: vb_ref['percent'] }
|
||||
end
|
||||
bet_refs = json['bets']
|
||||
stats = { non_valuebet: 0, unsupported_exchange: 0, duplicate: 0, processed: 0 }
|
||||
bet_refs.each do |bf|
|
||||
place_this_bet = place_bets
|
||||
unless bf['is_value_bet'] == true
|
||||
stats[:non_valuebet] += 1
|
||||
next
|
||||
end
|
||||
|
||||
exchange = exchanges[(bf['bookmaker_id']).to_s]
|
||||
unless exchange
|
||||
stats[:unsupported_exchange] += 1
|
||||
next
|
||||
end
|
||||
|
||||
tipster_event_id = bf['event_id']
|
||||
|
||||
log = []
|
||||
bet_id = bf['id']
|
||||
|
||||
vb = populate_bet_hash(subscription, bet_id, bf, exchange, log, percentages, tipster_event_id)
|
||||
# on live betting, strictly adhere to max_odds rule. Otherwise bet on all odds to generate learning data.
|
||||
odds_within_range = vb[:tip_provider_odds].to_f.between?( subscription.exchange_account.current_stake_strategy[:min_odds_to_bet], subscription.exchange_account.current_stake_strategy[:max_odds_to_bet])
|
||||
ev_within_range = vb[:tip_provider_percent].to_f.between?( subscription.exchange_account.current_stake_strategy[:min_ev], subscription.exchange_account.current_stake_strategy[:max_ev])
|
||||
unless odds_within_range && ev_within_range
|
||||
log << "[Parsing] Tip provider odds not within #{subscription.exchange_account.current_stake_strategy[:min_odds_to_bet]} and #{subscription.exchange_account.current_stake_strategy[:max_odds_to_bet]}" unless odds_within_range
|
||||
log << "[Parsing] Tip provider EV not within #{subscription.exchange_account.current_stake_strategy[:min_ev]} and #{subscription.exchange_account.current_stake_strategy[:max_ev]}" unless ev_within_range
|
||||
vb[:outcome] = 'ignored'
|
||||
place_this_bet = false
|
||||
end
|
||||
|
||||
vb[:log] = log
|
||||
skip_duplicate_event_check = false
|
||||
duplicates = subscription.exchange_account.my_bets.where(tip_provider_bet_id: bet_id)
|
||||
if duplicates.exists?
|
||||
stats[:duplicate] += 1
|
||||
if duplicates.retryable_bets.count.positive?
|
||||
bet = duplicates.retryable_bets.last #only supporting 1 duplicate!
|
||||
bet.log = []
|
||||
bet.update(vb)
|
||||
skip_duplicate_event_check = true
|
||||
else
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
unless skip_duplicate_event_check
|
||||
next if subscription.exchange_account.my_bets.where(tip_provider_event_id: tipster_event_id).exists? && !subscription.exchange_account.allow_multiple_bets_per_event
|
||||
end
|
||||
|
||||
begin
|
||||
bet ||= Bet.unscoped.create(vb)
|
||||
|
||||
if bet && place_this_bet && vb[:exchange_market_details]
|
||||
BetPlacementService.perform_later(bet: bet)
|
||||
end
|
||||
rescue Exception => e
|
||||
puts "Something weird happened: #{e.message}"
|
||||
end
|
||||
stats[:processed] += 1
|
||||
end
|
||||
subscription.subscription_runs.create(tip_source_data_id: tsd.id, log: ["Payload: #{bet_refs.size}. #{stats}"])
|
||||
end
|
||||
|
||||
def populate_bet_hash(subscription, bet_id, bf, exchange, log, percentages, tipster_event_id)
|
||||
vb = { tip_provider_id: 'betburger', tip_provider_bet_id: bet_id }
|
||||
vb[:exchange_id] = exchange
|
||||
vb[:exchange_account_id] = subscription.exchange_account.id
|
||||
vb[:tip_provider_event_id] = tipster_event_id
|
||||
vb[:tip_provider_odds] = bf['koef'] # what the tip provider says the odds are from the exchange
|
||||
vb[:exchange_event_id] = bf['bookmaker_event_direct_link'] # may be blank
|
||||
vb[:team1] = bf['team1_name']
|
||||
vb[:team2] = bf['team2_name']
|
||||
vb[:period] = periods[bf['period_id'].to_s.to_sym] unless bf['period_id'].blank?
|
||||
vb[:exchange_event_name] = vb[:team1]
|
||||
vb[:exchange_event_name] += " v #{vb[:team2]}" unless vb[:team2].blank?
|
||||
vb[:exchange_event_id] = bf['bookmaker_event_direct_link'] # may be blank
|
||||
vb[:tip_provider_percent] = percentages[bet_id][:percent] # dunno what this is used for
|
||||
tip_market_details = {}
|
||||
tip_market_details[:market] = variations[bf['market_and_bet_type']&.to_s]
|
||||
tip_market_details[:params] = bf['market_and_bet_type_param']&.to_s
|
||||
vb[:tip_provider_market_details] = tip_market_details
|
||||
exchange_market_details = map_exchange_market_details(exchange, tip_market_details, vb[:team1], vb[:team2])
|
||||
vb[:original_json] = bf
|
||||
if exchange_market_details
|
||||
vb[:exchange_market_details] = exchange_market_details
|
||||
else
|
||||
vb[:outcome] = 'ignored'
|
||||
log << '[Parsing] Unsupported market/selection'
|
||||
end
|
||||
vb
|
||||
end
|
||||
|
||||
def exchanges
|
||||
@exchanges ||= {
|
||||
'11' => 'betfair'
|
||||
}
|
||||
end
|
||||
|
||||
def map_exchange_market_details(exchange, tip_market_details, team1, team2)
|
||||
market_mapping = mapped_markets(exchange)
|
||||
return unless market_mapping
|
||||
|
||||
rtn = market_mapping[tip_market_details[:market]]&.clone
|
||||
return unless rtn
|
||||
|
||||
swaps = { 'Team1' => team1, 'Team2' => team2, '%s' => tip_market_details[:params]}
|
||||
if tip_market_details[:params]
|
||||
rounded = tip_market_details[:params].to_f.round
|
||||
swaps['%rounded_s'] = "#{rounded.positive? ? '+':''}#{rounded}" if tip_market_details[:params]
|
||||
swaps['%rounded_positive_s'] = "#{rounded.abs}"
|
||||
swaps['%nearest_half_over_s'] = determine_nearest_half(tip_market_details[:params], 'over') if tip_market_details[:params]
|
||||
swaps['%nearest_half_under_s'] = determine_nearest_half(tip_market_details[:params], 'under') if tip_market_details[:params]
|
||||
end
|
||||
|
||||
swaps.keys.each do |k|
|
||||
rtn[:market] = rtn[:market].sub(k, swaps[k])
|
||||
rtn[:selection] = rtn[:selection].sub(k, swaps[k])
|
||||
end
|
||||
if rtn.key?(:handicap)
|
||||
rtn[:handicap] = tip_market_details[:params].to_f
|
||||
end
|
||||
rtn
|
||||
end
|
||||
|
||||
def determine_nearest_half(line, direction )
|
||||
line_f = line.to_f - line.to_i
|
||||
return line if line_f == 0.5
|
||||
|
||||
line_f = line.to_i + 0.5
|
||||
line_f -= 1 if direction == 'over'
|
||||
line_f += 1 if direction == 'under'
|
||||
|
||||
line_f.to_s
|
||||
end
|
||||
|
||||
|
||||
def mapped_markets(key)
|
||||
# this is ready for other exchanges
|
||||
@mapped_markets ||= { 'betfair' => betfair_markets }
|
||||
@mapped_markets[key]
|
||||
end
|
||||
|
||||
def betfair_markets
|
||||
{
|
||||
'Team1 Win' => { market: 'Match Odds', selection: 'Team1' },
|
||||
'Team2 Win' => { market: 'Match Odds', selection: 'Team2' },
|
||||
'X' => { market: 'Match Odds', selection: 'The Draw' },
|
||||
'1' => { market: 'Match Odds', selection: 'Team1' },
|
||||
'2' => { market: 'Match Odds', selection: 'Team2' },
|
||||
'1X' => { market: 'Double Chance', selection: 'Home or Draw' },
|
||||
'12' => { market: 'Double Chance', selection: 'Home or Away' },
|
||||
'X2' => { market: 'Double Chance', selection: 'Draw or Away' },
|
||||
'Total Over(%s)' => { market: 'Over/Under %nearest_half_over_s Goals', selection: 'Over %nearest_half_over_s Goals' },
|
||||
'Total Over(%s) for Team1' => { market: 'Team1 Over/Under %nearest_half_over_s Goals', selection: 'Over %nearest_half_over_s Goals' },
|
||||
'Total Over(%s) for Team2' => { market: 'Team2 Over/Under %nearest_half_over_s Goals', selection: 'Over %nearest_half_over_s Goals' },
|
||||
'Total Under(%s)' => { market: 'Over/Under %nearest_half_under_s Goals', selection: 'Under %nearest_half_under_s Goals' },
|
||||
'Total Under(%s) for Team1' => { market: 'Team1 Over/Under %nearest_half_under_s Goals', selection: 'Under %nearest_half_under_s Goals' },
|
||||
'Total Under(%s) for Team2' => { market: 'Team2 Over/Under %nearest_half_under_s Goals', selection: 'Under %nearest_half_under_s Goals' },
|
||||
'Asian Handicap1(%s)' => { market: 'Asian Handicap', selection: 'Team1' },
|
||||
'Asian Handicap1(0.0)/Draw No Bet' => { market: 'Draw no Bet', selection: 'Team1' },
|
||||
'Asian Handicap2(0.0)/Draw No Bet' => { market: 'Draw no Bet', selection: 'Team2' },
|
||||
'Asian Handicap2(%s)' => { market: 'Asian Handicap', selection: 'Team2' },
|
||||
'European Handicap1(%s)' => { market: 'Team1 +%rounded_positive_s', selection: 'Team1 %rounded_s' },
|
||||
'European Handicap2(%s)' => { market: 'Team1 +%rounded_positive_s', selection: 'Team2 %rounded_s' },
|
||||
'European HandicapX(%s)' => { market: 'Team1 +%rounded_positive_s', selection: 'Draw' },
|
||||
'Both to score' => { market: 'Both teams to score?', selection: 'Yes' },
|
||||
'One scoreless' => { market: 'Both teams to score?', selection: 'No' },
|
||||
'Only one to score' => { market: 'Both teams to score?', selection: 'No' },
|
||||
'Odd' => { market: 'Total Goals Odd/Even', selection: 'Odd' },
|
||||
'Even' => { market: 'Total Goals Odd/Even', selection: 'Even' },
|
||||
'Mat' => { market: 'Total Goals Odd/Even', selection: 'Even' },
|
||||
}
|
||||
end
|
||||
|
||||
def variations
|
||||
@variations ||=
|
||||
{ '1' => 'Team1 Win',
|
||||
'2' => 'Team2 Win',
|
||||
'3' => 'Asian Handicap1(0.0)/Draw No Bet',
|
||||
'4' => 'Asian Handicap2(0.0)/Draw No Bet',
|
||||
'5' => 'European Handicap1(%s)',
|
||||
'6' => 'European HandicapX(%s)',
|
||||
'7' => 'European Handicap2(%s)',
|
||||
'8' => 'Both to score',
|
||||
'9' => 'One scoreless',
|
||||
'10' => 'Only one to score',
|
||||
'11' => '1',
|
||||
'12' => 'X',
|
||||
'13' => '2',
|
||||
'14' => '1X',
|
||||
'15' => 'X2',
|
||||
'16' => '12',
|
||||
'17' => 'Asian Handicap1(%s)',
|
||||
'18' => 'Asian Handicap2(%s)',
|
||||
'19' => 'Total Over(%s)',
|
||||
'20' => 'Total Under(%s)',
|
||||
'21' => 'Total Over(%s) for Team1',
|
||||
'22' => 'Total Under(%s) for Team1',
|
||||
'23' => 'Total Over(%s) for Team2',
|
||||
'24' => 'Total Under(%s) for Team2',
|
||||
'25' => 'Odd',
|
||||
'26' => 'Even',
|
||||
'27' => '1 - Yellow Cards',
|
||||
'28' => 'X - Yellow Cards',
|
||||
'29' => '2 - Yellow Cards',
|
||||
'30' => '1X - Yellow Cards',
|
||||
'31' => '12 - Yellow Cards',
|
||||
'32' => 'X2 - Yellow Cards',
|
||||
'33' => 'Asian Handicap1(%s) - Yellow Cards',
|
||||
'34' => 'Asian Handicap2(%s) - Yellow Cards',
|
||||
'35' => 'Total Over(%s) - Yellow Cards',
|
||||
'36' => 'Total Under(%s) - Yellow Cards',
|
||||
'37' => 'Total Under(%s) for Team1 - Yellow Cards',
|
||||
'38' => 'Total Over(%s) for Team1 - Yellow Cards',
|
||||
'39' => 'Total Under(%s) for Team2 - Yellow Cards',
|
||||
'40' => 'Total Over(%s) for Team2 - Yellow Cards',
|
||||
'41' => 'Even - Yellow Cards',
|
||||
'42' => 'Odd - Yellow Cards',
|
||||
'43' => '1 - Corners',
|
||||
'44' => 'X - Corners',
|
||||
'45' => '2 - Corners',
|
||||
'46' => '1X - Corners',
|
||||
'47' => '12 - Corners',
|
||||
'48' => 'X2 - Corners',
|
||||
'49' => 'Asian Handicap1(%s) - Corners',
|
||||
'50' => 'Asian Handicap2(%s) - Corners',
|
||||
'51' => 'Total Over(%s) - Corners',
|
||||
'52' => 'Total Under(%s) - Corners',
|
||||
'53' => 'Total Under(%s) for Team1 - Corners',
|
||||
'54' => 'Total Over(%s) for Team1 - Corners',
|
||||
'55' => 'Total Under(%s) for Team2 - Corners',
|
||||
'56' => 'Total Over(%s) for Team2 - Corners',
|
||||
'57' => 'Odd - Corners',
|
||||
'58' => 'Even - Corners',
|
||||
'63' => 'Red card - yes',
|
||||
'64' => 'No red card',
|
||||
'65' => 'Penalty - yes',
|
||||
'66' => 'No penalty',
|
||||
'67' => 'Score (%s)',
|
||||
'68' => 'Total Over(%s) - Tie Break',
|
||||
'69' => 'Total Under(%s) - Tie Break',
|
||||
'70' => 'Score (%s) - not',
|
||||
'71' => 'Any substitute to score a goal - yes',
|
||||
'72' => 'Any substitute to score a goal - no',
|
||||
'73' => 'Team1 to win by exactly 1 goal - yes',
|
||||
'74' => 'Team1 to win by exactly 1 goal - no',
|
||||
'75' => 'Team2 to win by exactly 1 goal - yes',
|
||||
'76' => 'Team2 to win by exactly 1 goal - no',
|
||||
'77' => 'Team1 to win by exactly 2 goals - yes',
|
||||
'78' => 'Team1 to win by exactly 2 goals - no',
|
||||
'79' => 'Team1 to win by exactly 3 goals - yes',
|
||||
'80' => 'Team1 to win by exactly 3 goals - no',
|
||||
'81' => 'Team2 to win by exactly 2 goals - yes',
|
||||
'82' => 'Team2 to win by exactly 2 goals - no',
|
||||
'83' => 'Team2 to win by exactly 3 goals - yes',
|
||||
'84' => 'Team2 to win by exactly 3 goals - no',
|
||||
'85' => 'Team1 to win to Nil - yes',
|
||||
'86' => 'Team1 to win to Nil - no',
|
||||
'87' => 'Team2 to win to Nil - yes',
|
||||
'88' => 'Team2 to win to Nil - no',
|
||||
'89' => 'Team1 to win either halves - yes',
|
||||
'90' => 'Team1 to win either halves - no',
|
||||
'91' => 'Team2 to win either halves - yes',
|
||||
'92' => 'Team2 to win either halves - no',
|
||||
'93' => 'Draw in either half - yes',
|
||||
'94' => 'Draw in either half - no',
|
||||
'95' => 'Team1 to win in both halves - yes',
|
||||
'96' => 'Team1 to win in both halves - no',
|
||||
'97' => 'Team2 to win in both halves - yes',
|
||||
'98' => 'Team2 to win in both halves - no',
|
||||
'99' => 'Team1 to win and Total Over 2.5 - yes',
|
||||
'100' => 'Team1 to win and Total Over 2.5 - no',
|
||||
'101' => 'Team1 to win and Total Under 2.5 - yes',
|
||||
'102' => 'Team1 to win and Total Under 2.5 - no',
|
||||
'103' => 'Team2 to win and Total Over 2.5 - yes',
|
||||
'104' => 'Team2 to win and Total Over 2.5 - no',
|
||||
'105' => 'Team2 to win and Total Under 2.5 - yes',
|
||||
'106' => 'Team2 to win and Total Under 2.5 - no',
|
||||
'107' => 'Draw in both half - yes',
|
||||
'108' => 'Draw in both half - no',
|
||||
'109' => 'Draw and Total Over 2.5 - yes',
|
||||
'110' => 'Draw and Total Over 2.5 - no',
|
||||
'111' => 'Draw and Total Under 2.5 - yes',
|
||||
'112' => 'Draw and Total Under 2.5 - no',
|
||||
'113' => 'Goals in both halves - yes',
|
||||
'114' => 'Goals in both halves - no',
|
||||
'115' => 'Team1 to score in both halves - yes',
|
||||
'116' => 'Team1 to score in both halves - no',
|
||||
'117' => 'Team2 to score in both halves - yes',
|
||||
'118' => 'Team2 to score in both halves - no',
|
||||
'119' => 'Double - yes',
|
||||
'120' => 'Double - no',
|
||||
'121' => 'Hattrick - yes',
|
||||
'122' => 'Hattrick - no',
|
||||
'123' => 'Own goal - yes',
|
||||
'124' => 'Own goal - no',
|
||||
'125' => 'Both halves > 1.5 goals - yes',
|
||||
'126' => 'Both halves > 1.5 goals - no',
|
||||
'127' => 'Both halves < 1.5 goals - yes',
|
||||
'128' => 'Both halves < 1.5 goals - no',
|
||||
'129' => 'Sets (%s)',
|
||||
'130' => 'Sets (%s) - not',
|
||||
'131' => 'Asian Handicap1(%s) - Sets',
|
||||
'132' => 'Asian Handicap2(%s) - Sets',
|
||||
'133' => 'Total Over(%s) - Sets',
|
||||
'134' => 'Total Under(%s) - Sets',
|
||||
'135' => 'Team1/Team1',
|
||||
'136' => 'Team1/Team1 - no',
|
||||
'137' => 'Team1/Draw',
|
||||
'138' => 'Team1/Draw - no',
|
||||
'139' => 'Team1/Team2',
|
||||
'140' => 'Team1/Team2 - no',
|
||||
'141' => 'Draw/Team1',
|
||||
'142' => 'Draw/Team1 - no',
|
||||
'143' => 'Draw/Draw',
|
||||
'144' => 'Draw/Draw - no',
|
||||
'145' => 'Draw/Team2',
|
||||
'146' => 'Draw/Team2 - no',
|
||||
'147' => 'Team2/Team1',
|
||||
'148' => 'Team2/Team1 - no',
|
||||
'149' => 'Team2/Draw',
|
||||
'150' => 'Team2/Draw - no',
|
||||
'151' => 'Team2/Team2',
|
||||
'152' => 'Team2/Team2 - no',
|
||||
'153' => 'Exact (%s)',
|
||||
'154' => 'Exact (%s) - no',
|
||||
'155' => 'Exact (%s) for Team1',
|
||||
'156' => 'Exact (%s) for Team1 - no',
|
||||
'157' => 'Exact (%s) for Team2',
|
||||
'158' => 'Exact (%s) for Team2 - no',
|
||||
'159' => 'More goals in the 1st half',
|
||||
'160' => 'Equal goals in halves',
|
||||
'161' => 'More goals in the 2nd half',
|
||||
'162' => '1 half most goals (draw no bet)',
|
||||
'163' => '2 half most goals (draw no bet)',
|
||||
'164' => 'Team1 - 1st goal',
|
||||
'165' => 'No goal',
|
||||
'166' => 'Team2 - 1st goal',
|
||||
'167' => 'Team1 - 1st goal (draw no bet)',
|
||||
'168' => 'Team2 - 1st goal (draw no bet)',
|
||||
'169' => 'Team1 - Last goal',
|
||||
'170' => 'No goal',
|
||||
'171' => 'Team2 - Last goal',
|
||||
'172' => 'Team1 - Last goal (draw no bet)',
|
||||
'173' => 'Team2 - Last goal (draw no bet)',
|
||||
'174' => 'Total Over(%s) - Aces',
|
||||
'175' => 'Total Under(%s) - Aces',
|
||||
'176' => 'Total Over(%s) for Team1 - Aces',
|
||||
'177' => 'Total Under(%s) for Team1 - Aces',
|
||||
'178' => 'Total Over(%s) for Team2 - Aces',
|
||||
'179' => 'Total Under(%s) for Team2 - Aces',
|
||||
'180' => 'Total Over(%s) - Double Faults',
|
||||
'181' => 'Total Under(%s) - Double Faults',
|
||||
'182' => 'Total Over(%s) for Team1 - Double Faults',
|
||||
'183' => 'Total Under(%s) for Team1 - Double Faults',
|
||||
'184' => 'Total Over(%s) for Team2 - Double Faults',
|
||||
'185' => 'Total Under(%s) for Team2 - Double Faults',
|
||||
'186' => 'Total Over(%s) for Team1 - 1st Serve',
|
||||
'187' => 'Total Under(%s) for Team1 - 1st Serve',
|
||||
'188' => 'Total Over(%s) for Team2 - 1st Serve',
|
||||
'189' => 'Total Under(%s) for Team2 - 1st Serve',
|
||||
'190' => 'Asian Handicap1(%s) - Aces',
|
||||
'191' => 'Asian Handicap2(%s) - Aces',
|
||||
'192' => 'Asian Handicap1(%s) - Double Faults',
|
||||
'193' => 'Asian Handicap2(%s) - Double Faults',
|
||||
'194' => 'Asian Handicap1(%s) - 1st Serve',
|
||||
'195' => 'Asian Handicap2(%s) - 1st Serve',
|
||||
'196' => 'Player1 - 1st Ace',
|
||||
'197' => 'Player2 - 1st Ace',
|
||||
'198' => 'Player1 - 1st Double Fault',
|
||||
'199' => 'Player2 - 1st Double Fault',
|
||||
'200' => 'Player1 - 1st Break',
|
||||
'201' => 'Player2 - 1st Break',
|
||||
'202' => 'Player1 - 1st Break',
|
||||
'203' => 'No break - 1st Break',
|
||||
'204' => 'Player2 - 1st Break',
|
||||
'205' => '6-0 Set - yes',
|
||||
'206' => '6-0 Set - no',
|
||||
'207' => 'Win From Behind - yes',
|
||||
'208' => 'Win From Behind - no',
|
||||
'209' => 'Exact (%s) - Sets',
|
||||
'210' => 'Exact (%s) - Sets - no',
|
||||
'211' => 'Team1 - 1st corner',
|
||||
'212' => 'No corners',
|
||||
'213' => 'Team2 - 1st corner',
|
||||
'214' => 'Team1 - Last corner',
|
||||
'215' => 'No corners',
|
||||
'216' => 'Team2 - Last corner',
|
||||
'217' => 'Team1 - 1st Yellow Card',
|
||||
'218' => 'No Yellow Card',
|
||||
'219' => 'Team2 - 1st Yellow Card',
|
||||
'220' => 'Team1 - Last Yellow Card',
|
||||
'221' => 'No Yellow Card',
|
||||
'222' => 'Team2 - Last Yellow Card',
|
||||
'223' => 'Team1 - 1st offside',
|
||||
'224' => 'No offsides',
|
||||
'225' => 'Team2 - 1st offside',
|
||||
'226' => 'Team1 - Last offside',
|
||||
'227' => 'No offsides',
|
||||
'228' => 'Team2 - Last offside',
|
||||
'229' => '1st substitution - 1 half',
|
||||
'230' => '1st substitution - intermission',
|
||||
'231' => '1st substitution - 2 half',
|
||||
'232' => '1st goal - 1 half',
|
||||
'233' => 'No goal',
|
||||
'234' => '1st goal - 2 half',
|
||||
'235' => 'Team1 - 1st subs',
|
||||
'236' => 'Team2 - 1st subs',
|
||||
'237' => 'Team1 - Last subs',
|
||||
'238' => 'Team2 - Last subs',
|
||||
'239' => '1 - Shots on goal',
|
||||
'240' => 'X - Shots on goal',
|
||||
'241' => '2 - Shots on goal',
|
||||
'242' => '1X - Shots on goal',
|
||||
'243' => '12 - Shots on goal',
|
||||
'244' => 'X2 - Shots on goal',
|
||||
'245' => 'Asian Handicap1(%s) - Shots on goal',
|
||||
'246' => 'Asian Handicap2(%s) - Shots on goal',
|
||||
'247' => 'Total Over(%s) - Shots on goal',
|
||||
'248' => 'Total Under(%s) - Shots on goal',
|
||||
'249' => 'Total Over(%s) for Team1 - Shots on goal',
|
||||
'250' => 'Total Under(%s) for Team1 - Shots on goal',
|
||||
'251' => 'Total Over(%s) for Team2 - Shots on goal',
|
||||
'252' => 'Total Under(%s) for Team2 - Shots on goal',
|
||||
'253' => 'Odd - Shots on goal',
|
||||
'254' => 'Even - Shots on goal',
|
||||
'255' => '1 - Fouls',
|
||||
'256' => 'X - Fouls',
|
||||
'257' => '2 - Fouls',
|
||||
'258' => '1X - Fouls',
|
||||
'259' => '12 - Fouls',
|
||||
'260' => 'X2 - Fouls',
|
||||
'261' => 'Asian Handicap1(%s) - Fouls',
|
||||
'262' => 'Asian Handicap2(%s) - Fouls',
|
||||
'263' => 'Total Over(%s) - Fouls',
|
||||
'264' => 'Total Under(%s) - Fouls',
|
||||
'265' => 'Total Over(%s) for Team1 - Fouls',
|
||||
'266' => 'Total Under(%s) for Team1 - Fouls',
|
||||
'267' => 'Total Over(%s) for Team2 - Fouls',
|
||||
'268' => 'Total Under(%s) for Team2 - Fouls',
|
||||
'269' => 'Odd - Fouls',
|
||||
'270' => 'Even - Fouls',
|
||||
'271' => '1 - Offsides',
|
||||
'272' => 'X - Offsides',
|
||||
'273' => '2 - Offsides',
|
||||
'274' => '1X - Offsides',
|
||||
'275' => '12 - Offsides',
|
||||
'276' => 'X2 - Offsides',
|
||||
'277' => 'Asian Handicap1(%s) - Offsides',
|
||||
'278' => 'Asian Handicap2(%s) - Offsides',
|
||||
'279' => 'Total Over(%s) - Offsides',
|
||||
'280' => 'Total Under(%s) - Offsides',
|
||||
'281' => 'Total Over(%s) for Team1 - Offsides',
|
||||
'282' => 'Total Under(%s) for Team1 - Offsides',
|
||||
'283' => 'Total Over(%s) for Team2 - Offsides',
|
||||
'284' => 'Total Under(%s) for Team2 - Offsides',
|
||||
'285' => 'Odd - Offsides',
|
||||
'286' => 'Even - Offsides',
|
||||
'287' => 'Team1 to Win From Behind - yes',
|
||||
'288' => 'Team1 to Win From Behind - no',
|
||||
'289' => 'Team2 to Win From Behind - yes',
|
||||
'290' => 'Team2 to Win From Behind - no',
|
||||
'291' => 'Both To Score and W1 - yes',
|
||||
'292' => 'Both To Score and W1 - no',
|
||||
'293' => 'Both To Score and W2 - yes',
|
||||
'294' => 'Both To Score and W2 - no',
|
||||
'295' => 'Both To Score and Draw - yes',
|
||||
'296' => 'Both To Score and Draw - no',
|
||||
'297' => 'Exact (%s) - Added time',
|
||||
'298' => 'Exact (%s) - Added time - no',
|
||||
'299' => 'Total Over(%s) - Added time',
|
||||
'300' => 'Total Under(%s) - Added time',
|
||||
'301' => 'Home No Bet - W2',
|
||||
'302' => 'Home No Bet - Draw',
|
||||
'303' => 'Home No Bet - No Draw',
|
||||
'304' => 'Away No Bet - W1',
|
||||
'305' => 'Away No Bet - Draw',
|
||||
'306' => 'Away No Bet - No Draw',
|
||||
'307' => 'Total Over(%s) - Subs',
|
||||
'308' => 'Total Under(%s) - Subs',
|
||||
'309' => 'Total Over(%s) for Team1 - Subs',
|
||||
'310' => 'Total Under(%s) for Team1 - Subs',
|
||||
'311' => 'Total Over(%s) for Team2 - Subs',
|
||||
'312' => 'Total Under(%s) for Team1 - Subs',
|
||||
'313' => '1 - Ball possession',
|
||||
'314' => 'X - Ball possession',
|
||||
'315' => '2 - Ball possession',
|
||||
'316' => '1X - Ball possession',
|
||||
'317' => '12 - Ball possession',
|
||||
'318' => 'X2 - Ball possession',
|
||||
'319' => 'Asian Handicap1(%s) - Ball possession',
|
||||
'320' => 'Asian Handicap2(%s) - Ball possession',
|
||||
'321' => 'Total Over(%s) for Team1 - Ball possession',
|
||||
'322' => 'Total Under(%s) for Team1 - Ball possession',
|
||||
'323' => 'Total Over(%s) for Team2 - Ball possession',
|
||||
'324' => 'Total Under(%s) for Team2 - Ball possession',
|
||||
'325' => 'Team1 - 1st corner',
|
||||
'326' => 'Team2 - 1st corner',
|
||||
'327' => 'Team1 - Last corner',
|
||||
'328' => 'Team2 - Last corner',
|
||||
'329' => 'Team1 - 1st Yellow Card',
|
||||
'330' => 'Team2 - 1st Yellow Card',
|
||||
'331' => 'Team1 - Last Yellow Card',
|
||||
'332' => 'Team2 - Last Yellow Card',
|
||||
'333' => 'Team1 - 1st offside',
|
||||
'334' => 'Team2 - 1st offside',
|
||||
'335' => 'Team1 - Last offside',
|
||||
'336' => 'Team2 - Last offside',
|
||||
'337' => 'Home No Bet - No W2',
|
||||
'338' => 'Away No Bet - No W1',
|
||||
'339' => '1X and Total Over 2.5 - yes',
|
||||
'340' => '1X and Total Over 2.5 - no',
|
||||
'341' => '1X and Total Under 2.5 - yes',
|
||||
'342' => '1X and Total Under 2.5 - no',
|
||||
'343' => 'X2 and Total Over 2.5 - yes',
|
||||
'344' => 'X2 and Total Over 2.5 - no',
|
||||
'345' => 'X2 and Total Under 2.5 - yes',
|
||||
'346' => 'X2 and Total Under 2.5 - no',
|
||||
'348' => 'Overtime - yes',
|
||||
'351' => 'Overtime - no',
|
||||
'354' => 'Score Draw - yes',
|
||||
'357' => 'Score Draw - no',
|
||||
'360' => 'Race to 2 - Team1',
|
||||
'363' => 'Race to 2 - Team2',
|
||||
'366' => 'Race to 2 - Team1',
|
||||
'369' => 'Race to 2 - neither',
|
||||
'372' => 'Race to 2 - Team2',
|
||||
'375' => 'Race to 3 - Team1',
|
||||
'378' => 'Race to 3 - Team2',
|
||||
'381' => 'Race to 3 - Team1',
|
||||
'384' => 'Race to 3 - neither',
|
||||
'387' => 'Race to 3 - Team2',
|
||||
'390' => 'Race to 4 - Team1',
|
||||
'393' => 'Race to 4 - Team2',
|
||||
'396' => 'Race to 4 - Team1',
|
||||
'399' => 'Race to 4 - neither',
|
||||
'402' => 'Race to 4 - Team2',
|
||||
'405' => 'Race to 5 - Team1',
|
||||
'408' => 'Race to 5 - Team2',
|
||||
'411' => 'Race to 5 - Team1',
|
||||
'414' => 'Race to 5 - neither',
|
||||
'417' => 'Race to 5 - Team2',
|
||||
'420' => 'Race to 10 - Team1',
|
||||
'423' => 'Race to 10 - Team2',
|
||||
'426' => 'Race to 10 - Team1',
|
||||
'429' => 'Race to 10 - neither',
|
||||
'432' => 'Race to 10 - Team2',
|
||||
'435' => 'Race to 15 - Team1',
|
||||
'438' => 'Race to 15 - Team2',
|
||||
'441' => 'Race to 15 - Team1',
|
||||
'444' => 'Race to 15 - neither',
|
||||
'447' => 'Race to 15 - Team2',
|
||||
'450' => 'Race to 20 - Team1',
|
||||
'453' => 'Race to 20 - Team2',
|
||||
'456' => 'Race to 20 - Team1',
|
||||
'459' => 'Race to 20 - neither',
|
||||
'462' => 'Race to 20 - Team2',
|
||||
'467' => 'Team1 - Next goal (draw no bet)',
|
||||
'470' => 'Team2 - Next goal (draw no bet)',
|
||||
'473' => 'Team1 - Next goal',
|
||||
'476' => 'No next goal',
|
||||
'479' => 'Team2 - Next goal',
|
||||
'481' => '1st - yes',
|
||||
'484' => '1st - no',
|
||||
'487' => '1st-3rd - yes',
|
||||
'490' => '1st-3rd - no',
|
||||
'493' => 'W1',
|
||||
'496' => 'W2',
|
||||
'499' => 'TO(%s) - Hits',
|
||||
'502' => 'TU(%s) - Hits',
|
||||
'505' => 'TO(%s) for Team1 - Hits',
|
||||
'508' => 'TU(%s) for Team1 - Hits',
|
||||
'511' => 'TO(%s) for Team2 - Hits',
|
||||
'514' => 'TU(%s) for Team2 - Hits',
|
||||
'517' => 'TO(%s) - Errors',
|
||||
'520' => 'TU(%s) - Errors',
|
||||
'523' => 'TO(%s) - Hits+Errors+Runs',
|
||||
'526' => 'TU(%s) - Hits+Errors+Runs',
|
||||
'529' => 'AH1(%s) - Hits',
|
||||
'532' => 'AH2(%s) - Hits',
|
||||
'535' => '1 - Hits',
|
||||
'538' => 'X - Hits',
|
||||
'541' => '2 - Hits',
|
||||
'546' => 'Team1 Win - Kills',
|
||||
'549' => 'Team2 Win - Kills',
|
||||
'552' => 'Asian Handicap1(%s) - Kills',
|
||||
'555' => 'Asian Handicap2(%s) - Kills',
|
||||
'558' => 'Total Over(%s) - Kills',
|
||||
'561' => 'Total Under(%s) - Kills',
|
||||
'564' => 'Total Over(%s) for Team1 - Kills',
|
||||
'567' => 'Total Under(%s) for Team1 - Kills',
|
||||
'570' => 'Total Over(%s) for Team2 - Kills',
|
||||
'573' => 'Total Under(%s) for Team2 - Kills',
|
||||
'574' => 'W1 - 1st blood',
|
||||
'575' => 'W2 - 1st blood',
|
||||
'576' => 'W1 - 1st tower',
|
||||
'577' => 'W2 - 1st tower',
|
||||
'578' => 'W1 - 1st dragon',
|
||||
'579' => 'W2 - 1st dragon',
|
||||
'580' => 'W1 - 1st baron',
|
||||
'581' => 'W2 - 1st baron',
|
||||
'582' => 'W1 - 1st inhibitor',
|
||||
'583' => 'W2 - 1st inhibitor',
|
||||
'584' => 'W1 - 1st roshan',
|
||||
'585' => 'W2 - 1st roshan',
|
||||
'586' => 'Win pistol rounds - Yes',
|
||||
'587' => 'Win pistol rounds - No',
|
||||
'588' => 'TO(%s) - Duration',
|
||||
'589' => 'TU(%s) - Duration',
|
||||
'590' => 'TO(%s) - Barons',
|
||||
'591' => 'TU(%s) - Barons',
|
||||
'592' => 'TO(%s) - Inhibitors',
|
||||
'593' => 'TU(%s) - Inhibitors',
|
||||
'594' => 'TO(%s) - Towers',
|
||||
'595' => 'TU(%s) - Towers',
|
||||
'596' => 'TO(%s) - Dragons',
|
||||
'597' => 'TU(%s) - Dragons',
|
||||
'598' => 'TO(%s) - Roshans',
|
||||
'599' => 'TU(%s) - Roshans',
|
||||
'600' => 'TO(%s) for Team1 - Sets',
|
||||
'601' => 'TO(%s) for Team1 - Sets',
|
||||
'602' => 'TO(%s) for Team2 - Sets',
|
||||
'603' => 'TO(%s) for Team2 - Sets',
|
||||
'604' => 'W1 - Longest TD',
|
||||
'605' => 'W2 - Longest TD',
|
||||
'606' => 'W1 - Longest FG',
|
||||
'607' => 'W2 - Longest FG',
|
||||
'608' => 'Touchdown - Yes',
|
||||
'609' => 'Touchdown - No',
|
||||
'610' => 'Safety - Yes',
|
||||
'611' => 'Safety - No',
|
||||
'612' => 'First score TD - Yes',
|
||||
'613' => 'First score TD - No',
|
||||
'614' => 'Both teams 10 pts - Yes',
|
||||
'615' => 'Both teams 10 pts - No',
|
||||
'616' => 'Both teams 15 pts - Yes',
|
||||
'617' => 'Both teams 15 pts - No',
|
||||
'618' => 'Both teams 20 pts - Yes',
|
||||
'619' => 'Both teams 20 pts - No',
|
||||
'620' => 'Both teams 25 pts - Yes',
|
||||
'621' => 'Both teams 25 pts - No',
|
||||
'622' => 'Both teams 30 pts - Yes',
|
||||
'623' => 'Both teams 30 pts - No',
|
||||
'624' => 'Both teams 35 pts - Yes',
|
||||
'625' => 'Both teams 35 pts - No',
|
||||
'626' => 'Both teams 40 pts - Yes',
|
||||
'627' => 'Both teams 40 pts - No',
|
||||
'628' => 'Both teams 45 pts - Yes',
|
||||
'629' => 'Both teams 45 pts - No',
|
||||
'630' => 'Both teams 50 pts - Yes',
|
||||
'631' => 'Both teams 50 pts - No',
|
||||
'632' => 'Highest Scoring Quarter - 1st',
|
||||
'633' => 'Highest Scoring Quarter - 2nd',
|
||||
'634' => 'Highest Scoring Quarter - 3rd',
|
||||
'635' => 'Highest Scoring Quarter - 4th',
|
||||
'636' => 'Highest Scoring Quarter - Tie',
|
||||
'637' => 'TO(%s) - Field Goals',
|
||||
'638' => 'TU(%s) - Field Goals',
|
||||
'639' => 'TO(%s) - Touchdowns',
|
||||
'640' => 'TU(%s) - Touchdowns',
|
||||
'641' => 'TO(%s) - Longest TD, distance',
|
||||
'642' => 'TU(%s) - Longest TD, distance',
|
||||
'643' => 'TO(%s) - Longest FG, distance',
|
||||
'644' => 'TU(%s) - Longest FG, distance',
|
||||
'645' => 'TO(%s) for Team1 - Field Goals',
|
||||
'646' => 'TU(%s) for Team1 - Field Goals',
|
||||
'647' => 'TO(%s) for Team2 - Field Goals',
|
||||
'648' => 'TU(%s) for Team2 - Field Goals',
|
||||
'649' => 'TO(%s) for Team1 - Touchdowns',
|
||||
'650' => 'TU(%s) for Team1 - Touchdowns',
|
||||
'651' => 'TO(%s) for Team2 - Touchdowns',
|
||||
'652' => 'TU(%s) for Team2 - Touchdowns',
|
||||
'653' => 'AH1(%s) - Maps',
|
||||
'654' => 'AH2(%s) - Maps',
|
||||
'655' => 'TO(%s) - Maps',
|
||||
'656' => 'TU(%s) - Maps',
|
||||
'657' => 'TO(%s) for Team1 - Maps',
|
||||
'658' => 'TU(%s) for Team1 - Maps',
|
||||
'659' => 'TO(%s) for Team2 - Maps',
|
||||
'660' => 'TU(%s) for Team2 - Maps',
|
||||
'661' => 'Maps (%s)',
|
||||
'662' => 'Maps (%s) - not',
|
||||
'663' => 'Exact (%s) - Maps',
|
||||
'664' => 'Exact (%s) - Maps - no',
|
||||
'665' => 'Win both pistol rounds - Yes',
|
||||
'666' => 'Win both pistol rounds - No',
|
||||
'667' => 'Team1 to win both pistol rounds - Yes',
|
||||
'668' => 'Team1 to win both pistol rounds - No',
|
||||
'669' => 'Team2 to win both pistol rounds - Yes',
|
||||
'670' => 'Team1 to win both pistol rounds - No',
|
||||
'671' => 'Team1 to win at least 1 set - Yes',
|
||||
'672' => 'Team1 to win at least 1 set - No',
|
||||
'673' => 'Team2 to win at least 1 set - Yes',
|
||||
'674' => 'Team1 to win at least 1 set - No',
|
||||
'675' => 'Team1 to win at least 1 map - Yes',
|
||||
'676' => 'Team1 to win at least 1 map - No',
|
||||
'677' => 'Team2 to win at least 1 map - Yes',
|
||||
'678' => 'Team1 to win at least 1 map - No',
|
||||
'679' => 'Both teams kill a dragon - Yes',
|
||||
'680' => 'Both teams kill a dragon - No',
|
||||
'681' => 'Both teams kill a baron - Yes',
|
||||
'682' => 'Both teams kill a baron - No',
|
||||
'683' => 'W1 - 1st Barracks',
|
||||
'684' => 'W2 - 1st Barracks',
|
||||
'685' => 'W1 - 1st Double Kill',
|
||||
'686' => 'W2 - 1st Double Kill',
|
||||
'687' => 'TO(%s) - Barracks',
|
||||
'688' => 'TU(%s) - Barracks',
|
||||
'689' => 'TO(%s) - Double Kills',
|
||||
'690' => 'TU(%s) - Double Kills',
|
||||
'691' => 'AH1(%s) - Barons',
|
||||
'692' => 'AH2(%s) - Barons',
|
||||
'693' => 'AH1(%s) - Dragons',
|
||||
'694' => 'AH2(%s) - Dragons',
|
||||
'695' => 'AH1(%s) - Towers/Turrets',
|
||||
'696' => 'AH2(%s) - Towers/Turrets',
|
||||
'697' => '1 - 3-points',
|
||||
'698' => 'X - 3-points',
|
||||
'699' => '2 - 3-points',
|
||||
'700' => '1 - Rebounds',
|
||||
'701' => 'X - Rebounds',
|
||||
'702' => '2 - Rebounds',
|
||||
'703' => '1 - Assists',
|
||||
'704' => 'X - Assists',
|
||||
'705' => '2 - Assists',
|
||||
'706' => '1X - 3-points',
|
||||
'707' => 'X2 - 3-points',
|
||||
'708' => '12 - 3-points',
|
||||
'709' => '1X - Rebounds',
|
||||
'710' => 'X2 - Rebounds',
|
||||
'711' => '12 - Rebounds',
|
||||
'712' => '1X - Assists',
|
||||
'713' => 'X2 - Assists',
|
||||
'714' => '12 - Assists',
|
||||
'715' => 'AH1(%s) - 3-points',
|
||||
'716' => 'AH2(%s) - 3-points',
|
||||
'717' => 'AH1(%s) - Rebounds',
|
||||
'718' => 'AH2(%s) - Rebounds',
|
||||
'719' => 'AH1(%s) - Assists',
|
||||
'720' => 'AH2(%s) - Assists',
|
||||
'721' => 'TO(%s) - 3-points',
|
||||
'722' => 'TU(%s) - 3-points',
|
||||
'723' => 'TO(%s) - Rebounds',
|
||||
'724' => 'TU(%s) - Rebounds',
|
||||
'725' => 'TO(%s) - Assists',
|
||||
'726' => 'TU(%s) - Assists',
|
||||
'727' => 'TO(%s) for Team1 - 3-points',
|
||||
'728' => 'TU(%s) for Team1 - 3-points',
|
||||
'729' => 'TO(%s) for Team1 - Rebounds',
|
||||
'730' => 'TU(%s) for Team1 - Rebounds',
|
||||
'731' => 'TO(%s) for Team1 - Assists',
|
||||
'732' => 'TU(%s) for Team1 - Assists',
|
||||
'733' => 'TO(%s) for Team2 - 3-points',
|
||||
'734' => 'TU(%s) for Team2 - 3-points',
|
||||
'735' => 'TO(%s) for Team2 - Rebounds',
|
||||
'736' => 'TU(%s) for Team2 - Rebounds',
|
||||
'737' => 'TO(%s) for Team2 - Assists',
|
||||
'738' => 'TU(%s) for Team2 - Assists',
|
||||
'739' => 'W1 - 180s',
|
||||
'740' => 'W2 - 180s',
|
||||
'741' => 'AH1(%s) - 180s',
|
||||
'742' => 'AH2(%s) - 180s',
|
||||
'743' => 'TO(%s) - 180s',
|
||||
'744' => 'TU(%s) - 180s',
|
||||
'745' => '1 - Cards',
|
||||
'746' => 'X - Cards',
|
||||
'747' => '2 - Cards',
|
||||
'748' => '1 - Booking points',
|
||||
'749' => 'X - Booking points',
|
||||
'750' => '2 - Booking points',
|
||||
'751' => '1X - Cards',
|
||||
'752' => 'X2 - Cards',
|
||||
'753' => '12 - Cards',
|
||||
'754' => '1X - Booking points',
|
||||
'755' => 'X2 - Booking points',
|
||||
'756' => '12 - Booking points',
|
||||
'757' => 'AH1(%s) - Cards',
|
||||
'758' => 'AH2(%s) - Cards',
|
||||
'759' => 'AH1(%s) - Booking points',
|
||||
'760' => 'AH2(%s) - Booking points',
|
||||
'761' => 'TO(%s) - Cards',
|
||||
'762' => 'TU(%s) - Cards',
|
||||
'763' => 'TO(%s) - Booking points',
|
||||
'764' => 'TU(%s) - Booking points',
|
||||
'765' => 'TO(%s) for Team1 - Cards',
|
||||
'766' => 'TU(%s) for Team1 - Cards',
|
||||
'767' => 'TO(%s) for Team1 - Booking points',
|
||||
'768' => 'TU(%s) for Team1 - Booking points',
|
||||
'769' => 'TO(%s) for Team2 - Cards',
|
||||
'770' => 'TU(%s) for Team2 - Cards',
|
||||
'771' => 'TO(%s) for Team2 - Booking points',
|
||||
'772' => 'TU(%s) for Team2 - Booking points',
|
||||
'773' => 'Odd - Cards',
|
||||
'774' => 'Even - Cards',
|
||||
'775' => 'Odd - Booking points',
|
||||
'776' => 'Even - Booking points' }
|
||||
|
||||
end
|
||||
|
||||
def periods
|
||||
@periods ||=
|
||||
{
|
||||
'1': 'match (pairs)',
|
||||
'2': 'with OT and SO',
|
||||
'3': 'with OT',
|
||||
'4': 'regular time', #match odds
|
||||
'5': '1st',
|
||||
'6': '2nd',
|
||||
'7': '3rd',
|
||||
'8': '4th',
|
||||
'9': '5th',
|
||||
'10': '1st half', #first
|
||||
'13': '2nd half',
|
||||
'14': '1 set, 01 game',
|
||||
'15': '1 set, 02 game',
|
||||
'16': '1 set, 03 game',
|
||||
'17': '1 set, 04 game',
|
||||
'18': '1 set, 05 game',
|
||||
'19': '1 set, 06 game',
|
||||
'20': '1 set, 07 game',
|
||||
'21': '1 set, 08 game',
|
||||
'22': '1 set, 09 game',
|
||||
'23': '1 set, 10 game',
|
||||
'24': '1 set, 11 game',
|
||||
'25': '1 set, 12 game',
|
||||
'26': '1 set, 13 game',
|
||||
'44': '2 set, 01 game',
|
||||
'45': '2 set, 02 game',
|
||||
'46': '2 set, 03 game',
|
||||
'47': '2 set, 04 game',
|
||||
'48': '2 set, 05 game',
|
||||
'49': '2 set, 06 game',
|
||||
'50': '2 set, 07 game',
|
||||
'51': '2 set, 08 game',
|
||||
'52': '2 set, 09 game',
|
||||
'53': '2 set, 10 game',
|
||||
'54': '2 set, 11 game',
|
||||
'55': '2 set, 12 game',
|
||||
'56': '2 set, 13 game',
|
||||
'57': '3 set, 01 game',
|
||||
'58': '3 set, 02 game',
|
||||
'59': '3 set, 03 game',
|
||||
'60': '3 set, 04 game',
|
||||
'61': '3 set, 05 game',
|
||||
'62': '3 set, 06 game',
|
||||
'63': '3 set, 07 game',
|
||||
'64': '3 set, 08 game',
|
||||
'65': '3 set, 09 game',
|
||||
'66': '3 set, 10 game',
|
||||
'67': '3 set, 11 game',
|
||||
'68': '3 set, 12 game',
|
||||
'71': '3 set, 13 game',
|
||||
'76': '6th',
|
||||
'78': '7th',
|
||||
'86': 'regular time',
|
||||
'92': 'regular time',
|
||||
'93': '1st half',
|
||||
'95': 'to qualify',
|
||||
'96': '8th',
|
||||
'97': '9th',
|
||||
'113': '4 set, 01 game',
|
||||
'114': '4 set, 02 game',
|
||||
'115': '4 set, 03 game',
|
||||
'116': '4 set, 04 game',
|
||||
'117': '4 set, 05 game',
|
||||
'118': '4 set, 06 game',
|
||||
'119': '4 set, 07 game',
|
||||
'120': '4 set, 08 game',
|
||||
'121': '4 set, 09 game',
|
||||
'122': '4 set, 10 game',
|
||||
'123': '4 set, 11 game',
|
||||
'124': '4 set, 12 game',
|
||||
'125': '5 set, 01 game',
|
||||
'126': '5 set, 02 game',
|
||||
'127': '5 set, 03 game',
|
||||
'128': '5 set, 04 game',
|
||||
'129': '5 set, 05 game',
|
||||
'130': '5 set, 06 game',
|
||||
'131': '5 set, 07 game',
|
||||
'132': '5 set, 08 game',
|
||||
'133': '5 set, 09 game',
|
||||
'134': '5 set, 10 game',
|
||||
'156': '4 set, 13 game',
|
||||
'159': '5 set, 11 game',
|
||||
'161': '5 set, 12 game',
|
||||
'169': '5 set, 13 game',
|
||||
'223': 'regular time',
|
||||
'243': '1st half',
|
||||
'245': '2nd half',
|
||||
'246': '2nd half',
|
||||
'317': '1st half',
|
||||
'318': '2nd half',
|
||||
'4091': 'regular time', #match odds
|
||||
'4094': '1st half'
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
16
portal/app/lib/integrations/betfair/account_manager.rb
Normal file
16
portal/app/lib/integrations/betfair/account_manager.rb
Normal file
@ -0,0 +1,16 @@
|
||||
module Integrations
|
||||
module Betfair
|
||||
class AccountManager < Base
|
||||
def refresh_account_balance
|
||||
# go check the account balance and update the ExchangeAccount for this account
|
||||
res = self.class.post("#{API_ACCOUNT_ENDPOINT}/getAccountFunds/", { headers: @connection.api_headers, data: {} })
|
||||
if res['availableToBetBalance']
|
||||
acc_bal = res['availableToBetBalance']
|
||||
@account.update(account_balance: acc_bal, account_balance_last_checked: Time.now)
|
||||
end
|
||||
@account.account_balance
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
40
portal/app/lib/integrations/betfair/base.rb
Normal file
40
portal/app/lib/integrations/betfair/base.rb
Normal file
@ -0,0 +1,40 @@
|
||||
module Integrations
|
||||
module Betfair
|
||||
class Base
|
||||
include HTTParty
|
||||
|
||||
API_BETTING_ENDPOINT = 'https://api.betfair.com/exchange/betting/rest/v1.0'.freeze
|
||||
API_ACCOUNT_ENDPOINT = 'https://api.betfair.com/exchange/account/rest/v1.0'.freeze
|
||||
|
||||
def initialize(account_friendly_id)
|
||||
@account = ExchangeAccount.find_by_id(account_friendly_id)
|
||||
unless @account
|
||||
puts "No exchange account for '#{account_friendly_id}'. Stopping"
|
||||
return
|
||||
end
|
||||
self.class.pem @account.ssl_pem
|
||||
@connection = Integrations::Betfair::Connection.new(@account)
|
||||
end
|
||||
|
||||
def minimum_stake
|
||||
1
|
||||
end
|
||||
|
||||
def list_events(filter)
|
||||
body = { filter: filter }
|
||||
self.class.post("#{API_BETTING_ENDPOINT}/listEvents/", { headers: @connection.api_headers, body: body.to_json })
|
||||
end
|
||||
|
||||
def debug_list_market_types
|
||||
body = { filter: {} }
|
||||
marketTypes = self.class.post("#{API_BETTING_ENDPOINT}/listMarketTypes/", { headers: @connection.api_headers, body: body.to_json })
|
||||
results = []
|
||||
marketTypes.each do |market|
|
||||
results << market['marketType']
|
||||
end
|
||||
File.write(Rails.root.join('samples/bf_market_types.json'), results.join("\n"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
173
portal/app/lib/integrations/betfair/bet_manager.rb
Normal file
173
portal/app/lib/integrations/betfair/bet_manager.rb
Normal file
@ -0,0 +1,173 @@
|
||||
module Integrations
|
||||
module Betfair
|
||||
class BetManager < Base
|
||||
def check_qualified_bet_outcome(bets)
|
||||
bets_lookup = {}
|
||||
market_ids = []
|
||||
bets_by_bet_ids = []
|
||||
reconciliation_limit = 20
|
||||
bet_count = 0
|
||||
bets.each do |bet|
|
||||
bet_count += 1
|
||||
next unless bet.exchange_market_details['market_id'] && bet.exchange_market_details['selection_id']
|
||||
|
||||
market_ids << bet.exchange_market_details['market_id']
|
||||
bets_lookup[bet.exchange_market_details['market_id'].to_s] = { selection_id: bet.exchange_market_details['selection_id'], bet_id: bet.id }
|
||||
bets_by_bet_ids << bet unless bet.exchange_bet_id.blank?
|
||||
next unless (reconciliation_limit == market_ids.size) || (bet_count == bets.count)
|
||||
|
||||
reconcile_bets_by_markets(market_ids, bets_lookup) unless market_ids.empty?
|
||||
market_ids = []
|
||||
bets_lookup = {}
|
||||
end
|
||||
reconcile_bets_by_bet_ids(bets_by_bet_ids) unless bets_by_bet_ids.empty?
|
||||
end
|
||||
|
||||
def reconcile_bets_by_markets(market_ids, bets_lookup)
|
||||
outcomes = { LOSER: [], WINNER: [] }
|
||||
body = { marketIds: market_ids }
|
||||
markets = self.class.post("#{API_BETTING_ENDPOINT}/listMarketBook/", { headers: @connection.api_headers, body: body.to_json })
|
||||
markets.each do |market|
|
||||
next unless market['status'] == 'CLOSED'
|
||||
|
||||
bet = bets_lookup[market['marketId']]
|
||||
next unless bet
|
||||
|
||||
runners = market['runners']
|
||||
runners.each do |runner|
|
||||
next unless runner['selectionId'] == bet[:selection_id]
|
||||
|
||||
x_status = runner['status'].to_sym
|
||||
if outcomes[x_status]
|
||||
outcomes[x_status] << bet[:bet_id]
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
ActiveRecord::Base.connection.execute("update bets set outcome = 'lost', outcome_value=stake where #{ActiveRecord::Base.sanitize_sql(['id in (?)', outcomes[:LOSER]])}") if outcomes[:LOSER].size.positive?
|
||||
ActiveRecord::Base.connection.execute("update bets set outcome = 'won', outcome_value=expected_value where #{ActiveRecord::Base.sanitize_sql(['id in (?)', outcomes[:WINNER]])}") if outcomes[:WINNER].size.positive?
|
||||
end
|
||||
|
||||
def reconcile_bets_by_bet_ids(bets)
|
||||
return unless bets.count.positive?
|
||||
|
||||
results = {}
|
||||
open_bet_ids = bets.pluck(:exchange_bet_id)
|
||||
%w[SETTLED VOIDED CANCELLED].each do |status|
|
||||
body = { betStatus: status, betIds: open_bet_ids }
|
||||
r = self.class.post("#{API_BETTING_ENDPOINT}/listClearedOrders/", { headers: @connection.api_headers, body: body.to_json })
|
||||
orders = r['clearedOrders']
|
||||
if status == 'SETTLED'
|
||||
# the status updates are by betOutcome
|
||||
orders.each do |o|
|
||||
results[o['betOutcome']] ||= []
|
||||
results[o['betOutcome']] << o['betId']
|
||||
end
|
||||
else
|
||||
results[status] ||= []
|
||||
bet_ids = orders.map { |o| o['betId'] }
|
||||
results[status] = bet_ids
|
||||
end
|
||||
end
|
||||
results.keys.each do |k|
|
||||
next if results[k].blank?
|
||||
|
||||
Bet.unscoped.open.where(exchange_bet_id: results[k]).update(outcome: k.downcase)
|
||||
end
|
||||
end
|
||||
|
||||
def place_bet(bet, stake)
|
||||
body = { marketId: bet.exchange_market_details['market_id'], customerRef: bet.tip_provider_bet_id }
|
||||
body[:instructions] =
|
||||
[{ orderType: 'LIMIT', side: 'BACK', selectionId: bet.exchange_market_details['selection_id'].to_s,
|
||||
limitOrder: { timeInForce: 'FILL_OR_KILL', size: stake.to_s, price: bet.tip_provider_odds.to_s, persistenceType: 'LAPSE' } }]
|
||||
r = self.class.post("#{API_BETTING_ENDPOINT}/placeOrders/", { headers: @connection.api_headers, body: body.to_json })
|
||||
|
||||
success = r['status'] == 'SUCCESS'
|
||||
if success
|
||||
bet_id = r['instructionReports'][0]['betId']
|
||||
return bet_id
|
||||
end
|
||||
error_code = r['errorCode']
|
||||
raise "[Place bet] Placing bet failed: #{error_code}"
|
||||
end
|
||||
|
||||
def bet_event(bet, update_bet = false)
|
||||
# return the event for this bet.
|
||||
# easy if we have the event_id, else we have to search.
|
||||
event_id = []
|
||||
event_id << bet.exchange_event_id unless bet.exchange_event_id.blank?
|
||||
events = list_events({ eventIds: event_id, textQuery: bet.exchange_event_name })
|
||||
e_id = events[0]['event']['id'] if events.length.positive?
|
||||
if e_id
|
||||
bet.update(exchange_event_id: e_id) if update_bet
|
||||
return e_id
|
||||
end
|
||||
raise '[bet_event] Error getting event id'
|
||||
end
|
||||
|
||||
def bet_odds(bet)
|
||||
event_market_selection_hash = event_market_selection(bet)
|
||||
raise '[bet odds] - market not available' unless event_market_selection_hash['market_id']
|
||||
raise '[bet odds] - selection not available' unless event_market_selection_hash['selection_id']
|
||||
|
||||
body = { marketId: event_market_selection_hash['market_id'], selectionId: event_market_selection_hash['selection_id'] }
|
||||
body[:priceProjection] = { priceData: ['EX_ALL_OFFERS'], virtualise: true }
|
||||
r = self.class.post("#{API_BETTING_ENDPOINT}/listRunnerBook/", { headers: @connection.api_headers, body: body.to_json })
|
||||
runners = r[0]['runners']
|
||||
raise '[Bet odds] - cannot identify prices' unless runners
|
||||
|
||||
rs = runners.first
|
||||
raise '[Bet odds] - cannot identify prices' unless rs && rs['ex'] && rs['ex']['availableToBack']
|
||||
|
||||
prices = []
|
||||
stakes = {}
|
||||
rs['ex']['availableToBack'].each do |ex|
|
||||
prices << ex['price']
|
||||
stakes[(ex['price']).to_s] = ex['size']
|
||||
end
|
||||
{ prices: prices, stakes: stakes, liquidity: r[0]["totalAvailable"] }
|
||||
end
|
||||
|
||||
def event_market_selection(bet)
|
||||
if bet.exchange_event_id.blank?
|
||||
bet_event(bet, true)
|
||||
raise '[No event id]' if bet.exchange_event_id.blank?
|
||||
end
|
||||
body = { maxResults: 500, filter: { eventIds: [bet.exchange_event_id] }, marketProjection: %w[MARKET_DESCRIPTION RUNNER_DESCRIPTION RUNNER_METADATA] }
|
||||
markets = self.class.post("#{API_BETTING_ENDPOINT}/listMarketCatalogue/", { headers: @connection.api_headers, body: body.to_json })
|
||||
m_details = bet.exchange_market_details
|
||||
returned_markets = []
|
||||
returned_selections = []
|
||||
markets.each do |market|
|
||||
returned_markets << market['marketName']
|
||||
next unless m_details['market'].casecmp(market['marketName']).zero?
|
||||
|
||||
m_details['market_id'] = market['marketId']
|
||||
fuzzy_match_runners = market['marketName'] == 'Match Odds'
|
||||
runners = market['runners']
|
||||
m_selection = m_details['selection']
|
||||
runners.each do |runner|
|
||||
returned_selections << runner['runnerName']
|
||||
runner_name_matched = runner['runnerName'].casecmp(m_selection).zero?
|
||||
if !runner_name_matched && fuzzy_match_runners
|
||||
runner_name_matched = (runner['runnerName'].downcase.split & m_selection.downcase.split).length.positive?
|
||||
end
|
||||
next unless runner_name_matched
|
||||
|
||||
m_details['selection_id'] = runner['selectionId']
|
||||
m_details['selection'] = runner['runnerName']
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
m_details['returned_markets'] = returned_markets unless m_details.key?('market_id')
|
||||
m_details['returned_selections'] = returned_selections unless m_details.key?('selection_id')
|
||||
|
||||
bet.update(exchange_market_details: m_details)
|
||||
m_details
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
39
portal/app/lib/integrations/betfair/connection.rb
Normal file
39
portal/app/lib/integrations/betfair/connection.rb
Normal file
@ -0,0 +1,39 @@
|
||||
module Integrations
|
||||
module Betfair
|
||||
class Connection
|
||||
|
||||
include HTTParty
|
||||
def initialize(account)
|
||||
@account = account
|
||||
self.class.pem @account.ssl_pem
|
||||
end
|
||||
|
||||
def api_headers
|
||||
{ 'X-Application' => @account.apikey, 'X-Authentication' => session_token, 'content-type' => 'application/json', 'accept' => 'application/json' }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def session_token
|
||||
# if
|
||||
puts 'Checking if session still fresh'
|
||||
if @account.last_session_token_saved_at && @account.last_session_token_saved_at > 10.hours.ago
|
||||
puts 'Returning cached session token'
|
||||
return @account.last_session_token
|
||||
end
|
||||
|
||||
puts 'Cache is stale or non-existent - getting fresh session key'
|
||||
url = 'https://identitysso-cert.betfair.com/api/certlogin'
|
||||
r = self.class.post(url, headers: { 'X-Application' => @account.apikey }, body: { username: @account.login_uid, password: @account.login_pass })
|
||||
resp = JSON.parse(r)
|
||||
if resp['loginStatus'] == 'SUCCESS'
|
||||
@account.update(last_session_token: resp['sessionToken'], last_session_token_saved_at: Time.now)
|
||||
return resp['sessionToken']
|
||||
end
|
||||
|
||||
raise '[Betfair Session token] Cannot get session to Betfair'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
77
portal/app/lib/integrations/betfair/opportunity_hunter.rb
Normal file
77
portal/app/lib/integrations/betfair/opportunity_hunter.rb
Normal file
@ -0,0 +1,77 @@
|
||||
module Integrations
|
||||
module Betfair
|
||||
class OpportunityHunter < Base
|
||||
def events_in_timeframe(from:, to:)
|
||||
raise "Timeframe not set " unless from.present? && to.present?
|
||||
|
||||
timeframe = { from: from.iso8601, to: to.iso8601 }
|
||||
filter = { marketStartTime: timeframe }
|
||||
body = { filter: filter }
|
||||
|
||||
r = self.class.post("#{API_BETTING_ENDPOINT}/listEvents/", { headers: @connection.api_headers, body: body.to_json })
|
||||
events = []
|
||||
r.each do |e|
|
||||
ev = e['event']
|
||||
events << BetfairEvent.new(event_id: ev['id'], event_name: ev['name'], event_start: DateTime.parse(ev['openDate']))
|
||||
end
|
||||
import_result = BetfairEvent.import(events, on_duplicate_key_ignore: true)
|
||||
puts "#{import_result.ids.size} events added"
|
||||
end
|
||||
|
||||
def event_markets_and_selections
|
||||
batches = []
|
||||
batch = 0
|
||||
limit = 10
|
||||
BetfairEvent.open.order(created_at: :desc).pluck(:event_id).each do |eid|
|
||||
batches[batch] ||= []
|
||||
if batches[batch].size < limit
|
||||
batches[batch] << eid
|
||||
else
|
||||
batch += 1
|
||||
end
|
||||
end
|
||||
batches.each { |b| batch_event_runners b }
|
||||
end
|
||||
|
||||
def runner_odds(runner)
|
||||
body = { marketId: runner.market_id, selectionId: runner.selection_id }
|
||||
body[:priceProjection] = { priceData: ['EX_BEST_OFFERS'], virtualise: true }
|
||||
r = self.class.post("#{API_BETTING_ENDPOINT}/listRunnerBook/", { headers: @connection.api_headers, body: body.to_json })
|
||||
runners = r[0]['runners']
|
||||
raise '[Odds] - cannot identify prices' unless runners
|
||||
|
||||
rs = runners.first
|
||||
raise '[Odds] - cannot identify prices' unless rs && rs['ex'] && rs['ex']['availableToBack']
|
||||
|
||||
imports = []
|
||||
rs['ex']['availableToBack'].each do |ex|
|
||||
imports << BetfairRunnerOdd.new(betfair_event_runner_id: runner.id, odds: ex['price'], total_matched: 0, total_available: ex['size'], bet_type: 'back')
|
||||
end
|
||||
rs['ex']['availableToLay'].each do |ex|
|
||||
imports << BetfairRunnerOdd.new(betfair_event_runner_id: runner.id, odds: ex['price'], total_matched: 0, total_available: ex['size'], bet_type: 'lay')
|
||||
end
|
||||
|
||||
BetfairRunnerOdd.import(imports, on_duplicate_key_ignore: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def batch_event_runners(batch)
|
||||
body = { maxResults: 1000 - (10 * (batch.size - 1)), filter: { eventIds: batch }, marketProjection: ['EVENT', 'RUNNER_DESCRIPTION'] }
|
||||
markets = self.class.post("#{API_BETTING_ENDPOINT}/listMarketCatalogue/", { headers: @connection.api_headers, body: body.to_json })
|
||||
import = []
|
||||
markets.each do |market|
|
||||
market_fragment = { event_id: market['event']['id'], market_id: market['marketId'], market_name: market['marketName'] }
|
||||
runners = market['runners']
|
||||
runners&.each do |runner|
|
||||
rec = { selection_id: runner['selectionId'] || runner['runnerName'], selection_name: runner['runnerName'] }.merge(market_fragment)
|
||||
import << BetfairEventRunner.new(rec)
|
||||
end
|
||||
end
|
||||
import_result = BetfairEventRunner.import(import, on_duplicate_key_ignore: true)
|
||||
puts "#{import_result.ids.size} runners added"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
6
portal/app/lib/services/bet_outcome_service.rb
Normal file
6
portal/app/lib/services/bet_outcome_service.rb
Normal file
@ -0,0 +1,6 @@
|
||||
module Services
|
||||
class BetOutcomeService
|
||||
def bet_outcome(bet_id)
|
||||
end
|
||||
end
|
||||
end
|
44
portal/app/mailers/application_mailer.rb
Normal file
44
portal/app/mailers/application_mailer.rb
Normal file
@ -0,0 +1,44 @@
|
||||
class ApplicationMailer < ActionMailer::Base
|
||||
default from: 'info@helpbuild.co'
|
||||
helper MailerStyleHelper
|
||||
layout 'mailer'
|
||||
|
||||
def try_delivering(_options = {})
|
||||
yield
|
||||
true
|
||||
rescue EOFError,
|
||||
IOError,
|
||||
Errno::ECONNRESET,
|
||||
Errno::ECONNABORTED,
|
||||
Errno::EPIPE,
|
||||
Errno::ETIMEDOUT,
|
||||
Net::SMTPAuthenticationError,
|
||||
Net::SMTPServerBusy,
|
||||
Net::SMTPSyntaxError,
|
||||
Net::SMTPUnknownError,
|
||||
OpenSSL::SSL::SSLError => e
|
||||
ExceptionHub.notify(e)
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def deliver_mail(email, subject, reply_to = nil, to_name = '')
|
||||
addresses = set_addresses_and_user(email: email, to_name: to_name)
|
||||
reply_to = addresses[:from] if reply_to.nil?
|
||||
try_delivering do
|
||||
I18n.with_locale('en') do
|
||||
mail(reply_to: reply_to, from: addresses[:from], to: addresses[:to], subject: "[Helpbuild] | #{subject}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_addresses_and_user(email: '', from_name: 'Helpbuild', to_name: '', from_email: nil)
|
||||
addresses = {}
|
||||
from_email ||= Rails.configuration.action_mailer[:default_options][:from]
|
||||
addresses[:from] = %(#{from_name} <#{from_email}>)
|
||||
addresses[:reply_to] = %(#{from_name} <#{from_email}>)
|
||||
addresses[:to] = %("#{to_name}" <#{email}>)
|
||||
addresses
|
||||
end
|
||||
end
|
76
portal/app/mailers/project_mailer.rb
Normal file
76
portal/app/mailers/project_mailer.rb
Normal file
@ -0,0 +1,76 @@
|
||||
class ProjectMailer < ApplicationMailer
|
||||
layout 'project_mailer'
|
||||
layout false, only: :send_project_invitation
|
||||
|
||||
def helper_join_request(builders, helper)
|
||||
subject = I18n.t('project_join_request', project: helper.project.title)
|
||||
@project_helper = helper
|
||||
@project = helper.project
|
||||
builders.each do |b|
|
||||
@user = b.user
|
||||
deliver_mail(@user.email, subject, reply_to = nil)
|
||||
end
|
||||
end
|
||||
|
||||
def notify_helper_for_join_request(helper)
|
||||
return if helper.builder?
|
||||
subject = I18n.t('project_join_request_response', project: helper.project.title)
|
||||
|
||||
@user = helper.user
|
||||
@project = helper.project
|
||||
@status = helper.status.titlecase
|
||||
|
||||
deliver_mail(@user.email, subject, reply_to = nil)
|
||||
end
|
||||
|
||||
def ask_request_to_helpers(helper, ask)
|
||||
@user = helper.user
|
||||
@project_helper = helper
|
||||
@ask = ask
|
||||
subject = I18n.t('ask_request_to_helpers', project: helper.project.title)
|
||||
@project = helper.project
|
||||
deliver_mail(helper.user.email, subject, reply_to = nil)
|
||||
end
|
||||
|
||||
def notify_builder_ask_status(ask, status, project_helper)
|
||||
@builder = ask.project_helper.user
|
||||
@ask = ask
|
||||
@status = status
|
||||
@project_helper = project_helper
|
||||
@project = ask.project
|
||||
subject = I18n.t('ask_status_to_builder_subject', project: @project.title, ask: ask.title, status: status)
|
||||
deliver_mail(@builder.email, subject, reply_to = nil)
|
||||
end
|
||||
|
||||
def notify_helper_ask_status(ask, status, project_helper)
|
||||
# @builder = ask.project_helper.user
|
||||
# @ask = ask
|
||||
# @status = status
|
||||
# @project_helper = project_helper
|
||||
# @project = ask.project
|
||||
# subject = I18n.t('ask_status_to_builder_subject', project: @project.title, ask: ask.title, status: status)
|
||||
# deliver_mail(project_helper.user.email, subject, reply_to = nil)
|
||||
end
|
||||
|
||||
def notify_announcement(project_helper, announcement)
|
||||
@user = project_helper.user
|
||||
@announcement = announcement
|
||||
@project = project_helper.project
|
||||
subject = I18n.t('announcement_notification', project: announcement.project.title)
|
||||
deliver_mail(project_helper.user.email, subject, reply_to = nil)
|
||||
end
|
||||
|
||||
def notify_monthly_announcement(project)
|
||||
emails = project.project_builders.map {|pb| pb.user.email }
|
||||
subject = I18n.t('monthly_announcement_check', project: project.title)
|
||||
@project = project
|
||||
|
||||
deliver_mail(emails.join(','), subject, reply_to = nil)
|
||||
end
|
||||
|
||||
def send_project_invitation(email, project)
|
||||
@project = project
|
||||
subject = I18n.t('project_invitation_subject', project: project.title, builder_name: project.user.first_name)
|
||||
deliver_mail(email, subject, reply_to = nil)
|
||||
end
|
||||
end
|
21
portal/app/mailers/user_mailer.rb
Normal file
21
portal/app/mailers/user_mailer.rb
Normal file
@ -0,0 +1,21 @@
|
||||
class UserMailer < ApplicationMailer
|
||||
def welcome_email user
|
||||
@user = user
|
||||
subject = I18n.t('welcome_email_subject')
|
||||
deliver_mail(user.email, subject, reply_to = nil, user.fullname)
|
||||
end
|
||||
|
||||
def notify_token_purchase(user, tokens, amount, balance)
|
||||
@user = user
|
||||
subject = I18n.t('token_purchase_confirmation_subject')
|
||||
@body = I18n.t('token_purchase_confirmation_body', tokens: tokens, amount: amount, balance: balance.to_i)
|
||||
deliver_mail(user.email, subject, reply_to = nil, user.fullname)
|
||||
end
|
||||
|
||||
def notify_hourly_unread_activities(user_id, activities_hash)
|
||||
@user = User.find_by(id: user_id)
|
||||
@activities_hash = activities_hash
|
||||
|
||||
deliver_mail(@user.email, '[Helpbuild] Activity Digest Mail', reply_to = nil, @user.fullname)
|
||||
end
|
||||
end
|
15
portal/app/models/account.rb
Normal file
15
portal/app/models/account.rb
Normal 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
|
3
portal/app/models/application_record.rb
Normal file
3
portal/app/models/application_record.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class ApplicationRecord < ActiveRecord::Base
|
||||
self.abstract_class = true
|
||||
end
|
82
portal/app/models/bet.rb
Normal file
82
portal/app/models/bet.rb
Normal 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
|
5
portal/app/models/betfair_event.rb
Normal file
5
portal/app/models/betfair_event.rb
Normal 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
|
6
portal/app/models/betfair_event_runner.rb
Normal file
6
portal/app/models/betfair_event_runner.rb
Normal 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
|
4
portal/app/models/betfair_runner_odd.rb
Normal file
4
portal/app/models/betfair_runner_odd.rb
Normal file
@ -0,0 +1,4 @@
|
||||
class BetfairRunnerOdd < ApplicationRecord
|
||||
belongs_to :betfair_event_runner
|
||||
enum bet_type: {back: 'back', lay: 'lay'}
|
||||
end
|
6
portal/app/models/concerns/latest.rb
Normal file
6
portal/app/models/concerns/latest.rb
Normal file
@ -0,0 +1,6 @@
|
||||
module Latest
|
||||
extend ActiveSupport::Concern
|
||||
included do
|
||||
scope :latest, -> { order(created_at: :desc).first }
|
||||
end
|
||||
end
|
218
portal/app/models/exchange_account.rb
Normal file
218
portal/app/models/exchange_account.rb
Normal 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
|
20
portal/app/models/loggable.rb
Normal file
20
portal/app/models/loggable.rb
Normal 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
|
17
portal/app/models/source_subscription.rb
Normal file
17
portal/app/models/source_subscription.rb
Normal 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
|
17
portal/app/models/subscription_run.rb
Normal file
17
portal/app/models/subscription_run.rb
Normal 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
|
22
portal/app/models/tip_source.rb
Normal file
22
portal/app/models/tip_source.rb
Normal 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
|
15
portal/app/models/tip_source_data.rb
Normal file
15
portal/app/models/tip_source_data.rb
Normal 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
|
18
portal/app/models/tipster_account.rb
Normal file
18
portal/app/models/tipster_account.rb
Normal 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
82
portal/app/models/user.rb
Normal 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
|
19
portal/app/views/layouts/application.html.erb
Normal file
19
portal/app/views/layouts/application.html.erb
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>betbeast : taking a bit out of arbitrage</title>
|
||||
<link href="<%= asset_path("favicon.png") %>" rel="shortcut icon"></link>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- End Google Tag Manager -->
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
|
||||
<%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id='app'><%= yield %></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
0
portal/app/views/pages/index.html.erb
Normal file
0
portal/app/views/pages/index.html.erb
Normal file
@ -0,0 +1,11 @@
|
||||
<% @activities_hash.except(:all_activities).each do |k, activities| %>
|
||||
<% if k.starts_with?('project_asks.') %>
|
||||
<div style="border: 1px solid #eee; padding: 15px; margin-bottom: 15px;border-radius: 15px; background: #f3f3f3">
|
||||
<h3 style="margin: 0 0 15px;">Activities</h3>
|
||||
|
||||
<% activities.each do |activity| %>
|
||||
<%= render partial: 'partials/project_ask_digest', locals: { activity: activity } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
Reference in New Issue
Block a user