init push - laying out the project
This commit is contained in:
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
|
||||
|
Reference in New Issue
Block a user