init push - laying out the project
This commit is contained in:
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