Wednesday, 16 December 2015

(Semi) automating Betfair

Now listen.
Don't too many of you guys do this - too many people automating matched bets will only mean that the bookies get wise to it, and clamp down, making their odds far too low (against the betting markets) to deny anyone a chance of finding a good match for backing and laying the same horse/football result/tennis player!

But a few people have asked for the source code, so here goes:
When you look at a market in Betfair, you should notice part of  the URL



Simply enter this as a parameter into your own show_market.php file (so your final URL will look something like http://myserver.com/betfair/show_market.php?market=123456)

Here's the show_market.php page

<?php
     $market="1.122075620";
     if(isset($_GET['market'])){
          $market=trim($_GET['market']);
     }
?>
<html>
<head>

<script src="js/jquery.min.js"></script>
<script src="js/show_market.js"></script>

<style>
     body, tr, td { font-family: trebuchet, tahoma, arial, sans; font-size:10pt;}
     td { padding-left: 8px; padding-right: 8px; }

     .lay_3 { background-color:#ADD8E6; color:black; text-align:center; width:38px; }
     .lay_2 { background-color:#D1EBF3; color:black; text-align:center; width:38px; }
     .lay_1 { background-color:#E9F5F9; color:black; text-align:center; width:38px; }

     .back_1 { background-color:#FFC0CB; color:black; text-align:center; width:38px; }
     .back_2 { background-color:#FFDDE3; color:black; text-align:center; width:38px; }
     .back_3 { background-color:#FFF1F3; color:black; text-align:center; width:38px; }

     .odds { font-size:100%; font-weight:bold; }
     .value { font-size:75%; font-weight:normal; }

     .odds_box { border:1px solid silver; width:40px; text-align:right; padding-right:4px; }
</style>

</head>
<body>

<table id="prices" border='1'>
<tbody>
<tr>
     <th>Selection</th><th>Name</th>
     <th colspan="3">Back</th>
     <th colspan="3">Lay</th>
     <th>Bookies price</th>
     <th>Actions</th>
</tr>

</tbody>
</table>

<script defer>
var market="<?php echo($market);?>";
refreshOdds();
</script>



</body>
</html>

You'll need the jQuery library, and this show_market.js page


function refreshOdds(){
     // get the odds from the server
     
     $.get("bet_market_js.php?market="+market, function(data, status){
      if(data.indexOf("arket not found")>0){
      alert("Market not found");
      }else if(data.indexOf("url error")>0){
      alert("Data accessing betfair API");

      }else{
populateOdds(data);
      }
     });
     
}

function populateOdds(s){
     var warning_threshold=0.15;
     var d=s.split("\n");
     var run_count=parseInt(d[0]);

     if(!$('#runners').length){
          // create the divs
          for(var i=1; i<=run_count; i++){
               var s='<tr><td id="id_'+i+'"></td><td id="name_'+i+'"></td>';
               for(var j=1; j<=3; j++){
                    s=s+'<td id="lay_'+i+'_'+j+'" class="lay_'+j+'"></td>';
               }
               for(var j=1; j<=3; j++){
                    s=s+'<td id="back_'+i+'_'+j+'" class="back_'+j+'"></td>';
               }
               s=s+'<td id="odds_'+i+'" align="right"><input type="text" class="odds_box" id="txt_odds_'+i+'" /></td>';
               s=s+'<td id="actions_'+i+'"></td>';
               s=s+'</tr>';
               $('#prices > tbody:last-child').append(s);
          }

          // add a footer
          var s='<tr><td colspan="9" id="runners">Runners: '+run_count+'</td></tr>';
          $('#prices > tbody:last-child').append(s);

          $(".odds_box").blur(function() {
               var k=$(this).val();
               k=parseFloat(k);
               if(isNaN(k)){
                    $(this).val('');
               }else{
                    k=k*100;
                    k=Math.floor(k);
                    k=k/100;
                    $(this).val(k.toFixed(2));
               }
          });

     }

     for(var i=1; i<=run_count; i++){
          // now populate the divs with the data
          var e=d[i].split("|");
          $('#id_'+i).text(e[0]);
          $('#name_'+i).text(e[1]);

          if(e.length<4){
               // non-runner: do nothing
               $('#txt_odds_'+i).hide();
          }else{

               var bookies_price=parseFloat($('#txt_odds_'+i).val());
               if(isNaN(bookies_price)){ bookies_price=0;}


               // 57.28@3.9,47.89@3.85,67.09@3.8,
               var f=e[2].split(",");
               for(var j=0; j<f.length; j++){
                    var t=f[j].split("@");
                    var odds=parseFloat(t[1]);
                    var val=parseFloat(t[0]);
     
                    if(isNaN(odds)){ odds="-";}
                    if(isNaN(val)) { val="-";} else { val=Math.floor(val); }

                    var p="<span class='odds'>"+odds+"</span><br/><span class='value'>£"+val+"</span>";
                    $('#lay_'+i+'_'+(3-j)).html(p);

                    if(j==0){
                         if( bookies_price>0 && (bookies_price+warning_threshold) >= odds){
                              $('#name_'+i).css("color","red");
                         }else{
                              $('#name_'+i).css("color","black");
                         }
                    }
                    
               }

               var f=e[3].split(",");
               for(var j=0; j<f.length; j++){
                    var t=f[j].split("@");
                    var odds=parseFloat(t[1]);
                    var val=parseFloat(t[0]);
     
                    if(isNaN(odds)){ odds="-";}
                    if(isNaN(val)) { val="-";} else { val=Math.floor(val); }

                    var p="<span class='odds'>"+odds+"</span><br/><span class='value'>£"+val+"</span>";
                    $('#back_'+i+'_'+(j+1)).html(p);

                    if(j==0){
                         if( bookies_price>0 && (bookies_price+warning_threshold) >= odds){
                              if( bookies_price>0 && (bookies_price) > odds){
                                   $('#name_'+i).css("background-color","#FF0000");
                                   $('#name_'+i).css("color","white");
                              }else{
                                   $('#name_'+i).css("background-color","#FFDDE3");
                                   $('#name_'+i).css("color","red");
                              }
                         }else{
                              $('#name_'+i).css("background-color","white");
                         }
                    }

               }
          }

     }

     setTimeout(refreshOdds, 1000);

}

This javascript function in turn calls a php page using AJAX to update the odds every second. You can, if you really must, change this to not less than 5 times a second. But just think about the battering your sever is going to take (as it is a pass-through server) as well as the Betfair server. In truth, because it's only a semi-automated approach (you still need to put the bets on manually) even refreshing every second is probably a bit overkill.

<?php

header("Expires: on, 01 Jan 1970 00:00:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");

$market="1.122075620";
if(isset($_GET['market'])){ $market=trim($_GET['market']); }


function sportsApingRequest($appKey, $sessionToken, $operation, $params) {
     $ch = curl_init();
     curl_setopt($ch, CURLOPT_URL, "https://api.betfair.com/exchange/betting/json-rpc/v1");
     curl_setopt($ch, CURLOPT_POST, 1);
     curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
     curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:',
          'X-Application: ' . $appKey,
          'X-Authentication: ' . $sessionToken,
          'Accept: application/json',
          'Content-Type: application/json'
     ));

     $postData = '[{ "jsonrpc": "2.0", "method": "SportsAPING/v1.0/' . $operation . '", "params" :' . $params . ', "id": 1}]';
     curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
     $response = json_decode(curl_exec($ch));
     curl_close($ch);

     if (isset($response[0]->error)) {
          echo 'Call to api-ng failed: ' . "\n";
          echo 'Response: ' . json_encode($response);
          exit(-1);
     } else {
          return $response;
     }
}

function getAllEventTypes($appKey, $sessionToken) {
     $jsonResponse = sportsApingRequest($appKey, $sessionToken, 'listEventTypes', '{"filter":{}}');
     return $jsonResponse[0]->result;
}

function extractHorseRacingEventTypeId($allEventTypes) {
     foreach ($allEventTypes as $eventType) {
          if ($eventType->eventType->name == 'Horse Racing') {
               return $eventType->eventType->id;
          }
     }
}

function getMarketBook($appKey, $sessionToken, $marketId) {
     $params = '{"marketIds":["' . $marketId . '"], "priceProjection":{"priceData":["EX_BEST_OFFERS"]}}';
     $jsonResponse = sportsApingRequest($appKey, $sessionToken, 'listMarketBook', $params);
     return $jsonResponse[0]->result[0];
}

function getMarketCatalogue($appKey, $sessionToken, $m_id, $eventTypeId) {
     $params = '{"filter":{"eventTypeIds":["' . $eventTypeId . '"],
          "marketIds":["'.$m_id.'"],
          "marketCountries":["GB"],
          "marketTypeCodes":["WIN"],
          "marketStartTime":{"from":"' . date('c') . '"}},
          "sort":"FIRST_TO_START",
          "maxResults":"1",
          "marketProjection":["RUNNER_DESCRIPTION"]}';

     $jsonResponse = sportsApingRequest($appKey, $sessionToken, 'listMarketCatalogue', $params);
     return $jsonResponse[0]->result[0];
}

function availablePrices($selectionId, $marketBook) {
     $t="";

     // Get selection
     foreach ($marketBook->runners as $runner) {
          if ($runner->selectionId == $selectionId) break;
     }
          
     foreach ($runner->ex->availableToBack as $availableToBack){
          echo $availableToBack->size . "@" . $availableToBack->price . ",";
          $t.= $availableToBack->size . "@" . $availableToBack->price . ",";
     }

     echo "|";
     $t.="|";

     foreach ($runner->ex->availableToLay as $availableToLay){
          echo $availableToLay->size . "@" . $availableToLay->price . ",";
          $t.= $availableToLay->size . "@" . $availableToLay->price . ",";
     }

     return($t);
}

function getACookie(){
     
     $loginEndpoint= "https://identitysso.betfair.com/api/login";     
     $cookie = "";
     
     $username = "YOUR_LOGIN_HERE";
     $password = "YOUR_PASSWORD_HERE";
     
     $login = "true";
     $redirectmethod = "POST";
     $product = "home.betfair.int";
     $url = "https://www.betfair.com/";

     $fields = array
          (
               'username' => urlencode($username),
               'password' => urlencode($password),
               'login' => urlencode($login),
               'redirectmethod' => urlencode($redirectmethod),
               'product' => urlencode($product),
               'url' => urlencode($url)
          );

     //open connection
     $ch = curl_init($loginEndpoint);
     //url-ify the data for the POST
     $counter = 0;
     $fields_string = "&";
     
     foreach($fields as $key=>$value) {
          if ($counter > 0) {
               $fields_string .= '&';
          }
          $fields_string .= $key.'='.$value;
          $counter++;
     }

     rtrim($fields_string,'&');

     curl_setopt($ch, CURLOPT_URL, $loginEndpoint);
     curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
     curl_setopt($ch, CURLOPT_POST, true);
     curl_setopt($ch, CURLOPT_POSTFIELDS,$fields_string);
     curl_setopt($ch, CURLOPT_HEADER, true); // DO RETURN HTTP HEADERS
     curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // DO RETURN THE CONTENTS OF THE CALL

     //execute post

     $result = curl_exec($ch);

     if($result == false) {
           echo 'Curl error: ' . curl_error($ch);
     } else {
          $temp = explode(";", $result);
          $result = $temp[0];
               
          $end = strlen($result);
          $start = strpos($result, 'ssoid=');
          $start = $start + 6;
          
          $cookie = substr($result, $start, $end);
     }
     curl_close($ch);     
     return $cookie;
}

// ------------- here's where the magic happens ----------------

$appKey="YOUR_APP_KEY";
$sessionToken="";
if(isset($_COOKIE['session'])){ $sessionToken=trim($_COOKIE['session']); }

if(!$sessionToken){
     ob_start();
     $sessionToken = getACookie();
     ob_end_clean();
}

setcookie('session', $sessionToken, time() + 900, "/"); // 900 = 60 sec * 15 mins

// get the id for horse racing events
$horseRacingEventTypeId=extractHorseRacingEventTypeId(getAllEventTypes($appKey, $sessionToken));      // horse racing events are ID 7

// provide a market ID
$market_id="1.122075620";
if($market){ $market_id=$market; }

// get the market catalogue for this market
$catalogue=getMarketCatalogue($appKey, $sessionToken, $market_id, $horseRacingEventTypeId);

if($catalogue){
     
     // get the market data for this market
     $nextMarketBook=getMarketBook($appKey, $sessionToken, $market_id);
     $num_runners=$nextMarketBook->numberOfRunners;
     echo("".$num_runners."\n");

     foreach ($catalogue->runners as $runner) {

          $s_id=$runner->selectionId;
          $sRunner=trim($runner->runnerName);

      echo "S:" . $s_id . "|" . $sRunner . "|";
     
          foreach ($nextMarketBook->runners as $runner) {
               $sel_id=$runner->selectionId;
               if(trim($sel_id)==trim($s_id)){
                    $status=$runner->status;
                    if(trim($status)=="ACTIVE"){
                         $prices=availablePrices($sel_id, $nextMarketBook);
                     echo $prices;                                                  
                    }
                    echo "\n";
               }               
          }          
     }


}else{
     echo("Market not found");
}


?>

You'll need to modify the php above, entering your betfair username, betfair password and betfair APP key. If you haven't already got an appKey, you'll need to log into your Betfair account and do a bit of poking around until you find their API section. There are some really easy-to-follow step-by-step instructions for generating and testing your app key.

That's it really.
Most of the betfair integration code was a straight copy-and-paste job from their website. I simply took the bits I needed and left the stuff I didn't. It may not be the most elegant way of integrating with their website (the bit where I get the ID for horse racing seems a bit convoluted) but it should be enough to get you started.

On my to-do list (though probably not until the new year now) is

  • one-click placing of a lay bet (the Betfair API allows to you place bets with them programatically)
  • parsing bookies websites to get the latest horse odds for any given race (though having looked at some of the obfusicated javascript on the William Hill website, I'm not sure that this will be possible without building some kind of browser screen-scraping plugin!)
  • menu system to select a race from my own page without having to find the market id in betfair first
Just like this blog post, the code is a bit rough-and-ready.
It was thrown together to get a working solution, not to be particularly elegant. Rather like this blog post - very little thought went into the planning, it was more like "this needs doing quickly, so let's just get it done and get it live".

Hope this helps those of you who were asking for the source code!